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, MAX_CHECKPOINT_PARTICIPANTS,
21};
22use crate::cluster::discovery::{assignable_node_ids, NodeId, NodeInfo, NodeState};
23use crate::state::Locality;
24
25const RECOVERY_INCARNATION_KEY: &str = "control:recovery-incarnation";
26const ADOPTED_ASSIGNMENT_KEY: &str = "control:adopted-assignment";
27const DRAIN_ACK_KEY: &str = "control:drain-ack";
28const DRAIN_ACK_PROTOCOL_VERSION: u16 = 1;
29const RELEASE_READY_ACK_KEY: &str = "control:recovery-release-ready";
30const RELEASE_READY_PROTOCOL_VERSION: u16 = 2;
31const RECOVERY_STOPPED_REPORT_KEY: &str = "control:recovery-stopped";
32const RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION: u16 = 4;
34const MAX_RECOVERY_STOPPED_REPORT_BYTES: usize = 1_024;
36pub(super) const MAX_RECOVERY_ANNOUNCEMENT_BYTES: usize = 256 * 1_024;
38const RECOVERY_CONTROL_IO_TIMEOUT: Duration = Duration::from_secs(5);
39const MAX_ADOPTED_ASSIGNMENT_BYTES: usize = 1_024;
40const MAX_DRAIN_ACK_BYTES: usize = 1_024;
41const MAX_RELEASE_READY_ACK_BYTES: usize = 1_024;
42const CONTROL_ROSTER_IO_CONCURRENCY: usize = 32;
43const PENDING_RELEASE_FAULT_AUDIT_INTERVAL: Duration = Duration::from_secs(1);
44#[cfg(feature = "cluster")]
45const CHECKPOINT_PREPARE_OBSERVATION_TIMEOUT: Duration = Duration::from_secs(30);
46
47#[cfg(feature = "cluster")]
48struct LeaderLeaseGate {
49 lease: watch::Receiver<Option<super::LeaderLease>>,
50 owner: super::LeaderLeaseOwner,
51 deadline: watch::Receiver<Arc<super::LeaseDeadline>>,
52}
53
54#[cfg(feature = "cluster")]
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum CheckpointPrepareObservation {
58 AssignmentReady(BarrierAnnouncement),
60 AssignmentRejected {
62 announcement: BarrierAnnouncement,
64 error: String,
66 },
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
71pub struct RecoveryRoundId {
72 pub generation: u64,
74 pub nonce: Uuid,
76 pub driver: NodeId,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct RecoveryRound {
84 pub id: RecoveryRoundId,
86 pub leader_proof: LeaderProof,
88 pub assignment_fence: CheckpointAssignmentFence,
90 pub evidence_participants: Vec<CheckpointParticipant>,
95 fault_revision: u64,
97 pub faults: Vec<RecoveryFault>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 topology: Option<Box<super::TopologyRecoveryBinding>>,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct RecoveryFault {
108 pub reporter: NodeId,
110 pub sequence: u64,
112 #[serde(
114 default,
115 skip_serializing_if = "RecoveryFaultDisposition::is_recoverable"
116 )]
117 pub disposition: RecoveryFaultDisposition,
118}
119
120#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
122#[serde(rename_all = "snake_case")]
123pub enum RecoveryFaultDisposition {
124 #[default]
126 Recoverable,
127 Terminal,
130}
131
132impl RecoveryFaultDisposition {
133 #[must_use]
135 pub const fn is_recoverable(&self) -> bool {
136 matches!(self, Self::Recoverable)
137 }
138}
139
140impl RecoveryFault {
141 #[must_use]
143 pub const fn is_terminal(self) -> bool {
144 matches!(self.disposition, RecoveryFaultDisposition::Terminal)
145 }
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
149#[serde(deny_unknown_fields)]
150pub(crate) struct RecoveryFaultPublisher {
151 pub(crate) participant: CheckpointParticipant,
152 pub(crate) process_term: u64,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct RecoveryFaultRequest {
158 sequence: std::num::NonZeroU64,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum RecoveryFaultReportOutcome {
164 Active,
166 AlreadyCleared,
168 CoveredByNewerRequest,
170 TerminalFenceActive,
173}
174
175impl RecoveryFaultRequest {
176 #[must_use]
178 pub const fn sequence(self) -> u64 {
179 self.sequence.get()
180 }
181}
182
183impl RecoveryFaultPublisher {
184 pub(crate) fn validate(&self) -> Result<(), String> {
185 if self.participant.node_id == 0
186 || self.participant.boot_incarnation.is_nil()
187 || self.process_term == 0
188 {
189 return Err("recovery fault publisher identity is not canonical".into());
190 }
191 Ok(())
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct RecoveryFaultInventory {
198 pub(crate) revision: u64,
199 pub(crate) faults: Vec<RecoveryFault>,
200}
201
202impl RecoveryFaultInventory {
203 #[must_use]
205 pub const fn revision(&self) -> u64 {
206 self.revision
207 }
208
209 #[must_use]
211 pub fn faults(&self) -> &[RecoveryFault] {
212 &self.faults
213 }
214
215 #[must_use]
217 pub fn has_terminal_fault(&self) -> bool {
218 self.faults.iter().copied().any(RecoveryFault::is_terminal)
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct RecoveryAdmissionSnapshot {
227 pub(crate) committed_release: Option<RecoveryAnnouncement>,
228 pub(crate) fault_inventory: RecoveryFaultInventory,
229 pub(crate) authority_sequence: u64,
230 pub(crate) release_head: Option<super::leader_lease::RecoveryReleaseLink>,
231 pub(crate) topology: Option<super::TopologyCommit>,
232}
233
234impl RecoveryAdmissionSnapshot {
235 #[must_use]
237 pub const fn topology_commit(&self) -> Option<&super::TopologyCommit> {
238 self.topology.as_ref()
239 }
240 #[must_use]
242 pub fn committed_release(&self) -> Option<&RecoveryAnnouncement> {
243 self.committed_release.as_ref()
244 }
245
246 #[must_use]
248 pub const fn fault_inventory(&self) -> &RecoveryFaultInventory {
249 &self.fault_inventory
250 }
251}
252
253impl RecoveryRound {
254 pub fn new(
259 generation: u64,
260 leader_proof: LeaderProof,
261 assignment_fence: CheckpointAssignmentFence,
262 evidence_participants: Vec<CheckpointParticipant>,
263 fault_revision: u64,
264 faults: Vec<RecoveryFault>,
265 ) -> Result<Self, String> {
266 let driver = NodeId(leader_proof.owner.node_id);
267 let round = Self {
268 id: RecoveryRoundId {
269 generation,
270 nonce: Uuid::new_v4(),
271 driver,
272 },
273 leader_proof,
274 assignment_fence,
275 evidence_participants,
276 fault_revision,
277 faults,
278 topology: None,
279 };
280 round.validate()?;
281 Ok(round)
282 }
283
284 pub(super) fn bind_topology(
285 mut self,
286 binding: Option<super::TopologyRecoveryBinding>,
287 ) -> Result<Self, String> {
288 self.topology = binding.map(Box::new);
289 self.validate()?;
290 Ok(self)
291 }
292
293 #[must_use]
295 pub fn topology_binding(&self) -> Option<&super::TopologyRecoveryBinding> {
296 self.topology.as_deref()
297 }
298
299 #[must_use]
301 pub fn contains_owner(&self, node: NodeId) -> bool {
302 self.assignment_fence.contains(node.0)
303 }
304
305 #[must_use]
307 pub fn owners(&self) -> Vec<NodeId> {
308 self.assignment_fence
309 .participants
310 .iter()
311 .map(|participant| NodeId(participant.node_id))
312 .collect()
313 }
314
315 #[must_use]
317 pub fn has_terminal_fault(&self) -> bool {
318 self.faults.iter().copied().any(RecoveryFault::is_terminal)
319 }
320
321 #[must_use]
323 pub(crate) fn owner_incarnation(&self, node: NodeId) -> Option<Uuid> {
324 self.assignment_fence.participant_incarnation(node.0)
325 }
326
327 #[must_use]
329 pub fn contains_stopped_participant(&self, node: NodeId) -> bool {
330 self.contains_owner(node)
331 || self
332 .evidence_participants
333 .binary_search_by_key(&node.0, |participant| participant.node_id)
334 .is_ok()
335 }
336
337 #[must_use]
339 pub fn stopped_participants(&self) -> Vec<NodeId> {
340 self.stopped_roster()
341 .into_iter()
342 .map(|participant| NodeId(participant.node_id))
343 .collect()
344 }
345
346 #[must_use]
348 pub(crate) fn stopped_participant_incarnation(&self, node: NodeId) -> Option<Uuid> {
349 self.assignment_fence
350 .participant_incarnation(node.0)
351 .or_else(|| {
352 self.evidence_participants
353 .binary_search_by_key(&node.0, |participant| participant.node_id)
354 .ok()
355 .map(|index| self.evidence_participants[index].boot_incarnation)
356 })
357 }
358
359 fn stopped_roster(&self) -> Vec<CheckpointParticipant> {
360 let mut roster = Vec::with_capacity(
361 self.assignment_fence.participants.len() + self.evidence_participants.len(),
362 );
363 roster.extend(self.assignment_fence.participants.iter().copied());
364 roster.extend(self.evidence_participants.iter().copied());
365 roster.sort_unstable_by_key(|participant| participant.node_id);
366 roster
367 }
368
369 #[must_use]
371 pub fn fault_sequence(&self, node: NodeId) -> Option<u64> {
372 self.faults
373 .binary_search_by_key(&node, |fault| fault.reporter)
374 .ok()
375 .map(|index| self.faults[index].sequence)
376 }
377
378 #[must_use]
380 pub const fn fault_revision(&self) -> u64 {
381 self.fault_revision
382 }
383
384 fn validate(&self) -> Result<(), String> {
385 if let Some(binding) = &self.topology {
386 binding.validate().map_err(|error| error.to_string())?;
387 if !binding
388 .processes()
389 .iter()
390 .map(|process| process.participant)
391 .eq(self.assignment_fence.participants.iter().copied())
392 {
393 return Err("topology recovery process terms differ from the complete frozen assignment roster".into());
394 }
395 }
396 if self.id.generation == 0 {
397 return Err("recovery generation must be nonzero".into());
398 }
399 if self.id.nonce.is_nil() {
400 return Err("recovery nonce must be non-nil".into());
401 }
402 if self.id.driver.is_unassigned() {
403 return Err("recovery driver must be assigned".into());
404 }
405 if !self.leader_proof.is_canonical() || self.id.driver.0 != self.leader_proof.owner.node_id
406 {
407 return Err("recovery driver must match a canonical leader proof".into());
408 }
409 if !self.assignment_fence.is_canonical() {
410 return Err("recovery assignment certificate is not canonical".into());
411 }
412 if !self.contains_owner(self.id.driver) {
413 return Err("recovery driver is absent from the frozen quorum".into());
414 }
415 if self.owner_incarnation(self.id.driver) != Some(self.leader_proof.owner.boot_id) {
416 return Err("recovery leader proof is not bound to the frozen driver process".into());
417 }
418 if self.fault_revision == 0 {
419 return Err("recovery fault revision must be nonzero".into());
420 }
421 if self.faults.is_empty()
422 || self.faults.iter().any(|fault| {
423 fault.reporter.is_unassigned()
424 || fault.sequence == 0
425 || fault.sequence > self.fault_revision
426 })
427 || self
428 .faults
429 .windows(2)
430 .any(|pair| pair[0].reporter >= pair[1].reporter)
431 {
432 return Err("recovery fault set is not canonical".into());
433 }
434 if self
435 .evidence_participants
436 .iter()
437 .any(|participant| participant.node_id == 0 || participant.boot_incarnation.is_nil())
438 || self
439 .evidence_participants
440 .windows(2)
441 .any(|pair| pair[0].node_id >= pair[1].node_id)
442 {
443 return Err("recovery evidence participant roster is not canonical".into());
444 }
445 if self.assignment_fence.participants.len() + self.evidence_participants.len()
446 > MAX_CHECKPOINT_PARTICIPANTS
447 {
448 return Err(format!(
449 "recovery stopped roster has {} participants; maximum is {MAX_CHECKPOINT_PARTICIPANTS}",
450 self.assignment_fence.participants.len() + self.evidence_participants.len()
451 ));
452 }
453 if self.evidence_participants.iter().any(|participant| {
454 self.assignment_fence.contains(participant.node_id)
455 || self
456 .faults
457 .binary_search_by_key(&NodeId(participant.node_id), |fault| fault.reporter)
458 .is_err()
459 }) {
460 return Err("recovery evidence participants must be non-owner fault reporters".into());
461 }
462 let largest_terminal = RecoveryAnnouncement {
463 round: self.clone(),
464 phase: RecoverPhase::ReleaseCommitted { epoch: u64::MAX },
465 };
466 let encoded = serde_json::to_vec(&largest_terminal)
467 .map_err(|error| format!("could not encode recovery round: {error}"))?;
468 validate_recovery_announcement_size(encoded.len())?;
469 Ok(())
470 }
471}
472
473#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
475pub enum RecoverPhase {
476 Prepare,
478 Start {
480 epoch: u64,
482 },
483 Release {
486 epoch: u64,
488 },
489 ReleaseCommitted {
491 epoch: u64,
493 },
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
498#[serde(deny_unknown_fields)]
499pub struct RecoveryAnnouncement {
500 pub round: RecoveryRound,
502 pub phase: RecoverPhase,
504}
505
506impl RecoveryAnnouncement {
507 pub(crate) fn validate(&self) -> Result<(), String> {
508 self.round.validate()?;
509 if self.round.has_terminal_fault() && self.phase != RecoverPhase::Prepare {
510 return Err("a terminal recovery fault may only be retained in Prepare".into());
511 }
512 let encoded = serde_json::to_vec(self)
513 .map_err(|error| format!("could not encode recovery announcement: {error}"))?;
514 validate_recovery_announcement_size(encoded.len())
515 }
516}
517
518#[derive(Debug, thiserror::Error, PartialEq, Eq)]
520pub enum LocalProcessAuthorityEvidenceError {
521 #[error("local process authority evidence is unavailable: {0}")]
526 Unavailable(String),
527 #[error("local process authority evidence is invalid: {0}")]
530 Invalid(String),
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
538#[serde(deny_unknown_fields)]
539pub struct LocalProcessAuthorityIdentity {
540 pub participant: CheckpointParticipant,
542 pub process_term: u64,
544}
545
546impl LocalProcessAuthorityIdentity {
547 #[must_use]
549 pub fn is_canonical(self) -> bool {
550 self.participant.node_id != 0
551 && !self.participant.boot_incarnation.is_nil()
552 && self.process_term != 0
553 }
554}
555
556#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct LocalProcessAuthorityEvidence {
563 pub participant: CheckpointParticipant,
565 pub process_term: u64,
567 pub adopted_assignment: CheckpointAssignmentAdoption,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
574#[serde(deny_unknown_fields)]
575pub struct RecoveryStoppedReport {
576 protocol_version: u16,
578 round_id: RecoveryRoundId,
580 round_sha256: String,
582 publisher: CheckpointParticipant,
584}
585
586impl RecoveryStoppedReport {
587 pub fn new(round: &RecoveryRound, publisher: CheckpointParticipant) -> Result<Self, String> {
592 let report = Self {
593 protocol_version: RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION,
594 round_id: round.id,
595 round_sha256: recovery_round_sha256(round)?,
596 publisher,
597 };
598 report.validate(round)?;
599 Ok(report)
600 }
601
602 #[must_use]
604 pub const fn round_id(&self) -> RecoveryRoundId {
605 self.round_id
606 }
607
608 #[must_use]
610 pub const fn publisher(&self) -> CheckpointParticipant {
611 self.publisher
612 }
613
614 pub fn validate(&self, round: &RecoveryRound) -> Result<(), String> {
619 self.validate_semantics(round)?;
620 let encoded = serde_json::to_vec(self)
621 .map_err(|error| format!("could not encode recovery stopped report: {error}"))?;
622 validate_stopped_report_size(encoded.len())
623 }
624
625 fn validate_shape(&self) -> Result<(), String> {
626 if self.protocol_version != RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION {
627 return Err(format!(
628 "unsupported recovery stopped report version {}; expected {RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION}",
629 self.protocol_version
630 ));
631 }
632 if self.round_id.generation == 0
633 || self.round_id.nonce.is_nil()
634 || self.round_id.driver.is_unassigned()
635 {
636 return Err("recovery stopped report round identity is not canonical".into());
637 }
638 if !is_sha256_hex(&self.round_sha256) {
639 return Err("recovery stopped report round digest is not canonical".into());
640 }
641 if self.publisher.node_id == 0 || self.publisher.boot_incarnation.is_nil() {
642 return Err("recovery stopped report publisher is not canonical".into());
643 }
644 Ok(())
645 }
646
647 fn validate_semantics(&self, round: &RecoveryRound) -> Result<(), String> {
648 self.validate_shape()?;
649 round.validate()?;
650 if self.round_id != round.id || self.round_sha256 != recovery_round_sha256(round)? {
651 return Err("recovery stopped report does not match the exact frozen round".into());
652 }
653 if round.stopped_participant_incarnation(NodeId(self.publisher.node_id))
654 != Some(self.publisher.boot_incarnation)
655 {
656 return Err("recovery stopped report publisher does not match the frozen round".into());
657 }
658 Ok(())
659 }
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
663#[serde(deny_unknown_fields)]
664struct RecoveryAnnouncementAck {
665 announcement: RecoveryAnnouncement,
666 incarnation: Uuid,
667}
668
669#[derive(serde::Serialize)]
670struct RecoveryStoppedRoundDigestInput<'a> {
671 protocol: &'static str,
672 round: &'a RecoveryRound,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
676#[serde(deny_unknown_fields)]
677pub(crate) struct RecoveryReleaseId {
678 protocol_version: u16,
679 generation: u64,
680 nonce: Uuid,
681 driver: NodeId,
682 epoch: u64,
683 round_sha256: String,
684}
685
686#[derive(serde::Serialize)]
687struct RecoveryReleaseDigestInput<'a> {
688 protocol: &'static str,
689 round: &'a RecoveryRound,
690 epoch: u64,
691}
692
693impl RecoveryReleaseId {
694 pub(crate) fn for_pending(release: &RecoveryAnnouncement) -> Result<Self, String> {
695 let RecoverPhase::Release { epoch } = release.phase else {
696 return Err("release readiness must bind a pending Release target".into());
697 };
698 release.validate()?;
699 let encoded = serde_json::to_vec(&RecoveryReleaseDigestInput {
700 protocol: "laminardb-recovery-release-v2",
701 round: &release.round,
702 epoch,
703 })
704 .map_err(|error| format!("could not encode recovery release identity: {error}"))?;
705 Ok(Self {
706 protocol_version: RELEASE_READY_PROTOCOL_VERSION,
707 generation: release.round.id.generation,
708 nonce: release.round.id.nonce,
709 driver: release.round.id.driver,
710 epoch,
711 round_sha256: sha256_hex(&encoded),
712 })
713 }
714
715 pub(crate) fn is_canonical(&self) -> bool {
716 self.protocol_version == RELEASE_READY_PROTOCOL_VERSION
717 && self.generation != 0
718 && !self.nonce.is_nil()
719 && !self.driver.is_unassigned()
720 && is_sha256_hex(&self.round_sha256)
721 }
722
723 pub(crate) fn generation(&self) -> u64 {
724 self.generation
725 }
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
729#[serde(deny_unknown_fields)]
730struct RecoveryReleaseReadyAck {
731 release: RecoveryReleaseId,
732 participant: CheckpointParticipant,
733}
734
735impl RecoveryReleaseReadyAck {
736 fn is_canonical(&self) -> bool {
737 self.release.is_canonical()
738 && self.participant.node_id != 0
739 && !self.participant.boot_incarnation.is_nil()
740 }
741}
742
743#[derive(Debug, thiserror::Error, PartialEq, Eq)]
745pub enum RecoveryControlError {
746 #[error("recovery control outcome is uncertain: {0}")]
748 Uncertain(String),
749 #[error("recovery control conflict: {0}")]
751 Conflict(String),
752 #[error("recovery round was superseded: {0}")]
754 Superseded(String),
755}
756
757impl RecoveryControlError {
758 #[cfg(feature = "cluster")]
759 fn from_authority(error: super::ClusterCheckpointAuthorityError) -> Self {
760 match error {
761 super::ClusterCheckpointAuthorityError::Authority(super::LeaseError::Io(reason)) => {
762 Self::Uncertain(reason)
763 }
764 super::ClusterCheckpointAuthorityError::Fenced => {
765 Self::Superseded("durable leader authority changed".into())
766 }
767 error => Self::Conflict(error.to_string()),
768 }
769 }
770
771 #[cfg(feature = "cluster")]
772 fn from_process_authority(error: super::ProcessLeaseError) -> Self {
773 match error {
774 super::ProcessLeaseError::Io(reason) | super::ProcessLeaseError::Deadline(reason) => {
775 Self::Uncertain(reason)
776 }
777 super::ProcessLeaseError::Invalid(reason) => Self::Conflict(reason),
778 super::ProcessLeaseError::Json(error) => Self::Conflict(error.to_string()),
779 }
780 }
781}
782
783#[derive(Debug, Clone, PartialEq, Eq)]
785enum ReleaseReadyStatus {
786 Pending {
788 missing: Vec<NodeId>,
790 },
791 Complete,
793}
794
795#[derive(Debug, Clone, PartialEq, Eq)]
797pub enum ReleaseCommitStatus {
798 Pending {
800 missing: Vec<NodeId>,
802 },
803 Committed {
805 terminal: RecoveryAnnouncement,
807 },
808}
809
810#[must_use = "keep the release guard until local source intake is opened or definitively fenced"]
813pub struct RecoveryReleaseGuard<'a> {
814 _write_guard: tokio::sync::MutexGuard<'a, ()>,
815}
816
817impl std::fmt::Debug for RecoveryReleaseGuard<'_> {
818 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819 formatter
820 .debug_struct("RecoveryReleaseGuard")
821 .finish_non_exhaustive()
822 }
823}
824
825pub struct ClusterController {
827 instance_id: NodeId,
828 kv: Arc<dyn ClusterKv>,
830 recovery_kv: Arc<dyn ClusterKv>,
833 barrier: BarrierCoordinator,
834 snapshot: Option<Arc<AssignmentSnapshotStore>>,
835 members_rx: watch::Receiver<Vec<NodeInfo>>,
836 checkpoint_assignment_fence: watch::Sender<Option<CheckpointAssignmentFence>>,
839 checkpoint_drain_transition: watch::Sender<Option<AssignmentDrainTransition>>,
842 process_authority_transition: parking_lot::Mutex<()>,
844 recovery_incarnation: Uuid,
846 recovery_process_term: AtomicU64,
848 recovery_fault_request_sequence: AtomicU64,
850 cluster_min_watermark: Arc<AtomicI64>,
853 committed_source_watermarks: parking_lot::RwLock<Arc<rustc_hash::FxHashMap<String, i64>>>,
857 committed_watermark_publication: parking_lot::Mutex<()>,
861 #[cfg(feature = "cluster")]
864 checkpoint_gap_waiters: std::sync::atomic::AtomicUsize,
865 draining: Arc<AtomicBool>,
868 recovering: Arc<AtomicBool>,
871 active: Arc<AtomicBool>,
873 process_lease_live: Arc<AtomicBool>,
875 process_lease_deadline: std::sync::OnceLock<Arc<super::LeaseDeadline>>,
877 process_lease_authority: std::sync::OnceLock<Arc<super::process_lease::ProcessLeaseAuthority>>,
879 leader_eligible: Arc<AtomicBool>,
882 leader_candidacy: watch::Sender<super::LeaderCandidacy>,
885 leadership_participants: parking_lot::RwLock<Option<(u64, Vec<u64>)>>,
889 recovery_writes: tokio::sync::Mutex<()>,
891 pending_release_fault_audit:
894 tokio::sync::Mutex<Option<(RecoveryReleaseId, tokio::time::Instant)>>,
895 unresponsive: Arc<parking_lot::Mutex<rustc_hash::FxHashMap<u64, Option<Uuid>>>>,
899 self_locality: parking_lot::RwLock<Locality>,
901 #[cfg(feature = "cluster")]
904 leader_lease: std::sync::OnceLock<LeaderLeaseGate>,
905}
906
907impl std::fmt::Debug for ClusterController {
908 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
909 f.debug_struct("ClusterController")
910 .field("instance_id", &self.instance_id)
911 .finish_non_exhaustive()
912 }
913}
914
915mod assignment;
916mod authority;
917mod barrier;
918mod construction;
919mod membership;
920mod recovery_faults;
921mod recovery_identity;
922mod recovery_protocol;
923#[cfg(feature = "cluster")]
924mod topology;
925#[cfg(feature = "cluster")]
926pub use topology::PeriodicCheckpointHold;
927mod wire;
928
929use wire::{
930 encode_drain_ack, encode_recovery_announcement, encode_recovery_stopped_report,
931 encode_release_ready_ack, is_sha256_hex, parse_drain_ack, parse_local_adopted_assignment,
932 parse_recovery_announcement, parse_recovery_announcement_ack,
933 parse_recovery_stopped_report_shape, parse_release_ready_ack, recovery_round_sha256,
934 sha256_hex, validate_recovery_announcement_size, validate_stopped_report_size, DrainAck,
935};
936
937#[cfg(test)]
938use wire::parse_recovery_stopped_report;
939
940#[cfg(test)]
941mod tests;