//! Lock-free WAL-state inspection shared by the `status` and `diagnostics` //! commands. //! //! [`gather_wal_state`] is the standalone path used by `status`: it lists //! segments, reads the checkpoint, and sizes the directory directly. //! [`WalState::from_report`] is the zero-extra-IO path used by `diagnostics`, //! which already holds a full [`WalDiagnosticReport`] and must not re-scan the //! directory or re-read the checkpoint. use serde::Serialize; use tidaldb::wal::diagnostics::WalDiagnosticReport; /// Read-only snapshot of a WAL directory's segment + checkpoint state. #[derive(Serialize)] pub(crate) struct WalState { pub(crate) segments: usize, pub(crate) first_seq: u64, pub(crate) last_segment_seq: u64, pub(crate) checkpoint_seq: u64, pub(crate) checkpoint_ts: u64, pub(crate) wal_dir_bytes: u64, } impl WalState { /// An empty WAL (no segments, no checkpoint, zero bytes). pub(crate) const fn empty() -> Self { Self { segments: 0, first_seq: 0, last_segment_seq: 0, checkpoint_seq: 0, checkpoint_ts: 0, wal_dir_bytes: 0, } } /// Project an already-computed [`WalDiagnosticReport`] into a [`WalState`]. /// /// `diagnostics` runs [`diagnose_wal`](tidaldb::wal::diagnostics::diagnose_wal) /// once and reuses its output here, so the segment scan and checkpoint read /// happen exactly one time per invocation. `last_segment_seq` mirrors the /// standalone [`gather_wal_state`] semantics: the `first_seq` of the last /// segment file (the legacy "last segment sequence", not the last event /// sequence). `wal_dir_bytes` uses the report's total segment bytes; unlike /// [`dir_size`] this excludes the checkpoint/session-journal sidecar files, /// which the diagnostics surface accounts for separately. pub(crate) fn from_report(report: &WalDiagnosticReport) -> Self { let first_seq = report.segments.first().map_or(0, |s| s.first_seq); let last_segment_seq = report.segments.last().map_or(0, |s| s.first_seq); Self { segments: report.segment_count, first_seq, last_segment_seq, checkpoint_seq: report.checkpoint_seq, checkpoint_ts: report.checkpoint_ts, wal_dir_bytes: report.total_segment_bytes, } } } /// Gather WAL state by directly scanning the directory (the `status` path). /// /// Returns an empty [`WalState`] when the WAL directory is absent (a fresh /// store is not an error). Returns `Err` when the directory exists but its /// segments, checkpoint, or size could not be read — the caller maps that to a /// degraded exit code. pub(crate) fn gather_wal_state(wal_dir: &std::path::Path) -> Result { if !wal_dir.exists() { return Ok(WalState::empty()); } let segments = tidaldb::wal::segment::list_segments(wal_dir).map_err(|e| format!("{e}"))?; let first_seq = segments.first().map_or(0, |(seq, _)| *seq); let last_segment_seq = segments.last().map_or(0, |(seq, _)| *seq); let (checkpoint_seq, checkpoint_ts) = match tidaldb::wal::checkpoint::CheckpointManager::read(wal_dir) { Ok(Some((seq, ts))) => (seq, ts), Ok(None) => (0, 0), Err(e) => return Err(format!("checkpoint read error: {e}")), }; let wal_dir_bytes = dir_size(wal_dir).map_err(|e| format!("{e}"))?; Ok(WalState { segments: segments.len(), first_seq, last_segment_seq, checkpoint_seq, checkpoint_ts, wal_dir_bytes, }) } /// Sum the byte size of every regular file directly in `dir` (non-recursive). pub(crate) fn dir_size(dir: &std::path::Path) -> Result { let mut total = 0u64; for entry in std::fs::read_dir(dir)? { let entry = entry?; let meta = entry.metadata()?; if meta.is_file() { total += meta.len(); } } Ok(total) }