1use std::fs::{File, OpenOptions};
13use std::io::{Read, Seek, SeekFrom, Write};
14use std::path::Path;
15
16use mentedb_core::error::{MenteError, MenteResult};
17use tracing::{debug, info, trace};
18
19pub type Lsn = u64;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[repr(u8)]
25pub enum WalEntryType {
26 PageWrite = 1,
27 Commit = 2,
29 Checkpoint = 3,
30 PageFree = 4,
32}
33
34impl TryFrom<u8> for WalEntryType {
35 type Error = MenteError;
36 fn try_from(v: u8) -> MenteResult<Self> {
37 match v {
38 1 => Ok(Self::PageWrite),
39 2 => Ok(Self::Commit),
40 3 => Ok(Self::Checkpoint),
41 4 => Ok(Self::PageFree),
42 _ => Err(MenteError::Storage(format!("invalid WAL entry type: {v}"))),
43 }
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct WalEntry {
50 pub lsn: u64,
52 pub entry_type: WalEntryType,
54 pub page_id: u64,
56 pub data: Vec<u8>,
58 pub checksum: u32,
60}
61
62pub struct Wal {
64 file: File,
65 dir_path: std::path::PathBuf,
66 next_lsn: u64,
67}
68
69const MIN_PAYLOAD: usize = 17;
71
72impl Wal {
73 pub fn open(dir_path: &Path) -> MenteResult<Self> {
75 let wal_path = dir_path.join("wal.log");
76 let exists = wal_path.exists()
77 && std::fs::metadata(&wal_path)
78 .map(|m| m.len() > 0)
79 .unwrap_or(false);
80
81 let file = OpenOptions::new()
82 .read(true)
83 .write(true)
84 .create(true)
85 .truncate(false)
86 .open(&wal_path)?;
87
88 let mut wal = Self {
89 file,
90 dir_path: dir_path.to_path_buf(),
91 next_lsn: 1,
92 };
93
94 if exists {
95 let entries = wal.read_all_entries()?;
96 if let Some(last) = entries.last() {
97 wal.next_lsn = last.lsn + 1;
98 }
99 info!(
100 next_lsn = wal.next_lsn,
101 entries = entries.len(),
102 "opened existing WAL"
103 );
104 } else {
105 info!("created new WAL");
106 }
107
108 Ok(wal)
109 }
110
111 pub fn lock_exclusive(&self) -> MenteResult<()> {
117 use fs2::FileExt;
118 self.file
119 .lock_exclusive()
120 .map_err(|e| MenteError::Storage(format!("WAL flock failed: {e}")))
121 }
122
123 pub fn unlock(&self) -> MenteResult<()> {
125 fs2::FileExt::unlock(&self.file)
126 .map_err(|e| MenteError::Storage(format!("WAL unlock failed: {e}")))
127 }
128
129 pub fn reload_lsn(&mut self) -> MenteResult<()> {
136 self.file.seek(SeekFrom::Start(0))?;
137 let file_len = self.file.metadata()?.len();
138 let mut offset: u64 = 0;
139 let mut last_lsn: Option<u64> = None;
140
141 while offset + 4 <= file_len {
142 let mut len_buf = [0u8; 4];
144 if self.file.read_exact(&mut len_buf).is_err() {
145 break;
146 }
147 let payload_len = u32::from_le_bytes(len_buf) as usize;
148 offset += 4;
149
150 if payload_len < MIN_PAYLOAD || offset + payload_len as u64 + 4 > file_len {
151 break;
152 }
153
154 let mut payload = vec![0u8; payload_len];
156 if self.file.read_exact(&mut payload).is_err() {
157 break;
158 }
159 offset += payload_len as u64;
160
161 let mut crc_buf = [0u8; 4];
163 if self.file.read_exact(&mut crc_buf).is_err() {
164 break;
165 }
166 let stored_crc = u32::from_le_bytes(crc_buf);
167 offset += 4;
168
169 let computed_crc = {
170 let mut h = crc32fast::Hasher::new();
171 h.update(&payload);
172 h.finalize()
173 };
174 if computed_crc != stored_crc {
175 break; }
177
178 let lsn = u64::from_le_bytes(payload[0..8].try_into().unwrap());
180 last_lsn = Some(lsn);
181 }
182
183 self.next_lsn = last_lsn.map_or(1, |l| l + 1);
184 debug!(next_lsn = self.next_lsn, "reloaded WAL LSN (fast scan)");
185 Ok(())
186 }
187
188 pub fn append(
190 &mut self,
191 entry_type: WalEntryType,
192 page_id: u64,
193 data: &[u8],
194 ) -> MenteResult<Lsn> {
195 let lsn = self.next_lsn;
196 self.next_lsn += 1;
197
198 let compressed = lz4_flex::compress_prepend_size(data);
199
200 let payload_len = 8 + 1 + 8 + compressed.len();
202 let mut payload = Vec::with_capacity(payload_len);
203 payload.extend_from_slice(&lsn.to_le_bytes());
204 payload.push(entry_type as u8);
205 payload.extend_from_slice(&page_id.to_le_bytes());
206 payload.extend_from_slice(&compressed);
207
208 let crc = {
209 let mut h = crc32fast::Hasher::new();
210 h.update(&payload);
211 h.finalize()
212 };
213
214 self.file.seek(SeekFrom::End(0))?;
215 self.file.write_all(&(payload_len as u32).to_le_bytes())?;
216 self.file.write_all(&payload)?;
217 self.file.write_all(&crc.to_le_bytes())?;
218
219 trace!(lsn, page_id, "appended WAL entry");
220 Ok(lsn)
221 }
222
223 pub fn sync(&mut self) -> MenteResult<()> {
225 self.file.sync_data()?;
226 debug!("WAL synced");
227 Ok(())
228 }
229
230 pub fn iterate(&mut self) -> MenteResult<Vec<WalEntry>> {
232 self.read_all_entries()
233 }
234
235 pub fn truncate(&mut self, before_lsn: Lsn) -> MenteResult<()> {
239 let entries = self.read_all_entries()?;
240 let to_keep: Vec<&WalEntry> = entries.iter().filter(|e| e.lsn >= before_lsn).collect();
241
242 let wal_path = self.dir_path.join("wal.log");
243 let tmp_path = self.dir_path.join("wal.log.tmp");
244
245 {
246 let mut tmp_file = OpenOptions::new()
247 .write(true)
248 .create(true)
249 .truncate(true)
250 .open(&tmp_path)?;
251
252 for entry in to_keep {
253 let compressed = lz4_flex::compress_prepend_size(&entry.data);
254
255 let payload_len = 8 + 1 + 8 + compressed.len();
256 let mut payload = Vec::with_capacity(payload_len);
257 payload.extend_from_slice(&entry.lsn.to_le_bytes());
258 payload.push(entry.entry_type as u8);
259 payload.extend_from_slice(&entry.page_id.to_le_bytes());
260 payload.extend_from_slice(&compressed);
261
262 let crc = {
263 let mut h = crc32fast::Hasher::new();
264 h.update(&payload);
265 h.finalize()
266 };
267
268 tmp_file.write_all(&(payload_len as u32).to_le_bytes())?;
269 tmp_file.write_all(&payload)?;
270 tmp_file.write_all(&crc.to_le_bytes())?;
271 }
272
273 tmp_file.sync_data()?;
274 }
275
276 std::fs::rename(&tmp_path, &wal_path)?;
277
278 let new_file = OpenOptions::new().read(true).write(true).open(&wal_path)?;
281 fs2::FileExt::lock_exclusive(&new_file)
282 .map_err(|e| MenteError::Storage(format!("WAL flock re-acquire failed: {e}")))?;
283 self.file = new_file;
284
285 debug!(before_lsn, "WAL truncated (atomic)");
286 Ok(())
287 }
288
289 pub fn next_lsn(&self) -> Lsn {
291 self.next_lsn
292 }
293
294 pub fn file_size(&self) -> u64 {
296 self.file.metadata().map(|m| m.len()).unwrap_or(0)
297 }
298
299 fn read_all_entries(&mut self) -> MenteResult<Vec<WalEntry>> {
302 self.file.seek(SeekFrom::Start(0))?;
303 let file_len = self.file.metadata()?.len();
304 let mut offset: u64 = 0;
305 let mut entries = Vec::new();
306
307 while offset + 4 <= file_len {
308 let mut len_buf = [0u8; 4];
310 if self.file.read_exact(&mut len_buf).is_err() {
311 break;
312 }
313 let payload_len = u32::from_le_bytes(len_buf) as usize;
314 offset += 4;
315
316 if payload_len < MIN_PAYLOAD || offset + payload_len as u64 + 4 > file_len {
317 break;
318 }
319
320 let mut payload = vec![0u8; payload_len];
322 if self.file.read_exact(&mut payload).is_err() {
323 break;
324 }
325 offset += payload_len as u64;
326
327 let mut crc_buf = [0u8; 4];
329 if self.file.read_exact(&mut crc_buf).is_err() {
330 break;
331 }
332 let stored_crc = u32::from_le_bytes(crc_buf);
333 offset += 4;
334
335 let computed_crc = {
337 let mut h = crc32fast::Hasher::new();
338 h.update(&payload);
339 h.finalize()
340 };
341 if computed_crc != stored_crc {
342 break; }
344
345 let lsn = u64::from_le_bytes(payload[0..8].try_into().unwrap());
347 let entry_type = match WalEntryType::try_from(payload[8]) {
348 Ok(t) => t,
349 Err(_) => break,
350 };
351 let page_id = u64::from_le_bytes(payload[9..17].try_into().unwrap());
352 let compressed_data = &payload[17..];
353
354 let data = lz4_flex::decompress_size_prepended(compressed_data)
355 .map_err(|e| MenteError::Storage(format!("LZ4 decompress failed: {e}")))?;
356
357 entries.push(WalEntry {
358 lsn,
359 entry_type,
360 page_id,
361 data,
362 checksum: stored_crc,
363 });
364 }
365
366 Ok(entries)
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 fn setup() -> (tempfile::TempDir, Wal) {
375 let dir = tempfile::tempdir().unwrap();
376 let wal = Wal::open(dir.path()).unwrap();
377 (dir, wal)
378 }
379
380 #[test]
381 fn test_append_and_iterate() {
382 let (_dir, mut wal) = setup();
383
384 let lsn1 = wal.append(WalEntryType::PageWrite, 1, b"hello").unwrap();
385 let lsn2 = wal.append(WalEntryType::PageWrite, 2, b"world").unwrap();
386 assert_eq!(lsn1, 1);
387 assert_eq!(lsn2, 2);
388
389 let entries = wal.iterate().unwrap();
390 assert_eq!(entries.len(), 2);
391 assert_eq!(entries[0].lsn, 1);
392 assert_eq!(entries[0].data, b"hello");
393 assert_eq!(entries[1].lsn, 2);
394 assert_eq!(entries[1].data, b"world");
395 }
396
397 #[test]
398 fn test_sync() {
399 let (_dir, mut wal) = setup();
400 wal.append(WalEntryType::Commit, 0, b"").unwrap();
401 wal.sync().unwrap(); }
403
404 #[test]
405 fn test_truncate() {
406 let (_dir, mut wal) = setup();
407
408 wal.append(WalEntryType::PageWrite, 1, b"a").unwrap();
409 wal.append(WalEntryType::PageWrite, 2, b"b").unwrap();
410 wal.append(WalEntryType::Checkpoint, 0, b"").unwrap();
411
412 wal.truncate(3).unwrap();
414
415 let entries = wal.iterate().unwrap();
416 assert_eq!(entries.len(), 1);
417 assert_eq!(entries[0].lsn, 3);
418 }
419
420 #[test]
421 fn test_recovery_reopen() {
422 let dir = tempfile::tempdir().unwrap();
423 {
424 let mut wal = Wal::open(dir.path()).unwrap();
425 wal.append(WalEntryType::PageWrite, 10, b"recovery-data")
426 .unwrap();
427 wal.sync().unwrap();
428 }
429 {
430 let mut wal = Wal::open(dir.path()).unwrap();
431 assert_eq!(wal.next_lsn(), 2);
432 let entries = wal.iterate().unwrap();
433 assert_eq!(entries.len(), 1);
434 assert_eq!(entries[0].page_id, 10);
435 assert_eq!(entries[0].data, b"recovery-data");
436 }
437 }
438
439 #[test]
440 fn test_empty_data_entry() {
441 let (_dir, mut wal) = setup();
442 let lsn = wal.append(WalEntryType::Checkpoint, 0, b"").unwrap();
443 let entries = wal.iterate().unwrap();
444 assert_eq!(entries.len(), 1);
445 assert_eq!(entries[0].lsn, lsn);
446 assert!(entries[0].data.is_empty());
447 }
448
449 #[test]
450 fn test_large_data_compression() {
451 let (_dir, mut wal) = setup();
452 let big_data = vec![0xABu8; 8192];
453 wal.append(WalEntryType::PageWrite, 5, &big_data).unwrap();
454
455 let entries = wal.iterate().unwrap();
456 assert_eq!(entries.len(), 1);
457 assert_eq!(entries[0].data, big_data);
458 }
459
460 #[test]
461 fn test_append_then_sync_is_durable() {
462 let dir = tempfile::tempdir().unwrap();
465 {
466 let mut wal = Wal::open(dir.path()).unwrap();
467 wal.append(WalEntryType::PageWrite, 1, b"batch1").unwrap();
468 wal.append(WalEntryType::PageWrite, 2, b"batch2").unwrap();
469 wal.sync().unwrap();
470 }
471 {
472 let mut wal = Wal::open(dir.path()).unwrap();
473 let entries = wal.iterate().unwrap();
474 assert_eq!(entries.len(), 2);
475 assert_eq!(entries[0].data, b"batch1");
476 assert_eq!(entries[1].data, b"batch2");
477 }
478 }
479
480 #[test]
481 fn test_truncate_atomic_preserves_kept_entries() {
482 let dir = tempfile::tempdir().unwrap();
483 {
484 let mut wal = Wal::open(dir.path()).unwrap();
485 wal.append(WalEntryType::PageWrite, 1, b"old1").unwrap();
486 wal.append(WalEntryType::PageWrite, 2, b"old2").unwrap();
487 wal.append(WalEntryType::PageWrite, 3, b"keep1").unwrap();
488 wal.append(WalEntryType::PageWrite, 4, b"keep2").unwrap();
489
490 wal.truncate(3).unwrap();
491
492 let entries = wal.iterate().unwrap();
493 assert_eq!(entries.len(), 2);
494 assert_eq!(entries[0].data, b"keep1");
495 assert_eq!(entries[1].data, b"keep2");
496 }
497 {
499 let mut wal = Wal::open(dir.path()).unwrap();
500 let entries = wal.iterate().unwrap();
501 assert_eq!(entries.len(), 2);
502 assert_eq!(entries[0].lsn, 3);
503 assert_eq!(entries[1].lsn, 4);
504 }
505 }
506
507 #[test]
508 fn test_truncate_no_temp_file_left_behind() {
509 let dir = tempfile::tempdir().unwrap();
510 let mut wal = Wal::open(dir.path()).unwrap();
511 wal.append(WalEntryType::PageWrite, 1, b"a").unwrap();
512 wal.truncate(2).unwrap();
513
514 assert!(!dir.path().join("wal.log.tmp").exists());
516 }
517
518 #[test]
519 fn test_append_after_truncate_works() {
520 let (_dir, mut wal) = setup();
521 wal.append(WalEntryType::PageWrite, 1, b"before").unwrap();
522 wal.truncate(2).unwrap();
523
524 wal.append(WalEntryType::PageWrite, 10, b"after").unwrap();
526 let entries = wal.iterate().unwrap();
527 assert_eq!(entries.len(), 1);
528 assert_eq!(entries[0].data, b"after");
529 }
530}