1use std::sync::Arc;
6
7use crate::db::{DbState, LaminarDB};
8use crate::error::DbError;
9
10impl LaminarDB {
11 #[must_use]
13 pub fn uptime(&self) -> std::time::Duration {
14 self.start_time.elapsed()
15 }
16
17 pub fn set_engine_metrics(&self, metrics: Arc<crate::engine_metrics::EngineMetrics>) {
22 metrics
23 .flow
24 .observe_memory_budgets(&crate::memory_budget::node_budgets(
25 &self.config,
26 self.runtime_mode(),
27 ));
28 #[cfg(feature = "cluster")]
29 {
30 let sender = self.shuffle_sender.lock().clone();
31 let receiver = self.shuffle_receiver.lock().clone();
32 if let (Some(sender), Some(receiver)) = (sender, receiver) {
33 metrics.flow.shuffle.observe(&sender, &receiver);
34 }
35 }
36 *self.engine_metrics.lock() = Some(metrics);
37 }
38
39 pub fn set_prometheus_registry(
50 &self,
51 registry: Arc<prometheus::Registry>,
52 ) -> Result<(), DbError> {
53 let _startup = self.startup_attempt.lock();
54 let mut slot = self.prometheus_registry.lock();
55 let state = DbState::load(&self.state);
56 if state != DbState::Created {
57 return Err(DbError::InvalidOperation(format!(
58 "Prometheus registry can only be installed while the database is Created, not {state:?}"
59 )));
60 }
61 if slot.is_some() {
62 return Err(DbError::InvalidOperation(
63 "Prometheus registry is already installed".into(),
64 ));
65 }
66 self.connector_registry
67 .register_schema_resolution_metrics(®istry)?;
68 *slot = Some(registry);
69 Ok(())
70 }
71
72 #[must_use]
74 pub fn engine_metrics(&self) -> Option<Arc<crate::engine_metrics::EngineMetrics>> {
75 self.engine_metrics.lock().clone()
76 }
77
78 #[cfg(feature = "cluster")]
80 #[must_use]
81 pub fn cluster_subscription_output_health(
82 &self,
83 ) -> Option<crate::metrics::ClusterSubscriptionOutputHealth> {
84 let metrics = self.engine_metrics.lock();
85 let metrics = &metrics.as_ref()?.cluster_subscription;
86 Some(crate::metrics::ClusterSubscriptionOutputHealth {
87 active_readers: nonnegative_gauge(metrics.active_readers.get()),
88 pending_bytes: nonnegative_gauge(metrics.pending_bytes.get()),
89 retained_bytes: nonnegative_gauge(metrics.retained_bytes.get()),
90 orphan_bytes: nonnegative_gauge(metrics.orphan_bytes.get()),
91 open_failures: metrics.open_failures_total.get(),
92 segment_write_failures: metrics.segment_write_failures_total.get(),
93 manifest_failures: metrics.manifest_failures_total.get(),
94 integrity_failures: metrics.integrity_failures_total.get(),
95 stale_writer_rejections: metrics.stale_writer_rejections_total.get(),
96 sequence_gaps: metrics.sequence_gaps_total.get(),
97 lag_disconnects: metrics.gateway_lag_disconnects_total.get(),
98 })
99 }
100
101 #[cfg(feature = "cluster")]
114 pub fn checkpoint_barrier_timing_snapshot(
115 &self,
116 expected_process: Option<laminar_core::cluster::control::LocalProcessAuthorityIdentity>,
117 after_sequence: u64,
118 limit: usize,
119 ) -> Result<
120 crate::checkpoint_timing::CheckpointBarrierTimingPage,
121 crate::checkpoint_timing::CheckpointBarrierTimingReadError,
122 > {
123 use crate::checkpoint_timing::{
124 CheckpointBarrierTimingPage, CheckpointBarrierTimingReadError,
125 };
126
127 if after_sequence != 0 && expected_process.is_none() {
128 return Err(CheckpointBarrierTimingReadError::ProcessIdentityRequired);
129 }
130 let controller = self
131 .cluster_controller
132 .try_lock()
133 .and_then(|controller| controller.clone())
134 .ok_or(CheckpointBarrierTimingReadError::ProcessIdentityUnavailable)?;
135 let before = controller
136 .try_live_local_process_authority_identity()
137 .map_err(|_| CheckpointBarrierTimingReadError::ProcessIdentityUnavailable)?;
138 if let Some(expected) = expected_process {
139 if expected != before {
140 return Err(CheckpointBarrierTimingReadError::ProcessIdentityMismatch {
141 expected,
142 actual: before,
143 });
144 }
145 }
146
147 let snapshot = self
148 .checkpoint_barrier_timings
149 .snapshot_after(after_sequence, limit)?;
150 let after = controller
151 .try_live_local_process_authority_identity()
152 .map_err(|_| CheckpointBarrierTimingReadError::ProcessIdentityUnavailable)?;
153 if before != after {
154 return Err(CheckpointBarrierTimingReadError::ProcessIdentityChanged { before, after });
155 }
156 if let Some(actual) = snapshot.process {
157 if actual != before {
158 return Err(CheckpointBarrierTimingReadError::LedgerProcessMismatch {
159 expected: before,
160 actual,
161 });
162 }
163 }
164 debug_assert!(snapshot
165 .records
166 .iter()
167 .all(|record| record.process == before));
168 Ok(CheckpointBarrierTimingPage {
169 process: before,
170 snapshot,
171 })
172 }
173
174 pub fn pipeline_state(&self) -> &'static str {
176 match DbState::load(&self.state) {
177 DbState::Created => "Created",
178 DbState::Starting => "Starting",
179 DbState::Running => "Running",
180 DbState::ShuttingDown => "ShuttingDown",
181 DbState::Stopped => "Stopped",
182 DbState::Faulted => "Faulted",
183 }
184 }
185
186 #[must_use]
189 pub fn last_fault(&self) -> Option<String> {
190 self.last_fault.lock().clone()
191 }
192
193 #[must_use]
198 #[allow(clippy::cast_sign_loss)]
199 pub fn metrics(&self) -> crate::metrics::PipelineMetrics {
200 let guard = self.engine_metrics.lock();
201 let (ingested, emitted, dropped, cycles, batches, mv_updates, mv_bytes) =
202 if let Some(ref m) = *guard {
203 (
204 m.events_ingested.get(),
205 m.events_emitted.get(),
206 m.events_dropped.get(),
207 m.cycles.get(),
208 m.batches.get(),
209 m.views.updates.get(),
210 m.views.bytes_stored.get() as u64,
211 )
212 } else {
213 (0, 0, 0, 0, 0, 0, 0)
214 };
215 crate::metrics::PipelineMetrics {
216 total_events_ingested: ingested,
217 total_events_emitted: emitted,
218 total_events_dropped: dropped,
219 total_cycles: cycles,
220 total_batches: batches,
221 uptime: self.start_time.elapsed(),
222 state: self.pipeline_state_enum(),
223 source_count: self.catalog.list_sources().len(),
224 stream_count: self.catalog.list_streams().len(),
225 sink_count: self.catalog.list_sinks().len(),
226 pipeline_watermark: self.pipeline_watermark(),
227 mv_updates,
228 mv_bytes_stored: mv_bytes,
229 }
230 }
231
232 #[must_use]
234 pub fn source_metrics(&self, name: &str) -> Option<crate::metrics::SourceMetrics> {
235 let entry = self.catalog.get_source(name)?;
236 let pending = entry.source.pending();
237 let capacity = entry.source.capacity();
238 Some(crate::metrics::SourceMetrics {
239 name: entry.name.clone(),
240 total_events: entry.source.sequence(),
241 pending,
242 capacity,
243 is_backpressured: entry.is_backpressured(),
244 watermark: entry.source.current_watermark(),
245 utilization: crate::metrics::utilization(pending, capacity),
246 })
247 }
248
249 #[must_use]
251 pub fn all_source_metrics(&self) -> Vec<crate::metrics::SourceMetrics> {
252 self.catalog
253 .list_sources()
254 .iter()
255 .filter_map(|name| self.source_metrics(name))
256 .collect()
257 }
258
259 #[must_use]
261 pub fn stream_metrics(&self, name: &str) -> Option<crate::metrics::StreamMetrics> {
262 let entry = self.catalog.get_stream_entry(name)?;
263 let sql = self
264 .connector_manager
265 .lock()
266 .streams()
267 .get(name)
268 .map(|reg| reg.query_sql.clone());
269 Some(crate::metrics::StreamMetrics {
270 name: entry.name.clone(),
271 total_events: entry.emitted_rows(),
272 sql,
273 })
274 }
275
276 #[must_use]
278 pub fn all_stream_metrics(&self) -> Vec<crate::metrics::StreamMetrics> {
279 self.catalog
280 .list_streams()
281 .iter()
282 .filter_map(|name| self.stream_metrics(name))
283 .collect()
284 }
285
286 #[must_use]
288 pub fn total_events_processed(&self) -> u64 {
289 let guard = self.engine_metrics.lock();
290 if let Some(ref m) = *guard {
291 m.events_ingested.get() + m.events_emitted.get()
292 } else {
293 0
294 }
295 }
296
297 #[must_use]
302 pub fn pipeline_watermark(&self) -> i64 {
303 self.pipeline_watermark
304 .load(std::sync::atomic::Ordering::Relaxed)
305 }
306
307 pub(crate) fn pipeline_state_enum(&self) -> crate::metrics::PipelineState {
308 match DbState::load(&self.state) {
309 DbState::Created => crate::metrics::PipelineState::Created,
310 DbState::Starting => crate::metrics::PipelineState::Starting,
311 DbState::Running => crate::metrics::PipelineState::Running,
312 DbState::ShuttingDown => crate::metrics::PipelineState::ShuttingDown,
313 DbState::Stopped => crate::metrics::PipelineState::Stopped,
314 DbState::Faulted => crate::metrics::PipelineState::Faulted,
315 }
316 }
317
318 pub fn cancel_query(&self, query_id: u64) -> Result<(), DbError> {
327 if self.catalog.deactivate_query(query_id) {
328 Ok(())
329 } else {
330 Err(DbError::QueryNotFound(query_id.to_string()))
331 }
332 }
333
334 pub fn source_count(&self) -> usize {
336 self.catalog.list_sources().len()
337 }
338
339 pub fn sink_count(&self) -> usize {
341 self.catalog.list_sinks().len()
342 }
343
344 pub fn checkpoint_stats_nonblocking(
349 &self,
350 ) -> Option<crate::checkpoint_coordinator::CheckpointStats> {
351 let guard = self.coordinator.try_lock().ok()?;
352 guard
353 .as_ref()
354 .map(crate::checkpoint_coordinator::CheckpointCoordinator::stats)
355 }
356
357 pub fn active_query_count(&self) -> usize {
359 self.catalog
360 .list_queries()
361 .iter()
362 .filter(|(_, _, active)| *active)
363 .count()
364 }
365}
366
367#[cfg(feature = "cluster")]
368fn nonnegative_gauge(value: i64) -> u64 {
369 u64::try_from(value).unwrap_or(0)
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[cfg(feature = "cluster")]
377 async fn cluster_db_with_live_process() -> (
378 Arc<LaminarDB>,
379 laminar_core::cluster::control::LocalProcessAuthorityIdentity,
380 ) {
381 use laminar_core::cluster::control::{
382 ClusterController, ClusterKv, LeaseDeadline, ProcessLeaseAuthority, ProcessLeaseOutcome,
383 };
384 use laminar_core::cluster::discovery::{NodeId, NodeInfo};
385
386 let node = NodeId(71);
387 let boot = uuid::Uuid::from_u128(71);
388 let kv: Arc<dyn ClusterKv> =
389 Arc::new(laminar_core::cluster::control::InMemoryKv::new(node));
390 let (_members_tx, members_rx) = tokio::sync::watch::channel(Vec::<NodeInfo>::new());
391 let controller = Arc::new(ClusterController::new_with_recovery_incarnation(
392 node,
393 Arc::clone(&kv),
394 kv,
395 None,
396 members_rx,
397 boot,
398 ));
399 controller
400 .set_process_lease_deadline(Arc::new(LeaseDeadline::live_for(
401 std::time::Duration::from_secs(30),
402 )))
403 .unwrap();
404 let authority = Arc::new(
405 ProcessLeaseAuthority::new(
406 Arc::new(object_store::memory::InMemory::new()),
407 std::time::Duration::from_secs(30),
408 )
409 .unwrap(),
410 );
411 let ProcessLeaseOutcome::Acquired(lease) = authority
412 .store_for(node)
413 .try_acquire(boot, 0)
414 .await
415 .unwrap()
416 else {
417 panic!("empty process authority must grant the test process");
418 };
419 controller.set_process_lease_authority(authority).unwrap();
420 controller
421 .publish_leased_recovery_incarnation(&lease)
422 .await
423 .unwrap();
424 let process = controller
425 .try_live_local_process_authority_identity()
426 .unwrap();
427
428 let db = Arc::new(
429 LaminarDB::open_with_config_and_vars_and_rules(
430 crate::config::LaminarConfig::default(),
431 std::collections::HashMap::new(),
432 &[],
433 None,
434 crate::db::RuntimeMode::Cluster,
435 )
436 .unwrap(),
437 );
438 db.set_cluster_controller(controller).unwrap();
439 (db, process)
440 }
441
442 #[cfg(feature = "cluster")]
443 fn timing_observation(
444 process: laminar_core::cluster::control::LocalProcessAuthorityIdentity,
445 checkpoint_id: u64,
446 ) -> crate::checkpoint_timing::CheckpointBarrierTimingObservation {
447 crate::checkpoint_timing::CheckpointBarrierTimingObservation {
448 process,
449 attempt: laminar_core::checkpoint::CheckpointAttempt::canonical(checkpoint_id),
450 role: crate::checkpoint_timing::CheckpointBarrierRole::Follower,
451 assignment_version: 3,
452 assignment_digest: [9; 32],
453 pipeline_stall_ns: 100,
454 local_barrier_ns: 60,
455 aligned_resume_ns: Some(20),
456 durable_tail_handoff: true,
457 deadline_exhausted: false,
458 }
459 }
460
461 #[test]
462 fn prometheus_registry_is_single_assignment() {
463 let db = LaminarDB::open().unwrap();
464 db.set_prometheus_registry(Arc::new(prometheus::Registry::new()))
465 .unwrap();
466
467 let error = db
468 .set_prometheus_registry(Arc::new(prometheus::Registry::new()))
469 .unwrap_err()
470 .to_string();
471
472 assert!(error.contains("already installed"), "{error}");
473 }
474
475 #[test]
476 fn prometheus_registry_cannot_change_after_start_begins() {
477 let db = LaminarDB::open().unwrap();
478 DbState::Starting.store(&db.state);
479
480 let error = db
481 .set_prometheus_registry(Arc::new(prometheus::Registry::new()))
482 .unwrap_err()
483 .to_string();
484
485 DbState::Created.store(&db.state);
486 assert!(error.contains("Created"), "{error}");
487 }
488
489 #[test]
490 fn prometheus_registry_install_is_serialized_with_startup_claim() {
491 let db = Arc::new(LaminarDB::open().unwrap());
492 let startup = db.startup_attempt.lock();
493 let (started_tx, started_rx) = std::sync::mpsc::sync_channel(1);
494 let installing = Arc::clone(&db);
495 let install = std::thread::spawn(move || {
496 started_tx.send(()).unwrap();
497 installing.set_prometheus_registry(Arc::new(prometheus::Registry::new()))
498 });
499 started_rx.recv().unwrap();
500 DbState::Starting.store(&db.state);
501 drop(startup);
502
503 let error = install.join().unwrap().unwrap_err().to_string();
504
505 DbState::Created.store(&db.state);
506 assert!(error.contains("Created"), "{error}");
507 }
508
509 #[test]
510 fn attached_metrics_report_the_database_memory_budgets() {
511 let db = LaminarDB::open_with_config(crate::LaminarConfig {
512 source_queue_max_bytes: 4096,
513 ..crate::LaminarConfig::default()
514 })
515 .unwrap();
516 let registry = prometheus::Registry::new();
517 db.set_engine_metrics(Arc::new(crate::engine_metrics::EngineMetrics::new(
518 ®istry,
519 )));
520
521 let budgets = registry
522 .gather()
523 .into_iter()
524 .find(|family| family.name() == "memory_budget_bytes")
525 .unwrap();
526 let budget = |area: &str| {
527 budgets
528 .get_metric()
529 .iter()
530 .find(|metric| metric.get_label()[0].value() == area)
531 .map(|metric| metric.get_gauge().value())
532 };
533 assert_eq!(budget("source_queue"), Some(4096.0));
534 assert_eq!(budget("managed_state"), Some(256.0 * 1024.0 * 1024.0));
535 assert_eq!(budget("subscription_logs"), Some(256.0 * 1024.0 * 1024.0));
536 assert_eq!(budget("shuffle_outbound"), None);
537 }
538
539 #[cfg(feature = "cluster")]
540 #[test]
541 fn cluster_subscription_health_is_aggregated_without_identity_labels() {
542 let db = LaminarDB::open().unwrap();
543 let registry = prometheus::Registry::new();
544 let metrics = Arc::new(crate::engine_metrics::EngineMetrics::new(®istry));
545 metrics.cluster_subscription.active_readers.set(2);
546 metrics.cluster_subscription.pending_bytes.set(128);
547 metrics.cluster_subscription.integrity_failures_total.inc();
548 db.set_engine_metrics(metrics);
549
550 let health = db.cluster_subscription_output_health().unwrap();
551 assert_eq!(health.active_readers, 2);
552 assert_eq!(health.pending_bytes, 128);
553 assert_eq!(health.integrity_failures, 1);
554 assert_eq!(health.retained_bytes, 0);
555 }
556
557 #[cfg(feature = "cluster")]
558 #[tokio::test]
559 async fn checkpoint_timing_cursor_is_bound_to_the_live_process() {
560 use crate::checkpoint_timing::CheckpointBarrierTimingReadError;
561
562 let (db, process) = cluster_db_with_live_process().await;
563 assert!(db
564 .checkpoint_barrier_timings
565 .try_record(timing_observation(process, 1)));
566 assert!(db
567 .checkpoint_barrier_timings
568 .try_record(timing_observation(process, 2)));
569
570 assert_eq!(
571 db.checkpoint_barrier_timing_snapshot(None, 1, 1),
572 Err(CheckpointBarrierTimingReadError::ProcessIdentityRequired)
573 );
574 let mut stale_process = process;
575 stale_process.process_term += 1;
576 assert_eq!(
577 db.checkpoint_barrier_timing_snapshot(Some(stale_process), 1, 1),
578 Err(CheckpointBarrierTimingReadError::ProcessIdentityMismatch {
579 expected: stale_process,
580 actual: process,
581 })
582 );
583
584 let first = db.checkpoint_barrier_timing_snapshot(None, 0, 1).unwrap();
585 assert_eq!(first.process, process);
586 assert_eq!(first.snapshot.process, Some(process));
587 assert_eq!(first.snapshot.records[0].sequence, 1);
588 let second = db
589 .checkpoint_barrier_timing_snapshot(Some(process), 1, 1)
590 .unwrap();
591 assert_eq!(second.snapshot.records[0].sequence, 2);
592 }
593
594 #[cfg(feature = "cluster")]
595 #[tokio::test]
596 async fn checkpoint_timing_read_rejects_a_foreign_ledger_domain() {
597 use crate::checkpoint_timing::CheckpointBarrierTimingReadError;
598
599 let (db, process) = cluster_db_with_live_process().await;
600 let mut foreign = process;
601 foreign.process_term += 1;
602 assert!(db
603 .checkpoint_barrier_timings
604 .try_record(timing_observation(foreign, 1)));
605
606 assert_eq!(
607 db.checkpoint_barrier_timing_snapshot(None, 0, 1),
608 Err(CheckpointBarrierTimingReadError::LedgerProcessMismatch {
609 expected: process,
610 actual: foreign,
611 })
612 );
613 }
614}