1#[path = "diagnostics/disk_guard.rs"]
64mod disk_guard;
65#[path = "diagnostics/writer_contention.rs"]
66mod writer_contention;
67
68use std::path::{Path, PathBuf};
69use std::sync::atomic::{AtomicBool, Ordering};
70use std::sync::Arc;
71use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
72
73use khive_storage::error::StorageError;
74use khive_storage::types::StorageResult;
75use khive_storage::StorageCapability;
76use rusqlite::Connection;
77use serde::Serialize;
78
79use crate::checkpoint;
80use crate::pool::{ConnectionPool, WalCeilingSource};
81
82pub use disk_guard::DiskGuardDiagnostics;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
92pub struct CheckpointProbe {
93 pub busy: i64,
94 pub log_frames: i64,
95 pub checkpointed_frames: i64,
96}
97
98impl CheckpointProbe {
99 pub fn backfill_gap_frames(&self) -> i64 {
103 if self.log_frames < 0 || self.checkpointed_frames < 0 {
104 return 0;
105 }
106 self.log_frames
107 .saturating_sub(self.checkpointed_frames)
108 .max(0)
109 }
110}
111
112pub fn checkpoint_probe(conn: &Connection) -> rusqlite::Result<CheckpointProbe> {
126 conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |row| {
127 Ok(CheckpointProbe {
128 busy: row.get(0)?,
129 log_frames: row.get(1)?,
130 checkpointed_frames: row.get(2)?,
131 })
132 })
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
141pub struct CheckpointCounters {
142 pub last_observed_wal_pages: Option<u64>,
143 pub truncate_attempts: u64,
144 pub truncate_consecutive_failures: u64,
145 pub checkpoint_skipped_ticks: u64,
146 pub checkpoint_consecutive_skips: u64,
147 pub checkpoint_last_skip_wal_pages: Option<u64>,
148 pub checkpoint_pressure_elevated_ticks: u64,
149 pub checkpoint_pressure_episodes_started: u64,
150 pub checkpoint_pressure_episodes_recovered: u64,
151 pub checkpoint_lifecycle_append_attempts: u64,
152 pub checkpoint_lifecycle_append_failures: u64,
153 pub checkpoint_lifecycle_enqueue_drops: u64,
154 pub read_tx_max_age_evictions: u64,
158}
159
160pub fn checkpoint_counters() -> CheckpointCounters {
162 CheckpointCounters {
163 last_observed_wal_pages: checkpoint::last_observed_wal_pages(),
164 truncate_attempts: checkpoint::truncate_attempts(),
165 truncate_consecutive_failures: checkpoint::truncate_consecutive_failures(),
166 checkpoint_skipped_ticks: checkpoint::checkpoint_skipped_ticks(),
167 checkpoint_consecutive_skips: checkpoint::checkpoint_consecutive_skips(),
168 checkpoint_last_skip_wal_pages: checkpoint::checkpoint_last_skip_wal_pages(),
169 checkpoint_pressure_elevated_ticks: checkpoint::checkpoint_pressure_elevated_ticks(),
170 checkpoint_pressure_episodes_started: checkpoint::checkpoint_pressure_episodes_started(),
171 checkpoint_pressure_episodes_recovered: checkpoint::checkpoint_pressure_episodes_recovered(
172 ),
173 checkpoint_lifecycle_append_attempts: checkpoint::checkpoint_lifecycle_append_attempts(),
174 checkpoint_lifecycle_append_failures: checkpoint::checkpoint_lifecycle_append_failures(),
175 checkpoint_lifecycle_enqueue_drops: checkpoint::checkpoint_lifecycle_enqueue_drops(),
176 read_tx_max_age_evictions: checkpoint::read_tx_max_age_evictions(),
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
187pub struct BuildIdentity {
188 pub version: String,
189 pub build_hash: Option<String>,
190}
191
192impl BuildIdentity {
193 pub fn from_env(version: &str, build_hash: Option<&str>) -> Self {
195 Self {
196 version: version.to_string(),
197 build_hash: build_hash.map(str::to_string),
198 }
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
204pub struct ProcessIdentity {
205 pub pid: u32,
206 pub started_at: Option<i64>,
209 pub started_at_unavailable_reason: Option<String>,
212 pub pool_generation: u64,
213}
214
215impl ProcessIdentity {
216 pub fn current(pool: &ConnectionPool) -> Self {
217 let pid = std::process::id();
218 Self::from_start_time(
219 pid,
220 crate::walpin::process_start_time_secs(pid),
221 pool.main_pool_generation(),
222 )
223 }
224
225 fn from_start_time(pid: u32, started_at: Option<i64>, pool_generation: u64) -> Self {
226 Self {
227 pid,
228 started_at,
229 started_at_unavailable_reason: started_at.is_none().then(|| {
230 format!(
231 "OS process start time is unsupported or unavailable on {}",
232 std::env::consts::OS
233 )
234 }),
235 pool_generation,
236 }
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
242pub struct WalFileState {
243 pub wal_path: String,
244 pub wal_size_bytes: Option<u64>,
247 pub unavailable_reason: Option<String>,
248}
249
250pub fn wal_file_state(db_path: &Path) -> WalFileState {
252 let wal_path = wal_sidecar_path(db_path);
253 match std::fs::metadata(&wal_path) {
254 Ok(md) => WalFileState {
255 wal_path: wal_path.display().to_string(),
256 wal_size_bytes: Some(md.len()),
257 unavailable_reason: None,
258 },
259 Err(e) => WalFileState {
260 wal_path: wal_path.display().to_string(),
261 wal_size_bytes: None,
262 unavailable_reason: Some(e.to_string()),
263 },
264 }
265}
266
267fn wal_sidecar_path(db_path: &Path) -> PathBuf {
270 let mut s = db_path.as_os_str().to_os_string();
271 s.push("-wal");
272 PathBuf::from(s)
273}
274
275#[derive(Debug, Clone, PartialEq, Serialize)]
284pub struct WalPinAttribution {
285 pub status: WalPinAttributionStatus,
287 pub status_reasons: Vec<String>,
289 pub census: WalPinCensus,
291 pub reporting_pid: u32,
293 pub reporting_process_is_holder: Option<bool>,
295 pub reporting_process_is_holder_unavailable_reason: Option<String>,
297 pub census_process_start_times: Vec<WalPinCensusProcessStart>,
299 pub start_time_resolution_secs: Option<u64>,
301 pub start_time_resolution_unavailable_reason: Option<String>,
303 pub available: bool,
306 pub unavailable_reason: Option<String>,
307 #[serde(skip_serializing)]
309 pub census_holder_pids: Vec<u32>,
310 #[serde(skip_serializing)]
311 pub census_uninspectable_pids: Vec<u32>,
312 #[serde(skip_serializing)]
313 pub census_truncated: bool,
314 #[serde(skip_serializing)]
315 pub census_is_complete: bool,
316 pub reporting: Vec<WalPinHolder>,
318 pub registered_silent_pids: Vec<u32>,
320 pub unknown_pids: Vec<u32>,
322 pub census_pids_without_attribution: Vec<u32>,
324 pub fully_attributed: bool,
327 pub sidecar_entries: Vec<serde_json::Value>,
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub sidecar_listing_truncated: Option<bool>,
332 #[serde(skip_serializing_if = "Option::is_none")]
335 pub sidecar_entries_cleanup_would_reap: Option<usize>,
336}
337
338#[derive(Debug, Clone, PartialEq, Serialize)]
340pub struct WalPinCensusProcessStart {
341 pub pid: u32,
343 pub process_start_time_secs: Option<i64>,
345 pub process_start_time_unavailable_reason: Option<String>,
347}
348
349fn census_process_start_times(holder_pids: &[u32]) -> Vec<WalPinCensusProcessStart> {
350 holder_pids
351 .iter()
352 .map(|pid| {
353 let process_start_time_secs = crate::walpin::process_start_time_secs(*pid);
354 let process_start_time_unavailable_reason =
355 process_start_time_secs.is_none().then(|| {
356 if crate::walpin::start_time_resolution_secs().is_none() {
357 "process start time is unavailable on this platform".to_string()
358 } else {
359 "the operating system did not report a start time for this process"
360 .to_string()
361 }
362 });
363 WalPinCensusProcessStart {
364 pid: *pid,
365 process_start_time_secs,
366 process_start_time_unavailable_reason,
367 }
368 })
369 .collect()
370}
371
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
377#[serde(rename_all = "snake_case")]
378pub enum WalPinAttributionStatus {
379 Complete,
381 Degraded,
383 Unavailable,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
395#[serde(tag = "status", rename_all = "snake_case")]
396pub enum WalPinCensus {
397 Complete {
399 holder_pids: Vec<u32>,
401 },
402 Incomplete {
404 holder_pids: Vec<u32>,
406 uninspectable_pids: Vec<u32>,
408 truncated: bool,
410 reason: String,
412 },
413 Unavailable {
415 reason: String,
417 },
418}
419
420fn census_truncation_cause(budget_exhausted: bool) -> String {
430 if budget_exhausted {
431 "the OS process walk stopped at its wall-clock budget (see collection_cost.wal_pin_census_budget_ms)"
432 .to_string()
433 } else {
434 "the OS process walk was truncated".to_string()
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Serialize)]
440pub struct WalPinHolder {
441 pub pid: u32,
442 pub process_role: String,
443 pub current_oldest_tx_age_secs: f64,
444 pub oldest_tx_label: Option<String>,
445 pub attribution_is_evidence_backed: bool,
446}
447
448impl WalPinAttribution {
449 fn unavailable(reason: impl Into<String>) -> Self {
450 let reason = reason.into();
451 let start_time_resolution_secs = crate::walpin::start_time_resolution_secs();
452 Self {
453 status: WalPinAttributionStatus::Unavailable,
454 status_reasons: vec![reason.clone()],
455 census: WalPinCensus::Unavailable {
456 reason: reason.clone(),
457 },
458 reporting_pid: crate::walpin::reporting_pid(),
459 reporting_process_is_holder: None,
460 reporting_process_is_holder_unavailable_reason: Some(
461 "the OS holder census supplied no holder evidence".to_string(),
462 ),
463 census_process_start_times: Vec::new(),
464 start_time_resolution_secs,
465 start_time_resolution_unavailable_reason: start_time_resolution_secs
466 .is_none()
467 .then(|| "process start time is unavailable on this platform".to_string()),
468 available: false,
469 unavailable_reason: Some(reason),
470 census_holder_pids: Vec::new(),
471 census_uninspectable_pids: Vec::new(),
472 census_truncated: false,
473 census_is_complete: false,
474 reporting: Vec::new(),
475 registered_silent_pids: Vec::new(),
476 unknown_pids: Vec::new(),
477 census_pids_without_attribution: Vec::new(),
478 fully_attributed: false,
479 sidecar_entries: Vec::new(),
480 sidecar_listing_truncated: None,
481 sidecar_entries_cleanup_would_reap: None,
482 }
483 }
484}
485
486#[cfg(all(unix, test))]
487fn wal_pin_attribution_from_census(census: crate::walpin::CensusResult) -> WalPinAttribution {
488 wal_pin_attribution_without_sidecar(
489 census,
490 "read-only sidecar enumeration did not run for this attribution snapshot".to_string(),
491 )
492}
493
494#[cfg(unix)]
495fn wal_pin_attribution_without_sidecar(
496 census: crate::walpin::CensusResult,
497 sidecar_reason: String,
498) -> WalPinAttribution {
499 let census_is_complete = census.is_complete();
500 let mut census_holder_pids: Vec<u32> = census.holders.iter().copied().collect();
501 census_holder_pids.sort_unstable();
502 let reporting_pid = crate::walpin::reporting_pid();
503 let reporting_process_is_holder = census_holder_pids
504 .contains(&reporting_pid)
505 .then_some(true)
506 .or_else(|| census_is_complete.then_some(false));
507 let reporting_process_is_holder_unavailable_reason =
508 reporting_process_is_holder.is_none().then(|| {
509 "the reporting process was absent from the incomplete OS holder census".to_string()
510 });
511 let census_process_start_times = census_process_start_times(&census_holder_pids);
512 let start_time_resolution_secs = crate::walpin::start_time_resolution_secs();
513 let mut census_uninspectable_pids = census.uninspectable_pids;
514 census_uninspectable_pids.sort_unstable();
515 census_uninspectable_pids.dedup();
516 let census_truncated = census.truncated;
517 let census_budget_exhausted = census.budget_exhausted;
518
519 let mut status_reasons = vec![sidecar_reason];
520 let census = if census_is_complete {
521 WalPinCensus::Complete {
522 holder_pids: census_holder_pids.clone(),
523 }
524 } else {
525 let mut causes = Vec::new();
526 if census_truncated {
527 causes.push(census_truncation_cause(census_budget_exhausted));
528 }
529 if !census_uninspectable_pids.is_empty() {
530 causes.push(format!(
531 "{} PID(s) could not be inspected",
532 census_uninspectable_pids.len()
533 ));
534 }
535 let reason = format!(
536 "OS holder census is incomplete: {}; additional database holders cannot be ruled out",
537 causes.join("; ")
538 );
539 status_reasons.push(reason.clone());
540 WalPinCensus::Incomplete {
541 holder_pids: census_holder_pids.clone(),
542 uninspectable_pids: census_uninspectable_pids.clone(),
543 truncated: census_truncated,
544 reason,
545 }
546 };
547
548 WalPinAttribution {
549 status: WalPinAttributionStatus::Degraded,
550 unavailable_reason: Some(status_reasons.join("; ")),
551 status_reasons,
552 census,
553 reporting_pid,
554 reporting_process_is_holder,
555 reporting_process_is_holder_unavailable_reason,
556 census_process_start_times,
557 start_time_resolution_secs,
558 start_time_resolution_unavailable_reason: start_time_resolution_secs
559 .is_none()
560 .then(|| "process start time is unavailable on this platform".to_string()),
561 available: false,
562 census_holder_pids,
563 census_uninspectable_pids,
564 census_truncated,
565 census_is_complete,
566 reporting: Vec::new(),
567 registered_silent_pids: Vec::new(),
568 unknown_pids: Vec::new(),
569 census_pids_without_attribution: Vec::new(),
570 fully_attributed: false,
571 sidecar_entries: Vec::new(),
572 sidecar_listing_truncated: None,
573 sidecar_entries_cleanup_would_reap: None,
574 }
575}
576
577#[cfg(unix)]
578fn wal_pin_attribution_from_evidence(
579 census: crate::walpin::CensusResult,
580 sidecar: crate::walpin::WalpinReport,
581) -> WalPinAttribution {
582 use std::collections::BTreeSet;
583
584 let census_is_complete = census.is_complete();
585 let mut census_holder_pids: Vec<u32> = census.holders.iter().copied().collect();
586 census_holder_pids.sort_unstable();
587 let reporting_pid = crate::walpin::reporting_pid();
588 let reporting_process_is_holder = census_holder_pids
589 .contains(&reporting_pid)
590 .then_some(true)
591 .or_else(|| census_is_complete.then_some(false));
592 let reporting_process_is_holder_unavailable_reason =
593 reporting_process_is_holder.is_none().then(|| {
594 "the reporting process was absent from the incomplete OS holder census".to_string()
595 });
596 let census_process_start_times = census_process_start_times(&census_holder_pids);
597 let start_time_resolution_secs = crate::walpin::start_time_resolution_secs();
598 let mut census_uninspectable_pids = census.uninspectable_pids;
599 census_uninspectable_pids.sort_unstable();
600 census_uninspectable_pids.dedup();
601 let census_truncated = census.truncated;
602 let census_budget_exhausted = census.budget_exhausted;
603 let census_carrier = if census_is_complete {
604 WalPinCensus::Complete {
605 holder_pids: census_holder_pids.clone(),
606 }
607 } else {
608 let mut causes = Vec::new();
609 if census_truncated {
610 causes.push(census_truncation_cause(census_budget_exhausted));
611 }
612 if !census_uninspectable_pids.is_empty() {
613 causes.push(format!(
614 "{} PID(s) could not be inspected",
615 census_uninspectable_pids.len()
616 ));
617 }
618 let reason = format!(
619 "OS holder census is incomplete: {}; additional database holders cannot be ruled out",
620 causes.join("; ")
621 );
622 WalPinCensus::Incomplete {
623 holder_pids: census_holder_pids.clone(),
624 uninspectable_pids: census_uninspectable_pids.clone(),
625 truncated: census_truncated,
626 reason,
627 }
628 };
629
630 let now_epoch_secs = SystemTime::now()
631 .duration_since(UNIX_EPOCH)
632 .map(|duration| duration.as_secs() as i64)
633 .unwrap_or(0);
634 let sidecar_listing_truncated = sidecar.sidecar_listing_truncated;
635 let sidecar_entries_cleanup_would_reap = sidecar.cleanup_would_reap;
636 let mut reporting = Vec::new();
637 let mut registered_silent_pids = Vec::new();
638 let mut unknown_pids = Vec::new();
639 let mut sidecar_entries = Vec::new();
640 let mut sidecar_known_pids = BTreeSet::new();
641
642 for entry in sidecar.entries {
643 match entry {
644 crate::walpin::WalpinPidHealth::Reporting(heartbeat) => {
645 let current_oldest_tx_age_secs =
646 heartbeat.current_oldest_tx_age_secs(now_epoch_secs);
647 let attribution_is_evidence_backed = heartbeat.attribution_is_evidence_backed();
648 sidecar_known_pids.insert(heartbeat.pid);
649 reporting.push(WalPinHolder {
650 pid: heartbeat.pid,
651 process_role: heartbeat.process_role.clone(),
652 current_oldest_tx_age_secs,
653 oldest_tx_label: heartbeat.oldest_tx_label.clone(),
654 attribution_is_evidence_backed,
655 });
656 sidecar_entries.push((
657 heartbeat.pid,
658 0u8,
659 serde_json::json!({
660 "pid": heartbeat.pid,
661 "status": "reporting",
662 "process_role": heartbeat.process_role,
663 "current_oldest_tx_age_secs": current_oldest_tx_age_secs,
664 "oldest_tx_label": heartbeat.oldest_tx_label,
665 "attribution_is_evidence_backed": attribution_is_evidence_backed,
666 }),
667 ));
668 }
669 crate::walpin::WalpinPidHealth::RegisteredSilent { pid } => {
670 sidecar_known_pids.insert(pid);
671 registered_silent_pids.push(pid);
672 sidecar_entries.push((
673 pid,
674 1u8,
675 serde_json::json!({"pid": pid, "status": "registered_silent"}),
676 ));
677 }
678 crate::walpin::WalpinPidHealth::Unknown { pid, reason } => {
679 sidecar_known_pids.insert(pid);
680 unknown_pids.push(pid);
681 sidecar_entries.push((
682 pid,
683 2u8,
684 serde_json::json!({"pid": pid, "status": "unknown", "reason": reason}),
685 ));
686 }
687 }
688 }
689
690 reporting.sort_by_key(|holder| holder.pid);
691 reporting.dedup_by_key(|holder| holder.pid);
692 registered_silent_pids.sort_unstable();
693 registered_silent_pids.dedup();
694 unknown_pids.sort_unstable();
695 unknown_pids.dedup();
696 sidecar_entries.sort_by_key(|(pid, status_rank, _)| (*pid, *status_rank));
697 let sidecar_entries = sidecar_entries
698 .into_iter()
699 .map(|(_, _, entry)| entry)
700 .collect();
701 let census_pids_without_attribution: Vec<u32> = census_holder_pids
702 .iter()
703 .copied()
704 .filter(|pid| !sidecar_known_pids.contains(pid))
705 .collect();
706
707 let mut status_reasons = Vec::new();
708 if let WalPinCensus::Incomplete { reason, .. } = &census_carrier {
709 status_reasons.push(reason.clone());
710 }
711 if sidecar_listing_truncated {
712 status_reasons.push(
713 "read-only sidecar enumeration reached its entry cap; additional entries may exist"
714 .to_string(),
715 );
716 }
717 if !unknown_pids.is_empty() {
718 status_reasons.push(format!(
719 "{} sidecar PID(s) could not be classified conclusively",
720 unknown_pids.len()
721 ));
722 }
723 if !census_pids_without_attribution.is_empty() {
724 status_reasons.push(format!(
725 "{} OS-confirmed holder(s) have no sidecar attribution",
726 census_pids_without_attribution.len()
727 ));
728 }
729
730 let fully_attributed = census_is_complete
731 && !sidecar_listing_truncated
732 && unknown_pids.is_empty()
733 && census_pids_without_attribution.is_empty();
734 let status = if fully_attributed {
735 WalPinAttributionStatus::Complete
736 } else {
737 WalPinAttributionStatus::Degraded
738 };
739 let unavailable_reason = (!fully_attributed).then(|| status_reasons.join("; "));
740
741 WalPinAttribution {
742 status,
743 status_reasons,
744 census: census_carrier,
745 reporting_pid,
746 reporting_process_is_holder,
747 reporting_process_is_holder_unavailable_reason,
748 census_process_start_times,
749 start_time_resolution_secs,
750 start_time_resolution_unavailable_reason: start_time_resolution_secs
751 .is_none()
752 .then(|| "process start time is unavailable on this platform".to_string()),
753 available: fully_attributed,
754 unavailable_reason,
755 census_holder_pids,
756 census_uninspectable_pids,
757 census_truncated,
758 census_is_complete,
759 reporting,
760 registered_silent_pids,
761 unknown_pids,
762 census_pids_without_attribution,
763 fully_attributed,
764 sidecar_entries,
765 sidecar_listing_truncated: Some(sidecar_listing_truncated),
766 sidecar_entries_cleanup_would_reap: Some(sidecar_entries_cleanup_would_reap),
767 }
768}
769
770#[cfg(unix)]
779pub fn wal_pin_attribution(db_path: &Path, sweep_interval: Duration) -> WalPinAttribution {
780 use crate::walpin;
781
782 let census = match walpin::census_holders(db_path) {
783 Ok(c) => c,
784 Err(e) => return WalPinAttribution::unavailable(format!("census_holders failed: {e}")),
785 };
786 if !walpin::sidecar_enabled(true) {
787 return wal_pin_attribution_without_sidecar(census, SIDECAR_DISABLED_REASON.to_string());
788 }
789 match walpin::inspect_live(&walpin::sidecar_dir_for(db_path), sweep_interval) {
790 Ok(sidecar) => wal_pin_attribution_from_evidence(census, sidecar),
791 Err(error) => wal_pin_attribution_without_sidecar(
792 census,
793 format!("read-only sidecar enumeration failed: {error}"),
794 ),
795 }
796}
797
798#[cfg(unix)]
800const SIDECAR_DISABLED_REASON: &str =
801 "walpin sidecar is explicitly disabled (KHIVE_WALPIN_SIDECAR); attribution has no sidecar \
802 evidence to reconcile against the OS holder census";
803
804#[cfg(not(unix))]
805pub fn wal_pin_attribution(_db_path: &Path, _sweep_interval: Duration) -> WalPinAttribution {
806 WalPinAttribution::unavailable("WAL-pin attribution requires a Unix platform")
807}
808
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
816pub struct ReaderContentionDiagnostics {
817 pub configured_reader_cap: usize,
820 pub configured_checkout_timeout_ms: u64,
822 pub configured_busy_timeout_ms: u64,
825 pub reader_admission_capacity: usize,
828 pub available_reader_admission_slots: usize,
830 pub reader_acquisitions: u64,
833 pub pooled_reader_checkouts: u64,
835 pub standalone_reader_opens: u64,
838 pub infrastructure_standalone_reader_opens: u64,
841 pub reader_checkout_timeouts: u64,
844 pub reader_busy_timeouts: u64,
850 pub active_pooled_reader_checkouts: u64,
852 pub peak_active_pooled_reader_checkouts: u64,
854 pub completed_pooled_reader_checkouts: u64,
856 pub max_completed_reader_hold_micros: u64,
858 pub max_completed_reader_hold_operation: Option<&'static str>,
863 pub reader_discards: u64,
867 pub reader_replacement_open_failures: u64,
872}
873
874impl ReaderContentionDiagnostics {
875 fn snapshot(pool: &ConnectionPool) -> Self {
876 let reader = pool.reader_acquisition_snapshot();
877 Self {
878 configured_reader_cap: pool.config().max_readers,
879 configured_checkout_timeout_ms: u64::try_from(
880 pool.config().checkout_timeout.as_millis(),
881 )
882 .unwrap_or(u64::MAX),
883 configured_busy_timeout_ms: u64::try_from(pool.config().busy_timeout.as_millis())
884 .unwrap_or(u64::MAX),
885 reader_admission_capacity: reader.reader_admission_capacity,
886 available_reader_admission_slots: reader.available_reader_admission_slots,
887 reader_acquisitions: reader.acquisitions,
888 pooled_reader_checkouts: reader.pooled_checkouts,
889 standalone_reader_opens: reader.standalone_opens,
890 infrastructure_standalone_reader_opens: reader.infrastructure_standalone_opens,
891 reader_checkout_timeouts: reader.checkout_timeouts,
892 reader_busy_timeouts: reader.busy_timeouts,
893 active_pooled_reader_checkouts: reader.active_pooled_checkouts,
894 peak_active_pooled_reader_checkouts: reader.peak_active_pooled_checkouts,
895 completed_pooled_reader_checkouts: reader.completed_pooled_checkouts,
896 max_completed_reader_hold_micros: reader.max_completed_hold_micros,
897 max_completed_reader_hold_operation: reader.max_completed_hold_operation,
898 reader_discards: reader.reader_discards,
899 reader_replacement_open_failures: reader.reader_replacement_open_failures,
900 }
901 }
902}
903
904#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
919pub struct WriterContentionDiagnostics {
920 pub writer_acquisitions: u64,
923 pub pooled_writer_acquisitions: u64,
925 pub standalone_writer_acquisitions: u64,
927 pub writer_task_acquisitions: u64,
929 pub writer_acquisition_timeouts: u64,
931 pub writer_lease_timeouts: u64,
937 pub configured_guard_deadline_ms: Option<u64>,
940 pub configured_checkout_timeout_ms: u64,
943 pub effective_writer_wait_bound_ms: u64,
948 pub direct_writer_busy_refusals: u64,
951 pub writer_task_begin_busy: u64,
956 pub writer_task_begin_busy_absorbed: u64,
961 pub writer_task_begin_errors: u64,
964 pub writer_task_request_failures: u64,
969 pub writer_task_side_effects_unknown: u64,
972 pub audit_append_failures: Option<u64>,
984 pub audit_append_failures_unavailable_reason: Option<String>,
986 pub audit_obligation_append_failures: Option<u64>,
991 pub audit_obligation_append_failures_unavailable_reason: Option<String>,
993 pub audit_batch_flush_failures: Option<u64>,
998 pub audit_batch_flush_failures_unavailable_reason: Option<String>,
1000 pub audit_degraded_rows: Option<u64>,
1003 pub audit_degraded_rows_unavailable_reason: Option<String>,
1005 pub audit_degraded: Option<bool>,
1011 pub audit_degraded_unavailable_reason: Option<String>,
1013 pub audit_admission_refused_obligations: Option<u64>,
1033 pub audit_admission_refused_obligations_last_at_ms: Option<u64>,
1039 pub audit_admission_refused_obligations_unavailable_reason: Option<String>,
1041 pub audit_retry_exhausted_obligations: Option<u64>,
1045 pub audit_retry_exhausted_obligations_last_at_ms: Option<u64>,
1047 pub audit_retry_exhausted_obligations_unavailable_reason: Option<String>,
1049 pub audit_admission_unresolved_obligations: Option<u64>,
1070 pub audit_admission_unresolved_obligations_last_at_ms: Option<u64>,
1074 pub audit_admission_unresolved_obligations_unavailable_reason: Option<String>,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1084pub struct RuntimeAuditBatchMetrics {
1085 pub flush_failures: u64,
1088 pub degraded_rows: u64,
1090 pub degraded: bool,
1092 pub admission_refused_obligations: u64,
1097 pub admission_refused_obligations_last_at_ms: Option<u64>,
1100 pub retry_exhausted_obligations: u64,
1102 pub retry_exhausted_obligations_last_at_ms: Option<u64>,
1104 pub admission_unresolved_obligations: u64,
1111 pub admission_unresolved_obligations_last_at_ms: Option<u64>,
1114}
1115
1116#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1132pub struct GraphEdgeIntegrity {
1133 pub duplicate_edge_id_groups: i64,
1134 pub graph_edges_rows: i64,
1135 pub graph_edges_seq_rows: i64,
1136 pub pre_v14_duplicate_edge_state_detected: bool,
1137 pub live_entities_carrying_merged_into: i64,
1138}
1139
1140fn graph_edge_integrity(conn: &Connection) -> rusqlite::Result<GraphEdgeIntegrity> {
1141 conn.query_row(
1142 "SELECT
1143 (SELECT COUNT(*) FROM (
1144 SELECT id FROM graph_edges GROUP BY id HAVING COUNT(*) > 1
1145 )),
1146 (SELECT COUNT(*) FROM graph_edges),
1147 (SELECT COUNT(*) FROM graph_edges_seq),
1148 (SELECT COUNT(*) FROM entities
1149 WHERE deleted_at IS NULL AND merged_into IS NOT NULL)",
1150 [],
1151 |row| {
1152 let duplicate_edge_id_groups = row.get(0)?;
1153 Ok(GraphEdgeIntegrity {
1154 duplicate_edge_id_groups,
1155 graph_edges_rows: row.get(1)?,
1156 graph_edges_seq_rows: row.get(2)?,
1157 pre_v14_duplicate_edge_state_detected: duplicate_edge_id_groups > 0,
1158 live_entities_carrying_merged_into: row.get(3)?,
1159 })
1160 },
1161 )
1162}
1163
1164const MAX_DATABASE_SIZE_OBJECTS: usize = 4_096;
1165
1166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1168#[serde(rename_all = "snake_case")]
1169pub enum DatabaseObjectKind {
1170 Table,
1171 Index,
1172 Internal,
1173}
1174
1175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1180#[serde(rename_all = "snake_case")]
1181pub enum DatabaseStorageClass {
1182 RowTable,
1183 Index,
1184 FullText,
1185 Vector,
1186 MixedRowAndEmbedding,
1187 Internal,
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1191pub struct DatabaseObjectSize {
1192 pub name: String,
1193 pub owner_table: Option<String>,
1194 pub object_kind: DatabaseObjectKind,
1195 pub storage_class: DatabaseStorageClass,
1196 pub pages: u64,
1197 pub bytes: u64,
1198}
1199
1200#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1203pub struct DatabaseSizeComposition {
1204 pub page_size_bytes: u64,
1205 pub page_count: u64,
1206 pub freelist_pages: u64,
1207 pub database_bytes: u64,
1208 pub freelist_bytes: u64,
1209 pub accounted_bytes: u64,
1210 pub unaccounted_bytes: u64,
1211 pub row_table_bytes: u64,
1212 pub index_bytes: u64,
1213 pub full_text_bytes: u64,
1214 pub vector_bytes: u64,
1215 pub mixed_embedding_bytes: u64,
1216 pub internal_bytes: u64,
1217 pub objects: Vec<DatabaseObjectSize>,
1218 pub objects_truncated: bool,
1219 pub objects_omitted: usize,
1220}
1221
1222fn nonnegative_sqlite_integer(column: usize, value: i64) -> rusqlite::Result<u64> {
1223 u64::try_from(value).map_err(|_| rusqlite::Error::IntegralValueOutOfRange(column, value))
1224}
1225
1226#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1230pub struct DatabasePageAccounting {
1231 pub page_size_bytes: u64,
1232 pub page_count: u64,
1233 pub freelist_pages: u64,
1235}
1236
1237fn database_page_accounting(conn: &Connection) -> rusqlite::Result<DatabasePageAccounting> {
1238 let read = |pragma: &str| -> rusqlite::Result<u64> {
1239 nonnegative_sqlite_integer(0, conn.query_row(pragma, [], |row| row.get::<_, i64>(0))?)
1240 };
1241 Ok(DatabasePageAccounting {
1242 page_size_bytes: read("PRAGMA page_size")?,
1243 page_count: read("PRAGMA page_count")?,
1244 freelist_pages: read("PRAGMA freelist_count")?,
1245 })
1246}
1247
1248fn declares_embedding_blob(sql: Option<&str>) -> bool {
1249 let Some(sql) = sql else {
1250 return false;
1251 };
1252 let tokens: Vec<_> = sql
1253 .split(|character: char| !(character.is_ascii_alphanumeric() || character == '_'))
1254 .filter(|token| !token.is_empty())
1255 .collect();
1256 tokens.windows(2).any(|pair| {
1257 pair[0].eq_ignore_ascii_case("embedding") && pair[1].eq_ignore_ascii_case("blob")
1258 })
1259}
1260
1261fn classify_database_object(
1262 name: &str,
1263 sqlite_type: &str,
1264 sql: Option<&str>,
1265) -> (DatabaseObjectKind, DatabaseStorageClass) {
1266 let object_kind = match sqlite_type {
1267 "table" => DatabaseObjectKind::Table,
1268 "index" => DatabaseObjectKind::Index,
1269 _ => DatabaseObjectKind::Internal,
1270 };
1271 let lower_name = name.to_ascii_lowercase();
1272 let lower_sql = sql.unwrap_or_default().to_ascii_lowercase();
1273 let storage_class = if lower_name.starts_with("fts_") || lower_sql.contains("using fts5") {
1274 DatabaseStorageClass::FullText
1275 } else if lower_name.starts_with("vec_")
1276 || lower_name == "_embedding_models"
1277 || lower_sql.contains("using vec0")
1278 {
1279 DatabaseStorageClass::Vector
1280 } else if declares_embedding_blob(sql) {
1281 DatabaseStorageClass::MixedRowAndEmbedding
1282 } else if object_kind == DatabaseObjectKind::Index {
1283 DatabaseStorageClass::Index
1284 } else if name.starts_with("sqlite_") || object_kind == DatabaseObjectKind::Internal {
1285 DatabaseStorageClass::Internal
1286 } else {
1287 DatabaseStorageClass::RowTable
1288 };
1289 (object_kind, storage_class)
1290}
1291
1292fn database_size_composition(conn: &Connection) -> rusqlite::Result<DatabaseSizeComposition> {
1293 let DatabasePageAccounting {
1294 page_size_bytes: page_size,
1295 page_count,
1296 freelist_pages,
1297 } = database_page_accounting(conn)?;
1298
1299 let mut statement = conn.prepare(
1300 "SELECT d.name, COALESCE(s.type, 'internal'), s.tbl_name, s.sql, d.pageno, d.pgsize
1301 FROM dbstat AS d
1302 LEFT JOIN sqlite_schema AS s ON s.name = d.name
1303 WHERE d.aggregate = TRUE
1304 ORDER BY d.name",
1305 )?;
1306 let mut rows = statement.query([])?;
1307 let mut objects = Vec::new();
1308 let mut objects_omitted = 0usize;
1309 let mut accounted_bytes = 0u64;
1310 let mut row_table_bytes = 0u64;
1311 let mut index_bytes = 0u64;
1312 let mut full_text_bytes = 0u64;
1313 let mut vector_bytes = 0u64;
1314 let mut mixed_embedding_bytes = 0u64;
1315 let mut internal_bytes = 0u64;
1316
1317 while let Some(row) = rows.next()? {
1318 let name: String = row.get(0)?;
1319 let sqlite_type: String = row.get(1)?;
1320 let owner_table: Option<String> = row.get(2)?;
1321 let sql: Option<String> = row.get(3)?;
1322 let pages = nonnegative_sqlite_integer(4, row.get(4)?)?;
1323 let bytes = nonnegative_sqlite_integer(5, row.get(5)?)?;
1324 let (object_kind, storage_class) =
1325 classify_database_object(&name, &sqlite_type, sql.as_deref());
1326 accounted_bytes = accounted_bytes.saturating_add(bytes);
1327 let class_total = match storage_class {
1328 DatabaseStorageClass::RowTable => &mut row_table_bytes,
1329 DatabaseStorageClass::Index => &mut index_bytes,
1330 DatabaseStorageClass::FullText => &mut full_text_bytes,
1331 DatabaseStorageClass::Vector => &mut vector_bytes,
1332 DatabaseStorageClass::MixedRowAndEmbedding => &mut mixed_embedding_bytes,
1333 DatabaseStorageClass::Internal => &mut internal_bytes,
1334 };
1335 *class_total = class_total.saturating_add(bytes);
1336
1337 if objects.len() < MAX_DATABASE_SIZE_OBJECTS {
1338 objects.push(DatabaseObjectSize {
1339 name,
1340 owner_table,
1341 object_kind,
1342 storage_class,
1343 pages,
1344 bytes,
1345 });
1346 } else {
1347 objects_omitted = objects_omitted.saturating_add(1);
1348 }
1349 }
1350
1351 let database_bytes = page_count.saturating_mul(page_size);
1352 let freelist_bytes = freelist_pages.saturating_mul(page_size);
1353 let unaccounted_bytes = database_bytes
1354 .saturating_sub(freelist_bytes)
1355 .saturating_sub(accounted_bytes);
1356 Ok(DatabaseSizeComposition {
1357 page_size_bytes: page_size,
1358 page_count,
1359 freelist_pages,
1360 database_bytes,
1361 freelist_bytes,
1362 accounted_bytes,
1363 unaccounted_bytes,
1364 row_table_bytes,
1365 index_bytes,
1366 full_text_bytes,
1367 vector_bytes,
1368 mixed_embedding_bytes,
1369 internal_bytes,
1370 objects,
1371 objects_truncated: objects_omitted > 0,
1372 objects_omitted,
1373 })
1374}
1375
1376#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1389pub struct CollectionCost {
1390 pub total_ms: u64,
1392 pub sqlite_ms: u64,
1395 pub wal_file_stat_ms: u64,
1398 pub wal_pin_census_ms: u64,
1400 pub wal_pin_sidecar_ms: u64,
1402 pub wal_pin_census_budget_ms: Option<u64>,
1407 pub wal_pin_census_budget_exhausted: bool,
1411}
1412
1413impl CollectionCost {
1414 fn in_memory(total_ms: u64) -> Self {
1418 Self {
1419 total_ms,
1420 sqlite_ms: 0,
1421 wal_file_stat_ms: 0,
1422 wal_pin_census_ms: 0,
1423 wal_pin_sidecar_ms: 0,
1424 wal_pin_census_budget_ms: None,
1425 wal_pin_census_budget_exhausted: false,
1426 }
1427 }
1428}
1429
1430const DEFAULT_CENSUS_BUDGET: Duration = Duration::from_millis(2000);
1437
1438const CENSUS_BUDGET_ENV: &str = "KHIVE_WALPIN_CENSUS_BUDGET_MS";
1448
1449fn request_census_budget() -> Option<Duration> {
1450 match std::env::var(CENSUS_BUDGET_ENV) {
1451 Ok(raw) => match raw.trim().parse::<u64>() {
1452 Ok(0) => None,
1453 Ok(ms) => Some(Duration::from_millis(ms)),
1454 Err(_) => Some(DEFAULT_CENSUS_BUDGET),
1455 },
1456 Err(_) => Some(DEFAULT_CENSUS_BUDGET),
1457 }
1458}
1459
1460#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1463pub struct WalCeilingDiagnostics {
1464 pub configured_bytes: u64,
1466 pub effective_bytes: u64,
1468 pub source: WalCeilingSource,
1469 pub enabled: bool,
1470 pub status: &'static str,
1472}
1473
1474impl WalCeilingDiagnostics {
1475 fn from_pool(pool: &ConnectionPool) -> Self {
1476 let policy = pool.config().wal_ceiling;
1477 let read_only = pool.config().read_only;
1478 let effective_bytes = policy.effective_bytes(read_only);
1479 Self {
1480 configured_bytes: policy.bytes,
1481 effective_bytes,
1482 source: policy.source,
1483 enabled: effective_bytes > 0,
1484 status: if effective_bytes > 0 {
1485 "enforced"
1486 } else if read_only && policy.bytes > 0 {
1487 "read_only_not_enforced"
1488 } else {
1489 "disabled"
1490 },
1491 }
1492 }
1493}
1494
1495#[derive(Debug, Clone, PartialEq, Serialize)]
1496pub struct DbDiagnostics {
1497 pub build: BuildIdentity,
1498 pub process: ProcessIdentity,
1499 pub note_search_ann_route_total: u64,
1502 pub note_search_fallback_route_total: u64,
1503 pub search_mechanism: crate::pool::SearchMechanismSnapshot,
1506 pub db_path: Option<String>,
1509 pub wal_ceiling: WalCeilingDiagnostics,
1511 pub disk_guard: DiskGuardDiagnostics,
1512 pub wal_file: Option<WalFileState>,
1513 pub checkpoint_counters: CheckpointCounters,
1514 pub checkpoint_probe: Option<CheckpointProbe>,
1515 pub checkpoint_probe_error: Option<String>,
1516 #[serde(flatten)]
1517 pub checkpoint_pin: CheckpointPinDiagnostics,
1519 pub reader_contention: ReaderContentionDiagnostics,
1521 pub writer_contention: WriterContentionDiagnostics,
1523 pub size_composition: Option<DatabaseSizeComposition>,
1524 pub size_composition_error: Option<String>,
1525 pub page_accounting: Option<DatabasePageAccounting>,
1528 pub page_accounting_error: Option<String>,
1529 pub graph_edge_integrity: Option<GraphEdgeIntegrity>,
1530 pub graph_edge_integrity_error: Option<String>,
1531 pub fts_segments: Option<crate::FtsSegmentDiagnostics>,
1535 pub fts_segments_error: Option<String>,
1536 pub fts_maintenance: crate::FtsMaintenanceCounters,
1539 pub wal_pin: WalPinAttribution,
1540 pub collection_cost: CollectionCost,
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Serialize)]
1546pub struct CheckpointPinDiagnostics {
1547 pub backfill_ceiling: Option<i64>,
1549 pub backfill_ceiling_unavailable_reason: Option<String>,
1551 pub oldest_pinned_frame: Option<i64>,
1553 pub oldest_pinned_frame_unavailable_reason: Option<String>,
1555 pub oldest_pinned_frame_run: Option<crate::checkpoint::CheckpointRun>,
1557 pub oldest_pinned_frame_run_unavailable_reason: Option<String>,
1559 pub pin_depth: Option<i64>,
1561 pub pin_depth_unavailable_reason: Option<String>,
1563}
1564
1565fn checkpoint_pin_diagnostics(
1566 pool: &ConnectionPool,
1567 probe: Option<&CheckpointProbe>,
1568 probe_error: Option<&str>,
1569) -> CheckpointPinDiagnostics {
1570 let (run_status, run_age) = checkpoint::checkpoint_run_snapshot(pool);
1571 checkpoint_pin_diagnostics_for_run(probe, probe_error, run_status, run_age)
1572}
1573
1574fn checkpoint_pin_diagnostics_for_run(
1575 probe: Option<&CheckpointProbe>,
1576 probe_error: Option<&str>,
1577 run_status: checkpoint::CheckpointRunStatus,
1578 run_age: Option<Duration>,
1579) -> CheckpointPinDiagnostics {
1580 let (backfill_ceiling, backfill_reason) = match probe {
1581 None => (
1582 None,
1583 Some(
1584 probe_error
1585 .unwrap_or("checkpoint probe did not return a row")
1586 .to_string(),
1587 ),
1588 ),
1589 Some(probe) if probe.busy != 0 => (
1590 None,
1591 Some(format!("PASSIVE checkpoint returned busy={}", probe.busy)),
1592 ),
1593 Some(probe) if probe.log_frames < 0 || probe.checkpointed_frames < 0 => (
1594 None,
1595 Some("PASSIVE checkpoint returned a negative frame count".to_string()),
1596 ),
1597 Some(probe) if probe.log_frames <= probe.checkpointed_frames => (
1598 None,
1599 Some("PASSIVE checkpoint found no frames beyond the backfill ceiling".to_string()),
1600 ),
1601 Some(probe) => (Some(probe.checkpointed_frames), None),
1602 };
1603
1604 let (oldest_pinned_frame, oldest_reason, oldest_pinned_frame_run, run_reason) =
1605 match backfill_ceiling {
1606 None => {
1607 let reason = backfill_reason
1608 .clone()
1609 .unwrap_or_else(|| "backfill ceiling is unavailable".to_string());
1610 (None, Some(reason.clone()), None, Some(reason))
1611 }
1612 Some(ceiling) => match run_status {
1613 checkpoint::CheckpointRunStatus::NoTask => {
1614 let reason = "no checkpoint task in this process".to_string();
1615 (None, Some(reason.clone()), None, Some(reason))
1616 }
1617 checkpoint::CheckpointRunStatus::NoObservation => {
1618 let reason = "no checkpoint run has been observed for this backend".to_string();
1619 (None, Some(reason.clone()), None, Some(reason))
1620 }
1621 checkpoint::CheckpointRunStatus::Observed(run) if run.frame != ceiling => {
1622 let reason = "checkpoint run frame does not match the current backfill ceiling";
1623 (
1624 None,
1625 Some(reason.to_string()),
1626 None,
1627 Some(reason.to_string()),
1628 )
1629 }
1630 checkpoint::CheckpointRunStatus::Observed(_)
1631 if run_age.is_none_or(|age| age < Duration::from_secs(1)) =>
1632 {
1633 let reason = "checkpoint run has been observed for less than one second";
1634 (
1635 None,
1636 Some(reason.to_string()),
1637 None,
1638 Some(reason.to_string()),
1639 )
1640 }
1641 checkpoint::CheckpointRunStatus::Observed(run) => {
1642 (Some(ceiling), None, Some(run), None)
1643 }
1644 },
1645 };
1646
1647 let pin_depth = oldest_pinned_frame.map(|frame| {
1648 probe
1649 .expect("a reported pin always has a probe row")
1650 .log_frames
1651 .saturating_sub(frame)
1652 .max(0)
1653 });
1654 let pin_depth_reason = if pin_depth.is_none() {
1655 oldest_reason.clone()
1656 } else {
1657 None
1658 };
1659
1660 CheckpointPinDiagnostics {
1661 backfill_ceiling,
1662 backfill_ceiling_unavailable_reason: backfill_reason,
1663 oldest_pinned_frame,
1664 oldest_pinned_frame_unavailable_reason: oldest_reason,
1665 oldest_pinned_frame_run,
1666 oldest_pinned_frame_run_unavailable_reason: run_reason,
1667 pin_depth,
1668 pin_depth_unavailable_reason: pin_depth_reason,
1669 }
1670}
1671
1672pub fn collect(
1692 pool: &ConnectionPool,
1693 build: BuildIdentity,
1694 sweep_interval: Duration,
1695) -> DbDiagnostics {
1696 collect_inner(pool, build, sweep_interval, None, None)
1697}
1698
1699pub fn collect_with_audit_append_failures(
1702 pool: &ConnectionPool,
1703 build: BuildIdentity,
1704 sweep_interval: Duration,
1705 audit_append_failures: u64,
1706) -> DbDiagnostics {
1707 collect_inner(
1708 pool,
1709 build,
1710 sweep_interval,
1711 Some(audit_append_failures),
1712 None,
1713 )
1714}
1715
1716pub async fn collect_with_audit_append_failures_interruptibly(
1726 pool: Arc<ConnectionPool>,
1727 build: BuildIdentity,
1728 sweep_interval: Duration,
1729 audit_append_failures: u64,
1730) -> StorageResult<DbDiagnostics> {
1731 collect_with_runtime_audit_metrics_interruptibly(
1732 pool,
1733 build,
1734 sweep_interval,
1735 audit_append_failures,
1736 None,
1737 )
1738 .await
1739}
1740
1741pub async fn collect_with_runtime_audit_metrics_interruptibly(
1748 pool: Arc<ConnectionPool>,
1749 build: BuildIdentity,
1750 sweep_interval: Duration,
1751 audit_append_failures: u64,
1752 runtime_audit_batch_metrics: Option<RuntimeAuditBatchMetrics>,
1753) -> StorageResult<DbDiagnostics> {
1754 let process = ProcessIdentity::current(&pool);
1755 collect_with_runtime_audit_metrics_for_process_interruptibly(
1756 pool,
1757 build,
1758 process,
1759 sweep_interval,
1760 audit_append_failures,
1761 runtime_audit_batch_metrics,
1762 true,
1763 )
1764 .await
1765}
1766
1767pub async fn collect_with_runtime_audit_metrics_for_process_interruptibly(
1776 pool: Arc<ConnectionPool>,
1777 build: BuildIdentity,
1778 process: ProcessIdentity,
1779 sweep_interval: Duration,
1780 audit_append_failures: u64,
1781 runtime_audit_batch_metrics: Option<RuntimeAuditBatchMetrics>,
1782 collect_size_composition: bool,
1783) -> StorageResult<DbDiagnostics> {
1784 crate::ensure_request_read_active("db_diagnostics")?;
1785 let started = Instant::now();
1786 let counters = checkpoint_counters();
1787 let reader_contention = ReaderContentionDiagnostics::snapshot(&pool);
1788 let search_mechanism = pool.search_mechanism_snapshot();
1789 let writer_contention = WriterContentionDiagnostics::snapshot(
1790 &pool,
1791 Some(audit_append_failures),
1792 runtime_audit_batch_metrics,
1793 );
1794
1795 let Some(path) = pool.config().path.clone() else {
1796 crate::ensure_request_read_active("db_diagnostics")?;
1797 return Ok(DbDiagnostics {
1798 build,
1799 process,
1800 note_search_ann_route_total: 0,
1801 note_search_fallback_route_total: 0,
1802 search_mechanism,
1803 db_path: None,
1804 wal_ceiling: WalCeilingDiagnostics::from_pool(&pool),
1805 disk_guard: DiskGuardDiagnostics::snapshot(&pool),
1806 wal_file: None,
1807 checkpoint_counters: counters,
1808 checkpoint_probe: None,
1809 checkpoint_probe_error: Some(
1810 "in-memory database: no WAL file and no checkpoint to probe".to_string(),
1811 ),
1812 checkpoint_pin: checkpoint_pin_diagnostics(
1813 &pool,
1814 None,
1815 Some("in-memory database: no WAL file and no checkpoint to probe"),
1816 ),
1817 reader_contention,
1818 writer_contention,
1819 size_composition: None,
1820 size_composition_error: Some(
1821 "in-memory database: no file-backed page composition to inspect".to_string(),
1822 ),
1823 page_accounting: None,
1824 page_accounting_error: Some(
1825 "in-memory database: no file-backed pages to count".to_string(),
1826 ),
1827 graph_edge_integrity: None,
1828 graph_edge_integrity_error: Some(
1829 "in-memory database: no durable graph-edge ledger to inspect".to_string(),
1830 ),
1831 fts_segments: None,
1832 fts_segments_error: Some(
1833 "in-memory database: no durable FTS5 indexes to inspect".to_string(),
1834 ),
1835 fts_maintenance: crate::fts_maintenance_counters(),
1836 wal_pin: WalPinAttribution::unavailable(
1837 "in-memory database: no file for the OS holder census",
1838 ),
1839 collection_cost: CollectionCost::in_memory(elapsed_ms(started)),
1840 });
1841 };
1842
1843 let inspection_pool = Arc::clone(&pool);
1844 let sqlite_started = Instant::now();
1845 let inspection = crate::read_cancellation::run_interruptible_read(
1846 StorageCapability::Sql,
1847 "db_diagnostics.sqlite",
1848 move |scope| inspect_pool_interruptibly(&inspection_pool, scope, collect_size_composition),
1849 )
1850 .await?;
1851 let sqlite_ms = elapsed_ms(sqlite_started);
1852 crate::ensure_request_read_active("db_diagnostics")?;
1853 let canonical = operational_db_path(&pool, &path);
1854 let budget = request_census_budget();
1855 let (wal_file, wal_pin, file_state_cost) =
1856 inspect_file_state_interruptibly(canonical, sweep_interval, budget).await?;
1857 crate::ensure_request_read_active("db_diagnostics")?;
1858
1859 Ok(DbDiagnostics {
1860 build,
1861 process,
1862 note_search_ann_route_total: 0,
1863 note_search_fallback_route_total: 0,
1864 search_mechanism,
1865 db_path: Some(path.display().to_string()),
1866 wal_ceiling: WalCeilingDiagnostics::from_pool(&pool),
1867 disk_guard: DiskGuardDiagnostics::snapshot(&pool),
1868 wal_file: Some(wal_file),
1869 checkpoint_counters: counters,
1870 checkpoint_probe: inspection.checkpoint_probe,
1871 checkpoint_probe_error: inspection.checkpoint_probe_error,
1872 checkpoint_pin: inspection.checkpoint_pin,
1873 reader_contention,
1874 writer_contention,
1875 size_composition: inspection.size_composition,
1876 size_composition_error: inspection.size_composition_error,
1877 page_accounting: inspection.page_accounting,
1878 page_accounting_error: inspection.page_accounting_error,
1879 graph_edge_integrity: inspection.graph_edge_integrity,
1880 graph_edge_integrity_error: inspection.graph_edge_integrity_error,
1881 fts_segments: inspection.fts_segments,
1882 fts_segments_error: inspection.fts_segments_error,
1883 fts_maintenance: crate::fts_maintenance_counters(),
1884 wal_pin,
1885 collection_cost: CollectionCost {
1886 total_ms: elapsed_ms(started),
1887 sqlite_ms,
1888 wal_file_stat_ms: file_state_cost.wal_file_stat_ms,
1889 wal_pin_census_ms: file_state_cost.census_ms,
1890 wal_pin_sidecar_ms: file_state_cost.sidecar_ms,
1891 wal_pin_census_budget_ms: budget.map(|b| b.as_millis() as u64),
1892 wal_pin_census_budget_exhausted: file_state_cost.census_budget_exhausted,
1893 },
1894 })
1895}
1896
1897fn operational_db_path(pool: &ConnectionPool, configured: &Path) -> PathBuf {
1909 pool.canonical_path()
1910 .map(Path::to_path_buf)
1911 .unwrap_or_else(|| configured.to_path_buf())
1912}
1913
1914fn collect_inner(
1915 pool: &ConnectionPool,
1916 build: BuildIdentity,
1917 sweep_interval: Duration,
1918 audit_append_failures: Option<u64>,
1919 runtime_audit_batch_metrics: Option<RuntimeAuditBatchMetrics>,
1920) -> DbDiagnostics {
1921 let started = Instant::now();
1922 let process = ProcessIdentity::current(pool);
1923 let counters = checkpoint_counters();
1924 let reader_contention = ReaderContentionDiagnostics::snapshot(pool);
1925 let search_mechanism = pool.search_mechanism_snapshot();
1926 let writer_contention = WriterContentionDiagnostics::snapshot(
1927 pool,
1928 audit_append_failures,
1929 runtime_audit_batch_metrics,
1930 );
1931
1932 let Some(path) = pool.config().path.clone() else {
1933 return DbDiagnostics {
1934 build,
1935 process,
1936 note_search_ann_route_total: 0,
1937 note_search_fallback_route_total: 0,
1938 search_mechanism,
1939 db_path: None,
1940 wal_ceiling: WalCeilingDiagnostics::from_pool(pool),
1941 disk_guard: DiskGuardDiagnostics::snapshot(pool),
1942 wal_file: None,
1943 checkpoint_counters: counters,
1944 checkpoint_probe: None,
1945 checkpoint_probe_error: Some(
1946 "in-memory database: no WAL file and no checkpoint to probe".to_string(),
1947 ),
1948 checkpoint_pin: checkpoint_pin_diagnostics(
1949 pool,
1950 None,
1951 Some("in-memory database: no WAL file and no checkpoint to probe"),
1952 ),
1953 reader_contention,
1954 writer_contention,
1955 size_composition: None,
1956 size_composition_error: Some(
1957 "in-memory database: no file-backed page composition to inspect".to_string(),
1958 ),
1959 page_accounting: None,
1960 page_accounting_error: Some(
1961 "in-memory database: no file-backed pages to count".to_string(),
1962 ),
1963 graph_edge_integrity: None,
1964 graph_edge_integrity_error: Some(
1965 "in-memory database: no durable graph-edge ledger to inspect".to_string(),
1966 ),
1967 fts_segments: None,
1968 fts_segments_error: Some(
1969 "in-memory database: no durable FTS5 indexes to inspect".to_string(),
1970 ),
1971 fts_maintenance: crate::fts_maintenance_counters(),
1972 wal_pin: WalPinAttribution::unavailable(
1973 "in-memory database: no file for the OS holder census",
1974 ),
1975 collection_cost: CollectionCost::in_memory(elapsed_ms(started)),
1976 };
1977 };
1978
1979 let sqlite_started = Instant::now();
1980 let inspection = inspect_pool(pool);
1981 let sqlite_ms = elapsed_ms(sqlite_started);
1982 let canonical = operational_db_path(pool, &path);
1983 let wal_file_started = Instant::now();
1984 let wal_file = wal_file_state(&canonical);
1985 let wal_file_stat_ms = elapsed_ms(wal_file_started);
1986 let census_started = Instant::now();
1987 let wal_pin = wal_pin_attribution(&canonical, sweep_interval);
1988 let wal_pin_ms = elapsed_ms(census_started);
1989
1990 DbDiagnostics {
1991 build,
1992 process,
1993 note_search_ann_route_total: 0,
1994 note_search_fallback_route_total: 0,
1995 search_mechanism,
1996 db_path: Some(path.display().to_string()),
1997 wal_ceiling: WalCeilingDiagnostics::from_pool(pool),
1998 disk_guard: DiskGuardDiagnostics::snapshot(pool),
1999 wal_file: Some(wal_file),
2000 checkpoint_counters: counters,
2001 checkpoint_probe: inspection.checkpoint_probe,
2002 checkpoint_probe_error: inspection.checkpoint_probe_error,
2003 checkpoint_pin: inspection.checkpoint_pin,
2004 reader_contention,
2005 writer_contention,
2006 size_composition: inspection.size_composition,
2007 size_composition_error: inspection.size_composition_error,
2008 page_accounting: inspection.page_accounting,
2009 page_accounting_error: inspection.page_accounting_error,
2010 graph_edge_integrity: inspection.graph_edge_integrity,
2011 graph_edge_integrity_error: inspection.graph_edge_integrity_error,
2012 fts_segments: inspection.fts_segments,
2013 fts_segments_error: inspection.fts_segments_error,
2014 fts_maintenance: crate::fts_maintenance_counters(),
2015 wal_pin,
2016 collection_cost: CollectionCost {
2017 total_ms: elapsed_ms(started),
2018 sqlite_ms,
2019 wal_file_stat_ms,
2020 wal_pin_census_ms: wal_pin_ms,
2026 wal_pin_sidecar_ms: 0,
2027 wal_pin_census_budget_ms: None,
2028 wal_pin_census_budget_exhausted: false,
2029 },
2030 }
2031}
2032
2033struct PoolInspection {
2034 checkpoint_probe: Option<CheckpointProbe>,
2035 checkpoint_probe_error: Option<String>,
2036 checkpoint_pin: CheckpointPinDiagnostics,
2037 size_composition: Option<DatabaseSizeComposition>,
2038 size_composition_error: Option<String>,
2039 page_accounting: Option<DatabasePageAccounting>,
2040 page_accounting_error: Option<String>,
2041 graph_edge_integrity: Option<GraphEdgeIntegrity>,
2042 graph_edge_integrity_error: Option<String>,
2043 fts_segments: Option<crate::FtsSegmentDiagnostics>,
2044 fts_segments_error: Option<String>,
2045}
2046
2047fn split_fts_segments_result(
2051 result: Result<crate::FtsSegmentDiagnostics, String>,
2052) -> (Option<crate::FtsSegmentDiagnostics>, Option<String>) {
2053 match result {
2054 Ok(segments) => (Some(segments), None),
2055 Err(error) => (None, Some(error)),
2056 }
2057}
2058
2059fn split_page_accounting_result(
2060 result: rusqlite::Result<DatabasePageAccounting>,
2061) -> (Option<DatabasePageAccounting>, Option<String>) {
2062 match result {
2063 Ok(pages) => (Some(pages), None),
2064 Err(error) => (None, Some(format!("page accounting query failed: {error}"))),
2065 }
2066}
2067
2068pub const SIZE_COMPOSITION_NOT_REQUESTED: &str =
2072 "not collected: size_composition=false skips the dbstat walk, which reads every page";
2073
2074fn record_diagnostic_checkpoint_probe(
2075 pool: &ConnectionPool,
2076 result: &rusqlite::Result<CheckpointProbe>,
2077) -> (checkpoint::CheckpointRunStatus, Option<Duration>) {
2078 checkpoint::record_checkpoint_run_result(
2079 pool,
2080 result
2081 .as_ref()
2082 .ok()
2083 .map(|probe| (probe.busy, probe.log_frames, probe.checkpointed_frames)),
2084 );
2085 checkpoint::checkpoint_run_snapshot(pool)
2086}
2087
2088fn inspect_pool_interruptibly(
2089 pool: &ConnectionPool,
2090 scope: &crate::read_cancellation::InterruptibleReadScope,
2091 collect_size_composition: bool,
2092) -> StorageResult<PoolInspection> {
2093 scope.ensure_active()?;
2094 let conn = match pool.open_standalone_writer_untracked() {
2095 Ok(conn) => conn,
2096 Err(e) => {
2097 scope.ensure_active()?;
2098 let reason = format!("guarded standalone open refused: {e}");
2099 return Ok(PoolInspection {
2100 checkpoint_probe: None,
2101 checkpoint_probe_error: Some(reason.clone()),
2102 checkpoint_pin: checkpoint_pin_diagnostics(pool, None, Some(&reason)),
2103 size_composition: None,
2104 size_composition_error: Some(reason.clone()),
2105 page_accounting: None,
2106 page_accounting_error: Some(reason.clone()),
2107 graph_edge_integrity: None,
2108 graph_edge_integrity_error: Some(reason.clone()),
2109 fts_segments: None,
2110 fts_segments_error: Some(reason),
2111 });
2112 }
2113 };
2114 scope.ensure_active()?;
2119
2120 let probe_result = checkpoint_probe(&conn);
2122 let (run_status, run_age) = record_diagnostic_checkpoint_probe(pool, &probe_result);
2123 let (checkpoint_probe, checkpoint_probe_error) = match probe_result {
2124 Ok(probe) => (Some(probe), None),
2125 Err(e) => (
2126 None,
2127 Some(format!("PRAGMA wal_checkpoint(PASSIVE) failed: {e}")),
2128 ),
2129 };
2130 let checkpoint_pin = checkpoint_pin_diagnostics_for_run(
2131 checkpoint_probe.as_ref(),
2132 checkpoint_probe_error.as_deref(),
2133 run_status,
2134 run_age,
2135 );
2136 #[cfg(test)]
2137 if TEST_PAUSE_AFTER_PASSIVE.load(Ordering::SeqCst) {
2138 TEST_REACHED_AFTER_PASSIVE.store(true, Ordering::SeqCst);
2139 while TEST_PAUSE_AFTER_PASSIVE.load(Ordering::SeqCst) && !scope.should_stop() {
2140 std::thread::yield_now();
2141 }
2142 }
2143 scope.ensure_active()?;
2144
2145 let (integrity, page_accounting, size_composition, fts_segments) = scope.run(&conn, || {
2150 Ok((
2151 graph_edge_integrity(&conn),
2152 database_page_accounting(&conn),
2153 collect_size_composition.then(|| database_size_composition(&conn)),
2154 crate::fts_maintenance::inspect_fts_segments(&conn),
2155 ))
2156 })?;
2157 let (graph_edge_integrity, graph_edge_integrity_error) = match integrity {
2158 Ok(integrity) => (Some(integrity), None),
2159 Err(e) => (
2160 None,
2161 Some(format!("graph-edge integrity query failed: {e}")),
2162 ),
2163 };
2164 let (fts_segments, fts_segments_error) = split_fts_segments_result(fts_segments);
2165 let (page_accounting, page_accounting_error) = split_page_accounting_result(page_accounting);
2166
2167 let (size_composition, size_composition_error) = match size_composition {
2168 Some(Ok(composition)) => (Some(composition), None),
2169 Some(Err(error)) => (
2170 None,
2171 Some(format!("database size composition query failed: {error}")),
2172 ),
2173 None => (None, Some(SIZE_COMPOSITION_NOT_REQUESTED.to_string())),
2174 };
2175
2176 Ok(PoolInspection {
2177 checkpoint_probe,
2178 checkpoint_probe_error,
2179 checkpoint_pin,
2180 size_composition,
2181 size_composition_error,
2182 page_accounting,
2183 page_accounting_error,
2184 graph_edge_integrity,
2185 graph_edge_integrity_error,
2186 fts_segments,
2187 fts_segments_error,
2188 })
2189}
2190
2191#[cfg(test)]
2192static TEST_PAUSE_AFTER_PASSIVE: AtomicBool = AtomicBool::new(false);
2193#[cfg(test)]
2194static TEST_REACHED_AFTER_PASSIVE: AtomicBool = AtomicBool::new(false);
2195
2196struct StopCensusOnDrop {
2197 stopped: Arc<AtomicBool>,
2198 armed: bool,
2199}
2200
2201impl Drop for StopCensusOnDrop {
2202 fn drop(&mut self) {
2203 if self.armed {
2204 self.stopped.store(true, Ordering::SeqCst);
2205 }
2206 }
2207}
2208
2209#[derive(Debug, Clone, Copy, Default)]
2212struct FileStateCost {
2213 wal_file_stat_ms: u64,
2214 census_ms: u64,
2215 sidecar_ms: u64,
2216 census_budget_exhausted: bool,
2217}
2218
2219fn elapsed_ms(since: Instant) -> u64 {
2220 since.elapsed().as_millis() as u64
2221}
2222
2223async fn inspect_file_state_interruptibly(
2224 path: PathBuf,
2225 sweep_interval: Duration,
2226 census_budget: Option<Duration>,
2227) -> StorageResult<(WalFileState, WalPinAttribution, FileStateCost)> {
2228 const OPERATION: &str = "db_diagnostics.wal_holder_census";
2229 crate::ensure_request_read_active(OPERATION)?;
2230 let stopped = Arc::new(AtomicBool::new(false));
2231 let worker_stopped = Arc::clone(&stopped);
2232 let mut stop_on_drop = StopCensusOnDrop {
2233 stopped: Arc::clone(&stopped),
2234 armed: true,
2235 };
2236 let mut worker = tokio::task::spawn_blocking(move || {
2237 let mut cost = FileStateCost::default();
2238 let wal_file_started = Instant::now();
2239 let wal_file = wal_file_state(&path);
2240 cost.wal_file_stat_ms = elapsed_ms(wal_file_started);
2241 if worker_stopped.load(Ordering::SeqCst) {
2242 return Err(std::io::Error::new(
2243 std::io::ErrorKind::Interrupted,
2244 "WAL holder census cancelled",
2245 ));
2246 }
2247 #[cfg(unix)]
2248 let census_started = Instant::now();
2249 #[cfg(unix)]
2250 let census_result = match census_budget {
2251 Some(budget) => crate::walpin::census_holders_until_within(
2252 &path,
2253 || worker_stopped.load(Ordering::SeqCst),
2254 budget,
2255 ),
2256 None => {
2257 crate::walpin::census_holders_until(&path, || worker_stopped.load(Ordering::SeqCst))
2258 }
2259 };
2260 #[cfg(unix)]
2261 {
2262 cost.census_ms = elapsed_ms(census_started);
2263 }
2264 #[cfg(unix)]
2265 let attribution = match census_result {
2266 Ok(census) => {
2267 cost.census_budget_exhausted = census.budget_exhausted;
2268 if worker_stopped.load(Ordering::SeqCst) {
2269 return Err(std::io::Error::new(
2270 std::io::ErrorKind::Interrupted,
2271 "WAL sidecar inspection cancelled",
2272 ));
2273 }
2274 if !crate::walpin::sidecar_enabled(true) {
2275 wal_pin_attribution_without_sidecar(census, SIDECAR_DISABLED_REASON.to_string())
2276 } else {
2277 let sidecar_started = Instant::now();
2278 let sidecar = crate::walpin::inspect_live(
2279 &crate::walpin::sidecar_dir_for(&path),
2280 sweep_interval,
2281 );
2282 cost.sidecar_ms = elapsed_ms(sidecar_started);
2283 if worker_stopped.load(Ordering::SeqCst) {
2284 return Err(std::io::Error::new(
2285 std::io::ErrorKind::Interrupted,
2286 "WAL sidecar inspection cancelled",
2287 ));
2288 }
2289 match sidecar {
2290 Ok(sidecar) => wal_pin_attribution_from_evidence(census, sidecar),
2291 Err(error) => wal_pin_attribution_without_sidecar(
2292 census,
2293 format!("read-only sidecar enumeration failed: {error}"),
2294 ),
2295 }
2296 }
2297 }
2298 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => return Err(error),
2299 Err(error) => WalPinAttribution::unavailable(format!("census_holders failed: {error}")),
2300 };
2301 #[cfg(not(unix))]
2302 let attribution = {
2303 let _ = census_budget;
2304 let census_started = Instant::now();
2305 let attribution = wal_pin_attribution(&path, sweep_interval);
2306 cost.census_ms = elapsed_ms(census_started);
2307 attribution
2308 };
2309 Ok((wal_file, attribution, cost))
2310 });
2311
2312 tokio::select! {
2313 joined = &mut worker => {
2314 stop_on_drop.armed = false;
2315 let result = joined
2316 .map_err(|error| StorageError::driver(StorageCapability::Sql, OPERATION, error))?
2317 .map_err(|error| StorageError::driver(StorageCapability::Sql, OPERATION, error))?;
2318 crate::ensure_request_read_active(OPERATION)?;
2319 Ok(result)
2320 }
2321 _ = crate::wait_for_request_read_cancellation() => {
2322 stopped.store(true, Ordering::SeqCst);
2323 if tokio::time::timeout(crate::sqlite_interrupt_grace_from_env(), &mut worker)
2324 .await
2325 .is_err()
2326 {
2327 worker.abort();
2328 }
2329 stop_on_drop.armed = false;
2330 Err(StorageError::Timeout { operation: OPERATION.into() })
2331 }
2332 }
2333}
2334
2335fn inspect_pool(pool: &ConnectionPool) -> PoolInspection {
2342 let conn = match pool.open_standalone_writer_untracked() {
2343 Ok(conn) => conn,
2344 Err(e) => {
2345 let reason = format!("guarded standalone open refused: {e}");
2346 return PoolInspection {
2347 checkpoint_probe: None,
2348 checkpoint_probe_error: Some(reason.clone()),
2349 checkpoint_pin: checkpoint_pin_diagnostics(pool, None, Some(&reason)),
2350 size_composition: None,
2351 size_composition_error: Some(reason.clone()),
2352 page_accounting: None,
2353 page_accounting_error: Some(reason.clone()),
2354 graph_edge_integrity: None,
2355 graph_edge_integrity_error: Some(reason.clone()),
2356 fts_segments: None,
2357 fts_segments_error: Some(reason),
2358 };
2359 }
2360 };
2361
2362 let probe_result = checkpoint_probe(&conn);
2363 let (run_status, run_age) = record_diagnostic_checkpoint_probe(pool, &probe_result);
2364 let (checkpoint_probe, checkpoint_probe_error) = match probe_result {
2365 Ok(probe) => (Some(probe), None),
2366 Err(e) => (
2367 None,
2368 Some(format!("PRAGMA wal_checkpoint(PASSIVE) failed: {e}")),
2369 ),
2370 };
2371 let checkpoint_pin = checkpoint_pin_diagnostics_for_run(
2372 checkpoint_probe.as_ref(),
2373 checkpoint_probe_error.as_deref(),
2374 run_status,
2375 run_age,
2376 );
2377 let (graph_edge_integrity, graph_edge_integrity_error) = match graph_edge_integrity(&conn) {
2378 Ok(integrity) => (Some(integrity), None),
2379 Err(e) => (
2380 None,
2381 Some(format!("graph-edge integrity query failed: {e}")),
2382 ),
2383 };
2384 let (page_accounting, page_accounting_error) =
2385 split_page_accounting_result(database_page_accounting(&conn));
2386 let (size_composition, size_composition_error) = match database_size_composition(&conn) {
2387 Ok(composition) => (Some(composition), None),
2388 Err(error) => (
2389 None,
2390 Some(format!("database size composition query failed: {error}")),
2391 ),
2392 };
2393 let (fts_segments, fts_segments_error) =
2394 split_fts_segments_result(crate::fts_maintenance::inspect_fts_segments(&conn));
2395
2396 PoolInspection {
2397 checkpoint_probe,
2398 checkpoint_probe_error,
2399 checkpoint_pin,
2400 size_composition,
2401 size_composition_error,
2402 page_accounting,
2403 page_accounting_error,
2404 graph_edge_integrity,
2405 graph_edge_integrity_error,
2406 fts_segments,
2407 fts_segments_error,
2408 }
2409}
2410
2411#[cfg(test)]
2412#[path = "diagnostics_tests.rs"]
2413mod tests;