1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use datafusion_expr::{AggregateUDF, ScalarUDF};
9use laminar_core::streaming::{BackpressureStrategy, StreamCheckpointConfig};
10
11use crate::config::LaminarConfig;
12use crate::db::{LaminarDB, RuntimeMode};
13use crate::error::DbError;
14use crate::profile::Profile;
15
16type ConnectorCallback = Box<
18 dyn FnOnce(
19 &laminar_connectors::registry::ConnectorRegistry,
20 ) -> Result<(), laminar_connectors::error::ConnectorError>
21 + Send,
22>;
23
24pub struct LaminarDbBuilder {
36 config: LaminarConfig,
37 config_vars: HashMap<String, String>,
38 connector_callbacks: Vec<ConnectorCallback>,
39 profile: Profile,
40 profile_explicit: bool,
41 delivery_explicit: bool,
42 object_store_url: Option<String>,
43 object_store_options: HashMap<String, String>,
44 custom_udfs: Vec<ScalarUDF>,
45 custom_udafs: Vec<AggregateUDF>,
46 #[cfg(feature = "cluster")]
47 cluster_controller: Option<std::sync::Arc<laminar_core::cluster::control::ClusterController>>,
48 #[cfg(feature = "cluster")]
49 shuffle_sender: Option<std::sync::Arc<laminar_core::shuffle::ShuffleSender>>,
50 #[cfg(feature = "cluster")]
51 shuffle_receiver: Option<std::sync::Arc<laminar_core::shuffle::ShuffleReceiver>>,
52 #[cfg(feature = "cluster")]
53 decision_store: Option<std::sync::Arc<laminar_core::cluster::control::CheckpointDecisionStore>>,
54 #[cfg(feature = "cluster")]
55 assignment_snapshot_store:
56 Option<std::sync::Arc<laminar_core::cluster::control::AssignmentSnapshotStore>>,
57 #[cfg(feature = "cluster")]
58 catalog_manifest_store:
59 Option<std::sync::Arc<laminar_core::cluster::control::CatalogManifestStore>>,
60 #[cfg(feature = "cluster")]
61 verified_cluster_namespaces: Option<laminar_core::cluster::control::VerifiedClusterNamespaces>,
62 #[cfg(all(feature = "cluster", test))]
63 unchecked_cluster_checkpoint_store: Option<Arc<dyn object_store::ObjectStore>>,
64 state_backend: Option<std::sync::Arc<dyn laminar_core::state::StateBackend>>,
65 vnode_registry: Option<std::sync::Arc<laminar_core::state::VnodeRegistry>>,
66 physical_optimizer_rules: Vec<
67 std::sync::Arc<dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync>,
68 >,
69 target_partitions: Option<usize>,
70 ai_runtime: Option<std::sync::Arc<crate::ai::AiRuntime>>,
71}
72
73impl LaminarDbBuilder {
74 #[must_use]
76 pub fn new() -> Self {
77 Self {
78 config: LaminarConfig::default(),
79 config_vars: HashMap::new(),
80 connector_callbacks: Vec::new(),
81 profile: Profile::default(),
82 profile_explicit: false,
83 delivery_explicit: false,
84 object_store_url: None,
85 object_store_options: HashMap::new(),
86 custom_udfs: Vec::new(),
87 custom_udafs: Vec::new(),
88 #[cfg(feature = "cluster")]
89 cluster_controller: None,
90 #[cfg(feature = "cluster")]
91 shuffle_sender: None,
92 #[cfg(feature = "cluster")]
93 shuffle_receiver: None,
94 #[cfg(feature = "cluster")]
95 decision_store: None,
96 #[cfg(feature = "cluster")]
97 assignment_snapshot_store: None,
98 #[cfg(feature = "cluster")]
99 catalog_manifest_store: None,
100 #[cfg(feature = "cluster")]
101 verified_cluster_namespaces: None,
102 #[cfg(all(feature = "cluster", test))]
103 unchecked_cluster_checkpoint_store: None,
104 state_backend: None,
105 vnode_registry: None,
106 physical_optimizer_rules: Vec::new(),
107 target_partitions: None,
108 ai_runtime: None,
109 }
110 }
111
112 #[must_use]
114 pub fn ai(mut self, runtime: std::sync::Arc<crate::ai::AiRuntime>) -> Self {
115 self.ai_runtime = Some(runtime);
116 self
117 }
118
119 #[must_use]
121 pub fn target_partitions(mut self, n: usize) -> Self {
122 self.target_partitions = Some(n);
123 self
124 }
125
126 #[must_use]
128 pub fn physical_optimizer_rule(
129 mut self,
130 rule: std::sync::Arc<
131 dyn datafusion::physical_optimizer::PhysicalOptimizerRule + Send + Sync,
132 >,
133 ) -> Self {
134 self.physical_optimizer_rules.push(rule);
135 self
136 }
137
138 #[must_use]
140 pub fn state_backend(
141 mut self,
142 backend: std::sync::Arc<dyn laminar_core::state::StateBackend>,
143 ) -> Self {
144 self.state_backend = Some(backend);
145 self
146 }
147
148 #[must_use]
150 pub fn vnode_registry(
151 mut self,
152 registry: std::sync::Arc<laminar_core::state::VnodeRegistry>,
153 ) -> Self {
154 self.vnode_registry = Some(registry);
155 self
156 }
157
158 #[cfg(feature = "cluster")]
161 #[must_use]
162 pub fn cluster_controller(
163 mut self,
164 controller: std::sync::Arc<laminar_core::cluster::control::ClusterController>,
165 ) -> Self {
166 self.cluster_controller = Some(controller);
167 self
168 }
169
170 #[cfg(feature = "cluster")]
172 #[must_use]
173 pub fn shuffle_sender(
174 mut self,
175 sender: std::sync::Arc<laminar_core::shuffle::ShuffleSender>,
176 ) -> Self {
177 self.shuffle_sender = Some(sender);
178 self
179 }
180
181 #[cfg(feature = "cluster")]
183 #[must_use]
184 pub fn shuffle_receiver(
185 mut self,
186 receiver: std::sync::Arc<laminar_core::shuffle::ShuffleReceiver>,
187 ) -> Self {
188 self.shuffle_receiver = Some(receiver);
189 self
190 }
191
192 #[cfg(feature = "cluster")]
194 #[must_use]
195 pub fn decision_store(
196 mut self,
197 store: std::sync::Arc<laminar_core::cluster::control::CheckpointDecisionStore>,
198 ) -> Self {
199 self.decision_store = Some(store);
200 self
201 }
202
203 #[cfg(feature = "cluster")]
205 #[must_use]
206 pub fn assignment_snapshot_store(
207 mut self,
208 store: std::sync::Arc<laminar_core::cluster::control::AssignmentSnapshotStore>,
209 ) -> Self {
210 self.assignment_snapshot_store = Some(store);
211 self
212 }
213
214 #[cfg(feature = "cluster")]
216 #[must_use]
217 pub fn catalog_manifest_store(
218 mut self,
219 store: std::sync::Arc<laminar_core::cluster::control::CatalogManifestStore>,
220 ) -> Self {
221 self.catalog_manifest_store = Some(store);
222 self
223 }
224
225 #[cfg(feature = "cluster")]
227 #[must_use]
228 pub fn verified_cluster_namespaces(
229 mut self,
230 namespaces: laminar_core::cluster::control::VerifiedClusterNamespaces,
231 ) -> Self {
232 self.verified_cluster_namespaces = Some(namespaces);
233 self
234 }
235
236 #[cfg(all(feature = "cluster", test))]
238 #[must_use]
239 pub(crate) fn cluster_checkpoint_object_store(
240 mut self,
241 store: Arc<dyn object_store::ObjectStore>,
242 ) -> Self {
243 self.unchecked_cluster_checkpoint_store = Some(store);
244 self
245 }
246
247 #[must_use]
249 pub fn config_var(mut self, key: &str, value: &str) -> Self {
250 self.config_vars.insert(key.to_string(), value.to_string());
251 self
252 }
253
254 #[must_use]
257 pub fn http_auth_token(mut self, token: impl Into<String>) -> Self {
258 self.config.http_auth_token = Some(crate::config::SecretString::new(token));
259 self
260 }
261
262 #[must_use]
264 pub fn buffer_size(mut self, size: usize) -> Self {
265 self.config.default_buffer_size = size;
266 self
267 }
268
269 #[must_use]
271 pub fn backpressure(mut self, strategy: BackpressureStrategy) -> Self {
272 self.config.default_backpressure = strategy;
273 self
274 }
275
276 #[must_use]
278 pub fn storage_dir(mut self, path: impl Into<PathBuf>) -> Self {
279 self.config.storage_dir = Some(path.into());
280 self
281 }
282
283 #[must_use]
285 pub fn checkpoint(mut self, config: StreamCheckpointConfig) -> Self {
286 self.config.checkpoint = Some(config);
287 self
288 }
289
290 #[must_use]
292 pub fn incremental_emit(mut self, enabled: bool) -> Self {
293 self.config.incremental_emit = enabled;
294 self
295 }
296
297 #[must_use]
302 pub fn profile(mut self, profile: Profile) -> Self {
303 self.profile = profile;
304 self.profile_explicit = true;
305 self
306 }
307
308 #[must_use]
313 pub fn object_store_url(mut self, url: impl Into<String>) -> Self {
314 self.object_store_url = Some(url.into());
315 self
316 }
317
318 #[must_use]
323 pub fn object_store_options(mut self, opts: HashMap<String, String>) -> Self {
324 self.object_store_options = opts;
325 self
326 }
327
328 #[must_use]
330 pub fn delivery_guarantee(
331 mut self,
332 guarantee: laminar_connectors::connector::DeliveryGuarantee,
333 ) -> Self {
334 self.config.delivery_guarantee = guarantee;
335 self.delivery_explicit = true;
336 self
337 }
338
339 #[must_use]
341 pub fn register_udf(mut self, udf: ScalarUDF) -> Self {
342 self.custom_udfs.push(udf);
343 self
344 }
345
346 #[must_use]
348 pub fn register_udaf(mut self, udaf: AggregateUDF) -> Self {
349 self.custom_udafs.push(udaf);
350 self
351 }
352
353 #[must_use]
355 pub fn pipeline_channel_capacity(mut self, capacity: usize) -> Self {
356 self.config.pipeline_channel_capacity = Some(capacity);
357 self
358 }
359
360 #[must_use]
362 pub fn pipeline_batch_window(mut self, window: std::time::Duration) -> Self {
363 self.config.pipeline_batch_window = Some(window);
364 self
365 }
366
367 #[must_use]
369 pub fn pipeline_drain_budget_ns(mut self, ns: u64) -> Self {
370 self.config.pipeline_drain_budget_ns = Some(ns);
371 self
372 }
373
374 #[must_use]
376 pub fn pipeline_query_budget_ns(mut self, ns: u64) -> Self {
377 self.config.pipeline_query_budget_ns = Some(ns);
378 self
379 }
380
381 #[must_use]
383 pub fn pipeline_max_input_buf_batches(mut self, batches: usize) -> Self {
384 self.config.pipeline_max_input_buf_batches = Some(batches);
385 self
386 }
387
388 #[must_use]
390 pub fn pipeline_max_input_buf_bytes(mut self, bytes: usize) -> Self {
391 self.config.pipeline_max_input_buf_bytes = Some(bytes);
392 self
393 }
394
395 #[must_use]
397 pub fn pipeline_backpressure_policy(
398 mut self,
399 policy: crate::config::BackpressurePolicy,
400 ) -> Self {
401 self.config.pipeline_backpressure_policy = policy;
402 self
403 }
404
405 #[must_use]
407 pub fn restart_policy(mut self, policy: crate::config::RestartPolicy) -> Self {
408 self.config.restart_policy = policy;
409 self
410 }
411
412 #[must_use]
415 pub fn register_connector(
416 mut self,
417 f: impl FnOnce(
418 &laminar_connectors::registry::ConnectorRegistry,
419 ) -> Result<(), laminar_connectors::error::ConnectorError>
420 + Send
421 + 'static,
422 ) -> Self {
423 self.connector_callbacks.push(Box::new(f));
424 self
425 }
426
427 #[allow(clippy::unused_async)]
433 pub async fn build(mut self) -> Result<Arc<LaminarDB>, DbError> {
434 let has_checkpoint_data_dir = self
435 .config
436 .checkpoint
437 .as_ref()
438 .and_then(|checkpoint| checkpoint.data_dir.as_ref())
439 .is_some();
440 if self.object_store_url.is_some() && has_checkpoint_data_dir {
441 return Err(DbError::Config(
442 "object_store_url conflicts with checkpoint.data_dir; configure exactly one checkpoint store"
443 .into(),
444 ));
445 }
446 #[cfg(feature = "cluster")]
447 if self.has_cluster_checkpoint_store() && self.object_store_url.is_some() {
448 return Err(DbError::Config(
449 "verified cluster namespaces conflict with object_store_url; configure exactly one checkpoint object store"
450 .into(),
451 ));
452 }
453 #[cfg(feature = "cluster")]
454 if self.has_cluster_checkpoint_store() && has_checkpoint_data_dir {
455 return Err(DbError::Config(
456 "verified cluster namespaces conflict with checkpoint.data_dir; configure exactly one checkpoint store"
457 .into(),
458 ));
459 }
460
461 self.config.object_store_url = self.object_store_url.take();
462 self.config.object_store_options = std::mem::take(&mut self.object_store_options);
463 if let Some(url) = self
464 .config
465 .object_store_url
466 .as_deref()
467 .filter(|url| url.starts_with("file://"))
468 {
469 laminar_core::storage::object_store_builder::file_url_path(url)
470 .map_err(|error| DbError::Config(format!("checkpoint storage URL: {error}")))?;
471 }
472
473 if !self.profile_explicit {
474 self.profile = Profile::from_config(&self.config, false);
475 }
476
477 let runtime_mode = self.resolve_runtime_mode()?;
478 if runtime_mode.is_cluster() && !self.profile_explicit {
479 self.profile = Profile::Cluster;
480 }
481 if runtime_mode.is_cluster() && !self.delivery_explicit {
482 self.config.delivery_guarantee =
483 laminar_connectors::connector::DeliveryGuarantee::AtLeastOnce;
484 }
485
486 self.profile
487 .validate_features()
488 .map_err(|e| DbError::Config(e.to_string()))?;
489 Self::validate_cluster_delivery(runtime_mode, self.config.delivery_guarantee)?;
490 #[cfg(feature = "cluster")]
491 if runtime_mode == RuntimeMode::Cluster {
492 if self.config.object_store_url.is_some() || has_checkpoint_data_dir {
493 return Err(DbError::Config(
494 "[LDB-0011] cluster checkpoint storage must be supplied by a successful shared namespace proof; object_store_url and checkpoint.data_dir are not cluster authorities"
495 .into(),
496 ));
497 }
498 if !self.has_cluster_checkpoint_store() {
499 return Err(DbError::Config(
500 "[LDB-0011] cluster runtime requires VerifiedClusterNamespaces from the shared checkpoint/state namespace proof"
501 .into(),
502 ));
503 }
504 if let Some(namespaces) = &self.verified_cluster_namespaces {
505 let controller = self.cluster_controller.as_ref().ok_or_else(|| {
506 DbError::Config("cluster namespace proof requires a cluster controller".into())
507 })?;
508 let expected = laminar_core::checkpoint::CheckpointParticipant {
509 node_id: controller.instance_id().0,
510 boot_incarnation: controller.recovery_incarnation(),
511 };
512 if namespaces.local_participant() != expected {
513 let proved = namespaces.local_participant();
514 return Err(DbError::Config(format!(
515 "[LDB-0011] verified cluster namespaces belong to node {} boot {}, but the controller is node {} boot {}",
516 proved.node_id,
517 proved.boot_incarnation,
518 expected.node_id,
519 expected.boot_incarnation,
520 )));
521 }
522 let state_backend = self.state_backend.as_ref().ok_or_else(|| {
523 DbError::Config(
524 "[LDB-0011] cluster runtime requires a ClusterShared state_backend built from the verified state namespace"
525 .into(),
526 )
527 })?;
528 let durability = state_backend.durability_scope();
529 if durability != laminar_core::state::StateBackendDurability::ClusterShared {
530 return Err(DbError::Config(format!(
531 "[LDB-0011] cluster state_backend must be ClusterShared, but is {durability:?}"
532 )));
533 }
534 let verified_state_store = namespaces.state_store();
535 if !state_backend.uses_exact_object_store(&verified_state_store) {
536 return Err(DbError::Config(
537 "[LDB-0011] cluster state_backend does not use the exact object-store handle admitted by the namespace proof"
538 .into(),
539 ));
540 }
541 }
542 }
543 #[cfg(feature = "cluster")]
544 let profile_object_store = self.config.object_store_url.as_deref().or_else(|| {
545 self.has_cluster_checkpoint_store()
546 .then_some("verified-cluster-checkpoint-store")
547 });
548 #[cfg(not(feature = "cluster"))]
549 let profile_object_store = self.config.object_store_url.as_deref();
550 self.profile
551 .validate_config(&self.config, profile_object_store)
552 .map_err(|e| DbError::Config(e.to_string()))?;
553
554 if runtime_mode == RuntimeMode::Local
555 && self.config.delivery_guarantee
556 != laminar_connectors::connector::DeliveryGuarantee::BestEffort
557 && self
558 .config
559 .object_store_url
560 .as_deref()
561 .is_some_and(|url| !url.starts_with("file://"))
562 {
563 return Err(DbError::Config(
564 "[LDB-0014] a local replay-capable deployment with a shared cloud checkpoint namespace is not admitted until its writer lease is term-fenced; use a built-in or file:// local checkpoint directory, or best_effort delivery"
565 .into(),
566 ));
567 }
568
569 Self::validate_backpressure(&self.config)?;
570 self.validate_state_topology(runtime_mode)?;
571 #[cfg(feature = "cluster")]
572 self.bind_cluster_process_lease(runtime_mode)?;
573
574 self.profile.apply_defaults(&mut self.config);
575
576 #[cfg(feature = "cluster")]
577 let cluster_checkpoint_store = self.cluster_checkpoint_store();
578 let mut db = LaminarDB::open_with_config_and_vars_and_rules(
579 self.config,
580 self.config_vars,
581 &self.physical_optimizer_rules,
582 self.target_partitions,
583 runtime_mode,
584 )?;
585 if let Some(runtime) = self.ai_runtime {
586 let handle = tokio::runtime::Handle::try_current().map_err(|_| {
587 DbError::InvalidOperation(
588 "LaminarDB::build() with an AI runtime must run inside a Tokio runtime"
589 .to_string(),
590 )
591 })?;
592 db.set_ai_runtime(runtime, handle);
593 }
594 for callback in self.connector_callbacks {
595 callback(db.connector_registry())?;
596 }
597 db.connector_registry().freeze();
598 for udf in self.custom_udfs {
599 db.register_custom_udf(udf);
600 }
601 for udaf in self.custom_udafs {
602 db.register_custom_udaf(udaf);
603 }
604 #[cfg(feature = "cluster")]
605 if let Some(controller) = self.cluster_controller {
606 db.set_cluster_controller(controller)?;
607 }
608 #[cfg(feature = "cluster")]
609 if let Some(store) = cluster_checkpoint_store {
610 db.set_cluster_checkpoint_object_store(store)?;
611 }
612 #[cfg(feature = "cluster")]
613 if let Some(sender) = self.shuffle_sender {
614 db.set_shuffle_sender(sender);
615 }
616 #[cfg(feature = "cluster")]
617 if let Some(receiver) = self.shuffle_receiver {
618 db.set_shuffle_receiver(receiver);
619 }
620 #[cfg(feature = "cluster")]
621 if let Some(store) = self.decision_store {
622 db.set_decision_store(store);
623 }
624 #[cfg(feature = "cluster")]
625 if let Some(store) = self.assignment_snapshot_store {
626 db.set_assignment_snapshot_store(store);
627 }
628 #[cfg(feature = "cluster")]
629 if let Some(store) = self.catalog_manifest_store {
630 db.set_catalog_manifest_store(store);
631 }
632 if let Some(backend) = self.state_backend {
633 db.set_state_backend(backend);
634 }
635 if let Some(registry) = self.vnode_registry {
636 db.set_vnode_registry(registry);
637 }
638 Ok(Arc::new(db))
639 }
640
641 fn validate_state_topology(&self, runtime_mode: RuntimeMode) -> Result<(), DbError> {
642 match (&self.state_backend, &self.vnode_registry) {
643 (Some(_), None) => {
644 return Err(DbError::Config(
645 "state_backend is set but vnode_registry is missing".into(),
646 ));
647 }
648 (None, Some(_)) => {
649 return Err(DbError::Config(
650 "vnode_registry is set but state_backend is missing".into(),
651 ));
652 }
653 _ => {}
654 }
655 if let (Some(backend), Some(registry)) =
656 (self.state_backend.as_ref(), self.vnode_registry.as_ref())
657 {
658 let backend_capacity = backend.key_group_capacity();
659 let backend_key_groups =
660 laminar_core::state::KeyGroupCount::try_from(backend_capacity).map_err(|_| {
661 DbError::Config(format!(
662 "state_backend key-group capacity must be between 1 and {}, got {backend_capacity}",
663 laminar_core::state::MAX_KEY_GROUP_COUNT
664 ))
665 })?;
666 let registry_count = registry.vnode_count();
667 let registry_key_groups = laminar_core::state::KeyGroupCount::try_from(registry_count)
668 .map_err(|_| {
669 DbError::Config(format!(
670 "vnode_registry count must be between 1 and {}, got {registry_count}",
671 laminar_core::state::MAX_KEY_GROUP_COUNT
672 ))
673 })?;
674 if backend_key_groups != registry_key_groups {
675 return Err(DbError::Config(format!(
676 "state_backend key-group capacity {backend_key_groups} does not match vnode_registry count {registry_key_groups}"
677 )));
678 }
679 if !runtime_mode.is_cluster()
680 && registry_key_groups != laminar_core::state::LOCAL_KEY_GROUP_COUNT
681 {
682 return Err(DbError::Config(format!(
683 "local runtime requires exactly {} key group; got {registry_key_groups}. Multi-key-group ownership requires cluster mode",
684 laminar_core::state::LOCAL_KEY_GROUP_COUNT
685 )));
686 }
687 }
688 Ok(())
689 }
690
691 #[cfg(feature = "cluster")]
692 fn bind_cluster_process_lease(&self, runtime_mode: RuntimeMode) -> Result<(), DbError> {
693 if !runtime_mode.is_cluster() {
694 return Ok(());
695 }
696 let controller = self.cluster_controller.as_ref().ok_or_else(|| {
697 DbError::Config("cluster runtime requires a cluster controller".into())
698 })?;
699 let deadline = controller.process_lease_deadline().ok_or_else(|| {
700 DbError::Config(
701 "cluster runtime requires one shared process lease deadline before construction"
702 .into(),
703 )
704 })?;
705 if !deadline.is_live() {
706 return Err(DbError::Config(
707 "cluster runtime process lease deadline is already expired".into(),
708 ));
709 }
710 match (&self.shuffle_sender, &self.shuffle_receiver) {
711 (Some(sender), Some(receiver)) => {
712 sender
713 .bind_process_lease_deadline_pair(receiver, deadline)
714 .map_err(|error| {
715 DbError::Config(format!(
716 "shuffle process lease does not match the controller: {error}"
717 ))
718 })?;
719 }
720 (None, None) => {}
721 _ => {
722 return Err(DbError::Config(
723 "cluster shuffle sender and receiver must be installed together".into(),
724 ));
725 }
726 }
727 Ok(())
728 }
729
730 fn validate_backpressure(config: &LaminarConfig) -> Result<(), DbError> {
731 use crate::config::BackpressurePolicy;
732 use laminar_connectors::connector::DeliveryGuarantee;
733
734 let policy = config.pipeline_backpressure_policy;
735 if policy == BackpressurePolicy::Backpressure {
736 return Ok(());
737 }
738
739 let has_count_cap = config.pipeline_max_input_buf_batches.is_none_or(|c| c > 0);
740 let has_byte_cap = config.pipeline_max_input_buf_bytes.is_some_and(|b| b > 0);
741 if !has_count_cap && !has_byte_cap {
742 return Err(DbError::Config(format!(
743 "backpressure_policy={policy:?} requires at least one of \
744 pipeline_max_input_buf_batches (>0) or pipeline_max_input_buf_bytes"
745 )));
746 }
747
748 if policy == BackpressurePolicy::ShedOldest
749 && config.delivery_guarantee == DeliveryGuarantee::ExactlyOnce
750 {
751 return Err(DbError::Config(
752 "ShedOldest drops data; it is incompatible with exactly-once \
753 delivery. Use Backpressure or Fail, or downgrade the guarantee."
754 .into(),
755 ));
756 }
757 Ok(())
758 }
759
760 #[cfg(feature = "cluster")]
761 fn has_cluster_checkpoint_store(&self) -> bool {
762 let has_store = self.verified_cluster_namespaces.is_some();
763 #[cfg(test)]
764 let has_store = has_store || self.unchecked_cluster_checkpoint_store.is_some();
765 has_store
766 }
767
768 #[cfg(feature = "cluster")]
769 fn cluster_checkpoint_store(&self) -> Option<Arc<dyn object_store::ObjectStore>> {
770 let store = self
771 .verified_cluster_namespaces
772 .as_ref()
773 .map(laminar_core::cluster::control::VerifiedClusterNamespaces::checkpoint_store);
774 #[cfg(test)]
775 let store = store.or_else(|| self.unchecked_cluster_checkpoint_store.clone());
776 store
777 }
778
779 fn resolve_runtime_mode(&self) -> Result<RuntimeMode, DbError> {
782 #[cfg(not(feature = "cluster"))]
783 {
784 if self.profile == Profile::Cluster {
785 return Err(DbError::Config(
786 "Profile::Cluster requires a cluster controller and a cluster-enabled build"
787 .into(),
788 ));
789 }
790 Ok(RuntimeMode::Local)
791 }
792
793 #[cfg(feature = "cluster")]
794 {
795 let has_controller = self.cluster_controller.is_some();
796 let has_cluster_only_handle = self.shuffle_sender.is_some()
797 || self.shuffle_receiver.is_some()
798 || self.decision_store.is_some()
799 || self.assignment_snapshot_store.is_some()
800 || self.catalog_manifest_store.is_some()
801 || self.has_cluster_checkpoint_store();
802
803 if has_cluster_only_handle && !has_controller {
804 return Err(DbError::Config(
805 "cluster-only stores and shuffle handles require a cluster controller".into(),
806 ));
807 }
808 if self.profile == Profile::Cluster && !has_controller {
809 return Err(DbError::Config(
810 "Profile::Cluster requires a cluster controller".into(),
811 ));
812 }
813 if self.profile_explicit && self.profile != Profile::Cluster && has_controller {
814 return Err(DbError::Config(format!(
815 "profile {} cannot be combined with a cluster controller; select Profile::Cluster or omit the explicit profile",
816 self.profile
817 )));
818 }
819
820 Ok(if has_controller {
821 RuntimeMode::Cluster
822 } else {
823 RuntimeMode::Local
824 })
825 }
826 }
827
828 fn validate_cluster_delivery(
834 runtime_mode: RuntimeMode,
835 guarantee: laminar_connectors::connector::DeliveryGuarantee,
836 ) -> Result<(), DbError> {
837 use laminar_connectors::connector::DeliveryGuarantee;
838
839 if !runtime_mode.is_cluster() {
840 return Ok(());
841 }
842 match guarantee {
843 DeliveryGuarantee::BestEffort => Err(DbError::Config(
844 "cluster mode requires at_least_once delivery; best_effort has no defined \
845 rebalance/state-loss contract"
846 .into(),
847 )),
848 DeliveryGuarantee::AtLeastOnce => Ok(()),
849 DeliveryGuarantee::ExactlyOnce => Err(DbError::Config(
850 "[LDB-0013] cluster exactly-once is not admitted: checkpoint decisions are \
851 term-fenced, but supported connectors do not yet provide a certified \
852 term-fenced source handoff and external sink cursor commit. Use cluster \
853 at_least_once, or exactly_once in embedded/single-node mode"
854 .into(),
855 )),
856 }
857 }
858}
859
860impl Default for LaminarDbBuilder {
861 fn default() -> Self {
862 Self::new()
863 }
864}
865
866impl std::fmt::Debug for LaminarDbBuilder {
867 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
868 f.debug_struct("LaminarDbBuilder")
869 .field("config", &self.config)
870 .field("profile", &self.profile)
871 .field("profile_explicit", &self.profile_explicit)
872 .field("delivery_explicit", &self.delivery_explicit)
873 .field("object_store_url", &self.object_store_url)
874 .field(
875 "object_store_options_count",
876 &self.object_store_options.len(),
877 )
878 .field("config_vars_count", &self.config_vars.len())
879 .field("connector_callbacks", &self.connector_callbacks.len())
880 .field("custom_udfs", &self.custom_udfs.len())
881 .field("custom_udafs", &self.custom_udafs.len())
882 .finish_non_exhaustive()
883 }
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[cfg(feature = "cluster")]
891 fn test_cluster_controller_without_deadline(
892 ) -> Arc<laminar_core::cluster::control::ClusterController> {
893 use laminar_core::cluster::control::{ClusterController, ClusterKv, InMemoryKv};
894 use laminar_core::cluster::discovery::{NodeId, NodeInfo};
895
896 let node = NodeId(1);
897 let kv: Arc<dyn ClusterKv> = Arc::new(InMemoryKv::new(node));
898 let (_members_tx, members_rx) = tokio::sync::watch::channel(Vec::<NodeInfo>::new());
899 Arc::new(ClusterController::new(node, kv, None, members_rx))
900 }
901
902 #[cfg(feature = "cluster")]
903 fn test_cluster_controller() -> Arc<laminar_core::cluster::control::ClusterController> {
904 use laminar_core::cluster::control::LeaseDeadline;
905
906 let controller = test_cluster_controller_without_deadline();
907 controller
908 .set_process_lease_deadline(Arc::new(LeaseDeadline::live_for(
909 std::time::Duration::from_secs(60),
910 )))
911 .unwrap();
912 controller
913 }
914
915 #[cfg(feature = "cluster")]
916 fn test_cluster_checkpoint_store() -> Arc<dyn object_store::ObjectStore> {
917 Arc::new(object_store::memory::InMemory::new())
918 }
919
920 #[cfg(feature = "cluster")]
921 async fn test_verified_cluster_namespaces(
922 controller: &laminar_core::cluster::control::ClusterController,
923 checkpoint_store: Arc<dyn object_store::ObjectStore>,
924 ) -> laminar_core::cluster::control::VerifiedClusterNamespaces {
925 use laminar_core::checkpoint::CheckpointParticipant;
926 use laminar_core::cluster::control::{
927 prove_shared_object_store_namespaces, ClusterKv, InMemoryKv,
928 };
929
930 let participant = CheckpointParticipant {
931 node_id: controller.instance_id().0,
932 boot_incarnation: controller.recovery_incarnation(),
933 };
934 let control: Arc<dyn ClusterKv> = Arc::new(InMemoryKv::new(controller.instance_id()));
935 prove_shared_object_store_namespaces(
936 participant,
937 &[participant],
938 control,
939 checkpoint_store,
940 test_cluster_checkpoint_store(),
941 std::time::Duration::from_secs(1),
942 )
943 .await
944 .unwrap()
945 }
946
947 #[cfg(feature = "cluster")]
948 fn test_verified_state_topology(
949 namespaces: &laminar_core::cluster::control::VerifiedClusterNamespaces,
950 ) -> (
951 Arc<dyn laminar_core::state::StateBackend>,
952 Arc<laminar_core::state::VnodeRegistry>,
953 ) {
954 let vnode_count = u32::from(laminar_core::state::DEFAULT_CLUSTER_KEY_GROUP_COUNT);
955 (
956 Arc::new(laminar_core::state::ObjectStoreBackend::cluster_shared(
957 namespaces.state_store(),
958 "test-node",
959 vnode_count,
960 )),
961 Arc::new(laminar_core::state::VnodeRegistry::new(vnode_count)),
962 )
963 }
964
965 #[tokio::test]
966 async fn test_default_builder() {
967 let db = LaminarDbBuilder::new().build().await.unwrap();
968 assert!(!db.is_closed());
969 assert!(!db.is_cluster_runtime());
970 #[cfg(feature = "cluster")]
971 assert!(!db.cluster_intake_fenced());
972 }
973
974 #[tokio::test]
975 async fn test_shed_oldest_requires_cap() {
976 use crate::config::BackpressurePolicy;
977 let err = LaminarDbBuilder::new()
978 .pipeline_backpressure_policy(BackpressurePolicy::ShedOldest)
979 .pipeline_max_input_buf_batches(0)
980 .build()
981 .await
982 .expect_err("ShedOldest with no caps must be rejected");
983 assert!(err.to_string().contains("requires at least one"), "{err}");
984 }
985
986 #[tokio::test]
987 async fn test_shed_oldest_rejects_exactly_once() {
988 use crate::config::BackpressurePolicy;
989 use laminar_connectors::connector::DeliveryGuarantee;
990 let err = LaminarDbBuilder::new()
991 .pipeline_backpressure_policy(BackpressurePolicy::ShedOldest)
992 .pipeline_max_input_buf_batches(64)
993 .delivery_guarantee(DeliveryGuarantee::ExactlyOnce)
994 .build()
995 .await
996 .expect_err("ShedOldest + ExactlyOnce must be rejected");
997 assert!(err.to_string().contains("exactly-once"), "{err}");
998 }
999
1000 #[tokio::test]
1001 async fn local_replay_delivery_rejects_cloud_checkpoint_storage_at_build() {
1002 use laminar_connectors::connector::DeliveryGuarantee;
1003
1004 let error = LaminarDbBuilder::new()
1005 .checkpoint(StreamCheckpointConfig::default())
1006 .delivery_guarantee(DeliveryGuarantee::AtLeastOnce)
1007 .object_store_url("s3://shared-checkpoints/deployment")
1008 .build()
1009 .await
1010 .expect_err("an unfenced local cloud writer must fail before startup");
1011 assert!(error.to_string().contains("[LDB-0014]"), "{error}");
1012 }
1013
1014 #[tokio::test]
1015 async fn checkpoint_url_conflicts_with_checkpoint_data_dir() {
1016 let error = LaminarDbBuilder::new()
1017 .checkpoint(StreamCheckpointConfig {
1018 data_dir: Some(std::path::PathBuf::from("checkpoint-data")),
1019 ..StreamCheckpointConfig::default()
1020 })
1021 .object_store_url("memory://checkpoint-test")
1022 .build()
1023 .await
1024 .expect_err("two checkpoint authorities must be rejected");
1025 assert!(error.to_string().contains("conflicts"), "{error}");
1026 }
1027
1028 #[tokio::test]
1029 async fn checkpoint_state_budget_is_resolved_and_propagated_to_status_stores() {
1030 for max_staged_bytes in [17, isize::MAX as u64] {
1031 let directory = tempfile::tempdir().unwrap();
1032 let db = LaminarDbBuilder::new()
1033 .storage_dir(directory.path())
1034 .checkpoint(StreamCheckpointConfig {
1035 max_staged_bytes: Some(max_staged_bytes),
1036 ..StreamCheckpointConfig::default()
1037 })
1038 .build()
1039 .await
1040 .unwrap();
1041
1042 assert_eq!(
1043 db.config
1044 .checkpoint
1045 .as_ref()
1046 .and_then(|checkpoint| checkpoint.max_staged_bytes),
1047 Some(max_staged_bytes)
1048 );
1049 assert_eq!(
1050 db.checkpoint_store()
1051 .unwrap()
1052 .unwrap()
1053 .max_state_data_bytes(),
1054 max_staged_bytes
1055 );
1056 }
1057 }
1058
1059 #[tokio::test]
1060 async fn checkpoint_state_budget_defaults_to_the_core_storage_limit() {
1061 let directory = tempfile::tempdir().unwrap();
1062 let db = LaminarDbBuilder::new()
1063 .storage_dir(directory.path())
1064 .checkpoint(StreamCheckpointConfig::default())
1065 .build()
1066 .await
1067 .unwrap();
1068 let expected = laminar_core::storage::checkpoint_store::DEFAULT_MAX_CHECKPOINT_STATE_BYTES;
1069
1070 assert_eq!(
1071 db.config
1072 .checkpoint
1073 .as_ref()
1074 .and_then(|checkpoint| checkpoint.max_staged_bytes),
1075 Some(expected)
1076 );
1077 assert_eq!(
1078 db.checkpoint_store()
1079 .unwrap()
1080 .unwrap()
1081 .max_state_data_bytes(),
1082 expected
1083 );
1084 }
1085
1086 #[tokio::test]
1087 async fn checkpoint_state_budget_rejects_an_unaddressable_limit_at_build() {
1088 let unaddressable = (isize::MAX as u64) + 1;
1089 let error = LaminarDbBuilder::new()
1090 .checkpoint(StreamCheckpointConfig {
1091 max_staged_bytes: Some(unaddressable),
1092 ..StreamCheckpointConfig::default()
1093 })
1094 .build()
1095 .await
1096 .expect_err("checkpoint recovery must not accept an unaddressable sidecar limit");
1097
1098 assert!(
1099 error
1100 .to_string()
1101 .contains("exceeds this process address space"),
1102 "{error}"
1103 );
1104 }
1105
1106 #[tokio::test]
1107 async fn explicit_profile_cannot_bypass_file_url_validation() {
1108 let error = LaminarDbBuilder::new()
1109 .profile(Profile::BareMetal)
1110 .object_store_url("file://./relative")
1111 .build()
1112 .await
1113 .expect_err("malformed checkpoint URLs must fail independently of tuning profile");
1114 assert!(
1115 error.to_string().contains("invalid object store URL"),
1116 "{error}"
1117 );
1118 }
1119
1120 #[test]
1121 fn cluster_delivery_admission_fails_closed_without_durable_term_binding() {
1122 use laminar_connectors::connector::DeliveryGuarantee;
1123
1124 assert!(LaminarDbBuilder::validate_cluster_delivery(
1125 RuntimeMode::Cluster,
1126 DeliveryGuarantee::AtLeastOnce,
1127 )
1128 .is_ok());
1129
1130 let error = LaminarDbBuilder::validate_cluster_delivery(
1131 RuntimeMode::Cluster,
1132 DeliveryGuarantee::ExactlyOnce,
1133 )
1134 .expect_err("cluster EO must remain closed until connectors consume the fence end to end");
1135 assert!(error.to_string().contains("[LDB-0013]"), "{error}");
1136 assert!(
1137 error.to_string().contains("external sink cursor commit"),
1138 "{error}"
1139 );
1140
1141 assert!(LaminarDbBuilder::validate_cluster_delivery(
1142 RuntimeMode::Local,
1143 DeliveryGuarantee::ExactlyOnce,
1144 )
1145 .is_ok());
1146 }
1147
1148 #[tokio::test]
1149 async fn cluster_profile_without_controller_is_rejected() {
1150 let error = LaminarDbBuilder::new()
1151 .profile(Profile::Cluster)
1152 .object_store_url("memory://checkpoint-test")
1153 .build()
1154 .await
1155 .expect_err("cluster profile must not degrade into a local runtime");
1156 assert!(error.to_string().contains("cluster controller"), "{error}");
1157 }
1158
1159 #[cfg(feature = "cluster")]
1160 #[tokio::test]
1161 async fn controller_selects_cluster_runtime_when_profile_is_inferred() {
1162 let checkpoint_store = test_cluster_checkpoint_store();
1163 let controller = test_cluster_controller();
1164 let verified_namespaces =
1165 test_verified_cluster_namespaces(controller.as_ref(), Arc::clone(&checkpoint_store))
1166 .await;
1167 let verified_state_store = verified_namespaces.state_store();
1168 let (state_backend, vnode_registry) = test_verified_state_topology(&verified_namespaces);
1169 let db = LaminarDbBuilder::new()
1170 .cluster_controller(controller)
1171 .verified_cluster_namespaces(verified_namespaces)
1172 .state_backend(state_backend)
1173 .vnode_registry(vnode_registry)
1174 .build()
1175 .await
1176 .unwrap();
1177 assert!(db.is_cluster_runtime());
1178 assert_eq!(db.config.default_buffer_size, 262_144);
1179 assert!(Arc::ptr_eq(
1180 &db.cluster_checkpoint_object_store().unwrap(),
1181 &checkpoint_store
1182 ));
1183 assert!(db
1184 .state_backend
1185 .lock()
1186 .as_ref()
1187 .unwrap()
1188 .uses_exact_object_store(&verified_state_store));
1189 assert_eq!(
1190 db.config.delivery_guarantee,
1191 laminar_connectors::connector::DeliveryGuarantee::AtLeastOnce
1192 );
1193 }
1194
1195 #[cfg(feature = "cluster")]
1196 #[tokio::test]
1197 async fn injected_checkpoint_store_conflicts_with_checkpoint_data_dir() {
1198 let error = LaminarDbBuilder::new()
1199 .cluster_controller(test_cluster_controller())
1200 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1201 .checkpoint(StreamCheckpointConfig {
1202 data_dir: Some(std::path::PathBuf::from("checkpoint-data")),
1203 ..StreamCheckpointConfig::default()
1204 })
1205 .build()
1206 .await
1207 .expect_err("two checkpoint authorities must be rejected");
1208 assert!(error.to_string().contains("conflict"), "{error}");
1209 }
1210
1211 #[cfg(feature = "cluster")]
1212 #[tokio::test]
1213 async fn checkpoint_status_reads_the_injected_participant_store() {
1214 use arrow::array::UInt64Array;
1215 use laminar_core::checkpoint::checkpoint_manifest::DurableCheckpointPhase;
1216 use laminar_core::storage::CheckpointStore;
1217
1218 let controller = test_cluster_controller();
1219 let participant_id = controller.instance_id().0;
1220 let object_store = test_cluster_checkpoint_store();
1221 let max_staged_bytes = 97;
1222 let db = LaminarDbBuilder::new()
1223 .cluster_controller(controller)
1224 .cluster_checkpoint_object_store(Arc::clone(&object_store))
1225 .checkpoint(StreamCheckpointConfig {
1226 max_staged_bytes: Some(max_staged_bytes),
1227 ..StreamCheckpointConfig::default()
1228 })
1229 .build()
1230 .await
1231 .unwrap();
1232 let status_store = db.checkpoint_store().unwrap().unwrap();
1233 assert_eq!(status_store.participant_id(), participant_id);
1234 assert_eq!(status_store.max_state_data_bytes(), max_staged_bytes);
1235 let participant_store =
1236 laminar_core::storage::checkpoint_store::ObjectStoreCheckpointStore::new(
1237 object_store,
1238 format!("nodes/{participant_id}/"),
1239 )
1240 .with_max_state_data_bytes(max_staged_bytes)
1241 .unwrap()
1242 .with_key_group_count(db.checkpoint_key_groups())
1243 .with_participant_id(participant_id);
1244 let mut manifest = laminar_core::checkpoint::CheckpointManifest::new_with_key_group_count(
1245 7,
1246 7,
1247 db.checkpoint_key_groups(),
1248 );
1249 manifest.participant_id = participant_id;
1250 manifest.durable_phase = DurableCheckpointPhase::Finalized;
1251 participant_store.save(&manifest).await.unwrap();
1252
1253 let status = db.build_show_checkpoint_status().await.unwrap();
1254 let checkpoint_ids = status
1255 .column(0)
1256 .as_any()
1257 .downcast_ref::<UInt64Array>()
1258 .unwrap();
1259 let total = status
1260 .column(5)
1261 .as_any()
1262 .downcast_ref::<UInt64Array>()
1263 .unwrap();
1264 assert_eq!(checkpoint_ids.value(0), 7);
1265 assert_eq!(total.value(0), 1);
1266 }
1267
1268 #[cfg(feature = "cluster")]
1269 #[tokio::test]
1270 async fn cluster_runtime_rejects_a_controller_without_process_lease_authority() {
1271 let error = LaminarDbBuilder::new()
1272 .cluster_controller(test_cluster_controller_without_deadline())
1273 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1274 .build()
1275 .await
1276 .expect_err("cluster construction must not fail open without a process lease clock");
1277
1278 assert!(
1279 error.to_string().contains("shared process lease deadline"),
1280 "{error}"
1281 );
1282 }
1283
1284 #[cfg(feature = "cluster")]
1285 #[tokio::test]
1286 async fn cluster_builder_binds_shuffle_to_the_controller_deadline() {
1287 use laminar_core::cluster::control::LeaseDeadline;
1288
1289 let controller = test_cluster_controller();
1290 let sender = Arc::new(laminar_core::shuffle::ShuffleSender::new(
1291 1,
1292 uuid::Uuid::from_u128(1),
1293 ));
1294 let receiver = Arc::new(
1295 laminar_core::shuffle::ShuffleReceiver::bind(
1296 1,
1297 "127.0.0.1:0".parse().unwrap(),
1298 uuid::Uuid::from_u128(1),
1299 )
1300 .await
1301 .unwrap(),
1302 );
1303 let _db = LaminarDbBuilder::new()
1304 .cluster_controller(controller)
1305 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1306 .shuffle_sender(Arc::clone(&sender))
1307 .shuffle_receiver(Arc::clone(&receiver))
1308 .build()
1309 .await
1310 .unwrap();
1311 let different = Arc::new(LeaseDeadline::live_for(std::time::Duration::from_secs(60)));
1312
1313 assert!(sender
1314 .install_process_lease_deadline(Arc::clone(&different))
1315 .is_err());
1316 assert!(receiver.install_process_lease_deadline(different).is_err());
1317 }
1318
1319 #[cfg(feature = "cluster")]
1320 #[tokio::test]
1321 async fn cluster_shuffle_binding_failure_does_not_partially_bind_sender() {
1322 use laminar_core::cluster::control::LeaseDeadline;
1323
1324 let sender = Arc::new(laminar_core::shuffle::ShuffleSender::new(
1325 1,
1326 uuid::Uuid::from_u128(1),
1327 ));
1328 let receiver = Arc::new(
1329 laminar_core::shuffle::ShuffleReceiver::bind(
1330 1,
1331 "127.0.0.1:0".parse().unwrap(),
1332 uuid::Uuid::from_u128(1),
1333 )
1334 .await
1335 .unwrap(),
1336 );
1337 receiver
1338 .install_process_lease_deadline(Arc::new(LeaseDeadline::live_for(
1339 std::time::Duration::from_secs(60),
1340 )))
1341 .unwrap();
1342
1343 let error = LaminarDbBuilder::new()
1344 .cluster_controller(test_cluster_controller())
1345 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1346 .shuffle_sender(Arc::clone(&sender))
1347 .shuffle_receiver(receiver)
1348 .build()
1349 .await
1350 .expect_err("an incompatible receiver lease must reject the pair");
1351 assert!(error.to_string().contains("already installed"), "{error}");
1352
1353 sender
1354 .install_process_lease_deadline(Arc::new(LeaseDeadline::live_for(
1355 std::time::Duration::from_secs(60),
1356 )))
1357 .expect("failed pair validation must leave the sender unbound");
1358 }
1359
1360 #[cfg(feature = "cluster")]
1361 #[tokio::test]
1362 async fn stateless_cluster_uses_default_key_group_topology() {
1363 let db = LaminarDbBuilder::new()
1364 .cluster_controller(test_cluster_controller())
1365 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1366 .build()
1367 .await
1368 .unwrap();
1369
1370 assert_eq!(
1371 db.checkpoint_key_groups(),
1372 laminar_core::state::DEFAULT_CLUSTER_KEY_GROUP_COUNT
1373 );
1374 assert!(db.state_backend.lock().is_none());
1375 assert!(db.vnode_registry.lock().is_none());
1376 }
1377
1378 #[cfg(feature = "cluster")]
1379 #[tokio::test]
1380 async fn fresh_cluster_database_keeps_intake_fenced() {
1381 let db = LaminarDbBuilder::new()
1382 .cluster_controller(test_cluster_controller())
1383 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1384 .build()
1385 .await
1386 .unwrap();
1387
1388 assert!(db.cluster_intake_fenced());
1389 db.fence_cluster_startup();
1390 db.fence_cluster_startup();
1391 assert!(db.cluster_intake_fenced());
1392 }
1393
1394 #[cfg(feature = "cluster")]
1395 #[tokio::test]
1396 async fn inferred_cluster_requires_shared_checkpoint_storage() {
1397 let error = LaminarDbBuilder::new()
1398 .cluster_controller(test_cluster_controller())
1399 .build()
1400 .await
1401 .expect_err("cluster runtime must not retain bare-metal storage admission");
1402 assert!(
1403 error.to_string().contains("VerifiedClusterNamespaces"),
1404 "{error}"
1405 );
1406 }
1407
1408 #[cfg(feature = "cluster")]
1409 #[tokio::test]
1410 async fn complete_cluster_profile_selects_cluster_runtime() {
1411 let controller = test_cluster_controller();
1412 let verified_namespaces =
1413 test_verified_cluster_namespaces(controller.as_ref(), test_cluster_checkpoint_store())
1414 .await;
1415 let (state_backend, vnode_registry) = test_verified_state_topology(&verified_namespaces);
1416 let db = LaminarDbBuilder::new()
1417 .profile(Profile::Cluster)
1418 .cluster_controller(controller)
1419 .verified_cluster_namespaces(verified_namespaces)
1420 .state_backend(state_backend)
1421 .vnode_registry(vnode_registry)
1422 .build()
1423 .await
1424 .unwrap();
1425 assert!(db.is_cluster_runtime());
1426 }
1427
1428 #[cfg(feature = "cluster")]
1429 #[tokio::test]
1430 async fn cluster_runtime_rejects_node_local_checkpoint_url() {
1431 let directory = tempfile::tempdir().unwrap();
1432 let path = directory.path().display().to_string().replace('\\', "/");
1433 let url = if path.starts_with('/') {
1434 format!("file://{path}")
1435 } else {
1436 format!("file:///{path}")
1437 };
1438
1439 let error = LaminarDbBuilder::new()
1440 .profile(Profile::Cluster)
1441 .object_store_url(url)
1442 .cluster_controller(test_cluster_controller())
1443 .build()
1444 .await
1445 .expect_err("cluster recovery must not depend on node-local checkpoint storage");
1446 assert!(error.to_string().contains("[LDB-0011]"), "{error}");
1447 assert!(
1448 error
1449 .to_string()
1450 .contains("successful shared namespace proof"),
1451 "{error}"
1452 );
1453 }
1454
1455 #[cfg(feature = "cluster")]
1456 #[tokio::test]
1457 async fn cluster_runtime_rejects_shared_checkpoint_url_without_proof() {
1458 let error = LaminarDbBuilder::new()
1459 .profile(Profile::Cluster)
1460 .object_store_url("s3://checkpoint-test/cluster")
1461 .cluster_controller(test_cluster_controller())
1462 .build()
1463 .await
1464 .expect_err("a shared URL must not bypass the exact namespace proof");
1465 assert!(error.to_string().contains("[LDB-0011]"), "{error}");
1466 assert!(
1467 error
1468 .to_string()
1469 .contains("successful shared namespace proof"),
1470 "{error}"
1471 );
1472 }
1473
1474 #[cfg(feature = "cluster")]
1475 #[tokio::test]
1476 async fn cluster_runtime_rejects_namespace_proof_from_another_process() {
1477 let proved_controller = test_cluster_controller();
1478 let verified_namespaces = test_verified_cluster_namespaces(
1479 proved_controller.as_ref(),
1480 test_cluster_checkpoint_store(),
1481 )
1482 .await;
1483 let runtime_controller = test_cluster_controller();
1484
1485 let error = LaminarDbBuilder::new()
1486 .cluster_controller(runtime_controller)
1487 .verified_cluster_namespaces(verified_namespaces)
1488 .build()
1489 .await
1490 .expect_err("a namespace proof must remain bound to its process incarnation");
1491 assert!(error.to_string().contains("[LDB-0011]"), "{error}");
1492 assert!(error.to_string().contains("belong to node"), "{error}");
1493 }
1494
1495 #[cfg(feature = "cluster")]
1496 #[tokio::test]
1497 async fn cluster_runtime_rejects_in_process_state_backend() {
1498 let controller = test_cluster_controller();
1499 let verified_namespaces =
1500 test_verified_cluster_namespaces(controller.as_ref(), test_cluster_checkpoint_store())
1501 .await;
1502 let error = LaminarDbBuilder::new()
1503 .cluster_controller(controller)
1504 .verified_cluster_namespaces(verified_namespaces)
1505 .state_backend(Arc::new(laminar_core::state::InProcessBackend::new(1)))
1506 .vnode_registry(Arc::new(laminar_core::state::VnodeRegistry::new(1)))
1507 .build()
1508 .await
1509 .expect_err("volatile state must not enter cluster runtime");
1510 assert!(error.to_string().contains("[LDB-0011]"), "{error}");
1511 assert!(error.to_string().contains("ClusterShared"), "{error}");
1512 }
1513
1514 #[cfg(feature = "cluster")]
1515 #[tokio::test]
1516 async fn cluster_runtime_rejects_different_state_object_store() {
1517 let controller = test_cluster_controller();
1518 let verified_namespaces =
1519 test_verified_cluster_namespaces(controller.as_ref(), test_cluster_checkpoint_store())
1520 .await;
1521 let wrong_backend: Arc<dyn laminar_core::state::StateBackend> =
1522 Arc::new(laminar_core::state::ObjectStoreBackend::cluster_shared(
1523 test_cluster_checkpoint_store(),
1524 "wrong-state-store",
1525 1,
1526 ));
1527 let error = LaminarDbBuilder::new()
1528 .cluster_controller(controller)
1529 .verified_cluster_namespaces(verified_namespaces)
1530 .state_backend(wrong_backend)
1531 .vnode_registry(Arc::new(laminar_core::state::VnodeRegistry::new(1)))
1532 .build()
1533 .await
1534 .expect_err("a different ClusterShared handle must not bypass namespace proof");
1535 assert!(error.to_string().contains("[LDB-0011]"), "{error}");
1536 assert!(
1537 error.to_string().contains("exact object-store handle"),
1538 "{error}"
1539 );
1540 }
1541
1542 #[cfg(feature = "cluster")]
1543 #[tokio::test]
1544 async fn explicit_local_profile_rejects_cluster_controller() {
1545 let error = LaminarDbBuilder::new()
1546 .profile(Profile::BareMetal)
1547 .cluster_controller(test_cluster_controller())
1548 .build()
1549 .await
1550 .expect_err("explicit local profile and cluster wiring must not disagree");
1551 assert!(error.to_string().contains("cannot be combined"), "{error}");
1552 }
1553
1554 #[cfg(feature = "cluster")]
1555 #[tokio::test]
1556 async fn explicit_cluster_best_effort_is_rejected() {
1557 let error = LaminarDbBuilder::new()
1558 .cluster_controller(test_cluster_controller())
1559 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1560 .delivery_guarantee(laminar_connectors::connector::DeliveryGuarantee::BestEffort)
1561 .build()
1562 .await
1563 .expect_err("explicit cluster best-effort must fail closed");
1564 assert!(
1565 error.to_string().contains("requires at_least_once"),
1566 "{error}"
1567 );
1568 }
1569
1570 #[cfg(feature = "cluster")]
1571 #[tokio::test]
1572 async fn cluster_only_handle_without_controller_is_rejected() {
1573 let error = LaminarDbBuilder::new()
1574 .shuffle_sender(Arc::new(laminar_core::shuffle::ShuffleSender::new(
1575 1,
1576 uuid::Uuid::from_u128(1),
1577 )))
1578 .build()
1579 .await
1580 .expect_err("partial cluster wiring must not create a local runtime");
1581 assert!(
1582 error.to_string().contains("require a cluster controller"),
1583 "{error}"
1584 );
1585 }
1586
1587 #[cfg(feature = "cluster")]
1588 #[tokio::test]
1589 async fn cluster_checkpoint_store_without_controller_is_rejected() {
1590 let error = LaminarDbBuilder::new()
1591 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1592 .build()
1593 .await
1594 .expect_err("shared cluster checkpoint wiring must select a controller authority");
1595 assert!(
1596 error.to_string().contains("require a cluster controller"),
1597 "{error}"
1598 );
1599 }
1600
1601 #[cfg(feature = "cluster")]
1602 #[tokio::test]
1603 async fn cluster_checkpoint_store_conflicts_with_url() {
1604 let error = LaminarDbBuilder::new()
1605 .cluster_controller(test_cluster_controller())
1606 .cluster_checkpoint_object_store(test_cluster_checkpoint_store())
1607 .object_store_url("memory://checkpoint-test")
1608 .build()
1609 .await
1610 .expect_err("two checkpoint namespace authorities must be rejected");
1611 assert!(error.to_string().contains("conflict"), "{error}");
1612 }
1613
1614 #[cfg(feature = "cluster")]
1615 #[tokio::test]
1616 async fn cluster_exactly_once_build_is_rejected_before_runtime_start() {
1617 use laminar_connectors::connector::DeliveryGuarantee;
1618
1619 let error = LaminarDbBuilder::new()
1620 .profile(Profile::Cluster)
1621 .object_store_url("memory://checkpoint-test")
1622 .cluster_controller(test_cluster_controller())
1623 .delivery_guarantee(DeliveryGuarantee::ExactlyOnce)
1624 .build()
1625 .await
1626 .expect_err("cluster EO must fail at database admission");
1627 assert!(error.to_string().contains("[LDB-0013]"), "{error}");
1628 }
1629
1630 #[tokio::test]
1631 async fn injected_vnode_registry_must_fit_checkpoint_abi() {
1632 let vnode_count = laminar_core::state::MAX_KEY_GROUP_COUNT + 1;
1633 let error = LaminarDbBuilder::new()
1634 .state_backend(Arc::new(laminar_core::state::InProcessBackend::new(1)))
1635 .vnode_registry(Arc::new(laminar_core::state::VnodeRegistry::new(
1636 vnode_count,
1637 )))
1638 .build()
1639 .await
1640 .expect_err("oversized injected vnode registry must be rejected");
1641 assert!(
1642 error
1643 .to_string()
1644 .contains("vnode_registry count must be between 1"),
1645 "{error}"
1646 );
1647 }
1648
1649 #[tokio::test]
1650 async fn injected_state_backend_capacity_must_fit_checkpoint_abi() {
1651 let error = LaminarDbBuilder::new()
1652 .state_backend(Arc::new(laminar_core::state::InProcessBackend::new(0)))
1653 .vnode_registry(Arc::new(laminar_core::state::VnodeRegistry::new(1)))
1654 .build()
1655 .await
1656 .expect_err("invalid backend topology must be rejected at admission");
1657 assert!(
1658 error
1659 .to_string()
1660 .contains("state_backend key-group capacity must be between 1"),
1661 "{error}"
1662 );
1663 }
1664
1665 #[tokio::test]
1666 async fn state_backend_capacity_must_match_registry() {
1667 let error = LaminarDbBuilder::new()
1668 .state_backend(Arc::new(laminar_core::state::InProcessBackend::new(2)))
1669 .vnode_registry(Arc::new(laminar_core::state::VnodeRegistry::new(1)))
1670 .build()
1671 .await
1672 .expect_err("backend and registry must describe one topology");
1673 assert!(
1674 error.to_string().contains(
1675 "state_backend key-group capacity 2 does not match vnode_registry count 1"
1676 ),
1677 "{error}"
1678 );
1679 }
1680
1681 #[tokio::test]
1682 async fn local_runtime_rejects_multi_key_group_registry() {
1683 let error = LaminarDbBuilder::new()
1684 .state_backend(Arc::new(laminar_core::state::InProcessBackend::new(2)))
1685 .vnode_registry(Arc::new(laminar_core::state::VnodeRegistry::new(2)))
1686 .build()
1687 .await
1688 .expect_err("local runtime must not expose cluster rescaling dimensions");
1689 assert!(
1690 error.to_string().contains("requires exactly 1 key group"),
1691 "{error}"
1692 );
1693 }
1694
1695 #[tokio::test]
1696 async fn test_valid_shed_oldest_builds() {
1697 use crate::config::BackpressurePolicy;
1698 let db = LaminarDbBuilder::new()
1699 .pipeline_backpressure_policy(BackpressurePolicy::ShedOldest)
1700 .pipeline_max_input_buf_batches(64)
1701 .build()
1702 .await
1703 .unwrap();
1704 assert!(!db.is_closed());
1705 }
1706
1707 #[tokio::test]
1708 async fn test_builder_with_config_vars() {
1709 let db = LaminarDbBuilder::new()
1710 .config_var("KAFKA_BROKERS", "localhost:9092")
1711 .config_var("GROUP_ID", "test-group")
1712 .build()
1713 .await
1714 .unwrap();
1715 assert!(!db.is_closed());
1716 }
1717
1718 #[tokio::test]
1719 async fn test_builder_with_options() {
1720 let db = LaminarDbBuilder::new()
1721 .buffer_size(131_072)
1722 .build()
1723 .await
1724 .unwrap();
1725 assert!(!db.is_closed());
1726 }
1727
1728 #[tokio::test]
1729 async fn test_builder_from_laminardb() {
1730 let db = LaminarDB::builder().build().await.unwrap();
1731 assert!(!db.is_closed());
1732 }
1733
1734 #[test]
1735 fn test_builder_debug() {
1736 let builder = LaminarDbBuilder::new().config_var("K", "V");
1737 let debug = format!("{builder:?}");
1738 assert!(debug.contains("LaminarDbBuilder"));
1739 assert!(debug.contains("config_vars_count: 1"));
1740 }
1741
1742 #[tokio::test]
1743 async fn test_builder_register_udf() {
1744 use std::any::Any;
1745 use std::hash::{Hash, Hasher};
1746
1747 use arrow::datatypes::DataType;
1748 use datafusion_expr::{
1749 ColumnarValue, ScalarFunctionArgs, ScalarUDFImpl, Signature, TypeSignature, Volatility,
1750 };
1751
1752 #[derive(Debug)]
1754 struct FortyTwo {
1755 signature: Signature,
1756 }
1757
1758 impl FortyTwo {
1759 fn new() -> Self {
1760 Self {
1761 signature: Signature::new(TypeSignature::Nullary, Volatility::Immutable),
1762 }
1763 }
1764 }
1765
1766 impl PartialEq for FortyTwo {
1767 fn eq(&self, _: &Self) -> bool {
1768 true
1769 }
1770 }
1771
1772 impl Eq for FortyTwo {}
1773
1774 impl Hash for FortyTwo {
1775 fn hash<H: Hasher>(&self, state: &mut H) {
1776 "forty_two".hash(state);
1777 }
1778 }
1779
1780 impl ScalarUDFImpl for FortyTwo {
1781 fn as_any(&self) -> &dyn Any {
1782 self
1783 }
1784 fn name(&self) -> &'static str {
1785 "forty_two"
1786 }
1787 fn signature(&self) -> &Signature {
1788 &self.signature
1789 }
1790 fn return_type(&self, _: &[DataType]) -> datafusion_common::Result<DataType> {
1791 Ok(DataType::Int64)
1792 }
1793 fn invoke_with_args(
1794 &self,
1795 _args: ScalarFunctionArgs,
1796 ) -> datafusion_common::Result<ColumnarValue> {
1797 Ok(ColumnarValue::Scalar(
1798 datafusion_common::ScalarValue::Int64(Some(42)),
1799 ))
1800 }
1801 }
1802
1803 let udf = ScalarUDF::new_from_impl(FortyTwo::new());
1804 let db = LaminarDB::builder()
1805 .register_udf(udf)
1806 .build()
1807 .await
1808 .unwrap();
1809
1810 let result = db.execute("SELECT forty_two()").await;
1812 assert!(result.is_ok(), "UDF should be callable: {result:?}");
1813 }
1814}