laminar_core/cluster/control/leader_lease/
topology_cut.rs1use super::topology_admission::{
4 topology_assignment_error, topology_checkpoint_error, CONTROL_TIMEOUT, MAX_ADMISSION_ATTEMPTS,
5};
6use super::*;
7use crate::checkpoint::{CheckpointAttempt, CheckpointAttemptRelation, CheckpointParticipant};
8use crate::cluster::control::topology::{
9 TopologyAbortReason, TopologyAdmissionPhase, TopologyAdmissionStatus, TopologyCheckpointCut,
10 TopologyCutCommit, TopologyError, TopologyOperationId, TopologyPlanRef,
11};
12
13impl LeaderAuthorityRecord {
14 pub(super) fn validate_topology_checkpoint_inventory(
15 &self,
16 inventory: &CheckpointArtifactInventory,
17 ) -> Result<(), ClusterCheckpointAuthorityError> {
18 let Some(operation) = self
19 .topology_operations
20 .iter()
21 .find(|entry| entry.blocks_admission())
22 else {
23 return Ok(());
24 };
25 if operation.phase == TopologyAdmissionPhase::Quiescing
26 && operation
27 .cut
28 .as_ref()
29 .is_some_and(|cut| cut.inventory == *inventory && cut.committed.is_none())
30 {
31 return Ok(());
32 }
33 Err(DecisionError::Conflict("checkpoint is not the exact bound topology cut".into()).into())
34 }
35
36 pub(super) fn record_topology_cut_outcome(
37 &mut self,
38 outcome: &CheckpointOutcome,
39 ) -> Result<(), LeaseError> {
40 let Some(operation) = self
41 .topology_operations
42 .iter_mut()
43 .find(|entry| entry.is_preparing() && entry.cut.is_some())
44 else {
45 return Ok(());
46 };
47 let cut = operation
48 .cut
49 .as_mut()
50 .ok_or_else(|| LeaseError::Invalid("topology cut binding is missing".into()))?;
51 if cut.inventory.attempt != CheckpointAttempt::new(outcome.epoch, outcome.checkpoint_id)
52 || cut.inventory.assignment_fence != outcome.assignment_fence
53 || cut.inventory.deployment_id != outcome.deployment_id
54 {
55 return Err(LeaseError::Invalid(
56 "terminal checkpoint does not settle the exact topology cut".into(),
57 ));
58 }
59 if outcome.is_commit() {
60 cut.committed = Some(TopologyCutCommit {
61 checkpoint: outcome.committed_checkpoint.clone().ok_or_else(|| {
62 LeaseError::Invalid("cut Commit has no checkpoint reference".into())
63 })?,
64 authority_sequence: self.lease.seq,
65 });
66 } else {
67 operation.phase = TopologyAdmissionPhase::Aborted {
68 reason: TopologyAbortReason::CheckpointAborted,
69 };
70 }
71 operation.status_sequence = self.lease.seq;
72 Ok(())
73 }
74
75 pub(super) fn topology_cut_blocks_cleanup(&self, _protected: &CommittedCheckpointRef) -> bool {
76 self.topology_operations.iter().any(|operation| {
77 operation.blocks_admission()
80 })
81 }
82}
83
84impl LeaderLeaseStore {
85 pub(super) async fn validate_committed_topology_checkpoint_inventory(
86 &self,
87 current: &LeaderAuthorityRecord,
88 inventory: &CheckpointArtifactInventory,
89 ) -> Result<(), ClusterCheckpointAuthorityError> {
90 let Some(operation) = current.committed_topology_operation() else {
91 return Ok(());
92 };
93 let plan = self
94 .load_topology_plan(&operation.plan)
95 .await
96 .map_err(|error| DecisionError::Conflict(error.to_string()))?;
97 let descriptor = self
98 .audit_topology_compatibility(&plan)
99 .await
100 .map_err(|error| DecisionError::Conflict(error.to_string()))?
101 .ok_or_else(|| {
102 DecisionError::Conflict("committed topology has no state descriptor".into())
103 })?;
104 if inventory.pipeline_identity != descriptor.target_pipeline
105 || inventory.deployment_id != descriptor.deployment_id
106 {
107 return Err(DecisionError::Conflict(
108 "checkpoint does not belong to the committed target topology".into(),
109 )
110 .into());
111 }
112 Ok(())
113 }
114
115 pub async fn topology_checkpoint_operation(
121 &self,
122 proof: &LeaderProof,
123 assignment: &CheckpointAssignmentFence,
124 ) -> Result<Option<TopologyAdmissionStatus>, TopologyError> {
125 tokio::time::timeout(CONTROL_TIMEOUT, async {
126 let current = self.load_record().await?.ok_or(TopologyError::Fenced)?;
127 if !proof.is_canonical() || !current.lease.matches_proof(proof) {
128 return Err(TopologyError::Fenced);
129 }
130 let Some(operation) = current
131 .topology_operations
132 .iter()
133 .find(|entry| entry.is_preparing())
134 else {
135 return Ok(None);
136 };
137 let plan = self.load_topology_plan(&operation.plan).await?;
138 if plan.assignment != *assignment || operation.admitted_by != *proof {
139 return Err(TopologyError::Conflict(
140 "topology cut assignment or leader changed".into(),
141 ));
142 }
143 self.audit_topology_operation(operation).await?;
144 Ok(Some(operation.clone()))
145 })
146 .await
147 .map_err(|_| TopologyError::ReadTimedOut)?
148 }
149
150 pub async fn begin_topology_checkpoint_cut(
158 &self,
159 proof: &LeaderProof,
160 assignments: &AssignmentSnapshotStore,
161 processes: &crate::cluster::control::ProcessLeaseAuthority,
162 operation_id: TopologyOperationId,
163 expected_plan: &TopologyPlanRef,
164 inventory: CheckpointArtifactInventory,
165 ) -> Result<TopologyAdmissionStatus, TopologyError> {
166 inventory.validate().map_err(TopologyError::Invalid)?;
167 expected_plan.validate()?;
168 tokio::time::timeout(CONTROL_TIMEOUT, async {
169 for _ in 0..MAX_ADMISSION_ATTEMPTS {
170 let published = self.load_published_authority_head().await?.ok_or(TopologyError::Fenced)?;
171 let current = &published.record;
172 if !proof.is_canonical() || !current.lease.matches_proof(proof) {
173 return Err(TopologyError::Fenced);
174 }
175 let index = current.topology_operations.iter().position(|entry| entry.operation_id == operation_id)
176 .ok_or_else(|| TopologyError::Conflict("unknown topology operation".into()))?;
177 let operation = ¤t.topology_operations[index];
178 if operation.plan != *expected_plan {
179 return Err(TopologyError::Conflict("operation payload differs".into()));
180 }
181 self.audit_topology_operation(operation).await?;
182 if let Some(cut) = &operation.cut {
183 if cut.inventory != inventory {
184 return Err(TopologyError::Conflict("topology operation is already bound to another exact checkpoint inventory".into()));
185 }
186 return Ok(operation.clone());
187 }
188 if !matches!(operation.phase, TopologyAdmissionPhase::Planned | TopologyAdmissionPhase::Preparing) || operation.admitted_by != *proof {
189 return Err(TopologyError::Conflict("topology operation cannot start a cut in its current disposition".into()));
190 }
191 let plan = self.load_topology_plan(&operation.plan).await?;
192 let descriptor = self.require_topology_prepared(operation, &plan, processes).await?;
193 if inventory.pipeline_identity != descriptor.parent_pipeline {
194 return Err(TopologyError::Conflict("cut pipeline differs from the certified parent".into()));
195 }
196 let baseline = current.topology_baseline.as_ref().ok_or_else(|| TopologyError::Invalid("cut has no adopted parent".into()))?;
197 if inventory.deployment_id != baseline.deployment_id || inventory.assignment_fence.as_ref() != Some(&plan.assignment)
198 || current.committed_topology_identity() != Some((plan.expected_parent, &plan.parent_manifest))
199 {
200 return Err(TopologyError::Conflict("cut inventory does not bind the admitted parent and assignment".into()));
201 }
202 if current.active_checkpoint_artifacts.is_some() || current.artifact_cleanup.is_some()
203 || current.assignment_drain_reservation.is_some() || current.assignment_handoff_pin.is_some()
204 || current.recovery_fault_slots.iter().any(|slot| slot.active || slot.disposition == RecoveryFaultDisposition::Terminal)
205 {
206 return Err(TopologyError::Conflict("cut overlaps unresolved checkpoint, assignment, cleanup or recovery authority".into()));
207 }
208 let assignment = assignments.load().await.map_err(topology_assignment_error)?
209 .ok_or_else(|| TopologyError::Conflict("cut assignment is missing".into()))?;
210 if assignment.draining || assignment.assignment_fence().map_err(|error| TopologyError::Invalid(error.to_string()))? != plan.assignment {
211 return Err(TopologyError::Conflict("cut assignment or process roster changed".into()));
212 }
213 self.reject_consumed_checkpoint_assignment(current, &plan.assignment).await.map_err(topology_checkpoint_error)?;
214 if current.outcome_head.is_some_and(|head| inventory.attempt.relation_to(CheckpointAttempt::new(head.epoch, head.checkpoint_id)) != CheckpointAttemptRelation::Newer) {
215 return Err(TopologyError::Conflict("topology cut does not advance the terminal checkpoint".into()));
216 }
217 let mut lease = current.lease.clone();
218 lease.seq = lease.seq.checked_add(1).ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
219 let sequence = lease.seq;
220 let mut next = current.preserve_with_lease(lease);
221 next.version = next.version.max(TOPOLOGY_CUT_RECORD_VERSION);
222 next.active_checkpoint_artifacts = Some(inventory.clone());
223 next.active_checkpoint_artifact_leader_proof = Some(proof.clone());
224 let operation = &mut next.topology_operations[index];
225 operation.phase = TopologyAdmissionPhase::Quiescing;
226 operation.status_sequence = sequence;
227 operation.cut = Some(TopologyCheckpointCut { inventory: inventory.clone(), bound_sequence: sequence, committed: None, completed_participants: Vec::new() });
228 let result = operation.clone();
229 match self.create_authority_record(Some(&published), &next).await? {
230 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => return Ok(result),
231 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
232 }
233 }
234 Err(TopologyError::Contended)
235 }).await.map_err(|_| TopologyError::Contended)?
236 }
237
238 #[cfg(feature = "cluster")]
240 pub(in crate::cluster::control) async fn validate_topology_checkpoint_barrier(
241 &self,
242 proof: &LeaderProof,
243 attempt: CheckpointAttempt,
244 assignment: Option<&CheckpointAssignmentFence>,
245 flags: u64,
246 ) -> Result<(), LeaseError> {
247 let current = self
248 .load_record()
249 .await?
250 .ok_or_else(|| LeaseError::Invalid("no durable leader lease exists".into()))?;
251 if !current.lease.matches_proof(proof) {
252 return Err(LeaseError::Invalid(
253 "checkpoint does not match the latest durable leader lease".into(),
254 ));
255 }
256 let operation = current
257 .topology_operations
258 .iter()
259 .find(|entry| entry.blocks_admission());
260 if flags & crate::checkpoint::flags::TOPOLOGY_CUT == 0 {
261 return if operation.is_none() {
262 Ok(())
263 } else {
264 Err(LeaseError::Invalid(
265 "ordinary barrier cannot cross a topology reservation".into(),
266 ))
267 };
268 }
269 let operation = operation.ok_or_else(|| {
270 LeaseError::Invalid("topology barrier has no reserved operation".into())
271 })?;
272 let cut = operation.cut.as_ref().ok_or_else(|| {
273 LeaseError::Invalid("topology barrier has no bound checkpoint".into())
274 })?;
275 if flags != crate::checkpoint::flags::TOPOLOGY_CUT
276 || operation.phase != TopologyAdmissionPhase::Quiescing
277 || operation.admitted_by != *proof
278 || cut.inventory.attempt != attempt
279 || assignment.is_none()
280 || cut.inventory.assignment_fence.as_ref() != assignment
281 || cut.committed.is_some()
282 {
283 return Err(LeaseError::Invalid(
284 "topology barrier differs from its exact live cut".into(),
285 ));
286 }
287 Ok(())
288 }
289
290 pub async fn complete_topology_checkpoint_cut(
300 &self,
301 proof: &LeaderProof,
302 attempt: CheckpointAttempt,
303 participant: CheckpointParticipant,
304 ) -> Result<TopologyAdmissionStatus, TopologyError> {
305 tokio::time::timeout(CONTROL_TIMEOUT, async {
306 for _ in 0..MAX_ADMISSION_ATTEMPTS {
307 let published = self
308 .load_published_authority_head()
309 .await?
310 .ok_or(TopologyError::Fenced)?;
311 let current = &published.record;
312 if !proof.is_canonical() || !current.lease.matches_proof(proof) {
313 return Err(TopologyError::Fenced);
314 }
315 let index = current
316 .topology_operations
317 .iter()
318 .position(|entry| {
319 entry
320 .cut
321 .as_ref()
322 .is_some_and(|cut| cut.inventory.attempt == attempt)
323 })
324 .ok_or_else(|| {
325 TopologyError::Conflict("checkpoint is not a bound topology cut".into())
326 })?;
327 let operation = ¤t.topology_operations[index];
328 let cut = operation
329 .cut
330 .as_ref()
331 .ok_or_else(|| TopologyError::Invalid("cut binding is missing".into()))?;
332 if !operation.is_preparing()
333 || operation.admitted_by != *proof
334 || cut.inventory.assignment_fence.as_ref().is_none_or(|fence| {
335 fence.participant_incarnation(participant.node_id)
336 != Some(participant.boot_incarnation)
337 })
338 || current.recovery_fault_slots.iter().any(|slot| {
339 slot.active || slot.disposition == RecoveryFaultDisposition::Terminal
340 })
341 {
342 return Err(TopologyError::Fenced);
343 }
344 self.audit_topology_operation(operation).await?;
345 if cut.committed.is_none() {
346 return Err(TopologyError::Conflict(
347 "cut has no definitive Commit; sink completion cannot certify it".into(),
348 ));
349 }
350 if cut.completed_participants.contains(&participant) {
351 return Ok(operation.clone());
352 }
353 let mut lease = current.lease.clone();
354 lease.seq = lease
355 .seq
356 .checked_add(1)
357 .ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
358 let sequence = lease.seq;
359 let mut next = current.preserve_with_lease(lease);
360 let operation = &mut next.topology_operations[index];
361 let cut = operation
362 .cut
363 .as_mut()
364 .ok_or_else(|| TopologyError::Invalid("cut binding is missing".into()))?;
365 cut.completed_participants.push(participant);
366 cut.completed_participants
367 .sort_unstable_by_key(|entry| entry.node_id);
368 if cut
369 .inventory
370 .assignment_fence
371 .as_ref()
372 .is_some_and(|fence| cut.completed_participants == fence.participants)
373 {
374 operation.phase = TopologyAdmissionPhase::CutPrepared;
375 }
376 operation.status_sequence = sequence;
377 let result = operation.clone();
378 match self
379 .create_authority_record(Some(&published), &next)
380 .await?
381 {
382 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => {
383 return Ok(result)
384 }
385 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
386 }
387 }
388 Err(TopologyError::Contended)
389 })
390 .await
391 .map_err(|_| TopologyError::Contended)?
392 }
393
394 pub(super) async fn audit_topology_cut(
395 &self,
396 operation: &TopologyAdmissionStatus,
397 ) -> Result<(), LeaseError> {
398 let Some(cut) = &operation.cut else {
399 return Ok(());
400 };
401 let bound = read_authority_record(self.store.as_ref(), cut.bound_sequence)
402 .await?
403 .ok_or_else(|| {
404 LeaseError::Invalid("topology cut admission anchor is missing".into())
405 })?;
406 let anchored = bound
407 .topology_operations
408 .iter()
409 .find(|entry| entry.operation_id == operation.operation_id)
410 .ok_or_else(|| {
411 LeaseError::Invalid("topology cut is absent from its admission anchor".into())
412 })?;
413 if anchored.phase != TopologyAdmissionPhase::Quiescing
414 || anchored.plan != operation.plan
415 || anchored.admitted_by != operation.admitted_by
416 || anchored.admitted_sequence != operation.admitted_sequence
417 || anchored.status_sequence != cut.bound_sequence
418 || anchored.cut.as_ref().is_none_or(|entry| {
419 entry.inventory != cut.inventory
420 || entry.bound_sequence != cut.bound_sequence
421 || entry.committed.is_some()
422 || !entry.completed_participants.is_empty()
423 })
424 || bound.active_checkpoint_artifacts.as_ref() != Some(&cut.inventory)
425 || bound.active_checkpoint_artifact_leader_proof.as_ref()
426 != Some(&operation.admitted_by)
427 {
428 return Err(LeaseError::Invalid(
429 "topology cut differs from its admitted checkpoint inventory".into(),
430 ));
431 }
432 if let Some(commit) = &cut.committed {
433 let record = read_authority_record(self.store.as_ref(), commit.authority_sequence)
434 .await?
435 .ok_or_else(|| {
436 LeaseError::Invalid("topology cut Commit anchor is missing".into())
437 })?;
438 let outcome = record.checkpoint_outcome.as_ref().ok_or_else(|| {
439 LeaseError::Invalid("topology cut anchor has no terminal outcome".into())
440 })?;
441 if !outcome.is_commit()
442 || outcome.committed_checkpoint.as_ref() != Some(&commit.checkpoint)
443 || outcome.assignment_fence != cut.inventory.assignment_fence
444 || outcome.deployment_id != cut.inventory.deployment_id
445 || outcome.leader_proof.as_ref() != Some(&operation.admitted_by)
446 || record
447 .topology_operations
448 .iter()
449 .find(|entry| entry.operation_id == operation.operation_id)
450 .is_none_or(|entry| {
451 entry.phase != TopologyAdmissionPhase::Quiescing
452 || entry.status_sequence != commit.authority_sequence
453 || entry.cut.as_ref().is_none_or(|anchored| {
454 anchored.inventory != cut.inventory
455 || anchored.committed.as_ref() != Some(commit)
456 || !anchored.completed_participants.is_empty()
457 })
458 })
459 {
460 return Err(LeaseError::Invalid(
461 "topology cut does not bind its exact definitive Commit".into(),
462 ));
463 }
464 if operation.blocks_admission() {
465 let index = CheckpointDecisionStore::new(self.store.clone())
466 .load_committed_checkpoint(&commit.checkpoint)
467 .await
468 .map_err(|error| match error {
469 DecisionError::Io(reason) => LeaseError::Io(reason),
470 error => LeaseError::Invalid(error.to_string()),
471 })?;
472 if index.pipeline_identity != cut.inventory.pipeline_identity
473 || index.assignment_fence != cut.inventory.assignment_fence
474 || index.deployment_id != cut.inventory.deployment_id
475 {
476 return Err(LeaseError::Invalid(
477 "topology cut index differs from its old pipeline identity".into(),
478 ));
479 }
480 }
481 }
482 Ok(())
483 }
484}