1use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
5use std::sync::Arc;
6use std::time::Duration;
7
8use futures::StreamExt as _;
9use sha2::{Digest as _, Sha256};
10use tokio::sync::watch;
11use uuid::Uuid;
12
13use super::barrier::{
14 BarrierAck, BarrierAnnouncement, BarrierCoordinator, ClusterKv, QuorumOutcome,
15};
16use super::leader::leader_of;
17use super::snapshot::AssignmentSnapshotStore;
18use crate::checkpoint::{
19 AssignmentDrainId, AssignmentDrainTransition, CheckpointAssignmentAdoption,
20 CheckpointAssignmentFence, CheckpointParticipant, LeaderProof, PreparedCheckpointWitness,
21 MAX_CHECKPOINT_PARTICIPANTS, MAX_PREPARED_CHECKPOINT_WITNESSES,
22};
23use crate::cluster::discovery::{assignable_node_ids, NodeId, NodeInfo, NodeState};
24#[cfg(test)]
25use crate::state::CheckpointAttempt;
26use crate::state::Locality;
27
28const RECOVERY_INCARNATION_KEY: &str = "control:recovery-incarnation";
29const DRAIN_ACK_KEY: &str = "control:drain-ack";
30const DRAIN_ACK_PROTOCOL_VERSION: u16 = 1;
31const RELEASE_READY_ACK_KEY: &str = "control:recovery-release-ready";
32const RELEASE_READY_PROTOCOL_VERSION: u16 = 2;
33const RECOVERY_STOPPED_REPORT_KEY: &str = "control:recovery-stopped";
34const RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION: u16 = 3;
36const MAX_RECOVERY_STOPPED_REPORT_BYTES: usize = 32 * 1_024;
38pub(super) const MAX_RECOVERY_ANNOUNCEMENT_BYTES: usize = 256 * 1_024;
40const RECOVERY_CONTROL_IO_TIMEOUT: Duration = Duration::from_secs(5);
41const MAX_DRAIN_ACK_BYTES: usize = 1_024;
42const MAX_RELEASE_READY_ACK_BYTES: usize = 1_024;
43const CONTROL_ROSTER_IO_CONCURRENCY: usize = 32;
44const PENDING_RELEASE_FAULT_AUDIT_INTERVAL: Duration = Duration::from_secs(1);
45
46#[cfg(feature = "cluster")]
47struct LeaderLeaseGate {
48 lease: watch::Receiver<Option<super::LeaderLease>>,
49 owner: super::LeaderLeaseOwner,
50 deadline: Arc<super::LeaseDeadline>,
51}
52
53#[cfg(feature = "cluster")]
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum CheckpointPrepareObservation {
57 AssignmentReady(BarrierAnnouncement),
59 AssignmentRejected {
61 announcement: BarrierAnnouncement,
63 error: String,
65 },
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
70pub struct RecoveryRoundId {
71 pub generation: u64,
73 pub nonce: Uuid,
75 pub driver: NodeId,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
81#[serde(deny_unknown_fields)]
82pub struct RecoveryRound {
83 pub id: RecoveryRoundId,
85 pub leader_proof: LeaderProof,
87 pub assignment_fence: CheckpointAssignmentFence,
89 pub evidence_participants: Vec<CheckpointParticipant>,
94 fault_revision: u64,
96 pub faults: Vec<RecoveryFault>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct RecoveryFault {
104 pub reporter: NodeId,
106 pub sequence: u64,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
111#[serde(deny_unknown_fields)]
112pub(crate) struct RecoveryFaultPublisher {
113 pub(crate) participant: CheckpointParticipant,
114 pub(crate) process_term: u64,
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct RecoveryFaultRequest {
120 sequence: std::num::NonZeroU64,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum RecoveryFaultReportOutcome {
126 Active,
128 AlreadyCleared,
130 CoveredByNewerRequest,
132}
133
134impl RecoveryFaultRequest {
135 #[must_use]
137 pub const fn sequence(self) -> u64 {
138 self.sequence.get()
139 }
140}
141
142impl RecoveryFaultPublisher {
143 pub(crate) fn validate(&self) -> Result<(), String> {
144 if self.participant.node_id == 0
145 || self.participant.boot_incarnation.is_nil()
146 || self.process_term == 0
147 {
148 return Err("recovery fault publisher identity is not canonical".into());
149 }
150 Ok(())
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct RecoveryFaultInventory {
157 pub(crate) revision: u64,
158 pub(crate) faults: Vec<RecoveryFault>,
159}
160
161impl RecoveryFaultInventory {
162 #[must_use]
164 pub const fn revision(&self) -> u64 {
165 self.revision
166 }
167
168 #[must_use]
170 pub fn faults(&self) -> &[RecoveryFault] {
171 &self.faults
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
179pub struct RecoveryAdmissionSnapshot {
180 pub(crate) committed_release: Option<RecoveryAnnouncement>,
181 pub(crate) fault_inventory: RecoveryFaultInventory,
182 pub(crate) authority_sequence: u64,
183 pub(crate) release_head: Option<super::leader_lease::RecoveryReleaseLink>,
184}
185
186impl RecoveryAdmissionSnapshot {
187 #[must_use]
189 pub fn committed_release(&self) -> Option<&RecoveryAnnouncement> {
190 self.committed_release.as_ref()
191 }
192
193 #[must_use]
195 pub const fn fault_inventory(&self) -> &RecoveryFaultInventory {
196 &self.fault_inventory
197 }
198}
199
200impl RecoveryRound {
201 pub fn new(
206 generation: u64,
207 leader_proof: LeaderProof,
208 assignment_fence: CheckpointAssignmentFence,
209 evidence_participants: Vec<CheckpointParticipant>,
210 fault_revision: u64,
211 faults: Vec<RecoveryFault>,
212 ) -> Result<Self, String> {
213 let driver = NodeId(leader_proof.owner.node_id);
214 let round = Self {
215 id: RecoveryRoundId {
216 generation,
217 nonce: Uuid::new_v4(),
218 driver,
219 },
220 leader_proof,
221 assignment_fence,
222 evidence_participants,
223 fault_revision,
224 faults,
225 };
226 round.validate()?;
227 Ok(round)
228 }
229
230 #[must_use]
232 pub fn contains_owner(&self, node: NodeId) -> bool {
233 self.assignment_fence.contains(node.0)
234 }
235
236 #[must_use]
238 pub fn owners(&self) -> Vec<NodeId> {
239 self.assignment_fence
240 .participants
241 .iter()
242 .map(|participant| NodeId(participant.node_id))
243 .collect()
244 }
245
246 #[must_use]
248 pub(crate) fn owner_incarnation(&self, node: NodeId) -> Option<Uuid> {
249 self.assignment_fence.participant_incarnation(node.0)
250 }
251
252 #[must_use]
254 pub fn contains_stopped_participant(&self, node: NodeId) -> bool {
255 self.contains_owner(node)
256 || self
257 .evidence_participants
258 .binary_search_by_key(&node.0, |participant| participant.node_id)
259 .is_ok()
260 }
261
262 #[must_use]
264 pub fn stopped_participants(&self) -> Vec<NodeId> {
265 self.stopped_roster()
266 .into_iter()
267 .map(|participant| NodeId(participant.node_id))
268 .collect()
269 }
270
271 #[must_use]
273 pub(crate) fn stopped_participant_incarnation(&self, node: NodeId) -> Option<Uuid> {
274 self.assignment_fence
275 .participant_incarnation(node.0)
276 .or_else(|| {
277 self.evidence_participants
278 .binary_search_by_key(&node.0, |participant| participant.node_id)
279 .ok()
280 .map(|index| self.evidence_participants[index].boot_incarnation)
281 })
282 }
283
284 fn stopped_roster(&self) -> Vec<CheckpointParticipant> {
285 let mut roster = Vec::with_capacity(
286 self.assignment_fence.participants.len() + self.evidence_participants.len(),
287 );
288 roster.extend(self.assignment_fence.participants.iter().copied());
289 roster.extend(self.evidence_participants.iter().copied());
290 roster.sort_unstable_by_key(|participant| participant.node_id);
291 roster
292 }
293
294 #[must_use]
296 pub fn fault_sequence(&self, node: NodeId) -> Option<u64> {
297 self.faults
298 .binary_search_by_key(&node, |fault| fault.reporter)
299 .ok()
300 .map(|index| self.faults[index].sequence)
301 }
302
303 #[must_use]
305 pub const fn fault_revision(&self) -> u64 {
306 self.fault_revision
307 }
308
309 fn validate(&self) -> Result<(), String> {
310 if self.id.generation == 0 {
311 return Err("recovery generation must be nonzero".into());
312 }
313 if self.id.nonce.is_nil() {
314 return Err("recovery nonce must be non-nil".into());
315 }
316 if self.id.driver.is_unassigned() {
317 return Err("recovery driver must be assigned".into());
318 }
319 if !self.leader_proof.is_canonical() || self.id.driver.0 != self.leader_proof.owner.node_id
320 {
321 return Err("recovery driver must match a canonical leader proof".into());
322 }
323 if !self.assignment_fence.is_canonical() {
324 return Err("recovery assignment certificate is not canonical".into());
325 }
326 if !self.contains_owner(self.id.driver) {
327 return Err("recovery driver is absent from the frozen quorum".into());
328 }
329 if self.owner_incarnation(self.id.driver) != Some(self.leader_proof.owner.boot_id) {
330 return Err("recovery leader proof is not bound to the frozen driver process".into());
331 }
332 if self.fault_revision == 0 {
333 return Err("recovery fault revision must be nonzero".into());
334 }
335 if self.faults.is_empty()
336 || self.faults.iter().any(|fault| {
337 fault.reporter.is_unassigned()
338 || fault.sequence == 0
339 || fault.sequence > self.fault_revision
340 })
341 || self
342 .faults
343 .windows(2)
344 .any(|pair| pair[0].reporter >= pair[1].reporter)
345 {
346 return Err("recovery fault set is not canonical".into());
347 }
348 if self
349 .evidence_participants
350 .iter()
351 .any(|participant| participant.node_id == 0 || participant.boot_incarnation.is_nil())
352 || self
353 .evidence_participants
354 .windows(2)
355 .any(|pair| pair[0].node_id >= pair[1].node_id)
356 {
357 return Err("recovery evidence participant roster is not canonical".into());
358 }
359 if self.assignment_fence.participants.len() + self.evidence_participants.len()
360 > MAX_CHECKPOINT_PARTICIPANTS
361 {
362 return Err(format!(
363 "recovery stopped roster has {} participants; maximum is {MAX_CHECKPOINT_PARTICIPANTS}",
364 self.assignment_fence.participants.len() + self.evidence_participants.len()
365 ));
366 }
367 if self.evidence_participants.iter().any(|participant| {
368 self.assignment_fence.contains(participant.node_id)
369 || self
370 .faults
371 .binary_search_by_key(&NodeId(participant.node_id), |fault| fault.reporter)
372 .is_err()
373 }) {
374 return Err("recovery evidence participants must be non-owner fault reporters".into());
375 }
376 let largest_terminal = RecoveryAnnouncement {
377 round: self.clone(),
378 phase: RecoverPhase::ReleaseCommitted { epoch: u64::MAX },
379 };
380 let encoded = serde_json::to_vec(&largest_terminal)
381 .map_err(|error| format!("could not encode recovery round: {error}"))?;
382 validate_recovery_announcement_size(encoded.len())?;
383 Ok(())
384 }
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
389pub enum RecoverPhase {
390 Prepare,
392 Start {
394 epoch: u64,
396 },
397 Release {
400 epoch: u64,
402 },
403 ReleaseCommitted {
405 epoch: u64,
407 },
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
412#[serde(deny_unknown_fields)]
413pub struct RecoveryAnnouncement {
414 pub round: RecoveryRound,
416 pub phase: RecoverPhase,
418}
419
420impl RecoveryAnnouncement {
421 pub(crate) fn validate(&self) -> Result<(), String> {
422 self.round.validate()?;
423 let encoded = serde_json::to_vec(self)
424 .map_err(|error| format!("could not encode recovery announcement: {error}"))?;
425 validate_recovery_announcement_size(encoded.len())
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
434#[serde(deny_unknown_fields)]
435pub struct RecoveryStoppedReport {
436 protocol_version: u16,
438 round_id: RecoveryRoundId,
440 round_sha256: String,
442 publisher: CheckpointParticipant,
444 prepared_witnesses: Vec<PreparedCheckpointWitness>,
446}
447
448impl RecoveryStoppedReport {
449 pub fn new(
455 round: &RecoveryRound,
456 publisher: CheckpointParticipant,
457 prepared_witnesses: Vec<PreparedCheckpointWitness>,
458 ) -> Result<Self, String> {
459 let report = Self {
460 protocol_version: RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION,
461 round_id: round.id,
462 round_sha256: recovery_round_sha256(round)?,
463 publisher,
464 prepared_witnesses,
465 };
466 report.validate(round)?;
467 Ok(report)
468 }
469
470 #[must_use]
472 pub const fn round_id(&self) -> RecoveryRoundId {
473 self.round_id
474 }
475
476 #[must_use]
478 pub const fn publisher(&self) -> CheckpointParticipant {
479 self.publisher
480 }
481
482 #[must_use]
484 pub fn prepared_witnesses(&self) -> &[PreparedCheckpointWitness] {
485 &self.prepared_witnesses
486 }
487
488 pub fn validate(&self, round: &RecoveryRound) -> Result<(), String> {
493 self.validate_semantics(round)?;
494 let encoded = serde_json::to_vec(self)
495 .map_err(|error| format!("could not encode recovery stopped report: {error}"))?;
496 validate_stopped_report_size(encoded.len())
497 }
498
499 fn validate_shape(&self) -> Result<(), String> {
500 if self.protocol_version != RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION {
501 return Err(format!(
502 "unsupported recovery stopped report version {}; expected {RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION}",
503 self.protocol_version
504 ));
505 }
506 if self.round_id.generation == 0
507 || self.round_id.nonce.is_nil()
508 || self.round_id.driver.is_unassigned()
509 {
510 return Err("recovery stopped report round identity is not canonical".into());
511 }
512 if !is_sha256_hex(&self.round_sha256) {
513 return Err("recovery stopped report round digest is not canonical".into());
514 }
515 if self.publisher.node_id == 0 || self.publisher.boot_incarnation.is_nil() {
516 return Err("recovery stopped report publisher is not canonical".into());
517 }
518 if self.prepared_witnesses.len() > MAX_PREPARED_CHECKPOINT_WITNESSES {
519 return Err(format!(
520 "recovery stopped report has {} prepared witnesses; maximum is {MAX_PREPARED_CHECKPOINT_WITNESSES}",
521 self.prepared_witnesses.len()
522 ));
523 }
524 for witness in &self.prepared_witnesses {
525 witness.validate()?;
526 if witness.participant_id != self.publisher.node_id {
527 return Err(format!(
528 "prepared checkpoint witness participant {} does not match report publisher {}",
529 witness.participant_id, self.publisher.node_id
530 ));
531 }
532 }
533 if self
534 .prepared_witnesses
535 .windows(2)
536 .any(|pair| pair[0].ordering_key() >= pair[1].ordering_key())
537 {
538 return Err(
539 "recovery stopped report witnesses must be uniquely sorted by epoch, checkpoint ID, and participant"
540 .into(),
541 );
542 }
543 Ok(())
544 }
545
546 fn validate_semantics(&self, round: &RecoveryRound) -> Result<(), String> {
547 self.validate_shape()?;
548 round.validate()?;
549 if self.round_id != round.id || self.round_sha256 != recovery_round_sha256(round)? {
550 return Err("recovery stopped report does not match the exact frozen round".into());
551 }
552 if round.stopped_participant_incarnation(NodeId(self.publisher.node_id))
553 != Some(self.publisher.boot_incarnation)
554 {
555 return Err("recovery stopped report publisher does not match the frozen round".into());
556 }
557 Ok(())
558 }
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
562#[serde(deny_unknown_fields)]
563struct RecoveryAnnouncementAck {
564 announcement: RecoveryAnnouncement,
565 incarnation: Uuid,
566}
567
568#[derive(serde::Serialize)]
569struct RecoveryStoppedRoundDigestInput<'a> {
570 protocol: &'static str,
571 round: &'a RecoveryRound,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
575#[serde(deny_unknown_fields)]
576pub(crate) struct RecoveryReleaseId {
577 protocol_version: u16,
578 generation: u64,
579 nonce: Uuid,
580 driver: NodeId,
581 epoch: u64,
582 round_sha256: String,
583}
584
585#[derive(serde::Serialize)]
586struct RecoveryReleaseDigestInput<'a> {
587 protocol: &'static str,
588 round: &'a RecoveryRound,
589 epoch: u64,
590}
591
592impl RecoveryReleaseId {
593 pub(crate) fn for_pending(release: &RecoveryAnnouncement) -> Result<Self, String> {
594 let RecoverPhase::Release { epoch } = release.phase else {
595 return Err("release readiness must bind a pending Release target".into());
596 };
597 release.validate()?;
598 let encoded = serde_json::to_vec(&RecoveryReleaseDigestInput {
599 protocol: "laminardb-recovery-release-v2",
600 round: &release.round,
601 epoch,
602 })
603 .map_err(|error| format!("could not encode recovery release identity: {error}"))?;
604 Ok(Self {
605 protocol_version: RELEASE_READY_PROTOCOL_VERSION,
606 generation: release.round.id.generation,
607 nonce: release.round.id.nonce,
608 driver: release.round.id.driver,
609 epoch,
610 round_sha256: sha256_hex(&encoded),
611 })
612 }
613
614 pub(crate) fn is_canonical(&self) -> bool {
615 self.protocol_version == RELEASE_READY_PROTOCOL_VERSION
616 && self.generation != 0
617 && !self.nonce.is_nil()
618 && !self.driver.is_unassigned()
619 && is_sha256_hex(&self.round_sha256)
620 }
621
622 pub(crate) fn generation(&self) -> u64 {
623 self.generation
624 }
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
628#[serde(deny_unknown_fields)]
629struct RecoveryReleaseReadyAck {
630 release: RecoveryReleaseId,
631 participant: CheckpointParticipant,
632}
633
634impl RecoveryReleaseReadyAck {
635 fn is_canonical(&self) -> bool {
636 self.release.is_canonical()
637 && self.participant.node_id != 0
638 && !self.participant.boot_incarnation.is_nil()
639 }
640}
641
642#[derive(Debug, thiserror::Error, PartialEq, Eq)]
644pub enum RecoveryControlError {
645 #[error("recovery control outcome is uncertain: {0}")]
647 Uncertain(String),
648 #[error("recovery control conflict: {0}")]
650 Conflict(String),
651 #[error("recovery round was superseded: {0}")]
653 Superseded(String),
654}
655
656impl RecoveryControlError {
657 #[cfg(feature = "cluster")]
658 fn from_authority(error: super::ClusterCheckpointAuthorityError) -> Self {
659 match error {
660 super::ClusterCheckpointAuthorityError::Authority(super::LeaseError::Io(reason)) => {
661 Self::Uncertain(reason)
662 }
663 super::ClusterCheckpointAuthorityError::Fenced => {
664 Self::Superseded("durable leader authority changed".into())
665 }
666 error => Self::Conflict(error.to_string()),
667 }
668 }
669
670 #[cfg(feature = "cluster")]
671 fn from_process_authority(error: super::ProcessLeaseError) -> Self {
672 match error {
673 super::ProcessLeaseError::Io(reason) | super::ProcessLeaseError::Deadline(reason) => {
674 Self::Uncertain(reason)
675 }
676 super::ProcessLeaseError::Invalid(reason) => Self::Conflict(reason),
677 super::ProcessLeaseError::Json(error) => Self::Conflict(error.to_string()),
678 }
679 }
680}
681
682#[derive(Debug, Clone, PartialEq, Eq)]
684enum ReleaseReadyStatus {
685 Pending {
687 missing: Vec<NodeId>,
689 },
690 Complete,
692}
693
694#[derive(Debug, Clone, PartialEq, Eq)]
696pub enum ReleaseCommitStatus {
697 Pending {
699 missing: Vec<NodeId>,
701 },
702 Committed {
704 terminal: RecoveryAnnouncement,
706 },
707}
708
709#[must_use = "keep the release guard until local source intake is opened or definitively fenced"]
712pub struct RecoveryReleaseGuard<'a> {
713 _write_guard: tokio::sync::MutexGuard<'a, ()>,
714}
715
716impl std::fmt::Debug for RecoveryReleaseGuard<'_> {
717 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
718 formatter
719 .debug_struct("RecoveryReleaseGuard")
720 .finish_non_exhaustive()
721 }
722}
723
724fn sha256_hex(encoded: &[u8]) -> String {
725 use std::fmt::Write as _;
726
727 let digest = Sha256::digest(encoded);
728 let mut hex = String::with_capacity(64);
729 for byte in digest {
730 write!(&mut hex, "{byte:02x}").expect("writing to a String cannot fail");
731 }
732 hex
733}
734
735fn recovery_round_sha256(round: &RecoveryRound) -> Result<String, String> {
736 let encoded = serde_json::to_vec(&RecoveryStoppedRoundDigestInput {
737 protocol: "laminardb-recovery-stopped-round-v3",
738 round,
739 })
740 .map_err(|error| format!("could not encode recovery stopped round identity: {error}"))?;
741 Ok(sha256_hex(&encoded))
742}
743
744fn is_sha256_hex(value: &str) -> bool {
745 value.len() == 64
746 && value
747 .bytes()
748 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
749}
750
751#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
755#[serde(deny_unknown_fields)]
756struct DrainAck {
757 protocol_version: u16,
758 participant: CheckpointParticipant,
759 round: AssignmentDrainId,
760}
761
762impl DrainAck {
763 fn for_transition(
764 participant: CheckpointParticipant,
765 transition: &AssignmentDrainTransition,
766 ) -> Self {
767 Self {
768 protocol_version: DRAIN_ACK_PROTOCOL_VERSION,
769 participant,
770 round: transition.id(),
771 }
772 }
773
774 fn is_canonical(&self) -> bool {
775 self.protocol_version == DRAIN_ACK_PROTOCOL_VERSION
776 && self.participant.node_id != 0
777 && !self.participant.boot_incarnation.is_nil()
778 && self.round.is_canonical()
779 }
780
781 fn matches_transition(&self, transition: &AssignmentDrainTransition) -> bool {
782 self.is_canonical()
783 && self.round == transition.id()
784 && transition
785 .predecessor
786 .participant_incarnation(self.participant.node_id)
787 == Some(self.participant.boot_incarnation)
788 }
789}
790
791fn encode_drain_ack(ack: &DrainAck) -> Result<String, String> {
792 if !ack.is_canonical() {
793 return Err("drain acknowledgement is not canonical".into());
794 }
795 let encoded = serde_json::to_string(ack)
796 .map_err(|error| format!("could not encode drain acknowledgement: {error}"))?;
797 if encoded.len() > MAX_DRAIN_ACK_BYTES {
798 return Err(format!(
799 "drain acknowledgement is {} bytes; maximum is {MAX_DRAIN_ACK_BYTES}",
800 encoded.len()
801 ));
802 }
803 Ok(encoded)
804}
805
806fn parse_drain_ack(raw: &str, publisher: NodeId) -> Result<DrainAck, String> {
807 if raw.len() > MAX_DRAIN_ACK_BYTES {
808 return Err(format!(
809 "drain acknowledgement from {publisher} is {} bytes; maximum is {MAX_DRAIN_ACK_BYTES}",
810 raw.len()
811 ));
812 }
813 let ack: DrainAck = serde_json::from_str(raw)
814 .map_err(|error| format!("invalid drain acknowledgement from {publisher}: {error}"))?;
815 if !ack.is_canonical() || ack.participant.node_id != publisher.0 {
816 return Err(format!(
817 "drain acknowledgement from {publisher} has a non-canonical publisher"
818 ));
819 }
820 if encode_drain_ack(&ack)? != raw {
821 return Err(format!(
822 "drain acknowledgement from {publisher} is not canonically encoded"
823 ));
824 }
825 Ok(ack)
826}
827
828fn parse_recovery_announcement(raw: &str) -> Result<Option<RecoveryAnnouncement>, String> {
829 if raw.is_empty() {
830 return Ok(None);
831 }
832 validate_recovery_announcement_size(raw.len())?;
833 let announcement: RecoveryAnnouncement = serde_json::from_str(raw)
834 .map_err(|error| format!("invalid recovery announcement: {error}"))?;
835 announcement.validate()?;
836 if encode_recovery_announcement(&announcement)? != raw {
837 return Err("recovery announcement is not canonically encoded".into());
838 }
839 Ok(Some(announcement))
840}
841
842fn validate_recovery_announcement_size(encoded_len: usize) -> Result<(), String> {
843 if encoded_len == 0 || encoded_len > MAX_RECOVERY_ANNOUNCEMENT_BYTES {
844 return Err(format!(
845 "recovery announcement is {encoded_len} bytes; maximum is {MAX_RECOVERY_ANNOUNCEMENT_BYTES}"
846 ));
847 }
848 Ok(())
849}
850
851fn encode_recovery_announcement(announcement: &RecoveryAnnouncement) -> Result<String, String> {
852 announcement.round.validate()?;
853 let encoded = serde_json::to_string(announcement)
854 .map_err(|error| format!("could not encode recovery announcement: {error}"))?;
855 validate_recovery_announcement_size(encoded.len())?;
856 Ok(encoded)
857}
858
859fn validate_stopped_report_size(encoded_len: usize) -> Result<(), String> {
860 if encoded_len > MAX_RECOVERY_STOPPED_REPORT_BYTES {
861 return Err(format!(
862 "recovery stopped report is {encoded_len} bytes; maximum is {MAX_RECOVERY_STOPPED_REPORT_BYTES}"
863 ));
864 }
865 Ok(())
866}
867
868fn encode_recovery_stopped_report(
869 report: &RecoveryStoppedReport,
870 round: &RecoveryRound,
871) -> Result<String, String> {
872 report.validate_semantics(round)?;
873 encode_recovery_stopped_report_shape(report)
874}
875
876fn encode_recovery_stopped_report_shape(report: &RecoveryStoppedReport) -> Result<String, String> {
877 report.validate_shape()?;
878 let encoded = serde_json::to_string(report)
879 .map_err(|error| format!("could not encode recovery stopped report: {error}"))?;
880 validate_stopped_report_size(encoded.len())?;
881 Ok(encoded)
882}
883
884#[cfg(test)]
885fn parse_recovery_stopped_report(
886 raw: &str,
887 publisher: NodeId,
888 round: &RecoveryRound,
889) -> Result<RecoveryStoppedReport, String> {
890 let report = parse_recovery_stopped_report_shape(raw, publisher)?;
891 report.validate_semantics(round)?;
892 Ok(report)
893}
894
895fn parse_recovery_stopped_report_shape(
896 raw: &str,
897 publisher: NodeId,
898) -> Result<RecoveryStoppedReport, String> {
899 validate_stopped_report_size(raw.len())?;
900 let report: RecoveryStoppedReport = serde_json::from_str(raw)
901 .map_err(|error| format!("invalid recovery stopped report from {publisher}: {error}"))?;
902 report.validate_shape()?;
903 if report.publisher.node_id != publisher.0 {
904 return Err(format!(
905 "recovery stopped report from {publisher} names publisher {}",
906 report.publisher.node_id
907 ));
908 }
909 if encode_recovery_stopped_report_shape(&report)? != raw {
910 return Err(format!(
911 "recovery stopped report from {publisher} is not canonically encoded"
912 ));
913 }
914 Ok(report)
915}
916
917fn parse_recovery_announcement_ack(
918 raw: &str,
919 publisher: NodeId,
920) -> Result<RecoveryAnnouncement, String> {
921 let ack: RecoveryAnnouncementAck = serde_json::from_str(raw)
922 .map_err(|error| format!("invalid recovery phase acknowledgement: {error}"))?;
923 ack.announcement.validate()?;
924 if ack.announcement.round.owner_incarnation(publisher) != Some(ack.incarnation) {
925 return Err(format!(
926 "recovery phase acknowledgement from {publisher} has a stale process incarnation"
927 ));
928 }
929 Ok(ack.announcement)
930}
931
932fn encode_release_ready_ack(ack: &RecoveryReleaseReadyAck) -> Result<String, String> {
933 if !ack.is_canonical() {
934 return Err("release readiness acknowledgement is not canonical".into());
935 }
936 let encoded = serde_json::to_string(ack)
937 .map_err(|error| format!("could not encode release readiness: {error}"))?;
938 if encoded.len() > MAX_RELEASE_READY_ACK_BYTES {
939 return Err(format!(
940 "release readiness is {} bytes; maximum is {MAX_RELEASE_READY_ACK_BYTES}",
941 encoded.len()
942 ));
943 }
944 Ok(encoded)
945}
946
947fn parse_release_ready_ack(
948 raw: &str,
949 publisher: NodeId,
950) -> Result<RecoveryReleaseReadyAck, String> {
951 if raw.len() > MAX_RELEASE_READY_ACK_BYTES {
952 return Err(format!(
953 "release readiness from {publisher} is {} bytes; maximum is {MAX_RELEASE_READY_ACK_BYTES}",
954 raw.len()
955 ));
956 }
957 let ack: RecoveryReleaseReadyAck = serde_json::from_str(raw)
958 .map_err(|error| format!("invalid release readiness from {publisher}: {error}"))?;
959 if !ack.is_canonical() || ack.participant.node_id != publisher.0 {
960 return Err(format!(
961 "release readiness from {publisher} has a non-canonical publisher"
962 ));
963 }
964 if encode_release_ready_ack(&ack)? != raw {
965 return Err(format!(
966 "release readiness from {publisher} is not canonically encoded"
967 ));
968 }
969 Ok(ack)
970}
971
972pub struct ClusterController {
974 instance_id: NodeId,
975 kv: Arc<dyn ClusterKv>,
977 recovery_kv: Arc<dyn ClusterKv>,
980 barrier: BarrierCoordinator,
981 snapshot: Option<Arc<AssignmentSnapshotStore>>,
982 members_rx: watch::Receiver<Vec<NodeInfo>>,
983 checkpoint_assignment_fence: watch::Sender<Option<CheckpointAssignmentFence>>,
986 checkpoint_drain_transition: watch::Sender<Option<AssignmentDrainTransition>>,
989 process_authority_transition: parking_lot::Mutex<()>,
991 recovery_incarnation: Uuid,
993 recovery_process_term: AtomicU64,
995 recovery_fault_request_sequence: AtomicU64,
997 cluster_min_watermark: Arc<AtomicI64>,
1000 draining: Arc<AtomicBool>,
1003 recovering: Arc<AtomicBool>,
1006 active: Arc<AtomicBool>,
1008 process_lease_live: Arc<AtomicBool>,
1010 process_lease_deadline: std::sync::OnceLock<Arc<super::LeaseDeadline>>,
1012 process_lease_authority: std::sync::OnceLock<Arc<super::process_lease::ProcessLeaseAuthority>>,
1014 leader_eligible: Arc<AtomicBool>,
1017 leader_candidacy: watch::Sender<super::LeaderCandidacy>,
1020 leadership_participants: parking_lot::RwLock<Option<(u64, Vec<u64>)>>,
1024 recovery_writes: tokio::sync::Mutex<()>,
1026 pending_release_fault_audit:
1029 tokio::sync::Mutex<Option<(RecoveryReleaseId, tokio::time::Instant)>>,
1030 unresponsive: Arc<parking_lot::Mutex<rustc_hash::FxHashMap<u64, Option<Uuid>>>>,
1034 self_locality: parking_lot::RwLock<Locality>,
1036 #[cfg(feature = "cluster")]
1039 leader_lease: std::sync::OnceLock<LeaderLeaseGate>,
1040}
1041
1042impl std::fmt::Debug for ClusterController {
1043 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1044 f.debug_struct("ClusterController")
1045 .field("instance_id", &self.instance_id)
1046 .finish_non_exhaustive()
1047 }
1048}
1049
1050impl ClusterController {
1051 #[must_use]
1053 pub fn new(
1054 instance_id: NodeId,
1055 kv: Arc<dyn ClusterKv>,
1056 snapshot: Option<Arc<AssignmentSnapshotStore>>,
1057 members_rx: watch::Receiver<Vec<NodeInfo>>,
1058 ) -> Self {
1059 Self::new_with_recovery_kv(instance_id, Arc::clone(&kv), kv, snapshot, members_rx)
1060 }
1061
1062 #[must_use]
1067 pub fn new_with_recovery_kv(
1068 instance_id: NodeId,
1069 kv: Arc<dyn ClusterKv>,
1070 recovery_kv: Arc<dyn ClusterKv>,
1071 snapshot: Option<Arc<AssignmentSnapshotStore>>,
1072 members_rx: watch::Receiver<Vec<NodeInfo>>,
1073 ) -> Self {
1074 Self::new_with_recovery_incarnation(
1075 instance_id,
1076 kv,
1077 recovery_kv,
1078 snapshot,
1079 members_rx,
1080 Uuid::new_v4(),
1081 )
1082 }
1083
1084 #[must_use]
1086 pub fn new_with_recovery_incarnation(
1087 instance_id: NodeId,
1088 kv: Arc<dyn ClusterKv>,
1089 recovery_kv: Arc<dyn ClusterKv>,
1090 snapshot: Option<Arc<AssignmentSnapshotStore>>,
1091 members_rx: watch::Receiver<Vec<NodeInfo>>,
1092 recovery_incarnation: Uuid,
1093 ) -> Self {
1094 let leader_eligible = Arc::new(AtomicBool::new(true));
1095 let mut barrier = BarrierCoordinator::new(Arc::clone(&kv));
1096 #[cfg(feature = "cluster")]
1097 barrier.set_leader_election(
1098 instance_id,
1099 members_rx.clone(),
1100 Arc::clone(&leader_eligible),
1101 );
1102 let controller = Self {
1103 instance_id,
1104 barrier,
1105 kv,
1106 recovery_kv,
1107 snapshot,
1108 members_rx,
1109 checkpoint_assignment_fence: watch::channel(None).0,
1111 checkpoint_drain_transition: watch::channel(None).0,
1112 process_authority_transition: parking_lot::Mutex::new(()),
1113 recovery_incarnation,
1114 recovery_process_term: AtomicU64::new(0),
1115 recovery_fault_request_sequence: AtomicU64::new(1),
1116 cluster_min_watermark: Arc::new(AtomicI64::new(i64::MIN)),
1117 draining: Arc::new(AtomicBool::new(false)),
1118 recovering: Arc::new(AtomicBool::new(false)),
1119 active: Arc::new(AtomicBool::new(true)),
1120 process_lease_live: Arc::new(AtomicBool::new(true)),
1121 process_lease_deadline: std::sync::OnceLock::new(),
1122 process_lease_authority: std::sync::OnceLock::new(),
1123 leader_eligible,
1124 leader_candidacy: watch::channel(super::LeaderCandidacy::initial(false)).0,
1125 leadership_participants: parking_lot::RwLock::new(None),
1126 recovery_writes: tokio::sync::Mutex::new(()),
1127 pending_release_fault_audit: tokio::sync::Mutex::new(None),
1128 unresponsive: Arc::new(parking_lot::Mutex::new(rustc_hash::FxHashMap::default())),
1129 self_locality: parking_lot::RwLock::new(Locality::default()),
1130 #[cfg(feature = "cluster")]
1131 leader_lease: std::sync::OnceLock::new(),
1132 };
1133 controller.notify_leader_eligibility_change();
1134 controller
1135 }
1136
1137 #[cfg(feature = "cluster")]
1139 pub fn set_leader_lease_store(&self, store: Arc<super::LeaderLeaseStore>) {
1140 self.barrier.set_leader_lease_store(store);
1141 }
1142
1143 #[cfg(feature = "cluster")]
1145 pub fn install_local_leader_proof_provider(self: &Arc<Self>) {
1146 let controller = Arc::downgrade(self);
1147 self.barrier
1148 .set_local_leader_proof_provider(Arc::new(move || {
1149 controller
1150 .upgrade()
1151 .and_then(|controller| controller.capture_leader_proof())
1152 }));
1153 }
1154
1155 #[cfg(feature = "cluster")]
1162 pub fn checkpoint_authority(
1163 &self,
1164 ) -> Result<Arc<super::LeaderLeaseStore>, super::ClusterCheckpointAuthorityError> {
1165 self.barrier.checkpoint_authority()
1166 }
1167
1168 #[must_use]
1171 pub fn cluster_min_watermark(&self) -> Option<i64> {
1172 let v = self.cluster_min_watermark.load(Ordering::Acquire);
1173 if v == i64::MIN {
1174 None
1175 } else {
1176 Some(v)
1177 }
1178 }
1179
1180 pub fn publish_cluster_min_watermark(&self, wm: i64) {
1183 let mut cur = self.cluster_min_watermark.load(Ordering::Acquire);
1184 while wm > cur {
1185 match self.cluster_min_watermark.compare_exchange(
1186 cur,
1187 wm,
1188 Ordering::AcqRel,
1189 Ordering::Acquire,
1190 ) {
1191 Ok(_) => break,
1192 Err(observed) => cur = observed,
1193 }
1194 }
1195 }
1196
1197 #[must_use]
1199 pub fn instance_id(&self) -> NodeId {
1200 self.instance_id
1201 }
1202
1203 #[must_use]
1205 pub fn kv(&self) -> &Arc<dyn ClusterKv> {
1206 &self.kv
1207 }
1208
1209 #[must_use]
1214 pub fn current_leader(&self) -> Option<NodeId> {
1215 let members = self.members_rx.borrow();
1216 let participants = self.leadership_participants.read();
1217 let unresponsive = self.unresponsive.lock();
1218 let is_participant = |node: NodeId| {
1219 participants
1220 .as_ref()
1221 .is_none_or(|(_, roster)| roster.binary_search(&node.0).is_ok())
1222 };
1223 let mut eligible: Vec<NodeId> = members
1224 .iter()
1225 .filter(|m| {
1226 m.id != self.instance_id
1227 && matches!(m.state, NodeState::Active)
1228 && !unresponsive.contains_key(&m.id.0)
1229 })
1230 .map(|m| m.id)
1231 .collect();
1232 if self.leader_eligible.load(Ordering::SeqCst)
1233 && self.process_lease_is_live()
1234 && !unresponsive.contains_key(&self.instance_id.0)
1235 {
1236 eligible.push(self.instance_id);
1237 }
1238 let certified = eligible
1239 .iter()
1240 .copied()
1241 .filter(|node| is_participant(*node))
1242 .collect::<Vec<_>>();
1243 if let Some(leader) = leader_of(&certified) {
1244 return Some(leader);
1245 }
1246 #[cfg(feature = "cluster")]
1247 if self.has_leader_lease_fencing() {
1248 return leader_of(&eligible);
1249 }
1250 None
1251 }
1252
1253 #[must_use]
1256 pub fn is_gossip_leader(&self) -> bool {
1257 self.current_leader() == Some(self.instance_id)
1258 }
1259
1260 #[must_use]
1263 pub fn is_leader(&self) -> bool {
1264 if !self.process_lease_is_live() {
1265 return false;
1266 }
1267 if !self.is_gossip_leader() {
1268 return false;
1269 }
1270 #[cfg(feature = "cluster")]
1271 if let Some(gate) = self.leader_lease.get() {
1272 return super::lease_grants_leadership(
1273 &gate.lease.borrow(),
1274 &gate.owner,
1275 &gate.deadline,
1276 );
1277 }
1278 true
1279 }
1280
1281 #[must_use]
1284 #[cfg(feature = "cluster")]
1285 pub fn has_leader_lease_fencing(&self) -> bool {
1286 self.leader_lease.get().is_some()
1287 }
1288
1289 #[must_use]
1296 #[cfg(feature = "cluster")]
1297 pub fn leader_fencing_token(&self) -> Option<u64> {
1298 self.capture_leader_proof().map(|proof| proof.fencing_token)
1299 }
1300
1301 #[must_use]
1307 #[cfg(feature = "cluster")]
1308 pub fn capture_leader_proof(&self) -> Option<super::LeaderProof> {
1309 if !self.process_lease_proof_is_live() || !self.is_gossip_leader() {
1310 return None;
1311 }
1312 let gate = self.leader_lease.get()?;
1313 let lease = gate.lease.borrow();
1314 if !super::lease_grants_leadership(&lease, &gate.owner, &gate.deadline) {
1315 return None;
1316 }
1317 lease.as_ref().map(super::LeaderLease::proof)
1318 }
1319
1320 #[must_use]
1322 #[cfg(feature = "cluster")]
1323 pub fn proof_is_live(&self, proof: &super::LeaderProof) -> bool {
1324 if !self.process_lease_proof_is_live() || !self.is_gossip_leader() {
1325 return false;
1326 }
1327 let Some(gate) = self.leader_lease.get() else {
1328 return false;
1329 };
1330 super::lease_grants_proof(&gate.lease.borrow(), &gate.owner, &gate.deadline, proof)
1331 }
1332
1333 #[must_use]
1339 #[cfg(feature = "cluster")]
1340 pub fn capture_catalog_bootstrap_proof(&self) -> Option<super::LeaderProof> {
1341 if !self.process_lease_proof_is_live() || !self.is_leader_lease_candidate() {
1342 return None;
1343 }
1344 let gate = self.leader_lease.get()?;
1345 let lease = gate.lease.borrow();
1346 if !super::lease_grants_leadership(&lease, &gate.owner, &gate.deadline) {
1347 return None;
1348 }
1349 lease.as_ref().map(super::LeaderLease::proof)
1350 }
1351
1352 #[must_use]
1354 #[cfg(feature = "cluster")]
1355 pub fn catalog_bootstrap_proof_is_live(&self, proof: &super::LeaderProof) -> bool {
1356 if !self.process_lease_proof_is_live() || !self.is_leader_lease_candidate() {
1357 return false;
1358 }
1359 let Some(gate) = self.leader_lease.get() else {
1360 return false;
1361 };
1362 super::lease_grants_proof(&gate.lease.borrow(), &gate.owner, &gate.deadline, proof)
1363 }
1364
1365 #[must_use]
1371 #[cfg(feature = "cluster")]
1372 pub fn leader_grant_watch(&self) -> Option<watch::Receiver<Option<super::LeaderLease>>> {
1373 self.leader_lease.get().map(|gate| gate.lease.clone())
1374 }
1375
1376 #[cfg(feature = "cluster")]
1377 fn process_lease_proof_is_live(&self) -> bool {
1378 self.process_lease_live.load(Ordering::Acquire)
1379 && self
1380 .process_lease_deadline
1381 .get()
1382 .is_some_and(|deadline| deadline.is_live())
1383 }
1384
1385 #[cfg(feature = "cluster")]
1390 pub fn set_leader_lease_watch(
1391 &self,
1392 lease: watch::Receiver<Option<super::LeaderLease>>,
1393 owner: super::LeaderLeaseOwner,
1394 deadline: Arc<super::LeaseDeadline>,
1395 ) -> Result<(), String> {
1396 if owner.node != self.instance_id || owner.boot.is_nil() || owner.process_term == 0 {
1397 return Err("leader lease owner does not match this process".into());
1398 }
1399 self.leader_lease
1400 .set(LeaderLeaseGate {
1401 lease,
1402 owner,
1403 deadline,
1404 })
1405 .map_err(|_| "leader lease fencing is already installed".into())
1406 }
1407
1408 fn notify_leader_eligibility_change(&self) {
1409 let eligible = self.is_leader_lease_candidate();
1410 self.leader_candidacy.send_if_modified(|published| {
1411 let next = published
1412 .transition(eligible)
1413 .unwrap_or_else(super::LeaderCandidacy::terminal);
1414 if next == *published {
1415 false
1416 } else {
1417 *published = next;
1418 true
1419 }
1420 });
1421 }
1422
1423 fn is_leader_lease_candidate(&self) -> bool {
1424 if self.is_gossip_leader() {
1425 return true;
1426 }
1427 !self.active.load(Ordering::Acquire)
1428 && !self.is_draining()
1429 && self.process_lease_is_live()
1430 && !self
1431 .members_rx
1432 .borrow()
1433 .iter()
1434 .any(|member| matches!(member.state, NodeState::Active))
1435 }
1436
1437 #[cfg(feature = "cluster")]
1439 #[must_use]
1440 pub fn leader_candidacy_watch(self: &Arc<Self>) -> watch::Receiver<super::LeaderCandidacy> {
1441 let mut members = self.members_rx.clone();
1442 members.borrow_and_update();
1443 self.notify_leader_eligibility_change();
1444 let candidacy = self.leader_candidacy.clone();
1445 let candidate_rx = candidacy.subscribe();
1446 let controller = Arc::downgrade(self);
1447 tokio::spawn(async move {
1448 loop {
1449 tokio::select! {
1450 biased;
1451 () = candidacy.closed() => return,
1452 changed = members.changed() => {
1453 if changed.is_err() {
1454 candidacy.send_replace(super::LeaderCandidacy::terminal());
1455 return;
1456 }
1457 }
1458 }
1459 let Some(controller) = controller.upgrade() else {
1460 return;
1461 };
1462 controller.notify_leader_eligibility_change();
1463 }
1464 });
1465 candidate_rx
1466 }
1467
1468 pub fn set_active(&self, active: bool) {
1470 let _transition = self.process_authority_transition.lock();
1471 let active = active && self.process_lease_is_live();
1472 self.active.store(active, Ordering::SeqCst);
1473 let eligible = active && !self.is_draining();
1474 if self.leader_eligible.swap(eligible, Ordering::SeqCst) != eligible {
1475 self.notify_leader_eligibility_change();
1476 }
1477 }
1478
1479 pub fn fence_process_lease(&self) {
1481 let _transition = self.process_authority_transition.lock();
1482 self.process_lease_live.store(false, Ordering::SeqCst);
1483 if let Some(deadline) = self.process_lease_deadline.get() {
1484 deadline.fence();
1485 }
1486 self.active.store(false, Ordering::SeqCst);
1487 self.recovering.store(true, Ordering::SeqCst);
1488 let roster_changed = self.leadership_participants.write().take().is_some();
1489 if self.leader_eligible.swap(false, Ordering::SeqCst) || roster_changed {
1490 self.notify_leader_eligibility_change();
1491 }
1492 self.checkpoint_assignment_fence.send_replace(None);
1493 self.checkpoint_drain_transition.send_replace(None);
1494 }
1495
1496 #[must_use]
1498 pub fn process_lease_is_live(&self) -> bool {
1499 self.process_lease_live.load(Ordering::Acquire)
1500 && self
1501 .process_lease_deadline
1502 .get()
1503 .is_none_or(|deadline| deadline.is_live())
1504 }
1505
1506 fn recovery_process_lease_is_live(&self) -> bool {
1507 self.process_lease_live.load(Ordering::Acquire)
1508 && self
1509 .process_lease_deadline
1510 .get()
1511 .is_some_and(|deadline| deadline.is_live())
1512 }
1513
1514 pub async fn wait_for_process_lease_loss(&self) {
1516 if !self.process_lease_live.load(Ordering::Acquire) {
1517 return;
1518 }
1519 let Some(deadline) = self.process_lease_deadline.get() else {
1520 std::future::pending::<()>().await;
1521 return;
1522 };
1523 deadline.wait_until_expired().await;
1524 }
1525
1526 pub fn set_process_lease_deadline(
1535 &self,
1536 deadline: Arc<super::LeaseDeadline>,
1537 ) -> Result<(), String> {
1538 let _transition = self.process_authority_transition.lock();
1539 if !self.process_lease_live.load(Ordering::Acquire) {
1540 return Err("process lease authority is already terminally fenced".into());
1541 }
1542 if let Some(current) = self.process_lease_deadline.get() {
1543 if !Arc::ptr_eq(current, &deadline) {
1544 return Err("process lease deadline is already installed".into());
1545 }
1546 #[cfg(feature = "cluster")]
1547 self.barrier.install_process_lease_deadline(deadline)?;
1548 return Ok(());
1549 }
1550 #[cfg(feature = "cluster")]
1551 self.barrier
1552 .install_process_lease_deadline(Arc::clone(&deadline))?;
1553 self.process_lease_deadline
1554 .set(deadline)
1555 .map_err(|_| "process lease deadline is already installed".to_string())
1556 }
1557
1558 #[must_use]
1560 pub fn process_lease_deadline(&self) -> Option<Arc<super::LeaseDeadline>> {
1561 self.process_lease_deadline.get().cloned()
1562 }
1563
1564 pub fn set_process_lease_authority(
1569 &self,
1570 authority: Arc<super::process_lease::ProcessLeaseAuthority>,
1571 ) -> Result<(), String> {
1572 self.process_lease_authority
1573 .set(authority)
1574 .map_err(|_| "process lease authority is already installed".to_string())
1575 }
1576
1577 pub async fn fence_process_incarnation(
1582 &self,
1583 participant: CheckpointParticipant,
1584 deadline: tokio::time::Instant,
1585 ) -> Result<super::process_lease::ProcessLeaseFence, String> {
1586 self.process_lease_authority
1587 .get()
1588 .ok_or_else(|| "process lease authority is not installed".to_string())?
1589 .fence_incarnation(participant, deadline)
1590 .await
1591 .map_err(|error| error.to_string())
1592 }
1593
1594 pub fn process_fencing_deadline(
1600 &self,
1601 io_budget: Duration,
1602 ) -> Result<tokio::time::Instant, String> {
1603 self.process_lease_authority
1604 .get()
1605 .ok_or_else(|| "process lease authority is not installed".to_string())?
1606 .fencing_deadline(io_budget)
1607 .map_err(|error| error.to_string())
1608 }
1609
1610 pub async fn verify_current_process_incarnation(
1615 &self,
1616 participant: CheckpointParticipant,
1617 deadline: tokio::time::Instant,
1618 ) -> Result<bool, String> {
1619 self.process_lease_authority
1620 .get()
1621 .ok_or_else(|| "process lease authority is not installed".to_string())?
1622 .verify_current_participant(participant, deadline)
1623 .await
1624 .map_err(|error| error.to_string())
1625 }
1626
1627 pub async fn verify_process_lease_fence(
1632 &self,
1633 fence: &super::process_lease::ProcessLeaseFence,
1634 deadline: tokio::time::Instant,
1635 ) -> Result<bool, String> {
1636 self.process_lease_authority
1637 .get()
1638 .ok_or_else(|| "process lease authority is not installed".to_string())?
1639 .verify_fence(fence, deadline)
1640 .await
1641 .map_err(|error| error.to_string())
1642 }
1643
1644 pub async fn record_assignment_recovery_decision(
1654 &self,
1655 proof: &super::LeaderProof,
1656 decision: super::AssignmentRecoveryDecision,
1657 deadline: tokio::time::Instant,
1658 ) -> Result<super::RecordAssignmentRecoveryDecisionResult, String> {
1659 let process_authority = Arc::clone(
1660 self.process_lease_authority
1661 .get()
1662 .ok_or_else(|| "process lease authority is not installed".to_string())?,
1663 );
1664 let checks = futures::stream::iter(decision.removed_process_fences.iter().cloned())
1665 .map(|fence| {
1666 let process_authority = Arc::clone(&process_authority);
1667 async move { process_authority.verify_fence(&fence, deadline).await }
1668 })
1669 .buffer_unordered(CONTROL_ROSTER_IO_CONCURRENCY)
1670 .collect::<Vec<_>>()
1671 .await;
1672 for check in checks {
1673 match check {
1674 Ok(true) => {}
1675 Ok(false) => {
1676 return Err("assignment recovery contains an unverified process fence".into());
1677 }
1678 Err(error) => {
1679 return Err(format!(
1680 "assignment recovery process fence verification failed: {error}"
1681 ));
1682 }
1683 }
1684 }
1685 if tokio::time::Instant::now() >= deadline {
1686 return Err("assignment recovery authority admission exceeded its deadline".into());
1687 }
1688 let authority = self
1689 .checkpoint_authority()
1690 .map_err(|error| error.to_string())?;
1691 Box::pin(tokio::time::timeout_at(
1692 deadline,
1693 authority.record_assignment_recovery_decision(proof, decision),
1694 ))
1695 .await
1696 .map_err(|_| "assignment recovery authority admission exceeded its deadline".to_string())?
1697 .map_err(|error| error.to_string())
1698 }
1699
1700 #[must_use]
1702 pub fn live_instances(&self) -> Vec<NodeId> {
1703 let mut ids: Vec<NodeId> = self
1704 .members_rx
1705 .borrow()
1706 .iter()
1707 .filter(|m| m.id != self.instance_id && matches!(m.state, NodeState::Active))
1708 .map(|m| m.id)
1709 .collect();
1710 if self.active.load(Ordering::SeqCst) && self.process_lease_is_live() {
1711 ids.push(self.instance_id);
1712 }
1713 ids
1714 }
1715
1716 #[must_use]
1723 pub fn checkpoint_instances(&self) -> Vec<NodeId> {
1724 let mut ids: Vec<NodeId> = self
1725 .members_rx
1726 .borrow()
1727 .iter()
1728 .filter(|member| {
1729 member.id != self.instance_id
1730 && matches!(member.state, NodeState::Active | NodeState::Draining)
1731 })
1732 .map(|member| member.id)
1733 .collect();
1734 if self.active.load(Ordering::SeqCst) && self.process_lease_is_live() {
1735 ids.push(self.instance_id);
1736 }
1737 ids.sort_unstable();
1738 ids.dedup();
1739 ids
1740 }
1741
1742 pub fn note_unresponsive(&self, peers: &[NodeId]) {
1744 let fence = self.checkpoint_assignment_fence.borrow().clone();
1745 let mut map = self.unresponsive.lock();
1746 let mut changed = false;
1747 for p in peers {
1748 let incarnation = fence
1749 .as_ref()
1750 .and_then(|certificate| certificate.participant_incarnation(p.0));
1751 changed |= map.insert(p.0, incarnation) != Some(incarnation);
1752 }
1753 drop(map);
1754 if changed {
1755 self.notify_leader_eligibility_change();
1756 }
1757 }
1758
1759 pub fn note_responsive(&self, peers: &[NodeId]) {
1761 let mut map = self.unresponsive.lock();
1762 let mut changed = false;
1763 for p in peers {
1764 changed |= map.remove(&p.0).is_some();
1765 }
1766 drop(map);
1767 if changed {
1768 self.notify_leader_eligibility_change();
1769 }
1770 }
1771
1772 #[must_use]
1774 pub fn is_unresponsive(&self, peer: NodeId) -> bool {
1775 self.unresponsive.lock().contains_key(&peer.0)
1776 }
1777
1778 pub fn admit_successor_process(&self, participant: CheckpointParticipant) -> bool {
1781 let mut unresponsive = self.unresponsive.lock();
1782 let admitted = match unresponsive.get(&participant.node_id).copied() {
1783 None => true,
1784 Some(Some(failed_boot)) if failed_boot != participant.boot_incarnation => {
1785 unresponsive.remove(&participant.node_id);
1786 drop(unresponsive);
1787 self.notify_leader_eligibility_change();
1788 return true;
1789 }
1790 Some(Some(_) | None) => false,
1791 };
1792 drop(unresponsive);
1793 admitted
1794 }
1795
1796 pub fn begin_drain(&self) -> bool {
1799 #[cfg(feature = "cluster")]
1800 let has_durable_leadership = self.has_leader_lease_fencing();
1801 #[cfg(not(feature = "cluster"))]
1802 let has_durable_leadership = false;
1803 let retain_leadership = has_durable_leadership
1804 && self.is_leader()
1805 && self
1806 .checkpoint_assignment_fence
1807 .borrow()
1808 .as_ref()
1809 .is_some_and(|fence| {
1810 fence.is_canonical()
1811 && fence.participant_incarnation(self.instance_id.0)
1812 == Some(self.recovery_incarnation)
1813 });
1814 self.draining.store(true, Ordering::SeqCst);
1815 if !retain_leadership && self.leader_eligible.swap(false, Ordering::SeqCst) {
1816 self.notify_leader_eligibility_change();
1817 }
1818 retain_leadership
1819 }
1820
1821 #[must_use]
1823 pub fn is_draining(&self) -> bool {
1824 self.draining.load(Ordering::SeqCst)
1825 }
1826
1827 pub fn set_recovering(&self, recovering: bool) {
1829 let _transition = self.process_authority_transition.lock();
1830 let recovering = recovering || !self.process_lease_is_live();
1831 self.recovering.store(recovering, Ordering::SeqCst);
1832 }
1833
1834 #[must_use]
1836 pub fn is_recovering(&self) -> bool {
1837 self.recovering.load(Ordering::SeqCst)
1838 }
1839
1840 async fn write_recovery_value(&self, key: &str, value: String) -> Result<(), String> {
1841 if !self.process_lease_is_live() {
1842 return Err("stable node process lease is no longer live".into());
1843 }
1844 tokio::time::timeout(
1845 RECOVERY_CONTROL_IO_TIMEOUT,
1846 self.recovery_kv.write_checked(key, value),
1847 )
1848 .await
1849 .map_err(|_| format!("recovery control write for {key} timed out"))?
1850 .map_err(|error| format!("recovery control write for {key} failed: {error}"))
1851 }
1852
1853 async fn write_recovery_value_exact(&self, key: &str, value: String) -> Result<(), String> {
1854 self.write_recovery_value(key, value.clone()).await?;
1855 let observed = self
1856 .read_recovery_value(self.instance_id, key)
1857 .await?
1858 .ok_or_else(|| format!("recovery control value for {key} vanished after write"))?;
1859 if observed != value {
1860 return Err(format!(
1861 "recovery control read-back mismatch for {key}; write was not durable"
1862 ));
1863 }
1864 Ok(())
1865 }
1866
1867 async fn read_recovery_value(&self, node: NodeId, key: &str) -> Result<Option<String>, String> {
1868 tokio::time::timeout(
1869 RECOVERY_CONTROL_IO_TIMEOUT,
1870 self.recovery_kv.read_from_checked(node, key),
1871 )
1872 .await
1873 .map_err(|_| format!("recovery control read for {key} from {node} timed out"))?
1874 .map_err(|error| format!("recovery control read for {key} from {node} failed: {error}"))
1875 }
1876
1877 async fn scan_recovery_values(&self, key: &str) -> Result<Vec<(NodeId, String)>, String> {
1878 tokio::time::timeout(
1879 RECOVERY_CONTROL_IO_TIMEOUT,
1880 self.recovery_kv.scan_checked(key),
1881 )
1882 .await
1883 .map_err(|_| format!("recovery control scan for {key} timed out"))?
1884 .map_err(|error| format!("recovery control scan for {key} failed: {error}"))
1885 }
1886
1887 pub async fn max_recovery_generation(&self) -> Result<u64, String> {
1892 let mut maximum = 0;
1893 for (node, raw) in self.scan_recovery_values("control:recovery-gen").await? {
1894 let generation = raw.parse::<u64>().map_err(|error| {
1895 format!("invalid replicated recovery generation from {node}: {error}")
1896 })?;
1897 maximum = maximum.max(generation);
1898 }
1899 Ok(maximum)
1900 }
1901
1902 pub async fn adopt_recovery_generation(&self, generation: u64) -> Result<(), String> {
1907 if generation == 0 {
1908 return Err("recovery generation must be nonzero".into());
1909 }
1910 let current = self
1911 .read_recovery_value(self.instance_id, "control:recovery-gen")
1912 .await?
1913 .map(|raw| {
1914 raw.parse::<u64>()
1915 .map_err(|error| format!("invalid local recovery generation: {error}"))
1916 })
1917 .transpose()?;
1918 if let Some(current) = current {
1919 if current > generation {
1920 return Err(format!(
1921 "local recovery generation {current} is newer than proposed generation {generation}"
1922 ));
1923 }
1924 if current == generation {
1925 return Ok(());
1926 }
1927 }
1928 self.write_recovery_value_exact("control:recovery-gen", generation.to_string())
1929 .await
1930 }
1931
1932 async fn read_recovery_stopped_reports(
1933 &self,
1934 round: &RecoveryRound,
1935 roster: &[CheckpointParticipant],
1936 ) -> Result<Vec<RecoveryStoppedReport>, RecoveryControlError> {
1937 round.validate().map_err(RecoveryControlError::Conflict)?;
1938 if roster
1939 .windows(2)
1940 .any(|pair| pair[0].node_id >= pair[1].node_id)
1941 || roster.iter().any(|participant| {
1942 round.stopped_participant_incarnation(NodeId(participant.node_id))
1943 != Some(participant.boot_incarnation)
1944 })
1945 {
1946 return Err(RecoveryControlError::Conflict(
1947 "recovery stopped-report read roster is not a canonical round subset".into(),
1948 ));
1949 }
1950 let reads = futures::stream::iter(roster.iter().copied().map(|participant| async move {
1951 let value = self
1952 .read_recovery_value(NodeId(participant.node_id), RECOVERY_STOPPED_REPORT_KEY)
1953 .await;
1954 (participant, value)
1955 }))
1956 .buffer_unordered(CONTROL_ROSTER_IO_CONCURRENCY)
1957 .collect::<Vec<_>>()
1958 .await;
1959 let mut reports = Vec::new();
1960 for (participant, value) in reads {
1961 let Some(raw) = value.map_err(RecoveryControlError::Uncertain)? else {
1962 continue;
1963 };
1964 let publisher = NodeId(participant.node_id);
1965 let report = parse_recovery_stopped_report_shape(&raw, publisher)
1966 .map_err(RecoveryControlError::Conflict)?;
1967 if report.round_id.generation < round.id.generation {
1968 continue;
1969 }
1970 if report.round_id.generation > round.id.generation {
1971 let adopted = self
1975 .read_recovery_value(publisher, "control:recovery-gen")
1976 .await
1977 .map_err(RecoveryControlError::Uncertain)?;
1978 let incarnation = self
1979 .read_recovery_value(publisher, RECOVERY_INCARNATION_KEY)
1980 .await
1981 .map_err(RecoveryControlError::Uncertain)?;
1982 let Some((adopted, incarnation)) = adopted.zip(incarnation) else {
1983 continue;
1984 };
1985 let adopted = adopted.parse::<u64>().map_err(|error| {
1986 RecoveryControlError::Conflict(format!(
1987 "invalid replicated recovery generation from {publisher}: {error}"
1988 ))
1989 })?;
1990 let incarnation = Uuid::parse_str(&incarnation).map_err(|error| {
1991 RecoveryControlError::Conflict(format!(
1992 "invalid recovery incarnation published by {publisher}: {error}"
1993 ))
1994 })?;
1995 if adopted >= report.round_id.generation
1996 && incarnation == report.publisher.boot_incarnation
1997 {
1998 reports.push(report);
1999 }
2000 continue;
2001 }
2002 report.validate_semantics(round).map_err(|error| {
2003 RecoveryControlError::Conflict(format!(
2004 "same-generation recovery stopped report from {publisher} conflicts with the exact frozen round: {error}"
2005 ))
2006 })?;
2007 reports.push(report);
2008 }
2009 reports.sort_unstable_by_key(|report| report.publisher.node_id);
2010 Ok(reports)
2011 }
2012
2013 async fn read_recovery_announcement_map(
2014 &self,
2015 key: &str,
2016 ) -> Result<Vec<(NodeId, RecoveryAnnouncement)>, String> {
2017 let mut announcements = Vec::new();
2018 for (node, raw) in self.scan_recovery_values(key).await? {
2019 let announcement = parse_recovery_announcement_ack(&raw, node)?;
2020 announcements.push((node, announcement));
2021 }
2022 Ok(announcements)
2023 }
2024
2025 #[must_use]
2027 pub fn recovery_incarnation(&self) -> Uuid {
2028 self.recovery_incarnation
2029 }
2030
2031 pub async fn publish_recovery_incarnation(&self) -> Result<(), String> {
2039 self.write_recovery_value(
2040 RECOVERY_INCARNATION_KEY,
2041 self.recovery_incarnation.to_string(),
2042 )
2043 .await?;
2044 let observed = self
2045 .read_recovery_value(self.instance_id, RECOVERY_INCARNATION_KEY)
2046 .await?
2047 .ok_or_else(|| "recovery incarnation was not readable after publication".to_string())?;
2048 let observed = Uuid::parse_str(&observed)
2049 .map_err(|error| format!("invalid recovery incarnation after publication: {error}"))?;
2050 if observed != self.recovery_incarnation {
2051 return Err("recovery incarnation read-back mismatch".into());
2052 }
2053 Ok(())
2054 }
2055
2056 pub async fn publish_leased_recovery_incarnation(
2061 &self,
2062 lease: &super::ProcessLease,
2063 ) -> Result<(), String> {
2064 lease
2065 .validate(self.instance_id)
2066 .map_err(|error| error.to_string())?;
2067 if lease.node != self.instance_id || lease.owner != self.recovery_incarnation {
2068 return Err("process lease does not bind this recovery incarnation".into());
2069 }
2070 if !self.recovery_process_lease_is_live() {
2071 return Err("live process lease deadline is not installed".into());
2072 }
2073 let publisher = RecoveryFaultPublisher {
2074 participant: CheckpointParticipant {
2075 node_id: lease.node.0,
2076 boot_incarnation: lease.owner,
2077 },
2078 process_term: lease.term,
2079 };
2080 if !self
2081 .recovery_fault_publisher_is_current(publisher)
2082 .await
2083 .map_err(|error| error.to_string())?
2084 {
2085 return Err("process lease is not the current durable stable-node term".into());
2086 }
2087 if !self.recovery_process_lease_is_live() {
2088 return Err("process lease changed before publishing recovery identity".into());
2089 }
2090 self.barrier.install_local_process_lease(lease)?;
2091 if let Err(error) = self.publish_recovery_incarnation().await {
2092 self.fence_process_lease();
2093 return Err(error);
2094 }
2095 if !self.recovery_process_lease_is_live() {
2096 self.fence_process_lease();
2097 return Err("process lease changed while publishing recovery identity".into());
2098 }
2099 match self.recovery_fault_publisher_is_current(publisher).await {
2100 Ok(true) if self.recovery_process_lease_is_live() => {}
2101 Ok(_) => {
2102 self.fence_process_lease();
2103 return Err("process lease changed while publishing recovery identity".into());
2104 }
2105 Err(error) => {
2106 self.fence_process_lease();
2107 return Err(format!(
2108 "process lease authority became uncertain after recovery publication: {error}"
2109 ));
2110 }
2111 }
2112 self.recovery_process_term
2113 .store(lease.term, Ordering::Release);
2114 Ok(())
2115 }
2116
2117 pub fn next_recovery_fault_request(&self) -> Result<RecoveryFaultRequest, String> {
2125 let sequence = self
2126 .recovery_fault_request_sequence
2127 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
2128 current.checked_add(1)
2129 })
2130 .map_err(|_| "recovery fault request sequence exhausted".to_string())?;
2131 Ok(RecoveryFaultRequest {
2132 sequence: std::num::NonZeroU64::new(sequence)
2133 .ok_or_else(|| "recovery fault request allocator produced zero".to_string())?,
2134 })
2135 }
2136
2137 pub fn recovery_fault_request(&self, sequence: u64) -> Result<RecoveryFaultRequest, String> {
2142 let sequence = std::num::NonZeroU64::new(sequence)
2143 .ok_or_else(|| "recovery fault request sequence must be nonzero".to_string())?;
2144 if sequence.get() >= self.recovery_fault_request_sequence.load(Ordering::Acquire) {
2145 return Err("recovery fault request was not allocated by this process".into());
2146 }
2147 Ok(RecoveryFaultRequest { sequence })
2148 }
2149
2150 fn recovery_fault_publisher(&self) -> Result<RecoveryFaultPublisher, String> {
2151 let publisher = RecoveryFaultPublisher {
2152 participant: CheckpointParticipant {
2153 node_id: self.instance_id.0,
2154 boot_incarnation: self.recovery_incarnation,
2155 },
2156 process_term: self.recovery_process_term.load(Ordering::Acquire),
2157 };
2158 publisher.validate()?;
2159 Ok(publisher)
2160 }
2161
2162 async fn recovery_fault_publisher_is_current(
2163 &self,
2164 publisher: RecoveryFaultPublisher,
2165 ) -> Result<bool, RecoveryControlError> {
2166 if !self.recovery_process_lease_is_live() {
2167 return Ok(false);
2168 }
2169 let authority = self.process_lease_authority.get().ok_or_else(|| {
2170 RecoveryControlError::Conflict("process lease authority is not installed".into())
2171 })?;
2172 let deadline = tokio::time::Instant::now()
2173 .checked_add(RECOVERY_CONTROL_IO_TIMEOUT)
2174 .ok_or_else(|| {
2175 RecoveryControlError::Conflict(
2176 "recovery fault process-lease deadline overflow".into(),
2177 )
2178 })?;
2179 let current = authority
2180 .verify_current_participant_term(
2181 publisher.participant,
2182 publisher.process_term,
2183 deadline,
2184 )
2185 .await
2186 .map_err(RecoveryControlError::from_process_authority)?;
2187 Ok(current && self.recovery_process_lease_is_live())
2188 }
2189
2190 async fn recovery_incarnation_is_current(&self) -> Result<bool, String> {
2191 let Some(raw) = self
2192 .read_recovery_value(self.instance_id, RECOVERY_INCARNATION_KEY)
2193 .await?
2194 else {
2195 return Ok(false);
2196 };
2197 let observed = Uuid::parse_str(&raw)
2198 .map_err(|error| format!("invalid local recovery incarnation: {error}"))?;
2199 Ok(observed == self.recovery_incarnation)
2200 }
2201
2202 pub async fn recovery_participant_incarnations(
2207 &self,
2208 participants: &[u64],
2209 ) -> Result<Vec<CheckpointParticipant>, String> {
2210 let available = self
2211 .available_recovery_participant_incarnations(participants)
2212 .await?;
2213 if available.len() != participants.len() {
2214 let available_ids: std::collections::BTreeSet<u64> = available
2215 .iter()
2216 .map(|participant| participant.node_id)
2217 .collect();
2218 let missing = participants
2219 .iter()
2220 .find(|node_id| !available_ids.contains(node_id))
2221 .copied()
2222 .unwrap_or(0);
2223 return Err(format!(
2224 "node {missing} has no current recovery incarnation"
2225 ));
2226 }
2227 Ok(available)
2228 }
2229
2230 pub async fn available_recovery_participant_incarnations(
2237 &self,
2238 participants: &[u64],
2239 ) -> Result<Vec<CheckpointParticipant>, String> {
2240 if participants.is_empty()
2241 || participants.contains(&0)
2242 || participants.windows(2).any(|pair| pair[0] >= pair[1])
2243 {
2244 return Err("recovery incarnation roster requires canonical participants".into());
2245 }
2246 let expected: std::collections::BTreeSet<u64> = participants.iter().copied().collect();
2247 let mut reported = std::collections::BTreeMap::new();
2248 for (node, raw) in self.scan_recovery_values(RECOVERY_INCARNATION_KEY).await? {
2249 if !expected.contains(&node.0) {
2250 continue;
2251 }
2252 let incarnation = Uuid::parse_str(&raw).map_err(|error| {
2253 format!("invalid recovery incarnation published by {node}: {error}")
2254 })?;
2255 if incarnation.is_nil() {
2256 return Err(format!("nil recovery incarnation published by {node}"));
2257 }
2258 if reported.insert(node.0, incarnation).is_some() {
2259 return Err(format!(
2260 "duplicate recovery incarnation published by {node}"
2261 ));
2262 }
2263 }
2264 Ok(participants
2265 .iter()
2266 .filter_map(|node_id| {
2267 reported
2268 .get(node_id)
2269 .copied()
2270 .map(|incarnation| CheckpointParticipant {
2271 node_id: *node_id,
2272 boot_incarnation: incarnation,
2273 })
2274 })
2275 .collect())
2276 }
2277
2278 pub async fn recovery_incarnations_match(&self, round: &RecoveryRound) -> Result<bool, String> {
2283 Ok(self
2284 .recovery_participant_incarnations(&round.assignment_fence.participant_ids())
2285 .await?
2286 == round.assignment_fence.participants)
2287 }
2288
2289 pub async fn recovery_stopped_incarnations_match(
2297 &self,
2298 round: &RecoveryRound,
2299 ) -> Result<bool, RecoveryControlError> {
2300 self.recovery_roster_incarnations_match_control(&round.stopped_roster())
2301 .await
2302 }
2303
2304 async fn recovery_incarnations_match_control(
2305 &self,
2306 round: &RecoveryRound,
2307 ) -> Result<bool, RecoveryControlError> {
2308 self.recovery_roster_incarnations_match_control(&round.assignment_fence.participants)
2309 .await
2310 }
2311
2312 async fn recovery_roster_incarnations_match_control(
2313 &self,
2314 roster: &[CheckpointParticipant],
2315 ) -> Result<bool, RecoveryControlError> {
2316 let expected: std::collections::BTreeSet<u64> = roster
2317 .iter()
2318 .map(|participant| participant.node_id)
2319 .collect();
2320 let mut reported = std::collections::BTreeMap::new();
2321 for (node, raw) in self
2322 .scan_recovery_values(RECOVERY_INCARNATION_KEY)
2323 .await
2324 .map_err(RecoveryControlError::Uncertain)?
2325 {
2326 if !expected.contains(&node.0) {
2327 continue;
2328 }
2329 let incarnation = Uuid::parse_str(&raw).map_err(|error| {
2330 RecoveryControlError::Conflict(format!(
2331 "invalid recovery incarnation published by {node}: {error}"
2332 ))
2333 })?;
2334 if incarnation.is_nil() || reported.insert(node.0, incarnation).is_some() {
2335 return Err(RecoveryControlError::Conflict(format!(
2336 "noncanonical recovery incarnation published by {node}"
2337 )));
2338 }
2339 }
2340 if reported.len() != roster.len() {
2341 return Ok(false);
2342 }
2343 Ok(roster.iter().all(|participant| {
2344 reported.get(&participant.node_id) == Some(&participant.boot_incarnation)
2345 }))
2346 }
2347
2348 async fn read_recovery_fault_inventory_control(
2349 &self,
2350 ) -> Result<RecoveryFaultInventory, RecoveryControlError> {
2351 self.checkpoint_authority()
2352 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?
2353 .recovery_fault_inventory()
2354 .await
2355 .map_err(RecoveryControlError::from_authority)
2356 }
2357
2358 pub async fn read_recovery_fault_inventory(&self) -> Result<RecoveryFaultInventory, String> {
2363 self.read_recovery_fault_inventory_control()
2364 .await
2365 .map_err(|error| error.to_string())
2366 }
2367
2368 pub async fn read_recovery_admission_snapshot(
2373 &self,
2374 ) -> Result<RecoveryAdmissionSnapshot, RecoveryControlError> {
2375 self.checkpoint_authority()
2376 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?
2377 .recovery_admission_snapshot()
2378 .await
2379 .map_err(RecoveryControlError::from_authority)
2380 }
2381
2382 pub async fn recovery_admission_is_current(
2388 &self,
2389 snapshot: &RecoveryAdmissionSnapshot,
2390 leader_proof: &LeaderProof,
2391 ) -> Result<bool, RecoveryControlError> {
2392 self.checkpoint_authority()
2393 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?
2394 .recovery_admission_is_current(snapshot, leader_proof)
2395 .await
2396 .map_err(RecoveryControlError::from_authority)
2397 }
2398
2399 pub async fn report_fault(
2404 &self,
2405 request: RecoveryFaultRequest,
2406 ) -> Result<RecoveryFaultReportOutcome, String> {
2407 let seq = request.sequence();
2408 let _guard = self.recovery_writes.lock().await;
2409 let publisher = self.recovery_fault_publisher()?;
2410 if !self
2411 .recovery_fault_publisher_is_current(publisher)
2412 .await
2413 .map_err(|error| error.to_string())?
2414 {
2415 return Err("stable node process lease is no longer current".into());
2416 }
2417 let authority = self
2418 .checkpoint_authority()
2419 .map_err(|error| error.to_string())?;
2420 let result = Box::pin(authority.record_recovery_fault(publisher, seq))
2421 .await
2422 .map_err(|error| RecoveryControlError::from_authority(error).to_string())?;
2423 if !self
2424 .recovery_fault_publisher_is_current(publisher)
2425 .await
2426 .map_err(|error| error.to_string())?
2427 {
2428 return Err("stable node process lease changed while publishing recovery fault".into());
2429 }
2430 match result {
2431 super::leader_lease::RecordRecoveryFaultResult::Active => {
2432 Ok(RecoveryFaultReportOutcome::Active)
2433 }
2434 super::leader_lease::RecordRecoveryFaultResult::AlreadyCleared => {
2435 Ok(RecoveryFaultReportOutcome::AlreadyCleared)
2436 }
2437 super::leader_lease::RecordRecoveryFaultResult::CoveredByNewerRequest => {
2438 Ok(RecoveryFaultReportOutcome::CoveredByNewerRequest)
2439 }
2440 super::leader_lease::RecordRecoveryFaultResult::Superseded => {
2441 Err("recovery fault request was superseded by a newer local process".into())
2442 }
2443 }
2444 }
2445
2446 pub async fn begin_recovery_release(
2456 &self,
2457 terminal: &RecoveryAnnouncement,
2458 ) -> Result<Option<RecoveryReleaseGuard<'_>>, RecoveryControlError> {
2459 terminal
2460 .validate()
2461 .map_err(RecoveryControlError::Conflict)?;
2462 if !matches!(terminal.phase, RecoverPhase::ReleaseCommitted { .. }) {
2463 return Err(RecoveryControlError::Conflict(
2464 "recovery fault consumption requires a terminal Release".into(),
2465 ));
2466 }
2467 let guard = self.recovery_writes.lock().await;
2468 let publisher = self
2469 .recovery_fault_publisher()
2470 .map_err(RecoveryControlError::Conflict)?;
2471 if !self.recovery_fault_publisher_is_current(publisher).await? {
2472 return Err(RecoveryControlError::Superseded(
2473 "stable node process lease is no longer current".into(),
2474 ));
2475 }
2476 let authorized = self
2477 .checkpoint_authority()
2478 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?
2479 .authorize_recovery_release(publisher, terminal)
2480 .await
2481 .map_err(RecoveryControlError::from_authority)?;
2482 if !authorized {
2483 return Ok(None);
2484 }
2485 if !self.recovery_fault_publisher_is_current(publisher).await? {
2486 return Err(RecoveryControlError::Superseded(
2487 "stable node process lease changed while authorizing recovery Release".into(),
2488 ));
2489 }
2490 Ok(Some(RecoveryReleaseGuard {
2491 _write_guard: guard,
2492 }))
2493 }
2494
2495 pub async fn read_local_fault_report_control(
2500 &self,
2501 ) -> Result<Option<u64>, RecoveryControlError> {
2502 Ok(self
2503 .read_recovery_fault_inventory_control()
2504 .await?
2505 .faults
2506 .into_iter()
2507 .find(|fault| fault.reporter == self.instance_id)
2508 .map(|fault| fault.sequence))
2509 }
2510
2511 pub async fn read_local_fault_report(&self) -> Result<Option<u64>, String> {
2516 self.read_local_fault_report_control()
2517 .await
2518 .map_err(|error| error.to_string())
2519 }
2520
2521 pub async fn read_fault_reports(&self) -> Result<Vec<(NodeId, u64)>, String> {
2526 Ok(self
2527 .read_recovery_fault_inventory()
2528 .await?
2529 .faults
2530 .into_iter()
2531 .map(|fault| (fault.reporter, fault.sequence))
2532 .collect())
2533 }
2534
2535 async fn audit_recovery_faults_control(
2536 &self,
2537 round: &RecoveryRound,
2538 ) -> Result<(), RecoveryControlError> {
2539 let inventory = self.read_recovery_fault_inventory_control().await?;
2540 if inventory.revision == round.fault_revision && inventory.faults == round.faults {
2541 Ok(())
2542 } else {
2543 Err(RecoveryControlError::Superseded(
2544 "recovery fault set changed before Release commit".into(),
2545 ))
2546 }
2547 }
2548
2549 async fn audit_pending_release_faults_control(
2550 &self,
2551 release: &RecoveryAnnouncement,
2552 ) -> Result<(), RecoveryControlError> {
2553 let release_id =
2554 RecoveryReleaseId::for_pending(release).map_err(RecoveryControlError::Conflict)?;
2555 let mut cached = self.pending_release_fault_audit.lock().await;
2556 let now = tokio::time::Instant::now();
2557 if cached
2558 .as_ref()
2559 .is_some_and(|(audited, valid_until)| audited == &release_id && now < *valid_until)
2560 {
2561 return Ok(());
2562 }
2563 self.audit_recovery_faults_control(&release.round).await?;
2564 *cached = Some((
2565 release_id,
2566 tokio::time::Instant::now() + PENDING_RELEASE_FAULT_AUDIT_INTERVAL,
2567 ));
2568 Ok(())
2569 }
2570
2571 pub async fn announce_recovered(&self, start: &RecoveryAnnouncement) -> Result<(), String> {
2576 start.validate()?;
2577 if !matches!(start.phase, RecoverPhase::Start { .. }) {
2578 return Err("restore acknowledgement must bind a Start target".into());
2579 }
2580 if !start.round.contains_owner(self.instance_id) {
2581 return Err("node outside recovery quorum cannot acknowledge restore".into());
2582 }
2583 if start.round.owner_incarnation(self.instance_id) != Some(self.recovery_incarnation) {
2584 return Err("restore acknowledgement has a stale local process incarnation".into());
2585 }
2586 if !self.recovery_incarnation_is_current().await? {
2587 return Err("restore acknowledgement came from a superseded local process".into());
2588 }
2589 let encoded = serde_json::to_string(&RecoveryAnnouncementAck {
2590 announcement: start.clone(),
2591 incarnation: self.recovery_incarnation,
2592 })
2593 .map_err(|error| format!("could not encode recovery ack: {error}"))?;
2594 self.write_recovery_value_exact("control:recovered", encoded)
2595 .await
2596 }
2597
2598 pub async fn read_recovered(&self) -> Result<Vec<(NodeId, RecoveryAnnouncement)>, String> {
2603 self.read_recovery_announcement_map("control:recovered")
2604 .await
2605 }
2606
2607 pub async fn announce_release_ready(
2615 &self,
2616 release: &RecoveryAnnouncement,
2617 ) -> Result<(), RecoveryControlError> {
2618 let release_id =
2619 RecoveryReleaseId::for_pending(release).map_err(RecoveryControlError::Conflict)?;
2620 if release.round.owner_incarnation(self.instance_id) != Some(self.recovery_incarnation) {
2621 return Err(RecoveryControlError::Superseded(
2622 "release readiness has a stale local process incarnation".into(),
2623 ));
2624 }
2625 if !self
2626 .recovery_incarnation_is_current()
2627 .await
2628 .map_err(RecoveryControlError::Uncertain)?
2629 {
2630 return Err(RecoveryControlError::Superseded(
2631 "release readiness came from a superseded local process".into(),
2632 ));
2633 }
2634 if self.read_local_fault_report_control().await?
2635 != release.round.fault_sequence(self.instance_id)
2636 {
2637 return Err(RecoveryControlError::Superseded(
2638 "local fault set changed before release readiness".into(),
2639 ));
2640 }
2641 match self.observe_recover_control().await? {
2642 Some(active) if active == *release => {}
2643 _ => {
2644 return Err(RecoveryControlError::Superseded(
2645 "release readiness no longer matches the active intent".into(),
2646 ));
2647 }
2648 }
2649 let participant = CheckpointParticipant {
2650 node_id: self.instance_id.0,
2651 boot_incarnation: self.recovery_incarnation,
2652 };
2653 let encoded = encode_release_ready_ack(&RecoveryReleaseReadyAck {
2654 release: release_id,
2655 participant,
2656 })
2657 .map_err(RecoveryControlError::Conflict)?;
2658 self.write_recovery_value_exact(RELEASE_READY_ACK_KEY, encoded)
2659 .await
2660 .map_err(RecoveryControlError::Uncertain)
2661 }
2662
2663 async fn read_release_ready(
2671 &self,
2672 release: &RecoveryAnnouncement,
2673 ) -> Result<ReleaseReadyStatus, RecoveryControlError> {
2674 let expected =
2675 RecoveryReleaseId::for_pending(release).map_err(RecoveryControlError::Conflict)?;
2676 let reads = futures::stream::iter(
2677 release
2678 .round
2679 .assignment_fence
2680 .participants
2681 .iter()
2682 .copied()
2683 .map(|participant| async move {
2684 let value = self
2685 .read_recovery_value(NodeId(participant.node_id), RELEASE_READY_ACK_KEY)
2686 .await;
2687 (participant, value)
2688 }),
2689 )
2690 .buffer_unordered(CONTROL_ROSTER_IO_CONCURRENCY)
2691 .collect::<Vec<_>>()
2692 .await;
2693 let mut reads = reads;
2694 reads.sort_unstable_by_key(|(participant, _)| participant.node_id);
2695 let mut missing = Vec::new();
2696 for (participant, value) in reads {
2697 let raw = value.map_err(RecoveryControlError::Uncertain)?;
2698 let Some(raw) = raw else {
2699 missing.push(NodeId(participant.node_id));
2700 continue;
2701 };
2702 let ack = parse_release_ready_ack(&raw, NodeId(participant.node_id))
2703 .map_err(RecoveryControlError::Conflict)?;
2704 if ack.participant != participant {
2705 return Err(RecoveryControlError::Conflict(format!(
2706 "release readiness from {} does not match the frozen process",
2707 participant.node_id
2708 )));
2709 }
2710 if ack.release != expected {
2711 if ack.release.generation < expected.generation {
2712 missing.push(NodeId(participant.node_id));
2713 continue;
2714 }
2715 if ack.release.generation > expected.generation {
2716 return Err(RecoveryControlError::Superseded(format!(
2717 "release readiness from {} has newer generation {}",
2718 participant.node_id, ack.release.generation
2719 )));
2720 }
2721 return Err(RecoveryControlError::Conflict(format!(
2722 "release readiness from {} conflicts with generation {}",
2723 participant.node_id, expected.generation
2724 )));
2725 }
2726 }
2727 if !missing.is_empty() {
2728 return Ok(ReleaseReadyStatus::Pending { missing });
2729 }
2730 Ok(ReleaseReadyStatus::Complete)
2731 }
2732
2733 #[must_use]
2736 pub fn assignable_instances(&self) -> Vec<NodeId> {
2737 let mut ids = assignable_node_ids(&self.members_rx.borrow());
2738 ids.retain(|id| *id != self.instance_id);
2739 if self.active.load(Ordering::SeqCst)
2740 && !self.is_draining()
2741 && !self.instance_id.is_unassigned()
2742 {
2743 ids.push(self.instance_id);
2744 }
2745 ids.sort_unstable();
2746 ids.dedup();
2747 ids
2748 }
2749
2750 pub fn set_self_locality(&self, locality: Locality) {
2752 *self.self_locality.write() = locality;
2753 }
2754
2755 #[must_use]
2758 pub fn assignable_with_locality(&self) -> Vec<(NodeId, Locality)> {
2759 let members = self.members_rx.borrow();
2760 self.assignable_instances()
2761 .into_iter()
2762 .map(|id| {
2763 let locality = if id == self.instance_id {
2764 self.self_locality.read().clone()
2765 } else {
2766 members
2767 .iter()
2768 .find(|m| m.id == id)
2769 .and_then(|m| m.metadata.failure_domain.as_deref())
2770 .map(Locality::parse)
2771 .unwrap_or_default()
2772 };
2773 (id, locality)
2774 })
2775 .collect()
2776 }
2777
2778 #[must_use]
2780 pub fn members_watch(&self) -> watch::Receiver<Vec<NodeInfo>> {
2781 self.members_rx.clone()
2782 }
2783
2784 async fn drain_transition_authority_is_current(
2785 &self,
2786 transition: &AssignmentDrainTransition,
2787 ) -> Result<bool, String> {
2788 #[cfg(feature = "cluster")]
2789 {
2790 let authority = self
2791 .checkpoint_authority()
2792 .map_err(|error| error.to_string())?;
2793 let Some(lease) = authority.load().await.map_err(|error| error.to_string())? else {
2794 return Ok(false);
2795 };
2796 Ok(lease.matches_proof(&transition.leader))
2797 }
2798 #[cfg(not(feature = "cluster"))]
2799 {
2800 let _ = transition;
2801 Err("assignment drain authority requires the cluster feature".into())
2802 }
2803 }
2804
2805 pub async fn announce_drain_ack(
2812 &self,
2813 transition: &AssignmentDrainTransition,
2814 ) -> Result<(), String> {
2815 if !transition.is_canonical() {
2816 return Err("drain acknowledgement requires an exact assignment transition".into());
2817 }
2818 let participant = CheckpointParticipant {
2819 node_id: self.instance_id.0,
2820 boot_incarnation: self.recovery_incarnation,
2821 };
2822 if transition
2823 .predecessor
2824 .participant_incarnation(participant.node_id)
2825 != Some(participant.boot_incarnation)
2826 {
2827 return Err("drain acknowledgement does not bind the local process".into());
2828 }
2829 if self.checkpoint_drain_transition.borrow().as_ref() != Some(transition) {
2830 return Err("drain acknowledgement is not the locally audited transition".into());
2831 }
2832 if !self.process_lease_is_live()
2833 || !self
2834 .drain_transition_authority_is_current(transition)
2835 .await?
2836 {
2837 return Err("drain acknowledgement authority is no longer live".into());
2838 }
2839 if !self.recovery_incarnation_is_current().await? {
2840 return Err("drain acknowledgement came from a superseded local process".into());
2841 }
2842 self.write_recovery_value_exact(
2843 DRAIN_ACK_KEY,
2844 encode_drain_ack(&DrainAck::for_transition(participant, transition))?,
2845 )
2846 .await
2847 }
2848
2849 pub async fn drain_ack_quorum_reached(
2859 &self,
2860 transition: &AssignmentDrainTransition,
2861 ) -> Result<bool, String> {
2862 if !transition.is_canonical() {
2863 return Err("drain quorum requires a canonical assignment transition".into());
2864 }
2865 let locally_audited =
2866 self.checkpoint_drain_transition.borrow().as_ref() == Some(transition);
2867 if !locally_audited
2868 || !self
2869 .drain_transition_authority_is_current(transition)
2870 .await?
2871 {
2872 return Ok(false);
2873 }
2874 if self
2875 .recovery_participant_incarnations(&transition.predecessor.participant_ids())
2876 .await?
2877 != transition.predecessor.participants
2878 {
2879 return Ok(false);
2880 }
2881
2882 let expected: std::collections::BTreeSet<NodeId> = transition
2883 .required_participants()
2884 .iter()
2885 .map(|participant| NodeId(participant.node_id))
2886 .collect();
2887 let mut seen = std::collections::BTreeSet::new();
2888 let mut matching = std::collections::BTreeSet::new();
2889 for (publisher, raw) in self.scan_recovery_values(DRAIN_ACK_KEY).await? {
2890 if !expected.contains(&publisher) {
2891 continue;
2892 }
2893 if !seen.insert(publisher) {
2894 return Err(format!(
2895 "duplicate drain acknowledgement from expected participant {publisher}"
2896 ));
2897 }
2898 let ack = parse_drain_ack(&raw, publisher)?;
2899 if ack.matches_transition(transition) {
2900 matching.insert(publisher);
2901 }
2902 }
2903 if matching != expected {
2904 return Ok(false);
2905 }
2906 Ok(self
2909 .recovery_participant_incarnations(&transition.predecessor.participant_ids())
2910 .await?
2911 == transition.predecessor.participants
2912 && self
2913 .drain_transition_authority_is_current(transition)
2914 .await?)
2915 }
2916
2917 pub async fn announce_adopted_assignment(
2922 &self,
2923 adoption: &CheckpointAssignmentAdoption,
2924 ) -> Result<(), String> {
2925 if !adoption.is_canonical()
2926 || adoption.participant.node_id != self.instance_id.0
2927 || adoption.participant.boot_incarnation != self.recovery_incarnation
2928 || !self.recovery_incarnation_is_current().await?
2929 {
2930 return Err("adopted assignment report does not bind the current process".into());
2931 }
2932 let encoded = serde_json::to_string(adoption)
2933 .map_err(|error| format!("could not encode adopted assignment: {error}"))?;
2934 self.write_recovery_value_exact("control:adopted-assignment", encoded)
2935 .await
2936 }
2937
2938 pub async fn read_adopted_assignments(
2943 &self,
2944 ) -> Result<Vec<(NodeId, CheckpointAssignmentAdoption)>, String> {
2945 self.scan_recovery_values("control:adopted-assignment")
2946 .await?
2947 .into_iter()
2948 .map(|(node, raw)| {
2949 let adoption: CheckpointAssignmentAdoption = serde_json::from_str(&raw)
2950 .map_err(|error| format!("invalid adopted assignment from {node}: {error}"))?;
2951 if !adoption.is_canonical() || adoption.participant.node_id != node.0 {
2952 return Err(format!(
2953 "adopted assignment from {node} has a non-canonical publisher"
2954 ));
2955 }
2956 Ok((node, adoption))
2957 })
2958 .collect()
2959 }
2960
2961 pub fn publish_checkpoint_assignment_fence(&self, fence: Option<CheckpointAssignmentFence>) {
2964 let _transition = self.process_authority_transition.lock();
2965 let fence = if self.process_lease_is_live() {
2966 fence
2967 } else {
2968 None
2969 };
2970 if let Some(fence) = fence.as_ref().filter(|fence| fence.is_canonical()) {
2971 let participants = fence.participant_ids();
2972 let changed = {
2973 let mut installed = self.leadership_participants.write();
2974 match installed.as_ref() {
2975 Some((version, _)) if *version >= fence.assignment_version => false,
2976 _ => {
2977 *installed = Some((fence.assignment_version, participants));
2978 true
2979 }
2980 }
2981 };
2982 if changed {
2983 self.notify_leader_eligibility_change();
2984 }
2985 }
2986 self.checkpoint_assignment_fence.send_replace(fence);
2987 }
2988
2989 pub fn publish_checkpoint_drain_transition(
2991 &self,
2992 transition: Option<AssignmentDrainTransition>,
2993 ) {
2994 let _transition = self.process_authority_transition.lock();
2995 let transition = if self.process_lease_is_live() {
2996 transition
2997 } else {
2998 None
2999 };
3000 self.checkpoint_drain_transition.send_replace(transition);
3001 }
3002
3003 #[must_use]
3008 pub fn clear_checkpoint_drain_transition_if_matches(
3009 &self,
3010 expected: &AssignmentDrainTransition,
3011 ) -> bool {
3012 let _transition = self.process_authority_transition.lock();
3013 let matches = self.checkpoint_drain_transition.borrow().as_ref() == Some(expected);
3014 if matches {
3015 self.checkpoint_drain_transition.send_replace(None);
3016 }
3017 matches
3018 }
3019
3020 #[must_use]
3022 pub fn checkpoint_drain_transition(&self) -> Option<AssignmentDrainTransition> {
3023 self.checkpoint_drain_transition.borrow().clone()
3024 }
3025
3026 #[must_use]
3029 pub fn checkpoint_assignment_watch(
3030 &self,
3031 ) -> watch::Receiver<Option<CheckpointAssignmentFence>> {
3032 self.checkpoint_assignment_fence.subscribe()
3033 }
3034
3035 #[must_use]
3039 pub fn checkpoint_assignment_fence(
3040 &self,
3041 assignment_version: u64,
3042 ) -> Option<CheckpointAssignmentFence> {
3043 let checkpoint_capable: Vec<u64> = self
3044 .checkpoint_instances()
3045 .into_iter()
3046 .map(|node| node.0)
3047 .collect();
3048 self.checkpoint_assignment_fence
3049 .borrow()
3050 .as_ref()
3051 .filter(|fence| {
3052 fence.is_canonical()
3053 && fence.assignment_version == assignment_version
3054 && fence
3055 .participant_incarnation(self.instance_id.0)
3056 .is_none_or(|incarnation| incarnation == self.recovery_incarnation)
3057 && fence.participants.iter().all(|participant| {
3058 checkpoint_capable
3059 .binary_search(&participant.node_id)
3060 .is_ok()
3061 })
3062 })
3063 .cloned()
3064 }
3065
3066 #[cfg(feature = "cluster")]
3070 pub async fn checkpoint_assignment_fence_for_leader(
3071 &self,
3072 assignment_version: u64,
3073 leader: &crate::checkpoint::LeaderProof,
3074 ) -> Option<CheckpointAssignmentFence> {
3075 if !leader.is_canonical() {
3076 return None;
3077 }
3078 let fence = self.checkpoint_assignment_fence(assignment_version)?;
3079 let authority = self.checkpoint_authority().ok()?;
3080 let lease = authority.load().await.ok().flatten()?;
3081 if !lease.matches_proof(leader) {
3082 return None;
3083 }
3084 self.checkpoint_assignment_fence_after_authority_validation(fence, leader)
3085 }
3086
3087 #[cfg(feature = "cluster")]
3088 fn checkpoint_assignment_fence_after_authority_validation(
3089 &self,
3090 fence: CheckpointAssignmentFence,
3091 leader: &crate::checkpoint::LeaderProof,
3092 ) -> Option<CheckpointAssignmentFence> {
3093 let transition = self.checkpoint_drain_transition.borrow();
3094 if transition
3095 .as_ref()
3096 .is_some_and(|transition| transition.predecessor == fence)
3097 && transition.as_ref().map(|transition| &transition.leader) != Some(leader)
3098 {
3099 return None;
3100 }
3101 Some(fence)
3102 }
3103
3104 #[cfg(feature = "cluster")]
3109 pub async fn start_barrier_server(
3110 &self,
3111 bind_addr: std::net::SocketAddr,
3112 advertise_host: Option<String>,
3113 ) -> Result<std::net::SocketAddr, String> {
3114 self.barrier.start_server(bind_addr, advertise_host).await
3115 }
3116
3117 #[cfg(feature = "cluster")]
3123 pub async fn start_leased_barrier_server(
3124 &self,
3125 bind_addr: std::net::SocketAddr,
3126 advertise_host: Option<String>,
3127 process_lease: &super::ProcessLease,
3128 ) -> Result<std::net::SocketAddr, String> {
3129 process_lease
3130 .validate(self.instance_id)
3131 .map_err(|error| error.to_string())?;
3132 if process_lease.owner != self.recovery_incarnation {
3133 return Err("control endpoint lease does not bind this process incarnation".into());
3134 }
3135 if !self.recovery_process_lease_is_live() {
3136 return Err("live process lease deadline is not installed".into());
3137 }
3138 self.barrier.install_local_process_lease(process_lease)?;
3139 self.barrier.start_server(bind_addr, advertise_host).await
3140 }
3141
3142 #[cfg(feature = "cluster")]
3149 pub async fn confirm_remote_leader_proof(
3150 &self,
3151 proof: &LeaderProof,
3152 deadline: tokio::time::Instant,
3153 ) -> Result<bool, String> {
3154 self.barrier
3155 .confirm_remote_leader_proof(proof, deadline)
3156 .await
3157 }
3158
3159 #[cfg(feature = "cluster")]
3160 async fn confirm_assignment_leader_proof(
3161 &self,
3162 leader: NodeId,
3163 proof: &LeaderProof,
3164 deadline: tokio::time::Instant,
3165 ) -> Result<(), String> {
3166 if tokio::time::Instant::now() >= deadline {
3167 return Err("assignment leader authority audit deadline expired".into());
3168 }
3169 if proof.owner.node_id != leader.0 {
3170 return Err("assignment leader proof does not match the current leader".into());
3171 }
3172 let confirmed = if leader == self.instance_id {
3173 self.capture_leader_proof().as_ref() == Some(proof)
3174 } else {
3175 self.confirm_remote_leader_proof(proof, deadline).await?
3176 };
3177 if !confirmed {
3178 return Err(format!(
3179 "assignment leader process {} has no live exact grant",
3180 leader.0
3181 ));
3182 }
3183 Ok(())
3184 }
3185
3186 #[cfg(feature = "cluster")]
3197 pub async fn audit_assignment_leader_authority(
3198 &self,
3199 fence: &CheckpointAssignmentFence,
3200 expected_proof: Option<&LeaderProof>,
3201 deadline: tokio::time::Instant,
3202 ) -> Result<LeaderProof, String> {
3203 if tokio::time::Instant::now() >= deadline {
3204 return Err("assignment leader authority audit deadline expired".into());
3205 }
3206 if !fence.is_canonical()
3207 || self
3208 .checkpoint_assignment_fence(fence.assignment_version)
3209 .as_ref()
3210 != Some(fence)
3211 {
3212 return Err(
3213 "assignment leader authority audit requires the exact installed fence".into(),
3214 );
3215 }
3216 if expected_proof.is_some_and(|proof| !proof.is_canonical()) {
3217 return Err("assignment leader authority audit expected proof is not canonical".into());
3218 }
3219
3220 let leader = self
3221 .current_leader()
3222 .ok_or_else(|| "assignment leader authority audit has no current leader".to_string())?;
3223 let participant = fence
3224 .participants
3225 .iter()
3226 .find(|participant| participant.node_id == leader.0)
3227 .copied()
3228 .ok_or_else(|| {
3229 "current leader is not a participant in the exact assignment fence".to_string()
3230 })?;
3231 let authority = self
3232 .checkpoint_authority()
3233 .map_err(|error| format!("assignment leader authority is unavailable: {error}"))?;
3234 let initial = tokio::time::timeout_at(deadline, authority.load())
3235 .await
3236 .map_err(|_| "assignment leader authority initial read timed out".to_string())?
3237 .map_err(|error| format!("assignment leader authority initial read failed: {error}"))?
3238 .ok_or_else(|| "assignment leader authority has no durable grant".to_string())?;
3239 if initial.owner.node != leader || initial.owner.boot != participant.boot_incarnation {
3240 return Err(
3241 "durable leader grant does not match the elected assignment participant".into(),
3242 );
3243 }
3244 let proof = match expected_proof {
3245 Some(expected) if initial.matches_proof(expected) => expected.clone(),
3246 Some(_) => {
3247 return Err(
3248 "durable leader grant does not match the drain-bound expected proof".into(),
3249 );
3250 }
3251 None => initial.proof(),
3252 };
3253
3254 self.confirm_assignment_leader_proof(leader, &proof, deadline)
3255 .await?;
3256 let process_authority = self
3257 .process_lease_authority
3258 .get()
3259 .ok_or_else(|| "process lease authority is not installed".to_string())?;
3260 let process_current = process_authority
3261 .verify_current_participant_term(participant, proof.owner.process_term, deadline)
3262 .await
3263 .map_err(|error| {
3264 format!("assignment leader process-term verification failed: {error}")
3265 })?;
3266 if !process_current {
3267 return Err("assignment leader durable process term is no longer current".to_string());
3268 }
3269 self.confirm_assignment_leader_proof(leader, &proof, deadline)
3270 .await?;
3271
3272 let final_grant = tokio::time::timeout_at(deadline, authority.load())
3273 .await
3274 .map_err(|_| "assignment leader authority final read timed out".to_string())?
3275 .map_err(|error| format!("assignment leader authority final read failed: {error}"))?
3276 .ok_or_else(|| "assignment leader authority grant disappeared".to_string())?;
3277 if final_grant.owner != initial.owner
3278 || final_grant.token != initial.token
3279 || final_grant.seq < initial.seq
3280 || !final_grant.matches_proof(&proof)
3281 {
3282 return Err("assignment leader durable grant changed during the audit".into());
3283 }
3284 if self.current_leader() != Some(leader)
3285 || self
3286 .checkpoint_assignment_fence(fence.assignment_version)
3287 .as_ref()
3288 != Some(fence)
3289 {
3290 return Err("assignment leader or exact fence changed during the audit".into());
3291 }
3292 if tokio::time::Instant::now() >= deadline {
3293 return Err("assignment leader authority audit deadline expired".into());
3294 }
3295 Ok(proof)
3296 }
3297
3298 pub async fn announce_barrier(&self, ann: &BarrierAnnouncement) -> Result<(), String> {
3303 if !self.process_lease_is_live() {
3304 return Err("stable node process lease is no longer live".into());
3305 }
3306 self.barrier.announce(ann).await
3307 }
3308
3309 #[cfg(feature = "cluster")]
3314 pub async fn announce_prepare_barrier(
3315 &self,
3316 ann: &BarrierAnnouncement,
3317 quorum_window: Duration,
3318 ) -> Result<(), String> {
3319 if !self.process_lease_is_live() {
3320 return Err("stable node process lease is no longer live".into());
3321 }
3322 self.barrier.announce_prepare(ann, quorum_window).await
3323 }
3324
3325 pub async fn observe_barrier_matching<F>(
3331 &self,
3332 mut predicate: F,
3333 ) -> Result<Option<BarrierAnnouncement>, String>
3334 where
3335 F: FnMut(&BarrierAnnouncement) -> bool,
3336 {
3337 let Some(leader) = self.current_leader() else {
3338 return Ok(None);
3339 };
3340 let Some(announcement) = self.barrier.observe_hint(leader).await? else {
3341 return Ok(None);
3342 };
3343 if !predicate(&announcement) {
3344 return Ok(None);
3345 }
3346 #[cfg(feature = "cluster")]
3347 self.barrier.validate_observed(&announcement).await?;
3348 Ok(Some(announcement))
3349 }
3350
3351 #[cfg(feature = "cluster")]
3357 pub async fn observe_checkpoint_prepare(
3358 &self,
3359 ) -> Result<Option<CheckpointPrepareObservation>, String> {
3360 let Some(leader) = self.current_leader() else {
3361 return Ok(None);
3362 };
3363 let Some(announcement) = self.barrier.observe_hint(leader).await? else {
3364 return Ok(None);
3365 };
3366 if announcement.phase != super::Phase::Prepare {
3367 return Ok(None);
3368 }
3369 self.barrier
3370 .validate_checkpoint_prepare(&announcement)
3371 .await?;
3372 let proof = announcement
3373 .leader_proof
3374 .as_ref()
3375 .filter(|proof| proof.is_canonical())
3376 .ok_or_else(|| "leader Prepare omitted its canonical authority proof".to_string())?;
3377 let assignment_error = match announcement.assignment_fence.as_ref() {
3378 None => Some(
3379 "[LDB-6055] leader Prepare omitted its canonical assignment certificate"
3380 .to_string(),
3381 ),
3382 Some(fence) if !fence.is_canonical() => Some(
3383 "[LDB-6055] leader Prepare carried a non-canonical assignment certificate"
3384 .to_string(),
3385 ),
3386 Some(fence) => self
3387 .checkpoint_assignment_fence(fence.assignment_version)
3388 .and_then(|certified| {
3389 self.checkpoint_assignment_fence_after_authority_validation(certified, proof)
3390 })
3391 .map_or_else(
3392 || {
3393 Some(format!(
3394 "[LDB-6055] follower cannot certify leader Prepare assignment {}",
3395 fence.assignment_version
3396 ))
3397 },
3398 |certified| {
3399 (certified != *fence).then(|| {
3400 format!(
3401 "[LDB-6055] follower assignment differs from leader Prepare assignment {}",
3402 fence.assignment_version
3403 )
3404 })
3405 },
3406 ),
3407 };
3408 Ok(Some(match assignment_error {
3409 Some(error) => CheckpointPrepareObservation::AssignmentRejected {
3410 announcement,
3411 error,
3412 },
3413 None => CheckpointPrepareObservation::AssignmentReady(announcement),
3414 }))
3415 }
3416
3417 #[cfg(feature = "cluster")]
3420 #[must_use]
3421 pub fn checkpoint_announcement_watch(
3422 &self,
3423 ) -> Option<watch::Receiver<Option<BarrierAnnouncement>>> {
3424 self.barrier.announcement_watch()
3425 }
3426
3427 #[must_use]
3429 pub fn checkpoint_prepare_received_at(
3430 &self,
3431 prepare: &BarrierAnnouncement,
3432 ) -> Option<std::time::Instant> {
3433 self.barrier.prepare_received_at(prepare)
3434 }
3435
3436 pub async fn ack_barrier(&self, ack: &BarrierAck) -> Result<(), String> {
3441 if !self.process_lease_is_live() {
3442 return Err("stable node process lease is no longer live".into());
3443 }
3444 self.barrier.ack(ack).await
3445 }
3446
3447 async fn durable_recovery_proof_is_current(&self, proof: &LeaderProof) -> Result<bool, String> {
3448 if !proof.is_canonical() {
3449 return Ok(false);
3450 }
3451 let authority = self
3452 .checkpoint_authority()
3453 .map_err(|error| format!("durable recovery authority is unavailable: {error}"))?;
3454 let Some(lease) = authority
3455 .load()
3456 .await
3457 .map_err(|error| format!("durable recovery authority read failed: {error}"))?
3458 else {
3459 return Ok(false);
3460 };
3461 Ok(lease.matches_proof(proof))
3462 }
3463
3464 async fn recovery_driver_proof_is_current(
3465 &self,
3466 round: &RecoveryRound,
3467 ) -> Result<bool, String> {
3468 Ok(round.id.driver == self.instance_id
3469 && self.proof_is_live(&round.leader_proof)
3470 && self
3471 .durable_recovery_proof_is_current(&round.leader_proof)
3472 .await?)
3473 }
3474
3475 async fn require_recovery_driver_proof(
3476 &self,
3477 round: &RecoveryRound,
3478 boundary: &str,
3479 ) -> Result<(), String> {
3480 if self.recovery_driver_proof_is_current(round).await? {
3481 Ok(())
3482 } else {
3483 Err(format!(
3484 "recovery driver proof is no longer live at {boundary}"
3485 ))
3486 }
3487 }
3488
3489 async fn require_recovery_driver_proof_control(
3490 &self,
3491 round: &RecoveryRound,
3492 boundary: &str,
3493 ) -> Result<(), RecoveryControlError> {
3494 if round.id.driver != self.instance_id || !self.proof_is_live(&round.leader_proof) {
3495 return Err(RecoveryControlError::Superseded(format!(
3496 "recovery driver proof is no longer live at {boundary}"
3497 )));
3498 }
3499 let authority = self
3500 .checkpoint_authority()
3501 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?;
3502 let current = authority.load().await.map_err(|error| match error {
3503 super::LeaseError::Io(reason) => RecoveryControlError::Uncertain(reason),
3504 error => RecoveryControlError::Conflict(error.to_string()),
3505 })?;
3506 if !current.is_some_and(|lease| lease.matches_proof(&round.leader_proof)) {
3507 return Err(RecoveryControlError::Superseded(format!(
3508 "durable recovery driver proof changed at {boundary}"
3509 )));
3510 }
3511 Ok(())
3512 }
3513
3514 async fn recovery_evidence_roster_matches(
3515 &self,
3516 round: &RecoveryRound,
3517 ) -> Result<bool, String> {
3518 let candidates = round
3519 .faults
3520 .iter()
3521 .filter(|fault| !round.assignment_fence.contains(fault.reporter.0))
3522 .map(|fault| fault.reporter.0)
3523 .collect::<Vec<_>>();
3524 let available = if candidates.is_empty() {
3525 Vec::new()
3526 } else {
3527 self.available_recovery_participant_incarnations(&candidates)
3528 .await?
3529 };
3530 Ok(available == round.evidence_participants)
3531 }
3532
3533 pub async fn announce_recover_prepare(&self, round: &RecoveryRound) -> Result<(), String> {
3538 round.validate()?;
3539 if round.id.driver != self.instance_id {
3540 return Err("only the current leader may prepare its recovery round".into());
3541 }
3542 self.require_recovery_driver_proof(round, "Prepare preflight")
3543 .await?;
3544 if !self.recovery_evidence_roster_matches(round).await? {
3545 return Err("available recovery evidence roster changed before Prepare".into());
3546 }
3547 if !self
3548 .recovery_stopped_incarnations_match(round)
3549 .await
3550 .map_err(|error| error.to_string())?
3551 {
3552 return Err("recovery stopped-process roster changed before Prepare".into());
3553 }
3554 let _guard = self.recovery_writes.lock().await;
3555 self.require_recovery_driver_proof(round, "Prepare publication")
3556 .await?;
3557 if !self.recovery_evidence_roster_matches(round).await? {
3558 return Err("available recovery evidence roster changed during Prepare".into());
3559 }
3560 let announcement = RecoveryAnnouncement {
3561 round: round.clone(),
3562 phase: RecoverPhase::Prepare,
3563 };
3564 let encoded = encode_recovery_announcement(&announcement)?;
3565 self.write_recovery_value_exact("control:recover", encoded)
3566 .await?;
3567 self.require_recovery_driver_proof(round, "Prepare read-back")
3568 .await
3569 }
3570
3571 pub async fn announce_recover_start(
3577 &self,
3578 round: &RecoveryRound,
3579 epoch: u64,
3580 ) -> Result<(), String> {
3581 round.validate()?;
3582 if round.id.driver != self.instance_id {
3583 return Err("only the current leader may start its recovery round".into());
3584 }
3585 self.require_recovery_driver_proof(round, "Start preflight")
3586 .await?;
3587 let _guard = self.recovery_writes.lock().await;
3588 if !self.recovery_incarnations_match(round).await? {
3589 return Err(
3590 "recovery driver or process-incarnation roster changed before Start".into(),
3591 );
3592 }
3593 let current = self
3594 .read_recovery_value(self.instance_id, "control:recover")
3595 .await?
3596 .ok_or_else(|| "recovery Prepare disappeared before Start".to_string())?;
3597 let prepared = parse_recovery_announcement(¤t)?
3598 .ok_or_else(|| "recovery Prepare was cleared before Start".to_string())?;
3599 if prepared.round != *round || prepared.phase != RecoverPhase::Prepare {
3600 return Err("recovery Start does not match the exact active Prepare".into());
3601 }
3602 self.require_recovery_driver_proof(round, "Start publication")
3603 .await?;
3604 let announcement = RecoveryAnnouncement {
3605 round: round.clone(),
3606 phase: RecoverPhase::Start { epoch },
3607 };
3608 let encoded = encode_recovery_announcement(&announcement)?;
3609 self.write_recovery_value_exact("control:recover", encoded)
3610 .await?;
3611 self.require_recovery_driver_proof(round, "Start read-back")
3612 .await
3613 }
3614
3615 pub async fn announce_recover_release(
3621 &self,
3622 round: &RecoveryRound,
3623 epoch: u64,
3624 ) -> Result<(), String> {
3625 round.validate()?;
3626 if round.id.driver != self.instance_id {
3627 return Err("only the current leader may release its recovery round".into());
3628 }
3629 self.require_recovery_driver_proof(round, "Release preflight")
3630 .await?;
3631 let _guard = self.recovery_writes.lock().await;
3632 if !self.recovery_incarnations_match(round).await? {
3633 return Err(
3634 "recovery driver or process-incarnation roster changed before Release".into(),
3635 );
3636 }
3637 let current = self
3638 .read_recovery_value(self.instance_id, "control:recover")
3639 .await?
3640 .ok_or_else(|| "recovery Start disappeared before Release".to_string())?;
3641 let started = parse_recovery_announcement(¤t)?
3642 .ok_or_else(|| "recovery Start was cleared before Release".to_string())?;
3643 if started.round != *round || started.phase != (RecoverPhase::Start { epoch }) {
3644 return Err("recovery Release does not match the exact active Start target".into());
3645 }
3646 self.require_recovery_driver_proof(round, "Release publication")
3647 .await?;
3648 let release = RecoveryAnnouncement {
3649 round: round.clone(),
3650 phase: RecoverPhase::Release { epoch },
3651 };
3652 let encoded = encode_recovery_announcement(&release)?;
3653 self.write_recovery_value_exact("control:recover", encoded)
3654 .await?;
3655 self.require_recovery_driver_proof(round, "Release read-back")
3656 .await
3657 }
3658
3659 pub async fn try_commit_recover_release(
3669 &self,
3670 release: &RecoveryAnnouncement,
3671 ) -> Result<ReleaseCommitStatus, RecoveryControlError> {
3672 release.validate().map_err(RecoveryControlError::Conflict)?;
3673 let RecoverPhase::Release { epoch } = release.phase else {
3674 return Err(RecoveryControlError::Conflict(
3675 "release commit must bind a pending Release target".into(),
3676 ));
3677 };
3678 let round = &release.round;
3679 if round.id.driver != self.instance_id {
3680 return Err(RecoveryControlError::Superseded(
3681 "only the current leader may commit its recovery Release".into(),
3682 ));
3683 }
3684 self.require_recovery_driver_proof_control(round, "Release commit preflight")
3685 .await?;
3686 match self.read_release_ready(release).await? {
3687 ReleaseReadyStatus::Complete => {}
3688 ReleaseReadyStatus::Pending { missing } => {
3689 self.audit_pending_release_faults_control(release).await?;
3690 return Ok(ReleaseCommitStatus::Pending { missing });
3691 }
3692 }
3693 let committed = RecoveryAnnouncement {
3694 round: round.clone(),
3695 phase: RecoverPhase::ReleaseCommitted { epoch },
3696 };
3697 let authority = self
3698 .checkpoint_authority()
3699 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?;
3700 let reference = authority
3701 .stage_recovery_release_terminal(&committed)
3702 .await
3703 .map_err(RecoveryControlError::from_authority)?;
3704
3705 let _guard = self.recovery_writes.lock().await;
3706 if !self.recovery_incarnations_match_control(round).await? {
3707 return Err(RecoveryControlError::Superseded(
3708 "recovery process-incarnation roster changed before Release commit".into(),
3709 ));
3710 }
3711 self.audit_recovery_faults_control(round).await?;
3712 let Some(current) = self
3713 .read_recovery_value(self.instance_id, "control:recover")
3714 .await
3715 .map_err(RecoveryControlError::Uncertain)?
3716 else {
3717 return Err(RecoveryControlError::Superseded(
3718 "pending recovery Release disappeared before commit".into(),
3719 ));
3720 };
3721 let active = match parse_recovery_announcement(¤t) {
3722 Ok(Some(active)) => active,
3723 Ok(None) => {
3724 return Err(RecoveryControlError::Superseded(
3725 "pending recovery Release was cleared before commit".into(),
3726 ));
3727 }
3728 Err(reason) => return Err(RecoveryControlError::Conflict(reason)),
3729 };
3730 if active != *release {
3731 let error = if active.round.id.generation > round.id.generation {
3732 RecoveryControlError::Superseded(
3733 "a newer recovery intent replaced the pending Release".into(),
3734 )
3735 } else {
3736 RecoveryControlError::Conflict(
3737 "recovery Release commit does not match the exact pending intent".into(),
3738 )
3739 };
3740 return Err(error);
3741 }
3742 self.require_recovery_driver_proof_control(round, "Release commit publication")
3743 .await?;
3744 match Box::pin(authority.record_recovery_release_commit(&round.leader_proof, reference))
3745 .await
3746 .map_err(RecoveryControlError::from_authority)?
3747 {
3748 super::leader_lease::RecordRecoveryReleaseCommitResult::Created(_)
3749 | super::leader_lease::RecordRecoveryReleaseCommitResult::Unchanged(_) => {
3750 Ok(ReleaseCommitStatus::Committed {
3751 terminal: committed,
3752 })
3753 }
3754 super::leader_lease::RecordRecoveryReleaseCommitResult::Conflict { winner } => {
3755 if winner.generation() > round.id.generation {
3756 Err(RecoveryControlError::Superseded(format!(
3757 "recovery release generation {} replaced generation {}",
3758 winner.generation(),
3759 round.id.generation
3760 )))
3761 } else {
3762 Err(RecoveryControlError::Conflict(format!(
3763 "recovery release generation {} has a different durable winner",
3764 round.id.generation
3765 )))
3766 }
3767 }
3768 super::leader_lease::RecordRecoveryReleaseCommitResult::FaultsChanged => {
3769 Err(RecoveryControlError::Superseded(
3770 "recovery fault inventory changed at Release authority admission".into(),
3771 ))
3772 }
3773 }
3774 }
3775
3776 pub async fn observe_recover_control(
3781 &self,
3782 ) -> Result<Option<RecoveryAnnouncement>, RecoveryControlError> {
3783 let Some(current_driver) = self.current_leader() else {
3784 return Ok(None);
3785 };
3786 let Some(raw) = self
3787 .read_recovery_value(current_driver, "control:recover")
3788 .await
3789 .map_err(RecoveryControlError::Uncertain)?
3790 else {
3791 return Ok(None);
3792 };
3793 let Some(announcement) =
3794 parse_recovery_announcement(&raw).map_err(RecoveryControlError::Conflict)?
3795 else {
3796 return Ok(None);
3797 };
3798 if matches!(announcement.phase, RecoverPhase::ReleaseCommitted { .. }) {
3799 return Err(RecoveryControlError::Conflict(
3800 "committed recovery release appeared in the mutable intent slot".into(),
3801 ));
3802 }
3803 if announcement.round.id.driver != current_driver {
3804 return Err(RecoveryControlError::Conflict(format!(
3805 "recovery publisher {current_driver} is not declared driver {}",
3806 announcement.round.id.driver
3807 )));
3808 }
3809 let authority = self
3810 .checkpoint_authority()
3811 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?;
3812 let Some(authority_before) = authority.load().await.map_err(|error| match error {
3813 super::LeaseError::Io(reason) => RecoveryControlError::Uncertain(reason),
3814 error => RecoveryControlError::Conflict(error.to_string()),
3815 })?
3816 else {
3817 return Err(RecoveryControlError::Superseded(
3818 "durable recovery authority has no leader".into(),
3819 ));
3820 };
3821 if !authority_before.matches_proof(&announcement.round.leader_proof) {
3822 return Err(RecoveryControlError::Superseded(format!(
3823 "recovery phase from {current_driver} does not match durable leader authority"
3824 )));
3825 }
3826 let Some(authority_after) = authority.load().await.map_err(|error| match error {
3827 super::LeaseError::Io(reason) => RecoveryControlError::Uncertain(reason),
3828 error => RecoveryControlError::Conflict(error.to_string()),
3829 })?
3830 else {
3831 return Err(RecoveryControlError::Superseded(
3832 "durable recovery authority vanished during observation".into(),
3833 ));
3834 };
3835 if self.current_leader() != Some(current_driver)
3836 || !authority_after.matches_proof(&announcement.round.leader_proof)
3837 {
3838 return Err(RecoveryControlError::Superseded(format!(
3839 "recovery authority changed while observing {current_driver}"
3840 )));
3841 }
3842 Ok(Some(announcement))
3843 }
3844
3845 pub async fn latest_committed_recover_release(
3851 &self,
3852 ) -> Result<Option<RecoveryAnnouncement>, RecoveryControlError> {
3853 self.checkpoint_authority()
3854 .map_err(|error| RecoveryControlError::Conflict(error.to_string()))?
3855 .latest_recovery_release_terminal()
3856 .await
3857 .map_err(RecoveryControlError::from_authority)
3858 }
3859
3860 pub async fn observe_committed_recover_release(
3867 &self,
3868 round: &RecoveryRound,
3869 epoch: u64,
3870 ) -> Result<Option<RecoveryAnnouncement>, RecoveryControlError> {
3871 round.validate().map_err(RecoveryControlError::Conflict)?;
3872 let expected = RecoveryAnnouncement {
3873 round: round.clone(),
3874 phase: RecoverPhase::ReleaseCommitted { epoch },
3875 };
3876 let Some(terminal) = self.latest_committed_recover_release().await? else {
3877 return Ok(None);
3878 };
3879 match terminal.round.id.generation.cmp(&round.id.generation) {
3880 std::cmp::Ordering::Less => Ok(None),
3881 std::cmp::Ordering::Greater => Err(RecoveryControlError::Superseded(format!(
3882 "recovery release generation {} replaced generation {}",
3883 terminal.round.id.generation, round.id.generation
3884 ))),
3885 std::cmp::Ordering::Equal if terminal == expected => Ok(Some(terminal)),
3886 std::cmp::Ordering::Equal => Err(RecoveryControlError::Conflict(format!(
3887 "committed recovery release generation {} differs from the expected round",
3888 round.id.generation
3889 ))),
3890 }
3891 }
3892
3893 pub async fn retire_committed_recover_release_hint(
3900 &self,
3901 round: &RecoveryRound,
3902 epoch: u64,
3903 ) -> Result<bool, RecoveryControlError> {
3904 round.validate().map_err(RecoveryControlError::Conflict)?;
3905 if round.id.driver != self.instance_id {
3906 return Err(RecoveryControlError::Conflict(
3907 "only the publishing driver may retire its recovery Release hint".into(),
3908 ));
3909 }
3910 if self
3911 .observe_committed_recover_release(round, epoch)
3912 .await?
3913 .is_none()
3914 {
3915 return Ok(false);
3916 }
3917 let pending = RecoveryAnnouncement {
3918 round: round.clone(),
3919 phase: RecoverPhase::Release { epoch },
3920 };
3921 let _guard = self.recovery_writes.lock().await;
3922 let Some(raw) = self
3923 .read_recovery_value(self.instance_id, "control:recover")
3924 .await
3925 .map_err(RecoveryControlError::Uncertain)?
3926 else {
3927 return Ok(false);
3928 };
3929 let active = parse_recovery_announcement(&raw)
3930 .map_err(RecoveryControlError::Conflict)?
3931 .ok_or_else(|| {
3932 RecoveryControlError::Conflict(
3933 "recovery Release hint decoded as an empty announcement".into(),
3934 )
3935 })?;
3936 if active != pending {
3937 return if active.round.id.generation > round.id.generation {
3938 Err(RecoveryControlError::Superseded(
3939 "a newer recovery intent replaced the committed Release hint".into(),
3940 ))
3941 } else {
3942 Err(RecoveryControlError::Conflict(
3943 "mutable recovery intent differs from its committed Release".into(),
3944 ))
3945 };
3946 }
3947 self.write_recovery_value_exact("control:recover", String::new())
3948 .await
3949 .map_err(RecoveryControlError::Uncertain)?;
3950 Ok(true)
3951 }
3952
3953 pub async fn observe_recover(&self) -> Result<Option<RecoveryAnnouncement>, String> {
3958 self.observe_recover_control()
3959 .await
3960 .map_err(|error| error.to_string())
3961 }
3962
3963 #[must_use]
3965 pub fn recovery_driver_is_current(&self, round: &RecoveryRound) -> bool {
3966 self.current_leader() == Some(round.id.driver)
3967 }
3968
3969 #[must_use]
3971 pub fn recovery_round_contains_current_process(&self, round: &RecoveryRound) -> bool {
3972 round.owner_incarnation(self.instance_id) == Some(self.recovery_incarnation)
3973 }
3974
3975 #[must_use]
3977 pub fn recovery_round_requires_current_process_stop(&self, round: &RecoveryRound) -> bool {
3978 round.stopped_participant_incarnation(self.instance_id) == Some(self.recovery_incarnation)
3979 }
3980
3981 pub async fn announce_stopped(
3986 &self,
3987 round: &RecoveryRound,
3988 prepared_witnesses: Vec<PreparedCheckpointWitness>,
3989 ) -> Result<(), String> {
3990 round.validate()?;
3991 if !round.contains_stopped_participant(self.instance_id) {
3992 return Err("node outside recovery stopped roster cannot acknowledge Prepare".into());
3993 }
3994 if !self.recovery_round_requires_current_process_stop(round) {
3995 return Err("Prepare acknowledgement has a stale local process incarnation".into());
3996 }
3997 if !self.recovery_incarnation_is_current().await? {
3998 return Err("Prepare acknowledgement came from a superseded local process".into());
3999 }
4000 let report = RecoveryStoppedReport::new(
4001 round,
4002 CheckpointParticipant {
4003 node_id: self.instance_id.0,
4004 boot_incarnation: self.recovery_incarnation,
4005 },
4006 prepared_witnesses,
4007 )?;
4008 let encoded = encode_recovery_stopped_report(&report, round)?;
4009 self.write_recovery_value_exact(RECOVERY_STOPPED_REPORT_KEY, encoded)
4010 .await
4011 }
4012
4013 pub async fn read_stopped(
4019 &self,
4020 round: &RecoveryRound,
4021 participants: &[NodeId],
4022 ) -> Result<Vec<RecoveryStoppedReport>, RecoveryControlError> {
4023 if participants.windows(2).any(|pair| pair[0].0 >= pair[1].0)
4024 || participants.iter().any(NodeId::is_unassigned)
4025 {
4026 return Err(RecoveryControlError::Conflict(
4027 "recovery stopped-report subset requires canonical participants".into(),
4028 ));
4029 }
4030 let roster = participants
4031 .iter()
4032 .map(|node| {
4033 round
4034 .stopped_participant_incarnation(*node)
4035 .map(|boot_incarnation| CheckpointParticipant {
4036 node_id: node.0,
4037 boot_incarnation,
4038 })
4039 .ok_or_else(|| {
4040 RecoveryControlError::Conflict(format!(
4041 "node {node} is outside the recovery stopped roster"
4042 ))
4043 })
4044 })
4045 .collect::<Result<Vec<_>, _>>()?;
4046 self.read_recovery_stopped_reports(round, &roster).await
4047 }
4048
4049 pub async fn clear_recover(&self, round: &RecoveryRound) -> Result<bool, String> {
4055 let _guard = self.recovery_writes.lock().await;
4056 let Some(raw) = self
4057 .read_recovery_value(self.instance_id, "control:recover")
4058 .await?
4059 else {
4060 return Ok(false);
4061 };
4062 let Some(active) = parse_recovery_announcement(&raw)? else {
4063 return Ok(false);
4064 };
4065 if active.round != *round {
4066 return Ok(false);
4067 }
4068 if matches!(active.phase, RecoverPhase::Release { .. }) {
4069 return Ok(false);
4070 }
4071 self.write_recovery_value_exact("control:recover", String::new())
4072 .await?;
4073 Ok(true)
4074 }
4075
4076 #[cfg(feature = "cluster")]
4086 pub async fn wait_for_barrier<F>(
4087 &self,
4088 mut pred: F,
4089 timeout: Duration,
4090 ) -> Result<Option<BarrierAnnouncement>, String>
4091 where
4092 F: FnMut(&BarrierAnnouncement) -> bool,
4093 {
4094 let mut watch = self.barrier.announcement_watch();
4095 let poll_for = |watch: &Option<_>| {
4098 if watch.is_some() {
4099 Duration::from_millis(250)
4100 } else {
4101 Duration::from_millis(25)
4102 }
4103 };
4104 let deadline = tokio::time::Instant::now() + timeout;
4105 loop {
4106 if let Some(ann) = self.observe_barrier_matching(&mut pred).await? {
4107 return Ok(Some(ann));
4108 }
4109 if tokio::time::Instant::now() >= deadline {
4110 return Ok(None);
4111 }
4112 let poll = poll_for(&watch);
4113 let pushed = async {
4114 match watch.as_mut() {
4115 Some(w) => w.changed().await.is_ok(),
4116 None => std::future::pending().await,
4117 }
4118 };
4119 tokio::select! {
4120 ok = pushed => {
4121 if !ok {
4122 watch = None;
4125 }
4126 }
4127 () = tokio::time::sleep(poll) => {}
4128 () = tokio::time::sleep_until(deadline) => return Ok(None),
4129 }
4130 }
4131 }
4132
4133 pub async fn wait_for_quorum(
4135 &self,
4136 prepare: &BarrierAnnouncement,
4137 expected: &[NodeId],
4138 deadline: Duration,
4139 ) -> QuorumOutcome {
4140 self.barrier
4141 .wait_for_quorum(prepare, expected, deadline)
4142 .await
4143 }
4144
4145 #[must_use]
4147 pub fn snapshot_store(&self) -> Option<&AssignmentSnapshotStore> {
4148 self.snapshot.as_deref()
4149 }
4150}
4151
4152#[cfg(test)]
4153mod tests;