Skip to main content

actionqueue_storage/wal/
repair.rs

1//! WAL file repair utilities.
2//!
3//! This module provides controlled truncation of trailing partial records
4//! in a WAL file, enabling recovery from crashes that left incomplete writes.
5
6use std::fs::OpenOptions;
7use std::io;
8
9/// Truncates a WAL file to the given `valid_end_offset`.
10///
11/// This removes any trailing bytes after the last complete, valid record.
12/// The file is synced after truncation to ensure durability.
13///
14/// # Safety
15///
16/// The caller must ensure that `valid_end_offset` actually points to a valid
17/// record boundary. Using an incorrect offset will corrupt the WAL.
18pub fn truncate_to_last_valid(path: &std::path::Path, valid_end_offset: u64) -> io::Result<()> {
19    let file = OpenOptions::new().write(true).open(path)?;
20    file.set_len(valid_end_offset)?;
21    file.sync_all()?;
22    Ok(())
23}
24
25/// Policy controlling how the WAL writer handles trailing corruption on bootstrap.
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27pub enum RepairPolicy {
28    /// Current behavior: hard-fail on any trailing corruption.
29    #[default]
30    Strict,
31    /// Truncate trailing partial record and continue.
32    /// Only the incomplete trailing record is removed; mid-stream corruption
33    /// still hard-fails.
34    TruncatePartial,
35}
36
37#[cfg(test)]
38mod tests {
39    use std::fs;
40    use std::io::Write;
41    use std::sync::atomic::{AtomicUsize, Ordering};
42
43    use super::*;
44
45    static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
46
47    fn temp_path() -> std::path::PathBuf {
48        let dir = std::env::temp_dir();
49        let count = TEST_COUNTER.fetch_add(1, Ordering::SeqCst);
50        let path =
51            dir.join(format!("actionqueue_wal_repair_test_{}_{}.tmp", std::process::id(), count));
52        let _ = fs::remove_file(&path);
53        path
54    }
55
56    #[test]
57    fn truncate_removes_trailing_bytes() {
58        let path = temp_path();
59        {
60            let mut file = fs::File::create(&path).unwrap();
61            file.write_all(b"valid_record_data_here_extra_junk").unwrap();
62        }
63
64        truncate_to_last_valid(&path, 22).unwrap();
65
66        let contents = fs::read(&path).unwrap();
67        assert_eq!(&contents, b"valid_record_data_here");
68
69        let _ = fs::remove_file(&path);
70    }
71
72    #[test]
73    fn truncate_to_zero_empties_file() {
74        let path = temp_path();
75        {
76            let mut file = fs::File::create(&path).unwrap();
77            file.write_all(b"some data").unwrap();
78        }
79
80        truncate_to_last_valid(&path, 0).unwrap();
81
82        let contents = fs::read(&path).unwrap();
83        assert!(contents.is_empty());
84
85        let _ = fs::remove_file(&path);
86    }
87}