Skip to main content

reddb_engine/storage/
pager_io.rs

1//! Byte I/O shared by the native pager and exclusively owned browser handles.
2//!
3//! The owner supplies locking, read-only policy, WAL-first ordering, allocation
4//! fences and encryption. It must stop using a handle after failed I/O and
5//! reopen/recover before resuming. These functions do not form a database API.
6
7#![deny(
8    clippy::unwrap_used,
9    clippy::too_many_lines,
10    clippy::cast_possible_truncation
11)]
12
13use super::page::{Page, PageError, PageType, PAGE_SIZE};
14use reddb_file::{VfsFile, PAGED_SUPERBLOCK_SLOT_SIZE, PAGED_SUPERBLOCK_ZONE_SIZE};
15use std::io::{self, SeekFrom};
16
17pub const DWB_ZONE_START_PAGE: u32 = 3;
18pub const DWB_ZONE_PAGES: u32 = 64;
19pub const DWB_ZONE_BYTES: usize = DWB_ZONE_PAGES as usize * PAGE_SIZE;
20pub const FIRST_ALLOCATABLE_PAGE: u32 = DWB_ZONE_START_PAGE + DWB_ZONE_PAGES;
21pub const DWB_MAX_PAGES_PER_FRAME: usize =
22    (DWB_ZONE_BYTES - reddb_file::PAGED_DWB_HEADER_SIZE) / reddb_file::PAGED_DWB_ENTRY_SIZE;
23
24#[derive(Debug)]
25pub enum PagerIoError {
26    Io(io::Error),
27    Page(PageError),
28    InvalidDatabase(String),
29}
30
31impl std::fmt::Display for PagerIoError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Self::Io(error) => write!(f, "I/O error: {error}"),
35            Self::Page(error) => write!(f, "Page error: {error}"),
36            Self::InvalidDatabase(message) => write!(f, "Invalid database: {message}"),
37        }
38    }
39}
40
41impl std::error::Error for PagerIoError {
42    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
43        match self {
44            Self::Io(error) => Some(error),
45            Self::Page(error) => Some(error),
46            Self::InvalidDatabase(_) => None,
47        }
48    }
49}
50
51impl From<io::Error> for PagerIoError {
52    fn from(error: io::Error) -> Self {
53        Self::Io(error)
54    }
55}
56
57impl From<PageError> for PagerIoError {
58    fn from(error: PageError) -> Self {
59        Self::Page(error)
60    }
61}
62
63pub fn read_page<F: VfsFile>(
64    file: &mut F,
65    page_id: u32,
66    verify_checksum: bool,
67) -> Result<Page, PagerIoError> {
68    file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
69    let mut buffer = [0u8; PAGE_SIZE];
70    file.read_exact(&mut buffer)?;
71    let page = Page::from_bytes(buffer);
72    // Page 0 contains independently sealed superblocks, not a page checksum.
73    if verify_checksum && page_id != 0 {
74        page.verify_checksum()?;
75    }
76    Ok(page)
77}
78
79/// Raw write, without a durability barrier. The owner controls publication.
80pub fn write_page<F: VfsFile>(file: &mut F, page_id: u32, page: &Page) -> Result<(), PagerIoError> {
81    file.seek(SeekFrom::Start(u64::from(page_id) * PAGE_SIZE as u64))?;
82    file.write_all(page.as_bytes())?;
83    Ok(())
84}
85
86pub fn read_superblock_zone<F: VfsFile>(
87    file: &mut F,
88) -> Result<[u8; PAGED_SUPERBLOCK_ZONE_SIZE], PagerIoError> {
89    let len = file.file_len()?;
90    let mut zone = [0u8; PAGED_SUPERBLOCK_ZONE_SIZE];
91    let readable = usize::try_from(len.min(PAGED_SUPERBLOCK_ZONE_SIZE as u64))
92        .expect("readable prefix is bounded by the superblock zone");
93    if readable > 0 {
94        file.seek(SeekFrom::Start(0))?;
95        file.read_exact(&mut zone[..readable])?;
96    }
97    Ok(zone)
98}
99
100/// Seal and sync only the stale slot; the owner selects it and its generation.
101pub fn write_superblock_slot<F: VfsFile>(
102    file: &mut F,
103    slot: &mut [u8; PAGED_SUPERBLOCK_SLOT_SIZE],
104    copy_index: usize,
105    generation: u64,
106) -> Result<(), PagerIoError> {
107    reddb_file::seal_paged_superblock_slot(slot, copy_index, generation)
108        .map_err(|error| PagerIoError::InvalidDatabase(error.to_string()))?;
109    file.seek(SeekFrom::Start(reddb_file::paged_superblock_slot_offset(
110        copy_index,
111    )))?;
112    file.write_all(slot)?;
113    file.sync_all()?;
114    Ok(())
115}
116
117pub fn dwb_zone_offset() -> u64 {
118    u64::from(DWB_ZONE_START_PAGE) * PAGE_SIZE as u64
119}
120
121/// Clear the in-file DWB zone with valid reserved pages and sync it.
122pub fn clear_dwb_zone<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
123    for page_id in DWB_ZONE_START_PAGE..FIRST_ALLOCATABLE_PAGE {
124        let mut page = Page::new(PageType::Header, page_id);
125        page.update_checksum();
126        write_page(file, page_id, &page)?;
127    }
128    file.sync_all()?;
129    Ok(())
130}
131
132pub fn dwb_zone_is_clear(buffer: &[u8]) -> bool {
133    if buffer.len() != DWB_ZONE_BYTES {
134        return false;
135    }
136    for (index, chunk) in buffer.chunks_exact(PAGE_SIZE).enumerate() {
137        let Ok(page) = Page::from_slice(chunk) else {
138            return false;
139        };
140        if page.verify_checksum().is_err()
141            || page.page_id()
142                != DWB_ZONE_START_PAGE
143                    + u32::try_from(index).expect("DWB page index is bounded by its zone")
144            || page.page_type().ok() != Some(PageType::Header)
145        {
146            return false;
147        }
148    }
149    true
150}
151
152/// The caller must make the batch's WAL durable before entering this function.
153/// With DWB enabled: stage+sync and home pages+sync for each bounded frame,
154/// then clear+sync once. With DWB disabled, the caller's proven atomic-write
155/// policy applies.
156pub fn write_pages<F: VfsFile>(
157    file: &mut F,
158    pages: &[(u32, Page)],
159    double_write: bool,
160) -> Result<(), PagerIoError> {
161    if !double_write {
162        for (page_id, page) in pages {
163            write_page(file, *page_id, page)?;
164        }
165        return Ok(());
166    }
167
168    for chunk in pages.chunks(DWB_MAX_PAGES_PER_FRAME.max(1)) {
169        let buffer = reddb_file::encode_paged_dwb_frame(
170            chunk
171                .iter()
172                .map(|(page_id, page)| (*page_id, page.as_bytes())),
173        );
174        assert!(buffer.len() <= DWB_ZONE_BYTES, "DWB frame fits its zone");
175        file.seek(SeekFrom::Start(dwb_zone_offset()))?;
176        file.write_all(&buffer)?;
177        file.sync_all()?;
178        for (page_id, page) in chunk {
179            write_page(file, *page_id, page)?;
180        }
181        file.sync_all()?;
182    }
183    // Each frame's home pages are durable before the next frame replaces its
184    // staging image. An interrupted replacement can replay the old frame or
185    // discard an incomplete new one: neither can lose those durable homes.
186    // Only the final frame needs clearing before returning to the owner.
187    if !pages.is_empty() {
188        clear_dwb_zone(file)?;
189    }
190    Ok(())
191}
192
193/// Replay a complete staged DWB frame before allowing page access. Incomplete
194/// staging is discarded as on the native path: home writes start only after
195/// the complete frame crossed its barrier. No database root is recreated.
196pub fn recover_from_dwb<F: VfsFile>(file: &mut F) -> Result<(), PagerIoError> {
197    if file.file_len()? < dwb_zone_offset() + DWB_ZONE_BYTES as u64 {
198        return Ok(());
199    }
200    let mut buffer = vec![0u8; DWB_ZONE_BYTES];
201    file.seek(SeekFrom::Start(dwb_zone_offset()))?;
202    file.read_exact(&mut buffer)?;
203    if buffer.get(..4) != Some(reddb_file::DWB_MAGIC.as_slice()) {
204        if dwb_zone_is_clear(&buffer) {
205            return Ok(());
206        }
207        return clear_dwb_zone(file);
208    }
209    let entries = match reddb_file::decode_paged_dwb_frame(&buffer) {
210        Ok(entries) => entries,
211        Err(_) => return clear_dwb_zone(file),
212    };
213    for entry in entries {
214        write_page(file, entry.page_id, &Page::from_bytes(entry.page))?;
215    }
216    file.sync_all()?;
217    clear_dwb_zone(file)
218}