laminar_core/cluster/control/leader_lease/
topology_target_preparation.rs1use super::topology_admission::{CONTROL_TIMEOUT, MAX_ADMISSION_ATTEMPTS};
4use super::{
5 read_authority_record, AssignmentSnapshotStore, AuthorityCreateOutcome, LeaderLeaseStore,
6 LeaseError, TOPOLOGY_COMMIT_RECORD_VERSION, TOPOLOGY_TARGET_PREPARATION_RECORD_VERSION,
7};
8use crate::cluster::control::{
9 ProcessLeaseAuthority, TopologyAdmissionPhase, TopologyAdmissionStatus, TopologyError,
10 TopologyRestoreInput, TopologyTargetPreparationReceipt, TOPOLOGY_COMMIT_PROTOCOL_VERSION,
11 TOPOLOGY_TARGET_PREPARATION_PROTOCOL_VERSION,
12};
13
14impl LeaderLeaseStore {
15 pub async fn certify_topology_target_preparation(
25 &self,
26 assignments: &AssignmentSnapshotStore,
27 processes: &ProcessLeaseAuthority,
28 input: &TopologyRestoreInput,
29 protocol_version: u16,
30 ) -> Result<TopologyAdmissionStatus, TopologyError> {
31 if !matches!(
32 protocol_version,
33 TOPOLOGY_TARGET_PREPARATION_PROTOCOL_VERSION | TOPOLOGY_COMMIT_PROTOCOL_VERSION
34 ) {
35 return Err(TopologyError::Protocol(
36 "participant lacks target preparation protocol three".into(),
37 ));
38 }
39 tokio::time::timeout(CONTROL_TIMEOUT, async {
40 for _ in 0..MAX_ADMISSION_ATTEMPTS {
41 let published = self
42 .load_published_authority_head()
43 .await?
44 .ok_or(TopologyError::Fenced)?;
45 let current = &published.record;
46 let index = current
47 .topology_operations
48 .iter()
49 .position(|entry| entry.operation_id == input.operation().operation_id)
50 .ok_or_else(|| TopologyError::Conflict("unknown topology operation".into()))?;
51 let operation = ¤t.topology_operations[index];
52 if operation.phase != TopologyAdmissionPhase::CutPrepared
53 || !current.lease.matches_proof(&operation.admitted_by)
54 || !operation.same_restore_binding(input.operation())
55 {
56 return Err(TopologyError::Fenced);
57 }
58 let fresh = self
59 .topology_restore_input(
60 assignments,
61 processes,
62 operation.operation_id,
63 input.process(),
64 )
65 .await?;
66 if !fresh.same_restore_requirements(input) {
67 return Err(TopologyError::Fenced);
68 }
69 if let Some(existing) =
70 fresh
71 .operation()
72 .target_preparations
73 .iter()
74 .find(|receipt| {
75 receipt.participant.node_id == input.process().participant.node_id
76 })
77 {
78 if existing.participant != input.process().participant
79 || existing.process_term != input.process().process_term
80 || existing.protocol_version != protocol_version
81 {
82 return Err(TopologyError::Fenced);
83 }
84 return Ok(fresh.operation().clone());
85 }
86 self.require_topology_process(processes, input.process())
87 .await?;
88 let mut lease = current.lease.clone();
89 lease.seq = lease
90 .seq
91 .checked_add(1)
92 .ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
93 let sequence = lease.seq;
94 let mut next = current.preserve_with_lease(lease);
95 next.version =
96 next.version
97 .max(if protocol_version == TOPOLOGY_COMMIT_PROTOCOL_VERSION {
98 TOPOLOGY_COMMIT_RECORD_VERSION
99 } else {
100 TOPOLOGY_TARGET_PREPARATION_RECORD_VERSION
101 });
102 let operation = &mut next.topology_operations[index];
103 operation.status_sequence = sequence;
104 operation
105 .target_preparations
106 .push(TopologyTargetPreparationReceipt {
107 participant: input.process().participant,
108 process_term: input.process().process_term,
109 protocol_version,
110 authority_sequence: sequence,
111 });
112 operation
113 .target_preparations
114 .sort_by_key(|receipt| receipt.participant.node_id);
115 match self
116 .create_authority_record(Some(&published), &next)
117 .await?
118 {
119 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => {
120 let after = self
121 .topology_restore_input(
122 assignments,
123 processes,
124 input.operation().operation_id,
125 input.process(),
126 )
127 .await?;
128 if !after.same_restore_requirements(input) {
129 return Err(TopologyError::Fenced);
130 }
131 return Ok(after.operation().clone());
132 }
133 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
134 }
135 }
136 Err(TopologyError::Contended)
137 })
138 .await
139 .map_err(|_| TopologyError::Contended)?
140 }
141
142 pub(super) async fn audit_topology_target_preparations(
143 &self,
144 operation: &TopologyAdmissionStatus,
145 ) -> Result<(), LeaseError> {
146 for receipt in &operation.target_preparations {
147 let record = read_authority_record(self.store.as_ref(), receipt.authority_sequence)
148 .await?
149 .ok_or_else(|| {
150 LeaseError::Invalid("target preparation authority anchor is missing".into())
151 })?;
152 if record.version < TOPOLOGY_TARGET_PREPARATION_RECORD_VERSION
153 || (receipt.protocol_version == TOPOLOGY_COMMIT_PROTOCOL_VERSION
154 && record.version < TOPOLOGY_COMMIT_RECORD_VERSION)
155 || record
156 .topology_operations
157 .iter()
158 .find(|entry| entry.operation_id == operation.operation_id)
159 .is_none_or(|anchored| {
160 anchored.phase != TopologyAdmissionPhase::CutPrepared
162 || anchored.operation_id != operation.operation_id
163 || anchored.plan != operation.plan
164 || anchored.admitted_by != operation.admitted_by
165 || anchored.admitted_sequence != operation.admitted_sequence
166 || anchored.cut != operation.cut
167 || anchored.preparation != operation.preparation
168 || anchored.migration_root != operation.migration_root
169 || anchored.status_sequence != receipt.authority_sequence
170 || !anchored.target_preparations.contains(receipt)
171 })
172 {
173 return Err(LeaseError::Invalid(
174 "target preparation differs from its immutable authority append".into(),
175 ));
176 }
177 }
178 Ok(())
179 }
180}