1use super::record::WalRecord;
2use std::fs::{File, OpenOptions};
3use std::io::{self, BufWriter, Seek, SeekFrom, Write};
4use std::path::Path;
5use std::sync::Arc;
6
7pub const WAL_BUFFER_BYTES: usize = 64 * 1024;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17enum WalSyncMethod {
18 Data,
19 All,
20}
21
22pub(crate) struct WalGroupSync {
23 target_lsn: u64,
24 sync_handle: Arc<File>,
25 method: WalSyncMethod,
26}
27
28impl WalGroupSync {
29 pub(crate) fn target_lsn(&self) -> u64 {
30 self.target_lsn
31 }
32
33 pub(crate) fn sync(&self) -> io::Result<()> {
34 match self.method {
35 WalSyncMethod::Data => self.sync_handle.sync_data(),
36 WalSyncMethod::All => self.sync_handle.sync_all(),
37 }
38 }
39}
40
41#[cfg(target_os = "linux")]
54fn reserve_wal_blocks(file: &File, offset: u64, len: u64) -> io::Result<()> {
55 use std::os::unix::io::AsRawFd;
56 if len == 0 {
57 return Ok(());
58 }
59 let ret = unsafe {
62 libc::fallocate(
63 file.as_raw_fd(),
64 libc::FALLOC_FL_KEEP_SIZE,
65 offset as libc::off_t,
66 len as libc::off_t,
67 )
68 };
69 if ret == 0 {
70 Ok(())
71 } else {
72 Err(io::Error::last_os_error())
73 }
74}
75
76#[cfg(not(target_os = "linux"))]
77fn reserve_wal_blocks(_file: &File, _offset: u64, _len: u64) -> io::Result<()> {
78 Err(io::Error::new(
79 io::ErrorKind::Unsupported,
80 "WAL preallocation is only implemented on linux",
81 ))
82}
83
84fn fallocate_unsupported(err: &io::Error) -> bool {
89 if err.kind() == io::ErrorKind::Unsupported {
90 return true;
91 }
92 #[cfg(target_os = "linux")]
93 {
94 matches!(
95 err.raw_os_error(),
96 Some(libc::EOPNOTSUPP) | Some(libc::ENOSYS) | Some(libc::EINVAL)
97 )
98 }
99 #[cfg(not(target_os = "linux"))]
100 {
101 false
102 }
103}
104
105pub struct WalWriter {
120 file: BufWriter<File>,
121 sync_handle: Arc<File>,
132 current_lsn: u64,
135 durable_lsn: u64,
141 last_synced_size: u64,
146 preallocated_to: u64,
153 prealloc_supported: bool,
158 prealloc_metadata_dirty: bool,
162 #[cfg(test)]
163 last_sync_method: Option<WalSyncMethod>,
164}
165
166impl WalWriter {
167 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
169 let exists = path.as_ref().exists();
170
171 let mut raw = OpenOptions::new()
175 .read(true)
176 .create(true)
177 .append(true)
178 .open(path)?;
179
180 let current_lsn = if !exists || raw.metadata()?.len() == 0 {
181 raw.write_all(&reddb_file::encode_wal_file_header())?;
182 raw.sync_all()?;
183 reddb_file::WAL_FILE_HEADER_BYTES as u64
184 } else {
185 raw.seek(SeekFrom::End(0))?
189 };
190
191 let sync_handle = Arc::new(raw.try_clone()?);
197 let file = BufWriter::with_capacity(WAL_BUFFER_BYTES, raw);
198
199 let mut writer = Self {
203 file,
204 sync_handle,
205 current_lsn,
206 durable_lsn: current_lsn,
207 last_synced_size: current_lsn,
208 preallocated_to: 0,
209 prealloc_supported: true,
210 prealloc_metadata_dirty: false,
211 #[cfg(test)]
212 last_sync_method: None,
213 };
214 writer.ensure_preallocated()?;
217 Ok(writer)
218 }
219
220 fn ensure_preallocated(&mut self) -> io::Result<()> {
231 if !self.prealloc_supported {
232 return Ok(());
233 }
234 let target = reddb_file::next_main_wal_segment_boundary(self.current_lsn);
235 if target <= self.preallocated_to {
236 return Ok(());
237 }
238 let from = self.preallocated_to;
239 match reserve_wal_blocks(self.file.get_ref(), from, target - from) {
240 Ok(()) => {
241 self.preallocated_to = target;
242 self.prealloc_metadata_dirty = true;
243 }
244 Err(ref e) if fallocate_unsupported(e) => self.prealloc_supported = false,
245 Err(_) => {
246 }
249 }
250 Ok(())
251 }
252
253 pub fn append(&mut self, record: &WalRecord) -> io::Result<u64> {
262 let bytes = record.encode();
263 self.file.write_all(&bytes)?;
264
265 let record_lsn = self.current_lsn;
266 self.current_lsn += bytes.len() as u64;
267
268 self.ensure_preallocated()?;
269 Ok(record_lsn)
270 }
271
272 pub fn append_bytes(&mut self, bytes: &[u8]) -> io::Result<u64> {
284 self.file.write_all(bytes)?;
285 let record_lsn = self.current_lsn;
286 self.current_lsn += bytes.len() as u64;
287 self.ensure_preallocated()?;
288 Ok(record_lsn)
289 }
290
291 pub fn set_current_lsn(&mut self, lsn: u64) {
298 self.current_lsn = lsn;
299 }
300
301 pub fn sync(&mut self) -> io::Result<()> {
309 self.file.flush()?;
310 self.sync_flushed_file()?;
311 self.durable_lsn = self.current_lsn;
312 Ok(())
313 }
314
315 pub fn flush_until(&mut self, target: u64) -> io::Result<()> {
323 if self.durable_lsn >= target {
324 return Ok(());
325 }
326 self.file.flush()?;
327 self.sync_flushed_file()?;
328 self.durable_lsn = self.current_lsn;
329 Ok(())
330 }
331
332 fn sync_flushed_file(&mut self) -> io::Result<()> {
333 let method = self.next_sync_method();
334 match method {
335 WalSyncMethod::Data => self.file.get_ref().sync_data()?,
336 WalSyncMethod::All => self.file.get_ref().sync_all()?,
337 }
338 self.mark_sync_complete(method, self.current_lsn);
339 Ok(())
340 }
341
342 fn next_sync_method(&self) -> WalSyncMethod {
343 if !self.prealloc_metadata_dirty && self.current_lsn <= self.last_synced_size {
344 WalSyncMethod::Data
345 } else {
346 WalSyncMethod::All
347 }
348 }
349
350 fn mark_sync_complete(&mut self, method: WalSyncMethod, lsn: u64) {
351 match method {
352 WalSyncMethod::Data => {}
353 WalSyncMethod::All => {
354 self.last_synced_size = self.preallocated_to.max(lsn);
355 self.prealloc_metadata_dirty = false;
356 }
357 }
358 #[cfg(test)]
359 {
360 self.last_sync_method = Some(method);
361 }
362 }
363
364 pub fn durable_lsn(&self) -> u64 {
368 self.durable_lsn
369 }
370
371 pub fn current_lsn(&self) -> u64 {
373 self.current_lsn
374 }
375
376 pub(crate) fn drain_for_group_sync(&mut self) -> io::Result<WalGroupSync> {
396 self.file.flush()?;
398 Ok(WalGroupSync {
399 target_lsn: self.current_lsn,
400 sync_handle: Arc::clone(&self.sync_handle),
401 method: self.next_sync_method(),
402 })
403 }
404
405 pub(crate) fn mark_durable(&mut self, sync: &WalGroupSync) {
411 let lsn = sync.target_lsn;
412 if lsn > self.durable_lsn {
413 self.durable_lsn = lsn;
414 }
415 self.mark_sync_complete(sync.method, lsn);
416 }
417
418 pub fn truncate(&mut self) -> io::Result<()> {
426 self.file.flush()?;
430
431 {
432 let raw = self.file.get_mut();
433 raw.set_len(0)?;
434 raw.seek(SeekFrom::Start(0))?;
435 }
436
437 self.file.write_all(&reddb_file::encode_wal_file_header())?;
439 self.file.flush()?;
440 self.file.get_ref().sync_all()?;
441
442 self.current_lsn = reddb_file::WAL_FILE_HEADER_BYTES as u64;
443 self.durable_lsn = reddb_file::WAL_FILE_HEADER_BYTES as u64;
444 self.last_synced_size = reddb_file::WAL_FILE_HEADER_BYTES as u64;
445 self.prealloc_metadata_dirty = false;
446 #[cfg(test)]
447 {
448 self.last_sync_method = Some(WalSyncMethod::All);
449 }
450
451 self.preallocated_to = 0;
455 self.ensure_preallocated()?;
456 Ok(())
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use std::path::PathBuf;
464
465 struct FileGuard {
466 path: PathBuf,
467 }
468
469 impl Drop for FileGuard {
470 fn drop(&mut self) {
471 let _ = std::fs::remove_file(&self.path);
472 }
473 }
474
475 fn temp_wal(name: &str) -> (FileGuard, PathBuf) {
476 let path = reddb_file::layout::wal_component_temp_path(
477 &std::env::temp_dir(),
478 "writer",
479 name,
480 std::process::id(),
481 );
482 let guard = FileGuard { path: path.clone() };
483 let _ = std::fs::remove_file(&path);
484 (guard, path)
485 }
486
487 #[test]
488 fn test_create_new_wal() {
489 let (_guard, path) = temp_wal("create");
490 let writer = WalWriter::open(&path).expect("open() should succeed");
491
492 assert_eq!(writer.current_lsn(), 8);
494 assert!(path.exists());
495 }
496
497 #[test]
498 fn test_append_record() {
499 let (_guard, path) = temp_wal("append");
500 let mut writer = WalWriter::open(&path).expect("open() should succeed");
501
502 let record = WalRecord::Begin { tx_id: 42 };
503 let lsn = writer.append(&record).expect("append() should succeed");
504
505 assert_eq!(lsn, 8);
507
508 assert_eq!(writer.current_lsn(), 8 + 29);
512 }
513
514 #[test]
515 fn test_append_multiple_records() {
516 let (_guard, path) = temp_wal("multi");
517 let mut writer = WalWriter::open(&path).expect("open() should succeed");
518
519 let lsn1 = writer
520 .append(&WalRecord::Begin { tx_id: 1 })
521 .expect("append() should succeed");
522 let lsn2 = writer
523 .append(&WalRecord::Begin { tx_id: 2 })
524 .expect("append() should succeed");
525 let lsn3 = writer
526 .append(&WalRecord::Commit { tx_id: 1 })
527 .expect("append() should succeed");
528
529 assert_eq!(lsn1, 8);
530 assert_eq!(lsn2, 8 + 29);
531 assert_eq!(lsn3, 8 + 29 + 29);
532 }
533
534 #[test]
535 fn test_page_write_lsn() {
536 let (_guard, path) = temp_wal("pagewrite");
537 let mut writer = WalWriter::open(&path).expect("open() should succeed");
538
539 let lsn1 = writer
541 .append(&WalRecord::Begin { tx_id: 1 })
542 .expect("append() should succeed");
543 assert_eq!(lsn1, 8);
544
545 let data = vec![1, 2, 3, 4, 5];
547 let lsn2 = writer
548 .append(&WalRecord::PageWrite {
549 tx_id: 1,
550 page_id: 100,
551 data: data.clone(),
552 })
553 .expect("value is present");
554
555 assert_eq!(lsn2, 8 + 29); assert_eq!(writer.current_lsn(), 8 + 29 + 42);
559 }
560
561 #[test]
562 fn test_sync() {
563 let (_guard, path) = temp_wal("sync");
564 let mut writer = WalWriter::open(&path).expect("open() should succeed");
565
566 writer
567 .append(&WalRecord::Begin { tx_id: 1 })
568 .expect("append() should succeed");
569 writer.sync().expect("sync() should succeed");
570
571 assert!(path.exists());
573 }
574
575 #[test]
576 fn test_truncate() {
577 let (_guard, path) = temp_wal("truncate");
578 let mut writer = WalWriter::open(&path).expect("open() should succeed");
579
580 writer
582 .append(&WalRecord::Begin { tx_id: 1 })
583 .expect("append() should succeed");
584 writer
585 .append(&WalRecord::PageWrite {
586 tx_id: 1,
587 page_id: 0,
588 data: vec![0; 100],
589 })
590 .expect("value is present");
591 writer
592 .append(&WalRecord::Commit { tx_id: 1 })
593 .expect("append() should succeed");
594
595 let lsn_before = writer.current_lsn();
596 assert!(lsn_before > 8);
597
598 writer.truncate().expect("truncate() should succeed");
600
601 assert_eq!(writer.current_lsn(), 8);
603
604 let len = std::fs::metadata(&path)
606 .expect("metadata() should succeed")
607 .len();
608 assert_eq!(len, 8);
609 }
610
611 #[test]
612 fn test_reopen_existing() {
613 let (_guard, path) = temp_wal("reopen");
614
615 let lsn_after_write;
617 {
618 let mut writer = WalWriter::open(&path).expect("open() should succeed");
619 writer
620 .append(&WalRecord::Begin { tx_id: 1 })
621 .expect("append() should succeed");
622 writer
623 .append(&WalRecord::Commit { tx_id: 1 })
624 .expect("append() should succeed");
625 lsn_after_write = writer.current_lsn();
626 }
627
628 {
630 let writer = WalWriter::open(&path).expect("open() should succeed");
631 assert_eq!(writer.current_lsn(), lsn_after_write);
633 }
634 }
635
636 #[test]
637 fn test_checkpoint_record() {
638 let (_guard, path) = temp_wal("checkpoint");
639 let mut writer = WalWriter::open(&path).expect("open() should succeed");
640
641 let lsn = writer
643 .append(&WalRecord::Checkpoint { lsn: 12345 })
644 .expect("value is present");
645 assert_eq!(lsn, 8);
646 assert_eq!(writer.current_lsn(), 8 + 29);
647 }
648
649 #[test]
654 fn fresh_wal_has_durable_lsn_at_header_end() {
655 let (_guard, path) = temp_wal("durable_init");
656 let writer = WalWriter::open(&path).expect("open() should succeed");
657 assert_eq!(writer.durable_lsn(), 8);
658 assert_eq!(writer.current_lsn(), 8);
659 }
660
661 #[test]
662 fn flush_until_below_durable_is_noop() {
663 let (_guard, path) = temp_wal("flush_noop");
664 let mut writer = WalWriter::open(&path).expect("open() should succeed");
665 let before = writer.durable_lsn();
667 writer.flush_until(0).expect("flush_until() should succeed");
668 writer.flush_until(8).expect("flush_until() should succeed");
669 assert_eq!(writer.durable_lsn(), before);
670 }
671
672 #[test]
673 fn flush_until_advances_durable_to_current() {
674 let (_guard, path) = temp_wal("flush_advance");
675 let mut writer = WalWriter::open(&path).expect("open() should succeed");
676 writer
677 .append(&WalRecord::Begin { tx_id: 7 })
678 .expect("append() should succeed");
679 writer
680 .append(&WalRecord::Commit { tx_id: 7 })
681 .expect("append() should succeed");
682 let target = writer.current_lsn();
683 assert_eq!(writer.durable_lsn(), 8);
685 writer
686 .flush_until(target)
687 .expect("flush_until() should succeed");
688 assert_eq!(writer.durable_lsn(), target);
689 }
690
691 #[test]
692 fn flush_until_is_monotonic() {
693 let (_guard, path) = temp_wal("flush_monotonic");
694 let mut writer = WalWriter::open(&path).expect("open() should succeed");
695 writer
696 .append(&WalRecord::Begin { tx_id: 1 })
697 .expect("append() should succeed");
698 let lo = writer.current_lsn();
699 writer
700 .flush_until(lo)
701 .expect("flush_until() should succeed");
702 let durable_after_lo = writer.durable_lsn();
703 writer
704 .append(&WalRecord::Commit { tx_id: 1 })
705 .expect("append() should succeed");
706 let hi = writer.current_lsn();
707 writer
708 .flush_until(hi)
709 .expect("flush_until() should succeed");
710 assert!(writer.durable_lsn() >= durable_after_lo);
711 writer
713 .flush_until(lo)
714 .expect("flush_until() should succeed");
715 assert_eq!(writer.durable_lsn(), hi);
716 }
717
718 #[test]
719 fn sync_advances_durable_lsn_too() {
720 let (_guard, path) = temp_wal("sync_durable");
721 let mut writer = WalWriter::open(&path).expect("open() should succeed");
722 writer
723 .append(&WalRecord::Begin { tx_id: 9 })
724 .expect("append() should succeed");
725 let before = writer.durable_lsn();
726 let after_append = writer.current_lsn();
727 assert!(after_append > before);
728 writer.sync().expect("sync() should succeed");
729 assert_eq!(writer.durable_lsn(), after_append);
730 }
731
732 #[test]
733 fn sync_all_is_used_when_wal_size_grew() {
734 let (_guard, path) = temp_wal("sync_all_grew");
735 let mut writer = WalWriter::open(&path).expect("open() should succeed");
736
737 writer
738 .append(&WalRecord::Begin { tx_id: 1 })
739 .expect("append() should succeed");
740 writer.sync().expect("sync() should succeed");
741
742 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::All));
743 assert!(writer.last_synced_size >= writer.current_lsn());
744 assert!(!writer.prealloc_metadata_dirty);
745 }
746
747 #[test]
748 fn sync_all_is_used_for_metadata_only_preallocation() {
749 let (_guard, path) = temp_wal("sync_all_prealloc_metadata");
750 let mut writer = WalWriter::open(&path).expect("open() should succeed");
751 if !writer.prealloc_supported {
752 return;
753 }
754
755 assert_eq!(writer.current_lsn(), 8);
756 assert!(writer.prealloc_metadata_dirty);
757
758 writer.sync().expect("sync() should succeed");
759
760 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::All));
761 assert_eq!(writer.last_synced_size, writer.preallocated_to);
762 assert!(!writer.prealloc_metadata_dirty);
763 }
764
765 #[test]
766 fn sync_data_is_used_when_wal_size_is_unchanged() {
767 let (_guard, path) = temp_wal("sync_data_unchanged");
768 let mut writer = WalWriter::open(&path).expect("open() should succeed");
769
770 writer
771 .append(&WalRecord::Begin { tx_id: 1 })
772 .expect("append() should succeed");
773 writer.sync().expect("sync() should succeed");
774 let synced_size = writer.last_synced_size;
775 writer.sync().expect("sync() should succeed");
776
777 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::Data));
778 assert_eq!(writer.last_synced_size, synced_size);
779 assert_eq!(writer.durable_lsn(), writer.current_lsn());
780 }
781
782 #[test]
783 fn sync_data_is_used_for_appends_within_synced_preallocation() {
784 let (_guard, path) = temp_wal("sync_data_preallocated_append");
785 let mut writer = WalWriter::open(&path).expect("open() should succeed");
786 if !writer.prealloc_supported {
787 return;
788 }
789
790 writer
791 .append(&WalRecord::Begin { tx_id: 1 })
792 .expect("append() should succeed");
793 writer.sync().expect("sync() should succeed");
794 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::All));
795
796 writer
797 .append(&WalRecord::Commit { tx_id: 1 })
798 .expect("append() should succeed");
799 writer.sync().expect("sync() should succeed");
800
801 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::Data));
802 assert_eq!(writer.durable_lsn(), writer.current_lsn());
803 assert!(writer.current_lsn() <= writer.last_synced_size);
804 }
805
806 #[test]
807 fn group_sync_uses_sync_data_within_synced_preallocation() {
808 let (_guard, path) = temp_wal("group_sync_data_preallocated_append");
809 let mut writer = WalWriter::open(&path).expect("open() should succeed");
810 if !writer.prealloc_supported {
811 return;
812 }
813
814 writer
815 .append(&WalRecord::Begin { tx_id: 1 })
816 .expect("append() should succeed");
817 writer.sync().expect("sync() should succeed");
818 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::All));
819
820 writer
821 .append(&WalRecord::Commit { tx_id: 1 })
822 .expect("append() should succeed");
823 let sync = writer
824 .drain_for_group_sync()
825 .expect("drain_for_group_sync() should succeed");
826 assert_eq!(sync.method, WalSyncMethod::Data);
827 sync.sync().expect("sync() should succeed");
828 writer.mark_durable(&sync);
829
830 assert_eq!(writer.last_sync_method, Some(WalSyncMethod::Data));
831 assert_eq!(writer.durable_lsn(), writer.current_lsn());
832 }
833
834 #[test]
835 fn truncate_resets_durable_lsn() {
836 let (_guard, path) = temp_wal("truncate_durable");
837 let mut writer = WalWriter::open(&path).expect("open() should succeed");
838 writer
839 .append(&WalRecord::Begin { tx_id: 1 })
840 .expect("append() should succeed");
841 writer.sync().expect("sync() should succeed");
842 assert!(writer.durable_lsn() > 8);
843 writer.truncate().expect("truncate() should succeed");
844 assert_eq!(writer.durable_lsn(), 8);
845 assert_eq!(writer.current_lsn(), 8);
846 }
847
848 #[test]
849 fn reopen_initialises_durable_to_current() {
850 let (_guard, path) = temp_wal("reopen_durable");
851 {
852 let mut writer = WalWriter::open(&path).expect("open() should succeed");
853 writer
854 .append(&WalRecord::Begin { tx_id: 1 })
855 .expect("append() should succeed");
856 writer.sync().expect("sync() should succeed");
857 }
858 let writer = WalWriter::open(&path).expect("open() should succeed");
859 assert_eq!(writer.durable_lsn(), writer.current_lsn());
861 }
862
863 #[test]
868 fn bufwriter_coalesces_until_sync() {
869 let (_guard, path) = temp_wal("bufwriter_coalesce");
873 let mut writer = WalWriter::open(&path).expect("open() should succeed");
874 for tx in 0..100u64 {
875 writer
876 .append(&WalRecord::Begin { tx_id: tx })
877 .expect("append() should succeed");
878 }
879 assert_eq!(writer.current_lsn(), 8 + 100 * 29);
883 let on_disk = std::fs::metadata(&path)
885 .expect("metadata() should succeed")
886 .len();
887 assert_eq!(on_disk, 8, "BufWriter leaked bytes to disk before sync");
888 }
889
890 #[test]
891 fn sync_drains_bufwriter_before_fsync() {
892 let (_guard, path) = temp_wal("sync_drains");
895 let mut writer = WalWriter::open(&path).expect("open() should succeed");
896 for tx in 0..50u64 {
897 writer
898 .append(&WalRecord::Begin { tx_id: tx })
899 .expect("append() should succeed");
900 }
901 writer.sync().expect("sync() should succeed");
902 let on_disk = std::fs::metadata(&path)
903 .expect("metadata() should succeed")
904 .len();
905 assert_eq!(on_disk, writer.current_lsn());
906 assert_eq!(writer.durable_lsn(), writer.current_lsn());
907 }
908
909 #[test]
910 fn flush_until_drains_bufwriter_too() {
911 let (_guard, path) = temp_wal("flush_until_drains");
915 let mut writer = WalWriter::open(&path).expect("open() should succeed");
916 for tx in 0..30u64 {
917 writer
918 .append(&WalRecord::Begin { tx_id: tx })
919 .expect("append() should succeed");
920 }
921 let target = writer.current_lsn();
922 writer
923 .flush_until(target)
924 .expect("flush_until() should succeed");
925 let on_disk = std::fs::metadata(&path)
926 .expect("metadata() should succeed")
927 .len();
928 assert_eq!(on_disk, target);
929 assert_eq!(writer.durable_lsn(), target);
930 }
931
932 #[test]
933 fn truncate_drains_pending_bufwriter_bytes_first() {
934 let (_guard, path) = temp_wal("truncate_drain");
939 let mut writer = WalWriter::open(&path).expect("open() should succeed");
940 for tx in 0..200u64 {
943 writer
944 .append(&WalRecord::Begin { tx_id: tx })
945 .expect("append() should succeed");
946 }
947 assert_eq!(
949 std::fs::metadata(&path)
950 .expect("metadata() should succeed")
951 .len(),
952 8
953 );
954
955 writer.truncate().expect("truncate() should succeed");
956 let on_disk = std::fs::metadata(&path)
958 .expect("metadata() should succeed")
959 .len();
960 assert_eq!(on_disk, 8);
961 assert_eq!(writer.current_lsn(), 8);
962 assert_eq!(writer.durable_lsn(), 8);
963
964 writer
966 .append(&WalRecord::Begin { tx_id: 99 })
967 .expect("append() should succeed");
968 writer.sync().expect("sync() should succeed");
969 assert_eq!(
970 std::fs::metadata(&path)
971 .expect("metadata() should succeed")
972 .len(),
973 8 + 29
974 );
975 }
976
977 #[test]
978 fn reopen_sees_only_synced_records() {
979 let (_guard, path) = temp_wal("reopen_synced_only");
988 let synced_lsn;
989 {
990 let mut writer = WalWriter::open(&path).expect("open() should succeed");
991 writer
992 .append(&WalRecord::Begin { tx_id: 1 })
993 .expect("append() should succeed");
994 writer.sync().expect("sync() should succeed");
995 synced_lsn = writer.current_lsn();
996 for tx in 100..120u64 {
999 writer
1000 .append(&WalRecord::Begin { tx_id: tx })
1001 .expect("append() should succeed");
1002 }
1003 }
1009 let writer = WalWriter::open(&path).expect("open() should succeed");
1010 assert!(writer.durable_lsn() >= synced_lsn);
1015 }
1016
1017 fn allocated_bytes(path: &std::path::Path) -> u64 {
1024 use fs2::FileExt;
1025 let f = std::fs::File::open(path).expect("open() should succeed");
1026 f.allocated_size().expect("allocated_size() should succeed")
1027 }
1028
1029 #[test]
1030 fn segment_boundary_rounds_strictly_up() {
1031 assert_eq!(
1034 reddb_file::next_main_wal_segment_boundary(0),
1035 reddb_file::MAIN_WAL_SEGMENT_BYTES
1036 );
1037 assert_eq!(
1038 reddb_file::next_main_wal_segment_boundary(8),
1039 reddb_file::MAIN_WAL_SEGMENT_BYTES
1040 );
1041 assert_eq!(
1042 reddb_file::next_main_wal_segment_boundary(reddb_file::MAIN_WAL_SEGMENT_BYTES - 1),
1043 reddb_file::MAIN_WAL_SEGMENT_BYTES
1044 );
1045 assert_eq!(
1047 reddb_file::next_main_wal_segment_boundary(reddb_file::MAIN_WAL_SEGMENT_BYTES),
1048 2 * reddb_file::MAIN_WAL_SEGMENT_BYTES
1049 );
1050 assert_eq!(
1051 reddb_file::next_main_wal_segment_boundary(reddb_file::MAIN_WAL_SEGMENT_BYTES + 1),
1052 2 * reddb_file::MAIN_WAL_SEGMENT_BYTES
1053 );
1054 }
1055
1056 #[test]
1057 fn open_preallocates_first_segment() {
1058 let (_guard, path) = temp_wal("prealloc_open");
1061 let writer = WalWriter::open(&path).expect("open() should succeed");
1062 if !writer.prealloc_supported {
1063 return; }
1065 assert_eq!(writer.preallocated_to, reddb_file::MAIN_WAL_SEGMENT_BYTES);
1066 assert!(allocated_bytes(&path) >= reddb_file::MAIN_WAL_SEGMENT_BYTES);
1069 assert_eq!(
1070 std::fs::metadata(&path)
1071 .expect("metadata() should succeed")
1072 .len(),
1073 8
1074 );
1075 }
1076
1077 #[test]
1078 fn preallocation_does_not_grow_logical_length() {
1079 let (_guard, path) = temp_wal("prealloc_logical");
1085 let mut writer = WalWriter::open(&path).expect("open() should succeed");
1086 for tx in 0..50u64 {
1087 writer
1088 .append(&WalRecord::Begin { tx_id: tx })
1089 .expect("append() should succeed");
1090 }
1091 writer.sync().expect("sync() should succeed");
1092 let logical = std::fs::metadata(&path)
1093 .expect("metadata() should succeed")
1094 .len();
1095 assert_eq!(logical, 8 + 50 * 29, "preallocation inflated i_size");
1096 assert_eq!(writer.current_lsn(), logical);
1097 }
1098
1099 #[test]
1100 fn truncate_re_extends_a_fresh_segment() {
1101 let (_guard, path) = temp_wal("prealloc_truncate");
1104 let mut writer = WalWriter::open(&path).expect("open() should succeed");
1105 writer
1106 .append(&WalRecord::Begin { tx_id: 1 })
1107 .expect("append() should succeed");
1108 writer.sync().expect("sync() should succeed");
1109
1110 writer.truncate().expect("truncate() should succeed");
1111
1112 assert_eq!(writer.current_lsn(), 8);
1113 assert_eq!(
1114 std::fs::metadata(&path)
1115 .expect("metadata() should succeed")
1116 .len(),
1117 8
1118 );
1119 if writer.prealloc_supported {
1120 assert_eq!(writer.preallocated_to, reddb_file::MAIN_WAL_SEGMENT_BYTES);
1121 assert!(allocated_bytes(&path) >= reddb_file::MAIN_WAL_SEGMENT_BYTES);
1122 }
1123 }
1124
1125 #[test]
1126 fn preallocated_wal_recovers_records_without_trailing_garbage() {
1127 use super::super::reader::WalReader;
1131 let (_guard, path) = temp_wal("prealloc_recover");
1132 {
1133 let mut writer = WalWriter::open(&path).expect("open() should succeed");
1134 writer
1135 .append(&WalRecord::Begin { tx_id: 1 })
1136 .expect("append() should succeed");
1137 writer
1138 .append(&WalRecord::PageWrite {
1139 tx_id: 1,
1140 page_id: 7,
1141 data: vec![1, 2, 3, 4],
1142 })
1143 .expect("value is present");
1144 writer
1145 .append(&WalRecord::Commit { tx_id: 1 })
1146 .expect("append() should succeed");
1147 writer.sync().expect("sync() should succeed");
1148 }
1149 let records: Vec<_> = WalReader::open(&path)
1150 .expect("value is present")
1151 .iter()
1152 .collect::<Result<_, _>>()
1153 .expect("reader must stop cleanly at real EOF, not in reserved tail");
1154 assert_eq!(records.len(), 3);
1155 assert_eq!(records[0].1, WalRecord::Begin { tx_id: 1 });
1156 assert_eq!(records[2].1, WalRecord::Commit { tx_id: 1 });
1157 }
1158}