Skip to main content

reddb_server/storage/engine/
pager.rs

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