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}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
103#[serde(deny_unknown_fields)]
104pub struct RecoveryFault {
105 pub reporter: NodeId,
107 pub sequence: u64,
109 #[serde(
111 default,
112 skip_serializing_if = "RecoveryFaultDisposition::is_recoverable"
113 )]
114 pub disposition: RecoveryFaultDisposition,
115}
116
117#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum RecoveryFaultDisposition {
121 #[default]
123 Recoverable,
124 Terminal,
127}
128
129impl RecoveryFaultDisposition {
130 #[must_use]
132 pub const fn is_recoverable(&self) -> bool {
133 matches!(self, Self::Recoverable)
134 }
135}
136
137impl RecoveryFault {
138 #[must_use]
140 pub const fn is_terminal(self) -> bool {
141 matches!(self.disposition, RecoveryFaultDisposition::Terminal)
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
146#[serde(deny_unknown_fields)]
147pub(crate) struct RecoveryFaultPublisher {
148 pub(crate) participant: CheckpointParticipant,
149 pub(crate) process_term: u64,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct RecoveryFaultRequest {
155 sequence: std::num::NonZeroU64,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum RecoveryFaultReportOutcome {
161 Active,
163 AlreadyCleared,
165 CoveredByNewerRequest,
167 TerminalFenceActive,
170}
171
172impl RecoveryFaultRequest {
173 #[must_use]
175 pub const fn sequence(self) -> u64 {
176 self.sequence.get()
177 }
178}
179
180impl RecoveryFaultPublisher {
181 pub(crate) fn validate(&self) -> Result<(), String> {
182 if self.participant.node_id == 0
183 || self.participant.boot_incarnation.is_nil()
184 || self.process_term == 0
185 {
186 return Err("recovery fault publisher identity is not canonical".into());
187 }
188 Ok(())
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct RecoveryFaultInventory {
195 pub(crate) revision: u64,
196 pub(crate) faults: Vec<RecoveryFault>,
197}
198
199impl RecoveryFaultInventory {
200 #[must_use]
202 pub const fn revision(&self) -> u64 {
203 self.revision
204 }
205
206 #[must_use]
208 pub fn faults(&self) -> &[RecoveryFault] {
209 &self.faults
210 }
211
212 #[must_use]
214 pub fn has_terminal_fault(&self) -> bool {
215 self.faults.iter().copied().any(RecoveryFault::is_terminal)
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct RecoveryAdmissionSnapshot {
224 pub(crate) committed_release: Option<RecoveryAnnouncement>,
225 pub(crate) fault_inventory: RecoveryFaultInventory,
226 pub(crate) authority_sequence: u64,
227 pub(crate) release_head: Option<super::leader_lease::RecoveryReleaseLink>,
228}
229
230impl RecoveryAdmissionSnapshot {
231 #[must_use]
233 pub fn committed_release(&self) -> Option<&RecoveryAnnouncement> {
234 self.committed_release.as_ref()
235 }
236
237 #[must_use]
239 pub const fn fault_inventory(&self) -> &RecoveryFaultInventory {
240 &self.fault_inventory
241 }
242}
243
244impl RecoveryRound {
245 pub fn new(
250 generation: u64,
251 leader_proof: LeaderProof,
252 assignment_fence: CheckpointAssignmentFence,
253 evidence_participants: Vec<CheckpointParticipant>,
254 fault_revision: u64,
255 faults: Vec<RecoveryFault>,
256 ) -> Result<Self, String> {
257 let driver = NodeId(leader_proof.owner.node_id);
258 let round = Self {
259 id: RecoveryRoundId {
260 generation,
261 nonce: Uuid::new_v4(),
262 driver,
263 },
264 leader_proof,
265 assignment_fence,
266 evidence_participants,
267 fault_revision,
268 faults,
269 };
270 round.validate()?;
271 Ok(round)
272 }
273
274 #[must_use]
276 pub fn contains_owner(&self, node: NodeId) -> bool {
277 self.assignment_fence.contains(node.0)
278 }
279
280 #[must_use]
282 pub fn owners(&self) -> Vec<NodeId> {
283 self.assignment_fence
284 .participants
285 .iter()
286 .map(|participant| NodeId(participant.node_id))
287 .collect()
288 }
289
290 #[must_use]
292 pub fn has_terminal_fault(&self) -> bool {
293 self.faults.iter().copied().any(RecoveryFault::is_terminal)
294 }
295
296 #[must_use]
298 pub(crate) fn owner_incarnation(&self, node: NodeId) -> Option<Uuid> {
299 self.assignment_fence.participant_incarnation(node.0)
300 }
301
302 #[must_use]
304 pub fn contains_stopped_participant(&self, node: NodeId) -> bool {
305 self.contains_owner(node)
306 || self
307 .evidence_participants
308 .binary_search_by_key(&node.0, |participant| participant.node_id)
309 .is_ok()
310 }
311
312 #[must_use]
314 pub fn stopped_participants(&self) -> Vec<NodeId> {
315 self.stopped_roster()
316 .into_iter()
317 .map(|participant| NodeId(participant.node_id))
318 .collect()
319 }
320
321 #[must_use]
323 pub(crate) fn stopped_participant_incarnation(&self, node: NodeId) -> Option<Uuid> {
324 self.assignment_fence
325 .participant_incarnation(node.0)
326 .or_else(|| {
327 self.evidence_participants
328 .binary_search_by_key(&node.0, |participant| participant.node_id)
329 .ok()
330 .map(|index| self.evidence_participants[index].boot_incarnation)
331 })
332 }
333
334 fn stopped_roster(&self) -> Vec<CheckpointParticipant> {
335 let mut roster = Vec::with_capacity(
336 self.assignment_fence.participants.len() + self.evidence_participants.len(),
337 );
338 roster.extend(self.assignment_fence.participants.iter().copied());
339 roster.extend(self.evidence_participants.iter().copied());
340 roster.sort_unstable_by_key(|participant| participant.node_id);
341 roster
342 }
343
344 #[must_use]
346 pub fn fault_sequence(&self, node: NodeId) -> Option<u64> {
347 self.faults
348 .binary_search_by_key(&node, |fault| fault.reporter)
349 .ok()
350 .map(|index| self.faults[index].sequence)
351 }
352
353 #[must_use]
355 pub const fn fault_revision(&self) -> u64 {
356 self.fault_revision
357 }
358
359 fn validate(&self) -> Result<(), String> {
360 if self.id.generation == 0 {
361 return Err("recovery generation must be nonzero".into());
362 }
363 if self.id.nonce.is_nil() {
364 return Err("recovery nonce must be non-nil".into());
365 }
366 if self.id.driver.is_unassigned() {
367 return Err("recovery driver must be assigned".into());
368 }
369 if !self.leader_proof.is_canonical() || self.id.driver.0 != self.leader_proof.owner.node_id
370 {
371 return Err("recovery driver must match a canonical leader proof".into());
372 }
373 if !self.assignment_fence.is_canonical() {
374 return Err("recovery assignment certificate is not canonical".into());
375 }
376 if !self.contains_owner(self.id.driver) {
377 return Err("recovery driver is absent from the frozen quorum".into());
378 }
379 if self.owner_incarnation(self.id.driver) != Some(self.leader_proof.owner.boot_id) {
380 return Err("recovery leader proof is not bound to the frozen driver process".into());
381 }
382 if self.fault_revision == 0 {
383 return Err("recovery fault revision must be nonzero".into());
384 }
385 if self.faults.is_empty()
386 || self.faults.iter().any(|fault| {
387 fault.reporter.is_unassigned()
388 || fault.sequence == 0
389 || fault.sequence > self.fault_revision
390 })
391 || self
392 .faults
393 .windows(2)
394 .any(|pair| pair[0].reporter >= pair[1].reporter)
395 {
396 return Err("recovery fault set is not canonical".into());
397 }
398 if self
399 .evidence_participants
400 .iter()
401 .any(|participant| participant.node_id == 0 || participant.boot_incarnation.is_nil())
402 || self
403 .evidence_participants
404 .windows(2)
405 .any(|pair| pair[0].node_id >= pair[1].node_id)
406 {
407 return Err("recovery evidence participant roster is not canonical".into());
408 }
409 if self.assignment_fence.participants.len() + self.evidence_participants.len()
410 > MAX_CHECKPOINT_PARTICIPANTS
411 {
412 return Err(format!(
413 "recovery stopped roster has {} participants; maximum is {MAX_CHECKPOINT_PARTICIPANTS}",
414 self.assignment_fence.participants.len() + self.evidence_participants.len()
415 ));
416 }
417 if self.evidence_participants.iter().any(|participant| {
418 self.assignment_fence.contains(participant.node_id)
419 || self
420 .faults
421 .binary_search_by_key(&NodeId(participant.node_id), |fault| fault.reporter)
422 .is_err()
423 }) {
424 return Err("recovery evidence participants must be non-owner fault reporters".into());
425 }
426 let largest_terminal = RecoveryAnnouncement {
427 round: self.clone(),
428 phase: RecoverPhase::ReleaseCommitted { epoch: u64::MAX },
429 };
430 let encoded = serde_json::to_vec(&largest_terminal)
431 .map_err(|error| format!("could not encode recovery round: {error}"))?;
432 validate_recovery_announcement_size(encoded.len())?;
433 Ok(())
434 }
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
439pub enum RecoverPhase {
440 Prepare,
442 Start {
444 epoch: u64,
446 },
447 Release {
450 epoch: u64,
452 },
453 ReleaseCommitted {
455 epoch: u64,
457 },
458}
459
460#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
462#[serde(deny_unknown_fields)]
463pub struct RecoveryAnnouncement {
464 pub round: RecoveryRound,
466 pub phase: RecoverPhase,
468}
469
470impl RecoveryAnnouncement {
471 pub(crate) fn validate(&self) -> Result<(), String> {
472 self.round.validate()?;
473 if self.round.has_terminal_fault() && self.phase != RecoverPhase::Prepare {
474 return Err("a terminal recovery fault may only be retained in Prepare".into());
475 }
476 let encoded = serde_json::to_vec(self)
477 .map_err(|error| format!("could not encode recovery announcement: {error}"))?;
478 validate_recovery_announcement_size(encoded.len())
479 }
480}
481
482#[derive(Debug, thiserror::Error, PartialEq, Eq)]
484pub enum LocalProcessAuthorityEvidenceError {
485 #[error("local process authority evidence is unavailable: {0}")]
490 Unavailable(String),
491 #[error("local process authority evidence is invalid: {0}")]
494 Invalid(String),
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
502#[serde(deny_unknown_fields)]
503pub struct LocalProcessAuthorityIdentity {
504 pub participant: CheckpointParticipant,
506 pub process_term: u64,
508}
509
510impl LocalProcessAuthorityIdentity {
511 #[must_use]
513 pub fn is_canonical(self) -> bool {
514 self.participant.node_id != 0
515 && !self.participant.boot_incarnation.is_nil()
516 && self.process_term != 0
517 }
518}
519
520#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct LocalProcessAuthorityEvidence {
527 pub participant: CheckpointParticipant,
529 pub process_term: u64,
531 pub adopted_assignment: CheckpointAssignmentAdoption,
534}
535
536#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
538#[serde(deny_unknown_fields)]
539pub struct RecoveryStoppedReport {
540 protocol_version: u16,
542 round_id: RecoveryRoundId,
544 round_sha256: String,
546 publisher: CheckpointParticipant,
548}
549
550impl RecoveryStoppedReport {
551 pub fn new(round: &RecoveryRound, publisher: CheckpointParticipant) -> Result<Self, String> {
556 let report = Self {
557 protocol_version: RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION,
558 round_id: round.id,
559 round_sha256: recovery_round_sha256(round)?,
560 publisher,
561 };
562 report.validate(round)?;
563 Ok(report)
564 }
565
566 #[must_use]
568 pub const fn round_id(&self) -> RecoveryRoundId {
569 self.round_id
570 }
571
572 #[must_use]
574 pub const fn publisher(&self) -> CheckpointParticipant {
575 self.publisher
576 }
577
578 pub fn validate(&self, round: &RecoveryRound) -> Result<(), String> {
583 self.validate_semantics(round)?;
584 let encoded = serde_json::to_vec(self)
585 .map_err(|error| format!("could not encode recovery stopped report: {error}"))?;
586 validate_stopped_report_size(encoded.len())
587 }
588
589 fn validate_shape(&self) -> Result<(), String> {
590 if self.protocol_version != RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION {
591 return Err(format!(
592 "unsupported recovery stopped report version {}; expected {RECOVERY_STOPPED_REPORT_PROTOCOL_VERSION}",
593 self.protocol_version
594 ));
595 }
596 if self.round_id.generation == 0
597 || self.round_id.nonce.is_nil()
598 || self.round_id.driver.is_unassigned()
599 {
600 return Err("recovery stopped report round identity is not canonical".into());
601 }
602 if !is_sha256_hex(&self.round_sha256) {
603 return Err("recovery stopped report round digest is not canonical".into());
604 }
605 if self.publisher.node_id == 0 || self.publisher.boot_incarnation.is_nil() {
606 return Err("recovery stopped report publisher is not canonical".into());
607 }
608 Ok(())
609 }
610
611 fn validate_semantics(&self, round: &RecoveryRound) -> Result<(), String> {
612 self.validate_shape()?;
613 round.validate()?;
614 if self.round_id != round.id || self.round_sha256 != recovery_round_sha256(round)? {
615 return Err("recovery stopped report does not match the exact frozen round".into());
616 }
617 if round.stopped_participant_incarnation(NodeId(self.publisher.node_id))
618 != Some(self.publisher.boot_incarnation)
619 {
620 return Err("recovery stopped report publisher does not match the frozen round".into());
621 }
622 Ok(())
623 }
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
627#[serde(deny_unknown_fields)]
628struct RecoveryAnnouncementAck {
629 announcement: RecoveryAnnouncement,
630 incarnation: Uuid,
631}
632
633#[derive(serde::Serialize)]
634struct RecoveryStoppedRoundDigestInput<'a> {
635 protocol: &'static str,
636 round: &'a RecoveryRound,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
640#[serde(deny_unknown_fields)]
641pub(crate) struct RecoveryReleaseId {
642 protocol_version: u16,
643 generation: u64,
644 nonce: Uuid,
645 driver: NodeId,
646 epoch: u64,
647 round_sha256: String,
648}
649
650#[derive(serde::Serialize)]
651struct RecoveryReleaseDigestInput<'a> {
652 protocol: &'static str,
653 round: &'a RecoveryRound,
654 epoch: u64,
655}
656
657impl RecoveryReleaseId {
658 pub(crate) fn for_pending(release: &RecoveryAnnouncement) -> Result<Self, String> {
659 let RecoverPhase::Release { epoch } = release.phase else {
660 return Err("release readiness must bind a pending Release target".into());
661 };
662 release.validate()?;
663 let encoded = serde_json::to_vec(&RecoveryReleaseDigestInput {
664 protocol: "laminardb-recovery-release-v2",
665 round: &release.round,
666 epoch,
667 })
668 .map_err(|error| format!("could not encode recovery release identity: {error}"))?;
669 Ok(Self {
670 protocol_version: RELEASE_READY_PROTOCOL_VERSION,
671 generation: release.round.id.generation,
672 nonce: release.round.id.nonce,
673 driver: release.round.id.driver,
674 epoch,
675 round_sha256: sha256_hex(&encoded),
676 })
677 }
678
679 pub(crate) fn is_canonical(&self) -> bool {
680 self.protocol_version == RELEASE_READY_PROTOCOL_VERSION
681 && self.generation != 0
682 && !self.nonce.is_nil()
683 && !self.driver.is_unassigned()
684 && is_sha256_hex(&self.round_sha256)
685 }
686
687 pub(crate) fn generation(&self) -> u64 {
688 self.generation
689 }
690}
691
692#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
693#[serde(deny_unknown_fields)]
694struct RecoveryReleaseReadyAck {
695 release: RecoveryReleaseId,
696 participant: CheckpointParticipant,
697}
698
699impl RecoveryReleaseReadyAck {
700 fn is_canonical(&self) -> bool {
701 self.release.is_canonical()
702 && self.participant.node_id != 0
703 && !self.participant.boot_incarnation.is_nil()
704 }
705}
706
707#[derive(Debug, thiserror::Error, PartialEq, Eq)]
709pub enum RecoveryControlError {
710 #[error("recovery control outcome is uncertain: {0}")]
712 Uncertain(String),
713 #[error("recovery control conflict: {0}")]
715 Conflict(String),
716 #[error("recovery round was superseded: {0}")]
718 Superseded(String),
719}
720
721impl RecoveryControlError {
722 #[cfg(feature = "cluster")]
723 fn from_authority(error: super::ClusterCheckpointAuthorityError) -> Self {
724 match error {
725 super::ClusterCheckpointAuthorityError::Authority(super::LeaseError::Io(reason)) => {
726 Self::Uncertain(reason)
727 }
728 super::ClusterCheckpointAuthorityError::Fenced => {
729 Self::Superseded("durable leader authority changed".into())
730 }
731 error => Self::Conflict(error.to_string()),
732 }
733 }
734
735 #[cfg(feature = "cluster")]
736 fn from_process_authority(error: super::ProcessLeaseError) -> Self {
737 match error {
738 super::ProcessLeaseError::Io(reason) | super::ProcessLeaseError::Deadline(reason) => {
739 Self::Uncertain(reason)
740 }
741 super::ProcessLeaseError::Invalid(reason) => Self::Conflict(reason),
742 super::ProcessLeaseError::Json(error) => Self::Conflict(error.to_string()),
743 }
744 }
745}
746
747#[derive(Debug, Clone, PartialEq, Eq)]
749enum ReleaseReadyStatus {
750 Pending {
752 missing: Vec<NodeId>,
754 },
755 Complete,
757}
758
759#[derive(Debug, Clone, PartialEq, Eq)]
761pub enum ReleaseCommitStatus {
762 Pending {
764 missing: Vec<NodeId>,
766 },
767 Committed {
769 terminal: RecoveryAnnouncement,
771 },
772}
773
774#[must_use = "keep the release guard until local source intake is opened or definitively fenced"]
777pub struct RecoveryReleaseGuard<'a> {
778 _write_guard: tokio::sync::MutexGuard<'a, ()>,
779}
780
781impl std::fmt::Debug for RecoveryReleaseGuard<'_> {
782 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
783 formatter
784 .debug_struct("RecoveryReleaseGuard")
785 .finish_non_exhaustive()
786 }
787}
788
789pub struct ClusterController {
791 instance_id: NodeId,
792 kv: Arc<dyn ClusterKv>,
794 recovery_kv: Arc<dyn ClusterKv>,
797 barrier: BarrierCoordinator,
798 snapshot: Option<Arc<AssignmentSnapshotStore>>,
799 members_rx: watch::Receiver<Vec<NodeInfo>>,
800 checkpoint_assignment_fence: watch::Sender<Option<CheckpointAssignmentFence>>,
803 checkpoint_drain_transition: watch::Sender<Option<AssignmentDrainTransition>>,
806 process_authority_transition: parking_lot::Mutex<()>,
808 recovery_incarnation: Uuid,
810 recovery_process_term: AtomicU64,
812 recovery_fault_request_sequence: AtomicU64,
814 cluster_min_watermark: Arc<AtomicI64>,
817 committed_source_watermarks: parking_lot::RwLock<Arc<rustc_hash::FxHashMap<String, i64>>>,
821 committed_watermark_publication: parking_lot::Mutex<()>,
825 draining: Arc<AtomicBool>,
828 recovering: Arc<AtomicBool>,
831 active: Arc<AtomicBool>,
833 process_lease_live: Arc<AtomicBool>,
835 process_lease_deadline: std::sync::OnceLock<Arc<super::LeaseDeadline>>,
837 process_lease_authority: std::sync::OnceLock<Arc<super::process_lease::ProcessLeaseAuthority>>,
839 leader_eligible: Arc<AtomicBool>,
842 leader_candidacy: watch::Sender<super::LeaderCandidacy>,
845 leadership_participants: parking_lot::RwLock<Option<(u64, Vec<u64>)>>,
849 recovery_writes: tokio::sync::Mutex<()>,
851 pending_release_fault_audit:
854 tokio::sync::Mutex<Option<(RecoveryReleaseId, tokio::time::Instant)>>,
855 unresponsive: Arc<parking_lot::Mutex<rustc_hash::FxHashMap<u64, Option<Uuid>>>>,
859 self_locality: parking_lot::RwLock<Locality>,
861 #[cfg(feature = "cluster")]
864 leader_lease: std::sync::OnceLock<LeaderLeaseGate>,
865}
866
867impl std::fmt::Debug for ClusterController {
868 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
869 f.debug_struct("ClusterController")
870 .field("instance_id", &self.instance_id)
871 .finish_non_exhaustive()
872 }
873}
874
875mod assignment;
876mod authority;
877mod barrier;
878mod construction;
879mod membership;
880mod recovery_faults;
881mod recovery_identity;
882mod recovery_protocol;
883mod wire;
884
885use wire::{
886 encode_drain_ack, encode_recovery_announcement, encode_recovery_stopped_report,
887 encode_release_ready_ack, is_sha256_hex, parse_drain_ack, parse_local_adopted_assignment,
888 parse_recovery_announcement, parse_recovery_announcement_ack,
889 parse_recovery_stopped_report_shape, parse_release_ready_ack, recovery_round_sha256,
890 sha256_hex, validate_recovery_announcement_size, validate_stopped_report_size, DrainAck,
891};
892
893#[cfg(test)]
894use wire::parse_recovery_stopped_report;
895
896#[cfg(test)]
897mod tests;