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 => {
83 drop(owned);
84 return Ok(());
85 }
86 DbState::Starting => {
87 return Err(DbError::Pipeline(
88 "shutdown found Starting without an incomplete owned startup attempt"
89 .into(),
90 ));
91 }
92 DbState::ShuttingDown => break false,
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 pub(super) async fn stop_pipeline_with_lifecycle_authority(
242 &self,
243 authority: PipelineLifecycleAuthority,
244 ) -> Result<(), DbError> {
245 let stop_timeout = match authority {
246 PipelineLifecycleAuthority::Public => PUBLIC_PIPELINE_STOP_TIMEOUT,
247 #[cfg(feature = "cluster")]
248 PipelineLifecycleAuthority::CoordinatedRecovery => {
249 self.coordinated_recovery_stop_timeout()
250 }
251 };
252 let deadline = checked_pipeline_deadline(stop_timeout, "pipeline stop")?;
253 let first_stop = loop {
254 let startup = {
255 let owned = self.startup_attempt.lock();
256 #[cfg(feature = "cluster")]
257 self.ensure_pipeline_lifecycle_authorized(authority, "stop")?;
258 #[cfg(not(feature = "cluster"))]
259 Self::ensure_pipeline_lifecycle_authorized(authority, "stop");
260 if let Some(in_flight) = owned.as_ref().filter(|attempt| !attempt.is_complete()) {
261 Arc::clone(in_flight)
262 } else {
263 match DbState::load(&self.state) {
264 DbState::Created | DbState::Stopped => {
265 drop(owned);
266 return Ok(());
267 }
268 DbState::Starting => {
269 return Err(DbError::InvalidOperation(
270 "pipeline stop found Starting without an incomplete owned startup attempt"
271 .into(),
272 ));
273 }
274 DbState::ShuttingDown => break false,
275 observed @ (DbState::Running | DbState::Faulted) => {
276 if DbState::compare_exchange(
277 observed,
278 DbState::ShuttingDown,
279 &self.state,
280 )
281 .is_ok()
282 {
283 break true;
284 }
285 continue;
286 }
287 }
288 }
289 };
290 self.await_startup_attempt_until(&startup, deadline, "pipeline stop")
291 .await
292 .map_err(|error| DbError::InvalidOperation(error.to_string()))?;
293 };
294 self.runtime_shutdown.write().cancel();
295 if first_stop {
296 *self.force_ckpt_tx.lock() = None;
297 *self.control_tx.lock() = None;
300 self.shutdown_signal.notify_one();
301 }
302
303 #[cfg(test)]
304 {
305 let stop_after_claim_gate = { self.stop_after_claim_gate.lock().clone() };
306 if let Some((entered, release)) = stop_after_claim_gate {
307 entered.notify_one();
308 release.notified().await;
309 }
310 }
311
312 let _topology = tokio::time::timeout_at(deadline, self.topology_ddl_lock.write())
313 .await
314 .map_err(|_| {
315 DbError::InvalidOperation(format!(
316 "pipeline stop could not acquire topology ownership within {stop_timeout:?}; catalog mutation remains fenced"
317 ))
318 })?;
319 let _lifecycle = tokio::time::timeout_at(deadline, self.lifecycle_lock.lock())
320 .await
321 .map_err(|_| {
322 DbError::InvalidOperation(format!(
323 "pipeline stop could not acquire lifecycle ownership within {stop_timeout:?}; \
324 an earlier lifecycle operation remains fenced"
325 ))
326 })?;
327 let mut runtime_handle = tokio::time::timeout_at(deadline, self.runtime_handle.lock())
328 .await
329 .map_err(|_| {
330 DbError::InvalidOperation(
331 "pipeline stop could not reacquire runtime watcher ownership; runtime remains fenced in ShuttingDown"
332 .into(),
333 )
334 })?;
335 if let Some(handle) = runtime_handle.as_mut() {
336 match tokio::time::timeout_at(deadline, handle).await {
337 Ok(Ok(())) => {
338 runtime_handle.take();
339 tracing::info!("Pipeline stopped cleanly");
340 }
341 Ok(Err(e)) => {
342 runtime_handle.take();
343 tracing::warn!(error = %e, "Pipeline task panicked during stop");
344 }
345 Err(_) => {
346 tracing::warn!(
347 timeout = ?stop_timeout,
348 "Pipeline stop still draining; will finalize when the coordinator exits"
349 );
350 return Err(DbError::InvalidOperation(
351 "pipeline stop is taking longer than expected; coordinator still \
352 draining, retry shortly"
353 .into(),
354 ));
355 }
356 }
357 }
358 drop(runtime_handle);
359
360 #[cfg(feature = "cluster")]
366 let _retired_cluster_generation = if self.is_cluster_runtime() {
367 Some(
368 retire_cluster_compute_generation_until(
369 &self.rotation_execution_fence,
370 &self.pending_vnode_transition,
371 &self.installed_vnode_state,
372 deadline,
373 )
374 .await
375 .map_err(|_| {
376 DbError::InvalidOperation(
377 "pipeline stop could not retire cluster vnode state before its deadline; \
378 runtime remains fenced in ShuttingDown"
379 .into(),
380 )
381 })?,
382 )
383 } else {
384 None
385 };
386
387 self.quiesce_checkpoint_decision_until(deadline).await?;
390 self.reconcile_sink_open_witness_until(deadline).await?;
391 self.quiesce_connector_generation_until(deadline).await?;
392
393 *self.checkpoint_namespace_lock.lock() = None;
394 if self.is_closed() {
395 return Ok(());
398 }
399 if self
400 .terminal_pipeline_halt
401 .load(std::sync::atomic::Ordering::Acquire)
402 {
403 match DbState::compare_exchange(DbState::ShuttingDown, DbState::Faulted, &self.state) {
404 Ok(_) | Err(DbState::Faulted) => return Ok(()),
405 Err(observed) => {
406 return Err(DbError::InvalidOperation(format!(
407 "terminal pipeline stop completed from unexpected lifecycle state {observed:?}; restart remains fenced"
408 )));
409 }
410 }
411 }
412 match DbState::compare_exchange(DbState::ShuttingDown, DbState::Created, &self.state) {
413 Ok(_) | Err(DbState::Created | DbState::Stopped) => Ok(()),
414 Err(observed) => Err(DbError::InvalidOperation(format!(
415 "pipeline stop completed from unexpected lifecycle state {observed:?}; restart remains fenced"
416 ))),
417 }
418 }
419}