Skip to main content

laminar_core/cluster/control/controller/
construction.rs

1//! Controller construction and committed-progress publication.
2
3use super::*;
4
5impl ClusterController {
6    /// Wrap the given primitives.
7    #[must_use]
8    pub fn new(
9        instance_id: NodeId,
10        kv: Arc<dyn ClusterKv>,
11        snapshot: Option<Arc<AssignmentSnapshotStore>>,
12        members_rx: watch::Receiver<Vec<NodeInfo>>,
13    ) -> Self {
14        Self::new_with_recovery_kv(instance_id, Arc::clone(&kv), kv, snapshot, members_rx)
15    }
16
17    /// Wrap the given primitives with a separate durable recovery authority.
18    ///
19    /// The barrier/discovery KV remains the low-latency path. `recovery_kv` owns recovery
20    /// incarnations, generations, phase announcements, and acknowledgements.
21    #[must_use]
22    pub fn new_with_recovery_kv(
23        instance_id: NodeId,
24        kv: Arc<dyn ClusterKv>,
25        recovery_kv: Arc<dyn ClusterKv>,
26        snapshot: Option<Arc<AssignmentSnapshotStore>>,
27        members_rx: watch::Receiver<Vec<NodeInfo>>,
28    ) -> Self {
29        Self::new_with_recovery_incarnation(
30            instance_id,
31            kv,
32            recovery_kv,
33            snapshot,
34            members_rx,
35            Uuid::new_v4(),
36        )
37    }
38
39    /// Wrap the control primitives with a caller-generated boot identity.
40    #[must_use]
41    pub fn new_with_recovery_incarnation(
42        instance_id: NodeId,
43        kv: Arc<dyn ClusterKv>,
44        recovery_kv: Arc<dyn ClusterKv>,
45        snapshot: Option<Arc<AssignmentSnapshotStore>>,
46        members_rx: watch::Receiver<Vec<NodeInfo>>,
47        recovery_incarnation: Uuid,
48    ) -> Self {
49        let leader_eligible = Arc::new(AtomicBool::new(true));
50        let mut barrier = BarrierCoordinator::new(Arc::clone(&kv));
51        #[cfg(feature = "cluster")]
52        barrier.set_leader_election(
53            instance_id,
54            members_rx.clone(),
55            Arc::clone(&leader_eligible),
56        );
57        let controller = Self {
58            instance_id,
59            barrier,
60            kv,
61            recovery_kv,
62            snapshot,
63            members_rx,
64            // A new leader must not checkpoint until it proves exact assignment convergence.
65            checkpoint_assignment_fence: watch::channel(None).0,
66            checkpoint_drain_transition: watch::channel(None).0,
67            process_authority_transition: parking_lot::Mutex::new(()),
68            recovery_incarnation,
69            recovery_process_term: AtomicU64::new(0),
70            recovery_fault_request_sequence: AtomicU64::new(1),
71            cluster_min_watermark: Arc::new(AtomicI64::new(i64::MIN)),
72            committed_source_watermarks: parking_lot::RwLock::new(Arc::new(
73                rustc_hash::FxHashMap::default(),
74            )),
75            committed_watermark_publication: parking_lot::Mutex::new(()),
76            draining: Arc::new(AtomicBool::new(false)),
77            recovering: Arc::new(AtomicBool::new(false)),
78            active: Arc::new(AtomicBool::new(true)),
79            process_lease_live: Arc::new(AtomicBool::new(true)),
80            process_lease_deadline: std::sync::OnceLock::new(),
81            process_lease_authority: std::sync::OnceLock::new(),
82            leader_eligible,
83            leader_candidacy: watch::channel(super::super::LeaderCandidacy::initial(false)).0,
84            leadership_participants: parking_lot::RwLock::new(None),
85            recovery_writes: tokio::sync::Mutex::new(()),
86            pending_release_fault_audit: tokio::sync::Mutex::new(None),
87            unresponsive: Arc::new(parking_lot::Mutex::new(rustc_hash::FxHashMap::default())),
88            self_locality: parking_lot::RwLock::new(Locality::default()),
89            #[cfg(feature = "cluster")]
90            leader_lease: std::sync::OnceLock::new(),
91        };
92        controller.notify_leader_eligibility_change();
93        controller
94    }
95
96    /// Install the durable authority used to validate clustered checkpoint barriers.
97    #[cfg(feature = "cluster")]
98    pub fn set_leader_lease_store(&self, store: Arc<super::super::LeaderLeaseStore>) {
99        self.barrier.set_leader_lease_store(store);
100    }
101
102    /// Serve only leader proofs that remain live on this process's monotonic lease gate.
103    #[cfg(feature = "cluster")]
104    pub fn install_local_leader_proof_provider(self: &Arc<Self>) {
105        let controller = Arc::downgrade(self);
106        self.barrier
107            .set_local_leader_proof_provider(Arc::new(move || {
108                controller
109                    .upgrade()
110                    .and_then(|controller| controller.capture_leader_proof())
111            }));
112    }
113
114    /// Exact durable authority installed for this cluster controller.
115    ///
116    /// Cluster checkpoint code must use this handle rather than standalone outcome keys.
117    ///
118    /// # Errors
119    /// Returns [`super::super::ClusterCheckpointAuthorityError::NotConfigured`] when no authority exists.
120    #[cfg(feature = "cluster")]
121    pub fn checkpoint_authority(
122        &self,
123    ) -> Result<Arc<super::super::LeaderLeaseStore>, super::super::ClusterCheckpointAuthorityError>
124    {
125        self.barrier.checkpoint_authority()
126    }
127
128    /// Latest recovery-safe cluster watermark installed from an immutable Commit outcome or a
129    /// validated committed checkpoint.
130    #[must_use]
131    pub fn cluster_min_watermark(&self) -> Option<i64> {
132        let v = self.cluster_min_watermark.load(Ordering::Acquire);
133        if v == i64::MIN {
134            None
135        } else {
136            Some(v)
137        }
138    }
139
140    /// Mirror the leader's computed cluster-min watermark into the atomic so its own operators
141    /// match followers. Monotonic — never lowers the published value.
142    pub fn publish_cluster_min_watermark(&self, wm: i64) {
143        let _publication = self.committed_watermark_publication.lock();
144        self.publish_cluster_min_watermark_locked(wm);
145    }
146
147    pub(super) fn publish_cluster_min_watermark_locked(&self, wm: i64) {
148        let mut cur = self.cluster_min_watermark.load(Ordering::Acquire);
149        while wm > cur {
150            match self.cluster_min_watermark.compare_exchange(
151                cur,
152                wm,
153                Ordering::AcqRel,
154                Ordering::Acquire,
155            ) {
156                Ok(_) => break,
157                Err(observed) => cur = observed,
158            }
159        }
160    }
161
162    /// Pin one internally consistent source-frontier snapshot for a compute cycle.
163    #[must_use]
164    pub fn committed_source_watermarks_snapshot(&self) -> Arc<rustc_hash::FxHashMap<String, i64>> {
165        let snapshot = self.committed_source_watermarks.read();
166        Arc::clone(&snapshot)
167    }
168
169    /// Install both scalar and source-keyed frontiers from one immutable committed channel cut.
170    ///
171    /// Publication is monotonic per source, matching [`Self::publish_cluster_min_watermark`]. A
172    /// source withheld by an active uninitialized channel remains unpublished until a later
173    /// committed cut initializes it.
174    ///
175    /// # Errors
176    /// Returns an error when the committed channel cut contains an invalid watermark sentinel.
177    pub fn publish_committed_channel_progress(
178        &self,
179        channels: &[crate::checkpoint::ChannelProgress],
180    ) -> Result<(), String> {
181        let source_watermarks = crate::checkpoint::channel_progress_frontiers_by_source(channels)?
182            .into_iter()
183            .filter_map(|(source, frontier)| frontier.map(|frontier| (source.to_owned(), frontier)))
184            .collect::<std::collections::BTreeMap<_, _>>();
185        self.publish_committed_checkpoint_progress(channels, &source_watermarks)
186    }
187
188    /// Install scalar and source-keyed frontiers from one committed checkpoint index.
189    ///
190    /// The explicit source map retains decisions for a source whose physical channel inventory is
191    /// empty in this cut. Publication remains monotonic so an older observer cannot regress a
192    /// newer installed decision.
193    ///
194    /// # Errors
195    /// Returns an error when channel progress or an explicit source watermark is invalid or the
196    /// explicit map disagrees with an initialized channel cut.
197    pub fn publish_committed_checkpoint_progress(
198        &self,
199        channels: &[crate::checkpoint::ChannelProgress],
200        source_watermarks: &std::collections::BTreeMap<String, i64>,
201    ) -> Result<(), String> {
202        let cluster_min = crate::checkpoint::channel_progress_frontier(channels)?;
203        let current = crate::checkpoint::channel_progress_frontiers_by_source(channels)?;
204        if source_watermarks
205            .values()
206            .any(|watermark| *watermark == i64::MIN)
207        {
208            return Err("committed source watermark uses the reserved uninitialized value".into());
209        }
210        for (source, frontier) in current {
211            let Some(frontier) = frontier else {
212                continue;
213            };
214            if source_watermarks.get(source) != Some(&frontier) {
215                return Err(format!(
216                    "committed source watermark '{source}' disagrees with channel progress"
217                ));
218            }
219        }
220        let _publication = self.committed_watermark_publication.lock();
221        {
222            let mut published = self.committed_source_watermarks.write();
223            let published = Arc::make_mut(&mut *published);
224            for (source_name, frontier) in source_watermarks {
225                published
226                    .entry(source_name.clone())
227                    .and_modify(|current| *current = (*current).max(*frontier))
228                    .or_insert(*frontier);
229            }
230        }
231        if let Some(cluster_min) = cluster_min {
232            self.publish_cluster_min_watermark_locked(cluster_min);
233        }
234        Ok(())
235    }
236
237    /// Replace the process-local committed frontier snapshot with one recovered exact cut.
238    ///
239    /// Unlike live publication, recovery may deliberately rewind to an older checkpoint or to
240    /// genesis. The caller must hold the pipeline recovery/intake fence; this method serialises
241    /// against any in-flight live publication and atomically establishes the new logical baseline
242    /// before intake is reopened.
243    ///
244    /// # Errors
245    /// Returns an error when channel progress or an explicit source watermark is invalid or the
246    /// explicit map disagrees with an initialized channel cut.
247    pub fn replace_recovered_checkpoint_progress(
248        &self,
249        channels: &[crate::checkpoint::ChannelProgress],
250        source_watermarks: &std::collections::BTreeMap<String, i64>,
251    ) -> Result<(), String> {
252        let cluster_min = crate::checkpoint::channel_progress_frontier(channels)?;
253        let current = crate::checkpoint::channel_progress_frontiers_by_source(channels)?;
254        if source_watermarks
255            .values()
256            .any(|watermark| *watermark == i64::MIN)
257        {
258            return Err("committed source watermark uses the reserved uninitialized value".into());
259        }
260        for (source, frontier) in current {
261            let Some(frontier) = frontier else {
262                continue;
263            };
264            if source_watermarks.get(source) != Some(&frontier) {
265                return Err(format!(
266                    "committed source watermark '{source}' disagrees with channel progress"
267                ));
268            }
269        }
270
271        let replacement = source_watermarks
272            .iter()
273            .map(|(source, watermark)| (source.clone(), *watermark))
274            .collect::<rustc_hash::FxHashMap<_, _>>();
275        let _publication = self.committed_watermark_publication.lock();
276        *self.committed_source_watermarks.write() = Arc::new(replacement);
277        self.cluster_min_watermark
278            .store(cluster_min.unwrap_or(i64::MIN), Ordering::Release);
279        Ok(())
280    }
281}