rustlite_wal/
lib.rs

1// Write-Ahead Log (WAL) implementation for RustLite
2// Provides durable, crash-recoverable transaction logging
3
4use rustlite_core::Result;
5use serde::{Deserialize, Serialize};
6
7pub mod reader;
8pub mod record;
9pub mod recovery;
10pub mod segment;
11pub mod writer;
12
13pub use reader::WalReader;
14pub use record::{RecordPayload, RecordType, WalRecord};
15pub use recovery::{RecoveryManager, RecoveryStats};
16pub use segment::{SegmentInfo, SegmentManager};
17pub use writer::WalWriter;
18
19/// WAL configuration options
20#[derive(Debug, Clone)]
21pub struct WalConfig {
22    /// Sync mode: sync, async, or none
23    pub sync_mode: SyncMode,
24    /// Maximum segment size in bytes before rotation
25    pub max_segment_size: u64,
26    /// Directory path for WAL segments
27    pub wal_dir: std::path::PathBuf,
28}
29
30impl Default for WalConfig {
31    fn default() -> Self {
32        Self {
33            sync_mode: SyncMode::Sync,
34            max_segment_size: 64 * 1024 * 1024, // 64 MB
35            wal_dir: std::path::PathBuf::from("wal"),
36        }
37    }
38}
39
40/// Sync mode for WAL writes
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub enum SyncMode {
43    /// Call fsync after every write (strongest durability)
44    Sync,
45    /// Buffer writes, fsync on segment boundaries (balanced)
46    Async,
47    /// No fsync (fastest, unsafe for power loss)
48    None,
49}
50
51/// WAL manager coordinates log writing and recovery
52pub struct WalManager {
53    config: WalConfig,
54    writer: Option<WalWriter>,
55}
56
57impl WalManager {
58    pub fn new(config: WalConfig) -> Result<Self> {
59        Ok(Self {
60            config,
61            writer: None,
62        })
63    }
64
65    /// Open the WAL for writing
66    ///
67    /// This creates or opens the current WAL segment for appending records.
68    pub fn open(&mut self) -> Result<()> {
69        let writer = WalWriter::new(
70            &self.config.wal_dir,
71            self.config.max_segment_size,
72            self.config.sync_mode,
73        )?;
74        self.writer = Some(writer);
75
76        Ok(())
77    }
78
79    /// Append a record to the WAL
80    pub fn append(&mut self, record: WalRecord) -> Result<u64> {
81        let writer = self
82            .writer
83            .as_mut()
84            .ok_or_else(|| rustlite_core::Error::InvalidOperation("WAL not opened".to_string()))?;
85        writer.append(record)
86    }
87
88    /// Sync the WAL to disk
89    pub fn sync(&mut self) -> Result<()> {
90        if let Some(writer) = &mut self.writer {
91            writer.sync()
92        } else {
93            Ok(())
94        }
95    }
96
97    /// Close the WAL
98    pub fn close(&mut self) -> Result<()> {
99        if let Some(mut writer) = self.writer.take() {
100            writer.sync()?;
101        }
102        Ok(())
103    }
104
105    /// Recover records from the WAL
106    ///
107    /// This reads all segments and returns committed records for replay.
108    /// Incomplete transactions are rolled back (not included).
109    pub fn recover(&self) -> Result<Vec<WalRecord>> {
110        let recovery = RecoveryManager::new(self.config.clone())?;
111        recovery.recover()
112    }
113
114    /// Recover records with transaction markers included
115    ///
116    /// Unlike `recover()`, this includes BEGIN_TX and COMMIT_TX markers.
117    pub fn recover_with_markers(&self) -> Result<Vec<WalRecord>> {
118        let recovery = RecoveryManager::new(self.config.clone())?;
119        recovery.recover_with_markers()
120    }
121
122    /// Get statistics about the WAL
123    pub fn stats(&self) -> Result<RecoveryStats> {
124        let recovery = RecoveryManager::new(self.config.clone())?;
125        recovery.get_stats()
126    }
127
128    /// Create a reader for the WAL
129    pub fn reader(&self) -> Result<WalReader> {
130        WalReader::new(&self.config.wal_dir)
131    }
132
133    /// Get a segment manager for the WAL
134    pub fn segment_manager(&self) -> SegmentManager {
135        SegmentManager::new(self.config.wal_dir.clone())
136    }
137
138    /// Get the current configuration
139    pub fn config(&self) -> &WalConfig {
140        &self.config
141    }
142
143    /// Check if the WAL is open for writing
144    pub fn is_open(&self) -> bool {
145        self.writer.is_some()
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use tempfile::TempDir;
153
154    fn setup_test_config() -> (TempDir, WalConfig) {
155        let temp_dir = TempDir::new().expect("Failed to create temp dir");
156        let wal_path = temp_dir.path().join("wal");
157        std::fs::create_dir_all(&wal_path).expect("Failed to create WAL dir");
158
159        let config = WalConfig {
160            wal_dir: wal_path,
161            sync_mode: SyncMode::Sync,
162            max_segment_size: 64 * 1024 * 1024,
163        };
164
165        (temp_dir, config)
166    }
167
168    #[test]
169    fn test_wal_config_default() {
170        let config = WalConfig::default();
171        assert_eq!(config.sync_mode, SyncMode::Sync);
172        assert_eq!(config.max_segment_size, 64 * 1024 * 1024);
173    }
174
175    #[test]
176    fn test_sync_mode() {
177        assert_eq!(SyncMode::Sync, SyncMode::Sync);
178        assert_ne!(SyncMode::Sync, SyncMode::Async);
179    }
180
181    #[test]
182    fn test_wal_manager_lifecycle() {
183        let (_temp_dir, config) = setup_test_config();
184
185        let mut manager = WalManager::new(config).expect("Failed to create manager");
186        assert!(!manager.is_open());
187
188        manager.open().expect("Failed to open");
189        assert!(manager.is_open());
190
191        manager.close().expect("Failed to close");
192        assert!(!manager.is_open());
193    }
194
195    #[test]
196    fn test_wal_manager_write_and_recover() {
197        let (_temp_dir, config) = setup_test_config();
198
199        // Write some records
200        {
201            let mut manager = WalManager::new(config.clone()).expect("Failed to create manager");
202            manager.open().expect("Failed to open");
203
204            for i in 0..5 {
205                let record = WalRecord::put(
206                    format!("key{}", i).into_bytes(),
207                    format!("value{}", i).into_bytes(),
208                );
209                manager.append(record).expect("Failed to append");
210            }
211
212            manager.sync().expect("Failed to sync");
213            manager.close().expect("Failed to close");
214        }
215
216        // Recover
217        {
218            let manager = WalManager::new(config).expect("Failed to create manager");
219            let records = manager.recover().expect("Failed to recover");
220
221            assert_eq!(records.len(), 5);
222        }
223    }
224
225    #[test]
226    fn test_wal_manager_stats() {
227        let (_temp_dir, config) = setup_test_config();
228
229        // Write some records
230        {
231            let mut manager = WalManager::new(config.clone()).expect("Failed to create manager");
232            manager.open().expect("Failed to open");
233
234            manager.append(WalRecord::begin_tx(1)).expect("Failed");
235            manager.append(WalRecord::put(b"k".to_vec(), b"v".to_vec())).expect("Failed");
236            manager.append(WalRecord::commit_tx(1)).expect("Failed");
237
238            manager.close().expect("Failed to close");
239        }
240
241        let manager = WalManager::new(config).expect("Failed to create manager");
242        let stats = manager.stats().expect("Failed to get stats");
243
244        assert_eq!(stats.total_records, 3);
245        assert_eq!(stats.transactions_started, 1);
246        assert_eq!(stats.transactions_committed, 1);
247    }
248
249    #[test]
250    fn test_wal_manager_segment_manager() {
251        let (_temp_dir, config) = setup_test_config();
252
253        {
254            let mut manager = WalManager::new(config.clone()).expect("Failed to create manager");
255            manager.open().expect("Failed to open");
256            manager.append(WalRecord::put(b"k".to_vec(), b"v".to_vec())).expect("Failed");
257            manager.close().expect("Failed to close");
258        }
259
260        let manager = WalManager::new(config).expect("Failed to create manager");
261        let seg_manager = manager.segment_manager();
262
263        assert_eq!(seg_manager.segment_count().unwrap(), 1);
264    }
265}