Skip to main content

reddb_file/
wal_header.rs

1//! Main WAL file header contract.
2//!
3//! The storage engine owns WAL record semantics. This module owns the file
4//! header bytes that let readers identify and version a WAL artifact.
5//!
6//! ```text
7//! [magic    4 bytes = b"RDBW"]
8//! [version  1 byte  = current format version]
9//! [reserved 3 bytes = zero]
10//! ```
11
12use std::io;
13
14pub const WAL_FILE_MAGIC: &[u8; 4] = b"RDBW";
15pub const WAL_FILE_VERSION: u8 = 3;
16pub const WAL_FILE_VERSION_V2: u8 = 2;
17pub const WAL_FILE_HEADER_BYTES: usize = 8;
18/// Size of one main-engine WAL segment/preallocation extent.
19///
20/// This is the file-contract boundary used by the server writer when it
21/// reserves disk blocks ahead of the append frontier. Runtime buffering and
22/// fsync policy stay in `reddb-server`; segment sizing lives here with the WAL
23/// artifact contract.
24pub const MAIN_WAL_SEGMENT_BYTES: u64 = 16 * 1024 * 1024;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct WalFileHeader {
28    pub version: u8,
29}
30
31pub fn encode_wal_file_header() -> [u8; WAL_FILE_HEADER_BYTES] {
32    let mut header = [0u8; WAL_FILE_HEADER_BYTES];
33    header[0..4].copy_from_slice(WAL_FILE_MAGIC);
34    header[4] = WAL_FILE_VERSION;
35    header
36}
37
38pub fn decode_wal_file_header(header: &[u8; WAL_FILE_HEADER_BYTES]) -> io::Result<WalFileHeader> {
39    if &header[0..4] != WAL_FILE_MAGIC {
40        return Err(io::Error::new(
41            io::ErrorKind::InvalidData,
42            "Invalid WAL magic bytes",
43        ));
44    }
45
46    let version = header[4];
47    if version != WAL_FILE_VERSION && version != WAL_FILE_VERSION_V2 {
48        return Err(io::Error::new(
49            io::ErrorKind::InvalidData,
50            format!("Unsupported WAL version: {version}"),
51        ));
52    }
53
54    Ok(WalFileHeader { version })
55}
56
57/// Next main-WAL segment boundary strictly above `pos`.
58///
59/// `pos` already at a boundary still rounds up to the following one, so a
60/// writer using this for preallocation always keeps at least one boundary ahead
61/// of the current append frontier.
62pub fn next_main_wal_segment_boundary(pos: u64) -> u64 {
63    (pos / MAIN_WAL_SEGMENT_BYTES + 1) * MAIN_WAL_SEGMENT_BYTES
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn wal_file_header_encodes_current_version() {
72        let header = encode_wal_file_header();
73        assert_eq!(&header[0..4], WAL_FILE_MAGIC);
74        assert_eq!(header[4], WAL_FILE_VERSION);
75        assert_eq!(
76            decode_wal_file_header(&header).unwrap().version,
77            WAL_FILE_VERSION
78        );
79    }
80
81    #[test]
82    fn wal_file_header_accepts_legacy_v2() {
83        let mut header = encode_wal_file_header();
84        header[4] = WAL_FILE_VERSION_V2;
85        assert_eq!(
86            decode_wal_file_header(&header).unwrap().version,
87            WAL_FILE_VERSION_V2
88        );
89    }
90
91    #[test]
92    fn wal_file_header_rejects_bad_magic_and_version() {
93        let mut bad_magic = encode_wal_file_header();
94        bad_magic[0] = b'X';
95        assert_eq!(
96            decode_wal_file_header(&bad_magic).unwrap_err().to_string(),
97            "Invalid WAL magic bytes"
98        );
99
100        let mut bad_version = encode_wal_file_header();
101        bad_version[4] = 99;
102        assert_eq!(
103            decode_wal_file_header(&bad_version)
104                .unwrap_err()
105                .to_string(),
106            "Unsupported WAL version: 99"
107        );
108    }
109
110    #[test]
111    fn main_wal_segment_boundary_rounds_strictly_above_position() {
112        assert_eq!(next_main_wal_segment_boundary(0), MAIN_WAL_SEGMENT_BYTES);
113        assert_eq!(next_main_wal_segment_boundary(8), MAIN_WAL_SEGMENT_BYTES);
114        assert_eq!(
115            next_main_wal_segment_boundary(MAIN_WAL_SEGMENT_BYTES - 1),
116            MAIN_WAL_SEGMENT_BYTES
117        );
118        assert_eq!(
119            next_main_wal_segment_boundary(MAIN_WAL_SEGMENT_BYTES),
120            2 * MAIN_WAL_SEGMENT_BYTES
121        );
122        assert_eq!(
123            next_main_wal_segment_boundary(MAIN_WAL_SEGMENT_BYTES + 1),
124            2 * MAIN_WAL_SEGMENT_BYTES
125        );
126    }
127}