Skip to main content

open_wal/
lib.rs

1//! `open-wal` — a focused, embeddable, single-writer append-only write-ahead
2//! log for an LMAX-style, event-sourced system.
3//!
4//! **Durability-first:** a committed record survives process crash and power
5//! loss on honest hardware. The WAL stores **opaque byte payloads** —
6//! serialization is entirely the caller's concern. It is not a database, not
7//! multi-writer, and runs no background threads.
8//!
9//! The normative design lives in `docs/wal_design_v6.md`; the durability
10//! invariants D1–D12 there are binding on every change.
11//!
12//! This crate is built in milestones (§13). **M0 (foundations)** provides the
13//! core value types — [`Lsn`], [`WalConfig`], [`WalError`] — and the CRC-32C
14//! checksum ([`crc32c`]). **M1** adds the internal record codec (`record` —
15//! encode/decode of the §5.3 framing). **M2** adds the single-segment write
16//! path and replay: [`Wal::open`]/[`append`](Wal::append)/[`commit`](Wal::commit),
17//! a streaming [`Reader`], the [`DurabilityObserver`] hook, segment
18//! pre-allocation and `fdatasync`, and a zero-allocation hot path. **M3** adds
19//! intra-segment crash recovery (torn-tail detection + durable zeroing, fatal
20//! mid-log corruption). **M4** adds the multi-segment write path — segment roll,
21//! commit-time whole-record split, sealed-segment immutability — and
22//! multi-segment recovery (discovery, cross-segment continuity, crash-during-roll
23//! handling). Checkpoint/retention (M5) arrives later.
24
25// This is an embeddable library; every public item must be documented. With
26// CI's `clippy -D warnings`, an undocumented public item fails the build.
27#![warn(missing_docs)]
28
29mod config;
30mod crc;
31mod error;
32mod lsn;
33mod observer;
34mod reader;
35mod record;
36mod recovery;
37mod segment;
38mod wal;
39
40pub use config::WalConfig;
41pub use crc::crc32c;
42pub use error::{Result, WalError};
43pub use lsn::Lsn;
44pub use observer::{DurabilityObserver, NullObserver};
45pub use reader::Reader;
46pub use wal::{RecoveryReport, TailState, Wal};
47
48/// Internal hooks exposed **only** under the `fuzzing` feature for the M9
49/// cargo-fuzz targets (§14.5). Not part of the public API or the §6 surface:
50/// `#[doc(hidden)]` and feature-gated, so a normal build neither sees nor pays
51/// for any of it.
52///
53/// **The public path stays the source of truth.** The F1 recovery fuzzer's
54/// *primary* surface is the real public [`Wal::open`] driven over an adversarial
55/// directory of segment files — that is the production entry point and the thing
56/// actually under test (filename parse → discovery → sort → header validate →
57/// cross-segment continuity → `recover_segment`). The helpers here are for the
58/// *secondary direct-probe mode only* (the bounded-scan counter and the isolated
59/// single-record decoder), plus input *generators* the fuzzer uses to craft
60/// valid bytes to feed that public path. They must never become the thing being
61/// tested in place of production.
62#[cfg(feature = "fuzzing")]
63#[doc(hidden)]
64pub mod fuzzing {
65    use std::fs::File;
66
67    use crate::Lsn;
68
69    /// F2 / secondary: decode one framed record from arbitrary bytes, bounded by
70    /// `max_record_size`. Returns `Some((payload_len, framed_len))` for a decoded
71    /// record (so the harness can assert `payload_len <= max_record_size` and
72    /// `framed_len <= buf.len()` — bounds-soundness), `None` for any non-record
73    /// outcome. Never panics or reads OOB for any input (D11, record level).
74    #[must_use]
75    pub fn decode_record(buf: &[u8], max_record_size: u32) -> Option<(usize, usize)> {
76        match crate::record::decode(buf, max_record_size) {
77            crate::record::Decoded::Record {
78                payload,
79                framed_len,
80                ..
81            } => Some((payload.len(), framed_len)),
82            _ => None,
83        }
84    }
85
86    /// F1 secondary direct-probe mode: run intra-segment recovery (§8.2) over a
87    /// single open segment `file`, then assert the bounded forward-scan probe
88    /// stayed within [`scan_bound`]. Returns whether recovery succeeded; both
89    /// `Ok` and a clean `Err` are acceptable (D11), only a panic / OOB / unbounded
90    /// scan is a bug. The caller is responsible for the header-validated `base_lsn`
91    /// contract that the real `open` enforces upstream (here we pass arbitrary
92    /// bases deliberately, but `base_lsn == 0` would underflow `base_lsn - 1`, so
93    /// it is clamped to 1 — the lowest legal base).
94    #[must_use]
95    pub fn recover_segment_probe(
96        file: &File,
97        base_lsn: u64,
98        is_active: bool,
99        segment_size: u64,
100        max_record_size: u32,
101    ) -> bool {
102        scan_probe_reset();
103        let base = Lsn(base_lsn.max(1));
104        let r =
105            crate::recovery::recover_segment(file, base, is_active, segment_size, max_record_size);
106        let peak = scan_probe_peak();
107        assert!(
108            peak <= scan_bound(max_record_size),
109            "bounded-scan probe peak {peak} exceeded scan_bound {} (max_record_size {max_record_size})",
110            scan_bound(max_record_size),
111        );
112        r.is_ok()
113    }
114
115    /// The production bounded forward-scan distance limit, exposed so a fuzz
116    /// harness asserts against the **same symbol** the scan loop uses (never a
117    /// re-typed number that could silently drift from production).
118    #[must_use]
119    pub fn scan_bound(max_record_size: u32) -> u64 {
120        crate::recovery::scan_bound(max_record_size)
121    }
122
123    /// Reset the bounded-scan peak before a recovery run the harness will
124    /// bound-check (e.g. around the real [`Wal::open`](crate::Wal::open)).
125    pub fn scan_probe_reset() {
126        crate::recovery::scan_probe::reset();
127    }
128
129    /// The largest forward-scan distance observed since [`scan_probe_reset`].
130    #[must_use]
131    pub fn scan_probe_peak() -> u64 {
132        crate::recovery::scan_probe::peak()
133    }
134
135    /// Generator helper: the 64-byte segment header bytes for `base_lsn` (the
136    /// `created_unix_nanos` field is informational and fixed to 0 — it never
137    /// influences recovery, §8.6). Lets the fuzzer craft *valid* segment files so
138    /// the public `Wal::open` reaches discovery / continuity / `recover_segment`
139    /// with fuzzer-chosen bases, rather than always tripping on a bad header.
140    #[must_use]
141    pub fn segment_header_bytes(base_lsn: u64) -> Vec<u8> {
142        crate::segment::encode_header(Lsn(base_lsn), 0).to_vec()
143    }
144
145    /// Generator helper: append one valid framed record for `lsn`/`payload` to
146    /// `buf`, returning the framed byte count. Used to build dense, valid segment
147    /// bodies for the public-path fuzzer.
148    pub fn encode_record_into(buf: &mut Vec<u8>, lsn: u64, payload: &[u8]) -> usize {
149        crate::record::encode_into(buf, Lsn(lsn), payload)
150    }
151
152    /// The production per-segment recovery **classification** (§8.2), flattened to
153    /// a plain value for the §14.9 differential tester (`tests/differential.rs`).
154    /// This is the exact classification surface an independent reference parser
155    /// must reproduce byte-for-byte; a divergence is a recovery-classifier bug.
156    ///
157    /// `Truncated`/`Clean` carry `max_lsn` and the truncation `offset`; the two
158    /// fatal arms carry the failing `offset`. `OtherErr` catches any error the
159    /// single-segment `recover_segment` is not expected to produce here (e.g. an
160    /// I/O error) so the differential can flag it rather than silently coerce it.
161    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
162    pub enum SegClass {
163        /// Clean end of records; `max_lsn` is the highest valid LSN (`base-1` if
164        /// the segment is empty).
165        Clean { max_lsn: u64 },
166        /// Active-segment torn tail truncated at `offset`; `max_lsn` is the last
167        /// valid record before it.
168        Truncated { offset: u64, max_lsn: u64 },
169        /// Mid-log corruption: an invalid record with a valid record still ahead
170        /// within the bounded forward scan (active segment) — fatal (D5).
171        TornMidLog { offset: u64 },
172        /// Any invalid record in a sealed segment — fatal, no forward scan (D5).
173        Corruption { offset: u64 },
174        /// An error outside the classification surface (e.g. I/O). The differential
175        /// treats this as its own class so it is never silently equated.
176        OtherErr,
177    }
178
179    /// Run the **real** production `recover_segment` (§8.2) over one open segment
180    /// `file` and return its classification. Used only by the §14.9 differential
181    /// tester to compare production against an independent reference parser.
182    ///
183    /// NOTE: on a torn tail this performs the production durable zeroing of
184    /// `[offset, EOF)` (§8.2.1) as a side effect — so the differential must pass
185    /// production its **own** copy of the segment file and read the classification
186    /// from this return value, never re-derive it from the mutated file.
187    #[must_use]
188    pub fn recover_segment_classify(
189        file: &File,
190        base_lsn: u64,
191        is_active: bool,
192        segment_size: u64,
193        max_record_size: u32,
194    ) -> SegClass {
195        use crate::error::WalError;
196        use crate::wal::TailState;
197        let base = Lsn(base_lsn.max(1));
198        match crate::recovery::recover_segment(file, base, is_active, segment_size, max_record_size)
199        {
200            Ok(rec) => match rec.tail_state {
201                TailState::Clean => SegClass::Clean {
202                    max_lsn: rec.max_lsn.0,
203                },
204                TailState::TruncatedAt { offset, .. } => SegClass::Truncated {
205                    offset,
206                    max_lsn: rec.max_lsn.0,
207                },
208            },
209            Err(WalError::TornMidLog { offset, .. }) => SegClass::TornMidLog { offset },
210            Err(WalError::Corruption { offset, .. }) => SegClass::Corruption { offset },
211            Err(_) => SegClass::OtherErr,
212        }
213    }
214}