use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
use crate::crypto::CipherId;
use crate::errors::PagedbError;
use crate::options::OpenOptions;
use crate::pager::Pager;
use crate::pager::structural_header::MainDbHeaderFields;
use crate::vfs::Vfs;
use crate::{CommitId, RealmId, Result};
use super::super::mode::DbMode;
use super::super::policy::ReaderStallPolicy;
use super::segment::SegmentReconciliation;
use crate::txn::write::SegmentSideEffect;
#[derive(Debug, Clone)]
pub(crate) struct PendingTombstone {
pub segment_id: [u8; 16],
pub commit_id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PendingKeyRetirement {
pub epoch: u64,
pub cipher_id: CipherId,
}
#[derive(Debug)]
pub(crate) struct TrackedReader {
pub entry_id: u64,
pub commit_id: CommitId,
#[allow(dead_code)]
pub root_page_id: u64,
pub next_page_id: u64,
pub catalog_root_page_id: u64,
pub non_abortable: bool,
}
#[cfg(test)]
#[derive(Default)]
pub(crate) struct VisibilityTestHook {
pub(crate) reader_selected: tokio::sync::Notify,
pub(crate) allow_reader_registration: tokio::sync::Notify,
pub(crate) reader_registered: tokio::sync::Notify,
pub(crate) reader_may_read: tokio::sync::Notify,
pub(crate) writer_waiting: tokio::sync::Notify,
pub(crate) apply_window_entered: tokio::sync::Notify,
pub(crate) apply_window_release: tokio::sync::Notify,
}
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RekeyTestFault {
Intent,
MainPagesTargetReadable,
HeaderTargetPublished,
MainDone,
SegmentsPending,
SegmentSeal,
ProgressRowCommit,
CatalogSwapEffects,
ProgressDeletion,
}
pub(crate) struct WriterState {
pub root_page_id: u64,
pub next_page_id: u64,
pub latest_commit_id: u64,
pub catalog_root_page_id: u64,
pub catalog_root_txn_id: u64,
pub free_list_root_page_id: u64,
pub commit_history_root_page_id: u64,
pub commit_history_root_version: u64,
pub commit_history_count: Option<u64>,
pub pending_apply_journal_id: [u8; 16],
pub restore_mode: u8,
pub commit_retain_policy_tag: u8,
pub commit_retain_policy_value: u64,
}
pub struct Db<V: Vfs + Clone> {
pub(crate) pager: Arc<Pager<V>>,
pub(crate) realm_id: RealmId,
pub(crate) page_size: usize,
pub(crate) hk: Arc<parking_lot::RwLock<crate::crypto::keys::DerivedKey>>,
pub(crate) main_db_path: String,
pub(crate) vfs: Arc<V>,
pub(crate) writer: Arc<AsyncMutex<WriterState>>,
#[cfg(not(target_arch = "wasm32"))]
pub(crate) apply_gate: AsyncMutex<()>,
pub(crate) visibility_gate: AsyncRwLock<()>,
pub(crate) tracked_readers: parking_lot::Mutex<Vec<TrackedReader>>,
pub(crate) reader_seq: AtomicU64,
pub(crate) stall_policy: parking_lot::Mutex<ReaderStallPolicy>,
pub(crate) cipher_id: CipherId,
pub(crate) format_version: u16,
pub(crate) header_flags: u32,
pub(crate) mk_epoch: AtomicU64,
pub(crate) file_id: [u8; 16],
pub(crate) kek_salt: [u8; 16],
pub(crate) pending_tombstones: parking_lot::Mutex<Vec<PendingTombstone>>,
pub(crate) pending_key_retirements: parking_lot::Mutex<Vec<PendingKeyRetirement>>,
pub(crate) options: OpenOptions,
pub(crate) mmap_bytes_in_use: std::sync::Arc<AtomicU64>,
pub(crate) spill_bytes_in_use: AtomicU64,
pub(crate) txn_seq: AtomicU64,
pub(crate) spill_epoch: [u8; 16],
pub(crate) orphaned_spill_paths: parking_lot::Mutex<Vec<String>>,
pub(crate) mode: DbMode,
pub(crate) aborted_readers: parking_lot::Mutex<std::collections::HashSet<u64>>,
pub(crate) any_reader_aborted: std::sync::atomic::AtomicBool,
pub(crate) sentinel_locks: Vec<<V as Vfs>::LockHandle>,
pub(crate) lock_required: bool,
pub(crate) snapshot: Arc<parking_lot::RwLock<ReaderSnapshot>>,
pub(crate) poisoned_commit: parking_lot::Mutex<Option<CommitId>>,
pub(crate) free_page_cache: Arc<parking_lot::Mutex<Vec<u64>>>,
pub(crate) free_page_consumed: Arc<parking_lot::Mutex<Vec<u64>>>,
#[cfg(test)]
pub(crate) visibility_test_hook: parking_lot::Mutex<Option<Arc<VisibilityTestHook>>>,
#[cfg(test)]
pub(crate) rekey_test_fault: parking_lot::Mutex<Option<RekeyTestFault>>,
}
#[derive(Debug, Clone, Copy)]
#[allow(clippy::struct_field_names)]
pub(crate) struct ReaderSnapshot {
pub commit_id: u64,
pub root_page_id: u64,
pub next_page_id: u64,
pub catalog_root_page_id: u64,
pub free_list_root_page_id: u64,
pub commit_history_root_page_id: u64,
}
pub(super) fn encode_root_ref(page_id: u64, txn_id: u64) -> [u8; 16] {
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&page_id.to_le_bytes());
bytes[8..].copy_from_slice(&txn_id.to_le_bytes());
bytes
}
#[must_use]
pub(crate) fn encode_free_list_root(head_page_id: u64) -> [u8; 16] {
let mut bytes = [0u8; 16];
bytes[..8].copy_from_slice(&head_page_id.to_le_bytes());
bytes
}
#[must_use]
pub(crate) fn decode_free_list_root(raw: &[u8; 16]) -> u64 {
let mut b = [0u8; 8];
b.copy_from_slice(&raw[..8]);
u64::from_le_bytes(b)
}
#[derive(Clone, Copy)]
pub(super) struct HeaderFieldsParams {
pub mk_epoch: u64,
pub seq: u64,
pub active_root_page_id: u64,
pub active_root_txn_id: u64,
pub counter_anchor: u64,
pub commit_id: u64,
pub catalog_root: [u8; 16],
pub commit_history_root_page_id: u64,
pub commit_history_root_version: u64,
pub next_page_id: u64,
pub free_list_root_page_id: u64,
}
impl<V: Vfs + Clone> Db<V> {
fn holds_write_lock(&self) -> bool {
!self.sentinel_locks.is_empty()
}
pub(crate) fn write_lock_satisfied(&self) -> bool {
!self.lock_required || self.holds_write_lock()
}
pub(crate) fn retire_rekey_source_when_safe(
&self,
epoch: u64,
cipher_id: CipherId,
) -> Result<()> {
if self.tracked_readers.lock().is_empty() {
return self.pager.retire_mk_epoch(epoch, cipher_id);
}
let pending = PendingKeyRetirement { epoch, cipher_id };
let mut retirements = self.pending_key_retirements.lock();
if !retirements.contains(&pending) {
retirements.push(pending);
}
Ok(())
}
pub(crate) fn drain_pending_key_retirements(&self) -> Result<()> {
if !self.tracked_readers.lock().is_empty() {
return Ok(());
}
let pending = std::mem::take(&mut *self.pending_key_retirements.lock());
for retirement in pending {
self.pager
.retire_mk_epoch(retirement.epoch, retirement.cipher_id)?;
}
Ok(())
}
pub(crate) fn ensure_usable(&self) -> Result<()> {
match *self.poisoned_commit.lock() {
Some(commit) => Err(PagedbError::durably_committed_but_unpublished(commit)),
None => Ok(()),
}
}
pub(crate) fn poison(&self, commit: CommitId) {
let mut poisoned = self.poisoned_commit.lock();
if poisoned.is_none() {
*poisoned = Some(commit);
}
}
pub(crate) async fn finish_durable_commit(
&self,
state: &WriterState,
commit: CommitId,
counter_anchor: u64,
effects: &[SegmentSideEffect],
) -> Result<SegmentReconciliation> {
#[cfg(test)]
self.notify_writer_waiting();
let visibility = self.visibility_gate.write().await;
self.finish_durable_commit_visible(&visibility, state, commit, counter_anchor, effects)
.await
}
pub(crate) async fn finish_durable_commit_visible(
&self,
_visibility: &tokio::sync::RwLockWriteGuard<'_, ()>,
state: &WriterState,
commit: CommitId,
counter_anchor: u64,
effects: &[SegmentSideEffect],
) -> Result<SegmentReconciliation> {
if let Err(error) = self.pager.commit_anchor(counter_anchor) {
tracing::error!(commit = commit.0, error = %error, "durable commit anchor failed");
self.poison(commit);
return Err(PagedbError::durably_committed_but_unpublished(commit));
}
match self.reconcile_segment_effects(effects, commit.0).await {
Ok(outcome) => {
self.publish_snapshot(state);
Ok(outcome)
}
Err(error) => {
tracing::error!(commit = commit.0, error = %error, "durable commit reconciliation failed");
self.poison(commit);
Err(PagedbError::durably_committed_but_unpublished(commit))
}
}
}
#[cfg(test)]
pub(crate) fn install_visibility_test_hook(&self, hook: Arc<VisibilityTestHook>) {
*self.visibility_test_hook.lock() = Some(hook);
}
#[cfg(test)]
pub(crate) fn clear_visibility_test_hook(&self) {
*self.visibility_test_hook.lock() = None;
}
#[cfg(test)]
pub(crate) fn interrupt_rekey_after(&self, point: RekeyTestFault) {
*self.rekey_test_fault.lock() = Some(point);
}
#[cfg(test)]
pub(crate) fn interrupt_rekey_if_requested(&self, point: RekeyTestFault) -> Result<()> {
let mut fault = self.rekey_test_fault.lock();
if *fault == Some(point) {
*fault = None;
return Err(PagedbError::Io(std::io::Error::other(
"rekey test interruption",
)));
}
Ok(())
}
#[cfg(test)]
pub(crate) async fn pause_after_snapshot_selection(&self) {
let hook = self.visibility_test_hook.lock().clone();
if let Some(hook) = hook {
hook.reader_selected.notify_one();
hook.allow_reader_registration.notified().await;
}
}
#[cfg(test)]
pub(crate) async fn pause_in_apply_publication_window(&self) {
let hook = self.visibility_test_hook.lock().clone();
if let Some(hook) = hook {
hook.apply_window_entered.notify_one();
hook.apply_window_release.notified().await;
}
}
#[cfg(test)]
pub(crate) fn notify_reader_registered(&self) {
if let Some(hook) = self.visibility_test_hook.lock().clone() {
hook.reader_registered.notify_one();
}
}
#[cfg(test)]
pub(crate) fn notify_writer_waiting(&self) {
if let Some(hook) = self.visibility_test_hook.lock().clone() {
hook.writer_waiting.notify_one();
}
}
#[must_use]
pub(crate) fn main_db_parent_dir(&self) -> &str {
match self.main_db_path.rsplit_once('/') {
Some(("", _)) | None => "/",
Some((parent, _)) => parent,
}
}
pub(super) fn header_fields(&self, params: HeaderFieldsParams) -> Result<MainDbHeaderFields> {
Ok(MainDbHeaderFields {
format_version: self.format_version,
cipher_id: self.cipher_id.as_byte(),
page_size_log2: super::util::page_size_log2(self.page_size)?,
flags: self.header_flags,
file_id: self.file_id,
kek_salt: self.kek_salt,
mk_epoch: params.mk_epoch,
seq: params.seq,
active_root_page_id: params.active_root_page_id,
active_root_txn_id: params.active_root_txn_id,
counter_anchor: params.counter_anchor,
commit_id: CommitId(params.commit_id),
free_list_root: encode_free_list_root(params.free_list_root_page_id),
catalog_root: params.catalog_root,
apply_journal_root_page_id: 0,
apply_journal_root_version: 0,
commit_history_root_page_id: params.commit_history_root_page_id,
commit_history_root_version: params.commit_history_root_version,
restore_mode: 0,
next_page_id: params.next_page_id,
commit_retain_policy_tag: 0,
commit_retain_policy_value: 0,
realm_id: self.realm_id,
})
}
}
pub(crate) struct CommitHistoryMeta {
pub active_root_page_id: u64,
pub catalog_root_page_id: u64,
pub free_list_root_page_id: u64,
pub next_page_id: u64,
pub unix_seconds: u64,
}
pub(crate) fn encode_commit_meta(m: &CommitHistoryMeta) -> Vec<u8> {
let mut out = Vec::with_capacity(40);
out.extend_from_slice(&m.active_root_page_id.to_le_bytes());
out.extend_from_slice(&m.catalog_root_page_id.to_le_bytes());
out.extend_from_slice(&m.free_list_root_page_id.to_le_bytes());
out.extend_from_slice(&m.next_page_id.to_le_bytes());
out.extend_from_slice(&m.unix_seconds.to_le_bytes());
out
}
pub(crate) fn decode_commit_meta(bytes: &[u8]) -> Result<CommitHistoryMeta> {
if bytes.len() < 40 {
return Err(PagedbError::catalog_row_invalid("commit_history.meta"));
}
let read_u64 = |b: &[u8], off: usize| {
let mut a = [0u8; 8];
a.copy_from_slice(&b[off..off + 8]);
u64::from_le_bytes(a)
};
Ok(CommitHistoryMeta {
active_root_page_id: read_u64(bytes, 0),
catalog_root_page_id: read_u64(bytes, 8),
free_list_root_page_id: read_u64(bytes, 16),
next_page_id: read_u64(bytes, 24),
unix_seconds: read_u64(bytes, 32),
})
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::{CommitHistoryMeta, decode_commit_meta, encode_commit_meta};
const META_LEN: usize = 40;
fn cases() -> u32 {
std::env::var("PAGEDB_PROPTEST_CASES")
.ok()
.and_then(|raw| raw.parse().ok())
.unwrap_or(32)
}
fn config() -> ProptestConfig {
ProptestConfig {
cases: cases(),
failure_persistence: None,
..ProptestConfig::default()
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn random_bytes_are_accepted_only_at_or_above_the_fixed_width(
bytes in prop::collection::vec(any::<u8>(), 0..=(META_LEN + 16)),
) {
match decode_commit_meta(&bytes) {
Ok(_) => prop_assert!(bytes.len() >= META_LEN),
Err(_) => prop_assert!(bytes.len() < META_LEN),
}
}
#[test]
fn encoded_rows_round_trip_for_every_field_value(
active_root_page_id in any::<u64>(),
catalog_root_page_id in any::<u64>(),
free_list_root_page_id in any::<u64>(),
next_page_id in any::<u64>(),
unix_seconds in any::<u64>(),
trailing in prop::collection::vec(any::<u8>(), 0..=16),
) {
let meta = CommitHistoryMeta {
active_root_page_id,
catalog_root_page_id,
free_list_root_page_id,
next_page_id,
unix_seconds,
};
let mut encoded = encode_commit_meta(&meta);
encoded.extend_from_slice(&trailing);
let decoded = decode_commit_meta(&encoded).unwrap();
prop_assert_eq!(decoded.active_root_page_id, active_root_page_id);
prop_assert_eq!(decoded.catalog_root_page_id, catalog_root_page_id);
prop_assert_eq!(decoded.free_list_root_page_id, free_list_root_page_id);
prop_assert_eq!(decoded.next_page_id, next_page_id);
prop_assert_eq!(decoded.unix_seconds, unix_seconds);
}
}
}