1mod catalog_history;
4mod validation;
5
6use super::*;
7use crate::cluster::control::topology::{
8 TopologyAbortReason, TopologyAdmissionPhase, TopologyAdmissionPlan, TopologyAdmissionStatus,
9 TopologyError, TopologyOperationId, TopologyPlanRef, MAX_TOPOLOGY_OPERATIONS,
10 MAX_TOPOLOGY_PLAN_BYTES,
11};
12
13pub(super) const CONTROL_TIMEOUT: Duration = Duration::from_secs(15);
14pub(super) const MAX_ADMISSION_ATTEMPTS: usize = 16;
15const PLAN_PREFIX: &str = "control/topology-plans/v1/";
16
17fn plan_path(reference: &TopologyPlanRef) -> OsPath {
18 OsPath::from(format!("{PLAN_PREFIX}{}.json", reference.sha256))
19}
20
21fn encode_plan(plan: &TopologyAdmissionPlan) -> Result<(Vec<u8>, TopologyPlanRef), TopologyError> {
22 plan.validate()?;
23 let bytes =
24 serde_json::to_vec(plan).map_err(|error| TopologyError::Invalid(error.to_string()))?;
25 if bytes.len() > MAX_TOPOLOGY_PLAN_BYTES {
26 return Err(TopologyError::Invalid(
27 "topology plan exceeds 32 KiB".into(),
28 ));
29 }
30 let reference = TopologyPlanRef {
31 sha256: format!("{:x}", Sha256::digest(&bytes)),
32 encoded_len: bytes.len() as u64,
33 };
34 reference.validate()?;
35 Ok((bytes, reference))
36}
37
38impl LeaderAuthorityRecord {
39 fn pending_topology_handoff_is_exact(&self) -> bool {
40 if self
41 .topology_operations
42 .iter()
43 .any(TopologyAdmissionStatus::is_preparing)
44 {
45 return false;
46 }
47 let (Some(pin), Some(operation)) = (
48 &self.assignment_handoff_pin,
49 self.committed_topology_operation(),
50 ) else {
51 return false;
52 };
53 let Some(cut) = operation.cut.as_ref() else {
54 return false;
55 };
56 let (Some(commit), Some(assignment)) = (&cut.committed, &cut.inventory.assignment_fence)
57 else {
58 return false;
59 };
60 operation.has_target_commit()
61 && operation.phase != TopologyAdmissionPhase::Active
62 && pin.checkpoint == commit.checkpoint
63 && pin.target.assignment_version >= assignment.assignment_version
64 && pin.target.assignment_digest == assignment.assignment_digest
65 && pin.target.vnode_count == assignment.vnode_count
66 && pin.target.partitioning_abi_version == assignment.partitioning_abi_version
67 && pin
68 .target
69 .participants
70 .iter()
71 .map(|participant| participant.node_id)
72 .eq(assignment
73 .participants
74 .iter()
75 .map(|participant| participant.node_id))
76 }
77
78 pub(super) fn abort_topology_preparation(&mut self, reason: TopologyAbortReason) {
79 for operation in &mut self.topology_operations {
80 if operation.is_preparing() {
81 operation.phase = TopologyAdmissionPhase::Aborted { reason };
82 operation.status_sequence = self.lease.seq;
83 }
84 }
85 }
86
87 pub(super) fn reject_topology_preparation(
88 &self,
89 ) -> Result<(), ClusterCheckpointAuthorityError> {
90 if let Some(operation) = self
91 .topology_operations
92 .iter()
93 .find(|operation| operation.blocks_admission())
94 {
95 return Err(DecisionError::Conflict(format!(
96 "topology operation {} reserves checkpoint and assignment admission",
97 operation.operation_id.get()
98 ))
99 .into());
100 }
101 Ok(())
102 }
103
104 pub(super) fn reject_uncommitted_topology_preparation(
105 &self,
106 ) -> Result<(), ClusterCheckpointAuthorityError> {
107 if let Some(operation) = self
108 .topology_operations
109 .iter()
110 .find(|operation| operation.is_preparing())
111 {
112 return Err(DecisionError::Conflict(format!(
113 "uncommitted topology operation {} reserves assignment recovery admission",
114 operation.operation_id.get()
115 ))
116 .into());
117 }
118 Ok(())
119 }
120
121 pub(super) fn validate_reserved_assignment_decision(
122 &self,
123 decision: &AuthorityAssignmentDecision,
124 ) -> Result<(), ClusterCheckpointAuthorityError> {
125 let Some(reservation) = &self.assignment_drain_reservation else {
126 return Ok(());
127 };
128 if decision.predecessor() != &reservation.transition.predecessor
129 || decision.target_version() != reservation.proposal.version
130 || matches!(decision, AuthorityAssignmentDecision::Drain(drain) if drain.transition != reservation.transition)
131 {
132 return Err(DecisionError::Conflict(
133 "assignment decision does not settle the exact reserved drain".into(),
134 )
135 .into());
136 }
137 Ok(())
138 }
139
140 pub(super) fn validate_topology_admission_successor(
141 &self,
142 next: &Self,
143 ) -> Result<(), LeaseError> {
144 if next.version < self.version
145 || next.topology_operations.len() < self.topology_operations.len()
146 {
147 return Err(LeaseError::Invalid(
148 "authority cannot downgrade or forget topology requests".into(),
149 ));
150 }
151 for (prior, after) in self
152 .topology_operations
153 .iter()
154 .zip(&next.topology_operations)
155 {
156 prior
157 .validate_successor(after, next.lease.seq)
158 .map_err(|error| LeaseError::Invalid(error.to_string()))?;
159 if after.activation != prior.activation
160 && after
161 .activation
162 .as_ref()
163 .is_none_or(|round| !next.lease.matches_proof(&round.leader))
164 {
165 return Err(LeaseError::Invalid(
166 "installation/Release append requires its exact current leader".into(),
167 ));
168 }
169 if let Some(cut) = &after.cut {
170 let prior_commit = prior.cut.as_ref().and_then(|cut| cut.committed.as_ref());
171 if prior_commit.is_none() {
172 if let Some(commit) = &cut.committed {
173 if commit.authority_sequence != next.lease.seq
174 || next.checkpoint_outcome.as_ref().is_none_or(|outcome| {
175 !outcome.is_commit()
176 || outcome.committed_checkpoint.as_ref()
177 != Some(&commit.checkpoint)
178 || outcome.assignment_fence != cut.inventory.assignment_fence
179 || outcome.deployment_id != cut.inventory.deployment_id
180 || outcome.leader_proof.as_ref() != Some(&after.admitted_by)
181 })
182 {
183 return Err(LeaseError::Invalid(
184 "new topology cut Commit must bind its terminal authority append"
185 .into(),
186 ));
187 }
188 }
189 }
190 }
191 }
192 for added in next
193 .topology_operations
194 .iter()
195 .skip(self.topology_operations.len())
196 {
197 if !added.is_planned()
198 || added.admitted_sequence != next.lease.seq
199 || !next.lease.matches_proof(&added.admitted_by)
200 {
201 return Err(LeaseError::Invalid(
202 "new topology request must bind its exact admission append".into(),
203 ));
204 }
205 }
206 if let Some(reservation) = &self.assignment_drain_reservation {
207 if next.assignment_drain_reservation.as_ref() != Some(reservation) {
208 let decision = next.assignment_decision.as_ref().ok_or_else(|| {
209 LeaseError::Invalid(
210 "reserved assignment can be cleared only by its definitive decision".into(),
211 )
212 })?;
213 self.validate_reserved_assignment_decision(decision)
214 .map_err(|e| LeaseError::Invalid(e.to_string()))?;
215 if next.assignment_drain_reservation.is_some() {
216 return Err(LeaseError::Invalid(
217 "cannot replace an unresolved assignment reservation".into(),
218 ));
219 }
220 }
221 } else if let Some(reservation) = &next.assignment_drain_reservation {
222 if reservation.authority_sequence != next.lease.seq
223 || !next.lease.matches_proof(&reservation.transition.leader)
224 {
225 return Err(LeaseError::Invalid(
226 "new assignment reservation must bind its admission append".into(),
227 ));
228 }
229 }
230 Ok(())
231 }
232}
233
234impl LeaderLeaseStore {
235 pub(super) async fn read_admission_blob(
236 &self,
237 path: &OsPath,
238 expected_len: u64,
239 ) -> Result<Bytes, LeaseError> {
240 let result = self.store.get(path).await.map_err(|e| match e {
241 object_store::Error::NotFound { .. } => {
242 LeaseError::Invalid(format!("admission artifact '{path}' is missing"))
243 }
244 e => LeaseError::Io(e.to_string()),
245 })?;
246 if result.meta.size != expected_len {
247 return Err(LeaseError::Invalid(format!(
248 "admission artifact '{path}' has the wrong length"
249 )));
250 }
251 let bytes = result
252 .bytes()
253 .await
254 .map_err(|e| LeaseError::Io(e.to_string()))?;
255 if bytes.len() as u64 != expected_len {
256 return Err(LeaseError::Invalid(
257 "admission artifact length changed while reading".into(),
258 ));
259 }
260 Ok(bytes)
261 }
262
263 pub(super) async fn stage_admission_blob(
264 &self,
265 path: &OsPath,
266 bytes: &[u8],
267 ) -> Result<(), LeaseError> {
268 let put_error = self
269 .store
270 .put_opts(
271 path,
272 PutPayload::from(Bytes::copy_from_slice(bytes)),
273 PutOptions {
274 mode: PutMode::Create,
275 ..PutOptions::default()
276 },
277 )
278 .await
279 .err();
280 let stored = self
281 .read_admission_blob(path, bytes.len() as u64)
282 .await
283 .map_err(|e| match put_error {
284 Some(put) => LeaseError::Io(format!(
285 "admission write failed ({put}); read-back failed ({e})"
286 )),
287 None => e,
288 })?;
289 if stored.as_ref() != bytes {
290 return Err(LeaseError::Invalid(
291 "immutable admission artifact differs from its payload".into(),
292 ));
293 }
294 Ok(())
295 }
296
297 pub(super) async fn load_topology_plan(
298 &self,
299 reference: &TopologyPlanRef,
300 ) -> Result<TopologyAdmissionPlan, TopologyError> {
301 reference.validate()?;
302 let bytes = self
303 .read_admission_blob(&plan_path(reference), reference.encoded_len)
304 .await?;
305 let plan: TopologyAdmissionPlan =
306 serde_json::from_slice(&bytes).map_err(|e| TopologyError::Invalid(e.to_string()))?;
307 let (canonical, actual) = encode_plan(&plan)?;
308 if actual != *reference || canonical.as_slice() != bytes.as_ref() {
309 return Err(TopologyError::Invalid(
310 "topology plan does not match its immutable reference".into(),
311 ));
312 }
313 Ok(plan)
314 }
315
316 pub(super) async fn audit_topology_operation(
317 &self,
318 operation: &TopologyAdmissionStatus,
319 ) -> Result<(), LeaseError> {
320 let plan = self
321 .load_topology_plan(&operation.plan)
322 .await
323 .map_err(topology_lease_error)?;
324 if plan.operation_id != operation.operation_id
325 || plan
326 .assignment
327 .participant_incarnation(operation.admitted_by.owner.node_id)
328 != Some(operation.admitted_by.owner.boot_id)
329 {
330 return Err(LeaseError::Invalid(
331 "topology request differs from its retained payload".into(),
332 ));
333 }
334 for reference in [&plan.parent_manifest, &plan.target_manifest] {
335 self.load_catalog_manifest(reference)
336 .await
337 .map_err(TopologyError::from)
338 .map_err(topology_lease_error)?;
339 }
340 if operation
341 .cut
342 .as_ref()
343 .is_some_and(|cut| cut.inventory.assignment_fence.as_ref() != Some(&plan.assignment))
344 {
345 return Err(LeaseError::Invalid(
346 "topology cut changed its frozen assignment".into(),
347 ));
348 }
349 for sequence in [operation.admitted_sequence, operation.status_sequence] {
350 let record = read_authority_record(self.store.as_ref(), sequence)
351 .await?
352 .ok_or_else(|| {
353 LeaseError::Invalid("topology operation authority anchor is missing".into())
354 })?;
355 if plan.protocol_version == super::super::topology::TOPOLOGY_SUBMISSION_PROTOCOL_VERSION
356 && record.version < TOPOLOGY_SUBMISSION_RECORD_VERSION
357 {
358 return Err(LeaseError::Invalid(
359 "public topology plan is missing its authority format 23 gate".into(),
360 ));
361 }
362 let anchored = record
363 .topology_operations
364 .iter()
365 .find(|entry| entry.operation_id == operation.operation_id)
366 .ok_or_else(|| {
367 LeaseError::Invalid(
368 "topology operation is absent from its authority anchor".into(),
369 )
370 })?;
371 let valid = if sequence == operation.admitted_sequence {
372 anchored.is_planned()
373 && anchored.admitted_sequence == sequence
374 && anchored.plan == operation.plan
375 && anchored.admitted_by == operation.admitted_by
376 && anchored
377 .preparation
378 .as_ref()
379 .map(|preparation| &preparation.compatibility)
380 == operation
381 .preparation
382 .as_ref()
383 .map(|preparation| &preparation.compatibility)
384 && record.topology_baseline.as_ref().is_some_and(|baseline| {
385 record.committed_topology_identity()
386 == Some((plan.expected_parent, &plan.parent_manifest))
387 && operation.cut.as_ref().is_none_or(|cut| {
388 cut.inventory.deployment_id == baseline.deployment_id
389 })
390 })
391 } else {
392 anchored == operation
393 };
394 if !valid {
395 return Err(LeaseError::Invalid(
396 "topology operation differs from its retained authority anchor".into(),
397 ));
398 }
399 }
400 let descriptor = self
401 .audit_topology_preparation(operation, &plan)
402 .await
403 .map_err(topology_lease_error)?;
404 self.audit_topology_cut(operation).await?;
405 self.audit_topology_migration_root(operation, &plan, descriptor.as_ref())
406 .await
407 .map_err(topology_lease_error)?;
408 self.audit_topology_target_preparations(operation).await?;
409 self.audit_topology_commit(operation, &plan).await?;
410 self.audit_topology_activation(operation).await
411 }
412
413 pub async fn latest_topology_operation_hint(
422 &self,
423 ) -> Result<Option<(TopologyOperationId, u64)>, TopologyError> {
424 tokio::time::timeout(CONTROL_TIMEOUT, async {
425 Ok(self.load_record().await?.and_then(|head| {
426 head.topology_operations
427 .last()
428 .map(|operation| (operation.operation_id, operation.status_sequence))
429 }))
430 })
431 .await
432 .map_err(|_| TopologyError::ReadTimedOut)?
433 }
434
435 pub async fn topology_operation_status(
440 &self,
441 operation_id: TopologyOperationId,
442 ) -> Result<Option<TopologyAdmissionStatus>, TopologyError> {
443 tokio::time::timeout(CONTROL_TIMEOUT, async {
444 let Some(head) = self.load_record().await? else {
445 return Ok(None);
446 };
447 let Some(operation) = head
448 .topology_operations
449 .iter()
450 .find(|entry| entry.operation_id == operation_id)
451 else {
452 return Ok(None);
453 };
454 self.audit_topology_operation(operation).await?;
455 Ok(Some(operation.clone()))
456 })
457 .await
458 .map_err(|_| TopologyError::ReadTimedOut)?
459 }
460
461 pub async fn admit_topology_plan(
472 &self,
473 proof: &LeaderProof,
474 assignments: &AssignmentSnapshotStore,
475 plan: &TopologyAdmissionPlan,
476 target: &CatalogManifest,
477 ) -> Result<TopologyAdmissionStatus, TopologyError> {
478 tokio::time::timeout(
479 CONTROL_TIMEOUT,
480 self.admit_topology_plan_inner(proof, assignments, plan, target),
481 )
482 .await
483 .map_err(|_| TopologyError::Contended)?
484 }
485
486 async fn admit_topology_plan_inner(
487 &self,
488 proof: &LeaderProof,
489 assignments: &AssignmentSnapshotStore,
490 plan: &TopologyAdmissionPlan,
491 target: &CatalogManifest,
492 ) -> Result<TopologyAdmissionStatus, TopologyError> {
493 let (encoded, reference) = encode_plan(plan)?;
494 let (target_bytes, target_ref) = target.encode_and_reference()?;
495 if target_ref != plan.target_manifest {
496 return Err(TopologyError::Invalid(
497 "proposal does not bind its target inventory".into(),
498 ));
499 }
500 if !proof.is_canonical() {
501 return Err(TopologyError::Fenced);
502 }
503 for _ in 0..MAX_ADMISSION_ATTEMPTS {
504 let published = self
505 .load_published_authority_head()
506 .await?
507 .ok_or(TopologyError::Fenced)?;
508 let current = &published.record;
509 if !current.lease.matches_proof(proof) {
510 return Err(TopologyError::Fenced);
511 }
512 if let Some(existing) = current
513 .topology_operations
514 .iter()
515 .find(|entry| entry.operation_id == plan.operation_id)
516 {
517 if existing.plan != reference {
518 return Err(TopologyError::Conflict(
519 "operation identity was already bound to another payload".into(),
520 ));
521 }
522 self.audit_topology_operation(existing).await?;
523 return Ok(existing.clone());
524 }
525 if plan.assignment.participant_incarnation(proof.owner.node_id)
526 != Some(proof.owner.boot_id)
527 {
528 return Err(TopologyError::Fenced);
529 }
530 let baseline = current.topology_baseline.as_ref().ok_or_else(|| {
531 TopologyError::Protocol(
532 "explicit legacy topology adoption is required before migration admission"
533 .into(),
534 )
535 })?;
536 if baseline.operation_id == plan.operation_id {
537 return Err(TopologyError::Conflict(
538 "operation identity was already used for legacy adoption".into(),
539 ));
540 }
541 if current.committed_topology_identity()
542 != Some((plan.expected_parent, &plan.parent_manifest))
543 {
544 return Err(TopologyError::Conflict(
545 "expected topology parent does not match committed authority".into(),
546 ));
547 }
548 self.audit_topology_adoption(baseline).await?;
549 self.require_topology_deployment(&baseline.deployment_id)
550 .await?;
551 if current.topology_operations.len() >= MAX_TOPOLOGY_OPERATIONS {
552 return Err(TopologyError::Conflict("topology journal is full; retention is required before admitting further requests".into()));
553 }
554 if current
555 .topology_operations
556 .iter()
557 .any(TopologyAdmissionStatus::blocks_admission)
558 || current.assignment_drain_reservation.is_some()
559 || current.assignment_handoff_pin.is_some()
560 || current.recovery_fault_slots.iter().any(|slot| {
561 slot.active || slot.disposition == RecoveryFaultDisposition::Terminal
562 })
563 {
564 return Err(TopologyError::Conflict(
565 "recovery, assignment or another topology operation is unresolved".into(),
566 ));
567 }
568 if current.active_checkpoint_artifacts.is_some() || current.artifact_cleanup.is_some() {
572 return Err(TopologyError::Contended);
573 }
574 let assignment = assignments
575 .load()
576 .await
577 .map_err(topology_assignment_error)?
578 .ok_or_else(|| {
579 TopologyError::Conflict("assignment inventory is uninitialized".into())
580 })?;
581 if assignment.draining
582 || assignment
583 .assignment_fence()
584 .map_err(|e| TopologyError::Invalid(e.to_string()))?
585 != plan.assignment
586 {
587 return Err(TopologyError::Conflict(
588 "required assignment/boot roster changed".into(),
589 ));
590 }
591 self.reject_consumed_checkpoint_assignment(current, &plan.assignment)
592 .await
593 .map_err(topology_checkpoint_error)?;
594 let parent = self.load_catalog_manifest(&plan.parent_manifest).await?;
595 self.validate_topology_inventory_proposal(current, plan, &parent, target)
596 .await?;
597 self.ensure_catalog_manifest_blob(&target_bytes, &target_ref)
598 .await?;
599 self.stage_admission_blob(&plan_path(&reference), &encoded)
600 .await?;
601 let mut lease = current.lease.clone();
602 lease.seq = lease
603 .seq
604 .checked_add(1)
605 .ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
606 let sequence = lease.seq;
607 let mut next = current.preserve_with_lease(lease);
608 next.version = next.version.max(
609 if plan.protocol_version
610 == super::super::topology::TOPOLOGY_SUBMISSION_PROTOCOL_VERSION
611 {
612 TOPOLOGY_SUBMISSION_RECORD_VERSION
613 } else if plan.compatibility.is_some() {
614 TOPOLOGY_PREPARATION_RECORD_VERSION
615 } else {
616 TOPOLOGY_ADMISSION_RECORD_VERSION
617 },
618 );
619 let operation = TopologyAdmissionStatus {
620 operation_id: plan.operation_id,
621 plan: reference.clone(),
622 admitted_by: proof.clone(),
623 admitted_sequence: sequence,
624 status_sequence: sequence,
625 phase: TopologyAdmissionPhase::Planned,
626 cut: None,
627 migration_root: None,
628 target_preparations: Vec::new(),
629 commit: None,
630 activation: None,
631 preparation: plan.compatibility.clone().map(|compatibility| {
632 crate::cluster::control::topology::TopologyPreparation {
633 compatibility,
634 certificates: Vec::new(),
635 complete_sequence: None,
636 }
637 }),
638 };
639 next.topology_operations.push(operation.clone());
640 match self
641 .create_authority_record(Some(&published), &next)
642 .await?
643 {
644 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => {
645 return Ok(operation)
646 }
647 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
648 }
649 }
650 Err(TopologyError::Contended)
651 }
652
653 pub(super) async fn validate_topology_inventory_proposal(
654 &self,
655 current: &LeaderAuthorityRecord,
656 plan: &TopologyAdmissionPlan,
657 parent: &CatalogManifest,
658 target: &CatalogManifest,
659 ) -> Result<(), TopologyError> {
660 if let Some(reference) = &plan.compatibility {
661 let descriptor = self.load_topology_compatibility(reference).await?;
662 descriptor.validate_catalogs(parent, target)?;
663 let baseline = current
664 .topology_baseline
665 .as_ref()
666 .ok_or(TopologyError::Fenced)?;
667 if descriptor.parent_version != plan.expected_parent
668 || descriptor.deployment_id != baseline.deployment_id
669 {
670 return Err(TopologyError::Conflict(
671 "descriptor parent/deployment differs from admission authority".into(),
672 ));
673 }
674 } else if target.entries.len() <= parent.entries.len()
675 || !target.entries.starts_with(&parent.entries)
676 {
677 return Err(TopologyError::Invalid(
678 "removal requires an explicit certified compatibility mapping".into(),
679 ));
680 }
681 self.validate_recreated_catalog_names(plan, parent, target, ¤t.topology_operations)
682 .await?;
683 Ok(())
684 }
685
686 pub async fn abort_topology_plan(
691 &self,
692 proof: &LeaderProof,
693 operation_id: TopologyOperationId,
694 expected_plan: &TopologyPlanRef,
695 ) -> Result<TopologyAdmissionStatus, TopologyError> {
696 tokio::time::timeout(CONTROL_TIMEOUT, async {
697 expected_plan.validate()?;
698 for _ in 0..MAX_ADMISSION_ATTEMPTS {
699 let published = self
700 .load_published_authority_head()
701 .await?
702 .ok_or(TopologyError::Fenced)?;
703 let current = &published.record;
704 if !current.lease.matches_proof(proof) {
705 return Err(TopologyError::Fenced);
706 }
707 let index = current
708 .topology_operations
709 .iter()
710 .position(|entry| entry.operation_id == operation_id)
711 .ok_or_else(|| TopologyError::Conflict("unknown topology operation".into()))?;
712 let operation = ¤t.topology_operations[index];
713 if operation.plan != *expected_plan {
714 return Err(TopologyError::Conflict("operation payload differs".into()));
715 }
716 self.audit_topology_operation(operation).await?;
717 if operation.has_target_commit() {
718 return Err(TopologyError::Conflict("committed topology requires target recovery and cannot abort".into()));
719 }
720 if !operation.is_preparing() {
721 return Ok(operation.clone());
722 }
723 if operation.phase == TopologyAdmissionPhase::Quiescing {
724 return Err(TopologyError::Conflict("cut sink settlement is unresolved; coordinated recovery must reconcile it before resuming the parent".into()));
725 }
726 let mut lease = current.lease.clone();
727 lease.seq = lease
728 .seq
729 .checked_add(1)
730 .ok_or_else(|| TopologyError::Invalid("authority sequence exhausted".into()))?;
731 let mut next = current.preserve_with_lease(lease);
732 next.abort_topology_preparation(TopologyAbortReason::Requested);
733 let result = next.topology_operations[index].clone();
734 match self
735 .create_authority_record(Some(&published), &next)
736 .await?
737 {
738 AuthorityCreateOutcome::Created | AuthorityCreateOutcome::ExistingIdentical => {
739 return Ok(result)
740 }
741 AuthorityCreateOutcome::Contended(_) => tokio::task::yield_now().await,
742 }
743 }
744 Err(TopologyError::Contended)
745 })
746 .await
747 .map_err(|_| TopologyError::Contended)?
748 }
749}
750
751pub(super) fn topology_checkpoint_error(error: ClusterCheckpointAuthorityError) -> TopologyError {
752 match error {
753 ClusterCheckpointAuthorityError::Authority(error) => TopologyError::Authority(error),
754 ClusterCheckpointAuthorityError::Decision(DecisionError::Io(reason)) => {
755 TopologyError::Authority(LeaseError::Io(reason))
756 }
757 ClusterCheckpointAuthorityError::Fenced => TopologyError::Fenced,
758 error => TopologyError::Conflict(error.to_string()),
759 }
760}
761
762pub(super) fn topology_assignment_error(error: SnapshotError) -> TopologyError {
763 match error {
764 SnapshotError::Io(reason) => TopologyError::Authority(LeaseError::Io(reason)),
765 error => TopologyError::Invalid(error.to_string()),
766 }
767}
768
769fn topology_lease_error(error: TopologyError) -> LeaseError {
770 match error {
771 TopologyError::Authority(error) => error,
772 error => LeaseError::Invalid(error.to_string()),
773 }
774}
775
776impl LeaderLeaseStore {
777 pub(super) async fn validate_assignment_topology_settlement(
778 &self,
779 current: &LeaderAuthorityRecord,
780 decision: &AuthorityAssignmentDecision,
781 assignments: Option<&AssignmentSnapshotStore>,
782 ) -> Result<(), ClusterCheckpointAuthorityError> {
783 match decision {
784 AuthorityAssignmentDecision::Recovery(_) => {
785 current.reject_uncommitted_topology_preparation()?;
786 }
787 AuthorityAssignmentDecision::Drain(_) => current.reject_topology_preparation()?,
788 }
789 let target = match decision {
790 AuthorityAssignmentDecision::Recovery(recovery) => &recovery.target,
791 AuthorityAssignmentDecision::Drain(drain) => match drain.verdict {
792 AssignmentDrainVerdict::Commit => &drain.transition.target,
793 AssignmentDrainVerdict::Abort => &drain.transition.predecessor,
794 },
795 };
796 self.validate_topology_assignment_proposal_from(current, target)
797 .await?;
798 current.validate_reserved_assignment_decision(decision)?;
799 if let (Some(reservation), AuthorityAssignmentDecision::Drain(_)) =
800 (¤t.assignment_drain_reservation, decision)
801 {
802 let assignments = assignments.ok_or_else(|| {
803 LeaseError::Invalid(
804 "reserved drain settlement requires its configured assignment store".into(),
805 )
806 })?;
807 self.materialize_drain_reservation(reservation, assignments)
808 .await?;
809 }
810 Ok(())
811 }
812}
813
814impl LeaderAuthorityRecord {
815 pub(super) fn reject_assignment_checkpoint_overtake(
816 &self,
817 decision: &AuthorityAssignmentDecision,
818 ) -> Result<(), ClusterCheckpointAuthorityError> {
819 if matches!(decision, AuthorityAssignmentDecision::Drain(_)) {
820 let predecessor = decision.predecessor();
821 if let Some(active) = self.active_checkpoint_artifacts.as_ref() {
822 let active_fence = active.assignment_fence.as_ref().ok_or_else(|| {
823 DecisionError::Conflict(
824 "active checkpoint artifact inventory lost its assignment fence".into(),
825 )
826 })?;
827 if active_fence.assignment_version == predecessor.assignment_version {
828 let detail = if active_fence == predecessor {
829 "matches"
830 } else {
831 "conflicts with"
832 };
833 return Err(DecisionError::Conflict(format!(
834 "assignment drain decision cannot overtake checkpoint {} whose assignment fence {detail} predecessor version {}",
835 active.attempt.checkpoint_id,
836 predecessor.assignment_version
837 ))
838 .into());
839 }
840 }
841 }
842 Ok(())
843 }
844}