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