Skip to main content

open_wal/
error.rs

1//! Error type for the WAL.
2//!
3//! `WalError` is a non-panicking enum (§10 of `docs/wal_design_v6.md`). The
4//! recovery parser MUST return these errors, never panic, for all inputs (D11).
5
6use std::fmt;
7
8use crate::Lsn;
9
10/// Convenience alias for results returned by this crate.
11pub type Result<T> = std::result::Result<T, WalError>;
12
13/// All error conditions surfaced by the WAL.
14///
15/// This is the normative `WalError` shape from §10. Variants beyond `Io` carry
16/// enough context (segment base LSN, byte offset) to locate the fault.
17#[derive(Debug)]
18#[non_exhaustive]
19pub enum WalError {
20    /// An underlying I/O error that is not itself a durability failure.
21    Io(std::io::Error),
22
23    /// A record or header failed CRC / structural validation mid-segment.
24    Corruption {
25        /// `base_lsn` of the segment containing the fault.
26        segment: Lsn,
27        /// Byte offset within the segment where the fault was detected.
28        offset: u64,
29        /// Short, static description of what was wrong.
30        detail: &'static str,
31    },
32
33    /// A valid record exists *after* a bad/torn one — a non-truncatable
34    /// internal gap. Fatal and loud, never silently truncated (D5).
35    TornMidLog {
36        /// `base_lsn` of the segment containing the fault.
37        segment: Lsn,
38        /// Byte offset within the segment of the torn record.
39        offset: u64,
40    },
41
42    /// An `append` payload exceeds `max_record_size` (no silent truncation,
43    /// no fragmentation in v1).
44    RecordTooLarge,
45
46    /// The supplied [`WalConfig`](crate::WalConfig) is invalid (e.g.
47    /// `max_record_size > segment_size - 91`, §5.3).
48    InvalidConfig,
49
50    /// A segment header has bad `magic` or `header_crc` (§5.2). The header is
51    /// written and synced at creation, so it is never a torn tail — always
52    /// fatal.
53    BadSegmentHeader,
54
55    /// The directory's exclusive writer lock is already held.
56    Locked,
57
58    /// A `write`/`fdatasync`/`fsync` failed. This **poisons** the handle
59    /// (§12); there is no safe resume for a dense-LSN log.
60    FsyncFailed,
61
62    /// Retained segments do not form a contiguous LSN suffix (§8.1).
63    ContiguityViolation,
64
65    /// The handle was poisoned by a prior durability failure (§12). All
66    /// subsequent `append`/`commit` calls return this.
67    Poisoned,
68}
69
70impl fmt::Display for WalError {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        match self {
73            WalError::Io(e) => write!(f, "I/O error: {e}"),
74            WalError::Corruption {
75                segment,
76                offset,
77                detail,
78            } => write!(
79                f,
80                "corruption in segment {segment} at offset {offset}: {detail}"
81            ),
82            WalError::TornMidLog { segment, offset } => {
83                write!(
84                    f,
85                    "torn record mid-log in segment {segment} at offset {offset}"
86                )
87            }
88            WalError::RecordTooLarge => write!(f, "record exceeds max_record_size"),
89            WalError::InvalidConfig => write!(f, "invalid WAL configuration"),
90            WalError::BadSegmentHeader => write!(f, "bad segment header"),
91            WalError::Locked => write!(f, "WAL directory is already locked by another writer"),
92            WalError::FsyncFailed => write!(f, "fsync failed; handle is poisoned"),
93            WalError::ContiguityViolation => write!(f, "retained segments are not contiguous"),
94            WalError::Poisoned => write!(f, "WAL handle is poisoned by a prior durability failure"),
95        }
96    }
97}
98
99impl std::error::Error for WalError {
100    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101        match self {
102            WalError::Io(e) => Some(e),
103            _ => None,
104        }
105    }
106}
107
108impl From<std::io::Error> for WalError {
109    fn from(e: std::io::Error) -> Self {
110        WalError::Io(e)
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn io_error_source_is_preserved() {
120        let io = std::io::Error::other("boom");
121        let err = WalError::from(io);
122        assert!(std::error::Error::source(&err).is_some());
123    }
124
125    #[test]
126    fn display_includes_location_context() {
127        let err = WalError::Corruption {
128            segment: Lsn(100001),
129            offset: 84,
130            detail: "crc mismatch",
131        };
132        let s = err.to_string();
133        assert!(s.contains("100001"));
134        assert!(s.contains("84"));
135        assert!(s.contains("crc mismatch"));
136    }
137}