Skip to main content

laminar_core/cluster/control/leader_lease/topology_admission/
mod.rs

1//! Pre-cut admission and assignment publication share the existing authority CAS.
2
3mod 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    /// Read the latest request identity and its durable progress sequence for an idle worker.
414    ///
415    /// This bounded head observation is only a polling hint. It does not audit referenced blobs
416    /// or authorize any phase, actor, output or gate change. Workers must read the definitive
417    /// status and use the existing fenced phase methods before doing work.
418    ///
419    /// # Errors
420    /// Rejects malformed authority or a read exceeding 15 seconds.
421    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    /// Read the definitive, payload-bound request status without allocating identities.
436    ///
437    /// # Errors
438    /// Rejects missing/corrupt referenced evidence or a read that exceeds 15 seconds.
439    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    /// Reserve a candidate under current authority. This does not commit or start the graph.
462    ///
463    /// The supplied assignment store must be the configured, namespace-verified store used by
464    /// the cluster controller. Its normal drain writers must use `publish_assignment_drain`.
465    /// Call only after coordinated binary upgrade; cached old actors are not revoked by format 14.
466    /// Cancellation/lost responses require retry with exactly the same plan and request identity.
467    ///
468    /// # Errors
469    /// Rejects changed parents, id reuse with different payload, concurrent operations, unresolved
470    /// checkpoint/recovery/assignment authority, malformed evidence, or bounded contention.
471    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            // A periodic checkpoint or its serialized cleanup can finish without changing the
569            // expected parent. Keep its reservation authoritative; the public caller can retry
570            // this same immutable plan within its original deadline.
571            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, &current.topology_operations)
682            .await?;
683        Ok(())
684    }
685
686    /// Durably abort an exact pre-cut request under current authority. No graph rollback occurs.
687    ///
688    /// # Errors
689    /// Rejects a stale proof, unknown request, different payload, or bounded contention.
690    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 = &current.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            (&current.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}