1use super::{
2 checked_pipeline_deadline, panic_message, publish_runtime_fault_state, required_recovery_scope,
3 Arc, CheckpointStorageScope, DbError, DbState, DeliveryGuarantee, FutureExt, HashMap,
4 LaminarDB, PipelineLifecycleAuthority, RuntimeMode, StartupAttempt, StartupDriverGuard,
5};
6#[cfg(feature = "cluster")]
7use super::{report_cluster_terminal_halt, retire_cluster_compute_generation};
8use laminar_core::storage_location::StorageProvider;
9
10fn checkpoint_store(
11 backing: Arc<dyn object_store::ObjectStore>,
12 max_node_data_bytes: u64,
13 key_group_count: laminar_core::state::KeyGroupCount,
14 participant_id: u64,
15 exclusive_writer: bool,
16) -> Result<Box<dyn laminar_core::checkpoint::CheckpointStore>, DbError> {
17 let store = laminar_core::checkpoint::ObjectStoreCheckpointStore::new(backing, "")
18 .with_max_node_data_bytes(max_node_data_bytes)?
19 .with_key_group_count(key_group_count)
20 .with_participant_id(participant_id);
21 let store = if exclusive_writer {
22 store.with_exclusive_writer()
23 } else {
24 store
25 };
26 Ok(Box::new(store))
27}
28
29fn validate_checkpoint_timing(
30 config: &laminar_core::streaming::StreamCheckpointConfig,
31) -> Result<(), DbError> {
32 if config.interval_ms == Some(0) {
33 return Err(DbError::Config(
34 "checkpoint.interval_ms must be greater than zero; use None for manual-only".into(),
35 ));
36 }
37 let Some(timeout_ms) = config.timeout_ms else {
38 return Ok(());
39 };
40 if timeout_ms == 0 {
41 return Err(DbError::Config(
42 "checkpoint.timeout_ms must be greater than zero".into(),
43 ));
44 }
45 checked_pipeline_deadline(std::time::Duration::from_millis(timeout_ms), "checkpoint").map_err(
46 |_| DbError::Config("checkpoint.timeout_ms exceeds the platform clock range".into()),
47 )?;
48 Ok(())
49}
50
51impl LaminarDB {
52 pub(super) fn finish_start_transition(&self) -> Result<(), DbError> {
58 let runtime_shutdown = self.runtime_shutdown.read();
61 if self.is_closed() || runtime_shutdown.is_cancelled() {
62 return match DbState::compare_exchange(
63 DbState::Starting,
64 DbState::Created,
65 &self.state,
66 ) {
67 Ok(_) | Err(DbState::Created) => Err(DbError::Shutdown),
68 Err(DbState::Faulted) => Err(DbError::Pipeline(format!(
69 "pipeline faulted while its cancelled generation was leaving startup: {}",
70 self.last_fault()
71 .unwrap_or_else(|| "compute loop exited without a fault reason".into())
72 ))),
73 Err(observed) => Err(DbError::InvalidOperation(format!(
74 "cancelled pipeline startup completed from an unexpected lifecycle state: {observed:?}"
75 ))),
76 };
77 }
78 match DbState::compare_exchange(DbState::Starting, DbState::Running, &self.state) {
79 Ok(_) => Ok(()),
80 Err(DbState::Faulted) => Err(DbError::Pipeline(format!(
81 "pipeline faulted while entering the runtime control loop: {}",
82 self.last_fault()
83 .unwrap_or_else(|| "compute loop exited without a fault reason".into())
84 ))),
85 Err(observed) => Err(DbError::InvalidOperation(format!(
86 "pipeline startup completed from an unexpected lifecycle state: {observed:?}"
87 ))),
88 }
89 }
90
91 pub async fn start(self: &Arc<Self>) -> Result<(), DbError> {
98 self.start_with_lifecycle_authority(PipelineLifecycleAuthority::Public)
99 .await
100 }
101
102 #[cfg(feature = "cluster")]
106 pub(crate) async fn start_for_coordinated_recovery(self: &Arc<Self>) -> Result<(), DbError> {
107 self.ensure_terminal_halt_allows_start(PipelineLifecycleAuthority::CoordinatedRecovery)?;
108 *self.startup_checkpoint_artifact_audit.lock() = None;
110 self.start_with_lifecycle_authority(PipelineLifecycleAuthority::CoordinatedRecovery)
111 .await
112 }
113
114 pub(super) async fn start_with_lifecycle_authority(
115 self: &Arc<Self>,
116 authority: PipelineLifecycleAuthority,
117 ) -> Result<(), DbError> {
118 if self.is_closed() {
119 return Err(DbError::Shutdown);
120 }
121 self.ensure_terminal_halt_allows_start(authority)?;
122 #[cfg(feature = "cluster")]
123 self.ensure_pipeline_lifecycle_authorized(authority, "start")?;
124 #[cfg(not(feature = "cluster"))]
125 Self::ensure_pipeline_lifecycle_authorized(authority, "start");
126 self.connector_registry.freeze();
127 let runtime = self.control_runtime.handle()?;
128 let attempt = {
129 let mut owned = self.startup_attempt.lock();
130 self.ensure_terminal_halt_allows_start(authority)?;
131 #[cfg(feature = "cluster")]
132 self.ensure_pipeline_lifecycle_authorized(authority, "start")?;
133 #[cfg(not(feature = "cluster"))]
134 Self::ensure_pipeline_lifecycle_authorized(authority, "start");
135 loop {
136 if let Some(in_flight) = owned.as_ref().filter(|attempt| !attempt.is_complete()) {
140 break Arc::clone(in_flight);
141 }
142 match DbState::load(&self.state) {
143 DbState::Running => return Ok(()),
144 DbState::Starting => {
145 break owned.clone().ok_or_else(|| {
146 DbError::Pipeline(
147 "pipeline is Starting without an owned startup attempt; restart is fenced"
148 .into(),
149 )
150 })?;
151 }
152 DbState::Stopped => {
153 return Err(DbError::InvalidOperation(
154 "Cannot start a stopped pipeline. Create a new LaminarDB instance."
155 .into(),
156 ));
157 }
158 DbState::ShuttingDown => {
159 return Err(DbError::InvalidOperation(
160 "cannot start pipeline: shutdown/stop in progress".into(),
161 ));
162 }
163 claimed @ (DbState::Created | DbState::Faulted) => {
164 self.ensure_terminal_halt_allows_start(authority)?;
168 #[cfg(feature = "cluster")]
169 self.ensure_pipeline_lifecycle_authorized(authority, "start")?;
170 #[cfg(not(feature = "cluster"))]
171 Self::ensure_pipeline_lifecycle_authorized(authority, "start");
172 let attempt = Arc::new(StartupAttempt::new());
173 *owned = Some(Arc::clone(&attempt));
176 let (start_tx, start_rx) = std::sync::mpsc::sync_channel(1);
177 let db = Arc::clone(self);
178 let driver_attempt = Arc::clone(&attempt);
179 let emergency_attempt = Arc::clone(&attempt);
180 let driver_runtime = runtime.clone();
181 let startup_thread = match std::thread::Builder::new()
182 .name("laminar-start".into())
183 .spawn(move || {
184 if !matches!(start_rx.recv(), Ok(true)) {
185 return;
186 }
187 let result =
188 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
189 driver_runtime.block_on(db.clone().drive_start_attempt(
190 driver_attempt,
191 claimed == DbState::Faulted,
192 authority,
193 ));
194 }));
195 if result.is_err() && !emergency_attempt.is_complete() {
196 let message =
197 "startup owner thread panicked before terminal cleanup";
198 db.last_fault.lock().get_or_insert(message.into());
199 DbState::Faulted.store(&db.state);
200 emergency_attempt
201 .complete(Err(DbError::Pipeline(message.into())));
202 }
203 }) {
204 Ok(thread) => thread,
205 Err(error) => {
206 *owned = None;
207 return Err(DbError::Pipeline(format!(
208 "failed to spawn startup owner thread: {error}"
209 )));
210 }
211 };
212 drop(startup_thread);
214 if DbState::compare_exchange(claimed, DbState::Starting, &self.state)
215 .is_err()
216 {
217 let _ = start_tx.send(false);
218 *owned = None;
219 continue;
220 }
221 if start_tx.send(true).is_err() {
222 let message = "startup owner thread exited before accepting ownership";
223 self.last_fault.lock().get_or_insert(message.into());
224 DbState::Faulted.store(&self.state);
225 attempt.complete(Err(DbError::Pipeline(message.into())));
226 }
227 break attempt;
228 }
229 }
230 }
231 };
232 attempt.wait().await
233 }
234
235 pub(super) async fn drive_start_attempt(
236 self: Arc<Self>,
237 attempt: Arc<StartupAttempt>,
238 starting_from_fault: bool,
239 authority: PipelineLifecycleAuthority,
240 ) {
241 let terminal = StartupDriverGuard::new(&self, Arc::clone(&attempt));
242 let result =
243 std::panic::AssertUnwindSafe(Box::pin(self.run_claimed_start(starting_from_fault)))
244 .catch_unwind()
245 .await;
246 let result = match result {
247 Ok(result) => result,
248 Err(panic) => {
249 let reason = format!("startup driver panicked: {}", panic_message(panic.as_ref()));
250 *self.last_fault.lock() = Some(reason.clone());
251 let cleanup = std::panic::AssertUnwindSafe(self.cleanup_failed_start())
252 .catch_unwind()
253 .await;
254 DbState::Faulted.store(&self.state);
255 match cleanup {
256 Ok(Ok(())) => Err(DbError::Pipeline(reason)),
257 Ok(Err(error)) if error.requires_pipeline_halt() => {
258 tracing::error!(
259 panic = %reason,
260 cleanup_error = %error,
261 "terminal cleanup error superseded a generic startup panic"
262 );
263 Err(error)
264 }
265 Ok(Err(error)) => Err(DbError::Pipeline(format!(
266 "{reason}; failed-start cleanup remains fenced: {error}"
267 ))),
268 Err(cleanup_panic) => Err(DbError::Pipeline(format!(
269 "{reason}; failed-start cleanup panicked: {}",
270 panic_message(cleanup_panic.as_ref())
271 ))),
272 }
273 }
274 };
275 let result = self.normalize_start_result_for_terminal_latch(result);
276 self.terminalize_start_attempt_if_needed(authority, &result)
277 .await;
278 terminal.finish(result);
279 }
280
281 pub(super) fn normalize_start_result_for_terminal_latch(
282 &self,
283 result: Result<(), DbError>,
284 ) -> Result<(), DbError> {
285 if !self
286 .terminal_pipeline_halt
287 .load(std::sync::atomic::Ordering::Acquire)
288 {
289 return result;
290 }
291 match result {
292 Err(error) if error.requires_pipeline_halt() => Err(error),
293 Ok(()) | Err(_) => Err(DbError::PipelineTerminal(
294 self.last_fault()
295 .unwrap_or_else(|| "terminal startup failure".into()),
296 )),
297 }
298 }
299
300 #[cfg_attr(
304 not(feature = "cluster"),
305 allow(unknown_lints, clippy::unused_async, clippy::unused_async_trait_impl)
306 )]
307 pub(super) async fn terminalize_start_attempt_if_needed(
308 &self,
309 authority: PipelineLifecycleAuthority,
310 result: &Result<(), DbError>,
311 ) {
312 let Err(error) = result else {
313 return;
314 };
315 if !error.requires_pipeline_halt() {
316 return;
317 }
318 let _ = authority;
319 let reason = if self
320 .terminal_pipeline_halt
321 .load(std::sync::atomic::Ordering::Acquire)
322 {
323 self.last_fault().unwrap_or_else(|| error.to_string())
324 } else {
325 error.to_string()
326 };
327 *self.last_fault.lock() = Some(reason.clone());
328
329 #[cfg(feature = "cluster")]
330 if self.is_cluster_runtime() {
331 self.latch_local_terminal_pipeline_halt();
332 let controller = self.cluster_controller.lock().clone();
333 if let Some(controller) = controller.as_deref() {
334 controller.set_recovering(true);
335 if let Err(publication_error) = crate::coordinated_recovery::queue_local_fault(
336 controller,
337 &self.pending_recovery_fault,
338 ) {
339 tracing::error!(
340 %publication_error,
341 "could not retain terminal startup fault request"
342 );
343 }
344 }
345 let generation = Arc::clone(&self.rotation_execution_fence)
346 .write_owned()
347 .await;
348 retire_cluster_compute_generation(
349 &self.pending_vnode_transition,
350 &self.installed_vnode_state,
351 );
352 publish_runtime_fault_state(&self.state);
353 drop(generation);
354 tracing::error!(
355 %reason,
356 "pipeline startup hit a permanent error; awaiting durable terminal fencing"
357 );
358 report_cluster_terminal_halt(controller, Arc::clone(&self.pending_recovery_fault))
359 .await;
360 self.latch_durable_terminal_recovery_fence();
361 return;
362 }
363
364 self.terminal_pipeline_halt
365 .store(true, std::sync::atomic::Ordering::SeqCst);
366 publish_runtime_fault_state(&self.state);
367 tracing::error!(
368 %reason,
369 "pipeline startup hit a permanent error; automatic restart is disabled"
370 );
371 }
372
373 pub(super) async fn run_claimed_start(&self, starting_from_fault: bool) -> Result<(), DbError> {
374 const FAULT_RESTART_QUIESCE_TIMEOUT: std::time::Duration =
375 std::time::Duration::from_secs(10);
376 let _topology = self.topology_ddl_lock.write().await;
377 let _lifecycle = self.lifecycle_lock.lock().await;
378 self.ensure_catalog_cleanup_unfenced("pipeline start")?;
379 if DbState::load(&self.state) != DbState::Starting {
380 return Err(DbError::Pipeline(
381 "startup ownership was superseded before the driver entered the lifecycle".into(),
382 ));
383 }
384
385 let generation_quiesce_deadline =
386 tokio::time::Instant::now() + FAULT_RESTART_QUIESCE_TIMEOUT;
387 if let Err(error) = self
388 .quiesce_connector_generation_until(generation_quiesce_deadline)
389 .await
390 {
391 if starting_from_fault || error.requires_pipeline_halt() {
392 DbState::Faulted.store(&self.state);
393 } else {
394 DbState::Created.store(&self.state);
395 }
396 return Err(error);
397 }
398
399 if starting_from_fault {
400 let deadline = tokio::time::Instant::now() + FAULT_RESTART_QUIESCE_TIMEOUT;
401 if let Err(error) = self.quiesce_checkpoint_decision_until(deadline).await {
402 DbState::Faulted.store(&self.state);
405 return Err(error);
406 }
407 }
408
409 *self.last_fault.lock() = None;
412 {
413 let mut guard = self.engine_metrics.lock();
414 if guard.is_none() {
415 *guard = Some(Arc::new(crate::engine_metrics::EngineMetrics::new(
416 &prometheus::Registry::new(),
417 )));
418 }
419 }
420
421 #[cfg(feature = "cluster")]
422 if let Err(error) = self.restore_catalog_from_manifest().await {
423 if let Err(cleanup_error) =
424 self.ensure_catalog_cleanup_unfenced("catalog bootstrap rollback")
425 {
426 DbState::Faulted.store(&self.state);
430 if error.requires_pipeline_halt() {
431 tracing::error!(
432 %cleanup_error,
433 "catalog rollback also failed after a permanent startup error"
434 );
435 return Err(error);
436 }
437 if cleanup_error.requires_pipeline_halt() {
438 return Err(DbError::PipelineTerminal(format!(
439 "{error}; catalog bootstrap rollback remains terminally fenced: {cleanup_error}"
440 )));
441 }
442 return Err(DbError::Pipeline(format!(
443 "{error}; catalog bootstrap rollback remains terminally fenced: {cleanup_error}"
444 )));
445 }
446
447 if error.requires_pipeline_halt() {
450 DbState::Faulted.store(&self.state);
451 return Err(error);
452 }
453 return match DbState::compare_exchange(DbState::Starting, DbState::Created, &self.state)
454 {
455 Ok(_) => Err(error),
456 Err(observed) => Err(DbError::Pipeline(format!(
457 "{error}; catalog bootstrap rollback completed but startup was superseded by \
458 lifecycle state {observed:?}: {}",
459 self.last_fault()
460 .unwrap_or_else(|| "no fault reason was recorded".into())
461 ))),
462 };
463 }
464
465 tokio::select! {
468 biased;
469 () = self.shutdown_signal.notified() => {}
470 () = std::future::ready(()) => {}
471 }
472
473 match self.start_inner().await {
474 Ok(()) => {
475 match self.finish_start_transition() {
478 Ok(()) => Ok(()),
479 Err(error) => match self.cleanup_failed_start().await {
480 Ok(()) => {
481 if error.requires_pipeline_halt() {
482 DbState::Faulted.store(&self.state);
483 }
484 Err(error)
485 }
486 Err(cleanup_error) => {
487 DbState::Faulted.store(&self.state);
488 if error.requires_pipeline_halt() {
489 tracing::error!(
490 %cleanup_error,
491 "failed-start cleanup also failed after a permanent startup error"
492 );
493 Err(error)
494 } else if cleanup_error.requires_pipeline_halt() {
495 tracing::error!(
496 startup_error = %error,
497 %cleanup_error,
498 "terminal cleanup error superseded a generic startup failure"
499 );
500 Err(cleanup_error)
501 } else {
502 Err(DbError::Pipeline(format!(
503 "{error}; failed-start cleanup remains fenced: {cleanup_error}"
504 )))
505 }
506 }
507 },
508 }
509 }
510 Err(e) => {
511 match self.cleanup_failed_start().await {
512 Ok(()) => {
513 if e.requires_pipeline_halt() {
514 DbState::Faulted.store(&self.state);
515 } else {
516 DbState::Created.store(&self.state);
518 }
519 Err(e)
520 }
521 Err(cleanup_error) => {
522 DbState::Faulted.store(&self.state);
523 if e.requires_pipeline_halt() {
524 tracing::error!(
525 %cleanup_error,
526 "failed-start cleanup also failed after a permanent startup error"
527 );
528 Err(e)
529 } else if cleanup_error.requires_pipeline_halt() {
530 tracing::error!(
531 startup_error = %e,
532 %cleanup_error,
533 "terminal cleanup error superseded a generic startup failure"
534 );
535 Err(cleanup_error)
536 } else {
537 Err(DbError::Pipeline(format!(
538 "{e}; failed-start cleanup remains fenced: {cleanup_error}"
539 )))
540 }
541 }
542 }
543 }
544 }
545 }
546
547 pub(super) fn validate_startup_durability(
548 &self,
549 startup_runtime: RuntimeMode,
550 ) -> Result<Option<Arc<dyn object_store::ObjectStore>>, DbError> {
551 #[cfg(feature = "cluster")]
552 if startup_runtime == RuntimeMode::Cluster
553 && (!self.mv_registry.lock().is_empty() || !self.mv_store.read().is_empty())
554 {
555 return Err(DbError::InvalidOperation(format!(
556 "[{}] cluster startup found materialized state without a planner-certified distribution and assignment-fenced checkpoint/read lifecycle",
557 laminar_core::error_codes::CLUSTER_STATE_LIFECYCLE_UNSUPPORTED,
558 )));
559 }
560
561 #[cfg(feature = "cluster")]
562 let has_injected_decision_store = self.decision_store.lock().is_some();
563 #[cfg(not(feature = "cluster"))]
564 let has_injected_decision_store = false;
565 if startup_runtime == RuntimeMode::Local
566 && self.config.delivery_guarantee != DeliveryGuarantee::BestEffort
567 && (self.config.object_store_url.as_deref().is_some_and(|url| {
568 StorageProvider::detect_uri(url) != Some(StorageProvider::Local)
569 }) || has_injected_decision_store)
570 {
571 return Err(DbError::Config(
572 "[LDB-0014] a local replay-capable deployment with a shared cloud checkpoint \
573 namespace or injected decision store is not admitted until its writer lease is \
574 term-fenced. Use a built-in or file:// local checkpoint directory, or \
575 best_effort delivery"
576 .into(),
577 ));
578 }
579
580 #[cfg(feature = "cluster")]
581 let injected_cluster_checkpoint_store = self.cluster_checkpoint_object_store();
582 #[cfg(not(feature = "cluster"))]
583 let injected_cluster_checkpoint_store: Option<Arc<dyn object_store::ObjectStore>> = None;
584
585 if self.config.checkpoint.is_some()
586 && self.config.delivery_guarantee != DeliveryGuarantee::BestEffort
587 {
588 let checkpoint_scope = if injected_cluster_checkpoint_store.is_some() {
592 CheckpointStorageScope::ClusterShared
593 } else {
594 match self.config.object_store_url.as_deref() {
595 Some(url) => CheckpointStorageScope::for_url(url),
596 None => CheckpointStorageScope::NodeDurable,
597 }
598 };
599 let required = required_recovery_scope(startup_runtime);
600 if !checkpoint_scope.satisfies(required) {
601 return Err(DbError::Config(format!(
602 "[LDB-5036] {startup_runtime:?} {:?} delivery requires {required:?} \
603 checkpoint/decision storage, but the configured checkpoint store is \
604 {checkpoint_scope:?}; use the built-in checkpoint data_dir for \
605 node-local recovery, or a supported shared object store",
606 self.config.delivery_guarantee
607 )));
608 }
609 }
610
611 Ok(injected_cluster_checkpoint_store)
612 }
613
614 pub(super) async fn initialize_checkpointing(
615 &self,
616 source_regs: &HashMap<String, crate::connector_manager::SourceRegistration>,
617 sink_regs: &HashMap<String, crate::connector_manager::SinkRegistration>,
618 stream_regs: &HashMap<String, crate::connector_manager::StreamRegistration>,
619 table_regs: &HashMap<String, crate::connector_manager::TableRegistration>,
620 startup_runtime: RuntimeMode,
621 injected_cluster_checkpoint_store: Option<Arc<dyn object_store::ObjectStore>>,
622 ) -> Result<Option<laminar_core::checkpoint::PipelineIdentity>, DbError> {
623 let participant = self.checkpoint_participant();
624 let bound_pipeline_identity =
625 if self.config.checkpoint.is_some() || startup_runtime == RuntimeMode::Cluster {
626 let identity_registrations = crate::pipeline_identity::PipelineRegistrations::new(
627 source_regs.values(),
628 sink_regs.values(),
629 stream_regs.values(),
630 table_regs.values(),
631 );
632 let identity_context = crate::pipeline_identity::PipelineIdentityContext::new(
633 &self.config,
634 &self.catalog,
635 &self.connector_registry,
636 identity_registrations,
637 self.checkpoint_key_groups().get(),
638 );
639 Some(crate::pipeline_identity::compute(&identity_context)?)
640 } else {
641 None
642 };
643 if let Some(ref cp_config) = self.config.checkpoint {
644 use crate::checkpoint_coordinator::{
645 CheckpointConfig as CkpConfig, CheckpointCoordinator,
646 };
647
648 let max_node_data_bytes = cp_config.max_node_data_bytes.ok_or_else(|| {
649 DbError::Config(
650 "checkpoint.max_node_data_bytes was not resolved at construction".into(),
651 )
652 })?;
653 validate_checkpoint_timing(cp_config)?;
654 let key_group_count = self.checkpoint_key_groups();
655
656 let data_dir = cp_config
657 .data_dir
658 .clone()
659 .or_else(|| self.config.storage_dir.clone())
660 .unwrap_or_else(|| std::path::PathBuf::from("./data"));
661 let explicit_file_checkpoint_root = self
662 .config
663 .object_store_url
664 .as_deref()
665 .filter(|url| StorageProvider::detect_uri(url) == Some(StorageProvider::Local))
666 .map(|url| {
667 laminar_core::checkpoint::object_store_builder::file_url_path(url)
668 .map_err(|error| DbError::Config(format!("object store: {error}")))
669 })
670 .transpose()?;
671 let local_checkpoint_root = explicit_file_checkpoint_root.as_ref().unwrap_or(&data_dir);
672 let uses_local_checkpoint_store = injected_cluster_checkpoint_store.is_none()
673 && (self.config.object_store_url.is_none()
674 || explicit_file_checkpoint_root.is_some());
675 if startup_runtime == RuntimeMode::Local
676 && uses_local_checkpoint_store
677 && self.checkpoint_namespace_lock.lock().is_none()
678 {
679 laminar_core::durable_fs::ensure_durable_directory(local_checkpoint_root).map_err(
680 |error| {
681 DbError::Config(format!(
682 "create local checkpoint directory {}: {error}",
683 local_checkpoint_root.display()
684 ))
685 },
686 )?;
687 let lock_path = local_checkpoint_root.join(".laminardb-checkpoint.lock");
688 let lock = std::fs::OpenOptions::new()
689 .read(true)
690 .write(true)
691 .create(true)
692 .truncate(false)
693 .open(&lock_path)
694 .map_err(|error| {
695 DbError::Config(format!(
696 "[LDB-0014] open checkpoint namespace lock {}: {error}",
697 lock_path.display()
698 ))
699 })?;
700 lock.try_lock().map_err(|error| {
701 DbError::Config(format!(
702 "[LDB-0014] checkpoint namespace {} is already owned by \
703 another live process: {error}",
704 local_checkpoint_root.display()
705 ))
706 })?;
707 *self.checkpoint_namespace_lock.lock() = Some(lock);
708 }
709 let participant_id = participant.unwrap_or(laminar_core::state::LOCAL_NODE_ID.0);
710 let pipeline_identity = bound_pipeline_identity.clone().ok_or_else(|| {
711 DbError::Checkpoint(
712 "checkpoint startup did not derive the pipeline identity".into(),
713 )
714 })?;
715
716 let checkpoint_backing = self
717 .checkpoint_object_store()?
718 .ok_or_else(|| DbError::Checkpoint("checkpoint object store is disabled".into()))?;
719 let probe_timeout = std::time::Duration::from_secs(10);
720 let probe = if uses_local_checkpoint_store {
721 laminar_core::checkpoint::probe_object_store_conditional_create(
722 checkpoint_backing.as_ref(),
723 "",
724 probe_timeout,
725 )
726 .await
727 } else {
728 laminar_core::checkpoint::probe_object_store_conditional_update(
729 checkpoint_backing.as_ref(),
730 "",
731 probe_timeout,
732 )
733 .await
734 };
735 probe.map_err(|error| {
736 DbError::Config(format!(
737 "checkpoint object store does not provide required conditional writes: {error}"
738 ))
739 })?;
740 let store = checkpoint_store(
741 Arc::clone(&checkpoint_backing),
742 max_node_data_bytes,
743 key_group_count,
744 participant_id,
745 uses_local_checkpoint_store,
746 )?;
747 let decision_backing = (!uses_local_checkpoint_store).then_some(checkpoint_backing);
748
749 let defaults = CkpConfig::default();
750 let config = CkpConfig {
751 checkpoint_timeout: cp_config.timeout_ms.map_or(
752 defaults.checkpoint_timeout,
753 std::time::Duration::from_millis,
754 ),
755 max_node_data_bytes,
756 ..defaults
757 };
758 let mut coord = CheckpointCoordinator::new(config, store)?;
759 coord.bind_pipeline_identity(pipeline_identity.clone())?;
760 if let Some(ref prom) = *self.engine_metrics.lock() {
761 coord.set_metrics(Arc::clone(prom));
762 }
763
764 #[cfg(feature = "cluster")]
765 if let Some(controller) = self.cluster_controller.lock().clone() {
766 if coord.participant_id() != controller.instance_id().0 {
767 return Err(DbError::Config(format!(
768 "[LDB-0012] checkpoint store participant {} does not match cluster \
769 instance {}",
770 coord.participant_id(),
771 controller.instance_id().0
772 )));
773 }
774 coord.set_cluster_controller(controller);
775 }
776
777 let ds = {
778 #[cfg(feature = "cluster")]
779 {
780 if let Some(injected) = self.decision_store.lock().clone() {
781 injected
782 } else if let Some(backing) = decision_backing.as_ref() {
783 Arc::new(
784 laminar_core::checkpoint_decision::CheckpointDecisionStore::new(
785 Arc::clone(backing),
786 ),
787 )
788 } else {
789 Arc::new(
790 laminar_core::checkpoint_decision::CheckpointDecisionStore::local_filesystem(
791 local_checkpoint_root,
792 )
793 .map_err(|error| {
794 DbError::Config(format!(
795 "open durable local checkpoint metadata store: {error}"
796 ))
797 })?,
798 )
799 }
800 }
801 #[cfg(not(feature = "cluster"))]
802 {
803 if let Some(backing) = decision_backing.as_ref() {
804 Arc::new(
805 laminar_core::checkpoint_decision::CheckpointDecisionStore::new(
806 Arc::clone(backing),
807 ),
808 )
809 } else {
810 Arc::new(
811 laminar_core::checkpoint_decision::CheckpointDecisionStore::local_filesystem(
812 local_checkpoint_root,
813 )
814 .map_err(|error| {
815 DbError::Config(format!(
816 "open durable local checkpoint metadata store: {error}"
817 ))
818 })?,
819 )
820 }
821 }
822 };
823 let deployment_id = ds.load_or_create_deployment_id().await.map_err(|error| {
824 DbError::Checkpoint(format!(
825 "load/create durable deployment identity before checkpoint startup: {error}"
826 ))
827 })?;
828 coord.set_decision_store(ds)?;
829 coord.bind_deployment_id(deployment_id.clone())?;
830
831 let vnode_registry = self.vnode_registry.lock().clone();
832 if let Some(registry) = vnode_registry {
833 let owner = {
834 #[cfg(feature = "cluster")]
835 {
836 self.cluster_controller
837 .lock()
838 .as_ref()
839 .map_or(laminar_core::state::LOCAL_NODE_ID, |c| {
840 laminar_core::state::NodeId(c.instance_id().0)
841 })
842 }
843 #[cfg(not(feature = "cluster"))]
844 {
845 laminar_core::state::LOCAL_NODE_ID
846 }
847 };
848 let version = registry.assignment_version();
849 coord.set_assignment_version(version);
850 if startup_runtime == RuntimeMode::Cluster {
851 coord.set_vnode_set(laminar_core::state::owned_vnodes(®istry, owner));
852 }
853 }
854
855 *self.coordinator.lock().await = Some(coord);
856 }
857 Ok(bound_pipeline_identity)
858 }
859}