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 RecordWrite = 5,
34 RecordFree = 6,
36}
37
38impl TryFrom<u8> for WalEntryType {
39 type Error = MenteError;
40 fn try_from(v: u8) -> MenteResult<Self> {
41 match v {
42 1 => Ok(Self::PageWrite),
43 2 => Ok(Self::Commit),
44 3 => Ok(Self::Checkpoint),
45 4 => Ok(Self::PageFree),
46 5 => Ok(Self::RecordWrite),
47 6 => Ok(Self::RecordFree),
48 _ => Err(MenteError::Storage(format!("invalid WAL entry type: {v}"))),
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct WalEntry {
56 pub lsn: u64,
58 pub entry_type: WalEntryType,
60 pub page_id: u64,
62 pub data: Vec<u8>,
64 pub checksum: u32,
66}
67
68pub struct Wal {
70 file: File,
71 path: std::path::PathBuf,
72 next_lsn: u64,
73 torn_tail: bool,
77}
78
79const MIN_PAYLOAD: usize = 17;
81
82fn encode_entry(lsn: Lsn, entry_type: WalEntryType, page_id: u64, data: &[u8]) -> Vec<u8> {
84 let compressed = lz4_flex::compress_prepend_size(data);
85 let payload_len = 8 + 1 + 8 + compressed.len();
86 let mut buf = Vec::with_capacity(4 + payload_len + 4);
87 buf.extend_from_slice(&(payload_len as u32).to_le_bytes());
88 buf.extend_from_slice(&lsn.to_le_bytes());
89 buf.push(entry_type as u8);
90 buf.extend_from_slice(&page_id.to_le_bytes());
91 buf.extend_from_slice(&compressed);
92 let crc = crc32fast::hash(&buf[4..]);
93 buf.extend_from_slice(&crc.to_le_bytes());
94 buf
95}
96
97impl Wal {
98 pub fn open(dir_path: &Path) -> MenteResult<Self> {
100 Self::open_file(&dir_path.join("wal.log"))
101 }
102
103 pub fn open_file(wal_path: &Path) -> MenteResult<Self> {
105 let exists = wal_path.exists()
106 && std::fs::metadata(wal_path)
107 .map(|m| m.len() > 0)
108 .unwrap_or(false);
109
110 let file = OpenOptions::new()
111 .read(true)
112 .write(true)
113 .create(true)
114 .truncate(false)
115 .open(wal_path)?;
116
117 let mut wal = Self {
118 file,
119 path: wal_path.to_path_buf(),
120 next_lsn: 1,
121 torn_tail: false,
122 };
123
124 if exists {
125 let entries = wal.read_all_entries()?;
126 if let Some(last) = entries.last() {
127 wal.next_lsn = last.lsn + 1;
128 }
129 info!(
130 next_lsn = wal.next_lsn,
131 entries = entries.len(),
132 "opened existing WAL"
133 );
134 } else {
135 info!("created new WAL");
136 }
137
138 Ok(wal)
139 }
140
141 pub fn lock_exclusive(&self) -> MenteResult<()> {
147 use fs2::FileExt;
148 self.file
149 .lock_exclusive()
150 .map_err(|e| MenteError::Storage(format!("WAL flock failed: {e}")))
151 }
152
153 pub fn unlock(&self) -> MenteResult<()> {
155 fs2::FileExt::unlock(&self.file)
156 .map_err(|e| MenteError::Storage(format!("WAL unlock failed: {e}")))
157 }
158
159 pub fn reload_lsn(&mut self) -> MenteResult<()> {
166 self.file.seek(SeekFrom::Start(0))?;
167 let file_len = self.file.metadata()?.len();
168 let mut offset: u64 = 0;
169 let mut last_lsn: Option<u64> = None;
170
171 while offset + 4 <= file_len {
172 let mut len_buf = [0u8; 4];
174 if self.file.read_exact(&mut len_buf).is_err() {
175 break;
176 }
177 let payload_len = u32::from_le_bytes(len_buf) as usize;
178 offset += 4;
179
180 if payload_len < MIN_PAYLOAD || offset + payload_len as u64 + 4 > file_len {
181 break;
182 }
183
184 let mut payload = vec![0u8; payload_len];
186 if self.file.read_exact(&mut payload).is_err() {
187 break;
188 }
189 offset += payload_len as u64;
190
191 let mut crc_buf = [0u8; 4];
193 if self.file.read_exact(&mut crc_buf).is_err() {
194 break;
195 }
196 let stored_crc = u32::from_le_bytes(crc_buf);
197 offset += 4;
198
199 let computed_crc = {
200 let mut h = crc32fast::Hasher::new();
201 h.update(&payload);
202 h.finalize()
203 };
204 if computed_crc != stored_crc {
205 break; }
207
208 let lsn = u64::from_le_bytes(payload[0..8].try_into().unwrap());
210 last_lsn = Some(lsn);
211 }
212
213 self.next_lsn = last_lsn.map_or(1, |l| l + 1);
214 debug!(next_lsn = self.next_lsn, "reloaded WAL LSN (fast scan)");
215 Ok(())
216 }
217
218 pub fn append(
220 &mut self,
221 entry_type: WalEntryType,
222 page_id: u64,
223 data: &[u8],
224 ) -> MenteResult<Lsn> {
225 let lsn = self.next_lsn;
226 self.next_lsn += 1;
227
228 let entry = encode_entry(lsn, entry_type, page_id, data);
232 self.file.seek(SeekFrom::End(0))?;
233 self.file.write_all(&entry)?;
234
235 trace!(lsn, page_id, "appended WAL entry");
236 Ok(lsn)
237 }
238
239 pub fn sync(&mut self) -> MenteResult<()> {
241 self.file.sync_data()?;
242 debug!("WAL synced");
243 Ok(())
244 }
245
246 pub fn iterate(&mut self) -> MenteResult<Vec<WalEntry>> {
248 self.read_all_entries()
249 }
250
251 pub fn truncate(&mut self, before_lsn: Lsn) -> MenteResult<()> {
255 let entries = self.read_all_entries()?;
256 let to_keep: Vec<&WalEntry> = entries.iter().filter(|e| e.lsn >= before_lsn).collect();
257
258 let wal_path = self.path.clone();
259 let tmp_path = self.tmp_path();
260
261 {
262 let mut tmp_file = OpenOptions::new()
263 .write(true)
264 .create(true)
265 .truncate(true)
266 .open(&tmp_path)?;
267
268 for entry in to_keep {
269 let compressed = lz4_flex::compress_prepend_size(&entry.data);
270
271 let payload_len = 8 + 1 + 8 + compressed.len();
272 let mut payload = Vec::with_capacity(payload_len);
273 payload.extend_from_slice(&entry.lsn.to_le_bytes());
274 payload.push(entry.entry_type as u8);
275 payload.extend_from_slice(&entry.page_id.to_le_bytes());
276 payload.extend_from_slice(&compressed);
277
278 let crc = {
279 let mut h = crc32fast::Hasher::new();
280 h.update(&payload);
281 h.finalize()
282 };
283
284 tmp_file.write_all(&(payload_len as u32).to_le_bytes())?;
285 tmp_file.write_all(&payload)?;
286 tmp_file.write_all(&crc.to_le_bytes())?;
287 }
288
289 tmp_file.sync_data()?;
290 }
291
292 std::fs::rename(&tmp_path, &wal_path)?;
293
294 let new_file = OpenOptions::new().read(true).write(true).open(&wal_path)?;
297 fs2::FileExt::lock_exclusive(&new_file)
298 .map_err(|e| MenteError::Storage(format!("WAL flock re-acquire failed: {e}")))?;
299 self.file = new_file;
300
301 debug!(before_lsn, "WAL truncated (atomic)");
302 Ok(())
303 }
304
305 pub fn reset_to(
311 &mut self,
312 lsn: Lsn,
313 entry_type: WalEntryType,
314 page_id: u64,
315 data: &[u8],
316 ) -> MenteResult<()> {
317 let tmp_path = self.tmp_path();
318 {
319 let mut tmp_file = OpenOptions::new()
320 .write(true)
321 .create(true)
322 .truncate(true)
323 .open(&tmp_path)?;
324 tmp_file.write_all(&encode_entry(lsn, entry_type, page_id, data))?;
325 tmp_file.sync_data()?;
326 }
327 std::fs::rename(&tmp_path, &self.path)?;
328 self.file = OpenOptions::new().read(true).write(true).open(&self.path)?;
329 self.next_lsn = self.next_lsn.max(lsn + 1);
330 self.torn_tail = false;
331 debug!(lsn, "WAL reset to a single entry");
332 Ok(())
333 }
334
335 pub fn read_entries(path: &Path) -> MenteResult<Vec<WalEntry>> {
338 let file = match File::open(path) {
339 Ok(f) => f,
340 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
341 Err(e) => return Err(e.into()),
342 };
343 let mut wal = Self {
344 file,
345 path: path.to_path_buf(),
346 next_lsn: 1,
347 torn_tail: false,
348 };
349 wal.read_all_entries()
350 }
351
352 fn tmp_path(&self) -> std::path::PathBuf {
353 let mut name = self.path.file_name().unwrap_or_default().to_os_string();
354 name.push(".tmp");
355 self.path.with_file_name(name)
356 }
357
358 pub fn has_torn_tail(&self) -> bool {
360 self.torn_tail
361 }
362
363 pub fn next_lsn(&self) -> Lsn {
365 self.next_lsn
366 }
367
368 pub fn file_size(&self) -> u64 {
370 self.file.metadata().map(|m| m.len()).unwrap_or(0)
371 }
372
373 fn read_all_entries(&mut self) -> MenteResult<Vec<WalEntry>> {
376 self.file.seek(SeekFrom::Start(0))?;
377 let file_len = self.file.metadata()?.len();
378 let mut offset: u64 = 0;
379 let mut good_end: u64 = 0;
380 let mut entries = Vec::new();
381
382 while offset + 4 <= file_len {
383 let mut len_buf = [0u8; 4];
385 if self.file.read_exact(&mut len_buf).is_err() {
386 break;
387 }
388 let payload_len = u32::from_le_bytes(len_buf) as usize;
389 offset += 4;
390
391 if payload_len < MIN_PAYLOAD || offset + payload_len as u64 + 4 > file_len {
392 break;
393 }
394
395 let mut payload = vec![0u8; payload_len];
397 if self.file.read_exact(&mut payload).is_err() {
398 break;
399 }
400 offset += payload_len as u64;
401
402 let mut crc_buf = [0u8; 4];
404 if self.file.read_exact(&mut crc_buf).is_err() {
405 break;
406 }
407 let stored_crc = u32::from_le_bytes(crc_buf);
408 offset += 4;
409
410 let computed_crc = {
412 let mut h = crc32fast::Hasher::new();
413 h.update(&payload);
414 h.finalize()
415 };
416 if computed_crc != stored_crc {
417 break; }
419
420 let lsn = u64::from_le_bytes(payload[0..8].try_into().unwrap());
422 let entry_type = match WalEntryType::try_from(payload[8]) {
423 Ok(t) => t,
424 Err(_) => break,
425 };
426 let page_id = u64::from_le_bytes(payload[9..17].try_into().unwrap());
427 let compressed_data = &payload[17..];
428
429 let data = lz4_flex::decompress_size_prepended(compressed_data)
430 .map_err(|e| MenteError::Storage(format!("LZ4 decompress failed: {e}")))?;
431
432 entries.push(WalEntry {
433 lsn,
434 entry_type,
435 page_id,
436 data,
437 checksum: stored_crc,
438 });
439 good_end = offset;
440 }
441
442 self.torn_tail = good_end < file_len;
443 Ok(entries)
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 fn setup() -> (tempfile::TempDir, Wal) {
452 let dir = tempfile::tempdir().unwrap();
453 let wal = Wal::open(dir.path()).unwrap();
454 (dir, wal)
455 }
456
457 #[test]
458 fn test_append_and_iterate() {
459 let (_dir, mut wal) = setup();
460
461 let lsn1 = wal.append(WalEntryType::PageWrite, 1, b"hello").unwrap();
462 let lsn2 = wal.append(WalEntryType::PageWrite, 2, b"world").unwrap();
463 assert_eq!(lsn1, 1);
464 assert_eq!(lsn2, 2);
465
466 let entries = wal.iterate().unwrap();
467 assert_eq!(entries.len(), 2);
468 assert_eq!(entries[0].lsn, 1);
469 assert_eq!(entries[0].data, b"hello");
470 assert_eq!(entries[1].lsn, 2);
471 assert_eq!(entries[1].data, b"world");
472 }
473
474 #[test]
475 fn test_sync() {
476 let (_dir, mut wal) = setup();
477 wal.append(WalEntryType::Commit, 0, b"").unwrap();
478 wal.sync().unwrap(); }
480
481 #[test]
482 fn test_truncate() {
483 let (_dir, mut wal) = setup();
484
485 wal.append(WalEntryType::PageWrite, 1, b"a").unwrap();
486 wal.append(WalEntryType::PageWrite, 2, b"b").unwrap();
487 wal.append(WalEntryType::Checkpoint, 0, b"").unwrap();
488
489 wal.truncate(3).unwrap();
491
492 let entries = wal.iterate().unwrap();
493 assert_eq!(entries.len(), 1);
494 assert_eq!(entries[0].lsn, 3);
495 }
496
497 #[test]
498 fn test_recovery_reopen() {
499 let dir = tempfile::tempdir().unwrap();
500 {
501 let mut wal = Wal::open(dir.path()).unwrap();
502 wal.append(WalEntryType::PageWrite, 10, b"recovery-data")
503 .unwrap();
504 wal.sync().unwrap();
505 }
506 {
507 let mut wal = Wal::open(dir.path()).unwrap();
508 assert_eq!(wal.next_lsn(), 2);
509 let entries = wal.iterate().unwrap();
510 assert_eq!(entries.len(), 1);
511 assert_eq!(entries[0].page_id, 10);
512 assert_eq!(entries[0].data, b"recovery-data");
513 }
514 }
515
516 #[test]
517 fn test_empty_data_entry() {
518 let (_dir, mut wal) = setup();
519 let lsn = wal.append(WalEntryType::Checkpoint, 0, b"").unwrap();
520 let entries = wal.iterate().unwrap();
521 assert_eq!(entries.len(), 1);
522 assert_eq!(entries[0].lsn, lsn);
523 assert!(entries[0].data.is_empty());
524 }
525
526 #[test]
527 fn test_large_data_compression() {
528 let (_dir, mut wal) = setup();
529 let big_data = vec![0xABu8; 8192];
530 wal.append(WalEntryType::PageWrite, 5, &big_data).unwrap();
531
532 let entries = wal.iterate().unwrap();
533 assert_eq!(entries.len(), 1);
534 assert_eq!(entries[0].data, big_data);
535 }
536
537 #[test]
538 fn test_append_then_sync_is_durable() {
539 let dir = tempfile::tempdir().unwrap();
542 {
543 let mut wal = Wal::open(dir.path()).unwrap();
544 wal.append(WalEntryType::PageWrite, 1, b"batch1").unwrap();
545 wal.append(WalEntryType::PageWrite, 2, b"batch2").unwrap();
546 wal.sync().unwrap();
547 }
548 {
549 let mut wal = Wal::open(dir.path()).unwrap();
550 let entries = wal.iterate().unwrap();
551 assert_eq!(entries.len(), 2);
552 assert_eq!(entries[0].data, b"batch1");
553 assert_eq!(entries[1].data, b"batch2");
554 }
555 }
556
557 #[test]
558 fn test_truncate_atomic_preserves_kept_entries() {
559 let dir = tempfile::tempdir().unwrap();
560 {
561 let mut wal = Wal::open(dir.path()).unwrap();
562 wal.append(WalEntryType::PageWrite, 1, b"old1").unwrap();
563 wal.append(WalEntryType::PageWrite, 2, b"old2").unwrap();
564 wal.append(WalEntryType::PageWrite, 3, b"keep1").unwrap();
565 wal.append(WalEntryType::PageWrite, 4, b"keep2").unwrap();
566
567 wal.truncate(3).unwrap();
568
569 let entries = wal.iterate().unwrap();
570 assert_eq!(entries.len(), 2);
571 assert_eq!(entries[0].data, b"keep1");
572 assert_eq!(entries[1].data, b"keep2");
573 }
574 {
576 let mut wal = Wal::open(dir.path()).unwrap();
577 let entries = wal.iterate().unwrap();
578 assert_eq!(entries.len(), 2);
579 assert_eq!(entries[0].lsn, 3);
580 assert_eq!(entries[1].lsn, 4);
581 }
582 }
583
584 #[test]
585 fn test_truncate_no_temp_file_left_behind() {
586 let dir = tempfile::tempdir().unwrap();
587 let mut wal = Wal::open(dir.path()).unwrap();
588 wal.append(WalEntryType::PageWrite, 1, b"a").unwrap();
589 wal.truncate(2).unwrap();
590
591 assert!(!dir.path().join("wal.log.tmp").exists());
593 }
594
595 #[test]
596 fn test_append_after_truncate_works() {
597 let (_dir, mut wal) = setup();
598 wal.append(WalEntryType::PageWrite, 1, b"before").unwrap();
599 wal.truncate(2).unwrap();
600
601 wal.append(WalEntryType::PageWrite, 10, b"after").unwrap();
603 let entries = wal.iterate().unwrap();
604 assert_eq!(entries.len(), 1);
605 assert_eq!(entries[0].data, b"after");
606 }
607}