1use super::freelist::FreeList;
9use super::page::{Page, PageError, PageType, PAGE_SIZE};
10use super::page_cache::PageCache;
11use super::pager_io::{self, FIRST_ALLOCATABLE_PAGE};
12use reddb_file::VfsFile;
13pub use reddb_file::{DatabaseHeader, PhysicalFileHeader};
14use std::path::{Path, PathBuf};
15use std::sync::{Mutex, RwLock};
16
17#[derive(Debug)]
19pub enum PagerError {
20 Io(std::io::Error),
22 Page(PageError),
24 InvalidDatabase(String),
26 ReadOnly,
28 PageNotFound(u32),
30 Locked,
32 LockPoisoned,
34 EncryptionRequired,
36 PlainDatabaseRefusesKey,
38 InvalidKey,
40 SuperblockZoneUnrecoverable(PathBuf),
42 ManifestZoneCorrupt(PathBuf),
44 DwbZoneCorrupt(PathBuf),
46 LegacySidecarStore { sidecar: PathBuf },
48 LegacyPagedStore { path: PathBuf },
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct ExtentId {
55 pub start_page: u32,
56 pub n_pages: u32,
57}
58
59impl std::fmt::Display for PagerError {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::Io(e) => write!(f, "I/O error: {}", e),
63 Self::Page(e) => write!(f, "Page error: {}", e),
64 Self::InvalidDatabase(msg) => write!(f, "Invalid database: {}", msg),
65 Self::ReadOnly => write!(f, "Database is read-only"),
66 Self::PageNotFound(id) => write!(f, "Page {} not found", id),
67 Self::Locked => write!(f, "Database is locked"),
68 Self::LockPoisoned => write!(f, "Internal lock poisoned (concurrent thread panicked)"),
69 Self::EncryptionRequired => write!(
70 f,
71 "Database is encrypted but no key was supplied (set PagerConfig::encryption)"
72 ),
73 Self::PlainDatabaseRefusesKey => write!(
74 f,
75 "Plain (unencrypted) database opened with an encryption key — refusing"
76 ),
77 Self::InvalidKey => write!(f, "Encryption key validation failed for this database"),
78 Self::SuperblockZoneUnrecoverable(path) => write!(
79 f,
80 "superblock zone of {} is unrecoverable: both ping-pong copies failed \
81 validation, so no generation can be trusted to root the store. The store \
82 will not be opened. Preserve the database and its recovery files; \
83 restore from a verified backup.",
84 path.display()
85 ),
86 Self::ManifestZoneCorrupt(path) => write!(
87 f,
88 "internal manifest zone of {} failed its checksum: the zone that names \
89 collections, indexes and the checkpoint boundary cannot be trusted, so no \
90 rows are returned. Preserve the database and its recovery files; \
91 restore from a verified backup.",
92 path.display()
93 ),
94 Self::DwbZoneCorrupt(path) => write!(
95 f,
96 "double-write zone of {} failed validation: page recovery cannot prove \
97 whether the protected in-place write completed. The store will not be opened \
98 silently. Preserve the database and its recovery files; restore from \
99 a verified backup.",
100 path.display()
101 ),
102 Self::LegacySidecarStore { sidecar } => write!(
103 f,
104 "refusing to open a legacy sidecar-backed store: found {}. The superblock \
105 pair, internal manifest, and double-write recovery state now live inside the \
106 .rdb file (ADR 0038 §2/§4), \
107 and the engine never reads the retired sidecars silently. Convert the store \
108 first with the offline migration tool \
109 (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is reversible.",
110 sidecar.display()
111 ),
112 Self::LegacyPagedStore { path } => write!(
113 f,
114 "refusing to open a pre-phase-3 paged store: {} does not carry the layout marker \
115 for the in-file double-write zone. Convert the store first with the offline \
116 migration tool (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is \
117 reversible.",
118 path.display()
119 ),
120 }
121 }
122}
123
124impl std::error::Error for PagerError {
125 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
126 match self {
127 Self::Io(e) => Some(e),
128 Self::Page(e) => Some(e),
129 _ => None,
130 }
131 }
132}
133
134impl From<std::io::Error> for PagerError {
135 fn from(e: std::io::Error) -> Self {
136 Self::Io(e)
137 }
138}
139
140impl From<PageError> for PagerError {
141 fn from(e: PageError) -> Self {
142 Self::Page(e)
143 }
144}
145
146impl From<pager_io::PagerIoError> for PagerError {
147 fn from(error: pager_io::PagerIoError) -> Self {
148 match error {
149 pager_io::PagerIoError::Io(error) => Self::Io(error),
150 pager_io::PagerIoError::Page(error) => Self::Page(error),
151 pager_io::PagerIoError::InvalidDatabase(message) => Self::InvalidDatabase(message),
152 }
153 }
154}
155
156#[derive(Debug, Clone)]
158pub struct PagerConfig {
159 pub disk_residency: bool,
161 pub cache_size: usize,
162 pub read_only: bool,
163 pub verify_checksums: bool,
164 pub double_write: bool,
165}
166
167impl Default for PagerConfig {
168 fn default() -> Self {
169 Self {
170 disk_residency: false,
171 cache_size: 10_000,
172 read_only: false,
173 verify_checksums: true,
174 double_write: true,
175 }
176 }
177}
178
179pub trait WalBarrier {
182 fn flush_until(&self, lsn: u64) -> std::io::Result<()>;
183
184 fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
187 Ok(false)
188 }
189
190 fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
193 Ok(())
194 }
195
196 fn reserve_pages(&self, _start: u32, _count: u32) -> std::io::Result<()> {
198 Ok(())
199 }
200
201 fn detach_free_pages(&self, _pages: &[u32]) -> std::io::Result<()> {
203 Ok(())
204 }
205
206 fn has_page_ownership_journal(&self) -> std::io::Result<bool> {
209 Ok(false)
210 }
211
212 fn retire_pages(&self, _pages: &[u32]) -> std::io::Result<()> {
214 Ok(())
215 }
216}
217
218pub struct NoWal;
220impl WalBarrier for NoWal {
221 fn flush_until(&self, _lsn: u64) -> std::io::Result<()> {
222 Err(std::io::Error::other(
223 "nonzero page LSN requires a WAL durability barrier",
224 ))
225 }
226}
227pub struct Pager<F: VfsFile, W: WalBarrier> {
228 path: PathBuf,
230 file: Mutex<F>,
232 dwb_enabled: bool,
234 writeback_failed: std::sync::atomic::AtomicBool,
237 cache: PageCache,
239 read_only_recovery: RwLock<std::collections::HashMap<u32, Page>>,
241 freelist: RwLock<FreeList>,
243 deferred_reclamation: Mutex<DeferredPageReclamation>,
246 ownership_mutations: parking_lot::ReentrantMutex<()>,
249 allocation_reused: std::sync::atomic::AtomicBool,
252 allocation_fence_pending: std::sync::atomic::AtomicBool,
254 header: RwLock<DatabaseHeader>,
256 superblock_publication: Mutex<()>,
258 config: PagerConfig,
260 header_dirty: Mutex<bool>,
262 wal: W,
263}
264
265#[derive(Default)]
266struct DeferredPageReclamation {
267 enabled: bool,
268 pending: std::collections::BTreeSet<u32>,
269 published: std::collections::BTreeSet<u32>,
270}
271
272const SUPERBLOCK_SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
274const SUPERBLOCK_ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
275const PHASE3_DWB_ZONE_FILE_VERSION: u32 = 0x0001_0001;
278
279pub type NewestSuperblock = (
281 reddb_file::PagedSuperblockSelection,
282 [u8; SUPERBLOCK_SLOT_SIZE],
283);
284
285fn superblock_slot_image(page: &[u8; PAGE_SIZE]) -> [u8; SUPERBLOCK_SLOT_SIZE] {
288 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
289 image.copy_from_slice(&page[..SUPERBLOCK_SLOT_SIZE]);
290 image
291}
292
293impl<F: VfsFile, W: WalBarrier> Pager<F, W> {
294 pub fn open_owned(
298 file: F,
299 path: PathBuf,
300 config: PagerConfig,
301 wal: W,
302 create: bool,
303 ) -> Result<Self, PagerError> {
304 if create && file.file_len()? != 0 {
305 return Err(PagerError::InvalidDatabase(
306 "new pager file is not empty".into(),
307 ));
308 }
309 let pager = Self {
310 path,
311 file: Mutex::new(file),
312 dwb_enabled: config.double_write && !config.read_only,
313 writeback_failed: std::sync::atomic::AtomicBool::new(false),
314 cache: PageCache::new(config.cache_size),
315 read_only_recovery: RwLock::new(std::collections::HashMap::new()),
316 freelist: RwLock::new(FreeList::new()),
317 deferred_reclamation: Mutex::new(DeferredPageReclamation::default()),
318 ownership_mutations: parking_lot::ReentrantMutex::new(()),
319 allocation_reused: std::sync::atomic::AtomicBool::new(false),
320 allocation_fence_pending: std::sync::atomic::AtomicBool::new(false),
321 header: RwLock::new(DatabaseHeader::default()),
322 superblock_publication: Mutex::new(()),
323 config,
324 header_dirty: Mutex::new(false),
325 wal,
326 };
327 if create {
328 pager.initialize()?;
329 } else {
330 pager.ensure_phase3_dwb_zone_layout()?;
331 pager.recover_from_dwb()?;
332 pager.load_header()?;
333 }
334 Ok(pager)
335 }
336
337 pub fn wal_barrier(&self) -> &W {
338 &self.wal
339 }
340
341 pub fn lock_ownership_mutations(&self) -> parking_lot::ReentrantMutexGuard<'_, ()> {
344 self.ownership_mutations.lock()
345 }
346
347 pub fn fence_interrupted_ownership(&self) {
349 self.writeback_failed
350 .store(true, std::sync::atomic::Ordering::Release);
351 }
352
353 #[doc(hidden)]
356 pub fn with_file<T>(&self, operation: impl FnOnce(&mut F) -> T) -> Result<T, PagerError> {
357 Ok(operation(&mut *self.file_lock()?))
358 }
359
360 fn initialize(&self) -> Result<(), PagerError> {
362 if self.config.read_only {
363 return Err(PagerError::ReadOnly);
364 }
365
366 let initial_page_count = FIRST_ALLOCATABLE_PAGE;
370 let header_page = Page::new_header_page(initial_page_count);
371 self.header_write()?.page_count = initial_page_count;
372
373 self.write_page_raw(0, &header_page)?;
378 let mut metadata_page = Page::new(PageType::Header, 1);
379 metadata_page.update_checksum();
380 self.write_page_raw(1, &metadata_page)?;
381 let mut vault_page = Page::new(PageType::Vault, 2);
382 vault_page.update_checksum();
383 self.write_page_raw(2, &vault_page)?;
384 self.clear_dwb_zone()?;
385
386 let mut slot = superblock_slot_image(header_page.as_bytes());
390 self.write_superblock_slot(&mut slot, 0, 1)?;
391 self.write_superblock_slot(&mut slot, 1, 2)?;
392
393 self.sync()?;
395
396 Ok(())
397 }
398
399 fn header_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, DatabaseHeader>, PagerError> {
401 self.header.write().map_err(|_| PagerError::LockPoisoned)
402 }
403
404 fn header_read(&self) -> Result<std::sync::RwLockReadGuard<'_, DatabaseHeader>, PagerError> {
406 self.header.read().map_err(|_| PagerError::LockPoisoned)
407 }
408
409 fn freelist_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, FreeList>, PagerError> {
411 self.freelist.write().map_err(|_| PagerError::LockPoisoned)
412 }
413
414 fn file_lock(&self) -> Result<std::sync::MutexGuard<'_, F>, PagerError> {
416 self.file.lock().map_err(|_| PagerError::LockPoisoned)
417 }
418
419 fn header_dirty_lock(&self) -> Result<std::sync::MutexGuard<'_, bool>, PagerError> {
421 self.header_dirty
422 .lock()
423 .map_err(|_| PagerError::LockPoisoned)
424 }
425
426 fn read_superblock_zone(&self) -> Result<[u8; SUPERBLOCK_ZONE_SIZE], PagerError> {
430 let mut file = self.file_lock()?;
431 Ok(pager_io::read_superblock_zone(&mut *file)?)
432 }
433
434 #[doc(hidden)]
440 pub fn newest_superblock(&self) -> Result<NewestSuperblock, PagerError> {
441 let zone = self.read_superblock_zone()?;
442 let selection = reddb_file::select_paged_superblock(&zone)
443 .ok_or_else(|| PagerError::SuperblockZoneUnrecoverable(self.path.clone()))?;
444
445 let start = selection.copy_index * SUPERBLOCK_SLOT_SIZE;
446 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
447 image.copy_from_slice(&zone[start..start + SUPERBLOCK_SLOT_SIZE]);
448 Ok((selection, image))
449 }
450
451 fn ensure_phase3_dwb_zone_layout(&self) -> Result<(), PagerError> {
452 let (_, image) = self.newest_superblock()?;
453 let header = reddb_file::decode_database_header(&image)
454 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
455 if header.version == reddb_file::file_format::PAGE_RECOVERY_READ_ONLY_FILE_VERSION
456 && !self.config.read_only
457 {
458 return Err(PagerError::InvalidDatabase(
459 "recovered database is read-only; export verified data into a new store".into(),
460 ));
461 }
462 if header.version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
463 && !self.config.disk_residency
464 && !self.config.read_only
465 {
466 return Err(PagerError::InvalidDatabase(
467 "prepared disk-resident database requires a disk-resident writer".into(),
468 ));
469 }
470 if header.version < PHASE3_DWB_ZONE_FILE_VERSION {
471 return Err(PagerError::LegacyPagedStore {
472 path: self.path.clone(),
473 });
474 }
475 Ok(())
476 }
477
478 fn write_superblock_slot(
481 &self,
482 slot: &mut [u8; SUPERBLOCK_SLOT_SIZE],
483 copy_index: usize,
484 generation: u64,
485 ) -> Result<(), PagerError> {
486 self.ensure_writeback_available()?;
487 if self.config.read_only {
488 return Err(PagerError::ReadOnly);
489 }
490 let mut file = self.file_lock()?;
491 self.ensure_writeback_available()?;
492 Ok(pager_io::write_superblock_slot(
493 &mut *file, slot, copy_index, generation,
494 )?)
495 }
496
497 #[doc(hidden)]
503 pub fn commit_superblock(
504 &self,
505 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
506 ) -> Result<(), PagerError> {
507 let _ownership = self.lock_ownership_mutations();
508 let _publication = self
509 .superblock_publication
510 .lock()
511 .map_err(|_| PagerError::LockPoisoned)?;
512 self.commit_superblock_locked(mutate)
513 }
514
515 fn commit_superblock_locked(
516 &self,
517 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
518 ) -> Result<(), PagerError> {
519 if self.config.read_only {
520 return Err(PagerError::ReadOnly);
521 }
522 let (newest, mut image) = self.newest_superblock()?;
523 mutate(&mut image)?;
524 let next_copy = reddb_file::paged_superblock_next_copy(Some(newest));
525 self.write_superblock_slot(&mut image, next_copy, newest.generation.saturating_add(1))
526 }
527
528 pub fn fence_file_format_version(&self, version: u32) -> Result<(), PagerError> {
534 let _ownership = self.lock_ownership_mutations();
535 self.ensure_writeback_available()?;
536 if self.config.read_only {
537 return Err(PagerError::ReadOnly);
538 }
539 if version == reddb_file::file_format::PAGE_RECOVERY_READ_ONLY_FILE_VERSION {
540 return Err(PagerError::InvalidDatabase(
541 "recovery read-only format is reserved".into(),
542 ));
543 }
544 if version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
545 && !self.config.disk_residency
546 {
547 return Err(PagerError::InvalidDatabase(
548 "disk-residency format requires a disk-resident writer".into(),
549 ));
550 }
551 if !(PHASE3_DWB_ZONE_FILE_VERSION..=reddb_file::PAGE_FILE_VERSION).contains(&version) {
552 return Err(PagerError::InvalidDatabase(
553 "unsupported file format fence".into(),
554 ));
555 }
556 let _publication = self
557 .superblock_publication
558 .lock()
559 .map_err(|_| PagerError::LockPoisoned)?;
560 if version < self.header_read()?.version {
561 return Err(PagerError::InvalidDatabase(
562 "file format downgrade is not supported".into(),
563 ));
564 }
565 let result = (|| {
566 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
567 self.commit_superblock_locked(|image| {
568 reddb_file::set_database_header_version(image, version)
569 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
570 })?;
571 }
572 self.header_write()?.version = version;
573 Ok(())
574 })();
575 if result.is_err() {
576 self.writeback_failed
577 .store(true, std::sync::atomic::Ordering::Release);
578 }
579 result
580 }
581
582 fn load_header(&self) -> Result<(), PagerError> {
584 let (_, image) = self.newest_superblock()?;
585
586 let mut decoded_header = reddb_file::decode_database_header(&image)
587 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
588 let physical_pages = self.file_lock()?.file_len()?.div_ceil(PAGE_SIZE as u64);
594 let mut physical_pages = u32::try_from(physical_pages).map_err(|_| {
595 PagerError::InvalidDatabase("data file exceeds the page address space".into())
596 })?;
597 if self.config.read_only {
598 if let Some(id) = self
599 .read_only_recovery
600 .read()
601 .map_err(|_| PagerError::LockPoisoned)?
602 .keys()
603 .max()
604 {
605 physical_pages = physical_pages.max(id.checked_add(1).ok_or_else(|| {
606 PagerError::InvalidDatabase(
607 "recovery page exceeds the page address space".into(),
608 )
609 })?);
610 }
611 }
612 if decoded_header.page_count < physical_pages {
613 decoded_header.page_count = physical_pages;
614 if !self.config.read_only {
615 *self.header_dirty_lock()? = true;
616 }
617 }
618 let freelist_head = decoded_header.freelist_head;
619
620 {
621 let mut header = self.header_write()?;
622 *header = decoded_header;
623 }
624
625 {
627 let mut freelist = self.freelist_write()?;
628 *freelist = FreeList::from_header(freelist_head, 0);
629 }
630
631 Ok(())
632 }
633
634 fn write_header(&self) -> Result<(), PagerError> {
636 if self.config.read_only {
637 return Err(PagerError::ReadOnly);
638 }
639
640 let header = self.header_read()?.clone();
641 self.commit_superblock(|image| {
642 reddb_file::encode_database_header(image, &header)
643 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
644 })?;
645 *self.header_dirty_lock()? = false;
646
647 Ok(())
648 }
649
650 fn read_page_raw(&self, page_id: u32) -> Result<Page, PagerError> {
652 let mut file = self.file_lock()?;
653 Ok(pager_io::read_page(
654 &mut *file,
655 page_id,
656 self.config.verify_checksums,
657 )?)
658 }
659
660 fn ensure_writeback_available(&self) -> Result<(), PagerError> {
661 if self
662 .writeback_failed
663 .load(std::sync::atomic::Ordering::Acquire)
664 {
665 return Err(PagerError::Io(std::io::Error::other(
666 "page writeback failed; reopen the database to recover before writing",
667 )));
668 }
669 Ok(())
670 }
671
672 fn record_ownership(
673 &self,
674 operation: impl FnOnce(&W) -> std::io::Result<()>,
675 ) -> Result<(), PagerError> {
676 if let Err(error) = operation(&self.wal) {
677 self.writeback_failed
678 .store(true, std::sync::atomic::Ordering::Release);
679 return Err(PagerError::Io(error));
680 }
681 Ok(())
682 }
683
684 fn write_evicted_dirty_page(
687 &self,
688 page: Page,
689 pending_page: Option<u32>,
690 ) -> Result<(), PagerError> {
691 let mut ids = self.cache.dirty_page_ids(pager_io::DWB_MAX_PAGES_PER_FRAME);
692 ids.retain(|id| Some(*id) != pending_page);
696 ids.truncate(pager_io::DWB_MAX_PAGES_PER_FRAME.saturating_sub(1));
699 ids.sort_unstable();
700 let mut pages = self.cache.flush_dirty_pages(&ids);
701 pages.push((page.page_id(), page));
702 pages.sort_unstable_by_key(|(id, _)| *id);
703 self.write_dirty_page_batch(&pages)
704 }
705
706 fn write_page_raw(&self, page_id: u32, page: &Page) -> Result<(), PagerError> {
708 self.ensure_writeback_available()?;
709 if self.config.read_only {
710 return Err(PagerError::ReadOnly);
711 }
712 let mut file = self.file_lock()?;
713 self.ensure_writeback_available()?;
714 Ok(pager_io::write_page(&mut *file, page_id, page)?)
715 }
716
717 pub fn read_page(&self, page_id: u32) -> Result<Page, PagerError> {
719 if self.config.read_only {
720 if let Some(page) = self
721 .read_only_recovery
722 .read()
723 .map_err(|_| PagerError::LockPoisoned)?
724 .get(&page_id)
725 {
726 if self.config.verify_checksums {
727 page.verify_checksum()?;
728 }
729 return Ok(page.clone());
730 }
731 }
732 if let Some(page) = self.cache.get(page_id) {
734 return Ok(page);
735 }
736
737 let _ownership = self.lock_ownership_mutations();
741 if let Some(page) = self.cache.get(page_id) {
742 return Ok(page);
743 }
744
745 let page = self.read_page_raw(page_id)?;
747
748 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
750 self.write_evicted_dirty_page(dirty_page, None)?;
752 }
753
754 Ok(page)
755 }
756
757 pub fn read_page_no_checksum(&self, page_id: u32) -> Result<Page, PagerError> {
762 if self.config.read_only {
763 if let Some(page) = self
764 .read_only_recovery
765 .read()
766 .map_err(|_| PagerError::LockPoisoned)?
767 .get(&page_id)
768 {
769 return Ok(page.clone());
770 }
771 }
772 if let Some(page) = self.cache.get(page_id) {
774 return Ok(page);
775 }
776
777 let _ownership = self.lock_ownership_mutations();
778 if let Some(page) = self.cache.get(page_id) {
779 return Ok(page);
780 }
781
782 let mut file = self.file_lock()?;
784 let page = pager_io::read_page(&mut *file, page_id, false)?;
785 drop(file);
786
787 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
789 self.write_evicted_dirty_page(dirty_page, None)?;
791 }
792
793 Ok(page)
794 }
795
796 pub fn write_page(&self, page_id: u32, mut page: Page) -> Result<(), PagerError> {
798 let _ownership = self.lock_ownership_mutations();
799 self.ensure_writeback_available()?;
800 if self.config.read_only {
801 return Err(PagerError::ReadOnly);
802 }
803
804 self.capture_page_before_write(page_id)?;
805
806 page.update_checksum();
808
809 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
814 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
815 }
816
817 Ok(())
818 }
819
820 pub fn write_page_no_checksum(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
825 let _ownership = self.lock_ownership_mutations();
826 self.ensure_writeback_available()?;
827 if self.config.read_only {
828 return Err(PagerError::ReadOnly);
829 }
830
831 self.capture_page_before_write(page_id)?;
832
833 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
836 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
837 }
838
839 Ok(())
840 }
841
842 fn capture_page_before_write(&self, page_id: u32) -> Result<(), PagerError> {
843 let mut required = false;
844 self.record_ownership(|wal| {
845 required = wal.needs_page_image(page_id)?;
846 Ok(())
847 })?;
848 if required {
849 let previous = self.read_page(page_id)?;
850 self.record_ownership(|wal| wal.capture_page_image(&previous))?;
851 }
852 Ok(())
853 }
854
855 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, PagerError> {
857 let _ownership = self.lock_ownership_mutations();
858 self.ensure_writeback_available()?;
859 if self.config.read_only {
860 return Err(PagerError::ReadOnly);
861 }
862
863 let page_id = {
865 let mut freelist = self.freelist_write()?;
866 self.complete_allocation_fence()?;
869 if let Some(id) = freelist.next_in_memory_page() {
870 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
871 let id = freelist
872 .allocate()
873 .expect("free page checked while allocator is locked");
874 self.allocation_reused
875 .store(true, std::sync::atomic::Ordering::Release);
876 id
877 } else if freelist.trunk_head() != 0 {
878 let trunk_id = freelist.trunk_head();
879
880 let trunk = self.read_page(trunk_id).map_err(|e| match e {
881 PagerError::PageNotFound(_) => {
882 PagerError::InvalidDatabase("Freelist trunk missing".to_string())
883 }
884 other => other,
885 })?;
886 if trunk.page_id() != trunk_id {
887 return Err(PagerError::InvalidDatabase(
888 "misdirected freelist trunk".into(),
889 ));
890 }
891
892 let mut detached = FreeList::new();
895 detached
896 .load_from_trunk(&trunk)
897 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {e}")))?;
898 let mut pages = Vec::new();
899 let mut seen = std::collections::HashSet::new();
900 let page_count = self.page_count()?;
901 while let Some(id) = detached.allocate() {
902 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count || !seen.insert(id) {
903 return Err(PagerError::InvalidDatabase(
904 "invalid freelist trunk page".into(),
905 ));
906 }
907 pages.push(id);
908 }
909 let mut journaled = false;
910 self.record_ownership(|wal| {
911 journaled = wal.has_page_ownership_journal()?;
912 Ok(())
913 })?;
914 if !journaled {
915 self.allocate_unjournaled_trunk(&mut freelist, trunk, &pages)?
916 } else {
917 self.record_ownership(|wal| wal.detach_free_pages(&pages))?;
918
919 freelist
920 .load_from_trunk(&trunk)
921 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {}", e)))?;
922 let mut header = self.header_write()?;
923 header.freelist_head = freelist.trunk_head();
924 *self.header_dirty_lock()? = true;
925 drop(header);
926 self.allocation_fence_pending
931 .store(true, std::sync::atomic::Ordering::Release);
932 self.complete_allocation_fence()?;
933 let next = freelist.next_in_memory_page().ok_or_else(|| {
934 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
935 })?;
936 self.record_ownership(|wal| wal.reserve_pages(next, 1))?;
937 let id = freelist.allocate().ok_or_else(|| {
938 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
939 })?;
940 self.allocation_reused
941 .store(true, std::sync::atomic::Ordering::Release);
942 id
943 }
944 } else {
945 let mut header = self.header_write()?;
947 let id = header.page_count;
948 let next = id
949 .checked_add(1)
950 .ok_or_else(|| PagerError::InvalidDatabase("page count overflow".into()))?;
951 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
952 header.page_count = next;
953 *self.header_dirty_lock()? = true;
954 id
955 }
956 };
957
958 let page = Page::new(page_type, page_id);
959
960 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page.clone()) {
972 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
973 }
974
975 Ok(page)
976 }
977
978 fn allocate_unjournaled_trunk(
982 &self,
983 freelist: &mut FreeList,
984 mut trunk: Page,
985 pages: &[u32],
986 ) -> Result<u32, PagerError> {
987 let id = *pages
988 .first()
989 .expect("validated trunk includes its own page");
990 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
991 if pages.len() > 1 {
992 let remaining =
993 u32::try_from(pages.len() - 2).expect("a validated trunk fits in one page");
994 trunk.content_mut()[4..8].copy_from_slice(&remaining.to_le_bytes());
995 trunk.update_checksum();
996 self.cache.remove(trunk.page_id());
997 self.write_pages_through_dwb(&[(trunk.page_id(), trunk)])?;
998 freelist.note_durable_trunk_allocation(remaining);
999 } else {
1000 freelist
1003 .load_from_trunk(&trunk)
1004 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
1005 self.header_write()?.freelist_head = freelist.trunk_head();
1006 *self.header_dirty_lock()? = true;
1007 self.allocation_fence_pending
1008 .store(true, std::sync::atomic::Ordering::Release);
1009 self.complete_allocation_fence()?;
1010 assert_eq!(freelist.allocate(), Some(id));
1011 }
1012 self.allocation_reused
1013 .store(true, std::sync::atomic::Ordering::Release);
1014 Ok(id)
1015 }
1016
1017 pub fn reserve_contig_extent(&self, n_pages: u32) -> Result<ExtentId, PagerError> {
1019 let _ownership = self.lock_ownership_mutations();
1020 self.ensure_writeback_available()?;
1021 if self.config.read_only {
1022 return Err(PagerError::ReadOnly);
1023 }
1024 if n_pages == 0 {
1025 return Err(PagerError::InvalidDatabase(
1026 "contiguous extent must reserve at least one page".to_string(),
1027 ));
1028 }
1029
1030 let start_page = {
1031 let mut header = self.header_write()?;
1032 let start = header.page_count;
1033 let end = header.page_count.checked_add(n_pages).ok_or_else(|| {
1034 PagerError::InvalidDatabase("contiguous extent page count overflow".to_string())
1035 })?;
1036 self.record_ownership(|wal| wal.reserve_pages(start, n_pages))?;
1037 header.page_count = end;
1038 *self.header_dirty_lock()? = true;
1039 start
1040 };
1041
1042 for page_id in start_page..start_page + n_pages {
1043 let mut page = Page::new(PageType::Vector, page_id);
1044 page.update_checksum();
1045 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
1046 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
1047 }
1048 }
1049
1050 Ok(ExtentId {
1051 start_page,
1052 n_pages,
1053 })
1054 }
1055
1056 pub fn free_page(&self, page_id: u32) -> Result<(), PagerError> {
1058 let _ownership = self.lock_ownership_mutations();
1059 self.ensure_writeback_available()?;
1060 if self.config.read_only {
1061 return Err(PagerError::ReadOnly);
1062 }
1063
1064 let mut deferred = self
1065 .deferred_reclamation
1066 .lock()
1067 .map_err(|_| PagerError::LockPoisoned)?;
1068 if deferred.enabled {
1069 if page_id < FIRST_ALLOCATABLE_PAGE || page_id >= self.page_count()? {
1070 return Err(PagerError::InvalidDatabase(
1071 "invalid deferred free page".into(),
1072 ));
1073 }
1074 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1075 deferred.pending.insert(page_id);
1078 return Ok(());
1079 }
1080 drop(deferred);
1081
1082 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1083
1084 self.cache.remove(page_id);
1086
1087 let mut freelist = self.freelist_write()?;
1089 freelist.free(page_id);
1090
1091 *self.header_dirty_lock()? = true;
1092
1093 Ok(())
1094 }
1095
1096 pub fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
1097 let _ownership = self.lock_ownership_mutations();
1098 self.deferred_reclamation
1099 .lock()
1100 .map_err(|_| PagerError::LockPoisoned)?
1101 .enabled = true;
1102 Ok(())
1103 }
1104
1105 pub fn deferred_reclamation_pages(&self) -> Result<(Vec<u32>, Vec<u32>), PagerError> {
1106 let deferred = self
1107 .deferred_reclamation
1108 .lock()
1109 .map_err(|_| PagerError::LockPoisoned)?;
1110 Ok((
1111 deferred.pending.iter().copied().collect(),
1112 deferred.published.iter().copied().collect(),
1113 ))
1114 }
1115
1116 pub fn restore_deferred_reclamation(&self, pages: &[u32]) -> Result<(), PagerError> {
1117 let _ownership = self.lock_ownership_mutations();
1118 let count = self.page_count()?;
1119 if pages
1120 .iter()
1121 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= count)
1122 {
1123 return Err(PagerError::InvalidDatabase(
1124 "invalid persisted deferred free page".into(),
1125 ));
1126 }
1127 let mut deferred = self
1128 .deferred_reclamation
1129 .lock()
1130 .map_err(|_| PagerError::LockPoisoned)?;
1131 deferred.pending.extend(pages);
1132 deferred.published.extend(pages);
1133 Ok(())
1134 }
1135
1136 pub fn complete_deferred_publication(
1139 &self,
1140 retained: &[u32],
1141 released: &[u32],
1142 ) -> Result<(), PagerError> {
1143 let _ownership = self.lock_ownership_mutations();
1144 let mut deferred = self
1145 .deferred_reclamation
1146 .lock()
1147 .map_err(|_| PagerError::LockPoisoned)?;
1148 if !released.is_empty() {
1149 self.free_retired_pages(released)?;
1150 }
1151 for id in released {
1152 deferred.pending.remove(id);
1153 }
1154 deferred.published = retained.iter().copied().collect();
1155 Ok(())
1156 }
1157
1158 pub fn record_retirement_intent(&self, pages: &[u32]) -> Result<(), PagerError> {
1160 let _ownership = self.lock_ownership_mutations();
1161 self.ensure_writeback_available()?;
1162 if self.config.read_only {
1163 return Err(PagerError::ReadOnly);
1164 }
1165 let page_count = self.page_count()?;
1166 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1167 if pages
1168 .iter()
1169 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !seen.insert(*id))
1170 {
1171 return Err(PagerError::InvalidDatabase(
1172 "invalid retirement intent".into(),
1173 ));
1174 }
1175 self.record_ownership(|wal| wal.retire_pages(pages))
1176 }
1177
1178 pub fn free_retired_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1181 let _ownership = self.lock_ownership_mutations();
1182 self.ensure_writeback_available()?;
1183 if self.config.read_only {
1184 return Err(PagerError::ReadOnly);
1185 }
1186 let page_count = self.page_count()?;
1187 if pages
1188 .iter()
1189 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count)
1190 {
1191 return Err(PagerError::InvalidDatabase(
1192 "invalid retired page range".into(),
1193 ));
1194 }
1195 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1196 if pages.iter().any(|id| !seen.insert(*id)) {
1197 return Err(PagerError::InvalidDatabase("duplicate retired page".into()));
1198 }
1199 let mut freelist = self.freelist_write()?;
1200 if freelist.contains_any(&seen) {
1201 return Err(PagerError::InvalidDatabase(
1202 "retired page is already free".into(),
1203 ));
1204 }
1205 let mut trunk_id = freelist.trunk_head();
1209 let mut visited = std::collections::HashSet::new();
1210 while trunk_id != 0 && !seen.is_empty() {
1211 if trunk_id < FIRST_ALLOCATABLE_PAGE
1212 || trunk_id >= page_count
1213 || !visited.insert(trunk_id)
1214 {
1215 return Err(PagerError::InvalidDatabase(
1216 "invalid freelist trunk chain".into(),
1217 ));
1218 }
1219 let trunk = self.read_page(trunk_id)?;
1220 if trunk.page_id() != trunk_id {
1221 return Err(PagerError::InvalidDatabase(
1222 "misdirected freelist trunk".into(),
1223 ));
1224 }
1225 let mut loaded = FreeList::new();
1226 let next = loaded
1227 .load_from_trunk(&trunk)
1228 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1229 while let Some(id) = loaded.allocate() {
1230 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1231 return Err(PagerError::InvalidDatabase(
1232 "invalid free page range".into(),
1233 ));
1234 }
1235 if seen.contains(&id) {
1236 return Err(PagerError::InvalidDatabase(
1237 "retired page is already free".into(),
1238 ));
1239 }
1240 }
1241 trunk_id = next.unwrap_or(0);
1242 }
1243 self.record_ownership(|wal| wal.retire_pages(pages))?;
1244 for id in pages {
1245 self.cache.remove(*id);
1246 }
1247 freelist.free_batch(pages);
1248 *self.header_dirty_lock()? = true;
1249 Ok(())
1250 }
1251
1252 fn pages_not_in_freelist(
1253 &self,
1254 pages: &[u32],
1255 freelist: &FreeList,
1256 page_count: u32,
1257 ) -> Result<std::collections::HashSet<u32>, PagerError> {
1258 let mut remaining = std::collections::HashSet::with_capacity(pages.len());
1259 if pages
1260 .iter()
1261 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !remaining.insert(*id))
1262 {
1263 return Err(PagerError::InvalidDatabase(
1264 "invalid recovery page set".into(),
1265 ));
1266 }
1267 let requested = remaining.clone();
1268 freelist
1269 .remove_existing_from(&requested, &mut remaining)
1270 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1271 let mut trunk_id = freelist.trunk_head();
1272 let mut visited = std::collections::HashSet::new();
1273 while trunk_id != 0 {
1274 if trunk_id < FIRST_ALLOCATABLE_PAGE
1275 || trunk_id >= page_count
1276 || !visited.insert(trunk_id)
1277 {
1278 return Err(PagerError::InvalidDatabase(
1279 "invalid freelist trunk chain".into(),
1280 ));
1281 }
1282 let trunk = self.read_page(trunk_id)?;
1283 if trunk.page_id() != trunk_id {
1284 return Err(PagerError::InvalidDatabase(
1285 "misdirected freelist trunk".into(),
1286 ));
1287 }
1288 let mut loaded = FreeList::new();
1289 let next = loaded
1290 .load_from_trunk(&trunk)
1291 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1292 while let Some(id) = loaded.allocate() {
1293 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1294 return Err(PagerError::InvalidDatabase(
1295 "invalid free page range".into(),
1296 ));
1297 }
1298 if requested.contains(&id) && !remaining.remove(&id) {
1299 return Err(PagerError::InvalidDatabase(
1300 "duplicate recovery page".into(),
1301 ));
1302 }
1303 }
1304 trunk_id = next.unwrap_or(0);
1305 }
1306 Ok(remaining)
1307 }
1308
1309 pub fn recover_unowned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1313 let _ownership = self.lock_ownership_mutations();
1314 self.ensure_writeback_available()?;
1315 if self.config.read_only {
1316 return Err(PagerError::ReadOnly);
1317 }
1318 let page_count = self.page_count()?;
1319 let mut freelist = self.freelist_write()?;
1320 let remaining = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1321 if !remaining.is_empty() {
1322 let mut missing: Vec<_> = remaining.into_iter().collect();
1323 missing.sort_unstable();
1324 for id in &missing {
1325 self.cache.remove(*id);
1326 }
1327 freelist.free_batch(&missing);
1328 *self.header_dirty_lock()? = true;
1329 }
1330 Ok(())
1331 }
1332
1333 pub fn validate_owned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1336 let page_count = self.page_count()?;
1337 let freelist = self.freelist_write()?;
1338 let absent = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1339 if absent.len() != pages.len() {
1340 return Err(PagerError::InvalidDatabase(
1341 "owned recovery page is free".into(),
1342 ));
1343 }
1344 Ok(())
1345 }
1346
1347 pub fn sync_recovered_allocator(&self) -> Result<(), PagerError> {
1350 let _ownership = self.lock_ownership_mutations();
1351 let result = (|| {
1352 self.sync()?;
1353 let _publication = self
1354 .superblock_publication
1355 .lock()
1356 .map_err(|_| PagerError::LockPoisoned)?;
1357 let header = self.header_read()?.clone();
1358 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1359 self.commit_superblock_locked(|image| {
1360 reddb_file::encode_database_header(image, &header)
1361 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
1362 })?;
1363 }
1364 Ok(())
1365 })();
1366 if result.is_err() {
1367 self.writeback_failed
1368 .store(true, std::sync::atomic::Ordering::Release);
1369 }
1370 result
1371 }
1372
1373 pub fn free_page_count(&self) -> Result<u32, PagerError> {
1376 Ok(self.freelist_write()?.total_free())
1377 }
1378
1379 pub fn header(&self) -> Result<DatabaseHeader, PagerError> {
1380 Ok(self.header_read()?.clone())
1381 }
1382
1383 pub fn physical_header(&self) -> Result<PhysicalFileHeader, PagerError> {
1384 Ok(self.header_read()?.physical)
1385 }
1386
1387 pub fn update_physical_header(&self, physical: PhysicalFileHeader) -> Result<(), PagerError> {
1388 let _ownership = self.lock_ownership_mutations();
1389 self.ensure_writeback_available()?;
1390 if self.config.read_only {
1391 return Err(PagerError::ReadOnly);
1392 }
1393
1394 let mut header = self.header_write()?;
1395 header.physical = physical;
1396 *self.header_dirty_lock()? = true;
1397 Ok(())
1398 }
1399
1400 pub fn page_count(&self) -> Result<u32, PagerError> {
1402 Ok(self.header_read()?.page_count)
1403 }
1404
1405 pub fn flush(&self) -> Result<(), PagerError> {
1407 let _ownership = self.lock_ownership_mutations();
1408 self.ensure_writeback_available()?;
1409 if self.config.read_only {
1410 return Ok(());
1411 }
1412
1413 let trunks = {
1415 let mut freelist = self.freelist_write()?;
1416 if freelist.is_dirty() {
1417 let mut header = self.header_write()?;
1418 let trunks = freelist.flush_to_trunks(0, || {
1419 let id = header.page_count;
1420 header.page_count += 1;
1421 id
1422 });
1423 header.freelist_head = freelist.trunk_head();
1424 *self.header_dirty_lock()? = true;
1425 freelist.mark_clean();
1426 trunks
1427 } else {
1428 Vec::new()
1429 }
1430 };
1431
1432 let mut dirty_pages = Vec::new();
1433 for trunk in trunks {
1434 let page_id = trunk.page_id();
1435 if let Some(evicted) = self.cache.insert_dirty(page_id, trunk) {
1436 dirty_pages.push((evicted.page_id(), evicted));
1439 }
1440 }
1441
1442 const FLUSH_BATCH_PAGES: usize = 256;
1446 for batch in dirty_pages.chunks(FLUSH_BATCH_PAGES) {
1447 self.write_dirty_page_batch(batch)?;
1448 }
1449 drop(dirty_pages);
1450 let mut page_ids = self.cache.dirty_page_ids(usize::MAX);
1454 page_ids.sort_unstable();
1455 for ids in page_ids.chunks(FLUSH_BATCH_PAGES) {
1456 let pages = self.cache.flush_dirty_pages(ids);
1457 self.write_dirty_page_batch(&pages)?;
1458 }
1459
1460 if *self.header_dirty_lock()? {
1462 self.write_header()?;
1463 }
1464
1465 Ok(())
1466 }
1467
1468 fn write_dirty_page_batch(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1469 if pages.is_empty() {
1470 return Ok(());
1471 }
1472 let result = (|| {
1476 let max_lsn = pages
1477 .iter()
1478 .filter_map(|(_, page)| page.header().ok().map(|header| header.lsn))
1479 .max()
1480 .unwrap_or(0);
1481 if max_lsn > 0 {
1482 self.wal.flush_until(max_lsn)?;
1483 }
1484 self.write_pages_through_dwb(pages)
1485 })();
1486 if result.is_err() {
1487 self.writeback_failed
1488 .store(true, std::sync::atomic::Ordering::Release);
1489 for (page_id, _) in pages {
1490 self.cache.mark_dirty(*page_id);
1491 }
1492 }
1493 result
1494 }
1495
1496 pub fn sync(&self) -> Result<(), PagerError> {
1498 let _ownership = self.lock_ownership_mutations();
1499 self.flush()?;
1500
1501 if self
1502 .allocation_reused
1503 .swap(false, std::sync::atomic::Ordering::AcqRel)
1504 {
1505 if let Err(error) = self.write_header_and_sync() {
1509 self.allocation_reused
1510 .store(true, std::sync::atomic::Ordering::Release);
1511 return Err(error);
1512 }
1513 }
1514
1515 let file = self.file_lock()?;
1516 file.sync_all()?;
1517
1518 Ok(())
1519 }
1520
1521 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
1523 self.cache.stats()
1524 }
1525
1526 pub fn cache_len(&self) -> usize {
1529 self.cache.len()
1530 + if self.config.read_only {
1531 self.read_only_recovery
1532 .read()
1533 .unwrap_or_else(|poisoned| poisoned.into_inner())
1534 .len()
1535 } else {
1536 0
1537 }
1538 }
1539
1540 pub fn cache_capacity(&self) -> usize {
1543 self.cache.capacity()
1544 }
1545
1546 pub fn dirty_page_count(&self) -> usize {
1548 self.cache.dirty_count()
1549 }
1550
1551 pub fn dirty_fraction(&self) -> f64 {
1555 let capacity = self.cache.capacity().max(1) as f64;
1556 self.cache.dirty_count() as f64 / capacity
1557 }
1558
1559 pub fn flush_some_dirty(&self, max: usize) -> Result<usize, PagerError> {
1563 let _ownership = self.lock_ownership_mutations();
1564 self.ensure_writeback_available()?;
1565 if self.config.read_only || max == 0 {
1566 return Ok(0);
1567 }
1568 let dirty_pages = self.cache.flush_some_dirty(max);
1569 if dirty_pages.is_empty() {
1570 return Ok(0);
1571 }
1572 let count = dirty_pages.len();
1573 for (page_id, page) in dirty_pages {
1580 self.write_page(page_id, page)?;
1581 }
1582 Ok(count)
1583 }
1584
1585 pub fn path(&self) -> &Path {
1587 &self.path
1588 }
1589
1590 pub fn is_read_only(&self) -> bool {
1592 self.config.read_only
1593 }
1594
1595 pub fn file_size(&self) -> Result<u64, PagerError> {
1597 let file = self.file_lock()?;
1598 Ok(file.file_len()?)
1599 }
1600
1601 fn clear_dwb_zone(&self) -> Result<(), PagerError> {
1603 let mut file = self.file_lock()?;
1604 Ok(pager_io::clear_dwb_zone(&mut *file)?)
1605 }
1606
1607 pub fn read_manifest_page(&self) -> Result<Page, PagerError> {
1612 self.read_page(1)
1613 .map_err(|_| PagerError::ManifestZoneCorrupt(self.path.clone()))
1614 }
1615
1616 fn write_pages_through_dwb(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1623 if self.config.read_only {
1624 return Err(PagerError::ReadOnly);
1625 }
1626 let mut file = self.file_lock()?;
1629 self.ensure_writeback_available()?;
1630 let result = pager_io::write_pages(&mut *file, pages, self.dwb_enabled);
1631 if result.is_err() {
1632 self.writeback_failed
1635 .store(true, std::sync::atomic::Ordering::Release);
1636 }
1637 Ok(result?)
1638 }
1639
1640 fn recover_from_dwb(&self) -> Result<(), PagerError> {
1645 let mut file = self.file_lock()?;
1646 if self.config.read_only {
1647 for (id, page) in pager_io::read_dwb_pages(&mut *file)? {
1648 self.install_read_only_page(id, page)?;
1652 }
1653 return Ok(());
1654 }
1655 Ok(pager_io::recover_from_dwb(&mut *file)?)
1656 }
1657
1658 pub fn restore_read_only_page(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
1662 if !self.config.read_only || page_id == 0 || page.page_id() != page_id {
1663 return Err(PagerError::InvalidDatabase(
1664 "invalid read-only recovery image".into(),
1665 ));
1666 }
1667 page.verify_checksum()?;
1668 self.install_read_only_page(page_id, page)
1669 }
1670
1671 fn install_read_only_page(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
1672 let mut images = self
1673 .read_only_recovery
1674 .write()
1675 .map_err(|_| PagerError::LockPoisoned)?;
1676 let limit = self
1677 .config
1678 .cache_size
1679 .max(pager_io::DWB_MAX_PAGES_PER_FRAME);
1680 if images.len() >= limit && !images.contains_key(&page_id) {
1681 return Err(PagerError::InvalidDatabase(
1682 "read-only recovery exceeds the page cache budget".into(),
1683 ));
1684 }
1685 images.insert(page_id, page);
1686 Ok(())
1687 }
1688
1689 fn complete_allocation_fence(&self) -> Result<(), PagerError> {
1693 if self
1694 .allocation_fence_pending
1695 .load(std::sync::atomic::Ordering::Acquire)
1696 {
1697 self.write_header_and_sync()?;
1698 self.write_header_and_sync()?;
1699 self.allocation_fence_pending
1700 .store(false, std::sync::atomic::Ordering::Release);
1701 }
1702 Ok(())
1703 }
1704
1705 pub fn write_header_and_sync(&self) -> Result<(), PagerError> {
1707 let _ownership = self.lock_ownership_mutations();
1708 self.write_header()?;
1709 let file = self.file_lock()?;
1710 file.sync_all()?;
1711 Ok(())
1712 }
1713
1714 pub fn set_checkpoint_in_progress(
1716 &self,
1717 in_progress: bool,
1718 target_lsn: u64,
1719 ) -> Result<(), PagerError> {
1720 let _ownership = self.lock_ownership_mutations();
1721 let mut header = self.header_write()?;
1722 header.checkpoint_in_progress = in_progress;
1723 header.checkpoint_target_lsn = target_lsn;
1724 *self.header_dirty_lock()? = true;
1725 drop(header);
1726 self.write_header_and_sync()
1727 }
1728
1729 pub fn complete_checkpoint(&self, lsn: u64) -> Result<(), PagerError> {
1731 let _ownership = self.lock_ownership_mutations();
1732 let mut header = self.header_write()?;
1733 header.checkpoint_lsn = lsn;
1734 header.checkpoint_in_progress = false;
1735 header.checkpoint_target_lsn = 0;
1736 *self.header_dirty_lock()? = true;
1737 drop(header);
1738 self.write_header_and_sync()
1739 }
1740}
1741
1742#[cfg(test)]
1743mod integrity_guidance_tests {
1744 use super::PagerError;
1745 use std::path::PathBuf;
1746
1747 #[test]
1748 fn damaged_paged_zones_recommend_preserving_files_and_a_verified_backup() {
1749 for error in [
1750 PagerError::SuperblockZoneUnrecoverable(PathBuf::from("data.rdb")),
1751 PagerError::ManifestZoneCorrupt(PathBuf::from("data.rdb")),
1752 PagerError::DwbZoneCorrupt(PathBuf::from("data.rdb")),
1753 ] {
1754 let message = error.to_string();
1755 assert!(message.contains("Preserve the database"), "{message}");
1756 assert!(message.contains("verified backup"), "{message}");
1757 assert!(!message.contains("salvage"), "{message}");
1758 assert!(!message.contains("SCRUB"), "{message}");
1759 }
1760 }
1761}