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_RECOVERY_READ_ONLY_FILE_VERSION
453 && !self.config.read_only
454 {
455 return Err(PagerError::InvalidDatabase(
456 "recovered database is read-only; export verified data into a new store".into(),
457 ));
458 }
459 if header.version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
460 && !self.config.disk_residency
461 && !self.config.read_only
462 {
463 return Err(PagerError::InvalidDatabase(
464 "prepared disk-resident database requires a disk-resident writer".into(),
465 ));
466 }
467 if header.version < PHASE3_DWB_ZONE_FILE_VERSION {
468 return Err(PagerError::LegacyPagedStore {
469 path: self.path.clone(),
470 });
471 }
472 Ok(())
473 }
474
475 fn write_superblock_slot(
478 &self,
479 slot: &mut [u8; SUPERBLOCK_SLOT_SIZE],
480 copy_index: usize,
481 generation: u64,
482 ) -> Result<(), PagerError> {
483 self.ensure_writeback_available()?;
484 if self.config.read_only {
485 return Err(PagerError::ReadOnly);
486 }
487 let mut file = self.file_lock()?;
488 self.ensure_writeback_available()?;
489 Ok(pager_io::write_superblock_slot(
490 &mut *file, slot, copy_index, generation,
491 )?)
492 }
493
494 #[doc(hidden)]
500 pub fn commit_superblock(
501 &self,
502 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
503 ) -> Result<(), PagerError> {
504 let _ownership = self.lock_ownership_mutations();
505 let _publication = self
506 .superblock_publication
507 .lock()
508 .map_err(|_| PagerError::LockPoisoned)?;
509 self.commit_superblock_locked(mutate)
510 }
511
512 fn commit_superblock_locked(
513 &self,
514 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
515 ) -> Result<(), PagerError> {
516 if self.config.read_only {
517 return Err(PagerError::ReadOnly);
518 }
519 let (newest, mut image) = self.newest_superblock()?;
520 mutate(&mut image)?;
521 let next_copy = reddb_file::paged_superblock_next_copy(Some(newest));
522 self.write_superblock_slot(&mut image, next_copy, newest.generation.saturating_add(1))
523 }
524
525 pub fn fence_file_format_version(&self, version: u32) -> Result<(), PagerError> {
531 let _ownership = self.lock_ownership_mutations();
532 self.ensure_writeback_available()?;
533 if self.config.read_only {
534 return Err(PagerError::ReadOnly);
535 }
536 if version == reddb_file::file_format::PAGE_RECOVERY_READ_ONLY_FILE_VERSION {
537 return Err(PagerError::InvalidDatabase(
538 "recovery read-only format is reserved".into(),
539 ));
540 }
541 if version >= reddb_file::file_format::PAGE_DISK_RESIDENCY_FILE_VERSION
542 && !self.config.disk_residency
543 {
544 return Err(PagerError::InvalidDatabase(
545 "disk-residency format requires a disk-resident writer".into(),
546 ));
547 }
548 if !(PHASE3_DWB_ZONE_FILE_VERSION..=reddb_file::PAGE_FILE_VERSION).contains(&version) {
549 return Err(PagerError::InvalidDatabase(
550 "unsupported file format fence".into(),
551 ));
552 }
553 let _publication = self
554 .superblock_publication
555 .lock()
556 .map_err(|_| PagerError::LockPoisoned)?;
557 if version < self.header_read()?.version {
558 return Err(PagerError::InvalidDatabase(
559 "file format downgrade is not supported".into(),
560 ));
561 }
562 let result = (|| {
563 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
564 self.commit_superblock_locked(|image| {
565 reddb_file::set_database_header_version(image, version)
566 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
567 })?;
568 }
569 self.header_write()?.version = version;
570 Ok(())
571 })();
572 if result.is_err() {
573 self.writeback_failed
574 .store(true, std::sync::atomic::Ordering::Release);
575 }
576 result
577 }
578
579 fn load_header(&self) -> Result<(), PagerError> {
581 let (_, image) = self.newest_superblock()?;
582
583 let mut decoded_header = reddb_file::decode_database_header(&image)
584 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
585 let physical_pages = self.file_lock()?.file_len()?.div_ceil(PAGE_SIZE as u64);
591 let physical_pages = u32::try_from(physical_pages).map_err(|_| {
592 PagerError::InvalidDatabase("data file exceeds the page address space".into())
593 })?;
594 if decoded_header.page_count < physical_pages {
595 decoded_header.page_count = physical_pages;
596 if !self.config.read_only {
597 *self.header_dirty_lock()? = true;
598 }
599 }
600 let freelist_head = decoded_header.freelist_head;
601
602 {
603 let mut header = self.header_write()?;
604 *header = decoded_header;
605 }
606
607 {
609 let mut freelist = self.freelist_write()?;
610 *freelist = FreeList::from_header(freelist_head, 0);
611 }
612
613 Ok(())
614 }
615
616 fn write_header(&self) -> Result<(), PagerError> {
618 if self.config.read_only {
619 return Err(PagerError::ReadOnly);
620 }
621
622 let header = self.header_read()?.clone();
623 self.commit_superblock(|image| {
624 reddb_file::encode_database_header(image, &header)
625 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
626 })?;
627 *self.header_dirty_lock()? = false;
628
629 Ok(())
630 }
631
632 fn read_page_raw(&self, page_id: u32) -> Result<Page, PagerError> {
634 let mut file = self.file_lock()?;
635 Ok(pager_io::read_page(
636 &mut *file,
637 page_id,
638 self.config.verify_checksums,
639 )?)
640 }
641
642 fn ensure_writeback_available(&self) -> Result<(), PagerError> {
643 if self
644 .writeback_failed
645 .load(std::sync::atomic::Ordering::Acquire)
646 {
647 return Err(PagerError::Io(std::io::Error::other(
648 "page writeback failed; reopen the database to recover before writing",
649 )));
650 }
651 Ok(())
652 }
653
654 fn record_ownership(
655 &self,
656 operation: impl FnOnce(&W) -> std::io::Result<()>,
657 ) -> Result<(), PagerError> {
658 if let Err(error) = operation(&self.wal) {
659 self.writeback_failed
660 .store(true, std::sync::atomic::Ordering::Release);
661 return Err(PagerError::Io(error));
662 }
663 Ok(())
664 }
665
666 fn write_evicted_dirty_page(
669 &self,
670 page: Page,
671 pending_page: Option<u32>,
672 ) -> Result<(), PagerError> {
673 let mut ids = self.cache.dirty_page_ids(pager_io::DWB_MAX_PAGES_PER_FRAME);
674 ids.retain(|id| Some(*id) != pending_page);
678 ids.truncate(pager_io::DWB_MAX_PAGES_PER_FRAME.saturating_sub(1));
681 ids.sort_unstable();
682 let mut pages = self.cache.flush_dirty_pages(&ids);
683 pages.push((page.page_id(), page));
684 pages.sort_unstable_by_key(|(id, _)| *id);
685 self.write_dirty_page_batch(&pages)
686 }
687
688 fn write_page_raw(&self, page_id: u32, page: &Page) -> Result<(), PagerError> {
690 self.ensure_writeback_available()?;
691 if self.config.read_only {
692 return Err(PagerError::ReadOnly);
693 }
694 let mut file = self.file_lock()?;
695 self.ensure_writeback_available()?;
696 Ok(pager_io::write_page(&mut *file, page_id, page)?)
697 }
698
699 pub fn read_page(&self, page_id: u32) -> Result<Page, PagerError> {
701 if let Some(page) = self.cache.get(page_id) {
703 return Ok(page);
704 }
705
706 let _ownership = self.lock_ownership_mutations();
710 if let Some(page) = self.cache.get(page_id) {
711 return Ok(page);
712 }
713
714 let page = self.read_page_raw(page_id)?;
716
717 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
719 self.write_evicted_dirty_page(dirty_page, None)?;
721 }
722
723 Ok(page)
724 }
725
726 pub fn read_page_no_checksum(&self, page_id: u32) -> Result<Page, PagerError> {
731 if let Some(page) = self.cache.get(page_id) {
733 return Ok(page);
734 }
735
736 let _ownership = self.lock_ownership_mutations();
737 if let Some(page) = self.cache.get(page_id) {
738 return Ok(page);
739 }
740
741 let mut file = self.file_lock()?;
743 let page = pager_io::read_page(&mut *file, page_id, false)?;
744 drop(file);
745
746 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
748 self.write_evicted_dirty_page(dirty_page, None)?;
750 }
751
752 Ok(page)
753 }
754
755 pub fn write_page(&self, page_id: u32, mut page: Page) -> Result<(), PagerError> {
757 let _ownership = self.lock_ownership_mutations();
758 self.ensure_writeback_available()?;
759 if self.config.read_only {
760 return Err(PagerError::ReadOnly);
761 }
762
763 self.capture_page_before_write(page_id)?;
764
765 page.update_checksum();
767
768 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
773 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
774 }
775
776 Ok(())
777 }
778
779 pub fn write_page_no_checksum(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
784 let _ownership = self.lock_ownership_mutations();
785 self.ensure_writeback_available()?;
786 if self.config.read_only {
787 return Err(PagerError::ReadOnly);
788 }
789
790 self.capture_page_before_write(page_id)?;
791
792 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
795 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
796 }
797
798 Ok(())
799 }
800
801 fn capture_page_before_write(&self, page_id: u32) -> Result<(), PagerError> {
802 let mut required = false;
803 self.record_ownership(|wal| {
804 required = wal.needs_page_image(page_id)?;
805 Ok(())
806 })?;
807 if required {
808 let previous = self.read_page(page_id)?;
809 self.record_ownership(|wal| wal.capture_page_image(&previous))?;
810 }
811 Ok(())
812 }
813
814 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, PagerError> {
816 let _ownership = self.lock_ownership_mutations();
817 self.ensure_writeback_available()?;
818 if self.config.read_only {
819 return Err(PagerError::ReadOnly);
820 }
821
822 let page_id = {
824 let mut freelist = self.freelist_write()?;
825 self.complete_allocation_fence()?;
828 if let Some(id) = freelist.next_in_memory_page() {
829 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
830 let id = freelist
831 .allocate()
832 .expect("free page checked while allocator is locked");
833 self.allocation_reused
834 .store(true, std::sync::atomic::Ordering::Release);
835 id
836 } else if freelist.trunk_head() != 0 {
837 let trunk_id = freelist.trunk_head();
838
839 let trunk = self.read_page(trunk_id).map_err(|e| match e {
840 PagerError::PageNotFound(_) => {
841 PagerError::InvalidDatabase("Freelist trunk missing".to_string())
842 }
843 other => other,
844 })?;
845 if trunk.page_id() != trunk_id {
846 return Err(PagerError::InvalidDatabase(
847 "misdirected freelist trunk".into(),
848 ));
849 }
850
851 let mut detached = FreeList::new();
854 detached
855 .load_from_trunk(&trunk)
856 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {e}")))?;
857 let mut pages = Vec::new();
858 let mut seen = std::collections::HashSet::new();
859 let page_count = self.page_count()?;
860 while let Some(id) = detached.allocate() {
861 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count || !seen.insert(id) {
862 return Err(PagerError::InvalidDatabase(
863 "invalid freelist trunk page".into(),
864 ));
865 }
866 pages.push(id);
867 }
868 let mut journaled = false;
869 self.record_ownership(|wal| {
870 journaled = wal.has_page_ownership_journal()?;
871 Ok(())
872 })?;
873 if !journaled {
874 self.allocate_unjournaled_trunk(&mut freelist, trunk, &pages)?
875 } else {
876 self.record_ownership(|wal| wal.detach_free_pages(&pages))?;
877
878 freelist
879 .load_from_trunk(&trunk)
880 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {}", e)))?;
881 let mut header = self.header_write()?;
882 header.freelist_head = freelist.trunk_head();
883 *self.header_dirty_lock()? = true;
884 drop(header);
885 self.allocation_fence_pending
890 .store(true, std::sync::atomic::Ordering::Release);
891 self.complete_allocation_fence()?;
892 let next = freelist.next_in_memory_page().ok_or_else(|| {
893 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
894 })?;
895 self.record_ownership(|wal| wal.reserve_pages(next, 1))?;
896 let id = freelist.allocate().ok_or_else(|| {
897 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
898 })?;
899 self.allocation_reused
900 .store(true, std::sync::atomic::Ordering::Release);
901 id
902 }
903 } else {
904 let mut header = self.header_write()?;
906 let id = header.page_count;
907 let next = id
908 .checked_add(1)
909 .ok_or_else(|| PagerError::InvalidDatabase("page count overflow".into()))?;
910 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
911 header.page_count = next;
912 *self.header_dirty_lock()? = true;
913 id
914 }
915 };
916
917 let page = Page::new(page_type, page_id);
918
919 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page.clone()) {
931 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
932 }
933
934 Ok(page)
935 }
936
937 fn allocate_unjournaled_trunk(
941 &self,
942 freelist: &mut FreeList,
943 mut trunk: Page,
944 pages: &[u32],
945 ) -> Result<u32, PagerError> {
946 let id = *pages
947 .first()
948 .expect("validated trunk includes its own page");
949 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
950 if pages.len() > 1 {
951 let remaining =
952 u32::try_from(pages.len() - 2).expect("a validated trunk fits in one page");
953 trunk.content_mut()[4..8].copy_from_slice(&remaining.to_le_bytes());
954 trunk.update_checksum();
955 self.cache.remove(trunk.page_id());
956 self.write_pages_through_dwb(&[(trunk.page_id(), trunk)])?;
957 freelist.note_durable_trunk_allocation(remaining);
958 } else {
959 freelist
962 .load_from_trunk(&trunk)
963 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
964 self.header_write()?.freelist_head = freelist.trunk_head();
965 *self.header_dirty_lock()? = true;
966 self.allocation_fence_pending
967 .store(true, std::sync::atomic::Ordering::Release);
968 self.complete_allocation_fence()?;
969 assert_eq!(freelist.allocate(), Some(id));
970 }
971 self.allocation_reused
972 .store(true, std::sync::atomic::Ordering::Release);
973 Ok(id)
974 }
975
976 pub fn reserve_contig_extent(&self, n_pages: u32) -> Result<ExtentId, 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 if n_pages == 0 {
984 return Err(PagerError::InvalidDatabase(
985 "contiguous extent must reserve at least one page".to_string(),
986 ));
987 }
988
989 let start_page = {
990 let mut header = self.header_write()?;
991 let start = header.page_count;
992 let end = header.page_count.checked_add(n_pages).ok_or_else(|| {
993 PagerError::InvalidDatabase("contiguous extent page count overflow".to_string())
994 })?;
995 self.record_ownership(|wal| wal.reserve_pages(start, n_pages))?;
996 header.page_count = end;
997 *self.header_dirty_lock()? = true;
998 start
999 };
1000
1001 for page_id in start_page..start_page + n_pages {
1002 let mut page = Page::new(PageType::Vector, page_id);
1003 page.update_checksum();
1004 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
1005 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
1006 }
1007 }
1008
1009 Ok(ExtentId {
1010 start_page,
1011 n_pages,
1012 })
1013 }
1014
1015 pub fn free_page(&self, page_id: u32) -> Result<(), PagerError> {
1017 let _ownership = self.lock_ownership_mutations();
1018 self.ensure_writeback_available()?;
1019 if self.config.read_only {
1020 return Err(PagerError::ReadOnly);
1021 }
1022
1023 let mut deferred = self
1024 .deferred_reclamation
1025 .lock()
1026 .map_err(|_| PagerError::LockPoisoned)?;
1027 if deferred.enabled {
1028 if page_id < FIRST_ALLOCATABLE_PAGE || page_id >= self.page_count()? {
1029 return Err(PagerError::InvalidDatabase(
1030 "invalid deferred free page".into(),
1031 ));
1032 }
1033 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1034 deferred.pending.insert(page_id);
1037 return Ok(());
1038 }
1039 drop(deferred);
1040
1041 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1042
1043 self.cache.remove(page_id);
1045
1046 let mut freelist = self.freelist_write()?;
1048 freelist.free(page_id);
1049
1050 *self.header_dirty_lock()? = true;
1051
1052 Ok(())
1053 }
1054
1055 pub fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
1056 let _ownership = self.lock_ownership_mutations();
1057 self.deferred_reclamation
1058 .lock()
1059 .map_err(|_| PagerError::LockPoisoned)?
1060 .enabled = true;
1061 Ok(())
1062 }
1063
1064 pub fn deferred_reclamation_pages(&self) -> Result<(Vec<u32>, Vec<u32>), PagerError> {
1065 let deferred = self
1066 .deferred_reclamation
1067 .lock()
1068 .map_err(|_| PagerError::LockPoisoned)?;
1069 Ok((
1070 deferred.pending.iter().copied().collect(),
1071 deferred.published.iter().copied().collect(),
1072 ))
1073 }
1074
1075 pub fn restore_deferred_reclamation(&self, pages: &[u32]) -> Result<(), PagerError> {
1076 let _ownership = self.lock_ownership_mutations();
1077 let count = self.page_count()?;
1078 if pages
1079 .iter()
1080 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= count)
1081 {
1082 return Err(PagerError::InvalidDatabase(
1083 "invalid persisted deferred free page".into(),
1084 ));
1085 }
1086 let mut deferred = self
1087 .deferred_reclamation
1088 .lock()
1089 .map_err(|_| PagerError::LockPoisoned)?;
1090 deferred.pending.extend(pages);
1091 deferred.published.extend(pages);
1092 Ok(())
1093 }
1094
1095 pub fn complete_deferred_publication(
1098 &self,
1099 retained: &[u32],
1100 released: &[u32],
1101 ) -> Result<(), PagerError> {
1102 let _ownership = self.lock_ownership_mutations();
1103 let mut deferred = self
1104 .deferred_reclamation
1105 .lock()
1106 .map_err(|_| PagerError::LockPoisoned)?;
1107 if !released.is_empty() {
1108 self.free_retired_pages(released)?;
1109 }
1110 for id in released {
1111 deferred.pending.remove(id);
1112 }
1113 deferred.published = retained.iter().copied().collect();
1114 Ok(())
1115 }
1116
1117 pub fn record_retirement_intent(&self, pages: &[u32]) -> Result<(), PagerError> {
1119 let _ownership = self.lock_ownership_mutations();
1120 self.ensure_writeback_available()?;
1121 if self.config.read_only {
1122 return Err(PagerError::ReadOnly);
1123 }
1124 let page_count = self.page_count()?;
1125 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1126 if pages
1127 .iter()
1128 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !seen.insert(*id))
1129 {
1130 return Err(PagerError::InvalidDatabase(
1131 "invalid retirement intent".into(),
1132 ));
1133 }
1134 self.record_ownership(|wal| wal.retire_pages(pages))
1135 }
1136
1137 pub fn free_retired_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1140 let _ownership = self.lock_ownership_mutations();
1141 self.ensure_writeback_available()?;
1142 if self.config.read_only {
1143 return Err(PagerError::ReadOnly);
1144 }
1145 let page_count = self.page_count()?;
1146 if pages
1147 .iter()
1148 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count)
1149 {
1150 return Err(PagerError::InvalidDatabase(
1151 "invalid retired page range".into(),
1152 ));
1153 }
1154 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1155 if pages.iter().any(|id| !seen.insert(*id)) {
1156 return Err(PagerError::InvalidDatabase("duplicate retired page".into()));
1157 }
1158 let mut freelist = self.freelist_write()?;
1159 if freelist.contains_any(&seen) {
1160 return Err(PagerError::InvalidDatabase(
1161 "retired page is already free".into(),
1162 ));
1163 }
1164 let mut trunk_id = freelist.trunk_head();
1168 let mut visited = std::collections::HashSet::new();
1169 while trunk_id != 0 && !seen.is_empty() {
1170 if trunk_id < FIRST_ALLOCATABLE_PAGE
1171 || trunk_id >= page_count
1172 || !visited.insert(trunk_id)
1173 {
1174 return Err(PagerError::InvalidDatabase(
1175 "invalid freelist trunk chain".into(),
1176 ));
1177 }
1178 let trunk = self.read_page(trunk_id)?;
1179 if trunk.page_id() != trunk_id {
1180 return Err(PagerError::InvalidDatabase(
1181 "misdirected freelist trunk".into(),
1182 ));
1183 }
1184 let mut loaded = FreeList::new();
1185 let next = loaded
1186 .load_from_trunk(&trunk)
1187 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1188 while let Some(id) = loaded.allocate() {
1189 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1190 return Err(PagerError::InvalidDatabase(
1191 "invalid free page range".into(),
1192 ));
1193 }
1194 if seen.contains(&id) {
1195 return Err(PagerError::InvalidDatabase(
1196 "retired page is already free".into(),
1197 ));
1198 }
1199 }
1200 trunk_id = next.unwrap_or(0);
1201 }
1202 self.record_ownership(|wal| wal.retire_pages(pages))?;
1203 for id in pages {
1204 self.cache.remove(*id);
1205 }
1206 freelist.free_batch(pages);
1207 *self.header_dirty_lock()? = true;
1208 Ok(())
1209 }
1210
1211 fn pages_not_in_freelist(
1212 &self,
1213 pages: &[u32],
1214 freelist: &FreeList,
1215 page_count: u32,
1216 ) -> Result<std::collections::HashSet<u32>, PagerError> {
1217 let mut remaining = std::collections::HashSet::with_capacity(pages.len());
1218 if pages
1219 .iter()
1220 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !remaining.insert(*id))
1221 {
1222 return Err(PagerError::InvalidDatabase(
1223 "invalid recovery page set".into(),
1224 ));
1225 }
1226 let requested = remaining.clone();
1227 freelist
1228 .remove_existing_from(&requested, &mut remaining)
1229 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1230 let mut trunk_id = freelist.trunk_head();
1231 let mut visited = std::collections::HashSet::new();
1232 while trunk_id != 0 {
1233 if trunk_id < FIRST_ALLOCATABLE_PAGE
1234 || trunk_id >= page_count
1235 || !visited.insert(trunk_id)
1236 {
1237 return Err(PagerError::InvalidDatabase(
1238 "invalid freelist trunk chain".into(),
1239 ));
1240 }
1241 let trunk = self.read_page(trunk_id)?;
1242 if trunk.page_id() != trunk_id {
1243 return Err(PagerError::InvalidDatabase(
1244 "misdirected freelist trunk".into(),
1245 ));
1246 }
1247 let mut loaded = FreeList::new();
1248 let next = loaded
1249 .load_from_trunk(&trunk)
1250 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1251 while let Some(id) = loaded.allocate() {
1252 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1253 return Err(PagerError::InvalidDatabase(
1254 "invalid free page range".into(),
1255 ));
1256 }
1257 if requested.contains(&id) && !remaining.remove(&id) {
1258 return Err(PagerError::InvalidDatabase(
1259 "duplicate recovery page".into(),
1260 ));
1261 }
1262 }
1263 trunk_id = next.unwrap_or(0);
1264 }
1265 Ok(remaining)
1266 }
1267
1268 pub fn recover_unowned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1272 let _ownership = self.lock_ownership_mutations();
1273 self.ensure_writeback_available()?;
1274 if self.config.read_only {
1275 return Err(PagerError::ReadOnly);
1276 }
1277 let page_count = self.page_count()?;
1278 let mut freelist = self.freelist_write()?;
1279 let remaining = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1280 if !remaining.is_empty() {
1281 let mut missing: Vec<_> = remaining.into_iter().collect();
1282 missing.sort_unstable();
1283 for id in &missing {
1284 self.cache.remove(*id);
1285 }
1286 freelist.free_batch(&missing);
1287 *self.header_dirty_lock()? = true;
1288 }
1289 Ok(())
1290 }
1291
1292 pub fn validate_owned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1295 let page_count = self.page_count()?;
1296 let freelist = self.freelist_write()?;
1297 let absent = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1298 if absent.len() != pages.len() {
1299 return Err(PagerError::InvalidDatabase(
1300 "owned recovery page is free".into(),
1301 ));
1302 }
1303 Ok(())
1304 }
1305
1306 pub fn sync_recovered_allocator(&self) -> Result<(), PagerError> {
1309 let _ownership = self.lock_ownership_mutations();
1310 let result = (|| {
1311 self.sync()?;
1312 let _publication = self
1313 .superblock_publication
1314 .lock()
1315 .map_err(|_| PagerError::LockPoisoned)?;
1316 let header = self.header_read()?.clone();
1317 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1318 self.commit_superblock_locked(|image| {
1319 reddb_file::encode_database_header(image, &header)
1320 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
1321 })?;
1322 }
1323 Ok(())
1324 })();
1325 if result.is_err() {
1326 self.writeback_failed
1327 .store(true, std::sync::atomic::Ordering::Release);
1328 }
1329 result
1330 }
1331
1332 pub fn free_page_count(&self) -> Result<u32, PagerError> {
1335 Ok(self.freelist_write()?.total_free())
1336 }
1337
1338 pub fn header(&self) -> Result<DatabaseHeader, PagerError> {
1339 Ok(self.header_read()?.clone())
1340 }
1341
1342 pub fn physical_header(&self) -> Result<PhysicalFileHeader, PagerError> {
1343 Ok(self.header_read()?.physical)
1344 }
1345
1346 pub fn update_physical_header(&self, physical: PhysicalFileHeader) -> Result<(), PagerError> {
1347 let _ownership = self.lock_ownership_mutations();
1348 self.ensure_writeback_available()?;
1349 if self.config.read_only {
1350 return Err(PagerError::ReadOnly);
1351 }
1352
1353 let mut header = self.header_write()?;
1354 header.physical = physical;
1355 *self.header_dirty_lock()? = true;
1356 Ok(())
1357 }
1358
1359 pub fn page_count(&self) -> Result<u32, PagerError> {
1361 Ok(self.header_read()?.page_count)
1362 }
1363
1364 pub fn flush(&self) -> Result<(), PagerError> {
1366 let _ownership = self.lock_ownership_mutations();
1367 self.ensure_writeback_available()?;
1368 if self.config.read_only {
1369 return Ok(());
1370 }
1371
1372 let trunks = {
1374 let mut freelist = self.freelist_write()?;
1375 if freelist.is_dirty() {
1376 let mut header = self.header_write()?;
1377 let trunks = freelist.flush_to_trunks(0, || {
1378 let id = header.page_count;
1379 header.page_count += 1;
1380 id
1381 });
1382 header.freelist_head = freelist.trunk_head();
1383 *self.header_dirty_lock()? = true;
1384 freelist.mark_clean();
1385 trunks
1386 } else {
1387 Vec::new()
1388 }
1389 };
1390
1391 let mut dirty_pages = Vec::new();
1392 for trunk in trunks {
1393 let page_id = trunk.page_id();
1394 if let Some(evicted) = self.cache.insert_dirty(page_id, trunk) {
1395 dirty_pages.push((evicted.page_id(), evicted));
1398 }
1399 }
1400
1401 const FLUSH_BATCH_PAGES: usize = 256;
1405 for batch in dirty_pages.chunks(FLUSH_BATCH_PAGES) {
1406 self.write_dirty_page_batch(batch)?;
1407 }
1408 drop(dirty_pages);
1409 let mut page_ids = self.cache.dirty_page_ids(usize::MAX);
1413 page_ids.sort_unstable();
1414 for ids in page_ids.chunks(FLUSH_BATCH_PAGES) {
1415 let pages = self.cache.flush_dirty_pages(ids);
1416 self.write_dirty_page_batch(&pages)?;
1417 }
1418
1419 if *self.header_dirty_lock()? {
1421 self.write_header()?;
1422 }
1423
1424 Ok(())
1425 }
1426
1427 fn write_dirty_page_batch(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1428 if pages.is_empty() {
1429 return Ok(());
1430 }
1431 let result = (|| {
1435 let max_lsn = pages
1436 .iter()
1437 .filter_map(|(_, page)| page.header().ok().map(|header| header.lsn))
1438 .max()
1439 .unwrap_or(0);
1440 if max_lsn > 0 {
1441 self.wal.flush_until(max_lsn)?;
1442 }
1443 self.write_pages_through_dwb(pages)
1444 })();
1445 if result.is_err() {
1446 self.writeback_failed
1447 .store(true, std::sync::atomic::Ordering::Release);
1448 for (page_id, _) in pages {
1449 self.cache.mark_dirty(*page_id);
1450 }
1451 }
1452 result
1453 }
1454
1455 pub fn sync(&self) -> Result<(), PagerError> {
1457 let _ownership = self.lock_ownership_mutations();
1458 self.flush()?;
1459
1460 if self
1461 .allocation_reused
1462 .swap(false, std::sync::atomic::Ordering::AcqRel)
1463 {
1464 if let Err(error) = self.write_header_and_sync() {
1468 self.allocation_reused
1469 .store(true, std::sync::atomic::Ordering::Release);
1470 return Err(error);
1471 }
1472 }
1473
1474 let file = self.file_lock()?;
1475 file.sync_all()?;
1476
1477 Ok(())
1478 }
1479
1480 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
1482 self.cache.stats()
1483 }
1484
1485 pub fn cache_len(&self) -> usize {
1488 self.cache.len()
1489 }
1490
1491 pub fn cache_capacity(&self) -> usize {
1494 self.cache.capacity()
1495 }
1496
1497 pub fn dirty_page_count(&self) -> usize {
1499 self.cache.dirty_count()
1500 }
1501
1502 pub fn dirty_fraction(&self) -> f64 {
1506 let capacity = self.cache.capacity().max(1) as f64;
1507 self.cache.dirty_count() as f64 / capacity
1508 }
1509
1510 pub fn flush_some_dirty(&self, max: usize) -> Result<usize, PagerError> {
1514 let _ownership = self.lock_ownership_mutations();
1515 self.ensure_writeback_available()?;
1516 if self.config.read_only || max == 0 {
1517 return Ok(0);
1518 }
1519 let dirty_pages = self.cache.flush_some_dirty(max);
1520 if dirty_pages.is_empty() {
1521 return Ok(0);
1522 }
1523 let count = dirty_pages.len();
1524 for (page_id, page) in dirty_pages {
1531 self.write_page(page_id, page)?;
1532 }
1533 Ok(count)
1534 }
1535
1536 pub fn path(&self) -> &Path {
1538 &self.path
1539 }
1540
1541 pub fn is_read_only(&self) -> bool {
1543 self.config.read_only
1544 }
1545
1546 pub fn file_size(&self) -> Result<u64, PagerError> {
1548 let file = self.file_lock()?;
1549 Ok(file.file_len()?)
1550 }
1551
1552 fn clear_dwb_zone(&self) -> Result<(), PagerError> {
1554 let mut file = self.file_lock()?;
1555 Ok(pager_io::clear_dwb_zone(&mut *file)?)
1556 }
1557
1558 pub fn read_manifest_page(&self) -> Result<Page, PagerError> {
1563 self.read_page(1)
1564 .map_err(|_| PagerError::ManifestZoneCorrupt(self.path.clone()))
1565 }
1566
1567 fn write_pages_through_dwb(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1574 if self.config.read_only {
1575 return Err(PagerError::ReadOnly);
1576 }
1577 let mut file = self.file_lock()?;
1580 self.ensure_writeback_available()?;
1581 let result = pager_io::write_pages(&mut *file, pages, self.dwb_enabled);
1582 if result.is_err() {
1583 self.writeback_failed
1586 .store(true, std::sync::atomic::Ordering::Release);
1587 }
1588 Ok(result?)
1589 }
1590
1591 fn recover_from_dwb(&self) -> Result<(), PagerError> {
1596 let mut file = self.file_lock()?;
1597 Ok(pager_io::recover_from_dwb(&mut *file)?)
1598 }
1599
1600 fn complete_allocation_fence(&self) -> Result<(), PagerError> {
1604 if self
1605 .allocation_fence_pending
1606 .load(std::sync::atomic::Ordering::Acquire)
1607 {
1608 self.write_header_and_sync()?;
1609 self.write_header_and_sync()?;
1610 self.allocation_fence_pending
1611 .store(false, std::sync::atomic::Ordering::Release);
1612 }
1613 Ok(())
1614 }
1615
1616 pub fn write_header_and_sync(&self) -> Result<(), PagerError> {
1618 let _ownership = self.lock_ownership_mutations();
1619 self.write_header()?;
1620 let file = self.file_lock()?;
1621 file.sync_all()?;
1622 Ok(())
1623 }
1624
1625 pub fn set_checkpoint_in_progress(
1627 &self,
1628 in_progress: bool,
1629 target_lsn: u64,
1630 ) -> Result<(), PagerError> {
1631 let _ownership = self.lock_ownership_mutations();
1632 let mut header = self.header_write()?;
1633 header.checkpoint_in_progress = in_progress;
1634 header.checkpoint_target_lsn = target_lsn;
1635 *self.header_dirty_lock()? = true;
1636 drop(header);
1637 self.write_header_and_sync()
1638 }
1639
1640 pub fn complete_checkpoint(&self, lsn: u64) -> Result<(), PagerError> {
1642 let _ownership = self.lock_ownership_mutations();
1643 let mut header = self.header_write()?;
1644 header.checkpoint_lsn = lsn;
1645 header.checkpoint_in_progress = false;
1646 header.checkpoint_target_lsn = 0;
1647 *self.header_dirty_lock()? = true;
1648 drop(header);
1649 self.write_header_and_sync()
1650 }
1651}
1652
1653#[cfg(test)]
1654mod integrity_guidance_tests {
1655 use super::PagerError;
1656 use std::path::PathBuf;
1657
1658 #[test]
1659 fn damaged_paged_zones_recommend_preserving_files_and_a_verified_backup() {
1660 for error in [
1661 PagerError::SuperblockZoneUnrecoverable(PathBuf::from("data.rdb")),
1662 PagerError::ManifestZoneCorrupt(PathBuf::from("data.rdb")),
1663 PagerError::DwbZoneCorrupt(PathBuf::from("data.rdb")),
1664 ] {
1665 let message = error.to_string();
1666 assert!(message.contains("Preserve the database"), "{message}");
1667 assert!(message.contains("verified backup"), "{message}");
1668 assert!(!message.contains("salvage"), "{message}");
1669 assert!(!message.contains("SCRUB"), "{message}");
1670 }
1671 }
1672}