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 freelist: RwLock<FreeList>,
241 deferred_reclamation: Mutex<DeferredPageReclamation>,
244 ownership_mutations: parking_lot::ReentrantMutex<()>,
247 allocation_reused: std::sync::atomic::AtomicBool,
250 allocation_fence_pending: std::sync::atomic::AtomicBool,
252 header: RwLock<DatabaseHeader>,
254 superblock_publication: Mutex<()>,
256 config: PagerConfig,
258 header_dirty: Mutex<bool>,
260 wal: W,
261}
262
263#[derive(Default)]
264struct DeferredPageReclamation {
265 enabled: bool,
266 pending: std::collections::BTreeSet<u32>,
267 published: std::collections::BTreeSet<u32>,
268}
269
270const SUPERBLOCK_SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
272const SUPERBLOCK_ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
273const PHASE3_DWB_ZONE_FILE_VERSION: u32 = 0x0001_0001;
276
277pub type NewestSuperblock = (
279 reddb_file::PagedSuperblockSelection,
280 [u8; SUPERBLOCK_SLOT_SIZE],
281);
282
283fn superblock_slot_image(page: &[u8; PAGE_SIZE]) -> [u8; SUPERBLOCK_SLOT_SIZE] {
286 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
287 image.copy_from_slice(&page[..SUPERBLOCK_SLOT_SIZE]);
288 image
289}
290
291impl<F: VfsFile, W: WalBarrier> Pager<F, W> {
292 pub fn open_owned(
296 file: F,
297 path: PathBuf,
298 config: PagerConfig,
299 wal: W,
300 create: bool,
301 ) -> Result<Self, PagerError> {
302 if create && file.file_len()? != 0 {
303 return Err(PagerError::InvalidDatabase(
304 "new pager file is not empty".into(),
305 ));
306 }
307 let pager = Self {
308 path,
309 file: Mutex::new(file),
310 dwb_enabled: config.double_write && !config.read_only,
311 writeback_failed: std::sync::atomic::AtomicBool::new(false),
312 cache: PageCache::new(config.cache_size),
313 freelist: RwLock::new(FreeList::new()),
314 deferred_reclamation: Mutex::new(DeferredPageReclamation::default()),
315 ownership_mutations: parking_lot::ReentrantMutex::new(()),
316 allocation_reused: std::sync::atomic::AtomicBool::new(false),
317 allocation_fence_pending: std::sync::atomic::AtomicBool::new(false),
318 header: RwLock::new(DatabaseHeader::default()),
319 superblock_publication: Mutex::new(()),
320 config,
321 header_dirty: Mutex::new(false),
322 wal,
323 };
324 if create {
325 pager.initialize()?;
326 } else {
327 pager.ensure_phase3_dwb_zone_layout()?;
328 pager.recover_from_dwb()?;
329 pager.load_header()?;
330 }
331 Ok(pager)
332 }
333
334 pub fn wal_barrier(&self) -> &W {
335 &self.wal
336 }
337
338 pub fn lock_ownership_mutations(&self) -> parking_lot::ReentrantMutexGuard<'_, ()> {
341 self.ownership_mutations.lock()
342 }
343
344 pub fn fence_interrupted_ownership(&self) {
346 self.writeback_failed
347 .store(true, std::sync::atomic::Ordering::Release);
348 }
349
350 #[doc(hidden)]
353 pub fn with_file<T>(&self, operation: impl FnOnce(&mut F) -> T) -> Result<T, PagerError> {
354 Ok(operation(&mut *self.file_lock()?))
355 }
356
357 fn initialize(&self) -> Result<(), PagerError> {
359 if self.config.read_only {
360 return Err(PagerError::ReadOnly);
361 }
362
363 let initial_page_count = FIRST_ALLOCATABLE_PAGE;
367 let header_page = Page::new_header_page(initial_page_count);
368 self.header_write()?.page_count = initial_page_count;
369
370 self.write_page_raw(0, &header_page)?;
375 let mut metadata_page = Page::new(PageType::Header, 1);
376 metadata_page.update_checksum();
377 self.write_page_raw(1, &metadata_page)?;
378 let mut vault_page = Page::new(PageType::Vault, 2);
379 vault_page.update_checksum();
380 self.write_page_raw(2, &vault_page)?;
381 self.clear_dwb_zone()?;
382
383 let mut slot = superblock_slot_image(header_page.as_bytes());
387 self.write_superblock_slot(&mut slot, 0, 1)?;
388 self.write_superblock_slot(&mut slot, 1, 2)?;
389
390 self.sync()?;
392
393 Ok(())
394 }
395
396 fn header_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, DatabaseHeader>, PagerError> {
398 self.header.write().map_err(|_| PagerError::LockPoisoned)
399 }
400
401 fn header_read(&self) -> Result<std::sync::RwLockReadGuard<'_, DatabaseHeader>, PagerError> {
403 self.header.read().map_err(|_| PagerError::LockPoisoned)
404 }
405
406 fn freelist_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, FreeList>, PagerError> {
408 self.freelist.write().map_err(|_| PagerError::LockPoisoned)
409 }
410
411 fn file_lock(&self) -> Result<std::sync::MutexGuard<'_, F>, PagerError> {
413 self.file.lock().map_err(|_| PagerError::LockPoisoned)
414 }
415
416 fn header_dirty_lock(&self) -> Result<std::sync::MutexGuard<'_, bool>, PagerError> {
418 self.header_dirty
419 .lock()
420 .map_err(|_| PagerError::LockPoisoned)
421 }
422
423 fn read_superblock_zone(&self) -> Result<[u8; SUPERBLOCK_ZONE_SIZE], PagerError> {
427 let mut file = self.file_lock()?;
428 Ok(pager_io::read_superblock_zone(&mut *file)?)
429 }
430
431 #[doc(hidden)]
437 pub fn newest_superblock(&self) -> Result<NewestSuperblock, PagerError> {
438 let zone = self.read_superblock_zone()?;
439 let selection = reddb_file::select_paged_superblock(&zone)
440 .ok_or_else(|| PagerError::SuperblockZoneUnrecoverable(self.path.clone()))?;
441
442 let start = selection.copy_index * SUPERBLOCK_SLOT_SIZE;
443 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
444 image.copy_from_slice(&zone[start..start + SUPERBLOCK_SLOT_SIZE]);
445 Ok((selection, image))
446 }
447
448 fn ensure_phase3_dwb_zone_layout(&self) -> Result<(), PagerError> {
449 let (_, image) = self.newest_superblock()?;
450 let header = reddb_file::decode_database_header(&image)
451 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
452 if header.version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
453 && !self.config.disk_residency
454 && !self.config.read_only
455 {
456 return Err(PagerError::InvalidDatabase(
457 "prepared disk-resident database requires a disk-resident writer".into(),
458 ));
459 }
460 if header.version < PHASE3_DWB_ZONE_FILE_VERSION {
461 return Err(PagerError::LegacyPagedStore {
462 path: self.path.clone(),
463 });
464 }
465 Ok(())
466 }
467
468 fn write_superblock_slot(
471 &self,
472 slot: &mut [u8; SUPERBLOCK_SLOT_SIZE],
473 copy_index: usize,
474 generation: u64,
475 ) -> Result<(), PagerError> {
476 self.ensure_writeback_available()?;
477 if self.config.read_only {
478 return Err(PagerError::ReadOnly);
479 }
480 let mut file = self.file_lock()?;
481 self.ensure_writeback_available()?;
482 Ok(pager_io::write_superblock_slot(
483 &mut *file, slot, copy_index, generation,
484 )?)
485 }
486
487 #[doc(hidden)]
493 pub fn commit_superblock(
494 &self,
495 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
496 ) -> Result<(), PagerError> {
497 let _ownership = self.lock_ownership_mutations();
498 let _publication = self
499 .superblock_publication
500 .lock()
501 .map_err(|_| PagerError::LockPoisoned)?;
502 self.commit_superblock_locked(mutate)
503 }
504
505 fn commit_superblock_locked(
506 &self,
507 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
508 ) -> Result<(), PagerError> {
509 if self.config.read_only {
510 return Err(PagerError::ReadOnly);
511 }
512 let (newest, mut image) = self.newest_superblock()?;
513 mutate(&mut image)?;
514 let next_copy = reddb_file::paged_superblock_next_copy(Some(newest));
515 self.write_superblock_slot(&mut image, next_copy, newest.generation.saturating_add(1))
516 }
517
518 pub fn fence_file_format_version(&self, version: u32) -> Result<(), PagerError> {
524 let _ownership = self.lock_ownership_mutations();
525 self.ensure_writeback_available()?;
526 if self.config.read_only {
527 return Err(PagerError::ReadOnly);
528 }
529 if version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
530 && !self.config.disk_residency
531 {
532 return Err(PagerError::InvalidDatabase(
533 "disk-residency format requires a disk-resident writer".into(),
534 ));
535 }
536 if !(PHASE3_DWB_ZONE_FILE_VERSION..=reddb_file::PAGE_FILE_VERSION).contains(&version) {
537 return Err(PagerError::InvalidDatabase(
538 "unsupported file format fence".into(),
539 ));
540 }
541 let _publication = self
542 .superblock_publication
543 .lock()
544 .map_err(|_| PagerError::LockPoisoned)?;
545 if version < self.header_read()?.version {
546 return Err(PagerError::InvalidDatabase(
547 "file format downgrade is not supported".into(),
548 ));
549 }
550 let result = (|| {
551 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
552 self.commit_superblock_locked(|image| {
553 reddb_file::set_database_header_version(image, version)
554 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
555 })?;
556 }
557 self.header_write()?.version = version;
558 Ok(())
559 })();
560 if result.is_err() {
561 self.writeback_failed
562 .store(true, std::sync::atomic::Ordering::Release);
563 }
564 result
565 }
566
567 fn load_header(&self) -> Result<(), PagerError> {
569 let (_, image) = self.newest_superblock()?;
570
571 let mut decoded_header = reddb_file::decode_database_header(&image)
572 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
573 let physical_pages = self.file_lock()?.file_len()?.div_ceil(PAGE_SIZE as u64);
579 let physical_pages = u32::try_from(physical_pages).map_err(|_| {
580 PagerError::InvalidDatabase("data file exceeds the page address space".into())
581 })?;
582 if decoded_header.page_count < physical_pages {
583 decoded_header.page_count = physical_pages;
584 if !self.config.read_only {
585 *self.header_dirty_lock()? = true;
586 }
587 }
588 let freelist_head = decoded_header.freelist_head;
589
590 {
591 let mut header = self.header_write()?;
592 *header = decoded_header;
593 }
594
595 {
597 let mut freelist = self.freelist_write()?;
598 *freelist = FreeList::from_header(freelist_head, 0);
599 }
600
601 Ok(())
602 }
603
604 fn write_header(&self) -> Result<(), PagerError> {
606 if self.config.read_only {
607 return Err(PagerError::ReadOnly);
608 }
609
610 let header = self.header_read()?.clone();
611 self.commit_superblock(|image| {
612 reddb_file::encode_database_header(image, &header)
613 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
614 })?;
615 *self.header_dirty_lock()? = false;
616
617 Ok(())
618 }
619
620 fn read_page_raw(&self, page_id: u32) -> Result<Page, PagerError> {
622 let mut file = self.file_lock()?;
623 Ok(pager_io::read_page(
624 &mut *file,
625 page_id,
626 self.config.verify_checksums,
627 )?)
628 }
629
630 fn ensure_writeback_available(&self) -> Result<(), PagerError> {
631 if self
632 .writeback_failed
633 .load(std::sync::atomic::Ordering::Acquire)
634 {
635 return Err(PagerError::Io(std::io::Error::other(
636 "page writeback failed; reopen the database to recover before writing",
637 )));
638 }
639 Ok(())
640 }
641
642 fn record_ownership(
643 &self,
644 operation: impl FnOnce(&W) -> std::io::Result<()>,
645 ) -> Result<(), PagerError> {
646 if let Err(error) = operation(&self.wal) {
647 self.writeback_failed
648 .store(true, std::sync::atomic::Ordering::Release);
649 return Err(PagerError::Io(error));
650 }
651 Ok(())
652 }
653
654 fn write_evicted_dirty_page(
657 &self,
658 page: Page,
659 pending_page: Option<u32>,
660 ) -> Result<(), PagerError> {
661 let mut ids = self.cache.dirty_page_ids(pager_io::DWB_MAX_PAGES_PER_FRAME);
662 ids.retain(|id| Some(*id) != pending_page);
666 ids.truncate(pager_io::DWB_MAX_PAGES_PER_FRAME.saturating_sub(1));
669 ids.sort_unstable();
670 let mut pages = self.cache.flush_dirty_pages(&ids);
671 pages.push((page.page_id(), page));
672 pages.sort_unstable_by_key(|(id, _)| *id);
673 self.write_dirty_page_batch(&pages)
674 }
675
676 fn write_page_raw(&self, page_id: u32, page: &Page) -> Result<(), PagerError> {
678 self.ensure_writeback_available()?;
679 if self.config.read_only {
680 return Err(PagerError::ReadOnly);
681 }
682 let mut file = self.file_lock()?;
683 self.ensure_writeback_available()?;
684 Ok(pager_io::write_page(&mut *file, page_id, page)?)
685 }
686
687 pub fn read_page(&self, page_id: u32) -> Result<Page, PagerError> {
689 if let Some(page) = self.cache.get(page_id) {
691 return Ok(page);
692 }
693
694 let _ownership = self.lock_ownership_mutations();
698 if let Some(page) = self.cache.get(page_id) {
699 return Ok(page);
700 }
701
702 let page = self.read_page_raw(page_id)?;
704
705 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
707 self.write_evicted_dirty_page(dirty_page, None)?;
709 }
710
711 Ok(page)
712 }
713
714 pub fn read_page_no_checksum(&self, page_id: u32) -> Result<Page, PagerError> {
719 if let Some(page) = self.cache.get(page_id) {
721 return Ok(page);
722 }
723
724 let _ownership = self.lock_ownership_mutations();
725 if let Some(page) = self.cache.get(page_id) {
726 return Ok(page);
727 }
728
729 let mut file = self.file_lock()?;
731 let page = pager_io::read_page(&mut *file, page_id, false)?;
732 drop(file);
733
734 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
736 self.write_evicted_dirty_page(dirty_page, None)?;
738 }
739
740 Ok(page)
741 }
742
743 pub fn write_page(&self, page_id: u32, mut page: Page) -> Result<(), PagerError> {
745 let _ownership = self.lock_ownership_mutations();
746 self.ensure_writeback_available()?;
747 if self.config.read_only {
748 return Err(PagerError::ReadOnly);
749 }
750
751 self.capture_page_before_write(page_id)?;
752
753 page.update_checksum();
755
756 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
761 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
762 }
763
764 Ok(())
765 }
766
767 pub fn write_page_no_checksum(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
772 let _ownership = self.lock_ownership_mutations();
773 self.ensure_writeback_available()?;
774 if self.config.read_only {
775 return Err(PagerError::ReadOnly);
776 }
777
778 self.capture_page_before_write(page_id)?;
779
780 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
783 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
784 }
785
786 Ok(())
787 }
788
789 fn capture_page_before_write(&self, page_id: u32) -> Result<(), PagerError> {
790 let mut required = false;
791 self.record_ownership(|wal| {
792 required = wal.needs_page_image(page_id)?;
793 Ok(())
794 })?;
795 if required {
796 let previous = self.read_page(page_id)?;
797 self.record_ownership(|wal| wal.capture_page_image(&previous))?;
798 }
799 Ok(())
800 }
801
802 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, PagerError> {
804 let _ownership = self.lock_ownership_mutations();
805 self.ensure_writeback_available()?;
806 if self.config.read_only {
807 return Err(PagerError::ReadOnly);
808 }
809
810 let page_id = {
812 let mut freelist = self.freelist_write()?;
813 self.complete_allocation_fence()?;
816 if let Some(id) = freelist.next_in_memory_page() {
817 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
818 let id = freelist
819 .allocate()
820 .expect("free page checked while allocator is locked");
821 self.allocation_reused
822 .store(true, std::sync::atomic::Ordering::Release);
823 id
824 } else if freelist.trunk_head() != 0 {
825 let trunk_id = freelist.trunk_head();
826
827 let trunk = self.read_page(trunk_id).map_err(|e| match e {
828 PagerError::PageNotFound(_) => {
829 PagerError::InvalidDatabase("Freelist trunk missing".to_string())
830 }
831 other => other,
832 })?;
833 if trunk.page_id() != trunk_id {
834 return Err(PagerError::InvalidDatabase(
835 "misdirected freelist trunk".into(),
836 ));
837 }
838
839 let mut detached = FreeList::new();
842 detached
843 .load_from_trunk(&trunk)
844 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {e}")))?;
845 let mut pages = Vec::new();
846 let mut seen = std::collections::HashSet::new();
847 let page_count = self.page_count()?;
848 while let Some(id) = detached.allocate() {
849 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count || !seen.insert(id) {
850 return Err(PagerError::InvalidDatabase(
851 "invalid freelist trunk page".into(),
852 ));
853 }
854 pages.push(id);
855 }
856 let mut journaled = false;
857 self.record_ownership(|wal| {
858 journaled = wal.has_page_ownership_journal()?;
859 Ok(())
860 })?;
861 if !journaled {
862 self.allocate_unjournaled_trunk(&mut freelist, trunk, &pages)?
863 } else {
864 self.record_ownership(|wal| wal.detach_free_pages(&pages))?;
865
866 freelist
867 .load_from_trunk(&trunk)
868 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {}", e)))?;
869 let mut header = self.header_write()?;
870 header.freelist_head = freelist.trunk_head();
871 *self.header_dirty_lock()? = true;
872 drop(header);
873 self.allocation_fence_pending
878 .store(true, std::sync::atomic::Ordering::Release);
879 self.complete_allocation_fence()?;
880 let next = freelist.next_in_memory_page().ok_or_else(|| {
881 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
882 })?;
883 self.record_ownership(|wal| wal.reserve_pages(next, 1))?;
884 let id = freelist.allocate().ok_or_else(|| {
885 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
886 })?;
887 self.allocation_reused
888 .store(true, std::sync::atomic::Ordering::Release);
889 id
890 }
891 } else {
892 let mut header = self.header_write()?;
894 let id = header.page_count;
895 let next = id
896 .checked_add(1)
897 .ok_or_else(|| PagerError::InvalidDatabase("page count overflow".into()))?;
898 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
899 header.page_count = next;
900 *self.header_dirty_lock()? = true;
901 id
902 }
903 };
904
905 let page = Page::new(page_type, page_id);
906
907 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page.clone()) {
919 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
920 }
921
922 Ok(page)
923 }
924
925 fn allocate_unjournaled_trunk(
929 &self,
930 freelist: &mut FreeList,
931 mut trunk: Page,
932 pages: &[u32],
933 ) -> Result<u32, PagerError> {
934 let id = *pages
935 .first()
936 .expect("validated trunk includes its own page");
937 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
938 if pages.len() > 1 {
939 let remaining =
940 u32::try_from(pages.len() - 2).expect("a validated trunk fits in one page");
941 trunk.content_mut()[4..8].copy_from_slice(&remaining.to_le_bytes());
942 trunk.update_checksum();
943 self.cache.remove(trunk.page_id());
944 self.write_pages_through_dwb(&[(trunk.page_id(), trunk)])?;
945 freelist.note_durable_trunk_allocation(remaining);
946 } else {
947 freelist
950 .load_from_trunk(&trunk)
951 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
952 self.header_write()?.freelist_head = freelist.trunk_head();
953 *self.header_dirty_lock()? = true;
954 self.allocation_fence_pending
955 .store(true, std::sync::atomic::Ordering::Release);
956 self.complete_allocation_fence()?;
957 assert_eq!(freelist.allocate(), Some(id));
958 }
959 self.allocation_reused
960 .store(true, std::sync::atomic::Ordering::Release);
961 Ok(id)
962 }
963
964 pub fn reserve_contig_extent(&self, n_pages: u32) -> Result<ExtentId, PagerError> {
966 let _ownership = self.lock_ownership_mutations();
967 self.ensure_writeback_available()?;
968 if self.config.read_only {
969 return Err(PagerError::ReadOnly);
970 }
971 if n_pages == 0 {
972 return Err(PagerError::InvalidDatabase(
973 "contiguous extent must reserve at least one page".to_string(),
974 ));
975 }
976
977 let start_page = {
978 let mut header = self.header_write()?;
979 let start = header.page_count;
980 let end = header.page_count.checked_add(n_pages).ok_or_else(|| {
981 PagerError::InvalidDatabase("contiguous extent page count overflow".to_string())
982 })?;
983 self.record_ownership(|wal| wal.reserve_pages(start, n_pages))?;
984 header.page_count = end;
985 *self.header_dirty_lock()? = true;
986 start
987 };
988
989 for page_id in start_page..start_page + n_pages {
990 let mut page = Page::new(PageType::Vector, page_id);
991 page.update_checksum();
992 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
993 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
994 }
995 }
996
997 Ok(ExtentId {
998 start_page,
999 n_pages,
1000 })
1001 }
1002
1003 pub fn free_page(&self, page_id: u32) -> Result<(), PagerError> {
1005 let _ownership = self.lock_ownership_mutations();
1006 self.ensure_writeback_available()?;
1007 if self.config.read_only {
1008 return Err(PagerError::ReadOnly);
1009 }
1010
1011 let mut deferred = self
1012 .deferred_reclamation
1013 .lock()
1014 .map_err(|_| PagerError::LockPoisoned)?;
1015 if deferred.enabled {
1016 if page_id < FIRST_ALLOCATABLE_PAGE || page_id >= self.page_count()? {
1017 return Err(PagerError::InvalidDatabase(
1018 "invalid deferred free page".into(),
1019 ));
1020 }
1021 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1022 deferred.pending.insert(page_id);
1025 return Ok(());
1026 }
1027 drop(deferred);
1028
1029 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1030
1031 self.cache.remove(page_id);
1033
1034 let mut freelist = self.freelist_write()?;
1036 freelist.free(page_id);
1037
1038 *self.header_dirty_lock()? = true;
1039
1040 Ok(())
1041 }
1042
1043 pub fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
1044 let _ownership = self.lock_ownership_mutations();
1045 self.deferred_reclamation
1046 .lock()
1047 .map_err(|_| PagerError::LockPoisoned)?
1048 .enabled = true;
1049 Ok(())
1050 }
1051
1052 pub fn deferred_reclamation_pages(&self) -> Result<(Vec<u32>, Vec<u32>), PagerError> {
1053 let deferred = self
1054 .deferred_reclamation
1055 .lock()
1056 .map_err(|_| PagerError::LockPoisoned)?;
1057 Ok((
1058 deferred.pending.iter().copied().collect(),
1059 deferred.published.iter().copied().collect(),
1060 ))
1061 }
1062
1063 pub fn restore_deferred_reclamation(&self, pages: &[u32]) -> Result<(), PagerError> {
1064 let _ownership = self.lock_ownership_mutations();
1065 let count = self.page_count()?;
1066 if pages
1067 .iter()
1068 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= count)
1069 {
1070 return Err(PagerError::InvalidDatabase(
1071 "invalid persisted deferred free page".into(),
1072 ));
1073 }
1074 let mut deferred = self
1075 .deferred_reclamation
1076 .lock()
1077 .map_err(|_| PagerError::LockPoisoned)?;
1078 deferred.pending.extend(pages);
1079 deferred.published.extend(pages);
1080 Ok(())
1081 }
1082
1083 pub fn complete_deferred_publication(
1086 &self,
1087 retained: &[u32],
1088 released: &[u32],
1089 ) -> Result<(), PagerError> {
1090 let _ownership = self.lock_ownership_mutations();
1091 let mut deferred = self
1092 .deferred_reclamation
1093 .lock()
1094 .map_err(|_| PagerError::LockPoisoned)?;
1095 if !released.is_empty() {
1096 self.free_retired_pages(released)?;
1097 }
1098 for id in released {
1099 deferred.pending.remove(id);
1100 }
1101 deferred.published = retained.iter().copied().collect();
1102 Ok(())
1103 }
1104
1105 pub fn record_retirement_intent(&self, pages: &[u32]) -> Result<(), PagerError> {
1107 let _ownership = self.lock_ownership_mutations();
1108 self.ensure_writeback_available()?;
1109 if self.config.read_only {
1110 return Err(PagerError::ReadOnly);
1111 }
1112 let page_count = self.page_count()?;
1113 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1114 if pages
1115 .iter()
1116 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !seen.insert(*id))
1117 {
1118 return Err(PagerError::InvalidDatabase(
1119 "invalid retirement intent".into(),
1120 ));
1121 }
1122 self.record_ownership(|wal| wal.retire_pages(pages))
1123 }
1124
1125 pub fn free_retired_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1128 let _ownership = self.lock_ownership_mutations();
1129 self.ensure_writeback_available()?;
1130 if self.config.read_only {
1131 return Err(PagerError::ReadOnly);
1132 }
1133 let page_count = self.page_count()?;
1134 if pages
1135 .iter()
1136 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count)
1137 {
1138 return Err(PagerError::InvalidDatabase(
1139 "invalid retired page range".into(),
1140 ));
1141 }
1142 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1143 if pages.iter().any(|id| !seen.insert(*id)) {
1144 return Err(PagerError::InvalidDatabase("duplicate retired page".into()));
1145 }
1146 let mut freelist = self.freelist_write()?;
1147 if freelist.contains_any(&seen) {
1148 return Err(PagerError::InvalidDatabase(
1149 "retired page is already free".into(),
1150 ));
1151 }
1152 let mut trunk_id = freelist.trunk_head();
1156 let mut visited = std::collections::HashSet::new();
1157 while trunk_id != 0 && !seen.is_empty() {
1158 if trunk_id < FIRST_ALLOCATABLE_PAGE
1159 || trunk_id >= page_count
1160 || !visited.insert(trunk_id)
1161 {
1162 return Err(PagerError::InvalidDatabase(
1163 "invalid freelist trunk chain".into(),
1164 ));
1165 }
1166 let trunk = self.read_page(trunk_id)?;
1167 if trunk.page_id() != trunk_id {
1168 return Err(PagerError::InvalidDatabase(
1169 "misdirected freelist trunk".into(),
1170 ));
1171 }
1172 let mut loaded = FreeList::new();
1173 let next = loaded
1174 .load_from_trunk(&trunk)
1175 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1176 while let Some(id) = loaded.allocate() {
1177 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1178 return Err(PagerError::InvalidDatabase(
1179 "invalid free page range".into(),
1180 ));
1181 }
1182 if seen.contains(&id) {
1183 return Err(PagerError::InvalidDatabase(
1184 "retired page is already free".into(),
1185 ));
1186 }
1187 }
1188 trunk_id = next.unwrap_or(0);
1189 }
1190 self.record_ownership(|wal| wal.retire_pages(pages))?;
1191 for id in pages {
1192 self.cache.remove(*id);
1193 }
1194 freelist.free_batch(pages);
1195 *self.header_dirty_lock()? = true;
1196 Ok(())
1197 }
1198
1199 fn pages_not_in_freelist(
1200 &self,
1201 pages: &[u32],
1202 freelist: &FreeList,
1203 page_count: u32,
1204 ) -> Result<std::collections::HashSet<u32>, PagerError> {
1205 let mut remaining = std::collections::HashSet::with_capacity(pages.len());
1206 if pages
1207 .iter()
1208 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !remaining.insert(*id))
1209 {
1210 return Err(PagerError::InvalidDatabase(
1211 "invalid recovery page set".into(),
1212 ));
1213 }
1214 let requested = remaining.clone();
1215 freelist
1216 .remove_existing_from(&requested, &mut remaining)
1217 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1218 let mut trunk_id = freelist.trunk_head();
1219 let mut visited = std::collections::HashSet::new();
1220 while trunk_id != 0 {
1221 if trunk_id < FIRST_ALLOCATABLE_PAGE
1222 || trunk_id >= page_count
1223 || !visited.insert(trunk_id)
1224 {
1225 return Err(PagerError::InvalidDatabase(
1226 "invalid freelist trunk chain".into(),
1227 ));
1228 }
1229 let trunk = self.read_page(trunk_id)?;
1230 if trunk.page_id() != trunk_id {
1231 return Err(PagerError::InvalidDatabase(
1232 "misdirected freelist trunk".into(),
1233 ));
1234 }
1235 let mut loaded = FreeList::new();
1236 let next = loaded
1237 .load_from_trunk(&trunk)
1238 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1239 while let Some(id) = loaded.allocate() {
1240 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1241 return Err(PagerError::InvalidDatabase(
1242 "invalid free page range".into(),
1243 ));
1244 }
1245 if requested.contains(&id) && !remaining.remove(&id) {
1246 return Err(PagerError::InvalidDatabase(
1247 "duplicate recovery page".into(),
1248 ));
1249 }
1250 }
1251 trunk_id = next.unwrap_or(0);
1252 }
1253 Ok(remaining)
1254 }
1255
1256 pub fn recover_unowned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1260 let _ownership = self.lock_ownership_mutations();
1261 self.ensure_writeback_available()?;
1262 if self.config.read_only {
1263 return Err(PagerError::ReadOnly);
1264 }
1265 let page_count = self.page_count()?;
1266 let mut freelist = self.freelist_write()?;
1267 let remaining = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1268 if !remaining.is_empty() {
1269 let mut missing: Vec<_> = remaining.into_iter().collect();
1270 missing.sort_unstable();
1271 for id in &missing {
1272 self.cache.remove(*id);
1273 }
1274 freelist.free_batch(&missing);
1275 *self.header_dirty_lock()? = true;
1276 }
1277 Ok(())
1278 }
1279
1280 pub fn validate_owned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1283 let page_count = self.page_count()?;
1284 let freelist = self.freelist_write()?;
1285 let absent = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1286 if absent.len() != pages.len() {
1287 return Err(PagerError::InvalidDatabase(
1288 "owned recovery page is free".into(),
1289 ));
1290 }
1291 Ok(())
1292 }
1293
1294 pub fn sync_recovered_allocator(&self) -> Result<(), PagerError> {
1297 let _ownership = self.lock_ownership_mutations();
1298 let result = (|| {
1299 self.sync()?;
1300 let _publication = self
1301 .superblock_publication
1302 .lock()
1303 .map_err(|_| PagerError::LockPoisoned)?;
1304 let header = self.header_read()?.clone();
1305 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1306 self.commit_superblock_locked(|image| {
1307 reddb_file::encode_database_header(image, &header)
1308 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
1309 })?;
1310 }
1311 Ok(())
1312 })();
1313 if result.is_err() {
1314 self.writeback_failed
1315 .store(true, std::sync::atomic::Ordering::Release);
1316 }
1317 result
1318 }
1319
1320 pub fn free_page_count(&self) -> Result<u32, PagerError> {
1323 Ok(self.freelist_write()?.total_free())
1324 }
1325
1326 pub fn header(&self) -> Result<DatabaseHeader, PagerError> {
1327 Ok(self.header_read()?.clone())
1328 }
1329
1330 pub fn physical_header(&self) -> Result<PhysicalFileHeader, PagerError> {
1331 Ok(self.header_read()?.physical)
1332 }
1333
1334 pub fn update_physical_header(&self, physical: PhysicalFileHeader) -> Result<(), PagerError> {
1335 let _ownership = self.lock_ownership_mutations();
1336 self.ensure_writeback_available()?;
1337 if self.config.read_only {
1338 return Err(PagerError::ReadOnly);
1339 }
1340
1341 let mut header = self.header_write()?;
1342 header.physical = physical;
1343 *self.header_dirty_lock()? = true;
1344 Ok(())
1345 }
1346
1347 pub fn page_count(&self) -> Result<u32, PagerError> {
1349 Ok(self.header_read()?.page_count)
1350 }
1351
1352 pub fn flush(&self) -> Result<(), PagerError> {
1354 let _ownership = self.lock_ownership_mutations();
1355 self.ensure_writeback_available()?;
1356 if self.config.read_only {
1357 return Ok(());
1358 }
1359
1360 let trunks = {
1362 let mut freelist = self.freelist_write()?;
1363 if freelist.is_dirty() {
1364 let mut header = self.header_write()?;
1365 let trunks = freelist.flush_to_trunks(0, || {
1366 let id = header.page_count;
1367 header.page_count += 1;
1368 id
1369 });
1370 header.freelist_head = freelist.trunk_head();
1371 *self.header_dirty_lock()? = true;
1372 freelist.mark_clean();
1373 trunks
1374 } else {
1375 Vec::new()
1376 }
1377 };
1378
1379 let mut dirty_pages = Vec::new();
1380 for trunk in trunks {
1381 let page_id = trunk.page_id();
1382 if let Some(evicted) = self.cache.insert_dirty(page_id, trunk) {
1383 dirty_pages.push((evicted.page_id(), evicted));
1386 }
1387 }
1388
1389 const FLUSH_BATCH_PAGES: usize = 256;
1393 for batch in dirty_pages.chunks(FLUSH_BATCH_PAGES) {
1394 self.write_dirty_page_batch(batch)?;
1395 }
1396 drop(dirty_pages);
1397 let mut page_ids = self.cache.dirty_page_ids(usize::MAX);
1401 page_ids.sort_unstable();
1402 for ids in page_ids.chunks(FLUSH_BATCH_PAGES) {
1403 let pages = self.cache.flush_dirty_pages(ids);
1404 self.write_dirty_page_batch(&pages)?;
1405 }
1406
1407 if *self.header_dirty_lock()? {
1409 self.write_header()?;
1410 }
1411
1412 Ok(())
1413 }
1414
1415 fn write_dirty_page_batch(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1416 if pages.is_empty() {
1417 return Ok(());
1418 }
1419 let result = (|| {
1423 let max_lsn = pages
1424 .iter()
1425 .filter_map(|(_, page)| page.header().ok().map(|header| header.lsn))
1426 .max()
1427 .unwrap_or(0);
1428 if max_lsn > 0 {
1429 self.wal.flush_until(max_lsn)?;
1430 }
1431 self.write_pages_through_dwb(pages)
1432 })();
1433 if result.is_err() {
1434 self.writeback_failed
1435 .store(true, std::sync::atomic::Ordering::Release);
1436 for (page_id, _) in pages {
1437 self.cache.mark_dirty(*page_id);
1438 }
1439 }
1440 result
1441 }
1442
1443 pub fn sync(&self) -> Result<(), PagerError> {
1445 let _ownership = self.lock_ownership_mutations();
1446 self.flush()?;
1447
1448 if self
1449 .allocation_reused
1450 .swap(false, std::sync::atomic::Ordering::AcqRel)
1451 {
1452 if let Err(error) = self.write_header_and_sync() {
1456 self.allocation_reused
1457 .store(true, std::sync::atomic::Ordering::Release);
1458 return Err(error);
1459 }
1460 }
1461
1462 let file = self.file_lock()?;
1463 file.sync_all()?;
1464
1465 Ok(())
1466 }
1467
1468 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
1470 self.cache.stats()
1471 }
1472
1473 pub fn cache_len(&self) -> usize {
1476 self.cache.len()
1477 }
1478
1479 pub fn cache_capacity(&self) -> usize {
1482 self.cache.capacity()
1483 }
1484
1485 pub fn dirty_page_count(&self) -> usize {
1487 self.cache.dirty_count()
1488 }
1489
1490 pub fn dirty_fraction(&self) -> f64 {
1494 let capacity = self.cache.capacity().max(1) as f64;
1495 self.cache.dirty_count() as f64 / capacity
1496 }
1497
1498 pub fn flush_some_dirty(&self, max: usize) -> Result<usize, PagerError> {
1502 let _ownership = self.lock_ownership_mutations();
1503 self.ensure_writeback_available()?;
1504 if self.config.read_only || max == 0 {
1505 return Ok(0);
1506 }
1507 let dirty_pages = self.cache.flush_some_dirty(max);
1508 if dirty_pages.is_empty() {
1509 return Ok(0);
1510 }
1511 let count = dirty_pages.len();
1512 for (page_id, page) in dirty_pages {
1519 self.write_page(page_id, page)?;
1520 }
1521 Ok(count)
1522 }
1523
1524 pub fn path(&self) -> &Path {
1526 &self.path
1527 }
1528
1529 pub fn is_read_only(&self) -> bool {
1531 self.config.read_only
1532 }
1533
1534 pub fn file_size(&self) -> Result<u64, PagerError> {
1536 let file = self.file_lock()?;
1537 Ok(file.file_len()?)
1538 }
1539
1540 fn clear_dwb_zone(&self) -> Result<(), PagerError> {
1542 let mut file = self.file_lock()?;
1543 Ok(pager_io::clear_dwb_zone(&mut *file)?)
1544 }
1545
1546 pub fn read_manifest_page(&self) -> Result<Page, PagerError> {
1551 self.read_page(1)
1552 .map_err(|_| PagerError::ManifestZoneCorrupt(self.path.clone()))
1553 }
1554
1555 fn write_pages_through_dwb(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1562 if self.config.read_only {
1563 return Err(PagerError::ReadOnly);
1564 }
1565 let mut file = self.file_lock()?;
1568 self.ensure_writeback_available()?;
1569 let result = pager_io::write_pages(&mut *file, pages, self.dwb_enabled);
1570 if result.is_err() {
1571 self.writeback_failed
1574 .store(true, std::sync::atomic::Ordering::Release);
1575 }
1576 Ok(result?)
1577 }
1578
1579 fn recover_from_dwb(&self) -> Result<(), PagerError> {
1584 let mut file = self.file_lock()?;
1585 Ok(pager_io::recover_from_dwb(&mut *file)?)
1586 }
1587
1588 fn complete_allocation_fence(&self) -> Result<(), PagerError> {
1592 if self
1593 .allocation_fence_pending
1594 .load(std::sync::atomic::Ordering::Acquire)
1595 {
1596 self.write_header_and_sync()?;
1597 self.write_header_and_sync()?;
1598 self.allocation_fence_pending
1599 .store(false, std::sync::atomic::Ordering::Release);
1600 }
1601 Ok(())
1602 }
1603
1604 pub fn write_header_and_sync(&self) -> Result<(), PagerError> {
1606 let _ownership = self.lock_ownership_mutations();
1607 self.write_header()?;
1608 let file = self.file_lock()?;
1609 file.sync_all()?;
1610 Ok(())
1611 }
1612
1613 pub fn set_checkpoint_in_progress(
1615 &self,
1616 in_progress: bool,
1617 target_lsn: u64,
1618 ) -> Result<(), PagerError> {
1619 let _ownership = self.lock_ownership_mutations();
1620 let mut header = self.header_write()?;
1621 header.checkpoint_in_progress = in_progress;
1622 header.checkpoint_target_lsn = target_lsn;
1623 *self.header_dirty_lock()? = true;
1624 drop(header);
1625 self.write_header_and_sync()
1626 }
1627
1628 pub fn complete_checkpoint(&self, lsn: u64) -> Result<(), PagerError> {
1630 let _ownership = self.lock_ownership_mutations();
1631 let mut header = self.header_write()?;
1632 header.checkpoint_lsn = lsn;
1633 header.checkpoint_in_progress = false;
1634 header.checkpoint_target_lsn = 0;
1635 *self.header_dirty_lock()? = true;
1636 drop(header);
1637 self.write_header_and_sync()
1638 }
1639}
1640
1641#[cfg(test)]
1642mod integrity_guidance_tests {
1643 use super::PagerError;
1644 use std::path::PathBuf;
1645
1646 #[test]
1647 fn damaged_paged_zones_recommend_preserving_files_and_a_verified_backup() {
1648 for error in [
1649 PagerError::SuperblockZoneUnrecoverable(PathBuf::from("data.rdb")),
1650 PagerError::ManifestZoneCorrupt(PathBuf::from("data.rdb")),
1651 PagerError::DwbZoneCorrupt(PathBuf::from("data.rdb")),
1652 ] {
1653 let message = error.to_string();
1654 assert!(message.contains("Preserve the database"), "{message}");
1655 assert!(message.contains("verified backup"), "{message}");
1656 assert!(!message.contains("salvage"), "{message}");
1657 assert!(!message.contains("SCRUB"), "{message}");
1658 }
1659 }
1660}