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
17mod batch;
18
19#[derive(Debug)]
21pub enum PagerError {
22 Io(std::io::Error),
24 Page(PageError),
26 InvalidDatabase(String),
28 ReadOnly,
30 PageNotFound(u32),
32 Locked,
34 LockPoisoned,
36 EncryptionRequired,
38 PlainDatabaseRefusesKey,
40 InvalidKey,
42 SuperblockZoneUnrecoverable(PathBuf),
44 ManifestZoneCorrupt(PathBuf),
46 DwbZoneCorrupt(PathBuf),
48 LegacySidecarStore { sidecar: PathBuf },
50 LegacyPagedStore { path: PathBuf },
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct ExtentId {
57 pub start_page: u32,
58 pub n_pages: u32,
59}
60
61impl std::fmt::Display for PagerError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 Self::Io(e) => write!(f, "I/O error: {}", e),
65 Self::Page(e) => write!(f, "Page error: {}", e),
66 Self::InvalidDatabase(msg) => write!(f, "Invalid database: {}", msg),
67 Self::ReadOnly => write!(f, "Database is read-only"),
68 Self::PageNotFound(id) => write!(f, "Page {} not found", id),
69 Self::Locked => write!(f, "Database is locked"),
70 Self::LockPoisoned => write!(f, "Internal lock poisoned (concurrent thread panicked)"),
71 Self::EncryptionRequired => write!(
72 f,
73 "Database is encrypted but no key was supplied (set PagerConfig::encryption)"
74 ),
75 Self::PlainDatabaseRefusesKey => write!(
76 f,
77 "Plain (unencrypted) database opened with an encryption key — refusing"
78 ),
79 Self::InvalidKey => write!(f, "Encryption key validation failed for this database"),
80 Self::SuperblockZoneUnrecoverable(path) => write!(
81 f,
82 "superblock zone of {} is unrecoverable: both ping-pong copies failed \
83 validation, so no generation can be trusted to root the store. The store \
84 will not be opened. Preserve the database and its recovery files; \
85 restore from a verified backup.",
86 path.display()
87 ),
88 Self::ManifestZoneCorrupt(path) => write!(
89 f,
90 "internal manifest zone of {} failed its checksum: the zone that names \
91 collections, indexes and the checkpoint boundary cannot be trusted, so no \
92 rows are returned. Preserve the database and its recovery files; \
93 restore from a verified backup.",
94 path.display()
95 ),
96 Self::DwbZoneCorrupt(path) => write!(
97 f,
98 "double-write zone of {} failed validation: page recovery cannot prove \
99 whether the protected in-place write completed. The store will not be opened \
100 silently. Preserve the database and its recovery files; restore from \
101 a verified backup.",
102 path.display()
103 ),
104 Self::LegacySidecarStore { sidecar } => write!(
105 f,
106 "refusing to open a legacy sidecar-backed store: found {}. The superblock \
107 pair, internal manifest, and double-write recovery state now live inside the \
108 .rdb file (ADR 0038 §2/§4), \
109 and the engine never reads the retired sidecars silently. Convert the store \
110 first with the offline migration tool \
111 (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is reversible.",
112 sidecar.display()
113 ),
114 Self::LegacyPagedStore { path } => write!(
115 f,
116 "refusing to open a pre-phase-3 paged store: {} does not carry the layout marker \
117 for the in-file double-write zone. Convert the store first with the offline \
118 migration tool (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is \
119 reversible.",
120 path.display()
121 ),
122 }
123 }
124}
125
126impl std::error::Error for PagerError {
127 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
128 match self {
129 Self::Io(e) => Some(e),
130 Self::Page(e) => Some(e),
131 _ => None,
132 }
133 }
134}
135
136impl From<std::io::Error> for PagerError {
137 fn from(e: std::io::Error) -> Self {
138 Self::Io(e)
139 }
140}
141
142impl From<PageError> for PagerError {
143 fn from(e: PageError) -> Self {
144 Self::Page(e)
145 }
146}
147
148impl From<pager_io::PagerIoError> for PagerError {
149 fn from(error: pager_io::PagerIoError) -> Self {
150 match error {
151 pager_io::PagerIoError::Io(error) => Self::Io(error),
152 pager_io::PagerIoError::Page(error) => Self::Page(error),
153 pager_io::PagerIoError::InvalidDatabase(message) => Self::InvalidDatabase(message),
154 }
155 }
156}
157
158#[derive(Debug, Clone)]
160pub struct PagerConfig {
161 pub disk_residency: bool,
163 pub cache_size: usize,
164 pub read_only: bool,
165 pub verify_checksums: bool,
166 pub double_write: bool,
167}
168
169impl Default for PagerConfig {
170 fn default() -> Self {
171 Self {
172 disk_residency: false,
173 cache_size: 10_000,
174 read_only: false,
175 verify_checksums: true,
176 double_write: true,
177 }
178 }
179}
180
181pub trait WalBarrier {
184 fn flush_until(&self, lsn: u64) -> std::io::Result<()>;
185
186 fn validate_page_lsn(&self, lsn: u64, checkpoint_lsn: u64) -> std::io::Result<()> {
189 if lsn <= checkpoint_lsn {
190 return Ok(());
191 }
192 Err(std::io::Error::new(
193 std::io::ErrorKind::InvalidData,
194 "page LSN has no durable WAL authority",
195 ))
196 }
197
198 fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
201 Ok(false)
202 }
203
204 fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
207 Ok(())
208 }
209
210 fn reserve_pages(&self, _start: u32, _count: u32) -> std::io::Result<()> {
212 Ok(())
213 }
214
215 fn detach_free_pages(&self, _pages: &[u32]) -> std::io::Result<()> {
217 Ok(())
218 }
219
220 fn has_page_ownership_journal(&self) -> std::io::Result<bool> {
223 Ok(false)
224 }
225
226 fn retire_pages(&self, _pages: &[u32]) -> std::io::Result<()> {
228 Ok(())
229 }
230}
231
232pub struct NoWal;
234impl WalBarrier for NoWal {
235 fn flush_until(&self, _lsn: u64) -> std::io::Result<()> {
236 Err(std::io::Error::other(
237 "nonzero page LSN requires a WAL durability barrier",
238 ))
239 }
240}
241pub struct Pager<F: VfsFile, W: WalBarrier> {
242 path: PathBuf,
244 file: Mutex<F>,
246 dwb_enabled: bool,
248 staged_pages: Mutex<Option<pager_io::StagedPageBatch>>,
249 writeback_failed: std::sync::atomic::AtomicBool,
252 cache: PageCache,
254 read_only_recovery: RwLock<std::collections::HashMap<u32, Page>>,
256 freelist: RwLock<FreeList>,
258 deferred_reclamation: Mutex<DeferredPageReclamation>,
261 ownership_mutations: parking_lot::ReentrantMutex<()>,
264 allocation_reused: std::sync::atomic::AtomicBool,
267 allocation_fence_pending: std::sync::atomic::AtomicBool,
269 header: RwLock<DatabaseHeader>,
271 superblock_publication: Mutex<()>,
273 config: PagerConfig,
275 header_dirty: Mutex<bool>,
277 wal: W,
278}
279
280#[derive(Default)]
281struct DeferredPageReclamation {
282 enabled: bool,
283 pending: std::collections::BTreeSet<u32>,
284 published: std::collections::BTreeSet<u32>,
285}
286
287const SUPERBLOCK_SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
289const SUPERBLOCK_ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
290const PHASE3_DWB_ZONE_FILE_VERSION: u32 = 0x0001_0001;
293
294pub type NewestSuperblock = (
296 reddb_file::PagedSuperblockSelection,
297 [u8; SUPERBLOCK_SLOT_SIZE],
298);
299
300fn superblock_slot_image(page: &[u8; PAGE_SIZE]) -> [u8; SUPERBLOCK_SLOT_SIZE] {
303 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
304 image.copy_from_slice(&page[..SUPERBLOCK_SLOT_SIZE]);
305 image
306}
307
308impl<F: VfsFile, W: WalBarrier> Pager<F, W> {
309 pub fn open_owned(
313 file: F,
314 path: PathBuf,
315 config: PagerConfig,
316 wal: W,
317 create: bool,
318 ) -> Result<Self, PagerError> {
319 if create && file.file_len()? != 0 {
320 return Err(PagerError::InvalidDatabase(
321 "new pager file is not empty".into(),
322 ));
323 }
324 let pager = Self {
325 path,
326 file: Mutex::new(file),
327 dwb_enabled: config.double_write && !config.read_only,
328 staged_pages: Mutex::new(None),
329 writeback_failed: std::sync::atomic::AtomicBool::new(false),
330 cache: PageCache::new(config.cache_size),
331 read_only_recovery: RwLock::new(std::collections::HashMap::new()),
332 freelist: RwLock::new(FreeList::new()),
333 deferred_reclamation: Mutex::new(DeferredPageReclamation::default()),
334 ownership_mutations: parking_lot::ReentrantMutex::new(()),
335 allocation_reused: std::sync::atomic::AtomicBool::new(false),
336 allocation_fence_pending: std::sync::atomic::AtomicBool::new(false),
337 header: RwLock::new(DatabaseHeader::default()),
338 superblock_publication: Mutex::new(()),
339 config,
340 header_dirty: Mutex::new(false),
341 wal,
342 };
343 if create {
344 pager.initialize()?;
345 } else {
346 pager.ensure_phase3_dwb_zone_layout()?;
347 pager.recover_from_dwb()?;
348 pager.load_header()?;
349 }
350 Ok(pager)
351 }
352
353 pub fn wal_barrier(&self) -> &W {
354 &self.wal
355 }
356
357 pub fn lock_ownership_mutations(&self) -> parking_lot::ReentrantMutexGuard<'_, ()> {
360 self.ownership_mutations.lock()
361 }
362
363 pub fn fence_interrupted_ownership(&self) {
365 self.writeback_failed
366 .store(true, std::sync::atomic::Ordering::Release);
367 }
368
369 #[doc(hidden)]
372 pub fn with_file<T>(&self, operation: impl FnOnce(&mut F) -> T) -> Result<T, PagerError> {
373 Ok(operation(&mut *self.file_lock()?))
374 }
375
376 fn initialize(&self) -> Result<(), PagerError> {
378 if self.config.read_only {
379 return Err(PagerError::ReadOnly);
380 }
381
382 let initial_page_count = FIRST_ALLOCATABLE_PAGE;
386 let header_page = Page::new_header_page(initial_page_count);
387 self.header_write()?.page_count = initial_page_count;
388
389 self.write_page_raw(0, &header_page)?;
394 let mut metadata_page = Page::new(PageType::Header, 1);
395 metadata_page.update_checksum();
396 self.write_page_raw(1, &metadata_page)?;
397 let mut vault_page = Page::new(PageType::Vault, 2);
398 vault_page.update_checksum();
399 self.write_page_raw(2, &vault_page)?;
400 self.clear_dwb_zone()?;
401
402 let mut slot = superblock_slot_image(header_page.as_bytes());
406 self.write_superblock_slot(&mut slot, 0, 1)?;
407 self.write_superblock_slot(&mut slot, 1, 2)?;
408
409 self.sync()?;
411
412 Ok(())
413 }
414
415 fn header_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, DatabaseHeader>, PagerError> {
417 self.header.write().map_err(|_| PagerError::LockPoisoned)
418 }
419
420 fn header_read(&self) -> Result<std::sync::RwLockReadGuard<'_, DatabaseHeader>, PagerError> {
422 self.header.read().map_err(|_| PagerError::LockPoisoned)
423 }
424
425 fn freelist_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, FreeList>, PagerError> {
427 self.freelist.write().map_err(|_| PagerError::LockPoisoned)
428 }
429
430 fn file_lock(&self) -> Result<std::sync::MutexGuard<'_, F>, PagerError> {
432 self.file.lock().map_err(|_| PagerError::LockPoisoned)
433 }
434
435 fn header_dirty_lock(&self) -> Result<std::sync::MutexGuard<'_, bool>, PagerError> {
437 self.header_dirty
438 .lock()
439 .map_err(|_| PagerError::LockPoisoned)
440 }
441
442 fn read_superblock_zone(&self) -> Result<[u8; SUPERBLOCK_ZONE_SIZE], PagerError> {
446 let mut file = self.file_lock()?;
447 Ok(pager_io::read_superblock_zone(&mut *file)?)
448 }
449
450 #[doc(hidden)]
456 pub fn newest_superblock(&self) -> Result<NewestSuperblock, PagerError> {
457 let zone = self.read_superblock_zone()?;
458 let selection = reddb_file::select_paged_superblock(&zone)
459 .ok_or_else(|| PagerError::SuperblockZoneUnrecoverable(self.path.clone()))?;
460
461 let start = selection.copy_index * SUPERBLOCK_SLOT_SIZE;
462 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
463 image.copy_from_slice(&zone[start..start + SUPERBLOCK_SLOT_SIZE]);
464 Ok((selection, image))
465 }
466
467 fn ensure_phase3_dwb_zone_layout(&self) -> Result<(), PagerError> {
468 let (_, image) = self.newest_superblock()?;
469 let header = reddb_file::decode_database_header(&image)
470 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
471 if header.version == reddb_file::file_format::PAGE_RECOVERY_READ_ONLY_FILE_VERSION
472 && !self.config.read_only
473 {
474 return Err(PagerError::InvalidDatabase(
475 "recovered database is read-only; export verified data into a new store".into(),
476 ));
477 }
478 if header.requires_disk_residency() && !self.config.disk_residency && !self.config.read_only
479 {
480 return Err(PagerError::InvalidDatabase(
481 "prepared disk-resident database requires a disk-resident writer".into(),
482 ));
483 }
484 if header.version < PHASE3_DWB_ZONE_FILE_VERSION {
485 return Err(PagerError::LegacyPagedStore {
486 path: self.path.clone(),
487 });
488 }
489 Ok(())
490 }
491
492 fn write_superblock_slot(
495 &self,
496 slot: &mut [u8; SUPERBLOCK_SLOT_SIZE],
497 copy_index: usize,
498 generation: u64,
499 ) -> Result<(), PagerError> {
500 self.ensure_writeback_available()?;
501 if self.config.read_only {
502 return Err(PagerError::ReadOnly);
503 }
504 let mut file = self.file_lock()?;
505 self.ensure_writeback_available()?;
506 Ok(pager_io::write_superblock_slot(
507 &mut *file, slot, copy_index, generation,
508 )?)
509 }
510
511 #[doc(hidden)]
517 pub fn commit_superblock(
518 &self,
519 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
520 ) -> Result<(), PagerError> {
521 let _ownership = self.lock_ownership_mutations();
522 let _publication = self
523 .superblock_publication
524 .lock()
525 .map_err(|_| PagerError::LockPoisoned)?;
526 self.commit_superblock_locked(mutate)
527 }
528
529 fn commit_superblock_locked(
530 &self,
531 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
532 ) -> Result<(), PagerError> {
533 if self.config.read_only {
534 return Err(PagerError::ReadOnly);
535 }
536 let (newest, mut image) = self.newest_superblock()?;
537 mutate(&mut image)?;
538 let next_copy = reddb_file::paged_superblock_next_copy(Some(newest));
539 self.write_superblock_slot(&mut image, next_copy, newest.generation.saturating_add(1))
540 }
541
542 pub fn fence_file_format_version(&self, version: u32) -> Result<(), PagerError> {
548 let _ownership = self.lock_ownership_mutations();
549 self.ensure_writeback_available()?;
550 if self.config.read_only {
551 return Err(PagerError::ReadOnly);
552 }
553 if version == reddb_file::file_format::PAGE_RECOVERY_READ_ONLY_FILE_VERSION {
554 return Err(PagerError::InvalidDatabase(
555 "recovery read-only format is reserved".into(),
556 ));
557 }
558 if !(PHASE3_DWB_ZONE_FILE_VERSION..=reddb_file::PAGE_FILE_VERSION).contains(&version) {
559 return Err(PagerError::InvalidDatabase(
560 "unsupported file format fence".into(),
561 ));
562 }
563 let current = self.header_read()?.clone();
564 if version < current.version {
565 return Err(PagerError::InvalidDatabase(
566 "file format downgrade is not supported".into(),
567 ));
568 }
569 let legacy_disk_residency =
570 reddb_file::file_format::page_format_is_disk_resident(current.version);
571 let required_capabilities = current.required_capabilities
572 | if version >= reddb_file::PAGE_REQUIRED_CAPABILITIES_FILE_VERSION
573 && legacy_disk_residency
574 {
575 reddb_file::FILE_CAPABILITY_DISK_RESIDENCY
576 } else {
577 0
578 };
579 let requires_disk_residency =
580 if version < reddb_file::PAGE_REQUIRED_CAPABILITIES_FILE_VERSION {
581 reddb_file::file_format::page_format_is_disk_resident(version)
582 } else {
583 required_capabilities & reddb_file::FILE_CAPABILITY_DISK_RESIDENCY != 0
584 };
585 if requires_disk_residency && !self.config.disk_residency {
586 return Err(PagerError::InvalidDatabase(
587 "disk-residency format requires a disk-resident writer".into(),
588 ));
589 }
590 let _publication = self
591 .superblock_publication
592 .lock()
593 .map_err(|_| PagerError::LockPoisoned)?;
594 let result = (|| {
595 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
596 self.commit_superblock_locked(|image| {
597 reddb_file::set_database_header_version(image, version)
598 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
599 if version >= reddb_file::PAGE_REQUIRED_CAPABILITIES_FILE_VERSION {
600 reddb_file::set_database_header_required_capabilities(
601 image,
602 required_capabilities,
603 )
604 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
605 }
606 Ok(())
607 })?;
608 }
609 let mut header = self.header_write()?;
610 header.version = version;
611 if version >= reddb_file::PAGE_REQUIRED_CAPABILITIES_FILE_VERSION {
612 header.required_capabilities = required_capabilities;
613 }
614 Ok(())
615 })();
616 if result.is_err() {
617 self.writeback_failed
618 .store(true, std::sync::atomic::Ordering::Release);
619 }
620 result
621 }
622
623 pub fn fence_required_capabilities(&self, capabilities: u64) -> Result<(), PagerError> {
629 let _ownership = self.lock_ownership_mutations();
630 self.ensure_writeback_available()?;
631 if self.config.read_only {
632 return Err(PagerError::ReadOnly);
633 }
634 let unsupported = capabilities & !reddb_file::FILE_CAPABILITIES_SUPPORTED;
635 if unsupported != 0 {
636 return Err(PagerError::InvalidDatabase(format!(
637 "unsupported required database capabilities: {unsupported:#018x}"
638 )));
639 }
640
641 let current = self.header_read()?.clone();
642 if current.version == reddb_file::file_format::PAGE_RECOVERY_READ_ONLY_FILE_VERSION {
643 return Err(PagerError::InvalidDatabase(
644 "recovery read-only format cannot enable capabilities".into(),
645 ));
646 }
647 let legacy_disk_residency =
648 reddb_file::file_format::page_format_is_disk_resident(current.version);
649 let required = current.required_capabilities
650 | capabilities
651 | if legacy_disk_residency {
652 reddb_file::FILE_CAPABILITY_DISK_RESIDENCY
653 } else {
654 0
655 };
656 let version = current
657 .version
658 .max(reddb_file::PAGE_REQUIRED_CAPABILITIES_FILE_VERSION);
659 if required == current.required_capabilities && version == current.version {
660 return Ok(());
661 }
662 if required & reddb_file::FILE_CAPABILITY_DISK_RESIDENCY != 0 && !self.config.disk_residency
663 {
664 return Err(PagerError::InvalidDatabase(
665 "disk-residency capability requires a disk-resident writer".into(),
666 ));
667 }
668
669 let _publication = self
670 .superblock_publication
671 .lock()
672 .map_err(|_| PagerError::LockPoisoned)?;
673 let result = (|| {
674 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
675 self.commit_superblock_locked(|image| {
676 reddb_file::set_database_header_version(image, version)
677 .and_then(|()| {
678 reddb_file::set_database_header_required_capabilities(image, required)
679 })
680 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
681 })?;
682 }
683 let mut header = self.header_write()?;
684 header.version = version;
685 header.required_capabilities = required;
686 Ok(())
687 })();
688 if result.is_err() {
689 self.writeback_failed
690 .store(true, std::sync::atomic::Ordering::Release);
691 }
692 result
693 }
694
695 fn load_header(&self) -> Result<(), PagerError> {
697 let (_, image) = self.newest_superblock()?;
698
699 let mut decoded_header = reddb_file::decode_database_header(&image)
700 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
701 let physical_pages = self.file_lock()?.file_len()?.div_ceil(PAGE_SIZE as u64);
707 let mut physical_pages = u32::try_from(physical_pages).map_err(|_| {
708 PagerError::InvalidDatabase("data file exceeds the page address space".into())
709 })?;
710 if self.config.read_only {
711 if let Some(id) = self
712 .read_only_recovery
713 .read()
714 .map_err(|_| PagerError::LockPoisoned)?
715 .keys()
716 .max()
717 {
718 physical_pages = physical_pages.max(id.checked_add(1).ok_or_else(|| {
719 PagerError::InvalidDatabase(
720 "recovery page exceeds the page address space".into(),
721 )
722 })?);
723 }
724 }
725 if decoded_header.page_count < physical_pages {
726 decoded_header.page_count = physical_pages;
727 if !self.config.read_only {
728 *self.header_dirty_lock()? = true;
729 }
730 }
731 let freelist_head = decoded_header.freelist_head;
732
733 {
734 let mut header = self.header_write()?;
735 *header = decoded_header;
736 }
737
738 {
740 let mut freelist = self.freelist_write()?;
741 *freelist = FreeList::from_header(freelist_head, 0);
742 }
743
744 Ok(())
745 }
746
747 fn write_header(&self) -> Result<(), PagerError> {
749 if self.config.read_only {
750 return Err(PagerError::ReadOnly);
751 }
752
753 let header = self.header_read()?.clone();
754 self.commit_superblock(|image| {
755 reddb_file::encode_database_header(image, &header)
756 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
757 })?;
758 *self.header_dirty_lock()? = false;
759
760 Ok(())
761 }
762
763 fn read_page_raw(&self, page_id: u32) -> Result<Page, PagerError> {
765 let mut file = self.file_lock()?;
766 let page = match self
767 .staged_pages
768 .lock()
769 .map_err(|_| PagerError::LockPoisoned)?
770 .as_ref()
771 {
772 Some(batch) => match batch.read(&mut *file, page_id)? {
773 Some(page) => page,
774 None => pager_io::read_page(&mut *file, page_id, self.config.verify_checksums)?,
775 },
776 None => pager_io::read_page(&mut *file, page_id, self.config.verify_checksums)?,
777 };
778 drop(file);
779 if self.config.verify_checksums && page_id != 0 && page.lsn() != 0 {
780 self.wal
781 .validate_page_lsn(page.lsn(), self.header_read()?.checkpoint_lsn)
782 .map_err(|error| {
783 PagerError::InvalidDatabase(format!(
784 "page {page_id} has unverified LSN {}: {error}",
785 page.lsn()
786 ))
787 })?;
788 }
789 Ok(page)
790 }
791
792 fn ensure_writeback_available(&self) -> Result<(), PagerError> {
793 if self
794 .writeback_failed
795 .load(std::sync::atomic::Ordering::Acquire)
796 {
797 return Err(PagerError::Io(std::io::Error::other(
798 "page writeback failed; reopen the database to recover before writing",
799 )));
800 }
801 Ok(())
802 }
803
804 pub fn check_writeback_available(&self) -> Result<(), PagerError> {
810 self.ensure_writeback_available()
811 }
812
813 fn record_ownership(
814 &self,
815 operation: impl FnOnce(&W) -> std::io::Result<()>,
816 ) -> Result<(), PagerError> {
817 if let Err(error) = operation(&self.wal) {
818 self.writeback_failed
819 .store(true, std::sync::atomic::Ordering::Release);
820 return Err(PagerError::Io(error));
821 }
822 Ok(())
823 }
824
825 fn write_evicted_dirty_page(
828 &self,
829 page: Page,
830 pending_page: Option<u32>,
831 ) -> Result<(), PagerError> {
832 if self.bounded_writeback()? {
833 return self.write_dirty_page_batch(&[(page.page_id(), page)]);
834 }
835 let mut ids = self.cache.dirty_page_ids(pager_io::DWB_MAX_PAGES_PER_FRAME);
836 ids.retain(|id| Some(*id) != pending_page);
840 ids.truncate(pager_io::DWB_MAX_PAGES_PER_FRAME.saturating_sub(1));
843 ids.sort_unstable();
844 let mut pages = self.cache.flush_dirty_pages(&ids);
845 pages.push((page.page_id(), page));
846 pages.sort_unstable_by_key(|(id, _)| *id);
847 self.write_dirty_page_batch(&pages)
848 }
849
850 fn write_page_raw(&self, page_id: u32, page: &Page) -> Result<(), PagerError> {
852 self.ensure_writeback_available()?;
853 if self.config.read_only {
854 return Err(PagerError::ReadOnly);
855 }
856 let mut file = self.file_lock()?;
857 self.ensure_writeback_available()?;
858 if let Some(batch) = self
859 .staged_pages
860 .lock()
861 .map_err(|_| PagerError::LockPoisoned)?
862 .as_mut()
863 {
864 return Ok(batch.stage(&mut *file, page_id, page)?);
865 }
866 Ok(pager_io::write_page(&mut *file, page_id, page)?)
867 }
868
869 pub fn read_page(&self, page_id: u32) -> Result<Page, PagerError> {
871 if self.config.read_only {
872 if let Some(page) = self
873 .read_only_recovery
874 .read()
875 .map_err(|_| PagerError::LockPoisoned)?
876 .get(&page_id)
877 {
878 if self.config.verify_checksums {
879 page.verify_checksum()?;
880 }
881 return Ok(page.clone());
882 }
883 }
884 if let Some(page) = self.cache.get(page_id) {
886 return Ok(page);
887 }
888
889 let _ownership = self.lock_ownership_mutations();
893 if let Some(page) = self.cache.get(page_id) {
894 return Ok(page);
895 }
896
897 let page = self.read_page_raw(page_id)?;
899
900 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
902 self.write_evicted_dirty_page(dirty_page, None)?;
904 }
905
906 Ok(page)
907 }
908
909 pub fn read_page_no_checksum(&self, page_id: u32) -> Result<Page, PagerError> {
914 if self.config.read_only {
915 if let Some(page) = self
916 .read_only_recovery
917 .read()
918 .map_err(|_| PagerError::LockPoisoned)?
919 .get(&page_id)
920 {
921 return Ok(page.clone());
922 }
923 }
924 if let Some(page) = self.cache.get(page_id) {
926 return Ok(page);
927 }
928
929 let _ownership = self.lock_ownership_mutations();
930 if let Some(page) = self.cache.get(page_id) {
931 return Ok(page);
932 }
933
934 let mut file = self.file_lock()?;
936 let page = pager_io::read_page(&mut *file, page_id, false)?;
937 drop(file);
938
939 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
941 self.write_evicted_dirty_page(dirty_page, None)?;
943 }
944
945 Ok(page)
946 }
947
948 pub fn write_page(&self, page_id: u32, mut page: Page) -> Result<(), PagerError> {
950 let _ownership = self.lock_ownership_mutations();
951 self.ensure_writeback_available()?;
952 if self.config.read_only {
953 return Err(PagerError::ReadOnly);
954 }
955
956 pager_io::check_page_identity(&page, page_id)?;
957 self.capture_page_before_write(page_id)?;
958
959 page.update_checksum();
961
962 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
967 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
968 }
969
970 Ok(())
971 }
972
973 pub fn write_page_no_checksum(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
978 let _ownership = self.lock_ownership_mutations();
979 self.ensure_writeback_available()?;
980 if self.config.read_only {
981 return Err(PagerError::ReadOnly);
982 }
983
984 self.capture_page_before_write(page_id)?;
985
986 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
989 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
990 }
991
992 Ok(())
993 }
994
995 fn capture_page_before_write(&self, page_id: u32) -> Result<(), PagerError> {
996 let mut required = false;
997 self.record_ownership(|wal| {
998 required = wal.needs_page_image(page_id)?;
999 Ok(())
1000 })?;
1001 if required {
1002 let previous = self.read_page(page_id)?;
1003 self.record_ownership(|wal| wal.capture_page_image(&previous))?;
1004 }
1005 Ok(())
1006 }
1007
1008 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, PagerError> {
1010 let _ownership = self.lock_ownership_mutations();
1011 self.ensure_writeback_available()?;
1012 if self.config.read_only {
1013 return Err(PagerError::ReadOnly);
1014 }
1015
1016 let page_id = if self.batch_active()? {
1018 let mut header = self.header_write()?;
1021 let id = header.page_count;
1022 header.page_count = id
1023 .checked_add(1)
1024 .ok_or_else(|| PagerError::InvalidDatabase("page count overflow".into()))?;
1025 *self.header_dirty_lock()? = true;
1026 id
1027 } else {
1028 let mut freelist = self.freelist_write()?;
1029 self.complete_allocation_fence()?;
1032 if let Some(id) = freelist.next_in_memory_page() {
1033 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
1034 let id = freelist
1035 .allocate()
1036 .expect("free page checked while allocator is locked");
1037 self.allocation_reused
1038 .store(true, std::sync::atomic::Ordering::Release);
1039 id
1040 } else if freelist.trunk_head() != 0 {
1041 let trunk_id = freelist.trunk_head();
1042
1043 let trunk = self.read_page(trunk_id).map_err(|e| match e {
1044 PagerError::PageNotFound(_) => {
1045 PagerError::InvalidDatabase("Freelist trunk missing".to_string())
1046 }
1047 other => other,
1048 })?;
1049 if trunk.page_id() != trunk_id {
1050 return Err(PagerError::InvalidDatabase(
1051 "misdirected freelist trunk".into(),
1052 ));
1053 }
1054
1055 let mut detached = FreeList::new();
1058 detached
1059 .load_from_trunk(&trunk)
1060 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {e}")))?;
1061 let mut pages = Vec::new();
1062 let mut seen = std::collections::HashSet::new();
1063 let page_count = self.page_count()?;
1064 while let Some(id) = detached.allocate() {
1065 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count || !seen.insert(id) {
1066 return Err(PagerError::InvalidDatabase(
1067 "invalid freelist trunk page".into(),
1068 ));
1069 }
1070 pages.push(id);
1071 }
1072 let mut journaled = false;
1073 self.record_ownership(|wal| {
1074 journaled = wal.has_page_ownership_journal()?;
1075 Ok(())
1076 })?;
1077 if !journaled {
1078 self.allocate_unjournaled_trunk(&mut freelist, trunk, &pages)?
1079 } else {
1080 self.record_ownership(|wal| wal.detach_free_pages(&pages))?;
1081
1082 freelist
1083 .load_from_trunk(&trunk)
1084 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {}", e)))?;
1085 let mut header = self.header_write()?;
1086 header.freelist_head = freelist.trunk_head();
1087 *self.header_dirty_lock()? = true;
1088 drop(header);
1089 self.allocation_fence_pending
1094 .store(true, std::sync::atomic::Ordering::Release);
1095 self.complete_allocation_fence()?;
1096 let next = freelist.next_in_memory_page().ok_or_else(|| {
1097 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
1098 })?;
1099 self.record_ownership(|wal| wal.reserve_pages(next, 1))?;
1100 let id = freelist.allocate().ok_or_else(|| {
1101 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
1102 })?;
1103 self.allocation_reused
1104 .store(true, std::sync::atomic::Ordering::Release);
1105 id
1106 }
1107 } else {
1108 let mut header = self.header_write()?;
1110 let id = header.page_count;
1111 let next = id
1112 .checked_add(1)
1113 .ok_or_else(|| PagerError::InvalidDatabase("page count overflow".into()))?;
1114 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
1115 header.page_count = next;
1116 *self.header_dirty_lock()? = true;
1117 id
1118 }
1119 };
1120
1121 let page = Page::new(page_type, page_id);
1122
1123 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page.clone()) {
1135 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
1136 }
1137
1138 Ok(page)
1139 }
1140
1141 fn allocate_unjournaled_trunk(
1145 &self,
1146 freelist: &mut FreeList,
1147 mut trunk: Page,
1148 pages: &[u32],
1149 ) -> Result<u32, PagerError> {
1150 let id = *pages
1151 .first()
1152 .expect("validated trunk includes its own page");
1153 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
1154 if pages.len() > 1 {
1155 let remaining =
1156 u32::try_from(pages.len() - 2).expect("a validated trunk fits in one page");
1157 trunk.content_mut()[4..8].copy_from_slice(&remaining.to_le_bytes());
1158 trunk.update_checksum();
1159 self.cache.remove(trunk.page_id());
1160 self.write_pages_through_dwb(&[(trunk.page_id(), trunk)])?;
1161 freelist.note_durable_trunk_allocation(remaining);
1162 } else {
1163 freelist
1166 .load_from_trunk(&trunk)
1167 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
1168 self.header_write()?.freelist_head = freelist.trunk_head();
1169 *self.header_dirty_lock()? = true;
1170 self.allocation_fence_pending
1171 .store(true, std::sync::atomic::Ordering::Release);
1172 self.complete_allocation_fence()?;
1173 assert_eq!(freelist.allocate(), Some(id));
1174 }
1175 self.allocation_reused
1176 .store(true, std::sync::atomic::Ordering::Release);
1177 Ok(id)
1178 }
1179
1180 pub fn reserve_contig_extent(&self, n_pages: u32) -> Result<ExtentId, PagerError> {
1182 let _ownership = self.lock_ownership_mutations();
1183 self.ensure_writeback_available()?;
1184 if self.config.read_only {
1185 return Err(PagerError::ReadOnly);
1186 }
1187 if n_pages == 0 {
1188 return Err(PagerError::InvalidDatabase(
1189 "contiguous extent must reserve at least one page".to_string(),
1190 ));
1191 }
1192
1193 let start_page = {
1194 let mut header = self.header_write()?;
1195 let start = header.page_count;
1196 let end = header.page_count.checked_add(n_pages).ok_or_else(|| {
1197 PagerError::InvalidDatabase("contiguous extent page count overflow".to_string())
1198 })?;
1199 self.record_ownership(|wal| wal.reserve_pages(start, n_pages))?;
1200 header.page_count = end;
1201 *self.header_dirty_lock()? = true;
1202 start
1203 };
1204
1205 for page_id in start_page..start_page + n_pages {
1206 let mut page = Page::new(PageType::Vector, page_id);
1207 page.update_checksum();
1208 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
1209 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
1210 }
1211 }
1212
1213 Ok(ExtentId {
1214 start_page,
1215 n_pages,
1216 })
1217 }
1218
1219 pub fn free_page(&self, page_id: u32) -> Result<(), PagerError> {
1221 if self.batch_active()? {
1222 return Err(PagerError::InvalidDatabase(
1223 "prepared DWB batches cannot retire published pages".into(),
1224 ));
1225 }
1226 let _ownership = self.lock_ownership_mutations();
1227 self.ensure_writeback_available()?;
1228 if self.config.read_only {
1229 return Err(PagerError::ReadOnly);
1230 }
1231
1232 let mut deferred = self
1233 .deferred_reclamation
1234 .lock()
1235 .map_err(|_| PagerError::LockPoisoned)?;
1236 if deferred.enabled {
1237 if page_id < FIRST_ALLOCATABLE_PAGE || page_id >= self.page_count()? {
1238 return Err(PagerError::InvalidDatabase(
1239 "invalid deferred free page".into(),
1240 ));
1241 }
1242 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1243 deferred.pending.insert(page_id);
1246 return Ok(());
1247 }
1248 drop(deferred);
1249
1250 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1251
1252 self.cache.remove(page_id);
1254
1255 let mut freelist = self.freelist_write()?;
1257 freelist.free(page_id);
1258
1259 *self.header_dirty_lock()? = true;
1260
1261 Ok(())
1262 }
1263
1264 pub fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
1265 let _ownership = self.lock_ownership_mutations();
1266 self.deferred_reclamation
1267 .lock()
1268 .map_err(|_| PagerError::LockPoisoned)?
1269 .enabled = true;
1270 Ok(())
1271 }
1272
1273 pub fn deferred_reclamation_pages(&self) -> Result<(Vec<u32>, Vec<u32>), PagerError> {
1274 let deferred = self
1275 .deferred_reclamation
1276 .lock()
1277 .map_err(|_| PagerError::LockPoisoned)?;
1278 Ok((
1279 deferred.pending.iter().copied().collect(),
1280 deferred.published.iter().copied().collect(),
1281 ))
1282 }
1283
1284 pub fn restore_deferred_reclamation(&self, pages: &[u32]) -> Result<(), PagerError> {
1285 let _ownership = self.lock_ownership_mutations();
1286 let count = self.page_count()?;
1287 if pages
1288 .iter()
1289 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= count)
1290 {
1291 return Err(PagerError::InvalidDatabase(
1292 "invalid persisted deferred free page".into(),
1293 ));
1294 }
1295 let mut deferred = self
1296 .deferred_reclamation
1297 .lock()
1298 .map_err(|_| PagerError::LockPoisoned)?;
1299 deferred.pending.extend(pages);
1300 deferred.published.extend(pages);
1301 Ok(())
1302 }
1303
1304 pub fn complete_deferred_publication(
1307 &self,
1308 retained: &[u32],
1309 released: &[u32],
1310 ) -> Result<(), PagerError> {
1311 let _ownership = self.lock_ownership_mutations();
1312 let mut deferred = self
1313 .deferred_reclamation
1314 .lock()
1315 .map_err(|_| PagerError::LockPoisoned)?;
1316 if !released.is_empty() {
1317 self.free_retired_pages(released)?;
1318 }
1319 for id in released {
1320 deferred.pending.remove(id);
1321 }
1322 deferred.published = retained.iter().copied().collect();
1323 Ok(())
1324 }
1325
1326 pub fn record_retirement_intent(&self, pages: &[u32]) -> Result<(), PagerError> {
1328 let _ownership = self.lock_ownership_mutations();
1329 self.ensure_writeback_available()?;
1330 if self.config.read_only {
1331 return Err(PagerError::ReadOnly);
1332 }
1333 let page_count = self.page_count()?;
1334 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1335 if pages
1336 .iter()
1337 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !seen.insert(*id))
1338 {
1339 return Err(PagerError::InvalidDatabase(
1340 "invalid retirement intent".into(),
1341 ));
1342 }
1343 self.record_ownership(|wal| wal.retire_pages(pages))
1344 }
1345
1346 pub fn free_retired_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1349 let _ownership = self.lock_ownership_mutations();
1350 self.ensure_writeback_available()?;
1351 if self.config.read_only {
1352 return Err(PagerError::ReadOnly);
1353 }
1354 let page_count = self.page_count()?;
1355 if pages
1356 .iter()
1357 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count)
1358 {
1359 return Err(PagerError::InvalidDatabase(
1360 "invalid retired page range".into(),
1361 ));
1362 }
1363 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1364 if pages.iter().any(|id| !seen.insert(*id)) {
1365 return Err(PagerError::InvalidDatabase("duplicate retired page".into()));
1366 }
1367 let mut freelist = self.freelist_write()?;
1368 if freelist.contains_any(&seen) {
1369 return Err(PagerError::InvalidDatabase(
1370 "retired page is already free".into(),
1371 ));
1372 }
1373 let mut trunk_id = freelist.trunk_head();
1377 let mut visited = std::collections::HashSet::new();
1378 while trunk_id != 0 && !seen.is_empty() {
1379 if trunk_id < FIRST_ALLOCATABLE_PAGE
1380 || trunk_id >= page_count
1381 || !visited.insert(trunk_id)
1382 {
1383 return Err(PagerError::InvalidDatabase(
1384 "invalid freelist trunk chain".into(),
1385 ));
1386 }
1387 let trunk = self.read_page(trunk_id)?;
1388 if trunk.page_id() != trunk_id {
1389 return Err(PagerError::InvalidDatabase(
1390 "misdirected freelist trunk".into(),
1391 ));
1392 }
1393 let mut loaded = FreeList::new();
1394 let next = loaded
1395 .load_from_trunk(&trunk)
1396 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1397 while let Some(id) = loaded.allocate() {
1398 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1399 return Err(PagerError::InvalidDatabase(
1400 "invalid free page range".into(),
1401 ));
1402 }
1403 if seen.contains(&id) {
1404 return Err(PagerError::InvalidDatabase(
1405 "retired page is already free".into(),
1406 ));
1407 }
1408 }
1409 trunk_id = next.unwrap_or(0);
1410 }
1411 self.record_ownership(|wal| wal.retire_pages(pages))?;
1412 for id in pages {
1413 self.cache.remove(*id);
1414 }
1415 freelist.free_batch(pages);
1416 *self.header_dirty_lock()? = true;
1417 Ok(())
1418 }
1419
1420 fn pages_not_in_freelist(
1421 &self,
1422 pages: &[u32],
1423 freelist: &FreeList,
1424 page_count: u32,
1425 ) -> Result<std::collections::HashSet<u32>, PagerError> {
1426 let mut remaining = std::collections::HashSet::with_capacity(pages.len());
1427 if pages
1428 .iter()
1429 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !remaining.insert(*id))
1430 {
1431 return Err(PagerError::InvalidDatabase(
1432 "invalid recovery page set".into(),
1433 ));
1434 }
1435 let requested = remaining.clone();
1436 freelist
1437 .remove_existing_from(&requested, &mut remaining)
1438 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1439 let mut trunk_id = freelist.trunk_head();
1440 let mut visited = std::collections::HashSet::new();
1441 while trunk_id != 0 {
1442 if trunk_id < FIRST_ALLOCATABLE_PAGE
1443 || trunk_id >= page_count
1444 || !visited.insert(trunk_id)
1445 {
1446 return Err(PagerError::InvalidDatabase(
1447 "invalid freelist trunk chain".into(),
1448 ));
1449 }
1450 let trunk = self.read_page(trunk_id)?;
1451 if trunk.page_id() != trunk_id {
1452 return Err(PagerError::InvalidDatabase(
1453 "misdirected freelist trunk".into(),
1454 ));
1455 }
1456 let mut loaded = FreeList::new();
1457 let next = loaded
1458 .load_from_trunk(&trunk)
1459 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1460 while let Some(id) = loaded.allocate() {
1461 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1462 return Err(PagerError::InvalidDatabase(
1463 "invalid free page range".into(),
1464 ));
1465 }
1466 if requested.contains(&id) && !remaining.remove(&id) {
1467 return Err(PagerError::InvalidDatabase(
1468 "duplicate recovery page".into(),
1469 ));
1470 }
1471 }
1472 trunk_id = next.unwrap_or(0);
1473 }
1474 Ok(remaining)
1475 }
1476
1477 pub fn recover_unowned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1481 let _ownership = self.lock_ownership_mutations();
1482 self.ensure_writeback_available()?;
1483 if self.config.read_only {
1484 return Err(PagerError::ReadOnly);
1485 }
1486 let page_count = self.page_count()?;
1487 let mut freelist = self.freelist_write()?;
1488 let remaining = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1489 if !remaining.is_empty() {
1490 let mut missing: Vec<_> = remaining.into_iter().collect();
1491 missing.sort_unstable();
1492 for id in &missing {
1493 self.cache.remove(*id);
1494 }
1495 freelist.free_batch(&missing);
1496 *self.header_dirty_lock()? = true;
1497 }
1498 Ok(())
1499 }
1500
1501 pub fn validate_owned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1504 let page_count = self.page_count()?;
1505 let freelist = self.freelist_write()?;
1506 let absent = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1507 if absent.len() != pages.len() {
1508 return Err(PagerError::InvalidDatabase(
1509 "owned recovery page is free".into(),
1510 ));
1511 }
1512 Ok(())
1513 }
1514
1515 pub fn sync_recovered_allocator(&self) -> Result<(), PagerError> {
1518 let _ownership = self.lock_ownership_mutations();
1519 let result = (|| {
1520 self.sync()?;
1521 let _publication = self
1522 .superblock_publication
1523 .lock()
1524 .map_err(|_| PagerError::LockPoisoned)?;
1525 let header = self.header_read()?.clone();
1526 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1527 self.commit_superblock_locked(|image| {
1528 reddb_file::encode_database_header(image, &header)
1529 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
1530 })?;
1531 }
1532 Ok(())
1533 })();
1534 if result.is_err() {
1535 self.writeback_failed
1536 .store(true, std::sync::atomic::Ordering::Release);
1537 }
1538 result
1539 }
1540
1541 pub fn free_page_count(&self) -> Result<u32, PagerError> {
1544 Ok(self.freelist_write()?.total_free())
1545 }
1546
1547 pub fn header(&self) -> Result<DatabaseHeader, PagerError> {
1548 Ok(self.header_read()?.clone())
1549 }
1550
1551 pub fn physical_header(&self) -> Result<PhysicalFileHeader, PagerError> {
1552 Ok(self.header_read()?.physical)
1553 }
1554
1555 pub fn update_physical_header(&self, physical: PhysicalFileHeader) -> Result<(), PagerError> {
1556 let _ownership = self.lock_ownership_mutations();
1557 self.ensure_writeback_available()?;
1558 if self.config.read_only {
1559 return Err(PagerError::ReadOnly);
1560 }
1561
1562 let mut header = self.header_write()?;
1563 header.physical = physical;
1564 *self.header_dirty_lock()? = true;
1565 Ok(())
1566 }
1567
1568 pub fn page_count(&self) -> Result<u32, PagerError> {
1570 Ok(self.header_read()?.page_count)
1571 }
1572
1573 pub fn flush(&self) -> Result<(), PagerError> {
1575 let _ownership = self.lock_ownership_mutations();
1576 self.ensure_writeback_available()?;
1577 if self.config.read_only {
1578 return Ok(());
1579 }
1580
1581 let trunks = {
1583 let mut freelist = self.freelist_write()?;
1584 if freelist.is_dirty() {
1585 let mut header = self.header_write()?;
1586 let trunks = freelist.flush_to_trunks(0, || {
1587 let id = header.page_count;
1588 header.page_count += 1;
1589 id
1590 });
1591 header.freelist_head = freelist.trunk_head();
1592 *self.header_dirty_lock()? = true;
1593 freelist.mark_clean();
1594 trunks
1595 } else {
1596 Vec::new()
1597 }
1598 };
1599
1600 let mut dirty_pages = Vec::new();
1601 for trunk in trunks {
1602 let page_id = trunk.page_id();
1603 if let Some(evicted) = self.cache.insert_dirty(page_id, trunk) {
1604 dirty_pages.push((evicted.page_id(), evicted));
1607 }
1608 }
1609
1610 const FLUSH_BATCH_PAGES: usize = 256;
1614 for batch in dirty_pages.chunks(FLUSH_BATCH_PAGES) {
1615 self.write_dirty_page_batch(batch)?;
1616 }
1617 drop(dirty_pages);
1618 let mut page_ids = self.cache.dirty_page_ids(usize::MAX);
1622 page_ids.sort_unstable();
1623 if self.bounded_writeback()? {
1624 for id in page_ids {
1625 if let Some(page) = self.cache.flush_dirty_page(id) {
1626 self.write_dirty_page_batch(&[(id, page)])?;
1627 }
1628 }
1629 } else {
1630 for ids in page_ids.chunks(FLUSH_BATCH_PAGES) {
1631 let pages = self.cache.flush_dirty_pages(ids);
1632 self.write_dirty_page_batch(&pages)?;
1633 }
1634 }
1635
1636 if !self.batch_active()? && *self.header_dirty_lock()? {
1638 self.write_header()?;
1639 }
1640
1641 Ok(())
1642 }
1643
1644 fn write_dirty_page_batch(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1645 if pages.is_empty() {
1646 return Ok(());
1647 }
1648 let result = (|| {
1652 let max_lsn = pages
1653 .iter()
1654 .filter_map(|(_, page)| page.header().ok().map(|header| header.lsn))
1655 .max()
1656 .unwrap_or(0);
1657 if max_lsn > 0 {
1658 self.wal.flush_until(max_lsn)?;
1659 }
1660 self.write_pages_through_dwb(pages)
1661 })();
1662 if result.is_err() {
1663 self.writeback_failed
1664 .store(true, std::sync::atomic::Ordering::Release);
1665 for (page_id, _) in pages {
1666 self.cache.mark_dirty(*page_id);
1667 }
1668 }
1669 result
1670 }
1671
1672 pub fn sync(&self) -> Result<(), PagerError> {
1674 let _ownership = self.lock_ownership_mutations();
1675 self.flush()?;
1676
1677 if self
1678 .allocation_reused
1679 .swap(false, std::sync::atomic::Ordering::AcqRel)
1680 {
1681 if let Err(error) = self.write_header_and_sync() {
1685 self.allocation_reused
1686 .store(true, std::sync::atomic::Ordering::Release);
1687 return Err(error);
1688 }
1689 }
1690
1691 let file = self.file_lock()?;
1692 file.sync_all()?;
1693
1694 Ok(())
1695 }
1696
1697 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
1699 self.cache.stats()
1700 }
1701
1702 pub fn cache_len(&self) -> usize {
1705 self.cache.len()
1706 + if self.config.read_only {
1707 self.read_only_recovery
1708 .read()
1709 .unwrap_or_else(|poisoned| poisoned.into_inner())
1710 .len()
1711 } else {
1712 0
1713 }
1714 }
1715
1716 pub fn cache_capacity(&self) -> usize {
1719 self.cache.capacity()
1720 }
1721
1722 pub fn dirty_page_count(&self) -> usize {
1724 self.cache.dirty_count()
1725 }
1726
1727 pub fn dirty_fraction(&self) -> f64 {
1731 let capacity = self.cache.capacity().max(1) as f64;
1732 self.cache.dirty_count() as f64 / capacity
1733 }
1734
1735 pub fn flush_some_dirty(&self, max: usize) -> Result<usize, PagerError> {
1739 let _ownership = self.lock_ownership_mutations();
1740 self.ensure_writeback_available()?;
1741 if self.config.read_only || max == 0 {
1742 return Ok(0);
1743 }
1744 let dirty_pages = self.cache.flush_some_dirty(max);
1745 if dirty_pages.is_empty() {
1746 return Ok(0);
1747 }
1748 let count = dirty_pages.len();
1749 for (page_id, page) in dirty_pages {
1756 self.write_page(page_id, page)?;
1757 }
1758 Ok(count)
1759 }
1760
1761 pub fn path(&self) -> &Path {
1763 &self.path
1764 }
1765
1766 pub fn into_inner(self) -> F {
1769 self.file
1770 .into_inner()
1771 .unwrap_or_else(|poisoned| poisoned.into_inner())
1772 }
1773
1774 pub fn is_read_only(&self) -> bool {
1776 self.config.read_only
1777 }
1778
1779 pub fn file_size(&self) -> Result<u64, PagerError> {
1781 let file = self.file_lock()?;
1782 Ok(file.file_len()?)
1783 }
1784
1785 fn clear_dwb_zone(&self) -> Result<(), PagerError> {
1787 let mut file = self.file_lock()?;
1788 Ok(pager_io::clear_dwb_zone(&mut *file)?)
1789 }
1790
1791 pub fn read_manifest_page(&self) -> Result<Page, PagerError> {
1796 self.read_page(1)
1797 .map_err(|_| PagerError::ManifestZoneCorrupt(self.path.clone()))
1798 }
1799
1800 fn write_pages_through_dwb(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1807 if self.config.read_only {
1808 return Err(PagerError::ReadOnly);
1809 }
1810 let mut file = self.file_lock()?;
1813 self.ensure_writeback_available()?;
1814 let result: Result<(), PagerError> = (|| {
1815 let mut staged = self
1816 .staged_pages
1817 .lock()
1818 .map_err(|_| PagerError::LockPoisoned)?;
1819 if let Some(batch) = staged.as_mut() {
1820 for (id, page) in pages {
1821 batch.stage(&mut *file, *id, page)?;
1822 }
1823 Ok(())
1824 } else {
1825 Ok(pager_io::write_pages(&mut *file, pages, self.dwb_enabled)?)
1826 }
1827 })();
1828 if result.is_err() {
1829 self.writeback_failed
1832 .store(true, std::sync::atomic::Ordering::Release);
1833 }
1834 result
1835 }
1836
1837 fn recover_from_dwb(&self) -> Result<(), PagerError> {
1842 let mut file = self.file_lock()?;
1843 if self.config.read_only {
1844 for (id, page) in pager_io::read_dwb_pages(&mut *file)? {
1845 self.install_read_only_page(id, page)?;
1849 }
1850 return Ok(());
1851 }
1852 Ok(pager_io::recover_from_dwb(&mut *file)?)
1853 }
1854
1855 pub fn restore_read_only_page(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
1859 if !self.config.read_only || page_id == 0 || page.page_id() != page_id {
1860 return Err(PagerError::InvalidDatabase(
1861 "invalid read-only recovery image".into(),
1862 ));
1863 }
1864 page.verify_checksum()?;
1865 self.install_read_only_page(page_id, page)
1866 }
1867
1868 fn install_read_only_page(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
1869 let mut images = self
1870 .read_only_recovery
1871 .write()
1872 .map_err(|_| PagerError::LockPoisoned)?;
1873 let limit = self
1874 .config
1875 .cache_size
1876 .max(pager_io::DWB_MAX_PAGES_PER_FRAME);
1877 if images.len() >= limit && !images.contains_key(&page_id) {
1878 return Err(PagerError::InvalidDatabase(
1879 "read-only recovery exceeds the page cache budget".into(),
1880 ));
1881 }
1882 images.insert(page_id, page);
1883 Ok(())
1884 }
1885
1886 fn complete_allocation_fence(&self) -> Result<(), PagerError> {
1890 if self
1891 .allocation_fence_pending
1892 .load(std::sync::atomic::Ordering::Acquire)
1893 {
1894 self.write_header_and_sync()?;
1895 self.write_header_and_sync()?;
1896 self.allocation_fence_pending
1897 .store(false, std::sync::atomic::Ordering::Release);
1898 }
1899 Ok(())
1900 }
1901
1902 pub fn write_header_and_sync(&self) -> Result<(), PagerError> {
1904 let _ownership = self.lock_ownership_mutations();
1905 self.write_header()?;
1906 let file = self.file_lock()?;
1907 file.sync_all()?;
1908 Ok(())
1909 }
1910
1911 pub fn set_checkpoint_in_progress(
1913 &self,
1914 in_progress: bool,
1915 target_lsn: u64,
1916 ) -> Result<(), PagerError> {
1917 let _ownership = self.lock_ownership_mutations();
1918 let mut header = self.header_write()?;
1919 header.checkpoint_in_progress = in_progress;
1920 header.checkpoint_target_lsn = target_lsn;
1921 *self.header_dirty_lock()? = true;
1922 drop(header);
1923 self.write_header_and_sync()
1924 }
1925
1926 pub fn complete_checkpoint(&self, lsn: u64) -> Result<(), PagerError> {
1928 let _ownership = self.lock_ownership_mutations();
1929 let mut header = self.header_write()?;
1930 header.checkpoint_lsn = lsn;
1931 header.checkpoint_in_progress = false;
1932 header.checkpoint_target_lsn = 0;
1933 *self.header_dirty_lock()? = true;
1934 drop(header);
1935 self.write_header_and_sync()
1936 }
1937}
1938
1939#[cfg(test)]
1940mod integrity_guidance_tests {
1941 use super::PagerError;
1942 use std::path::PathBuf;
1943
1944 #[test]
1945 fn damaged_paged_zones_recommend_preserving_files_and_a_verified_backup() {
1946 for error in [
1947 PagerError::SuperblockZoneUnrecoverable(PathBuf::from("data.rdb")),
1948 PagerError::ManifestZoneCorrupt(PathBuf::from("data.rdb")),
1949 PagerError::DwbZoneCorrupt(PathBuf::from("data.rdb")),
1950 ] {
1951 let message = error.to_string();
1952 assert!(message.contains("Preserve the database"), "{message}");
1953 assert!(message.contains("verified backup"), "{message}");
1954 assert!(!message.contains("salvage"), "{message}");
1955 assert!(!message.contains("SCRUB"), "{message}");
1956 }
1957 }
1958}