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            #[cfg(feature = "cluster")]
77            checkpoint_gap_waiters: std::sync::atomic::AtomicUsize::new(0),
78            draining: Arc::new(AtomicBool::new(false)),
79            recovering: Arc::new(AtomicBool::new(false)),
80            active: Arc::new(AtomicBool::new(true)),
81            process_lease_live: Arc::new(AtomicBool::new(true)),
82            process_lease_deadline: std::sync::OnceLock::new(),
83            process_lease_authority: std::sync::OnceLock::new(),
84            leader_eligible,
85            leader_candidacy: watch::channel(super::super::LeaderCandidacy::initial(false)).0,
86            leadership_participants: parking_lot::RwLock::new(None),
87            recovery_writes: tokio::sync::Mutex::new(()),
88            pending_release_fault_audit: tokio::sync::Mutex::new(None),
89            unresponsive: Arc::new(parking_lot::Mutex::new(rustc_hash::FxHashMap::default())),
90            self_locality: parking_lot::RwLock::new(Locality::default()),
91            #[cfg(feature = "cluster")]
92            leader_lease: std::sync::OnceLock::new(),
93        };
94        controller.notify_leader_eligibility_change();
95        controller
96    }
97
98    /// Install the durable authority used to validate clustered checkpoint barriers.
99    #[cfg(feature = "cluster")]
100    pub fn set_leader_lease_store(&self, store: Arc<super::super::LeaderLeaseStore>) {
101        self.barrier.set_leader_lease_store(store);
102    }
103
104    /// Serve only leader proofs that remain live on this process's monotonic lease gate.
105    #[cfg(feature = "cluster")]
106    pub fn install_local_leader_proof_provider(self: &Arc<Self>) {
107        let controller = Arc::downgrade(self);
108        self.barrier
109            .set_local_leader_proof_provider(Arc::new(move || {
110                controller
111                    .upgrade()
112                    .and_then(|controller| controller.capture_leader_proof())
113            }));
114    }
115
116    /// Exact durable authority installed for this cluster controller.
117    ///
118    /// Cluster checkpoint code must use this handle rather than standalone outcome keys.
119    ///
120    /// # Errors
121    /// Returns [`super::super::ClusterCheckpointAuthorityError::NotConfigured`] when no authority exists.
122    #[cfg(feature = "cluster")]
123    pub fn checkpoint_authority(
124        &self,
125    ) -> Result<Arc<super::super::LeaderLeaseStore>, super::super::ClusterCheckpointAuthorityError>
126    {
127        self.barrier.checkpoint_authority()
128    }
129
130    /// Latest recovery-safe cluster watermark installed from an immutable Commit outcome or a
131    /// validated committed checkpoint.
132    #[must_use]
133    pub fn cluster_min_watermark(&self) -> Option<i64> {
134        let v = self.cluster_min_watermark.load(Ordering::Acquire);
135        if v == i64::MIN {
136            None
137        } else {
138            Some(v)
139        }
140    }
141
142    /// Mirror the leader's computed cluster-min watermark into the atomic so its own operators
143    /// match followers. Monotonic — never lowers the published value.
144    pub fn publish_cluster_min_watermark(&self, wm: i64) {
145        let _publication = self.committed_watermark_publication.lock();
146        self.publish_cluster_min_watermark_locked(wm);
147    }
148
149    pub(super) fn publish_cluster_min_watermark_locked(&self, wm: i64) {
150        let mut cur = self.cluster_min_watermark.load(Ordering::Acquire);
151        while wm > cur {
152            match self.cluster_min_watermark.compare_exchange(
153                cur,
154                wm,
155                Ordering::AcqRel,
156                Ordering::Acquire,
157            ) {
158                Ok(_) => break,
159                Err(observed) => cur = observed,
160            }
161        }
162    }
163
164    /// Pin one internally consistent source-frontier snapshot for a compute cycle.
165    #[must_use]
166    pub fn committed_source_watermarks_snapshot(&self) -> Arc<rustc_hash::FxHashMap<String, i64>> {
167        let snapshot = self.committed_source_watermarks.read();
168        Arc::clone(&snapshot)
169    }
170
171    /// Install both scalar and source-keyed frontiers from one immutable committed channel cut.
172    ///
173    /// Publication is monotonic per source, matching [`Self::publish_cluster_min_watermark`]. A
174    /// source withheld by an active uninitialized channel remains unpublished until a later
175    /// committed cut initializes it.
176    ///
177    /// # Errors
178    /// Returns an error when the committed channel cut contains an invalid watermark sentinel.
179    pub fn publish_committed_channel_progress(
180        &self,
181        channels: &[crate::checkpoint::ChannelProgress],
182    ) -> Result<(), String> {
183        let source_watermarks = crate::checkpoint::channel_progress_frontiers_by_source(channels)?
184            .into_iter()
185            .filter_map(|(source, frontier)| frontier.map(|frontier| (source.to_owned(), frontier)))
186            .collect::<std::collections::BTreeMap<_, _>>();
187        self.publish_committed_checkpoint_progress(channels, &source_watermarks)
188    }
189
190    /// Install scalar and source-keyed frontiers from one committed checkpoint index.
191    ///
192    /// The explicit source map retains decisions for a source whose physical channel inventory is
193    /// empty in this cut. Publication remains monotonic so an older observer cannot regress a
194    /// newer installed decision.
195    ///
196    /// # Errors
197    /// Returns an error when channel progress or an explicit source watermark is invalid or the
198    /// explicit map disagrees with an initialized channel cut.
199    pub fn publish_committed_checkpoint_progress(
200        &self,
201        channels: &[crate::checkpoint::ChannelProgress],
202        source_watermarks: &std::collections::BTreeMap<String, i64>,
203    ) -> Result<(), String> {
204        let cluster_min = crate::checkpoint::channel_progress_frontier(channels)?;
205        let current = crate::checkpoint::channel_progress_frontiers_by_source(channels)?;
206        if source_watermarks
207            .values()
208            .any(|watermark| *watermark == i64::MIN)
209        {
210            return Err("committed source watermark uses the reserved uninitialized value".into());
211        }
212        for (source, frontier) in current {
213            let Some(frontier) = frontier else {
214                continue;
215            };
216            if source_watermarks.get(source) != Some(&frontier) {
217                return Err(format!(
218                    "committed source watermark '{source}' disagrees with channel progress"
219                ));
220            }
221        }
222        let _publication = self.committed_watermark_publication.lock();
223        {
224            let mut published = self.committed_source_watermarks.write();
225            let published = Arc::make_mut(&mut *published);
226            for (source_name, frontier) in source_watermarks {
227                published
228                    .entry(source_name.clone())
229                    .and_modify(|current| *current = (*current).max(*frontier))
230                    .or_insert(*frontier);
231            }
232        }
233        if let Some(cluster_min) = cluster_min {
234            self.publish_cluster_min_watermark_locked(cluster_min);
235        }
236        Ok(())
237    }
238
239    /// Replace the process-local committed frontier snapshot with one recovered exact cut.
240    ///
241    /// Unlike live publication, recovery may deliberately rewind to an older checkpoint or to
242    /// genesis. The caller must hold the pipeline recovery/intake fence; this method serialises
243    /// against any in-flight live publication and atomically establishes the new logical baseline
244    /// before intake is reopened.
245    ///
246    /// # Errors
247    /// Returns an error when channel progress or an explicit source watermark is invalid or the
248    /// explicit map disagrees with an initialized channel cut.
249    pub fn replace_recovered_checkpoint_progress(
250        &self,
251        channels: &[crate::checkpoint::ChannelProgress],
252        source_watermarks: &std::collections::BTreeMap<String, i64>,
253    ) -> Result<(), String> {
254        let cluster_min = crate::checkpoint::channel_progress_frontier(channels)?;
255        let current = crate::checkpoint::channel_progress_frontiers_by_source(channels)?;
256        if source_watermarks
257            .values()
258            .any(|watermark| *watermark == i64::MIN)
259        {
260            return Err("committed source watermark uses the reserved uninitialized value".into());
261        }
262        for (source, frontier) in current {
263            let Some(frontier) = frontier else {
264                continue;
265            };
266            if source_watermarks.get(source) != Some(&frontier) {
267                return Err(format!(
268                    "committed source watermark '{source}' disagrees with channel progress"
269                ));
270            }
271        }
272
273        let replacement = source_watermarks
274            .iter()
275            .map(|(source, watermark)| (source.clone(), *watermark))
276            .collect::<rustc_hash::FxHashMap<_, _>>();
277        let _publication = self.committed_watermark_publication.lock();
278        *self.committed_source_watermarks.write() = Arc::new(replacement);
279        self.cluster_min_watermark
280            .store(cluster_min.unwrap_or(i64::MIN), Ordering::Release);
281        Ok(())
282    }
283}