tidaldb/tidal/src/wal/error.rs
jordan 4f076c927d feat: M0p1 runtime skeleton, M0p2 tooling & diagnostics, m1p4 signal ledger
## M0p1 — Embeddable Runtime Skeleton (329 tests)
- TidalDb with builder(), health_check(), close(), and Drop-based cleanup
- TidalDbBuilder fluent API: ephemeral(), with_data_dir(), wal_dir(), cache_dir()
- Config, StorageMode, ConfigError types; Config(ConfigError) variant on LumenError
- Paths: single source of truth for directory layout (wal, items, users, creators, cache)
- TempTidalHome: test isolation helper gated behind #[cfg(test)] / test-utils feature
- 8 integration tests: tests/sandboxed_storage.rs

## M0p2 — Tooling & Diagnostics (349 tests)
- Workspace root Cargo.toml (members: ["tidal", "tidalctl"])
- tidal/build.rs: BUILD_HASH from GIT_HASH with option_env!() fallback to "dev"
- MetricsState: always-compiled Arc-shared atomics (uptime, health_ok)
- MetricsHandle (metrics feature): hand-rolled TcpListener HTTP, zero new deps
  - GET /healthz → {"status":"ok","uptime_secs":N}
  - GET /metrics → Prometheus text (tidaldb_uptime_seconds, health_ok, info)
- TidalDbBuilder.enable_metrics(addr) starts background metrics thread
- tidalctl binary: status + paths commands, manual std::env::args() parsing
- 7 metrics integration tests, 9 tidalctl CLI tests

## m1p4 Signal Ledger (in-progress)
- SignalLedger: DashMap<(EntityId, SignalTypeId), EntitySignalEntry>, WAL-first writes
- HotSignalState: #[repr(C, align(64))], lock-free CAS decay, out-of-order handling
- BucketedCounter: 60 per-minute + 168 per-hour circular buffers, trigger-based rotation
- CheckpointMeta + serialize/restore: 983-byte fixed records, atomic WriteBatch
- Property tests: running score matches analytical to 1e-6, decay monotonic, non-negative
- Proptest regression: signals/warm.txt

## Documentation and planning
- ROADMAP: m0p1 COMPLETE (329), m0p2 COMPLETE (349), product track milestones
- PRODUCT_ROADMAP: P0-P4 product milestone track (personal briefing beachhead)
- Milestone planning docs: milestone-0 (phases 1-3), milestone-p (phases 1-5)
- docs/research/tidaldb_tooling_and_diagnostics.md
- ARCHITECTURE.md, CLAUDE.md, VISION.md updates

## Site
- Blog: every-platform-builds-the-same-6-systems.mdx (new)
- Blog: why-tidaldb.mdx (updated)
- next.config.ts, layout.tsx, blog/page.tsx updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 20:32:00 -07:00

123 lines
3.3 KiB
Rust

use std::fmt;
/// Errors originating from WAL operations.
///
/// Covers I/O failures, data corruption detected during recovery,
/// and lifecycle violations (e.g., appending after shutdown).
#[derive(Debug)]
pub enum WalError {
/// Underlying filesystem I/O failure.
Io(std::io::Error),
/// Data corruption detected (BLAKE3 mismatch, invalid magic, etc.).
Corruption { message: String },
/// Current segment is full; internal signal to trigger rotation.
SegmentFull,
/// Attempted append after WAL has been shut down.
Closed,
/// Channel send to writer thread failed (writer thread panicked or exited).
SendFailed,
/// Writer thread join failed during shutdown.
ShutdownFailed,
}
impl fmt::Display for WalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(source) => write!(f, "WAL I/O error: {source}"),
Self::Corruption { message } => write!(f, "WAL corruption: {message}"),
Self::SegmentFull => f.write_str("WAL segment full"),
Self::Closed => f.write_str("WAL closed"),
Self::SendFailed => f.write_str("WAL channel send failed"),
Self::ShutdownFailed => f.write_str("WAL shutdown failed"),
}
}
}
impl std::error::Error for WalError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(source) => Some(source),
_ => None,
}
}
}
impl From<std::io::Error> for WalError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn display_io() {
let e = WalError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file not found",
));
assert!(e.to_string().contains("I/O error"));
assert!(e.to_string().contains("file not found"));
}
#[test]
fn display_corruption() {
let e = WalError::Corruption {
message: "bad checksum".into(),
};
assert_eq!(e.to_string(), "WAL corruption: bad checksum");
}
#[test]
fn display_segment_full() {
assert_eq!(WalError::SegmentFull.to_string(), "WAL segment full");
}
#[test]
fn display_closed() {
assert_eq!(WalError::Closed.to_string(), "WAL closed");
}
#[test]
fn display_send_failed() {
assert_eq!(WalError::SendFailed.to_string(), "WAL channel send failed");
}
#[test]
fn display_shutdown_failed() {
assert_eq!(WalError::ShutdownFailed.to_string(), "WAL shutdown failed");
}
#[test]
fn from_io_error() {
let io_err = std::io::Error::other("disk full");
let wal_err: WalError = io_err.into();
assert!(matches!(wal_err, WalError::Io(_)));
}
#[test]
fn source_io() {
use std::error::Error;
let e = WalError::Io(std::io::Error::other("test"));
assert!(e.source().is_some());
}
#[test]
fn source_corruption_is_none() {
use std::error::Error;
let e = WalError::Corruption {
message: "test".into(),
};
assert!(e.source().is_none());
}
#[test]
fn source_closed_is_none() {
use std::error::Error;
assert!(WalError::Closed.source().is_none());
}
}