laminar_db/pipeline_lifecycle/
shutdown.rs1use super::{
2 checked_pipeline_deadline, Arc, DbError, DbState, LaminarDB, PipelineLifecycleAuthority,
3 PUBLIC_PIPELINE_STOP_TIMEOUT,
4};
5#[cfg(feature = "cluster")]
6use super::{
7 configured_checkpoint_timeout, coordinated_recovery_stop_ceiling, report_cluster_terminal_halt,
8 retire_cluster_compute_generation_until,
9};
10
11impl LaminarDB {
12 pub(super) async fn quiesce_checkpoint_decision_until(
13 &self,
14 deadline: tokio::time::Instant,
15 ) -> Result<(), DbError> {
16 let _coordinator = tokio::time::timeout_at(deadline, self.coordinator.lock())
17 .await
18 .map_err(|_| {
19 DbError::Checkpoint(
20 "[LDB-6038] teardown could not acquire checkpoint coordinator ownership; durable decision writes remain fenced"
21 .into(),
22 )
23 })?;
24 Ok(())
25 }
26
27 pub(super) async fn reconcile_sink_open_witness_until(
28 &self,
29 deadline: tokio::time::Instant,
30 ) -> Result<(), DbError> {
31 let mut coordinator = tokio::time::timeout_at(deadline, self.coordinator.lock())
32 .await
33 .map_err(|_| {
34 DbError::Checkpoint(
35 "[LDB-6050] teardown could not acquire checkpoint coordinator ownership for sink-open reconciliation"
36 .into(),
37 )
38 })?;
39 if let Some(coordinator) = coordinator.as_mut() {
40 coordinator
41 .reconcile_sink_open_witness_until(deadline)
42 .await?;
43 }
44 Ok(())
45 }
46
47 pub async fn shutdown(&self) -> Result<(), DbError> {
53 const SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(45);
54 self.close();
58 #[cfg(feature = "cluster")]
59 if self.is_cluster_runtime()
60 && self
61 .terminal_pipeline_halt
62 .load(std::sync::atomic::Ordering::Acquire)
63 {
64 let controller = { self.cluster_controller.lock().clone() };
68 report_cluster_terminal_halt(controller, Arc::clone(&self.pending_recovery_fault))
69 .await;
70 self.latch_durable_terminal_recovery_fence();
71 }
72 let deadline = tokio::time::Instant::now() + SHUTDOWN_TIMEOUT;
73 #[cfg(feature = "cluster")]
74 self.quiesce_recovery_monitor_until(deadline).await?;
75 let first_shutdown = loop {
76 let startup = {
77 let owned = self.startup_attempt.lock();
78 if let Some(in_flight) = owned.as_ref().filter(|attempt| !attempt.is_complete()) {
79 Arc::clone(in_flight)
80 } else {
81 match DbState::load(&self.state) {
82 DbState::Stopped if self.checkpoint_namespace_lock.lock().is_none() => {
83 return Ok(());
84 }
85 DbState::Stopped | DbState::ShuttingDown => break false,
87 DbState::Starting => {
88 return Err(DbError::Pipeline(
89 "shutdown found Starting without an incomplete owned startup attempt"
90 .into(),
91 ));
92 }
93 observed @ (DbState::Created | DbState::Running | DbState::Faulted) => {
94 if DbState::compare_exchange(
95 observed,
96 DbState::ShuttingDown,
97 &self.state,
98 )
99 .is_ok()
100 {
101 break true;
102 }
103 continue;
104 }
105 }
106 }
107 };
108 self.await_startup_attempt_until(&startup, deadline, "pipeline shutdown")
109 .await?;
110 };
111 self.runtime_shutdown.write().cancel();
112 if first_shutdown {
113 *self.force_ckpt_tx.lock() = None;
114 self.shutdown_signal.notify_one();
115 }
116
117 let _topology = tokio::time::timeout_at(deadline, self.topology_ddl_lock.write())
118 .await
119 .map_err(|_| {
120 DbError::Pipeline(format!(
121 "pipeline shutdown could not acquire topology ownership within \
122 {SHUTDOWN_TIMEOUT:?}; catalog mutation remains fenced"
123 ))
124 })?;
125 let _lifecycle = tokio::time::timeout_at(deadline, self.lifecycle_lock.lock())
126 .await
127 .map_err(|_| {
128 DbError::Pipeline(format!(
129 "pipeline shutdown could not acquire lifecycle ownership within \
130 {SHUTDOWN_TIMEOUT:?}; startup/stop remains fenced"
131 ))
132 })?;
133
134 let mut runtime_handle = tokio::time::timeout_at(deadline, self.runtime_handle.lock())
135 .await
136 .map_err(|_| {
137 DbError::Pipeline(
138 "pipeline shutdown could not reacquire runtime watcher ownership; runtime remains fenced in ShuttingDown"
139 .into(),
140 )
141 })?;
142 let mut watcher_error = None;
143 if let Some(handle) = runtime_handle.as_mut() {
144 match tokio::time::timeout_at(deadline, handle).await {
145 Ok(Ok(())) => {
146 runtime_handle.take();
147 }
148 Ok(Err(error)) => {
149 runtime_handle.take();
150 watcher_error = Some(DbError::Pipeline(format!(
151 "pipeline watcher failed during shutdown: {error}"
152 )));
153 }
154 Err(_) => {
155 return Err(DbError::Pipeline(format!(
156 "pipeline shutdown exceeded {SHUTDOWN_TIMEOUT:?}; runtime is still \
157 draining and remains fenced in ShuttingDown; retry shutdown"
158 )));
159 }
160 }
161 }
162 drop(runtime_handle);
163
164 #[cfg(feature = "cluster")]
169 let _retired_cluster_generation = if self.is_cluster_runtime() {
170 Some(
171 retire_cluster_compute_generation_until(
172 &self.rotation_execution_fence,
173 &self.pending_vnode_transition,
174 &self.installed_vnode_state,
175 deadline,
176 )
177 .await
178 .map_err(|_| {
179 DbError::Pipeline(
180 "pipeline shutdown could not retire cluster vnode state before its \
181 deadline; runtime remains fenced in ShuttingDown"
182 .into(),
183 )
184 })?,
185 )
186 } else {
187 None
188 };
189 if watcher_error.is_none() {
190 if let Some(fault) = self.last_fault.lock().clone() {
191 watcher_error = Some(DbError::Pipeline(format!(
192 "pipeline faulted while shutting down: {fault}"
193 )));
194 } else {
195 tracing::info!("Pipeline shut down cleanly");
196 }
197 }
198
199 self.quiesce_checkpoint_decision_until(deadline).await?;
202 self.reconcile_sink_open_witness_until(deadline).await?;
203 self.quiesce_connector_generation_until(deadline).await?;
204
205 *self.checkpoint_namespace_lock.lock() = None;
206 DbState::Stopped.store(&self.state);
207 watcher_error.map_or(Ok(()), Err)
208 }
209
210 pub async fn stop_pipeline(&self) -> Result<(), DbError> {
216 self.stop_pipeline_with_lifecycle_authority(PipelineLifecycleAuthority::Public)
217 .await
218 }
219
220 #[cfg(feature = "cluster")]
225 pub(crate) fn coordinated_recovery_stop_timeout(&self) -> std::time::Duration {
226 let cleanup_timeout =
227 crate::checkpoint_coordinator::CheckpointConfig::default().cleanup_timeout;
228 coordinated_recovery_stop_ceiling(
229 configured_checkpoint_timeout(&self.config),
230 cleanup_timeout,
231 )
232 }
233
234 #[cfg(feature = "cluster")]
236 pub(crate) async fn stop_pipeline_for_coordinated_recovery(&self) -> Result<(), DbError> {
237 self.stop_pipeline_with_lifecycle_authority(PipelineLifecycleAuthority::CoordinatedRecovery)
238 .await
239 }
240
241 #[cfg(feature = "cluster")]
244 pub(crate) async fn stop_pipeline_for_topology_retirement(&self) -> Result<(), DbError> {
245 self.stop_pipeline_with_lifecycle_authority(PipelineLifecycleAuthority::TopologyRetirement)
246 .await
247 }
248
249 pub(super) async fn stop_pipeline_with_lifecycle_authority(
250 &self,
251 authority: PipelineLifecycleAuthority,
252 ) -> Result<(), DbError> {
253 let stop_timeout = self.pipeline_stop_timeout(authority)?;
254 let deadline = checked_pipeline_deadline(stop_timeout, "pipeline stop")?;
255 let first_stop = loop {
256 let startup = {
257 let owned = self.startup_attempt.lock();
258 #[cfg(feature = "cluster")]
259 self.ensure_pipeline_lifecycle_authorized(authority, "stop")?;
260 #[cfg(not(feature = "cluster"))]
261 Self::ensure_pipeline_lifecycle_authorized(authority, "stop");
262 if let Some(in_flight) = owned.as_ref().filter(|attempt| !attempt.is_complete()) {
263 Arc::clone(in_flight)
264 } else {
265 match DbState::load(&self.state) {
266 DbState::Created | DbState::Stopped => {
267 #[cfg(feature = "cluster")]
268 match self.claim_cold_recovery_stop(authority)? {
269 Some(true) => break true,
270 Some(false) => {}
271 None => continue,
272 }
273 drop(owned);
274 return Ok(());
275 }
276 DbState::Starting => {
277 return Err(DbError::InvalidOperation(
278 "pipeline stop found Starting without an incomplete owned startup attempt"
279 .into(),
280 ));
281 }
282 DbState::ShuttingDown => break false,
283 observed @ (DbState::Running | DbState::Faulted) => {
284 if DbState::compare_exchange(
285 observed,
286 DbState::ShuttingDown,
287 &self.state,
288 )
289 .is_ok()
290 {
291 break true;
292 }
293 continue;
294 }
295 }
296 }
297 };
298 self.await_startup_attempt_until(&startup, deadline, "pipeline stop")
299 .await
300 .map_err(|error| DbError::InvalidOperation(error.to_string()))?;
301 };
302 self.runtime_shutdown.write().cancel();
303 if first_stop {
304 *self.force_ckpt_tx.lock() = None;
305 *self.control_tx.lock() = None;
308 self.shutdown_signal.notify_one();
309 }
310
311 #[cfg(test)]
312 {
313 let stop_after_claim_gate = { self.stop_after_claim_gate.lock().clone() };
314 if let Some((entered, release)) = stop_after_claim_gate {
315 entered.notify_one();
316 release.notified().await;
317 }
318 }
319
320 let _topology = tokio::time::timeout_at(deadline, self.topology_ddl_lock.write())
321 .await
322 .map_err(|_| {
323 DbError::InvalidOperation(format!(
324 "pipeline stop could not acquire topology ownership within {stop_timeout:?}; catalog mutation remains fenced"
325 ))
326 })?;
327 let _lifecycle = tokio::time::timeout_at(deadline, self.lifecycle_lock.lock())
328 .await
329 .map_err(|_| {
330 DbError::InvalidOperation(format!(
331 "pipeline stop could not acquire lifecycle ownership within {stop_timeout:?}; \
332 an earlier lifecycle operation remains fenced"
333 ))
334 })?;
335 let mut runtime_handle = tokio::time::timeout_at(deadline, self.runtime_handle.lock())
336 .await
337 .map_err(|_| {
338 DbError::InvalidOperation(
339 "pipeline stop could not reacquire runtime watcher ownership; runtime remains fenced in ShuttingDown"
340 .into(),
341 )
342 })?;
343 if let Some(handle) = runtime_handle.as_mut() {
344 match tokio::time::timeout_at(deadline, handle).await {
345 Ok(Ok(())) => {
346 runtime_handle.take();
347 tracing::info!("Pipeline stopped cleanly");
348 }
349 Ok(Err(e)) => {
350 runtime_handle.take();
351 #[cfg(feature = "cluster")]
352 if authority == PipelineLifecycleAuthority::TopologyRetirement {
353 let reason = format!(
354 "parent runtime watcher failed during topology retirement: {e}"
355 );
356 self.last_fault.lock().get_or_insert_with(|| reason.clone());
357 return Err(DbError::Pipeline(reason));
358 }
359 tracing::warn!(error = %e, "Pipeline task panicked during stop");
360 }
361 Err(_) => {
362 tracing::warn!(
363 timeout = ?stop_timeout,
364 "Pipeline stop still draining; will finalize when the coordinator exits"
365 );
366 return Err(DbError::InvalidOperation(
367 "pipeline stop is taking longer than expected; coordinator still \
368 draining, retry shortly"
369 .into(),
370 ));
371 }
372 }
373 }
374 drop(runtime_handle);
375
376 #[cfg(feature = "cluster")]
382 let _retired_cluster_generation = if self.is_cluster_runtime() {
383 Some(
384 retire_cluster_compute_generation_until(
385 &self.rotation_execution_fence,
386 &self.pending_vnode_transition,
387 &self.installed_vnode_state,
388 deadline,
389 )
390 .await
391 .map_err(|_| {
392 DbError::InvalidOperation(
393 "pipeline stop could not retire cluster vnode state before its deadline; \
394 runtime remains fenced in ShuttingDown"
395 .into(),
396 )
397 })?,
398 )
399 } else {
400 None
401 };
402
403 self.quiesce_checkpoint_decision_until(deadline).await?;
406 self.reconcile_sink_open_witness_until(deadline).await?;
407 self.quiesce_connector_generation_until(deadline).await?;
408
409 #[cfg(feature = "cluster")]
410 if authority == PipelineLifecycleAuthority::TopologyRetirement {
411 return Ok(());
415 }
416 *self.checkpoint_namespace_lock.lock() = None;
417 if self.is_closed() {
418 return Ok(());
421 }
422 if self
423 .terminal_pipeline_halt
424 .load(std::sync::atomic::Ordering::Acquire)
425 {
426 match DbState::compare_exchange(DbState::ShuttingDown, DbState::Faulted, &self.state) {
427 Ok(_) | Err(DbState::Faulted) => return Ok(()),
428 Err(observed) => {
429 return Err(DbError::InvalidOperation(format!(
430 "terminal pipeline stop completed from unexpected lifecycle state {observed:?}; restart remains fenced"
431 )));
432 }
433 }
434 }
435 match DbState::compare_exchange(DbState::ShuttingDown, DbState::Created, &self.state) {
436 Ok(_) | Err(DbState::Created | DbState::Stopped) => Ok(()),
437 Err(observed) => Err(DbError::InvalidOperation(format!(
438 "pipeline stop completed from unexpected lifecycle state {observed:?}; restart remains fenced"
439 ))),
440 }
441 }
442}
443
444impl LaminarDB {
445 #[cfg_attr(not(feature = "cluster"), allow(clippy::unnecessary_wraps))]
446 fn pipeline_stop_timeout(
448 &self,
449 authority: PipelineLifecycleAuthority,
450 ) -> Result<std::time::Duration, DbError> {
451 let stop_timeout = match authority {
452 PipelineLifecycleAuthority::Public => PUBLIC_PIPELINE_STOP_TIMEOUT,
453 #[cfg(feature = "cluster")]
454 PipelineLifecycleAuthority::CoordinatedRecovery => {
455 self.coordinated_recovery_stop_timeout()
456 }
457 #[cfg(feature = "cluster")]
458 PipelineLifecycleAuthority::TopologyRetirement => std::time::Duration::from_secs(45),
459 #[cfg(feature = "cluster")]
460 PipelineLifecycleAuthority::TopologyInstallation => {
461 return Err(DbError::InvalidOperation(
462 "topology installation authority is confined to held startup".into(),
463 ));
464 }
465 };
466 Ok(stop_timeout)
467 }
468
469 #[cfg(feature = "cluster")]
470 fn claim_cold_recovery_stop(
471 &self,
472 authority: PipelineLifecycleAuthority,
473 ) -> Result<Option<bool>, DbError> {
474 if authority == PipelineLifecycleAuthority::TopologyRetirement {
475 return Err(DbError::InvalidOperation(
476 "topology retirement lost the held parent runtime".into(),
477 ));
478 }
479 if authority == PipelineLifecycleAuthority::CoordinatedRecovery {
480 return Ok(
482 match DbState::compare_exchange(
483 DbState::Created,
484 DbState::ShuttingDown,
485 &self.state,
486 ) {
487 Ok(_) => Some(true),
488 Err(DbState::Stopped) => Some(false),
489 Err(_) => None,
490 },
491 );
492 }
493 Ok(Some(false))
494 }
495}