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. Recover what survives with red salvage \
83 (ADR 0074 §4); it reads the damaged file without writing to it and \
84 reports what it could not verify.",
85 path.display()
86 ),
87 Self::ManifestZoneCorrupt(path) => write!(
88 f,
89 "internal manifest zone of {} failed its checksum: the zone that names \
90 collections, indexes and the checkpoint boundary cannot be trusted, so no \
91 rows are returned rather than garbage ones. Run the `SCRUB` statement to classify the fault \
92 and `red salvage` to extract what survives (ADR 0074 §2/§4).",
93 path.display()
94 ),
95 Self::DwbZoneCorrupt(path) => write!(
96 f,
97 "double-write zone of {} failed validation: page recovery cannot prove \
98 whether the protected in-place write completed. The store will not be opened \
99 silently; run the `SCRUB` statement to classify the fault and `red salvage` to extract trusted \
100 rows (ADR 0074 §2/§4).",
101 path.display()
102 ),
103 Self::LegacySidecarStore { sidecar } => write!(
104 f,
105 "refusing to open a legacy sidecar-backed store: found {}. The superblock \
106 pair, internal manifest, and double-write recovery state now live inside the \
107 .rdb file (ADR 0038 §2/§4), \
108 and the engine never reads the retired sidecars silently. Convert the store \
109 first with the offline migration tool \
110 (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is reversible.",
111 sidecar.display()
112 ),
113 Self::LegacyPagedStore { path } => write!(
114 f,
115 "refusing to open a pre-phase-3 paged store: {} does not carry the layout marker \
116 for the in-file double-write zone. Convert the store first with the offline \
117 migration tool (`reddb_server::pager_zone_migration::migrate_to_zoned`); it is \
118 reversible.",
119 path.display()
120 ),
121 }
122 }
123}
124
125impl std::error::Error for PagerError {
126 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127 match self {
128 Self::Io(e) => Some(e),
129 Self::Page(e) => Some(e),
130 _ => None,
131 }
132 }
133}
134
135impl From<std::io::Error> for PagerError {
136 fn from(e: std::io::Error) -> Self {
137 Self::Io(e)
138 }
139}
140
141impl From<PageError> for PagerError {
142 fn from(e: PageError) -> Self {
143 Self::Page(e)
144 }
145}
146
147impl From<pager_io::PagerIoError> for PagerError {
148 fn from(error: pager_io::PagerIoError) -> Self {
149 match error {
150 pager_io::PagerIoError::Io(error) => Self::Io(error),
151 pager_io::PagerIoError::Page(error) => Self::Page(error),
152 pager_io::PagerIoError::InvalidDatabase(message) => Self::InvalidDatabase(message),
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct PagerConfig {
160 pub cache_size: usize,
161 pub read_only: bool,
162 pub verify_checksums: bool,
163 pub double_write: bool,
164}
165
166impl Default for PagerConfig {
167 fn default() -> Self {
168 Self {
169 cache_size: 10_000,
170 read_only: false,
171 verify_checksums: true,
172 double_write: true,
173 }
174 }
175}
176
177pub trait WalBarrier {
180 fn flush_until(&self, lsn: u64) -> std::io::Result<()>;
181
182 fn needs_page_image(&self, _page_id: u32) -> std::io::Result<bool> {
185 Ok(false)
186 }
187
188 fn capture_page_image(&self, _page: &Page) -> std::io::Result<()> {
191 Ok(())
192 }
193
194 fn reserve_pages(&self, _start: u32, _count: u32) -> std::io::Result<()> {
196 Ok(())
197 }
198
199 fn detach_free_pages(&self, _pages: &[u32]) -> std::io::Result<()> {
201 Ok(())
202 }
203
204 fn has_page_ownership_journal(&self) -> std::io::Result<bool> {
207 Ok(false)
208 }
209
210 fn retire_pages(&self, _pages: &[u32]) -> std::io::Result<()> {
212 Ok(())
213 }
214}
215
216pub struct NoWal;
218impl WalBarrier for NoWal {
219 fn flush_until(&self, _lsn: u64) -> std::io::Result<()> {
220 Err(std::io::Error::other(
221 "nonzero page LSN requires a WAL durability barrier",
222 ))
223 }
224}
225pub struct Pager<F: VfsFile, W: WalBarrier> {
226 path: PathBuf,
228 file: Mutex<F>,
230 dwb_enabled: bool,
232 writeback_failed: std::sync::atomic::AtomicBool,
235 cache: PageCache,
237 freelist: RwLock<FreeList>,
239 deferred_reclamation: Mutex<DeferredPageReclamation>,
242 ownership_mutations: parking_lot::ReentrantMutex<()>,
245 allocation_reused: std::sync::atomic::AtomicBool,
248 allocation_fence_pending: std::sync::atomic::AtomicBool,
250 header: RwLock<DatabaseHeader>,
252 superblock_publication: Mutex<()>,
254 config: PagerConfig,
256 header_dirty: Mutex<bool>,
258 wal: W,
259}
260
261#[derive(Default)]
262struct DeferredPageReclamation {
263 enabled: bool,
264 pending: std::collections::BTreeSet<u32>,
265 published: std::collections::BTreeSet<u32>,
266}
267
268const SUPERBLOCK_SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
270const SUPERBLOCK_ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
271const PHASE3_DWB_ZONE_FILE_VERSION: u32 = 0x0001_0001;
274
275pub type NewestSuperblock = (
277 reddb_file::PagedSuperblockSelection,
278 [u8; SUPERBLOCK_SLOT_SIZE],
279);
280
281fn superblock_slot_image(page: &[u8; PAGE_SIZE]) -> [u8; SUPERBLOCK_SLOT_SIZE] {
284 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
285 image.copy_from_slice(&page[..SUPERBLOCK_SLOT_SIZE]);
286 image
287}
288
289impl<F: VfsFile, W: WalBarrier> Pager<F, W> {
290 pub fn open_owned(
294 file: F,
295 path: PathBuf,
296 config: PagerConfig,
297 wal: W,
298 create: bool,
299 ) -> Result<Self, PagerError> {
300 if create && file.file_len()? != 0 {
301 return Err(PagerError::InvalidDatabase(
302 "new pager file is not empty".into(),
303 ));
304 }
305 let pager = Self {
306 path,
307 file: Mutex::new(file),
308 dwb_enabled: config.double_write && !config.read_only,
309 writeback_failed: std::sync::atomic::AtomicBool::new(false),
310 cache: PageCache::new(config.cache_size),
311 freelist: RwLock::new(FreeList::new()),
312 deferred_reclamation: Mutex::new(DeferredPageReclamation::default()),
313 ownership_mutations: parking_lot::ReentrantMutex::new(()),
314 allocation_reused: std::sync::atomic::AtomicBool::new(false),
315 allocation_fence_pending: std::sync::atomic::AtomicBool::new(false),
316 header: RwLock::new(DatabaseHeader::default()),
317 superblock_publication: Mutex::new(()),
318 config,
319 header_dirty: Mutex::new(false),
320 wal,
321 };
322 if create {
323 pager.initialize()?;
324 } else {
325 pager.ensure_phase3_dwb_zone_layout()?;
326 pager.recover_from_dwb()?;
327 pager.load_header()?;
328 }
329 Ok(pager)
330 }
331
332 pub fn wal_barrier(&self) -> &W {
333 &self.wal
334 }
335
336 pub fn lock_ownership_mutations(&self) -> parking_lot::ReentrantMutexGuard<'_, ()> {
339 self.ownership_mutations.lock()
340 }
341
342 pub fn fence_interrupted_ownership(&self) {
344 self.writeback_failed
345 .store(true, std::sync::atomic::Ordering::Release);
346 }
347
348 #[doc(hidden)]
351 pub fn with_file<T>(&self, operation: impl FnOnce(&mut F) -> T) -> Result<T, PagerError> {
352 Ok(operation(&mut *self.file_lock()?))
353 }
354
355 fn initialize(&self) -> Result<(), PagerError> {
357 if self.config.read_only {
358 return Err(PagerError::ReadOnly);
359 }
360
361 let initial_page_count = FIRST_ALLOCATABLE_PAGE;
365 let header_page = Page::new_header_page(initial_page_count);
366 self.header_write()?.page_count = initial_page_count;
367
368 self.write_page_raw(0, &header_page)?;
373 let mut metadata_page = Page::new(PageType::Header, 1);
374 metadata_page.update_checksum();
375 self.write_page_raw(1, &metadata_page)?;
376 let mut vault_page = Page::new(PageType::Vault, 2);
377 vault_page.update_checksum();
378 self.write_page_raw(2, &vault_page)?;
379 self.clear_dwb_zone()?;
380
381 let mut slot = superblock_slot_image(header_page.as_bytes());
385 self.write_superblock_slot(&mut slot, 0, 1)?;
386 self.write_superblock_slot(&mut slot, 1, 2)?;
387
388 self.sync()?;
390
391 Ok(())
392 }
393
394 fn header_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, DatabaseHeader>, PagerError> {
396 self.header.write().map_err(|_| PagerError::LockPoisoned)
397 }
398
399 fn header_read(&self) -> Result<std::sync::RwLockReadGuard<'_, DatabaseHeader>, PagerError> {
401 self.header.read().map_err(|_| PagerError::LockPoisoned)
402 }
403
404 fn freelist_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, FreeList>, PagerError> {
406 self.freelist.write().map_err(|_| PagerError::LockPoisoned)
407 }
408
409 fn file_lock(&self) -> Result<std::sync::MutexGuard<'_, F>, PagerError> {
411 self.file.lock().map_err(|_| PagerError::LockPoisoned)
412 }
413
414 fn header_dirty_lock(&self) -> Result<std::sync::MutexGuard<'_, bool>, PagerError> {
416 self.header_dirty
417 .lock()
418 .map_err(|_| PagerError::LockPoisoned)
419 }
420
421 fn read_superblock_zone(&self) -> Result<[u8; SUPERBLOCK_ZONE_SIZE], PagerError> {
425 let mut file = self.file_lock()?;
426 Ok(pager_io::read_superblock_zone(&mut *file)?)
427 }
428
429 #[doc(hidden)]
435 pub fn newest_superblock(&self) -> Result<NewestSuperblock, PagerError> {
436 let zone = self.read_superblock_zone()?;
437 let selection = reddb_file::select_paged_superblock(&zone)
438 .ok_or_else(|| PagerError::SuperblockZoneUnrecoverable(self.path.clone()))?;
439
440 let start = selection.copy_index * SUPERBLOCK_SLOT_SIZE;
441 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
442 image.copy_from_slice(&zone[start..start + SUPERBLOCK_SLOT_SIZE]);
443 Ok((selection, image))
444 }
445
446 fn ensure_phase3_dwb_zone_layout(&self) -> Result<(), PagerError> {
447 let (_, image) = self.newest_superblock()?;
448 let header = reddb_file::decode_database_header(&image)
449 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
450 if header.version < PHASE3_DWB_ZONE_FILE_VERSION {
451 return Err(PagerError::LegacyPagedStore {
452 path: self.path.clone(),
453 });
454 }
455 Ok(())
456 }
457
458 fn write_superblock_slot(
461 &self,
462 slot: &mut [u8; SUPERBLOCK_SLOT_SIZE],
463 copy_index: usize,
464 generation: u64,
465 ) -> Result<(), PagerError> {
466 self.ensure_writeback_available()?;
467 if self.config.read_only {
468 return Err(PagerError::ReadOnly);
469 }
470 let mut file = self.file_lock()?;
471 self.ensure_writeback_available()?;
472 Ok(pager_io::write_superblock_slot(
473 &mut *file, slot, copy_index, generation,
474 )?)
475 }
476
477 #[doc(hidden)]
483 pub fn commit_superblock(
484 &self,
485 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
486 ) -> Result<(), PagerError> {
487 let _ownership = self.lock_ownership_mutations();
488 let _publication = self
489 .superblock_publication
490 .lock()
491 .map_err(|_| PagerError::LockPoisoned)?;
492 self.commit_superblock_locked(mutate)
493 }
494
495 fn commit_superblock_locked(
496 &self,
497 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
498 ) -> Result<(), PagerError> {
499 if self.config.read_only {
500 return Err(PagerError::ReadOnly);
501 }
502 let (newest, mut image) = self.newest_superblock()?;
503 mutate(&mut image)?;
504 let next_copy = reddb_file::paged_superblock_next_copy(Some(newest));
505 self.write_superblock_slot(&mut image, next_copy, newest.generation.saturating_add(1))
506 }
507
508 pub fn fence_file_format_version(&self, version: u32) -> Result<(), PagerError> {
514 let _ownership = self.lock_ownership_mutations();
515 self.ensure_writeback_available()?;
516 if self.config.read_only {
517 return Err(PagerError::ReadOnly);
518 }
519 if !(PHASE3_DWB_ZONE_FILE_VERSION..=reddb_file::PAGE_FILE_VERSION).contains(&version) {
520 return Err(PagerError::InvalidDatabase(
521 "unsupported file format fence".into(),
522 ));
523 }
524 let _publication = self
525 .superblock_publication
526 .lock()
527 .map_err(|_| PagerError::LockPoisoned)?;
528 if version < self.header_read()?.version {
529 return Err(PagerError::InvalidDatabase(
530 "file format downgrade is not supported".into(),
531 ));
532 }
533 let result = (|| {
534 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
535 self.commit_superblock_locked(|image| {
536 reddb_file::set_database_header_version(image, version)
537 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
538 })?;
539 }
540 self.header_write()?.version = version;
541 Ok(())
542 })();
543 if result.is_err() {
544 self.writeback_failed
545 .store(true, std::sync::atomic::Ordering::Release);
546 }
547 result
548 }
549
550 fn load_header(&self) -> Result<(), PagerError> {
552 let (_, image) = self.newest_superblock()?;
553
554 let mut decoded_header = reddb_file::decode_database_header(&image)
555 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
556 let physical_pages = self.file_lock()?.file_len()?.div_ceil(PAGE_SIZE as u64);
562 let physical_pages = u32::try_from(physical_pages).map_err(|_| {
563 PagerError::InvalidDatabase("data file exceeds the page address space".into())
564 })?;
565 if decoded_header.page_count < physical_pages {
566 decoded_header.page_count = physical_pages;
567 if !self.config.read_only {
568 *self.header_dirty_lock()? = true;
569 }
570 }
571 let freelist_head = decoded_header.freelist_head;
572
573 {
574 let mut header = self.header_write()?;
575 *header = decoded_header;
576 }
577
578 {
580 let mut freelist = self.freelist_write()?;
581 *freelist = FreeList::from_header(freelist_head, 0);
582 }
583
584 Ok(())
585 }
586
587 fn write_header(&self) -> Result<(), PagerError> {
589 if self.config.read_only {
590 return Err(PagerError::ReadOnly);
591 }
592
593 let header = self.header_read()?.clone();
594 self.commit_superblock(|image| {
595 reddb_file::encode_database_header(image, &header)
596 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
597 })?;
598 *self.header_dirty_lock()? = false;
599
600 Ok(())
601 }
602
603 fn read_page_raw(&self, page_id: u32) -> Result<Page, PagerError> {
605 let mut file = self.file_lock()?;
606 Ok(pager_io::read_page(
607 &mut *file,
608 page_id,
609 self.config.verify_checksums,
610 )?)
611 }
612
613 fn ensure_writeback_available(&self) -> Result<(), PagerError> {
614 if self
615 .writeback_failed
616 .load(std::sync::atomic::Ordering::Acquire)
617 {
618 return Err(PagerError::Io(std::io::Error::other(
619 "page writeback failed; reopen the database to recover before writing",
620 )));
621 }
622 Ok(())
623 }
624
625 fn record_ownership(
626 &self,
627 operation: impl FnOnce(&W) -> std::io::Result<()>,
628 ) -> Result<(), PagerError> {
629 if let Err(error) = operation(&self.wal) {
630 self.writeback_failed
631 .store(true, std::sync::atomic::Ordering::Release);
632 return Err(PagerError::Io(error));
633 }
634 Ok(())
635 }
636
637 fn write_evicted_dirty_page(
640 &self,
641 page: Page,
642 pending_page: Option<u32>,
643 ) -> Result<(), PagerError> {
644 let mut ids = self.cache.dirty_page_ids(pager_io::DWB_MAX_PAGES_PER_FRAME);
645 ids.retain(|id| Some(*id) != pending_page);
649 ids.truncate(pager_io::DWB_MAX_PAGES_PER_FRAME.saturating_sub(1));
652 ids.sort_unstable();
653 let mut pages = self.cache.flush_dirty_pages(&ids);
654 pages.push((page.page_id(), page));
655 pages.sort_unstable_by_key(|(id, _)| *id);
656 self.write_dirty_page_batch(&pages)
657 }
658
659 fn write_page_raw(&self, page_id: u32, page: &Page) -> Result<(), PagerError> {
661 self.ensure_writeback_available()?;
662 if self.config.read_only {
663 return Err(PagerError::ReadOnly);
664 }
665 let mut file = self.file_lock()?;
666 self.ensure_writeback_available()?;
667 Ok(pager_io::write_page(&mut *file, page_id, page)?)
668 }
669
670 pub fn read_page(&self, page_id: u32) -> Result<Page, PagerError> {
672 if let Some(page) = self.cache.get(page_id) {
674 return Ok(page);
675 }
676
677 let _ownership = self.lock_ownership_mutations();
681 if let Some(page) = self.cache.get(page_id) {
682 return Ok(page);
683 }
684
685 let page = self.read_page_raw(page_id)?;
687
688 if let Some(dirty_page) = self.cache.insert_if_absent(page_id, page.clone()) {
690 self.write_evicted_dirty_page(dirty_page, None)?;
692 }
693
694 Ok(page)
695 }
696
697 pub fn read_page_no_checksum(&self, page_id: u32) -> Result<Page, PagerError> {
702 if let Some(page) = self.cache.get(page_id) {
704 return Ok(page);
705 }
706
707 let _ownership = self.lock_ownership_mutations();
708 if let Some(page) = self.cache.get(page_id) {
709 return Ok(page);
710 }
711
712 let mut file = self.file_lock()?;
714 let page = pager_io::read_page(&mut *file, page_id, false)?;
715 drop(file);
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 write_page(&self, page_id: u32, mut page: Page) -> Result<(), PagerError> {
728 let _ownership = self.lock_ownership_mutations();
729 self.ensure_writeback_available()?;
730 if self.config.read_only {
731 return Err(PagerError::ReadOnly);
732 }
733
734 self.capture_page_before_write(page_id)?;
735
736 page.update_checksum();
738
739 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
744 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
745 }
746
747 Ok(())
748 }
749
750 pub fn write_page_no_checksum(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
755 let _ownership = self.lock_ownership_mutations();
756 self.ensure_writeback_available()?;
757 if self.config.read_only {
758 return Err(PagerError::ReadOnly);
759 }
760
761 self.capture_page_before_write(page_id)?;
762
763 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
766 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
767 }
768
769 Ok(())
770 }
771
772 fn capture_page_before_write(&self, page_id: u32) -> Result<(), PagerError> {
773 let mut required = false;
774 self.record_ownership(|wal| {
775 required = wal.needs_page_image(page_id)?;
776 Ok(())
777 })?;
778 if required {
779 let previous = self.read_page(page_id)?;
780 self.record_ownership(|wal| wal.capture_page_image(&previous))?;
781 }
782 Ok(())
783 }
784
785 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, PagerError> {
787 let _ownership = self.lock_ownership_mutations();
788 self.ensure_writeback_available()?;
789 if self.config.read_only {
790 return Err(PagerError::ReadOnly);
791 }
792
793 let page_id = {
795 let mut freelist = self.freelist_write()?;
796 self.complete_allocation_fence()?;
799 if let Some(id) = freelist.next_in_memory_page() {
800 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
801 let id = freelist
802 .allocate()
803 .expect("free page checked while allocator is locked");
804 self.allocation_reused
805 .store(true, std::sync::atomic::Ordering::Release);
806 id
807 } else if freelist.trunk_head() != 0 {
808 let trunk_id = freelist.trunk_head();
809
810 let trunk = self.read_page(trunk_id).map_err(|e| match e {
811 PagerError::PageNotFound(_) => {
812 PagerError::InvalidDatabase("Freelist trunk missing".to_string())
813 }
814 other => other,
815 })?;
816 if trunk.page_id() != trunk_id {
817 return Err(PagerError::InvalidDatabase(
818 "misdirected freelist trunk".into(),
819 ));
820 }
821
822 let mut detached = FreeList::new();
825 detached
826 .load_from_trunk(&trunk)
827 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {e}")))?;
828 let mut pages = Vec::new();
829 let mut seen = std::collections::HashSet::new();
830 let page_count = self.page_count()?;
831 while let Some(id) = detached.allocate() {
832 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count || !seen.insert(id) {
833 return Err(PagerError::InvalidDatabase(
834 "invalid freelist trunk page".into(),
835 ));
836 }
837 pages.push(id);
838 }
839 let mut journaled = false;
840 self.record_ownership(|wal| {
841 journaled = wal.has_page_ownership_journal()?;
842 Ok(())
843 })?;
844 if !journaled {
845 self.allocate_unjournaled_trunk(&mut freelist, trunk, &pages)?
846 } else {
847 self.record_ownership(|wal| wal.detach_free_pages(&pages))?;
848
849 freelist
850 .load_from_trunk(&trunk)
851 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {}", e)))?;
852 let mut header = self.header_write()?;
853 header.freelist_head = freelist.trunk_head();
854 *self.header_dirty_lock()? = true;
855 drop(header);
856 self.allocation_fence_pending
861 .store(true, std::sync::atomic::Ordering::Release);
862 self.complete_allocation_fence()?;
863 let next = freelist.next_in_memory_page().ok_or_else(|| {
864 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
865 })?;
866 self.record_ownership(|wal| wal.reserve_pages(next, 1))?;
867 let id = freelist.allocate().ok_or_else(|| {
868 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
869 })?;
870 self.allocation_reused
871 .store(true, std::sync::atomic::Ordering::Release);
872 id
873 }
874 } else {
875 let mut header = self.header_write()?;
877 let id = header.page_count;
878 let next = id
879 .checked_add(1)
880 .ok_or_else(|| PagerError::InvalidDatabase("page count overflow".into()))?;
881 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
882 header.page_count = next;
883 *self.header_dirty_lock()? = true;
884 id
885 }
886 };
887
888 let page = Page::new(page_type, page_id);
889
890 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page.clone()) {
902 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
903 }
904
905 Ok(page)
906 }
907
908 fn allocate_unjournaled_trunk(
912 &self,
913 freelist: &mut FreeList,
914 mut trunk: Page,
915 pages: &[u32],
916 ) -> Result<u32, PagerError> {
917 let id = *pages
918 .first()
919 .expect("validated trunk includes its own page");
920 self.record_ownership(|wal| wal.reserve_pages(id, 1))?;
921 if pages.len() > 1 {
922 let remaining =
923 u32::try_from(pages.len() - 2).expect("a validated trunk fits in one page");
924 trunk.content_mut()[4..8].copy_from_slice(&remaining.to_le_bytes());
925 trunk.update_checksum();
926 self.cache.remove(trunk.page_id());
927 self.write_pages_through_dwb(&[(trunk.page_id(), trunk)])?;
928 freelist.note_durable_trunk_allocation(remaining);
929 } else {
930 freelist
933 .load_from_trunk(&trunk)
934 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))?;
935 self.header_write()?.freelist_head = freelist.trunk_head();
936 *self.header_dirty_lock()? = true;
937 self.allocation_fence_pending
938 .store(true, std::sync::atomic::Ordering::Release);
939 self.complete_allocation_fence()?;
940 assert_eq!(freelist.allocate(), Some(id));
941 }
942 self.allocation_reused
943 .store(true, std::sync::atomic::Ordering::Release);
944 Ok(id)
945 }
946
947 pub fn reserve_contig_extent(&self, n_pages: u32) -> Result<ExtentId, PagerError> {
949 let _ownership = self.lock_ownership_mutations();
950 self.ensure_writeback_available()?;
951 if self.config.read_only {
952 return Err(PagerError::ReadOnly);
953 }
954 if n_pages == 0 {
955 return Err(PagerError::InvalidDatabase(
956 "contiguous extent must reserve at least one page".to_string(),
957 ));
958 }
959
960 let start_page = {
961 let mut header = self.header_write()?;
962 let start = header.page_count;
963 let end = header.page_count.checked_add(n_pages).ok_or_else(|| {
964 PagerError::InvalidDatabase("contiguous extent page count overflow".to_string())
965 })?;
966 self.record_ownership(|wal| wal.reserve_pages(start, n_pages))?;
967 header.page_count = end;
968 *self.header_dirty_lock()? = true;
969 start
970 };
971
972 for page_id in start_page..start_page + n_pages {
973 let mut page = Page::new(PageType::Vector, page_id);
974 page.update_checksum();
975 if let Some(dirty_page) = self.cache.insert_dirty(page_id, page) {
976 self.write_evicted_dirty_page(dirty_page, Some(page_id))?;
977 }
978 }
979
980 Ok(ExtentId {
981 start_page,
982 n_pages,
983 })
984 }
985
986 pub fn free_page(&self, page_id: u32) -> Result<(), PagerError> {
988 let _ownership = self.lock_ownership_mutations();
989 self.ensure_writeback_available()?;
990 if self.config.read_only {
991 return Err(PagerError::ReadOnly);
992 }
993
994 let mut deferred = self
995 .deferred_reclamation
996 .lock()
997 .map_err(|_| PagerError::LockPoisoned)?;
998 if deferred.enabled {
999 if page_id < FIRST_ALLOCATABLE_PAGE || page_id >= self.page_count()? {
1000 return Err(PagerError::InvalidDatabase(
1001 "invalid deferred free page".into(),
1002 ));
1003 }
1004 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1005 deferred.pending.insert(page_id);
1008 return Ok(());
1009 }
1010 drop(deferred);
1011
1012 self.record_ownership(|wal| wal.retire_pages(&[page_id]))?;
1013
1014 self.cache.remove(page_id);
1016
1017 let mut freelist = self.freelist_write()?;
1019 freelist.free(page_id);
1020
1021 *self.header_dirty_lock()? = true;
1022
1023 Ok(())
1024 }
1025
1026 pub fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
1027 let _ownership = self.lock_ownership_mutations();
1028 self.deferred_reclamation
1029 .lock()
1030 .map_err(|_| PagerError::LockPoisoned)?
1031 .enabled = true;
1032 Ok(())
1033 }
1034
1035 pub fn deferred_reclamation_pages(&self) -> Result<(Vec<u32>, Vec<u32>), PagerError> {
1036 let deferred = self
1037 .deferred_reclamation
1038 .lock()
1039 .map_err(|_| PagerError::LockPoisoned)?;
1040 Ok((
1041 deferred.pending.iter().copied().collect(),
1042 deferred.published.iter().copied().collect(),
1043 ))
1044 }
1045
1046 pub fn restore_deferred_reclamation(&self, pages: &[u32]) -> Result<(), PagerError> {
1047 let _ownership = self.lock_ownership_mutations();
1048 let count = self.page_count()?;
1049 if pages
1050 .iter()
1051 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= count)
1052 {
1053 return Err(PagerError::InvalidDatabase(
1054 "invalid persisted deferred free page".into(),
1055 ));
1056 }
1057 let mut deferred = self
1058 .deferred_reclamation
1059 .lock()
1060 .map_err(|_| PagerError::LockPoisoned)?;
1061 deferred.pending.extend(pages);
1062 deferred.published.extend(pages);
1063 Ok(())
1064 }
1065
1066 pub fn complete_deferred_publication(
1069 &self,
1070 retained: &[u32],
1071 released: &[u32],
1072 ) -> Result<(), PagerError> {
1073 let _ownership = self.lock_ownership_mutations();
1074 let mut deferred = self
1075 .deferred_reclamation
1076 .lock()
1077 .map_err(|_| PagerError::LockPoisoned)?;
1078 if !released.is_empty() {
1079 self.free_retired_pages(released)?;
1080 }
1081 for id in released {
1082 deferred.pending.remove(id);
1083 }
1084 deferred.published = retained.iter().copied().collect();
1085 Ok(())
1086 }
1087
1088 pub fn record_retirement_intent(&self, pages: &[u32]) -> Result<(), PagerError> {
1090 let _ownership = self.lock_ownership_mutations();
1091 self.ensure_writeback_available()?;
1092 if self.config.read_only {
1093 return Err(PagerError::ReadOnly);
1094 }
1095 let page_count = self.page_count()?;
1096 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1097 if pages
1098 .iter()
1099 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !seen.insert(*id))
1100 {
1101 return Err(PagerError::InvalidDatabase(
1102 "invalid retirement intent".into(),
1103 ));
1104 }
1105 self.record_ownership(|wal| wal.retire_pages(pages))
1106 }
1107
1108 pub fn free_retired_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1111 let _ownership = self.lock_ownership_mutations();
1112 self.ensure_writeback_available()?;
1113 if self.config.read_only {
1114 return Err(PagerError::ReadOnly);
1115 }
1116 let page_count = self.page_count()?;
1117 if pages
1118 .iter()
1119 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count)
1120 {
1121 return Err(PagerError::InvalidDatabase(
1122 "invalid retired page range".into(),
1123 ));
1124 }
1125 let mut seen = std::collections::HashSet::with_capacity(pages.len());
1126 if pages.iter().any(|id| !seen.insert(*id)) {
1127 return Err(PagerError::InvalidDatabase("duplicate retired page".into()));
1128 }
1129 let mut freelist = self.freelist_write()?;
1130 if freelist.contains_any(&seen) {
1131 return Err(PagerError::InvalidDatabase(
1132 "retired page is already free".into(),
1133 ));
1134 }
1135 let mut trunk_id = freelist.trunk_head();
1139 let mut visited = std::collections::HashSet::new();
1140 while trunk_id != 0 && !seen.is_empty() {
1141 if trunk_id < FIRST_ALLOCATABLE_PAGE
1142 || trunk_id >= page_count
1143 || !visited.insert(trunk_id)
1144 {
1145 return Err(PagerError::InvalidDatabase(
1146 "invalid freelist trunk chain".into(),
1147 ));
1148 }
1149 let trunk = self.read_page(trunk_id)?;
1150 if trunk.page_id() != trunk_id {
1151 return Err(PagerError::InvalidDatabase(
1152 "misdirected freelist trunk".into(),
1153 ));
1154 }
1155 let mut loaded = FreeList::new();
1156 let next = loaded
1157 .load_from_trunk(&trunk)
1158 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1159 while let Some(id) = loaded.allocate() {
1160 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1161 return Err(PagerError::InvalidDatabase(
1162 "invalid free page range".into(),
1163 ));
1164 }
1165 if seen.contains(&id) {
1166 return Err(PagerError::InvalidDatabase(
1167 "retired page is already free".into(),
1168 ));
1169 }
1170 }
1171 trunk_id = next.unwrap_or(0);
1172 }
1173 self.record_ownership(|wal| wal.retire_pages(pages))?;
1174 for id in pages {
1175 self.cache.remove(*id);
1176 }
1177 freelist.free_batch(pages);
1178 *self.header_dirty_lock()? = true;
1179 Ok(())
1180 }
1181
1182 fn pages_not_in_freelist(
1183 &self,
1184 pages: &[u32],
1185 freelist: &FreeList,
1186 page_count: u32,
1187 ) -> Result<std::collections::HashSet<u32>, PagerError> {
1188 let mut remaining = std::collections::HashSet::with_capacity(pages.len());
1189 if pages
1190 .iter()
1191 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count || !remaining.insert(*id))
1192 {
1193 return Err(PagerError::InvalidDatabase(
1194 "invalid recovery page set".into(),
1195 ));
1196 }
1197 let requested = remaining.clone();
1198 freelist
1199 .remove_existing_from(&requested, &mut remaining)
1200 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1201 let mut trunk_id = freelist.trunk_head();
1202 let mut visited = std::collections::HashSet::new();
1203 while trunk_id != 0 {
1204 if trunk_id < FIRST_ALLOCATABLE_PAGE
1205 || trunk_id >= page_count
1206 || !visited.insert(trunk_id)
1207 {
1208 return Err(PagerError::InvalidDatabase(
1209 "invalid freelist trunk chain".into(),
1210 ));
1211 }
1212 let trunk = self.read_page(trunk_id)?;
1213 if trunk.page_id() != trunk_id {
1214 return Err(PagerError::InvalidDatabase(
1215 "misdirected freelist trunk".into(),
1216 ));
1217 }
1218 let mut loaded = FreeList::new();
1219 let next = loaded
1220 .load_from_trunk(&trunk)
1221 .map_err(|error| PagerError::InvalidDatabase(format!("Freelist: {error}")))?;
1222 while let Some(id) = loaded.allocate() {
1223 if id < FIRST_ALLOCATABLE_PAGE || id >= page_count {
1224 return Err(PagerError::InvalidDatabase(
1225 "invalid free page range".into(),
1226 ));
1227 }
1228 if requested.contains(&id) && !remaining.remove(&id) {
1229 return Err(PagerError::InvalidDatabase(
1230 "duplicate recovery page".into(),
1231 ));
1232 }
1233 }
1234 trunk_id = next.unwrap_or(0);
1235 }
1236 Ok(remaining)
1237 }
1238
1239 pub fn recover_unowned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1243 let _ownership = self.lock_ownership_mutations();
1244 self.ensure_writeback_available()?;
1245 if self.config.read_only {
1246 return Err(PagerError::ReadOnly);
1247 }
1248 let page_count = self.page_count()?;
1249 let mut freelist = self.freelist_write()?;
1250 let remaining = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1251 if !remaining.is_empty() {
1252 let mut missing: Vec<_> = remaining.into_iter().collect();
1253 missing.sort_unstable();
1254 for id in &missing {
1255 self.cache.remove(*id);
1256 }
1257 freelist.free_batch(&missing);
1258 *self.header_dirty_lock()? = true;
1259 }
1260 Ok(())
1261 }
1262
1263 pub fn validate_owned_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1266 let page_count = self.page_count()?;
1267 let freelist = self.freelist_write()?;
1268 let absent = self.pages_not_in_freelist(pages, &freelist, page_count)?;
1269 if absent.len() != pages.len() {
1270 return Err(PagerError::InvalidDatabase(
1271 "owned recovery page is free".into(),
1272 ));
1273 }
1274 Ok(())
1275 }
1276
1277 pub fn sync_recovered_allocator(&self) -> Result<(), PagerError> {
1280 let _ownership = self.lock_ownership_mutations();
1281 let result = (|| {
1282 self.sync()?;
1283 let _publication = self
1284 .superblock_publication
1285 .lock()
1286 .map_err(|_| PagerError::LockPoisoned)?;
1287 let header = self.header_read()?.clone();
1288 for _ in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1289 self.commit_superblock_locked(|image| {
1290 reddb_file::encode_database_header(image, &header)
1291 .map_err(|error| PagerError::InvalidDatabase(error.to_string()))
1292 })?;
1293 }
1294 Ok(())
1295 })();
1296 if result.is_err() {
1297 self.writeback_failed
1298 .store(true, std::sync::atomic::Ordering::Release);
1299 }
1300 result
1301 }
1302
1303 pub fn free_page_count(&self) -> Result<u32, PagerError> {
1306 Ok(self.freelist_write()?.total_free())
1307 }
1308
1309 pub fn header(&self) -> Result<DatabaseHeader, PagerError> {
1310 Ok(self.header_read()?.clone())
1311 }
1312
1313 pub fn physical_header(&self) -> Result<PhysicalFileHeader, PagerError> {
1314 Ok(self.header_read()?.physical)
1315 }
1316
1317 pub fn update_physical_header(&self, physical: PhysicalFileHeader) -> Result<(), PagerError> {
1318 let _ownership = self.lock_ownership_mutations();
1319 self.ensure_writeback_available()?;
1320 if self.config.read_only {
1321 return Err(PagerError::ReadOnly);
1322 }
1323
1324 let mut header = self.header_write()?;
1325 header.physical = physical;
1326 *self.header_dirty_lock()? = true;
1327 Ok(())
1328 }
1329
1330 pub fn page_count(&self) -> Result<u32, PagerError> {
1332 Ok(self.header_read()?.page_count)
1333 }
1334
1335 pub fn flush(&self) -> Result<(), PagerError> {
1337 let _ownership = self.lock_ownership_mutations();
1338 self.ensure_writeback_available()?;
1339 if self.config.read_only {
1340 return Ok(());
1341 }
1342
1343 let trunks = {
1345 let mut freelist = self.freelist_write()?;
1346 if freelist.is_dirty() {
1347 let mut header = self.header_write()?;
1348 let trunks = freelist.flush_to_trunks(0, || {
1349 let id = header.page_count;
1350 header.page_count += 1;
1351 id
1352 });
1353 header.freelist_head = freelist.trunk_head();
1354 *self.header_dirty_lock()? = true;
1355 freelist.mark_clean();
1356 trunks
1357 } else {
1358 Vec::new()
1359 }
1360 };
1361
1362 let mut dirty_pages = Vec::new();
1363 for trunk in trunks {
1364 let page_id = trunk.page_id();
1365 if let Some(evicted) = self.cache.insert_dirty(page_id, trunk) {
1366 dirty_pages.push((evicted.page_id(), evicted));
1369 }
1370 }
1371
1372 const FLUSH_BATCH_PAGES: usize = 256;
1376 for batch in dirty_pages.chunks(FLUSH_BATCH_PAGES) {
1377 self.write_dirty_page_batch(batch)?;
1378 }
1379 drop(dirty_pages);
1380 let mut page_ids = self.cache.dirty_page_ids(usize::MAX);
1384 page_ids.sort_unstable();
1385 for ids in page_ids.chunks(FLUSH_BATCH_PAGES) {
1386 let pages = self.cache.flush_dirty_pages(ids);
1387 self.write_dirty_page_batch(&pages)?;
1388 }
1389
1390 if *self.header_dirty_lock()? {
1392 self.write_header()?;
1393 }
1394
1395 Ok(())
1396 }
1397
1398 fn write_dirty_page_batch(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1399 if pages.is_empty() {
1400 return Ok(());
1401 }
1402 let result = (|| {
1406 let max_lsn = pages
1407 .iter()
1408 .filter_map(|(_, page)| page.header().ok().map(|header| header.lsn))
1409 .max()
1410 .unwrap_or(0);
1411 if max_lsn > 0 {
1412 self.wal.flush_until(max_lsn)?;
1413 }
1414 self.write_pages_through_dwb(pages)
1415 })();
1416 if result.is_err() {
1417 self.writeback_failed
1418 .store(true, std::sync::atomic::Ordering::Release);
1419 for (page_id, _) in pages {
1420 self.cache.mark_dirty(*page_id);
1421 }
1422 }
1423 result
1424 }
1425
1426 pub fn sync(&self) -> Result<(), PagerError> {
1428 let _ownership = self.lock_ownership_mutations();
1429 self.flush()?;
1430
1431 if self
1432 .allocation_reused
1433 .swap(false, std::sync::atomic::Ordering::AcqRel)
1434 {
1435 if let Err(error) = self.write_header_and_sync() {
1439 self.allocation_reused
1440 .store(true, std::sync::atomic::Ordering::Release);
1441 return Err(error);
1442 }
1443 }
1444
1445 let file = self.file_lock()?;
1446 file.sync_all()?;
1447
1448 Ok(())
1449 }
1450
1451 pub fn cache_stats(&self) -> super::page_cache::CacheStats {
1453 self.cache.stats()
1454 }
1455
1456 pub fn cache_len(&self) -> usize {
1459 self.cache.len()
1460 }
1461
1462 pub fn cache_capacity(&self) -> usize {
1465 self.cache.capacity()
1466 }
1467
1468 pub fn dirty_page_count(&self) -> usize {
1470 self.cache.dirty_count()
1471 }
1472
1473 pub fn dirty_fraction(&self) -> f64 {
1477 let capacity = self.cache.capacity().max(1) as f64;
1478 self.cache.dirty_count() as f64 / capacity
1479 }
1480
1481 pub fn flush_some_dirty(&self, max: usize) -> Result<usize, PagerError> {
1485 let _ownership = self.lock_ownership_mutations();
1486 self.ensure_writeback_available()?;
1487 if self.config.read_only || max == 0 {
1488 return Ok(0);
1489 }
1490 let dirty_pages = self.cache.flush_some_dirty(max);
1491 if dirty_pages.is_empty() {
1492 return Ok(0);
1493 }
1494 let count = dirty_pages.len();
1495 for (page_id, page) in dirty_pages {
1502 self.write_page(page_id, page)?;
1503 }
1504 Ok(count)
1505 }
1506
1507 pub fn path(&self) -> &Path {
1509 &self.path
1510 }
1511
1512 pub fn is_read_only(&self) -> bool {
1514 self.config.read_only
1515 }
1516
1517 pub fn file_size(&self) -> Result<u64, PagerError> {
1519 let file = self.file_lock()?;
1520 Ok(file.file_len()?)
1521 }
1522
1523 fn clear_dwb_zone(&self) -> Result<(), PagerError> {
1525 let mut file = self.file_lock()?;
1526 Ok(pager_io::clear_dwb_zone(&mut *file)?)
1527 }
1528
1529 pub fn read_manifest_page(&self) -> Result<Page, PagerError> {
1534 self.read_page(1)
1535 .map_err(|_| PagerError::ManifestZoneCorrupt(self.path.clone()))
1536 }
1537
1538 fn write_pages_through_dwb(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1545 if self.config.read_only {
1546 return Err(PagerError::ReadOnly);
1547 }
1548 let mut file = self.file_lock()?;
1551 self.ensure_writeback_available()?;
1552 let result = pager_io::write_pages(&mut *file, pages, self.dwb_enabled);
1553 if result.is_err() {
1554 self.writeback_failed
1557 .store(true, std::sync::atomic::Ordering::Release);
1558 }
1559 Ok(result?)
1560 }
1561
1562 fn recover_from_dwb(&self) -> Result<(), PagerError> {
1567 let mut file = self.file_lock()?;
1568 Ok(pager_io::recover_from_dwb(&mut *file)?)
1569 }
1570
1571 fn complete_allocation_fence(&self) -> Result<(), PagerError> {
1575 if self
1576 .allocation_fence_pending
1577 .load(std::sync::atomic::Ordering::Acquire)
1578 {
1579 self.write_header_and_sync()?;
1580 self.write_header_and_sync()?;
1581 self.allocation_fence_pending
1582 .store(false, std::sync::atomic::Ordering::Release);
1583 }
1584 Ok(())
1585 }
1586
1587 pub fn write_header_and_sync(&self) -> Result<(), PagerError> {
1589 let _ownership = self.lock_ownership_mutations();
1590 self.write_header()?;
1591 let file = self.file_lock()?;
1592 file.sync_all()?;
1593 Ok(())
1594 }
1595
1596 pub fn set_checkpoint_in_progress(
1598 &self,
1599 in_progress: bool,
1600 target_lsn: u64,
1601 ) -> Result<(), PagerError> {
1602 let _ownership = self.lock_ownership_mutations();
1603 let mut header = self.header_write()?;
1604 header.checkpoint_in_progress = in_progress;
1605 header.checkpoint_target_lsn = target_lsn;
1606 *self.header_dirty_lock()? = true;
1607 drop(header);
1608 self.write_header_and_sync()
1609 }
1610
1611 pub fn complete_checkpoint(&self, lsn: u64) -> Result<(), PagerError> {
1613 let _ownership = self.lock_ownership_mutations();
1614 let mut header = self.header_write()?;
1615 header.checkpoint_lsn = lsn;
1616 header.checkpoint_in_progress = false;
1617 header.checkpoint_target_lsn = 0;
1618 *self.header_dirty_lock()? = true;
1619 drop(header);
1620 self.write_header_and_sync()
1621 }
1622}