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