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 pub 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 self.read_through_len(u64::MAX)
518 }
519
520 pub fn read_through_len(&self, max_len: u64) -> io::Result<Vec<WalRecord>> {
522 let mut file = File::open(&self.path)?;
523 let file_len = file.metadata()?.len().min(max_len);
524 let mut file_for_header = File::open(&self.path)?;
525 let mut pos = wal_records_start(&mut file_for_header)?;
526 let mut records = Vec::new();
527
528 while pos + WAL_HEADER_SIZE as u64 <= file_len {
529 file.seek(SeekFrom::Start(pos))?;
530
531 let mut header = [0u8; WAL_HEADER_SIZE];
532 if file.read_exact(&mut header).is_err() {
533 break;
534 }
535
536 let total_len_bytes: [u8; 4] = match header[0..4].try_into() {
540 Ok(b) => b,
541 Err(_) => break,
542 };
543 let total_len = u32::from_le_bytes(total_len_bytes) as usize;
544 let stored_crc_bytes: [u8; 4] = match header[4..8].try_into() {
545 Ok(b) => b,
546 Err(_) => break,
547 };
548 let stored_crc = u32::from_le_bytes(stored_crc_bytes);
549 let tx_id_bytes: [u8; 8] = match header[8..16].try_into() {
550 Ok(b) => b,
551 Err(_) => break,
552 };
553 let tx_id = u64::from_le_bytes(tx_id_bytes);
554 let record_type = match WalRecordType::from_u8(header[16]) {
555 Some(rt) => rt,
556 None => break,
557 };
558 let lsn_bytes: [u8; 8] = match header[17..25].try_into() {
559 Ok(b) => b,
560 Err(_) => break,
561 };
562 let lsn = u64::from_le_bytes(lsn_bytes);
563
564 if pos + total_len as u64 > file_len {
567 break; }
569
570 let data_len = match total_len.checked_sub(WAL_HEADER_SIZE) {
573 Some(len) => len,
574 None => break, };
576
577 if data_len > MAX_WAL_RECORD_SIZE {
581 break; }
583
584 let mut data = vec![0u8; data_len];
585 if data_len > 0 {
586 file.read_exact(&mut data)?;
587 }
588
589 let mut crc_input = Vec::with_capacity(17 + data.len());
591 crc_input.extend_from_slice(&tx_id.to_le_bytes());
592 crc_input.push(record_type as u8);
593 crc_input.extend_from_slice(&lsn.to_le_bytes());
594 crc_input.extend_from_slice(&data);
595 let computed_crc = crc32fast::hash(&crc_input);
596
597 if computed_crc != stored_crc {
598 break; }
600
601 records.push(WalRecord {
602 tx_id,
603 record_type,
604 lsn,
605 data,
606 });
607 pos += total_len as u64;
608 }
609
610 Ok(records)
611 }
612
613 pub fn truncate(&mut self) -> io::Result<()> {
615 let mut file = OpenOptions::new()
616 .write(true)
617 .read(true)
618 .truncate(true)
619 .open(&self.path)?;
620 write_wal_file_header(&mut file)?;
621 self.writer = Some(BufWriter::new(file));
622 self.records_start = WAL_FILE_HEADER_SIZE;
623 self.pending = 0;
624 self.synced_len = WAL_FILE_HEADER_SIZE;
625 Ok(())
626 }
627
628 pub fn discard_pending(&mut self) -> io::Result<()> {
637 if matches!(self.sync_mode, WalSyncMode::Off) {
638 self.pending = 0;
639 return Ok(());
640 }
641
642 if let Some(writer) = self.writer.take() {
643 let (_file, _buffer) = writer.into_parts();
644 }
645
646 let file = OpenOptions::new()
647 .read(true)
648 .append(true)
649 .create(true)
650 .truncate(false)
651 .open(&self.path)?;
652 file.set_len(self.synced_len)?;
653 file.sync_data()?;
654 self.writer = Some(BufWriter::new(file));
655 self.pending = 0;
656 self.synced_len = self.records_start;
657 Ok(())
658 }
659}
660
661impl Drop for Wal {
662 fn drop(&mut self) {
663 if !matches!(self.sync_mode, WalSyncMode::Off) {
669 if let Some(writer) = self.writer.as_mut() {
670 let _ = writer.flush();
671 let _ = writer.get_ref().sync_data();
672 }
673 }
674 self.stop_flusher();
675 }
676}
677
678#[cfg(test)]
679mod tests {
680 use super::*;
681
682 fn temp_wal(name: &str) -> (Wal, PathBuf) {
683 let path = std::env::temp_dir().join(format!("powdb_wal_{name}_{}", std::process::id()));
684 let wal = Wal::create(&path, 4).unwrap();
685 (wal, path)
686 }
687
688 #[test]
689 fn test_append_and_flush() {
690 let (mut wal, path) = temp_wal("basic");
691 wal.append(1, WalRecordType::Insert, b"row data 1").unwrap();
692 wal.append(1, WalRecordType::Insert, b"row data 2").unwrap();
693 wal.flush().unwrap();
694
695 let records = wal.read_all().unwrap();
696 assert_eq!(records.len(), 2);
697 assert_eq!(records[0].tx_id, 1);
698 assert_eq!(records[0].data, b"row data 1");
699 assert_eq!(records[1].data, b"row data 2");
700 drop(wal);
701 std::fs::remove_file(&path).ok();
702 }
703
704 #[test]
705 fn test_group_commit_auto_flush() {
706 let (mut wal, path) = temp_wal("group");
707 for i in 0..4 {
709 wal.append(1, WalRecordType::Insert, format!("row {i}").as_bytes())
710 .unwrap();
711 }
712 let records = wal.read_all().unwrap();
714 assert_eq!(records.len(), 4);
715 drop(wal);
716 std::fs::remove_file(&path).ok();
717 }
718
719 #[test]
720 fn test_normal_mode_persists_records_across_reopen() {
721 let path =
726 std::env::temp_dir().join(format!("powdb_wal_normal_reopen_{}", std::process::id()));
727 std::fs::remove_file(&path).ok();
728 {
729 let mut wal = Wal::create(&path, 4).unwrap();
730 wal.set_sync_mode(WalSyncMode::Normal);
731 assert_eq!(wal.sync_mode(), WalSyncMode::Normal);
732 wal.append(1, WalRecordType::Insert, b"n1").unwrap();
733 wal.append(1, WalRecordType::Insert, b"n2").unwrap();
734 wal.flush().unwrap();
735 } let wal = Wal::open(&path, 4).unwrap();
737 let records = wal.read_all().unwrap();
738 assert_eq!(records.len(), 2);
739 assert_eq!(records[0].data, b"n1");
740 assert_eq!(records[1].data, b"n2");
741 std::fs::remove_file(&path).ok();
742 }
743
744 #[test]
745 fn test_normal_mode_background_flusher_syncs_off_commit_path() {
746 let path = std::env::temp_dir().join(format!("powdb_wal_normal_bg_{}", std::process::id()));
750 std::fs::remove_file(&path).ok();
751 let mut wal = Wal::create(&path, 1000).unwrap(); wal.set_sync_mode(WalSyncMode::Normal);
753 wal.append(1, WalRecordType::Insert, b"bg1").unwrap();
754 wal.flush().unwrap(); std::thread::sleep(std::time::Duration::from_millis(80));
757 assert!(
758 wal.synced_generation() >= 1,
759 "background flusher did not sync (synced_generation = {})",
760 wal.synced_generation()
761 );
762 std::fs::remove_file(&path).ok();
763 }
764
765 #[test]
766 fn test_crc_integrity() {
767 let (mut wal, path) = temp_wal("crc");
768 wal.append(1, WalRecordType::Insert, b"important data")
769 .unwrap();
770 wal.flush().unwrap();
771
772 let records = wal.read_all().unwrap();
773 assert_eq!(records.len(), 1);
774 drop(wal);
776 std::fs::remove_file(&path).ok();
777 }
778
779 #[test]
780 fn test_multiple_transactions() {
781 let (mut wal, path) = temp_wal("multi_tx");
782 wal.append(1, WalRecordType::Insert, b"tx1 op1").unwrap();
783 wal.append(2, WalRecordType::Insert, b"tx2 op1").unwrap();
784 wal.append(1, WalRecordType::Commit, b"").unwrap();
785 wal.append(2, WalRecordType::Commit, b"").unwrap();
786 wal.flush().unwrap();
787
788 let records = wal.read_all().unwrap();
789 assert_eq!(records.len(), 4);
790 assert_eq!(records[0].tx_id, 1);
791 assert_eq!(records[2].tx_id, 1);
792 assert_eq!(records[2].record_type, WalRecordType::Commit);
793 drop(wal);
794 std::fs::remove_file(&path).ok();
795 }
796
797 #[test]
798 fn test_truncate() {
799 let (mut wal, path) = temp_wal("trunc");
800 for i in 0..8 {
801 wal.append(1, WalRecordType::Insert, format!("data {i}").as_bytes())
802 .unwrap();
803 }
804 wal.flush().unwrap();
805 assert_eq!(wal.read_all().unwrap().len(), 8);
806
807 wal.truncate().unwrap();
808 assert_eq!(wal.read_all().unwrap().len(), 0);
809 drop(wal);
810 std::fs::remove_file(&path).ok();
811 }
812
813 #[test]
814 fn test_reopen_wal() {
815 let path = std::env::temp_dir().join(format!("powdb_wal_reopen_{}", std::process::id()));
816 {
817 let mut wal = Wal::create(&path, 128).unwrap();
818 wal.append(1, WalRecordType::Insert, b"persistent").unwrap();
819 wal.append(1, WalRecordType::Commit, b"").unwrap();
820 wal.flush().unwrap();
821 }
822 {
823 let wal = Wal::open(&path, 128).unwrap();
824 let records = wal.read_all().unwrap();
825 assert_eq!(records.len(), 2);
826 assert_eq!(records[0].data, b"persistent");
827 assert_eq!(records[1].record_type, WalRecordType::Commit);
828 }
829 std::fs::remove_file(&path).ok();
830 }
831}