1#[cfg(test)]
36use super::page::PageType;
37use super::page::{Page, PAGE_SIZE};
38use crate::storage::wal::writer::WalWriter;
39use fs2::FileExt;
40pub use reddb_engine::storage::pager::{DatabaseHeader, ExtentId, PagerError, PhysicalFileHeader};
41use reddb_engine::storage::pager::{
42 Pager as SharedPager, PagerConfig as SharedPagerConfig, WalBarrier,
43};
44#[cfg(test)]
45use reddb_engine::storage::pager_io::{
46 DWB_ZONE_BYTES, DWB_ZONE_START_PAGE, FIRST_ALLOCATABLE_PAGE,
47};
48use std::fs::{File, OpenOptions};
49#[cfg(test)]
50use std::io::{Read, Seek, SeekFrom, Write};
51use std::path::{Path, PathBuf};
52#[cfg(test)]
53use std::sync::atomic::{AtomicU8, Ordering};
54use std::sync::{Arc, Mutex, RwLock};
55const DEFAULT_CACHE_SIZE: usize = 10_000;
56#[cfg(test)]
57static COW_ATOMIC_WRITE_TEST_OVERRIDE: AtomicU8 = AtomicU8::new(0);
58
59#[derive(Debug, Clone)]
61pub struct PagerConfig {
62 pub cache_size: usize,
64 pub read_only: bool,
66 pub create: bool,
68 pub verify_checksums: bool,
70 pub double_write: bool,
72 pub encryption: Option<crate::storage::encryption::SecureKey>,
78}
79
80impl Default for PagerConfig {
81 fn default() -> Self {
82 Self {
83 cache_size: DEFAULT_CACHE_SIZE,
84 read_only: false,
85 create: true,
86 verify_checksums: true,
87 double_write: true,
88 encryption: None,
89 }
90 }
91}
92
93pub struct Pager {
95 core: SharedPager<NativePagerFile, NativeWalBarrier>,
96 config: PagerConfig,
97 pub(crate) encryption: Option<(
98 crate::storage::encryption::PageEncryptor,
99 crate::storage::encryption::EncryptionHeader,
100 )>,
101}
102
103#[doc(hidden)]
104pub struct NativePagerFile(File);
105
106impl reddb_file::VfsFile for NativePagerFile {
107 fn try_clone(&self) -> std::io::Result<Self> {
108 Err(std::io::ErrorKind::Unsupported.into())
109 }
110 fn file_len(&self) -> std::io::Result<u64> {
111 Ok(self.0.metadata()?.len())
112 }
113 fn set_len(&self, len: u64) -> std::io::Result<()> {
114 self.0.set_len(len)
115 }
116 fn write_all(&mut self, bytes: &[u8]) -> std::io::Result<()> {
117 std::io::Write::write_all(&mut self.0, bytes)
118 }
119 fn read(&mut self, bytes: &mut [u8]) -> std::io::Result<usize> {
120 std::io::Read::read(&mut self.0, bytes)
121 }
122 fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
123 std::io::Seek::seek(&mut self.0, pos)
124 }
125 fn sync_all(&self) -> std::io::Result<()> {
126 self.0.sync_all()
127 }
128}
129
130pub(crate) trait PageOwnershipRecorder: Send + Sync {
132 fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()>;
133 fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()>;
134 fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()>;
135 fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
136 Ok(false)
137 }
138 fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
139 Ok(())
140 }
141}
142
143#[doc(hidden)]
144#[derive(Default)]
145pub struct NativeWalBarrier(
146 RwLock<Option<Arc<Mutex<WalWriter>>>>,
147 RwLock<Option<Arc<dyn PageOwnershipRecorder>>>,
148);
149
150impl NativeWalBarrier {
151 fn ownership_recorder(&self) -> std::io::Result<Option<Arc<dyn PageOwnershipRecorder>>> {
152 Ok(self
153 .1
154 .read()
155 .map_err(|_| std::io::Error::other("ownership recorder lock poisoned"))?
156 .clone())
157 }
158}
159
160impl WalBarrier for NativeWalBarrier {
161 fn needs_page_image(&self, page_id: u32) -> std::io::Result<bool> {
162 self.ownership_recorder()?
163 .map_or(Ok(false), |recorder| recorder.needs_page_image(page_id))
164 }
165
166 fn capture_page_image(&self, page: &Page) -> std::io::Result<()> {
167 if let Some(recorder) = self.ownership_recorder()? {
168 recorder.capture_page_image(page)?;
169 }
170 Ok(())
171 }
172
173 fn flush_until(&self, lsn: u64) -> std::io::Result<()> {
174 let wal = self
175 .0
176 .read()
177 .map_err(|_| std::io::Error::other("WAL slot lock poisoned"))?
178 .clone();
179 if let Some(wal) = wal {
180 let mut writer = wal.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
181 writer.flush_until(lsn)?;
182 } else if lsn != 0 {
183 return Err(std::io::Error::other(
184 "nonzero page LSN requires a WAL durability barrier",
185 ));
186 }
187 Ok(())
188 }
189
190 fn reserve_pages(&self, start: u32, count: u32) -> std::io::Result<()> {
191 if let Some(recorder) = self.ownership_recorder()? {
192 recorder.reserve_pages(start, count)?;
193 }
194 Ok(())
195 }
196
197 fn detach_free_pages(&self, pages: &[u32]) -> std::io::Result<()> {
198 if let Some(recorder) = self.ownership_recorder()? {
199 recorder.detach_free_pages(pages)?;
200 }
201 Ok(())
202 }
203
204 fn retire_pages(&self, pages: &[u32]) -> std::io::Result<()> {
205 if let Some(recorder) = self.ownership_recorder()? {
206 recorder.retire_pages(pages)?;
207 }
208 Ok(())
209 }
210}
211
212impl std::ops::Deref for Pager {
213 type Target = SharedPager<NativePagerFile, NativeWalBarrier>;
214 fn deref(&self) -> &Self::Target {
215 &self.core
216 }
217}
218
219impl reddb_engine::storage::page_store::PageStore for Pager {
220 fn read_page(&self, id: u32) -> Result<Page, PagerError> {
221 self.core.read_page(id)
222 }
223 fn write_page(&self, id: u32, page: Page) -> Result<(), PagerError> {
224 self.core.write_page(id, page)
225 }
226 fn allocate_page(&self, kind: super::page::PageType) -> Result<Page, PagerError> {
227 self.core.allocate_page(kind)
228 }
229 fn free_page(&self, id: u32) -> Result<(), PagerError> {
230 self.core.free_page(id)
231 }
232 fn page_count(&self) -> Result<u32, PagerError> {
233 self.core.page_count()
234 }
235 fn prefetch_hint(&self, id: u32) {
236 self.prefetch_hint(id)
237 }
238}
239
240#[path = "pager/impl.rs"]
241mod pager_impl;
242impl Drop for Pager {
243 fn drop(&mut self) {
244 let _ = self.flush();
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 #[cfg(target_os = "linux")]
253 use pager_impl::parse_mountinfo_options_for_path;
254 use pager_impl::{
255 classify_cow_filesystem, CowFilesystemKind, BTRFS_SUPER_MAGIC, FS_NOCOW_FL, ZFS_SUPER_MAGIC,
256 };
257 use std::fs;
258 use std::io::Write;
259
260 fn temp_db_path() -> PathBuf {
261 use std::sync::atomic::{AtomicU64, Ordering};
262 static COUNTER: AtomicU64 = AtomicU64::new(0);
263 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
264 let mut path = std::env::temp_dir();
265 path.push(format!("reddb_test_{}_{}.db", std::process::id(), id));
266 path
267 }
268
269 fn cleanup(path: &Path) {
270 let _ = fs::remove_file(path);
278 for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
279 let _ = fs::remove_file(sidecar);
280 }
281 }
282
283 fn dwb_path_for(path: &Path) -> PathBuf {
284 reddb_file::layout::pager_dwb_shadow_path(path)
285 }
286
287 fn dwb_zone_offset_for_test() -> u64 {
288 3 * PAGE_SIZE as u64
289 }
290
291 static COW_ATOMIC_WRITE_OVERRIDE_GUARD: Mutex<()> = Mutex::new(());
292
293 struct CowAtomicWriteOverrideGuard {
294 _guard: std::sync::MutexGuard<'static, ()>,
295 }
296
297 impl Drop for CowAtomicWriteOverrideGuard {
298 fn drop(&mut self) {
299 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(0, Ordering::Relaxed);
300 }
301 }
302
303 fn cow_atomic_write_override(value: bool) -> CowAtomicWriteOverrideGuard {
304 let guard = COW_ATOMIC_WRITE_OVERRIDE_GUARD
305 .lock()
306 .unwrap_or_else(|err| err.into_inner());
307 COW_ATOMIC_WRITE_TEST_OVERRIDE.store(if value { 1 } else { 2 }, Ordering::Relaxed);
308 CowAtomicWriteOverrideGuard { _guard: guard }
309 }
310
311 fn write_dwb_fixture(path: &Path, pages: &[(u32, Page)]) {
312 let pages: Vec<_> = pages
313 .iter()
314 .map(|(page_id, page)| {
315 let mut page = page.clone();
316 page.update_checksum();
317 (*page_id, page)
318 })
319 .collect();
320 let buf = reddb_file::encode_paged_dwb_frame(
321 pages
322 .iter()
323 .map(|(page_id, page)| (*page_id, page.as_bytes())),
324 );
325
326 let mut file = OpenOptions::new()
327 .read(true)
328 .write(true)
329 .open(path)
330 .expect("open() should succeed");
331 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
332 .expect("seek() should succeed");
333 file.write_all(&buf).expect("write_all() should succeed");
334 file.sync_all().expect("sync_all() should succeed");
335 }
336
337 fn write_page_bytes(path: &Path, page_id: u32, page: &Page) {
338 let mut file = OpenOptions::new()
339 .write(true)
340 .open(path)
341 .expect("open() should succeed");
342 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
343 .expect("value is present");
344 file.write_all(page.as_bytes())
345 .expect("write_all() should succeed");
346 file.sync_all().expect("sync_all() should succeed");
347 }
348
349 fn write_torn_page_bytes(path: &Path, page_id: u32, before: &Page, after: &Page) {
350 let mut torn = *before.as_bytes();
351 torn[..PAGE_SIZE / 2].copy_from_slice(&after.as_bytes()[..PAGE_SIZE / 2]);
352
353 let mut file = OpenOptions::new()
354 .write(true)
355 .open(path)
356 .expect("open() should succeed");
357 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))
358 .expect("value is present");
359 file.write_all(&torn).expect("write_all() should succeed");
360 file.sync_all().expect("sync_all() should succeed");
361 }
362
363 #[test]
364 fn test_pager_create_new() {
365 let path = temp_db_path();
366 cleanup(&path);
367
368 {
369 let pager = Pager::open_default(&path).expect("open_default() should succeed");
370 assert_eq!(
371 pager.page_count().expect("page_count() should succeed"),
372 FIRST_ALLOCATABLE_PAGE
373 );
374 }
376
377 cleanup(&path);
378 }
379
380 #[test]
381 fn native_pager_lock_guards_own_io_and_releases_on_drop() {
382 let directory = tempfile::tempdir().expect("directory");
383 let path = directory.path().join("locked-io.rdb");
384 let pager = Pager::open_default(&path).expect("locked pager opens");
385 let mut page = pager
386 .allocate_page(PageType::BTreeLeaf)
387 .expect("allocate under lock");
388 let id = page.page_id();
389 page.insert_cell(b"key", b"kept").expect("cell");
390 pager.write_page(id, page).expect("write under lock");
391 pager.sync().expect("sync under lock");
392 assert_eq!(
393 pager
394 .read_page(id)
395 .expect("read under lock")
396 .read_cell(0)
397 .expect("cell"),
398 (b"key".to_vec(), b"kept".to_vec())
399 );
400 assert!(
401 matches!(Pager::open_default(&path), Err(PagerError::Locked)),
402 "the writer fence remains exclusive"
403 );
404 drop(pager);
405 let reopened = Pager::open_default(&path).expect("last I/O handle releases writer fence");
406 assert_eq!(
407 reopened
408 .read_page(id)
409 .expect("durable page")
410 .read_cell(0)
411 .expect("durable cell"),
412 (b"key".to_vec(), b"kept".to_vec())
413 );
414 }
415
416 #[test]
417 fn test_pager_reopen() {
418 let path = temp_db_path();
419 cleanup(&path);
420
421 {
423 let pager = Pager::open_default(&path).expect("open_default() should succeed");
424
425 let page = pager
427 .allocate_page(PageType::BTreeLeaf)
428 .expect("allocate_page() should succeed");
429 assert_eq!(page.page_id(), FIRST_ALLOCATABLE_PAGE);
430
431 pager.sync().expect("sync() should succeed");
432 }
433
434 {
436 let pager = Pager::open_default(&path).expect("open_default() should succeed");
437 assert_eq!(
438 pager.page_count().expect("page_count() should succeed"),
439 FIRST_ALLOCATABLE_PAGE + 1
440 );
441 }
443
444 cleanup(&path);
445 }
446
447 #[test]
448 fn test_pager_read_write() {
449 let path = temp_db_path();
450 cleanup(&path);
451
452 {
453 let pager = Pager::open_default(&path).expect("open_default() should succeed");
454
455 let mut page = pager
457 .allocate_page(PageType::BTreeLeaf)
458 .expect("allocate_page() should succeed");
459 let page_id = page.page_id();
460
461 page.insert_cell(b"key", b"value")
462 .expect("insert_cell() should succeed");
463 pager
464 .write_page(page_id, page)
465 .expect("write_page() should succeed");
466
467 let read_page = pager
469 .read_page(page_id)
470 .expect("read_page() should succeed");
471 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
472 assert_eq!(key, b"key");
473 assert_eq!(value, b"value");
474 }
475
476 cleanup(&path);
477 }
478
479 #[test]
480 fn test_pager_cache() {
481 let path = temp_db_path();
482 cleanup(&path);
483
484 {
485 let pager = Pager::open_default(&path).expect("open_default() should succeed");
486
487 let page = pager
489 .allocate_page(PageType::BTreeLeaf)
490 .expect("allocate_page() should succeed");
491 let page_id = page.page_id();
492
493 let _ = pager
495 .read_page(page_id)
496 .expect("read_page() should succeed");
497
498 let _ = pager
500 .read_page(page_id)
501 .expect("read_page() should succeed");
502
503 let stats = pager.cache_stats();
504 assert!(stats.hits >= 1);
505 }
506
507 cleanup(&path);
508 }
509
510 #[test]
511 fn test_pager_free_page() {
512 let path = temp_db_path();
513 cleanup(&path);
514
515 {
516 let pager = Pager::open_default(&path).expect("open_default() should succeed");
517
518 let page1 = pager
520 .allocate_page(PageType::BTreeLeaf)
521 .expect("allocate_page() should succeed");
522 let page2 = pager
523 .allocate_page(PageType::BTreeLeaf)
524 .expect("allocate_page() should succeed");
525
526 let id1 = page1.page_id();
527 let id2 = page2.page_id();
528
529 pager.free_page(id1).expect("free_page() should succeed");
531
532 let page3 = pager
534 .allocate_page(PageType::BTreeLeaf)
535 .expect("allocate_page() should succeed");
536 assert_eq!(page3.page_id(), id1);
537 }
538
539 cleanup(&path);
540 }
541
542 #[test]
543 fn test_freelist_persistence() {
544 let path = temp_db_path();
545 cleanup(&path);
546
547 let freed_id;
548 {
549 let pager = Pager::open_default(&path).expect("open_default() should succeed");
550 let page1 = pager
551 .allocate_page(PageType::BTreeLeaf)
552 .expect("allocate_page() should succeed");
553 let _page2 = pager
554 .allocate_page(PageType::BTreeLeaf)
555 .expect("allocate_page() should succeed");
556 freed_id = page1.page_id();
557
558 pager
559 .free_page(freed_id)
560 .expect("free_page() should succeed");
561 pager.sync().expect("sync() should succeed");
562 }
563
564 {
565 let pager = Pager::open_default(&path).expect("open_default() should succeed");
566 let page = pager
567 .allocate_page(PageType::BTreeLeaf)
568 .expect("allocate_page() should succeed");
569 assert_eq!(page.page_id(), freed_id);
570 }
571
572 cleanup(&path);
573 }
574
575 #[test]
576 fn test_pager_read_only() {
577 let path = temp_db_path();
578 cleanup(&path);
579
580 {
582 let pager = Pager::open_default(&path).expect("open_default() should succeed");
583 pager.sync().expect("sync() should succeed");
584 }
585
586 {
588 let config = PagerConfig {
589 read_only: true,
590 ..Default::default()
591 };
592
593 let pager = Pager::open(&path, config).expect("open() should succeed");
594 assert!(pager.is_read_only());
595
596 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
598 }
599
600 cleanup(&path);
601 }
602
603 #[test]
604 fn test_dwb_recovery_clears_in_place_and_keeps_file_reusable() {
605 let path = temp_db_path();
606 cleanup(&path);
607
608 let config = PagerConfig {
609 double_write: true,
610 ..Default::default()
611 };
612
613 let page_id;
614 {
615 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
616 let page = pager
617 .allocate_page(PageType::BTreeLeaf)
618 .expect("allocate_page() should succeed");
619 page_id = page.page_id();
620 pager.sync().expect("sync() should succeed");
621 }
622
623 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
624 recovered_page
625 .insert_cell(b"key", b"value")
626 .expect("insert_cell() should succeed");
627 write_dwb_fixture(&path, &[(page_id, recovered_page.clone())]);
628
629 assert!(!dwb_path_for(&path).exists());
630
631 {
632 let pager = Pager::open(&path, config).expect("open() should succeed");
633
634 let read_page = pager
635 .read_page(page_id)
636 .expect("read_page() should succeed");
637 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
638 assert_eq!(key, b"key");
639 assert_eq!(value, b"value");
640
641 assert!(!dwb_path_for(&path).exists());
642
643 let mut updated_page = recovered_page.clone();
644 updated_page
645 .insert_cell(b"key2", b"value2")
646 .expect("insert_cell() should succeed");
647 pager
648 .write_page(page_id, updated_page)
649 .expect("write_page() should succeed");
650 pager.flush().expect("flush() should succeed");
651
652 assert!(!dwb_path_for(&path).exists());
653 }
654
655 cleanup(&path);
656 }
657
658 #[test]
659 fn retired_dwb_sidecar_routes_through_offline_migration() {
660 let path = temp_db_path();
661 cleanup(&path);
662
663 {
664 let pager = Pager::open_default(&path).expect("open_default() should succeed");
665 pager.sync().expect("sync() should succeed");
666 }
667
668 let dwb = dwb_path_for(&path);
669 fs::write(&dwb, b"legacy dwb sidecar").expect("write legacy sidecar");
670
671 let err = match Pager::open_default(&path) {
672 Ok(_) => panic!("retired DWB sidecar must refuse open"),
673 Err(err) => err,
674 };
675 assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
676
677 cleanup(&path);
678 }
679
680 #[test]
681 fn interrupted_dwb_staging_write_is_discarded_on_open() {
682 let path = temp_db_path();
683 cleanup(&path);
684
685 {
686 let pager = Pager::open_default(&path).expect("open_default() should succeed");
687 pager.sync().expect("sync() should succeed");
688 }
689
690 let mut file = OpenOptions::new()
691 .read(true)
692 .write(true)
693 .open(&path)
694 .expect("open() should succeed");
695 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
696 .expect("seek() should succeed");
697 file.write_all(b"RDDW")
698 .expect("write corrupt DWB magic should succeed");
699 file.sync_all().expect("sync_all() should succeed");
700
701 {
702 let pager = Pager::open_default(&path).expect("torn staging frame should be discarded");
703 pager.sync().expect("sync() should succeed");
704 }
705
706 let mut cleared = [0u8; 4];
707 let mut file = OpenOptions::new()
708 .read(true)
709 .open(&path)
710 .expect("open() should succeed");
711 file.seek(SeekFrom::Start(dwb_zone_offset_for_test()))
712 .expect("seek() should succeed");
713 file.read_exact(&mut cleared)
714 .expect("read_exact() should succeed");
715 assert_ne!(cleared, reddb_file::DWB_MAGIC);
716
717 cleanup(&path);
718 }
719
720 #[test]
721 fn cow_probe_classification_fails_closed_for_btrfs_nodatacow() {
722 assert_eq!(
723 classify_cow_filesystem(ZFS_SUPER_MAGIC, None, None),
724 Some(CowFilesystemKind::Zfs),
725 "ZFS is always CoW"
726 );
727 assert_eq!(
728 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,relatime"), Some(0)),
729 Some(CowFilesystemKind::BtrfsDataCow),
730 "btrfs qualifies only when datacow remains enabled"
731 );
732 assert_eq!(
733 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw,nodatacow"), Some(0)),
734 None,
735 "btrfs nodatacow mount option must reject DWB skip"
736 );
737 assert_eq!(
738 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), Some(FS_NOCOW_FL)),
739 None,
740 "btrfs chattr +C / NOCOW inode flag must reject DWB skip"
741 );
742 assert_eq!(
743 classify_cow_filesystem(BTRFS_SUPER_MAGIC, Some("rw"), None),
744 None,
745 "missing btrfs inode flags are uncertain and must fail closed"
746 );
747 assert_eq!(
748 classify_cow_filesystem(BTRFS_SUPER_MAGIC, None, Some(0)),
749 None,
750 "missing btrfs mount options are uncertain and must fail closed"
751 );
752 }
753
754 #[cfg(target_os = "linux")]
755 #[test]
756 fn mountinfo_parser_uses_longest_cow_mount_and_rejects_nodatacow() {
757 let mountinfo = "\
75824 18 0:21 / / rw,relatime - ext4 /dev/root rw\n\
75935 24 0:42 /subvol /mnt/reddb rw,relatime - btrfs /dev/sdb rw,space_cache=v2\n\
76036 35 0:43 /nocow /mnt/reddb/nocow rw,relatime - btrfs /dev/sdb rw,nodatacow\n\
761";
762
763 assert_eq!(
764 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/data.rdb"))
765 .as_deref(),
766 Some("rw,relatime,rw,space_cache=v2")
767 );
768 assert_eq!(
769 parse_mountinfo_options_for_path(mountinfo, Path::new("/mnt/reddb/nocow/data.rdb"))
770 .as_deref(),
771 Some("rw,relatime,rw,nodatacow")
772 );
773 }
774
775 #[test]
776 fn double_write_false_keeps_dwb_when_cow_probe_denies() {
777 let _override = cow_atomic_write_override(false);
778 let path = temp_db_path();
779 cleanup(&path);
780
781 {
782 let config = PagerConfig {
783 double_write: false,
784 ..Default::default()
785 };
786 let pager = Pager::open(&path, config).expect("open() should succeed");
787 let page = pager
788 .allocate_page(PageType::BTreeLeaf)
789 .expect("allocate_page() should succeed");
790 pager
791 .write_page(page.page_id(), page)
792 .expect("write_page() should succeed");
793 pager.flush().expect("flush() should succeed");
794 }
795
796 assert!(
797 !dwb_path_for(&path).exists(),
798 "DWB must stay enabled in-file when double_write=false is not proven safe"
799 );
800
801 cleanup(&path);
802 }
803
804 #[test]
805 fn double_write_false_skips_dwb_when_cow_probe_allows() {
806 let _override = cow_atomic_write_override(true);
807 let path = temp_db_path();
808 cleanup(&path);
809
810 {
811 let config = PagerConfig {
812 double_write: false,
813 ..Default::default()
814 };
815 let pager = Pager::open(&path, config).expect("open() should succeed");
816 let page = pager
817 .allocate_page(PageType::BTreeLeaf)
818 .expect("allocate_page() should succeed");
819 pager
820 .write_page(page.page_id(), page)
821 .expect("write_page() should succeed");
822 pager.flush().expect("flush() should succeed");
823 }
824
825 assert!(
826 !dwb_path_for(&path).exists(),
827 "DWB may be skipped only after the CoW probe allows it"
828 );
829
830 cleanup(&path);
831 }
832
833 #[test]
834 fn double_write_false_on_cow_recovers_from_in_file_dwb_zone() {
835 let _override = cow_atomic_write_override(true);
836 let path = temp_db_path();
837 cleanup(&path);
838
839 let page_id;
840 {
841 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
842 let page = pager
843 .allocate_page(PageType::BTreeLeaf)
844 .expect("allocate_page() should succeed");
845 page_id = page.page_id();
846 pager.sync().expect("sync() should succeed");
847 }
848
849 let mut recovered_page = Page::new(PageType::BTreeLeaf, page_id);
850 recovered_page
851 .insert_cell(b"key", b"value")
852 .expect("insert_cell() should succeed");
853 write_dwb_fixture(&path, &[(page_id, recovered_page)]);
854
855 {
856 let config = PagerConfig {
857 double_write: false,
858 ..Default::default()
859 };
860 let pager = Pager::open(&path, config).expect("open() should succeed");
861 let read_page = pager
862 .read_page(page_id)
863 .expect("read_page() should succeed");
864 let (key, value) = read_page.read_cell(0).expect("read_cell() should succeed");
865 assert_eq!(key, b"key");
866 assert_eq!(value, b"value");
867 }
868
869 assert!(!dwb_path_for(&path).exists());
870
871 cleanup(&path);
872 }
873
874 #[test]
875 fn simulated_cow_mid_write_leaves_a_whole_consistent_page_without_dwb() {
876 let _override = cow_atomic_write_override(true);
877 let path = temp_db_path();
878 cleanup(&path);
879
880 let config = PagerConfig {
881 double_write: false,
882 ..Default::default()
883 };
884
885 let page_id;
886 let before;
887 let after;
888 {
889 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
890 let mut page = pager
891 .allocate_page(PageType::BTreeLeaf)
892 .expect("allocate_page() should succeed");
893 page_id = page.page_id();
894 page.insert_cell(b"phase", b"before")
895 .expect("insert_cell() should succeed");
896 pager
897 .write_page(page_id, page)
898 .expect("write_page() should succeed");
899 pager.sync().expect("sync() should succeed");
900 before = pager
901 .read_page(page_id)
902 .expect("read_page() should succeed");
903
904 let mut page = before.clone();
905 page.insert_cell(b"phase2", b"after")
906 .expect("insert_cell() should succeed");
907 pager
908 .write_page(page_id, page)
909 .expect("write_page() should succeed");
910 pager.flush().expect("flush() should succeed");
911 after = pager
912 .read_page(page_id)
913 .expect("read_page() should succeed");
914 }
915
916 for (whole_page, expected_cells) in [(&before, 1), (&after, 2)] {
919 write_page_bytes(&path, page_id, whole_page);
920
921 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
922 let recovered = pager
923 .read_page(page_id)
924 .expect("read_page() should succeed");
925 assert_eq!(recovered.cell_count(), expected_cells);
926 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
927 assert_eq!(key, b"phase");
928 assert_eq!(value, b"before");
929 if expected_cells == 2 {
930 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
931 assert_eq!(key, b"phase2");
932 assert_eq!(value, b"after");
933 }
934 drop(pager);
935 }
936
937 cleanup(&path);
938 }
939
940 #[test]
941 fn same_mid_write_without_cow_recovers_from_dwb() {
942 let _override = cow_atomic_write_override(false);
943 let path = temp_db_path();
944 cleanup(&path);
945
946 let config = PagerConfig {
947 double_write: false,
948 ..Default::default()
949 };
950
951 let page_id;
952 let before;
953 let after;
954 {
955 let pager = Pager::open(&path, config.clone()).expect("open() should succeed");
956 let mut page = pager
957 .allocate_page(PageType::BTreeLeaf)
958 .expect("allocate_page() should succeed");
959 page_id = page.page_id();
960 page.insert_cell(b"phase", b"before")
961 .expect("insert_cell() should succeed");
962 pager
963 .write_page(page_id, page)
964 .expect("write_page() should succeed");
965 pager.sync().expect("sync() should succeed");
966 before = pager
967 .read_page(page_id)
968 .expect("read_page() should succeed");
969
970 let mut page = before.clone();
971 page.insert_cell(b"phase2", b"after")
972 .expect("insert_cell() should succeed");
973 pager
974 .write_page(page_id, page)
975 .expect("write_page() should succeed");
976 pager.flush().expect("flush() should succeed");
977 after = pager
978 .read_page(page_id)
979 .expect("read_page() should succeed");
980 }
981
982 write_dwb_fixture(&path, &[(page_id, after.clone())]);
983 write_torn_page_bytes(&path, page_id, &before, &after);
984
985 {
986 let pager = Pager::open(&path, config).expect("open() should succeed");
987 let recovered = pager
988 .read_page(page_id)
989 .expect("read_page() should succeed");
990 assert_eq!(recovered.cell_count(), 2);
991
992 let (key, value) = recovered.read_cell(0).expect("read_cell() should succeed");
993 assert_eq!(key, b"phase");
994 assert_eq!(value, b"before");
995
996 let (key, value) = recovered.read_cell(1).expect("read_cell() should succeed");
997 assert_eq!(key, b"phase2");
998 assert_eq!(value, b"after");
999 }
1000
1001 assert!(!dwb_path_for(&path).exists());
1002 cleanup(&path);
1003 }
1004
1005 #[test]
1010 fn pager_starts_without_wal_writer() {
1011 let path = temp_db_path();
1012 let pager = Pager::open(&path, PagerConfig::default()).expect("open() should succeed");
1013 assert!(!pager.has_wal_writer());
1014 drop(pager);
1015 cleanup(&path);
1016 }
1017
1018 #[test]
1019 fn flush_refuses_nonzero_lsn_without_a_wal_writer() {
1020 let path = temp_db_path();
1021 let pager = Pager::open(&path, PagerConfig::default()).expect("open pager");
1022 let mut page = pager
1023 .allocate_page(PageType::BTreeLeaf)
1024 .expect("allocate page");
1025 page.set_lsn(42);
1026 pager.write_page(page.page_id(), page).expect("write page");
1027
1028 assert!(
1029 pager.flush().is_err(),
1030 "a page with an LSN needs a WAL barrier"
1031 );
1032 assert!(
1033 pager.dirty_page_count() > 0,
1034 "failed flush must retain dirty pages"
1035 );
1036 assert!(
1037 pager.flush().is_err(),
1038 "failed writeback must remain fenced"
1039 );
1040 drop(pager);
1041 cleanup(&path);
1042 }
1043
1044 #[test]
1045 fn set_wal_writer_attaches_handle() {
1046 use crate::storage::wal::writer::WalWriter;
1047 use std::sync::{Arc, Mutex};
1048
1049 let db_path = temp_db_path();
1050 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1051 let _ = fs::remove_file(&wal_path);
1052
1053 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1054 let wal = Arc::new(Mutex::new(
1055 WalWriter::open(&wal_path).expect("open() should succeed"),
1056 ));
1057 pager.set_wal_writer(Arc::clone(&wal));
1058 assert!(pager.has_wal_writer());
1059
1060 pager.clear_wal_writer();
1061 assert!(!pager.has_wal_writer());
1062
1063 drop(pager);
1064 let _ = fs::remove_file(&wal_path);
1065 cleanup(&db_path);
1066 }
1067
1068 #[test]
1069 fn flush_with_lsn_zero_pages_skips_wal_call() {
1070 use crate::storage::wal::writer::WalWriter;
1076 use std::sync::{Arc, Mutex};
1077
1078 let db_path = temp_db_path();
1079 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1080 let _ = fs::remove_file(&wal_path);
1081
1082 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1083 let wal = Arc::new(Mutex::new(
1084 WalWriter::open(&wal_path).expect("open() should succeed"),
1085 ));
1086 let initial_durable = {
1087 let g = wal.lock().expect("lock() should succeed");
1088 g.durable_lsn()
1089 };
1090 pager.set_wal_writer(Arc::clone(&wal));
1091
1092 let mut page = pager
1094 .allocate_page(PageType::BTreeLeaf)
1095 .expect("allocate_page() should succeed");
1096 page.insert_cell(b"k", b"v")
1097 .expect("insert_cell() should succeed");
1098 pager
1100 .write_page(page.page_id(), page)
1101 .expect("write_page() should succeed");
1102 pager.flush().expect("flush() should succeed");
1103
1104 let after_flush = {
1107 let g = wal.lock().expect("lock() should succeed");
1108 g.durable_lsn()
1109 };
1110 assert_eq!(after_flush, initial_durable);
1111
1112 drop(pager);
1113 let _ = fs::remove_file(&wal_path);
1114 cleanup(&db_path);
1115 }
1116
1117 #[test]
1118 fn flush_advances_wal_durable_when_pages_carry_lsn() {
1119 use crate::storage::wal::record::WalRecord;
1123 use crate::storage::wal::writer::WalWriter;
1124 use std::sync::{Arc, Mutex};
1125
1126 let db_path = temp_db_path();
1127 let wal_path = reddb_file::layout::pager_legacy_wal_path(&db_path);
1128 let _ = fs::remove_file(&wal_path);
1129
1130 let pager = Pager::open(&db_path, PagerConfig::default()).expect("open() should succeed");
1131 let wal = Arc::new(Mutex::new(
1132 WalWriter::open(&wal_path).expect("open() should succeed"),
1133 ));
1134 pager.set_wal_writer(Arc::clone(&wal));
1135
1136 let stamped_lsn = {
1138 let mut wal_guard = wal.lock().expect("lock() should succeed");
1139 wal_guard
1140 .append(&WalRecord::Begin { tx_id: 1 })
1141 .expect("append() should succeed");
1142 wal_guard
1143 .append(&WalRecord::Commit { tx_id: 1 })
1144 .expect("append() should succeed");
1145 wal_guard.current_lsn()
1146 };
1147 let mut page = pager
1148 .allocate_page(PageType::BTreeLeaf)
1149 .expect("allocate_page() should succeed");
1150 page.insert_cell(b"k", b"v")
1151 .expect("insert_cell() should succeed");
1152 page.set_lsn(stamped_lsn);
1154 pager
1155 .write_page(page.page_id(), page)
1156 .expect("write_page() should succeed");
1157 pager.flush().expect("flush() should succeed");
1158
1159 let after_flush = {
1161 let g = wal.lock().expect("lock() should succeed");
1162 g.durable_lsn()
1163 };
1164 assert!(
1165 after_flush >= stamped_lsn,
1166 "after flush durable_lsn {} must be >= stamped {}",
1167 after_flush,
1168 stamped_lsn
1169 );
1170
1171 drop(pager);
1172 let _ = fs::remove_file(&wal_path);
1173 cleanup(&db_path);
1174 }
1175
1176 #[test]
1181 fn block_size_warn_fires_for_mismatched_block_size() {
1182 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6000));
1186 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 1_048_576));
1188 assert!(Pager::page_size_misaligned_with_block(PAGE_SIZE, 6 * 1024));
1190 }
1191
1192 #[test]
1193 fn block_size_silent_for_divisor() {
1194 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 4096));
1196 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 16384));
1197 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 512));
1198 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 8192));
1199 }
1200
1201 #[test]
1202 fn block_size_unavailable_is_silent() {
1203 assert!(!Pager::page_size_misaligned_with_block(PAGE_SIZE, 0));
1205 }
1206
1207 #[test]
1208 fn page_size_is_unchanged_16kib() {
1209 assert_eq!(PAGE_SIZE, 16 * 1024);
1211 }
1212}