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