1use super::*;
2
3pub(super) const BTRFS_SUPER_MAGIC: i64 = 0x9123_683e;
4pub(super) const ZFS_SUPER_MAGIC: i64 = 0x2fc1_2fc1;
5pub(super) const FS_NOCOW_FL: u64 = 0x0080_0000;
6
7pub(super) const SUPERBLOCK_SLOT_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_SLOT_SIZE;
9pub(super) const SUPERBLOCK_ZONE_SIZE: usize = reddb_file::PAGED_SUPERBLOCK_ZONE_SIZE;
10pub(super) const PHASE3_DWB_ZONE_FILE_VERSION: u32 = reddb_file::PAGE_FILE_VERSION;
11
12type NewestSuperblock = (
14 reddb_file::PagedSuperblockSelection,
15 [u8; SUPERBLOCK_SLOT_SIZE],
16);
17
18fn superblock_slot_image(page: &[u8; PAGE_SIZE]) -> [u8; SUPERBLOCK_SLOT_SIZE] {
21 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
22 image.copy_from_slice(&page[..SUPERBLOCK_SLOT_SIZE]);
23 image
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub(super) enum CowFilesystemKind {
28 Zfs,
29 BtrfsDataCow,
30 TestOverride,
31}
32
33pub(super) fn classify_cow_filesystem(
34 fs_type: i64,
35 mount_options: Option<&str>,
36 inode_flags: Option<u64>,
37) -> Option<CowFilesystemKind> {
38 match fs_type {
39 ZFS_SUPER_MAGIC => Some(CowFilesystemKind::Zfs),
40 BTRFS_SUPER_MAGIC => {
41 let mount_options = mount_options?;
42 if mount_options.split(',').any(|option| option == "nodatacow") {
43 return None;
44 }
45
46 let inode_flags = inode_flags?;
47 if inode_flags & FS_NOCOW_FL != 0 {
48 return None;
49 }
50
51 Some(CowFilesystemKind::BtrfsDataCow)
52 }
53 _ => None,
54 }
55}
56
57#[cfg(target_os = "linux")]
58fn linux_fstatfs_type(file: &File) -> Option<i64> {
59 use std::mem::MaybeUninit;
60 use std::os::fd::AsRawFd;
61
62 let mut stat = MaybeUninit::<libc::statfs>::uninit();
63 let rc = unsafe { libc::fstatfs(file.as_raw_fd(), stat.as_mut_ptr()) };
64 if rc != 0 {
65 return None;
66 }
67 let stat = unsafe { stat.assume_init() };
68 #[allow(clippy::unnecessary_cast)]
71 Some(stat.f_type as i64)
72}
73
74#[cfg(target_os = "linux")]
75fn linux_inode_flags(file: &File) -> Option<u64> {
76 use std::os::fd::AsRawFd;
77
78 let mut flags: libc::c_long = 0;
79 let rc = unsafe { libc::ioctl(file.as_raw_fd(), libc::FS_IOC_GETFLAGS, &mut flags) };
80 if rc != 0 {
81 return None;
82 }
83 Some(flags as u64)
84}
85
86#[cfg(target_os = "linux")]
87fn linux_mount_options_for_path(path: &Path) -> Option<String> {
88 let path = path.canonicalize().ok()?;
89 let mountinfo = std::fs::read_to_string("/proc/self/mountinfo").ok()?;
90 parse_mountinfo_options_for_path(&mountinfo, &path)
91}
92
93#[cfg(target_os = "linux")]
94pub(super) fn parse_mountinfo_options_for_path(mountinfo: &str, path: &Path) -> Option<String> {
95 let mut best: Option<(usize, String)> = None;
96
97 for line in mountinfo.lines() {
98 let fields: Vec<&str> = line.split(' ').collect();
99 if fields.len() < 10 {
100 continue;
101 }
102
103 let Some(separator) = fields.iter().position(|field| *field == "-") else {
104 continue;
105 };
106 if separator + 3 >= fields.len() || separator < 6 {
107 continue;
108 }
109
110 let mount_point = mountinfo_unescape_path(fields[4]);
111 if !path.starts_with(&mount_point) {
112 continue;
113 }
114
115 let fs_type = fields[separator + 1];
116 if fs_type != "btrfs" && fs_type != "zfs" {
117 continue;
118 }
119
120 let mount_options = fields[5];
121 let super_options = fields[separator + 3];
122 let options = format!("{mount_options},{super_options}");
123 let depth = mount_point.components().count();
124 if best
125 .as_ref()
126 .map(|(best_depth, _)| depth > *best_depth)
127 .unwrap_or(true)
128 {
129 best = Some((depth, options));
130 }
131 }
132
133 best.map(|(_, options)| options)
134}
135
136#[cfg(target_os = "linux")]
137fn mountinfo_unescape_path(value: &str) -> PathBuf {
138 let bytes = value.as_bytes();
139 let mut out = Vec::with_capacity(bytes.len());
140 let mut i = 0;
141
142 while i < bytes.len() {
143 if bytes[i] == b'\\' && i + 3 < bytes.len() {
144 let octal = &value[i + 1..i + 4];
145 if let Ok(byte) = u8::from_str_radix(octal, 8) {
146 out.push(byte);
147 i += 4;
148 continue;
149 }
150 }
151 out.push(bytes[i]);
152 i += 1;
153 }
154
155 PathBuf::from(String::from_utf8_lossy(&out).into_owned())
156}
157
158impl Pager {
159 pub fn open<P: AsRef<Path>>(path: P, mut config: PagerConfig) -> Result<Self, PagerError> {
161 let path = path.as_ref().to_path_buf();
162 let exists = path.exists();
163
164 if !exists && !config.create {
165 return Err(PagerError::InvalidDatabase(
166 "Database does not exist".into(),
167 ));
168 }
169
170 if exists {
174 if let Some(sidecar) = reddb_file::layout::retired::first_present_phase1_sidecar(&path)
175 {
176 return Err(PagerError::LegacySidecarStore { sidecar });
177 }
178 }
179
180 if !exists && config.read_only {
181 return Err(PagerError::InvalidDatabase(
182 "Cannot create read-only database".into(),
183 ));
184 }
185
186 let file = OpenOptions::new()
189 .read(true)
190 .write(!config.read_only)
191 .create(config.create && !config.read_only)
192 .open(&path)?;
193
194 #[cfg(unix)]
200 {
201 use std::os::unix::fs::MetadataExt;
202 if let Ok(meta) = file.metadata() {
203 let fs_block_size = meta.blksize();
204 if Self::page_size_misaligned_with_block(PAGE_SIZE, fs_block_size) {
205 tracing::warn!(
206 page_size = PAGE_SIZE,
207 fs_block_size,
208 path = %path.display(),
209 "database page size is not a multiple of the filesystem \
210 block size; page writes will straddle FS blocks \
211 (read-modify-write amplification). Diagnostic only — \
212 the page size is unchanged."
213 );
214 }
215 }
216 }
217
218 let lock_file = if !config.read_only {
220 let lf = OpenOptions::new().read(true).write(true).open(&path)?;
221 lf.try_lock_exclusive().map_err(|_| PagerError::Locked)?;
222 Some(lf)
223 } else {
224 let lf = OpenOptions::new().read(true).open(&path)?;
225 match lf.try_lock_shared() {
226 Ok(_) => Some(lf),
227 Err(_) => None,
228 }
229 };
230
231 let fold_dwb = crate::physical::fold_dwb_into_wal_enabled();
241 if exists {
242 if let Some(sidecar) =
243 reddb_file::layout::retired::first_present_phase3_dwb_sidecar(&path)
244 {
245 return Err(PagerError::LegacySidecarStore { sidecar });
246 }
247 }
248 if !config.double_write && !config.read_only && !fold_dwb {
249 let skip_dwb_on_cow =
250 Self::cow_filesystem_has_atomic_page_writes(&path, &file).is_some();
251 if !skip_dwb_on_cow {
252 tracing::warn!(
253 path = %path.display(),
254 "double_write=false requested, but the data file is not proven to be on \
255 ZFS or btrfs datacow; keeping the in-file double-write buffer enabled"
256 );
257 config.double_write = true;
258 }
259 }
260 let dwb_enabled = config.double_write && !config.read_only && !fold_dwb;
261
262 let mut pager = Self {
263 path,
264 file: Mutex::new(file),
265 _lock_file: lock_file,
266 dwb_enabled,
267 cache: PageCache::new(config.cache_size),
268 freelist: RwLock::new(FreeList::new()),
269 deferred_reclamation: Mutex::new(DeferredPageReclamation::default()),
270 allocation_reused: std::sync::atomic::AtomicBool::new(false),
271 allocation_fence_pending: std::sync::atomic::AtomicBool::new(false),
272 header: RwLock::new(DatabaseHeader::default()),
273 config,
274 header_dirty: Mutex::new(false),
275 wal: RwLock::new(None),
276 encryption: None,
277 };
278
279 if exists {
280 pager.ensure_phase3_dwb_zone_layout()?;
283 pager.recover_from_dwb()?;
284 pager.load_header()?;
286 pager.bind_encryption_for_existing()?;
287 } else {
288 pager.initialize()?;
290 pager.bind_encryption_for_new()?;
291 }
292
293 Ok(pager)
294 }
295
296 pub(crate) fn page_size_misaligned_with_block(page_size: usize, fs_block_size: u64) -> bool {
303 fs_block_size != 0 && !(page_size as u64).is_multiple_of(fs_block_size)
304 }
305
306 #[cfg(test)]
307 fn cow_filesystem_test_override() -> Option<bool> {
308 match COW_ATOMIC_WRITE_TEST_OVERRIDE.load(Ordering::Relaxed) {
309 1 => Some(true),
310 2 => Some(false),
311 _ => None,
312 }
313 }
314
315 #[cfg(not(test))]
316 fn cow_filesystem_test_override() -> Option<bool> {
317 None
318 }
319
320 fn cow_filesystem_has_atomic_page_writes(
321 path: &Path,
322 file: &File,
323 ) -> Option<CowFilesystemKind> {
324 if let Some(allowed) = Self::cow_filesystem_test_override() {
325 return allowed.then_some(CowFilesystemKind::TestOverride);
326 }
327 Self::probe_cow_filesystem(path, file)
328 }
329
330 #[cfg(target_os = "linux")]
331 fn probe_cow_filesystem(path: &Path, file: &File) -> Option<CowFilesystemKind> {
332 let fs_type = linux_fstatfs_type(file)?;
333 match fs_type {
334 ZFS_SUPER_MAGIC => Some(CowFilesystemKind::Zfs),
335 BTRFS_SUPER_MAGIC => {
336 let mount_options = linux_mount_options_for_path(path)?;
337 let inode_flags = linux_inode_flags(file)?;
338 classify_cow_filesystem(fs_type, Some(&mount_options), Some(inode_flags))
339 }
340 _ => None,
341 }
342 }
343
344 #[cfg(not(target_os = "linux"))]
345 fn probe_cow_filesystem(_path: &Path, _file: &File) -> Option<CowFilesystemKind> {
346 None
347 }
348
349 fn bind_encryption_for_existing(&mut self) -> Result<(), PagerError> {
359 if self.page_count().unwrap_or(0) == 0 {
360 return self.bind_encryption_for_new();
361 }
362 let (_, image) = self.newest_superblock()?;
363 let data = &image[..];
364 let has_marker = reddb_file::paged_encryption_marker_present(data);
365
366 let key = self.config.encryption.clone();
367 match (has_marker, key) {
368 (true, Some(key)) => {
369 let header_bytes =
370 reddb_file::paged_encryption_header_bytes(data).ok_or_else(|| {
371 PagerError::InvalidDatabase("encryption header parse failed".to_string())
372 })?;
373 let header = crate::storage::encryption::EncryptionHeader::from_bytes(header_bytes)
374 .map_err(|e| {
375 PagerError::InvalidDatabase(format!("encryption header parse failed: {e}"))
376 })?;
377 if !header.validate(&key) {
378 return Err(PagerError::InvalidKey);
379 }
380 let encryptor = crate::storage::encryption::PageEncryptor::new(key);
381 self.encryption = Some((encryptor, header));
382 Ok(())
383 }
384 (true, None) => Err(PagerError::EncryptionRequired),
385 (false, Some(_)) => Err(PagerError::PlainDatabaseRefusesKey),
386 (false, None) => Ok(()),
387 }
388 }
389
390 fn bind_encryption_for_new(&mut self) -> Result<(), PagerError> {
393 let Some(key) = self.config.encryption.clone() else {
394 return Ok(());
395 };
396 let header = crate::storage::encryption::EncryptionHeader::new(&key);
397 let encryptor = crate::storage::encryption::PageEncryptor::new(key);
398
399 if self.page_count().unwrap_or(0) > 0 {
402 let header_bytes = header.to_bytes();
403 self.commit_superblock(|image| {
404 reddb_file::write_paged_encryption_marker_and_header(image, &header_bytes)
405 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
406 })?;
407 }
408 self.encryption = Some((encryptor, header));
409 Ok(())
410 }
411
412 pub fn open_default<P: AsRef<Path>>(path: P) -> Result<Self, PagerError> {
414 Self::open(path, PagerConfig::default())
415 }
416
417 fn initialize(&self) -> Result<(), PagerError> {
419 if self.config.read_only {
420 return Err(PagerError::ReadOnly);
421 }
422
423 let initial_page_count = FIRST_ALLOCATABLE_PAGE;
427 let header_page = Page::new_header_page(initial_page_count);
428 self.header_write()?.page_count = initial_page_count;
429
430 self.write_page_raw(0, &header_page)?;
435 let mut metadata_page = Page::new(PageType::Header, 1);
436 metadata_page.update_checksum();
437 self.write_page_raw(1, &metadata_page)?;
438 let mut vault_page = Page::new(PageType::Vault, 2);
439 vault_page.update_checksum();
440 self.write_page_raw(2, &vault_page)?;
441 self.clear_dwb_zone()?;
442
443 let mut slot = superblock_slot_image(header_page.as_bytes());
447 self.write_superblock_slot(&mut slot, 0, 1)?;
448 self.write_superblock_slot(&mut slot, 1, 2)?;
449
450 self.sync()?;
452
453 Ok(())
454 }
455
456 fn header_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, DatabaseHeader>, PagerError> {
458 self.header.write().map_err(|_| PagerError::LockPoisoned)
459 }
460
461 fn header_read(&self) -> Result<std::sync::RwLockReadGuard<'_, DatabaseHeader>, PagerError> {
463 self.header.read().map_err(|_| PagerError::LockPoisoned)
464 }
465
466 fn freelist_write(&self) -> Result<std::sync::RwLockWriteGuard<'_, FreeList>, PagerError> {
468 self.freelist.write().map_err(|_| PagerError::LockPoisoned)
469 }
470
471 fn file_lock(&self) -> Result<std::sync::MutexGuard<'_, File>, PagerError> {
473 self.file.lock().map_err(|_| PagerError::LockPoisoned)
474 }
475
476 fn header_dirty_lock(&self) -> Result<std::sync::MutexGuard<'_, bool>, PagerError> {
478 self.header_dirty
479 .lock()
480 .map_err(|_| PagerError::LockPoisoned)
481 }
482
483 fn read_superblock_zone(&self) -> Result<[u8; SUPERBLOCK_ZONE_SIZE], PagerError> {
487 let mut file = self.file_lock()?;
488 let len = file.metadata().map(|meta| meta.len()).unwrap_or(0);
489 let mut zone = [0u8; SUPERBLOCK_ZONE_SIZE];
490 let readable = len.min(SUPERBLOCK_ZONE_SIZE as u64) as usize;
491 if readable > 0 {
492 file.seek(SeekFrom::Start(0))?;
493 file.read_exact(&mut zone[..readable])?;
494 }
495 Ok(zone)
496 }
497
498 fn newest_superblock(&self) -> Result<NewestSuperblock, PagerError> {
504 let zone = self.read_superblock_zone()?;
505 let selection = reddb_file::select_paged_superblock(&zone)
506 .ok_or_else(|| PagerError::SuperblockZoneUnrecoverable(self.path.clone()))?;
507
508 let start = selection.copy_index * SUPERBLOCK_SLOT_SIZE;
509 let mut image = [0u8; SUPERBLOCK_SLOT_SIZE];
510 image.copy_from_slice(&zone[start..start + SUPERBLOCK_SLOT_SIZE]);
511 Ok((selection, image))
512 }
513
514 fn ensure_phase3_dwb_zone_layout(&self) -> Result<(), PagerError> {
515 let (_, image) = self.newest_superblock()?;
516 let header = reddb_file::decode_database_header(&image)
517 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
518 if header.version < PHASE3_DWB_ZONE_FILE_VERSION {
519 return Err(PagerError::LegacyPagedStore {
520 path: self.path.clone(),
521 });
522 }
523 Ok(())
524 }
525
526 fn write_superblock_slot(
529 &self,
530 slot: &mut [u8; SUPERBLOCK_SLOT_SIZE],
531 copy_index: usize,
532 generation: u64,
533 ) -> Result<(), PagerError> {
534 if self.config.read_only {
535 return Err(PagerError::ReadOnly);
536 }
537 reddb_file::seal_paged_superblock_slot(slot, copy_index, generation)
538 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))?;
539
540 let mut file = self.file_lock()?;
541 file.seek(SeekFrom::Start(reddb_file::paged_superblock_slot_offset(
542 copy_index,
543 )))?;
544 file.write_all(slot)?;
545 file.sync_all()?;
546 Ok(())
547 }
548
549 fn commit_superblock(
555 &self,
556 mutate: impl FnOnce(&mut [u8; SUPERBLOCK_SLOT_SIZE]) -> Result<(), PagerError>,
557 ) -> Result<(), PagerError> {
558 if self.config.read_only {
559 return Err(PagerError::ReadOnly);
560 }
561 let (newest, mut image) = self.newest_superblock()?;
562 mutate(&mut image)?;
563 let next_copy = reddb_file::paged_superblock_next_copy(Some(newest));
564 self.write_superblock_slot(&mut image, next_copy, newest.generation.saturating_add(1))
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
579 .file_lock()?
580 .metadata()?
581 .len()
582 .div_ceil(PAGE_SIZE as u64);
583 let physical_pages = u32::try_from(physical_pages).map_err(|_| {
584 PagerError::InvalidDatabase("data file exceeds the page address space".into())
585 })?;
586 if decoded_header.page_count < physical_pages {
587 decoded_header.page_count = physical_pages;
588 if !self.config.read_only {
589 *self.header_dirty_lock()? = true;
590 }
591 }
592 let freelist_head = decoded_header.freelist_head;
593
594 {
595 let mut header = self.header_write()?;
596 *header = decoded_header;
597 }
598
599 {
601 let mut freelist = self.freelist_write()?;
602 *freelist = FreeList::from_header(freelist_head, 0);
603 }
604
605 Ok(())
606 }
607
608 fn write_header(&self) -> Result<(), PagerError> {
610 if self.config.read_only {
611 return Err(PagerError::ReadOnly);
612 }
613
614 let header = self.header_read()?.clone();
615 self.commit_superblock(|image| {
616 reddb_file::encode_database_header(image, &header)
617 .map_err(|err| PagerError::InvalidDatabase(err.to_string()))
618 })?;
619 *self.header_dirty_lock()? = false;
620
621 Ok(())
622 }
623
624 fn read_page_raw(&self, page_id: u32) -> Result<Page, PagerError> {
626 let mut file = self.file_lock()?;
627 let offset = (page_id as u64) * (PAGE_SIZE as u64);
628
629 file.seek(SeekFrom::Start(offset))?;
630
631 let mut buf = [0u8; PAGE_SIZE];
632 file.read_exact(&mut buf)?;
633
634 let page = Page::from_bytes(buf);
635
636 if self.config.verify_checksums && page_id != 0 {
640 page.verify_checksum()?;
641 }
642
643 Ok(page)
644 }
645
646 fn write_page_raw(&self, page_id: u32, page: &Page) -> Result<(), PagerError> {
648 if self.config.read_only {
649 return Err(PagerError::ReadOnly);
650 }
651
652 let mut file = self.file_lock()?;
653 let offset = (page_id as u64) * (PAGE_SIZE as u64);
654
655 file.seek(SeekFrom::Start(offset))?;
656 file.write_all(page.as_bytes())?;
657
658 Ok(())
659 }
660
661 pub fn read_page(&self, page_id: u32) -> Result<Page, PagerError> {
663 if let Some(page) = self.cache.get(page_id) {
665 return Ok(page);
666 }
667
668 let page = self.read_page_raw(page_id)?;
670
671 if let Some(dirty_page) = self.cache.insert(page_id, page.clone()) {
673 let evicted_id = dirty_page.page_id();
675 self.write_page_raw(evicted_id, &dirty_page)?;
676 }
677
678 Ok(page)
679 }
680
681 pub fn read_page_no_checksum(&self, page_id: u32) -> Result<Page, PagerError> {
686 if let Some(page) = self.cache.get(page_id) {
688 return Ok(page);
689 }
690
691 let mut file = self.file_lock()?;
693 let offset = (page_id as u64) * (PAGE_SIZE as u64);
694
695 file.seek(SeekFrom::Start(offset))?;
696
697 let mut buf = [0u8; PAGE_SIZE];
698 file.read_exact(&mut buf)?;
699 drop(file);
700
701 let page = Page::from_bytes(buf);
702
703 if let Some(dirty_page) = self.cache.insert(page_id, page.clone()) {
705 let evicted_id = dirty_page.page_id();
707 self.write_page_raw(evicted_id, &dirty_page)?;
708 }
709
710 Ok(page)
711 }
712
713 pub fn write_page(&self, page_id: u32, mut page: Page) -> Result<(), PagerError> {
715 if self.config.read_only {
716 return Err(PagerError::ReadOnly);
717 }
718
719 page.update_checksum();
721
722 if let Some(dirty_page) = self.cache.insert(page_id, page) {
727 let evicted_id = dirty_page.page_id();
728 self.write_page_raw(evicted_id, &dirty_page)?;
729 }
730 self.cache.mark_dirty(page_id);
731
732 Ok(())
733 }
734
735 pub fn read_page_decrypted(&self, page_id: u32) -> Result<Page, PagerError> {
740 if page_id == 0 || self.encryption.is_none() {
741 return self.read_page(page_id);
742 }
743 let raw = self.read_page_no_checksum(page_id)?;
744 let (enc, _) = self
745 .encryption
746 .as_ref()
747 .expect("encryption presence checked above");
748 let plaintext = enc
749 .decrypt(page_id, raw.as_bytes())
750 .map_err(|e| PagerError::InvalidDatabase(format!("decrypt page {page_id}: {e}")))?;
751 let mut buf = [0u8; PAGE_SIZE];
752 let n = plaintext.len().min(PAGE_SIZE);
753 buf[..n].copy_from_slice(&plaintext[..n]);
754 Ok(Page::from_bytes(buf))
755 }
756
757 pub fn write_page_encrypted(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
762 if page_id == 0 || self.encryption.is_none() {
763 return self.write_page(page_id, page);
764 }
765 const OVERHEAD: usize = crate::storage::encryption::page_encryptor::OVERHEAD;
768 let plaintext_len = PAGE_SIZE - OVERHEAD;
769 let plaintext = &page.as_bytes()[..plaintext_len];
770 let (enc, _) = self
771 .encryption
772 .as_ref()
773 .expect("encryption presence checked above");
774 let ciphertext = enc.encrypt(page_id, plaintext);
775 debug_assert_eq!(ciphertext.len(), PAGE_SIZE);
776 let mut buf = [0u8; PAGE_SIZE];
777 buf.copy_from_slice(&ciphertext);
778 let cipher_page = Page::from_bytes(buf);
779 self.write_page_no_checksum(page_id, cipher_page)
780 }
781
782 pub fn write_page_no_checksum(&self, page_id: u32, page: Page) -> Result<(), PagerError> {
787 if self.config.read_only {
788 return Err(PagerError::ReadOnly);
789 }
790
791 if let Some(dirty_page) = self.cache.insert(page_id, page) {
794 let evicted_id = dirty_page.page_id();
795 self.write_page_raw(evicted_id, &dirty_page)?;
796 }
797 self.cache.mark_dirty(page_id);
798
799 Ok(())
800 }
801
802 pub fn allocate_page(&self, page_type: PageType) -> Result<Page, PagerError> {
804 if self.config.read_only {
805 return Err(PagerError::ReadOnly);
806 }
807
808 let page_id = {
810 let mut freelist = self.freelist_write()?;
811 self.complete_allocation_fence()?;
814 if let Some(id) = freelist.allocate() {
815 self.allocation_reused
816 .store(true, std::sync::atomic::Ordering::Release);
817 id
818 } else if freelist.trunk_head() != 0 {
819 let trunk_id = freelist.trunk_head();
820
821 let trunk = self.read_page(trunk_id).map_err(|e| match e {
822 PagerError::PageNotFound(_) => {
823 PagerError::InvalidDatabase("Freelist trunk missing".to_string())
824 }
825 other => other,
826 })?;
827
828 freelist
829 .load_from_trunk(&trunk)
830 .map_err(|e| PagerError::InvalidDatabase(format!("Freelist: {}", e)))?;
831 let mut header = self.header_write()?;
832 header.freelist_head = freelist.trunk_head();
833 *self.header_dirty_lock()? = true;
834 drop(header);
835 self.allocation_fence_pending
840 .store(true, std::sync::atomic::Ordering::Release);
841 self.complete_allocation_fence()?;
842 let id = freelist.allocate().ok_or_else(|| {
843 PagerError::InvalidDatabase("Freelist empty after trunk load".to_string())
844 })?;
845 self.allocation_reused
846 .store(true, std::sync::atomic::Ordering::Release);
847 id
848 } else {
849 let mut header = self.header_write()?;
851 let id = header.page_count;
852 header.page_count += 1;
853 *self.header_dirty_lock()? = true;
854 id
855 }
856 };
857
858 let page = Page::new(page_type, page_id);
859
860 if let Some(dirty_page) = self.cache.insert(page_id, page.clone()) {
872 let evicted_id = dirty_page.page_id();
873 self.write_page_raw(evicted_id, &dirty_page)?;
874 }
875 self.cache.mark_dirty(page_id);
876
877 Ok(page)
878 }
879
880 pub fn reserve_contig_extent(&self, n_pages: u32) -> Result<super::ExtentId, PagerError> {
882 if self.config.read_only {
883 return Err(PagerError::ReadOnly);
884 }
885 if n_pages == 0 {
886 return Err(PagerError::InvalidDatabase(
887 "contiguous extent must reserve at least one page".to_string(),
888 ));
889 }
890
891 let start_page = {
892 let mut header = self.header_write()?;
893 let start = header.page_count;
894 header.page_count = header.page_count.checked_add(n_pages).ok_or_else(|| {
895 PagerError::InvalidDatabase("contiguous extent page count overflow".to_string())
896 })?;
897 *self.header_dirty_lock()? = true;
898 start
899 };
900
901 for page_id in start_page..start_page + n_pages {
902 let mut page = Page::new(PageType::Vector, page_id);
903 page.update_checksum();
904 if let Some(dirty_page) = self.cache.insert(page_id, page) {
905 let evicted_id = dirty_page.page_id();
906 self.write_page_raw(evicted_id, &dirty_page)?;
907 }
908 self.cache.mark_dirty(page_id);
909 }
910
911 Ok(super::ExtentId {
912 start_page,
913 n_pages,
914 })
915 }
916
917 pub fn free_page(&self, page_id: u32) -> Result<(), PagerError> {
919 if self.config.read_only {
920 return Err(PagerError::ReadOnly);
921 }
922
923 let mut deferred = self
924 .deferred_reclamation
925 .lock()
926 .map_err(|_| PagerError::LockPoisoned)?;
927 if deferred.enabled {
928 if page_id < FIRST_ALLOCATABLE_PAGE || page_id >= self.page_count()? {
929 return Err(PagerError::InvalidDatabase(
930 "invalid deferred free page".into(),
931 ));
932 }
933 deferred.pending.insert(page_id);
936 return Ok(());
937 }
938 drop(deferred);
939
940 self.cache.remove(page_id);
942
943 let mut freelist = self.freelist_write()?;
945 freelist.free(page_id);
946
947 *self.header_dirty_lock()? = true;
948
949 Ok(())
950 }
951
952 pub(crate) fn enable_deferred_reclamation(&self) -> Result<(), PagerError> {
953 self.deferred_reclamation
954 .lock()
955 .map_err(|_| PagerError::LockPoisoned)?
956 .enabled = true;
957 Ok(())
958 }
959
960 pub(crate) fn deferred_reclamation_pages(&self) -> Result<(Vec<u32>, Vec<u32>), PagerError> {
961 let deferred = self
962 .deferred_reclamation
963 .lock()
964 .map_err(|_| PagerError::LockPoisoned)?;
965 Ok((
966 deferred.pending.iter().copied().collect(),
967 deferred.published.iter().copied().collect(),
968 ))
969 }
970
971 pub(crate) fn restore_deferred_reclamation(&self, pages: &[u32]) -> Result<(), PagerError> {
972 let count = self.page_count()?;
973 if pages
974 .iter()
975 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= count)
976 {
977 return Err(PagerError::InvalidDatabase(
978 "invalid persisted deferred free page".into(),
979 ));
980 }
981 let mut deferred = self
982 .deferred_reclamation
983 .lock()
984 .map_err(|_| PagerError::LockPoisoned)?;
985 deferred.pending.extend(pages);
986 deferred.published.extend(pages);
987 Ok(())
988 }
989
990 pub(crate) fn complete_deferred_publication(
993 &self,
994 retained: &[u32],
995 released: &[u32],
996 ) -> Result<(), PagerError> {
997 let mut deferred = self
998 .deferred_reclamation
999 .lock()
1000 .map_err(|_| PagerError::LockPoisoned)?;
1001 if !released.is_empty() {
1002 self.free_retired_pages(released)?;
1003 }
1004 for id in released {
1005 deferred.pending.remove(id);
1006 }
1007 deferred.published = retained.iter().copied().collect();
1008 Ok(())
1009 }
1010
1011 pub(crate) fn free_retired_pages(&self, pages: &[u32]) -> Result<(), PagerError> {
1014 if self.config.read_only {
1015 return Err(PagerError::ReadOnly);
1016 }
1017 let page_count = self.page_count()?;
1018 if pages
1019 .iter()
1020 .any(|id| *id < FIRST_ALLOCATABLE_PAGE || *id >= page_count)
1021 {
1022 return Err(PagerError::InvalidDatabase(
1023 "invalid retired page range".into(),
1024 ));
1025 }
1026 let mut freelist = self.freelist_write()?;
1027 for id in pages {
1028 self.cache.remove(*id);
1029 }
1030 freelist.free_batch(pages);
1031 *self.header_dirty_lock()? = true;
1032 Ok(())
1033 }
1034
1035 pub fn free_page_count(&self) -> Result<u32, PagerError> {
1038 Ok(self.freelist_write()?.total_free())
1039 }
1040
1041 pub fn header(&self) -> Result<DatabaseHeader, PagerError> {
1042 Ok(self.header_read()?.clone())
1043 }
1044
1045 pub fn physical_header(&self) -> Result<PhysicalFileHeader, PagerError> {
1046 Ok(self.header_read()?.physical)
1047 }
1048
1049 pub fn update_physical_header(&self, physical: PhysicalFileHeader) -> Result<(), PagerError> {
1050 if self.config.read_only {
1051 return Err(PagerError::ReadOnly);
1052 }
1053
1054 let mut header = self.header_write()?;
1055 header.physical = physical;
1056 *self.header_dirty_lock()? = true;
1057 Ok(())
1058 }
1059
1060 pub fn page_count(&self) -> Result<u32, PagerError> {
1062 Ok(self.header_read()?.page_count)
1063 }
1064
1065 pub fn set_wal_writer(&self, wal: Arc<Mutex<crate::storage::wal::writer::WalWriter>>) {
1077 let mut slot = self.wal.write().unwrap_or_else(|p| p.into_inner());
1078 *slot = Some(wal);
1079 }
1080
1081 pub fn clear_wal_writer(&self) {
1083 let mut slot = self.wal.write().unwrap_or_else(|p| p.into_inner());
1084 *slot = None;
1085 }
1086
1087 pub fn has_wal_writer(&self) -> bool {
1089 self.wal.read().map(|s| s.is_some()).unwrap_or(false)
1090 }
1091
1092 pub fn flush(&self) -> Result<(), PagerError> {
1094 if self.config.read_only {
1095 return Ok(());
1096 }
1097
1098 let trunks = {
1100 let mut freelist = self.freelist_write()?;
1101 if freelist.is_dirty() {
1102 let mut header = self.header_write()?;
1103 let trunks = freelist.flush_to_trunks(0, || {
1104 let id = header.page_count;
1105 header.page_count += 1;
1106 id
1107 });
1108 header.freelist_head = freelist.trunk_head();
1109 *self.header_dirty_lock()? = true;
1110 freelist.mark_clean();
1111 trunks
1112 } else {
1113 Vec::new()
1114 }
1115 };
1116
1117 let mut dirty_pages = Vec::new();
1118 for trunk in trunks {
1119 let page_id = trunk.page_id();
1120 if let Some(evicted) = self.cache.insert(page_id, trunk) {
1121 dirty_pages.push((evicted.page_id(), evicted));
1124 }
1125 self.cache.mark_dirty(page_id);
1126 }
1127
1128 dirty_pages.extend(self.cache.flush_dirty());
1130 if !dirty_pages.is_empty() {
1131 let max_lsn = dirty_pages
1136 .iter()
1137 .filter_map(|(_, page)| page.header().ok().map(|h| h.lsn))
1138 .max()
1139 .unwrap_or(0);
1140 if max_lsn > 0 {
1141 if let Ok(slot) = self.wal.read() {
1142 if let Some(wal) = slot.as_ref() {
1143 let wal = Arc::clone(wal);
1144 drop(slot);
1148 let mut wal_guard = wal.lock().unwrap_or_else(|p| p.into_inner());
1149 wal_guard.flush_until(max_lsn).map_err(PagerError::Io)?;
1150 }
1151 }
1152 }
1153 self.write_pages_through_dwb(&dirty_pages)?;
1154 }
1155
1156 if *self.header_dirty_lock()? {
1158 self.write_header()?;
1159 }
1160
1161 Ok(())
1162 }
1163
1164 pub fn sync(&self) -> Result<(), PagerError> {
1166 self.flush()?;
1167
1168 if self
1169 .allocation_reused
1170 .swap(false, std::sync::atomic::Ordering::AcqRel)
1171 {
1172 if let Err(error) = self.write_header_and_sync() {
1176 self.allocation_reused
1177 .store(true, std::sync::atomic::Ordering::Release);
1178 return Err(error);
1179 }
1180 }
1181
1182 let file = self.file_lock()?;
1183 file.sync_all()?;
1184
1185 Ok(())
1186 }
1187
1188 pub fn cache_stats(&self) -> crate::storage::engine::page_cache::CacheStats {
1190 self.cache.stats()
1191 }
1192
1193 pub fn cache_len(&self) -> usize {
1196 self.cache.len()
1197 }
1198
1199 pub fn cache_capacity(&self) -> usize {
1202 self.cache.capacity()
1203 }
1204
1205 pub fn dirty_page_count(&self) -> usize {
1207 self.cache.dirty_count()
1208 }
1209
1210 pub fn dirty_fraction(&self) -> f64 {
1214 let capacity = self.cache.capacity().max(1) as f64;
1215 self.cache.dirty_count() as f64 / capacity
1216 }
1217
1218 pub fn flush_some_dirty(&self, max: usize) -> Result<usize, PagerError> {
1222 if self.config.read_only || max == 0 {
1223 return Ok(0);
1224 }
1225 let dirty_pages = self.cache.flush_some_dirty(max);
1226 if dirty_pages.is_empty() {
1227 return Ok(0);
1228 }
1229 let count = dirty_pages.len();
1230 for (page_id, page) in dirty_pages {
1237 self.write_page(page_id, page)?;
1238 }
1239 Ok(count)
1240 }
1241
1242 pub fn path(&self) -> &Path {
1244 &self.path
1245 }
1246
1247 pub fn is_read_only(&self) -> bool {
1249 self.config.read_only
1250 }
1251
1252 pub fn file_size(&self) -> Result<u64, PagerError> {
1254 let file = self.file_lock()?;
1255 Ok(file.metadata()?.len())
1256 }
1257
1258 pub fn prefetch_hint(&self, page_id: u32) {
1266 if let Ok(file) = self.file_lock() {
1267 let _ = super::super::prefetch::prefetch_page(&file, page_id as u64, PAGE_SIZE as u32);
1268 }
1269 }
1270
1271 fn dwb_path(db_path: &Path) -> PathBuf {
1275 reddb_file::layout::pager_dwb_shadow_path(db_path)
1276 }
1277
1278 fn dwb_zone_offset() -> u64 {
1279 DWB_ZONE_START_PAGE as u64 * PAGE_SIZE as u64
1280 }
1281
1282 fn write_dwb_zone_frame(&self, buf: &[u8]) -> Result<(), PagerError> {
1283 if buf.len() > DWB_ZONE_BYTES {
1284 return Err(PagerError::InvalidDatabase(
1285 "DWB frame exceeds the reserved in-file zone".to_string(),
1286 ));
1287 }
1288 let mut file = self.file_lock()?;
1289 file.seek(SeekFrom::Start(Self::dwb_zone_offset()))?;
1290 file.write_all(buf)?;
1291 file.sync_all()?;
1292 Ok(())
1293 }
1294
1295 fn clear_dwb_zone(&self) -> Result<(), PagerError> {
1297 let mut file = self.file_lock()?;
1298 for page_id in DWB_ZONE_START_PAGE..FIRST_ALLOCATABLE_PAGE {
1299 let mut page = Page::new(PageType::Header, page_id);
1300 page.update_checksum();
1301 file.seek(SeekFrom::Start(page_id as u64 * PAGE_SIZE as u64))?;
1302 file.write_all(page.as_bytes())?;
1303 }
1304 file.sync_all()?;
1305 Ok(())
1306 }
1307
1308 fn dwb_zone_is_clear(buf: &[u8]) -> bool {
1309 if buf.len() != DWB_ZONE_BYTES {
1310 return false;
1311 }
1312 for (idx, chunk) in buf.chunks_exact(PAGE_SIZE).enumerate() {
1313 let Ok(page) = Page::from_slice(chunk) else {
1314 return false;
1315 };
1316 if page.verify_checksum().is_err() {
1317 return false;
1318 }
1319 if page.page_id() != DWB_ZONE_START_PAGE + idx as u32 {
1320 return false;
1321 }
1322 if page.page_type().ok() != Some(PageType::Header) {
1323 return false;
1324 }
1325 }
1326 true
1327 }
1328
1329 pub fn read_manifest_page(&self) -> Result<Page, PagerError> {
1334 self.read_page(1)
1335 .map_err(|_| PagerError::ManifestZoneCorrupt(self.path.clone()))
1336 }
1337
1338 fn write_pages_through_dwb(&self, pages: &[(u32, Page)]) -> Result<(), PagerError> {
1345 if self.dwb_enabled {
1346 for chunk in pages.chunks(DWB_MAX_PAGES_PER_FRAME.max(1)) {
1347 let buf = reddb_file::encode_paged_dwb_frame(
1348 chunk
1349 .iter()
1350 .map(|(page_id, page)| (*page_id, page.as_bytes())),
1351 );
1352
1353 self.write_dwb_zone_frame(&buf)?;
1355
1356 for (page_id, page) in chunk {
1358 self.write_page_raw(*page_id, page)?;
1359 }
1360 {
1361 let file = self.file_lock()?;
1362 file.sync_all()?;
1363 }
1364
1365 self.clear_dwb_zone()?;
1367 }
1368
1369 Ok(())
1370 } else {
1371 for (page_id, page) in pages {
1373 self.write_page_raw(*page_id, page)?;
1374 }
1375 Ok(())
1376 }
1377 }
1378
1379 fn recover_from_dwb(&self) -> Result<(), PagerError> {
1384 let len = {
1385 let file = self.file_lock()?;
1386 file.metadata()?.len()
1387 };
1388 let zone_end = Self::dwb_zone_offset() + DWB_ZONE_BYTES as u64;
1389 if len < zone_end {
1390 return Ok(());
1391 }
1392
1393 let mut buf = vec![0u8; DWB_ZONE_BYTES];
1394 {
1395 let mut file = self.file_lock()?;
1396 file.seek(SeekFrom::Start(Self::dwb_zone_offset()))?;
1397 file.read_exact(&mut buf)?;
1398 }
1399 if buf.get(0..4) != Some(reddb_file::DWB_MAGIC.as_slice()) {
1400 if Self::dwb_zone_is_clear(&buf) {
1401 return Ok(());
1402 }
1403 self.clear_dwb_zone()?;
1404 return Ok(());
1405 }
1406
1407 let entries = match reddb_file::decode_paged_dwb_frame(&buf) {
1408 Ok(entries) => entries,
1409 Err(_) => {
1410 self.clear_dwb_zone()?;
1411 return Ok(());
1412 }
1413 };
1414
1415 for entry in entries {
1417 let page = Page::from_bytes(entry.page);
1418 self.write_page_raw(entry.page_id, &page)?;
1419 }
1420
1421 {
1423 let file = self.file_lock()?;
1424 file.sync_all()?;
1425 }
1426
1427 self.clear_dwb_zone()
1428 }
1429
1430 fn complete_allocation_fence(&self) -> Result<(), PagerError> {
1434 if self
1435 .allocation_fence_pending
1436 .load(std::sync::atomic::Ordering::Acquire)
1437 {
1438 self.write_header_and_sync()?;
1439 self.write_header_and_sync()?;
1440 self.allocation_fence_pending
1441 .store(false, std::sync::atomic::Ordering::Release);
1442 }
1443 Ok(())
1444 }
1445
1446 pub fn write_header_and_sync(&self) -> Result<(), PagerError> {
1448 self.write_header()?;
1449 let file = self.file_lock()?;
1450 file.sync_all()?;
1451 Ok(())
1452 }
1453
1454 pub fn set_checkpoint_in_progress(
1456 &self,
1457 in_progress: bool,
1458 target_lsn: u64,
1459 ) -> Result<(), PagerError> {
1460 let mut header = self.header_write()?;
1461 header.checkpoint_in_progress = in_progress;
1462 header.checkpoint_target_lsn = target_lsn;
1463 *self.header_dirty_lock()? = true;
1464 drop(header);
1465 self.write_header_and_sync()
1466 }
1467
1468 pub fn complete_checkpoint(&self, lsn: u64) -> Result<(), PagerError> {
1470 let mut header = self.header_write()?;
1471 header.checkpoint_lsn = lsn;
1472 header.checkpoint_in_progress = false;
1473 header.checkpoint_target_lsn = 0;
1474 *self.header_dirty_lock()? = true;
1475 drop(header);
1476 self.write_header_and_sync()
1477 }
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482 use super::*;
1483
1484 fn temp_db_path(name: &str) -> PathBuf {
1485 std::env::temp_dir().join(format!(
1486 "reddb-pager-{}-{}-{}.rdb",
1487 name,
1488 std::process::id(),
1489 crate::utils::now_unix_nanos()
1490 ))
1491 }
1492
1493 fn cleanup(path: &Path) {
1494 let _ = std::fs::remove_file(path);
1495 let _ = std::fs::remove_file(Pager::dwb_path(path));
1496 }
1497
1498 fn read_zone(path: &Path) -> Vec<u8> {
1499 let mut zone = vec![0u8; SUPERBLOCK_ZONE_SIZE];
1500 let mut file = File::open(path).expect("open() should succeed");
1501 file.read_exact(&mut zone)
1502 .expect("read_exact() should succeed");
1503 zone
1504 }
1505
1506 fn poke_slot(path: &Path, copy_index: usize, bytes: &[u8]) {
1508 let mut file = OpenOptions::new()
1509 .write(true)
1510 .open(path)
1511 .expect("open() should succeed");
1512 file.seek(SeekFrom::Start(reddb_file::paged_superblock_slot_offset(
1513 copy_index,
1514 )))
1515 .expect("operation should succeed");
1516 file.write_all(bytes).expect("write_all() should succeed");
1517 file.sync_all().expect("sync_all() should succeed");
1518 }
1519
1520 fn stamp_pre_phase3_layout(path: &Path, page_count: u32) {
1521 let zone = read_zone(path);
1522 for copy_index in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1523 let start = copy_index * SUPERBLOCK_SLOT_SIZE;
1524 let mut slot = zone[start..start + SUPERBLOCK_SLOT_SIZE].to_vec();
1525 let generation = reddb_file::paged_superblock_slot_generation(&slot, copy_index)
1526 .expect("fresh store should publish both slots");
1527 reddb_file::set_database_header_version(&mut slot, PHASE3_DWB_ZONE_FILE_VERSION - 1)
1528 .expect("set_database_header_version() should succeed");
1529 reddb_file::set_database_header_page_count(&mut slot, page_count)
1530 .expect("set_database_header_page_count() should succeed");
1531 reddb_file::seal_paged_superblock_slot(&mut slot, copy_index, generation)
1532 .expect("seal_paged_superblock_slot() should succeed");
1533 poke_slot(path, copy_index, &slot);
1534 }
1535 }
1536
1537 #[test]
1538 fn open_refuses_future_database_version() {
1539 let path = temp_db_path("future-version");
1540 let pager = Pager::open_default(&path).expect("open_default() should succeed");
1541 drop(pager);
1542
1543 let selection = reddb_file::select_paged_superblock(&read_zone(&path))
1547 .expect("operation should succeed");
1548 let mut slot = vec![0u8; SUPERBLOCK_SLOT_SIZE];
1549 reddb_file::init_database_header_page(&mut slot, 1)
1550 .expect("init_database_header_page() should succeed");
1551 reddb_file::set_database_header_version(&mut slot, reddb_file::PAGE_FILE_VERSION + 1)
1552 .expect("operation should succeed");
1553 reddb_file::seal_paged_superblock_slot(
1554 &mut slot,
1555 selection.copy_index,
1556 selection.generation + 1,
1557 )
1558 .expect("operation should succeed");
1559 poke_slot(&path, selection.copy_index, &slot);
1560
1561 let err = match Pager::open_default(&path) {
1562 Ok(_) => panic!("future database version should be rejected"),
1563 Err(err) => err,
1564 };
1565 match err {
1566 PagerError::InvalidDatabase(msg) => {
1567 assert!(msg.contains("newer than supported"), "{msg}");
1568 }
1569 other => panic!("expected InvalidDatabase, got {other:?}"),
1570 }
1571
1572 cleanup(&path);
1573 }
1574
1575 #[test]
1576 fn a_fresh_store_publishes_both_superblock_copies() {
1577 let path = temp_db_path("fresh-pair");
1580 let pager = Pager::open_default(&path).expect("open_default() should succeed");
1581 drop(pager);
1582
1583 let zone = read_zone(&path);
1584 for copy_index in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1585 let start = copy_index * SUPERBLOCK_SLOT_SIZE;
1586 assert!(
1587 reddb_file::paged_superblock_slot_generation(
1588 &zone[start..start + SUPERBLOCK_SLOT_SIZE],
1589 copy_index
1590 )
1591 .is_some(),
1592 "copy {copy_index} must be valid on a fresh store"
1593 );
1594 }
1595 cleanup(&path);
1596 }
1597
1598 #[test]
1599 fn an_update_writes_the_stale_copy_and_leaves_the_newest_one_intact() {
1600 let path = temp_db_path("ping-pong");
1601 let pager = Pager::open_default(&path).expect("open_default() should succeed");
1602 let before = reddb_file::select_paged_superblock(&read_zone(&path))
1603 .expect("operation should succeed");
1604
1605 pager
1606 .allocate_page(PageType::BTreeLeaf)
1607 .expect("allocate_page() should succeed");
1608 pager.sync().expect("sync() should succeed");
1609
1610 let zone = read_zone(&path);
1611 let after = reddb_file::select_paged_superblock(&zone)
1612 .expect("select_paged_superblock() should succeed");
1613 assert_ne!(
1614 after.copy_index, before.copy_index,
1615 "an update must target the stale copy"
1616 );
1617 assert!(after.generation > before.generation);
1618
1619 let start = before.copy_index * SUPERBLOCK_SLOT_SIZE;
1623 assert_eq!(
1624 reddb_file::paged_superblock_slot_generation(
1625 &zone[start..start + SUPERBLOCK_SLOT_SIZE],
1626 before.copy_index
1627 ),
1628 Some(before.generation)
1629 );
1630
1631 drop(pager);
1632 cleanup(&path);
1633 }
1634
1635 #[test]
1636 fn failed_trunk_fence_is_completed_before_retrying_allocation() {
1637 let path = temp_db_path("retry-trunk-fence");
1638 let pager = Pager::open_default(&path).expect("pager");
1639 let mut pages = Vec::new();
1640 for _ in 0..8 {
1641 pages.push(pager.allocate_page(PageType::BTreeLeaf).unwrap().page_id());
1642 }
1643 pager.sync().unwrap();
1644 for id in pages {
1645 pager.free_page(id).unwrap();
1646 }
1647 pager.sync().unwrap();
1648 let trunk = pager.header().unwrap().freelist_head;
1649 assert_ne!(trunk, 0);
1650
1651 let writable = {
1654 let mut file = pager.file_lock().unwrap();
1655 std::mem::replace(&mut *file, File::open(&path).unwrap())
1656 };
1657 assert!(pager.allocate_page(PageType::BTreeLeaf).is_err());
1658 *pager.file_lock().unwrap() = writable;
1659 pager.allocate_page(PageType::BTreeLeaf).unwrap();
1660
1661 let zone = read_zone(&path);
1662 for copy in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1663 let start = copy * SUPERBLOCK_SLOT_SIZE;
1664 let header =
1665 reddb_file::decode_database_header(&zone[start..start + SUPERBLOCK_SLOT_SIZE])
1666 .unwrap();
1667 assert_ne!(
1668 header.freelist_head, trunk,
1669 "copy {copy} still owns consumed trunk"
1670 );
1671 }
1672 drop(pager);
1673 cleanup(&path);
1674 }
1675
1676 #[test]
1677 fn sibling_superblock_recovery_does_not_reallocate_live_reused_pages() {
1678 let path = temp_db_path("reused-allocator-pair");
1679 let pager = Pager::open_default(&path).expect("pager");
1680 let mut pages = Vec::new();
1681 for _ in 0..32 {
1682 pages.push(
1683 pager
1684 .allocate_page(PageType::BTreeLeaf)
1685 .expect("allocate")
1686 .page_id(),
1687 );
1688 }
1689 pager.sync().expect("seed");
1690 for id in pages {
1691 pager.free_page(id).expect("free");
1692 }
1693 pager.sync().expect("publish free list");
1694 let mut live = std::collections::HashSet::new();
1695 for _ in 0..8 {
1696 let page = pager.allocate_page(PageType::BTreeLeaf).expect("reuse");
1697 live.insert(page.page_id());
1698 pager
1699 .write_page(page.page_id(), page)
1700 .expect("write valid page");
1701 }
1702 pager.sync().expect("publish reused ownership");
1703 drop(pager);
1704 let selection = reddb_file::select_paged_superblock(&read_zone(&path)).expect("superblock");
1705 poke_slot(&path, selection.copy_index, &[0; SUPERBLOCK_SLOT_SIZE]);
1706 let recovered = Pager::open_default(&path).expect("sibling recovery");
1707 for _ in 0..64 {
1708 let id = recovered
1709 .allocate_page(PageType::BTreeLeaf)
1710 .expect("allocate after recovery")
1711 .page_id();
1712 assert!(
1713 !live.contains(&id),
1714 "sibling allocator still considers live page {id} free"
1715 );
1716 }
1717 drop(recovered);
1718 cleanup(&path);
1719 }
1720
1721 #[test]
1722 fn open_recovers_from_the_older_copy_when_the_newest_is_torn() {
1723 let path = temp_db_path("torn-newest");
1724 {
1725 let pager = Pager::open_default(&path).expect("open_default() should succeed");
1726 pager
1727 .allocate_page(PageType::BTreeLeaf)
1728 .expect("allocate_page() should succeed");
1729 pager.sync().expect("sync() should succeed");
1730 }
1731
1732 let newest = reddb_file::select_paged_superblock(&read_zone(&path))
1733 .expect("operation should succeed");
1734 let mut torn = vec![0u8; SUPERBLOCK_SLOT_SIZE];
1736 torn.copy_from_slice(
1737 &read_zone(&path)[newest.copy_index * SUPERBLOCK_SLOT_SIZE
1738 ..newest.copy_index * SUPERBLOCK_SLOT_SIZE + SUPERBLOCK_SLOT_SIZE],
1739 );
1740 torn[64] ^= 0xFF;
1741 poke_slot(&path, newest.copy_index, &torn);
1742
1743 let pager = Pager::open_default(&path).expect("older copy must root the store");
1744 assert!(pager.page_count().expect("page_count() should succeed") >= 3);
1745 drop(pager);
1746 cleanup(&path);
1747 }
1748
1749 #[test]
1750 fn open_refuses_didactically_when_both_superblock_copies_are_invalid() {
1751 let path = temp_db_path("both-invalid");
1752 {
1753 let _ = Pager::open_default(&path).expect("open_default() should succeed");
1754 }
1755
1756 let zone = read_zone(&path);
1757 for copy_index in 0..reddb_file::PAGED_SUPERBLOCK_SLOT_COUNT {
1758 let start = copy_index * SUPERBLOCK_SLOT_SIZE;
1759 let mut slot = zone[start..start + SUPERBLOCK_SLOT_SIZE].to_vec();
1760 slot[64] ^= 0xFF;
1761 poke_slot(&path, copy_index, &slot);
1762 }
1763
1764 let err = match Pager::open_default(&path) {
1765 Ok(_) => panic!("a rootless store must not open"),
1766 Err(err) => err,
1767 };
1768 let rendered = err.to_string();
1769 assert!(matches!(err, PagerError::SuperblockZoneUnrecoverable(_)));
1770 assert!(rendered.contains("superblock zone"), "{rendered}");
1771 assert!(rendered.contains("salvage"), "{rendered}");
1772
1773 cleanup(&path);
1774 }
1775
1776 #[test]
1777 fn open_refuses_a_legacy_sidecar_store_and_names_the_migration_tool() {
1778 let path = temp_db_path("legacy-sidecar");
1779 {
1780 let _ = Pager::open_default(&path).expect("open_default() should succeed");
1781 }
1782
1783 let sidecar = reddb_file::layout::retired::pager_header_path_v0(&path);
1784 std::fs::write(&sidecar, b"legacy header shadow").expect("write() should succeed");
1785
1786 let err = match Pager::open_default(&path) {
1787 Ok(_) => panic!("legacy stores need migration first"),
1788 Err(err) => err,
1789 };
1790 let rendered = err.to_string();
1791 assert!(matches!(err, PagerError::LegacySidecarStore { .. }));
1792 assert!(rendered.contains("migration tool"), "{rendered}");
1793
1794 let _ = std::fs::remove_file(&sidecar);
1795 cleanup(&path);
1796 }
1797
1798 #[test]
1799 fn pre_phase3_large_store_without_sidecar_routes_to_migration() {
1800 let path = temp_db_path("pre-phase3-large");
1801 {
1802 let _ = Pager::open_default(&path).expect("open_default() should succeed");
1803 }
1804 stamp_pre_phase3_layout(&path, FIRST_ALLOCATABLE_PAGE);
1805
1806 let err = match Pager::open_default(&path) {
1807 Ok(_) => panic!("pre-phase-3 stores need migration first"),
1808 Err(err) => err,
1809 };
1810 let rendered = err.to_string();
1811 assert!(matches!(err, PagerError::LegacyPagedStore { .. }));
1812 assert!(rendered.contains("migration tool"), "{rendered}");
1813
1814 cleanup(&path);
1815 }
1816
1817 #[test]
1818 fn pre_phase3_small_store_without_sidecar_routes_to_migration() {
1819 let path = temp_db_path("pre-phase3-small");
1820 {
1821 let _ = Pager::open_default(&path).expect("open_default() should succeed");
1822 }
1823 stamp_pre_phase3_layout(&path, 3);
1824 OpenOptions::new()
1825 .write(true)
1826 .open(&path)
1827 .expect("open() should succeed")
1828 .set_len(3 * PAGE_SIZE as u64)
1829 .expect("set_len() should succeed");
1830
1831 let err = match Pager::open_default(&path) {
1832 Ok(_) => panic!("pre-phase-3 stores need migration first"),
1833 Err(err) => err,
1834 };
1835 assert!(matches!(err, PagerError::LegacyPagedStore { .. }));
1836
1837 cleanup(&path);
1838 }
1839
1840 #[test]
1841 fn no_phase_one_sidecar_survives_a_create_write_reopen_cycle() {
1842 let path = temp_db_path("census");
1843 {
1844 let pager = Pager::open_default(&path).expect("open_default() should succeed");
1845 let page = pager
1846 .allocate_page(PageType::BTreeLeaf)
1847 .expect("allocate_page() should succeed");
1848 pager
1849 .write_page(page.page_id(), page)
1850 .expect("operation should succeed");
1851 pager.sync().expect("sync() should succeed");
1852 }
1853 {
1854 let pager = Pager::open_default(&path).expect("open_default() should succeed");
1855 pager.sync().expect("sync() should succeed");
1856 }
1857
1858 assert_eq!(
1859 reddb_file::layout::retired::first_present_phase1_sidecar(&path),
1860 None,
1861 "the pager must never create a retired phase-1 sidecar"
1862 );
1863 cleanup(&path);
1864 }
1865}