1use super::freelist::FreeList;
36use super::page::{Page, PageError, PageType, PAGE_SIZE};
37use super::page_cache::PageCache;
38use crate::storage::wal::writer::WalWriter;
39use fs2::FileExt;
40use std::fs::{File, OpenOptions};
41use std::io::{Read, Seek, SeekFrom, Write};
42use std::path::{Path, PathBuf};
43#[cfg(test)]
44use std::sync::atomic::{AtomicU8, Ordering};
45use std::sync::{Arc, Mutex, RwLock};
46
47pub use reddb_file::{DatabaseHeader, PhysicalFileHeader};
48
49const DEFAULT_CACHE_SIZE: usize = 10_000;
51const DWB_ZONE_START_PAGE: u32 = 3;
52const DWB_ZONE_PAGES: u32 = 64;
53const DWB_ZONE_BYTES: usize = DWB_ZONE_PAGES as usize * PAGE_SIZE;
54const DWB_MAX_PAGES_PER_FRAME: usize =
55 (DWB_ZONE_BYTES - reddb_file::PAGED_DWB_HEADER_SIZE) / reddb_file::PAGED_DWB_ENTRY_SIZE;
56const FIRST_ALLOCATABLE_PAGE: u32 = DWB_ZONE_START_PAGE + DWB_ZONE_PAGES;
57
58#[cfg(test)]
59static COW_ATOMIC_WRITE_TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
60
61#[derive(Debug)]
63pub enum PagerError {
64 Io(std::io::Error),
66 Page(PageError),
68 InvalidDatabase(String),
70 ReadOnly,
72 PageNotFound(u32),
74 Locked,
76 LockPoisoned,
78 EncryptionRequired,
80 PlainDatabaseRefusesKey,
82 InvalidKey,
84 SuperblockZoneUnrecoverable(PathBuf),
86 ManifestZoneCorrupt(PathBuf),
88 DwbZoneCorrupt(PathBuf),
90 LegacySidecarStore { sidecar: PathBuf },
92 LegacyPagedStore { path: PathBuf },
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct ExtentId {
99 pub start_page: u32,
100 pub n_pages: u32,
101}
102
103impl std::fmt::Display for PagerError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::Io(e) => write!(f, "I/O error: {}", e),
107 Self::Page(e) => write!(f, "Page error: {}", e),
108 Self::InvalidDatabase(msg) => write!(f, "Invalid database: {}", msg),
109 Self::ReadOnly => write!(f, "Database is read-only"),
110 Self::PageNotFound(id) => write!(f, "Page {} not found", id),
111 Self::Locked => write!(f, "Database is locked"),
112 Self::LockPoisoned => write!(f, "Internal lock poisoned (concurrent thread panicked)"),
113 Self::EncryptionRequired => write!(
114 f,
115 "Database is encrypted but no key was supplied (set PagerConfig::encryption)"
116 ),
117 Self::PlainDatabaseRefusesKey => write!(
118 f,
119 "Plain (unencrypted) database opened with an encryption key — refusing"
120 ),
121 Self::InvalidKey => write!(f, "Encryption key validation failed for this database"),
122 Self::SuperblockZoneUnrecoverable(path) => write!(
123 f,
124 "superblock zone of {} is unrecoverable: both ping-pong copies failed \
125 validation, so no generation can be trusted to root the store. The store \
126 will not be opened. Recover what survives with red salvage \
127 (ADR 0074 §4); it reads the damaged file without writing to it and \
128 reports what it could not verify.",
129 path.display()
130 ),
131 Self::ManifestZoneCorrupt(path) => write!(
132 f,
133 "internal manifest zone of {} failed its checksum: the zone that names \
134 collections, indexes and the checkpoint boundary cannot be trusted, so no \
135 rows are returned rather than garbage ones. Run the `SCRUB` statement to classify the fault \
136 and `red salvage` to extract what survives (ADR 0074 §2/§4).",
137 path.display()
138 ),
139 Self::DwbZoneCorrupt(path) => write!(
140 f,
141 "double-write zone of {} failed validation: page recovery cannot prove \
142 whether the protected in-place write completed. The store will not be opened \
143 silently; run the `SCRUB` statement to classify the fault and `red salvage` to extract trusted \
144 rows (ADR 0074 §2/§4).",
145 path.display()
146 ),
147 Self::LegacySidecarStore { sidecar } => write!(
148 f,
149 "refusing to open a legacy sidecar-backed store: found {}. The superblock \
150 pair, internal manifest, and double-write recovery state now live inside the \
151 .rdb file (ADR 0038 §2/§4), \
152 and the engine never reads the retired sidecars silently. Convert the store \
153 first with the offline migration tool \
154 (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is reversible.",
155 sidecar.display()
156 ),
157 Self::LegacyPagedStore { path } => write!(
158 f,
159 "refusing to open a pre-phase-3 paged store: {} does not carry the layout marker \
160 for the in-file double-write zone. Convert the store first with the offline \
161 migration tool (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is \
162 reversible.",
163 path.display()
164 ),
165 }
166 }
167}
168
169impl std::error::Error for PagerError {
170 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
171 match self {
172 Self::Io(e) => Some(e),
173 Self::Page(e) => Some(e),
174 _ => None,
175 }
176 }
177}
178
179impl From<std::io::Error> for PagerError {
180 fn from(e: std::io::Error) -> Self {
181 Self::Io(e)
182 }
183}
184
185impl From<PageError> for PagerError {
186 fn from(e: PageError) -> Self {
187 Self::Page(e)
188 }
189}
190
191#[derive(Debug, Clone)]
193pub struct PagerConfig {
194 pub cache_size: usize,
196 pub read_only: bool,
198 pub create: bool,
200 pub verify_checksums: bool,
202 pub double_write: bool,
204 pub encryption: Option<crate::storage::encryption::SecureKey>,
210}
211
212impl Default for PagerConfig {
213 fn default() -> Self {
214 Self {
215 cache_size: DEFAULT_CACHE_SIZE,
216 read_only: false,
217 create: true,
218 verify_checksums: true,
219 double_write: true,
220 encryption: None,
221 }
222 }
223}
224
225pub struct Pager {
229 path: PathBuf,
231 file: Mutex<File>,
233 _lock_file: Option<File>,
235 dwb_enabled: bool,
237 cache: PageCache,
239 freelist: RwLock<FreeList>,
241 deferred_reclamation: Mutex<DeferredPageReclamation>,
244 allocation_reused: std::sync::atomic::AtomicBool,
247 allocation_fence_pending: std::sync::atomic::AtomicBool,
249 header: RwLock<DatabaseHeader>,
251 config: PagerConfig,
253 header_dirty: Mutex<bool>,
255 wal: RwLock<Option<Arc<Mutex<WalWriter>>>>,
267 pub(crate) encryption: Option<(
274 crate::storage::encryption::PageEncryptor,
275 crate::storage::encryption::EncryptionHeader,
276 )>,
277}
278
279#[derive(Default)]
280struct DeferredPageReclamation {
281 enabled: bool,
282 pending: std::collections::BTreeSet<u32>,
283 published: std::collections::BTreeSet<u32>,
284}
285
286#[path = "pager/impl.rs"]
287mod pager_impl;
288impl Drop for Pager {
289 fn drop(&mut self) {
290 let _ = self.flush();
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298 #[cfg(target_os = "linux")]
299 use pager_impl::parse_mountinfo_options_for_path;
300 use pager_impl::{
301 classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
302 };
303 use std::fs;
304 use std::io::Write;
305
306 fn temp_db_path() -> PathBuf {
307 use std::sync::atomic::{AtomicU64, Ordering};
308 static COUNTER: AtomicU64 = AtomicU64::new(0);
309 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
310 let mut path = std::env::temp_dir();
311 path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
312 path
313 }
314
315 fn cleanup(path: &Path) {
316 let _ = fs::remove_file(path);
324 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
325 let _ = fs::remove_file(sidecar);
326 }
327 }
328
329 fn dwb_path_for(path: &Path) -> PathBuf {
330 reddb_file::layout::pager_dwb_shadow_path(path)
331 }
332
333 fn dwb_zone_offset_for_test() -> u64 {
334 3 * PAGE_SIZE as u64
335 }
336
337 static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
338
339 struct CowAtomicWriteOverrideGuard {
340 _guard: std::sync::MutexGuard<'static, ()>,
341 }
342
343 impl Drop for CowAtomicWriteOverrideGuard {
344 fn drop(&mut self) {
345 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
346 }
347 }
348
349 fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
350 let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
351 .lock()
352 .unwrap_or_else(|err| err.into_inner());
353 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
354 CowAtomicWriteOverrideGuard { _guard: guard }
355 }
356
357 fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
358 let pages: Vec<_> = pages
359 .iter()
360 .map(|(page_id, page)| {
361 let mut page = page.clone();
362 page.update_checksum();
363 (*page_id, page)
364 })
365 .collect();
366 let buf = reddb_file::encode_paged_dwb_frame(
367 pages
368 .iter()
369 .map(|(page_id, page)| (*page_id, page.as_bytes())),
370 );
371
372 let mut file = OpenOptions::new()
373 .read(true)
374 .write(true)
375 .open(path)
376 .expect("open() should succeed");
377 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
378 .expect("seek() should succeed");
379 file.write_all(&buf).expect("write_all() should succeed");
380 file.sync_all().expect("sync_all() should succeed");
381 }
382
383 fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
384 let mut file = OpenOptions::new()
385 .write(true)
386 .open(path)
387 .expect("open() should succeed");
388 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
389 .expect("value is present");
390 file.write_all(page.as_bytes())
391 .expect("write_all() should succeed");
392 file.sync_all().expect("sync_all() should succeed");
393 }
394
395 fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
396 let mut torn = *before.as_bytes();
397 torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
398
399 let mut file = OpenOptions::new()
400 .write(true)
401 .open(path)
402 .expect("open() should succeed");
403 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
404 .expect("value is present");
405 file.write_all(&torn).expect("write_all() should succeed");
406 file.sync_all().expect("sync_all() should succeed");
407 }
408
409 #[test]
410 fn test_pager_create_new() {
411 let path = temp_db_path();
412 cleanup(&path);
413
414 {
415 let pager = Pager::open_default(&path).expect("open_default() should succeed");
416 assert_eq!(
417 pager.page_count().expect("page_count() should succeed"),
418 FIRST_ALLOCATABLE_PAGE
419 );
420 }
422
423 cleanup(&path);
424 }
425
426 #[test]
427 fn test_pager_reopen() {
428 let path = temp_db_path();
429 cleanup(&path);
430
431 {
433 let pager = Pager::open_default(&path).expect("open_default() should succeed");
434
435 let page = pager
437 .allocate_page(PageType::BTreeLeaf)
438 .expect("allocate_page() should succeed");
439 assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
440
441 pager.sync().expect("sync() should succeed");
442 }
443
444 {
446 let pager = Pager::open_default(&path).expect("open_default() should succeed");
447 assert_eq!(
448 pager.page_count().expect("page_count() should succeed"),
449 FIRST_ALLOCATABLE_PAGE + 1
450 );
451 }
453
454 cleanup(&path);
455 }
456
457 #[test]
458 fn test_pager_read_write() {
459 let path = temp_db_path();
460 cleanup(&path);
461
462 {
463 let pager = Pager::open_default(&path).expect("open_default() should succeed");
464
465 let mut page = pager
467 .allocate_page(PageType::BTreeLeaf)
468 .expect("allocate_page() should succeed");
469 let page_id = page.page_id();
470
471 page.insert_cell(b"key", b"value")
472 .expect("insert_cell() should succeed");
473 pager
474 .write_page(page_id, page)
475 .expect("write_page() should succeed");
476
477 let read_page = pager
479 .read_page(page_id)
480 .expect("read_page() should succeed");
481 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
482 assert_eq!(key, b"key");
483 assert_eq!(value, b"value");
484 }
485
486 cleanup(&path);
487 }
488
489 #[test]
490 fn test_pager_cache() {
491 let path = temp_db_path();
492 cleanup(&path);
493
494 {
495 let pager = Pager::open_default(&path).expect("open_default() should succeed");
496
497 let page = pager
499 .allocate_page(PageType::BTreeLeaf)
500 .expect("allocate_page() should succeed");
501 let page_id = page.page_id();
502
503 let _ = pager
505 .read_page(page_id)
506 .expect("read_page() should succeed");
507
508 let _ = pager
510 .read_page(page_id)
511 .expect("read_page() should succeed");
512
513 let stats = pager.cache_stats();
514 assert!(stats.hits >= 1);
515 }
516
517 cleanup(&path);
518 }
519
520 #[test]
521 fn test_pager_free_page() {
522 let path = temp_db_path();
523 cleanup(&path);
524
525 {
526 let pager = Pager::open_default(&path).expect("open_default() should succeed");
527
528 let page1 = pager
530 .allocate_page(PageType::BTreeLeaf)
531 .expect("allocate_page() should succeed");
532 let page2 = pager
533 .allocate_page(PageType::BTreeLeaf)
534 .expect("allocate_page() should succeed");
535
536 let id1 = page1.page_id();
537 let id2 = page2.page_id();
538
539 pager.free_page(id1).expect("free_page() should succeed");
541
542 let page3 = pager
544 .allocate_page(PageType::BTreeLeaf)
545 .expect("allocate_page() should succeed");
546 assert_eq!(page3.page_id(), id1);
547 }
548
549 cleanup(&path);
550 }
551
552 #[test]
553 fn test_freelist_persistence() {
554 let path = temp_db_path();
555 cleanup(&path);
556
557 let freed_id;
558 {
559 let pager = Pager::open_default(&path).expect("open_default() should succeed");
560 let page1 = pager
561 .allocate_page(PageType::BTreeLeaf)
562 .expect("allocate_page() should succeed");
563 let _page2 = pager
564 .allocate_page(PageType::BTreeLeaf)
565 .expect("allocate_page() should succeed");
566 freed_id = page1.page_id();
567
568 pager
569 .free_page(freed_id)
570 .expect("free_page() should succeed");
571 pager.sync().expect("sync() should succeed");
572 }
573
574 {
575 let pager = Pager::open_default(&path).expect("open_default() should succeed");
576 let page = pager
577 .allocate_page(PageType::BTreeLeaf)
578 .expect("allocate_page() should succeed");
579 assert_eq!(page.page_id(), freed_id);
580 }
581
582 cleanup(&path);
583 }
584
585 #[test]
586 fn test_pager_read_only() {
587 let path = temp_db_path();
588 cleanup(&path);
589
590 {
592 let pager = Pager::open_default(&path).expect("open_default() should succeed");
593 pager.sync().expect("sync() should succeed");
594 }
595
596 {
598 let config = PagerConfig {
599 read_only: true,
600 ..Default::default()
601 };
602
603 let pager = Pager::open(&path, config).expect("open() should succeed");
604 assert!(pager.is_read_only());
605
606 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
608 }
609
610 cleanup(&path);
611 }
612
613 #[test]
614 fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
615 let path = temp_db_path();
616 cleanup(&path);
617
618 let config = PagerConfig {
619 double_write: true,
620 ..Default::default()
621 };
622
623 let page_id;
624 {
625 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
626 let page = pager
627 .allocate_page(PageType::BTreeLeaf)
628 .expect("allocate_page() should succeed");
629 page_id = page.page_id();
630 pager.sync().expect("sync() should succeed");
631 }
632
633 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
634 recovered_page
635 .insert_cell(b"key", b"value")
636 .expect("insert_cell() should succeed");
637 write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
638
639 assert!(!dwb_path_for(&path).exists());
640
641 {
642 let pager = Pager::open(&path, config).expect("open() should succeed");
643
644 let read_page = pager
645 .read_page(page_id)
646 .expect("read_page() should succeed");
647 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
648 assert_eq!(key, b"key");
649 assert_eq!(value, b"value");
650
651 assert!(!dwb_path_for(&path).exists());
652
653 let mut updated_page = recovered_page.clone();
654 updated_page
655 .insert_cell(b"key2", b"value2")
656 .expect("insert_cell() should succeed");
657 pager
658 .write_page(page_id, updated_page)
659 .expect("write_page() should succeed");
660 pager.flush().expect("flush() should succeed");
661
662 assert!(!dwb_path_for(&path).exists());
663 }
664
665 cleanup(&path);
666 }
667
668 #[test]
669 fn retired_dwb_sidecar_routes_through_offline_migration() {
670 let path = temp_db_path();
671 cleanup(&path);
672
673 {
674 let pager = Pager::open_default(&path).expect("open_default() should succeed");
675 pager.sync().expect("sync() should succeed");
676 }
677
678 let dwb = dwb_path_for(&path);
679 fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
680
681 let err = match Pager::open_default(&path) {
682 Ok(_) => panic!("retired DWB sidecar must refuse open"),
683 Err(err) => err,
684 };
685 assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
686
687 cleanup(&path);
688 }
689
690 #[test]
691 fn interrupted_dwb_staging_write_is_discarded_on_open() {
692 let path = temp_db_path();
693 cleanup(&path);
694
695 {
696 let pager = Pager::open_default(&path).expect("open_default() should succeed");
697 pager.sync().expect("sync() should succeed");
698 }
699
700 let mut file = OpenOptions::new()
701 .read(true)
702 .write(true)
703 .open(&path)
704 .expect("open() should succeed");
705 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
706 .expect("seek() should succeed");
707 file.write_all(b"RDDW")
708 .expect("write corrupt DWB magic should succeed");
709 file.sync_all().expect("sync_all() should succeed");
710
711 {
712 let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
713 pager.sync().expect("sync() should succeed");
714 }
715
716 let mut cleared = [0u8; 4];
717 let mut file = OpenOptions::new()
718 .read(true)
719 .open(&path)
720 .expect("open() should succeed");
721 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
722 .expect("seek() should succeed");
723 file.read_exact(&mut cleared)
724 .expect("read_exact() should succeed");
725 assert_ne!(cleared, reddb_file::DWB_MAGIC);
726
727 cleanup(&path);
728 }
729
730 #[test]
731 fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
732 assert_eq!(
733 classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
734 Some(CowFilesystemKind::Zfs),
735 "ZFS is always CoW"
736 );
737 assert_eq!(
738 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
739 Some(CowFilesystemKind::BtrfsDataCow),
740 "btrfs qualifies only when datacow remains enabled"
741 );
742 assert_eq!(
743 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
744 None,
745 "btrfs nodatacow mount option must reject DWB skip"
746 );
747 assert_eq!(
748 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
749 None,
750 "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
751 );
752 assert_eq!(
753 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
754 None,
755 "missing btrfs inode flags are uncertain and must fail closed"
756 );
757 assert_eq!(
758 classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
759 None,
760 "missing btrfs mount options are uncertain and must fail closed"
761 );
762 }
763
764 #[cfg(target_os = "linux")]
765 #[test]
766 fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
767 let mountinfo = "\
76824 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
76935 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
77036 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
771";
772
773 assert_eq!(
774 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
775 .as_deref(),
776 Some("rw,relatime,rw,space_cache=v2")
777 );
778 assert_eq!(
779 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
780 .as_deref(),
781 Some("rw,relatime,rw,nodatacow")
782 );
783 }
784
785 #[test]
786 fn double_write_false_keeps_dwb_when_cow_probe_denies() {
787 let _override = cow_atomic_write_override(false);
788 let path = temp_db_path();
789 cleanup(&path);
790
791 {
792 let config = PagerConfig {
793 double_write: false,
794 ..Default::default()
795 };
796 let pager = Pager::open(&path, config).expect("open() should succeed");
797 let page = pager
798 .allocate_page(PageType::BTreeLeaf)
799 .expect("allocate_page() should succeed");
800 pager
801 .write_page(page.page_id(), page)
802 .expect("write_page() should succeed");
803 pager.flush().expect("flush() should succeed");
804 }
805
806 assert!(
807 !dwb_path_for(&path).exists(),
808 "DWB must stay enabled in-file when double_write=false is not proven safe"
809 );
810
811 cleanup(&path);
812 }
813
814 #[test]
815 fn double_write_false_skips_dwb_when_cow_probe_allows() {
816 let _override = cow_atomic_write_override(true);
817 let path = temp_db_path();
818 cleanup(&path);
819
820 {
821 let config = PagerConfig {
822 double_write: false,
823 ..Default::default()
824 };
825 let pager = Pager::open(&path, config).expect("open() should succeed");
826 let page = pager
827 .allocate_page(PageType::BTreeLeaf)
828 .expect("allocate_page() should succeed");
829 pager
830 .write_page(page.page_id(), page)
831 .expect("write_page() should succeed");
832 pager.flush().expect("flush() should succeed");
833 }
834
835 assert!(
836 !dwb_path_for(&path).exists(),
837 "DWB may be skipped only after the CoW probe allows it"
838 );
839
840 cleanup(&path);
841 }
842
843 #[test]
844 fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
845 let _override = cow_atomic_write_override(true);
846 let path = temp_db_path();
847 cleanup(&path);
848
849 let page_id;
850 {
851 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
852 let page = pager
853 .allocate_page(PageType::BTreeLeaf)
854 .expect("allocate_page() should succeed");
855 page_id = page.page_id();
856 pager.sync().expect("sync() should succeed");
857 }
858
859 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
860 recovered_page
861 .insert_cell(b"key", b"value")
862 .expect("insert_cell() should succeed");
863 write_dwb_fixture(&path, &[(page_id, recovered_page)]);
864
865 {
866 let config = PagerConfig {
867 double_write: false,
868 ..Default::default()
869 };
870 let pager = Pager::open(&path, config).expect("open() should succeed");
871 let read_page = pager
872 .read_page(page_id)
873 .expect("read_page() should succeed");
874 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
875 assert_eq!(key, b"key");
876 assert_eq!(value, b"value");
877 }
878
879 assert!(!dwb_path_for(&path).exists());
880
881 cleanup(&path);
882 }
883
884 #[test]
885 fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
886 let _override = cow_atomic_write_override(true);
887 let path = temp_db_path();
888 cleanup(&path);
889
890 let config = PagerConfig {
891 double_write: false,
892 ..Default::default()
893 };
894
895 let page_id;
896 let before;
897 let after;
898 {
899 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
900 let mut page = pager
901 .allocate_page(PageType::BTreeLeaf)
902 .expect("allocate_page() should succeed");
903 page_id = page.page_id();
904 page.insert_cell(b"phase", b"before")
905 .expect("insert_cell() should succeed");
906 pager
907 .write_page(page_id, page)
908 .expect("write_page() should succeed");
909 pager.sync().expect("sync() should succeed");
910 before = pager
911 .read_page(page_id)
912 .expect("read_page() should succeed");
913
914 let mut page = before.clone();
915 page.insert_cell(b"phase2", b"after")
916 .expect("insert_cell() should succeed");
917 pager
918 .write_page(page_id, page)
919 .expect("write_page() should succeed");
920 pager.flush().expect("flush() should succeed");
921 after = pager
922 .read_page(page_id)
923 .expect("read_page() should succeed");
924 }
925
926 for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
929 write_page_bytes(&path, page_id, whole_page);
930
931 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
932 let recovered = pager
933 .read_page(page_id)
934 .expect("read_page() should succeed");
935 assert_eq!(recovered.cell_count(), expected_cells);
936 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
937 assert_eq!(key, b"phase");
938 assert_eq!(value, b"before");
939 if expected_cells == 2 {
940 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
941 assert_eq!(key, b"phase2");
942 assert_eq!(value, b"after");
943 }
944 drop(pager);
945 }
946
947 cleanup(&path);
948 }
949
950 #[test]
951 fn same_mid_write_without_cow_recovers_from_dwb() {
952 let _override = cow_atomic_write_override(false);
953 let path = temp_db_path();
954 cleanup(&path);
955
956 let config = PagerConfig {
957 double_write: false,
958 ..Default::default()
959 };
960
961 let page_id;
962 let before;
963 let after;
964 {
965 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
966 let mut page = pager
967 .allocate_page(PageType::BTreeLeaf)
968 .expect("allocate_page() should succeed");
969 page_id = page.page_id();
970 page.insert_cell(b"phase", b"before")
971 .expect("insert_cell() should succeed");
972 pager
973 .write_page(page_id, page)
974 .expect("write_page() should succeed");
975 pager.sync().expect("sync() should succeed");
976 before = pager
977 .read_page(page_id)
978 .expect("read_page() should succeed");
979
980 let mut page = before.clone();
981 page.insert_cell(b"phase2", b"after")
982 .expect("insert_cell() should succeed");
983 pager
984 .write_page(page_id, page)
985 .expect("write_page() should succeed");
986 pager.flush().expect("flush() should succeed");
987 after = pager
988 .read_page(page_id)
989 .expect("read_page() should succeed");
990 }
991
992 write_dwb_fixture(&path, &[(page_id, after.clone())]);
993 write_torn_page_bytes(&path, page_id, &before, &after);
994
995 {
996 let pager = Pager::open(&path, config).expect("open() should succeed");
997 let recovered = pager
998 .read_page(page_id)
999 .expect("read_page() should succeed");
1000 assert_eq!(recovered.cell_count(), 2);
1001
1002 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
1003 assert_eq!(key, b"phase");
1004 assert_eq!(value, b"before");
1005
1006 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
1007 assert_eq!(key, b"phase2");
1008 assert_eq!(value, b"after");
1009 }
1010
1011 assert!(!dwb_path_for(&path).exists());
1012 cleanup(&path);
1013 }
1014
1015 #[test]
1020 fn pager_starts_without_wal_writer() {
1021 let path = temp_db_path();
1022 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1023 assert!(!pager.has_wal_writer());
1024 drop(pager);
1025 cleanup(&path);
1026 }
1027
1028 #[test]
1029 fn set_wal_writer_attaches_handle() {
1030 use crate::storage::wal::writer::WalWriter;
1031 use std::sync::{Arc, Mutex};
1032
1033 let db_path = temp_db_path();
1034 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1035 let _ = fs::remove_file(&wal_path);
1036
1037 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1038 let wal = Arc::new(Mutex::new(
1039 WalWriter::open(&wal_path).expect("open() should succeed"),
1040 ));
1041 pager.set_wal_writer(Arc::clone(&wal));
1042 assert!(pager.has_wal_writer());
1043
1044 pager.clear_wal_writer();
1045 assert!(!pager.has_wal_writer());
1046
1047 drop(pager);
1048 let _ = fs::remove_file(&wal_path);
1049 cleanup(&db_path);
1050 }
1051
1052 #[test]
1053 fn flush_with_lsn_zero_pages_skips_wal_call() {
1054 use crate::storage::wal::writer::WalWriter;
1060 use std::sync::{Arc, Mutex};
1061
1062 let db_path = temp_db_path();
1063 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1064 let _ = fs::remove_file(&wal_path);
1065
1066 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1067 let wal = Arc::new(Mutex::new(
1068 WalWriter::open(&wal_path).expect("open() should succeed"),
1069 ));
1070 let initial_durable = {
1071 let g = wal.lock().expect("lock() should succeed");
1072 g.durable_lsn()
1073 };
1074 pager.set_wal_writer(Arc::clone(&wal));
1075
1076 let mut page = pager
1078 .allocate_page(PageType::BTreeLeaf)
1079 .expect("allocate_page() should succeed");
1080 page.insert_cell(b"k", b"v")
1081 .expect("insert_cell() should succeed");
1082 pager
1084 .write_page(page.page_id(), page)
1085 .expect("write_page() should succeed");
1086 pager.flush().expect("flush() should succeed");
1087
1088 let after_flush = {
1091 let g = wal.lock().expect("lock() should succeed");
1092 g.durable_lsn()
1093 };
1094 assert_eq!(after_flush, initial_durable);
1095
1096 drop(pager);
1097 let _ = fs::remove_file(&wal_path);
1098 cleanup(&db_path);
1099 }
1100
1101 #[test]
1102 fn flush_advances_wal_durable_when_pages_carry_lsn() {
1103 use crate::storage::wal::record::WalRecord;
1107 use crate::storage::wal::writer::WalWriter;
1108 use std::sync::{Arc, Mutex};
1109
1110 let db_path = temp_db_path();
1111 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1112 let _ = fs::remove_file(&wal_path);
1113
1114 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1115 let wal = Arc::new(Mutex::new(
1116 WalWriter::open(&wal_path).expect("open() should succeed"),
1117 ));
1118 pager.set_wal_writer(Arc::clone(&wal));
1119
1120 let stamped_lsn = {
1122 let mut wal_guard = wal.lock().expect("lock() should succeed");
1123 wal_guard
1124 .append(&WalRecord::Begin { tx_id: 1 })
1125 .expect("append() should succeed");
1126 wal_guard
1127 .append(&WalRecord::Commit { tx_id: 1 })
1128 .expect("append() should succeed");
1129 wal_guard.current_lsn()
1130 };
1131 let mut page = pager
1132 .allocate_page(PageType::BTreeLeaf)
1133 .expect("allocate_page() should succeed");
1134 page.insert_cell(b"k", b"v")
1135 .expect("insert_cell() should succeed");
1136 page.set_lsn(stamped_lsn);
1138 pager
1139 .write_page(page.page_id(), page)
1140 .expect("write_page() should succeed");
1141 pager.flush().expect("flush() should succeed");
1142
1143 let after_flush = {
1145 let g = wal.lock().expect("lock() should succeed");
1146 g.durable_lsn()
1147 };
1148 assert!(
1149 after_flush >= stamped_lsn,
1150 "after flush durable_lsn {} must be >= stamped {}",
1151 after_flush,
1152 stamped_lsn
1153 );
1154
1155 drop(pager);
1156 let _ = fs::remove_file(&wal_path);
1157 cleanup(&db_path);
1158 }
1159
1160 #[test]
1165 fn block_size_warn_fires_for_mismatched_block_size() {
1166 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1170 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1172 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1174 }
1175
1176 #[test]
1177 fn block_size_silent_for_divisor() {
1178 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1180 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1181 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1182 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1183 }
1184
1185 #[test]
1186 fn block_size_unavailable_is_silent() {
1187 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1189 }
1190
1191 #[test]
1192 fn page_size_is_unchanged_16kib() {
1193 assert_eq!(PAGE_SIZE, 16 * 1024);
1195 }
1196}