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