use crate::Result;
use crate::crypto::keys::DerivedKey;
use crate::errors::PagedbError;
use crate::pager::format::structural_header::decode_main_db_header;
use crate::pager::header::{ActiveSlot, commit_header, read_header_slot};
use crate::vfs::Vfs;
use crate::vfs::types::OpenMode;
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AnchorTestFault {
Commit,
RefreshAfterHeader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HeaderCursor {
pub slot: ActiveSlot,
pub seq: u64,
}
impl HeaderCursor {
pub fn next_seq(&self) -> Result<u64> {
self.seq
.checked_add(1)
.ok_or_else(|| PagedbError::arithmetic_overflow("header sequence"))
}
}
pub(crate) struct LiveHeader {
pub hk: std::sync::Arc<parking_lot::RwLock<DerivedKey>>,
pub cursor: HeaderCursor,
}
pub(crate) async fn refresh_anchor<V: Vfs>(
vfs: &V,
path: &str,
hk: &DerivedKey,
cursor: HeaderCursor,
anchor: u64,
page_size: usize,
) -> Result<HeaderCursor> {
let page_size_u64 = u64::try_from(page_size)
.map_err(|_| PagedbError::arithmetic_overflow("header page size"))?;
let offset = cursor.slot.page_id().saturating_mul(page_size_u64);
let mut file = vfs.open(path, OpenMode::Read).await?;
let mut buf = vec![0u8; page_size];
read_header_slot(&mut file, offset, &mut buf).await?;
drop(file);
let mut fields = decode_main_db_header(&buf, hk, page_size)?;
if anchor < fields.counter_anchor {
return Err(PagedbError::structural_header_invalid(
"main.db",
"counter_anchor",
));
}
fields.counter_anchor = anchor;
fields.seq = cursor.next_seq()?;
let slot = commit_header(vfs, path, hk, &fields, cursor.slot, page_size).await?;
Ok(HeaderCursor {
slot,
seq: fields.seq,
})
}
#[cfg(test)]
mod tests {
use super::AnchorTestFault;
use crate::errors::PagedbError;
use crate::vfs::memory::MemVfs;
use crate::vfs::types::OpenMode;
use crate::vfs::{Vfs, VfsFile};
use crate::{CommitId, Db, OpenOptions, RealmId};
const PAGE: usize = 4096;
const KEK: [u8; 32] = [0x6Au8; 32];
const REALM: RealmId = RealmId::new([0xC3u8; 16]);
async fn seeded_db(vfs: MemVfs) -> Db<MemVfs> {
let db = Db::open_internal_with_options(
vfs,
KEK,
PAGE,
REALM,
OpenOptions::default().with_anchor_budget(8),
)
.await
.unwrap();
let mut writer = db.begin_write().await.unwrap();
for index in 0u32..16 {
writer
.put(format!("k-{index:03}").as_bytes(), b"anchored")
.await
.unwrap();
}
writer.commit().await.unwrap();
db
}
async fn assert_store_intact(vfs: MemVfs, expected_commit: CommitId) {
let reopened = Db::open_existing(vfs, KEK, PAGE, REALM).await.unwrap();
assert_eq!(
reopened.latest_commit(),
expected_commit,
"an anchor refresh must not move the committed state"
);
let reader = reopened.begin_read().await.unwrap();
for index in 0u32..16 {
let key = format!("k-{index:03}");
assert_eq!(
reader.get(key.as_bytes()).await.unwrap().as_deref(),
Some(b"anchored".as_slice()),
"value {key} lost across an anchor refresh"
);
}
}
#[tokio::test(flavor = "current_thread")]
async fn refreshing_the_anchor_publishes_nothing() {
let vfs = MemVfs::new();
let db = seeded_db(vfs.clone()).await;
let committed = db.latest_commit();
let before = db.pager.durable_anchor();
for _ in 0..3 {
db.pager.refresh_main_anchor().await.unwrap();
}
assert!(
db.pager.durable_anchor() >= before,
"a refresh may never lower the durable anchor"
);
drop(db);
assert_store_intact(vfs, committed).await;
}
#[tokio::test(flavor = "current_thread")]
async fn a_torn_refresh_reopens_at_the_same_commit() {
for (refreshes_before_tear, tear_authoritative) in
[(1u32, false), (2, false), (1, true), (2, true)]
{
let vfs = MemVfs::new();
let db = seeded_db(vfs.clone()).await;
let committed = db.latest_commit();
for _ in 0..refreshes_before_tear {
db.pager.refresh_main_anchor().await.unwrap();
}
let live = db.pager.header_cursor().unwrap().slot;
let target = if tear_authoritative {
live
} else {
live.other()
};
drop(db);
let offset = target.page_id() * PAGE as u64;
let mut file = vfs.open("/main.db", OpenMode::ReadWrite).await.unwrap();
file.write_at(offset, &[0u8; PAGE]).await.unwrap();
file.sync().await.unwrap();
drop(file);
assert_store_intact(vfs, committed).await;
}
}
#[tokio::test(flavor = "current_thread")]
async fn a_refresh_interrupted_after_its_header_reopens_at_the_same_commit() {
let vfs = MemVfs::new();
let db = seeded_db(vfs.clone()).await;
let committed = db.latest_commit();
let durable_before = db.pager.durable_anchor();
db.pager
.interrupt_anchor_after(AnchorTestFault::RefreshAfterHeader);
let error = db
.pager
.refresh_main_anchor()
.await
.expect_err("the armed interruption must abort the refresh");
assert!(matches!(error, PagedbError::Io(_)));
assert_eq!(
db.pager.durable_anchor(),
durable_before,
"an aborted refresh must not let the generator issue past an anchor it never acknowledged"
);
drop(db);
assert_store_intact(vfs.clone(), committed).await;
let reopened = Db::open_existing(vfs, KEK, PAGE, REALM).await.unwrap();
let report = crate::recovery::deep_walk::run_deep_walk(&reopened)
.await
.unwrap();
assert!(
report.is_clean(),
"torn refresh left a store the writer must repair: {report:?}"
);
}
}