Skip to main content

laminar_core/cluster/control/topology/
preparation.rs

1//! Monotonic compatibility evidence; neither readiness nor authority for target actors.
2
3use super::{
4    TopologyAdmissionPhase, TopologyAdmissionStatus, TopologyCompatibilityRef, TopologyError,
5};
6use crate::checkpoint::{CheckpointParticipant, MAX_CHECKPOINT_PARTICIPANTS};
7use serde::{Deserialize, Serialize};
8
9/// Candidate preparation requires this exact protocol on every frozen process.
10pub const TOPOLOGY_PREPARATION_PROTOCOL_VERSION: u16 = 2;
11
12/// Complete public migration support, including exact-target installation and recovery.
13/// Every frozen process must certify this plan protocol before the parent cut begins.
14pub const TOPOLOGY_SUBMISSION_PROTOCOL_VERSION: u16 = 6;
15
16/// One process's independently compiled candidate, bound to its first immutable authority append.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct TopologyParticipantCertificate {
20    /// Exact frozen node slot and boot incarnation, including required evidence participants.
21    pub participant: CheckpointParticipant,
22    /// Stable-node process term sampled around compilation/publication by its controller.
23    pub process_term: u64,
24    /// Exact supported preparation protocol; advertisements alone are insufficient.
25    pub protocol_version: u16,
26    /// Shared append that first recorded this certificate.
27    pub authority_sequence: u64,
28}
29
30/// Candidate binding retained by admission, renewals, cut preparation and definitive abort.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(deny_unknown_fields)]
33pub struct TopologyPreparation {
34    /// Canonical compiled descriptor bound by the immutable admission plan.
35    pub compatibility: TopologyCompatibilityRef,
36    /// Sorted exact-process certificates, never a majority of reachable processes.
37    pub certificates: Vec<TopologyParticipantCertificate>,
38    /// The final certificate append, only after the complete frozen roster agrees.
39    pub complete_sequence: Option<u64>,
40}
41
42impl TopologyAdmissionStatus {
43    pub(crate) fn validate_preparation(&self, head: u64) -> Result<(), TopologyError> {
44        let Some(preparation) = &self.preparation else {
45            return if self.phase == TopologyAdmissionPhase::Preparing {
46                Err(TopologyError::Invalid(
47                    "preparing operation has no descriptor".into(),
48                ))
49            } else {
50                Ok(())
51            };
52        };
53        preparation.compatibility.validate()?;
54        if preparation.certificates.len() > MAX_CHECKPOINT_PARTICIPANTS
55            || !preparation
56                .certificates
57                .windows(2)
58                .all(|pair| pair[0].participant.node_id < pair[1].participant.node_id)
59            || preparation.certificates.iter().any(|certificate| {
60                certificate.participant.node_id == 0
61                    || certificate.participant.boot_incarnation.is_nil()
62                    || certificate.process_term == 0
63                    || !matches!(
64                        certificate.protocol_version,
65                        TOPOLOGY_PREPARATION_PROTOCOL_VERSION
66                            | TOPOLOGY_SUBMISSION_PROTOCOL_VERSION
67                    )
68                    || certificate.authority_sequence <= self.admitted_sequence
69                    || certificate.authority_sequence > self.status_sequence
70                    || certificate.authority_sequence > head
71            })
72            || !preparation
73                .certificates
74                .windows(2)
75                .all(|pair| pair[0].protocol_version == pair[1].protocol_version)
76            || preparation.complete_sequence.is_some_and(|sequence| {
77                preparation
78                    .certificates
79                    .iter()
80                    .map(|cert| cert.authority_sequence)
81                    .max()
82                    != Some(sequence)
83            })
84            || (self.is_planned()
85                && (!preparation.certificates.is_empty()
86                    || preparation.complete_sequence.is_some()))
87            || (self.phase == TopologyAdmissionPhase::Preparing
88                && preparation.certificates.is_empty())
89            || (self.cut.is_some()
90                && preparation.complete_sequence.is_none_or(|sequence| {
91                    sequence >= self.cut.as_ref().map_or(0, |cut| cut.bound_sequence)
92                }))
93        {
94            return Err(TopologyError::Invalid(
95                "invalid participant preparation evidence".into(),
96            ));
97        }
98        Ok(())
99    }
100
101    pub(crate) fn validate_preparation_successor(
102        &self,
103        after: &Self,
104        sequence: u64,
105    ) -> Result<(), TopologyError> {
106        match (&self.preparation, &after.preparation) {
107            (None, None) => Ok(()),
108            (Some(prior), Some(next)) if prior.compatibility == next.compatibility => {
109                if prior
110                    .certificates
111                    .iter()
112                    .any(|cert| !next.certificates.contains(cert))
113                    || prior
114                        .complete_sequence
115                        .is_some_and(|complete| next.complete_sequence != Some(complete))
116                    || (prior != next
117                        && (!matches!(
118                            self.phase,
119                            TopologyAdmissionPhase::Planned | TopologyAdmissionPhase::Preparing
120                        ) || after.phase != TopologyAdmissionPhase::Preparing
121                            || next.certificates.len() != prior.certificates.len() + 1
122                            || next
123                                .certificates
124                                .iter()
125                                .filter(|cert| !prior.certificates.contains(cert))
126                                .any(|cert| cert.authority_sequence != sequence)
127                            || next
128                                .complete_sequence
129                                .is_some_and(|complete| complete != sequence)))
130                {
131                    return Err(TopologyError::Invalid(
132                        "authority cannot rewrite, forget or append stale preparation certificates"
133                            .into(),
134                    ));
135                }
136                Ok(())
137            }
138            _ => Err(TopologyError::Invalid(
139                "authority cannot replace an admitted descriptor binding".into(),
140            )),
141        }
142    }
143}