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