1use std::fs::{File, OpenOptions};
2use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::{Arc, Condvar, Mutex};
6use std::thread::JoinHandle;
7use std::time::Duration;
8use tracing::debug;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum WalRecordType {
13 Insert = 1,
14 Update = 2,
15 Delete = 3,
16 Commit = 4,
17 Rollback = 5,
18 DdlCreateTable = 6,
19 DdlDropTable = 7,
20 DdlAddColumn = 8,
21 DdlDropColumn = 9,
22 Begin = 10,
23}
24
25impl WalRecordType {
26 fn from_u8(v: u8) -> Option<Self> {
27 match v {
28 1 => Some(WalRecordType::Insert),
29 2 => Some(WalRecordType::Update),
30 3 => Some(WalRecordType::Delete),
31 4 => Some(WalRecordType::Commit),
32 5 => Some(WalRecordType::Rollback),
33 6 => Some(WalRecordType::DdlCreateTable),
34 7 => Some(WalRecordType::DdlDropTable),
35 8 => Some(WalRecordType::DdlAddColumn),
36 9 => Some(WalRecordType::DdlDropColumn),
37 10 => Some(WalRecordType::Begin),
38 _ => None,
39 }
40 }
41}
42
43pub const WAL_MAGIC: &[u8; 4] = b"PWAL";
44pub const WAL_FORMAT_VERSION: u16 = 1;
45const WAL_FILE_HEADER_SIZE: u64 = 8;
46
47const WAL_HEADER_SIZE: usize = 25;
49
50fn write_wal_file_header(file: &mut File) -> io::Result<()> {
51 file.seek(SeekFrom::Start(0))?;
52 file.write_all(WAL_MAGIC)?;
53 file.write_all(&WAL_FORMAT_VERSION.to_le_bytes())?;
54 file.write_all(&0u16.to_le_bytes())?;
55 file.seek(SeekFrom::End(0))?;
56 Ok(())
57}
58
59fn wal_records_start(file: &mut File) -> io::Result<u64> {
60 let len = file.metadata()?.len();
61 if len == 0 {
62 write_wal_file_header(file)?;
63 return Ok(WAL_FILE_HEADER_SIZE);
64 }
65 if len >= WAL_FILE_HEADER_SIZE {
66 file.seek(SeekFrom::Start(0))?;
67 let mut hdr = [0u8; WAL_FILE_HEADER_SIZE as usize];
68 file.read_exact(&mut hdr)?;
69 if &hdr[0..4] == WAL_MAGIC {
70 let version = u16::from_le_bytes(hdr[4..6].try_into().expect("2-byte WAL version"));
71 if version != WAL_FORMAT_VERSION {
72 return Err(io::Error::new(
73 io::ErrorKind::InvalidData,
74 format!("unsupported WAL format version: {version}"),
75 ));
76 }
77 return Ok(WAL_FILE_HEADER_SIZE);
78 }
79 }
80 Ok(0)
82}
83
84const MAX_WAL_RECORD_SIZE: usize = 256 * 1024 * 1024;
89
90#[derive(Debug)]
91pub struct WalRecord {
92 pub tx_id: u64,
93 pub record_type: WalRecordType,
94 pub lsn: u64,
99 pub data: Vec<u8>,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
116pub enum WalSyncMode {
117 #[default]
118 Full,
119 Normal,
129 Off,
130}
131
132const NORMAL_FSYNC_INTERVAL: Duration = Duration::from_millis(10);
135
136pub struct Wal {
137 path: PathBuf,
138 writer: Option<BufWriter<File>>,
139 batch_size: usize,
140 pending: usize,
141 sync_mode: WalSyncMode,
142 next_lsn: u64,
145 records_start: u64,
152 synced_len: u64,
153 dirty_gen: Arc<AtomicU64>,
156 synced_gen: Arc<AtomicU64>,
159 flusher: Option<Flusher>,
161}
162
163struct Flusher {
168 handle: Option<JoinHandle<()>>,
169 ctl: Arc<(Mutex<bool>, Condvar)>,
171}
172
173impl Flusher {
174 fn spawn(
175 file: File,
176 dirty_gen: Arc<AtomicU64>,
177 synced_gen: Arc<AtomicU64>,
178 interval: Duration,
179 ) -> Flusher {
180 let ctl: Arc<(Mutex<bool>, Condvar)> = Arc::new((Mutex::new(false), Condvar::new()));
181 let ctl_thread = Arc::clone(&ctl);
182 let handle = std::thread::Builder::new()
183 .name("powdb-wal-flusher".into())
184 .spawn(move || {
185 let (lock, cvar) = &*ctl_thread;
186 loop {
187 let stopping = {
188 let stop = lock.lock().expect("wal flusher lock");
189 if *stop {
190 true
191 } else {
192 let (stop, _timeout) =
193 cvar.wait_timeout(stop, interval).expect("wal flusher wait");
194 *stop
195 }
196 };
197 let d = dirty_gen.load(Ordering::Acquire);
199 if d > synced_gen.load(Ordering::Acquire) {
200 match file.sync_data() {
201 Ok(()) => synced_gen.store(d, Ordering::Release),
202 Err(e) => tracing::warn!(
209 error = %e,
210 "WAL background fsync failed; commits since the last \
211 successful sync are not yet durable (will retry)"
212 ),
213 }
214 }
215 if stopping {
216 break;
217 }
218 }
219 })
220 .expect("spawn wal flusher thread");
221 Flusher {
222 handle: Some(handle),
223 ctl,
224 }
225 }
226
227 fn stop(&mut self) {
228 {
229 let (lock, cvar) = &*self.ctl;
230 let mut stop = lock.lock().expect("wal flusher lock");
231 *stop = true;
232 cvar.notify_all();
233 }
234 if let Some(h) = self.handle.take() {
235 let _ = h.join();
236 }
237 }
238}
239
240impl Drop for Flusher {
241 fn drop(&mut self) {
242 self.stop();
243 }
244}
245
246impl Wal {
247 pub fn create(path: &Path, batch_size: usize) -> io::Result<Self> {
248 let mut file = OpenOptions::new()
249 .create(true)
250 .write(true)
251 .read(true)
252 .truncate(true)
253 .open(path)?;
254 write_wal_file_header(&mut file)?;
255 Ok(Wal {
256 path: path.to_path_buf(),
257 writer: Some(BufWriter::new(file)),
258 batch_size,
259 pending: 0,
260 sync_mode: WalSyncMode::default(),
261 next_lsn: 1,
262 records_start: WAL_FILE_HEADER_SIZE,
263 synced_len: WAL_FILE_HEADER_SIZE,
264 dirty_gen: Arc::new(AtomicU64::new(0)),
265 synced_gen: Arc::new(AtomicU64::new(0)),
266 flusher: None,
267 })
268 }
269
270 pub fn open(path: &Path, batch_size: usize) -> io::Result<Self> {
271 let mut file = OpenOptions::new()
272 .create(true)
273 .read(true)
274 .append(true)
275 .open(path)?;
276 let records_start = wal_records_start(&mut file)?;
277 let synced_len = file.metadata()?.len();
278 Ok(Wal {
279 path: path.to_path_buf(),
280 writer: Some(BufWriter::new(file)),
281 batch_size,
282 pending: 0,
283 sync_mode: WalSyncMode::default(),
284 next_lsn: 1,
285 records_start,
286 synced_len,
287 dirty_gen: Arc::new(AtomicU64::new(0)),
288 synced_gen: Arc::new(AtomicU64::new(0)),
289 flusher: None,
290 })
291 }
292
293 pub fn set_sync_mode(&mut self, mode: WalSyncMode) {
298 if mode == self.sync_mode {
299 return;
300 }
301 self.sync_mode = mode;
302 match mode {
303 WalSyncMode::Normal => self.start_flusher(),
304 WalSyncMode::Full | WalSyncMode::Off => self.stop_flusher(),
305 }
306 }
307
308 fn start_flusher(&mut self) {
311 if self.flusher.is_some() {
312 return;
313 }
314 if let Some(writer) = self.writer.as_ref() {
315 if let Ok(file) = writer.get_ref().try_clone() {
316 self.flusher = Some(Flusher::spawn(
317 file,
318 Arc::clone(&self.dirty_gen),
319 Arc::clone(&self.synced_gen),
320 NORMAL_FSYNC_INTERVAL,
321 ));
322 }
323 }
324 }
325
326 fn stop_flusher(&mut self) {
328 if let Some(mut f) = self.flusher.take() {
329 f.stop();
330 }
331 }
332
333 pub fn synced_generation(&self) -> u64 {
337 self.synced_gen.load(Ordering::Acquire)
338 }
339
340 pub fn sync_mode(&self) -> WalSyncMode {
342 self.sync_mode
343 }
344
345 #[inline]
354 pub fn is_off(&self) -> bool {
355 matches!(self.sync_mode, WalSyncMode::Off)
356 }
357
358 #[inline]
367 pub fn last_appended_lsn(&self) -> u64 {
368 if matches!(self.sync_mode, WalSyncMode::Off) {
369 return 0;
370 }
371 self.next_lsn.saturating_sub(1)
372 }
373
374 pub fn set_next_lsn_at_least(&mut self, lsn: u64) {
382 if lsn > self.next_lsn {
383 self.next_lsn = lsn;
384 }
385 }
386
387 pub fn append(
392 &mut self,
393 tx_id: u64,
394 record_type: WalRecordType,
395 data: &[u8],
396 ) -> io::Result<()> {
397 if matches!(self.sync_mode, WalSyncMode::Off) {
398 return Ok(());
399 }
400 let lsn = self.next_lsn;
401 self.next_lsn += 1;
402 let total_len = (WAL_HEADER_SIZE + data.len()) as u32;
403
404 let mut crc_input = Vec::with_capacity(17 + data.len());
406 crc_input.extend_from_slice(&tx_id.to_le_bytes());
407 crc_input.push(record_type as u8);
408 crc_input.extend_from_slice(&lsn.to_le_bytes());
409 crc_input.extend_from_slice(data);
410 let crc = crc32fast::hash(&crc_input);
411
412 let writer = self
414 .writer
415 .as_mut()
416 .ok_or_else(|| io::Error::other("WAL writer unavailable"))?;
417 writer.write_all(&total_len.to_le_bytes())?;
418 writer.write_all(&crc.to_le_bytes())?;
419 writer.write_all(&tx_id.to_le_bytes())?;
420 writer.write_all(&[record_type as u8])?;
421 writer.write_all(&lsn.to_le_bytes())?;
422 writer.write_all(data)?;
423
424 self.pending += 1;
425 if self.pending >= self.batch_size {
426 self.flush()?;
427 }
428 Ok(())
429 }
430
431 pub fn flush(&mut self) -> io::Result<()> {
437 let batch = self.pending;
438 if batch == 0 {
439 return Ok(());
440 }
441 let is_full = matches!(self.sync_mode, WalSyncMode::Full);
442 let is_off = matches!(self.sync_mode, WalSyncMode::Off);
443 let new_len = {
446 let writer = self
447 .writer
448 .as_mut()
449 .ok_or_else(|| io::Error::other("WAL writer unavailable"))?;
450 writer.flush()?;
451 if is_full {
456 writer.get_ref().sync_data()?;
457 }
458 writer.get_ref().metadata()?.len()
459 };
460 if !is_off {
461 let gen = self.dirty_gen.fetch_add(1, Ordering::Release) + 1;
462 if is_full {
463 self.synced_gen.store(gen, Ordering::Release);
464 }
465 }
466 self.synced_len = new_len;
467 self.pending = 0;
468 debug!(records = batch, "wal group commit");
469 Ok(())
470 }
471
472 #[inline]
475 pub fn has_pending(&self) -> bool {
476 self.pending > 0
477 }
478
479 pub fn synced_len(&mut self) -> io::Result<u64> {
482 self.flush()?;
483 Ok(self.synced_len)
484 }
485
486 pub fn discard_and_truncate_to(&mut self, len: u64) -> io::Result<()> {
491 if matches!(self.sync_mode, WalSyncMode::Off) {
492 self.pending = 0;
493 self.synced_len = len;
494 return Ok(());
495 }
496
497 if let Some(writer) = self.writer.take() {
498 let (_file, _buffer_result) = writer.into_parts();
499 }
500
501 let mut file = OpenOptions::new()
502 .create(true)
503 .read(true)
504 .append(true)
505 .open(&self.path)?;
506 file.set_len(len)?;
507 file.seek(SeekFrom::End(0))?;
508 file.sync_data()?;
509 self.writer = Some(BufWriter::new(file));
510 self.pending = 0;
511 self.synced_len = len;
512 Ok(())
513 }
514
515 pub fn read_all(&self) -> io::Result<Vec<WalRecord>> {
517 let mut file = File::open(&self.path)?;
518 let file_len = file.metadata()?.len();
519 let mut file_for_header = File::open(&self.path)?;
520 let mut pos = wal_records_start(&mut file_for_header)?;
521 let mut records = Vec::new();
522
523 while pos + WAL_HEADER_SIZE as u64 <= file_len {
524 file.seek(SeekFrom::Start(pos))?;
525
526 let mut header = [0u8; WAL_HEADER_SIZE];
527 if file.read_exact(&mut header).is_err() {
528 break;
529 }
530
531 let total_len_bytes: [u8; 4] = match header[0..4].try_into() {
535 Ok(b) => b,
536 Err(_) => break,
537 };
538 let total_len = u32::from_le_bytes(total_len_bytes) as usize;
539 let stored_crc_bytes: [u8; 4] = match header[4..8].try_into() {
540 Ok(b) => b,
541 Err(_) => break,
542 };
543 let stored_crc = u32::from_le_bytes(stored_crc_bytes);
544 let tx_id_bytes: [u8; 8] = match header[8..16].try_into() {
545 Ok(b) => b,
546 Err(_) => break,
547 };
548 let tx_id = u64::from_le_bytes(tx_id_bytes);
549 let record_type = match WalRecordType::from_u8(header[16]) {
550 Some(rt) => rt,
551 None => break,
552 };
553 let lsn_bytes: [u8; 8] = match header[17..25].try_into() {
554 Ok(b) => b,
555 Err(_) => break,
556 };
557 let lsn = u64::from_le_bytes(lsn_bytes);
558
559 if pos + total_len as u64 > file_len {
562 break; }
564
565 let data_len = match total_len.checked_sub(WAL_HEADER_SIZE) {
568 Some(len) => len,
569 None => break, };
571
572 if data_len > MAX_WAL_RECORD_SIZE {
576 break; }
578
579 let mut data = vec![0u8; data_len];
580 if data_len > 0 {
581 file.read_exact(&mut data)?;
582 }
583
584 let mut crc_input = Vec::with_capacity(17 + data.len());
586 crc_input.extend_from_slice(&tx_id.to_le_bytes());
587 crc_input.push(record_type as u8);
588 crc_input.extend_from_slice(&lsn.to_le_bytes());
589 crc_input.extend_from_slice(&data);
590 let computed_crc = crc32fast::hash(&crc_input);
591
592 if computed_crc != stored_crc {
593 break; }
595
596 records.push(WalRecord {
597 tx_id,
598 record_type,
599 lsn,
600 data,
601 });
602 pos += total_len as u64;
603 }
604
605 Ok(records)
606 }
607
608 pub fn truncate(&mut self) -> io::Result<()> {
610 let mut file = OpenOptions::new()
611 .write(true)
612 .read(true)
613 .truncate(true)
614 .open(&self.path)?;
615 write_wal_file_header(&mut file)?;
616 self.writer = Some(BufWriter::new(file));
617 self.records_start = WAL_FILE_HEADER_SIZE;
618 self.pending = 0;
619 self.synced_len = WAL_FILE_HEADER_SIZE;
620 Ok(())
621 }
622
623 pub fn discard_pending(&mut self) -> io::Result<()> {
632 if matches!(self.sync_mode, WalSyncMode::Off) {
633 self.pending = 0;
634 return Ok(());
635 }
636
637 if let Some(writer) = self.writer.take() {
638 let (_file, _buffer) = writer.into_parts();
639 }
640
641 let file = OpenOptions::new()
642 .read(true)
643 .append(true)
644 .create(true)
645 .truncate(false)
646 .open(&self.path)?;
647 file.set_len(self.synced_len)?;
648 file.sync_data()?;
649 self.writer = Some(BufWriter::new(file));
650 self.pending = 0;
651 self.synced_len = self.records_start;
652 Ok(())
653 }
654}
655
656impl Drop for Wal {
657 fn drop(&mut self) {
658 if !matches!(self.sync_mode, WalSyncMode::Off) {
664 if let Some(writer) = self.writer.as_mut() {
665 let _ = writer.flush();
666 let _ = writer.get_ref().sync_data();
667 }
668 }
669 self.stop_flusher();
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676
677 fn temp_wal(name: &str) -> (Wal, PathBuf) {
678 let path = std::env::temp_dir().join(format!("powdb_wal_{name}_{}", std::process::id()));
679 let wal = Wal::create(&path, 4).unwrap();
680 (wal, path)
681 }
682
683 #[test]
684 fn test_append_and_flush() {
685 let (mut wal, path) = temp_wal("basic");
686 wal.append(1, WalRecordType::Insert, b"row data 1").unwrap();
687 wal.append(1, WalRecordType::Insert, b"row data 2").unwrap();
688 wal.flush().unwrap();
689
690 let records = wal.read_all().unwrap();
691 assert_eq!(records.len(), 2);
692 assert_eq!(records[0].tx_id, 1);
693 assert_eq!(records[0].data, b"row data 1");
694 assert_eq!(records[1].data, b"row data 2");
695 drop(wal);
696 std::fs::remove_file(&path).ok();
697 }
698
699 #[test]
700 fn test_group_commit_auto_flush() {
701 let (mut wal, path) = temp_wal("group");
702 for i in 0..4 {
704 wal.append(1, WalRecordType::Insert, format!("row {i}").as_bytes())
705 .unwrap();
706 }
707 let records = wal.read_all().unwrap();
709 assert_eq!(records.len(), 4);
710 drop(wal);
711 std::fs::remove_file(&path).ok();
712 }
713
714 #[test]
715 fn test_normal_mode_persists_records_across_reopen() {
716 let path =
721 std::env::temp_dir().join(format!("powdb_wal_normal_reopen_{}", std::process::id()));
722 std::fs::remove_file(&path).ok();
723 {
724 let mut wal = Wal::create(&path, 4).unwrap();
725 wal.set_sync_mode(WalSyncMode::Normal);
726 assert_eq!(wal.sync_mode(), WalSyncMode::Normal);
727 wal.append(1, WalRecordType::Insert, b"n1").unwrap();
728 wal.append(1, WalRecordType::Insert, b"n2").unwrap();
729 wal.flush().unwrap();
730 } let wal = Wal::open(&path, 4).unwrap();
732 let records = wal.read_all().unwrap();
733 assert_eq!(records.len(), 2);
734 assert_eq!(records[0].data, b"n1");
735 assert_eq!(records[1].data, b"n2");
736 std::fs::remove_file(&path).ok();
737 }
738
739 #[test]
740 fn test_normal_mode_background_flusher_syncs_off_commit_path() {
741 let path = std::env::temp_dir().join(format!("powdb_wal_normal_bg_{}", std::process::id()));
745 std::fs::remove_file(&path).ok();
746 let mut wal = Wal::create(&path, 1000).unwrap(); wal.set_sync_mode(WalSyncMode::Normal);
748 wal.append(1, WalRecordType::Insert, b"bg1").unwrap();
749 wal.flush().unwrap(); std::thread::sleep(std::time::Duration::from_millis(80));
752 assert!(
753 wal.synced_generation() >= 1,
754 "background flusher did not sync (synced_generation = {})",
755 wal.synced_generation()
756 );
757 std::fs::remove_file(&path).ok();
758 }
759
760 #[test]
761 fn test_crc_integrity() {
762 let (mut wal, path) = temp_wal("crc");
763 wal.append(1, WalRecordType::Insert, b"important data")
764 .unwrap();
765 wal.flush().unwrap();
766
767 let records = wal.read_all().unwrap();
768 assert_eq!(records.len(), 1);
769 drop(wal);
771 std::fs::remove_file(&path).ok();
772 }
773
774 #[test]
775 fn test_multiple_transactions() {
776 let (mut wal, path) = temp_wal("multi_tx");
777 wal.append(1, WalRecordType::Insert, b"tx1 op1").unwrap();
778 wal.append(2, WalRecordType::Insert, b"tx2 op1").unwrap();
779 wal.append(1, WalRecordType::Commit, b"").unwrap();
780 wal.append(2, WalRecordType::Commit, b"").unwrap();
781 wal.flush().unwrap();
782
783 let records = wal.read_all().unwrap();
784 assert_eq!(records.len(), 4);
785 assert_eq!(records[0].tx_id, 1);
786 assert_eq!(records[2].tx_id, 1);
787 assert_eq!(records[2].record_type, WalRecordType::Commit);
788 drop(wal);
789 std::fs::remove_file(&path).ok();
790 }
791
792 #[test]
793 fn test_truncate() {
794 let (mut wal, path) = temp_wal("trunc");
795 for i in 0..8 {
796 wal.append(1, WalRecordType::Insert, format!("data {i}").as_bytes())
797 .unwrap();
798 }
799 wal.flush().unwrap();
800 assert_eq!(wal.read_all().unwrap().len(), 8);
801
802 wal.truncate().unwrap();
803 assert_eq!(wal.read_all().unwrap().len(), 0);
804 drop(wal);
805 std::fs::remove_file(&path).ok();
806 }
807
808 #[test]
809 fn test_reopen_wal() {
810 let path = std::env::temp_dir().join(format!("powdb_wal_reopen_{}", std::process::id()));
811 {
812 let mut wal = Wal::create(&path, 128).unwrap();
813 wal.append(1, WalRecordType::Insert, b"persistent").unwrap();
814 wal.append(1, WalRecordType::Commit, b"").unwrap();
815 wal.flush().unwrap();
816 }
817 {
818 let wal = Wal::open(&path, 128).unwrap();
819 let records = wal.read_all().unwrap();
820 assert_eq!(records.len(), 2);
821 assert_eq!(records[0].data, b"persistent");
822 assert_eq!(records[1].record_type, WalRecordType::Commit);
823 }
824 std::fs::remove_file(&path).ok();
825 }
826}