1#[cfg(feature = "cluster")]
2use super::TopologyStartup;
3use super::{
4 checked_pipeline_deadline, panic_message, publish_runtime_fault_state, required_recovery_scope,
5 Arc, CheckpointStorageScope, DbError, DbState, DeliveryGuarantee, FutureExt, LaminarDB,
6 PipelineLifecycleAuthority, RuntimeMode, StartupAttempt, StartupDriverGuard,
7};
8#[cfg(feature = "cluster")]
9use super::{report_cluster_terminal_halt, retire_cluster_compute_generation};
10use laminar_core::storage_location::StorageProvider;
11
12mod checkpoint_initialization;
13
14impl LaminarDB {
15 pub(super) fn finish_start_transition(&self) -> Result<(), DbError> {
21 let runtime_shutdown = self.runtime_shutdown.read();
24 if self.is_closed() || runtime_shutdown.is_cancelled() {
25 return match DbState::compare_exchange(
26 DbState::Starting,
27 DbState::Created,
28 &self.state,
29 ) {
30 Ok(_) | Err(DbState::Created) => Err(DbError::Shutdown),
31 Err(DbState::Faulted) => Err(DbError::Pipeline(format!(
32 "pipeline faulted while its cancelled generation was leaving startup: {}",
33 self.last_fault()
34 .unwrap_or_else(|| "compute loop exited without a fault reason".into())
35 ))),
36 Err(observed) => Err(DbError::InvalidOperation(format!(
37 "cancelled pipeline startup completed from an unexpected lifecycle state: {observed:?}"
38 ))),
39 };
40 }
41 match DbState::compare_exchange(DbState::Starting, DbState::Running, &self.state) {
42 Ok(_) => Ok(()),
43 Err(DbState::Faulted) => Err(DbError::Pipeline(format!(
44 "pipeline faulted while entering the runtime control loop: {}",
45 self.last_fault()
46 .unwrap_or_else(|| "compute loop exited without a fault reason".into())
47 ))),
48 Err(observed) => Err(DbError::InvalidOperation(format!(
49 "pipeline startup completed from an unexpected lifecycle state: {observed:?}"
50 ))),
51 }
52 }
53
54 pub async fn start(self: &Arc<Self>) -> Result<(), DbError> {
64 if DbState::load(&self.state) == DbState::Created {
65 crate::memory_budget::warn_if_over_cgroup_limit(&self.config, self.runtime_mode());
66 }
67 self.start_with_lifecycle_authority(PipelineLifecycleAuthority::Public)
68 .await
69 }
70
71 #[cfg(feature = "cluster")]
75 pub(crate) async fn start_for_coordinated_recovery(self: &Arc<Self>) -> Result<(), DbError> {
76 self.ensure_terminal_halt_allows_start(PipelineLifecycleAuthority::CoordinatedRecovery)?;
77 *self.startup_checkpoint_artifact_audit.lock() = None;
79 if !self.is_cluster_runtime() {
80 return self
81 .start_with_lifecycle_authority(PipelineLifecycleAuthority::CoordinatedRecovery)
82 .await;
83 }
84 let controller = self
85 .cluster_controller
86 .lock()
87 .clone()
88 .ok_or_else(|| DbError::Checkpoint("recovery has no controller".into()))?;
89 let announcement = controller
90 .observe_recover_control()
91 .await
92 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
93 if let Some(start) = announcement.filter(|start| start.round.topology_binding().is_some()) {
94 let laminar_core::cluster::control::RecoverPhase::Start { epoch } = start.phase else {
95 return Err(laminar_core::cluster::control::TopologyError::Fenced.into());
96 };
97 if *self.recover_target_epoch.lock() != Some(epoch) {
98 return Err(laminar_core::cluster::control::TopologyError::Fenced.into());
99 }
100 if DbState::load(&self.state) == DbState::Running {
101 return Box::pin(self.certify_recovered_cluster_topology(&start)).await;
102 }
103 let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(45);
104 let mut image = Box::pin(self.prepare_coordinated_topology_restore(&start)).await?;
105 tokio::time::timeout_at(
106 deadline,
107 self.prepare_coordinated_topology_transport(&mut image),
108 )
109 .await
110 .map_err(|_| laminar_core::cluster::control::TopologyError::Contended)??;
111 let _schema_creation =
112 tokio::time::timeout_at(deadline, self.schema_creation_lock.lock())
113 .await
114 .map_err(|_| {
115 DbError::Pipeline(
116 "schema preparation exceeded the recovery deadline".into(),
117 )
118 })?;
119 return Box::pin(self.start_with_runtime_image(
120 PipelineLifecycleAuthority::CoordinatedRecovery,
121 Some(TopologyStartup { image, deadline }),
122 ))
123 .await;
124 }
125 self.start_with_lifecycle_authority(PipelineLifecycleAuthority::CoordinatedRecovery)
126 .await
127 }
128
129 pub(super) async fn start_with_lifecycle_authority(
130 self: &Arc<Self>,
131 authority: PipelineLifecycleAuthority,
132 ) -> Result<(), DbError> {
133 if self.is_closed() {
134 return Err(DbError::Shutdown);
135 }
136 self.ensure_terminal_halt_allows_start(authority)?;
137 #[cfg(feature = "cluster")]
138 self.ensure_pipeline_lifecycle_authorized(authority, "start")?;
139 #[cfg(not(feature = "cluster"))]
140 Self::ensure_pipeline_lifecycle_authorized(authority, "start");
141 #[cfg(feature = "cluster")]
142 if self.is_cluster_runtime() {
143 let catalog = self.catalog_manifest_store.lock().clone();
144 if let Some(catalog) = catalog {
145 let live = catalog.live_topology_commit().await.map_err(|error| {
146 DbError::Pipeline(format!(
147 "[{}] catalog manifest load failed: {error}",
148 laminar_core::error_codes::RECOVERY_FAILED
149 ))
150 })?;
151 if live.is_some() {
152 return Err(laminar_core::cluster::control::TopologyError::Conflict(
153 "committed target requires migration-root reconstruction, installation and Release; ordinary startup cannot restore the parent checkpoint".into(),
154 ).into());
155 }
156 }
157 }
158 let _schema_creation = self.schema_creation_lock.lock().await;
159 self.start_with_runtime_image(
160 authority,
161 #[cfg(feature = "cluster")]
162 None,
163 )
164 .await
165 }
166
167 #[cfg(feature = "cluster")]
168 pub(crate) async fn start_with_topology_image(
169 self: &Arc<Self>,
170 image: crate::db::PreparedTopologyRestore,
171 deadline: tokio::time::Instant,
172 ) -> Result<(), DbError> {
173 let _schema_creation = tokio::time::timeout_at(deadline, self.schema_creation_lock.lock())
174 .await
175 .map_err(|_| {
176 DbError::Pipeline(
177 "schema preparation exceeded the topology installation deadline".into(),
178 )
179 })?;
180 self.start_with_runtime_image(
181 PipelineLifecycleAuthority::TopologyInstallation,
182 Some(TopologyStartup { image, deadline }),
183 )
184 .await
185 }
186
187 async fn start_with_runtime_image(
188 self: &Arc<Self>,
189 authority: PipelineLifecycleAuthority,
190 #[cfg(feature = "cluster")] mut topology: Option<TopologyStartup>,
191 ) -> Result<(), DbError> {
192 if self.is_closed() {
193 return Err(DbError::Shutdown);
194 }
195 self.connector_registry.freeze();
196 let runtime = self.control_runtime.handle()?;
197 let attempt = {
198 let mut owned = self.startup_attempt.lock();
199 self.ensure_terminal_halt_allows_start(authority)?;
200 #[cfg(feature = "cluster")]
201 self.ensure_pipeline_lifecycle_authorized(authority, "start")?;
202 #[cfg(not(feature = "cluster"))]
203 Self::ensure_pipeline_lifecycle_authorized(authority, "start");
204 loop {
205 if let Some(in_flight) = owned.as_ref().filter(|attempt| !attempt.is_complete()) {
209 break Arc::clone(in_flight);
210 }
211 match DbState::load(&self.state) {
212 DbState::Running => return Ok(()),
213 DbState::Starting => {
214 break owned.clone().ok_or_else(|| {
215 DbError::Pipeline(
216 "pipeline is Starting without an owned startup attempt; restart is fenced"
217 .into(),
218 )
219 })?;
220 }
221 DbState::Stopped => {
222 return Err(DbError::InvalidOperation(
223 "Cannot start a stopped pipeline. Create a new LaminarDB instance."
224 .into(),
225 ));
226 }
227 DbState::ShuttingDown
228 if {
229 #[cfg(feature = "cluster")]
230 {
231 authority != PipelineLifecycleAuthority::TopologyInstallation
232 }
233 #[cfg(not(feature = "cluster"))]
234 {
235 true
236 }
237 } =>
238 {
239 return Err(DbError::InvalidOperation(
240 "cannot start pipeline: shutdown/stop in progress".into(),
241 ));
242 }
243 claimed @ (DbState::Created | DbState::Faulted | DbState::ShuttingDown) => {
244 self.ensure_terminal_halt_allows_start(authority)?;
248 #[cfg(feature = "cluster")]
249 self.ensure_pipeline_lifecycle_authorized(authority, "start")?;
250 #[cfg(not(feature = "cluster"))]
251 Self::ensure_pipeline_lifecycle_authorized(authority, "start");
252 #[cfg(feature = "cluster")]
253 if authority == PipelineLifecycleAuthority::TopologyInstallation
254 && (topology.is_none()
255 || !matches!(claimed, DbState::Created | DbState::ShuttingDown)
256 || self.last_fault.lock().is_some()
257 || !self.owned_source_tasks.lock().is_empty()
258 || !self.owned_sink_handles.lock().is_empty()
259 || !self.owned_connector_task_fences.lock().is_empty())
260 {
261 return Err(
262 laminar_core::cluster::control::TopologyError::Fenced.into()
263 );
264 }
265 let attempt = Arc::new(StartupAttempt::new());
266 *owned = Some(Arc::clone(&attempt));
269 let (start_tx, start_rx) = std::sync::mpsc::sync_channel(1);
270 let db = Arc::clone(self);
271 let driver_attempt = Arc::clone(&attempt);
272 let emergency_attempt = Arc::clone(&attempt);
273 let driver_runtime = runtime.clone();
274 #[cfg(feature = "cluster")]
275 let driver_topology = topology.take();
276 let startup_thread = match std::thread::Builder::new()
277 .name("laminar-start".into())
278 .spawn(move || {
279 if !matches!(start_rx.recv(), Ok(true)) {
280 return;
281 }
282 let result =
283 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
284 driver_runtime.block_on(db.clone().drive_start_attempt(
285 driver_attempt,
286 claimed == DbState::Faulted,
287 authority,
288 #[cfg(feature = "cluster")]
289 driver_topology,
290 ));
291 }));
292 if result.is_err() && !emergency_attempt.is_complete() {
293 let message =
294 "startup owner thread panicked before terminal cleanup";
295 db.last_fault.lock().get_or_insert(message.into());
296 DbState::Faulted.store(&db.state);
297 emergency_attempt
298 .complete(Err(DbError::Pipeline(message.into())));
299 }
300 }) {
301 Ok(thread) => thread,
302 Err(error) => {
303 *owned = None;
304 return Err(DbError::Pipeline(format!(
305 "failed to spawn startup owner thread: {error}"
306 )));
307 }
308 };
309 drop(startup_thread);
311 if DbState::compare_exchange(claimed, DbState::Starting, &self.state)
312 .is_err()
313 {
314 let _ = start_tx.send(false);
315 *owned = None;
316 #[cfg(feature = "cluster")]
317 if authority == PipelineLifecycleAuthority::TopologyInstallation {
318 return Err(
319 laminar_core::cluster::control::TopologyError::Fenced.into()
320 );
321 }
322 continue;
323 }
324 if start_tx.send(true).is_err() {
325 let message = "startup owner thread exited before accepting ownership";
326 self.last_fault.lock().get_or_insert(message.into());
327 DbState::Faulted.store(&self.state);
328 attempt.complete(Err(DbError::Pipeline(message.into())));
329 }
330 break attempt;
331 }
332 }
333 }
334 };
335 attempt.wait().await
336 }
337
338 pub(super) async fn drive_start_attempt(
339 self: Arc<Self>,
340 attempt: Arc<StartupAttempt>,
341 starting_from_fault: bool,
342 authority: PipelineLifecycleAuthority,
343 #[cfg(feature = "cluster")] topology: Option<TopologyStartup>,
344 ) {
345 let terminal = StartupDriverGuard::new(&self, Arc::clone(&attempt));
346 let result = std::panic::AssertUnwindSafe(Box::pin(self.run_claimed_start(
347 starting_from_fault,
348 #[cfg(feature = "cluster")]
349 topology,
350 )))
351 .catch_unwind()
352 .await;
353 let result = match result {
354 Ok(result) => result,
355 Err(panic) => {
356 let reason = format!("startup driver panicked: {}", panic_message(panic.as_ref()));
357 *self.last_fault.lock() = Some(reason.clone());
358 let cleanup = std::panic::AssertUnwindSafe(self.cleanup_failed_start())
359 .catch_unwind()
360 .await;
361 DbState::Faulted.store(&self.state);
362 match cleanup {
363 Ok(Ok(())) => Err(DbError::Pipeline(reason)),
364 Ok(Err(error)) if error.requires_pipeline_halt() => {
365 tracing::error!(
366 panic = %reason,
367 cleanup_error = %error,
368 "terminal cleanup error superseded a generic startup panic"
369 );
370 Err(error)
371 }
372 Ok(Err(error)) => Err(DbError::Pipeline(format!(
373 "{reason}; failed-start cleanup remains fenced: {error}"
374 ))),
375 Err(cleanup_panic) => Err(DbError::Pipeline(format!(
376 "{reason}; failed-start cleanup panicked: {}",
377 panic_message(cleanup_panic.as_ref())
378 ))),
379 }
380 }
381 };
382 let result = if result.is_err() && DbState::load(&self.state) == DbState::Starting {
383 match self.cleanup_failed_start().await {
384 Ok(()) => {
385 if result
386 .as_ref()
387 .err()
388 .is_some_and(DbError::requires_pipeline_halt)
389 {
390 DbState::Faulted.store(&self.state);
391 } else {
392 let _ = DbState::compare_exchange(
393 DbState::Starting,
394 DbState::Created,
395 &self.state,
396 );
397 }
398 result
399 }
400 Err(error) => {
401 DbState::Faulted.store(&self.state);
402 if result
403 .as_ref()
404 .err()
405 .is_some_and(DbError::requires_pipeline_halt)
406 {
407 tracing::error!(cleanup_error = %error, "cleanup also failed after a permanent startup error");
408 result
409 } else {
410 Err(error)
411 }
412 }
413 }
414 } else {
415 result
416 };
417 let result = self.normalize_start_result_for_terminal_latch(result);
418 self.terminalize_start_attempt_if_needed(authority, &result)
419 .await;
420 terminal.finish(result);
421 }
422
423 pub(super) fn normalize_start_result_for_terminal_latch(
424 &self,
425 result: Result<(), DbError>,
426 ) -> Result<(), DbError> {
427 if !self
428 .terminal_pipeline_halt
429 .load(std::sync::atomic::Ordering::Acquire)
430 {
431 return result;
432 }
433 match result {
434 Err(error) if error.requires_pipeline_halt() => Err(error),
435 Ok(()) | Err(_) => Err(DbError::PipelineTerminal(
436 self.last_fault()
437 .unwrap_or_else(|| "terminal startup failure".into()),
438 )),
439 }
440 }
441
442 #[cfg_attr(
446 not(feature = "cluster"),
447 allow(unknown_lints, clippy::unused_async, clippy::unused_async_trait_impl)
448 )]
449 pub(super) async fn terminalize_start_attempt_if_needed(
450 &self,
451 authority: PipelineLifecycleAuthority,
452 result: &Result<(), DbError>,
453 ) {
454 let Err(error) = result else {
455 return;
456 };
457 if !error.requires_pipeline_halt() {
458 return;
459 }
460 let _ = authority;
461 let reason = if self
462 .terminal_pipeline_halt
463 .load(std::sync::atomic::Ordering::Acquire)
464 {
465 self.last_fault().unwrap_or_else(|| error.to_string())
466 } else {
467 error.to_string()
468 };
469 *self.last_fault.lock() = Some(reason.clone());
470
471 #[cfg(feature = "cluster")]
472 if self.is_cluster_runtime() {
473 self.latch_local_terminal_pipeline_halt();
474 let controller = self.cluster_controller.lock().clone();
475 if let Some(controller) = controller.as_deref() {
476 controller.set_recovering(true);
477 if let Err(publication_error) = crate::coordinated_recovery::queue_local_fault(
478 controller,
479 &self.pending_recovery_fault,
480 ) {
481 tracing::error!(
482 %publication_error,
483 "could not retain terminal startup fault request"
484 );
485 }
486 }
487 let generation = Arc::clone(&self.rotation_execution_fence)
488 .write_owned()
489 .await;
490 retire_cluster_compute_generation(
491 &self.pending_vnode_transition,
492 &self.installed_vnode_state,
493 );
494 publish_runtime_fault_state(&self.state);
495 drop(generation);
496 tracing::error!(
497 %reason,
498 "pipeline startup hit a permanent error; awaiting durable terminal fencing"
499 );
500 report_cluster_terminal_halt(controller, Arc::clone(&self.pending_recovery_fault))
501 .await;
502 self.latch_durable_terminal_recovery_fence();
503 return;
504 }
505
506 self.terminal_pipeline_halt
507 .store(true, std::sync::atomic::Ordering::SeqCst);
508 publish_runtime_fault_state(&self.state);
509 tracing::error!(
510 %reason,
511 "pipeline startup hit a permanent error; automatic restart is disabled"
512 );
513 }
514
515 pub(super) async fn run_claimed_start(
516 &self,
517 starting_from_fault: bool,
518 #[cfg(feature = "cluster")] topology: Option<TopologyStartup>,
519 ) -> Result<(), DbError> {
520 const FAULT_RESTART_QUIESCE_TIMEOUT: std::time::Duration =
521 std::time::Duration::from_secs(10);
522 #[cfg(feature = "cluster")]
523 let deadline = topology.as_ref().map(|startup| startup.deadline);
524 #[cfg(not(feature = "cluster"))]
525 let deadline: Option<tokio::time::Instant> = None;
526 let (_topology, _lifecycle) = self.lock_startup_ownership(deadline).await?;
527 self.ensure_catalog_cleanup_unfenced("pipeline start")?;
528 if DbState::load(&self.state) != DbState::Starting {
529 return Err(DbError::Pipeline(
530 "startup ownership was superseded before the driver entered the lifecycle".into(),
531 ));
532 }
533
534 let generation_quiesce_deadline =
535 tokio::time::Instant::now() + FAULT_RESTART_QUIESCE_TIMEOUT;
536 self.runtime_shutdown.read().cancel();
537 self.shutdown_signal.notify_one();
538 self.join_runtime_watcher_until(generation_quiesce_deadline)
539 .await?;
540 if let Err(error) = self
541 .quiesce_connector_generation_until(generation_quiesce_deadline)
542 .await
543 {
544 if starting_from_fault || error.requires_pipeline_halt() {
545 DbState::Faulted.store(&self.state);
546 } else {
547 DbState::Created.store(&self.state);
548 }
549 return Err(error);
550 }
551
552 if starting_from_fault {
553 let deadline = tokio::time::Instant::now() + FAULT_RESTART_QUIESCE_TIMEOUT;
554 if let Err(error) = self.quiesce_checkpoint_decision_until(deadline).await {
555 DbState::Faulted.store(&self.state);
558 return Err(error);
559 }
560 }
561
562 *self.last_fault.lock() = None;
565 {
566 let mut guard = self.engine_metrics.lock();
567 if guard.is_none() {
568 *guard = Some(Arc::new(crate::engine_metrics::EngineMetrics::new(
569 &prometheus::Registry::new(),
570 )));
571 }
572 }
573
574 #[cfg(feature = "cluster")]
575 let catalog_restore = match deadline {
576 Some(deadline) => {
577 tokio::time::timeout_at(deadline, self.restore_catalog_from_manifest())
578 .await
579 .map_err(|_| {
580 DbError::Pipeline(
581 "topology catalog replay exceeded the installation deadline".into(),
582 )
583 })
584 .and_then(|result| result)
585 }
586 None => self.restore_catalog_from_manifest().await,
587 };
588 #[cfg(feature = "cluster")]
589 if let Err(error) = catalog_restore {
590 return self.fail_catalog_bootstrap_start(error);
591 }
592
593 tokio::select! {
596 biased;
597 () = self.shutdown_signal.notified() => {}
598 () = std::future::ready(()) => {}
599 }
600
601 let start = Box::pin(self.start_inner(
602 #[cfg(feature = "cluster")]
603 topology.map(|startup| startup.image),
604 ));
605 #[cfg(feature = "cluster")]
606 let result = if let Some(deadline) = deadline {
607 tokio::time::timeout_at(deadline, start)
608 .await
609 .map_err(|_| laminar_core::cluster::control::TopologyError::Contended.into())
610 .and_then(|result| result)
611 } else {
612 start.await
613 };
614 #[cfg(not(feature = "cluster"))]
615 let result = start.await;
616 match result {
617 Ok(()) => {
618 match self.finish_start_transition() {
621 Ok(()) => Ok(()),
622 Err(error) => match self.cleanup_failed_start().await {
623 Ok(()) => {
624 if error.requires_pipeline_halt() {
625 DbState::Faulted.store(&self.state);
626 }
627 Err(error)
628 }
629 Err(cleanup_error) => {
630 DbState::Faulted.store(&self.state);
631 if error.requires_pipeline_halt() {
632 tracing::error!(
633 %cleanup_error,
634 "failed-start cleanup also failed after a permanent startup error"
635 );
636 Err(error)
637 } else if cleanup_error.requires_pipeline_halt() {
638 tracing::error!(
639 startup_error = %error,
640 %cleanup_error,
641 "terminal cleanup error superseded a generic startup failure"
642 );
643 Err(cleanup_error)
644 } else {
645 Err(DbError::Pipeline(format!(
646 "{error}; failed-start cleanup remains fenced: {cleanup_error}"
647 )))
648 }
649 }
650 },
651 }
652 }
653 Err(e) => {
654 match self.cleanup_failed_start().await {
655 Ok(()) => {
656 if e.requires_pipeline_halt() {
657 DbState::Faulted.store(&self.state);
658 } else {
659 DbState::Created.store(&self.state);
661 }
662 Err(e)
663 }
664 Err(cleanup_error) => {
665 DbState::Faulted.store(&self.state);
666 if e.requires_pipeline_halt() {
667 tracing::error!(
668 %cleanup_error,
669 "failed-start cleanup also failed after a permanent startup error"
670 );
671 Err(e)
672 } else if cleanup_error.requires_pipeline_halt() {
673 tracing::error!(
674 startup_error = %e,
675 %cleanup_error,
676 "terminal cleanup error superseded a generic startup failure"
677 );
678 Err(cleanup_error)
679 } else {
680 Err(DbError::Pipeline(format!(
681 "{e}; failed-start cleanup remains fenced: {cleanup_error}"
682 )))
683 }
684 }
685 }
686 }
687 }
688 }
689
690 pub(super) fn validate_startup_durability(
691 &self,
692 startup_runtime: RuntimeMode,
693 ) -> Result<Option<Arc<dyn object_store::ObjectStore>>, DbError> {
694 #[cfg(feature = "cluster")]
695 let has_injected_decision_store = self.decision_store.lock().is_some();
696 #[cfg(not(feature = "cluster"))]
697 let has_injected_decision_store = false;
698 if startup_runtime == RuntimeMode::Local
699 && self.config.delivery_guarantee != DeliveryGuarantee::BestEffort
700 && (self.config.object_store_url.as_deref().is_some_and(|url| {
701 StorageProvider::detect_uri(url) != Some(StorageProvider::Local)
702 }) || has_injected_decision_store)
703 {
704 return Err(DbError::Config(
705 "[LDB-0014] a local replay-capable deployment with a shared cloud checkpoint \
706 namespace or injected decision store is not admitted until its writer lease is \
707 term-fenced. Use a built-in or file:// local checkpoint directory, or \
708 best_effort delivery"
709 .into(),
710 ));
711 }
712
713 #[cfg(feature = "cluster")]
714 let injected_cluster_checkpoint_store = self.cluster_checkpoint_object_store();
715 #[cfg(not(feature = "cluster"))]
716 let injected_cluster_checkpoint_store: Option<Arc<dyn object_store::ObjectStore>> = None;
717
718 if self.config.checkpoint.is_some()
719 && self.config.delivery_guarantee != DeliveryGuarantee::BestEffort
720 {
721 let checkpoint_scope = if injected_cluster_checkpoint_store.is_some() {
725 CheckpointStorageScope::ClusterShared
726 } else {
727 match self.config.object_store_url.as_deref() {
728 Some(url) => CheckpointStorageScope::for_url(url),
729 None => CheckpointStorageScope::NodeDurable,
730 }
731 };
732 let required = required_recovery_scope(startup_runtime);
733 if !checkpoint_scope.satisfies(required) {
734 return Err(DbError::Config(format!(
735 "[LDB-5036] {startup_runtime:?} {:?} delivery requires {required:?} \
736 checkpoint/decision storage, but the configured checkpoint store is \
737 {checkpoint_scope:?}; use the built-in checkpoint data_dir for \
738 node-local recovery, or a supported shared object store",
739 self.config.delivery_guarantee
740 )));
741 }
742 }
743
744 Ok(injected_cluster_checkpoint_store)
745 }
746}
747
748impl LaminarDB {
749 async fn lock_startup_ownership(
750 &self,
751 deadline: Option<tokio::time::Instant>,
752 ) -> Result<
753 (
754 tokio::sync::RwLockWriteGuard<'_, ()>,
755 tokio::sync::MutexGuard<'_, ()>,
756 ),
757 DbError,
758 > {
759 let topology = match deadline {
760 Some(deadline) => tokio::time::timeout_at(deadline, self.topology_ddl_lock.write())
761 .await
762 .map_err(|_| {
763 DbError::Pipeline(
764 "topology installation exceeded its lifecycle deadline".into(),
765 )
766 })?,
767 None => self.topology_ddl_lock.write().await,
768 };
769 let lifecycle = match deadline {
770 Some(deadline) => tokio::time::timeout_at(deadline, self.lifecycle_lock.lock())
771 .await
772 .map_err(|_| {
773 DbError::Pipeline(
774 "topology installation exceeded its lifecycle deadline".into(),
775 )
776 })?,
777 None => self.lifecycle_lock.lock().await,
778 };
779 Ok((topology, lifecycle))
780 }
781
782 #[cfg(feature = "cluster")]
783 fn fail_catalog_bootstrap_start(&self, error: DbError) -> Result<(), DbError> {
784 if let Err(cleanup_error) =
785 self.ensure_catalog_cleanup_unfenced("catalog bootstrap rollback")
786 {
787 DbState::Faulted.store(&self.state);
791 if error.requires_pipeline_halt() {
792 tracing::error!(
793 %cleanup_error,
794 "catalog rollback also failed after a permanent startup error"
795 );
796 return Err(error);
797 }
798 if cleanup_error.requires_pipeline_halt() {
799 return Err(DbError::PipelineTerminal(format!(
800 "{error}; catalog bootstrap rollback remains terminally fenced: {cleanup_error}"
801 )));
802 }
803 return Err(DbError::Pipeline(format!(
804 "{error}; catalog bootstrap rollback remains terminally fenced: {cleanup_error}"
805 )));
806 }
807
808 if error.requires_pipeline_halt() {
811 DbState::Faulted.store(&self.state);
812 return Err(error);
813 }
814 match DbState::compare_exchange(DbState::Starting, DbState::Created, &self.state) {
815 Ok(_) => Err(error),
816 Err(observed) => Err(DbError::Pipeline(format!(
817 "{error}; catalog bootstrap rollback completed but startup was superseded by \
818 lifecycle state {observed:?}: {}",
819 self.last_fault()
820 .unwrap_or_else(|| "no fault reason was recorded".into())
821 ))),
822 }
823 }
824}