laminar_db/pipeline_lifecycle/
authority.rs1#[cfg(feature = "cluster")]
2use super::latch_cluster_terminal_data_plane;
3use super::{Arc, DbError, LaminarDB, PipelineLifecycleAuthority};
4
5impl LaminarDB {
6 #[cfg(feature = "cluster")]
9 pub(crate) fn latch_local_terminal_pipeline_halt(&self) {
10 latch_cluster_terminal_data_plane(
11 &self.cluster_authority_transition,
12 &self.terminal_pipeline_halt,
13 &self.source_gate,
14 &self.coordinated_recovery_fenced,
15 );
16 }
17
18 #[cfg(feature = "cluster")]
22 pub(crate) fn latch_durable_terminal_recovery_fence(&self) {
23 latch_cluster_terminal_data_plane(
24 &self.cluster_authority_transition,
25 &self.durable_terminal_recovery_fence,
26 &self.source_gate,
27 &self.coordinated_recovery_fenced,
28 );
29 }
30
31 #[cfg(feature = "cluster")]
32 pub(super) fn validate_fresh_cluster_vnode_start(&self) -> Result<(), DbError> {
33 if self.has_unapplied_vnode_transition() {
34 return Err(DbError::Checkpoint(
35 "[LDB-6031] cluster startup found staged vnode state but no exact recovered \
36 checkpoint; refusing a fresh graph"
37 .into(),
38 ));
39 }
40 Ok(())
41 }
42
43 #[cfg(feature = "cluster")]
47 pub(super) async fn prepare_graph_ready_vnode_state_binding(
48 &self,
49 deadline: tokio::time::Instant,
50 ) -> Result<Option<crate::vnode_transition_staging::InstalledVnodeStateBinding>, DbError> {
51 let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
52 DbError::Checkpoint(
53 "cluster graph readiness has no vnode registry for installed-state binding".into(),
54 )
55 })?;
56 let assignment = registry.versioned_snapshot();
57 if assignment.version() == 0 || self.has_unapplied_vnode_transition() {
58 return Ok(None);
59 }
60
61 let pipeline_identity = self
62 .coordinator
63 .lock()
64 .await
65 .as_ref()
66 .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
67 .transpose()?;
68 let Some(pipeline_identity) = pipeline_identity else {
69 return Ok(None);
70 };
71 let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
72 DbError::Checkpoint(
73 "cluster graph readiness has no controller for assignment validation".into(),
74 )
75 })?;
76 let store = self
77 .assignment_snapshot_store
78 .lock()
79 .clone()
80 .ok_or_else(|| {
81 DbError::Checkpoint(
82 "cluster graph readiness has no durable assignment history".into(),
83 )
84 })?;
85 let durable = tokio::time::timeout_at(deadline, store.load_version(assignment.version()))
86 .await
87 .map_err(|_| {
88 DbError::Checkpoint(format!(
89 "assignment {} history read timed out at graph readiness",
90 assignment.version()
91 ))
92 })?
93 .map_err(|error| DbError::Checkpoint(error.to_string()))?
94 .ok_or_else(|| {
95 DbError::Checkpoint(format!(
96 "assignment {} is absent from durable history at graph readiness",
97 assignment.version()
98 ))
99 })?;
100 tokio::time::timeout_at(
101 deadline,
102 crate::rebalance::audit_assignment_snapshot_authority(
103 &store,
104 Some(controller.as_ref()),
105 &durable,
106 ),
107 )
108 .await
109 .map_err(|_| {
110 DbError::Checkpoint(format!(
111 "assignment {} authority audit timed out at graph readiness",
112 assignment.version()
113 ))
114 })?
115 .map_err(DbError::Checkpoint)?;
116 let durable_owners = durable
117 .to_vnode_vec(registry.vnode_count())
118 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
119 if durable.draining
120 || durable.version != assignment.version()
121 || durable_owners.as_slice() != assignment.owners()
122 {
123 return Err(DbError::Checkpoint(format!(
124 "assignment {} durable history does not match the graph-ready registry",
125 assignment.version()
126 )));
127 }
128 let fence = durable
129 .assignment_fence()
130 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
131 match fence.participant_incarnation(controller.instance_id().0) {
132 Some(boot_incarnation) if boot_incarnation == controller.recovery_incarnation() => {}
133 Some(_) => {
134 return Err(DbError::Checkpoint(format!(
135 "assignment {} names a different local process incarnation at graph readiness",
136 assignment.version()
137 )));
138 }
139 None => return Ok(None),
140 }
141
142 let current = registry.versioned_snapshot();
144 if current.version() != assignment.version()
145 || current.owners() != assignment.owners()
146 || self.has_unapplied_vnode_transition()
147 {
148 return Err(DbError::Checkpoint(format!(
149 "assignment {} changed or gained vnode work before graph-ready publication",
150 assignment.version()
151 )));
152 }
153 Ok(Some(
154 crate::vnode_transition_staging::InstalledVnodeStateBinding::new(
155 fence,
156 pipeline_identity,
157 )?,
158 ))
159 }
160
161 pub fn is_closed(&self) -> bool {
163 self.shutdown.load(std::sync::atomic::Ordering::Acquire)
164 }
165
166 pub fn close(&self) {
168 let runtime_shutdown = self.runtime_shutdown.write();
169 self.shutdown
170 .store(true, std::sync::atomic::Ordering::Release);
171 #[cfg(feature = "cluster")]
172 self.assignment_restore_shutdown.cancel();
173 runtime_shutdown.cancel();
174 self.shutdown_signal.notify_one();
175 }
176
177 pub fn enable_supervision(self: &Arc<Self>) {
180 *self.supervisor_self.lock() = Arc::downgrade(self);
181 }
182
183 #[cfg(feature = "cluster")]
186 pub(crate) fn set_recover_target_epoch(&self, epoch: Option<u64>) {
187 *self.recover_target_epoch.lock() = epoch;
188 }
189
190 #[cfg(feature = "cluster")]
193 pub(crate) fn set_source_gate(&self, closed: bool) {
194 if closed {
195 self.source_gate
196 .store(true, std::sync::atomic::Ordering::SeqCst);
197 return;
198 }
199 let _transition = self.cluster_authority_transition.lock();
200 if self
201 .terminal_pipeline_halt
202 .load(std::sync::atomic::Ordering::Acquire)
203 || self
204 .durable_terminal_recovery_fence
205 .load(std::sync::atomic::Ordering::Acquire)
206 || self
207 .pending_recovery_fault
208 .load(std::sync::atomic::Ordering::Acquire)
209 != 0
210 {
211 self.source_gate
212 .store(true, std::sync::atomic::Ordering::SeqCst);
213 return;
214 }
215 let process_authority_live = !self.is_cluster_runtime()
216 || self
217 .cluster_controller
218 .lock()
219 .as_ref()
220 .is_some_and(|controller| controller.process_lease_is_live());
221 if !process_authority_live {
222 self.source_gate
223 .store(true, std::sync::atomic::Ordering::SeqCst);
224 return;
225 }
226 if !self
227 .cluster_authority_revoked
228 .load(std::sync::atomic::Ordering::Acquire)
229 {
230 self.source_gate
231 .store(false, std::sync::atomic::Ordering::SeqCst);
232 }
233 }
234
235 #[cfg(feature = "cluster")]
239 pub fn fence_cluster_startup(&self) {
240 *self.startup_checkpoint_artifact_audit.lock() = None;
243 self.set_source_gate(true);
244 if let Some(controller) = self.cluster_controller.lock().as_ref() {
245 controller.set_recovering(true);
246 }
247 }
248
249 #[cfg(feature = "cluster")]
251 pub(crate) fn fence_coordinated_recovery_lifecycle(&self) {
252 let _lifecycle_claim = self.startup_attempt.lock();
253 self.set_source_gate(true);
254 self.coordinated_recovery_fenced
255 .store(true, std::sync::atomic::Ordering::Release);
256 }
257
258 #[cfg(feature = "cluster")]
261 pub(crate) fn release_coordinated_recovery_lifecycle(&self) {
262 let _lifecycle_claim = self.startup_attempt.lock();
263 let keep_fenced = self
264 .terminal_pipeline_halt
265 .load(std::sync::atomic::Ordering::Acquire)
266 || self
267 .durable_terminal_recovery_fence
268 .load(std::sync::atomic::Ordering::Acquire)
269 || self
270 .pending_recovery_fault
271 .load(std::sync::atomic::Ordering::Acquire)
272 != 0;
273 self.coordinated_recovery_fenced
274 .store(keep_fenced, std::sync::atomic::Ordering::Release);
275 if keep_fenced {
276 self.set_source_gate(true);
277 }
278 }
279
280 #[cfg(feature = "cluster")]
281 pub(super) fn ensure_pipeline_lifecycle_authorized(
282 &self,
283 authority: PipelineLifecycleAuthority,
284 operation: &str,
285 ) -> Result<(), DbError> {
286 if self
287 .coordinated_recovery_fenced
288 .load(std::sync::atomic::Ordering::Acquire)
289 && authority == PipelineLifecycleAuthority::Public
290 {
291 return Err(DbError::InvalidOperation(format!(
292 "pipeline {operation} is fenced by coordinated recovery"
293 )));
294 }
295 Ok(())
296 }
297
298 pub(super) fn ensure_terminal_halt_allows_start(
299 &self,
300 authority: PipelineLifecycleAuthority,
301 ) -> Result<(), DbError> {
302 let _ = authority;
303 let local_halt = self
304 .terminal_pipeline_halt
305 .load(std::sync::atomic::Ordering::Acquire);
306 #[cfg(feature = "cluster")]
307 let durable_halt = self.is_cluster_runtime()
308 && self
309 .durable_terminal_recovery_fence
310 .load(std::sync::atomic::Ordering::Acquire);
311 #[cfg(not(feature = "cluster"))]
312 let durable_halt = false;
313 if local_halt || durable_halt {
314 return Err(DbError::PipelineTerminal(format!(
315 "cannot restart a permanently halted pipeline in this process: {}",
316 self.last_fault()
317 .unwrap_or_else(|| "terminal halt reason was not recorded".into())
318 )));
319 }
320 Ok(())
321 }
322
323 #[cfg(not(feature = "cluster"))]
324 pub(super) fn ensure_pipeline_lifecycle_authorized(
325 authority: PipelineLifecycleAuthority,
326 operation: &str,
327 ) {
328 let _ = (authority, operation);
329 }
330
331 #[cfg(feature = "cluster")]
332 pub(crate) fn ensure_coordinated_recovery_mutation_unfenced(
333 &self,
334 operation: &str,
335 ) -> Result<(), DbError> {
336 self.ensure_pipeline_lifecycle_authorized(PipelineLifecycleAuthority::Public, operation)
337 }
338
339 #[cfg(feature = "cluster")]
341 pub fn revoke_cluster_authority(&self) {
342 if !self.is_cluster_runtime() {
343 return;
344 }
345 let _transition = self.cluster_authority_transition.lock();
346 let first_revocation = !self
347 .cluster_authority_revoked
348 .swap(true, std::sync::atomic::Ordering::AcqRel);
349 self.source_gate
350 .store(true, std::sync::atomic::Ordering::SeqCst);
351 if first_revocation {
352 self.invalidate_shuffle_assignment_fence();
353 }
354 let controller = self.cluster_controller.lock().clone();
355 if let Some(controller) = controller.as_ref() {
356 controller.fence_process_lease();
357 }
358 }
359
360 #[cfg(feature = "cluster")]
362 #[must_use]
363 pub fn cluster_intake_fenced(&self) -> bool {
364 self.cluster_authority_revoked
365 .load(std::sync::atomic::Ordering::Acquire)
366 || self.source_gate.load(std::sync::atomic::Ordering::Acquire)
367 || (self.is_cluster_runtime()
368 && self
369 .cluster_controller
370 .lock()
371 .as_ref()
372 .is_none_or(|controller| !controller.process_lease_is_live()))
373 }
374
375 #[cfg(feature = "cluster")]
377 #[must_use]
378 pub fn coordinated_recovery_in_progress(&self) -> bool {
379 self.is_cluster_runtime()
380 && self
381 .coordinated_recovery_fenced
382 .load(std::sync::atomic::Ordering::Acquire)
383 }
384}