tidaldb/tidalctl/src/wal_state.rs
jx12n b55ad70141 fix: M0-M10 third-pass remediation — durability, replication, and CLI hardening
Resolves the 142 findings from tidal/docs/reviews/CODE_REVIEW_m0-m10.md across
the engine, server, net, and CLI surfaces:

- WAL/session-journal durability, checkpoint format, and crash-recovery hardening
- Replication shipper/receiver, tenant isolation, and migration paths
- Cluster scatter-gather, router, standalone server + health/offload endpoints
- tidalctl refactored into command modules with JSON output and WAL-state tooling
- Cohort, governance, signal-ledger, and vector-registry correctness fixes
- Expanded UAT/integration/durability test coverage across all milestones
2026-06-08 10:28:34 -06:00

109 lines
4.0 KiB
Rust

//! 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<WalState, String> {
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<u64, std::io::Error> {
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)
}