Skip to main content

laminar_core/cluster/control/topology/
activation.rs

1//! Exact runtime installation and owner-complete Release under the committed catalog.
2
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use super::{TopologyAdmissionPhase, TopologyAdmissionStatus, TopologyError};
7use crate::checkpoint::{CheckpointAssignmentFence, LeaderProof};
8use crate::cluster::control::LocalProcessAuthorityIdentity;
9
10/// Requires held actors, generation-fenced completions and exact installation receipts.
11pub const TOPOLOGY_INSTALLATION_PROTOCOL_VERSION: u16 = 5;
12
13/// One still-held runtime observed by its owning process. Connector children remaining after
14/// actor exit are termination obligations, never readiness evidence.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct TopologyInstallationReceipt {
18    /// Exact current boot and process term.
19    pub process: LocalProcessAuthorityIdentity,
20    /// Unique local runtime installation, retained on an identical request retry.
21    pub runtime_id: Uuid,
22    /// Capability of the actual installed runtime, not its historical preparation.
23    pub protocol_version: u16,
24    /// First shared append carrying the observation.
25    pub authority_sequence: u64,
26}
27
28/// Durable permission to activate the complete installation roster. This is separate from
29/// local gate opening; each process must still validate its own installed runtime and authority.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct TopologyRelease {
33    /// Exact append after all installation receipts, with current leader/process/assignment.
34    pub authority_sequence: u64,
35}
36
37/// A single current installation round. A replacement leader may supersede an unreleased round;
38/// it must collect every runtime again. A published Release is immutable.
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct TopologyActivation {
42    /// Current leader that owns this installation round.
43    pub leader: LeaderProof,
44    /// Existing recovery round that owns a replacement held installation before first Release.
45    /// Original released rounds remain immutable; later recoveries use their own recovery terminal.
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub recovery_round: Option<crate::cluster::control::RecoveryRoundId>,
48    /// Exact assignment, retaining the committed plan's owner map and stable participant IDs.
49    pub assignment: CheckpointAssignmentFence,
50    /// Complete sorted current owner/evidence process roster.
51    pub processes: Vec<LocalProcessAuthorityIdentity>,
52    /// Round identity and immutable first installation append.
53    pub authority_sequence: u64,
54    /// Monotonic sorted receipts from this round, never an arbitrary quorum.
55    pub installations: Vec<TopologyInstallationReceipt>,
56    /// Irreversible Release, absent until the complete roster has been revalidated.
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub release: Option<TopologyRelease>,
59}
60
61impl TopologyActivation {
62    /// Exact participant coverage; a current-authority audit is additionally required for Release.
63    #[must_use]
64    pub fn installation_complete(&self) -> bool {
65        self.installations.len() == self.processes.len()
66            && self
67                .installations
68                .iter()
69                .zip(&self.processes)
70                .all(|(receipt, process)| receipt.process == *process)
71    }
72
73    pub(crate) fn validate(&self, commit: u64, status: u64) -> Result<(), TopologyError> {
74        if !self.leader.is_canonical()
75            || self.recovery_round.is_some_and(|round| {
76                round.generation == 0
77                    || round.nonce.is_nil()
78                    || round.driver.0 != self.leader.owner.node_id
79            })
80            || !self.assignment.is_canonical()
81            || self.authority_sequence <= commit
82            || self.authority_sequence > status
83            || self.processes.len() != self.assignment.participants.len()
84            || !self
85                .processes
86                .iter()
87                .zip(&self.assignment.participants)
88                .all(|(process, participant)| {
89                    process.participant == *participant && process.process_term != 0
90                })
91            || !self.processes.iter().any(|process| {
92                process.participant.node_id == self.leader.owner.node_id
93                    && process.participant.boot_incarnation == self.leader.owner.boot_id
94            })
95            || self.installations.is_empty()
96            || self.installations.len() > self.processes.len()
97            || !self
98                .installations
99                .iter()
100                .any(|receipt| receipt.authority_sequence == self.authority_sequence)
101            || self
102                .installations
103                .iter()
104                .enumerate()
105                .any(|(index, receipt)| {
106                    self.installations[..index]
107                        .iter()
108                        .any(|prior| prior.authority_sequence == receipt.authority_sequence)
109                })
110            || !self.installations.windows(2).all(|pair| {
111                pair[0].process.participant.node_id < pair[1].process.participant.node_id
112            })
113            || self.installations.iter().any(|receipt| {
114                receipt.protocol_version != TOPOLOGY_INSTALLATION_PROTOCOL_VERSION
115                    || receipt.runtime_id.is_nil()
116                    || !self.processes.contains(&receipt.process)
117                    || receipt.authority_sequence < self.authority_sequence
118                    || receipt.authority_sequence > status
119            })
120            || self.release.as_ref().is_some_and(|release| {
121                !self.installation_complete()
122                    || release.authority_sequence > status
123                    || self
124                        .installations
125                        .iter()
126                        .any(|receipt| receipt.authority_sequence >= release.authority_sequence)
127            })
128        {
129            return Err(TopologyError::Invalid(
130                "invalid exact-runtime installation or Release roster".into(),
131            ));
132        }
133        Ok(())
134    }
135}
136
137impl TopologyAdmissionStatus {
138    pub(crate) fn validate_activation(&self) -> Result<(), TopologyError> {
139        match (&self.activation, self.phase) {
140            (None, TopologyAdmissionPhase::Activating | TopologyAdmissionPhase::Active) => Err(
141                TopologyError::Invalid("activated phase has no installation round".into()),
142            ),
143            (None, _) => Ok(()),
144            (
145                Some(activation),
146                TopologyAdmissionPhase::Activating | TopologyAdmissionPhase::Active,
147            ) => {
148                let commit = self.commit.as_ref().ok_or_else(|| {
149                    TopologyError::Invalid("installation has no topology Commit".into())
150                })?;
151                activation.validate(commit.authority_sequence, self.status_sequence)?;
152                let latest_evidence = activation.release.as_ref().map_or_else(
153                    || {
154                        activation
155                            .installations
156                            .iter()
157                            .map(|receipt| receipt.authority_sequence)
158                            .max()
159                    },
160                    |release| Some(release.authority_sequence),
161                );
162                if (self.phase == TopologyAdmissionPhase::Active) != activation.release.is_some()
163                    || latest_evidence != Some(self.status_sequence)
164                {
165                    return Err(TopologyError::Invalid(
166                        "activation phase and Release disagree".into(),
167                    ));
168                }
169                Ok(())
170            }
171            (Some(_), _) => Err(TopologyError::Invalid(
172                "installation outside committed activation".into(),
173            )),
174        }
175    }
176
177    pub(crate) fn validate_activation_successor(
178        &self,
179        after: &Self,
180        sequence: u64,
181    ) -> Result<(), TopologyError> {
182        if self.activation == after.activation {
183            return if self.has_target_commit() && self != after {
184                Err(TopologyError::Invalid(
185                    "committed installation status can change only with its exact evidence append"
186                        .into(),
187                ))
188            } else {
189                Ok(())
190            };
191        }
192        if self.commit.is_none()
193            || self.commit != after.commit
194            || !self.same_migration_binding(after)
195            || self.target_preparations != after.target_preparations
196            || after.status_sequence != sequence
197        {
198            return Err(TopologyError::Invalid(
199                "installation cannot change the committed migration binding".into(),
200            ));
201        }
202        after.validate_activation()?;
203        let next = after.activation.as_ref().ok_or_else(|| {
204            TopologyError::Invalid("authority cannot forget installation evidence".into())
205        })?;
206        match &self.activation {
207            None if self.phase == TopologyAdmissionPhase::Committed
208                && after.phase == TopologyAdmissionPhase::Activating
209                && next.authority_sequence == sequence
210                && next.installations.len() == 1
211                && next.installations[0].authority_sequence == sequence
212                && next.release.is_none() =>
213            {
214                Ok(())
215            }
216            Some(prior)
217                if prior.release.is_none()
218                    && (prior.leader != next.leader
219                        || (next.recovery_round.is_some()
220                            && prior.recovery_round != next.recovery_round
221                            && prior.recovery_round.is_none_or(|prior| {
222                                next.recovery_round
223                                    .is_some_and(|next| next.generation > prior.generation)
224                            })))
225                    && after.phase == TopologyAdmissionPhase::Activating
226                    && next.authority_sequence == sequence
227                    && next.installations.len() == 1
228                    && next.installations[0].authority_sequence == sequence
229                    && next.release.is_none() =>
230            {
231                Ok(())
232            }
233            Some(prior)
234                if prior.leader == next.leader
235                    && prior.recovery_round == next.recovery_round
236                    && prior.assignment == next.assignment
237                    && prior.processes == next.processes
238                    && prior.authority_sequence == next.authority_sequence
239                    && prior.release.is_none() =>
240            {
241                let receipt_append = after.phase == TopologyAdmissionPhase::Activating
242                    && next.release.is_none()
243                    && next.installations.len() == prior.installations.len() + 1
244                    && prior
245                        .installations
246                        .iter()
247                        .all(|receipt| next.installations.contains(receipt))
248                    && next
249                        .installations
250                        .iter()
251                        .filter(|receipt| !prior.installations.contains(receipt))
252                        .all(|receipt| receipt.authority_sequence == sequence);
253                let release = after.phase == TopologyAdmissionPhase::Active
254                    && prior.installations == next.installations
255                    && next
256                        .release
257                        .as_ref()
258                        .is_some_and(|release| release.authority_sequence == sequence);
259                if receipt_append || release {
260                    Ok(())
261                } else {
262                    Err(TopologyError::Invalid(
263                        "authority cannot rewrite installed runtimes or Release".into(),
264                    ))
265                }
266            }
267            _ => Err(TopologyError::Invalid(
268                "authority cannot rewind an installation round or Release".into(),
269            )),
270        }
271    }
272}