1#![allow(clippy::disallowed_types)] #[cfg(feature = "cluster")]
5mod assignment_authority;
6#[cfg(feature = "cluster")]
7mod cluster_subscription;
8#[cfg(feature = "cluster")]
9pub(crate) use assignment_authority::{
10 audited_stopped_recovery_successor_round, audited_stopped_terminal_round,
11};
12
13use std::collections::HashMap;
14use std::sync::Arc;
15use std::time::{Duration, Instant};
16
17use arrow::array::{Array, BinaryArray, RecordBatch, StringArray};
18use arrow::datatypes::{DataType, Field, Schema};
19use datafusion::prelude::SessionContext;
20use laminar_core::catalog::CatalogObjectKind;
21use laminar_core::streaming;
22use laminar_sql::parser::{parse_streaming_sql, ShowCommand, StreamingStatement};
23use laminar_sql::planner::StreamingPlanner;
24use laminar_sql::register_streaming_functions;
25
26use crate::builder::LaminarDbBuilder;
27use crate::catalog::SourceCatalog;
28use crate::config::LaminarConfig;
29use crate::error::DbError;
30use crate::handle::{
31 DdlInfo, ExecuteResult, QueryHandle, QueryInfo, SinkInfo, SourceHandle, SourceInfo,
32 UntypedSourceHandle,
33};
34use crate::pipeline::ControlMsg;
35use crate::sql_utils;
36
37pub(crate) type ControlMsgTx = crossfire::MAsyncTx<crossfire::mpsc::Array<ControlMsg>>;
39
40#[repr(u8)]
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub(crate) enum DbState {
44 Created = 0,
45 Starting = 1,
46 Running = 2,
47 ShuttingDown = 3,
48 Stopped = 4,
49 Faulted = 5,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub(crate) enum RuntimeMode {
61 Local,
62 Cluster,
63}
64
65impl RuntimeMode {
66 pub(crate) const fn is_cluster(self) -> bool {
67 matches!(self, Self::Cluster)
68 }
69}
70
71impl DbState {
72 pub(crate) fn from_u8(raw: u8) -> Option<Self> {
73 Some(match raw {
74 0 => Self::Created,
75 1 => Self::Starting,
76 2 => Self::Running,
77 3 => Self::ShuttingDown,
78 4 => Self::Stopped,
79 5 => Self::Faulted,
80 _ => return None,
81 })
82 }
83
84 pub(crate) fn load(atomic: &std::sync::atomic::AtomicU8) -> Self {
85 Self::from_u8(atomic.load(std::sync::atomic::Ordering::Acquire)).unwrap_or(Self::Stopped)
86 }
87
88 pub(crate) fn store(self, atomic: &std::sync::atomic::AtomicU8) {
89 atomic.store(self as u8, std::sync::atomic::Ordering::Release);
90 }
91
92 pub(crate) fn compare_exchange(
94 current: Self,
95 new: Self,
96 atomic: &std::sync::atomic::AtomicU8,
97 ) -> Result<Self, Self> {
98 use std::sync::atomic::Ordering;
99 atomic
100 .compare_exchange(
101 current as u8,
102 new as u8,
103 Ordering::AcqRel,
104 Ordering::Acquire,
105 )
106 .map(|v| Self::from_u8(v).unwrap_or(Self::Stopped))
107 .map_err(|v| Self::from_u8(v).unwrap_or(Self::Stopped))
108 }
109}
110
111const DB_IO_WORKER_THREADS: usize = 2;
112const DB_IO_WORKER_STACK_BYTES: usize = 4 * 1024 * 1024;
116
117struct DbControlRuntimeInner {
118 handle: tokio::runtime::Handle,
119 shutdown: tokio::sync::oneshot::Sender<()>,
120}
121
122pub(crate) struct DbControlRuntime {
128 worker_stack_bytes: usize,
129 inner: parking_lot::Mutex<Option<DbControlRuntimeInner>>,
130}
131
132impl DbControlRuntime {
133 fn new() -> Self {
134 Self {
135 worker_stack_bytes: DB_IO_WORKER_STACK_BYTES,
136 inner: parking_lot::Mutex::new(None),
137 }
138 }
139
140 pub(crate) fn handle(&self) -> Result<tokio::runtime::Handle, DbError> {
141 let mut runtime_slot = self.inner.lock();
142 if let Some(runtime) = runtime_slot.as_ref() {
143 return Ok(runtime.handle.clone());
144 }
145
146 let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel(1);
147 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
148 let worker_stack_bytes = self.worker_stack_bytes;
149 let owner = std::thread::Builder::new()
150 .name("laminar-io-owner".into())
151 .spawn(move || {
152 let mut builder = tokio::runtime::Builder::new_multi_thread();
153 builder
154 .worker_threads(DB_IO_WORKER_THREADS)
155 .thread_name("laminar-io")
156 .thread_stack_size(worker_stack_bytes)
157 .enable_all();
158 let runtime = match builder.build() {
159 Ok(runtime) => runtime,
160 Err(error) => {
161 let _ = ready_tx.send(Err(error.to_string()));
162 return;
163 }
164 };
165 if ready_tx.send(Ok(runtime.handle().clone())).is_err() {
166 return;
167 }
168 runtime.block_on(async {
169 let _ = shutdown_rx.await;
170 });
171 runtime.shutdown_timeout(Duration::from_secs(10));
172 })
173 .map_err(|error| {
174 DbError::Pipeline(format!("failed to spawn LaminarDB I/O runtime: {error}"))
175 })?;
176 drop(owner);
178
179 let handle = ready_rx
180 .recv()
181 .map_err(|_| {
182 DbError::Pipeline("LaminarDB I/O runtime exited during initialization".into())
183 })?
184 .map_err(|error| {
185 DbError::Pipeline(format!(
186 "failed to initialize LaminarDB I/O runtime: {error}"
187 ))
188 })?;
189 *runtime_slot = Some(DbControlRuntimeInner {
190 handle: handle.clone(),
191 shutdown: shutdown_tx,
192 });
193 Ok(handle)
194 }
195}
196
197impl Drop for DbControlRuntime {
198 fn drop(&mut self) {
199 if let Some(runtime) = self.inner.get_mut().take() {
200 let _ = runtime.shutdown.send(());
201 }
202 }
203}
204
205pub(crate) fn canonical_object_name(name: &sqlparser::ast::ObjectName) -> Result<String, DbError> {
206 let [part] = name.0.as_slice() else {
207 return Err(DbError::InvalidOperation(format!(
208 "catalog identifiers must be unqualified: {name}"
209 )));
210 };
211 let ident = part.as_ident().ok_or_else(|| {
212 DbError::InvalidOperation(format!(
213 "dynamic catalog identifier is not supported: {name}"
214 ))
215 })?;
216 Ok(ident.value.clone())
217}
218
219pub(crate) fn exact_table_reference(name: &str) -> datafusion::common::TableReference {
220 datafusion::common::TableReference::bare(name)
222}
223
224fn validate_custom_function_name(
225 kind: &str,
226 name: &str,
227 aliases: &[String],
228) -> Result<(), DbError> {
229 const CORE_WINDOW_MARKERS: [&str; 5] = ["tumble", "tumble_end", "hop", "hop_end", "session"];
230 let collision = std::iter::once(name)
231 .chain(aliases.iter().map(String::as_str))
232 .find(|candidate| {
233 CORE_WINDOW_MARKERS
234 .iter()
235 .any(|marker| candidate.eq_ignore_ascii_case(marker))
236 });
237 if let Some(collision) = collision {
238 return Err(DbError::Config(format!(
239 "custom {kind} '{name}' uses reserved CoreWindow marker name or alias '{collision}'"
240 )));
241 }
242 Ok(())
243}
244
245pub struct LaminarDB {
249 runtime_mode: RuntimeMode,
250 pub(crate) catalog: Arc<SourceCatalog>,
251 pub(crate) planner: parking_lot::Mutex<StreamingPlanner>,
252 pub(crate) ctx: SessionContext,
253 custom_udfs: Vec<datafusion_expr::ScalarUDF>,
254 custom_udafs: Vec<datafusion_expr::AggregateUDF>,
255 pub(crate) config: LaminarConfig,
256 pub(crate) config_vars: Arc<HashMap<String, String>>,
257 pub(crate) shutdown: std::sync::atomic::AtomicBool,
258 pub(crate) coordinator:
259 Arc<tokio::sync::Mutex<Option<crate::checkpoint_coordinator::CheckpointCoordinator>>>,
260 pub(crate) connector_manager: parking_lot::Mutex<crate::connector_manager::ConnectorManager>,
261 pub(crate) connector_registry: Arc<laminar_connectors::registry::ConnectorRegistry>,
262 pub(crate) mv_registry: parking_lot::Mutex<laminar_core::mv::MvRegistry>,
263 pub(crate) table_store: Arc<parking_lot::RwLock<crate::table_store::TableStore>>,
264 pub(crate) state: Arc<std::sync::atomic::AtomicU8>,
265 pub(crate) last_fault: Arc<parking_lot::Mutex<Option<String>>>,
268 pub(crate) catalog_cleanup_fenced: std::sync::atomic::AtomicBool,
271 #[cfg(test)]
273 pub(crate) catalog_cleanup_deregister_fault: parking_lot::Mutex<Option<String>>,
274 pub(crate) supervisor_self: Arc<parking_lot::Mutex<std::sync::Weak<LaminarDB>>>,
276 pub(crate) restart_history: Arc<parking_lot::Mutex<Vec<Instant>>>,
278 pub(crate) lifecycle_lock: tokio::sync::Mutex<()>,
282 pub(crate) control_runtime: DbControlRuntime,
283 pub(crate) startup_attempt:
284 parking_lot::Mutex<Option<Arc<crate::pipeline_lifecycle::StartupAttempt>>>,
285 pub(crate) topology_ddl_lock: tokio::sync::RwLock<()>,
288 pub(crate) catalog_namespace: parking_lot::Mutex<HashMap<String, CatalogObjectKind>>,
290 #[cfg(test)]
291 pub(crate) topology_planning_gate:
292 parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
293 #[cfg(test)]
294 pub(crate) stop_after_claim_gate:
295 parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
296 #[cfg(all(test, feature = "cluster"))]
297 pub(crate) catalog_seal_gate:
298 parking_lot::Mutex<Option<(Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>)>>,
299 #[cfg(all(test, feature = "cluster"))]
301 pub(crate) compute_before_ready_panic: Arc<std::sync::atomic::AtomicBool>,
302 pub(crate) runtime_handle: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
305 pub(crate) checkpoint_namespace_lock: parking_lot::Mutex<Option<std::fs::File>>,
309 pub(crate) owned_sink_handles: Arc<parking_lot::Mutex<Vec<crate::sink_task::SinkTaskHandle>>>,
312 pub(crate) owned_source_tasks: crate::pipeline::streaming_coordinator::OwnedSourceTasks,
315 pub(crate) owned_connector_task_fences: crate::connector_task_fence::OwnedConnectorTaskFences,
317 pub(crate) shutdown_signal: Arc<tokio::sync::Notify>,
318 pub(crate) runtime_shutdown: parking_lot::RwLock<tokio_util::sync::CancellationToken>,
321 #[cfg(feature = "cluster")]
325 pub(crate) assignment_restore_shutdown: tokio_util::sync::CancellationToken,
326 pub(crate) engine_metrics:
327 parking_lot::Mutex<Option<Arc<crate::engine_metrics::EngineMetrics>>>,
328 #[cfg(feature = "cluster")]
331 pub(crate) checkpoint_barrier_timings:
332 Arc<crate::checkpoint_timing::CheckpointBarrierTimingLedger>,
333 pub(crate) prometheus_registry: parking_lot::Mutex<Option<Arc<prometheus::Registry>>>,
334 pub(crate) start_time: Instant,
335 pub(crate) session_properties: parking_lot::Mutex<HashMap<String, String>>,
336 pub(crate) pipeline_watermark: Arc<std::sync::atomic::AtomicI64>,
338 pub(crate) lookup_registry: Arc<laminar_sql::datafusion::LookupTableRegistry>,
339 pub(crate) ai_runtime: Option<Arc<crate::ai::AiRuntime>>,
341 pub(crate) ai_handle: Option<tokio::runtime::Handle>,
343 pub(crate) control_tx: parking_lot::Mutex<Option<ControlMsgTx>>,
345 pub(crate) mv_store: Arc<parking_lot::RwLock<crate::mv_store::MvStore>>,
346 #[cfg(feature = "cluster")]
348 pub(crate) cluster_controller:
349 parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::ClusterController>>>,
350 #[cfg(feature = "cluster")]
353 pub(crate) recover_target_epoch: parking_lot::Mutex<Option<u64>>,
354 #[cfg(feature = "cluster")]
357 pub(crate) recovery_monitor: parking_lot::Mutex<Option<tokio::task::JoinHandle<()>>>,
358 #[cfg(feature = "cluster")]
361 pub(crate) coordinated_lifecycle_active: Arc<std::sync::atomic::AtomicBool>,
362 #[cfg(feature = "cluster")]
367 pub(crate) coordinated_recovery_fenced: Arc<std::sync::atomic::AtomicBool>,
368 #[cfg(feature = "cluster")]
373 pub(crate) durable_terminal_recovery_fence: std::sync::atomic::AtomicBool,
374 pub(crate) terminal_pipeline_halt: Arc<std::sync::atomic::AtomicBool>,
377 #[cfg(feature = "cluster")]
380 pub(crate) pending_recovery_fault: Arc<std::sync::atomic::AtomicU64>,
381 #[cfg(feature = "cluster")]
385 pub(crate) startup_checkpoint_artifact_audit:
386 parking_lot::Mutex<Option<StartupCheckpointArtifactAudit>>,
387 #[cfg(feature = "cluster")]
390 pub(crate) last_recovery_epoch: parking_lot::Mutex<Option<u64>>,
391 #[cfg(feature = "cluster")]
396 pub(crate) source_gate: Arc<std::sync::atomic::AtomicBool>,
397 #[cfg(feature = "cluster")]
399 pub(crate) cluster_authority_revoked: std::sync::atomic::AtomicBool,
400 #[cfg(feature = "cluster")]
402 pub(crate) cluster_authority_transition: Arc<parking_lot::Mutex<()>>,
403 pub(crate) vnode_registry: parking_lot::Mutex<Option<Arc<laminar_core::state::VnodeRegistry>>>,
404 pub(crate) physical_optimizer_rules:
405 Arc<[Arc<dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync>]>,
406 pub(crate) pipeline_target_partitions: Option<usize>,
408 #[cfg(feature = "cluster")]
409 pub(crate) shuffle_sender:
410 parking_lot::Mutex<Option<Arc<laminar_core::shuffle::ShuffleSender>>>,
411 #[cfg(feature = "cluster")]
412 pub(crate) shuffle_receiver:
413 Arc<parking_lot::Mutex<Option<Arc<laminar_core::shuffle::ShuffleReceiver>>>>,
414 #[cfg(feature = "cluster")]
415 pub(crate) decision_store:
416 parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::CheckpointDecisionStore>>>,
417 #[cfg(feature = "cluster")]
418 pub(crate) assignment_snapshot_store:
419 parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::AssignmentSnapshotStore>>>,
420 #[cfg(feature = "cluster")]
422 pub(crate) catalog_manifest_store:
423 parking_lot::Mutex<Option<Arc<laminar_core::cluster::control::CatalogManifestStore>>>,
424 #[cfg(feature = "cluster")]
426 cluster_checkpoint_object_store: Option<Arc<dyn object_store::ObjectStore>>,
427 #[cfg(feature = "cluster")]
429 pub(crate) pending_vnode_transition:
430 crate::vnode_transition_staging::PendingVnodeTransitionHandle,
431 #[cfg(feature = "cluster")]
433 pub(crate) installed_vnode_state: crate::vnode_transition_staging::InstalledVnodeStateHandle,
434 #[cfg(feature = "cluster")]
438 pub(crate) assignment_adoption_lock: Arc<tokio::sync::Mutex<()>>,
439 #[cfg(feature = "cluster")]
442 pub(crate) assignment_authority_revision: std::sync::atomic::AtomicU64,
443 #[cfg(feature = "cluster")]
446 pub(crate) rotation_execution_fence: Arc<tokio::sync::RwLock<()>>,
447 pub(crate) force_ckpt_tx: parking_lot::Mutex<Option<ForceCheckpointTx>>,
450 pub(crate) subscription_registry: Arc<crate::subscription::SubscriptionRegistry>,
451 pub(crate) stream_schemas: parking_lot::RwLock<HashMap<String, arrow_schema::SchemaRef>>,
453}
454
455impl Drop for LaminarDB {
456 fn drop(&mut self) {
457 #[cfg(feature = "cluster")]
458 if let Some(monitor) = self.recovery_monitor.get_mut().take() {
459 monitor.abort();
460 }
461 if matches!(
462 DbState::load(&self.state),
463 DbState::Starting | DbState::Running | DbState::ShuttingDown | DbState::Faulted
464 ) {
465 if let Some(lock) = self.checkpoint_namespace_lock.get_mut().take() {
471 tracing::error!(
472 "dropping an active checkpoint writer; retaining its namespace lock until process exit"
473 );
474 std::mem::forget(lock);
475 }
476 }
477 }
478}
479
480#[cfg(feature = "cluster")]
481fn checkpoint_participant_for_runtime(db: &LaminarDB) -> Option<u64> {
482 if !db.is_cluster_runtime() {
483 return None;
484 }
485 db.cluster_controller
486 .lock()
487 .as_ref()
488 .map(|controller| controller.instance_id().0)
489}
490
491#[cfg(feature = "cluster")]
492tokio::task_local! {
493 static CATALOG_MANIFEST_REPLAY: ();
494}
495
496#[cfg(feature = "cluster")]
497tokio::task_local! {
498 static CATALOG_BOOTSTRAP: ();
499}
500
501#[cfg(feature = "cluster")]
502struct CatalogBootstrapGuard<'a> {
503 db: &'a LaminarDB,
504 created: Vec<(String, CatalogObjectKind)>,
505 sealed: bool,
506}
507
508#[cfg(feature = "cluster")]
509impl CatalogBootstrapGuard<'_> {
510 fn record(&mut self, name: String, kind: CatalogObjectKind) {
511 self.created.push((name, kind));
512 }
513
514 fn sealed(mut self) {
515 self.sealed = true;
516 }
517}
518
519#[cfg(feature = "cluster")]
520impl Drop for CatalogBootstrapGuard<'_> {
521 fn drop(&mut self) {
522 if !self.sealed {
523 for (name, kind) in self.created.iter().rev() {
524 self.db
525 .rollback_catalog_create_or_fence(name, *kind, "catalog bootstrap rollback");
526 }
527 }
528 }
529}
530
531#[cfg(feature = "cluster")]
532pub(crate) fn catalog_manifest_replay_active() -> bool {
533 CATALOG_MANIFEST_REPLAY.try_with(|()| ()).is_ok()
534}
535
536#[cfg(feature = "cluster")]
537fn catalog_bootstrap_active() -> bool {
538 CATALOG_BOOTSTRAP.try_with(|()| ()).is_ok()
539}
540
541#[cfg(not(feature = "cluster"))]
542pub(crate) const fn catalog_manifest_replay_active() -> bool {
543 false
544}
545
546fn is_topology_ddl(statement: &StreamingStatement) -> bool {
547 match statement {
548 StreamingStatement::CreateSource(_)
549 | StreamingStatement::CreateSink(_)
550 | StreamingStatement::CreateContinuousQuery { .. }
551 | StreamingStatement::DropSource { .. }
552 | StreamingStatement::DropSink { .. }
553 | StreamingStatement::DropMaterializedView { .. }
554 | StreamingStatement::CreateMaterializedView { .. }
555 | StreamingStatement::CreateStream { .. }
556 | StreamingStatement::DropStream { .. }
557 | StreamingStatement::AlterSource { .. }
558 | StreamingStatement::CreateLookupTable(_)
559 | StreamingStatement::DropLookupTable { .. } => true,
560 StreamingStatement::Standard(statement) => matches!(
561 statement.as_ref(),
562 sqlparser::ast::Statement::CreateTable(_)
563 | sqlparser::ast::Statement::Drop {
564 object_type: sqlparser::ast::ObjectType::Table,
565 ..
566 }
567 | sqlparser::ast::Statement::AlterTable { .. }
568 ),
569 _ => false,
570 }
571}
572
573fn mutates_database(statement: &StreamingStatement) -> bool {
574 is_topology_ddl(statement)
575 || matches!(
576 statement,
577 StreamingStatement::InsertInto { .. }
578 | StreamingStatement::Checkpoint
579 | StreamingStatement::RestoreCheckpoint { .. }
580 )
581 || matches!(
582 statement,
583 StreamingStatement::Standard(statement)
584 if matches!(statement.as_ref(), sqlparser::ast::Statement::Set(_))
585 )
586}
587
588fn reads_catalog(statement: &StreamingStatement) -> bool {
589 matches!(
590 statement,
591 StreamingStatement::Standard(statement)
592 if matches!(statement.as_ref(), sqlparser::ast::Statement::Query(_))
593 ) || matches!(statement, StreamingStatement::TemporalProbeQuery { .. })
594 || matches!(
595 statement,
596 StreamingStatement::InsertInto { .. }
597 | StreamingStatement::Show(_)
598 | StreamingStatement::Describe { .. }
599 | StreamingStatement::Explain { .. }
600 )
601}
602
603#[cfg(feature = "cluster")]
604fn catalog_create_identity(
605 statement: &StreamingStatement,
606) -> Result<Option<(String, CatalogObjectKind, &'static str)>, DbError> {
607 let identity = match statement {
608 StreamingStatement::CreateSource(create) => (
609 canonical_object_name(&create.name)?,
610 CatalogObjectKind::Source,
611 "CREATE SOURCE",
612 ),
613 StreamingStatement::CreateSink(create) => (
614 canonical_object_name(&create.name)?,
615 CatalogObjectKind::Sink,
616 "CREATE SINK",
617 ),
618 StreamingStatement::CreateMaterializedView { name, .. } => (
619 canonical_object_name(name)?,
620 CatalogObjectKind::MaterializedView,
621 "CREATE MATERIALIZED VIEW",
622 ),
623 StreamingStatement::CreateStream { name, .. } => (
624 canonical_object_name(name)?,
625 CatalogObjectKind::Stream,
626 "CREATE STREAM",
627 ),
628 StreamingStatement::CreateLookupTable(create) => (
629 canonical_object_name(&create.name)?,
630 CatalogObjectKind::LookupTable,
631 "CREATE LOOKUP TABLE",
632 ),
633 StreamingStatement::Standard(statement) => {
634 let sqlparser::ast::Statement::CreateTable(create) = statement.as_ref() else {
635 return Ok(None);
636 };
637 (
638 canonical_object_name(&create.name)?,
639 CatalogObjectKind::Table,
640 "CREATE TABLE",
641 )
642 }
643 _ => return Ok(None),
644 };
645 Ok(Some(identity))
646}
647
648#[cfg(feature = "cluster")]
649fn validate_cluster_catalog_create(
650 db: &LaminarDB,
651 sql: &str,
652 statement: &StreamingStatement,
653) -> Result<(String, CatalogObjectKind, &'static str), DbError> {
654 if let StreamingStatement::CreateStream {
655 query_sql,
656 emit_clause,
657 retention_bytes: Some(_),
658 ..
659 } = statement
660 {
661 let output = crate::subscription::distribution::plan(
662 query_sql,
663 emit_clause.as_ref(),
664 &db.ctx,
665 db.checkpoint_key_groups(),
666 )?;
667 if !output.as_ref().is_some_and(
668 crate::subscription::distribution::PlannedSubscriptionOutput::is_vnode_partitioned,
669 ) {
670 return Err(
671 crate::subscription::ClusterSubscriptionError::UnsupportedPlan {
672 reason:
673 "durable cluster history requires a non-windowed managed keyed aggregate"
674 .into(),
675 }
676 .into(),
677 );
678 }
679 }
680 if catalog_ddl_contains_comment(sql)? {
681 return Err(DbError::InvalidOperation(
682 "cluster catalog DDL cannot persist SQL comments; submit one canonical typed definition"
683 .into(),
684 ));
685 }
686 if let Some(key) = sensitive_catalog_property(statement) {
687 return Err(DbError::InvalidOperation(format!(
688 "cluster catalog DDL cannot persist secret property '{key}'; use $${{ENV_VAR}} in server TOML or ${{ENV_VAR}} through the SQL API (without a default), or omit it for a connector environment fallback"
689 )));
690 }
691 if connector_source_requires_schema_discovery(statement) {
692 return Err(DbError::InvalidOperation(
693 "cluster connector sources require an explicit column schema; runtime schema discovery is not a durable catalog identity"
694 .into(),
695 ));
696 }
697 let identity = catalog_create_identity(statement)?.ok_or_else(|| {
698 DbError::InvalidOperation(
699 "cluster catalog bootstrap accepts only reversible typed CREATE statements".into(),
700 )
701 })?;
702 if matches!(
703 identity.1,
704 CatalogObjectKind::Table | CatalogObjectKind::LookupTable
705 ) {
706 return Err(DbError::InvalidOperation(format!(
707 "{} is not supported in cluster mode until reference-table rows and source positions are atomically replicated through distributed state",
708 identity.2
709 )));
710 }
711 Ok(identity)
712}
713
714#[cfg(feature = "cluster")]
715fn uri_contains_unsupported_secret(value: &str, allow_reference: bool) -> bool {
716 if value.contains("://") {
717 return laminar_connectors::security::value_contains_uri_secret(value, allow_reference);
718 }
719 {
720 let lower = value.to_ascii_lowercase();
721 lower.split_whitespace().any(|part| {
722 part.strip_prefix("password=")
723 .or_else(|| part.strip_prefix("pwd="))
724 .is_some_and(|password| {
725 let password = password.trim_matches(|ch| ch == '\'' || ch == '"');
726 !(password.is_empty()
727 || allow_reference
728 && laminar_connectors::security::is_env_reference(password))
729 })
730 })
731 }
732}
733
734#[cfg(feature = "cluster")]
735fn catalog_property_contains_unsupported_secret(
736 key: &str,
737 value: &str,
738 allow_reference: bool,
739) -> bool {
740 let lower = key.to_ascii_lowercase();
741 let normalized = lower.replace(['.', '-'], "_");
742 let secret_key = laminar_connectors::security::is_secret_option_key(key);
743 if secret_key {
744 return !(allow_reference && laminar_connectors::security::is_env_reference(value));
745 }
746 if value.contains("://")
747 && laminar_connectors::security::value_contains_uri_secret(value, allow_reference)
748 {
749 return true;
750 }
751 (normalized.contains("connection")
752 || normalized == "uri"
753 || normalized.ends_with("_uri")
754 || normalized == "url"
755 || normalized.ends_with("_url")
756 || normalized == "dsn")
757 && uri_contains_unsupported_secret(value, allow_reference)
758}
759
760#[cfg(feature = "cluster")]
761fn catalog_ddl_contains_comment(sql: &str) -> Result<bool, DbError> {
762 use sqlparser::tokenizer::{Token, Tokenizer, Whitespace};
763
764 let tokens = Tokenizer::new(&sqlparser::dialect::GenericDialect {}, sql)
765 .tokenize()
766 .map_err(|error| {
767 DbError::InvalidOperation(format!("catalog DDL tokenization failed: {error}"))
768 })?;
769 Ok(tokens.into_iter().any(|token| {
770 matches!(
771 token,
772 Token::Whitespace(
773 Whitespace::SingleLineComment { .. } | Whitespace::MultiLineComment(_)
774 )
775 )
776 }))
777}
778
779#[cfg(feature = "cluster")]
780fn sensitive_catalog_property(statement: &StreamingStatement) -> Option<String> {
781 fn find<'a>(
782 options: impl IntoIterator<Item = (&'a String, &'a String)>,
783 allow_reference: bool,
784 ) -> Option<String> {
785 options
786 .into_iter()
787 .find(|(key, value)| {
788 catalog_property_contains_unsupported_secret(key, value, allow_reference)
789 })
790 .map(|(key, _)| key.clone())
791 }
792
793 match statement {
794 StreamingStatement::CreateSource(create) => find(create.with_options.iter(), true)
795 .or_else(|| find(create.connector_options.iter(), true))
796 .or_else(|| {
797 create
798 .format
799 .as_ref()
800 .and_then(|format| find(format.options.iter(), true))
801 }),
802 StreamingStatement::CreateSink(create) => find(create.connector_options.iter(), true)
803 .or_else(|| {
804 create
805 .format
806 .as_ref()
807 .and_then(|format| find(format.options.iter(), true))
808 }),
809 StreamingStatement::CreateLookupTable(create) => find(create.with_options.iter(), false),
810 StreamingStatement::Standard(statement) => {
811 let sqlparser::ast::Statement::CreateTable(create) = statement.as_ref() else {
812 return None;
813 };
814 let sqlparser::ast::CreateTableOptions::With(options) = &create.table_options else {
815 return None;
816 };
817 options.iter().find_map(|option| {
818 let sqlparser::ast::SqlOption::KeyValue { key, value } = option else {
819 return None;
820 };
821 let key = key.to_string();
822 let value = value.to_string();
823 catalog_property_contains_unsupported_secret(&key, value.trim_matches('\''), false)
824 .then_some(key)
825 })
826 }
827 _ => None,
828 }
829}
830
831#[cfg(feature = "cluster")]
832fn connector_source_requires_schema_discovery(statement: &StreamingStatement) -> bool {
833 let StreamingStatement::CreateSource(create) = statement else {
834 return false;
835 };
836 create.connector_type.is_some() && create.columns.is_empty()
837}
838
839#[cfg(not(feature = "cluster"))]
840const fn checkpoint_participant_for_runtime(_db: &LaminarDB) -> Option<u64> {
841 None
842}
843
844pub(crate) type ForceCheckpointReply =
846 crossfire::oneshot::TxOneshot<Result<crate::checkpoint_coordinator::CheckpointResult, DbError>>;
847
848const FORCE_CHECKPOINT_RESERVATION_WAITING: u8 = 0;
849const FORCE_CHECKPOINT_RESERVATION_CLAIMED: u8 = 1;
850const FORCE_CHECKPOINT_RESERVATION_CANCELLED: u8 = 2;
851
852pub(crate) struct ForceCheckpointReservationClaim {
857 state: Arc<std::sync::atomic::AtomicU8>,
858 wake: tokio::sync::oneshot::Sender<()>,
859}
860
861pub(crate) struct ForceCheckpointReservationWait {
862 state: Arc<std::sync::atomic::AtomicU8>,
863 wake: tokio::sync::oneshot::Receiver<()>,
864}
865
866#[derive(Clone, Copy, Debug, PartialEq, Eq)]
867pub(crate) enum ForceCheckpointReservationAttachment {
868 Attached,
869 Expired,
870 Cancelled,
871}
872
873impl ForceCheckpointReservationClaim {
874 pub(crate) fn new() -> (Self, ForceCheckpointReservationWait) {
875 let state = Arc::new(std::sync::atomic::AtomicU8::new(
876 FORCE_CHECKPOINT_RESERVATION_WAITING,
877 ));
878 let (wake_tx, wake_rx) = tokio::sync::oneshot::channel();
879 (
880 Self {
881 state: Arc::clone(&state),
882 wake: wake_tx,
883 },
884 ForceCheckpointReservationWait {
885 state,
886 wake: wake_rx,
887 },
888 )
889 }
890
891 pub(crate) fn try_claim(&self) -> bool {
894 self.state
895 .compare_exchange(
896 FORCE_CHECKPOINT_RESERVATION_WAITING,
897 FORCE_CHECKPOINT_RESERVATION_CLAIMED,
898 std::sync::atomic::Ordering::AcqRel,
899 std::sync::atomic::Ordering::Acquire,
900 )
901 .is_ok()
902 }
903
904 pub(crate) fn attach(
908 self,
909 deadline: tokio::time::Instant,
910 now: tokio::time::Instant,
911 ) -> ForceCheckpointReservationAttachment {
912 use ForceCheckpointReservationAttachment::{Attached, Cancelled, Expired};
913
914 let attachment = match self.state.load(std::sync::atomic::Ordering::Acquire) {
915 FORCE_CHECKPOINT_RESERVATION_CLAIMED => Attached,
916 FORCE_CHECKPOINT_RESERVATION_WAITING if now >= deadline => {
917 match self.state.compare_exchange(
918 FORCE_CHECKPOINT_RESERVATION_WAITING,
919 FORCE_CHECKPOINT_RESERVATION_CANCELLED,
920 std::sync::atomic::Ordering::AcqRel,
921 std::sync::atomic::Ordering::Acquire,
922 ) {
923 Ok(_) => Expired,
924 Err(FORCE_CHECKPOINT_RESERVATION_CLAIMED) => Attached,
925 Err(_) => Cancelled,
926 }
927 }
928 FORCE_CHECKPOINT_RESERVATION_WAITING => match self.state.compare_exchange(
929 FORCE_CHECKPOINT_RESERVATION_WAITING,
930 FORCE_CHECKPOINT_RESERVATION_CLAIMED,
931 std::sync::atomic::Ordering::AcqRel,
932 std::sync::atomic::Ordering::Acquire,
933 ) {
934 Ok(_) | Err(FORCE_CHECKPOINT_RESERVATION_CLAIMED) => Attached,
935 Err(_) => Cancelled,
936 },
937 _ => Cancelled,
938 };
939 if attachment == Attached {
940 let _ = self.wake.send(());
941 }
942 attachment
943 }
944}
945
946impl Drop for ForceCheckpointReservationWait {
947 fn drop(&mut self) {
948 let _ = self.state.compare_exchange(
951 FORCE_CHECKPOINT_RESERVATION_WAITING,
952 FORCE_CHECKPOINT_RESERVATION_CANCELLED,
953 std::sync::atomic::Ordering::AcqRel,
954 std::sync::atomic::Ordering::Acquire,
955 );
956 }
957}
958
959impl ForceCheckpointReservationWait {
960 fn cancel_before_reservation(&self) -> bool {
962 self.state
963 .compare_exchange(
964 FORCE_CHECKPOINT_RESERVATION_WAITING,
965 FORCE_CHECKPOINT_RESERVATION_CANCELLED,
966 std::sync::atomic::Ordering::AcqRel,
967 std::sync::atomic::Ordering::Acquire,
968 )
969 .is_ok()
970 }
971
972 fn is_claimed(&self) -> bool {
973 self.state.load(std::sync::atomic::Ordering::Acquire)
974 == FORCE_CHECKPOINT_RESERVATION_CLAIMED
975 }
976
977 pub(crate) async fn wait(&mut self) -> Result<(), tokio::sync::oneshot::error::RecvError> {
978 (&mut self.wake).await
979 }
980}
981
982pub(crate) struct ForceCheckpointRequest {
987 pub(crate) reply: ForceCheckpointReply,
988 pub(crate) reservation_claim: Option<ForceCheckpointReservationClaim>,
991 pub(crate) deadline: tokio::time::Instant,
992}
993
994pub(crate) type ForceCheckpointTx =
995 crossfire::MAsyncTx<crossfire::mpsc::Array<ForceCheckpointRequest>>;
996
997pub(crate) type ForceCheckpointRx =
998 crossfire::AsyncRx<crossfire::mpsc::Array<ForceCheckpointRequest>>;
999
1000pub(crate) const FORCE_CHECKPOINT_CHANNEL_CAPACITY: usize = 64;
1001
1002#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1003pub(crate) struct RecoveredInputChannelProgress {
1004 pub(crate) watermark: Option<i64>,
1005 pub(crate) idle: bool,
1006}
1007
1008pub(crate) struct InputChannelProgress {
1009 pub(crate) input_channel: Vec<u8>,
1010 pub(crate) watermark: Option<i64>,
1011 pub(crate) idle: bool,
1012}
1013
1014struct InputChannelWatermark {
1015 generator: laminar_core::time::BoundedOutOfOrdernessGenerator,
1016 idle: bool,
1017 last_activity: Instant,
1018}
1019
1020struct PartitionedSourceWatermarks {
1021 max_out_of_orderness_ms: i64,
1022 max_future_skew_ms: i64,
1023 idle_timeout: Option<Duration>,
1024 inventory: Option<Arc<[Vec<u8>]>>,
1025 channels: rustc_hash::FxHashMap<Box<[u8]>, InputChannelWatermark>,
1026 recovered: rustc_hash::FxHashMap<Box<[u8]>, RecoveredInputChannelProgress>,
1027 recovered_inventory: Option<Arc<[Vec<u8>]>>,
1028 external_floor: i64,
1029}
1030
1031impl PartitionedSourceWatermarks {
1032 fn channel(
1033 &self,
1034 recovered: Option<RecoveredInputChannelProgress>,
1035 admission_floor: i64,
1036 ) -> InputChannelWatermark {
1037 let mut generator =
1038 laminar_core::time::BoundedOutOfOrdernessGenerator::new(self.max_out_of_orderness_ms)
1039 .with_max_future_skew(self.max_future_skew_ms);
1040 let watermark = recovered
1041 .and_then(|progress| progress.watermark)
1042 .map_or(admission_floor, |watermark| watermark.max(admission_floor));
1043 if watermark > i64::MIN {
1044 laminar_core::time::WatermarkGenerator::restore_watermark_for_recovery(
1045 &mut generator,
1046 watermark,
1047 );
1048 }
1049 InputChannelWatermark {
1050 generator,
1051 idle: recovered.is_some_and(|progress| progress.idle),
1052 last_activity: Instant::now(),
1053 }
1054 }
1055
1056 fn effective_watermark(&self, channel: &InputChannelWatermark) -> i64 {
1057 laminar_core::time::WatermarkGenerator::current_watermark(&channel.generator)
1058 .max(self.external_floor)
1059 }
1060
1061 fn frontier(&self) -> i64 {
1062 if self.channels.is_empty() {
1063 return i64::MIN;
1064 }
1065 let mut active = false;
1066 let mut active_min = i64::MAX;
1067 let mut idle_max = i64::MIN;
1068 for channel in self.channels.values() {
1069 let watermark = self.effective_watermark(channel);
1070 idle_max = idle_max.max(watermark);
1071 if !channel.idle {
1072 active = true;
1073 active_min = active_min.min(watermark);
1074 }
1075 }
1076 if active {
1077 active_min
1078 } else {
1079 idle_max
1080 }
1081 }
1082
1083 fn all_idle(&self) -> bool {
1084 self.inventory
1085 .as_ref()
1086 .is_some_and(|_| self.channels.values().all(|channel| channel.idle))
1087 }
1088}
1089
1090pub(crate) struct SourceWatermarkState {
1091 pub(crate) extractor: laminar_core::time::EventTimeExtractor,
1092 pub(crate) generator: Box<dyn laminar_core::time::WatermarkGenerator>,
1093 pub(crate) column: String,
1094 partitioned: Option<PartitionedSourceWatermarks>,
1095}
1096
1097impl SourceWatermarkState {
1098 pub(crate) fn new(
1099 extractor: laminar_core::time::EventTimeExtractor,
1100 generator: Box<dyn laminar_core::time::WatermarkGenerator>,
1101 column: String,
1102 ) -> Self {
1103 Self {
1104 extractor,
1105 generator,
1106 column,
1107 partitioned: None,
1108 }
1109 }
1110
1111 pub(crate) fn with_input_channels(
1112 mut self,
1113 max_out_of_orderness: Duration,
1114 max_future_skew_ms: i64,
1115 idle_timeout: Option<Duration>,
1116 recovered: rustc_hash::FxHashMap<Box<[u8]>, RecoveredInputChannelProgress>,
1117 recovered_inventory: Option<Arc<[Vec<u8>]>>,
1118 ) -> Self {
1119 self.partitioned = Some(PartitionedSourceWatermarks {
1120 max_out_of_orderness_ms: i64::try_from(max_out_of_orderness.as_millis())
1121 .unwrap_or(i64::MAX),
1122 max_future_skew_ms,
1123 idle_timeout,
1124 inventory: None,
1125 channels: rustc_hash::FxHashMap::default(),
1126 recovered,
1127 recovered_inventory,
1128 external_floor: i64::MIN,
1129 });
1130 self
1131 }
1132
1133 pub(crate) const fn is_partitioned(&self) -> bool {
1134 self.partitioned.is_some()
1135 }
1136
1137 pub(crate) fn input_channels_all_idle(&self) -> Option<bool> {
1138 self.partitioned
1139 .as_ref()
1140 .map(PartitionedSourceWatermarks::all_idle)
1141 }
1142
1143 pub(crate) fn install_committed_watermark_floor(&mut self, watermark: i64) -> Option<i64> {
1150 if watermark == i64::MIN {
1151 return None;
1152 }
1153 if let Some(state) = self.partitioned.as_mut() {
1154 state.external_floor = state.external_floor.max(watermark);
1155 }
1156 if watermark <= self.generator.current_watermark() {
1157 return None;
1158 }
1159 self.generator.restore_watermark_for_recovery(watermark);
1160 Some(watermark)
1161 }
1162
1163 pub(crate) fn install_input_channels(
1164 &mut self,
1165 inventory: Option<Arc<[Vec<u8>]>>,
1166 admission_floor: i64,
1167 ) -> Result<bool, String> {
1168 let activation_floor = admission_floor.max(self.generator.current_watermark());
1169 let Some(state) = self.partitioned.as_mut() else {
1170 return Ok(false);
1171 };
1172 let inventory = inventory.ok_or_else(|| {
1173 "ordered event-time source checkpoint omitted its input-channel inventory".to_string()
1174 })?;
1175 if inventory.iter().any(Vec::is_empty)
1176 || !inventory.windows(2).all(|pair| pair[0] < pair[1])
1177 {
1178 return Err(
1179 "input-channel inventory must contain non-empty, strictly ordered identities"
1180 .into(),
1181 );
1182 }
1183 if state.inventory.as_ref().is_some_and(|installed| {
1184 Arc::ptr_eq(installed, &inventory) || installed.as_ref() == inventory.as_ref()
1185 }) {
1186 return Ok(false);
1187 }
1188
1189 let initial_install = state.inventory.is_none();
1190 if initial_install {
1191 let recovered_expected = state.recovered_inventory.as_deref();
1192 if let Some(input_channel) = inventory.iter().find(|input_channel| {
1193 !state.recovered.contains_key(input_channel.as_slice())
1194 && recovered_expected.is_some_and(|expected| {
1195 expected
1196 .binary_search_by(|candidate| candidate.as_slice().cmp(input_channel))
1197 .is_ok()
1198 })
1199 }) {
1200 return Err(format!(
1201 "recovered input channel {input_channel:02x?} has no committed watermark progress"
1202 ));
1203 }
1204 }
1205 let mut previous = std::mem::take(&mut state.channels);
1206 let mut channels = rustc_hash::FxHashMap::with_capacity_and_hasher(
1207 inventory.len(),
1208 rustc_hash::FxBuildHasher,
1209 );
1210 for input_channel in inventory.iter() {
1211 if let Some(channel) = previous.remove(input_channel.as_slice()) {
1212 channels.insert(input_channel.clone().into_boxed_slice(), channel);
1213 continue;
1214 }
1215 let recovered = if initial_install {
1216 state.recovered.remove(input_channel.as_slice())
1217 } else {
1218 None
1219 };
1220 channels.insert(
1221 input_channel.clone().into_boxed_slice(),
1222 state.channel(recovered, activation_floor),
1223 );
1224 }
1225 if initial_install {
1226 state.recovered.clear();
1227 state.recovered_inventory = None;
1228 }
1229 state.inventory = Some(inventory);
1230 state.channels = channels;
1231 self.generator.advance_watermark(state.frontier());
1232 Ok(true)
1233 }
1234
1235 pub(crate) fn observe_input_channels(
1236 &mut self,
1237 batch: &RecordBatch,
1238 admission_floor: i64,
1239 ) -> Result<Option<i64>, String> {
1240 let Some(state) = self.partitioned.as_mut() else {
1241 let floor = self.generator.advance_watermark(admission_floor);
1242 let event = match self.extractor.extract(batch) {
1243 Ok(timestamp) => self.generator.on_event(timestamp).map(|wm| wm.timestamp()),
1244 Err(laminar_core::time::EventTimeError::NullTimestamp { .. }) => None,
1245 Err(error) => return Err(error.to_string()),
1246 };
1247 return Ok(event.or_else(|| floor.map(|watermark| watermark.timestamp())));
1248 };
1249 if state.inventory.is_none() {
1250 return Err("ordered event-time source emitted a batch before installing its input-channel inventory".into());
1251 }
1252 let timestamps = self
1253 .extractor
1254 .extract_millis_array(batch)
1255 .map_err(|error| error.to_string())?;
1256 let partitions = batch
1257 .column_by_name(laminar_connectors::connector::SOURCE_PARTITION_COLUMN)
1258 .ok_or_else(|| "ordered event-time batch omitted __source_partition".to_string())?
1259 .as_any()
1260 .downcast_ref::<BinaryArray>()
1261 .ok_or_else(|| {
1262 "ordered event-time batch __source_partition must be Binary".to_string()
1263 })?;
1264 if partitions.len() != timestamps.len() {
1265 return Err("event-time and input-channel column lengths differ".into());
1266 }
1267
1268 let observed_at = Instant::now();
1269 let activation_floor = admission_floor.max(self.generator.current_watermark());
1270 for row in 0..timestamps.len() {
1271 if partitions.is_null(row) {
1272 return Err(format!("null input-channel identity at row {row}"));
1273 }
1274 let input_channel = partitions.value(row);
1275 let channel = state.channels.get_mut(input_channel).ok_or_else(|| {
1276 format!("row {row} references an input channel outside the installed inventory")
1277 })?;
1278 if channel.idle {
1279 laminar_core::time::WatermarkGenerator::advance_watermark(
1280 &mut channel.generator,
1281 activation_floor,
1282 );
1283 }
1284 channel.idle = false;
1285 channel.last_activity = observed_at;
1286 if !timestamps.is_null(row) {
1287 laminar_core::time::WatermarkGenerator::on_event(
1288 &mut channel.generator,
1289 timestamps.value(row),
1290 );
1291 }
1292 }
1293 let frontier = state.frontier();
1294 Ok(self
1295 .generator
1296 .advance_watermark(frontier)
1297 .map(|watermark| watermark.timestamp()))
1298 }
1299
1300 pub(crate) fn advance_external_watermark(&mut self, watermark: i64) -> Option<i64> {
1301 if let Some(state) = self.partitioned.as_mut() {
1302 let candidate_floor = state.external_floor.max(watermark);
1303 let frontier = state.frontier().max(candidate_floor);
1304 let current = self.generator.current_watermark();
1305 let advanced = self.generator.advance_watermark(frontier);
1306 if frontier > current && advanced.is_none() {
1307 return None;
1308 }
1309 state.external_floor = candidate_floor;
1310 advanced.map(|watermark| watermark.timestamp())
1311 } else {
1312 self.generator
1313 .advance_watermark(watermark)
1314 .map(|watermark| watermark.timestamp())
1315 }
1316 }
1317
1318 pub(crate) fn tick_input_channel_idleness(&mut self) -> (Option<i64>, bool) {
1319 let Some(state) = self.partitioned.as_mut() else {
1320 return (
1321 self.generator
1322 .on_periodic()
1323 .map(|watermark| watermark.timestamp()),
1324 false,
1325 );
1326 };
1327 let now = Instant::now();
1328 let external_floor = state.external_floor;
1329 let mut has_channel = false;
1330 let mut has_active = false;
1331 let mut active_min = i64::MAX;
1332 let mut idle_max = i64::MIN;
1333 for channel in state.channels.values_mut() {
1334 has_channel = true;
1335 if !channel.idle
1336 && state.idle_timeout.is_some_and(|timeout| {
1337 now.saturating_duration_since(channel.last_activity) >= timeout
1338 })
1339 {
1340 channel.idle = true;
1341 }
1342 let watermark =
1343 laminar_core::time::WatermarkGenerator::current_watermark(&channel.generator)
1344 .max(external_floor);
1345 idle_max = idle_max.max(watermark);
1346 if !channel.idle {
1347 has_active = true;
1348 active_min = active_min.min(watermark);
1349 }
1350 }
1351 let frontier = if !has_channel {
1352 i64::MIN
1353 } else if has_active {
1354 active_min
1355 } else {
1356 idle_max
1357 };
1358 (
1359 self.generator
1360 .advance_watermark(frontier)
1361 .map(|watermark| watermark.timestamp()),
1362 state.inventory.is_some() && !has_active,
1363 )
1364 }
1365
1366 pub(crate) fn input_channel_progress(
1367 &self,
1368 ) -> Result<Option<Vec<InputChannelProgress>>, String> {
1369 let Some(state) = self.partitioned.as_ref() else {
1370 return Ok(None);
1371 };
1372 let inventory = state.inventory.as_ref().ok_or_else(|| {
1373 "ordered event-time source has no installed input-channel inventory".to_string()
1374 })?;
1375 let mut progress = Vec::with_capacity(inventory.len());
1376 for input_channel in inventory.iter() {
1377 let channel = state
1378 .channels
1379 .get(input_channel.as_slice())
1380 .ok_or_else(|| {
1381 "installed input-channel inventory and watermark state diverged".to_string()
1382 })?;
1383 let watermark = state.effective_watermark(channel);
1384 progress.push(InputChannelProgress {
1385 input_channel: input_channel.clone(),
1386 watermark: (watermark > i64::MIN).then_some(watermark),
1387 idle: channel.idle,
1388 });
1389 }
1390 Ok(Some(progress))
1391 }
1392}
1393
1394pub(crate) fn filter_late_rows(
1397 batch: &RecordBatch,
1398 column: &str,
1399 watermark: i64,
1400) -> Result<Option<RecordBatch>, laminar_core::time::FilterError> {
1401 laminar_core::time::filter_batch_by_timestamp(
1402 batch,
1403 column,
1404 watermark,
1405 laminar_core::time::ThresholdOp::GreaterEq,
1406 )
1407}
1408
1409pub(crate) use laminar_core::time::parse_duration_str;
1410
1411#[cfg(feature = "cluster")]
1413#[derive(Debug, Default)]
1414pub struct SnapshotAdoption {
1415 pub adopted: bool,
1417 pub version: u64,
1419 pub recovery_required: bool,
1422}
1423
1424#[cfg(feature = "cluster")]
1425#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1426enum AssignmentAdoptionMode {
1427 LiveTransition,
1428 ColdRecovery,
1429 StoppedRecoveryTopology,
1435}
1436
1437#[cfg(feature = "cluster")]
1439#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1440pub enum ClusterStartupDisposition {
1441 Serving,
1443 Idle,
1445 RecoveryFenced,
1447}
1448
1449#[cfg(feature = "cluster")]
1455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1456pub(crate) enum StartupCheckpointArtifactAudit {
1457 Clean(laminar_core::cluster::control::LocalProcessAuthorityIdentity),
1459 Artifacts(laminar_core::cluster::control::LocalProcessAuthorityIdentity),
1461}
1462
1463#[cfg(feature = "cluster")]
1464impl StartupCheckpointArtifactAudit {
1465 pub(crate) const fn process(
1467 self,
1468 ) -> laminar_core::cluster::control::LocalProcessAuthorityIdentity {
1469 match self {
1470 Self::Clean(process) | Self::Artifacts(process) => process,
1471 }
1472 }
1473}
1474
1475#[cfg(feature = "cluster")]
1477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1478pub(crate) struct AssignmentAuthorityActivation {
1479 pub(crate) installed: bool,
1481 pub(crate) intake_open: bool,
1483 pub(crate) revision: u64,
1485}
1486
1487#[cfg(feature = "cluster")]
1488fn assignment_transition_allows_activation(
1489 fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1490 requested: Option<&laminar_core::checkpoint::AssignmentDrainTransition>,
1491 installed: Option<&laminar_core::checkpoint::AssignmentDrainTransition>,
1492) -> Result<bool, DbError> {
1493 if requested.is_some_and(|transition| transition.predecessor != *fence) {
1494 return Err(DbError::Checkpoint(
1495 "assignment drain transition does not bind the installed predecessor certificate"
1496 .into(),
1497 ));
1498 }
1499 Ok(requested.is_some() || installed.is_none())
1500}
1501
1502#[cfg(feature = "cluster")]
1503fn owned_vnode_indices(
1504 assignment: &[laminar_core::state::NodeId],
1505 self_id: laminar_core::state::NodeId,
1506) -> Result<Vec<u32>, DbError> {
1507 assignment
1508 .iter()
1509 .enumerate()
1510 .filter(|(_, owner)| **owner == self_id)
1511 .map(|(vnode, _)| {
1512 u32::try_from(vnode).map_err(|_| {
1513 DbError::Checkpoint(
1514 "vnode assignment is too large to encode a u32 vnode identifier".into(),
1515 )
1516 })
1517 })
1518 .collect()
1519}
1520
1521#[cfg(feature = "cluster")]
1522fn local_state_lacks_exact_predecessor_binding(
1523 observed_assignment_version: u64,
1524 predecessor: Option<&laminar_core::checkpoint::CheckpointAssignmentFence>,
1525 pipeline_identity: Option<&laminar_core::checkpoint::PipelineIdentity>,
1526 installed: Option<&crate::vnode_transition_staging::InstalledVnodeStateBinding>,
1527) -> bool {
1528 observed_assignment_version != 0
1529 && !predecessor
1530 .zip(pipeline_identity)
1531 .zip(installed)
1532 .is_some_and(|((assignment, pipeline_identity), installed)| {
1533 installed.matches(assignment, pipeline_identity)
1534 })
1535}
1536
1537#[cfg(feature = "cluster")]
1538const fn predecessor_readiness_withdrawal_required(
1539 requires_predecessor_binding: bool,
1540 observed_assignment_version: u64,
1541) -> bool {
1542 requires_predecessor_binding && observed_assignment_version != 0
1543}
1544
1545impl LaminarDB {
1546 pub(crate) const fn runtime_mode(&self) -> RuntimeMode {
1547 self.runtime_mode
1548 }
1549
1550 pub(crate) const fn is_cluster_runtime(&self) -> bool {
1552 self.runtime_mode().is_cluster()
1553 }
1554
1555 #[cfg(feature = "cluster")]
1558 #[doc(hidden)]
1559 #[must_use]
1560 pub fn pending_revoke_vnode_count(&self) -> usize {
1561 self.pending_vnode_transition
1562 .lock()
1563 .as_ref()
1564 .map_or(0, |transition| transition.revoked_vnodes().len())
1565 }
1566
1567 #[cfg(feature = "cluster")]
1570 pub(crate) fn has_unapplied_vnode_transition(&self) -> bool {
1571 self.pending_vnode_transition.lock().is_some()
1572 }
1573
1574 #[cfg(feature = "cluster")]
1578 pub(crate) async fn local_vnode_state_is_ready(
1579 &self,
1580 registry: &laminar_core::state::VnodeRegistry,
1581 assignment: &laminar_core::checkpoint::CheckpointAssignmentFence,
1582 ) -> Result<bool, DbError> {
1583 if DbState::load(&self.state) != DbState::Running {
1587 return Ok(false);
1588 }
1589 let local_assignment = registry.versioned_snapshot();
1590 let owner_ids: Vec<u64> = local_assignment
1591 .owners()
1592 .iter()
1593 .map(|owner| owner.0)
1594 .collect();
1595 if !assignment.is_canonical()
1596 || assignment.assignment_version != local_assignment.version()
1597 || !assignment.matches_owner_map(&owner_ids)
1598 {
1599 return Ok(false);
1600 }
1601 if self.has_unapplied_vnode_transition() {
1602 return Ok(false);
1603 }
1604 let pipeline_identity = self
1605 .coordinator
1606 .lock()
1607 .await
1608 .as_ref()
1609 .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
1610 .transpose()?;
1611 Ok(pipeline_identity.as_ref().is_none_or(|pipeline_identity| {
1612 self.installed_vnode_state
1613 .lock()
1614 .as_ref()
1615 .is_some_and(|installed| installed.matches(assignment, pipeline_identity))
1616 }))
1617 }
1618
1619 #[cfg(feature = "cluster")]
1621 pub(crate) fn local_vnode_state_report(
1622 &self,
1623 controller: &laminar_core::cluster::control::ClusterController,
1624 assignment: &laminar_core::state::VnodeAssignmentSnapshot,
1625 vnode_state_ready: bool,
1626 ) -> Result<laminar_core::checkpoint::CheckpointAssignmentAdoption, DbError> {
1627 let owner_ids: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
1628 let vnode_count = u32::try_from(owner_ids.len()).map_err(|_| {
1629 DbError::Checkpoint("local vnode-state report owner count exceeds u32::MAX".into())
1630 })?;
1631 let report = laminar_core::checkpoint::CheckpointAssignmentAdoption {
1632 participant: laminar_core::checkpoint::CheckpointParticipant {
1633 node_id: controller.instance_id().0,
1634 boot_incarnation: controller.recovery_incarnation(),
1635 },
1636 assignment_version: assignment.version(),
1637 partitioning_abi_version: laminar_core::state::PARTITIONING_ABI_VERSION,
1638 vnode_count,
1639 assignment_digest:
1640 laminar_core::checkpoint::CheckpointAssignmentFence::owner_map_digest(
1641 vnode_count,
1642 &owner_ids,
1643 ),
1644 vnode_state_ready,
1645 };
1646 Ok(report)
1647 }
1648
1649 #[cfg(feature = "cluster")]
1652 pub(crate) async fn publish_local_vnode_state_report(
1653 &self,
1654 controller: &laminar_core::cluster::control::ClusterController,
1655 assignment: &laminar_core::state::VnodeAssignmentSnapshot,
1656 vnode_state_ready: bool,
1657 ) -> Result<laminar_core::checkpoint::CheckpointAssignmentAdoption, DbError> {
1658 let report = self.local_vnode_state_report(controller, assignment, vnode_state_ready)?;
1659 controller
1660 .announce_adopted_assignment(&report)
1661 .await
1662 .map_err(|error| {
1663 DbError::Checkpoint(format!(
1664 "could not publish local vnode-state readiness for assignment {}: {error}",
1665 assignment.version()
1666 ))
1667 })?;
1668 Ok(report)
1669 }
1670
1671 pub fn open() -> Result<Arc<Self>, DbError> {
1677 Self::open_with_config(LaminarConfig::default())
1678 }
1679
1680 pub fn open_with_config(config: LaminarConfig) -> Result<Arc<Self>, DbError> {
1686 let db = Self::open_with_config_and_vars(config, HashMap::new())?;
1687 db.connector_registry.freeze();
1688 Ok(Arc::new(db))
1689 }
1690
1691 #[allow(clippy::unnecessary_wraps)]
1697 pub(crate) fn open_with_config_and_vars(
1698 config: LaminarConfig,
1699 config_vars: HashMap<String, String>,
1700 ) -> Result<Self, DbError> {
1701 Self::open_with_config_and_vars_and_rules(
1702 config,
1703 config_vars,
1704 &[],
1705 None,
1706 RuntimeMode::Local,
1707 )
1708 }
1709
1710 #[allow(clippy::unnecessary_wraps)]
1713 #[allow(clippy::too_many_lines)] pub(crate) fn open_with_config_and_vars_and_rules(
1715 mut config: LaminarConfig,
1716 config_vars: HashMap<String, String>,
1717 extra_optimizer_rules: &[Arc<
1718 dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync,
1719 >],
1720 target_partitions: Option<usize>,
1721 runtime_mode: RuntimeMode,
1722 ) -> Result<Self, DbError> {
1723 config.source_idle_timeout =
1724 crate::config::source_idle_timeout_ms(config.source_idle_timeout)
1725 .map_err(|error| DbError::Config(error.to_string()))?
1726 .map(Duration::from_millis);
1727 let future_skew_ms =
1728 crate::config::event_time_max_future_skew_ms(config.event_time_max_future_skew)
1729 .map_err(|error| DbError::Config(error.to_string()))?;
1730 config.event_time_max_future_skew = Duration::from_millis(future_skew_ms.unsigned_abs());
1731 let max_managed_state_bytes = config
1732 .pipeline_max_managed_state_bytes
1733 .unwrap_or(crate::config::DEFAULT_MAX_MANAGED_STATE_BYTES);
1734 if max_managed_state_bytes == 0 {
1735 return Err(DbError::Config(
1736 "pipeline_max_managed_state_bytes must be greater than zero".into(),
1737 ));
1738 }
1739 config.pipeline_max_managed_state_bytes = Some(max_managed_state_bytes);
1740
1741 if let Some(checkpoint) = config.checkpoint.as_mut() {
1742 let max_node_data_bytes = checkpoint.max_node_data_bytes.unwrap_or(
1743 laminar_core::checkpoint::checkpoint_store::DEFAULT_MAX_CHECKPOINT_NODE_DATA_BYTES,
1744 );
1745 laminar_core::checkpoint::checkpoint_store::validate_max_checkpoint_node_data_bytes(
1746 max_node_data_bytes,
1747 )
1748 .map_err(|error| DbError::Config(format!("checkpoint.max_node_data_bytes: {error}")))?;
1749 checkpoint.max_node_data_bytes = Some(max_node_data_bytes);
1750 }
1751
1752 crossfire::detect_backoff_cfg();
1754
1755 let lookup_registry = Arc::new(laminar_sql::datafusion::LookupTableRegistry::new());
1756
1757 let ctx = {
1759 let mut session_config = laminar_sql::datafusion::base_session_config();
1760 if let Some(n) = target_partitions {
1761 session_config = session_config.with_target_partitions(n);
1762 }
1763 let extension_planner: Arc<
1764 dyn datafusion::physical_planner::ExtensionPlanner + Send + Sync,
1765 > = Arc::new(laminar_sql::datafusion::LookupJoinExtensionPlanner::new(
1766 Arc::clone(&lookup_registry),
1767 ));
1768 let query_planner: Arc<dyn datafusion::execution::context::QueryPlanner + Send + Sync> =
1769 Arc::new(LookupQueryPlanner { extension_planner });
1770 let mut state_builder = datafusion::execution::SessionStateBuilder::new()
1771 .with_config(session_config)
1772 .with_default_features()
1773 .with_query_planner(query_planner);
1774 for rule in extra_optimizer_rules {
1775 state_builder = state_builder.with_physical_optimizer_rule(Arc::clone(rule));
1776 }
1777 SessionContext::new_with_state(state_builder.build())
1778 };
1779 register_streaming_functions(&ctx);
1780
1781 let catalog = Arc::new(SourceCatalog::new(
1782 config.default_buffer_size,
1783 config.default_backpressure,
1784 ));
1785
1786 let connector_registry = Arc::new(laminar_connectors::registry::ConnectorRegistry::new());
1787 Self::register_builtin_connectors(&connector_registry)?;
1788 let physical_rules = extra_optimizer_rules.to_vec();
1789
1790 Ok(Self {
1791 runtime_mode,
1792 catalog,
1793 planner: parking_lot::Mutex::new(StreamingPlanner::new()),
1794 ctx,
1795 custom_udfs: Vec::new(),
1796 custom_udafs: Vec::new(),
1797 config,
1798 config_vars: Arc::new(config_vars),
1799 shutdown: std::sync::atomic::AtomicBool::new(false),
1800 coordinator: Arc::new(tokio::sync::Mutex::new(None)),
1801 connector_manager: parking_lot::Mutex::new(
1802 crate::connector_manager::ConnectorManager::new(),
1803 ),
1804 connector_registry,
1805 mv_registry: parking_lot::Mutex::new(laminar_core::mv::MvRegistry::new()),
1806 table_store: Arc::new(parking_lot::RwLock::new(
1807 crate::table_store::TableStore::new(),
1808 )),
1809 state: Arc::new(std::sync::atomic::AtomicU8::new(DbState::Created as u8)),
1810 last_fault: Arc::new(parking_lot::Mutex::new(None)),
1811 catalog_cleanup_fenced: std::sync::atomic::AtomicBool::new(false),
1812 #[cfg(test)]
1813 catalog_cleanup_deregister_fault: parking_lot::Mutex::new(None),
1814 supervisor_self: Arc::new(parking_lot::Mutex::new(std::sync::Weak::new())),
1815 restart_history: Arc::new(parking_lot::Mutex::new(Vec::new())),
1816 lifecycle_lock: tokio::sync::Mutex::new(()),
1817 control_runtime: DbControlRuntime::new(),
1818 startup_attempt: parking_lot::Mutex::new(None),
1819 topology_ddl_lock: tokio::sync::RwLock::new(()),
1820 catalog_namespace: parking_lot::Mutex::new(HashMap::new()),
1821 #[cfg(test)]
1822 topology_planning_gate: parking_lot::Mutex::new(None),
1823 #[cfg(test)]
1824 stop_after_claim_gate: parking_lot::Mutex::new(None),
1825 #[cfg(all(test, feature = "cluster"))]
1826 catalog_seal_gate: parking_lot::Mutex::new(None),
1827 #[cfg(all(test, feature = "cluster"))]
1828 compute_before_ready_panic: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1829 runtime_handle: tokio::sync::Mutex::new(None),
1830 checkpoint_namespace_lock: parking_lot::Mutex::new(None),
1831 owned_sink_handles: Arc::new(parking_lot::Mutex::new(Vec::new())),
1832 owned_source_tasks: Arc::new(parking_lot::Mutex::new(Vec::new())),
1833 owned_connector_task_fences: Arc::new(parking_lot::Mutex::new(Vec::new())),
1834 shutdown_signal: Arc::new(tokio::sync::Notify::new()),
1835 runtime_shutdown: parking_lot::RwLock::new(tokio_util::sync::CancellationToken::new()),
1836 #[cfg(feature = "cluster")]
1837 assignment_restore_shutdown: tokio_util::sync::CancellationToken::new(),
1838 engine_metrics: parking_lot::Mutex::new(None),
1839 #[cfg(feature = "cluster")]
1840 checkpoint_barrier_timings: Arc::new(
1841 crate::checkpoint_timing::CheckpointBarrierTimingLedger::new(),
1842 ),
1843 prometheus_registry: parking_lot::Mutex::new(None),
1844 start_time: Instant::now(),
1845 session_properties: parking_lot::Mutex::new(HashMap::new()),
1846 pipeline_watermark: Arc::new(std::sync::atomic::AtomicI64::new(i64::MIN)),
1847 lookup_registry,
1848 ai_runtime: None,
1849 ai_handle: None,
1850 control_tx: parking_lot::Mutex::new(None),
1851 mv_store: Arc::new(parking_lot::RwLock::new(crate::mv_store::MvStore::new())),
1852 #[cfg(feature = "cluster")]
1853 cluster_controller: parking_lot::Mutex::new(None),
1854 #[cfg(feature = "cluster")]
1855 recover_target_epoch: parking_lot::Mutex::new(None),
1856 #[cfg(feature = "cluster")]
1857 recovery_monitor: parking_lot::Mutex::new(None),
1858 #[cfg(feature = "cluster")]
1859 coordinated_lifecycle_active: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1860 #[cfg(feature = "cluster")]
1861 coordinated_recovery_fenced: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1862 #[cfg(feature = "cluster")]
1863 durable_terminal_recovery_fence: std::sync::atomic::AtomicBool::new(false),
1864 terminal_pipeline_halt: Arc::new(std::sync::atomic::AtomicBool::new(false)),
1865 #[cfg(feature = "cluster")]
1866 pending_recovery_fault: Arc::new(std::sync::atomic::AtomicU64::new(0)),
1867 #[cfg(feature = "cluster")]
1868 startup_checkpoint_artifact_audit: parking_lot::Mutex::new(None),
1869 #[cfg(feature = "cluster")]
1870 last_recovery_epoch: parking_lot::Mutex::new(None),
1871 #[cfg(feature = "cluster")]
1872 source_gate: Arc::new(std::sync::atomic::AtomicBool::new(
1873 runtime_mode.is_cluster(),
1874 )),
1875 #[cfg(feature = "cluster")]
1876 cluster_authority_revoked: std::sync::atomic::AtomicBool::new(false),
1877 #[cfg(feature = "cluster")]
1878 cluster_authority_transition: Arc::new(parking_lot::Mutex::new(())),
1879 vnode_registry: parking_lot::Mutex::new((runtime_mode == RuntimeMode::Local).then(
1880 || {
1881 Arc::new(laminar_core::state::VnodeRegistry::single_owner(
1882 u32::from(laminar_core::state::DEFAULT_KEY_GROUP_COUNT),
1883 laminar_core::state::LOCAL_NODE_ID,
1884 ))
1885 },
1886 )),
1887 physical_optimizer_rules: physical_rules.into(),
1888 pipeline_target_partitions: target_partitions,
1889 #[cfg(feature = "cluster")]
1890 shuffle_sender: parking_lot::Mutex::new(None),
1891 #[cfg(feature = "cluster")]
1892 shuffle_receiver: Arc::new(parking_lot::Mutex::new(None)),
1893 #[cfg(feature = "cluster")]
1894 decision_store: parking_lot::Mutex::new(None),
1895 #[cfg(feature = "cluster")]
1896 assignment_snapshot_store: parking_lot::Mutex::new(None),
1897 #[cfg(feature = "cluster")]
1898 catalog_manifest_store: parking_lot::Mutex::new(None),
1899 #[cfg(feature = "cluster")]
1900 cluster_checkpoint_object_store: None,
1901 #[cfg(feature = "cluster")]
1902 pending_vnode_transition: Arc::new(parking_lot::Mutex::new(None)),
1903 #[cfg(feature = "cluster")]
1904 installed_vnode_state: Arc::new(parking_lot::Mutex::new(None)),
1905 #[cfg(feature = "cluster")]
1906 assignment_adoption_lock: Arc::new(tokio::sync::Mutex::new(())),
1907 #[cfg(feature = "cluster")]
1908 assignment_authority_revision: std::sync::atomic::AtomicU64::new(0),
1909 #[cfg(feature = "cluster")]
1910 rotation_execution_fence: Arc::new(tokio::sync::RwLock::new(())),
1911 force_ckpt_tx: parking_lot::Mutex::new(None),
1912 subscription_registry: Arc::new(crate::subscription::SubscriptionRegistry::new()),
1913 stream_schemas: parking_lot::RwLock::new(HashMap::new()),
1914 })
1915 }
1916
1917 pub(crate) fn set_ai_runtime(
1920 &mut self,
1921 runtime: Arc<crate::ai::AiRuntime>,
1922 handle: tokio::runtime::Handle,
1923 ) {
1924 if let Err(e) = crate::ai_catalog::register_ai_catalog(&self.ctx, &runtime) {
1926 tracing::warn!(error = %e, "failed to register laminar.* AI catalog views");
1927 }
1928 self.ai_runtime = Some(runtime);
1929 self.ai_handle = Some(handle);
1930 }
1931
1932 #[cfg(feature = "cluster")]
1933 pub(crate) fn set_shuffle_sender(&self, sender: Arc<laminar_core::shuffle::ShuffleSender>) {
1934 *self.shuffle_sender.lock() = Some(sender);
1935 }
1936
1937 #[cfg(feature = "cluster")]
1938 pub(crate) fn set_shuffle_receiver(
1939 &self,
1940 receiver: Arc<laminar_core::shuffle::ShuffleReceiver>,
1941 ) {
1942 *self.shuffle_receiver.lock() = Some(receiver);
1943 }
1944
1945 #[cfg(feature = "cluster")]
1946 pub(crate) fn invalidate_shuffle_assignment_fence(&self) {
1947 self.assignment_authority_revision
1948 .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1949 if let Some(receiver) = self.shuffle_receiver.lock().as_ref() {
1950 receiver.invalidate_assignment_fence();
1951 }
1952 if let Some(sender) = self.shuffle_sender.lock().as_ref() {
1953 sender.invalidate_assignment_fence();
1954 }
1955 }
1956
1957 #[cfg(feature = "cluster")]
1960 pub(crate) fn suspend_shuffle_assignment_fence(&self) {
1961 self.assignment_authority_revision
1962 .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
1963 if let Some(receiver) = self.shuffle_receiver.lock().as_ref() {
1964 receiver.suspend_assignment_fence();
1965 }
1966 if let Some(sender) = self.shuffle_sender.lock().as_ref() {
1967 sender.suspend_assignment_fence();
1968 }
1969 }
1970
1971 #[cfg(feature = "cluster")]
1972 fn withdraw_assignment_authority(
1973 &self,
1974 controller: &laminar_core::cluster::control::ClusterController,
1975 ) {
1976 self.set_source_gate(true);
1977 controller.publish_checkpoint_drain_transition(None);
1978 controller.publish_checkpoint_assignment_fence(None);
1979 self.suspend_shuffle_assignment_fence();
1980 }
1981
1982 #[cfg(feature = "cluster")]
1983 pub(crate) fn install_shuffle_assignment_fence(
1984 &self,
1985 fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
1986 ) -> Result<(), DbError> {
1987 let _transition = self.cluster_authority_transition.lock();
1988 if self
1989 .cluster_authority_revoked
1990 .load(std::sync::atomic::Ordering::Acquire)
1991 {
1992 return Err(DbError::Checkpoint(
1993 "shuffle assignment install has terminally revoked process authority".into(),
1994 ));
1995 }
1996 let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
1997 DbError::Checkpoint("shuffle assignment install has no cluster controller".into())
1998 })?;
1999 if !controller.process_lease_is_live() {
2000 self.invalidate_shuffle_assignment_fence();
2001 return Err(DbError::Checkpoint(
2002 "shuffle assignment install has no live process lease".into(),
2003 ));
2004 }
2005 let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
2006 DbError::Checkpoint("shuffle assignment install has no vnode registry".into())
2007 })?;
2008 let assignment = registry.versioned_snapshot();
2009 let owners: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
2010 if !fence.is_canonical()
2014 || fence.assignment_version != assignment.version()
2015 || fence.vnode_count != registry.vnode_count()
2016 || !fence.matches_owner_map(&owners)
2017 || fence.participant_incarnation(controller.instance_id().0)
2018 != Some(controller.recovery_incarnation())
2019 {
2020 self.invalidate_shuffle_assignment_fence();
2021 return Err(DbError::Checkpoint(format!(
2022 "shuffle assignment certificate does not match local assignment {} and process {}",
2023 assignment.version(),
2024 controller.instance_id().0
2025 )));
2026 }
2027
2028 let receiver = self.shuffle_receiver.lock().clone();
2029 let sender = self.shuffle_sender.lock().clone();
2030 let expected_digest = fence.digest();
2031 if self.shuffle_assignment_authority_is_exact(fence) {
2032 if !controller.process_lease_is_live() {
2033 self.invalidate_shuffle_assignment_fence();
2034 return Err(DbError::Checkpoint(
2035 "shuffle assignment install lost its process lease".into(),
2036 ));
2037 }
2038 return Ok(());
2039 }
2040 let endpoint_conflict = receiver.as_ref().is_some_and(|endpoint| {
2041 endpoint.local_id() != controller.instance_id().0
2042 || endpoint.incarnation() != controller.recovery_incarnation()
2043 || endpoint.assignment_version() > fence.assignment_version
2044 || (endpoint.assignment_version() == fence.assignment_version
2045 && endpoint.active_assignment_digest() != Some(expected_digest))
2046 }) || sender.as_ref().is_some_and(|endpoint| {
2047 endpoint.local_id() != controller.instance_id().0
2048 || endpoint.incarnation() != controller.recovery_incarnation()
2049 || endpoint.assignment_version() > fence.assignment_version
2050 || (endpoint.assignment_version() == fence.assignment_version
2051 && endpoint.active_assignment_digest() != Some(expected_digest))
2052 });
2053 if endpoint_conflict {
2054 self.invalidate_shuffle_assignment_fence();
2055 return Err(DbError::Checkpoint(
2056 "shuffle endpoint identity conflicts with the certified assignment".into(),
2057 ));
2058 }
2059
2060 let result = (|| {
2063 if let Some(receiver) = receiver.as_ref() {
2064 receiver
2065 .install_assignment_fence(fence, &owners)
2066 .map_err(|error| {
2067 DbError::Checkpoint(format!(
2068 "failed to install receiver shuffle assignment certificate: {error}"
2069 ))
2070 })?;
2071 if receiver.assignment_version() != fence.assignment_version
2072 || receiver.active_assignment_digest() != Some(expected_digest)
2073 {
2074 return Err(DbError::Checkpoint(
2075 "receiver did not activate the exact shuffle assignment certificate".into(),
2076 ));
2077 }
2078 }
2079 if let Some(sender) = sender.as_ref() {
2080 sender
2081 .install_assignment_fence(fence, &owners)
2082 .map_err(|error| {
2083 DbError::Checkpoint(format!(
2084 "failed to install sender shuffle assignment certificate: {error}"
2085 ))
2086 })?;
2087 if sender.assignment_version() != fence.assignment_version
2088 || sender.active_assignment_digest() != Some(expected_digest)
2089 {
2090 return Err(DbError::Checkpoint(
2091 "sender did not activate the exact shuffle assignment certificate".into(),
2092 ));
2093 }
2094 }
2095 Ok(())
2096 })();
2097 if result.is_ok() && !controller.process_lease_is_live() {
2098 self.invalidate_shuffle_assignment_fence();
2099 return Err(DbError::Checkpoint(
2100 "shuffle assignment install lost its process lease".into(),
2101 ));
2102 }
2103 if result.is_err() {
2104 self.invalidate_shuffle_assignment_fence();
2107 }
2108 result
2109 }
2110
2111 #[cfg(feature = "cluster")]
2112 async fn assignment_recovery_admission(
2113 &self,
2114 controller: &laminar_core::cluster::control::ClusterController,
2115 deadline: tokio::time::Instant,
2116 ) -> Result<Option<laminar_core::cluster::control::RecoveryAdmissionSnapshot>, DbError> {
2117 let snapshot =
2118 tokio::time::timeout_at(deadline, controller.read_recovery_admission_snapshot())
2119 .await
2120 .map_err(|_| {
2121 DbError::Checkpoint(
2122 "recovery admission audit timed out before opening intake".into(),
2123 )
2124 })?
2125 .map_err(|error| {
2126 DbError::Checkpoint(format!(
2127 "recovery admission authority is unavailable: {error}"
2128 ))
2129 })?;
2130 let (committed_generation, committed_epoch) = match snapshot.committed_release() {
2131 None => (0, 0),
2132 Some(release) => match release.phase {
2133 laminar_core::cluster::control::RecoverPhase::ReleaseCommitted { epoch } => {
2134 (release.round.id.generation, epoch)
2135 }
2136 _ => {
2137 return Err(DbError::Checkpoint(
2138 "latest recovery terminal is not a committed Release".into(),
2139 ));
2140 }
2141 },
2142 };
2143 let local_generation = self.shuffle_recovery_generation()?;
2144 if !snapshot.fault_inventory().faults().is_empty() {
2145 controller.set_recovering(true);
2146 return Ok(None);
2147 }
2148 let transport_is_current =
2149 local_generation.is_none_or(|generation| generation == committed_generation);
2150 let restored_epoch = *self.last_recovery_epoch.lock();
2151 let state_is_current =
2152 committed_epoch == 0 || restored_epoch.is_some_and(|epoch| epoch >= committed_epoch);
2153 if transport_is_current && state_is_current {
2154 return Ok(Some(snapshot));
2155 }
2156
2157 controller.set_recovering(true);
2158 tokio::time::timeout_at(
2159 deadline,
2160 crate::coordinated_recovery::request_local_fault(
2161 controller,
2162 &self.pending_recovery_fault,
2163 ),
2164 )
2165 .await
2166 .map_err(|_| {
2167 DbError::Checkpoint(
2168 "recovery fault publication timed out for stale recovery admission".into(),
2169 )
2170 })?
2171 .map_err(|error| {
2172 DbError::Checkpoint(format!(
2173 "could not publish recovery fault for stale recovery admission: {error}"
2174 ))
2175 })?;
2176 tracing::debug!(
2177 ?local_generation,
2178 committed_generation,
2179 ?restored_epoch,
2180 committed_epoch,
2181 "assignment intake remains fenced for coordinated recovery"
2182 );
2183 Ok(None)
2184 }
2185
2186 #[cfg(feature = "cluster")]
2194 pub(crate) async fn activate_assignment_authority(
2195 &self,
2196 fence: &laminar_core::checkpoint::CheckpointAssignmentFence,
2197 drain_transition: Option<laminar_core::checkpoint::AssignmentDrainTransition>,
2198 expected_revision: u64,
2199 deadline: tokio::time::Instant,
2200 ) -> Result<AssignmentAuthorityActivation, DbError> {
2201 let _adoption = tokio::time::timeout_at(deadline, self.assignment_adoption_lock.lock())
2202 .await
2203 .map_err(|_| {
2204 DbError::Checkpoint("timed out serializing assignment authority activation".into())
2205 })?;
2206 let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
2207 DbError::Checkpoint("assignment activation has no cluster controller".into())
2208 })?;
2209 let installed_transition = controller.checkpoint_drain_transition();
2210 if !assignment_transition_allows_activation(
2213 fence,
2214 drain_transition.as_ref(),
2215 installed_transition.as_ref(),
2216 )? {
2217 return Ok(AssignmentAuthorityActivation {
2218 installed: false,
2219 intake_open: false,
2220 revision: self
2221 .assignment_authority_revision
2222 .load(std::sync::atomic::Ordering::Acquire),
2223 });
2224 }
2225 let _execution = tokio::time::timeout_at(
2226 deadline,
2227 Arc::clone(&self.rotation_execution_fence).write_owned(),
2228 )
2229 .await
2230 .map_err(|_| {
2231 DbError::Checkpoint("timed out draining the prior assignment execution scope".into())
2232 })?;
2233 let revision = self
2234 .assignment_authority_revision
2235 .load(std::sync::atomic::Ordering::Acquire);
2236 if revision != expected_revision || tokio::time::Instant::now() >= deadline {
2237 return Ok(AssignmentAuthorityActivation {
2238 installed: false,
2239 intake_open: false,
2240 revision,
2241 });
2242 }
2243
2244 let intake_was_closed = self.cluster_intake_fenced();
2250 self.set_source_gate(true);
2251 if !controller.process_lease_is_live() {
2252 self.revoke_cluster_authority();
2253 return Ok(AssignmentAuthorityActivation {
2254 installed: false,
2255 intake_open: false,
2256 revision: self
2257 .assignment_authority_revision
2258 .load(std::sync::atomic::Ordering::Acquire),
2259 });
2260 }
2261 let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
2262 DbError::Checkpoint("assignment activation has no vnode registry".into())
2263 })?;
2264 let assignment = registry.versioned_snapshot();
2265 let owners: Vec<u64> = assignment.owners().iter().map(|owner| owner.0).collect();
2266 let local_id = controller.instance_id().0;
2267 let local_incarnation = fence.participant_incarnation(local_id);
2268 if !fence.is_canonical()
2269 || fence.assignment_version != assignment.version()
2270 || fence.vnode_count != registry.vnode_count()
2271 || !fence.matches_owner_map(&owners)
2272 || local_incarnation
2273 .is_some_and(|incarnation| incarnation != controller.recovery_incarnation())
2274 || (local_incarnation.is_none() && owners.contains(&local_id))
2275 {
2276 return Err(DbError::Checkpoint(format!(
2277 "assignment certificate does not match local assignment {} and process {}",
2278 assignment.version(),
2279 local_id
2280 )));
2281 }
2282
2283 if local_incarnation.is_none() {
2287 let shuffle_active = self
2288 .shuffle_receiver
2289 .lock()
2290 .as_ref()
2291 .is_some_and(|receiver| receiver.assignment_version() != 0)
2292 || self
2293 .shuffle_sender
2294 .lock()
2295 .as_ref()
2296 .is_some_and(|sender| sender.assignment_version() != 0);
2297 let revision = if shuffle_active {
2298 self.invalidate_shuffle_assignment_fence();
2299 self.assignment_authority_revision
2300 .load(std::sync::atomic::Ordering::Acquire)
2301 } else {
2302 expected_revision
2303 };
2304 controller.publish_checkpoint_drain_transition(drain_transition);
2305 controller.publish_checkpoint_assignment_fence(Some(fence.clone()));
2306 if !controller.process_lease_is_live()
2307 || self
2308 .assignment_authority_revision
2309 .load(std::sync::atomic::Ordering::Acquire)
2310 != revision
2311 {
2312 if controller.process_lease_is_live() {
2313 self.withdraw_assignment_authority(&controller);
2314 } else {
2315 self.revoke_cluster_authority();
2316 }
2317 return Ok(AssignmentAuthorityActivation {
2318 installed: false,
2319 intake_open: false,
2320 revision: self
2321 .assignment_authority_revision
2322 .load(std::sync::atomic::Ordering::Acquire),
2323 });
2324 }
2325 return Ok(AssignmentAuthorityActivation {
2326 installed: true,
2327 intake_open: false,
2328 revision,
2329 });
2330 }
2331 if let Err(error) = self.install_shuffle_assignment_fence(fence) {
2332 controller.publish_checkpoint_drain_transition(None);
2333 controller.publish_checkpoint_assignment_fence(None);
2334 return Err(error);
2335 }
2336 if self
2337 .assignment_authority_revision
2338 .load(std::sync::atomic::Ordering::Acquire)
2339 != expected_revision
2340 {
2341 self.withdraw_assignment_authority(&controller);
2342 return Ok(AssignmentAuthorityActivation {
2343 installed: false,
2344 intake_open: false,
2345 revision: self
2346 .assignment_authority_revision
2347 .load(std::sync::atomic::Ordering::Acquire),
2348 });
2349 }
2350
2351 let source_drain_active = drain_transition.is_some();
2352 let expected_drain_transition = drain_transition.clone();
2353 controller.publish_checkpoint_drain_transition(drain_transition);
2354 controller.publish_checkpoint_assignment_fence(Some(fence.clone()));
2355 if self
2356 .assignment_authority_revision
2357 .load(std::sync::atomic::Ordering::Acquire)
2358 != expected_revision
2359 {
2360 self.withdraw_assignment_authority(&controller);
2361 return Ok(AssignmentAuthorityActivation {
2362 installed: false,
2363 intake_open: false,
2364 revision: self
2365 .assignment_authority_revision
2366 .load(std::sync::atomic::Ordering::Acquire),
2367 });
2368 }
2369 let intake_open = false;
2370 let preserve_predecessor_execution = source_drain_active && !intake_was_closed;
2371 if !controller.is_recovering() && (!source_drain_active || preserve_predecessor_execution) {
2372 let recovery_admission = match self
2376 .assignment_recovery_admission(&controller, deadline)
2377 .await
2378 {
2379 Ok(Some(snapshot)) => snapshot,
2380 Ok(None) => {
2381 return Ok(AssignmentAuthorityActivation {
2382 installed: true,
2383 intake_open: false,
2384 revision: expected_revision,
2385 });
2386 }
2387 Err(error) => {
2388 self.withdraw_assignment_authority(&controller);
2389 return Err(error);
2390 }
2391 };
2392
2393 if controller.is_recovering() {
2394 return Ok(AssignmentAuthorityActivation {
2395 installed: true,
2396 intake_open: false,
2397 revision: expected_revision,
2398 });
2399 }
2400 let shuffle_sender = { self.shuffle_sender.lock().clone() };
2401 if let Some(sender) = shuffle_sender {
2402 let mut retry_delay = Duration::from_millis(25);
2403 let mut last_error = None;
2404 loop {
2405 if !controller.process_lease_is_live() {
2406 self.revoke_cluster_authority();
2407 return Ok(AssignmentAuthorityActivation {
2408 installed: false,
2409 intake_open: false,
2410 revision: self
2411 .assignment_authority_revision
2412 .load(std::sync::atomic::Ordering::Acquire),
2413 });
2414 }
2415 if self
2416 .assignment_authority_revision
2417 .load(std::sync::atomic::Ordering::Acquire)
2418 != expected_revision
2419 || controller
2420 .checkpoint_assignment_fence(fence.assignment_version)
2421 .as_ref()
2422 != Some(fence)
2423 || controller.checkpoint_drain_transition() != expected_drain_transition
2424 {
2425 return Ok(AssignmentAuthorityActivation {
2426 installed: false,
2427 intake_open: false,
2428 revision: self
2429 .assignment_authority_revision
2430 .load(std::sync::atomic::Ordering::Acquire),
2431 });
2432 }
2433 if tokio::time::Instant::now() >= deadline {
2434 self.withdraw_assignment_authority(&controller);
2435 return Err(DbError::Checkpoint(format!(
2436 "assignment shuffle mesh did not become ready before the activation deadline{}",
2437 last_error
2438 .as_ref()
2439 .map(|error| format!("; last error: {error}"))
2440 .unwrap_or_default()
2441 )));
2442 }
2443 match tokio::time::timeout_at(deadline, sender.establish_assignment_mesh(fence))
2444 .await
2445 {
2446 Ok(Ok(())) => break,
2447 Ok(Err(error)) => last_error = Some(error.to_string()),
2448 Err(_) => {
2449 self.withdraw_assignment_authority(&controller);
2450 return Err(DbError::Checkpoint(format!(
2451 "assignment shuffle mesh readiness timed out{}",
2452 last_error
2453 .as_ref()
2454 .map(|error| format!("; last error: {error}"))
2455 .unwrap_or_default()
2456 )));
2457 }
2458 }
2459 let wake = tokio::time::Instant::now()
2460 .checked_add(retry_delay)
2461 .map_or(deadline, |wake| wake.min(deadline));
2462 tokio::time::sleep_until(wake).await;
2463 retry_delay = retry_delay
2464 .checked_mul(2)
2465 .unwrap_or(Duration::from_millis(250))
2466 .min(Duration::from_millis(250));
2467 }
2468 }
2469
2470 let expected_leader = expected_drain_transition
2471 .as_ref()
2472 .filter(|_| preserve_predecessor_execution)
2473 .map(|transition| transition.leader.clone());
2474 let audited_leader = match controller
2475 .audit_assignment_leader_authority(fence, expected_leader.as_ref(), deadline)
2476 .await
2477 {
2478 Ok(proof) => proof,
2479 Err(error) => {
2480 self.withdraw_assignment_authority(&controller);
2481 return Err(DbError::Checkpoint(format!(
2482 "assignment leader authority audit failed before opening intake: {error}"
2483 )));
2484 }
2485 };
2486 let recovery_admission_current = match tokio::time::timeout_at(
2487 deadline,
2488 controller.recovery_admission_is_current(&recovery_admission, &audited_leader),
2489 )
2490 .await
2491 {
2492 Ok(Ok(current)) => current,
2493 Ok(Err(error)) => {
2494 self.withdraw_assignment_authority(&controller);
2495 return Err(DbError::Checkpoint(format!(
2496 "recovery admission revalidation failed before opening intake: {error}"
2497 )));
2498 }
2499 Err(_) => {
2500 self.withdraw_assignment_authority(&controller);
2501 return Err(DbError::Checkpoint(
2502 "recovery admission revalidation timed out before opening intake".into(),
2503 ));
2504 }
2505 };
2506 if !recovery_admission_current {
2507 self.withdraw_assignment_authority(&controller);
2508 return Ok(AssignmentAuthorityActivation {
2509 installed: false,
2510 intake_open: false,
2511 revision: self
2512 .assignment_authority_revision
2513 .load(std::sync::atomic::Ordering::Acquire),
2514 });
2515 }
2516 return self
2517 .open_assignment_intake_after_audit(
2518 &controller,
2519 fence,
2520 expected_drain_transition.as_ref(),
2521 audited_leader.owner.node_id,
2522 expected_revision,
2523 deadline,
2524 )
2525 .await;
2526 }
2527
2528 Ok(AssignmentAuthorityActivation {
2529 installed: true,
2530 intake_open,
2531 revision: expected_revision,
2532 })
2533 }
2534
2535 #[cfg(feature = "cluster")]
2538 pub(crate) fn purge_shuffle_receiver_buffers(&self) {
2539 let Some(receiver) = self.shuffle_receiver.lock().clone() else {
2540 return;
2541 };
2542 let queued = receiver.drain_available().len();
2543 let staged = receiver.drain_all_staged().len();
2544 let barriers = receiver.drain_staged_barriers().len();
2545 if queued + staged + barriers > 0 {
2546 tracing::info!(
2547 queued,
2548 staged,
2549 barriers,
2550 "purged stale shuffle buffers before coordinated rewind"
2551 );
2552 }
2553 }
2554
2555 #[cfg(feature = "cluster")]
2556 pub(crate) fn set_decision_store(
2557 &self,
2558 store: Arc<laminar_core::cluster::control::CheckpointDecisionStore>,
2559 ) {
2560 *self.decision_store.lock() = Some(store);
2561 }
2562
2563 #[cfg(feature = "cluster")]
2564 pub(crate) fn set_assignment_snapshot_store(
2565 &self,
2566 store: Arc<laminar_core::cluster::control::AssignmentSnapshotStore>,
2567 ) {
2568 *self.assignment_snapshot_store.lock() = Some(store);
2569 }
2570
2571 #[cfg(feature = "cluster")]
2573 pub(crate) fn set_catalog_manifest_store(
2574 &self,
2575 store: Arc<laminar_core::cluster::control::CatalogManifestStore>,
2576 ) {
2577 *self.catalog_manifest_store.lock() = Some(store);
2578 }
2579
2580 #[cfg(feature = "cluster")]
2582 pub(crate) fn set_cluster_checkpoint_object_store(
2583 &mut self,
2584 store: Arc<dyn object_store::ObjectStore>,
2585 ) -> Result<(), DbError> {
2586 if !self.is_cluster_runtime() || self.cluster_controller.lock().is_none() {
2587 return Err(DbError::Config(
2588 "cluster checkpoint object store requires a cluster controller".into(),
2589 ));
2590 }
2591 self.cluster_checkpoint_object_store = Some(store);
2592 Ok(())
2593 }
2594
2595 #[cfg(feature = "cluster")]
2597 pub(crate) fn cluster_checkpoint_object_store(
2598 &self,
2599 ) -> Option<Arc<dyn object_store::ObjectStore>> {
2600 self.cluster_checkpoint_object_store.clone()
2601 }
2602
2603 #[cfg(feature = "cluster")]
2604 fn exact_bootstrap_noop(
2605 &self,
2606 sql: &str,
2607 statement: &StreamingStatement,
2608 ) -> Result<Option<ExecuteResult>, DbError> {
2609 let Some((name, kind, statement_type)) = catalog_create_identity(statement)? else {
2610 return Ok(None);
2611 };
2612 let local_ddl = self
2613 .connector_manager
2614 .lock()
2615 .get_ddl(&name)
2616 .map(str::to_owned);
2617 let local_kind = self.catalog_namespace.lock().get(&name).copied();
2618 if local_ddl.is_none() && local_kind.is_none() {
2619 return Ok(None);
2620 }
2621 if local_ddl.as_deref() != Some(sql) || local_kind != Some(kind) {
2622 return Err(DbError::Pipeline(format!(
2623 "cluster bootstrap definition for '{name}' differs from the durable typed catalog"
2624 )));
2625 }
2626 Ok(Some(ExecuteResult::Ddl(DdlInfo {
2627 statement_type: statement_type.to_string(),
2628 object_name: name,
2629 applied: false,
2630 })))
2631 }
2632
2633 #[cfg(feature = "cluster")]
2634 fn validate_catalog_seal_authority(
2635 &self,
2636 proof: Option<&laminar_core::cluster::control::LeaderProof>,
2637 ) -> Result<(), DbError> {
2638 let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
2639 DbError::Pipeline(
2640 "[LDB-6043] cluster catalog bootstrap requires a cluster controller".into(),
2641 )
2642 })?;
2643 let Some(proof) = proof else {
2644 return Err(DbError::Pipeline(
2645 "[LDB-6043] cluster catalog bootstrap requires the active durable leader lease"
2646 .into(),
2647 ));
2648 };
2649 if !controller.catalog_bootstrap_proof_is_live(proof) {
2650 return Err(DbError::Pipeline(
2651 "[LDB-6043] cluster catalog bootstrap lost its durable leader lease before sealing"
2652 .into(),
2653 ));
2654 }
2655 Ok(())
2656 }
2657
2658 #[cfg(feature = "cluster")]
2664 pub(crate) async fn restore_catalog_from_manifest(
2665 &self,
2666 ) -> Result<Option<laminar_core::cluster::control::CatalogManifest>, DbError> {
2667 self.connector_registry.freeze();
2668 let Some(store) = self.catalog_manifest_store.lock().clone() else {
2669 return Ok(None);
2670 };
2671 let Some(manifest) = store.load().await.map_err(|error| {
2672 DbError::Pipeline(format!(
2673 "[{}] catalog manifest load failed: {error}",
2674 laminar_core::error_codes::RECOVERY_FAILED
2675 ))
2676 })?
2677 else {
2678 return Ok(None);
2679 };
2680
2681 let mut replay_guard = CatalogBootstrapGuard {
2682 db: self,
2683 created: Vec::new(),
2684 sealed: false,
2685 };
2686 for entry in &manifest.entries {
2687 if catalog_ddl_contains_comment(&entry.ddl)? {
2688 return Err(DbError::Pipeline(format!(
2689 "[{}] catalog manifest entry '{}' contains SQL comments rather than one canonical typed definition",
2690 laminar_core::error_codes::RECOVERY_FAILED,
2691 entry.canonical_name
2692 )));
2693 }
2694 let statements = parse_streaming_sql(&entry.ddl).map_err(|error| {
2695 DbError::Pipeline(format!(
2696 "[{}] catalog manifest entry '{}' is not valid topology DDL: {error}",
2697 laminar_core::error_codes::RECOVERY_FAILED,
2698 entry.canonical_name
2699 ))
2700 })?;
2701 if statements.len() != 1 {
2702 return Err(DbError::Pipeline(format!(
2703 "[{}] catalog manifest entry '{}' must contain exactly one typed CREATE statement",
2704 laminar_core::error_codes::RECOVERY_FAILED,
2705 entry.canonical_name
2706 )));
2707 }
2708 let Some((name, kind, _)) = catalog_create_identity(&statements[0])? else {
2709 return Err(DbError::Pipeline(format!(
2710 "[{}] catalog manifest entry '{}' must contain exactly one typed CREATE statement",
2711 laminar_core::error_codes::RECOVERY_FAILED,
2712 entry.canonical_name
2713 )));
2714 };
2715 if name != entry.canonical_name || kind != entry.kind {
2716 return Err(DbError::Pipeline(format!(
2717 "[{}] catalog manifest entry '{}' does not match its typed DDL identity",
2718 laminar_core::error_codes::RECOVERY_FAILED,
2719 entry.canonical_name
2720 )));
2721 }
2722 if connector_source_requires_schema_discovery(&statements[0]) {
2723 return Err(DbError::Pipeline(format!(
2724 "[{}] catalog manifest source '{}' lacks an explicit durable schema",
2725 laminar_core::error_codes::RECOVERY_FAILED,
2726 entry.canonical_name
2727 )));
2728 }
2729 if let Some(key) = sensitive_catalog_property(&statements[0]) {
2730 return Err(DbError::Pipeline(format!(
2731 "[{}] catalog manifest entry '{}' contains secret property '{key}'",
2732 laminar_core::error_codes::RECOVERY_FAILED,
2733 entry.canonical_name
2734 )));
2735 }
2736 }
2737
2738 for entry in &manifest.entries {
2739 let local_ddl = self
2740 .connector_manager
2741 .lock()
2742 .get_ddl(&entry.canonical_name)
2743 .map(str::to_owned);
2744 if let Some(local_ddl) = local_ddl {
2745 let local_kind = self
2746 .catalog_namespace
2747 .lock()
2748 .get(&entry.canonical_name)
2749 .copied();
2750 if local_ddl != entry.ddl || local_kind != Some(entry.kind) {
2751 return Err(DbError::Pipeline(format!(
2752 "[{}] local catalog definition for '{}' conflicts with catalog manifest",
2753 laminar_core::error_codes::RECOVERY_FAILED,
2754 entry.canonical_name
2755 )));
2756 }
2757 continue;
2758 }
2759 CATALOG_MANIFEST_REPLAY
2760 .scope((), self.execute_single_already_gated(&entry.ddl))
2761 .await
2762 .map_err(|error| {
2763 DbError::Pipeline(format!(
2764 "[{}] catalog manifest replay failed for '{}': {error}",
2765 laminar_core::error_codes::RECOVERY_FAILED,
2766 entry.canonical_name
2767 ))
2768 })?;
2769 let replayed_ddl = self
2770 .connector_manager
2771 .lock()
2772 .get_ddl(&entry.canonical_name)
2773 .map(str::to_owned);
2774 let replayed_kind = self
2775 .catalog_namespace
2776 .lock()
2777 .get(&entry.canonical_name)
2778 .copied();
2779 if replayed_ddl.as_deref() != Some(entry.ddl.as_str())
2780 || replayed_kind != Some(entry.kind)
2781 {
2782 return Err(DbError::Pipeline(format!(
2783 "[{}] catalog manifest entry '{}' completed without installing its exact \
2784 durable DDL identity",
2785 laminar_core::error_codes::RECOVERY_FAILED,
2786 entry.canonical_name
2787 )));
2788 }
2789 replay_guard.record(entry.canonical_name.clone(), entry.kind);
2790 tracing::info!(name = %entry.canonical_name, "replayed catalog DDL from manifest");
2791 }
2792
2793 let local_inventory = self.reconcile_catalog_manifest_inventory(&manifest)?;
2794 if local_inventory.as_slice() != manifest.entries.as_slice() {
2795 return Err(DbError::Pipeline(format!(
2796 "[{}] local catalog DDL inventory does not match the ordered catalog manifest \
2797 (local entries: {}, manifest entries: {}); refusing cluster startup",
2798 laminar_core::error_codes::RECOVERY_FAILED,
2799 local_inventory.len(),
2800 manifest.entries.len()
2801 )));
2802 }
2803 replay_guard.sealed();
2804 Ok(Some(manifest))
2805 }
2806
2807 #[cfg(feature = "cluster")]
2814 pub async fn adopt_assignment_snapshot(
2815 &self,
2816 snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2817 deadline: tokio::time::Instant,
2818 ) -> Result<SnapshotAdoption, DbError> {
2819 let version = snapshot.version;
2820 tokio::time::timeout_at(deadline, async {
2821 let _adoption = self.assignment_adoption_lock.lock().await;
2822 self.adopt_assignment_snapshot_locked(
2823 snapshot,
2824 deadline,
2825 AssignmentAdoptionMode::LiveTransition,
2826 )
2827 .await
2828 })
2829 .await
2830 .unwrap_or_else(|_| {
2831 Err(DbError::Checkpoint(format!(
2832 "assignment {version} adoption exceeded its end-to-end deadline"
2833 )))
2834 })
2835 }
2836
2837 #[cfg(feature = "cluster")]
2845 pub(crate) async fn adopt_recovery_assignment_snapshot(
2846 &self,
2847 snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2848 operation_timeout: Duration,
2849 ) -> Result<SnapshotAdoption, DbError> {
2850 let version = snapshot.version;
2851 let _adoption = tokio::time::timeout(operation_timeout, self.assignment_adoption_lock.lock())
2852 .await
2853 .map_err(|_| {
2854 DbError::Checkpoint(format!(
2855 "recovery assignment {version} adoption timed out waiting for assignment serialization"
2856 ))
2857 })?;
2858 let deadline = tokio::time::Instant::now() + operation_timeout;
2859 let mode = if DbState::load(&self.state) == DbState::Faulted {
2860 AssignmentAdoptionMode::ColdRecovery
2861 } else {
2862 AssignmentAdoptionMode::LiveTransition
2863 };
2864 tokio::time::timeout_at(
2865 deadline,
2866 self.adopt_assignment_snapshot_locked(snapshot, deadline, mode),
2867 )
2868 .await
2869 .unwrap_or_else(|_| {
2870 Err(DbError::Checkpoint(format!(
2871 "recovery assignment {version} adoption exceeded its end-to-end deadline"
2872 )))
2873 })
2874 }
2875
2876 #[cfg(feature = "cluster")]
2877 fn adopt_assignment_snapshot_locked(
2878 &self,
2879 snapshot: laminar_core::cluster::control::AssignmentSnapshot,
2880 deadline: tokio::time::Instant,
2881 mode: AssignmentAdoptionMode,
2882 ) -> futures::future::BoxFuture<'_, Result<SnapshotAdoption, DbError>> {
2883 Box::pin(async move {
2884 if snapshot.draining {
2885 return Err(DbError::Checkpoint(format!(
2886 "assignment {} is a draining generation and cannot publish ownership",
2887 snapshot.version
2888 )));
2889 }
2890 if !snapshot.has_canonical_participants() {
2891 return Err(DbError::Checkpoint(format!(
2892 "assignment {} has no canonical process roster",
2893 snapshot.version
2894 )));
2895 }
2896 let Some(registry) = self.vnode_registry.lock().clone() else {
2897 return Ok(SnapshotAdoption::default());
2898 };
2899 let vnode_count = registry.vnode_count();
2900 let new_assignment: Arc<[laminar_core::state::NodeId]> = snapshot
2901 .to_vnode_vec(vnode_count)
2902 .map_err(|error| DbError::Checkpoint(error.to_string()))?
2903 .into();
2904 if snapshot.version <= registry.assignment_version() {
2905 return Ok(SnapshotAdoption {
2906 adopted: false,
2907 version: snapshot.version,
2908 recovery_required: false,
2909 });
2910 }
2911 if self.has_unapplied_vnode_transition() {
2915 return Err(DbError::ShuffleNotReady(format!(
2916 "[LDB-6053] assignment {} cannot overtake an unapplied vnode transition for \
2917 assignment {}; retry after graph execution completes it or an explicitly fenced \
2918 replacement graph restores the committed cut",
2919 snapshot.version,
2920 registry.assignment_version()
2921 )));
2922 }
2923 let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
2924 DbError::Checkpoint(format!(
2925 "[LDB-6053] assignment {} has no live local process identity",
2926 snapshot.version
2927 ))
2928 })?;
2929 let assignment_snapshot_store = self
2930 .assignment_snapshot_store
2931 .lock()
2932 .clone()
2933 .ok_or_else(|| {
2934 DbError::Checkpoint(format!(
2935 "[LDB-6053] assignment {} cannot be adopted without durable assignment history",
2936 snapshot.version
2937 ))
2938 })?;
2939 let durable_target = assignment_snapshot_store
2940 .load_version(snapshot.version)
2941 .await
2942 .map_err(|error| {
2943 DbError::Checkpoint(format!(
2944 "[LDB-6053] failed to load durable assignment {}: {error}",
2945 snapshot.version
2946 ))
2947 })?
2948 .ok_or_else(|| {
2949 DbError::Checkpoint(format!(
2950 "[LDB-6053] assignment {} is absent from durable history",
2951 snapshot.version
2952 ))
2953 })?;
2954 if durable_target != snapshot {
2955 return Err(DbError::Checkpoint(format!(
2956 "[LDB-6053] assignment {} does not match its durable materialization",
2957 snapshot.version
2958 )));
2959 }
2960 let audited_target = crate::rebalance::audit_assignment_snapshot_authority_outcome(
2961 &assignment_snapshot_store,
2962 Some(controller.as_ref()),
2963 &snapshot,
2964 )
2965 .await
2966 .map_err(|error| {
2967 DbError::Checkpoint(format!(
2968 "assignment {} authority audit failed: {error}",
2969 snapshot.version
2970 ))
2971 })?;
2972 let audited_recovery = audited_target.is_recovery();
2973 let audited_predecessor = audited_target.predecessor().cloned();
2974 let committed_handoff_checkpoint = audited_target.handoff_checkpoint().cloned();
2975 let recovery_origin = audited_target.recovery_origin().cloned();
2976 let recovery_proof = audited_target.active_recovery_leader_proof().cloned();
2977 let recovery_was_consumed = audited_target.recovery_checkpoint_was_consumed();
2978 let audited_drain = audited_target.into_terminal();
2979 let terminal_drain_authority = !audited_recovery && audited_drain.is_some();
2980 let aborted_drain_authority = !audited_recovery
2981 && audited_drain
2982 .as_ref()
2983 .is_some_and(crate::rebalance::AuditedDrainOutcome::is_abort);
2984 let committed_drain_authority = !audited_recovery
2985 && committed_handoff_checkpoint.is_some()
2986 && audited_drain
2987 .clone()
2988 .and_then(crate::rebalance::AuditedDrainOutcome::into_committed_transition)
2989 .is_some();
2990 let (mut mode, faulted_terminal_drain_cold) = mode
3001 .after_authority_audit(self, audited_recovery, terminal_drain_authority, deadline)
3002 .await?;
3003 let target_fence = snapshot
3004 .assignment_fence()
3005 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
3006 let recovery_requires_cold = audited_recovery
3007 && (recovery_was_consumed
3008 || recovery_origin.as_ref().is_some_and(|origin| {
3009 audited_predecessor.as_ref().is_some_and(|predecessor| {
3010 origin.assignment_version < predecessor.assignment_version
3011 })
3012 }));
3013 if mode == AssignmentAdoptionMode::ColdRecovery {
3014 if !audited_recovery && !faulted_terminal_drain_cold {
3015 return Err(DbError::Checkpoint(format!(
3016 "[LDB-6053] assignment {} is neither an authority-sequenced recovery successor nor an audited terminal drain",
3017 snapshot.version
3018 )));
3019 }
3020 if audited_recovery {
3021 let expected_handoff = committed_handoff_checkpoint.as_ref().ok_or_else(|| {
3022 DbError::Checkpoint(format!(
3023 "[LDB-6053] recovery assignment {} has no pinned predecessor checkpoint",
3024 snapshot.version
3025 ))
3026 })?;
3027 let authority = controller.checkpoint_authority().map_err(|error| {
3028 DbError::Checkpoint(format!(
3029 "[LDB-6053] recovery assignment {} has no checkpoint authority: {error}",
3030 snapshot.version
3031 ))
3032 })?;
3033 if recovery_proof.is_some() {
3034 let pinned = tokio::time::timeout_at(
3035 deadline,
3036 authority.assignment_handoff_checkpoint(&target_fence),
3037 )
3038 .await
3039 .map_err(|_| {
3040 DbError::Checkpoint(format!(
3041 "[LDB-6053] recovery assignment {} checkpoint-pin audit timed out",
3042 snapshot.version
3043 ))
3044 })?
3045 .map_err(|error| {
3046 DbError::Checkpoint(format!(
3047 "[LDB-6053] recovery assignment {} checkpoint-pin audit failed: {error}",
3048 snapshot.version
3049 ))
3050 })?;
3051 if pinned.as_ref() != Some(expected_handoff) {
3052 return Err(DbError::Checkpoint(format!(
3053 "[LDB-6053] recovery assignment {} does not retain its exact authorized checkpoint pin",
3054 snapshot.version
3055 )));
3056 }
3057 } else if recovery_was_consumed {
3058 let head = tokio::time::timeout_at(
3059 deadline,
3060 authority.highest_cluster_committed_outcome(),
3061 )
3062 .await
3063 .map_err(|_| {
3064 DbError::Checkpoint(format!(
3065 "[LDB-6053] recovery assignment {} consumed-checkpoint audit timed out",
3066 snapshot.version
3067 ))
3068 })?
3069 .map_err(|error| {
3070 DbError::Checkpoint(format!(
3071 "[LDB-6053] recovery assignment {} consumed-checkpoint audit failed: {error}",
3072 snapshot.version
3073 ))
3074 })?
3075 .ok_or_else(|| {
3076 DbError::Checkpoint(format!(
3077 "[LDB-6053] recovery assignment {} has no committed recovery head",
3078 snapshot.version
3079 ))
3080 })?;
3081 if head.assignment_fence.as_ref() != Some(&target_fence)
3082 || head.committed_checkpoint.as_ref() != Some(expected_handoff)
3083 {
3084 return Err(DbError::Checkpoint(format!(
3085 "[LDB-6053] recovery assignment {} no longer owns its consumed committed head",
3086 snapshot.version
3087 )));
3088 }
3089 } else {
3090 return Err(DbError::Checkpoint(format!(
3091 "[LDB-6053] recovery assignment {} has no audited checkpoint lifecycle",
3092 snapshot.version
3093 )));
3094 }
3095 }
3096 }
3097 let self_id = laminar_core::state::NodeId(controller.instance_id().0);
3098 let observed_assignment = registry.versioned_snapshot();
3099 let observed_version = observed_assignment.version();
3100 if observed_version == 0 {
3101 let pristine_boot = observed_assignment
3102 .owners()
3103 .iter()
3104 .all(|owner| *owner == laminar_core::state::NodeId::UNASSIGNED)
3105 && self.pending_vnode_transition.lock().is_none()
3106 && self.installed_vnode_state.lock().is_none();
3107 if !pristine_boot {
3108 return Err(DbError::Checkpoint(
3109 "[LDB-6053] assignment-zero bootstrap contains retained vnode lifecycle state"
3110 .into(),
3111 ));
3112 }
3113 let durable_head = assignment_snapshot_store
3114 .load()
3115 .await
3116 .map_err(|error| {
3117 DbError::Checkpoint(format!(
3118 "[LDB-6053] failed to load the durable assignment head: {error}"
3119 ))
3120 })?
3121 .ok_or_else(|| {
3122 DbError::Checkpoint(
3123 "[LDB-6053] assignment-zero bootstrap has no durable assignment head"
3124 .into(),
3125 )
3126 })?;
3127 let committed = crate::rebalance::startup_committed_assignment(
3128 assignment_snapshot_store.as_ref(),
3129 Some(controller.as_ref()),
3130 durable_head,
3131 )
3132 .await
3133 .map_err(|error| DbError::Checkpoint(format!("[LDB-6053] {error}")))?;
3134 if committed != snapshot {
3135 return Err(DbError::Checkpoint(format!(
3136 "[LDB-6053] assignment-zero bootstrap target {} is not the current committed assignment {}",
3137 snapshot.version, committed.version
3138 )));
3139 }
3140 } else if (mode == AssignmentAdoptionMode::LiveTransition
3141 || faulted_terminal_drain_cold)
3142 && snapshot.version != observed_version.saturating_add(1)
3143 {
3144 return Err(DbError::Checkpoint(format!(
3145 "[LDB-6053] assignment {} is not the exact successor of local assignment {observed_version}; coordinated recovery must install missing generations",
3146 snapshot.version
3147 )));
3148 }
3149 let predecessor_snapshot = if observed_version == 0
3150 && mode == AssignmentAdoptionMode::LiveTransition
3151 {
3152 None
3153 } else {
3154 let predecessor_version = if mode == AssignmentAdoptionMode::ColdRecovery {
3160 snapshot.version.checked_sub(1).ok_or_else(|| {
3161 DbError::Checkpoint(
3162 "[LDB-6053] recovery target has no predecessor generation".into(),
3163 )
3164 })?
3165 } else {
3166 observed_version
3167 };
3168 let predecessor = assignment_snapshot_store
3169 .load_version(predecessor_version)
3170 .await
3171 .map_err(|error| {
3172 DbError::Checkpoint(format!(
3173 "[LDB-6053] failed to load predecessor assignment {predecessor_version}: {error}"
3174 ))
3175 })?
3176 .ok_or_else(|| {
3177 DbError::Checkpoint(format!(
3178 "[LDB-6053] predecessor assignment {predecessor_version} is unavailable"
3179 ))
3180 })?;
3181 let predecessor_owners = predecessor
3182 .to_vnode_vec(vnode_count)
3183 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
3184 let predecessor_fence = predecessor
3185 .assignment_fence()
3186 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
3187 if audited_predecessor.as_ref() != Some(&predecessor_fence) {
3188 return Err(DbError::Checkpoint(format!(
3189 "[LDB-6053] predecessor assignment {predecessor_version} does not match the audited target edge"
3190 )));
3191 }
3192 if (mode == AssignmentAdoptionMode::LiveTransition || faulted_terminal_drain_cold)
3193 && predecessor_owners.as_slice() != observed_assignment.owners()
3194 {
3195 return Err(DbError::Checkpoint(format!(
3196 "[LDB-6053] local owner map at assignment {observed_version} does not match durable history"
3197 )));
3198 }
3199 Some(predecessor)
3200 };
3201 let predecessor_fence = predecessor_snapshot
3202 .as_ref()
3203 .map(laminar_core::cluster::control::AssignmentSnapshot::assignment_fence)
3204 .transpose()
3205 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
3206 let current_incarnation = controller.recovery_incarnation();
3207 let local_participant = laminar_core::checkpoint::CheckpointParticipant {
3208 node_id: self_id.0,
3209 boot_incarnation: current_incarnation,
3210 };
3211 crate::vnode_transition_staging::PendingVnodeTransition::validate_target_process(
3212 &target_fence,
3213 &new_assignment,
3214 local_participant,
3215 )?;
3216 let predecessor_process_is_current =
3217 predecessor_fence.as_ref().is_some_and(|predecessor| {
3218 predecessor.participant_incarnation(self_id.0) == Some(current_incarnation)
3219 });
3220 let effective_predecessor_owned = if predecessor_process_is_current {
3224 owned_vnode_indices(observed_assignment.owners(), self_id)?
3225 } else {
3226 Vec::new()
3227 };
3228 let preflight_new_owned = owned_vnode_indices(&new_assignment, self_id)?;
3229 if mode == AssignmentAdoptionMode::ColdRecovery && preflight_new_owned.is_empty() {
3230 return Err(DbError::Checkpoint(format!(
3231 "[LDB-6053] recovery assignment {} cold publication requires target-bound local vnode ownership",
3232 snapshot.version
3233 )));
3234 }
3235 let final_owner_exit = if mode == AssignmentAdoptionMode::LiveTransition
3236 && !effective_predecessor_owned.is_empty()
3237 && preflight_new_owned.is_empty()
3238 {
3239 Some(audited_drain
3240 .clone()
3241 .and_then(crate::rebalance::AuditedDrainOutcome::into_committed_transition)
3242 .ok_or_else(|| {
3243 DbError::Checkpoint(format!(
3244 "[LDB-6053] assignment {} cannot revoke this process's final vnode without an audited committed drain transition",
3245 snapshot.version
3246 ))
3247 })?)
3248 } else {
3249 None
3250 };
3251 let cold_source_gate_was_closed =
3255 self.source_gate.load(std::sync::atomic::Ordering::Acquire);
3256 self.set_source_gate(true);
3261 controller.publish_checkpoint_assignment_fence(None);
3262 self.invalidate_shuffle_assignment_fence();
3263 let predecessor_drain = Arc::clone(&self.rotation_execution_fence)
3264 .write_owned()
3265 .await;
3266 drop(predecessor_drain);
3267
3268 let guard = self.coordinator.lock().await;
3270 let prepared_pipeline_identity = guard
3271 .as_ref()
3272 .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
3273 .transpose()?;
3274 if snapshot.version <= registry.assignment_version() {
3277 return Ok(SnapshotAdoption {
3278 adopted: false,
3279 version: snapshot.version,
3280 recovery_required: false,
3281 });
3282 }
3283
3284 let prepared_assignment = registry.versioned_snapshot();
3285 let prepared_from_version = prepared_assignment.version();
3286 if prepared_from_version != observed_version {
3287 return Err(DbError::Checkpoint(format!(
3288 "[LDB-6053] assignment base advanced from {observed_version} to {prepared_from_version} while preparing target {}",
3289 snapshot.version
3290 )));
3291 }
3292 let prepared_db_state = DbState::load(&self.state);
3293 let prepared_runtime_shutdown = self.runtime_shutdown.read().clone();
3294 let prepared_installed_state = self.installed_vnode_state.lock().clone();
3295 let prepared_recovery_fault = self
3296 .pending_recovery_fault
3297 .load(std::sync::atomic::Ordering::Acquire);
3298 let prepared_startup_attempt = self.startup_attempt.lock().clone();
3299 let stopped_recovery_topology_common = mode == AssignmentAdoptionMode::LiveTransition
3306 && prepared_db_state == DbState::Created
3307 && prepared_runtime_shutdown.is_cancelled()
3308 && cold_source_gate_was_closed
3309 && self.source_gate.load(std::sync::atomic::Ordering::Acquire)
3310 && controller.process_lease_is_live()
3311 && !self
3312 .cluster_authority_revoked
3313 .load(std::sync::atomic::Ordering::Acquire)
3314 && self
3315 .coordinated_recovery_fenced
3316 .load(std::sync::atomic::Ordering::Acquire)
3317 && !self
3318 .coordinated_lifecycle_active
3319 .load(std::sync::atomic::Ordering::Acquire)
3320 && !self.assignment_restore_shutdown.is_cancelled()
3321 && !self
3322 .catalog_cleanup_fenced
3323 .load(std::sync::atomic::Ordering::Acquire)
3324 && !self.is_closed()
3325 && prepared_startup_attempt
3326 .as_ref()
3327 .is_some_and(|attempt| attempt.is_complete())
3328 && guard.is_some()
3329 && prepared_pipeline_identity.is_some()
3330 && self.pending_vnode_transition.lock().is_none()
3331 && prepared_installed_state.is_none()
3332 && self.recover_target_epoch.lock().is_none()
3333 && snapshot.version == observed_version.saturating_add(1);
3334 let stopped_committed_topology_shape = committed_drain_authority
3335 && observed_assignment
3336 .owners()
3337 .iter()
3338 .all(|owner| *owner != self_id)
3339 && effective_predecessor_owned.is_empty()
3340 && predecessor_fence.as_ref().is_some_and(|predecessor| {
3341 predecessor.participant_incarnation(self_id.0).is_none()
3342 })
3343 && !preflight_new_owned.is_empty()
3344 && target_fence.participant_incarnation(self_id.0) == Some(current_incarnation);
3345 let stopped_committed_owner_topology_shape = committed_drain_authority
3346 && !effective_predecessor_owned.is_empty()
3347 && predecessor_process_is_current
3348 && audited_drain.as_ref().is_some_and(|terminal| {
3349 terminal.is_commit()
3350 && predecessor_fence.as_ref().is_some_and(|predecessor| {
3351 terminal.transition().predecessor == *predecessor
3352 && terminal.transition().target == target_fence
3353 })
3354 })
3355 && if preflight_new_owned.is_empty() {
3356 target_fence.participant_incarnation(self_id.0).is_none()
3357 } else {
3358 target_fence.participant_incarnation(self_id.0) == Some(current_incarnation)
3359 };
3360 let stopped_aborted_owner_topology_shape = aborted_drain_authority
3361 && !effective_predecessor_owned.is_empty()
3362 && predecessor_process_is_current
3363 && predecessor_snapshot.as_ref().is_some_and(|predecessor| {
3364 predecessor.vnodes == snapshot.vnodes
3365 && predecessor.participants == snapshot.participants
3366 })
3367 && new_assignment.as_ref() == observed_assignment.owners()
3368 && preflight_new_owned == effective_predecessor_owned
3369 && predecessor_fence.as_ref().is_some_and(|predecessor| {
3370 predecessor.participants == target_fence.participants
3371 && predecessor.assignment_digest == target_fence.assignment_digest
3372 && predecessor.vnode_count == target_fence.vnode_count
3373 && predecessor.partitioning_abi_version
3374 == target_fence.partitioning_abi_version
3375 && predecessor.assignment_version.checked_add(1)
3376 == Some(target_fence.assignment_version)
3377 })
3378 && target_fence.participant_incarnation(self_id.0) == Some(current_incarnation);
3379 let stopped_aborted_ownerless_topology_shape = aborted_drain_authority
3384 && effective_predecessor_owned.is_empty()
3385 && !predecessor_process_is_current
3386 && observed_assignment
3387 .owners()
3388 .iter()
3389 .all(|owner| *owner != self_id)
3390 && preflight_new_owned.is_empty()
3391 && predecessor_snapshot.as_ref().is_some_and(|predecessor| {
3392 predecessor.vnodes == snapshot.vnodes
3393 && predecessor.participants == snapshot.participants
3394 })
3395 && new_assignment.as_ref() == observed_assignment.owners()
3396 && predecessor_fence.as_ref().is_some_and(|predecessor| {
3397 predecessor.participant_incarnation(self_id.0).is_none()
3398 && predecessor.participants == target_fence.participants
3399 && predecessor.assignment_digest == target_fence.assignment_digest
3400 && predecessor.vnode_count == target_fence.vnode_count
3401 && predecessor.partitioning_abi_version
3402 == target_fence.partitioning_abi_version
3403 && predecessor.assignment_version.checked_add(1)
3404 == Some(target_fence.assignment_version)
3405 })
3406 && target_fence.participant_incarnation(self_id.0).is_none();
3407 let stopped_aborted_topology_shape =
3408 stopped_aborted_owner_topology_shape || stopped_aborted_ownerless_topology_shape;
3409 let stopped_recovery_successor_topology_shape = recovery_proof.is_some()
3410 && committed_handoff_checkpoint.is_some()
3411 && predecessor_fence.is_some();
3412 let prepared_recovery_fault_is_active = if stopped_recovery_topology_common
3413 && stopped_committed_topology_shape
3414 && prepared_recovery_fault != 0
3415 {
3416 let request = controller
3417 .recovery_fault_request(prepared_recovery_fault)
3418 .map_err(|error| {
3419 DbError::Checkpoint(format!(
3420 "[LDB-6053] assignment {} stopped-recovery fault identity is invalid: {error}",
3421 snapshot.version
3422 ))
3423 })?;
3424 let outcome = tokio::time::timeout_at(deadline, controller.report_fault(request))
3425 .await
3426 .map_err(|_| {
3427 DbError::Checkpoint(format!(
3428 "[LDB-6053] assignment {} stopped-recovery fault audit timed out",
3429 snapshot.version
3430 ))
3431 })?
3432 .map_err(|error| {
3433 DbError::Checkpoint(format!(
3434 "[LDB-6053] assignment {} stopped-recovery fault audit failed: {error}",
3435 snapshot.version
3436 ))
3437 })?;
3438 outcome == laminar_core::cluster::control::RecoveryFaultReportOutcome::Active
3439 } else {
3440 false
3441 };
3442 let stopped_abort_round =
3443 if stopped_recovery_topology_common && stopped_aborted_topology_shape {
3444 audited_stopped_terminal_round(
3445 controller.as_ref(),
3446 predecessor_fence
3447 .as_ref()
3448 .expect("abort shape has predecessor"),
3449 deadline,
3450 )
3451 .await?
3452 } else {
3453 None
3454 };
3455 let stopped_commit_owner_round =
3456 if stopped_recovery_topology_common && stopped_committed_owner_topology_shape {
3457 audited_stopped_terminal_round(
3458 controller.as_ref(),
3459 predecessor_fence
3460 .as_ref()
3461 .expect("committed owner shape has predecessor"),
3462 deadline,
3463 )
3464 .await?
3465 } else {
3466 None
3467 };
3468 let stopped_recovery_successor_round =
3469 if stopped_recovery_topology_common && stopped_recovery_successor_topology_shape {
3470 audited_stopped_recovery_successor_round(
3471 controller.as_ref(),
3472 predecessor_fence
3473 .as_ref()
3474 .expect("recovery successor shape has predecessor"),
3475 recovery_proof.as_ref().expect("audited active proof"),
3476 deadline,
3477 )
3478 .await?
3479 } else {
3480 None
3481 };
3482 let stopped_commit_owner_pin_is_active = if stopped_recovery_topology_common
3483 && stopped_committed_owner_topology_shape
3484 {
3485 let expected = committed_handoff_checkpoint
3486 .as_ref()
3487 .expect("committed drain authority has a handoff checkpoint");
3488 let authority = controller.checkpoint_authority().map_err(|error| {
3489 DbError::Checkpoint(format!(
3490 "[LDB-6053] assignment {} stopped-Commit checkpoint authority is unavailable: {error}",
3491 snapshot.version
3492 ))
3493 })?;
3494 let pinned = tokio::time::timeout_at(
3495 deadline,
3496 authority.assignment_handoff_checkpoint(&target_fence),
3497 )
3498 .await
3499 .map_err(|_| {
3500 DbError::Checkpoint(format!(
3501 "[LDB-6053] assignment {} stopped-Commit handoff-pin audit timed out",
3502 snapshot.version
3503 ))
3504 })?
3505 .map_err(|error| {
3506 DbError::Checkpoint(format!(
3507 "[LDB-6053] assignment {} stopped-Commit handoff-pin audit failed: {error}",
3508 snapshot.version
3509 ))
3510 })?;
3511 pinned.as_ref() == Some(expected)
3512 } else {
3513 false
3514 };
3515 let stopped_recovery_topology = stopped_recovery_topology_common
3516 && ((stopped_committed_topology_shape && prepared_recovery_fault_is_active)
3517 || (stopped_aborted_topology_shape && stopped_abort_round.is_some())
3518 || (stopped_committed_owner_topology_shape
3519 && stopped_commit_owner_round.is_some()
3520 && stopped_commit_owner_pin_is_active)
3521 || (stopped_recovery_successor_topology_shape
3522 && stopped_recovery_successor_round.is_some()));
3523 if stopped_recovery_topology {
3524 mode = AssignmentAdoptionMode::StoppedRecoveryTopology;
3525 }
3526 let pristine_startup_bootstrap = prepared_db_state == DbState::Created
3539 && prepared_installed_state.is_none()
3540 && guard.is_none()
3541 && effective_predecessor_owned.is_empty()
3542 && cold_source_gate_was_closed
3543 && controller.is_recovering()
3544 && controller.process_lease_is_live()
3545 && (!audited_recovery || prepared_recovery_fault != 0)
3546 && self.pending_vnode_transition.lock().is_none()
3547 && self.recover_target_epoch.lock().is_none()
3548 && !self
3549 .coordinated_recovery_fenced
3550 .load(std::sync::atomic::Ordering::Acquire)
3551 && !self
3552 .catalog_cleanup_fenced
3553 .load(std::sync::atomic::Ordering::Acquire)
3554 && !self.is_closed();
3555 let metadata_only_zero_owner_transition = effective_predecessor_owned.is_empty()
3556 && preflight_new_owned.is_empty()
3557 && prepared_installed_state.is_none();
3558 if mode == AssignmentAdoptionMode::LiveTransition
3559 && recovery_requires_cold
3560 && !pristine_startup_bootstrap
3561 && !metadata_only_zero_owner_transition
3562 {
3563 return Err(DbError::Checkpoint(format!(
3564 "[LDB-6053] recovery assignment {} must wait for a faulted cold bootstrap because its committed cut is not the live predecessor edge",
3565 snapshot.version
3566 )));
3567 }
3568 if mode == AssignmentAdoptionMode::ColdRecovery
3569 && (prepared_db_state != DbState::Faulted
3570 || !cold_source_gate_was_closed
3571 || !self.source_gate.load(std::sync::atomic::Ordering::Acquire)
3572 || !controller.is_recovering()
3573 || !self
3574 .coordinated_recovery_fenced
3575 .load(std::sync::atomic::Ordering::Acquire)
3576 || prepared_recovery_fault == 0
3577 || !controller.process_lease_is_live()
3578 || self
3579 .catalog_cleanup_fenced
3580 .load(std::sync::atomic::Ordering::Acquire)
3581 || self.is_closed()
3582 || guard.is_none()
3583 || self.pending_vnode_transition.lock().is_some()
3584 || prepared_installed_state.is_some())
3585 {
3586 return Err(DbError::Checkpoint(format!(
3587 "[LDB-6053] recovery assignment {} cold publication requires the exact faulted graph, live process lease, closed source gate, recovery lifecycle fence, pending fault, coordinator, and empty pending/installed vnode state",
3588 snapshot.version
3589 )));
3590 }
3591 let predecessor_binding_missing = local_state_lacks_exact_predecessor_binding(
3592 observed_version,
3593 predecessor_fence.as_ref(),
3594 prepared_pipeline_identity.as_ref(),
3595 (prepared_db_state == DbState::Running)
3596 .then_some(prepared_installed_state.as_ref())
3597 .flatten(),
3598 );
3599 let old_owned = if matches!(
3600 mode,
3601 AssignmentAdoptionMode::ColdRecovery
3602 | AssignmentAdoptionMode::StoppedRecoveryTopology
3603 ) {
3604 Vec::new()
3605 } else {
3606 effective_predecessor_owned
3607 };
3608 if mode == AssignmentAdoptionMode::LiveTransition
3609 && predecessor_binding_missing
3610 && !old_owned.is_empty()
3611 {
3612 return Err(DbError::Checkpoint(format!(
3613 "[LDB-6053] assignment {} cannot reuse retained vnode memory without its exact predecessor binding; coordinated recovery is required",
3614 snapshot.version
3615 )));
3616 }
3617 if mode == AssignmentAdoptionMode::LiveTransition
3618 && old_owned.is_empty()
3619 && prepared_installed_state.is_some()
3620 {
3621 return Err(DbError::Checkpoint(format!(
3622 "[LDB-6053] assignment {} found installed vnode state without current-process predecessor ownership; coordinated recovery is required",
3623 snapshot.version
3624 )));
3625 }
3626 let old_set: rustc_hash::FxHashSet<u32> = old_owned.iter().copied().collect();
3627 let new_owned = preflight_new_owned;
3628 let vnodes_requiring_restore: Vec<u32> = if matches!(
3630 mode,
3631 AssignmentAdoptionMode::ColdRecovery
3632 | AssignmentAdoptionMode::StoppedRecoveryTopology
3633 ) {
3634 Vec::new()
3635 } else {
3636 new_owned
3637 .iter()
3638 .copied()
3639 .filter(|vnode| !old_set.contains(vnode))
3640 .collect()
3641 };
3642 let startup_restore_deferred = matches!(
3645 mode,
3646 AssignmentAdoptionMode::ColdRecovery
3647 | AssignmentAdoptionMode::StoppedRecoveryTopology
3648 ) || pristine_startup_bootstrap;
3649 let prepared_handoff_cut_mismatch = if mode == AssignmentAdoptionMode::LiveTransition
3650 && !vnodes_requiring_restore.is_empty()
3651 && !startup_restore_deferred
3652 {
3653 let handoff = committed_handoff_checkpoint.as_ref().ok_or_else(|| {
3654 DbError::Checkpoint(format!(
3655 "[LDB-6050] assignment {} has no exact committed checkpoint authority for vnode acquisition",
3656 snapshot.version
3657 ))
3658 })?;
3659 let coordinator = guard.as_ref().ok_or_else(|| {
3660 DbError::Checkpoint(format!(
3661 "[LDB-6050] assignment {} has no checkpoint coordinator for vnode acquisition",
3662 snapshot.version
3663 ))
3664 })?;
3665 coordinator.last_committed_ref() != Some(handoff)
3666 } else {
3667 false
3668 };
3669 let state_frames = if mode == AssignmentAdoptionMode::LiveTransition
3670 && !vnodes_requiring_restore.is_empty()
3671 && !startup_restore_deferred
3672 && !prepared_handoff_cut_mismatch
3673 {
3674 let handoff = committed_handoff_checkpoint
3675 .as_ref()
3676 .expect("live vnode acquisition checked exact committed checkpoint authority");
3677 let predecessor = predecessor_fence.as_ref().ok_or_else(|| {
3678 DbError::Checkpoint(format!(
3679 "[LDB-6050] assignment {} has no predecessor certificate for vnode acquisition",
3680 snapshot.version
3681 ))
3682 })?;
3683 let coordinator = guard.as_ref().ok_or_else(|| {
3684 DbError::Checkpoint(format!(
3685 "[LDB-6050] assignment {} has no checkpoint coordinator for vnode acquisition",
3686 snapshot.version
3687 ))
3688 })?;
3689 let max_payload_bytes = self
3690 .config
3691 .pipeline_max_managed_state_bytes
3692 .unwrap_or(crate::config::DEFAULT_MAX_MANAGED_STATE_BYTES);
3693 tokio::select! {
3694 biased;
3695 () = self.assignment_restore_shutdown.cancelled() => {
3696 return Err(DbError::Shutdown);
3697 }
3698 () = prepared_runtime_shutdown.cancelled() => {
3699 return Err(DbError::Shutdown);
3700 }
3701 result = coordinator.load_handoff_state_frames(
3702 handoff,
3703 predecessor,
3704 observed_assignment.owners(),
3705 &vnodes_requiring_restore,
3706 true,
3709 max_payload_bytes,
3710 deadline,
3711 ) => result?,
3712 }
3713 } else {
3714 Vec::new()
3715 };
3716
3717 drop(guard);
3718
3719 let _execution_guard = Arc::clone(&self.rotation_execution_fence)
3722 .write_owned()
3723 .await;
3724 let mut guard = self.coordinator.lock().await;
3725 let current_pipeline_identity = guard
3726 .as_ref()
3727 .map(crate::checkpoint_coordinator::CheckpointCoordinator::bound_pipeline_identity)
3728 .transpose()?;
3729 if current_pipeline_identity != prepared_pipeline_identity {
3730 return Err(DbError::Checkpoint(format!(
3731 "[LDB-6053] pipeline identity changed while preparing assignment {}; retrying the complete transition",
3732 snapshot.version
3733 )));
3734 }
3735 let durable_recovery_fault_is_active = if mode
3736 == AssignmentAdoptionMode::StoppedRecoveryTopology
3737 && stopped_committed_topology_shape
3738 {
3739 let request = controller
3740 .recovery_fault_request(prepared_recovery_fault)
3741 .map_err(|error| {
3742 DbError::Checkpoint(format!(
3743 "[LDB-6053] assignment {} stopped-recovery fault identity changed: {error}",
3744 snapshot.version
3745 ))
3746 })?;
3747 let outcome = tokio::time::timeout_at(deadline, controller.report_fault(request))
3748 .await
3749 .map_err(|_| {
3750 DbError::Checkpoint(format!(
3751 "[LDB-6053] assignment {} stopped-recovery fault recheck timed out",
3752 snapshot.version
3753 ))
3754 })?
3755 .map_err(|error| {
3756 DbError::Checkpoint(format!(
3757 "[LDB-6053] assignment {} stopped-recovery fault recheck failed: {error}",
3758 snapshot.version
3759 ))
3760 })?;
3761 outcome == laminar_core::cluster::control::RecoveryFaultReportOutcome::Active
3762 } else {
3763 true
3764 };
3765 let durable_stopped_abort_round_is_active = if mode
3766 == AssignmentAdoptionMode::StoppedRecoveryTopology
3767 && stopped_aborted_topology_shape
3768 {
3769 audited_stopped_terminal_round(
3770 controller.as_ref(),
3771 predecessor_fence
3772 .as_ref()
3773 .expect("abort shape has predecessor"),
3774 deadline,
3775 )
3776 .await?
3777 == stopped_abort_round
3778 } else {
3779 true
3780 };
3781 let durable_stopped_commit_owner_round_is_active = if mode
3782 == AssignmentAdoptionMode::StoppedRecoveryTopology
3783 && stopped_committed_owner_topology_shape
3784 {
3785 audited_stopped_terminal_round(
3786 controller.as_ref(),
3787 predecessor_fence
3788 .as_ref()
3789 .expect("committed owner shape has predecessor"),
3790 deadline,
3791 )
3792 .await?
3793 == stopped_commit_owner_round
3794 } else {
3795 true
3796 };
3797 let durable_stopped_recovery_successor_round_is_active = if mode
3798 == AssignmentAdoptionMode::StoppedRecoveryTopology
3799 && stopped_recovery_successor_topology_shape
3800 {
3801 audited_stopped_recovery_successor_round(
3802 controller.as_ref(),
3803 predecessor_fence
3804 .as_ref()
3805 .expect("recovery successor shape has predecessor"),
3806 recovery_proof.as_ref().expect("audited active proof"),
3807 deadline,
3808 )
3809 .await?
3810 == stopped_recovery_successor_round
3811 } else {
3812 true
3813 };
3814 let durable_stopped_commit_owner_pin_is_active = if mode
3815 == AssignmentAdoptionMode::StoppedRecoveryTopology
3816 && stopped_committed_owner_topology_shape
3817 {
3818 let expected = committed_handoff_checkpoint
3819 .as_ref()
3820 .expect("committed drain authority has a handoff checkpoint");
3821 let authority = controller.checkpoint_authority().map_err(|error| {
3822 DbError::Checkpoint(format!(
3823 "[LDB-6053] assignment {} stopped-Commit checkpoint authority changed: {error}",
3824 snapshot.version
3825 ))
3826 })?;
3827 let pinned = tokio::time::timeout_at(
3828 deadline,
3829 authority.assignment_handoff_checkpoint(&target_fence),
3830 )
3831 .await
3832 .map_err(|_| {
3833 DbError::Checkpoint(format!(
3834 "[LDB-6053] assignment {} stopped-Commit handoff-pin recheck timed out",
3835 snapshot.version
3836 ))
3837 })?
3838 .map_err(|error| {
3839 DbError::Checkpoint(format!(
3840 "[LDB-6053] assignment {} stopped-Commit handoff-pin recheck failed: {error}",
3841 snapshot.version
3842 ))
3843 })?;
3844 pinned.as_ref() == Some(expected)
3845 } else {
3846 true
3847 };
3848 let durable_stopped_recovery_successor_pin_is_active = if mode
3849 == AssignmentAdoptionMode::StoppedRecoveryTopology
3850 && stopped_recovery_successor_topology_shape
3851 {
3852 let expected = committed_handoff_checkpoint
3853 .as_ref()
3854 .expect("recovery successor authority has a handoff checkpoint");
3855 let authority = controller.checkpoint_authority().map_err(|error| {
3856 DbError::Checkpoint(format!(
3857 "[LDB-6053] assignment {} stopped-recovery checkpoint authority changed: {error}",
3858 snapshot.version
3859 ))
3860 })?;
3861 let pinned = tokio::time::timeout_at(
3862 deadline,
3863 authority.assignment_handoff_checkpoint(&target_fence),
3864 )
3865 .await
3866 .map_err(|_| {
3867 DbError::Checkpoint(format!(
3868 "[LDB-6053] assignment {} stopped-recovery handoff-pin recheck timed out",
3869 snapshot.version
3870 ))
3871 })?
3872 .map_err(|error| {
3873 DbError::Checkpoint(format!(
3874 "[LDB-6053] assignment {} stopped-recovery handoff-pin recheck failed: {error}",
3875 snapshot.version
3876 ))
3877 })?;
3878 pinned.as_ref() == Some(expected)
3879 } else {
3880 true
3881 };
3882 if tokio::time::Instant::now() >= deadline {
3883 return Err(DbError::Checkpoint(format!(
3884 "assignment {} adoption reached its deadline before publication",
3885 snapshot.version
3886 )));
3887 }
3888 let handoff_cut_mismatch = if mode == AssignmentAdoptionMode::LiveTransition
3889 && !vnodes_requiring_restore.is_empty()
3890 && !startup_restore_deferred
3891 {
3892 guard.as_ref().and_then(
3893 crate::checkpoint_coordinator::CheckpointCoordinator::last_committed_ref,
3894 ) != committed_handoff_checkpoint.as_ref()
3895 } else {
3896 false
3897 };
3898 if prepared_handoff_cut_mismatch && !handoff_cut_mismatch {
3899 return Err(DbError::Checkpoint(format!(
3902 "[LDB-6050] assignment {} resident checkpoint advanced to its handoff cut during adoption; retrying",
3903 snapshot.version
3904 )));
3905 }
3906 let current_version = registry.assignment_version();
3907 if snapshot.version <= current_version {
3908 return Ok(SnapshotAdoption {
3909 adopted: false,
3910 version: snapshot.version,
3911 recovery_required: false,
3912 });
3913 }
3914 if current_version != prepared_from_version {
3915 return Err(DbError::Checkpoint(format!(
3916 "[LDB-6053] assignment base advanced from {prepared_from_version} to {current_version} while preparing target {}; retrying from the new owner set",
3917 snapshot.version
3918 )));
3919 }
3920 let adoption = SnapshotAdoption {
3921 adopted: true,
3922 version: snapshot.version,
3923 recovery_required: handoff_cut_mismatch,
3924 };
3925 let has_local_transition = mode == AssignmentAdoptionMode::LiveTransition
3928 && !startup_restore_deferred
3929 && !handoff_cut_mismatch
3930 && (!old_owned.is_empty()
3931 || !vnodes_requiring_restore.is_empty()
3932 || (observed_version != 0 && prepared_db_state == DbState::Running));
3933 let pending_transition = if has_local_transition {
3934 guard.as_ref().ok_or_else(|| {
3935 DbError::Checkpoint(format!(
3936 "[LDB-6053] assignment {} has no checkpoint coordinator for transition identity",
3937 snapshot.version
3938 ))
3939 })?;
3940 let pipeline_identity = current_pipeline_identity.clone().ok_or_else(|| {
3941 DbError::Checkpoint(format!(
3942 "[LDB-6053] assignment {} has no bound pipeline identity",
3943 snapshot.version
3944 ))
3945 })?;
3946 let transition =
3947 crate::vnode_transition_staging::PendingVnodeTransition::assignment_change(
3948 predecessor_fence.clone().ok_or_else(|| {
3949 DbError::Checkpoint(format!(
3950 "[LDB-6053] assignment {} has no predecessor certificate",
3951 snapshot.version
3952 ))
3953 })?,
3954 observed_assignment.owners(),
3955 target_fence.clone(),
3956 &new_assignment,
3957 local_participant,
3958 pipeline_identity,
3959 state_frames,
3960 final_owner_exit,
3961 )?;
3962 if transition.acquired_vnodes() != vnodes_requiring_restore {
3963 return Err(DbError::Checkpoint(format!(
3964 "[LDB-6053] assignment {} transition acquisition roster changed during preparation",
3965 snapshot.version
3966 )));
3967 }
3968 Some(Arc::new(transition))
3969 } else {
3970 None
3971 };
3972 let installed_after_publication = if mode == AssignmentAdoptionMode::LiveTransition
3973 && !new_owned.is_empty()
3974 && pending_transition.is_none()
3975 && vnodes_requiring_restore.is_empty()
3976 {
3977 current_pipeline_identity
3978 .clone()
3979 .map(|pipeline_identity| {
3980 crate::vnode_transition_staging::InstalledVnodeStateBinding::new(
3981 target_fence.clone(),
3982 pipeline_identity,
3983 )
3984 })
3985 .transpose()?
3986 } else {
3987 None
3988 };
3989
3990 if predecessor_readiness_withdrawal_required(
3994 pending_transition
3995 .as_ref()
3996 .is_some_and(|pending| pending.requires_predecessor_binding()),
3997 observed_assignment.version(),
3998 ) {
3999 tokio::time::timeout_at(
4000 deadline,
4001 self.publish_local_vnode_state_report(&controller, &observed_assignment, false),
4002 )
4003 .await
4004 .map_err(|_| {
4005 DbError::Checkpoint(format!(
4006 "assignment {} timed out withdrawing predecessor vnode-state readiness",
4007 snapshot.version
4008 ))
4009 })??;
4010 }
4011
4012 let lifecycle_claim = (mode == AssignmentAdoptionMode::ColdRecovery
4021 || mode == AssignmentAdoptionMode::StoppedRecoveryTopology
4022 || startup_restore_deferred
4023 || handoff_cut_mismatch)
4024 .then(|| self.startup_attempt.lock());
4025 let runtime_shutdown = self.runtime_shutdown.read();
4026 let runtime_generation_is_valid =
4027 if mode == AssignmentAdoptionMode::StoppedRecoveryTopology {
4028 runtime_shutdown.is_cancelled() && prepared_runtime_shutdown.is_cancelled()
4029 } else {
4030 !runtime_shutdown.is_cancelled() && !prepared_runtime_shutdown.is_cancelled()
4031 };
4032 if self.is_closed()
4033 || self.assignment_restore_shutdown.is_cancelled()
4034 || !runtime_generation_is_valid
4035 {
4036 return Err(DbError::Shutdown);
4037 }
4038 let mut pending_slot = self.pending_vnode_transition.lock();
4039 if pending_slot.is_some() {
4040 return Err(DbError::Checkpoint(format!(
4041 "[LDB-6053] assignment {} reached publication while another vnode transition was pending",
4042 snapshot.version
4043 )));
4044 }
4045 let mut installed_state = self.installed_vnode_state.lock();
4046 if DbState::load(&self.state) != prepared_db_state
4047 || installed_state.as_ref() != prepared_installed_state.as_ref()
4048 {
4049 return Err(DbError::Checkpoint(format!(
4050 "[LDB-6053] local execution state changed while preparing assignment {}; retrying from the current state",
4051 snapshot.version
4052 )));
4053 }
4054 if startup_restore_deferred
4055 && mode == AssignmentAdoptionMode::LiveTransition
4056 && (prepared_db_state != DbState::Created
4057 || lifecycle_claim
4058 .as_ref()
4059 .is_none_or(|attempt| attempt.as_ref().is_some())
4060 || !self.source_gate.load(std::sync::atomic::Ordering::Acquire)
4061 || !controller.is_recovering()
4062 || !controller.process_lease_is_live()
4063 || (audited_recovery
4064 && (prepared_recovery_fault == 0
4065 || self
4066 .pending_recovery_fault
4067 .load(std::sync::atomic::Ordering::Acquire)
4068 != prepared_recovery_fault))
4069 || guard.is_some()
4070 || pending_slot.is_some()
4071 || installed_state.is_some()
4072 || self.recover_target_epoch.lock().is_some()
4073 || self
4074 .coordinated_recovery_fenced
4075 .load(std::sync::atomic::Ordering::Acquire)
4076 || self
4077 .catalog_cleanup_fenced
4078 .load(std::sync::atomic::Ordering::Acquire))
4079 {
4080 return Err(DbError::Checkpoint(format!(
4081 "[LDB-6053] assignment {} startup-deferred publication requires the exact Created assignment-zero lifecycle with closed intake, live process authority, and no concurrent startup or recovered state",
4082 snapshot.version
4083 )));
4084 }
4085 if mode == AssignmentAdoptionMode::ColdRecovery
4086 && (!self.source_gate.load(std::sync::atomic::Ordering::Acquire)
4087 || !controller.is_recovering()
4088 || !self
4089 .coordinated_recovery_fenced
4090 .load(std::sync::atomic::Ordering::Acquire)
4091 || self
4092 .pending_recovery_fault
4093 .load(std::sync::atomic::Ordering::Acquire)
4094 != prepared_recovery_fault
4095 || prepared_recovery_fault == 0
4096 || !controller.process_lease_is_live()
4097 || self
4098 .catalog_cleanup_fenced
4099 .load(std::sync::atomic::Ordering::Acquire)
4100 || pending_slot.is_some()
4101 || installed_state.is_some())
4102 {
4103 return Err(DbError::Checkpoint(format!(
4104 "[LDB-6053] recovery assignment {} cold-publication authority changed before registry commit",
4105 snapshot.version
4106 )));
4107 }
4108 let stopped_startup_attempt_is_same = lifecycle_claim.as_ref().is_some_and(|owned| {
4113 let (Some(current), Some(prepared)) =
4114 (owned.as_ref(), prepared_startup_attempt.as_ref())
4115 else {
4116 return false;
4117 };
4118 Arc::ptr_eq(current, prepared) && current.is_complete()
4119 });
4120 let stopped_topology_shape_is_same = (stopped_committed_topology_shape
4121 && prepared_recovery_fault != 0
4122 && self
4123 .pending_recovery_fault
4124 .load(std::sync::atomic::Ordering::Acquire)
4125 == prepared_recovery_fault
4126 && durable_recovery_fault_is_active)
4127 || (stopped_aborted_topology_shape
4128 && stopped_abort_round.is_some()
4129 && durable_stopped_abort_round_is_active)
4130 || (stopped_committed_owner_topology_shape
4131 && stopped_commit_owner_round.is_some()
4132 && stopped_commit_owner_pin_is_active
4133 && durable_stopped_commit_owner_round_is_active
4134 && durable_stopped_commit_owner_pin_is_active)
4135 || (stopped_recovery_successor_topology_shape
4136 && stopped_recovery_successor_round.is_some()
4137 && durable_stopped_recovery_successor_round_is_active
4138 && durable_stopped_recovery_successor_pin_is_active);
4139 if mode == AssignmentAdoptionMode::StoppedRecoveryTopology
4140 && (prepared_db_state != DbState::Created
4141 || !cold_source_gate_was_closed
4142 || !self.source_gate.load(std::sync::atomic::Ordering::Acquire)
4143 || !controller.process_lease_is_live()
4144 || self
4145 .cluster_authority_revoked
4146 .load(std::sync::atomic::Ordering::Acquire)
4147 || !self
4148 .coordinated_recovery_fenced
4149 .load(std::sync::atomic::Ordering::Acquire)
4150 || self
4151 .coordinated_lifecycle_active
4152 .load(std::sync::atomic::Ordering::Acquire)
4153 || self
4154 .catalog_cleanup_fenced
4155 .load(std::sync::atomic::Ordering::Acquire)
4156 || self.recover_target_epoch.lock().is_some()
4157 || !stopped_startup_attempt_is_same
4158 || !stopped_topology_shape_is_same
4159 || guard.is_none()
4160 || current_pipeline_identity.is_none()
4161 || pending_slot.is_some()
4162 || installed_state.is_some()
4163 || registry.versioned_snapshot().owners() != prepared_assignment.owners()
4164 || if new_owned.is_empty() {
4165 target_fence.participant_incarnation(self_id.0).is_some()
4166 } else {
4167 target_fence.participant_incarnation(self_id.0) != Some(current_incarnation)
4168 }
4169 || snapshot.version != observed_version.saturating_add(1))
4170 {
4171 return Err(DbError::Checkpoint(format!(
4172 "[LDB-6053] assignment {} stopped-recovery topology authority changed before registry commit",
4173 snapshot.version
4174 )));
4175 }
4176 if mode == AssignmentAdoptionMode::StoppedRecoveryTopology || handoff_cut_mismatch {
4177 controller.set_recovering(true);
4181 controller.publish_checkpoint_drain_transition(None);
4182 controller.publish_checkpoint_assignment_fence(None);
4183 }
4184 if handoff_cut_mismatch {
4185 self.coordinated_recovery_fenced
4189 .store(true, std::sync::atomic::Ordering::Release);
4190 crate::coordinated_recovery::queue_local_fault(
4191 &controller,
4192 &self.pending_recovery_fault,
4193 )
4194 .map_err(|error| {
4195 DbError::Checkpoint(format!(
4196 "[LDB-6050] assignment {} could not queue exact-handoff recovery: {error}",
4197 snapshot.version
4198 ))
4199 })?;
4200 }
4201 *pending_slot = pending_transition;
4202 registry.set_assignment_and_version(new_assignment, snapshot.version);
4203 if let Some(coord) = guard.as_mut() {
4204 coord.set_assignment_version(snapshot.version);
4205 coord.set_vnode_set(new_owned.clone());
4206 }
4207 if pending_slot.is_none() {
4208 *installed_state = installed_after_publication;
4209 }
4210 if handoff_cut_mismatch {
4211 *installed_state = None;
4215 }
4216 drop(installed_state);
4217 drop(pending_slot);
4218 drop(guard);
4219 drop(runtime_shutdown);
4220 drop(lifecycle_claim);
4221
4222 tracing::info!(version = snapshot.version, "adopted assignment snapshot",);
4223 Ok(adoption)
4224 })
4225 }
4226
4227 #[cfg(feature = "cluster")]
4229 pub(crate) async fn prepare_local_source_drain(
4230 &self,
4231 transition: &laminar_core::checkpoint::AssignmentDrainTransition,
4232 participant: laminar_core::checkpoint::CheckpointParticipant,
4233 deadline: tokio::time::Instant,
4234 ) -> Result<(), DbError> {
4235 crate::pipeline::streaming_coordinator::prepare_owned_source_drain(
4236 &self.owned_source_tasks,
4237 transition,
4238 participant,
4239 deadline,
4240 )
4241 .await
4242 .map_err(|error| DbError::Checkpoint(format!("source drain failed: {error}")))
4243 }
4244
4245 #[cfg(feature = "cluster")]
4247 pub(crate) async fn resolve_local_source_drain(
4248 &self,
4249 round: laminar_core::checkpoint::AssignmentDrainId,
4250 outcome: laminar_connectors::connector::SourceDrainOutcome,
4251 deadline: tokio::time::Instant,
4252 ) -> Result<(), DbError> {
4253 crate::pipeline::streaming_coordinator::resolve_owned_source_drain(
4254 &self.owned_source_tasks,
4255 laminar_connectors::connector::SourceDrainResolution { round, outcome },
4256 deadline,
4257 )
4258 .await
4259 .map_err(|error| DbError::Checkpoint(format!("source drain resolution failed: {error}")))
4260 }
4261
4262 #[cfg(feature = "cluster")]
4265 pub(crate) fn validate_source_drain_snapshot(
4266 &self,
4267 snapshot: &laminar_core::cluster::control::AssignmentSnapshot,
4268 ) -> Result<(), DbError> {
4269 if !snapshot.draining {
4270 return Err(DbError::Checkpoint(format!(
4271 "assignment {} is not a draining generation",
4272 snapshot.version
4273 )));
4274 }
4275 if !snapshot.has_canonical_participants() {
4276 return Err(DbError::Checkpoint(format!(
4277 "draining assignment {} has no canonical process roster",
4278 snapshot.version
4279 )));
4280 }
4281 let registry = self.vnode_registry.lock().clone().ok_or_else(|| {
4282 DbError::Checkpoint(format!(
4283 "draining assignment {} cannot be validated without a vnode registry",
4284 snapshot.version
4285 ))
4286 })?;
4287 let expected_version = registry
4288 .assignment_version()
4289 .checked_add(1)
4290 .ok_or_else(|| DbError::Checkpoint("assignment version overflow".into()))?;
4291 if snapshot.version != expected_version {
4292 return Err(DbError::Checkpoint(format!(
4293 "draining assignment {} is not the exact successor of local assignment {}",
4294 snapshot.version,
4295 registry.assignment_version()
4296 )));
4297 }
4298 snapshot
4299 .to_vnode_vec(registry.vnode_count())
4300 .map_err(|error| DbError::Checkpoint(error.to_string()))?;
4301 tracing::info!(
4302 version = snapshot.version,
4303 "validated global source-drain generation"
4304 );
4305 Ok(())
4306 }
4307
4308 #[cfg(feature = "cluster")]
4309 pub(crate) fn set_cluster_controller(
4310 &self,
4311 controller: Arc<laminar_core::cluster::control::ClusterController>,
4312 ) -> Result<(), DbError> {
4313 if !self.is_cluster_runtime() {
4314 return Err(DbError::Config(
4315 "cluster controller cannot be installed on a local runtime".into(),
4316 ));
4317 }
4318 let mut installed = self.cluster_controller.lock();
4319 match installed.as_ref() {
4320 None => {
4321 *installed = Some(controller);
4322 Ok(())
4323 }
4324 Some(current) if Arc::ptr_eq(current, &controller) => Ok(()),
4325 Some(_) => Err(DbError::Config(
4326 "cluster controller replacement requires a new LaminarDB graph generation".into(),
4327 )),
4328 }
4329 }
4330
4331 pub(crate) fn set_vnode_registry(&self, registry: Arc<laminar_core::state::VnodeRegistry>) {
4332 *self.vnode_registry.lock() = Some(registry);
4333 }
4334
4335 #[cfg(feature = "cluster")]
4343 pub async fn collect_local_table(&self, name: &str) -> Result<Vec<RecordBatch>, DbError> {
4344 const LOCAL_SCAN_NAME: &str = "__laminar_local_table_scan";
4345
4346 let _catalog_guard = self.topology_ddl_lock.read().await;
4347 let provider = self.ctx.table_provider(exact_table_reference(name)).await?;
4348 let context = SessionContext::new();
4349 context.register_table(exact_table_reference(LOCAL_SCAN_NAME), provider)?;
4350 Ok(context
4351 .sql(&format!("SELECT * FROM {LOCAL_SCAN_NAME}"))
4352 .await?
4353 .collect()
4354 .await?)
4355 }
4356
4357 #[must_use]
4359 pub fn builder() -> LaminarDbBuilder {
4360 LaminarDbBuilder::new()
4361 }
4362
4363 #[allow(unused_variables)]
4364 fn register_builtin_connectors(
4365 registry: &laminar_connectors::registry::ConnectorRegistry,
4366 ) -> Result<(), laminar_connectors::error::ConnectorError> {
4367 laminar_connectors::generator::register_generator_source(registry)?;
4368 #[cfg(feature = "kafka")]
4369 {
4370 laminar_connectors::kafka::register_kafka_source(registry)?;
4371 laminar_connectors::kafka::register_kafka_sink(registry)?;
4372 }
4373 #[cfg(feature = "postgres-cdc")]
4374 {
4375 laminar_connectors::postgres::register_postgres_cdc_source(registry)?;
4376 }
4377 #[cfg(feature = "postgres-sink")]
4378 {
4379 laminar_connectors::postgres::register_postgres_sink(registry)?;
4380 }
4381 #[cfg(feature = "delta-lake")]
4382 {
4383 laminar_connectors::lakehouse::register_delta_lake_sink(registry)?;
4384 laminar_connectors::lakehouse::register_delta_lake_source(registry)?;
4385 }
4386 #[cfg(any(
4387 feature = "iceberg",
4388 feature = "iceberg-gcs",
4389 feature = "iceberg-azure"
4390 ))]
4391 {
4392 laminar_connectors::lakehouse::register_iceberg_sink(registry)?;
4393 laminar_connectors::lakehouse::register_iceberg_source(registry)?;
4394 }
4395 #[cfg(feature = "websocket")]
4396 {
4397 laminar_connectors::websocket::register_websocket_source(registry)?;
4398 laminar_connectors::websocket::register_websocket_sink(registry)?;
4399 }
4400 #[cfg(feature = "mongodb-cdc")]
4401 {
4402 laminar_connectors::mongodb::register_mongodb_cdc_source(registry)?;
4403 laminar_connectors::mongodb::register_mongodb_sink(registry)?;
4404 }
4405 #[cfg(feature = "files")]
4406 {
4407 laminar_connectors::files::register_file_source(registry)?;
4408 laminar_connectors::files::register_file_sink(registry)?;
4409 }
4410 #[cfg(feature = "otel")]
4411 {
4412 laminar_connectors::otel::register_otel_source(registry)?;
4413 }
4414 #[cfg(feature = "nats")]
4415 {
4416 laminar_connectors::nats::register_nats_source(registry)?;
4417 laminar_connectors::nats::register_nats_sink(registry)?;
4418 }
4419 Ok(())
4420 }
4421
4422 fn handle_register_lookup_table(
4423 &self,
4424 info: laminar_sql::planner::LookupTableInfo,
4425 ) -> Result<ExecuteResult, DbError> {
4426 use laminar_sql::parser::lookup_table::LookupConnector;
4427
4428 self.preflight_lookup_connector(&info.properties)?;
4429 if info.primary_key.len() != 1 {
4430 return Err(DbError::InvalidOperation(
4431 "Lookup table requires a single-column primary key".into(),
4432 ));
4433 }
4434 let pk = info.primary_key[0].clone();
4435
4436 self.table_store
4437 .write()
4438 .create_table(&info.name, info.arrow_schema.clone(), &pk)?;
4439
4440 if matches!(&info.properties.connector, LookupConnector::External(_)) {
4441 self.register_lookup_connector(&info, &pk);
4442 }
4443
4444 {
4445 let provider = crate::table_provider::ReferenceTableProvider::new(
4446 info.name.clone(),
4447 info.arrow_schema.clone(),
4448 self.table_store.clone(),
4449 );
4450 match self
4451 .ctx
4452 .register_table(exact_table_reference(&info.name), Arc::new(provider))
4453 {
4454 Ok(None) => {}
4455 Ok(Some(previous)) => {
4456 let _ = self
4457 .ctx
4458 .register_table(exact_table_reference(&info.name), previous);
4459 return Err(DbError::InvalidOperation(format!(
4460 "cannot create lookup table '{}': its provider was claimed concurrently",
4461 info.name
4462 )));
4463 }
4464 Err(error) => {
4465 return Err(DbError::InvalidOperation(format!(
4466 "failed to register lookup table '{}': {error}",
4467 info.name
4468 )));
4469 }
4470 }
4471 }
4472
4473 if let Some(batch) = self.table_store.read().to_record_batch(&info.name)? {
4474 self.lookup_registry.register(
4475 &info.name,
4476 laminar_sql::datafusion::LookupSnapshot { batch },
4477 );
4478 }
4479
4480 self.refresh_lookup_optimizer_rule();
4481
4482 Ok(ExecuteResult::Ddl(DdlInfo {
4483 statement_type: "CREATE LOOKUP TABLE".to_string(),
4484 object_name: info.name,
4485 applied: true,
4486 }))
4487 }
4488
4489 fn preflight_lookup_connector(
4490 &self,
4491 properties: &laminar_sql::parser::lookup_table::LookupTableProperties,
4492 ) -> Result<(), DbError> {
4493 use laminar_sql::parser::lookup_table::{LookupConnector, LookupStrategy};
4494
4495 if properties.strategy == LookupStrategy::OnDemand
4496 && self.config.delivery_guarantee
4497 == laminar_connectors::connector::DeliveryGuarantee::ExactlyOnce
4498 {
4499 return Err(DbError::InvalidOperation(
4500 "on-demand LOOKUP TABLE is incompatible with exactly-once delivery because \
4501 external lookup results are not checkpointed"
4502 .into(),
4503 ));
4504 }
4505 let connector = match &properties.connector {
4506 LookupConnector::Static => {
4507 if properties.strategy == LookupStrategy::OnDemand {
4508 return Err(DbError::InvalidOperation(
4509 "static LOOKUP TABLE supports only the replicated strategy".into(),
4510 ));
4511 }
4512 return Ok(());
4513 }
4514 LookupConnector::External(name) => name,
4515 };
4516 let (available, capability) = match properties.strategy {
4517 LookupStrategy::Replicated => (
4518 self.connector_registry.has_table_source(connector),
4519 "snapshot-capable table source",
4520 ),
4521 LookupStrategy::OnDemand => (
4522 self.connector_registry.has_lookup_source(connector),
4523 "on-demand lookup source",
4524 ),
4525 };
4526 if !available {
4527 return Err(DbError::InvalidOperation(format!(
4528 "LOOKUP TABLE connector '{connector}' has no registered {capability} required \
4529 by strategy '{}'",
4530 properties.strategy
4531 )));
4532 }
4533 Ok(())
4534 }
4535
4536 fn register_lookup_connector(&self, info: &laminar_sql::planner::LookupTableInfo, pk: &str) {
4537 use laminar_sql::parser::lookup_table::LookupConnector;
4538
4539 let connector_type = match &info.properties.connector {
4540 LookupConnector::External(name) => name.clone(),
4541 LookupConnector::Static => unreachable!(),
4542 };
4543
4544 self.table_store
4545 .write()
4546 .set_connector(&info.name, &connector_type);
4547
4548 let consumed = [
4551 "connector",
4552 "strategy",
4553 "cache.memory",
4554 "cache.ttl",
4555 "pushdown",
4556 "format",
4557 ];
4558 let mut connector_options = HashMap::with_capacity(info.raw_options.len());
4559 let mut format_options = HashMap::with_capacity(4);
4560 for (k, v) in &info.raw_options {
4561 let lower = k.to_lowercase();
4562 if consumed.contains(&lower.as_str()) {
4563 continue;
4564 }
4565 if let Some(suffix) = lower.strip_prefix("format.") {
4566 format_options.insert(suffix.to_string(), v.clone());
4567 } else {
4568 connector_options.insert(k.clone(), v.clone());
4569 }
4570 }
4571
4572 let cache_max_bytes = info
4574 .properties
4575 .cache_memory
4576 .map(|m| usize::try_from(m.as_bytes()).unwrap_or(usize::MAX));
4577
4578 let cache_ttl = info
4579 .properties
4580 .cache_ttl
4581 .map(std::time::Duration::from_secs);
4582
4583 self.connector_manager
4584 .lock()
4585 .register_table(crate::connector_manager::TableRegistration {
4586 name: info.name.clone(),
4587 primary_key: pk.to_string(),
4588 connector_type: Some(connector_type),
4589 connector_options,
4590 format: info.raw_options.get("format").cloned(),
4591 format_options,
4592 on_demand: matches!(
4593 info.properties.strategy,
4594 laminar_sql::parser::lookup_table::LookupStrategy::OnDemand
4595 ),
4596 cache_max_bytes,
4597 cache_ttl,
4598 });
4599 }
4600
4601 pub(crate) fn refresh_lookup_optimizer_rule(&self) {
4603 use laminar_sql::planner::lookup_join::{LookupColumnPruningRule, LookupJoinRewriteRule};
4604 use laminar_sql::planner::predicate_split::{
4605 PlanPushdownMode, PlanSourceCapabilities, PredicateSplitterRule,
4606 SourceCapabilitiesRegistry,
4607 };
4608
4609 self.ctx.remove_optimizer_rule("lookup_join_rewrite");
4610 self.ctx.remove_optimizer_rule("predicate_splitter");
4611 self.ctx.remove_optimizer_rule("lookup_column_pruning");
4612
4613 let tables = self.planner.lock().lookup_tables_cloned();
4614 if tables.is_empty() {
4615 return;
4616 }
4617
4618 let mut caps_registry = SourceCapabilitiesRegistry::default();
4619 for (name, info) in &tables {
4620 let mode = match info.properties.pushdown_mode {
4621 laminar_sql::parser::lookup_table::PushdownMode::Enabled
4622 | laminar_sql::parser::lookup_table::PushdownMode::Auto => PlanPushdownMode::Full,
4623 laminar_sql::parser::lookup_table::PushdownMode::Disabled => PlanPushdownMode::None,
4624 };
4625 let pk_set: std::collections::HashSet<String> =
4626 info.primary_key.iter().cloned().collect();
4627 caps_registry.register(
4628 name.clone(),
4629 PlanSourceCapabilities {
4630 pushdown_mode: mode,
4631 eq_columns: pk_set,
4632 range_columns: std::collections::HashSet::new(),
4633 in_columns: std::collections::HashSet::new(),
4634 supports_null_check: false,
4635 },
4636 );
4637 }
4638
4639 self.ctx
4640 .add_optimizer_rule(Arc::new(LookupJoinRewriteRule::new(tables)));
4641 self.ctx
4642 .add_optimizer_rule(Arc::new(PredicateSplitterRule::new(caps_registry)));
4643 self.ctx
4644 .add_optimizer_rule(Arc::new(LookupColumnPruningRule));
4645 }
4646
4647 #[must_use]
4650 pub fn connector_registry(&self) -> &laminar_connectors::registry::ConnectorRegistry {
4651 &self.connector_registry
4652 }
4653
4654 pub(crate) fn register_custom_udf(
4656 &mut self,
4657 udf: datafusion_expr::ScalarUDF,
4658 ) -> Result<(), DbError> {
4659 validate_custom_function_name("scalar UDF", udf.name(), udf.aliases())?;
4660 self.ctx.register_udf(udf.clone());
4661 self.custom_udfs.push(udf);
4662 Ok(())
4663 }
4664
4665 pub(crate) fn register_custom_udaf(
4667 &mut self,
4668 udaf: datafusion_expr::AggregateUDF,
4669 ) -> Result<(), DbError> {
4670 validate_custom_function_name("aggregate UDF", udaf.name(), udaf.aliases())?;
4671 self.ctx.register_udaf(udaf.clone());
4672 self.custom_udafs.push(udaf);
4673 Ok(())
4674 }
4675
4676 pub(crate) fn register_custom_functions_into(&self, ctx: &SessionContext) {
4677 for udf in &self.custom_udfs {
4678 ctx.register_udf(udf.clone());
4679 }
4680 for udaf in &self.custom_udafs {
4681 ctx.register_udaf(udaf.clone());
4682 }
4683 }
4684
4685 pub async fn execute(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4691 if self.shutdown.load(std::sync::atomic::Ordering::Relaxed) {
4692 return Err(DbError::Shutdown);
4693 }
4694
4695 let stmts = sql_utils::split_statements(sql);
4696 if stmts.is_empty() {
4697 return Err(DbError::InvalidOperation("Empty SQL statement".into()));
4698 }
4699
4700 let mut last_result = None;
4701 for stmt_sql in &stmts {
4702 last_result = Some(self.execute_single(stmt_sql).await?);
4703 }
4704
4705 last_result.ok_or_else(|| DbError::InvalidOperation("Empty SQL statement".into()))
4706 }
4707
4708 #[cfg(feature = "cluster")]
4715 pub async fn execute_cluster_bootstrap_batch(
4716 &self,
4717 sql: &[String],
4718 ) -> Result<Vec<ExecuteResult>, DbError> {
4719 self.ensure_catalog_cleanup_unfenced("cluster catalog bootstrap")?;
4720 self.connector_registry.freeze();
4721 if self.shutdown.load(std::sync::atomic::Ordering::Relaxed) {
4722 return Err(DbError::Shutdown);
4723 }
4724 if DbState::load(&self.state) != DbState::Created {
4725 return Err(DbError::InvalidOperation(
4726 "cluster catalog bootstrap is only valid before pipeline startup".into(),
4727 ));
4728 }
4729
4730 let mut parsed = Vec::new();
4731 for batch_entry in sql {
4732 for stmt_sql in sql_utils::split_statements(batch_entry) {
4733 let mut statements = parse_streaming_sql(stmt_sql)?;
4734 if statements.len() != 1 {
4735 return Err(DbError::InvalidOperation(
4736 "cluster bootstrap entries must contain exactly one SQL statement".into(),
4737 ));
4738 }
4739 let statement = statements.pop().ok_or_else(|| {
4740 DbError::InvalidOperation(
4741 "cluster bootstrap entries must contain exactly one SQL statement".into(),
4742 )
4743 })?;
4744 let (name, kind, _) = validate_cluster_catalog_create(self, stmt_sql, &statement)?;
4745 parsed.push((stmt_sql.to_owned(), statement, name, kind));
4746 }
4747 }
4748 {
4749 let mut names = std::collections::HashSet::with_capacity(parsed.len());
4750 for (_, _, name, _) in &parsed {
4751 if !names.insert(name.as_str()) {
4752 return Err(DbError::InvalidOperation(format!(
4753 "cluster bootstrap defines '{name}' more than once"
4754 )));
4755 }
4756 }
4757 }
4758
4759 let _topology_ddl = self.topology_ddl_lock.write().await;
4760 self.ensure_catalog_cleanup_unfenced("cluster catalog bootstrap")?;
4761 self.ensure_coordinated_recovery_mutation_unfenced("cluster catalog bootstrap")?;
4762 if DbState::load(&self.state) != DbState::Created {
4763 return Err(DbError::InvalidOperation(
4764 "cluster catalog bootstrap is only valid before pipeline startup".into(),
4765 ));
4766 }
4767
4768 if let Some(manifest) = self.restore_catalog_from_manifest().await? {
4769 let configured_matches = parsed.iter().zip(&manifest.entries).all(
4770 |((ddl, _, canonical_name, kind), entry)| {
4771 ddl == &entry.ddl
4772 && canonical_name == &entry.canonical_name
4773 && kind == &entry.kind
4774 },
4775 );
4776 if parsed.len() != manifest.entries.len() || !configured_matches {
4777 return Err(DbError::Pipeline(format!(
4778 "configured cluster catalog must exactly match the complete ordered sealed inventory (configured entries: {}, sealed entries: {})",
4779 parsed.len(),
4780 manifest.entries.len()
4781 )));
4782 }
4783 let mut results = Vec::with_capacity(parsed.len());
4784 for (stmt_sql, statement, name, _) in &parsed {
4785 let result = self
4786 .exact_bootstrap_noop(stmt_sql, statement)?
4787 .ok_or_else(|| {
4788 DbError::Pipeline(format!(
4789 "sealed cluster catalog rejects startup addition '{name}'"
4790 ))
4791 })?;
4792 results.push(result);
4793 }
4794 return Ok(results);
4795 }
4796
4797 if !self.catalog_manifest_inventory()?.is_empty() {
4798 return Err(DbError::Pipeline(
4799 "cannot seal a new cluster catalog over uncommitted local topology".into(),
4800 ));
4801 }
4802 let store = self.catalog_manifest_store.lock().clone().ok_or_else(|| {
4803 DbError::Pipeline("cluster catalog manifest store is not configured".into())
4804 })?;
4805 let controller = self.cluster_controller.lock().clone().ok_or_else(|| {
4806 DbError::Pipeline(
4807 "[LDB-6043] cluster catalog bootstrap requires a cluster controller".into(),
4808 )
4809 })?;
4810 let leader_proof = controller
4811 .capture_catalog_bootstrap_proof()
4812 .ok_or_else(|| {
4813 DbError::Pipeline(
4814 "[LDB-6043] cluster catalog bootstrap requires the active durable leader lease"
4815 .into(),
4816 )
4817 })?;
4818 self.validate_catalog_seal_authority(Some(&leader_proof))?;
4819
4820 let mut bootstrap_guard = CatalogBootstrapGuard {
4821 db: self,
4822 created: Vec::with_capacity(parsed.len()),
4823 sealed: false,
4824 };
4825 let mut results = Vec::with_capacity(parsed.len());
4826 for (stmt_sql, statement, name, kind) in &parsed {
4827 let result = CATALOG_BOOTSTRAP
4828 .scope((), self.execute_parsed_single(stmt_sql, statement))
4829 .await?;
4830 let ExecuteResult::Ddl(info) = &result else {
4831 return Err(DbError::Pipeline(format!(
4832 "cluster catalog create '{name}' returned a non-DDL result"
4833 )));
4834 };
4835 if !info.applied || info.object_name != *name {
4836 return Err(DbError::Pipeline(format!(
4837 "cluster catalog create '{name}' did not apply exactly once"
4838 )));
4839 }
4840 bootstrap_guard.record(name.clone(), *kind);
4841 results.push(result);
4842 }
4843
4844 let manifest = laminar_core::cluster::control::CatalogManifest::new(
4845 self.catalog_manifest_inventory()?,
4846 )
4847 .map_err(|error| {
4848 DbError::Pipeline(format!("invalid cluster catalog inventory: {error}"))
4849 })?;
4850
4851 #[cfg(test)]
4852 let catalog_seal_gate = { self.catalog_seal_gate.lock().clone() };
4853 #[cfg(test)]
4854 if let Some((entered, release)) = catalog_seal_gate {
4855 entered.notify_one();
4856 release.notified().await;
4857 }
4858 self.validate_catalog_seal_authority(Some(&leader_proof))?;
4859 store
4860 .seal(&manifest, &leader_proof)
4861 .await
4862 .map_err(|error| DbError::Pipeline(format!("catalog manifest seal failed: {error}")))?;
4863 bootstrap_guard.sealed();
4864 Ok(results)
4865 }
4866
4867 #[cfg(feature = "cluster")]
4873 pub async fn execute_cluster_bootstrap(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4874 let entries = vec![sql.to_owned()];
4875 self.execute_cluster_bootstrap_batch(&entries)
4876 .await?
4877 .into_iter()
4878 .last()
4879 .ok_or_else(|| DbError::InvalidOperation("Empty SQL statement".into()))
4880 }
4881
4882 async fn execute_single(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4883 let statements = parse_streaming_sql(sql)?;
4884 if statements.is_empty() {
4885 return Err(DbError::InvalidOperation("Empty SQL statement".into()));
4886 }
4887 let statement = &statements[0];
4888 if mutates_database(statement) {
4889 self.ensure_catalog_cleanup_unfenced("database mutation")?;
4890 }
4891 if is_topology_ddl(statement) {
4892 let _topology_ddl = self.topology_ddl_lock.write().await;
4893 self.ensure_catalog_cleanup_unfenced("database mutation")?;
4894 #[cfg(feature = "cluster")]
4895 self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
4896 self.execute_parsed_single(sql, statement).await
4897 } else if reads_catalog(statement) {
4898 let _topology_read = self.topology_ddl_lock.read().await;
4899 if mutates_database(statement) {
4900 self.ensure_catalog_cleanup_unfenced("database mutation")?;
4901 #[cfg(feature = "cluster")]
4902 self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
4903 }
4904 self.execute_parsed_single(sql, statement).await
4905 } else if mutates_database(statement) {
4906 let _topology_read = self.topology_ddl_lock.read().await;
4907 self.ensure_catalog_cleanup_unfenced("database mutation")?;
4908 #[cfg(feature = "cluster")]
4909 self.ensure_coordinated_recovery_mutation_unfenced("database mutation")?;
4910 self.execute_parsed_single(sql, statement).await
4911 } else {
4912 self.execute_parsed_single(sql, statement).await
4913 }
4914 }
4915
4916 #[cfg(feature = "cluster")]
4917 async fn execute_single_already_gated(&self, sql: &str) -> Result<ExecuteResult, DbError> {
4918 let statements = parse_streaming_sql(sql)?;
4919 if statements.len() != 1 || !is_topology_ddl(&statements[0]) {
4920 return Err(DbError::InvalidOperation(
4921 "catalog manifest entries must contain exactly one topology DDL statement".into(),
4922 ));
4923 }
4924 self.execute_parsed_single(sql, &statements[0]).await
4925 }
4926
4927 async fn execute_parsed_single(
4928 &self,
4929 sql: &str,
4930 statement: &StreamingStatement,
4931 ) -> Result<ExecuteResult, DbError> {
4932 #[cfg(feature = "cluster")]
4933 if is_topology_ddl(statement) && !catalog_manifest_replay_active() {
4934 let store_configured = self.catalog_manifest_store.lock().is_some();
4935 let cluster_runtime = self.is_cluster_runtime();
4936 if store_configured || cluster_runtime {
4937 validate_cluster_catalog_create(self, sql, statement)?;
4938 if !store_configured {
4939 return Err(DbError::Pipeline(
4940 "cluster topology DDL requires a catalog manifest store".into(),
4941 ));
4942 }
4943 if !catalog_bootstrap_active() {
4944 return Err(DbError::Pipeline(
4945 "[LDB-6043] configured cluster topology can change only through startup bootstrap/replay until a replicated topology-version barrier is implemented"
4946 .into(),
4947 ));
4948 }
4949 }
4950 }
4951
4952 let result = match statement {
4953 StreamingStatement::CreateSource(create) => {
4954 let result = self.handle_create_source(create).await?;
4955 if let ExecuteResult::Ddl(ref info) = result {
4956 if info.applied {
4957 self.connector_manager
4958 .lock()
4959 .store_ddl(&info.object_name, sql);
4960 }
4961 }
4962 Ok(result)
4963 }
4964 StreamingStatement::CreateSink(create) => {
4965 let result = self.handle_create_sink(create)?;
4966 if let ExecuteResult::Ddl(ref info) = result {
4967 if info.applied {
4968 self.connector_manager
4969 .lock()
4970 .store_ddl(&info.object_name, sql);
4971 }
4972 }
4973 Ok(result)
4974 }
4975 StreamingStatement::CreateStream {
4976 name,
4977 query,
4978 emit_clause,
4979 if_not_exists,
4980 query_sql,
4981 retention_bytes,
4982 ..
4983 } => {
4984 let result = self
4985 .handle_create_stream(
4986 sql,
4987 name,
4988 query,
4989 emit_clause.as_ref(),
4990 *if_not_exists,
4991 query_sql,
4992 *retention_bytes,
4993 )
4994 .await?;
4995 if let ExecuteResult::Ddl(ref info) = result {
4996 if info.applied {
4997 self.connector_manager
4998 .lock()
4999 .store_ddl(&info.object_name, sql);
5000 }
5001 }
5002 Ok(result)
5003 }
5004 StreamingStatement::CreateContinuousQuery { .. } => Err(DbError::InvalidOperation(
5005 "CREATE CONTINUOUS QUERY has no typed catalog/drop lifecycle; use CREATE STREAM"
5006 .into(),
5007 )),
5008 StreamingStatement::DropLookupTable { name, if_exists } => {
5009 self.handle_drop_lookup_table(name, *if_exists)
5010 }
5011 StreamingStatement::CreateLookupTable(create) => {
5012 if create.or_replace {
5013 return Err(DbError::InvalidOperation(
5014 "CREATE OR REPLACE LOOKUP TABLE is not atomic; use DROP LOOKUP TABLE followed by CREATE LOOKUP TABLE"
5015 .into(),
5016 ));
5017 }
5018 let name = canonical_object_name(&create.name)?;
5019 let Some(reservation) = self.reserve_catalog_name(
5020 &name,
5021 CatalogObjectKind::LookupTable,
5022 create.if_not_exists,
5023 )?
5024 else {
5025 return Ok(ExecuteResult::Ddl(DdlInfo {
5026 statement_type: "CREATE LOOKUP TABLE".into(),
5027 object_name: name,
5028 applied: false,
5029 }));
5030 };
5031 let result = self.handle_query(sql).await?;
5032 if let ExecuteResult::Ddl(ref info) = result {
5033 if info.applied {
5034 self.connector_manager
5035 .lock()
5036 .store_ddl(&info.object_name, sql);
5037 }
5038 }
5039 reservation.commit();
5040 Ok(result)
5041 }
5042 StreamingStatement::Standard(stmt) => {
5043 if let sqlparser::ast::Statement::CreateTable(ct) = stmt.as_ref() {
5044 let result = self.handle_create_table(ct)?;
5045 if let ExecuteResult::Ddl(ref info) = result {
5046 if info.applied {
5047 self.connector_manager
5048 .lock()
5049 .store_ddl(&info.object_name, sql);
5050 }
5051 }
5052 Ok(result)
5053 } else if let sqlparser::ast::Statement::Drop {
5054 object_type: sqlparser::ast::ObjectType::Table,
5055 names,
5056 if_exists,
5057 cascade,
5058 ..
5059 } = stmt.as_ref()
5060 {
5061 self.handle_drop_table(names, *if_exists, *cascade)
5062 } else if let sqlparser::ast::Statement::Set(set_stmt) = stmt.as_ref() {
5063 self.handle_set(set_stmt)
5064 } else if matches!(stmt.as_ref(), sqlparser::ast::Statement::AlterTable { .. }) {
5065 Err(DbError::InvalidOperation(
5066 "ALTER TABLE is disabled until catalog/provider changes are transactional"
5067 .into(),
5068 ))
5069 } else if matches!(stmt.as_ref(), sqlparser::ast::Statement::Query(_)) {
5070 if crate::sql_analysis::has_temporal_query(sql) {
5071 Err(DbError::Unsupported(
5072 "direct temporal queries require the managed vnode runtime; create the temporal stream while the pipeline is stopped"
5073 .into(),
5074 ))
5075 } else {
5076 self.handle_query(sql).await
5077 }
5078 } else {
5079 Err(DbError::InvalidOperation(format!(
5080 "unsupported standard SQL statement; catalog mutation is not typed or transactional: {stmt}"
5081 )))
5082 }
5083 }
5084 StreamingStatement::TemporalProbeQuery { .. } => Err(DbError::Unsupported(
5085 "TEMPORAL PROBE JOIN requires a managed stream created while the pipeline is stopped and cannot execute as an ordinary SQL query"
5086 .into(),
5087 )),
5088 StreamingStatement::InsertInto {
5089 table_name,
5090 columns,
5091 values,
5092 } => self.handle_insert_into(table_name, columns, values).await,
5093 StreamingStatement::DropSource {
5094 name,
5095 if_exists,
5096 cascade,
5097 } => self.handle_drop_source(name, *if_exists, *cascade),
5098 StreamingStatement::DropSink {
5099 name,
5100 if_exists,
5101 cascade,
5102 } => self.handle_drop_sink(name, *if_exists, *cascade),
5103 StreamingStatement::DropStream {
5104 name,
5105 if_exists,
5106 cascade,
5107 } => self.handle_drop_stream(name, *if_exists, *cascade).await,
5108 StreamingStatement::DropMaterializedView {
5109 name,
5110 if_exists,
5111 cascade,
5112 } => {
5113 self.handle_drop_materialized_view(name, *if_exists, *cascade)
5114 .await
5115 }
5116 StreamingStatement::Show(cmd) => {
5117 let batch = match cmd {
5118 ShowCommand::Sources => self.build_show_sources(),
5119 ShowCommand::Sinks => self.build_show_sinks(),
5120 ShowCommand::Queries => self.build_show_queries(),
5121 ShowCommand::MaterializedViews => self.build_show_materialized_views(),
5122 ShowCommand::Streams => self.build_show_streams(),
5123 ShowCommand::Tables => self.build_show_tables(),
5124 ShowCommand::CheckpointStatus => self.build_show_checkpoint_status().await?,
5125 ShowCommand::CreateSource { name } => {
5126 self.build_show_create_source(&canonical_object_name(name)?)?
5127 }
5128 ShowCommand::CreateSink { name } => {
5129 self.build_show_create_sink(&canonical_object_name(name)?)?
5130 }
5131 };
5132 Ok(ExecuteResult::Metadata(batch))
5133 }
5134 StreamingStatement::Checkpoint => {
5135 let result = self.checkpoint().await?;
5136 Ok(ExecuteResult::Ddl(DdlInfo {
5137 statement_type: "CHECKPOINT".to_string(),
5138 object_name: format!("checkpoint_{}", result.checkpoint_id),
5139 applied: true,
5140 }))
5141 }
5142 StreamingStatement::RestoreCheckpoint { checkpoint_id } => {
5143 self.handle_restore_checkpoint(*checkpoint_id)
5144 }
5145 StreamingStatement::Describe { name, .. } => {
5146 let name_str = name.to_string();
5147 let batch = self.build_describe(&name_str)?;
5148 Ok(ExecuteResult::Metadata(batch))
5149 }
5150 StreamingStatement::Explain {
5151 statement, analyze, ..
5152 } => {
5153 if *analyze {
5154 self.handle_explain_analyze(statement, sql).await
5155 } else {
5156 self.handle_explain(statement)
5157 }
5158 }
5159 StreamingStatement::CreateMaterializedView {
5160 name,
5161 query,
5162 emit_clause,
5163 or_replace,
5164 if_not_exists,
5165 query_sql,
5166 ..
5167 } => {
5168 let result = self
5169 .handle_create_materialized_view(
5170 sql,
5171 name,
5172 query,
5173 emit_clause.clone(),
5174 *or_replace,
5175 *if_not_exists,
5176 query_sql,
5177 )
5178 .await?;
5179 if let ExecuteResult::Ddl(ref info) = result {
5180 if info.applied {
5181 self.connector_manager
5182 .lock()
5183 .store_ddl(&info.object_name, sql);
5184 }
5185 }
5186 Ok(result)
5187 }
5188 StreamingStatement::AlterSource { .. } => Err(DbError::InvalidOperation(
5189 "ALTER SOURCE is disabled until catalog/provider changes are transactional".into(),
5190 )),
5191 StreamingStatement::Subscribe(_) => Err(DbError::InvalidOperation(
5192 "SUBSCRIBE requires the pgwire endpoint, not HTTP /api/v1/sql".into(),
5193 )),
5194 StreamingStatement::DeclareCursorForSubscribe { .. } => Err(DbError::InvalidOperation(
5195 "DECLARE CURSOR FOR SUBSCRIBE requires the pgwire endpoint, not HTTP /api/v1/sql"
5196 .into(),
5197 )),
5198 };
5199
5200 result
5201 }
5202
5203 async fn handle_insert_into(
5204 &self,
5205 table_name: &sqlparser::ast::ObjectName,
5206 columns: &[sqlparser::ast::Ident],
5207 values: &[Vec<sqlparser::ast::Expr>],
5208 ) -> Result<ExecuteResult, DbError> {
5209 let name = canonical_object_name(table_name)?;
5210 if !columns.is_empty() {
5211 return Err(DbError::InvalidOperation(
5212 "INSERT column lists are unsupported until projection and default-value semantics are implemented"
5213 .into(),
5214 ));
5215 }
5216 if values.is_empty() {
5217 return Err(DbError::InvalidOperation(
5218 "INSERT requires at least one VALUES row".into(),
5219 ));
5220 }
5221
5222 if let Some(entry) = self.catalog.get_source(&name) {
5223 let batch = sql_utils::sql_values_to_record_batch(&entry.schema, values)?;
5224 entry
5225 .push_and_buffer(batch)
5226 .map_err(|e| DbError::InsertError(format!("Failed to push to source: {e}")))?;
5227 return Ok(ExecuteResult::RowsAffected(values.len() as u64));
5228 }
5229
5230 if !self.table_store.read().has_table(&name) {
5231 return Err(DbError::InvalidOperation(format!(
5232 "INSERT target '{name}' is not a typed mutable source or table"
5233 )));
5234 }
5235
5236 let _lifecycle = self.lifecycle_lock.lock().await;
5240 self.ensure_offline_topology_ddl_allowed(&format!("INSERT INTO TABLE '{name}'"))?;
5241 self.ensure_reference_table_insert_is_ephemeral(&name)?;
5242
5243 {
5245 let mut ts = self.table_store.write();
5246 if ts.has_table(&name) {
5247 let schema = ts
5248 .table_schema(&name)
5249 .ok_or_else(|| DbError::TableNotFound(name.clone()))?;
5250 let batch = sql_utils::sql_values_to_record_batch(&schema, values)?;
5251 ts.upsert(&name, &batch)?;
5252 drop(ts); self.sync_table_to_datafusion(&name)?;
5255 return Ok(ExecuteResult::RowsAffected(values.len() as u64));
5256 }
5257 }
5258
5259 Err(DbError::TableNotFound(name))
5260 }
5261
5262 fn ensure_reference_table_insert_is_ephemeral(&self, table_name: &str) -> Result<(), DbError> {
5263 if self.is_cluster_runtime() {
5264 return Err(DbError::InvalidOperation(format!(
5265 "[LDB-6043] INSERT INTO TABLE '{table_name}' is not coordinated cluster state; load a versioned reference-table snapshot during deployment bootstrap"
5266 )));
5267 }
5268 if self.is_checkpoint_enabled() {
5269 return Err(DbError::InvalidOperation(format!(
5270 "[LDB-6043] INSERT INTO TABLE '{table_name}' is process-local and cannot be admitted into a recoverable deployment; load a versioned reference-table snapshot through its connector"
5271 )));
5272 }
5273 Ok(())
5274 }
5275
5276 #[allow(clippy::unused_self)] fn handle_restore_checkpoint(&self, _checkpoint_id: u64) -> Result<ExecuteResult, DbError> {
5278 Err(DbError::Unsupported(
5279 "RESTORE FROM CHECKPOINT is not yet implemented — \
5280 requires pipeline stop, state reload from manifest, \
5281 source offset seek, and pipeline restart"
5282 .to_string(),
5283 ))
5284 }
5285
5286 #[must_use]
5288 pub fn get_session_property(&self, key: &str) -> Option<String> {
5289 self.session_properties
5290 .lock()
5291 .get(&key.to_lowercase())
5292 .cloned()
5293 }
5294
5295 #[must_use]
5297 pub fn session_properties(&self) -> HashMap<String, String> {
5298 self.session_properties.lock().clone()
5299 }
5300
5301 pub fn set_session_property(&self, key: &str, value: &str) {
5303 self.session_properties
5304 .lock()
5305 .insert(key.to_lowercase(), value.to_string());
5306 }
5307
5308 pub async fn subscribe<T: crate::handle::FromBatch>(
5314 &self,
5315 name: &str,
5316 ) -> Result<crate::handle::TypedSubscription<T>, DbError> {
5317 let portal = self
5318 .open_subscription(name, None, crate::subscription::SubscribeStart::Tail)
5319 .await?;
5320 Ok(crate::handle::TypedSubscription::new(portal))
5321 }
5322
5323 #[must_use]
5327 pub fn lookup_subscription_schema(&self, name: &str) -> Option<arrow_schema::SchemaRef> {
5328 if let Some(mv) = self.mv_registry.lock().get(name).cloned() {
5329 return Some(mv.schema);
5330 }
5331 if let Some(schema) = self.stream_schemas.read().get(name).cloned() {
5332 return Some(schema);
5333 }
5334 None
5335 }
5336
5337 pub async fn open_subscription(
5345 &self,
5346 name: &str,
5347 filter_sql: Option<&str>,
5348 start: crate::subscription::SubscribeStart,
5349 ) -> Result<crate::subscription::SubscriptionPortal, DbError> {
5350 let _topology = self.topology_ddl_lock.read().await;
5354
5355 let schema = self
5359 .lookup_subscription_schema(name)
5360 .ok_or_else(|| DbError::StreamNotFound(name.to_string()))?;
5361
5362 let filter = match filter_sql {
5363 None => None,
5364 Some(sql) => Some(crate::filter_compile::compile(&self.ctx, sql, &schema).await?),
5365 };
5366
5367 #[cfg(feature = "cluster")]
5368 if self.is_cluster_runtime() {
5369 return self
5370 .open_cluster_subscription(name, schema, filter, start)
5371 .await;
5372 }
5373
5374 let reader =
5375 self.subscription_registry
5376 .subscribe(name, start)
5377 .map_err(|error| match error {
5378 crate::subscription::SubscriptionOpenError::ReplayPruned {
5379 earliest_retained,
5380 } => {
5381 let requested = match start {
5382 crate::subscription::SubscribeStart::AsOfEpoch(n) => n,
5383 crate::subscription::SubscribeStart::Tail => 0,
5384 };
5385 DbError::SubscriptionReplayPruned {
5386 name: name.to_string(),
5387 requested,
5388 earliest_retained,
5389 }
5390 }
5391 crate::subscription::SubscriptionOpenError::EpochNotCommitted {
5392 requested,
5393 latest_committed,
5394 } => DbError::SubscriptionEpochNotCommitted {
5395 name: name.to_string(),
5396 requested,
5397 latest_committed,
5398 },
5399 crate::subscription::SubscriptionOpenError::Capacity { attached, limit } => {
5400 DbError::Pipeline(format!(
5401 "subscriber cap reached for '{name}' ({attached}/{limit})"
5402 ))
5403 }
5404 })?;
5405
5406 Ok(match filter {
5407 Some(phys) => crate::subscription::SubscriptionPortal::open_with_filter(
5408 name, schema, reader, phys,
5409 ),
5410 None => crate::subscription::SubscriptionPortal::open(name, schema, reader),
5411 })
5412 }
5413
5414 fn handle_explain(&self, statement: &StreamingStatement) -> Result<ExecuteResult, DbError> {
5415 let mut planner = self.planner.lock();
5416 let plan_result = planner.plan(statement);
5417
5418 let mut rows: Vec<(String, String)> = Vec::new();
5419
5420 match plan_result {
5421 Ok(plan) => {
5422 rows.push((
5423 "plan_type".into(),
5424 match &plan {
5425 laminar_sql::planner::StreamingPlan::Query(_) => "Query",
5426 laminar_sql::planner::StreamingPlan::RegisterSource(_) => "RegisterSource",
5427 laminar_sql::planner::StreamingPlan::RegisterSink(_) => "RegisterSink",
5428 laminar_sql::planner::StreamingPlan::Standard(_) => "Standard",
5429 laminar_sql::planner::StreamingPlan::RegisterLookupTable(_) => {
5430 "RegisterLookupTable"
5431 }
5432 laminar_sql::planner::StreamingPlan::DropLookupTable { .. } => {
5433 "DropLookupTable"
5434 }
5435 }
5436 .into(),
5437 ));
5438 match &plan {
5439 laminar_sql::planner::StreamingPlan::Query(qp) => {
5440 if let Some(name) = &qp.name {
5441 rows.push(("query_name".into(), name.clone()));
5442 }
5443 if let Some(wc) = &qp.window_config {
5444 rows.push(("window".into(), format!("{wc}")));
5445 }
5446 if let Some(jcs) = &qp.join_config {
5447 if jcs.len() == 1 {
5448 rows.push(("join".into(), format!("{}", jcs[0])));
5449 } else {
5450 for (i, jc) in jcs.iter().enumerate() {
5451 rows.push((format!("join_step_{}", i + 1), format!("{jc}")));
5452 }
5453 }
5454 }
5455 if let Some(oc) = &qp.order_config {
5456 rows.push(("order_by".into(), format!("{oc:?}")));
5457 }
5458 if let Some(fc) = &qp.frame_config {
5459 rows.push((
5460 "frame_functions".into(),
5461 format!("{}", fc.functions.len()),
5462 ));
5463 }
5464 if let Some(ec) = &qp.emit_clause {
5465 rows.push(("emit".into(), format!("{ec}")));
5466 }
5467 }
5468 laminar_sql::planner::StreamingPlan::RegisterSource(info) => {
5469 rows.push(("source".into(), info.name.clone()));
5470 }
5471 laminar_sql::planner::StreamingPlan::RegisterSink(info) => {
5472 rows.push(("sink".into(), info.name.clone()));
5473 }
5474 laminar_sql::planner::StreamingPlan::Standard(_) => {
5475 rows.push(("execution".into(), "DataFusion pass-through".into()));
5476 }
5477 laminar_sql::planner::StreamingPlan::RegisterLookupTable(info) => {
5478 rows.push(("lookup_table".into(), info.name.clone()));
5479 }
5480 laminar_sql::planner::StreamingPlan::DropLookupTable { name } => {
5481 rows.push(("drop_lookup_table".into(), name.clone()));
5482 }
5483 }
5484 }
5485 Err(e) => {
5486 rows.push(("error".into(), format!("{e}")));
5487 rows.push((
5488 "statement".into(),
5489 format!("{:?}", std::mem::discriminant(statement)),
5490 ));
5491 }
5492 }
5493
5494 let keys: Vec<&str> = rows.iter().map(|(k, _)| k.as_str()).collect();
5495 let values: Vec<&str> = rows.iter().map(|(_, v)| v.as_str()).collect();
5496
5497 let schema = Arc::new(Schema::new(vec![
5498 Field::new("plan_key", DataType::Utf8, false),
5499 Field::new("plan_value", DataType::Utf8, false),
5500 ]));
5501
5502 let batch = RecordBatch::try_new(
5503 schema,
5504 vec![
5505 Arc::new(StringArray::from(keys)),
5506 Arc::new(StringArray::from(values)),
5507 ],
5508 )
5509 .map_err(|e| DbError::InvalidOperation(format!("explain metadata: {e}")))?;
5510
5511 Ok(ExecuteResult::Metadata(batch))
5512 }
5513
5514 async fn handle_explain_analyze(
5515 &self,
5516 statement: &StreamingStatement,
5517 original_sql: &str,
5518 ) -> Result<ExecuteResult, DbError> {
5519 let explain_result = self.handle_explain(statement)?;
5520 let mut rows: Vec<(String, String)> = Vec::new();
5521
5522 if let ExecuteResult::Metadata(explain_batch) = &explain_result {
5523 let keys_col = explain_batch
5524 .column(0)
5525 .as_any()
5526 .downcast_ref::<StringArray>();
5527 let vals_col = explain_batch
5528 .column(1)
5529 .as_any()
5530 .downcast_ref::<StringArray>();
5531 if let (Some(keys), Some(vals)) = (keys_col, vals_col) {
5532 for i in 0..explain_batch.num_rows() {
5533 rows.push((keys.value(i).to_string(), vals.value(i).to_string()));
5534 }
5535 }
5536 }
5537
5538 let upper = original_sql.to_uppercase();
5539 let inner_start = upper.find("ANALYZE").map_or(0, |pos| pos + "ANALYZE".len());
5540 let inner_sql = original_sql[inner_start..].trim();
5541
5542 let start = std::time::Instant::now();
5543 match self.ctx.sql(inner_sql).await {
5544 Ok(df) => match df.collect().await {
5545 Ok(batches) => {
5546 let elapsed = start.elapsed();
5547 let total_rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
5548 rows.push(("rows_produced".into(), total_rows.to_string()));
5549 rows.push(("execution_time_ms".into(), elapsed.as_millis().to_string()));
5550 rows.push(("batches_processed".into(), batches.len().to_string()));
5551 }
5552 Err(e) => {
5553 let elapsed = start.elapsed();
5554 rows.push(("execution_time_ms".into(), elapsed.as_millis().to_string()));
5555 rows.push(("analyze_error".into(), format!("{e}")));
5556 }
5557 },
5558 Err(e) => {
5559 rows.push(("analyze_error".into(), format!("{e}")));
5560 }
5561 }
5562
5563 let keys: Vec<&str> = rows.iter().map(|(k, _)| k.as_str()).collect();
5564 let values: Vec<&str> = rows.iter().map(|(_, v)| v.as_str()).collect();
5565
5566 let schema = Arc::new(Schema::new(vec![
5567 Field::new("plan_key", DataType::Utf8, false),
5568 Field::new("plan_value", DataType::Utf8, false),
5569 ]));
5570
5571 let batch = RecordBatch::try_new(
5572 schema,
5573 vec![
5574 Arc::new(StringArray::from(keys)),
5575 Arc::new(StringArray::from(values)),
5576 ],
5577 )
5578 .map_err(|e| DbError::InvalidOperation(format!("explain analyze metadata: {e}")))?;
5579
5580 Ok(ExecuteResult::Metadata(batch))
5581 }
5582
5583 #[allow(clippy::too_many_lines)]
5584 pub(crate) async fn handle_query(&self, sql: &str) -> Result<ExecuteResult, DbError> {
5585 let plan = {
5586 let statements = parse_streaming_sql(sql)?;
5587 if statements.is_empty() {
5588 return Err(DbError::InvalidOperation("Empty SQL statement".into()));
5589 }
5590 match &statements[0] {
5591 StreamingStatement::CreateContinuousQuery { .. } => {
5592 return Err(DbError::InvalidOperation(
5593 "CREATE CONTINUOUS QUERY has no typed catalog/drop lifecycle; use CREATE STREAM"
5594 .into(),
5595 ));
5596 }
5597 StreamingStatement::CreateLookupTable(statement) => {
5598 self.ensure_offline_topology_ddl_allowed("CREATE LOOKUP TABLE")?;
5599 let name = canonical_object_name(&statement.name)?;
5600 if self.catalog_namespace.lock().get(&name)
5601 != Some(&CatalogObjectKind::LookupTable)
5602 {
5603 return Err(DbError::InvalidOperation(
5604 "CREATE LOOKUP TABLE must pass typed namespace admission".into(),
5605 ));
5606 }
5607 }
5608 StreamingStatement::DropLookupTable { name, if_exists } => {
5609 return self.handle_drop_lookup_table(name, *if_exists);
5610 }
5611 _ => {}
5612 }
5613 let mut planner = self.planner.lock();
5614 planner
5615 .plan(&statements[0])
5616 .map_err(laminar_sql::Error::from)?
5617 };
5618
5619 match plan {
5620 laminar_sql::planner::StreamingPlan::RegisterSource(info) => {
5621 Ok(ExecuteResult::Ddl(DdlInfo {
5622 statement_type: "DDL".to_string(),
5623 object_name: info.name,
5624 applied: true,
5625 }))
5626 }
5627 laminar_sql::planner::StreamingPlan::RegisterSink(info) => {
5628 Ok(ExecuteResult::Ddl(DdlInfo {
5629 statement_type: "DDL".to_string(),
5630 object_name: info.name,
5631 applied: true,
5632 }))
5633 }
5634 laminar_sql::planner::StreamingPlan::Query(query_plan) => {
5635 if query_plan.join_config.as_ref().is_some_and(|joins| {
5636 joins.iter().any(|join| {
5637 matches!(
5638 join,
5639 laminar_sql::translator::JoinOperatorConfig::Temporal(_)
5640 )
5641 })
5642 }) {
5643 return Err(DbError::Unsupported(
5644 "temporal queries require the managed vnode runtime and cannot execute through DataFusion's ordinary join path"
5645 .into(),
5646 ));
5647 }
5648 let plan_sql = query_plan.statement.to_string();
5649 let logical_plan = self.ctx.state().create_logical_plan(&plan_sql).await?;
5650 let df = self.ctx.execute_logical_plan(logical_plan).await?;
5651 let stream = df.execute_stream().await?;
5652
5653 Ok(self.bridge_query_stream(sql, stream))
5654 }
5655 laminar_sql::planner::StreamingPlan::Standard(stmt) => {
5656 let sql_str = stmt.to_string();
5657 let df = self.ctx.sql(&sql_str).await?;
5658 let stream = df.execute_stream().await?;
5659
5660 Ok(self.bridge_query_stream(sql, stream))
5661 }
5662 laminar_sql::planner::StreamingPlan::RegisterLookupTable(info) => {
5663 self.handle_register_lookup_table(info)
5664 }
5665 laminar_sql::planner::StreamingPlan::DropLookupTable { .. } => Err(
5666 DbError::InvalidOperation("lookup drop bypassed typed catalog admission".into()),
5667 ),
5668 }
5669 }
5670
5671 fn bridge_query_stream(
5672 &self,
5673 sql: &str,
5674 stream: datafusion::physical_plan::SendableRecordBatchStream,
5675 ) -> ExecuteResult {
5676 let query_id = self.catalog.register_query(sql);
5677 let schema = stream.schema();
5678
5679 let source_cfg = streaming::SourceConfig::with_buffer_size(self.config.default_buffer_size);
5680 let (source, sink) =
5681 streaming::create_with_config::<crate::catalog::ArrowRecord>(source_cfg);
5682
5683 let subscription = sink.subscribe();
5684
5685 let cancel_token = tokio_util::sync::CancellationToken::new();
5686 let cancel_token_clone = cancel_token.clone();
5687
5688 let source_clone = source.clone();
5689 let catalog = Arc::clone(&self.catalog);
5690 let query_id_clone = query_id;
5691 tokio::spawn(async move {
5692 use tokio_stream::StreamExt;
5693 let mut stream = stream;
5694 loop {
5695 tokio::select! {
5696 () = cancel_token_clone.cancelled() => {
5697 break;
5698 }
5699 result = stream.next() => {
5700 match result {
5701 Some(Ok(batch)) => {
5702 if source_clone.push_arrow(batch).is_err() {
5703 break;
5704 }
5705 }
5706 _ => break,
5707 }
5708 }
5709 }
5710 }
5711 drop(source_clone);
5712 catalog.deactivate_query(query_id_clone);
5713 });
5714
5715 ExecuteResult::Query(QueryHandle {
5716 id: query_id,
5717 schema,
5718 sql: sql.to_string(),
5719 subscription: Some(subscription),
5720 active: true,
5721 cancel_token,
5722 })
5723 }
5724 pub fn source<T: laminar_core::streaming::Record>(
5731 &self,
5732 name: &str,
5733 ) -> Result<SourceHandle<T>, DbError> {
5734 let entry = self
5735 .catalog
5736 .get_source(name)
5737 .ok_or_else(|| DbError::SourceNotFound(name.to_string()))?;
5738 SourceHandle::new(entry)
5739 }
5740
5741 pub fn source_untyped(&self, name: &str) -> Result<UntypedSourceHandle, DbError> {
5747 let entry = self
5748 .catalog
5749 .get_source(name)
5750 .ok_or_else(|| DbError::SourceNotFound(name.to_string()))?;
5751 Ok(UntypedSourceHandle::new(entry))
5752 }
5753
5754 pub fn sources(&self) -> Vec<SourceInfo> {
5756 let names = self.catalog.list_sources();
5757 names
5758 .into_iter()
5759 .filter_map(|name| {
5760 self.catalog.get_source(&name).map(|e| SourceInfo {
5761 name: e.name.clone(),
5762 schema: e.schema.clone(),
5763 watermark_column: e.watermark_column.clone(),
5764 })
5765 })
5766 .collect()
5767 }
5768
5769 pub fn sinks(&self) -> Vec<SinkInfo> {
5771 self.catalog
5772 .list_sinks()
5773 .into_iter()
5774 .map(|name| SinkInfo { name })
5775 .collect()
5776 }
5777
5778 pub fn materialized_views(&self) -> Vec<crate::handle::MaterializedViewInfo> {
5780 let registry = self.mv_registry.lock();
5781 registry
5782 .views()
5783 .map(crate::handle::MaterializedViewInfo::from)
5784 .collect()
5785 }
5786
5787 pub fn streams(&self) -> Vec<crate::handle::StreamInfo> {
5789 let mgr = self.connector_manager.lock();
5790 mgr.streams()
5791 .iter()
5792 .map(|(name, reg)| crate::handle::StreamInfo {
5793 name: name.clone(),
5794 sql: Some(reg.query_sql.clone()),
5795 })
5796 .collect()
5797 }
5798
5799 pub fn pipeline_topology(&self) -> crate::handle::PipelineTopology {
5801 use crate::handle::{PipelineEdge, PipelineNode, PipelineNodeType};
5802
5803 let mut nodes = Vec::new();
5804 let mut edges = Vec::new();
5805
5806 let source_names = self.catalog.list_sources();
5807
5808 for name in &source_names {
5809 let schema = self.catalog.get_source(name).map(|e| e.schema.clone());
5810 nodes.push(PipelineNode {
5811 name: name.clone(),
5812 node_type: PipelineNodeType::Source,
5813 schema,
5814 sql: None,
5815 });
5816 }
5817
5818 let mgr = self.connector_manager.lock();
5819 let stream_names: Vec<String> = mgr.streams().keys().cloned().collect();
5820 for (name, reg) in mgr.streams() {
5821 nodes.push(PipelineNode {
5822 name: name.clone(),
5823 node_type: PipelineNodeType::Stream,
5824 schema: None,
5825 sql: Some(reg.query_sql.clone()),
5826 });
5827
5828 let sql_upper = reg.query_sql.to_uppercase();
5830 for src in &source_names {
5831 if sql_upper.contains(&src.to_uppercase()) {
5832 edges.push(PipelineEdge {
5833 from: src.clone(),
5834 to: name.clone(),
5835 });
5836 }
5837 }
5838 for other in &stream_names {
5839 if other != name && sql_upper.contains(&other.to_uppercase()) {
5840 edges.push(PipelineEdge {
5841 from: other.clone(),
5842 to: name.clone(),
5843 });
5844 }
5845 }
5846 }
5847
5848 for (name, reg) in mgr.sinks() {
5849 nodes.push(PipelineNode {
5850 name: name.clone(),
5851 node_type: PipelineNodeType::Sink,
5852 schema: None,
5853 sql: None,
5854 });
5855
5856 if !reg.input.is_empty() {
5857 edges.push(PipelineEdge {
5858 from: reg.input.clone(),
5859 to: name.clone(),
5860 });
5861 }
5862 }
5863
5864 let cm_sink_names: std::collections::HashSet<&String> = mgr.sinks().keys().collect();
5865 for name in self.catalog.list_sinks() {
5866 if !cm_sink_names.contains(&name) {
5867 if let Some(input) = self.catalog.get_sink_input(&name) {
5868 nodes.push(PipelineNode {
5869 name: name.clone(),
5870 node_type: PipelineNodeType::Sink,
5871 schema: None,
5872 sql: None,
5873 });
5874 if !input.is_empty() {
5875 edges.push(PipelineEdge {
5876 from: input,
5877 to: name,
5878 });
5879 }
5880 }
5881 }
5882 }
5883
5884 drop(mgr);
5885
5886 crate::handle::PipelineTopology { nodes, edges }
5887 }
5888
5889 pub fn queries(&self) -> Vec<QueryInfo> {
5891 self.catalog
5892 .list_queries()
5893 .into_iter()
5894 .map(|(id, sql, active)| QueryInfo { id, sql, active })
5895 .collect()
5896 }
5897
5898 #[must_use]
5900 pub fn is_checkpoint_enabled(&self) -> bool {
5901 self.config.checkpoint.is_some()
5902 }
5903
5904 pub(crate) fn checkpoint_participant(&self) -> Option<u64> {
5910 checkpoint_participant_for_runtime(self)
5911 }
5912
5913 pub(crate) fn checkpoint_key_groups(&self) -> laminar_core::state::KeyGroupCount {
5917 self.vnode_registry.lock().as_ref().map_or(
5918 laminar_core::state::DEFAULT_KEY_GROUP_COUNT,
5919 |registry| {
5920 laminar_core::state::KeyGroupCount::try_from(registry.vnode_count())
5921 .expect("builder validated the vnode registry key-group count")
5922 },
5923 )
5924 }
5925
5926 pub(crate) fn checkpoint_object_store(
5927 &self,
5928 ) -> Result<Option<Arc<dyn object_store::ObjectStore>>, DbError> {
5929 let Some(cp_config) = self.config.checkpoint.as_ref() else {
5930 return Ok(None);
5931 };
5932 #[cfg(feature = "cluster")]
5933 if let Some(object_store) = self.cluster_checkpoint_object_store() {
5934 return Ok(Some(object_store));
5935 }
5936 let object_store: Arc<dyn object_store::ObjectStore> = if let Some(ref url) =
5937 self.config.object_store_url
5938 {
5939 laminar_core::checkpoint::object_store_builder::build_object_store(
5940 url,
5941 &self.config.object_store_options,
5942 )
5943 .map_err(|error| DbError::Checkpoint(format!("checkpoint object store: {error}")))?
5944 } else {
5945 let data_dir = cp_config
5946 .data_dir
5947 .clone()
5948 .or_else(|| self.config.storage_dir.clone())
5949 .unwrap_or_else(|| std::path::PathBuf::from("./data"));
5950 laminar_core::checkpoint::object_store_builder::durable_local_object_store(data_dir)
5951 .map_err(|error| DbError::Checkpoint(format!("checkpoint object store: {error}")))?
5952 };
5953 Ok(Some(object_store))
5954 }
5955
5956 pub async fn checkpoint(
5964 &self,
5965 ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5966 let timeout = self
5967 .config
5968 .checkpoint
5969 .as_ref()
5970 .and_then(|checkpoint| checkpoint.timeout_ms)
5971 .map_or(
5972 crate::checkpoint_coordinator::CheckpointConfig::default().checkpoint_timeout,
5973 std::time::Duration::from_millis,
5974 );
5975 self.checkpoint_with_timeout(timeout).await
5976 }
5977
5978 pub async fn checkpoint_with_timeout(
5989 &self,
5990 timeout: std::time::Duration,
5991 ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
5992 let deadline = self.manual_checkpoint_deadline(timeout)?;
5993 self.checkpoint_until(deadline).await
5994 }
5995
5996 #[doc(hidden)]
6007 pub async fn checkpoint_forwarded_with_timeout(
6008 &self,
6009 timeout: std::time::Duration,
6010 ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
6011 let deadline = self.manual_checkpoint_deadline(timeout)?;
6012 self.checkpoint_until_inner(deadline, false).await
6013 }
6014
6015 fn manual_checkpoint_deadline(
6016 &self,
6017 timeout: std::time::Duration,
6018 ) -> Result<tokio::time::Instant, DbError> {
6019 let configured_timeout = self
6020 .config
6021 .checkpoint
6022 .as_ref()
6023 .and_then(|checkpoint| checkpoint.timeout_ms)
6024 .map_or(
6025 crate::checkpoint_coordinator::CheckpointConfig::default().checkpoint_timeout,
6026 std::time::Duration::from_millis,
6027 );
6028 let deadline = tokio::time::Instant::now()
6029 .checked_add(timeout.min(configured_timeout))
6030 .ok_or_else(|| DbError::Checkpoint("manual checkpoint deadline overflowed".into()))?;
6031 Ok(deadline)
6032 }
6033
6034 pub(crate) async fn checkpoint_until(
6040 &self,
6041 deadline: tokio::time::Instant,
6042 ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
6043 self.checkpoint_until_inner(deadline, true).await
6044 }
6045
6046 async fn checkpoint_until_inner(
6047 &self,
6048 deadline: tokio::time::Instant,
6049 allow_forwarding: bool,
6050 ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
6051 #[cfg(not(feature = "cluster"))]
6052 let _ = allow_forwarding;
6053 self.ensure_catalog_cleanup_unfenced("manual checkpoint")?;
6054 #[cfg(feature = "cluster")]
6055 self.ensure_coordinated_recovery_mutation_unfenced("manual checkpoint")?;
6056 if self.config.checkpoint.is_none() {
6057 return Err(DbError::Checkpoint(
6058 "checkpointing is not enabled".to_string(),
6059 ));
6060 }
6061 if DbState::load(&self.state) != DbState::Running {
6062 return Err(DbError::Checkpoint(
6063 "manual checkpoint coordinator is not running — a fully running pipeline is required"
6064 .into(),
6065 ));
6066 }
6067
6068 #[cfg(feature = "cluster")]
6069 if self.is_cluster_runtime() {
6070 let leader_rpc: Result<Option<String>, DbError> = {
6071 let cc_guard = self.cluster_controller.lock();
6072 let cc = cc_guard.as_ref().ok_or_else(|| {
6073 DbError::Checkpoint("cluster runtime has no cluster controller".into())
6074 })?;
6075 match cc {
6076 cc if cc.is_leader() => Ok(None),
6077 _ if !allow_forwarding => Err(DbError::Checkpoint(
6078 "forwarded checkpoint reached a node that is no longer the cluster leader"
6079 .into(),
6080 )),
6081 cc => {
6082 let leader_id = cc.current_leader().ok_or_else(|| {
6083 DbError::Checkpoint(
6084 "cannot route checkpoint: cluster leader is unresolved".into(),
6085 )
6086 })?;
6087 let watch = cc.members_watch();
6088 let members = watch.borrow();
6089 let address = members
6090 .iter()
6091 .find(|member| member.id == leader_id)
6092 .map(|member| member.rpc_address.trim())
6093 .filter(|address| !address.is_empty())
6094 .ok_or_else(|| {
6095 DbError::Checkpoint(format!(
6096 "cannot route checkpoint: RPC address for cluster leader \
6097 {leader_id} is unresolved"
6098 ))
6099 })?;
6100 Ok(Some(address.to_owned()))
6101 }
6102 }
6103 };
6104 if let Some(leader_rpc) = leader_rpc? {
6105 tracing::info!(
6106 "Forwarding checkpoint request to leader node at HTTP address {}",
6107 leader_rpc
6108 );
6109 return self
6110 .forward_checkpoint_to_leader(&leader_rpc, deadline)
6111 .await;
6112 }
6113 }
6114
6115 let tx = self.force_ckpt_tx.lock().clone().ok_or_else(|| {
6118 DbError::Checkpoint(
6119 "manual checkpoint coordinator is not running — call start() first".into(),
6120 )
6121 })?;
6122 let (reply_tx, mut reply_rx) = crossfire::oneshot::oneshot();
6123 let (reservation_claim, mut reservation_wait) = ForceCheckpointReservationClaim::new();
6124 let request = ForceCheckpointRequest {
6125 reply: reply_tx,
6126 reservation_claim: Some(reservation_claim),
6127 deadline,
6128 };
6129 tokio::time::timeout_at(deadline, tx.send(request))
6130 .await
6131 .map_err(|_| {
6132 DbError::Checkpoint(
6133 "manual checkpoint deadline expired before coordinator admission".into(),
6134 )
6135 })?
6136 .map_err(|_| {
6137 DbError::Checkpoint(
6138 "manual checkpoint receiver closed — engine may be shutting down".into(),
6139 )
6140 })?;
6141 let mut reservation_wake_open = true;
6147 loop {
6148 tokio::select! {
6149 biased;
6150 result = &mut reply_rx => {
6151 return result.map_err(|_| {
6152 DbError::Checkpoint(
6153 "manual checkpoint ended without a terminal reply".into(),
6154 )
6155 })?;
6156 }
6157 reservation = reservation_wait.wait(), if reservation_wake_open => {
6158 if reservation.is_ok() {
6159 break;
6160 }
6161 reservation_wake_open = false;
6165 }
6166 () = tokio::time::sleep_until(deadline) => {
6167 if reservation_wait.cancel_before_reservation() {
6168 return Err(DbError::Checkpoint(
6169 "manual checkpoint deadline expired before exact attempt reservation"
6170 .into(),
6171 ));
6172 }
6173 if reservation_wait.is_claimed() {
6174 break;
6175 }
6176 return Err(DbError::Checkpoint(
6177 "manual checkpoint deadline expired before exact attempt reservation"
6178 .into(),
6179 ));
6180 }
6181 }
6182 }
6183 let result = reply_rx.await.map_err(|_| {
6184 DbError::Checkpoint("manual checkpoint ended without a terminal reply".into())
6185 })?;
6186
6187 result
6188 }
6189
6190 #[cfg(feature = "cluster")]
6191 async fn forward_checkpoint_to_leader(
6192 &self,
6193 addr: &str,
6194 deadline: tokio::time::Instant,
6195 ) -> Result<crate::checkpoint_coordinator::CheckpointResult, DbError> {
6196 #[derive(serde::Deserialize)]
6197 struct ForwardedCheckpointResponse {
6198 success: bool,
6199 checkpoint_id: u64,
6200 epoch: u64,
6201 duration_ms: u64,
6202 error: Option<String>,
6203 failure_disposition:
6204 Option<crate::checkpoint_coordinator::CheckpointFailureDisposition>,
6205 }
6206
6207 let remaining = deadline
6208 .checked_duration_since(tokio::time::Instant::now())
6209 .filter(|remaining| !remaining.is_zero())
6210 .ok_or_else(|| {
6211 DbError::Checkpoint(
6212 "manual checkpoint deadline expired before leader forwarding".into(),
6213 )
6214 })?;
6215 let budget_nanos = u64::try_from(remaining.as_nanos()).unwrap_or(u64::MAX);
6216 let client = reqwest::Client::builder()
6220 .connect_timeout(remaining)
6221 .build()
6222 .map_err(|error| {
6223 DbError::Checkpoint(format!(
6224 "failed to build checkpoint forwarding client: {error}"
6225 ))
6226 })?;
6227 let mut req = client
6228 .post(format!("http://{addr}/api/v1/checkpoint"))
6229 .header(
6230 "x-laminar-checkpoint-budget-nanos",
6231 budget_nanos.to_string(),
6232 );
6233 if let Some(token) = &self.config.http_auth_token {
6234 req = req.bearer_auth(token.expose());
6235 }
6236 let resp = req.send().await.map_err(|e| {
6237 DbError::Checkpoint(format!(
6238 "failed to forward checkpoint to leader at {addr}: {e}"
6239 ))
6240 })?;
6241
6242 let status = resp.status();
6243 let body = resp.text().await.map_err(|e| {
6244 DbError::Checkpoint(format!("failed to read leader checkpoint response: {e}"))
6245 })?;
6246
6247 match serde_json::from_str::<ForwardedCheckpointResponse>(&body) {
6251 Ok(response) => Ok(crate::checkpoint_coordinator::CheckpointResult {
6252 success: response.success,
6253 checkpoint_id: response.checkpoint_id,
6254 epoch: response.epoch,
6255 duration: std::time::Duration::from_millis(response.duration_ms),
6256 error: response.error,
6257 failure_disposition: response.failure_disposition,
6258 }),
6259 Err(_) => Err(DbError::Checkpoint(format!(
6260 "leader rejected checkpoint ({status}): {body}"
6261 ))),
6262 }
6263 }
6264
6265 pub async fn checkpoint_stats(&self) -> Option<crate::checkpoint_coordinator::CheckpointStats> {
6267 let guard = self.coordinator.lock().await;
6268 guard
6269 .as_ref()
6270 .map(crate::checkpoint_coordinator::CheckpointCoordinator::stats)
6271 }
6272}
6273
6274impl std::fmt::Debug for LaminarDB {
6275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6276 f.debug_struct("LaminarDB")
6277 .field("runtime_mode", &self.runtime_mode)
6278 .field("sources", &self.catalog.list_sources().len())
6279 .field("sinks", &self.catalog.list_sinks().len())
6280 .field("materialized_views", &self.mv_registry.lock().len())
6281 .field("checkpoint_enabled", &self.is_checkpoint_enabled())
6282 .field("shutdown", &self.is_closed())
6283 .finish_non_exhaustive()
6284 }
6285}
6286
6287struct LookupQueryPlanner {
6289 extension_planner: Arc<dyn datafusion::physical_planner::ExtensionPlanner + Send + Sync>,
6290}
6291
6292impl std::fmt::Debug for LookupQueryPlanner {
6293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6294 f.debug_struct("LookupQueryPlanner").finish_non_exhaustive()
6295 }
6296}
6297
6298#[async_trait::async_trait]
6299impl datafusion::execution::context::QueryPlanner for LookupQueryPlanner {
6300 async fn create_physical_plan(
6301 &self,
6302 logical_plan: &datafusion::logical_expr::LogicalPlan,
6303 session_state: &datafusion::execution::SessionState,
6304 ) -> datafusion_common::Result<Arc<dyn datafusion::physical_plan::ExecutionPlan>> {
6305 use datafusion::physical_planner::PhysicalPlanner;
6306 let planner =
6307 datafusion::physical_planner::DefaultPhysicalPlanner::with_extension_planners(vec![
6308 Arc::clone(&self.extension_planner),
6309 ]);
6310 planner
6311 .create_physical_plan(logical_plan, session_state)
6312 .await
6313 }
6314}
6315
6316#[cfg(test)]
6317mod tests;