#[cfg(target_arch = "x86_64")]
use core::intrinsics::prefetch_read_data;
use std::cell::{Cell, RefCell};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{
AtomicBool, AtomicU8, AtomicU32, AtomicU64, AtomicUsize, Ordering as AtomicOrdering,
};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, RwLock, Weak};
use std::time::{Duration, Instant};
use dashmap::DashMap;
use fsqlite_error::{FrankenError, Result};
use fsqlite_observability::PageCacheEfficiencySnapshot;
use fsqlite_types::cx::Cx;
use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
use fsqlite_types::{
BTreePageHeader, CommitSeq, DATABASE_HEADER_MAGIC, DATABASE_HEADER_SIZE, DatabaseHeader,
DatabaseHeaderError, FRANKENSQLITE_SQLITE_VERSION_NUMBER, LockLevel, PageData, PageNumber,
PageNumberBuildHasher, PageSize,
};
use fsqlite_vfs::{Vfs, VfsFile};
use smallvec::SmallVec;
use crate::journal::{JournalHeader, JournalPageRecord};
use crate::page_buf::{PageBuf, PageBufPool};
use crate::page_cache::{
PageCacheEvictionPolicy, PageCacheMetricsSnapshot, PageCachePageSnapshot, ShardedPageCache,
};
use crate::s3_fifo::S3FifoConfig;
use crate::traits::{self, JournalMode, MvccPager, TransactionHandle, TransactionMode, WalBackend};
type PagePageMap<V> = HashMap<PageNumber, V, PageNumberBuildHasher>;
use fsqlite_wal::{
ConsolidationPhase, FrameSubmission, GLOBAL_CONSOLIDATION_METRICS, GroupCommitConfig,
GroupCommitConsolidator, PARALLEL_WAL_COMPATIBILITY_SELECTOR, PARALLEL_WAL_FLUSH_SCENARIO_ID,
PARALLEL_WAL_LANE_POLICY_VERSION, PARALLEL_WAL_STAGE_SCENARIO_ID, ParallelWalControlSurface,
ParallelWalFallbackReason, ParallelWalLaneBatch, ParallelWalLaneStager,
ParallelWalOperatingMode, ParallelWalShadowVerdict, RecoveryFence, SubmitOutcome,
TransactionConflictSnapshot, TransactionFrameBatch, TransactionFrameBatchContext, WalFile,
WalGenerationIdentity, commit_phase_timing_enabled, detailed_consolidation_metrics_enabled,
parallel_wal_fallback_reason_name, parallel_wal_mode_name, parallel_wal_shadow_verdict_name,
parallel_wal_should_shadow_compare, resolve_parallel_wal_control_surface_from_env,
};
#[cfg(target_arch = "x86_64")]
#[inline]
fn prefetch_l1_read<T>(ptr: *const T) {
if ptr.is_null() {
return;
}
prefetch_read_data::<T, 3>(ptr);
}
#[cfg(not(target_arch = "x86_64"))]
#[inline]
fn prefetch_l1_read<T>(_ptr: *const T) {}
#[inline]
fn elapsed_profile_us(start: Option<Instant>) -> u64 {
start.map_or(0, |start| {
u64::try_from(Instant::now().duration_since(start).as_micros()).unwrap_or(u64::MAX)
})
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PagerCommitProfileSnapshot {
pub commit_calls: u64,
pub phase_a_time_ns: u64,
pub wal_commit_time_ns: u64,
pub memory_flush_time_ns: u64,
pub journal_commit_time_ns: u64,
pub phase_c_metadata_time_ns: u64,
pub file_size_time_ns: u64,
pub unlock_time_ns: u64,
pub publish_time_ns: u64,
pub cache_finish_time_ns: u64,
}
static PAGER_COMMIT_PROFILE_ENABLED: AtomicBool = AtomicBool::new(false);
static PAGER_COMMIT_CALLS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_PHASE_A_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_WAL_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_MEMORY_FLUSH_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_JOURNAL_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_PHASE_C_METADATA_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_FILE_SIZE_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_UNLOCK_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_PUBLISH_TIME_NS: AtomicU64 = AtomicU64::new(0);
static PAGER_COMMIT_CACHE_FINISH_TIME_NS: AtomicU64 = AtomicU64::new(0);
pub fn set_pager_commit_profile_enabled(enabled: bool) {
PAGER_COMMIT_PROFILE_ENABLED.store(enabled, AtomicOrdering::Relaxed);
}
pub fn reset_pager_commit_profile() {
PAGER_COMMIT_CALLS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_PHASE_A_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_WAL_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_MEMORY_FLUSH_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_JOURNAL_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_PHASE_C_METADATA_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_FILE_SIZE_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_UNLOCK_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_PUBLISH_TIME_NS.store(0, AtomicOrdering::Relaxed);
PAGER_COMMIT_CACHE_FINISH_TIME_NS.store(0, AtomicOrdering::Relaxed);
}
#[must_use]
pub fn pager_commit_profile_snapshot() -> PagerCommitProfileSnapshot {
PagerCommitProfileSnapshot {
commit_calls: PAGER_COMMIT_CALLS.load(AtomicOrdering::Relaxed),
phase_a_time_ns: PAGER_COMMIT_PHASE_A_TIME_NS.load(AtomicOrdering::Relaxed),
wal_commit_time_ns: PAGER_COMMIT_WAL_TIME_NS.load(AtomicOrdering::Relaxed),
memory_flush_time_ns: PAGER_COMMIT_MEMORY_FLUSH_TIME_NS.load(AtomicOrdering::Relaxed),
journal_commit_time_ns: PAGER_COMMIT_JOURNAL_TIME_NS.load(AtomicOrdering::Relaxed),
phase_c_metadata_time_ns: PAGER_COMMIT_PHASE_C_METADATA_TIME_NS
.load(AtomicOrdering::Relaxed),
file_size_time_ns: PAGER_COMMIT_FILE_SIZE_TIME_NS.load(AtomicOrdering::Relaxed),
unlock_time_ns: PAGER_COMMIT_UNLOCK_TIME_NS.load(AtomicOrdering::Relaxed),
publish_time_ns: PAGER_COMMIT_PUBLISH_TIME_NS.load(AtomicOrdering::Relaxed),
cache_finish_time_ns: PAGER_COMMIT_CACHE_FINISH_TIME_NS.load(AtomicOrdering::Relaxed),
}
}
#[inline]
fn pager_commit_profile_enabled() -> bool {
PAGER_COMMIT_PROFILE_ENABLED.load(AtomicOrdering::Relaxed)
}
#[inline]
fn pager_commit_profile_start(enabled: bool) -> Option<Instant> {
enabled.then(Instant::now)
}
#[inline]
fn record_pager_commit_duration(metric: &AtomicU64, start: Option<Instant>) {
if let Some(start) = start {
metric.fetch_add(
u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX),
AtomicOrdering::Relaxed,
);
}
}
#[inline]
fn record_pager_commit_call(enabled: bool) {
if enabled {
PAGER_COMMIT_CALLS.fetch_add(1, AtomicOrdering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)] enum WaitPathMode {
KeyedEventcount,
LegacyCondvarTimeout,
}
impl WaitPathMode {
const fn as_str(self) -> &'static str {
match self {
Self::KeyedEventcount => "keyed_eventcount",
Self::LegacyCondvarTimeout => "legacy_condvar_timeout",
}
}
}
const GROUP_COMMIT_WAIT_PATH_MODE: WaitPathMode = WaitPathMode::KeyedEventcount;
const PUBLISHED_SEQUENCE_WAIT_PATH_MODE: WaitPathMode = WaitPathMode::KeyedEventcount;
const GROUP_COMMIT_WAIT_TIMEOUT_FALLBACK: Duration = Duration::from_millis(200);
const LEGACY_GROUP_COMMIT_ARRIVAL_WAIT: Duration = Duration::from_micros(20);
const GROUP_COMMIT_SPARSE_ARRIVAL_WAIT: Duration = Duration::from_micros(8);
const GROUP_COMMIT_BALANCED_ARRIVAL_WAIT: Duration = LEGACY_GROUP_COMMIT_ARRIVAL_WAIT;
const GROUP_COMMIT_BURST_ARRIVAL_WAIT: Duration = Duration::from_micros(40);
const GROUP_COMMIT_ARRIVAL_WAIT_POLICY: &str = "bounded_fair_commit_v1";
const SINGLE_WRITER_BATON_SPINS: u32 = 128;
const SINGLE_WRITER_BATON_PARK: Duration = Duration::from_micros(50);
const PHYSICAL_WRITER_LANE_RUN_ID: &str = "physical-writer-batching-lane";
const PHYSICAL_WRITER_CHECKPOINT_RUN_ID: &str = "physical-writer-checkpoint-decoupling";
const PHYSICAL_WRITER_CHECKPOINT_SCENARIO_ID: &str = "parallel_wal_checkpoint_coordination";
const FLUSH_BUSY_HANDOFF_BASE_SPINS: u32 = 64;
const FLUSH_BUSY_HANDOFF_MAX_SPINS: u32 = 2_048;
const FLUSH_BUSY_HANDOFF_YIELD_EVERY: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FlushBusyRetryWait {
attempt: u32,
spin_loops: u32,
yielded: bool,
}
const fn flush_busy_retry_spin_loops(attempt: u32) -> u32 {
let growth = attempt.saturating_sub(1);
let shift = if growth > 5 { 5 } else { growth };
let spins = FLUSH_BUSY_HANDOFF_BASE_SPINS << shift;
if spins > FLUSH_BUSY_HANDOFF_MAX_SPINS {
FLUSH_BUSY_HANDOFF_MAX_SPINS
} else {
spins
}
}
const fn flush_busy_retry_should_yield(attempt: u32) -> bool {
attempt >= FLUSH_BUSY_HANDOFF_YIELD_EVERY && attempt % FLUSH_BUSY_HANDOFF_YIELD_EVERY == 0
}
const fn flush_busy_retry_wait(attempt: u32) -> FlushBusyRetryWait {
FlushBusyRetryWait {
attempt,
spin_loops: flush_busy_retry_spin_loops(attempt),
yielded: flush_busy_retry_should_yield(attempt),
}
}
fn perform_flush_busy_retry_handoff(wait: FlushBusyRetryWait) {
for _ in 0..wait.spin_loops {
std::hint::spin_loop();
}
#[cfg(not(target_arch = "wasm32"))]
if wait.yielded {
std::thread::yield_now();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ArrivalWaitObservation {
pending_batch_count: usize,
should_flush_now: bool,
fill_age: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CommitServiceMode {
LowLatency,
Balanced,
Throughput,
}
impl CommitServiceMode {
const fn as_u8(self) -> u8 {
match self {
Self::LowLatency => 0,
Self::Balanced => 1,
Self::Throughput => 2,
}
}
const fn from_u8(value: u8) -> Self {
match value {
0 => Self::LowLatency,
2 => Self::Throughput,
_ => Self::Balanced,
}
}
}
fn commit_service_mode_name(mode: CommitServiceMode) -> &'static str {
match mode {
CommitServiceMode::LowLatency => "low_latency",
CommitServiceMode::Balanced => "balanced",
CommitServiceMode::Throughput => "throughput",
}
}
fn duration_from_nanos_saturating(nanos: u128) -> Duration {
Duration::from_nanos(u64::try_from(nanos.min(u128::from(u64::MAX))).unwrap_or(u64::MAX))
}
fn duration_fraction(duration: Duration, numerator: u32, denominator: u32) -> Duration {
debug_assert!(denominator > 0);
duration_from_nanos_saturating(
duration.as_nanos().saturating_mul(u128::from(numerator)) / u128::from(denominator.max(1)),
)
}
fn commit_service_fairness_budget(
control: &ParallelWalControlSurface,
max_wait: Duration,
) -> Duration {
let configured_budget = control
.max_flush_delay_ms
.map(Duration::from_millis)
.unwrap_or(max_wait);
std::cmp::min(configured_budget, max_wait)
}
fn recent_queue_age_p95(fill_age: Duration) -> Duration {
let recent_arrival_wait_tail = Duration::from_micros(
GLOBAL_CONSOLIDATION_METRICS
.hist_arrival_wait
.recent_tail_us(),
);
std::cmp::max(fill_age, recent_arrival_wait_tail)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ArrivalWaitDecision {
control_epoch: u64,
wait_budget: Duration,
fairness_budget: Duration,
max_wait: Duration,
mode: CommitServiceMode,
policy: &'static str,
reason: &'static str,
mode_switch_reason: &'static str,
queue_age_p95: Duration,
fill_age: Duration,
used_legacy_fallback: bool,
starvation_prevented: bool,
}
impl ArrivalWaitDecision {
#[allow(clippy::too_many_arguments)]
fn skip(
control_epoch: u64,
reason: &'static str,
fill_age: Duration,
queue_age_p95: Duration,
fairness_budget: Duration,
max_wait: Duration,
mode: CommitServiceMode,
starvation_prevented: bool,
) -> Self {
Self {
control_epoch,
wait_budget: Duration::ZERO,
fairness_budget,
max_wait,
mode,
policy: GROUP_COMMIT_ARRIVAL_WAIT_POLICY,
reason,
mode_switch_reason: reason,
queue_age_p95,
fill_age,
used_legacy_fallback: false,
starvation_prevented,
}
}
#[allow(clippy::too_many_arguments)]
fn wait(
control_epoch: u64,
wait_budget: Duration,
reason: &'static str,
fill_age: Duration,
queue_age_p95: Duration,
fairness_budget: Duration,
max_wait: Duration,
mode: CommitServiceMode,
used_legacy_fallback: bool,
) -> Self {
Self {
control_epoch,
wait_budget,
fairness_budget,
max_wait,
mode,
policy: GROUP_COMMIT_ARRIVAL_WAIT_POLICY,
reason,
mode_switch_reason: reason,
queue_age_p95,
fill_age,
used_legacy_fallback,
starvation_prevented: false,
}
}
fn wait_budget_us(self) -> u64 {
self.wait_budget.as_micros() as u64
}
fn fill_age_us(self) -> u64 {
self.fill_age.as_micros() as u64
}
fn target_wait_ns(self) -> u64 {
self.wait_budget.as_nanos() as u64
}
fn max_wait_ns(self) -> u64 {
self.max_wait.as_nanos() as u64
}
fn fairness_budget_ns(self) -> u64 {
self.fairness_budget.as_nanos() as u64
}
fn queue_age_p95_ns(self) -> u64 {
self.queue_age_p95.as_nanos() as u64
}
fn queue_delay_ns(self) -> u64 {
self.fill_age.as_nanos() as u64
}
}
fn decide_group_commit_arrival_wait(
observation: Option<ArrivalWaitObservation>,
max_wait: Duration,
fairness_budget: Duration,
queue_age_p95: Duration,
previous_mode: CommitServiceMode,
control_epoch: u64,
) -> ArrivalWaitDecision {
let fairness_budget = std::cmp::min(fairness_budget, max_wait);
match observation {
None => ArrivalWaitDecision::skip(
control_epoch,
"promoted_follow_on",
Duration::ZERO,
queue_age_p95,
fairness_budget,
max_wait,
previous_mode,
false,
),
Some(observation) => {
if observation.should_flush_now {
let mode = if matches!(previous_mode, CommitServiceMode::Throughput)
|| observation.pending_batch_count >= 3
{
CommitServiceMode::Throughput
} else {
CommitServiceMode::Balanced
};
let reason = if matches!(mode, CommitServiceMode::Throughput)
&& matches!(previous_mode, CommitServiceMode::Throughput)
{
"throughput_hysteresis"
} else {
"queue_flushable"
};
return ArrivalWaitDecision::skip(
control_epoch,
reason,
observation.fill_age,
queue_age_p95,
fairness_budget,
max_wait,
mode,
false,
);
}
if fairness_budget.is_zero()
|| observation.fill_age >= fairness_budget
|| queue_age_p95 >= fairness_budget
{
let reason = if fairness_budget.is_zero() || observation.fill_age >= fairness_budget
{
"fairness_budget_exhausted"
} else {
"tail_latency_pressure"
};
return ArrivalWaitDecision::skip(
control_epoch,
reason,
observation.fill_age,
queue_age_p95,
fairness_budget,
max_wait,
CommitServiceMode::LowLatency,
true,
);
}
let remaining_budget = fairness_budget.saturating_sub(observation.fill_age);
if observation.pending_batch_count >= 3
|| (matches!(previous_mode, CommitServiceMode::Throughput)
&& observation.pending_batch_count >= 2
&& queue_age_p95 <= duration_fraction(fairness_budget, 1, 2))
{
let reason = if matches!(previous_mode, CommitServiceMode::Throughput)
&& observation.pending_batch_count >= 2
{
"throughput_hysteresis"
} else {
"burst_backlog"
};
return ArrivalWaitDecision::wait(
control_epoch,
std::cmp::min(remaining_budget, GROUP_COMMIT_BURST_ARRIVAL_WAIT),
reason,
observation.fill_age,
queue_age_p95,
fairness_budget,
max_wait,
CommitServiceMode::Throughput,
false,
);
}
if observation.pending_batch_count >= 2 {
return ArrivalWaitDecision::wait(
control_epoch,
std::cmp::min(remaining_budget, GROUP_COMMIT_BALANCED_ARRIVAL_WAIT),
"mixed_backlog",
observation.fill_age,
queue_age_p95,
fairness_budget,
max_wait,
CommitServiceMode::Balanced,
false,
);
}
ArrivalWaitDecision::wait(
control_epoch,
std::cmp::min(remaining_budget, GROUP_COMMIT_SPARSE_ARRIVAL_WAIT),
"sparse_queue",
observation.fill_age,
queue_age_p95,
fairness_budget,
max_wait,
CommitServiceMode::LowLatency,
true,
)
}
}
}
fn physical_writer_batch_membership(batches: &[TransactionFrameBatch]) -> String {
batches
.iter()
.map(|batch| batch.context.batch_id.to_string())
.collect::<Vec<_>>()
.join(",")
}
fn physical_writer_primary_batch_id(batches: &[TransactionFrameBatch]) -> u64 {
batches
.first()
.map(|batch| batch.context.batch_id)
.unwrap_or_default()
}
fn physical_writer_rollback_mode_active(
mode: ParallelWalOperatingMode,
fallback_reason: Option<ParallelWalFallbackReason>,
) -> bool {
matches!(mode, ParallelWalOperatingMode::Conservative) || fallback_reason.is_some()
}
fn physical_writer_fsync_boundary(sync_policy: WalCommitSyncPolicy) -> &'static str {
if sync_policy.should_sync_on_commit() {
"commit_sync"
} else {
"deferred_sync"
}
}
fn physical_writer_ordering_phase(arrival_wait_reason: &'static str) -> &'static str {
if arrival_wait_reason == "promoted_follow_on" {
"promoted_follow_on_flush"
} else {
"group_flush"
}
}
fn group_commit_phase_name(phase: ConsolidationPhase) -> &'static str {
match phase {
ConsolidationPhase::Filling => "filling",
ConsolidationPhase::Flushing => "flushing",
ConsolidationPhase::Complete => "complete",
}
}
fn transaction_mode_name(mode: TransactionMode) -> &'static str {
match mode {
TransactionMode::ReadOnly => "read_only",
TransactionMode::Deferred => "deferred",
TransactionMode::Immediate => "immediate",
TransactionMode::Exclusive => "exclusive",
TransactionMode::Concurrent => "concurrent",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct CheckpointCoordinationQueueSnapshot {
queue_phase: &'static str,
queue_epoch: u64,
pending_batch_count: usize,
}
fn checkpoint_coordination_queue_snapshot(
queue: &GroupCommitQueueRef,
) -> CheckpointCoordinationQueueSnapshot {
let consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
CheckpointCoordinationQueueSnapshot {
queue_phase: group_commit_phase_name(consolidator.phase()),
queue_epoch: consolidator.epoch(),
pending_batch_count: consolidator.pending_batch_count(),
}
}
#[allow(clippy::too_many_arguments)]
fn log_checkpoint_coordination(
cx: &Cx,
queue: &GroupCommitQueueRef,
checkpoint_phase: &'static str,
foreground_phase: &'static str,
foreground_action: &'static str,
interaction_rule: &'static str,
stall_avoided: bool,
active_transactions: u32,
checkpoint_active: bool,
) {
let queue_snapshot = checkpoint_coordination_queue_snapshot(queue);
tracing::debug!(
target: "fsqlite::wal::checkpoint_coordination",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_CHECKPOINT_RUN_ID,
scenario_id = PHYSICAL_WRITER_CHECKPOINT_SCENARIO_ID,
checkpoint_phase,
foreground_phase,
foreground_action,
interaction_rule,
stall_avoided,
active_transactions,
checkpoint_active,
queue_phase = queue_snapshot.queue_phase,
queue_epoch = queue_snapshot.queue_epoch,
pending_batch_count = queue_snapshot.pending_batch_count,
"checkpoint/foreground coordination event"
);
}
fn pager_group_commit_queue<V: Vfs>(pager: &SimplePager<V>) -> GroupCommitQueueRef {
group_commit_queue_for_backend(pager.vfs.as_ref(), &pager.db_path)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KeyedWaitResult {
Signaled,
TimedOut,
}
#[derive(Debug, Default)]
struct KeyedWaitSlot {
state: Mutex<u64>,
cv: Condvar,
}
impl KeyedWaitSlot {
fn generation(&self) -> u64 {
*self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn wait_for_change(&self, observed_generation: u64, timeout: Duration) -> KeyedWaitResult {
let guard = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if *guard != observed_generation {
return KeyedWaitResult::Signaled;
}
let (_guard, timeout_result) = self
.cv
.wait_timeout_while(guard, timeout, |generation| {
*generation == observed_generation
})
.unwrap_or_else(std::sync::PoisonError::into_inner);
if timeout_result.timed_out() {
KeyedWaitResult::TimedOut
} else {
KeyedWaitResult::Signaled
}
}
fn signal(&self) {
let mut generation = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*generation = generation.saturating_add(1);
self.cv.notify_all();
}
}
#[derive(Debug, Default)]
struct KeyedWaitRegistry {
slots: Mutex<HashMap<u64, Weak<KeyedWaitSlot>>>,
}
impl KeyedWaitRegistry {
fn new() -> Self {
Self {
slots: Mutex::new(HashMap::new()),
}
}
fn slot(&self, key: u64) -> Arc<KeyedWaitSlot> {
let mut slots = self
.slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
slots.retain(|_, slot| slot.strong_count() > 0);
if let Some(slot) = slots.get(&key).and_then(Weak::upgrade) {
return slot;
}
let slot = Arc::new(KeyedWaitSlot::default());
slots.insert(key, Arc::downgrade(&slot));
slot
}
fn signal(&self, key: u64) -> bool {
let slot = {
let mut slots = self
.slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
slots.retain(|_, slot| slot.strong_count() > 0);
slots.get(&key).and_then(Weak::upgrade)
};
if let Some(slot) = slot {
slot.signal();
true
} else {
false
}
}
#[cfg(test)]
fn has_slot(&self, key: u64) -> bool {
self.slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&key)
.is_some_and(|slot| slot.strong_count() > 0)
}
}
struct GroupCommitQueue {
consolidator: Mutex<GroupCommitConsolidator>,
flush_complete: Condvar,
completed_epoch: AtomicU64,
failed_epochs: Mutex<HashMap<u64, GroupCommitEpochFailure>>,
persisted_epochs: Mutex<HashMap<u64, PersistedGroupCommitEpoch>>,
epoch_waiters: KeyedWaitRegistry,
commit_service_control_epoch: AtomicU64,
commit_service_mode: AtomicU8,
parallel_wal_lanes: ParallelWalLaneStager<traits::PreparedWalFrameBatch>,
}
type LaneStagedPreparedBatch = ParallelWalLaneBatch<traits::PreparedWalFrameBatch>;
static GROUP_COMMIT_TRACE_ENABLED: OnceLock<bool> = OnceLock::new();
static GROUP_COMMIT_TRACE_FSYNC_SEQ: AtomicU64 = AtomicU64::new(0);
fn group_commit_trace_enabled() -> bool {
*GROUP_COMMIT_TRACE_ENABLED.get_or_init(|| {
std::env::var_os("FSQLITE_TRACE_GROUP_COMMIT").is_some_and(|value| {
let value = value.to_string_lossy();
!value.is_empty() && value != "0" && !value.eq_ignore_ascii_case("false")
})
})
}
fn trace_group_commit(args: std::fmt::Arguments<'_>) {
if group_commit_trace_enabled() {
eprintln!("[fsqlite_group_commit] {args}");
}
}
#[derive(Debug, Clone)]
struct PersistedGroupCommitEpoch {
members: HashSet<u64>,
frames_start: u64,
frames_end: u64,
fsync_seq: u64,
}
#[derive(Debug)]
enum WaitForEpochOutcome {
Completed,
TakeOverFlusher {
batches: Vec<TransactionFrameBatch>,
flush_epoch: u64,
},
}
#[derive(Debug, Clone)]
enum GroupCommitEpochFailure {
Busy,
BusyRecovery,
BusySnapshot { conflicting_pages: String },
Other(String),
}
impl GroupCommitEpochFailure {
fn from_error(error: &FrankenError) -> Self {
match error {
FrankenError::Busy => Self::Busy,
FrankenError::BusyRecovery => Self::BusyRecovery,
FrankenError::BusySnapshot { conflicting_pages } => Self::BusySnapshot {
conflicting_pages: conflicting_pages.clone(),
},
_ => Self::Other(error.to_string()),
}
}
fn into_error(self, target_epoch: u64) -> FrankenError {
match self {
Self::Busy => FrankenError::Busy,
Self::BusyRecovery => FrankenError::BusyRecovery,
Self::BusySnapshot { conflicting_pages } => {
FrankenError::BusySnapshot { conflicting_pages }
}
Self::Other(detail) => FrankenError::internal(format!(
"group commit flush failed for epoch {target_epoch}: {detail}"
)),
}
}
}
#[cfg(test)]
static PARALLEL_WAL_CONTROL_OVERRIDE: OnceLock<Mutex<Option<ParallelWalControlSurface>>> =
OnceLock::new();
fn resolve_parallel_wal_control_surface() -> ParallelWalControlSurface {
#[cfg(test)]
let test_override = PARALLEL_WAL_CONTROL_OVERRIDE
.get_or_init(|| Mutex::new(None))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
#[cfg(test)]
if let Some(control) = test_override {
return control;
}
resolve_parallel_wal_control_surface_from_env()
}
#[cfg(test)]
fn set_parallel_wal_control_override(control: Option<ParallelWalControlSurface>) {
*PARALLEL_WAL_CONTROL_OVERRIDE
.get_or_init(|| Mutex::new(None))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = control;
}
impl GroupCommitQueue {
fn new(config: GroupCommitConfig) -> Self {
Self::with_parallel_wal_control(config, resolve_parallel_wal_control_surface())
}
fn with_parallel_wal_control(
config: GroupCommitConfig,
parallel_wal_control: ParallelWalControlSurface,
) -> Self {
Self {
consolidator: Mutex::new(GroupCommitConsolidator::new(config)),
flush_complete: Condvar::new(),
completed_epoch: AtomicU64::new(0),
failed_epochs: Mutex::new(HashMap::new()),
persisted_epochs: Mutex::new(HashMap::new()),
epoch_waiters: KeyedWaitRegistry::new(),
commit_service_control_epoch: AtomicU64::new(0),
commit_service_mode: AtomicU8::new(CommitServiceMode::Balanced.as_u8()),
parallel_wal_lanes: ParallelWalLaneStager::new(parallel_wal_control),
}
}
fn parallel_wal_control(&self) -> &ParallelWalControlSurface {
self.parallel_wal_lanes.control()
}
fn next_parallel_wal_batch_id(&self) -> u64 {
self.parallel_wal_lanes.next_batch_id()
}
fn current_parallel_wal_lane_id(&self) -> u16 {
self.parallel_wal_lanes.current_lane_id()
}
fn current_lane_backlog(&self, lane_id: u16) -> usize {
self.parallel_wal_lanes.current_lane_backlog(lane_id)
}
fn current_commit_service_mode(&self) -> CommitServiceMode {
CommitServiceMode::from_u8(self.commit_service_mode.load(AtomicOrdering::Relaxed))
}
fn next_commit_service_control_epoch(&self) -> u64 {
self.commit_service_control_epoch
.fetch_add(1, AtomicOrdering::Relaxed)
.saturating_add(1)
}
fn store_commit_service_mode(&self, mode: CommitServiceMode) {
self.commit_service_mode
.store(mode.as_u8(), AtomicOrdering::Relaxed);
}
fn record_prepared_batch(&self, prepared_batch: LaneStagedPreparedBatch) -> usize {
self.parallel_wal_lanes.record_batch(prepared_batch)
}
fn take_prepared_batches_for_flush(
&self,
batches: &[TransactionFrameBatch],
) -> Option<HashMap<u64, LaneStagedPreparedBatch>> {
let contexts = batches
.iter()
.map(|batch| batch.context)
.collect::<Vec<_>>();
self.parallel_wal_lanes.take_batches_for_flush(&contexts)
}
fn discard_prepared_batches_for_flush(&self, batches: &[TransactionFrameBatch]) -> usize {
let contexts = batches
.iter()
.map(|batch| batch.context)
.collect::<Vec<_>>();
self.parallel_wal_lanes.discard_batches_for_flush(&contexts)
}
fn record_persisted_epoch(
&self,
epoch: u64,
batches: &[TransactionFrameBatch],
frames_start: u64,
frames_end: u64,
fsync_seq: u64,
) {
if !group_commit_trace_enabled() {
return;
}
let members = batches
.iter()
.map(|batch| batch.context.batch_id)
.collect::<HashSet<_>>();
let mut members_display = members.iter().copied().collect::<Vec<_>>();
members_display.sort_unstable();
let members_display = members_display
.into_iter()
.map(|member| member.to_string())
.collect::<Vec<_>>()
.join(",");
let record = PersistedGroupCommitEpoch {
members,
frames_start,
frames_end,
fsync_seq,
};
self.persisted_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(epoch, record);
trace_group_commit(format_args!(
"batch epoch={epoch} members=[{members_display}] frames_written_range={frames_start}..={frames_end} fsync_seq={fsync_seq}"
));
}
fn persisted_epoch_for(&self, epoch: u64) -> Option<PersistedGroupCommitEpoch> {
if !group_commit_trace_enabled() {
return None;
}
self.persisted_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&epoch)
.cloned()
}
fn publish_completed_epoch(&self, epoch: u64, wake_next_epoch: bool) {
let _guard = self
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.completed_epoch.store(epoch, AtomicOrdering::Release);
#[cfg(any(test, feature = "fault-injection"))]
if crate::fault_hooks::maybe_inject_drop_condvar_notify(epoch) {
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
published_epoch = epoch,
wake_next_epoch,
fallback = "timeout_recheck",
"suppressed direct waiter wake after completion publish"
);
return;
}
self.signal_completed_epoch_waiters(epoch, wake_next_epoch);
self.prune_stale_epoch_metadata(epoch);
}
fn publish_failed_epoch(&self, epoch: u64, error: &FrankenError, wake_next_epoch: bool) {
let _guard = self
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut failed_epochs = self
.failed_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
failed_epochs.insert(epoch, GroupCommitEpochFailure::from_error(error));
drop(failed_epochs);
self.signal_failed_epoch_waiters(epoch, wake_next_epoch);
}
fn prune_stale_epoch_metadata(&self, current_epoch: u64) {
const RETENTION: u64 = 128;
let cutoff = current_epoch.saturating_sub(RETENTION);
if cutoff == 0 {
return;
}
self.failed_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.retain(|&epoch, _| epoch > cutoff);
if group_commit_trace_enabled() {
self.persisted_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.retain(|&epoch, _| epoch > cutoff);
}
}
fn is_epoch_complete(&self, epoch: u64) -> bool {
self.completed_epoch.load(AtomicOrdering::Acquire) >= epoch
}
fn signal_completed_epoch_waiters(&self, epoch: u64, wake_next_epoch: bool) {
match GROUP_COMMIT_WAIT_PATH_MODE {
WaitPathMode::KeyedEventcount => {
let woke_target_epoch = self.epoch_waiters.signal(epoch);
let woke_next_epoch = wake_next_epoch
&& epoch
.checked_add(1)
.is_some_and(|next_epoch| self.epoch_waiters.signal(next_epoch));
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
published_epoch = epoch,
wake_next_epoch,
woke_target_epoch,
woke_next_epoch,
"published completed epoch to targeted waiters"
);
}
WaitPathMode::LegacyCondvarTimeout => self.flush_complete.notify_all(),
}
}
fn signal_failed_epoch_waiters(&self, epoch: u64, wake_next_epoch: bool) {
match GROUP_COMMIT_WAIT_PATH_MODE {
WaitPathMode::KeyedEventcount => {
let woke_failed_epoch = self.epoch_waiters.signal(epoch);
let woke_next_epoch = wake_next_epoch
&& epoch
.checked_add(1)
.is_some_and(|next_epoch| self.epoch_waiters.signal(next_epoch));
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
failed_epoch = epoch,
wake_next_epoch,
woke_failed_epoch,
woke_next_epoch,
"published failed epoch to targeted waiters"
);
}
WaitPathMode::LegacyCondvarTimeout => self.flush_complete.notify_all(),
}
}
fn observe_epoch_outcome(
&self,
guard: &mut std::sync::MutexGuard<'_, GroupCommitConsolidator>,
target_epoch: u64,
) -> Result<Option<WaitForEpochOutcome>> {
let failed_detail = self
.failed_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&target_epoch)
.cloned();
if let Some(failure) = failed_detail {
GLOBAL_CONSOLIDATION_METRICS
.wake_reasons
.failed_epoch
.fetch_add(1, AtomicOrdering::Relaxed);
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
wake_reason = "failed_epoch",
target_epoch,
"waiter observed failed epoch"
);
return Err(failure.into_error(target_epoch));
}
if self.is_epoch_complete(target_epoch) {
GLOBAL_CONSOLIDATION_METRICS
.wake_reasons
.notify
.fetch_add(1, AtomicOrdering::Relaxed);
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
wake_reason = "notify",
target_epoch,
completed_epoch = self.completed_epoch.load(AtomicOrdering::Acquire),
"waiter observed completed epoch"
);
return Ok(Some(WaitForEpochOutcome::Completed));
}
if guard.has_flusher_vacancy()
&& guard.epoch().checked_add(1) == Some(target_epoch)
&& guard.claim_flusher_vacancy()
{
GLOBAL_CONSOLIDATION_METRICS
.wake_reasons
.flusher_takeover
.fetch_add(1, AtomicOrdering::Relaxed);
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
wake_reason = "flusher_takeover",
target_epoch,
current_epoch = guard.epoch(),
"waiter claimed promoted flusher vacancy"
);
let batches = guard.begin_flush()?;
return Ok(Some(WaitForEpochOutcome::TakeOverFlusher {
flush_epoch: guard.epoch(),
batches,
}));
}
Ok(None)
}
fn wait_for_epoch_outcome_legacy(
&self,
mut guard: std::sync::MutexGuard<'_, GroupCommitConsolidator>,
target_epoch: u64,
) -> Result<WaitForEpochOutcome> {
loop {
if let Some(outcome) = self.observe_epoch_outcome(&mut guard, target_epoch)? {
return Ok(outcome);
}
let (new_guard, timeout_result) = self
.flush_complete
.wait_timeout(guard, GROUP_COMMIT_WAIT_TIMEOUT_FALLBACK)
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard = new_guard;
if timeout_result.timed_out() {
GLOBAL_CONSOLIDATION_METRICS
.wake_reasons
.timeout
.fetch_add(1, AtomicOrdering::Relaxed);
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
wake_reason = "timeout",
target_epoch,
"legacy waiter timeout fallback fired"
);
}
}
}
fn wait_for_epoch_outcome_keyed<'a>(
&'a self,
mut guard: std::sync::MutexGuard<'a, GroupCommitConsolidator>,
target_epoch: u64,
) -> Result<WaitForEpochOutcome> {
loop {
if let Some(outcome) = self.observe_epoch_outcome(&mut guard, target_epoch)? {
return Ok(outcome);
}
let slot = self.epoch_waiters.slot(target_epoch);
let observed_generation = slot.generation();
if let Some(outcome) = self.observe_epoch_outcome(&mut guard, target_epoch)? {
return Ok(outcome);
}
drop(guard);
let wait_result =
slot.wait_for_change(observed_generation, GROUP_COMMIT_WAIT_TIMEOUT_FALLBACK);
guard = self
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if wait_result == KeyedWaitResult::TimedOut {
GLOBAL_CONSOLIDATION_METRICS
.wake_reasons
.timeout
.fetch_add(1, AtomicOrdering::Relaxed);
tracing::trace!(
target: "fsqlite::wal::epoch_wait",
wait_strategy = GROUP_COMMIT_WAIT_PATH_MODE.as_str(),
wake_reason = "timeout",
target_epoch,
fallback = "timeout_recheck",
"keyed epoch waiter timeout fallback fired"
);
}
}
}
fn wait_for_epoch_outcome(
&self,
guard: std::sync::MutexGuard<'_, GroupCommitConsolidator>,
target_epoch: u64,
) -> Result<WaitForEpochOutcome> {
match GROUP_COMMIT_WAIT_PATH_MODE {
WaitPathMode::KeyedEventcount => self.wait_for_epoch_outcome_keyed(guard, target_epoch),
WaitPathMode::LegacyCondvarTimeout => {
self.wait_for_epoch_outcome_legacy(guard, target_epoch)
}
}
}
}
type GroupCommitQueueRef = Arc<GroupCommitQueue>;
pub type SharedWalBackend = Arc<std::sync::RwLock<Option<Box<dyn WalBackend>>>>;
fn new_shared_wal_backend() -> SharedWalBackend {
Arc::new(std::sync::RwLock::new(None))
}
fn with_wal_backend_read<T>(
wal_backend: &SharedWalBackend,
f: impl FnOnce(&dyn WalBackend) -> Result<T>,
) -> Result<T> {
let guard = wal_backend
.read()
.map_err(|_| FrankenError::internal("SharedWalBackend lock poisoned"))?;
let wal = guard
.as_deref()
.ok_or_else(|| FrankenError::internal("WAL mode active but no WAL backend installed"))?;
f(wal)
}
enum WalReadLookup {
Ready(Option<Vec<u8>>),
NeedsWriteFallback,
}
fn read_page_from_wal_backend(
wal_backend: &SharedWalBackend,
cx: &Cx,
page_no: PageNumber,
) -> Result<Option<Vec<u8>>> {
match with_wal_backend_read(wal_backend, |wal| {
if wal.supports_pinned_reads() {
wal.read_page_pinned(cx, page_no.get())
.map(WalReadLookup::Ready)
} else {
Ok(WalReadLookup::NeedsWriteFallback)
}
})? {
WalReadLookup::Ready(data) => Ok(data),
WalReadLookup::NeedsWriteFallback => {
with_wal_backend(wal_backend, |wal| wal.read_page(cx, page_no.get()))
}
}
}
fn with_wal_backend<T>(
wal_backend: &SharedWalBackend,
f: impl FnOnce(&mut dyn WalBackend) -> Result<T>,
) -> Result<T> {
let mut guard = wal_backend
.write()
.map_err(|_| FrankenError::internal("SharedWalBackend lock poisoned"))?;
let wal = guard
.as_deref_mut()
.ok_or_else(|| FrankenError::internal("WAL mode active but no WAL backend installed"))?;
f(wal)
}
fn has_wal_backend(wal_backend: &SharedWalBackend) -> Result<bool> {
let guard = wal_backend
.read()
.map_err(|_| FrankenError::internal("SharedWalBackend lock poisoned"))?;
Ok(guard.is_some())
}
static GROUP_COMMIT_QUEUES: OnceLock<Mutex<HashMap<PathBuf, GroupCommitQueueRef>>> =
OnceLock::new();
static RECOVERY_FENCES: OnceLock<Mutex<HashMap<PathBuf, Arc<RecoveryFence>>>> = OnceLock::new();
fn lexical_normalize_path(path: PathBuf) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !normalized.pop() && !normalized.has_root() {
normalized.push(component.as_os_str());
}
}
Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
normalized.push(component.as_os_str());
}
}
}
normalized
}
fn shared_file_state_key(db_path: &Path) -> PathBuf {
std::fs::canonicalize(db_path).unwrap_or_else(|_| {
let absolute = if db_path.is_absolute() {
db_path.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(db_path))
.unwrap_or_else(|_| db_path.to_path_buf())
};
lexical_normalize_path(absolute)
})
}
fn recovery_fence_for_path(db_path: &Path) -> Arc<RecoveryFence> {
let key = shared_file_state_key(db_path);
let fences = RECOVERY_FENCES.get_or_init(|| Mutex::new(HashMap::new()));
let mut fences = fences
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Arc::clone(
fences
.entry(key)
.or_insert_with(|| Arc::new(RecoveryFence::new())),
)
}
fn recovery_fence_for_backend<V: Vfs>(vfs: &V, db_path: &Path) -> Arc<RecoveryFence> {
if vfs.is_memory() {
Arc::new(RecoveryFence::new())
} else {
recovery_fence_for_path(db_path)
}
}
fn group_commit_queue_for_path(db_path: &Path) -> GroupCommitQueueRef {
let key = shared_file_state_key(db_path);
let queues = GROUP_COMMIT_QUEUES.get_or_init(|| Mutex::new(HashMap::new()));
let mut queues = queues
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Arc::clone(
queues
.entry(key)
.or_insert_with(|| Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()))),
)
}
fn group_commit_queue_for_backend<V: Vfs>(vfs: &V, db_path: &Path) -> GroupCommitQueueRef {
if vfs.is_memory() {
Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()))
} else {
group_commit_queue_for_path(db_path)
}
}
pub fn remove_group_commit_queue(db_path: &Path) {
if let Some(queues) = GROUP_COMMIT_QUEUES.get() {
let key = shared_file_state_key(db_path);
let mut queues = queues
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
queues.remove(&key);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalCommitSyncPolicy {
Deferred,
PerCommit,
}
impl WalCommitSyncPolicy {
#[must_use]
const fn should_sync_on_commit(self) -> bool {
matches!(self, Self::PerCommit)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PagerAccessMode {
ReadWrite,
ReadOnly,
}
impl PagerAccessMode {
#[must_use]
const fn is_readonly(self) -> bool {
matches!(self, Self::ReadOnly)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RollbackJournalRecoveryState {
Clean,
Pending,
}
impl RollbackJournalRecoveryState {
#[must_use]
const fn is_pending(self) -> bool {
matches!(self, Self::Pending)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PagerCommittedSnapshot {
pub commit_seq: CommitSeq,
pub db_size: u32,
pub journal_mode: JournalMode,
pub freelist_count: usize,
pub checkpoint_active: bool,
pub writer_active: bool,
pub db_file_size_bytes: u64,
}
impl PagerCommittedSnapshot {
fn from_inner<F: VfsFile>(inner: &PagerInner<F>) -> Self {
Self {
commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
writer_active: inner.writer_active,
db_file_size_bytes: inner.committed_db_file_size_bytes,
}
}
}
pub(crate) struct PagerInner<F: VfsFile> {
db_file: F,
page_size: PageSize,
db_size: u32,
next_page: u32,
writer_active: bool,
active_transactions: u32,
checkpoint_active: bool,
access_mode: PagerAccessMode,
freelist: Vec<PageNumber>,
journal_mode: JournalMode,
wal_commit_sync_policy: WalCommitSyncPolicy,
rollback_journal_recovery_state: RollbackJournalRecoveryState,
commit_seq: CommitSeq,
committed_db_file_size_bytes: u64,
committed_db_change_counter: u64,
committed_wal_generation: Option<WalGenerationIdentity>,
}
impl<F: VfsFile> PagerInner<F> {
fn read_page_copy(
&mut self,
cx: &Cx,
cache: &ShardedPageCache,
wal_backend: &SharedWalBackend,
page_no: PageNumber,
) -> Result<Vec<u8>> {
if self.journal_mode == JournalMode::Wal {
if let Some(data) = read_page_from_wal_backend(wal_backend, cx, page_no)? {
return Ok(data);
}
}
if page_no.get() > self.db_size {
return Ok(vec![0_u8; self.page_size.as_usize()]);
}
if let Some(data) = cache.get_copy(page_no) {
return Ok(data);
}
match cache.read_page_copy(cx, &mut self.db_file, page_no) {
Ok(data) => Ok(data),
Err(FrankenError::OutOfMemory) => {
if cache.evict_any() {
cache.read_page_copy(cx, &mut self.db_file, page_no)
} else {
let page_size = self.page_size.as_usize();
let offset = u64::from(page_no.get() - 1) * page_size as u64;
let mut out = vec![0_u8; page_size];
let bytes_read = self.db_file.read(cx, &mut out, offset)?;
if bytes_read < page_size {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read fetching page {page}: got {bytes_read} of {page_size}",
page = page_no.get()
),
});
}
Ok(out)
}
}
Err(err) => Err(err),
}
}
fn read_committed_page_copy(
&self,
cx: &Cx,
wal_backend: &SharedWalBackend,
page_no: PageNumber,
) -> Result<Vec<u8>> {
if self.journal_mode == JournalMode::Wal {
if let Some(data) = read_page_from_wal_backend(wal_backend, cx, page_no)? {
return Ok(data);
}
}
let page_size = self.page_size.as_usize();
let offset = u64::from(page_no.get().saturating_sub(1)) * page_size as u64;
let file_size = self.db_file.file_size(cx)?;
if offset >= file_size {
return Ok(vec![0_u8; page_size]);
}
let mut out = vec![0_u8; page_size];
let bytes_read = self.db_file.read(cx, &mut out, offset)?;
if bytes_read < page_size {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read fetching committed page {page}: got {bytes_read} of {page_size}",
page = page_no.get()
),
});
}
Ok(out)
}
fn read_database_file_header_bytes(
&self,
cx: &Cx,
file_size: u64,
) -> Result<[u8; DATABASE_HEADER_SIZE]> {
if file_size == 0 {
return Ok([0_u8; DATABASE_HEADER_SIZE]);
}
let mut out = [0_u8; DATABASE_HEADER_SIZE];
let bytes_read = self.db_file.read(cx, &mut out, 0)?;
if bytes_read < DATABASE_HEADER_SIZE {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read fetching database-file header: got {bytes_read} of {DATABASE_HEADER_SIZE}"
),
});
}
Ok(out)
}
fn probe_visible_commit_seq(
&mut self,
cx: &Cx,
wal_backend: &SharedWalBackend,
) -> Result<(CommitSeq, u64, bool)> {
let file_size = self.db_file.file_size(cx)?;
let (wal_visible_commit_count, wal_generation) = if self.journal_mode == JournalMode::Wal {
with_wal_backend(wal_backend, |wal| {
wal.begin_transaction(cx)?;
let snapshot = wal.pinned_read_snapshot();
let commit_count =
snapshot.map_or_else(|| wal.committed_txn_count(cx), |s| Ok(s.commit_count))?;
Ok((commit_count, snapshot.map(|s| s.generation)))
})?
} else {
(0, None)
};
let wal_snapshot_initialized = self.journal_mode == JournalMode::Wal;
let cached_wal_base_is_current = self.journal_mode == JournalMode::Wal
&& file_size == self.committed_db_file_size_bytes
&& wal_generation.is_some()
&& self.committed_wal_generation == wal_generation;
let base_change_counter = if cached_wal_base_is_current {
self.committed_db_change_counter
} else {
let base_header_bytes = self.read_database_file_header_bytes(cx, file_size)?;
let base_change_counter = if self.journal_mode == JournalMode::Wal {
match DatabaseHeader::from_bytes(&base_header_bytes) {
Ok(base_header) => u64::from(base_header.change_counter),
Err(error) => {
stale_main_header_change_counter_under_wal(&base_header_bytes, &error)
.ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!(
"invalid database-file header during WAL refresh: {error}"
),
})?
}
}
} else {
u64::from(
DatabaseHeader::from_bytes(&base_header_bytes)
.map_err(|error| FrankenError::DatabaseCorrupt {
detail: format!(
"invalid database header during pager refresh: {error}"
),
})?
.change_counter,
)
};
self.committed_db_change_counter = base_change_counter;
self.committed_wal_generation = wal_generation;
base_change_counter
};
let visible_commit_seq =
CommitSeq::new(base_change_counter.saturating_add(wal_visible_commit_count));
Ok((visible_commit_seq, file_size, wal_snapshot_initialized))
}
fn refresh_committed_state(
&mut self,
cx: &Cx,
cache: &ShardedPageCache,
wal_backend: &SharedWalBackend,
) -> Result<bool> {
let (new_commit_seq, current_file_size, wal_snapshot_initialized) =
self.probe_visible_commit_seq(cx, wal_backend)?;
if new_commit_seq == self.commit_seq
&& current_file_size == self.committed_db_file_size_bytes
{
return Ok(wal_snapshot_initialized);
}
let page1 = self.read_committed_page_copy(cx, wal_backend, PageNumber::ONE)?;
if page1.len() < DATABASE_HEADER_SIZE {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"committed page 1 too small for database header: got {}, need {}",
page1.len(),
DATABASE_HEADER_SIZE
),
});
}
let mut header_bytes = [0_u8; DATABASE_HEADER_SIZE];
header_bytes.copy_from_slice(&page1[..DATABASE_HEADER_SIZE]);
let header = DatabaseHeader::from_bytes(&header_bytes).map_err(|error| {
FrankenError::DatabaseCorrupt {
detail: format!("invalid database header during pager refresh: {error}"),
}
})?;
let file_size = self.db_file.file_size(cx)?;
let file_derived = header
.page_count_from_file_size(file_size)
.unwrap_or(header.page_count);
let db_size = header.page_count.max(file_derived).max(1);
let freelist = if self.access_mode.is_readonly() {
Vec::new()
} else {
load_freelist_from_committed_state(
cx,
self,
wal_backend,
db_size,
header.freelist_trunk,
header.freelist_count,
)?
};
self.db_size = self.db_size.max(db_size);
let effective_db_size = self.db_size;
self.next_page = if effective_db_size >= 2 {
effective_db_size.saturating_add(1)
} else {
2
};
self.freelist = freelist;
if new_commit_seq != self.commit_seq {
cache.clear();
}
self.commit_seq = new_commit_seq;
self.committed_db_file_size_bytes = current_file_size;
Ok(wal_snapshot_initialized)
}
fn flush_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()> {
let page_size = self.page_size.as_usize();
let offset = u64::from(page_no.get() - 1) * page_size as u64;
self.db_file.write(cx, data, offset)?;
Ok(())
}
}
fn normalize_freelist(pages: &[PageNumber], db_size: u32) -> Vec<PageNumber> {
let mut normalized: Vec<PageNumber> = pages
.iter()
.copied()
.filter(|p| {
let raw = p.get();
raw > 1 && raw <= db_size
})
.collect();
if normalized
.windows(2)
.all(|window| window[0].get() > window[1].get())
{
return normalized;
}
normalized.sort_unstable_by_key(|page| std::cmp::Reverse(page.get()));
normalized.dedup_by_key(|p| p.get());
normalized
}
fn merge_descending_unique_freelists(left: &[PageNumber], right: &[PageNumber]) -> Vec<PageNumber> {
let mut merged = Vec::with_capacity(left.len() + right.len());
let mut left_index = 0;
let mut right_index = 0;
while left_index < left.len() && right_index < right.len() {
let left_page = left[left_index];
let right_page = right[right_index];
match left_page.get().cmp(&right_page.get()) {
std::cmp::Ordering::Greater => {
merged.push(left_page);
left_index += 1;
}
std::cmp::Ordering::Less => {
merged.push(right_page);
right_index += 1;
}
std::cmp::Ordering::Equal => {
merged.push(left_page);
left_index += 1;
right_index += 1;
}
}
}
merged.extend_from_slice(&left[left_index..]);
merged.extend_from_slice(&right[right_index..]);
merged
}
fn return_pages_to_freelist(
freelist: &mut Vec<PageNumber>,
pages: impl IntoIterator<Item = PageNumber>,
) {
let mut returned_pages: Vec<PageNumber> = pages.into_iter().collect();
if returned_pages.is_empty() {
return;
}
returned_pages.sort_unstable_by_key(|page| std::cmp::Reverse(page.get()));
returned_pages.dedup_by_key(|page| page.get());
if freelist
.windows(2)
.all(|window| window[0].get() > window[1].get())
{
*freelist = merge_descending_unique_freelists(freelist, &returned_pages);
} else {
freelist.extend(returned_pages);
freelist.sort_unstable_by_key(|page| std::cmp::Reverse(page.get()));
freelist.dedup_by_key(|page| page.get());
}
}
fn load_freelist_from_disk<F: VfsFile>(
cx: &Cx,
db_file: &F,
page_size: PageSize,
db_size: u32,
first_trunk: u32,
freelist_count: u32,
) -> Result<Vec<PageNumber>> {
if first_trunk == 0 || freelist_count == 0 {
return Ok(Vec::new());
}
let ps = page_size.as_usize();
let mut visited: HashSet<u32> = HashSet::new();
let mut out: Vec<PageNumber> = Vec::with_capacity(freelist_count as usize);
let mut trunk = first_trunk;
while trunk != 0 && out.len() < freelist_count as usize {
if trunk > db_size {
break;
}
if !visited.insert(trunk) {
return Err(FrankenError::DatabaseCorrupt {
detail: format!("freelist loop detected at trunk page {trunk}"),
});
}
let trunk_page = PageNumber::new(trunk).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!("invalid freelist trunk page number {trunk}"),
})?;
out.push(trunk_page);
let mut buf = vec![0u8; ps];
let offset = u64::from(trunk.saturating_sub(1)) * ps as u64;
let bytes_read = db_file.read(cx, &mut buf, offset)?;
if bytes_read < ps {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read loading freelist trunk page {trunk}: got {bytes_read} of {ps}"
),
});
}
let next_trunk = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
let leaf_count = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
let max_leaf_entries = (ps / 4).saturating_sub(2);
if leaf_count > max_leaf_entries {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"freelist trunk {trunk} leaf_count {leaf_count} exceeds max {max_leaf_entries}"
),
});
}
for idx in 0..leaf_count {
if out.len() >= freelist_count as usize {
break;
}
let base = 8 + idx * 4;
let leaf = u32::from_be_bytes([buf[base], buf[base + 1], buf[base + 2], buf[base + 3]]);
if leaf == 0 || leaf > db_size {
continue;
}
let leaf_page = PageNumber::new(leaf).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!("invalid freelist leaf page number {leaf}"),
})?;
out.push(leaf_page);
}
trunk = next_trunk;
}
out.truncate(freelist_count as usize);
Ok(normalize_freelist(&out, db_size))
}
fn load_freelist_from_committed_state<F: VfsFile>(
cx: &Cx,
inner: &PagerInner<F>,
wal_backend: &SharedWalBackend,
db_size: u32,
first_trunk: u32,
freelist_count: u32,
) -> Result<Vec<PageNumber>> {
if first_trunk == 0 || freelist_count == 0 {
return Ok(Vec::new());
}
let ps = inner.page_size.as_usize();
let mut visited: HashSet<u32> = HashSet::new();
let mut out: Vec<PageNumber> = Vec::with_capacity(freelist_count as usize);
let mut trunk = first_trunk;
while trunk != 0 && out.len() < freelist_count as usize {
if trunk > db_size {
break;
}
if !visited.insert(trunk) {
return Err(FrankenError::DatabaseCorrupt {
detail: format!("freelist loop detected at trunk page {trunk}"),
});
}
let trunk_page = PageNumber::new(trunk).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!("invalid freelist trunk page number {trunk}"),
})?;
out.push(trunk_page);
let buf = inner.read_committed_page_copy(cx, wal_backend, trunk_page)?;
if buf.len() < ps {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read loading committed freelist trunk page {trunk}: got {} of {ps}",
buf.len()
),
});
}
let next_trunk = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
let leaf_count = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
let max_leaf_entries = (ps / 4).saturating_sub(2);
if leaf_count > max_leaf_entries {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"freelist trunk {trunk} leaf_count {leaf_count} exceeds max {max_leaf_entries}"
),
});
}
for idx in 0..leaf_count {
if out.len() >= freelist_count as usize {
break;
}
let base = 8 + idx * 4;
let leaf = u32::from_be_bytes([buf[base], buf[base + 1], buf[base + 2], buf[base + 3]]);
if leaf == 0 || leaf > db_size {
continue;
}
let leaf_page = PageNumber::new(leaf).ok_or_else(|| FrankenError::DatabaseCorrupt {
detail: format!("invalid freelist leaf page number {leaf}"),
})?;
out.push(leaf_page);
}
trunk = next_trunk;
}
out.truncate(freelist_count as usize);
Ok(normalize_freelist(&out, db_size))
}
#[allow(clippy::too_many_arguments)]
fn serialize_freelist_to_write_set<F: VfsFile, S: std::hash::BuildHasher>(
cx: &Cx,
inner: &mut PagerInner<F>,
cache: &ShardedPageCache,
wal_backend: &SharedWalBackend,
pool: &PageBufPool,
write_set: &mut HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &mut Vec<PageNumber>,
committed_db_size: u32,
pending_free_pages: &[PageNumber],
) -> Result<()> {
if committed_db_size == 0 {
inner.freelist.clear();
return Ok(());
}
let upper_bound = inner.next_page.saturating_sub(1).max(committed_db_size);
let mut predicted_freelist = inner.freelist.clone();
return_pages_to_freelist(&mut predicted_freelist, pending_free_pages.iter().copied());
let predicted_normalized = normalize_freelist(&predicted_freelist, upper_bound);
let durable_freelist: Vec<PageNumber> = predicted_normalized
.iter()
.copied()
.filter(|page| page.get() <= committed_db_size)
.collect();
let ps = inner.page_size.as_usize();
let total_free = durable_freelist.len() as u32;
let (first_trunk, trunk_pages) = if durable_freelist.is_empty() {
(0u32, Vec::<u32>::new())
} else {
let max_leaf_entries = (ps / 4).saturating_sub(2).max(1);
let trunk_count = durable_freelist.len().div_ceil(max_leaf_entries + 1);
let trunks: Vec<u32> = durable_freelist
.iter()
.take(trunk_count)
.map(|p| p.get())
.collect();
(trunks[0], trunks)
};
if !trunk_pages.is_empty() {
let mut leaf_index = trunk_pages.len();
let max_leaf_entries = (ps / 4).saturating_sub(2).max(1);
for (idx, trunk_pg) in trunk_pages.iter().enumerate() {
let next = trunk_pages.get(idx + 1).copied().unwrap_or(0);
let remaining = durable_freelist.len().saturating_sub(leaf_index);
let take = remaining.min(max_leaf_entries);
let mut buf = pool.acquire()?;
buf.fill(0);
buf[0..4].copy_from_slice(&next.to_be_bytes());
buf[4..8].copy_from_slice(&(take as u32).to_be_bytes());
for i in 0..take {
let leaf = durable_freelist[leaf_index + i].get();
let base = 8 + i * 4;
buf[base..base + 4].copy_from_slice(&leaf.to_be_bytes());
}
leaf_index += take;
if let Some(pg) = PageNumber::new(*trunk_pg) {
insert_staged_page(write_set, write_pages_sorted, pg, StagedPage::from_buf(buf));
}
}
}
let mut page1 = ensure_page_one_in_write_set(cx, inner, cache, wal_backend, pool, write_set)?;
{
let page1_bytes = page1.as_page_bytes_mut();
page1_bytes[32..36].copy_from_slice(&first_trunk.to_be_bytes());
page1_bytes[36..40].copy_from_slice(&total_free.to_be_bytes());
}
insert_staged_page(write_set, write_pages_sorted, PageNumber::ONE, page1);
Ok(())
}
fn ensure_page_one_in_write_set<F: VfsFile, S: std::hash::BuildHasher>(
cx: &Cx,
inner: &mut PagerInner<F>,
cache: &ShardedPageCache,
wal_backend: &SharedWalBackend,
pool: &PageBufPool,
write_set: &mut HashMap<PageNumber, StagedPage, S>,
) -> Result<StagedPage> {
if let Some(staged) = write_set.remove(&PageNumber::ONE) {
return Ok(staged.into_unpublished_for_mutation(pool));
}
let page1_vec = inner.read_page_copy(cx, cache, wal_backend, PageNumber::ONE)?;
Ok(StagedPage::from_page_data(PageData::from_vec(page1_vec)))
}
fn insert_page_sorted(pages: &mut Vec<PageNumber>, page_no: PageNumber) {
match pages.binary_search_by_key(&page_no.get(), |page| page.get()) {
Ok(_) => {}
Err(idx) => pages.insert(idx, page_no),
}
}
fn remove_page_sorted(pages: &mut Vec<PageNumber>, page_no: PageNumber) {
if let Ok(idx) = pages.binary_search_by_key(&page_no.get(), |page| page.get()) {
pages.remove(idx);
}
}
fn insert_staged_page<S: std::hash::BuildHasher>(
write_set: &mut HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &mut Vec<PageNumber>,
page_no: PageNumber,
staged: StagedPage,
) {
if write_set.insert(page_no, staged).is_none() {
insert_page_sorted(write_pages_sorted, page_no);
}
}
static STAGED_PAGE_OVERWRITE_STEALS_TOTAL: AtomicUsize = AtomicUsize::new(0);
#[must_use]
pub fn staged_page_overwrite_steals_total() -> u64 {
u64::try_from(STAGED_PAGE_OVERWRITE_STEALS_TOTAL.load(AtomicOrdering::Relaxed))
.unwrap_or(u64::MAX)
}
pub fn reset_staged_page_overwrite_steals_total() {
STAGED_PAGE_OVERWRITE_STEALS_TOTAL.store(0, AtomicOrdering::Relaxed);
}
#[cfg(test)]
struct WalCommitBatch<'a> {
new_db_size: u32,
frames: Vec<traits::WalFrameRef<'a>>,
}
#[cfg(test)]
fn collect_wal_commit_batch<'a, S: std::hash::BuildHasher>(
current_db_size: u32,
write_set: &'a HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &[PageNumber],
) -> Result<Option<WalCommitBatch<'a>>> {
if write_pages_sorted.is_empty() {
return Ok(None);
}
let max_written = write_pages_sorted.last().map_or(0, |page| page.get());
let new_db_size = current_db_size.max(max_written);
let frame_count = write_pages_sorted.len();
let mut frames = Vec::with_capacity(frame_count);
for (idx, page_no) in write_pages_sorted.iter().enumerate() {
let staged_page = write_set.get(page_no).ok_or_else(|| {
FrankenError::internal(format!(
"WAL commit batch missing page {} from write_set",
page_no.get()
))
})?;
let db_size_if_commit = if idx + 1 == frame_count {
new_db_size
} else {
0
};
frames.push(traits::WalFrameRef {
page_number: page_no.get(),
page_data: staged_page.as_page_bytes(),
db_size_if_commit,
});
}
Ok(Some(WalCommitBatch {
new_db_size,
frames,
}))
}
fn build_group_commit_batch<S: std::hash::BuildHasher>(
current_db_size: u32,
write_set: &HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &[PageNumber],
) -> Result<Option<(TransactionFrameBatch, u32)>> {
if write_pages_sorted.is_empty() {
return Ok(None);
}
let max_written = write_pages_sorted.last().map_or(0, |page| page.get());
let new_db_size = current_db_size.max(max_written);
let frame_count = write_pages_sorted.len();
let mut frames = Vec::with_capacity(frame_count);
for (idx, page_no) in write_pages_sorted.iter().enumerate() {
let staged_page = write_set.get(page_no).ok_or_else(|| {
FrankenError::internal(format!(
"group commit batch missing page {} from write_set",
page_no.get()
))
})?;
let db_size_if_commit = if idx + 1 == frame_count {
new_db_size
} else {
0
};
frames.push(FrameSubmission {
page_number: page_no.get(),
page_data: staged_page.as_page_bytes().to_vec(), db_size_if_commit,
});
}
Ok(Some((TransactionFrameBatch::new(frames), new_db_size)))
}
fn transaction_conflict_snapshot_from_wal(
snapshot: traits::WalPublicationSnapshot,
) -> TransactionConflictSnapshot {
TransactionConflictSnapshot {
generation: snapshot.generation,
last_commit_frame: snapshot.last_commit_frame,
commit_count: snapshot.commit_count,
}
}
fn attach_group_commit_conflict_metadata(
mut batch: TransactionFrameBatch,
conflict_pages: &[PageNumber],
conflict_snapshot: Option<traits::WalPublicationSnapshot>,
) -> TransactionFrameBatch {
let conflict_pages = conflict_pages
.iter()
.map(|page| page.get())
.collect::<Vec<_>>();
let conflict_snapshot = conflict_snapshot.map(transaction_conflict_snapshot_from_wal);
batch = batch.with_conflict_snapshot(conflict_pages, conflict_snapshot);
batch
}
fn conflicting_pages_since_batch_snapshots(
cx: &Cx,
wal: &mut dyn WalBackend,
batches: &[TransactionFrameBatch],
) -> Result<Vec<u32>> {
let mut conflicts = Vec::<u32>::new();
for batch in batches {
let Some(snapshot) = batch.conflict_snapshot else {
continue;
};
let batch_conflicts =
wal.conflicting_pages_since_snapshot(cx, snapshot, &batch.conflict_pages)?;
conflicts.extend(batch_conflicts);
}
conflicts.sort_unstable();
conflicts.dedup();
Ok(conflicts)
}
fn group_commit_final_db_size(current_db_size: u32, batches: &[TransactionFrameBatch]) -> u32 {
batches
.iter()
.flat_map(|batch| batch.frames.iter())
.fold(current_db_size, |db_size, frame| {
db_size.max(frame.db_size_if_commit)
})
}
fn promote_group_commit_page_one_headers(
batches: &mut [TransactionFrameBatch],
final_db_size: u32,
) -> bool {
let mut promoted = false;
for batch in batches {
for frame in &mut batch.frames {
if frame.page_number != 1 || frame.page_data.len() < DATABASE_HEADER_SIZE {
continue;
}
let mut existing_page_count_bytes = [0_u8; 4];
existing_page_count_bytes.copy_from_slice(&frame.page_data[28..32]);
let existing_page_count = u32::from_be_bytes(existing_page_count_bytes);
let promoted_page_count = existing_page_count.max(final_db_size);
if promoted_page_count != existing_page_count {
frame.page_data[28..32].copy_from_slice(&promoted_page_count.to_be_bytes());
promoted = true;
}
}
}
promoted
}
fn flatten_group_commit_batches<'a>(
current_db_size: u32,
batches: &'a [TransactionFrameBatch],
) -> (Vec<traits::WalFrameRef<'a>>, u32) {
let total_frames: usize = batches.iter().map(|batch| batch.frames.len()).sum();
let mut frame_refs: Vec<traits::WalFrameRef<'a>> = Vec::with_capacity(total_frames);
let final_db_size = group_commit_final_db_size(current_db_size, batches);
let mut last_commit_frame_idx = None;
for batch in batches {
for frame in &batch.frames {
if frame.db_size_if_commit != 0 {
last_commit_frame_idx = Some(frame_refs.len());
}
frame_refs.push(traits::WalFrameRef {
page_number: frame.page_number,
page_data: &frame.page_data,
db_size_if_commit: 0,
});
}
}
if let Some(last_commit_frame_idx) = last_commit_frame_idx {
frame_refs[last_commit_frame_idx].db_size_if_commit = final_db_size;
}
(frame_refs, final_db_size)
}
fn group_commit_batch_frame_refs(batch: &TransactionFrameBatch) -> Vec<traits::WalFrameRef<'_>> {
batch
.frames
.iter()
.map(|frame| traits::WalFrameRef {
page_number: frame.page_number,
page_data: &frame.page_data,
db_size_if_commit: frame.db_size_if_commit,
})
.collect()
}
fn group_commit_batch_staged_bytes(batch: &TransactionFrameBatch) -> u64 {
batch.frames.iter().fold(0_u64, |acc, frame| {
acc.saturating_add(u64::try_from(frame.page_data.len()).unwrap_or(u64::MAX))
.saturating_add(u64::try_from(fsqlite_wal::WAL_FRAME_HEADER_SIZE).unwrap_or(u64::MAX))
})
}
fn prepared_batch_matches_frame_refs(
prepared: &traits::PreparedWalFrameBatch,
frame_refs: &[traits::WalFrameRef<'_>],
) -> bool {
prepared.frame_count() == frame_refs.len()
&& prepared
.frame_metas
.iter()
.zip(frame_refs)
.enumerate()
.all(|(index, (meta, frame))| {
meta.page_number == frame.page_number
&& meta.db_size_if_commit == frame.db_size_if_commit
&& prepared.page_data(index) == frame.page_data
})
}
fn should_shadow_compare_batches(
control: &ParallelWalControlSurface,
batches: &[TransactionFrameBatch],
) -> bool {
batches
.iter()
.any(|batch| parallel_wal_should_shadow_compare(control, batch.context.batch_id))
}
fn prepare_group_commit_batch_for_lane(
cx: &Cx,
wal_backend: &SharedWalBackend,
batch: &TransactionFrameBatch,
batch_id: u64,
lane_id: u16,
control: &ParallelWalControlSurface,
) -> Result<Option<LaneStagedPreparedBatch>> {
if matches!(control.mode, ParallelWalOperatingMode::Conservative) {
return Ok(None);
}
if let Some(limit) = control.max_parallel_commit_bytes {
if group_commit_batch_staged_bytes(batch) > limit {
return Ok(None);
}
}
let frame_refs = group_commit_batch_frame_refs(batch);
if frame_refs.is_empty() {
return Ok(None);
}
let started = Instant::now();
let mut prepared = with_wal_backend_read(wal_backend, |wal| {
let mut prepared = wal.prepare_append_frames(&frame_refs)?;
if let Some(prepared) = prepared.as_mut() {
wal.finalize_prepared_frames(cx, prepared)?;
}
Ok(prepared)
})?;
let Some(prepared) = prepared.take() else {
return Ok(None);
};
Ok(Some(LaneStagedPreparedBatch {
batch_id,
lane_id,
staged_frame_count: u32::try_from(frame_refs.len()).unwrap_or(u32::MAX),
staging_elapsed_ns: u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX),
shadow_verdict: ParallelWalShadowVerdict::NotRun,
payload: prepared,
}))
}
fn merge_prepared_group_commit_batches(
prepared_batches: Vec<LaneStagedPreparedBatch>,
final_db_size: u32,
) -> Result<traits::PreparedWalFrameBatch> {
if prepared_batches.is_empty() {
return Err(FrankenError::internal(
"cannot merge empty prepared group-commit batch set",
));
}
if prepared_batches.len() == 1 {
let mut iter = prepared_batches.into_iter();
let staged = iter
.next()
.ok_or_else(|| FrankenError::internal("missing prepared group-commit batch"))?;
if prepared_batch_is_canonical_single_commit(&staged.payload, final_db_size) {
return Ok(staged.payload);
}
return merge_prepared_group_commit_batches_uncanonicalized(vec![staged], final_db_size);
}
merge_prepared_group_commit_batches_uncanonicalized(prepared_batches, final_db_size)
}
fn merge_prepared_group_commit_batches_uncanonicalized(
prepared_batches: Vec<LaneStagedPreparedBatch>,
final_db_size: u32,
) -> Result<traits::PreparedWalFrameBatch> {
let Some(first) = prepared_batches.first() else {
return Err(FrankenError::internal(
"cannot merge empty prepared group-commit batch set",
));
};
let frame_size = first.payload.frame_size;
let page_data_offset = first.payload.page_data_offset;
let big_endian_checksum = first.payload.big_endian_checksum;
for staged in &prepared_batches {
let prepared = &staged.payload;
if prepared.frame_size != frame_size
|| prepared.page_data_offset != page_data_offset
|| prepared.big_endian_checksum != big_endian_checksum
{
return Err(FrankenError::internal(format!(
"incompatible prepared WAL batch merge for lane {} batch {}",
staged.lane_id, staged.batch_id
)));
}
}
let total_frames = prepared_batches
.iter()
.map(|batch| batch.payload.frame_count())
.sum::<usize>();
let total_bytes = prepared_batches
.iter()
.map(|batch| batch.payload.frame_bytes.len())
.sum::<usize>();
let mut iter = prepared_batches.into_iter();
let first = iter
.next()
.ok_or_else(|| FrankenError::internal("missing prepared group-commit batch"))?;
let mut merged = first.payload;
merged
.frame_metas
.reserve(total_frames.saturating_sub(merged.frame_metas.len()));
merged
.frame_bytes
.reserve(total_bytes.saturating_sub(merged.frame_bytes.len()));
let mut saw_commit = false;
for meta in &mut merged.frame_metas {
saw_commit |= meta.db_size_if_commit != 0;
meta.db_size_if_commit = 0;
}
for staged in iter {
let prepared = staged.payload;
merged
.frame_metas
.extend(prepared.frame_metas.into_iter().map(|mut meta| {
saw_commit |= meta.db_size_if_commit != 0;
meta.db_size_if_commit = 0;
meta
}));
merged.frame_bytes.extend_from_slice(&prepared.frame_bytes);
}
merged.checksum_transforms.clear();
merged.last_commit_frame_offset = None;
merged.finalized_for = None;
merged.finalized_running_checksum = None;
for frame_index in 0..merged.frame_count() {
merged.set_db_size_if_commit(frame_index, 0);
}
if saw_commit {
let last_frame_index = merged.frame_metas.len().saturating_sub(1);
merged.last_commit_frame_offset = Some(last_frame_index);
merged.set_db_size_if_commit(last_frame_index, final_db_size);
}
merged.recompute_checksum_transforms()?;
Ok(merged)
}
fn prepared_batch_is_canonical_single_commit(
prepared: &traits::PreparedWalFrameBatch,
final_db_size: u32,
) -> bool {
let frame_count = prepared.frame_count();
if frame_count == 0 {
return false;
}
let mut commit_index = None;
for (index, meta) in prepared.frame_metas.iter().enumerate() {
let frame = prepared.frame_slice(index);
if frame.len() < 8 {
return false;
}
let frame_db_size = u32::from_be_bytes([frame[4], frame[5], frame[6], frame[7]]);
if meta.db_size_if_commit == 0 {
if frame_db_size != 0 {
return false;
}
continue;
}
if commit_index.is_some() || index + 1 != frame_count {
return false;
}
if meta.db_size_if_commit != final_db_size {
return false;
}
if frame_db_size != final_db_size {
return false;
}
commit_index = Some(index);
}
prepared.last_commit_frame_offset == commit_index
}
fn aggregate_shadow_verdict(
prepared_batches: &[LaneStagedPreparedBatch],
) -> ParallelWalShadowVerdict {
if prepared_batches
.iter()
.any(|batch| matches!(batch.shadow_verdict, ParallelWalShadowVerdict::Diverged))
{
ParallelWalShadowVerdict::Diverged
} else if prepared_batches
.iter()
.any(|batch| matches!(batch.shadow_verdict, ParallelWalShadowVerdict::Clean))
{
ParallelWalShadowVerdict::Clean
} else {
ParallelWalShadowVerdict::NotRun
}
}
fn lane_flush_stats(
queue: &GroupCommitQueue,
batches: &[TransactionFrameBatch],
) -> Vec<(u16, usize, u32, u64)> {
let mut by_lane = HashMap::<u16, (u32, u64)>::new();
for batch in batches {
let entry = by_lane.entry(batch.context.lane_id).or_insert((0, 0));
entry.0 = entry.0.saturating_add(batch.context.staged_frame_count);
entry.1 = entry.1.saturating_add(batch.context.staging_elapsed_ns);
}
let mut stats = by_lane
.into_iter()
.map(|(lane_id, (staged_frame_count, elapsed_ns))| {
(
lane_id,
queue.current_lane_backlog(lane_id),
staged_frame_count,
elapsed_ns,
)
})
.collect::<Vec<_>>();
stats.sort_unstable_by_key(|(lane_id, _, _, _)| *lane_id);
stats
}
fn conflicting_pages_across_group_commit_batches(batches: &[TransactionFrameBatch]) -> Vec<u32> {
let mut first_batch_by_page = HashMap::<u32, usize>::new();
let mut conflicts = HashSet::<u32>::new();
let mut page_one_conflict_batches = Vec::<usize>::new();
let mut page_one_frame_batches = Vec::<usize>::new();
for (batch_idx, batch) in batches.iter().enumerate() {
let mut seen_in_batch = HashSet::<u32>::new();
let mut batch_has_page_one_frame = false;
let mut batch_has_page_one_conflict = false;
for &page_number in &batch.conflict_pages {
if page_number == 1 {
batch_has_page_one_conflict = true;
}
if !seen_in_batch.insert(page_number) {
continue;
}
match first_batch_by_page.get(&page_number).copied() {
Some(previous_batch_idx) if previous_batch_idx != batch_idx => {
conflicts.insert(page_number);
}
None => {
first_batch_by_page.insert(page_number, batch_idx);
}
Some(_) => {}
}
}
for frame in &batch.frames {
if frame.page_number == 1 {
batch_has_page_one_frame = true;
continue;
}
if !seen_in_batch.insert(frame.page_number) {
continue;
}
match first_batch_by_page.get(&frame.page_number).copied() {
Some(previous_batch_idx) if previous_batch_idx != batch_idx => {
conflicts.insert(frame.page_number);
}
None => {
first_batch_by_page.insert(frame.page_number, batch_idx);
}
Some(_) => {}
}
}
if batch_has_page_one_conflict {
page_one_conflict_batches.push(batch_idx);
}
if batch_has_page_one_frame {
page_one_frame_batches.push(batch_idx);
}
}
if page_one_conflict_batches.iter().any(|conflict_batch_idx| {
page_one_frame_batches
.iter()
.any(|frame_batch_idx| frame_batch_idx != conflict_batch_idx)
}) {
conflicts.insert(1);
}
let mut conflicts = conflicts.into_iter().collect::<Vec<_>>();
conflicts.sort_unstable();
conflicts
}
const SNAPSHOT_PUBLICATION_MODE: &str = "seqlock_published_pages";
const PUBLISHED_SNAPSHOT_WAIT_SLICE: Duration = Duration::from_micros(50);
const PUBLISHED_READ_FAST_RETRY_LIMIT: usize = 64;
const PUBLISHED_COUNTER_STRIPE_COUNT: usize = 64;
static NEXT_PUBLISHED_COUNTER_STRIPE: AtomicUsize = AtomicUsize::new(0);
std::thread_local! {
static PUBLISHED_COUNTER_STRIPE_INDEX: usize =
NEXT_PUBLISHED_COUNTER_STRIPE.fetch_add(1, AtomicOrdering::Relaxed)
% PUBLISHED_COUNTER_STRIPE_COUNT;
}
#[derive(Debug)]
#[repr(align(64))]
struct CacheAlignedAtomicU64(AtomicU64);
impl CacheAlignedAtomicU64 {
const fn new(value: u64) -> Self {
Self(AtomicU64::new(value))
}
fn fetch_add(&self, value: u64, ordering: AtomicOrdering) {
self.0.fetch_add(value, ordering);
}
fn load(&self, ordering: AtomicOrdering) -> u64 {
self.0.load(ordering)
}
}
#[derive(Debug)]
struct StripedCounter64 {
stripes: [CacheAlignedAtomicU64; PUBLISHED_COUNTER_STRIPE_COUNT],
}
impl StripedCounter64 {
fn new() -> Self {
Self {
stripes: std::array::from_fn(|_| CacheAlignedAtomicU64::new(0)),
}
}
fn increment(&self) {
PUBLISHED_COUNTER_STRIPE_INDEX.with(|stripe| {
self.stripes[*stripe].fetch_add(1, AtomicOrdering::Relaxed);
});
}
fn load(&self) -> u64 {
self.stripes.iter().fold(0_u64, |sum, stripe| {
sum.saturating_add(stripe.load(AtomicOrdering::Acquire))
})
}
}
const ATOMIC_PUBLISHED_PAGE_LIMIT: u32 = 65_535;
const ATOMIC_PUBLISHED_MIN_SLOT_COUNT: usize = 512;
const ATOMIC_PUBLISHED_MAX_SLOT_COUNT: usize = ATOMIC_PUBLISHED_PAGE_LIMIT as usize;
#[derive(Debug)]
struct AtomicPublishedPageSlot {
present: AtomicBool,
page: Mutex<Option<PageData>>,
active_pos: AtomicUsize,
}
#[derive(Debug)]
struct AtomicPublishedPages {
slots: Box<[AtomicPublishedPageSlot]>,
active_indices: Mutex<Vec<usize>>,
page_count: AtomicUsize,
}
impl AtomicPublishedPages {
fn new(initial_db_size: u32) -> Self {
let initial_pages =
usize::try_from(initial_db_size).unwrap_or(ATOMIC_PUBLISHED_MAX_SLOT_COUNT);
let slot_count = initial_pages.clamp(
ATOMIC_PUBLISHED_MIN_SLOT_COUNT,
ATOMIC_PUBLISHED_MAX_SLOT_COUNT,
);
let slots = (0..slot_count)
.map(|_| AtomicPublishedPageSlot {
present: AtomicBool::new(false),
page: Mutex::new(None),
active_pos: AtomicUsize::new(0),
})
.collect::<Vec<_>>()
.into_boxed_slice();
Self {
slots,
active_indices: Mutex::new(Vec::new()),
page_count: AtomicUsize::new(0),
}
}
#[inline]
fn slot_index(&self, page_no: PageNumber) -> Option<usize> {
let raw = page_no.get();
if raw > ATOMIC_PUBLISHED_PAGE_LIMIT {
return None;
}
let idx = usize::try_from(raw.saturating_sub(1)).ok()?;
(idx < self.slots.len()).then_some(idx)
}
#[inline]
fn accepts(&self, page_no: PageNumber) -> bool {
self.slot_index(page_no).is_some()
}
#[cfg(test)]
fn slot_count(&self) -> usize {
self.slots.len()
}
#[cfg(test)]
fn active_slot_count(&self) -> usize {
self.active_indices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
}
fn get(&self, page_no: PageNumber) -> Option<PageData> {
let idx = self.slot_index(page_no)?;
let slot = &self.slots[idx];
if !slot.present.load(AtomicOrdering::Acquire) {
return None;
}
slot.page
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn insert(&self, page_no: PageNumber, page: PageData) -> bool {
let Some(idx) = self.slot_index(page_no) else {
return false;
};
let mut active_indices = self
.active_indices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let slot = &self.slots[idx];
let mut guard = slot
.page
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let inserted = guard.is_none();
*guard = Some(page);
if inserted {
slot.active_pos
.store(active_indices.len(), AtomicOrdering::Relaxed);
active_indices.push(idx);
}
drop(guard);
slot.present.store(true, AtomicOrdering::Release);
if inserted {
self.page_count.fetch_add(1, AtomicOrdering::Relaxed);
}
inserted
}
fn remove(&self, page_no: PageNumber) -> bool {
let Some(idx) = self.slot_index(page_no) else {
return false;
};
let mut active_indices = self
.active_indices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let slot = &self.slots[idx];
if !slot.present.swap(false, AtomicOrdering::AcqRel) {
return false;
}
let removed = slot
.page
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
.is_some();
if removed {
let pos = slot.active_pos.load(AtomicOrdering::Relaxed);
if pos < active_indices.len() && active_indices[pos] == idx {
active_indices.swap_remove(pos);
if let Some(&moved_idx) = active_indices.get(pos) {
self.slots[moved_idx]
.active_pos
.store(pos, AtomicOrdering::Relaxed);
}
} else if let Some(fallback_pos) = active_indices
.iter()
.position(|&active_idx| active_idx == idx)
{
active_indices.swap_remove(fallback_pos);
if let Some(&moved_idx) = active_indices.get(fallback_pos) {
self.slots[moved_idx]
.active_pos
.store(fallback_pos, AtomicOrdering::Relaxed);
}
}
self.page_count.fetch_sub(1, AtomicOrdering::Relaxed);
}
removed
}
fn clear(&self) {
if self.page_count.load(AtomicOrdering::Acquire) == 0 {
return;
}
let mut active_indices = self
.active_indices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for idx in active_indices.drain(..) {
let slot = &self.slots[idx];
if slot.present.swap(false, AtomicOrdering::AcqRel) {
let _ = slot
.page
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
}
self.page_count.store(0, AtomicOrdering::Release);
}
fn retain<F>(&self, f: F)
where
F: Fn(&PageNumber) -> bool,
{
let mut active_indices = self
.active_indices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut retained_indices = Vec::with_capacity(active_indices.len());
for idx in active_indices.drain(..) {
let slot = &self.slots[idx];
if !slot.present.load(AtomicOrdering::Acquire) {
continue;
}
let page_no = PageNumber::new(u32::try_from(idx + 1).unwrap_or(u32::MAX))
.expect("atomic publication slot index must map to a valid page number");
if f(&page_no) {
slot.active_pos
.store(retained_indices.len(), AtomicOrdering::Relaxed);
retained_indices.push(idx);
continue;
}
if slot.present.swap(false, AtomicOrdering::AcqRel) {
let _ = slot
.page
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
}
}
let retained_total = retained_indices.len();
*active_indices = retained_indices;
self.page_count
.store(retained_total, AtomicOrdering::Release);
}
fn insert_batch<I>(&self, pages: I)
where
I: IntoIterator<Item = (PageNumber, PageData)>,
{
let mut active_indices = self
.active_indices
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut total_added = 0_usize;
for (page_no, page) in pages {
let Some(idx) = self.slot_index(page_no) else {
continue;
};
let slot = &self.slots[idx];
let mut guard = slot
.page
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let inserted = guard.is_none();
*guard = Some(page);
if inserted {
slot.active_pos
.store(active_indices.len(), AtomicOrdering::Relaxed);
active_indices.push(idx);
total_added = total_added.saturating_add(1);
}
drop(guard);
slot.present.store(true, AtomicOrdering::Release);
}
if total_added > 0 {
self.page_count
.fetch_add(total_added, AtomicOrdering::Relaxed);
}
}
fn len(&self) -> usize {
self.page_count.load(AtomicOrdering::Acquire)
}
fn prefetch_hint(&self, page_no: PageNumber) {
let Some(idx) = self.slot_index(page_no) else {
return;
};
let slot = &self.slots[idx];
prefetch_l1_read(std::ptr::from_ref(slot));
if slot.present.load(AtomicOrdering::Relaxed)
&& let Ok(guard) = slot.page.try_lock()
&& let Some(page) = guard.as_ref()
{
prefetch_l1_read(page.as_bytes().as_ptr());
}
}
}
#[derive(Debug)]
struct ConcurrentPublishedPages {
pages: DashMap<PageNumber, PageData, foldhash::fast::FixedState>,
page_count: AtomicUsize,
}
impl ConcurrentPublishedPages {
fn new() -> Self {
Self {
pages: DashMap::with_hasher(foldhash::fast::FixedState::default()),
page_count: AtomicUsize::new(0),
}
}
fn get(&self, page_no: PageNumber) -> Option<PageData> {
self.pages.get(&page_no).map(|page| page.value().clone())
}
fn insert(&self, page_no: PageNumber, page: PageData) -> bool {
let inserted = self.pages.insert(page_no, page).is_none();
if inserted {
self.page_count.fetch_add(1, AtomicOrdering::Relaxed);
}
inserted
}
fn remove(&self, page_no: PageNumber) -> bool {
let removed = self.pages.remove(&page_no).is_some();
if removed {
self.page_count.fetch_sub(1, AtomicOrdering::Relaxed);
}
removed
}
fn clear(&self) {
if self.page_count.load(AtomicOrdering::Acquire) == 0 {
return;
}
self.pages.clear();
self.page_count.store(0, AtomicOrdering::Release);
}
fn retain<F>(&self, f: F)
where
F: Fn(&PageNumber) -> bool,
{
self.pages.retain(|page_no, _| f(page_no));
self.page_count
.store(self.pages.len(), AtomicOrdering::Release);
}
fn insert_batch<I>(&self, pages: I)
where
I: IntoIterator<Item = (PageNumber, PageData)>,
{
let mut total_added = 0_usize;
for (page_no, page) in pages {
if self.pages.insert(page_no, page).is_none() {
total_added = total_added.saturating_add(1);
}
}
if total_added > 0 {
self.page_count
.fetch_add(total_added, AtomicOrdering::Relaxed);
}
}
fn len(&self) -> usize {
self.page_count.load(AtomicOrdering::Acquire)
}
fn prefetch_hint(&self, page_no: PageNumber) {
prefetch_l1_read(std::ptr::from_ref(&self.pages));
if let Some(page) = self.pages.get(&page_no) {
prefetch_l1_read(page.value().as_bytes().as_ptr());
}
}
}
#[derive(Debug)]
struct PublishedPages {
atomic: AtomicPublishedPages,
overflow: ConcurrentPublishedPages,
}
impl PublishedPages {
fn new(initial_db_size: u32) -> Self {
Self {
atomic: AtomicPublishedPages::new(initial_db_size),
overflow: ConcurrentPublishedPages::new(),
}
}
fn get(&self, page_no: PageNumber) -> Option<PageData> {
if self.atomic.accepts(page_no) {
self.atomic.get(page_no)
} else {
self.overflow.get(page_no)
}
}
fn insert(&self, page_no: PageNumber, page: PageData) -> bool {
if self.atomic.accepts(page_no) {
self.atomic.insert(page_no, page)
} else {
self.overflow.insert(page_no, page)
}
}
fn remove(&self, page_no: PageNumber) -> bool {
if self.atomic.accepts(page_no) {
self.atomic.remove(page_no)
} else {
self.overflow.remove(page_no)
}
}
fn clear(&self) {
self.atomic.clear();
self.overflow.clear();
}
fn retain<F>(&self, f: F)
where
F: Fn(&PageNumber) -> bool,
{
self.atomic.retain(&f);
self.overflow.retain(f);
}
fn insert_batch<I>(&self, pages: I)
where
I: IntoIterator<Item = (PageNumber, PageData)>,
{
let mut direct_pages = Vec::new();
let mut overflow_pages = Vec::new();
for (page_no, page) in pages {
if self.atomic.accepts(page_no) {
direct_pages.push((page_no, page));
} else {
overflow_pages.push((page_no, page));
}
}
if !direct_pages.is_empty() {
self.atomic.insert_batch(direct_pages);
}
if !overflow_pages.is_empty() {
self.overflow.insert_batch(overflow_pages);
}
}
fn len(&self) -> usize {
self.atomic.len().saturating_add(self.overflow.len())
}
fn prefetch_hint(&self, page_no: PageNumber) {
if self.atomic.accepts(page_no) {
self.atomic.prefetch_hint(page_no);
} else {
self.overflow.prefetch_hint(page_no);
}
}
#[cfg(test)]
fn atomic_slot_count(&self) -> usize {
self.atomic.slot_count()
}
#[cfg(test)]
fn atomic_active_slot_count(&self) -> usize {
self.atomic.active_slot_count()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParallelWalPublicationIntent {
pub certificate_epoch: u64,
pub visible_commit_seq: CommitSeq,
pub page_plane_visible_commit_seq: CommitSeq,
pub db_size: u32,
pub journal_mode: JournalMode,
pub freelist_count: usize,
pub checkpoint_active: bool,
pub page_set_size: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PagerMetadataPublicationClass {
SnapshotSummary,
PagePlaneResidency,
CertificateDerivedIntent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PagerMetadataPublicationContract {
pub class: PagerMetadataPublicationClass,
pub touchpoint: &'static str,
pub current_primitive: &'static str,
pub selected_primitive: &'static str,
pub retry_contract: &'static str,
pub fallback_contract: &'static str,
}
pub const PAGER_METADATA_PUBLICATION_CONTRACTS: [PagerMetadataPublicationContract; 3] = [
PagerMetadataPublicationContract {
class: PagerMetadataPublicationClass::SnapshotSummary,
touchpoint: "pager.rs::PublishedPagerState::{snapshot,finalize_publish}",
current_primitive: "writer-serialized seqlock summary over visible_commit_seq/db_size/journal_mode/freelist/checkpoint/page_set_size",
selected_primitive: "keep seqlock-style summary publication",
retry_contract: "readers retry while the sequence is odd or unstable and never take the publish lock",
fallback_contract: "targeted sequence waits may park briefly before re-reading the summary",
},
PagerMetadataPublicationContract {
class: PagerMetadataPublicationClass::PagePlaneResidency,
touchpoint: "pager.rs::PublishedPages plus page_plane_visible_commit_seq",
current_primitive: "lock-free published-page plane gated by a monotone lag-detecting horizon",
selected_primitive: "keep monotone horizon with explicit cache/inner fallback",
retry_contract: "published pages are readable only when the page-plane horizon covers the bound snapshot; otherwise the read must fall back",
fallback_contract: "a lagging page plane is intentional and forces cache/inner reads instead of serving stale published pages",
},
PagerMetadataPublicationContract {
class: PagerMetadataPublicationClass::CertificateDerivedIntent,
touchpoint: "pager.rs::ParallelWalPublicationIntent",
current_primitive: "immutable certificate-derived intent handed from ordered residue into publish",
selected_primitive: "keep immutable certificate-derived handoff; never publish beyond durable certificate scope",
retry_contract: "not directly polled by readers; it constrains what the pager may expose as visible",
fallback_contract: "certificate mismatch or gap forces conservative publication and blocks newer visibility",
},
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PagerPublishedSnapshot {
pub snapshot_gen: u64,
pub visible_commit_seq: CommitSeq,
pub db_size: u32,
pub journal_mode: JournalMode,
pub freelist_count: usize,
pub checkpoint_active: bool,
pub page_set_size: usize,
}
#[cfg(test)]
mod metadata_publication_contract_tests {
use super::{
PAGER_METADATA_PUBLICATION_CONTRACTS, PagerMetadataPublicationClass,
PagerMetadataPublicationContract,
};
fn contract(class: PagerMetadataPublicationClass) -> PagerMetadataPublicationContract {
PAGER_METADATA_PUBLICATION_CONTRACTS
.iter()
.find(|contract| contract.class == class)
.copied()
.expect("pager metadata contract must exist")
}
#[test]
fn test_pager_metadata_publication_contract_keeps_seqlock_summary() {
let summary = contract(PagerMetadataPublicationClass::SnapshotSummary);
assert_eq!(
summary.selected_primitive,
"keep seqlock-style summary publication"
);
assert!(
summary.retry_contract.contains("retry"),
"summary publication must preserve explicit seqlock retry semantics"
);
}
#[test]
fn test_pager_metadata_publication_contract_keeps_page_plane_fallback_boundary() {
let page_plane = contract(PagerMetadataPublicationClass::PagePlaneResidency);
assert!(
page_plane.fallback_contract.contains("cache/inner"),
"page-plane contract must preserve the explicit fallback boundary"
);
}
}
#[derive(Debug, Clone, Copy)]
struct PublishedPagerUpdate {
visible_commit_seq: CommitSeq,
db_size: u32,
journal_mode: JournalMode,
freelist_count: usize,
checkpoint_active: bool,
}
#[derive(Debug)]
struct PublishedPagerState {
publish_lock: Mutex<()>,
sequence_gate: Mutex<()>,
sequence_cv: Condvar,
sequence_waiters: KeyedWaitRegistry,
sequence: AtomicU64,
visible_commit_seq: AtomicU64,
page_plane_visible_commit_seq: AtomicU64,
db_size: AtomicU32,
journal_mode: AtomicU8,
freelist_count: AtomicUsize,
checkpoint_active: AtomicBool,
page_set_size: AtomicUsize,
publication_write_count: AtomicU64,
read_retry_count: StripedCounter64,
published_page_hits: StripedCounter64,
pages: PublishedPages,
}
impl PublishedPagerState {
fn new(
db_size: u32,
visible_commit_seq: CommitSeq,
journal_mode: JournalMode,
freelist_count: usize,
) -> Self {
Self {
publish_lock: Mutex::new(()),
sequence_gate: Mutex::new(()),
sequence_cv: Condvar::new(),
sequence_waiters: KeyedWaitRegistry::new(),
sequence: AtomicU64::new(2),
visible_commit_seq: AtomicU64::new(visible_commit_seq.get()),
page_plane_visible_commit_seq: AtomicU64::new(visible_commit_seq.get()),
db_size: AtomicU32::new(db_size),
journal_mode: AtomicU8::new(encode_journal_mode(journal_mode)),
freelist_count: AtomicUsize::new(freelist_count),
checkpoint_active: AtomicBool::new(false),
page_set_size: AtomicUsize::new(0),
publication_write_count: AtomicU64::new(0),
read_retry_count: StripedCounter64::new(),
published_page_hits: StripedCounter64::new(),
pages: PublishedPages::new(db_size),
}
}
fn signal_sequence_waiters(&self, observed_sequence: u64, stage: &'static str) {
match PUBLISHED_SEQUENCE_WAIT_PATH_MODE {
WaitPathMode::KeyedEventcount => {
let woke_waiters = self.sequence_waiters.signal(observed_sequence);
tracing::trace!(
target: "fsqlite.snapshot_publication",
run_id = "pager-publication",
scenario_id = "sequence_wait_signal",
observed_sequence,
stage,
wait_strategy = PUBLISHED_SEQUENCE_WAIT_PATH_MODE.as_str(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
woke_waiters,
"signaled targeted publication waiters"
);
}
WaitPathMode::LegacyCondvarTimeout if stage == "publish_complete" => {
self.sequence_cv.notify_all();
}
WaitPathMode::LegacyCondvarTimeout => {}
}
}
fn snapshot(&self) -> PagerPublishedSnapshot {
loop {
let snapshot_gen = self.sequence.load(AtomicOrdering::Acquire);
if snapshot_gen % 2 == 1 {
self.record_retry();
self.wait_for_sequence_change(snapshot_gen, PUBLISHED_SNAPSHOT_WAIT_SLICE);
continue;
}
let visible_commit_seq =
CommitSeq::new(self.visible_commit_seq.load(AtomicOrdering::Acquire));
let db_size = self.db_size.load(AtomicOrdering::Acquire);
let journal_mode = decode_journal_mode(self.journal_mode.load(AtomicOrdering::Acquire));
let freelist_count = self.freelist_count.load(AtomicOrdering::Acquire);
let checkpoint_active = self.checkpoint_active.load(AtomicOrdering::Acquire);
let page_set_size = self.page_set_size.load(AtomicOrdering::Acquire);
if self.sequence.load(AtomicOrdering::Acquire) == snapshot_gen {
return PagerPublishedSnapshot {
snapshot_gen,
visible_commit_seq,
db_size,
journal_mode,
freelist_count,
checkpoint_active,
page_set_size,
};
}
self.record_retry();
self.wait_for_sequence_change(snapshot_gen, PUBLISHED_SNAPSHOT_WAIT_SLICE);
}
}
fn try_get_page(&self, page_no: PageNumber) -> Option<PageData> {
self.pages.get(page_no)
}
fn current_sequence_gen(&self) -> u64 {
self.sequence.load(AtomicOrdering::Acquire)
}
fn prefetch_page_hint(&self, page_no: PageNumber) {
self.pages.prefetch_hint(page_no);
}
fn page_plane_visible_commit_seq(&self) -> CommitSeq {
CommitSeq::new(
self.page_plane_visible_commit_seq
.load(AtomicOrdering::Acquire),
)
}
fn should_skip_stale_publish(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
scenario_id: &'static str,
) -> bool {
let current_visible_commit_seq =
CommitSeq::new(self.visible_commit_seq.load(AtomicOrdering::Acquire));
if current_visible_commit_seq > update.visible_commit_seq {
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id,
observed_commit_seq = update.visible_commit_seq.get(),
visible_commit_seq = current_visible_commit_seq.get(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
"skipping stale pager publication"
);
return true;
}
false
}
fn publish_observed_page(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
page_no: PageNumber,
page: PageData,
) -> bool {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_read_publish_skip") {
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = "stale_read_publish_page_skip",
page_no = page_no.get(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
"skipping stale published page observation"
);
return false;
}
self.publish_insert_page_locked(cx, update, page_no, page);
true
}
fn publish_clear_if(&self, cx: &Cx, update: PublishedPagerUpdate, should_clear: bool) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_clear_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
if should_clear {
self.pages.clear();
}
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, should_clear);
}
fn publish_remove_page(&self, cx: &Cx, update: PublishedPagerUpdate, page_no: PageNumber) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_remove_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
self.pages.remove(page_no);
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
}
fn publish_metadata_only(&self, cx: &Cx, update: PublishedPagerUpdate) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_metadata_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, false);
}
fn publish_single_connection_metadata_update(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
clear_pages: bool,
) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_single_connection_metadata_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
if clear_pages {
self.pages.clear();
}
self.sync_metadata_without_page_publish(update);
let previous_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
let snapshot_gen = previous_sequence.saturating_add(1);
self.signal_sequence_waiters(previous_sequence, "publish_complete");
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = if clear_pages {
"metadata_only_plane_clear"
} else {
"metadata_only_summary_only"
},
snapshot_gen,
visible_commit_seq = self.visible_commit_seq.load(AtomicOrdering::Acquire),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
freelist_count = self.freelist_count.load(AtomicOrdering::Acquire),
checkpoint_active = self.checkpoint_active.load(AtomicOrdering::Acquire),
read_retry_count = self.read_retry_count(),
page_set_size = self.page_set_size.load(AtomicOrdering::Acquire),
elapsed_ns = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX),
"published metadata-only single-connection summary"
);
}
fn publish_truncate_checkpoint(&self, cx: &Cx, update: PublishedPagerUpdate, max_page: u32) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_truncate_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
self.pages.retain(|page_no| page_no.get() <= max_page);
self.pages.remove(PageNumber::ONE);
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
self.db_size.store(update.db_size, AtomicOrdering::Release);
}
fn publish_commit<S: std::hash::BuildHasher>(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
write_set: &HashMap<PageNumber, StagedPage, S>,
) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_commit_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
let previous_db_size = self.db_size.load(AtomicOrdering::Acquire);
let previous_visible_commit_seq = self.visible_commit_seq.load(AtomicOrdering::Acquire);
if update.db_size < previous_db_size
&& update.visible_commit_seq.get() >= previous_visible_commit_seq
{
self.pages.retain(|page_no| page_no.get() <= update.db_size);
}
self.pages.insert_batch(
write_set
.iter()
.map(|(&page_no, staged)| (page_no, staged.published_page())),
);
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
}
fn publish_commit_consuming_pages<I>(&self, cx: &Cx, update: PublishedPagerUpdate, pages: I)
where
I: IntoIterator<Item = (PageNumber, StagedPage)>,
{
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_commit_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
let previous_db_size = self.db_size.load(AtomicOrdering::Acquire);
let previous_visible_commit_seq = self.visible_commit_seq.load(AtomicOrdering::Acquire);
if update.db_size < previous_db_size
&& update.visible_commit_seq.get() >= previous_visible_commit_seq
{
self.pages.retain(|page_no| page_no.get() <= update.db_size);
}
self.pages.insert_batch(
pages
.into_iter()
.map(|(page_no, staged)| (page_no, staged.into_published_page())),
);
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
}
fn publish_commit_single_page(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
page_no: PageNumber,
staged: StagedPage,
) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_commit_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
let previous_db_size = self.db_size.load(AtomicOrdering::Acquire);
let previous_visible_commit_seq = self.visible_commit_seq.load(AtomicOrdering::Acquire);
if update.db_size < previous_db_size
&& update.visible_commit_seq.get() >= previous_visible_commit_seq
{
self.pages
.retain(|published_page_no| published_page_no.get() <= update.db_size);
}
let _ = self.pages.insert(page_no, staged.into_published_page());
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
}
fn publish_commit_draining_write_set<S: std::hash::BuildHasher>(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
write_set: &mut HashMap<PageNumber, StagedPage, S>,
) {
if write_set.len() == 1
&& let Some((&page_no, _)) = write_set.iter().next()
&& let Some(staged) = write_set.remove(&page_no)
{
self.publish_commit_single_page(cx, update, page_no, staged);
return;
}
self.publish_commit_consuming_pages(cx, update, write_set.drain());
}
#[cfg(test)]
fn publish_commit_staged_pages(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
staged_pages: Vec<(PageNumber, StagedPage)>,
) {
self.publish_commit_consuming_pages(cx, update, staged_pages);
}
#[cfg(test)]
fn publish_insert_single(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
page_no: PageNumber,
page: PageData,
) {
let _publish_guard = self
.publish_lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.should_skip_stale_publish(cx, update, "stale_insert_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
self.pages.insert(page_no, page);
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
}
fn publish_insert_page_locked(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
page_no: PageNumber,
page: PageData,
) {
if self.should_skip_stale_publish(cx, update, "stale_insert_publish_skip") {
return;
}
let start = Instant::now();
let publish_start_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
self.signal_sequence_waiters(publish_start_sequence, "publish_begin");
self.pages.insert(page_no, page);
let page_set_size = self.pages.len();
self.finalize_publish(cx, update, page_set_size, start, true);
}
fn finalize_publish(
&self,
cx: &Cx,
update: PublishedPagerUpdate,
page_set_size: usize,
start: Instant,
page_plane_synced: bool,
) {
self.publication_write_count
.fetch_add(1, AtomicOrdering::Relaxed);
self.visible_commit_seq
.fetch_max(update.visible_commit_seq.get(), AtomicOrdering::Release);
self.db_size
.fetch_max(update.db_size, AtomicOrdering::Release);
self.journal_mode.store(
encode_journal_mode(update.journal_mode),
AtomicOrdering::Release,
);
self.freelist_count
.store(update.freelist_count, AtomicOrdering::Release);
self.checkpoint_active
.store(update.checkpoint_active, AtomicOrdering::Release);
self.page_set_size
.store(page_set_size, AtomicOrdering::Release);
if page_plane_synced {
self.page_plane_visible_commit_seq
.store(update.visible_commit_seq.get(), AtomicOrdering::Release);
}
let previous_sequence = self.sequence.fetch_add(1, AtomicOrdering::AcqRel);
let snapshot_gen = previous_sequence.saturating_add(1);
self.signal_sequence_waiters(previous_sequence, "publish_complete");
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = "metadata_publish",
snapshot_gen,
visible_commit_seq = update.visible_commit_seq.get(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
freelist_count = update.freelist_count,
checkpoint_active = update.checkpoint_active,
read_retry_count = self.read_retry_count(),
page_set_size,
elapsed_ns = u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX),
"published pager snapshot"
);
}
fn sync_metadata_without_page_publish(&self, update: PublishedPagerUpdate) {
self.visible_commit_seq
.fetch_max(update.visible_commit_seq.get(), AtomicOrdering::Release);
self.db_size
.fetch_max(update.db_size, AtomicOrdering::Release);
self.journal_mode.store(
encode_journal_mode(update.journal_mode),
AtomicOrdering::Release,
);
self.freelist_count
.store(update.freelist_count, AtomicOrdering::Release);
self.checkpoint_active
.store(update.checkpoint_active, AtomicOrdering::Release);
self.page_set_size.store(0, AtomicOrdering::Release);
}
fn note_published_hit(&self) {
self.published_page_hits.increment();
}
fn record_retry(&self) {
self.read_retry_count.increment();
}
fn wait_for_sequence_change(&self, observed_sequence: u64, timeout: Duration) {
match PUBLISHED_SEQUENCE_WAIT_PATH_MODE {
WaitPathMode::KeyedEventcount => {
if self.sequence.load(AtomicOrdering::Acquire) != observed_sequence {
return;
}
let slot = self.sequence_waiters.slot(observed_sequence);
let observed_generation = slot.generation();
if self.sequence.load(AtomicOrdering::Acquire) != observed_sequence {
return;
}
let wait_result = slot.wait_for_change(observed_generation, timeout);
tracing::trace!(
target: "fsqlite.snapshot_publication",
run_id = "pager-publication",
scenario_id = match wait_result {
KeyedWaitResult::Signaled => "sequence_wait_woke",
KeyedWaitResult::TimedOut => "sequence_wait_timeout_fallback",
},
observed_sequence,
wait_strategy = PUBLISHED_SEQUENCE_WAIT_PATH_MODE.as_str(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
"publication wait finished"
);
}
WaitPathMode::LegacyCondvarTimeout => {
let guard = self
.sequence_gate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (_guard, _timeout) = self
.sequence_cv
.wait_timeout_while(guard, timeout, |()| {
self.sequence.load(AtomicOrdering::Acquire) == observed_sequence
})
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
}
fn read_retry_count(&self) -> u64 {
self.read_retry_count.load()
}
fn published_page_hits(&self) -> u64 {
self.published_page_hits.load()
}
fn publication_write_count(&self) -> u64 {
self.publication_write_count.load(AtomicOrdering::Relaxed)
}
}
const fn encode_journal_mode(mode: JournalMode) -> u8 {
match mode {
JournalMode::Delete => 0,
JournalMode::Wal => 1,
}
}
const fn decode_journal_mode(raw: u8) -> JournalMode {
match raw {
1 => JournalMode::Wal,
_ => JournalMode::Delete,
}
}
#[inline]
const fn transaction_mode_is_eager_writer(mode: TransactionMode) -> bool {
matches!(
mode,
TransactionMode::Immediate | TransactionMode::Exclusive
)
}
fn wait_for_single_writer_baton<'a, F: VfsFile>(
inner_mutex: &'a Mutex<PagerInner<F>>,
writer_idle: &Condvar,
mut inner: MutexGuard<'a, PagerInner<F>>,
) -> Result<MutexGuard<'a, PagerInner<F>>> {
if !inner.writer_active {
return Ok(inner);
}
drop(inner);
for _ in 0..SINGLE_WRITER_BATON_SPINS {
std::hint::spin_loop();
}
inner = inner_mutex
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if !inner.writer_active {
return Ok(inner);
}
let deadline = Instant::now() + SINGLE_WRITER_BATON_PARK;
loop {
let Some(remaining) = deadline.checked_duration_since(Instant::now()) else {
return Err(FrankenError::Busy);
};
if remaining.is_zero() {
return Err(FrankenError::Busy);
}
let wait_result = writer_idle
.wait_timeout(inner, remaining)
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
inner = wait_result.0;
if !inner.writer_active {
return Ok(inner);
}
if wait_result.1.timed_out() {
return Err(FrankenError::Busy);
}
}
}
fn release_single_writer_baton<F: VfsFile>(inner: &mut PagerInner<F>) -> bool {
let was_active = inner.writer_active;
inner.writer_active = false;
was_active
}
pub struct SimplePager<V: Vfs> {
vfs: Arc<V>,
db_path: PathBuf,
inner: Arc<Mutex<PagerInner<V::File>>>,
writer_idle: Arc<Condvar>,
cache: Arc<ShardedPageCache>,
pool: PageBufPool,
published: Arc<PublishedPagerState>,
wal_backend: SharedWalBackend,
committed_snapshot: Arc<RwLock<Arc<PagerCommittedSnapshot>>>,
shared_connection_count: OnceLock<Arc<AtomicUsize>>,
}
impl<V: Vfs> traits::sealed::Sealed for SimplePager<V> {}
fn page_size_from_header_bytes(header_bytes: &[u8; DATABASE_HEADER_SIZE]) -> Option<PageSize> {
if &header_bytes[..DATABASE_HEADER_MAGIC.len()] != DATABASE_HEADER_MAGIC {
return None;
}
let raw = u16::from_be_bytes([header_bytes[16], header_bytes[17]]);
let page_size = match raw {
1 => 65_536,
0 => return None,
value => u32::from(value),
};
PageSize::new(page_size)
}
fn stale_main_header_is_wal_recoverable_error(error: &DatabaseHeaderError) -> bool {
matches!(
error,
DatabaseHeaderError::InvalidSchemaFormat { raw: 0 }
| DatabaseHeaderError::InvalidTextEncoding { raw: 0 }
)
}
fn change_counter_from_header_bytes(header_bytes: &[u8; DATABASE_HEADER_SIZE]) -> u32 {
u32::from_be_bytes([
header_bytes[24],
header_bytes[25],
header_bytes[26],
header_bytes[27],
])
}
fn stale_main_header_change_counter_under_wal(
header_bytes: &[u8; DATABASE_HEADER_SIZE],
error: &DatabaseHeaderError,
) -> Option<u64> {
if !stale_main_header_is_wal_recoverable_error(error) {
return None;
}
page_size_from_header_bytes(header_bytes)?;
Some(u64::from(change_counter_from_header_bytes(header_bytes)))
}
fn wal_contains_valid_database_page1<F: VfsFile>(
cx: &Cx,
wal: &mut WalFile<F>,
expected_page_size: PageSize,
) -> bool {
let Ok(expected_page_size_usize) = usize::try_from(expected_page_size.get()) else {
return false;
};
if wal.page_size() != expected_page_size_usize {
return false;
}
let Some(last_commit_frame) = wal.last_commit_frame(cx).ok().flatten() else {
return false;
};
for frame_index in (0..=last_commit_frame).rev() {
let Ok((frame_header, page_data)) = wal.read_frame(cx, frame_index) else {
return false;
};
if frame_header.page_number != PageNumber::ONE.get() {
continue;
}
if page_data.len() < DATABASE_HEADER_SIZE {
return false;
}
let mut page1_header_bytes = [0_u8; DATABASE_HEADER_SIZE];
page1_header_bytes.copy_from_slice(&page_data[..DATABASE_HEADER_SIZE]);
return DatabaseHeader::from_bytes(&page1_header_bytes).is_ok();
}
false
}
fn stale_main_header_can_be_recovered_from_live_wal<V: Vfs>(
cx: &Cx,
vfs: &V,
path: &Path,
header_bytes: &[u8; DATABASE_HEADER_SIZE],
error: &DatabaseHeaderError,
allow_readonly_wal_probe: bool,
) -> Result<bool> {
if !stale_main_header_is_wal_recoverable_error(error) {
return Ok(false);
}
let Some(expected_page_size) = page_size_from_header_bytes(header_bytes) else {
return Ok(false);
};
let mut wal_path = path.to_owned().into_os_string();
wal_path.push("-wal");
let wal_path = PathBuf::from(wal_path);
if !vfs.access(cx, &wal_path, AccessFlags::EXISTS)? {
return Ok(false);
}
let (wal_file, _) = match vfs.open(
cx,
Some(&wal_path),
VfsOpenFlags::READWRITE | VfsOpenFlags::WAL,
) {
Ok(opened) => opened,
Err(_) if allow_readonly_wal_probe => {
match vfs.open(
cx,
Some(&wal_path),
VfsOpenFlags::READONLY | VfsOpenFlags::WAL,
) {
Ok(opened) => opened,
Err(_) => return Ok(false),
}
}
Err(_) => return Ok(false),
};
let Ok(mut wal) = WalFile::open(cx, wal_file) else {
return Ok(false);
};
let wal_contains_valid_page1 =
wal_contains_valid_database_page1(cx, &mut wal, expected_page_size);
let _ = wal.close(cx);
Ok(wal_contains_valid_page1)
}
fn bootstrap_header_from_stale_main_file(
header_bytes: &[u8; DATABASE_HEADER_SIZE],
page_size: PageSize,
) -> DatabaseHeader {
DatabaseHeader {
page_size,
write_version: header_bytes[18],
read_version: header_bytes[19],
reserved_per_page: header_bytes[20],
change_counter: u32::from_be_bytes([
header_bytes[24],
header_bytes[25],
header_bytes[26],
header_bytes[27],
]),
page_count: u32::from_be_bytes([
header_bytes[28],
header_bytes[29],
header_bytes[30],
header_bytes[31],
]),
freelist_trunk: u32::from_be_bytes([
header_bytes[32],
header_bytes[33],
header_bytes[34],
header_bytes[35],
]),
freelist_count: u32::from_be_bytes([
header_bytes[36],
header_bytes[37],
header_bytes[38],
header_bytes[39],
]),
schema_cookie: u32::from_be_bytes([
header_bytes[40],
header_bytes[41],
header_bytes[42],
header_bytes[43],
]),
schema_format: u32::from_be_bytes([
header_bytes[44],
header_bytes[45],
header_bytes[46],
header_bytes[47],
]),
default_cache_size: i32::from_be_bytes([
header_bytes[48],
header_bytes[49],
header_bytes[50],
header_bytes[51],
]),
largest_root_page: u32::from_be_bytes([
header_bytes[52],
header_bytes[53],
header_bytes[54],
header_bytes[55],
]),
text_encoding: fsqlite_types::TextEncoding::Utf8,
user_version: u32::from_be_bytes([
header_bytes[60],
header_bytes[61],
header_bytes[62],
header_bytes[63],
]),
incremental_vacuum: u32::from_be_bytes([
header_bytes[64],
header_bytes[65],
header_bytes[66],
header_bytes[67],
]),
application_id: u32::from_be_bytes([
header_bytes[68],
header_bytes[69],
header_bytes[70],
header_bytes[71],
]),
version_valid_for: u32::from_be_bytes([
header_bytes[92],
header_bytes[93],
header_bytes[94],
header_bytes[95],
]),
sqlite_version: u32::from_be_bytes([
header_bytes[96],
header_bytes[97],
header_bytes[98],
header_bytes[99],
]),
}
}
impl<V> MvccPager for SimplePager<V>
where
V: Vfs + Send + Sync,
V::File: Send + Sync,
{
type Txn = SimpleTransaction<V>;
fn begin(&self, cx: &Cx, mode: TransactionMode) -> Result<Self::Txn> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.checkpoint_active {
let active_transactions = inner.active_transactions;
let checkpoint_active = inner.checkpoint_active;
drop(inner);
log_checkpoint_coordination(
cx,
&pager_group_commit_queue(self),
"active_gate",
"begin",
transaction_mode_name(mode),
"checkpoint_excludes_new_transactions",
true,
active_transactions,
checkpoint_active,
);
return Err(FrankenError::Busy);
}
let eager_writer = transaction_mode_is_eager_writer(mode);
if eager_writer {
inner = wait_for_single_writer_baton(&self.inner, &self.writer_idle, inner)?;
}
let active_transactions_before_begin = inner.active_transactions;
if self.vfs.is_memory() {
let had_recovery_pending = inner.rollback_journal_recovery_state.is_pending();
let commit_seq_before_refresh = inner.commit_seq;
if active_transactions_before_begin == 0 {
let journal_path = Self::journal_path(&self.db_path);
let journal_exists = self.vfs.access(cx, &journal_path, AccessFlags::EXISTS)?;
let journal_visibility_invalidation = had_recovery_pending || journal_exists;
{
if inner.rollback_journal_recovery_state.is_pending() || journal_exists {
let page_size = inner.page_size;
match Self::recover_rollback_journal_if_present_locked(
cx,
&*self.vfs,
&mut inner.db_file,
&journal_path,
page_size,
LockLevel::None,
) {
Ok(false) if had_recovery_pending => {
return Err(FrankenError::internal(
"rollback journal missing while local recovery was pending",
));
}
Ok(_) => {}
Err(err) => return Err(err),
}
self.cache.clear();
inner.rollback_journal_recovery_state = RollbackJournalRecoveryState::Clean;
}
inner.refresh_committed_state(cx, &self.cache, &self.wal_backend)?;
}
let clear_published_pages = journal_visibility_invalidation
|| inner.commit_seq != commit_seq_before_refresh;
self.published.publish_clear_if(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
clear_published_pages,
);
}
if eager_writer && inner.writer_active {
return Err(FrankenError::Busy);
}
if eager_writer {
inner.writer_active = true;
}
inner.active_transactions += 1;
let original_db_size = inner.db_size;
let journal_mode = inner.journal_mode;
let published_snapshot = self.published.snapshot();
let bound_visible_commit_seq =
std::cmp::max(published_snapshot.visible_commit_seq, inner.commit_seq);
let bound_db_size = published_snapshot.db_size.max(original_db_size);
let pool = self.pool.clone();
let cleanup_cx = cx.clone();
let memory_db_bump_alloc =
self.vfs.is_memory() && self.db_path == Path::new("/:memory:");
return Ok(SimpleTransaction {
vfs: Arc::clone(&self.vfs),
journal_path: Self::journal_path(&self.db_path),
group_commit_queue: group_commit_queue_for_backend(
self.vfs.as_ref(),
&self.db_path,
),
inner: Arc::clone(&self.inner),
writer_idle: Arc::clone(&self.writer_idle),
cache: Arc::clone(&self.cache),
published: Arc::clone(&self.published),
wal_backend: Arc::clone(&self.wal_backend),
committed_snapshot: Arc::clone(&self.committed_snapshot),
shared_connection_count: self.shared_connection_count.get().cloned(),
published_visible_commit_seq: Cell::new(bound_visible_commit_seq),
published_db_size: Cell::new(bound_db_size),
write_set: PagePageMap::default(),
write_pages_sorted: Vec::new(),
freed_pages: Vec::new(),
freed_page_bounds: None,
allocated_from_freelist: Vec::new(),
allocated_from_eof: Vec::new(),
writes_observed: false,
mode,
is_writer: eager_writer,
committed: false,
finished: false,
original_db_size,
savepoint_stack: Vec::new(),
journal_mode,
pool,
cleanup_cx,
page_lease: Vec::new(),
memory_db_bump_alloc,
rolled_back_pages: HashSet::new(),
txn_read_cache: RefCell::new(PagePageMap::default()),
retained_memory_overlay_dirty_pages: BTreeSet::new(),
scratch_arena: bumpalo::Bump::new(),
});
}
let had_recovery_pending = inner.rollback_journal_recovery_state.is_pending();
let commit_seq_before_refresh = inner.commit_seq;
if active_transactions_before_begin == 0 {
inner.db_file.lock(cx, LockLevel::Shared)?;
}
let mut journal_visibility_invalidation = false;
let wal_snapshot_initialized = if active_transactions_before_begin == 0 {
let journal_path = Self::journal_path(&self.db_path);
let journal_exists = match self.vfs.access(cx, &journal_path, AccessFlags::EXISTS) {
Ok(exists) => exists,
Err(err) => {
let _ = inner.db_file.unlock(cx, LockLevel::None);
return Err(err);
}
};
journal_visibility_invalidation = had_recovery_pending || journal_exists;
if inner.rollback_journal_recovery_state.is_pending() || journal_exists {
let page_size = inner.page_size;
match Self::recover_rollback_journal_if_present_locked(
cx,
&*self.vfs,
&mut inner.db_file,
&journal_path,
page_size,
LockLevel::Shared,
) {
Ok(false) if inner.rollback_journal_recovery_state.is_pending() => {
inner.db_file.unlock(cx, LockLevel::None)?;
return Err(FrankenError::internal(
"rollback journal missing while local recovery was pending",
));
}
Ok(_) => {}
Err(err) => {
let _ = inner.db_file.unlock(cx, LockLevel::None);
return Err(err);
}
}
self.cache.clear();
inner.rollback_journal_recovery_state = RollbackJournalRecoveryState::Clean;
}
match inner.refresh_committed_state(cx, &self.cache, &self.wal_backend) {
Ok(v) => v,
Err(err) => {
let _ = inner.db_file.unlock(cx, LockLevel::None);
return Err(err);
}
}
} else {
false
};
if active_transactions_before_begin == 0 {
let clear_published_pages =
journal_visibility_invalidation || inner.commit_seq != commit_seq_before_refresh;
self.published.publish_clear_if(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
clear_published_pages,
);
}
let published_snapshot = self.published.snapshot();
let commit_seq_lagged = published_snapshot.visible_commit_seq < inner.commit_seq;
let db_size_lagged = published_snapshot.db_size < inner.db_size;
let journal_mode_lagged = published_snapshot.journal_mode != inner.journal_mode;
let freelist_lagged = published_snapshot.freelist_count != inner.freelist.len();
let checkpoint_lagged = published_snapshot.checkpoint_active != inner.checkpoint_active;
let publication_lagged = commit_seq_lagged
|| db_size_lagged
|| journal_mode_lagged
|| freelist_lagged
|| checkpoint_lagged;
if publication_lagged {
let publication_update = PublishedPagerUpdate {
visible_commit_seq: std::cmp::max(
published_snapshot.visible_commit_seq,
inner.commit_seq,
),
db_size: published_snapshot.db_size.max(inner.db_size),
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
};
let clear_published_pages = published_snapshot.visible_commit_seq
!= publication_update.visible_commit_seq
|| published_snapshot.db_size != publication_update.db_size
|| published_snapshot.journal_mode != publication_update.journal_mode;
self.published
.publish_clear_if(cx, publication_update, clear_published_pages);
}
if eager_writer && inner.writer_active {
if active_transactions_before_begin == 0 {
inner.db_file.unlock(cx, LockLevel::None)?;
}
return Err(FrankenError::Busy);
}
if eager_writer {
if let Err(err) = inner.db_file.lock(cx, LockLevel::Reserved) {
let preserve_level = retained_lock_level_after_txn_exit(
active_transactions_before_begin,
inner.writer_active,
);
inner.db_file.unlock(cx, preserve_level)?;
return Err(err);
}
inner.writer_active = true;
}
if inner.journal_mode == JournalMode::Wal && !wal_snapshot_initialized {
let wal_begin_result =
with_wal_backend(&self.wal_backend, |wal| wal.begin_transaction(cx));
if let Err(err) = wal_begin_result {
let notify_writer_idle = eager_writer && release_single_writer_baton(&mut inner);
let preserve_level = retained_lock_level_after_txn_exit(
active_transactions_before_begin,
inner.writer_active,
);
inner.db_file.unlock(cx, preserve_level)?;
drop(inner);
if notify_writer_idle {
self.writer_idle.notify_one();
}
return Err(err);
}
}
inner.active_transactions = inner.active_transactions.saturating_add(1);
let original_db_size = inner.db_size;
let journal_mode = inner.journal_mode;
let pool = self.pool.clone();
let published_snapshot = self.published.snapshot();
let cleanup_cx = cleanup_child_cx(cx);
let memory_db_bump_alloc = self.vfs.is_memory() && self.db_path == Path::new("/:memory:");
drop(inner);
Ok(SimpleTransaction {
vfs: Arc::clone(&self.vfs),
journal_path: Self::journal_path(&self.db_path),
group_commit_queue: group_commit_queue_for_backend(self.vfs.as_ref(), &self.db_path),
inner: Arc::clone(&self.inner),
writer_idle: Arc::clone(&self.writer_idle),
cache: Arc::clone(&self.cache),
published: Arc::clone(&self.published),
wal_backend: Arc::clone(&self.wal_backend),
committed_snapshot: Arc::clone(&self.committed_snapshot),
shared_connection_count: self.shared_connection_count.get().cloned(),
published_visible_commit_seq: Cell::new(published_snapshot.visible_commit_seq),
published_db_size: Cell::new(published_snapshot.db_size),
write_set: PagePageMap::default(),
write_pages_sorted: Vec::new(),
freed_pages: Vec::new(),
freed_page_bounds: None,
allocated_from_freelist: Vec::new(),
allocated_from_eof: Vec::new(),
writes_observed: false,
mode,
is_writer: eager_writer,
committed: false,
finished: false,
original_db_size,
savepoint_stack: Vec::new(),
journal_mode,
pool,
cleanup_cx,
page_lease: Vec::new(),
memory_db_bump_alloc,
rolled_back_pages: HashSet::new(),
txn_read_cache: RefCell::new(PagePageMap::default()),
retained_memory_overlay_dirty_pages: BTreeSet::new(),
scratch_arena: bumpalo::Bump::new(),
})
}
fn journal_mode(&self) -> JournalMode {
self.published.snapshot().journal_mode
}
fn is_readonly(&self) -> bool {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.access_mode.is_readonly()
}
fn set_journal_mode(&self, cx: &Cx, mode: JournalMode) -> Result<JournalMode> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.journal_mode == mode {
if mode == JournalMode::Wal && !has_wal_backend(&self.wal_backend)? {
return Err(FrankenError::Unsupported);
}
if mode == JournalMode::Wal {
self.cache.evict(PageNumber::ONE);
self.published.publish_remove_page(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
PageNumber::ONE,
);
}
return Ok(mode);
}
if inner.checkpoint_active {
return Err(FrankenError::Busy);
}
if inner.active_transactions > 0 {
return Err(FrankenError::Busy);
}
if mode == JournalMode::Wal && !has_wal_backend(&self.wal_backend)? {
return Err(FrankenError::Unsupported);
}
let version_byte: u8 = if mode == JournalMode::Wal { 2 } else { 1 };
if inner.db_size > 0 && !inner.access_mode.is_readonly() {
let page_size = inner.page_size.as_usize();
let mut page1 = vec![0u8; page_size];
let bytes_read = inner.db_file.read(cx, &mut page1, 0)?;
if bytes_read >= DATABASE_HEADER_SIZE {
page1[18] = version_byte;
page1[19] = version_byte;
inner.db_file.write(cx, &page1, 0)?;
self.cache.evict(PageNumber::ONE);
}
}
inner.journal_mode = mode;
self.published.publish_remove_page(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
PageNumber::ONE,
);
drop(inner);
Ok(mode)
}
fn set_wal_backend(&self, backend: Box<dyn WalBackend>) -> Result<()> {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.checkpoint_active {
return Err(FrankenError::Busy);
}
drop(inner);
let mut wal_guard = self
.wal_backend
.write()
.map_err(|_| FrankenError::internal("SharedWalBackend lock poisoned"))?;
*wal_guard = Some(backend);
drop(wal_guard);
Ok(())
}
}
impl<V: Vfs> SimplePager<V>
where
V::File: Send + Sync,
{
const EXPORT_COPY_CHUNK_SIZE: usize = 64 * 1024;
#[must_use]
pub fn db_path(&self) -> &Path {
&self.db_path
}
pub fn vfs_handle(&self) -> Arc<V> {
Arc::clone(&self.vfs)
}
pub fn set_vfs_busy_timeout_ms(&self, ms: u64) {
if let Ok(mut inner) = self.inner.lock() {
inner.db_file.set_busy_timeout_ms(ms);
}
}
pub fn set_wal_backend_owned<B>(&self, backend: B) -> std::result::Result<(), (FrankenError, B)>
where
B: WalBackend + 'static,
{
let inner = match self.inner.lock() {
Ok(inner) => inner,
Err(_) => {
return Err((FrankenError::internal("SimplePager lock poisoned"), backend));
}
};
if inner.checkpoint_active {
return Err((FrankenError::Busy, backend));
}
drop(inner);
let mut wal_guard = match self.wal_backend.write() {
Ok(guard) => guard,
Err(_) => {
return Err((
FrankenError::internal("SharedWalBackend lock poisoned"),
backend,
));
}
};
*wal_guard = Some(Box::new(backend));
drop(wal_guard);
Ok(())
}
#[must_use]
pub fn wal_commit_sync_policy(&self) -> WalCommitSyncPolicy {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.wal_commit_sync_policy
}
pub fn set_wal_commit_sync_policy(&self, policy: WalCommitSyncPolicy) -> Result<()> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
inner.wal_commit_sync_policy = policy;
Ok(())
}
pub fn export_database_bytes(&self, cx: &Cx) -> Result<Vec<u8>> {
let source_full = self.vfs.full_pathname(cx, &self.db_path)?;
let journal_mode = {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.active_transactions > 0 || inner.checkpoint_active {
return Err(FrankenError::Busy);
}
inner.journal_mode
};
if journal_mode == JournalMode::Wal {
self.checkpoint(cx, traits::CheckpointMode::Truncate)?;
}
let source_flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let (mut source_file, _) = self.vfs.open(cx, Some(&source_full), source_flags)?;
let export_result = (|| -> Result<Vec<u8>> {
let file_size = source_file.file_size(cx)?;
let output_len = usize::try_from(file_size).map_err(|_| FrankenError::OutOfRange {
what: "database export size".to_owned(),
value: file_size.to_string(),
})?;
let mut bytes = vec![0_u8; output_len];
let mut copied = 0_usize;
while copied < output_len {
let chunk_len = (output_len - copied).min(Self::EXPORT_COPY_CHUNK_SIZE);
let bytes_read =
source_file.read(cx, &mut bytes[copied..copied + chunk_len], copied as u64)?;
if bytes_read == 0 {
return Err(FrankenError::internal(
"unexpected EOF while exporting database image",
));
}
copied = copied
.checked_add(bytes_read)
.ok_or_else(|| FrankenError::internal("export size overflow"))?;
}
Ok(bytes)
})();
let source_close = source_file.close(cx);
let bytes = export_result?;
source_close?;
Ok(bytes)
}
pub fn copy_database_to(&self, cx: &Cx, target_path: &Path) -> Result<()> {
let source_path = self.db_path.clone();
let source_full = self.vfs.full_pathname(cx, &source_path)?;
let target_full = self.vfs.full_pathname(cx, target_path)?;
if source_full == target_full {
return Err(FrankenError::CannotOpen { path: target_full });
}
if self.vfs.access(cx, &target_full, AccessFlags::EXISTS)? {
return Err(FrankenError::CannotOpen { path: target_full });
}
let journal_mode = {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.active_transactions > 0 || inner.checkpoint_active {
return Err(FrankenError::Busy);
}
inner.journal_mode
};
if journal_mode == JournalMode::Wal {
self.checkpoint(cx, traits::CheckpointMode::Truncate)?;
}
let source_flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let target_flags = VfsOpenFlags::MAIN_DB
| VfsOpenFlags::CREATE
| VfsOpenFlags::EXCLUSIVE
| VfsOpenFlags::READWRITE;
let (mut source_file, _) = self.vfs.open(cx, Some(&source_full), source_flags)?;
let (mut target_file, _) = self.vfs.open(cx, Some(&target_full), target_flags)?;
let copy_result = (|| -> Result<()> {
let file_size = source_file.file_size(cx)?;
let mut copied = 0_u64;
let mut buffer = vec![0_u8; Self::EXPORT_COPY_CHUNK_SIZE];
while copied < file_size {
let remaining = file_size - copied;
let chunk_len = usize::try_from(remaining.min(buffer.len() as u64))
.map_err(|_| FrankenError::internal("copy chunk length overflow"))?;
let bytes_read = source_file.read(cx, &mut buffer[..chunk_len], copied)?;
if bytes_read == 0 {
return Err(FrankenError::internal(
"unexpected EOF while copying database image",
));
}
target_file.write(cx, &buffer[..bytes_read], copied)?;
copied = copied
.checked_add(
u64::try_from(bytes_read)
.map_err(|_| FrankenError::internal("copy size overflow"))?,
)
.ok_or_else(|| FrankenError::internal("copy offset overflow"))?;
}
target_file.truncate(cx, file_size)?;
target_file.sync(cx, SyncFlags::FULL)?;
Ok(())
})();
let source_close = source_file.close(cx);
let target_close = target_file.close(cx);
copy_result?;
source_close?;
target_close?;
Ok(())
}
pub fn cache_metrics_snapshot(&self) -> Result<PageCacheMetricsSnapshot> {
Ok(self.cache.metrics_snapshot())
}
pub fn cache_metrics_lightweight_snapshot(
&self,
) -> Result<crate::page_cache::PageCacheLightweightSnapshot> {
Ok(self.cache.metrics_lightweight_snapshot())
}
pub fn cache_page_snapshots(&self) -> Result<Vec<PageCachePageSnapshot>> {
Ok(self.cache.page_snapshots())
}
pub fn cache_efficiency_snapshot(&self) -> Result<PageCacheEfficiencySnapshot> {
Ok(self.cache.metrics_snapshot().efficiency_snapshot())
}
pub fn reset_cache_metrics(&self) -> Result<()> {
self.cache.reset_metrics();
Ok(())
}
#[must_use]
pub fn published_snapshot(&self) -> PagerPublishedSnapshot {
self.published.snapshot()
}
pub fn refresh_published_snapshot(&self, cx: &Cx) -> Result<PagerPublishedSnapshot> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.active_transactions > 0 || inner.checkpoint_active {
return Ok(self.published.snapshot());
}
inner.db_file.lock(cx, LockLevel::Shared)?;
let had_recovery_pending = inner.rollback_journal_recovery_state.is_pending();
let commit_seq_before_refresh = inner.commit_seq;
let journal_path = Self::journal_path(&self.db_path);
let journal_exists = match self.vfs.access(cx, &journal_path, AccessFlags::EXISTS) {
Ok(exists) => exists,
Err(err) => {
let _ = inner.db_file.unlock(cx, LockLevel::None);
return Err(err);
}
};
let mut recovered_or_invalidated_journal = false;
if inner.rollback_journal_recovery_state.is_pending() || journal_exists {
let page_size = inner.page_size;
match Self::recover_rollback_journal_if_present_locked(
cx,
&*self.vfs,
&mut inner.db_file,
&journal_path,
page_size,
LockLevel::Shared,
) {
Ok(false) if inner.rollback_journal_recovery_state.is_pending() => {
inner.db_file.unlock(cx, LockLevel::None)?;
return Err(FrankenError::internal(
"rollback journal missing while local recovery was pending",
));
}
Ok(_) => {
recovered_or_invalidated_journal = true;
inner.rollback_journal_recovery_state = RollbackJournalRecoveryState::Clean;
}
Err(err) => {
let _ = inner.db_file.unlock(cx, LockLevel::None);
return Err(err);
}
}
}
if recovered_or_invalidated_journal {
self.cache.clear();
}
if let Err(err) = inner.refresh_committed_state(cx, &self.cache, &self.wal_backend) {
let _ = inner.db_file.unlock(cx, LockLevel::None);
return Err(err);
}
let clear_published_pages =
had_recovery_pending || journal_exists || inner.commit_seq != commit_seq_before_refresh;
self.published.publish_clear_if(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
clear_published_pages,
);
inner.db_file.unlock(cx, LockLevel::None)?;
Ok(self.published.snapshot())
}
pub fn refresh_published_snapshot_for_clean_wal_read(
&self,
cx: &Cx,
) -> Result<PagerPublishedSnapshot> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.active_transactions > 0 || inner.checkpoint_active {
return Ok(self.published.snapshot());
}
if inner.journal_mode != JournalMode::Wal
|| inner.rollback_journal_recovery_state.is_pending()
{
drop(inner);
return self.refresh_published_snapshot(cx);
}
let commit_seq_before_refresh = inner.commit_seq;
inner.refresh_committed_state(cx, &self.cache, &self.wal_backend)?;
self.published.publish_clear_if(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
inner.commit_seq != commit_seq_before_refresh,
);
Ok(self.published.snapshot())
}
#[must_use]
pub fn published_read_retry_count(&self) -> u64 {
self.published.read_retry_count()
}
#[must_use]
pub fn page_size(&self) -> PageSize {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.page_size
}
#[must_use]
pub fn committed_snapshot(&self) -> Arc<PagerCommittedSnapshot> {
if self
.shared_connection_count
.get()
.is_some_and(|counter| counter.load(AtomicOrdering::Acquire) == 1)
{
let published = self.published.snapshot();
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
return Arc::new(PagerCommittedSnapshot {
commit_seq: published.visible_commit_seq,
db_size: published.db_size,
journal_mode: published.journal_mode,
freelist_count: published.freelist_count,
checkpoint_active: published.checkpoint_active,
writer_active: inner.writer_active,
db_file_size_bytes: inner.committed_db_file_size_bytes,
});
}
Arc::clone(
&self
.committed_snapshot
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
}
#[cfg(test)]
fn publish_committed_snapshot_from_inner(&self, inner: &PagerInner<V::File>) {
let snapshot = Arc::new(PagerCommittedSnapshot::from_inner(inner));
let mut guard = self
.committed_snapshot
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = snapshot;
}
pub fn bind_shared_connection_count(&self, counter: Arc<AtomicUsize>) {
let _ = self.shared_connection_count.set(counter);
}
#[must_use]
pub fn published_page_hits(&self) -> u64 {
self.published.published_page_hits()
}
#[must_use]
pub fn publication_write_count(&self) -> u64 {
self.published.publication_write_count()
}
pub fn wal_frame_count(&self) -> usize {
with_wal_backend_read(&self.wal_backend, |wal| Ok(wal.frame_count())).unwrap_or(0)
}
fn journal_path(db_path: &Path) -> PathBuf {
let mut jp = db_path.as_os_str().to_owned();
jp.push("-journal");
PathBuf::from(jp)
}
fn recover_rollback_journal_if_present(
cx: &Cx,
vfs: &V,
db_file: &mut V::File,
journal_path: &Path,
page_size: PageSize,
) -> Result<bool> {
if !vfs.access(cx, journal_path, AccessFlags::EXISTS)? {
return Ok(false);
}
Self::replay_journal(cx, vfs, db_file, journal_path, page_size)?;
let _ = vfs.delete(cx, journal_path, true);
Ok(true)
}
fn recover_rollback_journal_if_present_locked(
cx: &Cx,
vfs: &V,
db_file: &mut V::File,
journal_path: &Path,
page_size: PageSize,
restore_lock_level: LockLevel,
) -> Result<bool> {
if !vfs.access(cx, journal_path, AccessFlags::EXISTS)? {
return Ok(false);
}
db_file.lock(cx, LockLevel::Exclusive)?;
let recovery_result =
Self::recover_rollback_journal_if_present(cx, vfs, db_file, journal_path, page_size);
let restore_result = db_file.unlock(cx, restore_lock_level);
match (recovery_result, restore_result) {
(Ok(recovered), Ok(())) => Ok(recovered),
(Err(recovery_err), Ok(())) => Err(recovery_err),
(Ok(_), Err(restore_err)) => Err(restore_err),
(Err(recovery_err), Err(restore_err)) => Err(FrankenError::internal(format!(
"hot journal recovery failed and could not restore lock level {restore_lock_level:?}: recovery={recovery_err}; restore={restore_err}"
))),
}
}
#[allow(clippy::too_many_lines)]
pub fn open_with_cx(
cx: &Cx,
vfs: V,
path: &Path,
requested_page_size: PageSize,
) -> Result<Self> {
Self::open_with_cx_and_page_buffer_max(cx, vfs, path, requested_page_size, None)
}
#[allow(clippy::too_many_lines)]
pub fn open_with_cx_and_page_buffer_max(
cx: &Cx,
vfs: V,
path: &Path,
requested_page_size: PageSize,
page_buffer_max: Option<usize>,
) -> Result<Self> {
let vfs = Arc::new(vfs);
let flags = VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _actual_flags) = vfs.open(cx, Some(path), flags)?;
let mut file_size = db_file.file_size(cx)?;
let page_size = if file_size >= DATABASE_HEADER_SIZE as u64 {
let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
let header_read = db_file.read(cx, &mut header_bytes, 0)?;
if header_read >= DATABASE_HEADER_SIZE {
match DatabaseHeader::from_bytes(&header_bytes) {
Ok(header) => header.page_size,
Err(error)
if stale_main_header_can_be_recovered_from_live_wal(
cx,
&*vfs,
path,
&header_bytes,
&error,
false,
)? =>
{
page_size_from_header_bytes(&header_bytes).unwrap_or(requested_page_size)
}
Err(_) => requested_page_size,
}
} else {
requested_page_size
}
} else {
requested_page_size
};
let journal_path = Self::journal_path(path);
let rollback_journal_exists = vfs.access(cx, &journal_path, AccessFlags::EXISTS)?;
if rollback_journal_exists {
let recovery_fence = recovery_fence_for_backend(&*vfs, path);
let _recovery_guard = recovery_fence.acquire_for_recovery()?;
let _ = Self::recover_rollback_journal_if_present_locked(
cx,
&*vfs,
&mut db_file,
&journal_path,
page_size,
LockLevel::None,
)?;
}
file_size = db_file.file_size(cx)?;
let (header, bootstrapped_from_live_wal_stub) = if file_size == 0 {
let page_len = page_size.as_usize();
let mut page1 = vec![0u8; page_len];
let header = DatabaseHeader {
page_size,
page_count: 1,
sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
..DatabaseHeader::default()
};
let hdr_bytes = header.to_bytes().map_err(|err| {
FrankenError::internal(format!("failed to encode new database header: {err}"))
})?;
page1[..DATABASE_HEADER_SIZE].copy_from_slice(&hdr_bytes);
let usable = page_size.usable(header.reserved_per_page);
BTreePageHeader::write_empty_leaf_table(&mut page1, DATABASE_HEADER_SIZE, usable);
db_file.write(cx, &page1, 0)?;
db_file.sync(cx, SyncFlags::NORMAL)?;
file_size = db_file.file_size(cx)?;
(header, false)
} else {
if file_size < DATABASE_HEADER_SIZE as u64 {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"database file too small for header: {file_size} bytes (< {DATABASE_HEADER_SIZE})"
),
});
}
let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
let header_read = db_file.read(cx, &mut header_bytes, 0)?;
if header_read < DATABASE_HEADER_SIZE {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read fetching database header: got {header_read} of {DATABASE_HEADER_SIZE}"
),
});
}
let (header, bootstrapped_from_live_wal_stub) =
match DatabaseHeader::from_bytes(&header_bytes) {
Ok(header) => (header, false),
Err(error)
if stale_main_header_can_be_recovered_from_live_wal(
cx,
&*vfs,
path,
&header_bytes,
&error,
false,
)? =>
{
(
bootstrap_header_from_stale_main_file(&header_bytes, page_size),
true,
)
}
Err(error) => {
return Err(FrankenError::DatabaseCorrupt {
detail: format!("invalid database header: {error}"),
});
}
};
if header.page_size != page_size {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"database page size mismatch: header={} requested={}",
header.page_size.get(),
page_size.get()
),
});
}
(header, bootstrapped_from_live_wal_stub)
};
let page_size_u64 = page_size.as_usize() as u64;
if file_size % page_size_u64 != 0 {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"database file size {file_size} is not aligned to page size {}",
page_size.get()
),
});
}
let db_pages = file_size
.checked_div(page_size_u64)
.ok_or_else(|| FrankenError::internal("page size must be non-zero"))?;
let db_size = u32::try_from(db_pages).map_err(|_| FrankenError::OutOfRange {
what: "database page count".to_owned(),
value: db_pages.to_string(),
})?;
let next_page = if db_size >= 2 {
db_size.saturating_add(1)
} else {
2
};
let freelist = if bootstrapped_from_live_wal_stub {
Vec::new()
} else {
load_freelist_from_disk(
cx,
&db_file,
page_size,
db_size,
header.freelist_trunk,
header.freelist_count,
)?
};
let initial_commit_seq = CommitSeq::new(u64::from(header.change_counter));
let freelist_count = freelist.len();
let resolved_max = crate::page_cache::resolve_page_buffer_max(page_buffer_max);
let cache =
ShardedPageCache::with_max_buffers_for_initial_pages(page_size, resolved_max, db_size);
cache.set_eviction_policy(PageCacheEvictionPolicy::S3Fifo(S3FifoConfig::new(
resolved_max,
)));
let pool = cache.pool().clone();
Ok(Self {
vfs,
db_path: path.to_owned(),
inner: Arc::new(Mutex::new(PagerInner {
db_file,
page_size,
db_size,
next_page,
writer_active: false,
active_transactions: 0,
checkpoint_active: false,
access_mode: PagerAccessMode::ReadWrite,
freelist,
journal_mode: JournalMode::Delete,
wal_commit_sync_policy: WalCommitSyncPolicy::PerCommit,
rollback_journal_recovery_state: RollbackJournalRecoveryState::Clean,
commit_seq: initial_commit_seq,
committed_db_file_size_bytes: file_size,
committed_db_change_counter: u64::from(header.change_counter),
committed_wal_generation: None,
})),
writer_idle: Arc::new(Condvar::new()),
cache: Arc::new(cache),
pool,
published: Arc::new(PublishedPagerState::new(
db_size,
initial_commit_seq,
JournalMode::Delete,
freelist_count,
)),
wal_backend: new_shared_wal_backend(),
committed_snapshot: Arc::new(RwLock::new(Arc::new(PagerCommittedSnapshot {
commit_seq: initial_commit_seq,
db_size,
journal_mode: JournalMode::Delete,
freelist_count,
checkpoint_active: false,
writer_active: false,
db_file_size_bytes: file_size,
}))),
shared_connection_count: OnceLock::new(),
})
}
pub fn enable_single_connection_cache_fast_path(&mut self) -> bool {
let Some(cache) = Arc::get_mut(&mut self.cache) else {
return false;
};
cache.enable_fast_path();
true
}
#[must_use]
pub fn is_single_connection_cache_fast_path_enabled(&self) -> bool {
self.cache.is_fast_path_enabled()
}
#[allow(clippy::too_many_lines)]
pub fn open_readonly_with_cx(
cx: &Cx,
vfs: V,
path: &Path,
_requested_page_size: PageSize,
) -> Result<Self> {
Self::open_readonly_with_cx_and_page_buffer_max(cx, vfs, path, _requested_page_size, None)
}
#[allow(clippy::too_many_lines)]
pub fn open_readonly_with_cx_and_page_buffer_max(
cx: &Cx,
vfs: V,
path: &Path,
_requested_page_size: PageSize,
page_buffer_max: Option<usize>,
) -> Result<Self> {
let vfs = Arc::new(vfs);
let flags = VfsOpenFlags::READONLY | VfsOpenFlags::MAIN_DB;
let (db_file, _actual_flags) = vfs.open(cx, Some(path), flags)?;
let file_size = db_file.file_size(cx)?;
if file_size == 0 {
return Err(FrankenError::CannotOpen {
path: path.to_owned(),
});
}
if file_size < DATABASE_HEADER_SIZE as u64 {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"database file too small for header: {file_size} bytes (< {DATABASE_HEADER_SIZE})"
),
});
}
let mut header_bytes = [0u8; DATABASE_HEADER_SIZE];
let header_read = db_file.read(cx, &mut header_bytes, 0)?;
if header_read < DATABASE_HEADER_SIZE {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read fetching database header: got {header_read} of {DATABASE_HEADER_SIZE}"
),
});
}
let (header, page_size) = match DatabaseHeader::from_bytes(&header_bytes) {
Ok(header) => {
let page_size = header.page_size;
(Some(header), page_size)
}
Err(error)
if stale_main_header_can_be_recovered_from_live_wal(
cx,
&*vfs,
path,
&header_bytes,
&error,
true,
)? =>
{
let page_size = page_size_from_header_bytes(&header_bytes).ok_or_else(|| {
FrankenError::DatabaseCorrupt {
detail: "live WAL bootstrap could not recover database page size"
.to_owned(),
}
})?;
(None, page_size)
}
Err(error) => {
return Err(FrankenError::DatabaseCorrupt {
detail: format!("invalid database header: {error}"),
});
}
};
let page_size_u64 = page_size.as_usize() as u64;
if file_size % page_size_u64 != 0 {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"database file size {file_size} is not aligned to page size {}",
page_size.get()
),
});
}
let db_pages = file_size
.checked_div(page_size_u64)
.ok_or_else(|| FrankenError::internal("page size must be non-zero"))?;
let db_size = u32::try_from(db_pages).map_err(|_| FrankenError::OutOfRange {
what: "database page count".to_owned(),
value: db_pages.to_string(),
})?;
let next_page = if db_size >= 2 {
db_size.saturating_add(1)
} else {
2
};
let freelist = Vec::new();
let initial_commit_seq = CommitSeq::new(u64::from(
header.as_ref().map_or(0, |header| header.change_counter),
));
let resolved_max = crate::page_cache::resolve_page_buffer_max(page_buffer_max);
let cache =
ShardedPageCache::with_max_buffers_for_initial_pages(page_size, resolved_max, db_size);
cache.set_eviction_policy(PageCacheEvictionPolicy::S3Fifo(S3FifoConfig::new(
resolved_max,
)));
let pool = cache.pool().clone();
Ok(Self {
vfs,
db_path: path.to_owned(),
inner: Arc::new(Mutex::new(PagerInner {
db_file,
page_size,
db_size,
next_page,
writer_active: false,
active_transactions: 0,
checkpoint_active: false,
freelist,
journal_mode: JournalMode::Delete,
wal_commit_sync_policy: WalCommitSyncPolicy::PerCommit,
access_mode: PagerAccessMode::ReadOnly,
rollback_journal_recovery_state: RollbackJournalRecoveryState::Clean,
commit_seq: initial_commit_seq,
committed_db_file_size_bytes: file_size,
committed_db_change_counter: header
.as_ref()
.map_or(0, |header| u64::from(header.change_counter)),
committed_wal_generation: None,
})),
writer_idle: Arc::new(Condvar::new()),
cache: Arc::new(cache),
pool,
published: Arc::new(PublishedPagerState::new(
db_size,
initial_commit_seq,
JournalMode::Delete,
0, )),
wal_backend: new_shared_wal_backend(),
committed_snapshot: Arc::new(RwLock::new(Arc::new(PagerCommittedSnapshot {
commit_seq: initial_commit_seq,
db_size,
journal_mode: JournalMode::Delete,
freelist_count: 0,
checkpoint_active: false,
writer_active: false,
db_file_size_bytes: file_size,
}))),
shared_connection_count: OnceLock::new(),
})
}
#[cfg(test)]
#[allow(clippy::too_many_lines)]
pub fn open(vfs: V, path: &Path, page_size: PageSize) -> Result<Self> {
let cx = Cx::new();
Self::open_with_cx(&cx, vfs, path, page_size)
}
fn replay_journal(
cx: &Cx,
vfs: &V,
db_file: &mut V::File,
journal_path: &Path,
page_size: PageSize,
) -> Result<()> {
let jrnl_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let Ok((mut jrnl_file, _)) = vfs.open(cx, Some(journal_path), jrnl_flags) else {
return Ok(()); };
let jrnl_size = jrnl_file.file_size(cx)?;
if jrnl_size < crate::journal::JOURNAL_HEADER_SIZE as u64 {
return Ok(()); }
let mut hdr_buf = vec![0u8; crate::journal::JOURNAL_HEADER_SIZE];
let _ = jrnl_file.read(cx, &mut hdr_buf, 0)?;
let Ok(header) = JournalHeader::decode(&hdr_buf) else {
return Ok(()); };
if header.page_size != page_size.get() {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"hot journal page size mismatch: header={} expected={}",
header.page_size,
page_size.get()
),
});
}
let page_count = if header.page_count < 0 {
header.compute_page_count_from_file_size(jrnl_size)
} else {
#[allow(clippy::cast_sign_loss)]
let c = header.page_count as u32;
c
};
let header_size = u64::try_from(crate::journal::JOURNAL_HEADER_SIZE)
.expect("journal header size should fit in u64");
let hdr_padded = u64::from(header.sector_size).max(header_size);
let ps = page_size.as_usize();
let record_size = 4 + ps + 4;
let mut offset = hdr_padded;
for _ in 0..page_count {
let mut rec_buf = vec![0u8; record_size];
let bytes_read = jrnl_file.read(cx, &mut rec_buf, offset)?;
if bytes_read < record_size {
break; }
#[allow(clippy::cast_possible_truncation)]
let Ok(record) = JournalPageRecord::decode(&rec_buf, ps as u32) else {
break; };
if record.verify_checksum(header.nonce).is_err() {
break; }
let Some(page_no) = PageNumber::new(record.page_number) else {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"hot journal contains invalid page number {}",
record.page_number
),
});
};
let page_offset = u64::from(page_no.get() - 1) * ps as u64;
db_file.write(cx, &record.content, page_offset)?;
offset += record_size as u64;
}
db_file.sync(cx, SyncFlags::NORMAL)?;
if header.initial_db_size > 0 {
let target_size = u64::from(header.initial_db_size) * ps as u64;
let current_size = db_file.file_size(cx)?;
if current_size > target_size {
db_file.truncate(cx, target_size)?;
db_file.sync(cx, SyncFlags::NORMAL)?;
}
}
jrnl_file.truncate(cx, 0)?;
jrnl_file.sync(cx, SyncFlags::NORMAL)?;
Ok(())
}
}
struct SavepointEntry {
name: String,
write_set_snapshot: HashMap<PageNumber, PageData>,
write_pages_sorted_snapshot: Vec<PageNumber>,
freed_pages_snapshot: Vec<PageNumber>,
next_page_snapshot: u32,
freelist_snapshot: Vec<PageNumber>,
allocated_from_freelist_snapshot: Vec<PageNumber>,
allocated_from_eof_snapshot: Vec<PageNumber>,
}
#[derive(Debug)]
enum StagedPageBacking {
Buffered(PageBuf),
Owned(PageData),
}
#[derive(Debug)]
struct StagedPage {
backing: StagedPageBacking,
published: OnceLock<PageData>,
}
impl StagedPage {
fn from_buf(buf: PageBuf) -> Self {
Self {
backing: StagedPageBacking::Buffered(buf),
published: OnceLock::new(),
}
}
fn from_bytes(pool: &PageBufPool, data: &[u8]) -> Result<Self> {
let mut buf = pool.acquire()?;
let len = buf.len().min(data.len());
buf[..len].copy_from_slice(&data[..len]);
if len < buf.len() {
buf[len..].fill(0);
}
Ok(Self::from_buf(buf))
}
fn from_page_data(data: PageData) -> Self {
Self {
backing: StagedPageBacking::Owned(data),
published: OnceLock::new(),
}
}
fn from_page_data_for_pool(pool: &PageBufPool, data: PageData) -> Result<Self> {
if data.len() == pool.page_size() {
Ok(Self::from_page_data(data))
} else {
Self::from_bytes(pool, data.as_bytes())
}
}
fn as_page_bytes(&self) -> &[u8] {
match &self.backing {
StagedPageBacking::Buffered(buf) => buf.as_slice(),
StagedPageBacking::Owned(data) => data.as_bytes(),
}
}
fn as_page_bytes_mut(&mut self) -> &mut [u8] {
debug_assert!(
self.published.get().is_none(),
"staged pages must be unpublished before in-place mutation"
);
match &mut self.backing {
StagedPageBacking::Buffered(buf) => buf.as_mut_slice(),
StagedPageBacking::Owned(data) => data.as_bytes_mut(),
}
}
fn into_unpublished_for_mutation(self, pool: &PageBufPool) -> Self {
let has_published_snapshot = self.published.get().is_some();
if has_published_snapshot {
Self::from_buf(self.into_buf(pool))
} else {
self
}
}
fn published_page(&self) -> PageData {
self.published
.get_or_init(|| match &self.backing {
StagedPageBacking::Buffered(buf) => {
PageData::from_shared(Arc::<[u8]>::from(buf.as_slice()))
}
StagedPageBacking::Owned(data) => data.clone(),
})
.clone()
}
fn into_published_page(self) -> PageData {
let Self { backing, published } = self;
if let Some(page) = published.into_inner() {
return page;
}
match backing {
StagedPageBacking::Buffered(buf) => {
PageData::from_shared(Arc::<[u8]>::from(buf.as_slice()))
}
StagedPageBacking::Owned(data) => data,
}
}
fn try_into_unpublished_owned_page_data(self) -> std::result::Result<PageData, Self> {
let Self { backing, published } = self;
if let Some(page) = published.into_inner() {
let published = OnceLock::new();
let _ = published.set(page);
return Err(Self { backing, published });
}
match backing {
StagedPageBacking::Owned(data) => Ok(data),
other => Err(Self {
backing: other,
published: OnceLock::new(),
}),
}
}
fn into_buf(self, pool: &PageBufPool) -> PageBuf {
match self.backing {
StagedPageBacking::Buffered(buf) => buf,
StagedPageBacking::Owned(data) => {
let page_size = PageSize::new(
u32::try_from(pool.page_size()).expect("pool page size fits u32"),
)
.expect("pool page size invariant");
let mut buf = pool.acquire().unwrap_or_else(|_| PageBuf::new(page_size));
let len = buf.len().min(data.len());
buf[..len].copy_from_slice(&data.as_bytes()[..len]);
if len < buf.len() {
buf[len..].fill(0);
}
buf
}
}
}
fn try_overwrite_bytes_in_place(&mut self, data: &[u8]) -> bool {
if self.published.get().is_some() {
return false;
}
match &mut self.backing {
StagedPageBacking::Buffered(buf) => {
if buf.len() != data.len() {
return false;
}
buf.as_mut_slice().copy_from_slice(data);
true
}
StagedPageBacking::Owned(page_data) => {
if page_data.len() != data.len() {
return false;
}
if !page_data.is_single_owner_owned() {
return false;
}
page_data.as_bytes_mut().copy_from_slice(data);
true
}
}
}
fn try_overwrite_page_data_in_place(&mut self, data: &PageData) -> bool {
self.try_overwrite_bytes_in_place(data.as_bytes())
}
}
const PAGE_LEASE_BATCH_SIZE: u32 = 8;
#[allow(clippy::struct_excessive_bools)]
pub struct SimpleTransaction<V: Vfs> {
vfs: Arc<V>,
journal_path: PathBuf,
group_commit_queue: GroupCommitQueueRef,
inner: Arc<Mutex<PagerInner<V::File>>>,
writer_idle: Arc<Condvar>,
cache: Arc<ShardedPageCache>,
published: Arc<PublishedPagerState>,
wal_backend: SharedWalBackend,
committed_snapshot: Arc<RwLock<Arc<PagerCommittedSnapshot>>>,
shared_connection_count: Option<Arc<AtomicUsize>>,
published_visible_commit_seq: Cell<CommitSeq>,
published_db_size: Cell<u32>,
write_set: PagePageMap<StagedPage>,
write_pages_sorted: Vec<PageNumber>,
freed_pages: Vec<PageNumber>,
freed_page_bounds: Option<(PageNumber, PageNumber)>,
allocated_from_freelist: Vec<PageNumber>,
allocated_from_eof: Vec<PageNumber>,
writes_observed: bool,
mode: TransactionMode,
is_writer: bool,
committed: bool,
finished: bool,
original_db_size: u32,
savepoint_stack: Vec<SavepointEntry>,
journal_mode: JournalMode,
pool: PageBufPool,
cleanup_cx: Cx,
page_lease: Vec<PageNumber>,
memory_db_bump_alloc: bool,
rolled_back_pages: HashSet<PageNumber>,
txn_read_cache: RefCell<PagePageMap<PageData>>,
retained_memory_overlay_dirty_pages: BTreeSet<PageNumber>,
scratch_arena: bumpalo::Bump,
}
impl<V: Vfs> traits::sealed::Sealed for SimpleTransaction<V> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct WalPageOneWritePlan {
max_written: u32,
page_one_dirty: bool,
freelist_metadata_dirty: bool,
db_growth: bool,
}
impl WalPageOneWritePlan {
#[must_use]
fn requires_page_one_rewrite(self) -> bool {
self.page_one_dirty
}
#[must_use]
fn requires_page_count_advance(self) -> bool {
self.db_growth
}
}
impl<V: Vfs> SimpleTransaction<V> {
#[must_use]
pub fn is_writer(&self) -> bool {
self.is_writer
}
#[must_use]
pub(crate) fn scratch_arena(&self) -> &bumpalo::Bump {
&self.scratch_arena
}
pub fn prefetch_page_hints_greedy(
&self,
candidates: &[crate::submodular_prefetch::Candidate],
budget: usize,
penalty: f64,
) {
let selected = crate::submodular_prefetch::greedy_select(candidates, budget, penalty);
if selected.is_empty() {
return;
}
for page_no in selected {
if let Some(staged) = self.write_set.get(&page_no) {
prefetch_l1_read(staged.as_page_bytes().as_ptr());
continue;
}
if let Ok(txn_read_cache) = self.txn_read_cache.try_borrow()
&& let Some(page) = txn_read_cache.get(&page_no)
{
prefetch_l1_read(page.as_bytes().as_ptr());
continue;
}
if self.published.page_plane_visible_commit_seq()
== self.published_visible_commit_seq.get()
{
self.published.prefetch_page_hint(page_no);
}
self.cache.prefetch_page_hint(page_no);
}
}
fn single_connection_fast_path_enabled(&self) -> bool {
self.shared_connection_count
.as_ref()
.is_some_and(|counter| counter.load(AtomicOrdering::Acquire) == 1)
}
#[must_use]
fn durable_freelist_pages_with_inner(
inner: &PagerInner<V::File>,
db_size: u32,
restored_pages: &[PageNumber],
) -> Vec<PageNumber> {
if db_size == 0 {
return Vec::new();
}
let upper_bound = inner.next_page.saturating_sub(1).max(db_size);
let mut freelist = inner.freelist.clone();
return_pages_to_freelist(&mut freelist, restored_pages.iter().copied());
normalize_freelist(&freelist, upper_bound)
.into_iter()
.filter(|page| page.get() <= db_size)
.collect()
}
#[must_use]
fn committed_durable_freelist_pages_with_inner(
&self,
inner: &PagerInner<V::File>,
) -> Vec<PageNumber> {
Self::durable_freelist_pages_with_inner(inner, inner.db_size, &self.allocated_from_freelist)
}
#[must_use]
fn predicted_durable_freelist_pages_with_inner(
&self,
inner: &PagerInner<V::File>,
committed_db_size: u32,
) -> Vec<PageNumber> {
Self::durable_freelist_pages_with_inner(inner, committed_db_size, &self.freed_pages)
}
#[must_use]
fn freelist_metadata_dirty_with_inner(
&self,
inner: &PagerInner<V::File>,
committed_db_size: u32,
) -> bool {
self.committed_durable_freelist_pages_with_inner(inner)
!= self.predicted_durable_freelist_pages_with_inner(inner, committed_db_size)
}
#[must_use]
fn freelist_metadata_dirty_with_pending_free_pages(
&self,
inner: &PagerInner<V::File>,
committed_db_size: u32,
pending_free_pages: &[PageNumber],
) -> bool {
if !pending_free_pages
.iter()
.any(|page| page.get() <= committed_db_size)
&& self.allocated_from_freelist.is_empty()
{
return false;
}
self.committed_durable_freelist_pages_with_inner(inner)
!= Self::durable_freelist_pages_with_inner(inner, committed_db_size, pending_free_pages)
}
#[must_use]
fn freelist_metadata_dirty(&self) -> bool {
self.inner.lock().map_or(true, |inner| {
let committed_db_size = self.committed_db_size_with_inner(&inner);
self.freelist_metadata_dirty_with_inner(&inner, committed_db_size)
})
}
#[must_use]
fn max_live_written_page(&self) -> Option<PageNumber> {
self.write_pages_sorted
.iter()
.rev()
.copied()
.find(|page| self.write_set.contains_key(page))
}
fn note_freed_page_bound(&mut self, page_no: PageNumber) {
match self.freed_page_bounds {
Some((low, high)) => {
self.freed_page_bounds = Some((low.min(page_no), high.max(page_no)));
}
None => self.freed_page_bounds = Some((page_no, page_no)),
}
}
fn refresh_freed_page_bounds(&mut self) {
let mut pages = self.freed_pages.iter().copied();
let Some(first) = pages.next() else {
self.freed_page_bounds = None;
return;
};
let (low, high) = pages.fold((first, first), |(low, high), page| {
(low.min(page), high.max(page))
});
self.freed_page_bounds = Some((low, high));
}
fn clear_freed_pages(&mut self) {
self.freed_pages.clear();
self.freed_page_bounds = None;
}
fn restore_pending_freed_pages(&mut self, pending_freed: Vec<PageNumber>) {
self.freed_pages.extend(pending_freed);
self.refresh_freed_page_bounds();
}
fn might_have_freed_page(&self, page_no: PageNumber) -> bool {
self.freed_page_bounds
.is_some_and(|(low, high)| low <= page_no && page_no <= high)
}
fn contains_freed_page(&self, page_no: PageNumber) -> bool {
self.might_have_freed_page(page_no) && self.freed_pages.contains(&page_no)
}
fn remove_freed_page_if_present(&mut self, page_no: PageNumber) {
if !self.might_have_freed_page(page_no) {
return;
}
if let Some(pos) = self.freed_pages.iter().position(|&p| p == page_no) {
self.freed_pages.swap_remove(pos);
if self.freed_pages.is_empty() {
self.freed_page_bounds = None;
}
}
}
#[must_use]
fn committed_db_size_with_inner(&self, inner: &PagerInner<V::File>) -> u32 {
self.max_live_written_page()
.map_or(inner.db_size, |page| inner.db_size.max(page.get()))
}
#[must_use]
fn classify_wal_page_one_write(
&self,
current_db_size: u32,
freelist_dirty: bool,
) -> WalPageOneWritePlan {
let max_written = self.max_live_written_page().map_or(0, |page| page.get());
WalPageOneWritePlan {
max_written,
page_one_dirty: self.write_set.contains_key(&PageNumber::ONE),
freelist_metadata_dirty: freelist_dirty,
db_growth: max_written > current_db_size,
}
}
#[must_use]
fn current_page_one_conflict_tracking_required_with_inner(
&self,
inner: &PagerInner<V::File>,
) -> bool {
let committed_db_size = self.committed_db_size_with_inner(inner);
let freelist_dirty = self.freelist_metadata_dirty_with_inner(inner, committed_db_size);
let wal_page1_plan = self.classify_wal_page_one_write(inner.db_size, freelist_dirty);
if self.journal_mode == JournalMode::Wal {
wal_page1_plan.requires_page_one_rewrite()
} else {
!self.write_set.is_empty() || freelist_dirty
}
}
#[must_use]
fn allocate_page_requires_page_one_conflict_tracking_with_inner(
&self,
inner: &PagerInner<V::File>,
) -> bool {
if self.memory_db_bump_alloc {
return false;
}
if self.mode == TransactionMode::Concurrent {
return false;
}
if self.current_page_one_conflict_tracking_required_with_inner(inner) {
return true;
}
let committed_db_size = self.committed_db_size_with_inner(inner);
let committed_freelist_is_snapshot_pinned = inner.active_transactions > 1;
if committed_freelist_is_snapshot_pinned {
return false;
}
match inner.freelist.last().copied() {
Some(page) => page.get() <= committed_db_size,
None => false,
}
}
#[must_use]
fn free_page_requires_page_one_conflict_tracking_with_inner(
&self,
inner: &PagerInner<V::File>,
page_no: PageNumber,
) -> bool {
if self.mode == TransactionMode::Concurrent {
return false;
}
if self.current_page_one_conflict_tracking_required_with_inner(inner) {
return true;
}
page_no.get() <= self.committed_db_size_with_inner(inner)
}
#[must_use]
fn write_page_requires_page_one_conflict_tracking_with_inner(
&self,
inner: &PagerInner<V::File>,
page_no: PageNumber,
) -> bool {
if page_no == PageNumber::ONE {
return true;
}
if self.mode == TransactionMode::Concurrent {
return false;
}
if self.current_page_one_conflict_tracking_required_with_inner(inner) {
return true;
}
page_no.get() > self.committed_db_size_with_inner(inner)
}
#[must_use]
fn page_one_in_pending_commit_surface_with_inner(&self, inner: &PagerInner<V::File>) -> bool {
let committed_db_size = self.committed_db_size_with_inner(inner);
let durable_freelist =
self.predicted_durable_freelist_pages_with_inner(inner, committed_db_size);
let freelist_dirty =
self.committed_durable_freelist_pages_with_inner(inner) != durable_freelist;
let wal_page1_plan = self.classify_wal_page_one_write(inner.db_size, freelist_dirty);
if self.journal_mode == JournalMode::Wal {
wal_page1_plan.requires_page_one_rewrite()
} else {
!self.write_set.is_empty() || freelist_dirty
}
}
fn predicted_commit_pages_with_inner(&self, inner: &PagerInner<V::File>) -> Vec<PageNumber> {
let mut pages = self.write_pages_sorted.clone();
let committed_db_size = self.committed_db_size_with_inner(inner);
let durable_freelist =
self.predicted_durable_freelist_pages_with_inner(inner, committed_db_size);
let freelist_dirty =
self.committed_durable_freelist_pages_with_inner(inner) != durable_freelist;
if freelist_dirty && !durable_freelist.is_empty() {
let max_leaf_entries = (inner.page_size.as_usize() / 4).saturating_sub(2).max(1);
let trunk_count = durable_freelist.len().div_ceil(max_leaf_entries + 1);
pages.extend(durable_freelist.into_iter().take(trunk_count));
}
if self.page_one_in_pending_commit_surface_with_inner(inner) {
pages.push(PageNumber::ONE);
}
pages.sort_unstable();
pages.dedup();
pages
}
fn predicted_conflict_pages_with_inner(&self, inner: &PagerInner<V::File>) -> Vec<PageNumber> {
let committed_db_size = self.committed_db_size_with_inner(inner);
let freelist_dirty = self.freelist_metadata_dirty_with_inner(inner, committed_db_size);
let wal_page1_plan = self.classify_wal_page_one_write(inner.db_size, freelist_dirty);
self.predicted_conflict_pages_for_wal_commit_with_inner(
inner,
wal_page1_plan,
&self.freed_pages,
)
}
fn predicted_conflict_pages_for_wal_commit_with_inner(
&self,
inner: &PagerInner<V::File>,
wal_page1_plan: WalPageOneWritePlan,
freed_conflict_pages: &[PageNumber],
) -> Vec<PageNumber> {
let mut pages = self.predicted_commit_pages_with_inner(inner);
pages.extend(freed_conflict_pages.iter().copied());
if self.mode == TransactionMode::Concurrent && self.journal_mode == JournalMode::Wal {
if !wal_page1_plan.requires_page_one_rewrite() {
pages.retain(|page| *page != PageNumber::ONE);
}
}
pages.sort_unstable();
pages.dedup();
pages
}
fn publish_committed_state(&self, cx: &Cx, update: PublishedPagerUpdate) {
self.published.publish_commit(cx, update, &self.write_set);
}
fn publish_committed_state_draining_write_set(
&mut self,
cx: &Cx,
update: PublishedPagerUpdate,
) {
self.published
.publish_commit_draining_write_set(cx, update, &mut self.write_set);
}
fn publish_single_connection_metadata_only(&self, cx: &Cx, update: PublishedPagerUpdate) {
let clear_pages = self.published.page_set_size.load(AtomicOrdering::Acquire) != 0;
self.published
.publish_single_connection_metadata_update(cx, update, clear_pages);
}
fn note_retained_memory_overlay_from_write_set(&mut self) {
self.retained_memory_overlay_dirty_pages
.extend(self.write_pages_sorted.iter().copied());
}
fn materialize_retained_memory_overlay_into_write_set(&mut self) -> Result<()> {
if self.retained_memory_overlay_dirty_pages.is_empty() {
return Ok(());
}
let mut overlay_page_nos: bumpalo::collections::Vec<'_, PageNumber> =
bumpalo::collections::Vec::new_in(self.scratch_arena());
overlay_page_nos.extend(
self.retained_memory_overlay_dirty_pages
.iter()
.copied()
.filter(|page_no| !self.write_set.contains_key(page_no)),
);
if overlay_page_nos.is_empty() {
return Ok(());
}
let overlay_pages = {
let txn_read_cache = self.txn_read_cache.borrow();
overlay_page_nos
.iter()
.copied()
.map(|page_no| {
let page = txn_read_cache.get(&page_no).cloned().ok_or_else(|| {
FrankenError::internal(format!(
"retained memory overlay missing authoritative page {}",
page_no.get()
))
})?;
Ok((page_no, page))
})
.collect::<Result<Vec<_>>>()?
};
drop(overlay_page_nos);
for (page_no, page) in overlay_pages {
insert_staged_page(
&mut self.write_set,
&mut self.write_pages_sorted,
page_no,
StagedPage::from_page_data_for_pool(&self.pool, page)?,
);
}
Ok(())
}
fn collect_retained_memory_overlay_pages(&self) -> Result<Vec<(PageNumber, PageData)>> {
if self.retained_memory_overlay_dirty_pages.is_empty() {
return Ok(Vec::new());
}
let txn_read_cache = self.txn_read_cache.borrow();
self.retained_memory_overlay_dirty_pages
.iter()
.copied()
.map(|page_no| {
let page = txn_read_cache.get(&page_no).cloned().ok_or_else(|| {
FrankenError::internal(format!(
"retained memory overlay missing authoritative page {}",
page_no.get()
))
})?;
Ok((page_no, page))
})
.collect::<Result<Vec<_>>>()
}
fn flush_retained_memory_overlay_pages_to_db_file(
cx: &Cx,
inner: &mut PagerInner<V::File>,
original_db_size: u32,
overlay_pages: &[(PageNumber, PageData)],
) -> Result<()> {
if overlay_pages.is_empty() {
return Ok(());
}
let page_size_bytes = u64::from(inner.page_size.get());
let mut batched_writes: SmallVec<[(u64, &[u8]); 8]> =
SmallVec::with_capacity(overlay_pages.len());
for (page_no, page) in overlay_pages {
let offset = u64::from(page_no.get() - 1) * page_size_bytes;
batched_writes.push((offset, page.as_bytes()));
}
inner
.db_file
.write_page_batch(cx, batched_writes.as_slice())?;
inner.committed_db_file_size_bytes =
u64::from(original_db_size) * u64::from(inner.page_size.get());
Ok(())
}
fn retain_committed_pages_in_txn_read_cache(&mut self) {
let mut txn_read_cache = self.txn_read_cache.borrow_mut();
for (page_no, staged) in self.write_set.drain() {
txn_read_cache.insert(page_no, staged.into_published_page());
}
self.write_pages_sorted.clear();
}
fn publish_committed_snapshot_from_inner(&self, inner: &PagerInner<V::File>) {
let snapshot = Arc::new(PagerCommittedSnapshot::from_inner(inner));
let mut guard = self
.committed_snapshot
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = snapshot;
}
fn drain_committed_cache_pages(&mut self) -> Vec<(PageNumber, PageBuf)> {
let mut committed_pages = Vec::with_capacity(self.write_set.len());
for (page_no, staged) in self.write_set.drain() {
committed_pages.push((page_no, staged.into_buf(&self.pool)));
}
self.write_pages_sorted.clear();
committed_pages
}
fn drain_committed_cache_pages_into_cache(&mut self) {
for (page_no, staged) in self.write_set.drain() {
self.cache
.insert_buffer(page_no, staged.into_buf(&self.pool));
}
self.write_pages_sorted.clear();
}
fn discard_committed_pages(&mut self) {
self.write_set.clear();
self.write_pages_sorted.clear();
}
fn collect_unstaged_allocated_pages(&self) -> Vec<PageNumber> {
self.allocated_from_eof
.iter()
.chain(self.allocated_from_freelist.iter())
.copied()
.filter(|page| !self.write_set.contains_key(page) && !self.contains_freed_page(*page))
.collect()
}
fn pending_free_pages_for_commit(&self) -> Vec<PageNumber> {
let mut pending = self.collect_unstaged_allocated_pages();
pending.extend(self.page_lease.iter().copied());
pending.extend(self.freed_pages.iter().copied());
pending
}
fn drain_unstaged_allocated_pages(&mut self) -> Vec<PageNumber> {
let mut unstaged = Vec::new();
let write_set = &self.write_set;
let freed_pages = &self.freed_pages;
let freed_page_bounds = self.freed_page_bounds;
self.allocated_from_eof.retain(|page| {
let keep = write_set.contains_key(page)
|| freed_page_bounds.is_some_and(|(low, high)| low <= *page && *page <= high)
&& freed_pages.contains(page);
if !keep {
unstaged.push(*page);
}
keep
});
self.allocated_from_freelist.retain(|page| {
let keep = write_set.contains_key(page)
|| freed_page_bounds.is_some_and(|(low, high)| low <= *page && *page <= high)
&& freed_pages.contains(page);
if !keep {
unstaged.push(*page);
}
keep
});
unstaged
}
fn restore_uncommitted_allocations_for_clean_commit(
&mut self,
inner: &mut PagerInner<V::File>,
) {
return_pages_to_freelist(&mut inner.freelist, self.allocated_from_freelist.drain(..));
if self.mode == TransactionMode::Concurrent {
return_pages_to_freelist(&mut inner.freelist, self.page_lease.drain(..));
return_pages_to_freelist(&mut inner.freelist, self.allocated_from_eof.drain(..));
} else {
self.page_lease.clear();
self.allocated_from_eof.clear();
inner.db_size = self.original_db_size;
inner.next_page = if inner.db_size >= 2 {
inner.db_size.saturating_add(1)
} else {
2
};
}
}
}
fn cleanup_child_cx(cx: &Cx) -> Cx {
cx.create_child()
}
impl<V> SimpleTransaction<V>
where
V: Vfs + Send,
V::File: Send + Sync,
{
fn invalidate_journal_after_commit(cx: &Cx, journal_file: &mut V::File) -> Result<()> {
journal_file.truncate(cx, 0)?;
journal_file.sync(cx, SyncFlags::NORMAL)?;
Ok(())
}
#[allow(clippy::too_many_lines)]
fn commit_journal<S: std::hash::BuildHasher>(
cx: &Cx,
vfs: &Arc<V>,
journal_path: &Path,
inner: &mut PagerInner<V::File>,
write_set: &HashMap<PageNumber, StagedPage, S>,
original_db_size: u32,
) -> Result<()> {
if !write_set.is_empty() {
inner.db_file.lock(cx, LockLevel::Exclusive)?;
let nonce = 0x4652_414E; let page_size = inner.page_size;
let ps = page_size.as_usize();
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl_file, _) = vfs.open(cx, Some(journal_path), jrnl_flags)?;
let header = JournalHeader {
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
page_count: write_set.len() as i32,
nonce,
initial_db_size: original_db_size,
sector_size: 512,
page_size: page_size.get(),
};
let hdr_bytes = header.encode_padded();
jrnl_file.write(cx, &hdr_bytes, 0)?;
let mut jrnl_offset = hdr_bytes.len() as u64;
for &page_no in write_set.keys() {
let mut pre_image = vec![0u8; ps];
if page_no.get() <= original_db_size {
let disk_offset = u64::from(page_no.get() - 1) * ps as u64;
let bytes_read = inner.db_file.read(cx, &mut pre_image, disk_offset)?;
if bytes_read < ps {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read while journaling pre-image for page {}: got {bytes_read} of {ps}",
page_no.get()
),
});
}
}
let record = JournalPageRecord::new(page_no.get(), pre_image, nonce);
let rec_bytes = record.encode();
jrnl_file.write(cx, &rec_bytes, jrnl_offset)?;
jrnl_offset += rec_bytes.len() as u64;
}
jrnl_file.sync(cx, SyncFlags::NORMAL)?;
inner.rollback_journal_recovery_state = RollbackJournalRecoveryState::Pending;
let saved_db_size = inner.db_size;
for (page_no, staged) in write_set {
if let Err(e) = inner.flush_page(cx, *page_no, staged.as_page_bytes()) {
inner.db_size = saved_db_size;
return Err(e);
}
inner.db_size = inner.db_size.max(page_no.get());
}
inner.db_file.sync(cx, SyncFlags::NORMAL)?;
let cleanup_result = match Self::invalidate_journal_after_commit(cx, &mut jrnl_file) {
Ok(()) => {
let _ = vfs.delete(cx, journal_path, true);
Ok(())
}
Err(invalidate_err) => {
if let Err(delete_err) = vfs.delete(cx, journal_path, true) {
Err(FrankenError::internal(format!(
"committed database but failed to invalidate or delete rollback journal: invalidate={invalidate_err}; delete={delete_err}"
)))
} else {
Ok(())
}
}
};
inner.rollback_journal_recovery_state = RollbackJournalRecoveryState::Clean;
cleanup_result?;
}
Ok(())
}
fn flush_write_set_to_db_file_batch<S: std::hash::BuildHasher>(
cx: &Cx,
inner: &mut PagerInner<V::File>,
write_set: &HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &[PageNumber],
) -> Result<()> {
if write_pages_sorted.is_empty() {
return Ok(());
}
let page_size_bytes = u64::from(inner.page_size.get());
let mut batched_writes: SmallVec<[(u64, &[u8]); 8]> =
SmallVec::with_capacity(write_pages_sorted.len());
for &page_no in write_pages_sorted {
let staged = write_set.get(&page_no).ok_or_else(|| {
FrankenError::internal(format!(
"write_set missing staged page {} referenced by write_pages_sorted",
page_no.get()
))
})?;
let offset = u64::from(page_no.get() - 1) * page_size_bytes;
batched_writes.push((offset, staged.as_page_bytes()));
}
inner
.db_file
.write_page_batch(cx, batched_writes.as_slice())
}
#[cfg(test)]
fn commit_wal_group_commit<S: std::hash::BuildHasher>(
cx: &Cx,
wal_backend: &SharedWalBackend,
inner_arc: &Arc<Mutex<PagerInner<V::File>>>,
write_set: &HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &[PageNumber],
conflict_pages: &[PageNumber],
queue: &GroupCommitQueueRef,
) -> Result<()> {
let (current_db_size, sync_policy) = {
let inner = inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
(inner.db_size, inner.wal_commit_sync_policy)
};
Self::commit_wal_group_commit_with_snapshot(
cx,
wal_backend,
inner_arc,
current_db_size,
sync_policy,
write_set,
write_pages_sorted,
conflict_pages,
queue,
)
}
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
fn commit_wal_group_commit_with_snapshot<S: std::hash::BuildHasher>(
cx: &Cx,
wal_backend: &SharedWalBackend,
inner_arc: &Arc<Mutex<PagerInner<V::File>>>,
current_db_size: u32,
sync_policy: WalCommitSyncPolicy,
write_set: &HashMap<PageNumber, StagedPage, S>,
write_pages_sorted: &[PageNumber],
conflict_pages: &[PageNumber],
queue: &GroupCommitQueueRef,
) -> Result<()> {
let detailed_metrics = detailed_consolidation_metrics_enabled();
let lane_staging_debug_enabled =
tracing::enabled!(target: "fsqlite::wal::lane_staging", tracing::Level::DEBUG);
let lock_scope_debug_enabled =
tracing::enabled!(target: "fsqlite::wal::lock_scope", tracing::Level::DEBUG);
let phase_timing =
commit_phase_timing_enabled() || lane_staging_debug_enabled || lock_scope_debug_enabled;
let t_start = phase_timing.then(Instant::now);
let t_batch_build_start = detailed_metrics.then(Instant::now);
let (batch, _our_new_db_size) =
match build_group_commit_batch(current_db_size, write_set, write_pages_sorted)? {
Some(b) => b,
None => return Ok(()), };
let batch_build_us = elapsed_profile_us(t_batch_build_start);
let t_conflict_snapshot_start = detailed_metrics.then(Instant::now);
let conflict_snapshot =
with_wal_backend_read(wal_backend, |wal| Ok(wal.pinned_read_snapshot()))?;
let batch = attach_group_commit_conflict_metadata(batch, conflict_pages, conflict_snapshot);
let conflict_snapshot_us = elapsed_profile_us(t_conflict_snapshot_start);
let parallel_wal_control = queue.parallel_wal_control().clone();
let batch_id = queue.next_parallel_wal_batch_id();
let lane_id = queue.current_parallel_wal_lane_id();
let mut staging_fallback_reason = if matches!(
parallel_wal_control.mode,
ParallelWalOperatingMode::Conservative
) {
Some(ParallelWalFallbackReason::OperatorForced)
} else if let Some(limit) = parallel_wal_control.max_parallel_commit_bytes {
if group_commit_batch_staged_bytes(&batch) > limit {
Some(ParallelWalFallbackReason::LaneOverflow)
} else {
None
}
} else {
None
};
let mut lane_shadow_verdict = ParallelWalShadowVerdict::NotRun;
let mut lane_backlog = if lane_staging_debug_enabled {
Some(queue.current_lane_backlog(lane_id))
} else {
None
};
let mut batch = batch.with_context(TransactionFrameBatchContext {
batch_id,
lane_id,
staged_frame_count: 0,
staging_elapsed_ns: 0,
});
let mut lane_prepare_us = 0;
if staging_fallback_reason.is_none() {
let t_lane_prepare_start = detailed_metrics.then(Instant::now);
let staged_prepared = prepare_group_commit_batch_for_lane(
cx,
wal_backend,
&batch,
batch_id,
lane_id,
¶llel_wal_control,
)?;
lane_prepare_us = elapsed_profile_us(t_lane_prepare_start);
if let Some(staged_prepared) = staged_prepared {
let staged_frame_count = staged_prepared.staged_frame_count;
let staging_elapsed_ns = staged_prepared.staging_elapsed_ns;
lane_shadow_verdict = staged_prepared.shadow_verdict;
let new_lane_backlog = queue.record_prepared_batch(staged_prepared);
if lane_staging_debug_enabled {
lane_backlog = Some(new_lane_backlog);
}
batch = batch.with_context(TransactionFrameBatchContext {
batch_id,
lane_id,
staged_frame_count,
staging_elapsed_ns,
});
} else {
staging_fallback_reason = Some(ParallelWalFallbackReason::ControllerEvidenceLost);
}
}
if lane_staging_debug_enabled {
let queue_submit_max_wait = {
let consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
consolidator.max_group_delay()
};
let queue_submit_batch_membership = batch.context.batch_id.to_string();
let queue_submit_rollback_mode_active = physical_writer_rollback_mode_active(
parallel_wal_control.mode,
staging_fallback_reason,
);
tracing::debug!(
target: "fsqlite::wal::lane_staging",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_LANE_RUN_ID,
scenario_id = PARALLEL_WAL_STAGE_SCENARIO_ID,
batch_id = batch.context.batch_id,
batch_membership = queue_submit_batch_membership.as_str(),
queue_delay_ns = 0_u64,
target_wait_ns = 0_u64,
max_wait_ns =
u64::try_from(queue_submit_max_wait.as_nanos()).unwrap_or(u64::MAX),
fsync_boundary = physical_writer_fsync_boundary(sync_policy),
ordering_phase = "queue_submit",
rollback_mode_active = queue_submit_rollback_mode_active,
wal_lane_id = lane_id,
lane_backlog = lane_backlog.unwrap_or(0),
staged_frame_count = batch.context.staged_frame_count,
flush_trigger = "queue_submit",
control_mode = parallel_wal_mode_name(parallel_wal_control.mode),
lane_policy_version = PARALLEL_WAL_LANE_POLICY_VERSION,
shadow_verdict = parallel_wal_shadow_verdict_name(lane_shadow_verdict),
compatibility_selector = PARALLEL_WAL_COMPATIBILITY_SELECTOR,
fallback_reason = parallel_wal_fallback_reason_name(staging_fallback_reason),
elapsed_ns = batch.context.staging_elapsed_ns,
"queued lane-local WAL staging candidate"
);
}
let prepare_us = elapsed_profile_us(t_start);
let waiter_id = batch.context.batch_id;
let waiter_frames_contributed = batch.frames.len();
let t_consolidator_lock_start = phase_timing.then(Instant::now);
let (outcome, our_epoch, consolidator_lock_wait_us, flushing_wait_us) = {
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let lock_wait_us = elapsed_profile_us(t_consolidator_lock_start);
let flushing_wait = 0u64;
let epoch = consolidator.epoch();
let outcome = consolidator.submit_batch(batch)?;
(outcome, epoch, lock_wait_us, flushing_wait)
};
let target_epoch = our_epoch.saturating_add(1);
trace_group_commit(format_args!(
"waiter waiter_id={waiter_id} role={outcome:?} epoch_at_queue={our_epoch} target_epoch={target_epoch} frames_i_contributed={waiter_frames_contributed}"
));
let run_flusher_loop = |mut record_initial_metrics: bool,
mut needs_arrival_wait: bool,
mut prefetched_flush: Option<(Vec<TransactionFrameBatch>, u64)>|
-> Result<()> {
'flusher_loop: loop {
let arrival_wait_decision = {
let (observation, max_wait) = {
let consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let observation = if prefetched_flush.is_none() && needs_arrival_wait {
Some(ArrivalWaitObservation {
pending_batch_count: consolidator.pending_batch_count(),
should_flush_now: consolidator.should_flush_now(),
fill_age: consolidator.fill_age(),
})
} else {
None
};
(observation, consolidator.max_group_delay())
};
let fairness_budget =
commit_service_fairness_budget(queue.parallel_wal_control(), max_wait);
let queue_age_p95 = observation
.map_or(Duration::ZERO, |obs| recent_queue_age_p95(obs.fill_age));
let previous_mode = queue.current_commit_service_mode();
let control_epoch = queue.next_commit_service_control_epoch();
let decision = decide_group_commit_arrival_wait(
observation,
max_wait,
fairness_budget,
queue_age_p95,
previous_mode,
control_epoch,
);
queue.store_commit_service_mode(decision.mode);
decision
};
let arrival_wait_us = if !arrival_wait_decision.wait_budget.is_zero() {
let t_arrival_wait_start = Instant::now();
let deadline = t_arrival_wait_start + arrival_wait_decision.wait_budget;
loop {
let should_flush = {
let consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
consolidator.should_flush_now()
};
if should_flush || Instant::now() >= deadline {
break;
}
std::hint::spin_loop();
}
Instant::now()
.duration_since(t_arrival_wait_start)
.as_micros() as u64
} else {
0
};
let actual_wait_ns =
u64::try_from(Duration::from_micros(arrival_wait_us).as_nanos())
.unwrap_or(u64::MAX);
let (mut batches, flush_epoch) = if let Some(prefetched) = prefetched_flush.take() {
prefetched
} else {
let maybe_flush = {
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !record_initial_metrics
&& consolidator.phase() != fsqlite_wal::ConsolidationPhase::Filling
{
None
} else {
let batches = consolidator.begin_flush()?;
Some((batches, consolidator.epoch()))
}
};
let Some(flush) = maybe_flush else {
break;
};
flush
};
let t_flush_frame_prep_start = detailed_metrics.then(Instant::now);
let conflicting_pages = conflicting_pages_across_group_commit_batches(&batches);
if !conflicting_pages.is_empty() {
let flush_batch_membership = physical_writer_batch_membership(&batches);
let flush_batch_id = physical_writer_primary_batch_id(&batches);
let queue_delay_ns = arrival_wait_decision.queue_delay_ns();
let target_wait_ns = arrival_wait_decision.target_wait_ns();
let max_wait_ns = arrival_wait_decision.max_wait_ns();
let fsync_boundary = physical_writer_fsync_boundary(sync_policy);
let error = FrankenError::BusySnapshot {
conflicting_pages: conflicting_pages
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(","),
};
tracing::warn!(
target: "fsqlite::wal::lock_scope",
epoch = flush_epoch,
conflicting_pages = ?conflicting_pages,
"aborting group-commit epoch with cross-batch same-page overlap"
);
tracing::warn!(
target: "fsqlite::wal::lane_staging",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_LANE_RUN_ID,
scenario_id = PARALLEL_WAL_FLUSH_SCENARIO_ID,
batch_id = flush_batch_id,
batch_membership = flush_batch_membership.as_str(),
queue_delay_ns,
target_wait_ns,
actual_wait_ns,
max_wait_ns,
control_epoch = arrival_wait_decision.control_epoch,
queue_age_p95_ns = arrival_wait_decision.queue_age_p95_ns(),
batch_size = batches.len(),
fairness_budget_ns = arrival_wait_decision.fairness_budget_ns(),
starvation_prevented = arrival_wait_decision.starvation_prevented,
mode_switch_reason = arrival_wait_decision.mode_switch_reason,
service_policy_mode = commit_service_mode_name(arrival_wait_decision.mode),
fsync_boundary,
ordering_phase = "overlap_abort",
rollback_mode_active =
physical_writer_rollback_mode_active(parallel_wal_control.mode, None),
failure_context = "cross_batch_page_overlap",
conflicting_pages = ?conflicting_pages,
"aborting physical writer flush because batch membership overlaps on the same page"
);
let discarded_prepared_batches =
queue.discard_prepared_batches_for_flush(&batches);
if discarded_prepared_batches > 0 {
tracing::trace!(
target: "fsqlite::wal::lane_staging",
discarded_prepared_batches,
"discarded stale prepared WAL lane payloads before group-commit overlap abort"
);
}
let (abort_result, wake_next_epoch) = {
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let abort_result = consolidator.abort_flush();
let wake_next_epoch =
abort_result.is_ok() && consolidator.has_flusher_vacancy();
(abort_result, wake_next_epoch)
};
queue.publish_failed_epoch(flush_epoch, &error, wake_next_epoch);
if let Err(abort_error) = abort_result {
return Err(FrankenError::internal(format!(
"group commit overlap abort failed for epoch {flush_epoch}: overlap={error}; abort={abort_error}"
)));
}
return Err(error);
}
let batch_count = batches.len();
let flush_base_db_size = {
let inner = inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
inner.db_size
};
let consolidated_db_size = group_commit_final_db_size(flush_base_db_size, &batches);
let promoted_page_one_headers =
promote_group_commit_page_one_headers(&mut batches, consolidated_db_size);
let (frame_refs, final_db_size) =
flatten_group_commit_batches(flush_base_db_size, &batches);
debug_assert_eq!(final_db_size, consolidated_db_size);
let frame_count = frame_refs.len();
let mut fallback_reason = if matches!(
parallel_wal_control.mode,
ParallelWalOperatingMode::Conservative
) {
Some(ParallelWalFallbackReason::OperatorForced)
} else {
None
};
let mut shadow_verdict = ParallelWalShadowVerdict::NotRun;
let mut prepared_batch = if fallback_reason.is_none() {
match queue.take_prepared_batches_for_flush(&batches) {
Some(mut staged_by_batch_id) => {
if promoted_page_one_headers {
fallback_reason =
Some(ParallelWalFallbackReason::ControllerEvidenceLost);
None
} else {
match batches
.iter()
.map(|batch| staged_by_batch_id.remove(&batch.context.batch_id))
.collect::<Option<Vec<_>>>()
{
Some(ordered_staged_batches) => {
let staged_shadow_verdict =
aggregate_shadow_verdict(&ordered_staged_batches);
match merge_prepared_group_commit_batches(
ordered_staged_batches,
final_db_size,
) {
Ok(merged) => {
if should_shadow_compare_batches(
¶llel_wal_control,
&batches,
) {
if prepared_batch_matches_frame_refs(
&merged,
&frame_refs,
) {
shadow_verdict =
ParallelWalShadowVerdict::Clean;
Some(merged)
} else {
shadow_verdict =
ParallelWalShadowVerdict::Diverged;
fallback_reason = Some(
ParallelWalFallbackReason::PublicationMismatch,
);
None
}
} else {
shadow_verdict = staged_shadow_verdict;
Some(merged)
}
}
Err(_) => {
fallback_reason = Some(
ParallelWalFallbackReason::ControllerEvidenceLost,
);
None
}
}
}
None => {
fallback_reason =
Some(ParallelWalFallbackReason::ControllerEvidenceLost);
None
}
}
}
}
None => {
let discarded_prepared_batches =
queue.discard_prepared_batches_for_flush(&batches);
if discarded_prepared_batches > 0 {
tracing::trace!(
target: "fsqlite::wal::lane_staging",
discarded_prepared_batches,
"discarded stale prepared WAL lane payloads after raw fallback"
);
}
fallback_reason =
Some(ParallelWalFallbackReason::ControllerEvidenceLost);
None
}
}
} else {
None
};
if prepared_batch.is_none() {
let prepared = with_wal_backend_read(wal_backend, |wal| {
let mut prepared_batch = wal.prepare_append_frames(&frame_refs)?;
if let Some(prepared) = prepared_batch.as_mut() {
wal.finalize_prepared_frames(cx, prepared)?;
}
Ok(prepared_batch)
})?;
prepared_batch = prepared;
}
let flush_frame_prep_us = elapsed_profile_us(t_flush_frame_prep_start);
GLOBAL_CONSOLIDATION_METRICS.transactions_batched.fetch_add(
u64::try_from(batch_count).unwrap_or(u64::MAX),
AtomicOrdering::Relaxed,
);
const MAX_FLUSH_RETRIES: u32 = 10;
let mut flush_result: Result<()> = Ok(());
let mut inner_lock_wait_us: u64 = 0;
let mut exclusive_lock_us: u64 = 0;
let mut wal_append_us: u64 = 0;
let mut append_conflict_check_us: u64 = 0;
let mut append_frames_us: u64 = 0;
let mut wal_sync_us: u64 = 0;
let mut frames_written_start: u64 = 0;
let mut frames_written_end: u64 = 0;
let mut fsync_seq: u64 = 0;
for attempt in 0..MAX_FLUSH_RETRIES {
let t_inner_lock_start = phase_timing.then(Instant::now);
flush_result = (|| -> Result<()> {
let mut inner = inner_arc.lock().map_err(|_| {
FrankenError::internal("SimpleTransaction lock poisoned")
})?;
inner_lock_wait_us = elapsed_profile_us(t_inner_lock_start);
let t_excl_start = phase_timing.then(Instant::now);
inner.db_file.lock(cx, LockLevel::Reserved)?;
exclusive_lock_us = elapsed_profile_us(t_excl_start);
let restore_lock_level = if inner.writer_active {
LockLevel::Reserved
} else {
LockLevel::Shared
};
let t_append_start = phase_timing.then(Instant::now);
let flush_io_result = (|| -> Result<()> {
with_wal_backend(wal_backend, |wal| {
frames_written_start = u64::try_from(wal.frame_count())
.unwrap_or(u64::MAX)
.saturating_add(1);
let t_append_conflict_check_start =
detailed_metrics.then(Instant::now);
let stale_conflict_pages =
conflicting_pages_since_batch_snapshots(cx, wal, &batches)?;
append_conflict_check_us =
elapsed_profile_us(t_append_conflict_check_start);
if !stale_conflict_pages.is_empty() {
return Err(FrankenError::BusySnapshot {
conflicting_pages: stale_conflict_pages
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(","),
});
}
let t_append_frames_start = detailed_metrics.then(Instant::now);
if let Some(prepared) = prepared_batch.as_mut() {
wal.append_prepared_frames(cx, prepared)?;
} else {
wal.append_frames(cx, &frame_refs)?;
}
append_frames_us = elapsed_profile_us(t_append_frames_start);
frames_written_end =
u64::try_from(wal.frame_count()).unwrap_or(u64::MAX);
Ok(())
})?;
wal_append_us = elapsed_profile_us(t_append_start);
if sync_policy.should_sync_on_commit() {
let t_sync_start = phase_timing.then(Instant::now);
with_wal_backend(wal_backend, |wal| wal.sync(cx))?;
wal_sync_us = elapsed_profile_us(t_sync_start);
fsync_seq = GROUP_COMMIT_TRACE_FSYNC_SEQ
.fetch_add(1, AtomicOrdering::Relaxed)
.saturating_add(1);
GLOBAL_CONSOLIDATION_METRICS
.fsyncs_total
.fetch_add(1, AtomicOrdering::Relaxed);
}
inner.db_size = final_db_size;
Ok(())
})();
let restore_result = inner.db_file.unlock(cx, restore_lock_level);
match (flush_io_result, restore_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(flush_error), Err(restore_error)) => {
Err(FrankenError::internal(format!(
"flush failed and could not restore SHARED lock: flush={flush_error}; restore={restore_error}"
)))
}
}
})();
match &flush_result {
Err(
FrankenError::Busy
| FrankenError::BusyRecovery
| FrankenError::BusySnapshot { .. },
) if attempt + 1 < MAX_FLUSH_RETRIES => {
perform_flush_busy_retry_handoff(flush_busy_retry_wait(attempt + 1));
GLOBAL_CONSOLIDATION_METRICS.record_busy_retry();
GLOBAL_CONSOLIDATION_METRICS
.wake_reasons
.busy_retry
.fetch_add(1, AtomicOrdering::Relaxed);
}
_ => break,
}
}
match flush_result {
Ok(()) => {
#[cfg(any(test, feature = "fault-injection"))]
if let Err(error) =
crate::fault_hooks::maybe_inject_after_flush_before_publish(
flush_epoch,
batch_count,
frame_count,
)
{
let (abort_result, wake_next_epoch) = {
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let abort_result = consolidator.abort_flush();
let wake_next_epoch =
abort_result.is_ok() && consolidator.has_flusher_vacancy();
(abort_result, wake_next_epoch)
};
queue.publish_failed_epoch(flush_epoch, &error, wake_next_epoch);
if let Err(abort_error) = abort_result {
return Err(FrankenError::internal(format!(
"group commit flush hook failed for epoch {flush_epoch} and abort_flush also failed: hook={error}; abort={abort_error}"
)));
}
return Err(error);
}
GLOBAL_CONSOLIDATION_METRICS
.groups_flushed
.fetch_add(1, AtomicOrdering::Relaxed);
GLOBAL_CONSOLIDATION_METRICS.frames_consolidated.fetch_add(
u64::try_from(frame_count).unwrap_or(u64::MAX),
AtomicOrdering::Relaxed,
);
GLOBAL_CONSOLIDATION_METRICS
.max_group_size_observed
.fetch_max(
u64::try_from(frame_count).unwrap_or(u64::MAX),
AtomicOrdering::Relaxed,
);
if detailed_metrics {
if record_initial_metrics {
GLOBAL_CONSOLIDATION_METRICS.record_prepare_breakdown(
batch_build_us,
conflict_snapshot_us,
lane_prepare_us,
);
}
GLOBAL_CONSOLIDATION_METRICS.record_flush_breakdown(
flush_frame_prep_us,
append_conflict_check_us,
append_frames_us,
);
}
if phase_timing {
GLOBAL_CONSOLIDATION_METRICS.record_phase_timing(
if record_initial_metrics {
prepare_us
} else {
0
},
if record_initial_metrics {
consolidator_lock_wait_us
} else {
0
},
flushing_wait_us,
true,
arrival_wait_us,
inner_lock_wait_us,
exclusive_lock_us,
wal_append_us,
wal_sync_us,
0,
);
}
let lock_wait_total_us =
inner_lock_wait_us + exclusive_lock_us + flushing_wait_us;
let wal_service_total_us = wal_append_us + wal_sync_us;
tracing::debug!(
target: "fsqlite::wal::lock_scope",
role = "flusher",
epoch = flush_epoch,
frames = frame_count,
lock_wait_us = lock_wait_total_us,
inner_lock_wait_us,
exclusive_lock_us,
flushing_wait_us,
wal_service_us = wal_service_total_us,
wal_append_us,
wal_sync_us,
arrival_wait_us,
arrival_wait_policy = arrival_wait_decision.policy,
arrival_wait_reason = arrival_wait_decision.reason,
arrival_wait_budget_us = arrival_wait_decision.wait_budget_us(),
arrival_wait_fill_age_us = arrival_wait_decision.fill_age_us(),
service_policy_mode =
commit_service_mode_name(arrival_wait_decision.mode),
service_policy_control_epoch = arrival_wait_decision.control_epoch,
queue_age_p95_us =
arrival_wait_decision.queue_age_p95.as_micros() as u64,
fairness_budget_us =
arrival_wait_decision.fairness_budget.as_micros() as u64,
starvation_prevented = arrival_wait_decision.starvation_prevented,
arrival_wait_used_legacy_fallback =
arrival_wait_decision.used_legacy_fallback,
"WAL backend commit: lock_wait={lock_wait_total_us}us \
service={wal_service_total_us}us \
(append={wal_append_us}us sync={wal_sync_us}us) \
frames={frame_count}"
);
if tracing::enabled!(target: "fsqlite::wal::lane_staging", tracing::Level::DEBUG)
{
let flush_batch_membership = physical_writer_batch_membership(&batches);
let flush_batch_id = physical_writer_primary_batch_id(&batches);
let queue_delay_ns = arrival_wait_decision.queue_delay_ns();
let target_wait_ns = arrival_wait_decision.target_wait_ns();
let max_wait_ns = arrival_wait_decision.max_wait_ns();
let fsync_boundary = physical_writer_fsync_boundary(sync_policy);
let ordering_phase =
physical_writer_ordering_phase(arrival_wait_decision.reason);
let flush_rollback_mode_active = physical_writer_rollback_mode_active(
parallel_wal_control.mode,
fallback_reason,
);
let lane_stats = lane_flush_stats(queue, &batches);
for (lane_id, lane_backlog, staged_frame_count, elapsed_ns) in
&lane_stats
{
tracing::debug!(
target: "fsqlite::wal::lane_staging",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_LANE_RUN_ID,
scenario_id = PARALLEL_WAL_FLUSH_SCENARIO_ID,
batch_id = flush_batch_id,
batch_membership = flush_batch_membership.as_str(),
queue_delay_ns,
target_wait_ns,
actual_wait_ns,
max_wait_ns,
control_epoch = arrival_wait_decision.control_epoch,
queue_age_p95_ns = arrival_wait_decision.queue_age_p95_ns(),
batch_size = batch_count,
fairness_budget_ns = arrival_wait_decision.fairness_budget_ns(),
starvation_prevented =
arrival_wait_decision.starvation_prevented,
mode_switch_reason = arrival_wait_decision.mode_switch_reason,
service_policy_mode =
commit_service_mode_name(arrival_wait_decision.mode),
fsync_boundary,
ordering_phase,
rollback_mode_active = flush_rollback_mode_active,
wal_lane_id = *lane_id,
lane_backlog = *lane_backlog,
staged_frame_count = *staged_frame_count,
flush_trigger = arrival_wait_decision.reason,
control_mode = parallel_wal_mode_name(parallel_wal_control.mode),
lane_policy_version = PARALLEL_WAL_LANE_POLICY_VERSION,
shadow_verdict =
parallel_wal_shadow_verdict_name(shadow_verdict),
compatibility_selector = PARALLEL_WAL_COMPATIBILITY_SELECTOR,
fallback_reason =
parallel_wal_fallback_reason_name(fallback_reason),
elapsed_ns = *elapsed_ns,
"flushed lane-local WAL staging candidate"
);
}
}
queue.record_persisted_epoch(
flush_epoch,
&batches,
frames_written_start,
frames_written_end,
fsync_seq,
);
let (completed_epoch, has_promoted) = {
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let promoted = consolidator.complete_flush()?;
if promoted && !consolidator.claim_flusher_vacancy() {
return Err(FrankenError::internal(format!(
"group commit flusher could not reserve promoted epoch after completing epoch {flush_epoch}"
)));
}
(consolidator.epoch(), promoted)
};
queue.publish_completed_epoch(completed_epoch, false);
if has_promoted {
record_initial_metrics = false;
needs_arrival_wait = false;
continue 'flusher_loop;
}
}
Err(error) => {
let flush_batch_membership = physical_writer_batch_membership(&batches);
let flush_batch_id = physical_writer_primary_batch_id(&batches);
let queue_delay_ns = arrival_wait_decision.queue_delay_ns();
let target_wait_ns = arrival_wait_decision.target_wait_ns();
let max_wait_ns = arrival_wait_decision.max_wait_ns();
let fsync_boundary = physical_writer_fsync_boundary(sync_policy);
tracing::warn!(
target: "fsqlite::wal::lane_staging",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_LANE_RUN_ID,
scenario_id = PARALLEL_WAL_FLUSH_SCENARIO_ID,
batch_id = flush_batch_id,
batch_membership = flush_batch_membership.as_str(),
queue_delay_ns,
target_wait_ns,
actual_wait_ns,
max_wait_ns,
control_epoch = arrival_wait_decision.control_epoch,
queue_age_p95_ns = arrival_wait_decision.queue_age_p95_ns(),
batch_size = batch_count,
fairness_budget_ns = arrival_wait_decision.fairness_budget_ns(),
starvation_prevented = arrival_wait_decision.starvation_prevented,
mode_switch_reason = arrival_wait_decision.mode_switch_reason,
service_policy_mode =
commit_service_mode_name(arrival_wait_decision.mode),
fsync_boundary,
ordering_phase = "flush_error",
rollback_mode_active = physical_writer_rollback_mode_active(
parallel_wal_control.mode,
fallback_reason,
),
shadow_verdict =
parallel_wal_shadow_verdict_name(shadow_verdict),
fallback_reason =
parallel_wal_fallback_reason_name(fallback_reason),
failure_context = %error,
"physical writer flush failed"
);
let (abort_result, wake_next_epoch) = {
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let abort_result = consolidator.abort_flush();
let wake_next_epoch =
abort_result.is_ok() && consolidator.has_flusher_vacancy();
(abort_result, wake_next_epoch)
};
queue.publish_failed_epoch(flush_epoch, &error, wake_next_epoch);
if let Err(abort_error) = abort_result {
return Err(FrankenError::internal(format!(
"group commit flush failed for epoch {flush_epoch} and abort_flush also failed: flush={error}; abort={abort_error}"
)));
}
return Err(error);
}
}
break;
}
Ok(())
};
match outcome {
SubmitOutcome::Flusher => {
run_flusher_loop(true, true, None)?;
}
SubmitOutcome::Waiter => {
let t_waiter_start = phase_timing.then(Instant::now);
let guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let wait_outcome = queue.wait_for_epoch_outcome(guard, target_epoch)?;
let waiter_epoch_wait_us = elapsed_profile_us(t_waiter_start);
match wait_outcome {
WaitForEpochOutcome::Completed => {
if group_commit_trace_enabled() {
let completed_epoch =
queue.completed_epoch.load(AtomicOrdering::Acquire);
let persisted = queue.persisted_epoch_for(target_epoch);
let persisted_contains_waiter = persisted
.as_ref()
.is_some_and(|record| record.members.contains(&waiter_id));
let (frames_start, frames_end, fsync_seq) =
persisted.as_ref().map_or((0, 0, 0), |record| {
(record.frames_start, record.frames_end, record.fsync_seq)
});
trace_group_commit(format_args!(
"waiter_wake waiter_id={waiter_id} epoch_at_queue={our_epoch} target_epoch={target_epoch} completed_epoch_when_woken={completed_epoch} frames_i_contributed={waiter_frames_contributed} persisted_contains_waiter={persisted_contains_waiter} frames_written_range={frames_start}..={frames_end} fsync_seq={fsync_seq}"
));
assert!(
target_epoch <= completed_epoch && persisted_contains_waiter,
"group commit waiter {waiter_id} woke for completed_epoch={completed_epoch}, target_epoch={target_epoch}, epoch_at_queue={our_epoch}, but its frames were not recorded in the persisted epoch"
);
}
if detailed_metrics {
GLOBAL_CONSOLIDATION_METRICS.record_prepare_breakdown(
batch_build_us,
conflict_snapshot_us,
lane_prepare_us,
);
}
if phase_timing {
GLOBAL_CONSOLIDATION_METRICS.record_phase_timing(
prepare_us,
consolidator_lock_wait_us,
flushing_wait_us,
false, 0, 0, 0, 0, 0, waiter_epoch_wait_us,
);
}
let lock_wait_total_us =
consolidator_lock_wait_us + flushing_wait_us + waiter_epoch_wait_us;
tracing::debug!(
target: "fsqlite::wal::lock_scope",
role = "waiter",
lock_wait_us = lock_wait_total_us,
consolidator_lock_wait_us,
flushing_wait_us,
waiter_epoch_wait_us,
wal_service_us = 0_u64,
wal_append_us = 0_u64,
wal_sync_us = 0_u64,
"WAL backend commit: lock_wait={lock_wait_total_us}us \
service=0us (waiter — flusher did I/O)"
);
}
WaitForEpochOutcome::TakeOverFlusher {
batches,
flush_epoch,
} => {
let _ = waiter_epoch_wait_us;
run_flusher_loop(true, false, Some((batches, flush_epoch)))?;
}
}
}
}
Ok(())
}
fn ensure_writer(&mut self, cx: &Cx) -> Result<()> {
if self.is_writer {
return Ok(());
}
match self.mode {
TransactionMode::ReadOnly => Err(FrankenError::ReadOnly),
TransactionMode::Concurrent => {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
if inner.checkpoint_active {
let active_transactions = inner.active_transactions;
let checkpoint_active = inner.checkpoint_active;
drop(inner);
log_checkpoint_coordination(
cx,
&self.group_commit_queue,
"active_gate",
"ensure_writer",
transaction_mode_name(self.mode),
"checkpoint_excludes_foreground_writer_upgrade",
true,
active_transactions,
checkpoint_active,
);
return Err(FrankenError::Busy);
}
drop(inner);
self.is_writer = true;
Ok(())
}
TransactionMode::Deferred => {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
if inner.checkpoint_active {
let active_transactions = inner.active_transactions;
let checkpoint_active = inner.checkpoint_active;
drop(inner);
log_checkpoint_coordination(
cx,
&self.group_commit_queue,
"active_gate",
"ensure_writer",
transaction_mode_name(self.mode),
"checkpoint_excludes_foreground_writer_upgrade",
true,
active_transactions,
checkpoint_active,
);
return Err(FrankenError::Busy);
}
if inner.writer_active {
inner = wait_for_single_writer_baton(&self.inner, &self.writer_idle, inner)?;
}
if inner.checkpoint_active {
let active_transactions = inner.active_transactions;
let checkpoint_active = inner.checkpoint_active;
drop(inner);
log_checkpoint_coordination(
cx,
&self.group_commit_queue,
"active_gate",
"ensure_writer",
transaction_mode_name(self.mode),
"checkpoint_excludes_foreground_writer_upgrade_after_baton_wait",
true,
active_transactions,
checkpoint_active,
);
return Err(FrankenError::Busy);
}
inner.db_file.lock(cx, LockLevel::Reserved)?;
inner.writer_active = true;
drop(inner);
self.is_writer = true;
Ok(())
}
TransactionMode::Immediate | TransactionMode::Exclusive => Err(FrankenError::internal(
"writer transaction lost writer role",
)),
}
}
}
const fn retained_lock_level_after_txn_exit(
remaining_active_transactions: u32,
writer_active: bool,
) -> LockLevel {
if remaining_active_transactions == 0 {
LockLevel::None
} else if writer_active {
LockLevel::Reserved
} else {
LockLevel::Shared
}
}
impl<V> TransactionHandle for SimpleTransaction<V>
where
V: Vfs + Send,
V::File: Send + Sync,
{
fn get_page(&self, cx: &Cx, page_no: PageNumber) -> Result<PageData> {
if self.contains_freed_page(page_no) {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"page {} was freed earlier in this transaction",
page_no.get()
),
});
}
if let Some(staged) = self.write_set.get(&page_no) {
return Ok(staged.published_page());
}
if self.rolled_back_pages.contains(&page_no) {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
return Ok(PageData::zeroed(inner.page_size));
}
if page_no.get() > self.published_db_size.get() {
let page_allocated_by_this_txn = self.allocated_from_eof.contains(&page_no)
|| self.allocated_from_freelist.contains(&page_no)
|| self.page_lease.contains(&page_no);
if !page_allocated_by_this_txn {
let fresh_snapshot = self.published.snapshot();
if page_no.get() <= fresh_snapshot.db_size {
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = "snapshot_refresh_success",
page_no = page_no.get(),
old_db_size = self.published_db_size.get(),
new_db_size = fresh_snapshot.db_size,
old_commit_seq = self.published_visible_commit_seq.get().get(),
new_commit_seq = fresh_snapshot.visible_commit_seq.get(),
"refreshed snapshot to include requested page"
);
self.published_db_size.set(fresh_snapshot.db_size);
self.published_visible_commit_seq
.set(fresh_snapshot.visible_commit_seq);
} else {
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = "page_beyond_snapshot_db_size",
page_no = page_no.get(),
published_db_size = self.published_db_size.get(),
latest_db_size = fresh_snapshot.db_size,
"page beyond transaction's snapshot db_size (even after refresh)"
);
return Err(FrankenError::BusySnapshot {
conflicting_pages: format!(
"page {} > snapshot db_size {} (latest: {})",
page_no.get(),
self.published_db_size.get(),
fresh_snapshot.db_size
),
});
}
}
}
let single_connection_fast_path = self.single_connection_fast_path_enabled();
if single_connection_fast_path
&& let Some(cached) = self.txn_read_cache.borrow().get(&page_no)
{
return Ok(cached.clone());
}
let trace_read_start =
tracing::enabled!(target: "fsqlite.snapshot_publication", tracing::Level::TRACE)
.then(Instant::now);
let mut published_retry_count = 0_usize;
while self.published.page_plane_visible_commit_seq()
== self.published_visible_commit_seq.get()
{
let snapshot = self.published.snapshot();
if page_no.get() > snapshot.db_size {
if self.published.current_sequence_gen() == snapshot.snapshot_gen {
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = "zero_fill_read",
snapshot_gen = snapshot.snapshot_gen,
visible_commit_seq = snapshot.visible_commit_seq.get(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
read_retry_count = self.published.read_retry_count(),
page_set_size = snapshot.page_set_size,
elapsed_ns = trace_read_start
.map_or(0, |start| {
u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX)
}),
"resolved zero-filled page from published metadata"
);
return Ok(PageData::from_vec(vec![0_u8; self.pool.page_size()]));
}
self.published.record_retry();
if published_retry_count >= PUBLISHED_READ_FAST_RETRY_LIMIT {
break;
}
self.published
.wait_for_sequence_change(snapshot.snapshot_gen, PUBLISHED_SNAPSHOT_WAIT_SLICE);
published_retry_count = published_retry_count.saturating_add(1);
continue;
}
if let Some(page) = self.published.try_get_page(page_no) {
if self.published.current_sequence_gen() == snapshot.snapshot_gen {
self.published.note_published_hit();
tracing::trace!(
target: "fsqlite.snapshot_publication",
trace_id = cx.trace_id(),
run_id = "pager-publication",
scenario_id = "published_read_hit",
snapshot_gen = snapshot.snapshot_gen,
visible_commit_seq = snapshot.visible_commit_seq.get(),
publication_mode = SNAPSHOT_PUBLICATION_MODE,
read_retry_count = self.published.read_retry_count(),
page_set_size = snapshot.page_set_size,
elapsed_ns = trace_read_start
.map_or(0, |start| {
u64::try_from(start.elapsed().as_nanos()).unwrap_or(u64::MAX)
}),
"served page from published snapshot"
);
return Ok(page);
}
self.published.record_retry();
if published_retry_count >= PUBLISHED_READ_FAST_RETRY_LIMIT {
break;
}
self.published
.wait_for_sequence_change(snapshot.snapshot_gen, PUBLISHED_SNAPSHOT_WAIT_SLICE);
published_retry_count = published_retry_count.saturating_add(1);
continue;
}
break;
}
let committed_snapshot = self.published.snapshot();
if committed_snapshot.visible_commit_seq == self.published_visible_commit_seq.get()
&& committed_snapshot.journal_mode != JournalMode::Wal
&& page_no.get() <= committed_snapshot.db_size
{
if let Some(page_data) = self.cache.get_shared(page_no) {
return Ok(page_data);
}
}
if let Some(cached) = self.txn_read_cache.borrow().get(&page_no) {
return Ok(cached.clone());
}
if self.journal_mode == JournalMode::Wal {
if let Some(data) = read_page_from_wal_backend(&self.wal_backend, cx, page_no)? {
let page = PageData::from_vec(data);
self.txn_read_cache
.borrow_mut()
.insert(page_no, page.clone());
return Ok(page);
}
}
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
let data = inner.read_page_copy(cx, &self.cache, &self.wal_backend, page_no)?;
let page = PageData::from_vec(data);
let publish_update = PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
};
let publish_page = page_no.get() <= inner.db_size
&& inner.commit_seq == self.published_visible_commit_seq.get();
drop(inner);
if publish_page && !single_connection_fast_path {
self.published
.publish_observed_page(cx, publish_update, page_no, page.clone());
}
self.txn_read_cache
.borrow_mut()
.insert(page_no, page.clone());
Ok(page)
}
fn prefetch_page_hint(&self, _cx: &Cx, page_no: PageNumber) {
if let Some(staged) = self.write_set.get(&page_no) {
prefetch_l1_read(staged.as_page_bytes().as_ptr());
return;
}
if let Ok(txn_read_cache) = self.txn_read_cache.try_borrow()
&& let Some(page) = txn_read_cache.get(&page_no)
{
prefetch_l1_read(page.as_bytes().as_ptr());
return;
}
if self.published.page_plane_visible_commit_seq() == self.published_visible_commit_seq.get()
{
self.published.prefetch_page_hint(page_no);
}
self.cache.prefetch_page_hint(page_no);
}
fn write_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()> {
self.ensure_writer(cx)?;
self.writes_observed = true;
self.remove_freed_page_if_present(page_no);
if let Some(existing) = self.write_set.get_mut(&page_no)
&& existing.try_overwrite_bytes_in_place(data)
{
STAGED_PAGE_OVERWRITE_STEALS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
return Ok(());
}
let staged = StagedPage::from_bytes(&self.pool, data)?;
insert_staged_page(
&mut self.write_set,
&mut self.write_pages_sorted,
page_no,
staged,
);
Ok(())
}
fn write_page_data(&mut self, cx: &Cx, page_no: PageNumber, data: PageData) -> Result<()> {
self.ensure_writer(cx)?;
self.writes_observed = true;
self.remove_freed_page_if_present(page_no);
if let Some(existing) = self.write_set.get_mut(&page_no) {
if existing.try_overwrite_page_data_in_place(&data) {
STAGED_PAGE_OVERWRITE_STEALS_TOTAL.fetch_add(1, AtomicOrdering::Relaxed);
return Ok(());
}
*existing = StagedPage::from_page_data_for_pool(&self.pool, data)?;
return Ok(());
}
let staged = StagedPage::from_page_data_for_pool(&self.pool, data)?;
insert_staged_page(
&mut self.write_set,
&mut self.write_pages_sorted,
page_no,
staged,
);
Ok(())
}
fn try_take_staged_page_data(&mut self, page_no: PageNumber) -> Option<PageData> {
let staged = self.write_set.remove(&page_no)?;
match staged.try_into_unpublished_owned_page_data() {
Ok(data) => {
remove_page_sorted(&mut self.write_pages_sorted, page_no);
Some(data)
}
Err(staged) => {
self.write_set.insert(page_no, staged);
None
}
}
}
fn try_mutate_staged_page_data(
&mut self,
page_no: PageNumber,
f: &mut dyn FnMut(&mut PageData),
) -> bool {
let Some(staged) = self.write_set.get_mut(&page_no) else {
return false;
};
if staged.published.get().is_some() {
return false;
}
let StagedPageBacking::Owned(data) = &mut staged.backing else {
return false;
};
f(data);
true
}
fn restore_staged_page_data(
&mut self,
cx: &Cx,
page_no: PageNumber,
data: PageData,
) -> Result<()> {
self.ensure_writer(cx)?;
self.writes_observed = true;
let staged = StagedPage::from_page_data_for_pool(&self.pool, data)?;
insert_staged_page(
&mut self.write_set,
&mut self.write_pages_sorted,
page_no,
staged,
);
Ok(())
}
fn allocate_page(&mut self, cx: &Cx) -> Result<PageNumber> {
self.ensure_writer(cx)?;
if let Some(page) = self.page_lease.pop() {
self.allocated_from_eof.push(page);
return Ok(page);
}
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
if !self.memory_db_bump_alloc {
let committed_freelist_is_snapshot_pinned =
self.mode == TransactionMode::Concurrent || inner.active_transactions > 1;
if committed_freelist_is_snapshot_pinned {
if let Some(idx) = inner
.freelist
.iter()
.rposition(|page| page.get() > inner.db_size)
{
let page = inner.freelist.remove(idx);
self.allocated_from_freelist.push(page);
return Ok(page);
}
} else if let Some(page) = inner.freelist.pop() {
self.allocated_from_freelist.push(page);
return Ok(page);
}
}
let pending_byte_page = (0x4000_0000 / inner.page_size.get()) + 1;
let already_allocated =
!self.allocated_from_eof.is_empty() || !self.allocated_from_freelist.is_empty();
let batch = if self.mode == TransactionMode::Concurrent && already_allocated {
PAGE_LEASE_BATCH_SIZE
} else {
1
};
let mut first_page: Option<PageNumber> = None;
for _ in 0..batch {
let mut raw = inner.next_page;
if raw == pending_byte_page {
raw = raw.saturating_add(1);
}
let next = raw.saturating_add(1);
if next == raw {
break;
}
inner.next_page = next;
if let Some(page) = PageNumber::new(raw) {
if first_page.is_none() {
first_page = Some(page);
} else {
self.page_lease.push(page);
}
}
}
drop(inner);
let page = first_page.ok_or_else(|| FrankenError::OutOfRange {
what: "allocated page number".to_owned(),
value: "0".to_owned(),
})?;
self.allocated_from_eof.push(page);
Ok(page)
}
fn free_page(&mut self, cx: &Cx, page_no: PageNumber) -> Result<()> {
self.ensure_writer(cx)?;
if page_no == PageNumber::ONE {
return Err(FrankenError::OutOfRange {
what: "free page number".to_owned(),
value: page_no.get().to_string(),
});
}
if !self.contains_freed_page(page_no) {
self.freed_pages.push(page_no);
self.note_freed_page_bound(page_no);
}
if self.write_set.remove(&page_no).is_some() {
remove_page_sorted(&mut self.write_pages_sorted, page_no);
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn commit(&mut self, cx: &Cx) -> Result<()> {
if self.finished {
return Ok(());
}
if !self.is_writer {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
return_pages_to_freelist(&mut inner.freelist, self.page_lease.drain(..));
inner.active_transactions = inner.active_transactions.saturating_sub(1);
let preserve_level =
retained_lock_level_after_txn_exit(inner.active_transactions, inner.writer_active);
let _ = inner.db_file.unlock(cx, preserve_level);
drop(inner);
self.committed = true;
self.finished = true;
self.scratch_arena.reset();
return Ok(());
}
if self.vfs.is_memory()
&& self.memory_db_bump_alloc
&& !self.retained_memory_overlay_dirty_pages.is_empty()
{
self.materialize_retained_memory_overlay_into_write_set()?;
}
if !self.has_pending_writes() {
if self.writes_observed {
return Err(FrankenError::internal(
"transaction committed with observed writes but empty write_set; \
state was dropped between staging and commit",
));
}
let inner_arc = Arc::clone(&self.inner);
let mut inner = inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
self.restore_uncommitted_allocations_for_clean_commit(&mut inner);
inner.active_transactions = inner.active_transactions.saturating_sub(1);
let notify_writer_idle =
self.mode != TransactionMode::Concurrent && release_single_writer_baton(&mut inner);
let preserve_level =
retained_lock_level_after_txn_exit(inner.active_transactions, inner.writer_active);
let _ = inner.db_file.unlock(cx, preserve_level);
drop(inner);
if notify_writer_idle {
self.writer_idle.notify_one();
}
self.committed = true;
self.finished = true;
self.scratch_arena.reset();
return Ok(());
}
let phase_timing = commit_phase_timing_enabled();
let t_commit_start = phase_timing.then(Instant::now);
let pager_commit_profile_active = pager_commit_profile_enabled();
record_pager_commit_call(pager_commit_profile_active);
let t_pager_phase_a_start = pager_commit_profile_start(pager_commit_profile_active);
let inner_arc = Arc::clone(&self.inner);
let mut inner = inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
let committed_db_size = self.committed_db_size_with_inner(&inner);
let mut pending_returned_pages = self.drain_unstaged_allocated_pages();
pending_returned_pages.append(&mut self.page_lease);
let mut pending_free_pages = pending_returned_pages.clone();
pending_free_pages.extend(self.freed_pages.iter().copied());
let pending_freed: Vec<PageNumber>;
let cross_process_conflict_pages: Vec<PageNumber>;
{
let freelist_dirty = self.freelist_metadata_dirty_with_pending_free_pages(
&inner,
committed_db_size,
&pending_free_pages,
);
let wal_page1_plan = self.classify_wal_page_one_write(inner.db_size, freelist_dirty);
pending_freed = self.freed_pages.drain(..).collect();
self.freed_page_bounds = None;
if freelist_dirty {
if let Err(e) = serialize_freelist_to_write_set(
cx,
&mut inner,
&self.cache,
&self.wal_backend,
&self.pool,
&mut self.write_set,
&mut self.write_pages_sorted,
committed_db_size,
&pending_free_pages,
) {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return Err(e);
}
}
let must_write_page1 = if self.journal_mode == JournalMode::Wal {
wal_page1_plan.requires_page_one_rewrite()
|| wal_page1_plan.requires_page_count_advance()
} else {
true
};
if must_write_page1 {
let mut page1 = match ensure_page_one_in_write_set(
cx,
&mut inner,
&self.cache,
&self.wal_backend,
&self.pool,
&mut self.write_set,
) {
Ok(p) => p,
Err(e) => {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return Err(e);
}
};
let page1_bytes = page1.as_page_bytes_mut();
if page1_bytes.len() >= DATABASE_HEADER_SIZE {
let mut page_count_bytes = [0_u8; 4];
page_count_bytes.copy_from_slice(&page1_bytes[28..32]);
let existing_page_count = u32::from_be_bytes(page_count_bytes);
let new_change_counter = inner.commit_seq.get().wrapping_add(1) as u32;
page1_bytes[24..28].copy_from_slice(&new_change_counter.to_be_bytes());
if self.journal_mode != JournalMode::Wal
|| wal_page1_plan.requires_page_count_advance()
{
let new_db_size = committed_db_size.max(existing_page_count);
page1_bytes[28..32].copy_from_slice(&new_db_size.to_be_bytes());
}
page1_bytes[92..96].copy_from_slice(&new_change_counter.to_be_bytes());
}
insert_staged_page(
&mut self.write_set,
&mut self.write_pages_sorted,
PageNumber::ONE,
page1,
);
}
cross_process_conflict_pages = if self.journal_mode == JournalMode::Wal {
self.predicted_conflict_pages_for_wal_commit_with_inner(
&inner,
wal_page1_plan,
&pending_free_pages,
)
} else {
Vec::new()
};
}
let wal_current_db_size = inner.db_size;
let wal_sync_policy = inner.wal_commit_sync_policy;
let t_phase_a_done = phase_timing.then(Instant::now);
record_pager_commit_duration(&PAGER_COMMIT_PHASE_A_TIME_NS, t_pager_phase_a_start);
let commit_result = if self.journal_mode == JournalMode::Wal {
drop(inner);
let t_wal_commit_start = pager_commit_profile_start(pager_commit_profile_active);
let result = Self::commit_wal_group_commit_with_snapshot(
cx,
&self.wal_backend,
&self.inner,
wal_current_db_size,
wal_sync_policy,
&self.write_set,
&self.write_pages_sorted,
&cross_process_conflict_pages,
&self.group_commit_queue,
);
record_pager_commit_duration(&PAGER_COMMIT_WAL_TIME_NS, t_wal_commit_start);
inner = match inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))
{
Ok(guard) => guard,
Err(e) => {
self.restore_pending_freed_pages(pending_freed);
if let Ok(mut recovery_inner) = inner_arc.lock() {
return_pages_to_freelist(
&mut recovery_inner.freelist,
pending_returned_pages,
);
}
return Err(e);
}
};
result
} else if self.memory_db_bump_alloc {
let t_memory_flush_start = pager_commit_profile_start(pager_commit_profile_active);
let result = Self::flush_write_set_to_db_file_batch(
cx,
&mut inner,
&self.write_set,
&self.write_pages_sorted,
);
record_pager_commit_duration(&PAGER_COMMIT_MEMORY_FLUSH_TIME_NS, t_memory_flush_start);
result
} else {
let t_journal_commit_start = pager_commit_profile_start(pager_commit_profile_active);
let result = Self::commit_journal(
cx,
&self.vfs,
&self.journal_path,
&mut inner,
&self.write_set,
self.original_db_size,
);
record_pager_commit_duration(&PAGER_COMMIT_JOURNAL_TIME_NS, t_journal_commit_start);
result
};
let t_phase_b_done = phase_timing.then(Instant::now);
if commit_result.is_ok() {
let t_phase_c_metadata_start = pager_commit_profile_start(pager_commit_profile_active);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return_pages_to_freelist(&mut inner.freelist, pending_freed);
inner.db_size = inner.db_size.max(committed_db_size);
inner.commit_seq = inner.commit_seq.next();
let t_file_size_start = pager_commit_profile_start(pager_commit_profile_active);
if let Ok(file_size) = inner.db_file.file_size(cx) {
inner.committed_db_file_size_bytes = file_size;
}
record_pager_commit_duration(&PAGER_COMMIT_FILE_SIZE_TIME_NS, t_file_size_start);
inner.active_transactions = inner.active_transactions.saturating_sub(1);
let notify_writer_idle =
self.mode != TransactionMode::Concurrent && release_single_writer_baton(&mut inner);
let publish_update = PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
};
let single_connection_fast_path = self.single_connection_fast_path_enabled();
let metadata_only_single_connection_fast_path = single_connection_fast_path;
self.publish_committed_snapshot_from_inner(&inner);
let preserve_level =
retained_lock_level_after_txn_exit(inner.active_transactions, inner.writer_active);
let t_unlock_start = pager_commit_profile_start(pager_commit_profile_active);
let _ = inner.db_file.unlock(cx, preserve_level);
record_pager_commit_duration(&PAGER_COMMIT_UNLOCK_TIME_NS, t_unlock_start);
drop(inner);
if notify_writer_idle {
self.writer_idle.notify_one();
}
record_pager_commit_duration(
&PAGER_COMMIT_PHASE_C_METADATA_TIME_NS,
t_phase_c_metadata_start,
);
let t_phase_c1_done = phase_timing.then(Instant::now);
#[cfg(any(test, feature = "fault-injection"))]
crate::fault_hooks::maybe_inject_during_phase_c(
publish_update.visible_commit_seq.get(),
publish_update.db_size,
)?;
let t_publish_start = pager_commit_profile_start(pager_commit_profile_active);
if metadata_only_single_connection_fast_path {
self.publish_single_connection_metadata_only(cx, publish_update);
} else {
self.publish_committed_state(cx, publish_update);
}
record_pager_commit_duration(&PAGER_COMMIT_PUBLISH_TIME_NS, t_publish_start);
self.published_visible_commit_seq
.set(publish_update.visible_commit_seq);
self.published_db_size.set(publish_update.db_size);
let t_phase_c2_done = phase_timing.then(Instant::now);
if self.journal_mode == JournalMode::Wal
&& let (
Some(t_commit_start),
Some(t_phase_a_done),
Some(t_phase_b_done),
Some(t_phase_c1_done),
Some(t_phase_c2_done),
) = (
t_commit_start,
t_phase_a_done,
t_phase_b_done,
t_phase_c1_done,
t_phase_c2_done,
)
{
let phase_a_us = t_phase_a_done.duration_since(t_commit_start).as_micros() as u64;
let phase_b_us = t_phase_b_done.duration_since(t_phase_a_done).as_micros() as u64;
let phase_c1_us = t_phase_c1_done.duration_since(t_phase_b_done).as_micros() as u64;
let phase_c2_us =
t_phase_c2_done.duration_since(t_phase_c1_done).as_micros() as u64;
GLOBAL_CONSOLIDATION_METRICS.record_commit_phases(
phase_a_us,
phase_b_us,
phase_c1_us,
phase_c2_us,
);
}
let t_cache_finish_start = pager_commit_profile_start(pager_commit_profile_active);
if publish_update.journal_mode == JournalMode::Wal
&& !metadata_only_single_connection_fast_path
{
self.discard_committed_pages();
} else if metadata_only_single_connection_fast_path {
self.drain_committed_cache_pages_into_cache();
} else {
let committed_cache_pages = self.drain_committed_cache_pages();
if !committed_cache_pages.is_empty() {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner.commit_seq == publish_update.visible_commit_seq {
for (page_no, buf) in committed_cache_pages {
self.cache.insert_buffer(page_no, buf);
}
}
}
}
self.retained_memory_overlay_dirty_pages.clear();
self.committed = true;
self.finished = true;
self.scratch_arena.reset();
record_pager_commit_duration(&PAGER_COMMIT_CACHE_FINISH_TIME_NS, t_cache_finish_start);
} else {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
drop(inner);
}
commit_result
}
fn commit_and_retain(&mut self, cx: &Cx) -> Result<bool> {
if !self.vfs.is_memory() {
self.commit(cx)?;
return Ok(false);
}
if !self.is_writer || !self.has_pending_writes() {
self.commit(cx)?;
return Ok(false);
}
let inner_arc = Arc::clone(&self.inner);
let mut inner = inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
let mut committed_db_size = self.committed_db_size_with_inner(&inner);
let mut pending_free_pages_for_retain = self.pending_free_pages_for_commit();
let mut freelist_dirty_for_retain = self.freelist_metadata_dirty_with_pending_free_pages(
&inner,
committed_db_size,
&pending_free_pages_for_retain,
);
let mut single_connection_fast_path = self.single_connection_fast_path_enabled();
let mut metadata_only_single_connection_fast_path = single_connection_fast_path
&& !freelist_dirty_for_retain
&& !self.write_set.contains_key(&PageNumber::ONE);
let mut defer_private_memory_flush =
self.memory_db_bump_alloc && metadata_only_single_connection_fast_path;
if self.vfs.is_memory()
&& self.memory_db_bump_alloc
&& !defer_private_memory_flush
&& !self.retained_memory_overlay_dirty_pages.is_empty()
{
drop(inner);
self.materialize_retained_memory_overlay_into_write_set()?;
inner = inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
committed_db_size = self.committed_db_size_with_inner(&inner);
pending_free_pages_for_retain = self.pending_free_pages_for_commit();
freelist_dirty_for_retain = self.freelist_metadata_dirty_with_pending_free_pages(
&inner,
committed_db_size,
&pending_free_pages_for_retain,
);
single_connection_fast_path = self.single_connection_fast_path_enabled();
metadata_only_single_connection_fast_path = single_connection_fast_path
&& !freelist_dirty_for_retain
&& !self.write_set.contains_key(&PageNumber::ONE);
defer_private_memory_flush =
self.memory_db_bump_alloc && metadata_only_single_connection_fast_path;
}
let mut pending_returned_pages = self.drain_unstaged_allocated_pages();
pending_returned_pages.append(&mut self.page_lease);
let mut pending_free_pages = pending_returned_pages.clone();
pending_free_pages.extend(self.freed_pages.iter().copied());
let pending_freed: Vec<PageNumber> = self.freed_pages.drain(..).collect();
self.freed_page_bounds = None;
let commit_result = {
let freelist_dirty = freelist_dirty_for_retain;
let wal_page1_plan = self.classify_wal_page_one_write(inner.db_size, freelist_dirty);
if freelist_dirty {
if let Err(e) = serialize_freelist_to_write_set(
cx,
&mut inner,
&self.cache,
&self.wal_backend,
&self.pool,
&mut self.write_set,
&mut self.write_pages_sorted,
committed_db_size,
&pending_free_pages,
) {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return Err(e);
}
}
let must_write_page1 = if self.journal_mode == JournalMode::Wal {
wal_page1_plan.requires_page_one_rewrite()
|| wal_page1_plan.requires_page_count_advance()
} else if self.vfs.is_memory() {
freelist_dirty || self.write_set.contains_key(&PageNumber::ONE)
} else {
true
};
if must_write_page1 {
let mut page1 = match ensure_page_one_in_write_set(
cx,
&mut inner,
&self.cache,
&self.wal_backend,
&self.pool,
&mut self.write_set,
) {
Ok(p) => p,
Err(e) => {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return Err(e);
}
};
let page1_bytes = page1.as_page_bytes_mut();
if page1_bytes.len() >= DATABASE_HEADER_SIZE {
let mut page_count_bytes = [0_u8; 4];
page_count_bytes.copy_from_slice(&page1_bytes[28..32]);
let existing_page_count = u32::from_be_bytes(page_count_bytes);
let new_change_counter = inner.commit_seq.get().wrapping_add(1) as u32;
page1_bytes[24..28].copy_from_slice(&new_change_counter.to_be_bytes());
if self.journal_mode != JournalMode::Wal
|| wal_page1_plan.requires_page_count_advance()
{
let new_db_size = committed_db_size.max(existing_page_count);
page1_bytes[28..32].copy_from_slice(&new_db_size.to_be_bytes());
}
page1_bytes[92..96].copy_from_slice(&new_change_counter.to_be_bytes());
}
insert_staged_page(
&mut self.write_set,
&mut self.write_pages_sorted,
PageNumber::ONE,
page1,
);
}
let cross_process_conflict_pages = if self.journal_mode == JournalMode::Wal {
self.predicted_conflict_pages_for_wal_commit_with_inner(
&inner,
wal_page1_plan,
&pending_free_pages,
)
} else {
Vec::new()
};
let wal_current_db_size = inner.db_size;
let wal_sync_policy = inner.wal_commit_sync_policy;
if self.journal_mode == JournalMode::Wal {
drop(inner);
let result = Self::commit_wal_group_commit_with_snapshot(
cx,
&self.wal_backend,
&self.inner,
wal_current_db_size,
wal_sync_policy,
&self.write_set,
&self.write_pages_sorted,
&cross_process_conflict_pages,
&self.group_commit_queue,
);
inner = match inner_arc
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))
{
Ok(guard) => guard,
Err(e) => {
self.restore_pending_freed_pages(pending_freed);
if let Ok(mut recovery_inner) = inner_arc.lock() {
return_pages_to_freelist(
&mut recovery_inner.freelist,
pending_returned_pages,
);
}
return Err(e);
}
};
result
} else if self.vfs.is_memory() {
if defer_private_memory_flush {
} else {
if let Err(e) = Self::flush_write_set_to_db_file_batch(
cx,
&mut inner,
&self.write_set,
&self.write_pages_sorted,
) {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return Err(e);
}
}
Ok(())
} else {
Self::commit_journal(
cx,
&self.vfs,
&self.journal_path,
&mut inner,
&self.write_set,
self.original_db_size,
)
}
};
if commit_result.is_ok() {
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
return_pages_to_freelist(&mut inner.freelist, pending_freed);
inner.db_size = inner.db_size.max(committed_db_size);
inner.commit_seq = inner.commit_seq.next();
if self.vfs.is_memory() {
inner.committed_db_file_size_bytes =
u64::from(inner.db_size) * u64::from(inner.page_size.get());
} else if let Ok(file_size) = inner.db_file.file_size(cx) {
inner.committed_db_file_size_bytes = file_size;
}
let publish_update = PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
};
self.publish_committed_snapshot_from_inner(&inner);
drop(inner);
if metadata_only_single_connection_fast_path {
if defer_private_memory_flush {
self.note_retained_memory_overlay_from_write_set();
} else {
self.retained_memory_overlay_dirty_pages.clear();
}
self.publish_single_connection_metadata_only(cx, publish_update);
self.retain_committed_pages_in_txn_read_cache();
} else {
self.retained_memory_overlay_dirty_pages.clear();
self.publish_committed_state_draining_write_set(cx, publish_update);
}
self.write_pages_sorted.clear();
self.clear_freed_pages();
self.allocated_from_freelist.clear();
self.allocated_from_eof.clear();
self.savepoint_stack.clear();
self.rolled_back_pages.clear();
self.writes_observed = false;
if !metadata_only_single_connection_fast_path {
self.txn_read_cache.borrow_mut().clear();
}
self.scratch_arena.reset();
self.original_db_size = committed_db_size;
self.published_visible_commit_seq
.set(publish_update.visible_commit_seq);
self.published_db_size.set(publish_update.db_size);
Ok(true)
} else {
self.restore_pending_freed_pages(pending_freed);
return_pages_to_freelist(&mut inner.freelist, pending_returned_pages);
drop(inner);
commit_result?;
unreachable!()
}
}
fn is_writer(&self) -> bool {
self.is_writer
}
fn has_pending_writes(&self) -> bool {
!self.write_set.is_empty() || self.freelist_metadata_dirty()
}
fn published_visible_commit_seq_hint(&self) -> Option<CommitSeq> {
Some(self.published_visible_commit_seq.get())
}
fn pending_commit_pages(&self) -> Result<Vec<PageNumber>> {
if !self.has_pending_writes() {
return Ok(Vec::new());
}
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Ok(self.predicted_commit_pages_with_inner(&inner))
}
fn pending_conflict_pages(&self) -> Result<Vec<PageNumber>> {
if !self.has_pending_writes() {
return Ok(Vec::new());
}
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Ok(self.predicted_conflict_pages_with_inner(&inner))
}
fn write_set_page_numbers(&self) -> Vec<PageNumber> {
self.write_pages_sorted.clone()
}
fn page_size(&self) -> PageSize {
PageSize::new(u32::try_from(self.pool.page_size()).expect("pool page size fits u32"))
.expect("pool page size invariant")
}
fn page_one_in_pending_commit_surface(&self) -> Result<bool> {
if !self.has_pending_writes() {
return Ok(false);
}
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Ok(self.page_one_in_pending_commit_surface_with_inner(&inner))
}
fn allocate_page_requires_page_one_conflict_tracking(&self) -> Result<bool> {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Ok(self.allocate_page_requires_page_one_conflict_tracking_with_inner(&inner))
}
fn free_page_requires_page_one_conflict_tracking(&self, page_no: PageNumber) -> Result<bool> {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Ok(self.free_page_requires_page_one_conflict_tracking_with_inner(&inner, page_no))
}
fn write_page_requires_page_one_conflict_tracking(&self, page_no: PageNumber) -> Result<bool> {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Ok(self.write_page_requires_page_one_conflict_tracking_with_inner(&inner, page_no))
}
fn rollback(&mut self, cx: &Cx) -> Result<()> {
if self.finished {
return Ok(());
}
if self.vfs.is_memory()
&& self.memory_db_bump_alloc
&& !self.retained_memory_overlay_dirty_pages.is_empty()
{
let overlay_pages = self.collect_retained_memory_overlay_pages()?;
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
Self::flush_retained_memory_overlay_pages_to_db_file(
cx,
&mut inner,
self.original_db_size,
&overlay_pages,
)?;
drop(inner);
self.retained_memory_overlay_dirty_pages.clear();
}
self.write_set.clear();
self.write_pages_sorted.clear();
self.clear_freed_pages();
self.savepoint_stack.clear();
self.rolled_back_pages.clear();
self.writes_observed = false;
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
let restored_from_journal = if self.is_writer
&& self.journal_mode != JournalMode::Wal
&& inner.rollback_journal_recovery_state.is_pending()
{
let page_size = inner.page_size;
if !SimplePager::<V>::recover_rollback_journal_if_present(
cx,
&*self.vfs,
&mut inner.db_file,
&self.journal_path,
page_size,
)? {
return Err(FrankenError::internal(
"rollback journal missing while failed commit recovery was pending",
));
}
true
} else {
false
};
let mut notify_writer_idle = false;
if restored_from_journal {
self.cache.clear();
inner.refresh_committed_state(cx, &self.cache, &self.wal_backend)?;
inner.rollback_journal_recovery_state = RollbackJournalRecoveryState::Clean;
self.allocated_from_freelist.clear();
self.allocated_from_eof.clear();
self.page_lease.clear();
if self.mode != TransactionMode::Concurrent {
notify_writer_idle |= release_single_writer_baton(&mut inner);
}
} else {
return_pages_to_freelist(&mut inner.freelist, self.allocated_from_freelist.drain(..));
if self.is_writer && self.mode != TransactionMode::Concurrent {
self.page_lease.clear();
inner.db_size = self.original_db_size;
let db_size = inner.db_size;
inner.next_page = if db_size >= 2 {
db_size.saturating_add(1)
} else {
2
};
notify_writer_idle |= release_single_writer_baton(&mut inner);
} else if self.is_writer && self.mode == TransactionMode::Concurrent {
return_pages_to_freelist(&mut inner.freelist, self.page_lease.drain(..));
return_pages_to_freelist(&mut inner.freelist, self.allocated_from_eof.drain(..));
} else {
self.page_lease.clear();
}
}
inner.active_transactions = inner.active_transactions.saturating_sub(1);
let preserve_level =
retained_lock_level_after_txn_exit(inner.active_transactions, inner.writer_active);
let _ = inner.db_file.unlock(cx, preserve_level);
drop(inner);
if notify_writer_idle {
self.writer_idle.notify_one();
}
if self.is_writer {
let _ = self.vfs.delete(cx, &self.journal_path, true);
}
self.committed = false;
self.finished = true;
self.scratch_arena.reset();
Ok(())
}
fn record_write_witness(&mut self, _cx: &Cx, _key: fsqlite_types::WitnessKey) {}
fn savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
let inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
self.savepoint_stack.push(SavepointEntry {
name: name.to_owned(),
write_set_snapshot: self
.write_set
.iter()
.map(|(&k, v)| (k, v.published_page()))
.collect(),
write_pages_sorted_snapshot: self.write_pages_sorted.clone(),
freed_pages_snapshot: self.freed_pages.clone(),
next_page_snapshot: inner.next_page,
freelist_snapshot: inner.freelist.clone(),
allocated_from_freelist_snapshot: self.allocated_from_freelist.clone(),
allocated_from_eof_snapshot: self.allocated_from_eof.clone(),
});
drop(inner);
Ok(())
}
fn release_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
let pos = self
.savepoint_stack
.iter()
.rposition(|sp| sp.name == name)
.ok_or_else(|| FrankenError::internal(format!("no savepoint named '{name}'")))?;
self.savepoint_stack.truncate(pos);
Ok(())
}
fn rollback_to_savepoint(&mut self, _cx: &Cx, name: &str) -> Result<()> {
let pos = self
.savepoint_stack
.iter()
.rposition(|sp| sp.name == name)
.ok_or_else(|| FrankenError::internal(format!("no savepoint named '{name}'")))?;
let entry = &self.savepoint_stack[pos];
let new_write_set = entry
.write_set_snapshot
.iter()
.map(|(&k, v)| -> Result<(PageNumber, StagedPage)> {
Ok((
k,
StagedPage::from_page_data_for_pool(&self.pool, v.clone())?,
))
})
.collect::<Result<PagePageMap<_>>>()?;
for page_no in self
.allocated_from_eof
.iter()
.skip(entry.allocated_from_eof_snapshot.len())
{
self.rolled_back_pages.insert(*page_no);
}
for page_no in self
.allocated_from_freelist
.iter()
.skip(entry.allocated_from_freelist_snapshot.len())
{
self.rolled_back_pages.insert(*page_no);
}
{
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimpleTransaction lock poisoned"))?;
if self.mode != TransactionMode::Concurrent {
inner.next_page = entry.next_page_snapshot;
inner.freelist.clone_from(&entry.freelist_snapshot);
self.page_lease.clear();
} else {
return_pages_to_freelist(&mut inner.freelist, self.page_lease.drain(..));
return_pages_to_freelist(
&mut inner.freelist,
self.allocated_from_eof
.drain(entry.allocated_from_eof_snapshot.len()..),
);
return_pages_to_freelist(
&mut inner.freelist,
self.allocated_from_freelist
.drain(entry.allocated_from_freelist_snapshot.len()..),
);
}
}
let allocated_from_freelist_snapshot = entry.allocated_from_freelist_snapshot.clone();
let allocated_from_eof_snapshot = entry.allocated_from_eof_snapshot.clone();
let freed_pages_snapshot = entry.freed_pages_snapshot.clone();
let write_pages_sorted_snapshot = entry.write_pages_sorted_snapshot.clone();
let snapshot_was_empty = entry.write_set_snapshot.is_empty();
self.allocated_from_freelist = allocated_from_freelist_snapshot;
self.allocated_from_eof = allocated_from_eof_snapshot;
self.freed_pages = freed_pages_snapshot;
self.refresh_freed_page_bounds();
self.write_set = new_write_set;
self.write_pages_sorted = write_pages_sorted_snapshot;
if snapshot_was_empty {
self.writes_observed = false;
}
self.savepoint_stack.truncate(pos + 1);
Ok(())
}
}
impl<V: Vfs> Drop for SimpleTransaction<V> {
fn drop(&mut self) {
if self.finished {
return;
}
let mut notify_writer_idle = false;
if let Ok(mut inner) = self.inner.lock() {
return_pages_to_freelist(&mut inner.freelist, self.allocated_from_freelist.drain(..));
if self.is_writer && self.mode != TransactionMode::Concurrent {
self.page_lease.clear();
inner.db_size = self.original_db_size;
let db_size = inner.db_size;
inner.next_page = if db_size >= 2 {
db_size.saturating_add(1)
} else {
2
};
notify_writer_idle = release_single_writer_baton(&mut inner);
} else if self.is_writer && self.mode == TransactionMode::Concurrent {
return_pages_to_freelist(&mut inner.freelist, self.page_lease.drain(..));
return_pages_to_freelist(&mut inner.freelist, self.allocated_from_eof.drain(..));
} else {
self.page_lease.clear();
}
inner.active_transactions = inner.active_transactions.saturating_sub(1);
let preserve_level =
retained_lock_level_after_txn_exit(inner.active_transactions, inner.writer_active);
let _mask = self.cleanup_cx.masked();
let _ = inner.db_file.unlock(&self.cleanup_cx, preserve_level);
}
if notify_writer_idle {
self.writer_idle.notify_one();
}
self.finished = true;
}
}
pub struct SimplePagerCheckpointWriter<V: Vfs>
where
V::File: Send + Sync,
{
inner: Arc<Mutex<PagerInner<V::File>>>,
cache: Arc<ShardedPageCache>,
published: Arc<PublishedPagerState>,
}
impl<V: Vfs> traits::sealed::Sealed for SimplePagerCheckpointWriter<V> where V::File: Send + Sync {}
impl<V> SimplePagerCheckpointWriter<V>
where
V: Vfs + Send + Sync,
V::File: Send + Sync,
{
fn patch_page1_header(
inner: &mut PagerInner<V::File>,
cache: &ShardedPageCache,
cx: &Cx,
) -> Result<()> {
if inner.db_size == 0 {
return Ok(());
}
let page_size = inner.page_size.as_usize();
let mut page1 = vec![0u8; page_size];
let bytes_read = inner.db_file.read(cx, &mut page1, 0)?;
if bytes_read < DATABASE_HEADER_SIZE {
return Err(FrankenError::DatabaseCorrupt {
detail: format!(
"short read while patching page 1 header: got {bytes_read} bytes, need at least {DATABASE_HEADER_SIZE}",
),
});
}
let new_page_count = inner.db_size;
let current_change_counter = inner.commit_seq.get() as u32;
page1[24..28].copy_from_slice(¤t_change_counter.to_be_bytes());
page1[28..32].copy_from_slice(&new_page_count.to_be_bytes());
page1[92..96].copy_from_slice(¤t_change_counter.to_be_bytes());
inner.db_file.write(cx, &page1, 0)?;
cache.evict(PageNumber::ONE);
Ok(())
}
}
impl<V> traits::CheckpointPageWriter for SimplePagerCheckpointWriter<V>
where
V: Vfs + Send + Sync,
V::File: Send + Sync,
{
fn write_page(&mut self, cx: &Cx, page_no: PageNumber, data: &[u8]) -> Result<()> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePagerCheckpointWriter lock poisoned"))?;
inner.db_size = inner.db_size.max(page_no.get());
let page_size = inner.page_size.as_usize();
let offset = u64::from(page_no.get() - 1) * page_size as u64;
inner.db_file.write(cx, data, offset)?;
if page_no == PageNumber::ONE && data.len() >= DATABASE_HEADER_SIZE {
Self::patch_page1_header(&mut inner, &self.cache, cx)?;
}
self.cache.evict(page_no);
if page_no == PageNumber::ONE {
self.published.publish_remove_page(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
PageNumber::ONE,
);
}
drop(inner);
Ok(())
}
fn truncate(&mut self, cx: &Cx, n_pages: u32) -> Result<()> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePagerCheckpointWriter lock poisoned"))?;
let old_db_size = inner.db_size;
let page_size = inner.page_size.as_usize();
let target_size = u64::from(n_pages) * page_size as u64;
inner.db_file.truncate(cx, target_size)?;
inner.db_size = n_pages;
for pgno in (n_pages.saturating_add(1))..=old_db_size {
if let Some(page_no) = PageNumber::new(pgno) {
self.cache.evict(page_no);
}
}
self.published.publish_truncate_checkpoint(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
n_pages,
);
drop(inner);
Ok(())
}
fn sync(&mut self, cx: &Cx) -> Result<()> {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePagerCheckpointWriter lock poisoned"))?;
Self::patch_page1_header(&mut inner, &self.cache, cx)?;
self.published.publish_remove_page(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
PageNumber::ONE,
);
inner.db_file.sync(cx, SyncFlags::NORMAL)
}
}
impl<V: Vfs> SimplePager<V>
where
V::File: Send + Sync,
{
#[must_use]
pub fn checkpoint_writer(&self) -> SimplePagerCheckpointWriter<V> {
SimplePagerCheckpointWriter {
inner: Arc::clone(&self.inner),
cache: Arc::clone(&self.cache),
published: Arc::clone(&self.published),
}
}
pub fn checkpoint(
&self,
cx: &Cx,
mode: traits::CheckpointMode,
) -> Result<traits::CheckpointResult> {
let cleanup_cx = cleanup_child_cx(cx);
let checkpoint_gate_state;
let wal = {
let mut inner = self
.inner
.lock()
.map_err(|_| FrankenError::internal("SimplePager lock poisoned"))?;
if inner.journal_mode != JournalMode::Wal {
return Err(FrankenError::Unsupported);
}
if inner.checkpoint_active {
let active_transactions = inner.active_transactions;
let checkpoint_active = inner.checkpoint_active;
drop(inner);
log_checkpoint_coordination(
cx,
&pager_group_commit_queue(self),
"active_gate",
"checkpoint_begin",
"return_busy",
"checkpoint_already_owns_serialized_backend",
true,
active_transactions,
checkpoint_active,
);
return Err(FrankenError::Busy);
}
if inner.active_transactions > 0 {
let active_transactions = inner.active_transactions;
let checkpoint_active = inner.checkpoint_active;
drop(inner);
log_checkpoint_coordination(
cx,
&pager_group_commit_queue(self),
"writer_gate",
"checkpoint_begin",
"return_busy",
"checkpoint_waits_for_foreground_writers_to_quiesce",
true,
active_transactions,
checkpoint_active,
);
return Err(FrankenError::Busy);
}
let wal_is_empty =
with_wal_backend_read(&self.wal_backend, |wal| Ok(wal.frame_count())).unwrap_or(0)
== 0;
if wal_is_empty {
drop(inner);
return Ok(traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: mode,
effective_mode: mode,
});
}
inner.checkpoint_active = true;
checkpoint_gate_state = (inner.active_transactions, inner.checkpoint_active);
let mut wal_guard = self
.wal_backend
.write()
.map_err(|_| FrankenError::internal("SharedWalBackend lock poisoned"))?;
let Some(wal) = wal_guard.take() else {
inner.checkpoint_active = false;
return Err(FrankenError::internal(
"WAL mode active but no WAL backend installed",
));
};
self.published.publish_metadata_only(
cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
);
wal
};
log_checkpoint_coordination(
cx,
&pager_group_commit_queue(self),
"backend_owned",
"foreground_idle",
"checkpoint_runs",
"checkpoint_only_enters_after_group_commit_lane_quiesces",
true,
checkpoint_gate_state.0,
checkpoint_gate_state.1,
);
struct CheckpointGuard<'a, F: VfsFile> {
inner: &'a std::sync::Mutex<PagerInner<F>>,
published: &'a PublishedPagerState,
wal_backend: &'a SharedWalBackend,
wal: Option<Box<dyn WalBackend>>,
cleanup_cx: Cx,
}
impl<F: VfsFile> Drop for CheckpointGuard<'_, F> {
fn drop(&mut self) {
if let Ok(mut inner) = self.inner.lock() {
if let Some(wal) = self.wal.take() {
if let Ok(mut wal_guard) = self.wal_backend.write() {
*wal_guard = Some(wal);
}
}
inner.checkpoint_active = false;
let _mask = self.cleanup_cx.masked();
self.published.publish_metadata_only(
&self.cleanup_cx,
PublishedPagerUpdate {
visible_commit_seq: inner.commit_seq,
db_size: inner.db_size,
journal_mode: inner.journal_mode,
freelist_count: inner.freelist.len(),
checkpoint_active: inner.checkpoint_active,
},
);
}
}
}
let mut guard = CheckpointGuard {
inner: &self.inner,
published: self.published.as_ref(),
wal_backend: &self.wal_backend,
wal: Some(wal),
cleanup_cx,
};
let mut writer = self.checkpoint_writer();
let sole_connection = self
.shared_connection_count
.get()
.is_some_and(|counter| counter.load(std::sync::atomic::Ordering::Acquire) == 1);
let effective_mode = match mode {
traits::CheckpointMode::Restart | traits::CheckpointMode::Truncate
if !sole_connection =>
{
tracing::debug!(
requested_mode = ?mode,
"downgrading checkpoint mode because pager has multiple connections or lacks reader-tracking for safe WAL reset"
);
traits::CheckpointMode::Full
}
_ => mode,
};
let checkpoint_result = guard
.wal
.as_mut()
.expect("wal was just inserted")
.checkpoint(cx, effective_mode, &mut writer, 0, None);
if let Err(error) = &checkpoint_result {
let queue_snapshot =
checkpoint_coordination_queue_snapshot(&pager_group_commit_queue(self));
tracing::warn!(
target: "fsqlite::wal::checkpoint_coordination",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_CHECKPOINT_RUN_ID,
scenario_id = PHYSICAL_WRITER_CHECKPOINT_SCENARIO_ID,
checkpoint_phase = "error",
foreground_phase = "foreground_idle",
foreground_action = "checkpoint_failed",
interaction_rule = "checkpoint_keeps_foreground_writer_lane_decoupled_even_on_error",
stall_avoided = true,
active_transactions = checkpoint_gate_state.0,
checkpoint_active = checkpoint_gate_state.1,
queue_phase = queue_snapshot.queue_phase,
queue_epoch = queue_snapshot.queue_epoch,
pending_batch_count = queue_snapshot.pending_batch_count,
requested_mode = ?mode,
effective_mode = ?effective_mode,
failure_context = %error,
"checkpoint failed after acquiring exclusive checkpoint ownership"
);
}
let mut result = checkpoint_result?;
result.requested_mode = mode;
result.effective_mode = effective_mode;
let queue_snapshot =
checkpoint_coordination_queue_snapshot(&pager_group_commit_queue(self));
tracing::debug!(
target: "fsqlite::wal::checkpoint_coordination",
trace_id = cx.trace_id(),
run_id = PHYSICAL_WRITER_CHECKPOINT_RUN_ID,
scenario_id = PHYSICAL_WRITER_CHECKPOINT_SCENARIO_ID,
checkpoint_phase = "complete",
foreground_phase = "foreground_idle",
foreground_action = "checkpoint_complete",
interaction_rule = "checkpoint_keeps_foreground_writer_lane_idle_until_release",
stall_avoided = true,
active_transactions = checkpoint_gate_state.0,
checkpoint_active = checkpoint_gate_state.1,
queue_phase = queue_snapshot.queue_phase,
queue_epoch = queue_snapshot.queue_epoch,
pending_batch_count = queue_snapshot.pending_batch_count,
requested_mode = ?result.requested_mode,
effective_mode = ?result.effective_mode,
total_frames = result.total_frames,
frames_backfilled = result.frames_backfilled,
completed = result.completed,
wal_was_reset = result.wal_was_reset,
"checkpoint completed without re-entering the foreground physical writer lane"
);
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::{MvccPager, TransactionHandle, TransactionMode};
use fsqlite_types::PageSize;
use fsqlite_types::flags::{AccessFlags, SyncFlags, VfsOpenFlags};
use fsqlite_types::{BTreePageHeader, DatabaseHeader};
use fsqlite_vfs::{MemoryFile, MemoryVfs, Vfs, VfsFile};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex, OnceLock};
static FAULT_HOOK_TEST_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
const BEAD_ID: &str = "bd-bca.1";
const DB300_E3_3_BEAD_ID: &str = "bd-db300.5.3.3";
const DB300_E3_3_A_BEAD_ID: &str = "bd-db300.5.3.3.1";
const TRACK_U_BEAD_ID: &str = "bd-c9pxw";
const CHECKPOINT_DECOUPLING_BEAD_ID: &str = "bd-1dp9.6.7.9.2";
const COMMIT_SERVICE_POLICY_BEAD_ID: &str = "bd-1dp9.6.7.9.4";
type ObservedLockLevel = Arc<Mutex<LockLevel>>;
type ObservedUnlockTraceIds = Arc<Mutex<Vec<u64>>>;
type ObservedCleanupUnlockHarness = (
SimplePager<ObservedLockVfs>,
ObservedLockLevel,
ObservedUnlockTraceIds,
);
fn init_publication_test_tracing() {
static TRACING_INIT: OnceLock<()> = OnceLock::new();
TRACING_INIT.get_or_init(|| {
if tracing_subscriber::fmt()
.with_ansi(false)
.with_max_level(tracing::Level::TRACE)
.with_test_writer()
.try_init()
.is_err()
{
}
});
}
fn test_pager() -> (SimplePager<MemoryVfs>, PathBuf) {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/test.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
(pager, path)
}
fn sample_page(seed: u8) -> Vec<u8> {
let page_size = PageSize::DEFAULT.as_usize();
let mut page = vec![0u8; page_size];
for (i, byte) in page.iter_mut().enumerate() {
let reduced = u8::try_from(i % 251).expect("modulo fits u8");
*byte = reduced ^ seed;
}
page
}
#[test]
fn shared_file_state_key_matches_relative_and_absolute_spellings() {
let rel = Path::new("__fsqlite_nonexistent_shared_state_key_probe__.db");
let abs = std::env::current_dir().unwrap().join(rel);
assert_eq!(shared_file_state_key(rel), shared_file_state_key(&abs));
}
#[test]
fn shared_file_state_key_normalizes_fallback_dot_components() {
let probe = Path::new("__fsqlite_nonexistent_shared_state_key_probe__.db");
let dotted = Path::new(".").join(probe);
let parent = Path::new("__fsqlite_unused_probe_parent__")
.join("..")
.join(probe);
assert_eq!(shared_file_state_key(probe), shared_file_state_key(&dotted));
assert_eq!(shared_file_state_key(probe), shared_file_state_key(&parent));
}
#[test]
fn shared_file_state_key_matches_before_and_after_file_creation() {
let dir = tempfile::tempdir().unwrap();
let canonical_path = dir.path().join("coordination.db");
let dotted_path = dir.path().join(".").join("coordination.db");
let before_create_key = shared_file_state_key(&dotted_path);
std::fs::write(&canonical_path, b"").unwrap();
let after_create_key = shared_file_state_key(&dotted_path);
assert_eq!(
before_create_key, after_create_key,
"shared state keys must stay stable when the db file is created after first open"
);
}
#[cfg(all(feature = "native", unix))]
#[test]
fn open_without_rollback_journal_skips_recovery_fence_convoy() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("clean-open.db");
let cx = Cx::new();
{
let _pager = SimplePager::open_with_cx(
&cx,
fsqlite_vfs::UnixVfs::new(),
&path,
PageSize::DEFAULT,
)
.expect("create clean database");
}
let journal_path = SimplePager::<fsqlite_vfs::UnixVfs>::journal_path(&path);
assert!(
!fsqlite_vfs::UnixVfs::new()
.access(&cx, &journal_path, AccessFlags::EXISTS)
.expect("journal existence probe"),
"test precondition: clean database must not have a rollback journal"
);
let fence = recovery_fence_for_path(&path);
let held = fence
.try_acquire_for_recovery()
.expect("hold recovery fence to simulate unrelated opener recovery");
let started = std::time::Instant::now();
let _reopened =
SimplePager::open_with_cx(&cx, fsqlite_vfs::UnixVfs::new(), &path, PageSize::DEFAULT)
.expect("clean open should not wait behind recovery fence");
let elapsed = started.elapsed();
drop(held);
assert!(
elapsed < Duration::from_millis(100),
"clean shared-file open should skip the recovery fence when no rollback journal exists \
(elapsed {elapsed:?})"
);
}
fn default_commit_service_fairness_budget(max_wait: Duration) -> Duration {
commit_service_fairness_budget(&ParallelWalControlSurface::default(), max_wait)
}
fn track_u_log_counts(
case: &str,
dirty_set_count: usize,
already_dirty_skip_count: usize,
commit_flush_count: usize,
) {
eprintln!(
"INFO bead_id={TRACK_U_BEAD_ID} case={case} dirty_set_count={dirty_set_count} \
already_dirty_skip_count={already_dirty_skip_count} \
commit_flush_count={commit_flush_count}"
);
}
fn private_memory_pager() -> SimplePager<MemoryVfs> {
SimplePager::open(MemoryVfs::new(), Path::new("/:memory:"), PageSize::DEFAULT).unwrap()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct FlushBusyRetryScheduleSummary {
attempts: u32,
total_spin_loops: u64,
max_spin_loops: u32,
yield_count: u32,
}
fn summarize_flush_busy_retry_schedule(attempts: u32) -> FlushBusyRetryScheduleSummary {
assert!(
attempts > 0,
"flush busy retry validation needs at least one attempt"
);
let waits = (1..=attempts)
.map(flush_busy_retry_wait)
.collect::<Vec<_>>();
let total_spin_loops = waits.iter().map(|wait| u64::from(wait.spin_loops)).sum();
let max_spin_loops = waits
.iter()
.map(|wait| wait.spin_loops)
.max()
.unwrap_or_default();
let yield_count = u32::try_from(waits.iter().filter(|wait| wait.yielded).count())
.expect("flush busy retry validation should fit within u32 attempt counts");
FlushBusyRetryScheduleSummary {
attempts,
total_spin_loops,
max_spin_loops,
yield_count,
}
}
#[test]
fn test_flush_busy_retry_yield_cadence_is_bounded() {
for attempt in 1..FLUSH_BUSY_HANDOFF_YIELD_EVERY {
assert!(
!flush_busy_retry_should_yield(attempt),
"attempt {attempt} should stay on-CPU"
);
}
assert!(flush_busy_retry_should_yield(
FLUSH_BUSY_HANDOFF_YIELD_EVERY
));
assert!(!flush_busy_retry_should_yield(
FLUSH_BUSY_HANDOFF_YIELD_EVERY + 1
));
assert!(flush_busy_retry_should_yield(
FLUSH_BUSY_HANDOFF_YIELD_EVERY * 2
));
}
#[test]
fn test_flush_busy_retry_schedule_summary_bounds_tail_budget() {
let summary = summarize_flush_busy_retry_schedule(10);
assert_eq!(summary.attempts, 10);
assert_eq!(summary.total_spin_loops, 12_224);
assert_eq!(summary.max_spin_loops, FLUSH_BUSY_HANDOFF_MAX_SPINS);
assert_eq!(
summary.yield_count,
10 / FLUSH_BUSY_HANDOFF_YIELD_EVERY,
"wake amplification must stay at one scheduler yield per bounded retry window"
);
}
#[derive(Debug, Clone, Copy)]
struct ReadSurfaceSnapshot {
cache: PageCacheMetricsSnapshot,
published_hits: u64,
}
fn read_surface_snapshot<V>(pager: &SimplePager<V>) -> ReadSurfaceSnapshot
where
V: Vfs + Send + Sync,
V::File: Send + Sync,
{
ReadSurfaceSnapshot {
cache: pager.cache_metrics_snapshot().unwrap(),
published_hits: pager.published_page_hits(),
}
}
fn observed_read_total(before: ReadSurfaceSnapshot, after: ReadSurfaceSnapshot) -> u64 {
after
.cache
.total_accesses()
.saturating_sub(before.cache.total_accesses())
.saturating_add(after.published_hits.saturating_sub(before.published_hits))
}
fn observed_read_hit_rate_percent(
before: ReadSurfaceSnapshot,
after: ReadSurfaceSnapshot,
) -> f64 {
let cache_hits = after.cache.hits.saturating_sub(before.cache.hits);
let published_hits = after.published_hits.saturating_sub(before.published_hits);
let total_reads = observed_read_total(before, after);
if total_reads == 0 {
0.0
} else {
(cache_hits.saturating_add(published_hits) as f64 * 100.0) / total_reads as f64
}
}
struct DropAwareWalBackend {
dropped: Arc<Mutex<bool>>,
}
impl Drop for DropAwareWalBackend {
fn drop(&mut self) {
*self
.dropped
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = true;
}
}
impl crate::traits::WalBackend for DropAwareWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> Result<()> {
Ok(())
}
fn read_page(&mut self, _cx: &Cx, _page_number: u32) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn sync(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
0
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: false,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
#[derive(Clone, Default)]
struct JournalDeleteFailVfs {
inner: MemoryVfs,
}
impl JournalDeleteFailVfs {
fn new() -> Self {
Self {
inner: MemoryVfs::new(),
}
}
}
impl Vfs for JournalDeleteFailVfs {
type File = MemoryFile;
fn name(&self) -> &'static str {
self.inner.name()
}
fn open(
&self,
cx: &Cx,
path: Option<&std::path::Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
self.inner.open(cx, path, flags)
}
fn delete(&self, cx: &Cx, path: &std::path::Path, sync_dir: bool) -> Result<()> {
if path.to_string_lossy().ends_with("-journal") {
return Err(FrankenError::internal(
"simulated journal delete failure".to_owned(),
));
}
self.inner.delete(cx, path, sync_dir)
}
fn access(&self, cx: &Cx, path: &std::path::Path, flags: AccessFlags) -> Result<bool> {
self.inner.access(cx, path, flags)
}
fn full_pathname(&self, cx: &Cx, path: &std::path::Path) -> Result<PathBuf> {
self.inner.full_pathname(cx, path)
}
}
#[derive(Clone, Default)]
struct WalReadonlyFallbackProbeVfs {
inner: MemoryVfs,
readonly_wal_open_attempted: Arc<AtomicBool>,
}
impl WalReadonlyFallbackProbeVfs {
fn new() -> Self {
Self {
inner: MemoryVfs::new(),
readonly_wal_open_attempted: Arc::new(AtomicBool::new(false)),
}
}
fn readonly_wal_open_attempted(&self) -> bool {
self.readonly_wal_open_attempted
.load(AtomicOrdering::Relaxed)
}
}
impl Vfs for WalReadonlyFallbackProbeVfs {
type File = MemoryFile;
fn name(&self) -> &'static str {
self.inner.name()
}
fn open(
&self,
cx: &Cx,
path: Option<&std::path::Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
let is_wal = flags.contains(VfsOpenFlags::WAL);
if is_wal && flags.contains(VfsOpenFlags::READONLY) {
self.readonly_wal_open_attempted
.store(true, AtomicOrdering::Relaxed);
}
if is_wal && flags.contains(VfsOpenFlags::READWRITE) {
return Err(FrankenError::CannotOpen {
path: path
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("<wal-probe>")),
});
}
self.inner.open(cx, path, flags)
}
fn delete(&self, cx: &Cx, path: &std::path::Path, sync_dir: bool) -> Result<()> {
self.inner.delete(cx, path, sync_dir)
}
fn access(&self, cx: &Cx, path: &std::path::Path, flags: AccessFlags) -> Result<bool> {
self.inner.access(cx, path, flags)
}
fn full_pathname(&self, cx: &Cx, path: &std::path::Path) -> Result<PathBuf> {
self.inner.full_pathname(cx, path)
}
}
#[derive(Clone)]
struct ObservedLockVfs {
inner: MemoryVfs,
observed_lock_level: ObservedLockLevel,
observed_unlock_trace_ids: ObservedUnlockTraceIds,
fail_unlock_on_checkpoint_error: bool,
}
impl ObservedLockVfs {
fn new() -> Self {
Self {
inner: MemoryVfs::new(),
observed_lock_level: Arc::new(Mutex::new(LockLevel::None)),
observed_unlock_trace_ids: Arc::new(Mutex::new(Vec::new())),
fail_unlock_on_checkpoint_error: false,
}
}
fn observed_lock_level(&self) -> ObservedLockLevel {
Arc::clone(&self.observed_lock_level)
}
fn observed_unlock_trace_ids(&self) -> ObservedUnlockTraceIds {
Arc::clone(&self.observed_unlock_trace_ids)
}
fn with_checkpoint_enforced_unlock() -> Self {
Self {
fail_unlock_on_checkpoint_error: true,
..Self::new()
}
}
}
struct ObservedLockFile {
inner: MemoryFile,
observed_lock_level: ObservedLockLevel,
observed_unlock_trace_ids: ObservedUnlockTraceIds,
fail_unlock_on_checkpoint_error: bool,
}
impl Vfs for ObservedLockVfs {
type File = ObservedLockFile;
fn name(&self) -> &'static str {
self.inner.name()
}
fn open(
&self,
cx: &Cx,
path: Option<&std::path::Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
let (inner, actual_flags) = self.inner.open(cx, path, flags)?;
Ok((
ObservedLockFile {
inner,
observed_lock_level: self.observed_lock_level(),
observed_unlock_trace_ids: self.observed_unlock_trace_ids(),
fail_unlock_on_checkpoint_error: self.fail_unlock_on_checkpoint_error,
},
actual_flags,
))
}
fn delete(&self, cx: &Cx, path: &std::path::Path, sync_dir: bool) -> Result<()> {
self.inner.delete(cx, path, sync_dir)
}
fn access(&self, cx: &Cx, path: &std::path::Path, flags: AccessFlags) -> Result<bool> {
self.inner.access(cx, path, flags)
}
fn full_pathname(&self, cx: &Cx, path: &std::path::Path) -> Result<PathBuf> {
self.inner.full_pathname(cx, path)
}
}
impl VfsFile for ObservedLockFile {
fn close(&mut self, cx: &Cx) -> Result<()> {
let result = self.inner.close(cx);
if result.is_ok() {
*self.observed_lock_level.lock().unwrap() = LockLevel::None;
}
result
}
fn read(&self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize> {
self.inner.read(cx, buf, offset)
}
fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()> {
self.inner.write(cx, buf, offset)
}
fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()> {
self.inner.truncate(cx, size)
}
fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()> {
self.inner.sync(cx, flags)
}
fn file_size(&self, cx: &Cx) -> Result<u64> {
self.inner.file_size(cx)
}
fn lock(&mut self, cx: &Cx, level: LockLevel) -> Result<()> {
self.inner.lock(cx, level)?;
*self.observed_lock_level.lock().unwrap() = level;
Ok(())
}
fn unlock(&mut self, cx: &Cx, level: LockLevel) -> Result<()> {
self.observed_unlock_trace_ids
.lock()
.unwrap()
.push(cx.trace_id());
if self.fail_unlock_on_checkpoint_error {
cx.checkpoint()
.map_err(|err| FrankenError::internal(err.to_string()))?;
}
self.inner.unlock(cx, level)?;
*self.observed_lock_level.lock().unwrap() = level;
Ok(())
}
fn check_reserved_lock(&self, cx: &Cx) -> Result<bool> {
self.inner.check_reserved_lock(cx)
}
fn sector_size(&self) -> u32 {
self.inner.sector_size()
}
fn device_characteristics(&self) -> u32 {
self.inner.device_characteristics()
}
fn shm_map(
&mut self,
cx: &Cx,
region: u32,
size: u32,
extend: bool,
) -> Result<fsqlite_vfs::ShmRegion> {
self.inner.shm_map(cx, region, size, extend)
}
fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
self.inner.shm_lock(cx, offset, n, flags)
}
fn shm_barrier(&self) {
self.inner.shm_barrier();
}
fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
self.inner.shm_unmap(cx, delete)
}
}
fn observed_lock_pager() -> (SimplePager<ObservedLockVfs>, ObservedLockLevel) {
let vfs = ObservedLockVfs::new();
let observed_lock_level = vfs.observed_lock_level();
let path = PathBuf::from("/observed-lock.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
(pager, observed_lock_level)
}
fn observed_lock_pager_with_checkpoint_enforced_unlock() -> ObservedCleanupUnlockHarness {
let vfs = ObservedLockVfs::with_checkpoint_enforced_unlock();
let observed_lock_level = vfs.observed_lock_level();
let observed_unlock_trace_ids = vfs.observed_unlock_trace_ids();
let path = PathBuf::from("/observed-lock-checkpoint.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
(pager, observed_lock_level, observed_unlock_trace_ids)
}
#[derive(Debug, Default)]
struct ExclusiveLockMetrics {
owner: Option<u64>,
acquired_at: Option<Instant>,
acquisition_count: usize,
hold_samples_ns: Vec<u64>,
wait_samples_ns: Vec<u64>,
}
#[derive(Clone)]
struct BlockingObservedLockVfs {
inner: MemoryVfs,
observed_lock_level: Arc<Mutex<LockLevel>>,
next_handle_id: StdArc<AtomicU64>,
exclusive_metrics: StdArc<(StdMutex<ExclusiveLockMetrics>, StdCondvar)>,
}
impl BlockingObservedLockVfs {
fn new() -> Self {
Self {
inner: MemoryVfs::new(),
observed_lock_level: Arc::new(Mutex::new(LockLevel::None)),
next_handle_id: StdArc::new(AtomicU64::new(1)),
exclusive_metrics: StdArc::new((
StdMutex::new(ExclusiveLockMetrics::default()),
StdCondvar::new(),
)),
}
}
fn observed_lock_level(&self) -> Arc<Mutex<LockLevel>> {
Arc::clone(&self.observed_lock_level)
}
fn wait_for_exclusive_acquisitions(&self, target: usize) {
let (metrics_lock, metrics_ready) = &*self.exclusive_metrics;
let mut metrics = metrics_lock.lock().unwrap();
while metrics.acquisition_count < target {
metrics = metrics_ready.wait(metrics).unwrap();
}
}
fn exclusive_hold_samples_ns(&self) -> Vec<u64> {
let (metrics_lock, _) = &*self.exclusive_metrics;
metrics_lock.lock().unwrap().hold_samples_ns.clone()
}
fn exclusive_wait_samples_ns(&self) -> Vec<u64> {
let (metrics_lock, _) = &*self.exclusive_metrics;
metrics_lock.lock().unwrap().wait_samples_ns.clone()
}
fn clear_exclusive_metrics(&self) {
let (metrics_lock, _) = &*self.exclusive_metrics;
let mut metrics = metrics_lock.lock().unwrap();
*metrics = ExclusiveLockMetrics::default();
}
}
struct BlockingObservedLockFile {
inner: MemoryFile,
observed_lock_level: Arc<Mutex<LockLevel>>,
handle_id: u64,
lock_level: LockLevel,
exclusive_metrics: StdArc<(StdMutex<ExclusiveLockMetrics>, StdCondvar)>,
}
impl BlockingObservedLockFile {
fn release_exclusive_hold(&self) {
let (metrics_lock, metrics_ready) = &*self.exclusive_metrics;
let mut metrics = metrics_lock.lock().unwrap();
if metrics.owner == Some(self.handle_id) {
if let Some(acquired_at) = metrics.acquired_at.take() {
metrics
.hold_samples_ns
.push(u64::try_from(acquired_at.elapsed().as_nanos()).unwrap_or(u64::MAX));
}
metrics.owner = None;
metrics_ready.notify_all();
}
}
}
impl Vfs for BlockingObservedLockVfs {
type File = BlockingObservedLockFile;
fn name(&self) -> &'static str {
self.inner.name()
}
fn open(
&self,
cx: &Cx,
path: Option<&std::path::Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
let (inner, actual_flags) = self.inner.open(cx, path, flags)?;
Ok((
BlockingObservedLockFile {
inner,
observed_lock_level: self.observed_lock_level(),
handle_id: self.next_handle_id.fetch_add(1, AtomicOrdering::Relaxed),
lock_level: LockLevel::None,
exclusive_metrics: StdArc::clone(&self.exclusive_metrics),
},
actual_flags,
))
}
fn delete(&self, cx: &Cx, path: &std::path::Path, sync_dir: bool) -> Result<()> {
self.inner.delete(cx, path, sync_dir)
}
fn access(&self, cx: &Cx, path: &std::path::Path, flags: AccessFlags) -> Result<bool> {
self.inner.access(cx, path, flags)
}
fn full_pathname(&self, cx: &Cx, path: &std::path::Path) -> Result<PathBuf> {
self.inner.full_pathname(cx, path)
}
}
impl VfsFile for BlockingObservedLockFile {
fn close(&mut self, cx: &Cx) -> Result<()> {
self.release_exclusive_hold();
let result = self.inner.close(cx);
if result.is_ok() {
self.lock_level = LockLevel::None;
*self.observed_lock_level.lock().unwrap() = LockLevel::None;
}
result
}
fn read(&self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize> {
self.inner.read(cx, buf, offset)
}
fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()> {
self.inner.write(cx, buf, offset)
}
fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()> {
self.inner.truncate(cx, size)
}
fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()> {
self.inner.sync(cx, flags)
}
fn file_size(&self, cx: &Cx) -> Result<u64> {
self.inner.file_size(cx)
}
fn lock(&mut self, cx: &Cx, level: LockLevel) -> Result<()> {
if self.lock_level < LockLevel::Exclusive && level >= LockLevel::Exclusive {
let wait_started = Instant::now();
let (metrics_lock, metrics_ready) = &*self.exclusive_metrics;
let mut metrics = metrics_lock.lock().unwrap();
while metrics.owner.is_some() && metrics.owner != Some(self.handle_id) {
metrics = metrics_ready.wait(metrics).unwrap();
}
metrics
.wait_samples_ns
.push(u64::try_from(wait_started.elapsed().as_nanos()).unwrap_or(u64::MAX));
metrics.owner = Some(self.handle_id);
metrics.acquired_at = Some(Instant::now());
metrics.acquisition_count = metrics.acquisition_count.saturating_add(1);
metrics_ready.notify_all();
}
self.inner.lock(cx, level)?;
if self.lock_level < level {
self.lock_level = level;
}
*self.observed_lock_level.lock().unwrap() = self.lock_level;
Ok(())
}
fn unlock(&mut self, cx: &Cx, level: LockLevel) -> Result<()> {
if self.lock_level >= LockLevel::Exclusive && level < LockLevel::Exclusive {
self.release_exclusive_hold();
}
self.inner.unlock(cx, level)?;
if self.lock_level > level {
self.lock_level = level;
}
*self.observed_lock_level.lock().unwrap() = self.lock_level;
Ok(())
}
fn check_reserved_lock(&self, cx: &Cx) -> Result<bool> {
self.inner.check_reserved_lock(cx)
}
fn sector_size(&self) -> u32 {
self.inner.sector_size()
}
fn device_characteristics(&self) -> u32 {
self.inner.device_characteristics()
}
fn shm_map(
&mut self,
cx: &Cx,
region: u32,
size: u32,
extend: bool,
) -> Result<fsqlite_vfs::ShmRegion> {
self.inner.shm_map(cx, region, size, extend)
}
fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
self.inner.shm_lock(cx, offset, n, flags)
}
fn shm_barrier(&self) {
self.inner.shm_barrier();
}
fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
self.inner.shm_unmap(cx, delete)
}
}
#[derive(Debug)]
struct DbWriteFailState {
target_path: PathBuf,
armed: bool,
remaining_successful_db_writes: usize,
}
#[derive(Clone)]
struct DbWriteFailOnceVfs {
inner: MemoryVfs,
state: Arc<Mutex<DbWriteFailState>>,
}
impl DbWriteFailOnceVfs {
fn new(target_path: PathBuf) -> Self {
Self {
inner: MemoryVfs::new(),
state: Arc::new(Mutex::new(DbWriteFailState {
target_path,
armed: false,
remaining_successful_db_writes: 0,
})),
}
}
fn arm_after_db_writes(&self, successful_db_writes_before_failure: usize) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.armed = true;
state.remaining_successful_db_writes = successful_db_writes_before_failure;
}
}
#[derive(Debug)]
struct DbWriteFailOnceFile {
inner: MemoryFile,
state: Arc<Mutex<DbWriteFailState>>,
is_target_db: bool,
}
impl Vfs for DbWriteFailOnceVfs {
type File = DbWriteFailOnceFile;
fn name(&self) -> &'static str {
self.inner.name()
}
fn open(
&self,
cx: &Cx,
path: Option<&std::path::Path>,
flags: VfsOpenFlags,
) -> Result<(Self::File, VfsOpenFlags)> {
let (inner, actual_flags) = self.inner.open(cx, path, flags)?;
let is_target_db = {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
path == Some(state.target_path.as_path()) && flags.contains(VfsOpenFlags::MAIN_DB)
};
Ok((
DbWriteFailOnceFile {
inner,
state: Arc::clone(&self.state),
is_target_db,
},
actual_flags,
))
}
fn delete(&self, cx: &Cx, path: &std::path::Path, sync_dir: bool) -> Result<()> {
self.inner.delete(cx, path, sync_dir)
}
fn access(&self, cx: &Cx, path: &std::path::Path, flags: AccessFlags) -> Result<bool> {
self.inner.access(cx, path, flags)
}
fn full_pathname(&self, cx: &Cx, path: &std::path::Path) -> Result<PathBuf> {
self.inner.full_pathname(cx, path)
}
}
impl VfsFile for DbWriteFailOnceFile {
fn close(&mut self, cx: &Cx) -> Result<()> {
self.inner.close(cx)
}
fn read(&self, cx: &Cx, buf: &mut [u8], offset: u64) -> Result<usize> {
self.inner.read(cx, buf, offset)
}
fn write(&mut self, cx: &Cx, buf: &[u8], offset: u64) -> Result<()> {
if self.is_target_db {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.armed {
if state.remaining_successful_db_writes == 0 {
state.armed = false;
return Err(FrankenError::Io(std::io::Error::other(
"simulated main-db write failure",
)));
}
state.remaining_successful_db_writes -= 1;
}
}
self.inner.write(cx, buf, offset)
}
fn truncate(&mut self, cx: &Cx, size: u64) -> Result<()> {
self.inner.truncate(cx, size)
}
fn sync(&mut self, cx: &Cx, flags: SyncFlags) -> Result<()> {
self.inner.sync(cx, flags)
}
fn file_size(&self, cx: &Cx) -> Result<u64> {
self.inner.file_size(cx)
}
fn lock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
self.inner.lock(cx, level)
}
fn unlock(&mut self, cx: &Cx, level: fsqlite_types::LockLevel) -> Result<()> {
self.inner.unlock(cx, level)
}
fn check_reserved_lock(&self, cx: &Cx) -> Result<bool> {
self.inner.check_reserved_lock(cx)
}
fn sector_size(&self) -> u32 {
self.inner.sector_size()
}
fn device_characteristics(&self) -> u32 {
self.inner.device_characteristics()
}
fn shm_map(
&mut self,
cx: &Cx,
region: u32,
size: u32,
extend: bool,
) -> Result<fsqlite_vfs::ShmRegion> {
self.inner.shm_map(cx, region, size, extend)
}
fn shm_lock(&mut self, cx: &Cx, offset: u32, n: u32, flags: u32) -> Result<()> {
self.inner.shm_lock(cx, offset, n, flags)
}
fn shm_barrier(&self) {
self.inner.shm_barrier();
}
fn shm_unmap(&mut self, cx: &Cx, delete: bool) -> Result<()> {
self.inner.shm_unmap(cx, delete)
}
}
#[test]
fn test_open_empty_database() {
let (pager, _) = test_pager();
let inner = pager.inner.lock().unwrap();
assert_eq!(inner.db_size, 1, "bead_id={BEAD_ID} case=empty_db_size");
assert_eq!(
inner.page_size,
PageSize::DEFAULT,
"bead_id={BEAD_ID} case=page_size_default"
);
drop(inner);
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw_page = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let hdr: [u8; DATABASE_HEADER_SIZE] = raw_page[..DATABASE_HEADER_SIZE]
.try_into()
.expect("page 1 must contain database header");
let parsed = DatabaseHeader::from_bytes(&hdr).expect("header should parse");
assert_eq!(
parsed.page_size,
PageSize::DEFAULT,
"bead_id={BEAD_ID} case=page1_header_page_size"
);
assert_eq!(
parsed.page_count, 1,
"bead_id={BEAD_ID} case=page1_header_page_count"
);
let btree_hdr =
BTreePageHeader::parse(&raw_page, PageSize::DEFAULT, 0, true).expect("btree header");
assert_eq!(
btree_hdr.cell_count, 0,
"bead_id={BEAD_ID} case=sqlite_master_initially_empty"
);
}
#[test]
fn test_open_existing_database_uses_header_page_size() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/page_size_autodetect.db");
let expected_page_size = PageSize::new(8192).unwrap();
let _pager = SimplePager::open(vfs.clone(), &path, expected_page_size).unwrap();
let reopened = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
assert!(
reopened.page_size() == expected_page_size,
"bead_id={BEAD_ID} case=autodetect_existing_page_size"
);
}
#[test]
fn test_begin_refreshes_external_page_growth_before_allocation() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/pager_refresh_external_growth.db");
let pager1 = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let pager2 = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut writer1 = pager1.begin(&cx, TransactionMode::Immediate).unwrap();
let page2 = writer1.allocate_page(&cx).unwrap();
assert_eq!(page2.get(), 2, "first writer should allocate page 2");
writer1.write_page(&cx, page2, &vec![0xAB; ps]).unwrap();
writer1.commit(&cx).unwrap();
let mut writer2 = pager2.begin(&cx, TransactionMode::Immediate).unwrap();
let page3 = writer2.allocate_page(&cx).unwrap();
assert_eq!(
page3.get(),
3,
"bead_id={BEAD_ID} case=refresh_external_growth_reissues_next_page"
);
}
#[test]
fn test_open_existing_database_rejects_non_page_aligned_size() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/misaligned.db");
let cx = Cx::new();
let _pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
let file_size = db_file.file_size(&cx).unwrap();
db_file.write(&cx, &[0xAB], file_size).unwrap();
let Err(err) = SimplePager::open(vfs, &path, PageSize::DEFAULT) else {
panic!("expected non-page-aligned file size error");
};
assert!(
matches!(err, FrankenError::DatabaseCorrupt { .. }),
"bead_id={BEAD_ID} case=reject_non_page_aligned_file_size"
);
}
#[test]
fn test_begin_readonly_transaction() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert!(!txn.is_writer, "bead_id={BEAD_ID} case=readonly_not_writer");
}
#[test]
fn test_begin_write_transaction() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
assert!(txn.is_writer, "bead_id={BEAD_ID} case=immediate_is_writer");
}
#[test]
fn test_begin_deferred_transaction_starts_reader() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::Deferred).unwrap();
assert!(
!txn.is_writer,
"bead_id={BEAD_ID} case=deferred_starts_readonly"
);
}
#[test]
fn test_begin_concurrent_transaction_starts_reader() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
assert!(
!txn.is_writer,
"bead_id={BEAD_ID} case=concurrent_starts_readonly"
);
}
#[test]
fn test_deferred_upgrades_on_first_write_intent() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut deferred = pager.begin(&cx, TransactionMode::Deferred).unwrap();
assert!(
!deferred.is_writer,
"bead_id={BEAD_ID} case=deferred_pre_upgrade"
);
let _page = deferred.allocate_page(&cx).unwrap();
assert!(
deferred.is_writer,
"bead_id={BEAD_ID} case=deferred_upgraded_to_writer"
);
}
#[test]
fn test_deferred_upgrade_busy_when_writer_active() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut deferred = pager.begin(&cx, TransactionMode::Deferred).unwrap();
let started = Instant::now();
let err = deferred.allocate_page(&cx).unwrap_err();
assert!(matches!(err, FrankenError::Busy));
assert!(
started.elapsed() < Duration::from_millis(10),
"bead_id={BEAD_ID} case=deferred_upgrade_baton_wait_is_bounded"
);
}
#[test]
fn test_single_writer_baton_handoff_budget_is_bounded() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let started = Instant::now();
let err = match pager.begin(&cx, TransactionMode::Immediate) {
Ok(_) => panic!("bead_id={BEAD_ID} case=single_writer_baton_should_return_busy"),
Err(err) => err,
};
assert!(matches!(err, FrankenError::Busy));
assert!(
started.elapsed() < Duration::from_millis(10),
"bead_id={BEAD_ID} case=single_writer_baton_wait_is_bounded"
);
}
#[test]
fn test_concurrent_begin_bypasses_single_writer_baton() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let concurrent = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
assert!(
!concurrent.is_writer,
"bead_id={BEAD_ID} case=concurrent_begin_not_blocked_by_single_writer"
);
}
#[test]
fn test_concurrent_writer_blocked() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _txn1 = pager.begin(&cx, TransactionMode::Exclusive).unwrap();
let result = pager.begin(&cx, TransactionMode::Immediate);
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=concurrent_writer_busy"
);
}
#[test]
fn test_multiple_readers_allowed() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _r1 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let _r2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
}
#[test]
fn test_write_page_and_read_back() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
let page_size = PageSize::DEFAULT.as_usize();
let mut data = vec![0_u8; page_size];
data[0] = 0xDE;
data[1] = 0xAD;
txn.write_page(&cx, page_no, &data).unwrap();
let read_back = txn.get_page(&cx, page_no).unwrap();
assert_eq!(
read_back.as_ref()[0],
0xDE,
"bead_id={BEAD_ID} case=read_back_byte0"
);
assert_eq!(
read_back.as_ref()[1],
0xAD,
"bead_id={BEAD_ID} case=read_back_byte1"
);
}
#[test]
fn test_commit_persists_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
let page_size = PageSize::DEFAULT.as_usize();
let mut data = vec![0_u8; page_size];
data[0..4].copy_from_slice(&[0xCA, 0xFE, 0xBA, 0xBE]);
txn.write_page(&cx, page_no, &data).unwrap();
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let read_back = txn2.get_page(&cx, page_no).unwrap();
assert_eq!(
&read_back.as_ref()[0..4],
&[0xCA, 0xFE, 0xBA, 0xBE],
"bead_id={BEAD_ID} case=commit_persists"
);
}
#[test]
fn test_rollback_discards_writes() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
let page_size = PageSize::DEFAULT.as_usize();
let original = vec![0x11_u8; page_size];
txn.write_page(&cx, page_no, &original).unwrap();
txn.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let modified = vec![0x99_u8; page_size];
txn2.write_page(&cx, page_no, &modified).unwrap();
txn2.rollback(&cx).unwrap();
let txn3 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let read_back = txn3.get_page(&cx, page_no).unwrap();
assert_eq!(
read_back.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=rollback_restores"
);
}
#[test]
fn test_allocate_returns_sequential_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
assert!(
p2.get() > p1.get(),
"bead_id={BEAD_ID} case=sequential_alloc p1={} p2={}",
p1.get(),
p2.get()
);
}
#[test]
fn test_free_page_reuses_on_next_alloc() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn1.allocate_page(&cx).unwrap();
txn1.write_page(&cx, p, &vec![0_u8; ps]).unwrap();
txn1.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p).unwrap();
txn2.commit(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert_eq!(inner.freelist.len(), 1);
assert_eq!(inner.freelist[0], p);
drop(inner);
}
let mut txn3 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = txn3.allocate_page(&cx).unwrap();
assert_eq!(
p2,
p,
"bead_id={BEAD_ID} case=freelist_reuse p3={} p1={}",
p2.get(),
p.get()
);
}
#[test]
fn test_return_pages_to_freelist_keeps_unique_descending_order() {
let p2 = PageNumber::new(2).unwrap();
let p3 = PageNumber::new(3).unwrap();
let p4 = PageNumber::new(4).unwrap();
let p5 = PageNumber::new(5).unwrap();
let p6 = PageNumber::new(6).unwrap();
let mut freelist = vec![p5, p3, p2];
return_pages_to_freelist(&mut freelist, [p4, p3, p6, p4]);
assert_eq!(
freelist,
vec![p6, p5, p4, p3, p2],
"bead_id={BEAD_ID} case=return_pages_dedupes_after_bulk_extend"
);
}
#[test]
fn test_normalize_freelist_keeps_unique_descending_order() {
let p2 = PageNumber::new(2).unwrap();
let p3 = PageNumber::new(3).unwrap();
let p4 = PageNumber::new(4).unwrap();
let p5 = PageNumber::new(5).unwrap();
let p6 = PageNumber::new(6).unwrap();
let normalized = normalize_freelist(&[p3, p6, p4, p3, p5, p2], 5);
assert_eq!(
normalized,
vec![p5, p4, p3, p2],
"bead_id={BEAD_ID} case=normalize_freelist_dedupes_descending_and_filters"
);
}
#[test]
fn test_freed_pages_are_quarantined_until_commit() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p2, &vec![0xAA; ps]).unwrap();
txn.free_page(&cx, p2).unwrap();
let p3 = txn.allocate_page(&cx).unwrap();
assert_eq!(
p3.get(),
p2.get() + 1,
"bead_id={BEAD_ID} case=freed_pages_quarantined_until_commit"
);
txn.write_page(&cx, p3, &vec![0xBB; ps]).unwrap();
txn.commit(&cx).unwrap();
let mut next_txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused = next_txn.allocate_page(&cx).unwrap();
assert_eq!(
reused, p2,
"bead_id={BEAD_ID} case=freed_pages_reenter_committed_freelist_after_commit"
);
next_txn.rollback(&cx).unwrap();
}
#[test]
fn test_get_page_rejects_page_freed_in_same_transaction() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page, &vec![0xAA; ps]).unwrap();
seed.commit(&cx).unwrap();
page
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.free_page(&cx, page).unwrap();
let err = txn
.get_page(&cx, page)
.expect_err("read-after-free must be rejected");
let detail = err.to_string();
assert!(
detail.contains("freed earlier in this transaction"),
"expected read-after-free error, got: {detail}"
);
}
#[test]
fn test_cannot_free_page_one() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let result = txn.free_page(&cx, PageNumber::ONE);
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=cannot_free_page_one"
);
}
#[test]
fn test_readonly_cannot_write() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let result = txn.write_page(&cx, PageNumber::ONE, &[0_u8; 4096]);
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=readonly_cannot_write"
);
}
#[test]
fn test_readonly_cannot_allocate() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let result = txn.allocate_page(&cx);
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=readonly_cannot_allocate"
);
}
#[test]
fn test_drop_uncommitted_writer_releases_lock() {
let (pager, _) = test_pager();
let cx = Cx::new();
{
let _txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
}
let txn2 = pager.begin(&cx, TransactionMode::Immediate);
assert!(
txn2.is_ok(),
"bead_id={BEAD_ID} case=drop_releases_writer_lock"
);
}
#[test]
fn test_drop_cleanup_unlock_preserves_lineage_and_masks_cancellation() {
let (pager, observed_lock_level, observed_unlock_trace_ids) =
observed_lock_pager_with_checkpoint_enforced_unlock();
let cx = Cx::new().with_trace_context(41, 0, 0);
{
let _txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
cx.cancel();
}
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::None,
"bead_id={BEAD_ID} case=drop_cleanup_unlock_releases_lock_after_parent_cancel"
);
assert_eq!(
observed_unlock_trace_ids.lock().unwrap().last().copied(),
Some(41),
"bead_id={BEAD_ID} case=drop_cleanup_unlock_uses_parent_trace_lineage"
);
}
#[test]
fn test_reader_exit_preserves_shared_lock_for_other_reader() {
let (pager, observed_lock_level) = observed_lock_pager();
let cx = Cx::new();
let mut reader1 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let reader2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(*observed_lock_level.lock().unwrap(), LockLevel::Shared);
reader1.commit(&cx).unwrap();
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::Shared,
"bead_id={BEAD_ID} case=reader_commit_keeps_shared_for_other_reader"
);
drop(reader2);
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::None,
"bead_id={BEAD_ID} case=last_reader_releases_shared"
);
}
#[test]
fn test_reader_exit_preserves_reserved_lock_for_active_writer() {
let (pager, observed_lock_level) = observed_lock_pager();
let cx = Cx::new();
let mut writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(*observed_lock_level.lock().unwrap(), LockLevel::Reserved);
drop(reader);
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::Reserved,
"bead_id={BEAD_ID} case=reader_drop_keeps_reserved_for_writer"
);
writer.commit(&cx).unwrap();
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::None,
"bead_id={BEAD_ID} case=writer_commit_releases_last_lock"
);
}
#[test]
fn test_commit_then_drop_no_double_release() {
let (pager, _) = test_pager();
let cx = Cx::new();
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.commit(&cx).unwrap();
}
let txn2 = pager.begin(&cx, TransactionMode::Immediate);
assert!(
txn2.is_ok(),
"bead_id={BEAD_ID} case=commit_releases_writer"
);
}
#[test]
fn test_double_commit_is_idempotent() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.commit(&cx).unwrap();
txn.commit(&cx).unwrap();
}
#[test]
fn test_multi_page_write_commit_read() {
let (pager, _) = test_pager();
let cx = Cx::new();
let page_size = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut allocated_pages = Vec::new();
for i in 0_u8..5 {
let p = txn.allocate_page(&cx).unwrap();
let data = vec![i; page_size];
txn.write_page(&cx, p, &data).unwrap();
allocated_pages.push(p);
}
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for (i, &p) in allocated_pages.iter().enumerate() {
let data = txn2.get_page(&cx, p).unwrap();
#[allow(clippy::cast_possible_truncation)]
let expected = i as u8;
assert_eq!(
data.as_ref()[0],
expected,
"bead_id={BEAD_ID} case=multi_page idx={i}"
);
}
}
#[test]
fn test_commit_journal_skips_preimage_for_pages_allocated_after_transaction_start() {
let (pager, _) = test_pager();
let cx = Cx::new();
let page_size = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let new_page = txn.allocate_page(&cx).unwrap();
assert!(
new_page.get() > txn.original_db_size,
"bead_id={BEAD_ID} case=new_page_is_after_transaction_start"
);
txn.write_page(&cx, new_page, &vec![0x77; page_size])
.unwrap();
{
let mut inner = txn.inner.lock().unwrap();
inner.db_size = new_page.get();
}
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn2.get_page(&cx, new_page).unwrap();
assert_eq!(
data.as_ref()[0],
0x77,
"bead_id={BEAD_ID} case=post_start_page_commits_without_preimage_read"
);
}
#[test]
fn test_commit_journal_page_count_ignores_stale_sorted_pages_missing_from_write_set() {
let (pager, _) = test_pager();
let cx = Cx::new();
let page_size = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let durable_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, durable_page, &vec![0x11; page_size])
.unwrap();
let stale_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, stale_page, &vec![0x22; page_size])
.unwrap();
let removed = txn.write_set.remove(&stale_page);
assert!(
removed.is_some() && txn.write_pages_sorted.contains(&stale_page),
"bead_id={BEAD_ID} case=test_setup_left_stale_sorted_page"
);
txn.commit(&cx).unwrap();
let inner = pager.inner.lock().unwrap();
let mut header_bytes = [0_u8; DATABASE_HEADER_SIZE];
let bytes_read = inner.db_file.read(&cx, &mut header_bytes, 0).unwrap();
assert_eq!(
bytes_read, DATABASE_HEADER_SIZE,
"bead_id={BEAD_ID} case=page_one_header_read"
);
let header = DatabaseHeader::from_bytes(&header_bytes).unwrap();
assert_eq!(
header.page_count,
durable_page.get(),
"bead_id={BEAD_ID} case=page_count_uses_authoritative_write_set"
);
assert_eq!(
inner.db_file.file_size(&cx).unwrap(),
u64::from(durable_page.get()) * u64::from(PageSize::DEFAULT.get()),
"bead_id={BEAD_ID} case=file_size_matches_page_count"
);
}
#[test]
fn test_commit_journal_short_preimage_read_errors() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/short_preimage.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let page_two = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
p
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page_two, &vec![0x22; ps]).unwrap();
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
db_file
.truncate(&cx, PageSize::DEFAULT.as_usize() as u64)
.unwrap();
let err = txn.commit(&cx).unwrap_err();
assert!(
matches!(err, FrankenError::DatabaseCorrupt { .. }),
"bead_id={BEAD_ID} case=short_preimage_read_is_corruption"
);
let Err(busy) = pager.begin(&cx, TransactionMode::Immediate) else {
panic!("expected begin to fail while writer lock is still held");
};
assert!(
matches!(busy, FrankenError::Busy),
"bead_id={BEAD_ID} case=commit_error_keeps_writer_lock"
);
txn.rollback(&cx).unwrap();
let _next_writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
}
#[test]
fn test_rollback_recovers_after_partial_commit_failure() {
let path = PathBuf::from("/rollback_after_failed_commit.db");
let journal_path = SimplePager::<DbWriteFailOnceVfs>::journal_path(&path);
let vfs = DbWriteFailOnceVfs::new(path.clone());
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let original_two = vec![0x11; ps];
let original_three = vec![0x44; ps];
let (page_two, page_three) = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
let page_three = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &original_two).unwrap();
txn.write_page(&cx, page_three, &original_three).unwrap();
txn.commit(&cx).unwrap();
(page_two, page_three)
};
vfs.arm_after_db_writes(1);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page_two, &vec![0x22; ps]).unwrap();
txn.write_page(&cx, page_three, &vec![0x55; ps]).unwrap();
let err = txn.commit(&cx).unwrap_err();
assert!(
matches!(err, FrankenError::Io(_)),
"bead_id={BEAD_ID} case=partial_commit_surfaces_io_error"
);
txn.rollback(&cx).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
original_two
);
assert_eq!(
reader.get_page(&cx, page_three).unwrap().into_vec(),
original_three
);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=rollback_removes_failed_commit_journal"
);
let reopened = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let reopened_reader = reopened.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reopened_reader.get_page(&cx, page_two).unwrap().into_vec(),
vec![0x11; ps]
);
assert_eq!(
reopened_reader
.get_page(&cx, page_three)
.unwrap()
.into_vec(),
vec![0x44; ps]
);
}
#[test]
fn test_begin_recovers_abandoned_failed_commit() {
let path = PathBuf::from("/begin_recovers_failed_commit.db");
let journal_path = SimplePager::<DbWriteFailOnceVfs>::journal_path(&path);
let vfs = DbWriteFailOnceVfs::new(path.clone());
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let original_two = vec![0x61; ps];
let original_three = vec![0x73; ps];
let (page_two, page_three) = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
let page_three = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &original_two).unwrap();
txn.write_page(&cx, page_three, &original_three).unwrap();
txn.commit(&cx).unwrap();
(page_two, page_three)
};
vfs.arm_after_db_writes(1);
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page_two, &vec![0x62; ps]).unwrap();
txn.write_page(&cx, page_three, &vec![0x74; ps]).unwrap();
let err = txn.commit(&cx).unwrap_err();
assert!(
matches!(err, FrankenError::Io(_)),
"bead_id={BEAD_ID} case=abandoned_failed_commit_surfaces_io_error"
);
}
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
original_two
);
assert_eq!(
reader.get_page(&cx, page_three).unwrap().into_vec(),
original_three
);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=next_begin_cleans_abandoned_failed_commit_journal"
);
let reopened = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let reopened_reader = reopened.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reopened_reader.get_page(&cx, page_two).unwrap().into_vec(),
vec![0x61; ps]
);
assert_eq!(
reopened_reader
.get_page(&cx, page_three)
.unwrap()
.into_vec(),
vec![0x73; ps]
);
}
#[test]
fn test_commit_survives_journal_delete_failure() {
let vfs = JournalDeleteFailVfs::new();
let path = PathBuf::from("/journal_delete_failure_commit.db");
let journal_path = SimplePager::<JournalDeleteFailVfs>::journal_path(&path);
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0xAB; ps]).unwrap();
txn.commit(&cx).unwrap();
page_two
};
let reopened = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let reader = reopened.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
vec![0xAB; ps]
);
assert!(
vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=delete_failure_leaves_journal_inode"
);
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (journal_file, _) = vfs.open(&cx, Some(&journal_path), flags).unwrap();
assert_eq!(
journal_file.file_size(&cx).unwrap(),
0,
"bead_id={BEAD_ID} case=delete_failure_still_invalidates_journal"
);
}
#[test]
fn test_hot_journal_recovery_survives_delete_failure() {
let vfs = JournalDeleteFailVfs::new();
let path = PathBuf::from("/journal_delete_failure_recovery.db");
let journal_path = SimplePager::<JournalDeleteFailVfs>::journal_path(&path);
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
let header = JournalHeader {
page_count: 1,
nonce: 0x4652_414E,
initial_db_size: 2,
sector_size: 512,
page_size: PageSize::DEFAULT.get(),
};
let hdr_bytes = header.encode_padded();
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl_file, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
jrnl_file.write(&cx, &hdr_bytes, 0).unwrap();
let record = JournalPageRecord::new(2, vec![0x11; ps], header.nonce);
jrnl_file
.write(&cx, &record.encode(), hdr_bytes.len() as u64)
.unwrap();
jrnl_file.sync(&cx, SyncFlags::NORMAL).unwrap();
let page_offset = PageSize::DEFAULT.as_usize() as u64;
db_file.write(&cx, &vec![0x22; ps], page_offset).unwrap();
db_file.sync(&cx, SyncFlags::NORMAL).unwrap();
}
let reopened = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let reader = reopened.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page_two = PageNumber::new(2).unwrap();
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
vec![0x11; ps]
);
assert!(
vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=recovery_delete_failure_leaves_journal_inode"
);
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (journal_file, _) = vfs.open(&cx, Some(&journal_path), flags).unwrap();
assert_eq!(
journal_file.file_size(&cx).unwrap(),
0,
"bead_id={BEAD_ID} case=recovery_delete_failure_still_invalidates_journal"
);
}
#[test]
fn test_commit_creates_and_deletes_journal() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/jrnl_test.db");
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=no_journal_before_commit"
);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xAA; 4096]).unwrap();
txn.commit(&cx).unwrap();
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=journal_deleted_after_commit"
);
}
#[test]
fn test_private_memory_commit_skips_journal_creation() {
let pager = private_memory_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&pager.db_path);
assert!(
!pager
.vfs
.access(&cx, &journal_path, AccessFlags::EXISTS)
.unwrap(),
"bead_id={BEAD_ID} case=private_memory_commit_starts_without_journal"
);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0xA5; ps]).unwrap();
txn.commit(&cx).unwrap();
assert!(
!pager
.vfs
.access(&cx, &journal_path, AccessFlags::EXISTS)
.unwrap(),
"bead_id={BEAD_ID} case=private_memory_commit_avoids_journal_creation"
);
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page).unwrap().as_ref()[0],
0xA5,
"bead_id={BEAD_ID} case=private_memory_commit_persists_visible_page_image"
);
}
#[test]
fn test_hot_journal_recovery_restores_original_data() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/crash_test.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
assert_eq!(p.get(), 2);
txn.write_page(&cx, p, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
}
{
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
let corrupt_data = vec![0x99; ps];
let offset = u64::from(2_u32 - 1) * ps as u64;
db_file.write(&cx, &corrupt_data, offset).unwrap();
}
{
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
let nonce = 42;
let header = JournalHeader {
page_count: 1,
nonce,
initial_db_size: 2,
sector_size: 512,
page_size: 4096,
};
let hdr_bytes = header.encode_padded();
jrnl.write(&cx, &hdr_bytes, 0).unwrap();
let record = JournalPageRecord::new(2, vec![0x11; ps], nonce);
let rec_bytes = record.encode();
jrnl.write(&cx, &rec_bytes, hdr_bytes.len() as u64).unwrap();
}
{
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page_no_2 = PageNumber::new(2).unwrap();
let data = txn.get_page(&cx, page_no_2).unwrap();
assert_eq!(
data.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=journal_recovery_restores"
);
assert_eq!(
data.as_ref()[ps - 1],
0x11,
"bead_id={BEAD_ID} case=journal_recovery_restores_last_byte"
);
}
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=journal_deleted_after_recovery"
);
}
#[test]
fn test_long_lived_pager_recovers_external_hot_journal_on_begin() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/long_lived_hot_journal.db");
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = PageNumber::new(2).unwrap();
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
{
let seed = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn = seed.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
assert_eq!(page_two.get(), 2);
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
}
let warm_reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
warm_reader.get_page(&cx, page_two).unwrap().into_vec(),
vec![0x11; ps],
"bead_id={BEAD_ID} case=existing_pager_populates_publication_before_hot_journal"
);
drop(warm_reader);
assert!(
pager.published_snapshot().page_set_size > 0,
"bead_id={BEAD_ID} case=existing_pager_has_published_pages_before_hot_journal"
);
{
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
let header = JournalHeader {
page_count: 1,
nonce: 0x4652_414E,
initial_db_size: 2,
sector_size: 512,
page_size: PageSize::DEFAULT.get(),
};
let hdr_bytes = header.encode_padded();
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl_file, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
jrnl_file.write(&cx, &hdr_bytes, 0).unwrap();
let record = JournalPageRecord::new(2, vec![0x11; ps], header.nonce);
jrnl_file
.write(&cx, &record.encode(), hdr_bytes.len() as u64)
.unwrap();
jrnl_file.sync(&cx, SyncFlags::NORMAL).unwrap();
let page_offset = PageSize::DEFAULT.as_usize() as u64;
db_file.write(&cx, &vec![0x99; ps], page_offset).unwrap();
db_file.sync(&cx, SyncFlags::NORMAL).unwrap();
}
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
pager.published_snapshot().page_set_size,
0,
"bead_id={BEAD_ID} case=existing_pager_clears_published_pages_after_hot_journal"
);
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
vec![0x11; ps],
"bead_id={BEAD_ID} case=existing_pager_recovers_hot_journal"
);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=existing_pager_removes_hot_journal"
);
}
#[test]
fn test_refresh_published_snapshot_recovers_external_hot_journal() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/published_refresh_hot_journal.db");
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
assert_eq!(page_two.get(), 2);
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
}
let page_two = PageNumber::new(2).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(reader.get_page(&cx, page_two).unwrap().as_ref()[0], 0x11);
drop(reader);
assert!(
pager.published_snapshot().page_set_size > 0,
"bead_id={BEAD_ID} case=publication_plane_populated_before_hot_journal_refresh"
);
{
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
let header = JournalHeader {
page_count: 1,
nonce: 0x5245_4652,
initial_db_size: 2,
sector_size: 512,
page_size: PageSize::DEFAULT.get(),
};
let hdr_bytes = header.encode_padded();
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl_file, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
jrnl_file.write(&cx, &hdr_bytes, 0).unwrap();
let record = JournalPageRecord::new(2, vec![0x11; ps], header.nonce);
jrnl_file
.write(&cx, &record.encode(), hdr_bytes.len() as u64)
.unwrap();
jrnl_file.sync(&cx, SyncFlags::NORMAL).unwrap();
let page_offset = PageSize::DEFAULT.as_usize() as u64;
db_file.write(&cx, &vec![0x99; ps], page_offset).unwrap();
db_file.sync(&cx, SyncFlags::NORMAL).unwrap();
}
let refreshed = pager.refresh_published_snapshot(&cx).unwrap();
assert_eq!(
refreshed.page_set_size, 0,
"bead_id={BEAD_ID} case=published_refresh_clears_published_pages_after_hot_journal"
);
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
let mut restored = vec![0u8; ps];
let bytes_read = db_file
.read(&cx, &mut restored, PageSize::DEFAULT.as_usize() as u64)
.unwrap();
assert_eq!(bytes_read, ps);
assert_eq!(
restored,
vec![0x11; ps],
"bead_id={BEAD_ID} case=published_refresh_recovers_hot_journal_bytes"
);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=published_refresh_removes_hot_journal"
);
}
#[test]
fn test_hot_journal_truncated_record_stops_replay() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/trunc_jrnl.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0xAB; ps]).unwrap();
txn.write_page(&cx, p2, &vec![0xBB; ps]).unwrap();
txn.commit(&cx).unwrap();
}
{
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
db_file
.write(&cx, &vec![0xFF; ps], u64::from(2_u32 - 1) * ps as u64)
.unwrap();
}
{
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
let nonce = 7;
let header = JournalHeader {
page_count: 2,
nonce,
initial_db_size: 3,
sector_size: 512,
page_size: 4096,
};
let hdr_bytes = header.encode_padded();
jrnl.write(&cx, &hdr_bytes, 0).unwrap();
let rec1 = JournalPageRecord::new(3, vec![0xCC; ps], nonce);
let rec1_bytes = rec1.encode();
jrnl.write(&cx, &rec1_bytes, hdr_bytes.len() as u64)
.unwrap();
let rec2 = JournalPageRecord::new(2, vec![0xBB; ps], nonce);
let rec2_bytes = rec2.encode();
let trunc_len = rec2_bytes.len() / 2;
let offset = hdr_bytes.len() as u64 + rec1_bytes.len() as u64;
jrnl.write(&cx, &rec2_bytes[..trunc_len], offset).unwrap();
}
{
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page_no_2 = PageNumber::new(2).unwrap();
let data2 = txn.get_page(&cx, page_no_2).unwrap();
assert_eq!(
data2.as_ref()[0],
0xFF,
"bead_id={BEAD_ID} case=truncated_journal_page2_not_restored"
);
}
}
#[test]
fn test_hot_journal_checksum_mismatch_stops_replay() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/cksum_jrnl.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x55; ps]).unwrap();
txn.commit(&cx).unwrap();
}
{
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_DB;
let (mut db_file, _) = vfs.open(&cx, Some(&path), flags).unwrap();
db_file
.write(&cx, &vec![0xEE; ps], u64::from(2_u32 - 1) * ps as u64)
.unwrap();
}
{
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
let nonce = 99;
let header = JournalHeader {
page_count: 1,
nonce,
initial_db_size: 2,
sector_size: 512,
page_size: 4096,
};
let hdr_bytes = header.encode_padded();
jrnl.write(&cx, &hdr_bytes, 0).unwrap();
let record = JournalPageRecord::new(2, vec![0x55; ps], nonce + 1);
let rec_bytes = record.encode();
jrnl.write(&cx, &rec_bytes, hdr_bytes.len() as u64).unwrap();
}
{
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page_no_2 = PageNumber::new(2).unwrap();
let data = txn.get_page(&cx, page_no_2).unwrap();
assert_eq!(
data.as_ref()[0],
0xEE,
"bead_id={BEAD_ID} case=bad_checksum_stops_replay"
);
}
}
#[test]
fn test_hot_journal_invalid_page_number_errors() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/bad_pgno_jrnl.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let _pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let jrnl_flags =
VfsOpenFlags::CREATE | VfsOpenFlags::READWRITE | VfsOpenFlags::MAIN_JOURNAL;
let (mut jrnl, _) = vfs.open(&cx, Some(&journal_path), jrnl_flags).unwrap();
let nonce = 321;
let header = JournalHeader {
page_count: 1,
nonce,
initial_db_size: 1,
sector_size: 512,
page_size: 4096,
};
let hdr_bytes = header.encode_padded();
jrnl.write(&cx, &hdr_bytes, 0).unwrap();
let record = JournalPageRecord::new(0, vec![0xAA; ps], nonce);
let rec_bytes = record.encode();
jrnl.write(&cx, &rec_bytes, hdr_bytes.len() as u64).unwrap();
}
let Err(err) = SimplePager::open(vfs, &path, PageSize::DEFAULT) else {
panic!("expected invalid journal page number error");
};
assert!(
matches!(err, FrankenError::DatabaseCorrupt { .. }),
"bead_id={BEAD_ID} case=invalid_journal_page_number_rejected"
);
}
#[test]
fn test_journal_not_created_for_readonly_commit() {
let (pager, _) = test_pager();
let cx = Cx::new();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&pager.db_path);
let mut txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
txn.commit(&cx).unwrap();
assert!(
!pager
.vfs
.access(&cx, &journal_path, AccessFlags::EXISTS)
.unwrap(),
"bead_id={BEAD_ID} case=journal_deleted_for_readonly"
);
}
#[test]
fn test_rollback_deletes_journal() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/rollback_jrnl.db");
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xDD; 4096]).unwrap();
txn.rollback(&cx).unwrap();
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=journal_deleted_on_rollback"
);
}
#[test]
fn test_savepoint_basic_rollback_to() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p2, &vec![0x22; ps]).unwrap();
txn.write_page(&cx, p1, &vec![0x33; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "sp1").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=savepoint_rollback_restores_p1"
);
let data2 = txn.get_page(&cx, p2).unwrap();
assert_eq!(
data2.as_ref()[0],
0x00,
"bead_id={BEAD_ID} case=savepoint_rollback_removes_p2"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_release_keeps_changes() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0xAA; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.write_page(&cx, p1, &vec![0xBB; ps]).unwrap();
txn.release_savepoint(&cx, "sp1").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0xBB,
"bead_id={BEAD_ID} case=release_keeps_changes"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_nested_rollback_to_inner() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "outer").unwrap();
txn.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn.savepoint(&cx, "inner").unwrap();
txn.write_page(&cx, p1, &vec![0x33; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "inner").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=nested_rollback_inner"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_nested_rollback_to_outer() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "outer").unwrap();
txn.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn.savepoint(&cx, "inner").unwrap();
txn.write_page(&cx, p1, &vec![0x33; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "outer").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=nested_rollback_outer"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_rollback_to_preserves_savepoint() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "sp1").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=rollback_to_preserves_savepoint"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_rollback_reclaims_allocated_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap(); txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p2, &vec![0x22; ps]).unwrap();
assert_eq!(p2.get(), p1.get() + 1, "Expected sequential allocation");
txn.rollback_to_savepoint(&cx, "sp1").unwrap();
let p3 = txn.allocate_page(&cx).unwrap();
assert_eq!(
p3.get(),
p2.get(),
"bead_id={BEAD_ID} case=rollback_reclaims_allocation: expected page {} but got {}",
p2.get(),
p3.get()
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_rollback_to_preserves_savepoint_multi() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.write_page(&cx, p1, &vec![0x33; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "sp1").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=rollback_to_preserves_savepoint_multi"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_freed_pages_restored_on_rollback() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0xAA; ps]).unwrap();
txn.write_page(&cx, p2, &vec![0xBB; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.free_page(&cx, p2).unwrap();
txn.rollback_to_savepoint(&cx, "sp1").unwrap();
let data = txn.get_page(&cx, p2).unwrap();
assert_eq!(
data.as_ref()[0],
0xBB,
"bead_id={BEAD_ID} case=freed_pages_restored"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_unknown_name_errors() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let result = txn.rollback_to_savepoint(&cx, "nonexistent");
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=rollback_to_unknown_savepoint_errors"
);
let result = txn.release_savepoint(&cx, "nonexistent");
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=release_unknown_savepoint_errors"
);
}
#[test]
fn test_savepoint_release_then_rollback_to_outer() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "outer").unwrap();
txn.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn.savepoint(&cx, "inner").unwrap();
txn.write_page(&cx, p1, &vec![0x33; ps]).unwrap();
txn.release_savepoint(&cx, "inner").unwrap();
txn.rollback_to_savepoint(&cx, "outer").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x11,
"bead_id={BEAD_ID} case=release_inner_then_rollback_outer"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_savepoint_commit_with_active_savepoints() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn2.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=commit_with_savepoints_persists_all"
);
}
#[test]
fn test_savepoint_full_rollback_clears_savepoints() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.savepoint(&cx, "sp2").unwrap();
txn.rollback(&cx).unwrap();
let result = txn.rollback_to_savepoint(&cx, "sp1");
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=full_rollback_clears_savepoints"
);
}
#[test]
fn test_savepoint_three_levels_deep() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x00; ps]).unwrap();
txn.savepoint(&cx, "L0").unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "L1").unwrap();
txn.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn.savepoint(&cx, "L2").unwrap();
txn.write_page(&cx, p1, &vec![0x33; ps]).unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x33,
"bead_id={BEAD_ID} case=3level_current"
);
txn.rollback_to_savepoint(&cx, "L2").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(data.as_ref()[0], 0x22, "bead_id={BEAD_ID} case=3level_L2");
txn.rollback_to_savepoint(&cx, "L1").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(data.as_ref()[0], 0x11, "bead_id={BEAD_ID} case=3level_L1");
txn.rollback_to_savepoint(&cx, "L0").unwrap();
let data = txn.get_page(&cx, p1).unwrap();
assert_eq!(data.as_ref()[0], 0x00, "bead_id={BEAD_ID} case=3level_L0");
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn2.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x00,
"bead_id={BEAD_ID} case=3level_committed"
);
}
use fsqlite_wal::checksum::{WAL_FRAME_HEADER_SIZE, WalChecksumTransform};
use fsqlite_wal::wal::WalAppendFrameRef;
use fsqlite_wal::{WalFile, WalSalts};
use serde_json::json;
use std::sync::{
Arc as StdArc, Condvar as StdCondvar, LazyLock as StdLazyLock, Mutex as StdMutex,
};
use std::time::Instant;
type WalFrame = (u32, Vec<u8>, u32);
type SharedFrames = StdArc<StdMutex<Vec<WalFrame>>>;
type SharedCounter = StdArc<StdMutex<usize>>;
type SharedLockLevels = StdArc<StdMutex<Vec<LockLevel>>>;
type SharedGate = StdArc<(StdMutex<bool>, StdCondvar)>;
static PARALLEL_WAL_LANE_TEST_LOCK: StdLazyLock<StdMutex<()>> =
StdLazyLock::new(|| StdMutex::new(()));
fn signal_gate(gate: &SharedGate) {
let (lock, cvar) = &**gate;
*lock.lock().unwrap() = true;
cvar.notify_all();
}
fn wait_gate(gate: &SharedGate) {
let (lock, cvar) = &**gate;
let mut ready = lock.lock().unwrap();
while !*ready {
ready = cvar.wait(ready).unwrap();
}
}
fn wait_gate_timeout(gate: &SharedGate, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
let (lock, cvar) = &**gate;
let mut ready = lock.lock().unwrap();
while !*ready {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return false;
}
let (next_ready, wait_result) = cvar.wait_timeout(ready, remaining).unwrap();
ready = next_ready;
if wait_result.timed_out() && !*ready {
return false;
}
}
true
}
fn prepared_batch_from_frame_refs(
frames: &[crate::traits::WalFrameRef<'_>],
corrupt_first_payload: bool,
) -> crate::traits::PreparedWalFrameBatch {
let frame_size = WAL_FRAME_HEADER_SIZE + frames[0].page_data.len();
let mut frame_bytes = Vec::with_capacity(frame_size * frames.len());
let mut frame_metas = Vec::with_capacity(frames.len());
for frame in frames {
frame_metas.push(crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
});
frame_bytes.extend_from_slice(&frame.page_number.to_be_bytes());
frame_bytes.extend_from_slice(&frame.db_size_if_commit.to_be_bytes());
frame_bytes.extend_from_slice(&[0_u8; WAL_FRAME_HEADER_SIZE - 8]);
frame_bytes.extend_from_slice(frame.page_data);
}
if corrupt_first_payload {
let payload_offset = WAL_FRAME_HEADER_SIZE;
frame_bytes[payload_offset] ^= 0xFF;
}
let mut prepared = crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: WAL_FRAME_HEADER_SIZE,
big_endian_checksum: false,
frame_metas,
checksum_transforms: Vec::new(),
frame_bytes,
last_commit_frame_offset: frames
.iter()
.enumerate()
.rev()
.find_map(|(offset, frame)| (frame.db_size_if_commit != 0).then_some(offset)),
finalized_for: None,
finalized_running_checksum: None,
};
prepared.recompute_checksum_transforms().unwrap();
prepared
}
fn lane_staged_batch_for_test(
batch_id: u64,
lane_id: u16,
frames: &[crate::traits::WalFrameRef<'_>],
) -> LaneStagedPreparedBatch {
LaneStagedPreparedBatch {
batch_id,
lane_id,
staged_frame_count: u32::try_from(frames.len()).unwrap_or(u32::MAX),
staging_elapsed_ns: u64::try_from(frames.len()).unwrap_or(u64::MAX) * 10,
shadow_verdict: ParallelWalShadowVerdict::NotRun,
payload: prepared_batch_from_frame_refs(frames, false),
}
}
const TRACK_C_BATCH_BENCH_BEAD_ID: &str = "bd-db300.3.1.4";
const TRACK_C_PUBLISH_WINDOW_INVENTORY_BEAD_ID: &str = "bd-db300.3.2.1";
const TRACK_C_PUBLISH_WINDOW_BENCH_BEAD_ID: &str = "bd-db300.3.2.3";
const TRACK_C_BATCH_BENCH_WARMUP_ITERS: usize = 5;
const TRACK_C_BATCH_BENCH_MEASURE_ITERS: usize = 25;
const TRACK_C_BATCH_BENCH_CASES: [(&str, usize); 3] = [
("page1_plus_1_new_page", 2),
("page1_plus_7_new_pages", 8),
("page1_plus_31_new_pages", 32),
];
const TRACK_C_PUBLISH_WINDOW_BENCH_WARMUP_ITERS: usize = 5;
const TRACK_C_PUBLISH_WINDOW_BENCH_MEASURE_ITERS: usize = 20;
const TRACK_C_PUBLISH_WINDOW_BENCH_CASES: [(&str, usize); 3] = [
("interior_only_1_dirty_page", 1),
("interior_only_7_dirty_pages", 7),
("interior_only_31_dirty_pages", 31),
];
const TRACK_C_METADATA_BENCH_BEAD_ID: &str = "bd-db300.3.3.3";
const TRACK_C_METADATA_BENCH_WARMUP_ITERS: usize = 5;
const TRACK_C_METADATA_BENCH_MEASURE_ITERS: usize = 25;
const TRACK_C_METADATA_BENCH_CASES: [(&str, usize); 3] = [
("interior_only_1_dirty_page", 1),
("interior_only_7_dirty_pages", 7),
("interior_only_31_dirty_pages", 31),
];
struct MockWalBackend {
frames: SharedFrames,
begin_calls: SharedCounter,
batch_calls: SharedCounter,
sync_calls: SharedCounter,
read_page_calls: SharedCounter,
}
impl MockWalBackend {
fn new() -> (Self, SharedFrames, SharedCounter, SharedCounter) {
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let begin_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let batch_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let sync_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let read_page_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
frames: StdArc::clone(&frames),
begin_calls: StdArc::clone(&begin_calls),
batch_calls: StdArc::clone(&batch_calls),
sync_calls,
read_page_calls,
},
frames,
begin_calls,
batch_calls,
)
}
fn new_with_sync_tracking() -> (
Self,
SharedFrames,
SharedCounter,
SharedCounter,
SharedCounter,
) {
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let begin_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let batch_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let sync_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let read_page_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
frames: StdArc::clone(&frames),
begin_calls: StdArc::clone(&begin_calls),
batch_calls: StdArc::clone(&batch_calls),
sync_calls: StdArc::clone(&sync_calls),
read_page_calls,
},
frames,
begin_calls,
batch_calls,
sync_calls,
)
}
fn with_shared_frames(frames: SharedFrames) -> (Self, SharedCounter, SharedCounter) {
let begin_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let batch_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let sync_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let read_page_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
frames,
begin_calls: StdArc::clone(&begin_calls),
batch_calls: StdArc::clone(&batch_calls),
sync_calls,
read_page_calls,
},
begin_calls,
batch_calls,
)
}
fn new_with_read_tracking() -> (
Self,
SharedFrames,
SharedCounter,
SharedCounter,
SharedCounter,
) {
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let begin_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let batch_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let sync_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let read_page_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
frames: StdArc::clone(&frames),
begin_calls: StdArc::clone(&begin_calls),
batch_calls: StdArc::clone(&batch_calls),
sync_calls,
read_page_calls: StdArc::clone(&read_page_calls),
},
frames,
begin_calls,
batch_calls,
read_page_calls,
)
}
}
struct FailingGroupCommitWalBackend {
append_frames_calls: SharedCounter,
}
impl FailingGroupCommitWalBackend {
fn new() -> (Self, SharedCounter) {
let append_frames_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
append_frames_calls: StdArc::clone(&append_frames_calls),
},
append_frames_calls,
)
}
}
struct FailingCheckpointWalBackend;
struct PreparedBatchObservedWalBackend {
frames: SharedFrames,
append_frames_calls: SharedCounter,
append_prepared_calls: SharedCounter,
prepare_lock_levels: SharedLockLevels,
append_lock_levels: SharedLockLevels,
observed_lock_level: Arc<Mutex<LockLevel>>,
}
impl PreparedBatchObservedWalBackend {
fn new(
observed_lock_level: Arc<Mutex<LockLevel>>,
) -> (
Self,
SharedFrames,
SharedCounter,
SharedCounter,
SharedLockLevels,
SharedLockLevels,
) {
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let append_frames_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let append_prepared_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let prepare_lock_levels: SharedLockLevels = StdArc::new(StdMutex::new(Vec::new()));
let append_lock_levels: SharedLockLevels = StdArc::new(StdMutex::new(Vec::new()));
(
Self {
frames: StdArc::clone(&frames),
append_frames_calls: StdArc::clone(&append_frames_calls),
append_prepared_calls: StdArc::clone(&append_prepared_calls),
prepare_lock_levels: StdArc::clone(&prepare_lock_levels),
append_lock_levels: StdArc::clone(&append_lock_levels),
observed_lock_level,
},
frames,
append_frames_calls,
append_prepared_calls,
prepare_lock_levels,
append_lock_levels,
)
}
}
struct ShadowCompareMismatchWalBackend {
frames: SharedFrames,
prepare_calls: SharedCounter,
append_frames_calls: SharedCounter,
append_prepared_calls: SharedCounter,
}
impl ShadowCompareMismatchWalBackend {
fn new() -> (
Self,
SharedFrames,
SharedCounter,
SharedCounter,
SharedCounter,
) {
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let prepare_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let append_frames_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let append_prepared_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
frames: StdArc::clone(&frames),
prepare_calls: StdArc::clone(&prepare_calls),
append_frames_calls: StdArc::clone(&append_frames_calls),
append_prepared_calls: StdArc::clone(&append_prepared_calls),
},
frames,
prepare_calls,
append_frames_calls,
append_prepared_calls,
)
}
}
struct BlockingFirstPrepareWalBackend {
frames: SharedFrames,
prepare_calls: SharedCounter,
first_prepare_entered: SharedGate,
release_first_prepare: SharedGate,
}
impl BlockingFirstPrepareWalBackend {
fn new() -> (Self, SharedFrames, SharedGate, SharedGate, SharedCounter) {
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let first_prepare_entered: SharedGate =
StdArc::new((StdMutex::new(false), StdCondvar::new()));
let release_first_prepare: SharedGate =
StdArc::new((StdMutex::new(false), StdCondvar::new()));
let prepare_calls: SharedCounter = StdArc::new(StdMutex::new(0));
(
Self {
frames: StdArc::clone(&frames),
prepare_calls: StdArc::clone(&prepare_calls),
first_prepare_entered: StdArc::clone(&first_prepare_entered),
release_first_prepare: StdArc::clone(&release_first_prepare),
},
frames,
first_prepare_entered,
release_first_prepare,
prepare_calls,
)
}
}
#[derive(Clone, Copy)]
enum TrackCBatchMode {
SingleFrame,
Batched,
}
impl TrackCBatchMode {
const fn as_str(self) -> &'static str {
match self {
Self::SingleFrame => "single_frame",
Self::Batched => "batch_append",
}
}
}
#[derive(Clone, Copy)]
enum TrackCPublishWindowMode {
InlinePrepareBaseline,
PreparedCandidate,
}
impl TrackCPublishWindowMode {
const fn as_str(self) -> &'static str {
match self {
Self::InlinePrepareBaseline => "inline_prepare_baseline",
Self::PreparedCandidate => "prepared_candidate",
}
}
}
#[derive(Clone, Copy)]
enum TrackCMetadataMode {
ForcedPageOneBaseline,
SemanticCleanupCandidate,
}
impl TrackCMetadataMode {
const fn as_str(self) -> &'static str {
match self {
Self::ForcedPageOneBaseline => "forced_page_one_baseline",
Self::SemanticCleanupCandidate => "semantic_cleanup_candidate",
}
}
}
struct TrackCBenchmarkWalBackend {
wal: WalFile<MemoryFile>,
mode: TrackCBatchMode,
}
struct TrackCPublishWindowBenchWalBackend {
wal: WalFile<BlockingObservedLockFile>,
mode: TrackCPublishWindowMode,
}
impl TrackCBenchmarkWalBackend {
fn new(vfs: &MemoryVfs, cx: &Cx, path: &std::path::Path, mode: TrackCBatchMode) -> Self {
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
let (file, _) = vfs.open(cx, Some(path), flags).unwrap();
let wal = WalFile::create(
cx,
file,
PageSize::DEFAULT.get(),
0,
WalSalts {
salt1: 0xDB30_0314,
salt2: 0xC1C1_C1C1,
},
)
.unwrap();
Self { wal, mode }
}
}
impl TrackCPublishWindowBenchWalBackend {
fn new(
vfs: &BlockingObservedLockVfs,
cx: &Cx,
path: &std::path::Path,
mode: TrackCPublishWindowMode,
) -> Self {
let flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
let (file, _) = vfs.open(cx, Some(path), flags).unwrap();
let wal = WalFile::create(
cx,
file,
PageSize::DEFAULT.get(),
0,
WalSalts {
salt1: 0xDB30_0323,
salt2: 0xC2C3_C2C3,
},
)
.unwrap();
Self { wal, mode }
}
}
impl crate::traits::WalBackend for TrackCBenchmarkWalBackend {
fn append_frame(
&mut self,
cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
self.wal
.append_frame(cx, page_number, page_data, db_size_if_commit)
}
fn append_frames(
&mut self,
cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
match self.mode {
TrackCBatchMode::SingleFrame => {
for frame in frames {
self.wal.append_frame(
cx,
frame.page_number,
frame.page_data,
frame.db_size_if_commit,
)?;
}
Ok(())
}
TrackCBatchMode::Batched => {
let wal_frames: Vec<_> = frames
.iter()
.map(|frame| WalAppendFrameRef {
page_number: frame.page_number,
page_data: frame.page_data,
db_size_if_commit: frame.db_size_if_commit,
})
.collect();
self.wal.append_frames(cx, &wal_frames)
}
}
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn committed_txn_count(&mut self, cx: &Cx) -> fsqlite_error::Result<u64> {
let Some(last_commit_frame) = self.wal.last_commit_frame(cx)? else {
return Ok(0);
};
let mut commit_count = 0_u64;
for frame_index in 0..=last_commit_frame {
if self.wal.read_frame_header(cx, frame_index)?.is_commit() {
commit_count = commit_count.saturating_add(1);
}
}
Ok(commit_count)
}
fn sync(&mut self, cx: &Cx) -> fsqlite_error::Result<()> {
self.wal.sync(cx, SyncFlags::NORMAL)
}
fn frame_count(&self) -> usize {
self.wal.frame_count()
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
let total_frames = u32::try_from(self.wal.frame_count()).unwrap_or(u32::MAX);
Ok(crate::traits::CheckpointResult {
total_frames,
frames_backfilled: 0,
completed: false,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
impl crate::traits::WalBackend for TrackCPublishWindowBenchWalBackend {
fn append_frame(
&mut self,
cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
self.wal.file_mut().lock(cx, LockLevel::Exclusive)?;
let append_result =
self.wal
.append_frame(cx, page_number, page_data, db_size_if_commit);
let unlock_result = self.wal.file_mut().unlock(cx, LockLevel::None);
match (append_result, unlock_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(append_error), Err(unlock_error)) => Err(FrankenError::internal(format!(
"publish-window WAL append failed and unlock failed: append={append_error}; unlock={unlock_error}"
))),
}
}
fn append_frames(
&mut self,
cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
let wal_frames: Vec<_> = frames
.iter()
.map(|frame| WalAppendFrameRef {
page_number: frame.page_number,
page_data: frame.page_data,
db_size_if_commit: frame.db_size_if_commit,
})
.collect();
self.wal.file_mut().lock(cx, LockLevel::Exclusive)?;
let append_result = self.wal.append_frames(cx, &wal_frames);
let unlock_result = self.wal.file_mut().unlock(cx, LockLevel::None);
match (append_result, unlock_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(append_error), Err(unlock_error)) => Err(FrankenError::internal(format!(
"publish-window WAL append failed and unlock failed: append={append_error}; unlock={unlock_error}"
))),
}
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
if matches!(self.mode, TrackCPublishWindowMode::InlinePrepareBaseline) {
return Ok(None);
}
if frames.is_empty() {
return Ok(None);
}
let wal_frames: Vec<_> = frames
.iter()
.map(|frame| WalAppendFrameRef {
page_number: frame.page_number,
page_data: frame.page_data,
db_size_if_commit: frame.db_size_if_commit,
})
.collect();
let frame_size = WAL_FRAME_HEADER_SIZE + wal_frames[0].page_data.len();
let frame_metas = wal_frames
.iter()
.map(|frame| crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
})
.collect();
let frame_bytes = self.wal.prepare_frame_bytes(&wal_frames)?;
let checksum_transforms = wal_frames
.iter()
.enumerate()
.map(|(index, _)| {
let frame_start = index
.checked_mul(frame_size)
.expect("frame start fits usize");
let frame_end = frame_start
.checked_add(frame_size)
.expect("frame end fits usize");
let transform = WalChecksumTransform::for_wal_frame(
&frame_bytes[frame_start..frame_end],
self.wal.page_size(),
self.wal.big_endian_checksum(),
)?;
Ok(transform)
})
.collect::<Result<Vec<_>>>()?;
Ok(Some(crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: WAL_FRAME_HEADER_SIZE,
big_endian_checksum: self.wal.big_endian_checksum(),
frame_metas,
checksum_transforms,
frame_bytes,
last_commit_frame_offset: frames
.iter()
.enumerate()
.rev()
.find_map(|(offset, frame)| (frame.db_size_if_commit != 0).then_some(offset)),
finalized_for: None,
finalized_running_checksum: None,
}))
}
fn append_prepared_frames(
&mut self,
cx: &Cx,
prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
let checksum_transforms: Vec<_> = prepared
.checksum_transforms
.iter()
.map(|transform| WalChecksumTransform {
a11: transform.a11,
a12: transform.a12,
a21: transform.a21,
a22: transform.a22,
c1: transform.c1,
c2: transform.c2,
})
.collect();
self.wal.file_mut().lock(cx, LockLevel::Exclusive)?;
let append_result = self.wal.append_prepared_frame_bytes(
cx,
&mut prepared.frame_bytes,
&checksum_transforms,
);
let unlock_result = self.wal.file_mut().unlock(cx, LockLevel::None);
match (append_result, unlock_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(append_error), Err(unlock_error)) => Err(FrankenError::internal(format!(
"publish-window prepared WAL append failed and unlock failed: append={append_error}; unlock={unlock_error}"
))),
}
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn committed_txn_count(&mut self, cx: &Cx) -> fsqlite_error::Result<u64> {
let Some(last_commit_frame) = self.wal.last_commit_frame(cx)? else {
return Ok(0);
};
let mut commit_count = 0_u64;
for frame_index in 0..=last_commit_frame {
if self.wal.read_frame_header(cx, frame_index)?.is_commit() {
commit_count = commit_count.saturating_add(1);
}
}
Ok(commit_count)
}
fn sync(&mut self, cx: &Cx) -> fsqlite_error::Result<()> {
self.wal.sync(cx, SyncFlags::NORMAL)
}
fn frame_count(&self) -> usize {
self.wal.frame_count()
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
let total_frames = u32::try_from(self.wal.frame_count()).unwrap_or(u32::MAX);
Ok(crate::traits::CheckpointResult {
total_frames,
frames_backfilled: 0,
completed: false,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
impl crate::traits::WalBackend for FailingCheckpointWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> Result<()> {
Ok(())
}
fn read_page(&mut self, _cx: &Cx, _page_number: u32) -> Result<Option<Vec<u8>>> {
Ok(None)
}
fn sync(&mut self, _cx: &Cx) -> Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
1
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> Result<crate::traits::CheckpointResult> {
Err(FrankenError::BusyRecovery)
}
}
fn track_c_prepared_commit(
mode: TrackCBatchMode,
dirty_pages: usize,
) -> (Cx, SimpleTransaction<MemoryVfs>) {
assert!(dirty_pages >= 2);
let cx = Cx::new();
let vfs = MemoryVfs::new();
let db_path = PathBuf::from(format!(
"/track_c_batch_commit_{}_{}.db",
mode.as_str(),
dirty_pages
));
let wal_path = PathBuf::from(format!(
"/track_c_batch_commit_{}_{}.db-wal",
mode.as_str(),
dirty_pages
));
let pager = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
let backend = TrackCBenchmarkWalBackend::new(&vfs, &cx, &wal_path, mode);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_bytes = PageSize::DEFAULT.as_usize();
txn.write_page(&cx, PageNumber::ONE, &vec![0xA1; page_bytes])
.unwrap();
for page_idx in 1..dirty_pages {
let page_no = txn.allocate_page(&cx).unwrap();
let fill = u8::try_from((page_idx % 251) + 1).unwrap();
txn.write_page(&cx, page_no, &vec![fill; page_bytes])
.unwrap();
}
(cx, txn)
}
fn track_c_measure_commit_ns(mode: TrackCBatchMode, dirty_pages: usize) -> Vec<u64> {
let total_iters = TRACK_C_BATCH_BENCH_WARMUP_ITERS + TRACK_C_BATCH_BENCH_MEASURE_ITERS;
let mut samples = Vec::with_capacity(TRACK_C_BATCH_BENCH_MEASURE_ITERS);
for iter_idx in 0..total_iters {
let (cx, mut txn) = track_c_prepared_commit(mode, dirty_pages);
let started = Instant::now();
txn.commit(&cx).unwrap();
if iter_idx >= TRACK_C_BATCH_BENCH_WARMUP_ITERS {
let elapsed_ns = u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX);
samples.push(elapsed_ns);
}
}
samples
}
fn track_c_metadata_seed_existing_pages(
pager: &SimplePager<MemoryVfs>,
cx: &Cx,
interior_dirty_pages: usize,
) {
assert!(interior_dirty_pages > 0);
let mut txn = pager.begin(cx, TransactionMode::Immediate).unwrap();
let page_bytes = PageSize::DEFAULT.as_usize();
for page_idx in 0..interior_dirty_pages {
let page_no = txn.allocate_page(cx).unwrap();
let fill = u8::try_from((page_idx % 251) + 1).unwrap();
txn.write_page(cx, page_no, &vec![fill; page_bytes])
.unwrap();
}
txn.commit(cx).unwrap();
}
fn track_c_metadata_apply_workload(
txn: &mut SimpleTransaction<MemoryVfs>,
cx: &Cx,
interior_dirty_pages: usize,
mode: TrackCMetadataMode,
) {
let page_bytes = PageSize::DEFAULT.as_usize();
for page_idx in 0..interior_dirty_pages {
let page_no = PageNumber::new(u32::try_from(page_idx + 2).unwrap()).unwrap();
let fill = u8::try_from(((page_idx + 17) % 251) + 1).unwrap();
txn.write_page(cx, page_no, &vec![fill; page_bytes])
.unwrap();
}
if matches!(mode, TrackCMetadataMode::ForcedPageOneBaseline) {
let page_one = txn.get_page(cx, PageNumber::ONE).unwrap().into_vec();
txn.write_page(cx, PageNumber::ONE, &page_one).unwrap();
}
}
fn track_c_metadata_prepared_commit(
mode: TrackCMetadataMode,
interior_dirty_pages: usize,
) -> (Cx, SimpleTransaction<MemoryVfs>) {
assert!(interior_dirty_pages > 0);
let cx = Cx::new();
let vfs = MemoryVfs::new();
let db_path = PathBuf::from(format!(
"/track_c_metadata_cleanup_{}_{}.db",
mode.as_str(),
interior_dirty_pages
));
let wal_path = PathBuf::from(format!(
"/track_c_metadata_cleanup_{}_{}.db-wal",
mode.as_str(),
interior_dirty_pages
));
let pager = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
track_c_metadata_seed_existing_pages(&pager, &cx, interior_dirty_pages);
let backend =
TrackCBenchmarkWalBackend::new(&vfs, &cx, &wal_path, TrackCBatchMode::Batched);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
track_c_metadata_apply_workload(&mut txn, &cx, interior_dirty_pages, mode);
(cx, txn)
}
fn track_c_metadata_measure_commit_ns(
mode: TrackCMetadataMode,
interior_dirty_pages: usize,
) -> Vec<u64> {
let total_iters =
TRACK_C_METADATA_BENCH_WARMUP_ITERS + TRACK_C_METADATA_BENCH_MEASURE_ITERS;
let mut samples = Vec::with_capacity(TRACK_C_METADATA_BENCH_MEASURE_ITERS);
for iter_idx in 0..total_iters {
let (cx, mut txn) = track_c_metadata_prepared_commit(mode, interior_dirty_pages);
let started = Instant::now();
txn.commit(&cx).unwrap();
if iter_idx >= TRACK_C_METADATA_BENCH_WARMUP_ITERS {
let elapsed_ns = u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX);
samples.push(elapsed_ns);
}
}
samples
}
fn track_c_metadata_capture_frame_pages(
mode: TrackCMetadataMode,
interior_dirty_pages: usize,
) -> Vec<u32> {
assert!(interior_dirty_pages > 0);
let cx = Cx::new();
let vfs = MemoryVfs::new();
let db_path = PathBuf::from(format!(
"/track_c_metadata_capture_{}_{}.db",
mode.as_str(),
interior_dirty_pages
));
let pager = SimplePager::open(vfs, &db_path, PageSize::DEFAULT).unwrap();
track_c_metadata_seed_existing_pages(&pager, &cx, interior_dirty_pages);
let (backend, frames, _begin_calls, _batch_calls) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
frames.lock().unwrap().clear();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
track_c_metadata_apply_workload(&mut txn, &cx, interior_dirty_pages, mode);
txn.commit(&cx).unwrap();
frames
.lock()
.unwrap()
.iter()
.map(|(page_number, _, _)| *page_number)
.collect()
}
fn track_c_seed_existing_pages_blocking(
pager: &SimplePager<BlockingObservedLockVfs>,
cx: &Cx,
page_count: usize,
) {
assert!(page_count > 0);
let mut txn = pager.begin(cx, TransactionMode::Immediate).unwrap();
let page_bytes = PageSize::DEFAULT.as_usize();
for page_idx in 0..page_count {
let page_no = txn.allocate_page(cx).unwrap();
let fill = u8::try_from((page_idx % 251) + 1).unwrap();
txn.write_page(cx, page_no, &vec![fill; page_bytes])
.unwrap();
}
txn.commit(cx).unwrap();
}
fn track_c_write_existing_page_range(
txn: &mut SimpleTransaction<BlockingObservedLockVfs>,
cx: &Cx,
start_page_no: u32,
page_count: usize,
fill_offset: usize,
) {
let page_bytes = PageSize::DEFAULT.as_usize();
for page_idx in 0..page_count {
let page_no =
PageNumber::new(start_page_no + u32::try_from(page_idx).unwrap()).unwrap();
let fill = u8::try_from(((page_idx + fill_offset) % 251) + 1).unwrap();
txn.write_page(cx, page_no, &vec![fill; page_bytes])
.unwrap();
}
}
fn track_c_publish_window_prepared_commit(
mode: TrackCPublishWindowMode,
dirty_pages: usize,
) -> (
Cx,
SimpleTransaction<BlockingObservedLockVfs>,
BlockingObservedLockVfs,
) {
assert!(dirty_pages > 0);
let cx = Cx::new();
let vfs = BlockingObservedLockVfs::new();
let db_path = PathBuf::from(format!(
"/track_c_publish_window_hold_{}_{}.db",
mode.as_str(),
dirty_pages
));
let wal_path = PathBuf::from(format!(
"/track_c_publish_window_hold_{}_{}.db-wal",
mode.as_str(),
dirty_pages
));
let seed_pager = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
track_c_seed_existing_pages_blocking(&seed_pager, &cx, dirty_pages);
drop(seed_pager);
let pager = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
let backend = TrackCPublishWindowBenchWalBackend::new(&vfs, &cx, &wal_path, mode);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
vfs.clear_exclusive_metrics();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
track_c_write_existing_page_range(&mut txn, &cx, 2, dirty_pages, 37);
(cx, txn, vfs)
}
fn track_c_measure_publish_window_hold_ns(
mode: TrackCPublishWindowMode,
dirty_pages: usize,
) -> Vec<u64> {
let total_iters =
TRACK_C_PUBLISH_WINDOW_BENCH_WARMUP_ITERS + TRACK_C_PUBLISH_WINDOW_BENCH_MEASURE_ITERS;
let mut samples = Vec::with_capacity(TRACK_C_PUBLISH_WINDOW_BENCH_MEASURE_ITERS);
for iter_idx in 0..total_iters {
let (cx, mut txn, vfs) = track_c_publish_window_prepared_commit(mode, dirty_pages);
txn.commit(&cx).unwrap();
let hold_ns = *vfs.exclusive_hold_samples_ns().last().unwrap_or(&0);
if iter_idx >= TRACK_C_PUBLISH_WINDOW_BENCH_WARMUP_ITERS {
samples.push(hold_ns);
}
}
samples
}
fn track_c_open_contending_pagers(
mode: TrackCPublishWindowMode,
dirty_pages: usize,
) -> (
BlockingObservedLockVfs,
SimplePager<BlockingObservedLockVfs>,
SimplePager<BlockingObservedLockVfs>,
) {
assert!(dirty_pages > 0);
let vfs = BlockingObservedLockVfs::new();
let db_path = PathBuf::from(format!(
"/track_c_publish_window_contention_{}_{}.db",
mode.as_str(),
dirty_pages
));
let wal_path = PathBuf::from(format!(
"/track_c_publish_window_contention_{}_{}.db-wal",
mode.as_str(),
dirty_pages
));
let seed_cx = Cx::new();
let seed_pager = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
track_c_seed_existing_pages_blocking(&seed_pager, &seed_cx, dirty_pages.saturating_mul(2));
drop(seed_pager);
let pager_a = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
let pager_b = SimplePager::open(vfs.clone(), &db_path, PageSize::DEFAULT).unwrap();
let cx_a = Cx::new();
let cx_b = Cx::new();
pager_a
.set_wal_backend(Box::new(TrackCPublishWindowBenchWalBackend::new(
&vfs, &cx_a, &wal_path, mode,
)))
.unwrap();
pager_b
.set_wal_backend(Box::new(TrackCPublishWindowBenchWalBackend::new(
&vfs, &cx_b, &wal_path, mode,
)))
.unwrap();
pager_a.set_journal_mode(&cx_a, JournalMode::Wal).unwrap();
pager_b.set_journal_mode(&cx_b, JournalMode::Wal).unwrap();
vfs.clear_exclusive_metrics();
(vfs, pager_a, pager_b)
}
fn track_c_measure_competing_writer_stall_ns(
mode: TrackCPublishWindowMode,
dirty_pages: usize,
) -> Vec<u64> {
let total_iters =
TRACK_C_PUBLISH_WINDOW_BENCH_WARMUP_ITERS + TRACK_C_PUBLISH_WINDOW_BENCH_MEASURE_ITERS;
let mut samples = Vec::with_capacity(TRACK_C_PUBLISH_WINDOW_BENCH_MEASURE_ITERS);
for iter_idx in 0..total_iters {
let (vfs, pager_a, pager_b) = track_c_open_contending_pagers(mode, dirty_pages);
let writer_a = std::thread::spawn(move || {
let cx = Cx::new();
let mut txn = pager_a.begin(&cx, TransactionMode::Immediate).unwrap();
track_c_write_existing_page_range(&mut txn, &cx, 2, dirty_pages, 53);
txn.commit(&cx).unwrap();
});
vfs.wait_for_exclusive_acquisitions(1);
let cx_b = Cx::new();
let mut txn_b = pager_b.begin(&cx_b, TransactionMode::Immediate).unwrap();
let contender_start_page = u32::try_from(dirty_pages).unwrap() + 2;
track_c_write_existing_page_range(
&mut txn_b,
&cx_b,
contender_start_page,
dirty_pages,
97,
);
txn_b.commit(&cx_b).unwrap();
writer_a.join().unwrap();
let stall_ns = vfs
.exclusive_wait_samples_ns()
.into_iter()
.max()
.unwrap_or(0);
if iter_idx >= TRACK_C_PUBLISH_WINDOW_BENCH_WARMUP_ITERS {
samples.push(stall_ns);
}
}
samples
}
fn track_c_percentile_ns(sorted_samples: &[u64], percentile: usize) -> u64 {
let last_idx = sorted_samples.len().saturating_sub(1);
let rank = last_idx.saturating_mul(percentile).div_ceil(100);
sorted_samples[rank]
}
fn track_c_sample_summary(samples: &[u64]) -> serde_json::Value {
let mut sorted = samples.to_vec();
sorted.sort_unstable();
let total_ns: u128 = sorted.iter().map(|value| u128::from(*value)).sum();
let mean_ns = (total_ns as f64) / (sorted.len() as f64);
json!({
"sample_count": sorted.len(),
"min_ns": sorted.first().copied().unwrap_or(0),
"median_ns": track_c_percentile_ns(&sorted, 50),
"p95_ns": track_c_percentile_ns(&sorted, 95),
"max_ns": sorted.last().copied().unwrap_or(0),
"mean_ns": mean_ns,
"raw_samples_ns": sorted,
})
}
impl crate::traits::WalBackend for MockWalBackend {
fn begin_transaction(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
let mut begin_calls = self.begin_calls.lock().unwrap();
*begin_calls += 1;
Ok(())
}
fn append_frame(
&mut self,
_cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
self.frames
.lock()
.unwrap()
.push((page_number, page_data.to_vec(), db_size_if_commit));
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
let mut batch_calls = self.batch_calls.lock().unwrap();
*batch_calls += 1;
drop(batch_calls);
let mut written = self.frames.lock().unwrap();
for frame in frames {
written.push((
frame.page_number,
frame.page_data.to_vec(),
frame.db_size_if_commit,
));
}
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
*self.read_page_calls.lock().unwrap() += 1;
let frames = self.frames.lock().unwrap();
let result = frames
.iter()
.rev()
.find(|(pn, _, _)| *pn == page_number)
.map(|(_, data, _)| data.clone());
drop(frames);
Ok(result)
}
fn committed_txns_since_page(
&mut self,
_cx: &Cx,
page_number: u32,
) -> fsqlite_error::Result<u64> {
let frames = self.frames.lock().unwrap();
let last_page_frame = frames.iter().rposition(|(pn, _, _)| *pn == page_number);
let Some(last_page_frame) = last_page_frame else {
return Ok(frames
.iter()
.filter(|(_, _, db_size_if_commit)| *db_size_if_commit > 0)
.count() as u64);
};
let mut page_commit_seen = false;
let mut committed_txns_after_page = 0_u64;
for (frame_index, (_, _, db_size_if_commit)) in frames.iter().enumerate() {
if *db_size_if_commit == 0 {
continue;
}
if !page_commit_seen && frame_index >= last_page_frame {
page_commit_seen = true;
continue;
}
if page_commit_seen {
committed_txns_after_page = committed_txns_after_page.saturating_add(1);
}
}
Ok(committed_txns_after_page)
}
fn committed_txn_count(&mut self, _cx: &Cx) -> fsqlite_error::Result<u64> {
Ok(self
.frames
.lock()
.unwrap()
.iter()
.filter(|(_, _, db_size_if_commit)| *db_size_if_commit > 0)
.count() as u64)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
*self.sync_calls.lock().unwrap() += 1;
Ok(())
}
fn frame_count(&self) -> usize {
self.frames.lock().unwrap().len()
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
let total_frames = u32::try_from(self.frames.lock().unwrap().len()).map_err(|_| {
fsqlite_error::FrankenError::internal("mock wal frame count exceeds u32")
})?;
Ok(crate::traits::CheckpointResult {
total_frames,
frames_backfilled: total_frames,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
impl crate::traits::WalBackend for FailingGroupCommitWalBackend {
fn begin_transaction(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Err(FrankenError::internal(
"forced single-frame group commit failure",
))
}
fn append_frames(
&mut self,
_cx: &Cx,
_frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
let mut append_frames_calls = self.append_frames_calls.lock().unwrap();
*append_frames_calls += 1;
drop(append_frames_calls);
std::thread::sleep(std::time::Duration::from_millis(1));
Err(FrankenError::internal(
"forced batched group commit append failure",
))
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn committed_txn_count(&mut self, _cx: &Cx) -> fsqlite_error::Result<u64> {
Ok(0)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
0
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
impl crate::traits::WalBackend for PreparedBatchObservedWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
self.frames
.lock()
.unwrap()
.push((page_number, page_data.to_vec(), db_size_if_commit));
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
*self.append_frames_calls.lock().unwrap() += 1;
let mut written = self.frames.lock().unwrap();
for frame in frames {
written.push((
frame.page_number,
frame.page_data.to_vec(),
frame.db_size_if_commit,
));
}
Ok(())
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
self.prepare_lock_levels
.lock()
.unwrap()
.push(*self.observed_lock_level.lock().unwrap());
if frames.is_empty() {
return Ok(None);
}
let frame_size =
fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE + frames[0].page_data.len();
let mut frame_bytes = Vec::with_capacity(frame_size * frames.len());
let mut frame_metas = Vec::with_capacity(frames.len());
let mut checksum_transforms = Vec::with_capacity(frames.len());
for frame in frames {
frame_metas.push(crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
});
checksum_transforms.push(crate::traits::PreparedWalChecksumTransform {
a11: 0,
a12: 0,
a21: 0,
a22: 0,
c1: 0,
c2: 0,
});
frame_bytes
.extend_from_slice(&[0_u8; fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE]);
frame_bytes.extend_from_slice(frame.page_data);
}
Ok(Some(crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE,
big_endian_checksum: false,
frame_metas,
checksum_transforms,
frame_bytes,
last_commit_frame_offset: frames
.iter()
.enumerate()
.rev()
.find_map(|(offset, frame)| (frame.db_size_if_commit != 0).then_some(offset)),
finalized_for: None,
finalized_running_checksum: None,
}))
}
fn append_prepared_frames(
&mut self,
_cx: &Cx,
prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
self.append_lock_levels
.lock()
.unwrap()
.push(*self.observed_lock_level.lock().unwrap());
*self.append_prepared_calls.lock().unwrap() += 1;
let mut written = self.frames.lock().unwrap();
for frame in prepared.frame_refs() {
written.push((
frame.page_number,
frame.page_data.to_vec(),
frame.db_size_if_commit,
));
}
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
let frames = self.frames.lock().unwrap();
Ok(frames
.iter()
.rev()
.find(|(pn, _, _)| *pn == page_number)
.map(|(_, data, _)| data.clone()))
}
fn committed_txn_count(&mut self, _cx: &Cx) -> fsqlite_error::Result<u64> {
Ok(self
.frames
.lock()
.unwrap()
.iter()
.filter(|(_, _, db_size_if_commit)| *db_size_if_commit > 0)
.count() as u64)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
self.frames.lock().unwrap().len()
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
let total_frames = u32::try_from(self.frames.lock().unwrap().len()).map_err(|_| {
fsqlite_error::FrankenError::internal("observed wal frame count exceeds u32")
})?;
Ok(crate::traits::CheckpointResult {
total_frames,
frames_backfilled: total_frames,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
impl crate::traits::WalBackend for ShadowCompareMismatchWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
self.frames
.lock()
.unwrap()
.push((page_number, page_data.to_vec(), db_size_if_commit));
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
*self.append_frames_calls.lock().unwrap() += 1;
let mut written = self.frames.lock().unwrap();
for frame in frames {
written.push((
frame.page_number,
frame.page_data.to_vec(),
frame.db_size_if_commit,
));
}
Ok(())
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
let mut prepare_calls = self.prepare_calls.lock().unwrap();
*prepare_calls += 1;
let call_index = *prepare_calls;
drop(prepare_calls);
if frames.is_empty() {
return Ok(None);
}
if call_index == 1 {
Ok(Some(prepared_batch_from_frame_refs(frames, true)))
} else {
Ok(None)
}
}
fn append_prepared_frames(
&mut self,
_cx: &Cx,
prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
*self.append_prepared_calls.lock().unwrap() += 1;
let mut written = self.frames.lock().unwrap();
for frame in prepared.frame_refs() {
written.push((
frame.page_number,
frame.page_data.to_vec(),
frame.db_size_if_commit,
));
}
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
let frames = self.frames.lock().unwrap();
Ok(frames
.iter()
.rev()
.find(|(pn, _, _)| *pn == page_number)
.map(|(_, data, _)| data.clone()))
}
fn committed_txn_count(&mut self, _cx: &Cx) -> fsqlite_error::Result<u64> {
Ok(self
.frames
.lock()
.unwrap()
.iter()
.filter(|(_, _, db_size_if_commit)| *db_size_if_commit > 0)
.count() as u64)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
self.frames.lock().unwrap().len()
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
let total_frames = u32::try_from(self.frames.lock().unwrap().len()).unwrap_or(u32::MAX);
Ok(crate::traits::CheckpointResult {
total_frames,
frames_backfilled: total_frames,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
impl crate::traits::WalBackend for BlockingFirstPrepareWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
page_number: u32,
page_data: &[u8],
db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
self.frames
.lock()
.unwrap()
.push((page_number, page_data.to_vec(), db_size_if_commit));
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
let mut written = self.frames.lock().unwrap();
for frame in frames {
written.push((
frame.page_number,
frame.page_data.to_vec(),
frame.db_size_if_commit,
));
}
Ok(())
}
fn prepare_append_frames(
&self,
_frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
let call_index = {
let mut calls = self.prepare_calls.lock().unwrap();
*calls += 1;
*calls
};
if call_index == 1 {
signal_gate(&self.first_prepare_entered);
wait_gate(&self.release_first_prepare);
}
Ok(None)
}
fn read_page(
&mut self,
_cx: &Cx,
page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
let frames = self.frames.lock().unwrap();
Ok(frames
.iter()
.rev()
.find(|(pn, _, _)| *pn == page_number)
.map(|(_, data, _)| data.clone()))
}
fn committed_txn_count(&mut self, _cx: &Cx) -> fsqlite_error::Result<u64> {
Ok(self
.frames
.lock()
.unwrap()
.iter()
.filter(|(_, _, db_size_if_commit)| *db_size_if_commit > 0)
.count() as u64)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
self.frames.lock().unwrap().len()
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
let total_frames = u32::try_from(self.frames.lock().unwrap().len()).unwrap_or(u32::MAX);
Ok(crate::traits::CheckpointResult {
total_frames,
frames_backfilled: total_frames,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
fn wal_pager() -> (SimplePager<MemoryVfs>, SharedFrames) {
let (pager, frames, _, _) = wal_pager_with_tracking();
(pager, frames)
}
fn wal_pager_with_tracking() -> (
SimplePager<MemoryVfs>,
SharedFrames,
SharedCounter,
SharedCounter,
) {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/wal_test.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, begin_calls, batch_calls) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
(pager, frames, begin_calls, batch_calls)
}
fn wal_pager_with_read_tracking() -> (
SimplePager<MemoryVfs>,
SharedFrames,
SharedCounter,
SharedCounter,
SharedCounter,
) {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/wal_read_tracking.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, begin_calls, batch_calls, read_page_calls) =
MockWalBackend::new_with_read_tracking();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
(pager, frames, begin_calls, batch_calls, read_page_calls)
}
fn wal_pager_pair_with_shared_backend()
-> (SimplePager<MemoryVfs>, SimplePager<MemoryVfs>, SharedFrames) {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/wal_shared_refresh.db");
let pager1 = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let pager2 = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let frames: SharedFrames = StdArc::new(StdMutex::new(Vec::new()));
let (backend1, _, _) = MockWalBackend::with_shared_frames(StdArc::clone(&frames));
let (backend2, _, _) = MockWalBackend::with_shared_frames(StdArc::clone(&frames));
pager1.set_wal_backend(Box::new(backend1)).unwrap();
pager2.set_wal_backend(Box::new(backend2)).unwrap();
pager1.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager2.set_journal_mode(&cx, JournalMode::Wal).unwrap();
(pager1, pager2, frames)
}
#[test]
fn test_journal_mode_default_is_delete() {
let (pager, _) = test_pager();
assert_eq!(
pager.journal_mode(),
JournalMode::Delete,
"bead_id={BEAD_ID} case=default_journal_mode"
);
}
#[test]
fn test_set_journal_mode_wal_requires_backend() {
let (pager, _) = test_pager();
let cx = Cx::new();
let result = pager.set_journal_mode(&cx, JournalMode::Wal);
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=wal_requires_backend"
);
}
#[test]
fn test_set_journal_mode_wal_with_backend() {
let (pager, _) = test_pager();
let cx = Cx::new();
let (backend, _frames, _, _) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
let mode = pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
assert_eq!(
mode,
JournalMode::Wal,
"bead_id={BEAD_ID} case=wal_mode_set"
);
assert_eq!(
pager.journal_mode(),
JournalMode::Wal,
"bead_id={BEAD_ID} case=wal_mode_persisted"
);
}
#[test]
fn test_set_journal_mode_updates_header_version_bytes() {
let (pager, _) = test_pager();
let cx = Cx::new();
{
let inner = pager.inner.lock().unwrap();
let mut page1 = vec![0u8; inner.page_size.as_usize()];
let n = inner.db_file.read(&cx, &mut page1, 0).unwrap();
assert!(n >= DATABASE_HEADER_SIZE);
assert_eq!(page1[18], 1, "bead_id={BEAD_ID} case=default_write_version");
assert_eq!(page1[19], 1, "bead_id={BEAD_ID} case=default_read_version");
}
let (backend, _frames, _, _) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
{
let inner = pager.inner.lock().unwrap();
let mut page1 = vec![0u8; inner.page_size.as_usize()];
let n = inner.db_file.read(&cx, &mut page1, 0).unwrap();
assert!(n >= DATABASE_HEADER_SIZE);
assert_eq!(page1[18], 2, "bead_id={BEAD_ID} case=wal_write_version");
assert_eq!(page1[19], 2, "bead_id={BEAD_ID} case=wal_read_version");
}
pager.set_journal_mode(&cx, JournalMode::Delete).unwrap();
{
let inner = pager.inner.lock().unwrap();
let mut page1 = vec![0u8; inner.page_size.as_usize()];
let n = inner.db_file.read(&cx, &mut page1, 0).unwrap();
assert!(n >= DATABASE_HEADER_SIZE);
assert_eq!(page1[18], 1, "bead_id={BEAD_ID} case=delete_write_version");
assert_eq!(page1[19], 1, "bead_id={BEAD_ID} case=delete_read_version");
}
}
#[test]
fn test_set_journal_mode_blocked_during_write() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let (backend, _frames, _, _) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
let result = pager.set_journal_mode(&cx, JournalMode::Wal);
assert!(
result.is_err(),
"bead_id={BEAD_ID} case=mode_switch_blocked_during_write"
);
}
#[test]
fn test_set_journal_mode_same_mode_succeeds_during_reader() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let _reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let mode = pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
assert_eq!(mode, JournalMode::Wal);
}
#[test]
fn test_checkpoint_busy_with_active_reader() {
init_publication_test_tracing();
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let err = pager
.checkpoint(&cx, crate::traits::CheckpointMode::Passive)
.expect_err("checkpoint should be blocked by active reader");
assert!(matches!(err, FrankenError::Busy));
drop(reader);
let result = pager
.checkpoint(&cx, crate::traits::CheckpointMode::Passive)
.expect("checkpoint should succeed after reader closes");
assert_eq!(result.total_frames, 0);
}
#[test]
fn test_checkpoint_busy_with_active_writer() {
init_publication_test_tracing();
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let _writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let err = pager
.checkpoint(&cx, crate::traits::CheckpointMode::Passive)
.expect_err("checkpoint should be blocked by active writer");
assert!(matches!(err, FrankenError::Busy));
}
#[test]
fn test_checkpoint_coordination_blocks_new_writer_when_checkpoint_active() {
init_publication_test_tracing();
let (pager, _frames) = wal_pager();
let cx = Cx::new();
{
let mut inner = pager
.inner
.lock()
.expect("checkpoint coordination test lock poisoned");
inner.checkpoint_active = true;
}
let err = match pager.begin(&cx, TransactionMode::Concurrent) {
Ok(_) => panic!("checkpoint-active pager should reject foreground writer begin"),
Err(err) => err,
};
assert!(
matches!(err, FrankenError::Busy),
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_active_blocks_writer_begin error={err}"
);
}
#[test]
fn test_checkpoint_coordination_reports_idle_queue_on_success() {
init_publication_test_tracing();
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let result = pager
.checkpoint(&cx, crate::traits::CheckpointMode::Passive)
.expect("idle checkpoint should succeed");
let queue_snapshot =
checkpoint_coordination_queue_snapshot(&pager_group_commit_queue(&pager));
assert_eq!(
queue_snapshot.queue_phase, "filling",
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_success_queue_phase"
);
assert_eq!(
queue_snapshot.pending_batch_count, 0,
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_success_queue_pending_count"
);
assert_eq!(
result.total_frames, 0,
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_success_total_frames"
);
}
#[test]
fn test_checkpoint_empty_wal_fast_path_does_not_extract_backend() {
init_publication_test_tracing();
let (pager, _frames) = wal_pager();
let cx = Cx::new();
for mode in [
crate::traits::CheckpointMode::Passive,
crate::traits::CheckpointMode::Full,
crate::traits::CheckpointMode::Restart,
crate::traits::CheckpointMode::Truncate,
] {
let result = pager
.checkpoint(&cx, mode)
.expect("empty-WAL checkpoint should succeed as a no-op");
assert_eq!(result.total_frames, 0);
assert_eq!(result.frames_backfilled, 0);
assert!(result.completed);
assert!(!result.wal_was_reset);
assert_eq!(result.requested_mode, mode);
assert_eq!(result.effective_mode, mode);
}
let frame_count_after =
with_wal_backend_read(&pager.wal_backend, |wal| Ok(wal.frame_count()))
.expect("SharedWalBackend must still hold the installed backend after fast-path");
assert_eq!(
frame_count_after, 0,
"fast path must not disturb the installed WAL backend state"
);
}
#[test]
fn test_checkpoint_coordination_preserves_foreground_isolation_on_checkpoint_error() {
init_publication_test_tracing();
let (pager, _frames) = wal_pager();
let cx = Cx::new();
pager
.set_wal_backend(Box::new(FailingCheckpointWalBackend))
.expect("install failing checkpoint wal backend");
let err = pager
.checkpoint(&cx, crate::traits::CheckpointMode::Passive)
.expect_err("failing checkpoint backend should surface error");
assert!(
matches!(err, FrankenError::BusyRecovery),
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_error_surfaces_backend_failure error={err}"
);
let queue_snapshot =
checkpoint_coordination_queue_snapshot(&pager_group_commit_queue(&pager));
assert_eq!(
queue_snapshot.pending_batch_count, 0,
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_error_keeps_foreground_queue_idle"
);
let metadata = pager.published_snapshot();
assert!(
!metadata.checkpoint_active,
"bead_id={CHECKPOINT_DECOUPLING_BEAD_ID} case=checkpoint_error_releases_checkpoint_flag"
);
}
#[test]
fn test_checkpoint_writer_sync_repairs_page1_header_after_late_growth() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0xCD; ps]).unwrap();
txn.commit(&cx).unwrap();
}
let mut page1_data = {
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec()
};
let header: [u8; DATABASE_HEADER_SIZE] = page1_data[..DATABASE_HEADER_SIZE]
.try_into()
.expect("page 1 header must be present");
let expected_change_counter = DatabaseHeader::from_bytes(&header)
.expect("header must parse")
.change_counter;
page1_data[24..28].copy_from_slice(&0_u32.to_be_bytes());
page1_data[92..96].copy_from_slice(&0_u32.to_be_bytes());
let mut writer = pager.checkpoint_writer();
crate::traits::CheckpointPageWriter::write_page(
&mut writer,
&cx,
PageNumber::ONE,
&page1_data,
)
.unwrap();
let page_three = PageNumber::new(3).unwrap();
crate::traits::CheckpointPageWriter::write_page(
&mut writer,
&cx,
page_three,
&vec![0xAB; ps],
)
.unwrap();
crate::traits::CheckpointPageWriter::sync(&mut writer, &cx).unwrap();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw_page1 = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let header: [u8; DATABASE_HEADER_SIZE] = raw_page1[..DATABASE_HEADER_SIZE]
.try_into()
.expect("page 1 header must be present");
let parsed = DatabaseHeader::from_bytes(&header).expect("header must parse");
assert_eq!(
parsed.page_count,
page_three.get(),
"bead_id={BEAD_ID} case=checkpoint_sync_repairs_page_count"
);
assert_eq!(
parsed.change_counter, expected_change_counter,
"bead_id={BEAD_ID} case=checkpoint_sync_repairs_change_counter"
);
assert_eq!(
parsed.version_valid_for, parsed.change_counter,
"bead_id={BEAD_ID} case=checkpoint_sync_repairs_version_valid_for"
);
}
#[test]
fn test_wal_commit_appends_frames() {
let (pager, frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
let data = vec![0xAA_u8; ps];
txn.write_page(&cx, p1, &data).unwrap();
txn.commit(&cx).unwrap();
let locked_frames = frames.lock().unwrap();
assert_eq!(
locked_frames.len(),
2,
"bead_id={BEAD_ID} case=wal_two_frames_appended_including_header"
);
let p1_frame = locked_frames.iter().find(|f| f.0 == p1.get()).unwrap();
assert_eq!(p1_frame.1[0], 0xAA, "bead_id={BEAD_ID} case=wal_frame_data");
let commit_count = locked_frames.iter().filter(|f| f.2 > 0).count();
assert_eq!(commit_count, 1, "bead_id={BEAD_ID} case=wal_commit_marker");
drop(locked_frames);
}
#[test]
fn test_wal_commit_multi_page() {
let (pager, frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.write_page(&cx, p2, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
let locked_frames = frames.lock().unwrap();
assert_eq!(
locked_frames.len(),
3,
"bead_id={BEAD_ID} case=wal_multi_page_count"
);
let commit_count = locked_frames.iter().filter(|f| f.2 > 0).count();
drop(locked_frames);
assert_eq!(
commit_count, 1,
"bead_id={BEAD_ID} case=wal_exactly_one_commit_marker"
);
}
#[test]
fn test_wal_commit_uses_single_batch_append() {
let (pager, frames, _begin_calls, batch_calls) = wal_pager_with_tracking();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let new_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, PageNumber::ONE, &vec![0x11; ps])
.unwrap();
txn.write_page(&cx, new_page, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
assert_eq!(
*batch_calls.lock().unwrap(),
1,
"bead_id={BEAD_ID} case=wal_batch_append_single_call"
);
let frames = frames.lock().unwrap();
assert_eq!(
frames.len(),
2,
"bead_id={BEAD_ID} case=wal_batch_append_frame_count"
);
assert_eq!(
frames[0].0,
PageNumber::ONE.get(),
"bead_id={BEAD_ID} case=wal_batch_append_sorted_page1"
);
assert_eq!(
frames[1].0,
new_page.get(),
"bead_id={BEAD_ID} case=wal_batch_append_sorted_new_page"
);
assert_eq!(
frames[0].2, 0,
"bead_id={BEAD_ID} case=wal_batch_append_non_commit_first"
);
assert_eq!(
frames[1].2,
new_page.get(),
"bead_id={BEAD_ID} case=wal_batch_append_commit_marker_last"
);
}
#[test]
fn test_wal_preparation_happens_before_reserved_publish_lock() {
let (pager, observed_lock_level) = observed_lock_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let (
backend,
frames,
append_frames_calls,
append_prepared_calls,
prepare_lock_levels,
append_lock_levels,
) = PreparedBatchObservedWalBackend::new(Arc::clone(&observed_lock_level));
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let new_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, PageNumber::ONE, &vec![0x11; ps])
.unwrap();
txn.write_page(&cx, new_page, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
assert_eq!(
*append_frames_calls.lock().unwrap(),
0,
"bead_id=bd-db300.3.2 case=prepared_path_skips_fallback_append"
);
assert_eq!(
*append_prepared_calls.lock().unwrap(),
1,
"bead_id=bd-db300.3.2 case=prepared_path_uses_prepared_append"
);
assert_eq!(
prepare_lock_levels.lock().unwrap().as_slice(),
&[LockLevel::Reserved],
"bead_id=bd-db300.3.2 case=prepare_runs_before_reserved_publish"
);
assert_eq!(
append_lock_levels.lock().unwrap().as_slice(),
&[LockLevel::Reserved],
"bead_id=bd-db300.3.2 case=prepared_append_runs_inside_reserved_publish"
);
let frames = frames.lock().unwrap();
assert_eq!(
frames.len(),
2,
"bead_id=bd-db300.3.2 case=prepared_path_preserves_frame_count"
);
assert_eq!(
frames[0].0,
PageNumber::ONE.get(),
"bead_id=bd-db300.3.2 case=prepared_path_preserves_sorted_page_one"
);
assert_eq!(
frames[1].0,
new_page.get(),
"bead_id=bd-db300.3.2 case=prepared_path_preserves_sorted_commit_frame"
);
}
#[test]
fn test_wal_commit_sync_policy_deferred_skips_sync() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/wal_deferred_sync_policy.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, _begin_calls, _batch_calls, sync_calls) =
MockWalBackend::new_with_sync_tracking();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager
.set_wal_commit_sync_policy(WalCommitSyncPolicy::Deferred)
.unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x55; PageSize::DEFAULT.as_usize()])
.unwrap();
txn.commit(&cx).unwrap();
assert_eq!(
*sync_calls.lock().unwrap(),
0,
"bead_id={BEAD_ID} case=wal_sync_policy_deferred_skips_commit_sync"
);
assert_eq!(
frames.lock().unwrap().len(),
2,
"bead_id={BEAD_ID} case=wal_sync_policy_deferred_still_appends_wal_frames"
);
}
#[test]
fn test_wal_commit_sync_policy_per_commit_syncs() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/wal_per_commit_sync_policy.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, _begin_calls, _batch_calls, sync_calls) =
MockWalBackend::new_with_sync_tracking();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager
.set_wal_commit_sync_policy(WalCommitSyncPolicy::PerCommit)
.unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x66; PageSize::DEFAULT.as_usize()])
.unwrap();
txn.commit(&cx).unwrap();
assert_eq!(
*sync_calls.lock().unwrap(),
1,
"bead_id={BEAD_ID} case=wal_sync_policy_per_commit_runs_commit_sync"
);
assert_eq!(
frames.lock().unwrap().len(),
2,
"bead_id={BEAD_ID} case=wal_sync_policy_per_commit_appends_wal_frames"
);
}
#[test]
fn test_group_commit_flush_failure_wakes_waiters_with_error() {
for attempt in 0..32 {
let vfs = MemoryVfs::new();
let path = PathBuf::from(format!("/wal_group_commit_failure_waiter_{attempt}.db"));
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, append_frames_calls) = FailingGroupCommitWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager
.set_wal_commit_sync_policy(WalCommitSyncPolicy::Deferred)
.unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
let pool = pager.pool.clone();
let start = StdArc::new(std::sync::Barrier::new(3));
let spawn_commit = |page_number: u32, fill: u8| {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let start = StdArc::clone(&start);
std::thread::spawn(move || {
let cx = Cx::new();
let page_no = PageNumber::new(page_number).unwrap();
let page =
StagedPage::from_bytes(&pool, &vec![fill; pool.page_size()]).unwrap();
let mut write_set = HashMap::new();
write_set.insert(page_no, page);
let write_pages_sorted = vec![page_no];
start.wait();
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&write_pages_sorted,
&[],
&queue,
)
})
};
let writer_a = spawn_commit(2, 0x11);
let writer_b = spawn_commit(3, 0x22);
start.wait();
let result_a = writer_a.join().unwrap();
let result_b = writer_b.join().unwrap();
let append_call_count = *append_frames_calls.lock().unwrap();
if append_call_count != 1 {
continue;
}
let error_a = result_a.expect_err("flusher should observe append failure");
let error_b = result_b.expect_err("waiter should observe propagated failure");
let error_a = error_a.to_string();
let error_b = error_b.to_string();
assert!(
error_a.contains("forced batched group commit append failure")
|| error_b.contains("forced batched group commit append failure"),
"bead_id={BEAD_ID} case=group_commit_flusher_reports_backend_failure error_a={error_a} error_b={error_b}"
);
assert!(
error_a.contains("group commit flush failed")
|| error_b.contains("group commit flush failed"),
"bead_id={BEAD_ID} case=group_commit_waiter_reports_epoch_failure error_a={error_a} error_b={error_b}"
);
return;
}
panic!(
"bead_id={BEAD_ID} case=group_commit_flush_failure_wakes_waiters could not coalesce flusher+waiter in allotted attempts"
);
}
#[test]
fn test_group_commit_flush_failure_restores_reserved_lock_level() {
let vfs = BlockingObservedLockVfs::new();
let observed_lock_level = vfs.observed_lock_level();
let path = PathBuf::from("/wal_group_commit_failure_restores_reserved.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, _append_frames_calls) = FailingGroupCommitWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager
.set_wal_commit_sync_policy(WalCommitSyncPolicy::Deferred)
.unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x77; PageSize::DEFAULT.as_usize()])
.unwrap();
let error = txn.commit(&cx).expect_err(
"failing WAL backend should surface a group commit error instead of committing",
);
assert!(
error
.to_string()
.contains("forced batched group commit append failure"),
"bead_id={BEAD_ID} case=group_commit_failure_surfaces_backend_error error={error}"
);
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::Reserved,
"bead_id={BEAD_ID} case=group_commit_failure_restores_writer_lock_after_exclusive_error"
);
drop(txn);
assert_eq!(
*observed_lock_level.lock().unwrap(),
LockLevel::None,
"bead_id={BEAD_ID} case=group_commit_failure_drop_releases_retained_writer_lock"
);
}
#[test]
fn test_group_commit_fault_hook_after_flush_before_publish_wakes_waiters_with_error_and_records_context()
{
let _guard = FAULT_HOOK_TEST_GUARD
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for attempt in 0..32 {
crate::fault_hooks::clear();
let vfs = MemoryVfs::new();
let path = PathBuf::from(format!("/wal_group_commit_publish_hook_{attempt}.db"));
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, _frames, _begin_calls, batch_calls, sync_calls) =
MockWalBackend::new_with_sync_tracking();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager
.set_wal_commit_sync_policy(WalCommitSyncPolicy::PerCommit)
.unwrap();
crate::fault_hooks::arm_after_flush_before_publish(
crate::fault_hooks::FaultHookArm::new(
"bd-db300.7.2.2-after-flush-before-publish",
"GROUP-COMMIT-PUBLISH",
"group_commit_publish_recovery",
),
);
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
let pool = pager.pool.clone();
let start = StdArc::new(std::sync::Barrier::new(3));
let spawn_commit = |page_number: u32, fill: u8| {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let start = StdArc::clone(&start);
std::thread::spawn(move || {
let cx = Cx::new();
let page_no = PageNumber::new(page_number).unwrap();
let page =
StagedPage::from_bytes(&pool, &vec![fill; pool.page_size()]).unwrap();
let mut write_set = HashMap::new();
write_set.insert(page_no, page);
let write_pages_sorted = vec![page_no];
start.wait();
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&write_pages_sorted,
&[],
&queue,
)
})
};
let writer_a = spawn_commit(2, 0x11);
let writer_b = spawn_commit(3, 0x22);
start.wait();
let result_a = writer_a.join().unwrap();
let result_b = writer_b.join().unwrap();
if *batch_calls.lock().unwrap() != 1 {
continue;
}
assert_eq!(
*sync_calls.lock().unwrap(),
1,
"bead_id={BEAD_ID} case=group_commit_publish_hook_runs_after_real_sync"
);
let error_a = result_a
.expect_err("flusher should surface publish-hook failure")
.to_string();
let error_b = result_b
.expect_err("waiter should observe propagated publish-hook failure")
.to_string();
assert!(
error_a.contains("fault_inject:after_flush_before_publish")
|| error_b.contains("fault_inject:after_flush_before_publish"),
"bead_id={BEAD_ID} case=group_commit_publish_hook_reports_primary_failure error_a={error_a} error_b={error_b}"
);
assert!(
error_a.contains("group commit flush failed")
|| error_b.contains("group commit flush failed"),
"bead_id={BEAD_ID} case=group_commit_publish_hook_reports_epoch_failure error_a={error_a} error_b={error_b}"
);
let records = crate::fault_hooks::take_records();
assert_eq!(records.len(), 1, "publish hook should record exactly once");
assert_eq!(records[0].point, "after_flush_before_publish");
assert_eq!(
records[0].run_id,
"bd-db300.7.2.2-after-flush-before-publish"
);
assert_eq!(records[0].scenario_id, "GROUP-COMMIT-PUBLISH");
assert_eq!(records[0].invariant_family, "group_commit_publish_recovery");
assert!(
records[0].detail.contains("batch_count=2"),
"record should capture coalesced batch context: {}",
records[0].detail
);
assert!(
records[0].detail.contains("frame_count=2"),
"record should capture coalesced frame context: {}",
records[0].detail
);
crate::fault_hooks::clear();
return;
}
panic!(
"bead_id={BEAD_ID} case=group_commit_publish_hook_wakes_waiters could not coalesce flusher+waiter in allotted attempts"
);
}
#[test]
fn test_fault_drop_condvar_notify_waiters_recover_via_timeout() {
let _guard = FAULT_HOOK_TEST_GUARD
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for attempt in 0..32 {
crate::fault_hooks::clear();
let cx = Cx::new();
let vfs = MemoryVfs::new();
let path = PathBuf::from(format!("/condvar_notify_drop_{attempt}.db"));
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let (backend, _frames, _begin_calls, batch_calls, _sync_calls) =
MockWalBackend::new_with_sync_tracking();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
pager
.set_wal_commit_sync_policy(WalCommitSyncPolicy::PerCommit)
.unwrap();
crate::fault_hooks::arm_drop_condvar_notify(crate::fault_hooks::FaultHookArm::new(
"bd-db300.7.2.2-drop-condvar",
"DROP-CONDVAR-NOTIFY",
"liveness_under_fault",
));
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
let pool = pager.pool.clone();
let start = StdArc::new(std::sync::Barrier::new(3));
let spawn_commit = |page_number: u32, fill: u8| {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let start = StdArc::clone(&start);
std::thread::spawn(move || {
let cx = Cx::new();
let page_no = PageNumber::new(page_number).unwrap();
let page =
StagedPage::from_bytes(&pool, &vec![fill; pool.page_size()]).unwrap();
let mut write_set = HashMap::new();
write_set.insert(page_no, page);
let write_pages_sorted = vec![page_no];
start.wait();
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&write_pages_sorted,
&[],
&queue,
)
})
};
let writer_a = spawn_commit(2, 0x33);
let writer_b = spawn_commit(3, 0x44);
let started = Instant::now();
start.wait();
let result_a = writer_a.join().unwrap();
let result_b = writer_b.join().unwrap();
let elapsed = started.elapsed();
if *batch_calls.lock().unwrap() != 1 {
crate::fault_hooks::clear();
continue;
}
assert!(
elapsed < std::time::Duration::from_secs(2),
"bead_id={BEAD_ID} case=condvar_drop_no_hang elapsed={elapsed:?}"
);
let _any_ok = result_a.is_ok() || result_b.is_ok();
let records = crate::fault_hooks::take_records();
assert_eq!(records.len(), 1, "condvar-drop hook should fire once");
assert_eq!(records[0].point, "drop_condvar_notify");
assert_eq!(records[0].run_id, "bd-db300.7.2.2-drop-condvar");
assert!(
records[0].detail.contains("completed_epoch="),
"record should capture epoch: {}",
records[0].detail
);
crate::fault_hooks::clear();
return;
}
panic!(
"bead_id={BEAD_ID} case=condvar_drop_no_hang could not coalesce in allotted attempts"
);
}
#[test]
fn test_fault_during_phase_c_returns_error_and_wal_frames_survive() {
let _guard = FAULT_HOOK_TEST_GUARD
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
crate::fault_hooks::clear();
let cx = Cx::new();
let vfs = MemoryVfs::new();
let path = PathBuf::from("/fault_phase_c_test.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let (backend, frames, _begin_calls, _batch_calls) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
crate::fault_hooks::arm_during_phase_c(crate::fault_hooks::FaultHookArm::new(
"bd-db300.7.2.2-phase-c",
"PHASE-C-CRASH",
"commit_publish_recovery",
));
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_bytes = PageSize::DEFAULT.as_usize();
txn.write_page(&cx, PageNumber::ONE, &vec![0xCC; page_bytes])
.unwrap();
let err = txn
.commit(&cx)
.expect_err("Phase C fault hook should cause commit to fail");
assert!(
err.to_string().contains("fault_inject:during_phase_c"),
"error should identify the Phase C hook: {err}"
);
let written_frames = frames.lock().unwrap();
assert!(
!written_frames.is_empty(),
"WAL frames should survive — the fault fires after WAL I/O completes"
);
let records = crate::fault_hooks::take_records();
assert_eq!(records.len(), 1, "exactly one Phase C fault should fire");
assert_eq!(records[0].point, "during_phase_c");
assert_eq!(records[0].run_id, "bd-db300.7.2.2-phase-c");
assert_eq!(records[0].scenario_id, "PHASE-C-CRASH");
assert!(
records[0].detail.contains("commit_seq="),
"record should capture commit_seq: {}",
records[0].detail
);
crate::fault_hooks::clear();
}
#[test]
fn test_publish_window_measurement_captures_exclusive_hold_sample() {
let (cx, mut txn, vfs) =
track_c_publish_window_prepared_commit(TrackCPublishWindowMode::PreparedCandidate, 7);
txn.commit(&cx).unwrap();
let hold_samples = vfs.exclusive_hold_samples_ns();
assert_eq!(
hold_samples.len(),
1,
"bead_id={TRACK_C_PUBLISH_WINDOW_BENCH_BEAD_ID} case=hold_sample_count"
);
assert!(
hold_samples[0] > 0,
"bead_id={TRACK_C_PUBLISH_WINDOW_BENCH_BEAD_ID} case=hold_sample_positive"
);
}
#[test]
fn test_publish_window_contention_measurement_captures_competing_writer_wait() {
let (vfs, pager_a, pager_b) =
track_c_open_contending_pagers(TrackCPublishWindowMode::PreparedCandidate, 7);
let writer_a = std::thread::spawn(move || {
let cx = Cx::new();
let mut txn = pager_a.begin(&cx, TransactionMode::Immediate).unwrap();
track_c_write_existing_page_range(&mut txn, &cx, 2, 7, 53);
txn.commit(&cx).unwrap();
});
vfs.wait_for_exclusive_acquisitions(1);
let cx_b = Cx::new();
let mut txn_b = pager_b.begin(&cx_b, TransactionMode::Immediate).unwrap();
track_c_write_existing_page_range(&mut txn_b, &cx_b, 9, 7, 97);
txn_b.commit(&cx_b).unwrap();
writer_a.join().unwrap();
let wait_samples = vfs.exclusive_wait_samples_ns();
assert_eq!(
wait_samples.len(),
2,
"bead_id={TRACK_C_PUBLISH_WINDOW_BENCH_BEAD_ID} case=wait_sample_count"
);
assert!(
wait_samples.iter().copied().max().unwrap_or(0) > 0,
"bead_id={TRACK_C_PUBLISH_WINDOW_BENCH_BEAD_ID} case=contending_writer_wait_positive"
);
}
#[test]
fn test_collect_wal_commit_batch_keeps_sorted_order_and_single_commit_boundary() {
let page_three = PageNumber::new(3).unwrap();
let mut write_set = HashMap::new();
let mut page1 = PageBuf::new(PageSize::DEFAULT);
page1.fill(0x11);
write_set.insert(PageNumber::ONE, StagedPage::from_buf(page1));
let mut page3 = PageBuf::new(PageSize::DEFAULT);
page3.fill(0x33);
write_set.insert(page_three, StagedPage::from_buf(page3));
let write_pages_sorted = vec![PageNumber::ONE, page_three];
let batch = collect_wal_commit_batch(2, &write_set, &write_pages_sorted)
.unwrap()
.expect("non-empty write set should yield a WAL batch");
assert_eq!(
batch.new_db_size,
page_three.get(),
"bead_id={BEAD_ID} case=wal_batch_helper_new_db_size"
);
assert_eq!(
batch.frames.len(),
2,
"bead_id={BEAD_ID} case=wal_batch_helper_frame_count"
);
assert_eq!(
batch.frames[0].page_number,
PageNumber::ONE.get(),
"bead_id={BEAD_ID} case=wal_batch_helper_sorted_first"
);
assert_eq!(
batch.frames[0].db_size_if_commit, 0,
"bead_id={BEAD_ID} case=wal_batch_helper_non_commit_prefix"
);
assert_eq!(
batch.frames[1].page_number,
page_three.get(),
"bead_id={BEAD_ID} case=wal_batch_helper_sorted_last"
);
assert_eq!(
batch.frames[1].db_size_if_commit,
page_three.get(),
"bead_id={BEAD_ID} case=wal_batch_helper_commit_boundary_last"
);
}
#[test]
fn test_collect_wal_commit_batch_preserves_existing_db_size_for_interior_updates() {
let page_two = PageNumber::new(2).unwrap();
let mut write_set = HashMap::new();
let mut page2 = PageBuf::new(PageSize::DEFAULT);
page2.fill(0x22);
write_set.insert(page_two, StagedPage::from_buf(page2));
let batch = collect_wal_commit_batch(9, &write_set, &[page_two])
.unwrap()
.expect("single dirty page should yield a WAL batch");
assert_eq!(
batch.new_db_size, 9,
"bead_id={BEAD_ID} case=wal_batch_helper_preserve_db_size"
);
assert_eq!(
batch.frames[0].db_size_if_commit, 9,
"bead_id={BEAD_ID} case=wal_batch_helper_commit_marker_uses_existing_db_size"
);
}
#[test]
fn test_collect_wal_commit_batch_returns_none_for_empty_write_set() {
let write_set = HashMap::new();
let batch = collect_wal_commit_batch(4, &write_set, &[]).unwrap();
assert!(
batch.is_none(),
"bead_id={BEAD_ID} case=wal_batch_helper_empty_batch"
);
}
#[test]
fn test_collect_wal_commit_batch_errors_when_sorted_page_missing_from_write_set() {
let missing_page = PageNumber::new(4).unwrap();
let write_set = HashMap::new();
let err = match collect_wal_commit_batch(1, &write_set, &[missing_page]) {
Ok(_) => panic!(
"bead_id={BEAD_ID} case=wal_batch_helper_missing_page expected helper to fail"
),
Err(err) => err,
};
let message = err.to_string();
assert!(
message.contains("missing page 4"),
"bead_id={BEAD_ID} case=wal_batch_helper_missing_page error={message}"
);
}
#[test]
fn test_build_group_commit_batch_clones_owned_frames_and_commit_boundary() {
let page_two = PageNumber::new(2).unwrap();
let page_three = PageNumber::new(3).unwrap();
let mut write_set = HashMap::new();
let mut page2 = PageBuf::new(PageSize::DEFAULT);
page2.fill(0x22);
write_set.insert(page_two, StagedPage::from_buf(page2));
let mut page3 = PageBuf::new(PageSize::DEFAULT);
page3.fill(0x33);
write_set.insert(page_three, StagedPage::from_buf(page3));
let (batch, new_db_size) = build_group_commit_batch(2, &write_set, &[page_two, page_three])
.unwrap()
.expect("sorted write set should yield a group commit batch");
drop(write_set);
assert_eq!(
new_db_size, 3,
"bead_id={BEAD_ID} case=group_commit_batch_helper_new_db_size"
);
assert_eq!(
batch.frames.len(),
2,
"bead_id={BEAD_ID} case=group_commit_batch_helper_frame_count"
);
assert_eq!(
batch.frames[0].page_number,
page_two.get(),
"bead_id={BEAD_ID} case=group_commit_batch_helper_sorted_first"
);
assert_eq!(
batch.frames[0].db_size_if_commit, 0,
"bead_id={BEAD_ID} case=group_commit_batch_helper_non_commit_first"
);
assert_eq!(
batch.frames[1].page_number,
page_three.get(),
"bead_id={BEAD_ID} case=group_commit_batch_helper_sorted_last"
);
assert_eq!(
batch.frames[1].db_size_if_commit,
page_three.get(),
"bead_id={BEAD_ID} case=group_commit_batch_helper_commit_marker_last"
);
assert_eq!(
batch.frames[0].page_data[0], 0x22,
"bead_id={BEAD_ID} case=group_commit_batch_helper_preserves_first_payload_after_source_drop"
);
assert_eq!(
batch.frames[1].page_data[0], 0x33,
"bead_id={BEAD_ID} case=group_commit_batch_helper_preserves_second_payload_after_source_drop"
);
}
#[test]
fn test_build_group_commit_batch_returns_none_for_empty_write_set() {
let write_set = HashMap::new();
let batch = build_group_commit_batch(4, &write_set, &[]).unwrap();
assert!(
batch.is_none(),
"bead_id={BEAD_ID} case=group_commit_batch_helper_empty_batch"
);
}
#[test]
fn test_build_group_commit_batch_errors_when_sorted_page_missing_from_write_set() {
let missing_page = PageNumber::new(4).unwrap();
let write_set = HashMap::new();
let err = match build_group_commit_batch(1, &write_set, &[missing_page]) {
Ok(_) => panic!(
"bead_id={BEAD_ID} case=group_commit_batch_helper_missing_page expected helper to fail"
),
Err(err) => err,
};
let message = err.to_string();
assert!(
message.contains("missing page 4"),
"bead_id={BEAD_ID} case=group_commit_batch_helper_missing_page error={message}"
);
}
#[test]
fn test_parallel_wal_control_override_applies_to_new_queue() {
let control = ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Conservative,
lane_count_override: Some(3),
max_parallel_commit_bytes: Some(4096),
..ParallelWalControlSurface::default()
};
set_parallel_wal_control_override(Some(control.clone()));
let queue = GroupCommitQueue::new(GroupCommitConfig::default());
assert_eq!(
queue.parallel_wal_control(),
&control,
"bead_id=bd-3wop3.1.2 case=parallel_wal_control_override_applies_to_new_queue"
);
set_parallel_wal_control_override(None);
}
#[test]
fn test_parallel_wal_lane_identity_is_stable_within_thread() {
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let queue = GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(4),
..ParallelWalControlSurface::default()
},
);
let first = queue.current_parallel_wal_lane_id();
let second = queue.current_parallel_wal_lane_id();
assert_eq!(
first, second,
"bead_id=bd-3wop3.1.2 case=lane_identity_stable_within_thread"
);
assert!(
usize::from(first) < 4,
"bead_id=bd-3wop3.1.2 case=lane_identity_respects_override lane_id={first}"
);
}
#[test]
fn test_parallel_wal_lane_reuse_after_worker_churn() {
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
const LANE_COUNT: usize = 2;
let queue = StdArc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(LANE_COUNT),
..ParallelWalControlSurface::default()
},
));
let spawn_wave = || {
let mut lanes = Vec::new();
for _ in 0..2 {
let queue = StdArc::clone(&queue);
lanes.push(std::thread::spawn(move || {
queue.current_parallel_wal_lane_id()
}));
}
let mut observed = lanes
.into_iter()
.map(|handle| handle.join().unwrap())
.collect::<Vec<_>>();
observed.sort_unstable();
observed
};
let first_wave = spawn_wave();
let second_wave = spawn_wave();
for (wave_name, wave) in [("first", &first_wave), ("second", &second_wave)] {
assert_eq!(
wave.len(),
LANE_COUNT,
"bead_id=bd-3wop3.1.2 case=lane_reuse_{wave_name}_wave_size"
);
assert!(
wave.iter().all(|lane| usize::from(*lane) < LANE_COUNT),
"bead_id=bd-3wop3.1.2 case=lane_reuse_{wave_name}_wave_bounds lanes={wave:?}"
);
}
}
#[test]
fn test_parallel_wal_same_lane_order_mismatch_returns_none_without_drain() {
let queue = GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
);
let page_a = sample_page(0xA1);
let page_b = sample_page(0xB2);
let frame_refs_a = [crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_a,
db_size_if_commit: 2,
}];
let frame_refs_b = [crate::traits::WalFrameRef {
page_number: 3,
page_data: &page_b,
db_size_if_commit: 3,
}];
assert_eq!(
queue.record_prepared_batch(lane_staged_batch_for_test(10, 0, &frame_refs_a)),
1
);
assert_eq!(
queue.record_prepared_batch(lane_staged_batch_for_test(11, 0, &frame_refs_b)),
2
);
let out_of_order = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 3,
page_data: page_b.clone(),
db_size_if_commit: 3,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 11,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: page_a.clone(),
db_size_if_commit: 2,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 10,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
];
assert!(
queue
.take_prepared_batches_for_flush(&out_of_order)
.is_none(),
"bead_id=bd-3wop3.1.2 case=same_lane_order_mismatch_forces_fallback"
);
assert_eq!(
queue.current_lane_backlog(0),
2,
"bead_id=bd-3wop3.1.2 case=same_lane_order_mismatch_does_not_drain"
);
let in_order = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: page_a,
db_size_if_commit: 2,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 10,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 3,
page_data: page_b,
db_size_if_commit: 3,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 11,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
];
let drained = queue
.take_prepared_batches_for_flush(&in_order)
.expect("verified in-order same-lane batches should drain");
assert_eq!(
drained.len(),
2,
"bead_id=bd-3wop3.1.2 case=same_lane_order_in_order_drains"
);
assert_eq!(
queue.current_lane_backlog(0),
0,
"bead_id=bd-3wop3.1.2 case=same_lane_order_drain_clears_backlog"
);
}
#[test]
fn test_parallel_wal_raw_fallback_discard_removes_stale_prepared_batches() {
let queue = GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
);
let page_a = sample_page(0xC1);
let page_b = sample_page(0xD2);
let frame_refs_a = [crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_a,
db_size_if_commit: 2,
}];
let frame_refs_b = [crate::traits::WalFrameRef {
page_number: 3,
page_data: &page_b,
db_size_if_commit: 3,
}];
assert_eq!(
queue.record_prepared_batch(lane_staged_batch_for_test(10, 0, &frame_refs_a)),
1
);
assert_eq!(
queue.record_prepared_batch(lane_staged_batch_for_test(11, 0, &frame_refs_b)),
2
);
let raw_fallback_batches = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 3,
page_data: page_b,
db_size_if_commit: 3,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 11,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: page_a,
db_size_if_commit: 2,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 10,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
];
assert!(
queue
.take_prepared_batches_for_flush(&raw_fallback_batches)
.is_none(),
"bead_id=bd-3wop3.1.2 case=raw_fallback_order_mismatch_forces_frame_ref_path"
);
assert_eq!(
queue.current_lane_backlog(0),
2,
"bead_id=bd-3wop3.1.2 case=raw_fallback_mismatch_leaves_prepared_batches_until_explicit_cleanup"
);
assert_eq!(
queue.discard_prepared_batches_for_flush(&raw_fallback_batches),
2,
"bead_id=bd-3wop3.1.2 case=raw_fallback_discards_exact_flushed_prepared_batches"
);
assert_eq!(
queue.current_lane_backlog(0),
0,
"bead_id=bd-3wop3.1.2 case=raw_fallback_discard_clears_stale_lane_backlog"
);
}
#[test]
fn test_parallel_wal_prepared_merge_clears_intermediate_commit_headers() {
let page_a = sample_page(0xC3);
let page_b = sample_page(0xD4);
let frame_refs_a = [crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_a,
db_size_if_commit: 2,
}];
let frame_refs_b = [crate::traits::WalFrameRef {
page_number: 3,
page_data: &page_b,
db_size_if_commit: 3,
}];
let staged = vec![
lane_staged_batch_for_test(10, 0, &frame_refs_a),
lane_staged_batch_for_test(11, 1, &frame_refs_b),
];
let merged = merge_prepared_group_commit_batches(staged, 3).unwrap();
let db_size_headers = (0..merged.frame_count())
.map(|index| {
let frame = merged.frame_slice(index);
u32::from_be_bytes([frame[4], frame[5], frame[6], frame[7]])
})
.collect::<Vec<_>>();
assert_eq!(
merged
.frame_metas
.iter()
.map(|meta| meta.db_size_if_commit)
.collect::<Vec<_>>(),
vec![0, 3],
"bead_id=bd-3wop3.8 case=prepared_merge_metadata_has_single_group_commit_marker"
);
assert_eq!(
db_size_headers,
vec![0, 3],
"bead_id=bd-3wop3.8 case=prepared_merge_bytes_clear_hidden_intermediate_commit_marker"
);
assert_eq!(
merged.last_commit_frame_offset,
Some(1),
"bead_id=bd-3wop3.8 case=prepared_merge_last_commit_points_to_group_tail"
);
}
#[test]
fn test_parallel_wal_prepared_merge_reuses_canonical_single_batch() {
let page_a = sample_page(0xA7);
let page_b = sample_page(0xB8);
let frame_refs = [
crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_a,
db_size_if_commit: 0,
},
crate::traits::WalFrameRef {
page_number: 3,
page_data: &page_b,
db_size_if_commit: 3,
},
];
let mut staged_batch = lane_staged_batch_for_test(12, 0, &frame_refs);
staged_batch.payload.finalized_for = Some(crate::traits::PreparedWalFinalizationState {
salt1: 11,
salt2: 22,
checkpoint_seq: 0,
start_frame_index: 0,
seed: crate::traits::PreparedWalChecksumSeed { s1: 33, s2: 44 },
});
staged_batch.payload.finalized_running_checksum =
Some(crate::traits::PreparedWalChecksumSeed { s1: 55, s2: 66 });
let staged = vec![staged_batch.clone()];
let merged = merge_prepared_group_commit_batches(staged, 3).unwrap();
assert_eq!(
merged, staged_batch.payload,
"case=prepared_merge_single_batch_reuses_payload_without_checksum_recompute"
);
assert_eq!(
merged.finalized_for, staged_batch.payload.finalized_for,
"case=prepared_merge_single_batch_preserves_prelock_finalization"
);
}
#[test]
fn test_parallel_wal_prepared_merge_rejects_hidden_single_batch_commit_header() {
let page_a = sample_page(0xA9);
let page_b = sample_page(0xBA);
let frame_refs = [
crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_a,
db_size_if_commit: 0,
},
crate::traits::WalFrameRef {
page_number: 3,
page_data: &page_b,
db_size_if_commit: 3,
},
];
let mut staged_batch = lane_staged_batch_for_test(13, 0, &frame_refs);
staged_batch.payload.frame_bytes[4..8].copy_from_slice(&99_u32.to_be_bytes());
staged_batch.payload.finalized_for = Some(crate::traits::PreparedWalFinalizationState {
salt1: 11,
salt2: 22,
checkpoint_seq: 0,
start_frame_index: 0,
seed: crate::traits::PreparedWalChecksumSeed { s1: 33, s2: 44 },
});
let staged = vec![staged_batch];
let merged = merge_prepared_group_commit_batches(staged, 3).unwrap();
let first_frame = merged.frame_slice(0);
assert_eq!(
u32::from_be_bytes([
first_frame[4],
first_frame[5],
first_frame[6],
first_frame[7],
]),
0,
"case=prepared_merge_single_batch_fallback_clears_hidden_commit_header"
);
assert_eq!(
merged.finalized_for, None,
"case=prepared_merge_single_batch_fallback_invalidates_stale_finalization"
);
}
#[test]
fn test_concurrent_commits_in_process_waiter_path_no_ghost_commits_stress() {
type BatchMarkerCounts = StdArc<StdMutex<Vec<(usize, usize, usize)>>>;
struct WaiterStressBackend {
per_batch: BatchMarkerCounts,
total_frames: SharedCounter,
append_prepared_calls: SharedCounter,
}
impl crate::traits::WalBackend for WaiterStressBackend {
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
let meta_commits = frames.iter().filter(|f| f.db_size_if_commit != 0).count();
self.per_batch
.lock()
.unwrap()
.push((meta_commits, meta_commits, frames.len()));
*self.total_frames.lock().unwrap() += frames.len();
Ok(())
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
if frames.is_empty() {
return Ok(None);
}
let frame_size =
fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE + frames[0].page_data.len();
let mut frame_bytes = Vec::with_capacity(frame_size * frames.len());
let mut frame_metas = Vec::with_capacity(frames.len());
let mut checksum_transforms = Vec::with_capacity(frames.len());
let mut last_commit: Option<usize> = None;
for (i, frame) in frames.iter().enumerate() {
frame_metas.push(crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
});
checksum_transforms.push(crate::traits::PreparedWalChecksumTransform {
a11: 0,
a12: 0,
a21: 0,
a22: 0,
c1: 0,
c2: 0,
});
let mut header = [0_u8; fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE];
header[0..4].copy_from_slice(&frame.page_number.to_be_bytes());
header[4..8].copy_from_slice(&frame.db_size_if_commit.to_be_bytes());
frame_bytes.extend_from_slice(&header);
frame_bytes.extend_from_slice(frame.page_data);
if frame.db_size_if_commit != 0 {
last_commit = Some(i);
}
}
Ok(Some(crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE,
big_endian_checksum: false,
frame_metas,
checksum_transforms,
frame_bytes,
last_commit_frame_offset: last_commit,
finalized_for: None,
finalized_running_checksum: None,
}))
}
fn append_prepared_frames(
&mut self,
_cx: &Cx,
prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
*self.append_prepared_calls.lock().unwrap() += 1;
let meta_commits = prepared
.frame_metas
.iter()
.filter(|meta| meta.db_size_if_commit != 0)
.count();
let mut byte_commits = 0_usize;
for i in 0..prepared.frame_count() {
let slice = prepared.frame_slice(i);
let byte_db_size = u32::from_be_bytes([slice[4], slice[5], slice[6], slice[7]]);
if byte_db_size != 0 {
byte_commits += 1;
}
}
self.per_batch.lock().unwrap().push((
meta_commits,
byte_commits,
prepared.frame_count(),
));
*self.total_frames.lock().unwrap() += prepared.frame_count();
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
*self.total_frames.lock().unwrap()
}
fn checkpoint(
&mut self,
_cx: &Cx,
mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: mode,
effective_mode: mode,
})
}
}
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let vfs = MemoryVfs::new();
let path = PathBuf::from("/waiter_path_stress.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let per_batch: BatchMarkerCounts = StdArc::new(StdMutex::new(Vec::new()));
let total_frames: SharedCounter = StdArc::new(StdMutex::new(0));
let append_prepared_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let backend = WaiterStressBackend {
per_batch: StdArc::clone(&per_batch),
total_frames: StdArc::clone(&total_frames),
append_prepared_calls: StdArc::clone(&append_prepared_calls),
};
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface::default(),
));
let pool = pager.pool.clone();
const WORKERS: u32 = 8;
const ITERS: u32 = 50;
let start = StdArc::new(std::sync::Barrier::new(WORKERS as usize));
let mut handles = Vec::with_capacity(WORKERS as usize);
for worker_id in 0..WORKERS {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let start = StdArc::clone(&start);
let handle = std::thread::spawn(move || {
let cx = Cx::new();
start.wait();
for iter in 0..ITERS {
let page_base = 2 + worker_id * ITERS * 2 + iter * 2;
let page_a = PageNumber::new(page_base).unwrap();
let page_b = PageNumber::new(page_base + 1).unwrap();
let fill_a = u8::try_from((worker_id * 31 + iter) & 0xff).unwrap();
let fill_b = u8::try_from((worker_id * 47 + iter) & 0xff).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_a,
StagedPage::from_bytes(&pool, &sample_page(fill_a)).unwrap(),
);
write_set.insert(
page_b,
StagedPage::from_bytes(&pool, &sample_page(fill_b)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_a, page_b],
&[],
&queue,
)
.expect("commit_wal_group_commit succeeded under contention");
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("worker thread joined cleanly");
}
let per_batch = per_batch.lock().unwrap().clone();
let mut merged_batches = 0_usize;
for (i, (meta_commits, byte_commits, n)) in per_batch.iter().enumerate() {
assert!(
*meta_commits <= 1,
"bead_id=bd-3wop3.8 case=waiter_stress_batch_{i}_meta_commits_exceed_one meta={meta_commits} frames={n}"
);
assert!(
*byte_commits <= 1,
"bead_id=bd-3wop3.8 case=waiter_stress_batch_{i}_byte_commits_exceed_one byte={byte_commits} frames={n}"
);
assert_eq!(
meta_commits, byte_commits,
"bead_id=bd-3wop3.8 case=waiter_stress_batch_{i}_meta_byte_disagree meta={meta_commits} byte={byte_commits} frames={n}"
);
if *n > 2 {
merged_batches += 1;
}
}
assert!(
merged_batches >= 1,
"bead_id=bd-3wop3.8 case=waiter_stress_no_merge_observed_test_did_not_exercise_waiter_path per_batch_len={} batches={:?}",
per_batch.len(),
per_batch
);
let expected_frames = (WORKERS * ITERS * 2) as usize;
assert_eq!(
*total_frames.lock().unwrap(),
expected_frames,
"bead_id=bd-3wop3.8 case=waiter_stress_total_frames_equals_submitted"
);
}
#[test]
fn test_group_commit_overlap_abort_discards_stale_prepared_batches() {
let queue = GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
);
let page_a = sample_page(0xE1);
let page_b = sample_page(0xE2);
let frame_refs_a = [crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_a,
db_size_if_commit: 2,
}];
let frame_refs_b = [crate::traits::WalFrameRef {
page_number: 2,
page_data: &page_b,
db_size_if_commit: 2,
}];
assert_eq!(
queue.record_prepared_batch(lane_staged_batch_for_test(20, 0, &frame_refs_a)),
1
);
assert_eq!(
queue.record_prepared_batch(lane_staged_batch_for_test(21, 0, &frame_refs_b)),
2
);
let overlapping_batches = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: page_a,
db_size_if_commit: 2,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 20,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: page_b,
db_size_if_commit: 2,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 21,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 10,
}),
];
assert_eq!(
conflicting_pages_across_group_commit_batches(&overlapping_batches),
vec![2],
"bead_id=bd-3wop3.1.2 case=overlap_abort_detects_same_page_batches"
);
assert_eq!(
queue.discard_prepared_batches_for_flush(&overlapping_batches),
2,
"bead_id=bd-3wop3.1.2 case=overlap_abort_discards_failed_prepared_batches"
);
assert_eq!(
queue.current_lane_backlog(0),
0,
"bead_id=bd-3wop3.1.2 case=overlap_abort_clears_stale_lane_backlog"
);
}
#[test]
fn test_parallel_wal_lane_staging_does_not_need_consolidator_lock() {
let queue = GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
);
let wal_backend = new_shared_wal_backend();
let observed_lock_level = Arc::new(Mutex::new(LockLevel::Reserved));
let (
backend,
_frames,
_append_frames_calls,
_append_prepared_calls,
_prepare_lock_levels,
_append_lock_levels,
) = PreparedBatchObservedWalBackend::new(observed_lock_level);
*wal_backend.write().unwrap() = Some(Box::new(backend));
let _guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batch = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x42),
db_size_if_commit: 2,
}]);
let prepared = prepare_group_commit_batch_for_lane(
&Cx::new(),
&wal_backend,
&batch,
1,
0,
queue.parallel_wal_control(),
)
.unwrap();
assert!(
prepared.is_some(),
"bead_id=bd-3wop3.1.2 case=ordinary_lane_staging_avoids_consolidator_gate"
);
}
#[test]
fn test_parallel_wal_control_modes_preserve_commit_equivalence() {
let run_commit = |mode: ParallelWalOperatingMode| {
let vfs = MemoryVfs::new();
let path = PathBuf::from(format!("/parallel_wal_control_mode_{:?}.db", mode));
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let observed_lock_level = Arc::new(Mutex::new(LockLevel::Reserved));
let (
backend,
frames,
_append_frames_calls,
_append_prepared_calls,
_prepare_lock_levels,
_append_lock_levels,
) = PreparedBatchObservedWalBackend::new(observed_lock_level);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
));
let page_two = PageNumber::new(2).unwrap();
let page_three = PageNumber::new(3).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_two,
StagedPage::from_bytes(&pager.pool, &sample_page(0x11)).unwrap(),
);
write_set.insert(
page_three,
StagedPage::from_bytes(&pager.pool, &sample_page(0x22)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_two, page_three],
&[],
&queue,
)
.unwrap();
frames
};
let auto_frames = run_commit(ParallelWalOperatingMode::Auto);
let conservative_frames = run_commit(ParallelWalOperatingMode::Conservative);
assert_eq!(
*auto_frames.lock().unwrap(),
*conservative_frames.lock().unwrap(),
"bead_id=bd-3wop3.1.2 case=control_mode_equivalence"
);
}
#[test]
fn test_parallel_wal_shadow_compare_divergence_falls_back_to_append_frames() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/parallel_wal_shadow_compare_fallback.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, prepare_calls, append_frames_calls, append_prepared_calls) =
ShadowCompareMismatchWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::ShadowCompare,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
));
let page_two = PageNumber::new(2).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_two,
StagedPage::from_bytes(&pager.pool, &sample_page(0x5A)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_two],
&[],
&queue,
)
.unwrap();
assert_eq!(
*prepare_calls.lock().unwrap(),
2,
"bead_id=bd-3wop3.1.2 case=shadow_compare_divergence_rebuilds_conservative_candidate"
);
assert_eq!(
*append_frames_calls.lock().unwrap(),
1,
"bead_id=bd-3wop3.1.2 case=shadow_compare_divergence_falls_back_to_append_frames"
);
assert_eq!(
*append_prepared_calls.lock().unwrap(),
0,
"bead_id=bd-3wop3.1.2 case=shadow_compare_divergence_skips_bad_prepared_append"
);
let written = frames.lock().unwrap();
assert_eq!(written.len(), 1);
assert_eq!(written[0].0, 2);
assert_eq!(
written[0].1,
sample_page(0x5A),
"bead_id=bd-3wop3.1.2 case=shadow_compare_fallback_preserves_committed_payload"
);
}
#[test]
fn test_parallel_wal_auto_shadow_sampling_can_force_compare_fallback() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/parallel_wal_auto_shadow_sampled_fallback.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, prepare_calls, append_frames_calls, append_prepared_calls) =
ShadowCompareMismatchWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
shadow_compare_sampling_per_mille: Some(1_000),
..ParallelWalControlSurface::default()
},
));
let page_two = PageNumber::new(2).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_two,
StagedPage::from_bytes(&pager.pool, &sample_page(0x6C)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_two],
&[],
&queue,
)
.unwrap();
assert_eq!(
*prepare_calls.lock().unwrap(),
2,
"bead_id=bd-3wop3.1.2 case=auto_shadow_sampling_rebuilds_conservative_candidate"
);
assert_eq!(
*append_frames_calls.lock().unwrap(),
1,
"bead_id=bd-3wop3.1.2 case=auto_shadow_sampling_falls_back_to_append_frames"
);
assert_eq!(
*append_prepared_calls.lock().unwrap(),
0,
"bead_id=bd-3wop3.1.2 case=auto_shadow_sampling_skips_bad_prepared_append"
);
let written = frames.lock().unwrap();
assert_eq!(written.len(), 1);
assert_eq!(written[0].0, 2);
assert_eq!(
written[0].1,
sample_page(0x6C),
"bead_id=bd-3wop3.1.2 case=auto_shadow_sampling_preserves_committed_payload"
);
}
#[test]
fn test_parallel_wal_concurrent_writers_on_disjoint_lanes_commit_successfully() {
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let vfs = MemoryVfs::new();
let path = PathBuf::from("/parallel_wal_disjoint_lane_commit.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let observed_lock_level = Arc::new(Mutex::new(LockLevel::Reserved));
let (
backend,
frames,
_append_frames_calls,
_append_prepared_calls,
_prepare_lock_levels,
_append_lock_levels,
) = PreparedBatchObservedWalBackend::new(observed_lock_level);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
));
let pool = pager.pool.clone();
let start = StdArc::new(std::sync::Barrier::new(3));
let spawn_commit = |page_number: u32, fill: u8| {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let start = StdArc::clone(&start);
std::thread::spawn(move || {
let lane_id = queue.current_parallel_wal_lane_id();
let cx = Cx::new();
let page_no = PageNumber::new(page_number).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_no,
StagedPage::from_bytes(&pool, &sample_page(fill)).unwrap(),
);
start.wait();
(
lane_id,
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_no],
&[],
&queue,
),
)
})
};
let writer_a = spawn_commit(2, 0x31);
let writer_b = spawn_commit(3, 0x47);
start.wait();
let (lane_a, result_a) = writer_a.join().unwrap();
let (lane_b, result_b) = writer_b.join().unwrap();
result_a.unwrap();
result_b.unwrap();
assert_ne!(
lane_a, lane_b,
"bead_id=bd-3wop3.1.2 case=disjoint_writers_receive_distinct_lanes"
);
let written = frames.lock().unwrap();
assert_eq!(
written.len(),
2,
"bead_id=bd-3wop3.1.2 case=disjoint_writers_commit_all_frames"
);
}
#[test]
fn test_group_commit_promoted_epoch_uses_live_db_size_floor() {
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let vfs = MemoryVfs::new();
let path = PathBuf::from("/group_commit_promoted_epoch_live_db_size_floor.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, frames, first_prepare_entered, release_first_prepare, _prepare_calls) =
BlockingFirstPrepareWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
max_parallel_commit_bytes: Some(0),
..ParallelWalControlSurface::default()
},
));
let pool = pager.pool.clone();
let spawn_commit = |page_number: u32, fill: u8| {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
std::thread::spawn(move || {
let cx = Cx::new();
let page_no = PageNumber::new(page_number).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_no,
StagedPage::from_bytes(&pool, &sample_page(fill)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_no],
&[],
&queue,
)
})
};
let writer_a = spawn_commit(5, 0x51);
if !wait_gate_timeout(&first_prepare_entered, Duration::from_secs(1)) {
signal_gate(&release_first_prepare);
let _ = writer_a.join();
panic!("bead_id={BEAD_ID} case=promoted_epoch_first_flush_reached_prepare");
}
let writer_b = spawn_commit(2, 0x22);
let deadline = Instant::now() + Duration::from_secs(1);
let mut pipelined = false;
while Instant::now() < deadline {
let observed_pipelined = {
let consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
consolidator.has_pipelined_batches()
};
if observed_pipelined {
pipelined = true;
break;
}
std::thread::yield_now();
}
if !pipelined {
signal_gate(&release_first_prepare);
let _ = writer_a.join();
let _ = writer_b.join();
panic!("bead_id={BEAD_ID} case=promoted_epoch_second_writer_pipelined");
}
signal_gate(&release_first_prepare);
writer_a
.join()
.expect("first writer thread should not panic")
.unwrap();
writer_b
.join()
.expect("second writer thread should not panic")
.unwrap();
let written = frames.lock().unwrap();
let commit_sizes = written
.iter()
.map(|(_, _, db_size_if_commit)| *db_size_if_commit)
.collect::<Vec<_>>();
assert_eq!(
commit_sizes,
vec![5, 5],
"bead_id={BEAD_ID} case=promoted_epoch_commit_marker_must_not_shrink_db_size"
);
assert_eq!(
inner.lock().unwrap().db_size,
5,
"bead_id={BEAD_ID} case=promoted_epoch_inner_db_size_must_not_shrink"
);
}
#[test]
fn test_group_commit_queue_retains_failed_epoch_for_late_waiter() {
let queue = GroupCommitQueue::new(GroupCommitConfig::default());
queue.publish_failed_epoch(
1,
&FrankenError::internal("forced group commit flush failure"),
false,
);
queue.publish_completed_epoch(2, false);
let guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let err = queue.wait_for_epoch_outcome(guard, 1).unwrap_err();
let message = err.to_string();
assert!(
message.contains("epoch 1"),
"bead_id={BEAD_ID} case=group_commit_failed_epoch_late_waiter_mentions_epoch message={message}"
);
assert!(
message.contains("forced group commit flush failure"),
"bead_id={BEAD_ID} case=group_commit_failed_epoch_late_waiter_preserves_detail message={message}"
);
}
#[test]
fn test_group_commit_queue_success_not_poisoned_by_other_failed_epoch() {
let queue = GroupCommitQueue::new(GroupCommitConfig::default());
queue.publish_failed_epoch(
1,
&FrankenError::internal("forced group commit flush failure"),
false,
);
queue.publish_completed_epoch(2, false);
let guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(matches!(
queue.wait_for_epoch_outcome(guard, 2).unwrap(),
WaitForEpochOutcome::Completed
));
}
#[test]
fn test_group_commit_queue_publish_synchronizes_with_waiter_mutex() {
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
let guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (started_tx, started_rx) = std::sync::mpsc::channel();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let publish_queue = Arc::clone(&queue);
let handle = std::thread::spawn(move || {
started_tx
.send(())
.expect("publisher thread should signal start");
publish_queue.publish_completed_epoch(1, false);
done_tx
.send(())
.expect("publisher thread should signal completion");
});
started_rx
.recv_timeout(std::time::Duration::from_secs(1))
.expect("publisher thread should start while waiter holds the mutex");
assert!(
done_rx
.recv_timeout(std::time::Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=group_commit_publish_must_block_behind_waiter_mutex"
);
drop(guard);
done_rx
.recv_timeout(std::time::Duration::from_secs(1))
.expect("publisher thread should complete once the waiter mutex is released");
handle.join().unwrap();
let guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(matches!(
queue.wait_for_epoch_outcome(guard, 1).unwrap(),
WaitForEpochOutcome::Completed
));
}
#[test]
fn test_group_commit_queue_waiter_takes_over_promoted_epoch_vacancy() {
let queue = GroupCommitQueue::new(GroupCommitConfig::default());
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batch1 = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: sample_page(0x01),
db_size_if_commit: 1,
}]);
assert_eq!(
consolidator.submit_batch(batch1).unwrap(),
SubmitOutcome::Flusher
);
let _ = consolidator.begin_flush().unwrap();
let pipelined_batch = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x02),
db_size_if_commit: 2,
}]);
assert_eq!(
consolidator.submit_batch(pipelined_batch).unwrap(),
SubmitOutcome::Waiter
);
}
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(consolidator.complete_flush().unwrap());
}
queue.publish_completed_epoch(1, true);
let guard = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let outcome = queue.wait_for_epoch_outcome(guard, 2).unwrap();
let WaitForEpochOutcome::TakeOverFlusher {
flush_epoch,
batches,
} = outcome
else {
panic!("promoted epoch waiter should take over the flusher vacancy");
};
assert_eq!(flush_epoch, 2);
assert_eq!(batches.len(), 1);
}
#[test]
fn test_keyed_wait_registry_signals_only_target_key() {
let registry = Arc::new(KeyedWaitRegistry::new());
let slot_a = registry.slot(11);
let slot_b = registry.slot(17);
let generation_a = slot_a.generation();
let generation_b = slot_b.generation();
let (done_a_tx, done_a_rx) = std::sync::mpsc::channel();
let (done_b_tx, done_b_rx) = std::sync::mpsc::channel();
let waiter_a = std::thread::spawn(move || {
done_a_tx
.send(slot_a.wait_for_change(generation_a, Duration::from_secs(1)))
.expect("waiter A should report");
});
let waiter_b = std::thread::spawn(move || {
done_b_tx
.send(slot_b.wait_for_change(generation_b, Duration::from_secs(1)))
.expect("waiter B should report");
});
assert!(registry.signal(11));
assert_eq!(
done_a_rx
.recv_timeout(Duration::from_millis(100))
.expect("targeted waiter should wake"),
KeyedWaitResult::Signaled,
"bead_id={BEAD_ID} case=keyed_wait_registry_wakes_target_key"
);
assert!(
done_b_rx.recv_timeout(Duration::from_millis(20)).is_err(),
"bead_id={BEAD_ID} case=keyed_wait_registry_does_not_herd_wake_other_keys"
);
assert!(registry.signal(17));
assert_eq!(
done_b_rx
.recv_timeout(Duration::from_millis(100))
.expect("second waiter should wake once signaled"),
KeyedWaitResult::Signaled,
"bead_id={BEAD_ID} case=keyed_wait_registry_wakes_second_key"
);
waiter_a.join().unwrap();
waiter_b.join().unwrap();
}
#[test]
fn test_keyed_wait_slot_returns_signaled_after_generation_advance() {
let slot = KeyedWaitSlot::default();
let observed_generation = slot.generation();
slot.signal();
assert_eq!(
slot.wait_for_change(observed_generation, Duration::from_millis(1)),
KeyedWaitResult::Signaled,
"bead_id={BEAD_ID} case=keyed_wait_slot_pre_signaled_generation_must_not_timeout"
);
}
#[test]
fn test_keyed_wait_registry_prunes_stale_slots_after_last_waiter_drops() {
let registry = KeyedWaitRegistry::new();
{
let _slot = registry.slot(23);
assert!(
registry.has_slot(23),
"bead_id={BEAD_ID} case=keyed_wait_registry_live_slot_visible_while_held"
);
}
assert!(
!registry.signal(23),
"bead_id={BEAD_ID} case=keyed_wait_registry_stale_slot_must_not_report_signal"
);
assert!(
!registry.has_slot(23),
"bead_id={BEAD_ID} case=keyed_wait_registry_prunes_stale_slot_after_signal_attempt"
);
}
#[test]
fn test_group_commit_queue_promoted_waiter_wakes_on_targeted_publish() {
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batch1 = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: sample_page(0x10),
db_size_if_commit: 1,
}]);
assert_eq!(
consolidator.submit_batch(batch1).unwrap(),
SubmitOutcome::Flusher
);
let _ = consolidator.begin_flush().unwrap();
let pipelined_batch = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x20),
db_size_if_commit: 2,
}]);
assert_eq!(
consolidator.submit_batch(pipelined_batch).unwrap(),
SubmitOutcome::Waiter
);
}
let waiter_queue = Arc::clone(&queue);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
let guard = waiter_queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _registered_slot = waiter_queue.epoch_waiters.slot(2);
ready_tx.send(()).expect("waiter should signal readiness");
let outcome = waiter_queue.wait_for_epoch_outcome(guard, 2);
done_tx.send(outcome).expect("waiter should report outcome");
});
ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("waiter should start");
assert!(
done_rx.recv_timeout(Duration::from_millis(20)).is_err(),
"bead_id={BEAD_ID} case=group_commit_promoted_waiter_stays_parked_until_targeted_publish"
);
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(consolidator.complete_flush().unwrap());
}
queue.publish_completed_epoch(1, true);
let outcome = done_rx
.recv_timeout(Duration::from_millis(100))
.expect("targeted publish should wake the promoted waiter")
.expect("waiter should succeed");
let WaitForEpochOutcome::TakeOverFlusher {
flush_epoch,
batches,
} = outcome
else {
panic!("promoted epoch waiter should take over after targeted wake");
};
assert_eq!(flush_epoch, 2);
assert_eq!(batches.len(), 1);
waiter.join().unwrap();
}
#[test]
fn test_group_commit_queue_completed_publish_wakes_target_and_next_epoch_waiters() {
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batch1 = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: sample_page(0x31),
db_size_if_commit: 1,
}]);
assert_eq!(
consolidator.submit_batch(batch1).unwrap(),
SubmitOutcome::Flusher
);
let _ = consolidator.begin_flush().unwrap();
let pipelined_batch = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x32),
db_size_if_commit: 2,
}]);
assert_eq!(
consolidator.submit_batch(pipelined_batch).unwrap(),
SubmitOutcome::Waiter
);
}
let _target_slot = queue.epoch_waiters.slot(1);
let _next_slot = queue.epoch_waiters.slot(2);
let spawn_waiter = |target_epoch: u64| {
let waiter_queue = Arc::clone(&queue);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
let guard = waiter_queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ready_tx.send(()).expect("waiter should signal readiness");
let outcome = waiter_queue.wait_for_epoch_outcome(guard, target_epoch);
done_tx.send(outcome).expect("waiter should report outcome");
});
(handle, ready_rx, done_rx)
};
let (target_handle, target_ready_rx, target_done_rx) = spawn_waiter(1);
let (next_handle, next_ready_rx, next_done_rx) = spawn_waiter(2);
target_ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("target waiter should start");
next_ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("next-epoch waiter should start");
assert!(
target_done_rx
.recv_timeout(Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=group_commit_completed_publish_target_waiter_stays_parked_until_publish"
);
assert!(
next_done_rx
.recv_timeout(Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=group_commit_completed_publish_next_waiter_stays_parked_until_publish"
);
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
consolidator.complete_flush().unwrap(),
"bead_id={BEAD_ID} case=group_commit_completed_publish_must_promote_next_epoch"
);
}
queue.publish_completed_epoch(1, true);
let target_outcome = target_done_rx
.recv_timeout(Duration::from_millis(100))
.expect("target waiter should wake")
.expect("target waiter should succeed");
assert!(
matches!(target_outcome, WaitForEpochOutcome::Completed),
"bead_id={BEAD_ID} case=group_commit_completed_publish_target_waiter_observes_completed_epoch outcome={target_outcome:?}"
);
let next_outcome = next_done_rx
.recv_timeout(Duration::from_millis(100))
.expect("next-epoch waiter should wake")
.expect("next-epoch waiter should succeed");
let WaitForEpochOutcome::TakeOverFlusher {
flush_epoch,
batches,
} = next_outcome
else {
panic!(
"bead_id={BEAD_ID} case=group_commit_completed_publish_next_waiter_must_take_over_flusher outcome={next_outcome:?}"
);
};
assert_eq!(
flush_epoch, 2,
"bead_id={BEAD_ID} case=group_commit_completed_publish_next_waiter_flush_epoch"
);
assert_eq!(
batches.len(),
1,
"bead_id={BEAD_ID} case=group_commit_completed_publish_next_waiter_batch_count"
);
target_handle.join().unwrap();
next_handle.join().unwrap();
}
#[test]
fn test_group_commit_queue_claimed_promoted_epoch_blocks_parallel_takeover() {
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batch1 = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: sample_page(0x51),
db_size_if_commit: 1,
}]);
assert_eq!(
consolidator.submit_batch(batch1).unwrap(),
SubmitOutcome::Flusher
);
let _ = consolidator.begin_flush().unwrap();
let pipelined_batch = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x52),
db_size_if_commit: 2,
}]);
assert_eq!(
consolidator.submit_batch(pipelined_batch).unwrap(),
SubmitOutcome::Waiter
);
}
let waiter_queue = Arc::clone(&queue);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
let guard = waiter_queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _registered_slot = waiter_queue.epoch_waiters.slot(2);
ready_tx.send(()).expect("waiter should signal readiness");
let outcome = waiter_queue.wait_for_epoch_outcome(guard, 2);
done_tx.send(outcome).expect("waiter should report outcome");
});
ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("next-epoch waiter should start");
assert!(
done_rx.recv_timeout(Duration::from_millis(20)).is_err(),
"bead_id={BEAD_ID} case=group_commit_claimed_promoted_epoch_waiter_stays_parked_before_publish"
);
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(consolidator.complete_flush().unwrap());
assert!(
consolidator.claim_flusher_vacancy(),
"bead_id={BEAD_ID} case=group_commit_claimed_promoted_epoch_original_flusher_claims"
);
}
queue.publish_completed_epoch(1, true);
assert!(
done_rx.recv_timeout(Duration::from_millis(20)).is_err(),
"bead_id={BEAD_ID} case=group_commit_claimed_promoted_epoch_waiter_must_not_take_over"
);
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batches = consolidator.begin_flush().unwrap();
assert_eq!(batches.len(), 1);
assert_eq!(consolidator.epoch(), 2);
assert!(!consolidator.complete_flush().unwrap());
}
queue.publish_completed_epoch(2, false);
let outcome = done_rx
.recv_timeout(Duration::from_millis(100))
.expect("next-epoch waiter should wake after its epoch completes")
.expect("next-epoch waiter should succeed");
assert!(
matches!(outcome, WaitForEpochOutcome::Completed),
"bead_id={BEAD_ID} case=group_commit_claimed_promoted_epoch_waiter_observes_completion outcome={outcome:?}"
);
waiter.join().unwrap();
}
#[test]
fn test_group_commit_queue_failed_publish_wakes_failed_and_next_epoch_waiters() {
let queue = Arc::new(GroupCommitQueue::new(GroupCommitConfig::default()));
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let batch1 = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: sample_page(0x41),
db_size_if_commit: 1,
}]);
assert_eq!(
consolidator.submit_batch(batch1).unwrap(),
SubmitOutcome::Flusher
);
let _ = consolidator.begin_flush().unwrap();
let pipelined_batch = TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x42),
db_size_if_commit: 2,
}]);
assert_eq!(
consolidator.submit_batch(pipelined_batch).unwrap(),
SubmitOutcome::Waiter
);
}
let _failed_slot = queue.epoch_waiters.slot(1);
let _next_slot = queue.epoch_waiters.slot(2);
let spawn_waiter = |target_epoch: u64| {
let waiter_queue = Arc::clone(&queue);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let (done_tx, done_rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
let guard = waiter_queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ready_tx.send(()).expect("waiter should signal readiness");
let outcome = waiter_queue.wait_for_epoch_outcome(guard, target_epoch);
done_tx.send(outcome).expect("waiter should report outcome");
});
(handle, ready_rx, done_rx)
};
let (failed_handle, failed_ready_rx, failed_done_rx) = spawn_waiter(1);
let (next_handle, next_ready_rx, next_done_rx) = spawn_waiter(2);
failed_ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("failed-epoch waiter should start");
next_ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("next-epoch waiter should start");
assert!(
failed_done_rx
.recv_timeout(Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=group_commit_failed_publish_failed_waiter_stays_parked_until_publish"
);
assert!(
next_done_rx
.recv_timeout(Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=group_commit_failed_publish_next_waiter_stays_parked_until_publish"
);
{
let mut consolidator = queue
.consolidator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
consolidator.abort_flush().unwrap();
assert!(
consolidator.has_flusher_vacancy(),
"bead_id={BEAD_ID} case=group_commit_failed_publish_abort_must_expose_flusher_vacancy"
);
}
let failure = FrankenError::internal("forced keyed failed epoch for proof coverage");
queue.publish_failed_epoch(1, &failure, true);
let failed_error = failed_done_rx
.recv_timeout(Duration::from_millis(100))
.expect("failed-epoch waiter should wake")
.expect_err("failed-epoch waiter should surface the flush failure")
.to_string();
assert!(
failed_error.contains("epoch 1"),
"bead_id={BEAD_ID} case=group_commit_failed_publish_failed_waiter_mentions_epoch error={failed_error}"
);
assert!(
failed_error.contains("forced keyed failed epoch for proof coverage"),
"bead_id={BEAD_ID} case=group_commit_failed_publish_failed_waiter_preserves_detail error={failed_error}"
);
let next_outcome = next_done_rx
.recv_timeout(Duration::from_millis(100))
.expect("next-epoch waiter should wake")
.expect("next-epoch waiter should succeed");
let WaitForEpochOutcome::TakeOverFlusher {
flush_epoch,
batches,
} = next_outcome
else {
panic!(
"bead_id={BEAD_ID} case=group_commit_failed_publish_next_waiter_must_take_over_flusher outcome={next_outcome:?}"
);
};
assert_eq!(
flush_epoch, 2,
"bead_id={BEAD_ID} case=group_commit_failed_publish_next_waiter_flush_epoch"
);
assert_eq!(
batches.len(),
1,
"bead_id={BEAD_ID} case=group_commit_failed_publish_next_waiter_batch_count"
);
failed_handle.join().unwrap();
next_handle.join().unwrap();
}
#[test]
fn test_commit_service_policy_sparse_queue_prefers_low_latency_mode() {
let max_wait = Duration::from_micros(250);
let fill_age = Duration::from_micros(3);
let fairness_budget = default_commit_service_fairness_budget(max_wait);
let decision = decide_group_commit_arrival_wait(
Some(ArrivalWaitObservation {
pending_batch_count: 1,
should_flush_now: false,
fill_age,
}),
max_wait,
fairness_budget,
fill_age,
CommitServiceMode::Balanced,
1,
);
assert_eq!(
decision.mode,
CommitServiceMode::LowLatency,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=sparse_queue_prefers_low_latency_mode"
);
assert_eq!(
decision.wait_budget, GROUP_COMMIT_SPARSE_ARRIVAL_WAIT,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=sparse_queue_uses_low_latency_budget"
);
assert_eq!(
decision.reason, "sparse_queue",
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=sparse_queue_reason"
);
assert!(
decision.used_legacy_fallback,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=sparse_queue_marks_sparse_fallback"
);
}
#[test]
fn test_commit_service_policy_throughput_hysteresis_stays_enabled_for_bursty_backlog() {
let max_wait = Duration::from_micros(250);
let fill_age = Duration::from_micros(7);
let fairness_budget = default_commit_service_fairness_budget(max_wait);
let queue_age_p95 = Duration::from_micros(30);
let decision = decide_group_commit_arrival_wait(
Some(ArrivalWaitObservation {
pending_batch_count: 3,
should_flush_now: false,
fill_age,
}),
max_wait,
fairness_budget,
queue_age_p95,
CommitServiceMode::Throughput,
2,
);
assert_eq!(
decision.mode,
CommitServiceMode::Throughput,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=bursty_backlog_stays_in_throughput_mode"
);
assert_eq!(
decision.reason, "throughput_hysteresis",
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=bursty_backlog_hysteresis_reason"
);
assert!(
decision.wait_budget <= GROUP_COMMIT_BURST_ARRIVAL_WAIT,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=bursty_backlog_wait_budget_is_bounded"
);
assert!(
!decision.starvation_prevented,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=bursty_backlog_does_not_trip_starvation_guard"
);
}
#[test]
fn test_commit_service_policy_starvation_guard_skips_wait_under_tail_pressure() {
let max_wait = Duration::from_micros(120);
let fill_age = Duration::from_micros(40);
let fairness_budget = default_commit_service_fairness_budget(max_wait);
let decision = decide_group_commit_arrival_wait(
Some(ArrivalWaitObservation {
pending_batch_count: 2,
should_flush_now: false,
fill_age,
}),
max_wait,
fairness_budget,
Duration::from_micros(180),
CommitServiceMode::Throughput,
3,
);
assert_eq!(
decision.wait_budget,
Duration::ZERO,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=tail_pressure_zeroes_wait_budget"
);
assert_eq!(
decision.mode,
CommitServiceMode::LowLatency,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=tail_pressure_falls_back_to_low_latency"
);
assert_eq!(
decision.reason, "tail_latency_pressure",
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=tail_pressure_reason"
);
assert!(
decision.starvation_prevented,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=tail_pressure_sets_starvation_guard"
);
}
#[test]
fn test_commit_service_policy_skips_promoted_follow_on_flushes() {
let max_wait = Duration::from_micros(250);
let fairness_budget = default_commit_service_fairness_budget(max_wait);
let decision = decide_group_commit_arrival_wait(
None,
max_wait,
fairness_budget,
Duration::ZERO,
CommitServiceMode::Balanced,
4,
);
assert_eq!(
decision.wait_budget,
Duration::ZERO,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=promoted_follow_on_budget"
);
assert_eq!(
decision.reason, "promoted_follow_on",
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=promoted_follow_on_reason"
);
assert_eq!(
decision.max_wait, max_wait,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=promoted_follow_on_preserves_max_wait"
);
}
#[test]
fn test_commit_service_policy_records_wait_contract_metadata() {
let max_wait = Duration::from_micros(250);
let fill_age = Duration::from_micros(3);
let fairness_budget = default_commit_service_fairness_budget(max_wait);
let queue_age_p95 = Duration::from_micros(11);
let decision = decide_group_commit_arrival_wait(
Some(ArrivalWaitObservation {
pending_batch_count: 1,
should_flush_now: false,
fill_age,
}),
max_wait,
fairness_budget,
queue_age_p95,
CommitServiceMode::Balanced,
9,
);
assert_eq!(
decision.control_epoch, 9,
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=metadata_control_epoch"
);
assert_eq!(
decision.target_wait_ns(),
u64::try_from(GROUP_COMMIT_SPARSE_ARRIVAL_WAIT.as_nanos()).unwrap_or(u64::MAX),
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=metadata_target_wait_ns"
);
assert_eq!(
decision.max_wait_ns(),
u64::try_from(max_wait.as_nanos()).unwrap_or(u64::MAX),
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=metadata_max_wait_ns"
);
assert_eq!(
decision.fairness_budget_ns(),
u64::try_from(fairness_budget.as_nanos()).unwrap_or(u64::MAX),
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=metadata_fairness_budget_ns"
);
assert_eq!(
decision.queue_age_p95_ns(),
u64::try_from(queue_age_p95.as_nanos()).unwrap_or(u64::MAX),
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=metadata_queue_age_p95_ns"
);
assert_eq!(
decision.queue_delay_ns(),
u64::try_from(fill_age.as_nanos()).unwrap_or(u64::MAX),
"bead_id={COMMIT_SERVICE_POLICY_BEAD_ID} case=metadata_queue_delay_ns"
);
}
#[test]
fn test_commit_service_policy_logs_sparse_queue_metadata() {
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
GLOBAL_CONSOLIDATION_METRICS.reset();
init_publication_test_tracing();
let vfs = MemoryVfs::new();
let path = PathBuf::from("/commit_service_policy_sparse_queue.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let observed_lock_level = Arc::new(Mutex::new(LockLevel::Reserved));
let (
backend,
_frames,
_append_frames_calls,
_append_prepared_calls,
_prepare_lock_levels,
_append_lock_levels,
) = PreparedBatchObservedWalBackend::new(observed_lock_level);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
..ParallelWalControlSurface::default()
},
));
let page_two = PageNumber::new(2).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_two,
StagedPage::from_bytes(&pager.pool, &sample_page(0x71)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_two],
&[],
&queue,
)
.unwrap();
GLOBAL_CONSOLIDATION_METRICS.reset();
}
#[test]
fn test_commit_service_policy_logs_tail_guard_metadata() {
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
GLOBAL_CONSOLIDATION_METRICS.reset();
for _ in 0..8 {
GLOBAL_CONSOLIDATION_METRICS.record_phase_timing(0, 0, 0, true, 2_000, 0, 0, 0, 0, 0);
}
init_publication_test_tracing();
let vfs = MemoryVfs::new();
let path = PathBuf::from("/commit_service_policy_tail_guard.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let observed_lock_level = Arc::new(Mutex::new(LockLevel::Reserved));
let (
backend,
_frames,
_append_frames_calls,
_append_prepared_calls,
_prepare_lock_levels,
_append_lock_levels,
) = PreparedBatchObservedWalBackend::new(observed_lock_level);
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface {
mode: ParallelWalOperatingMode::Auto,
lane_count_override: Some(2),
max_flush_delay_ms: Some(1),
..ParallelWalControlSurface::default()
},
));
let page_two = PageNumber::new(2).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_two,
StagedPage::from_bytes(&pager.pool, &sample_page(0x83)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_two],
&[],
&queue,
)
.unwrap();
GLOBAL_CONSOLIDATION_METRICS.reset();
}
#[test]
fn test_physical_writer_batch_membership_preserves_submission_order() {
let batches = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: sample_page(0x55),
db_size_if_commit: 2,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 41,
lane_id: 0,
staged_frame_count: 1,
staging_elapsed_ns: 17,
}),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 3,
page_data: sample_page(0x66),
db_size_if_commit: 3,
}])
.with_context(TransactionFrameBatchContext {
batch_id: 42,
lane_id: 1,
staged_frame_count: 1,
staging_elapsed_ns: 19,
}),
];
assert_eq!(
physical_writer_primary_batch_id(&batches),
41,
"bead_id=bd-1dp9.6.7.9.1 case=physical_writer_primary_batch_id_tracks_head"
);
assert_eq!(
physical_writer_batch_membership(&batches),
"41,42",
"bead_id=bd-1dp9.6.7.9.1 case=physical_writer_batch_membership_tracks_order"
);
}
#[test]
fn test_physical_writer_rollback_mode_active_tracks_disable_paths() {
assert!(
physical_writer_rollback_mode_active(ParallelWalOperatingMode::Conservative, None),
"bead_id=bd-1dp9.6.7.9.1 case=physical_writer_rollback_mode_active_operator_forced"
);
assert!(
physical_writer_rollback_mode_active(
ParallelWalOperatingMode::Auto,
Some(ParallelWalFallbackReason::LaneOverflow),
),
"bead_id=bd-1dp9.6.7.9.1 case=physical_writer_rollback_mode_active_runtime_fallback"
);
assert!(
!physical_writer_rollback_mode_active(ParallelWalOperatingMode::Auto, None),
"bead_id=bd-1dp9.6.7.9.1 case=physical_writer_rollback_mode_active_clean_auto_mode"
);
}
#[test]
#[ignore = "benchmark evidence only"]
fn wal_publish_window_shrink_benchmark_report() {
let cases: Vec<_> = TRACK_C_PUBLISH_WINDOW_BENCH_CASES
.iter()
.map(|(scenario_id, dirty_pages)| {
let baseline_hold_samples = track_c_measure_publish_window_hold_ns(
TrackCPublishWindowMode::InlinePrepareBaseline,
*dirty_pages,
);
let candidate_hold_samples = track_c_measure_publish_window_hold_ns(
TrackCPublishWindowMode::PreparedCandidate,
*dirty_pages,
);
let baseline_stall_samples = track_c_measure_competing_writer_stall_ns(
TrackCPublishWindowMode::InlinePrepareBaseline,
*dirty_pages,
);
let candidate_stall_samples = track_c_measure_competing_writer_stall_ns(
TrackCPublishWindowMode::PreparedCandidate,
*dirty_pages,
);
let baseline_hold_summary = track_c_sample_summary(&baseline_hold_samples);
let candidate_hold_summary = track_c_sample_summary(&candidate_hold_samples);
let baseline_stall_summary = track_c_sample_summary(&baseline_stall_samples);
let candidate_stall_summary = track_c_sample_summary(&candidate_stall_samples);
let baseline_hold_median =
baseline_hold_summary["median_ns"].as_u64().unwrap_or(0);
let candidate_hold_median =
candidate_hold_summary["median_ns"].as_u64().unwrap_or(0);
let baseline_stall_median =
baseline_stall_summary["median_ns"].as_u64().unwrap_or(0);
let candidate_stall_median =
candidate_stall_summary["median_ns"].as_u64().unwrap_or(0);
json!({
"scenario_id": scenario_id,
"dirty_pages": dirty_pages,
"exclusive_window_hold_baseline": baseline_hold_summary,
"exclusive_window_hold_candidate": candidate_hold_summary,
"contending_writer_stall_baseline": baseline_stall_summary,
"contending_writer_stall_candidate": candidate_stall_summary,
"hold_reduction_ratio_median": if baseline_hold_median == 0 {
0.0
} else {
1.0 - (candidate_hold_median as f64 / baseline_hold_median as f64)
},
"stall_reduction_ratio_median": if baseline_stall_median == 0 {
0.0
} else {
1.0 - (candidate_stall_median as f64 / baseline_stall_median as f64)
},
"faster_variant_by_hold_median": if candidate_hold_median <= baseline_hold_median {
"prepared_candidate"
} else {
"inline_prepare_baseline"
},
"faster_variant_by_stall_median": if candidate_stall_median <= baseline_stall_median {
"prepared_candidate"
} else {
"inline_prepare_baseline"
},
})
})
.collect();
let report = json!({
"schema_version": "fsqlite.track_c.publish_window_benchmark.v1",
"bead_id": TRACK_C_PUBLISH_WINDOW_BENCH_BEAD_ID,
"parent_bead_id": "bd-db300.3.2",
"measured_operation": "pager_commit_wal_publish_window",
"warmup_iterations": TRACK_C_PUBLISH_WINDOW_BENCH_WARMUP_ITERS,
"measurement_iterations": TRACK_C_PUBLISH_WINDOW_BENCH_MEASURE_ITERS,
"vfs": "blocking_memory_vfs",
"baseline_variant": "inline_prepare_under_exclusive_lock",
"candidate_variant": "prepared_batch_before_exclusive_lock",
"cases": cases,
});
println!("BEGIN_BD_DB300_3_2_3_REPORT");
println!("{}", serde_json::to_string_pretty(&report).unwrap());
println!("END_BD_DB300_3_2_3_REPORT");
}
#[test]
#[ignore = "benchmark evidence only"]
fn wal_commit_batch_benchmark_report() {
let cases: Vec<_> = TRACK_C_BATCH_BENCH_CASES
.iter()
.map(|(scenario_id, dirty_pages)| {
let single_samples =
track_c_measure_commit_ns(TrackCBatchMode::SingleFrame, *dirty_pages);
let batch_samples =
track_c_measure_commit_ns(TrackCBatchMode::Batched, *dirty_pages);
let single_summary = track_c_sample_summary(&single_samples);
let batch_summary = track_c_sample_summary(&batch_samples);
let single_median = single_summary["median_ns"].as_u64().unwrap_or(0);
let batch_median = batch_summary["median_ns"].as_u64().unwrap_or(0);
let single_mean = single_summary["mean_ns"].as_f64().unwrap_or(0.0);
let batch_mean = batch_summary["mean_ns"].as_f64().unwrap_or(0.0);
json!({
"scenario_id": scenario_id,
"dirty_pages": dirty_pages,
"single_frame": single_summary,
"batch_append": batch_summary,
"speedup_vs_single_median": if batch_median == 0 {
0.0
} else {
(single_median as f64) / (batch_median as f64)
},
"speedup_vs_single_mean": if batch_mean == 0.0 {
0.0
} else {
single_mean / batch_mean
},
"faster_variant_by_median": if batch_median <= single_median {
"batch_append"
} else {
"single_frame"
},
})
})
.collect();
let report = json!({
"schema_version": "fsqlite.track_c.batch_commit_benchmark.v1",
"bead_id": TRACK_C_BATCH_BENCH_BEAD_ID,
"parent_bead_id": "bd-db300.3.1",
"measured_operation": "pager_commit_wal_path",
"warmup_iterations": TRACK_C_BATCH_BENCH_WARMUP_ITERS,
"measurement_iterations": TRACK_C_BATCH_BENCH_MEASURE_ITERS,
"vfs": "memory",
"sync_mode": "normal_noop_memory_vfs",
"baseline_variant": "single_frame_append_loop",
"candidate_variant": "transaction_wide_batch_append",
"cases": cases,
});
println!("BEGIN_BD_DB300_3_1_4_REPORT");
println!("{}", serde_json::to_string_pretty(&report).unwrap());
println!("END_BD_DB300_3_1_4_REPORT");
}
#[test]
#[ignore = "inventory evidence only"]
fn wal_publish_window_inventory_report() {
let outside_window = vec![
json!({
"component": "collect_wal_commit_batch",
"location": "crates/fsqlite-pager/src/pager.rs::commit_wal",
"classification": "already_outside_publish_window",
"move_candidate": "not_applicable",
"rationale": "frame ordering, commit-marker boundary, and new_db_size derivation happen before EXCLUSIVE lock acquisition",
}),
json!({
"component": "wal_adapter_frame_ref_copy",
"location": "crates/fsqlite-core/src/wal_adapter.rs::prepare_append_frames",
"classification": "pure_copy",
"move_candidate": "completed",
"rationale": "borrowed WalFrameRef values are copied into owned batch metadata before the exclusive publish window starts",
}),
json!({
"component": "wal_batch_buffer_allocation",
"location": "crates/fsqlite-wal/src/wal.rs::prepare_frame_bytes",
"classification": "allocation",
"move_candidate": "completed",
"rationale": "contiguous frame buffer allocation now happens in the prepare phase before EXCLUSIVE lock acquisition",
}),
json!({
"component": "wal_header_and_payload_copy",
"location": "crates/fsqlite-wal/src/wal.rs::prepare_frame_bytes",
"classification": "pure_copy",
"move_candidate": "completed",
"rationale": "page-number/db-size stamping, salt staging, and payload copies are fully serialized before publish time",
}),
json!({
"component": "wal_checksum_transform_precompute",
"location": "crates/fsqlite-core/src/wal_adapter.rs::prepare_append_frames",
"classification": "pure_compute",
"move_candidate": "completed",
"rationale": "per-frame checksum transforms are now derived outside the lock so publish only rebinds the live seed",
}),
json!({
"component": "wal_prelock_checksum_finalize",
"location": "crates/fsqlite-core/src/wal_adapter.rs::finalize_prepared_frames",
"classification": "pure_compute",
"move_candidate": "completed",
"rationale": "prepared batches now refresh/publish the base snapshot and stamp checksum fields before EXCLUSIVE when the live append window is still open to optimistic reuse",
}),
];
let inside_window = vec![
json!({
"component": "lock_exclusive",
"location": "crates/fsqlite-pager/src/pager.rs::commit_wal",
"classification": "serialized_boundary",
"move_candidate": "no",
"rationale": "cross-process WAL append exclusion is the start of the current publish window",
}),
json!({
"component": "wal_refresh_before_append",
"location": "crates/fsqlite-core/src/wal_adapter.rs::append_prepared_frames",
"classification": "state_refresh_read_only",
"move_candidate": "conditional",
"rationale": "prepared batches can skip the lock-held refresh when the pre-lock append-window token still matches on disk, but stale windows still require a refresh before durable append",
}),
json!({
"component": "wal_append_window_validation",
"location": "crates/fsqlite-core/src/wal_adapter.rs::append_prepared_frames",
"classification": "state_validation",
"move_candidate": "conditional",
"rationale": "the lock-held path now performs a cheap file-size/header check to decide whether the pre-lock finalized batch can be reused or whether it must fall back to refresh/re-finalize",
}),
json!({
"component": "wal_checksum_seed_rebind_fallback",
"location": "crates/fsqlite-core/src/wal_adapter.rs::append_prepared_frames",
"classification": "publish_seed_binding",
"move_candidate": "conditional",
"rationale": "checksum rebinding remains inside the publish window only for stale-window fallback cases where another writer changed the live append seed before EXCLUSIVE was acquired",
}),
json!({
"component": "wal_file_write",
"location": "crates/fsqlite-wal/src/wal.rs::append_finalized_prepared_frame_bytes",
"classification": "durable_state_transition",
"move_candidate": "no",
"rationale": "single contiguous file write is the core serialized append that must observe the authoritative WAL end",
}),
json!({
"component": "wal_state_advance_after_write",
"location": "crates/fsqlite-wal/src/wal.rs::append_finalized_prepared_frame_bytes",
"classification": "durable_state_transition",
"move_candidate": "no",
"rationale": "frame_count/running_checksum advancement must match the durable append that just occurred",
}),
json!({
"component": "fec_hook_on_frame",
"location": "crates/fsqlite-core/src/wal_adapter.rs::append_prepared_frames",
"classification": "post_append_compute",
"move_candidate": "yes",
"rationale": "FEC hook work remains explicitly non-fatal and still runs after append while the publish window is open",
}),
json!({
"component": "wal_sync",
"location": "crates/fsqlite-pager/src/pager.rs::commit_wal",
"classification": "durability_barrier",
"move_candidate": "no",
"rationale": "sync is the durability barrier for the commit and therefore part of the required serialized state transition",
}),
json!({
"component": "inner_db_size_update",
"location": "crates/fsqlite-pager/src/pager.rs::commit_wal",
"classification": "pager_state_publish",
"move_candidate": "no",
"rationale": "pager-visible db_size must only advance after the WAL append and sync succeed",
}),
];
let definitely_movable = inside_window
.iter()
.filter(|entry| entry["move_candidate"] == "yes")
.count();
let conditionally_movable = inside_window
.iter()
.filter(|entry| entry["move_candidate"] == "conditional")
.count();
let required_serialized = inside_window
.iter()
.filter(|entry| entry["move_candidate"] == "no")
.count();
let report = json!({
"schema_version": "fsqlite.track_c.publish_window_inventory.v1",
"bead_id": TRACK_C_PUBLISH_WINDOW_INVENTORY_BEAD_ID,
"parent_bead_id": "bd-db300.3.2",
"measured_operation": "pager_commit_wal_path",
"measurement_anchor_bead_id": TRACK_C_BATCH_BENCH_BEAD_ID,
"measurement_anchor_report": "fsqlite.track_c.batch_commit_benchmark.v1",
"current_window": {
"entry_function": "crates/fsqlite-pager/src/pager.rs::commit_wal",
"window_begins_at": "inner.db_file.lock(cx, LockLevel::Exclusive)?",
"window_ends_after": "inner.db_size = batch.new_db_size",
},
"outside_window": outside_window,
"inside_window": inside_window,
"summary": {
"outside_window_steps": outside_window.len(),
"inside_window_steps": inside_window.len(),
"definitely_movable_inside_window_steps": definitely_movable,
"conditionally_movable_inside_window_steps": conditionally_movable,
"required_serialized_inside_window_steps": required_serialized,
},
});
println!("BEGIN_BD_DB300_3_2_1_REPORT");
println!("{}", serde_json::to_string_pretty(&report).unwrap());
println!("END_BD_DB300_3_2_1_REPORT");
assert_eq!(
report["measured_operation"], "pager_commit_wal_path",
"bead_id={TRACK_C_PUBLISH_WINDOW_INVENTORY_BEAD_ID} case=measured_operation_anchor"
);
assert_eq!(
report["summary"]["definitely_movable_inside_window_steps"], 5,
"bead_id={TRACK_C_PUBLISH_WINDOW_INVENTORY_BEAD_ID} case=movable_step_count"
);
assert_eq!(
report["summary"]["conditionally_movable_inside_window_steps"], 1,
"bead_id={TRACK_C_PUBLISH_WINDOW_INVENTORY_BEAD_ID} case=conditional_step_count"
);
assert_eq!(
report["summary"]["required_serialized_inside_window_steps"], 5,
"bead_id={TRACK_C_PUBLISH_WINDOW_INVENTORY_BEAD_ID} case=required_step_count"
);
}
#[test]
#[ignore = "benchmark evidence only"]
fn wal_metadata_cleanup_benchmark_report() {
let cases: Vec<_> = TRACK_C_METADATA_BENCH_CASES
.iter()
.map(|(scenario_id, interior_dirty_pages)| {
let baseline_samples = track_c_metadata_measure_commit_ns(
TrackCMetadataMode::ForcedPageOneBaseline,
*interior_dirty_pages,
);
let candidate_samples = track_c_metadata_measure_commit_ns(
TrackCMetadataMode::SemanticCleanupCandidate,
*interior_dirty_pages,
);
let baseline_summary = track_c_sample_summary(&baseline_samples);
let candidate_summary = track_c_sample_summary(&candidate_samples);
let baseline_median = baseline_summary["median_ns"].as_u64().unwrap_or(0);
let candidate_median = candidate_summary["median_ns"].as_u64().unwrap_or(0);
let baseline_mean = baseline_summary["mean_ns"].as_f64().unwrap_or(0.0);
let candidate_mean = candidate_summary["mean_ns"].as_f64().unwrap_or(0.0);
let baseline_frame_pages = track_c_metadata_capture_frame_pages(
TrackCMetadataMode::ForcedPageOneBaseline,
*interior_dirty_pages,
);
let candidate_frame_pages = track_c_metadata_capture_frame_pages(
TrackCMetadataMode::SemanticCleanupCandidate,
*interior_dirty_pages,
);
let baseline_page_one_frames = baseline_frame_pages
.iter()
.filter(|page_number| **page_number == PageNumber::ONE.get())
.count();
let candidate_page_one_frames = candidate_frame_pages
.iter()
.filter(|page_number| **page_number == PageNumber::ONE.get())
.count();
json!({
"scenario_id": scenario_id,
"interior_dirty_pages": interior_dirty_pages,
"forced_page_one_baseline": baseline_summary,
"semantic_cleanup_candidate": candidate_summary,
"baseline_frame_pages": baseline_frame_pages,
"candidate_frame_pages": candidate_frame_pages,
"baseline_total_frames_per_commit": baseline_frame_pages.len(),
"candidate_total_frames_per_commit": candidate_frame_pages.len(),
"baseline_page_one_frames_per_commit": baseline_page_one_frames,
"candidate_page_one_frames_per_commit": candidate_page_one_frames,
"frame_count_reduction_per_commit": baseline_frame_pages.len().saturating_sub(candidate_frame_pages.len()),
"page_one_exposure_reduction_per_commit": baseline_page_one_frames.saturating_sub(candidate_page_one_frames),
"speedup_vs_baseline_median": if candidate_median == 0 {
0.0
} else {
(baseline_median as f64) / (candidate_median as f64)
},
"speedup_vs_baseline_mean": if candidate_mean == 0.0 {
0.0
} else {
baseline_mean / candidate_mean
},
"faster_variant_by_median": if candidate_median <= baseline_median {
"semantic_cleanup_candidate"
} else {
"forced_page_one_baseline"
},
})
})
.collect();
let report = json!({
"schema_version": "fsqlite.track_c.metadata_cleanup_benchmark.v1",
"bead_id": TRACK_C_METADATA_BENCH_BEAD_ID,
"parent_bead_id": "bd-db300.3.3",
"measured_operation": "wal_commit_interior_only_workload",
"warmup_iterations": TRACK_C_METADATA_BENCH_WARMUP_ITERS,
"measurement_iterations": TRACK_C_METADATA_BENCH_MEASURE_ITERS,
"vfs": "memory",
"sync_mode": "normal_noop_memory_vfs",
"baseline_variant": "forced_page_one_rewrite_every_commit",
"candidate_variant": "semantic_trigger_page_one_cleanup",
"cases": cases,
});
println!("BEGIN_BD_DB300_3_3_3_REPORT");
println!("{}", serde_json::to_string_pretty(&report).unwrap());
println!("END_BD_DB300_3_3_3_REPORT");
}
#[test]
fn test_wal_commit_preserves_sorted_unique_frame_order() {
let (pager, frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = txn.allocate_page(&cx).unwrap();
let p3 = txn.allocate_page(&cx).unwrap();
let p4 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p4, &vec![0x44; ps]).unwrap();
txn.write_page(&cx, p2, &vec![0x22; ps]).unwrap();
txn.write_page(&cx, p3, &vec![0x33; ps]).unwrap();
txn.write_page(&cx, p4, &vec![0x55; ps]).unwrap();
txn.commit(&cx).unwrap();
let frame_pages: Vec<u32> = frames.lock().unwrap().iter().map(|frame| frame.0).collect();
assert_eq!(
frame_pages,
vec![PageNumber::ONE.get(), p2.get(), p3.get(), p4.get()],
"bead_id={BEAD_ID} case=wal_frames_sorted_and_unique"
);
}
#[test]
fn test_wal_read_page_from_wal() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
let data = vec![0xBB_u8; ps];
txn.write_page(&cx, p1, &data).unwrap();
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let read_back = txn2.get_page(&cx, p1).unwrap();
assert_eq!(
read_back.as_ref()[0],
0xBB,
"bead_id={BEAD_ID} case=wal_read_back_from_wal"
);
}
#[test]
fn test_wal_no_journal_file_created() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/wal_no_jrnl.db");
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (backend, _frames, _, _) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0xFF; 4096]).unwrap();
txn.commit(&cx).unwrap();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&path);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={BEAD_ID} case=wal_no_journal_created"
);
}
#[test]
fn test_wal_begin_not_called_for_rejected_eager_writer() {
let (pager, _) = test_pager();
let cx = Cx::new();
let (backend, _frames, begin_calls, _) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let _writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
assert_eq!(
*begin_calls.lock().unwrap(),
1,
"bead_id={BEAD_ID} case=first_writer_initializes_wal_snapshot"
);
let err = pager
.begin(&cx, TransactionMode::Immediate)
.err()
.expect("second eager writer should be rejected");
assert!(matches!(err, FrankenError::Busy));
assert_eq!(
*begin_calls.lock().unwrap(),
1,
"bead_id={BEAD_ID} case=rejected_writer_must_not_mutate_wal_state"
);
}
#[test]
fn test_wal_mode_switch_back_to_delete() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
assert_eq!(pager.journal_mode(), JournalMode::Wal);
let mode = pager.set_journal_mode(&cx, JournalMode::Delete).unwrap();
assert_eq!(
mode,
JournalMode::Delete,
"bead_id={BEAD_ID} case=switch_back_to_delete"
);
}
#[test]
fn test_wal_overwrite_page_reads_latest() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.write_page(&cx, p1, &vec![0x22; ps]).unwrap();
txn2.commit(&cx).unwrap();
let txn3 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn3.get_page(&cx, p1).unwrap();
assert_eq!(
data.as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=wal_latest_version"
);
}
#[test]
fn test_wal_interior_update_skips_page1_when_metadata_unchanged() {
let (pager, frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
page_two
};
let frames_before = frames.lock().unwrap().len();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page_two, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
let frames = frames.lock().unwrap();
let appended = &frames[frames_before..];
assert_eq!(
appended.len(),
1,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_frame_count"
);
assert_eq!(
appended[0].0,
page_two.get(),
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_page"
);
assert_eq!(
appended[0].2,
page_two.get(),
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_commit_marker_uses_existing_db_size"
);
}
#[test]
fn test_wal_page_one_write_plan_for_interior_update_has_no_trigger() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
page_two
};
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page_two, &vec![0x22; ps]).unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert_eq!(
plan,
WalPageOneWritePlan {
max_written: page_two.get(),
page_one_dirty: false,
freelist_metadata_dirty: false,
db_growth: false,
},
"bead_id={BEAD_ID} case=wal_page1_plan_interior_update"
);
assert!(
!plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=wal_page1_plan_interior_update_no_rewrite"
);
assert!(
!plan.requires_page_count_advance(),
"bead_id={BEAD_ID} case=wal_page1_plan_interior_update_no_growth"
);
}
#[test]
fn test_wal_page_one_write_plan_marks_page_one_dirty_trigger() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
}
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, PageNumber::ONE, &vec![0x77; ps])
.unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert_eq!(
plan,
WalPageOneWritePlan {
max_written: PageNumber::ONE.get(),
page_one_dirty: true,
freelist_metadata_dirty: false,
db_growth: false,
},
"bead_id={BEAD_ID} case=wal_page1_plan_page1_dirty"
);
assert!(
plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=wal_page1_plan_page1_dirty_requires_rewrite"
);
assert!(
!plan.requires_page_count_advance(),
"bead_id={BEAD_ID} case=wal_page1_plan_page1_dirty_no_growth"
);
}
#[test]
fn test_wal_page_one_write_plan_freelist_trigger_defers_page_one() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
page_two
};
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.free_page(&cx, page_two).unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert!(
!plan.page_one_dirty,
"bead_id={BEAD_ID} case=wal_page1_plan_freelist_page1_not_dirty"
);
assert!(
!plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=wal_page1_plan_freelist_defers_page_one"
);
assert!(
!plan.requires_page_count_advance(),
"bead_id={BEAD_ID} case=wal_page1_plan_freelist_no_growth"
);
}
#[test]
fn test_wal_page_one_write_plan_marks_database_growth_trigger() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert_eq!(
plan,
WalPageOneWritePlan {
max_written: page_two.get(),
page_one_dirty: false,
freelist_metadata_dirty: false,
db_growth: true,
},
"bead_id={BEAD_ID} case=wal_page1_plan_db_growth"
);
assert!(
!plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=wal_page1_plan_db_growth_skips_page_one_rewrite"
);
assert!(
plan.requires_page_count_advance(),
"bead_id={BEAD_ID} case=wal_page1_plan_db_growth_advances_count"
);
}
#[test]
fn test_wal_page_one_write_plan_clears_net_zero_freelist_reuse() {
let (pager, _frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
page_two
};
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.free_page(&cx, page_two).unwrap();
txn.commit(&cx).unwrap();
}
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused = txn.allocate_page(&cx).unwrap();
assert_eq!(
reused, page_two,
"bead_id={BEAD_ID} case=wal_page1_plan_net_zero_reuse_reclaims_committed_freelist_page"
);
txn.free_page(&cx, reused).unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert_eq!(
plan,
WalPageOneWritePlan {
max_written: 0,
page_one_dirty: false,
freelist_metadata_dirty: false,
db_growth: false,
},
"bead_id={BEAD_ID} case=wal_page1_plan_net_zero_reuse_has_no_trigger"
);
assert!(
!plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=wal_page1_plan_net_zero_reuse_skips_page_one"
);
assert!(
!txn.has_pending_writes(),
"bead_id={BEAD_ID} case=wal_page1_plan_net_zero_reuse_has_no_pending_writes"
);
assert!(
txn.pending_commit_pages().unwrap().is_empty(),
"bead_id={BEAD_ID} case=wal_page1_plan_net_zero_reuse_has_no_commit_pages"
);
}
#[test]
fn test_wal_net_zero_eof_allocate_free_does_not_append_frames_or_advance_seq() {
let (pager, frames) = wal_pager();
let cx = Cx::new();
let seq_before = pager.published_snapshot().visible_commit_seq;
let frames_before = frames.lock().unwrap().len();
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.free_page(&cx, page_two).unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert_eq!(
plan,
WalPageOneWritePlan {
max_written: 0,
page_one_dirty: false,
freelist_metadata_dirty: false,
db_growth: false,
},
"bead_id={BEAD_ID} case=wal_page1_plan_eof_allocate_then_free_has_no_trigger"
);
assert!(
!txn.has_pending_writes(),
"bead_id={BEAD_ID} case=wal_net_zero_eof_allocate_free_has_no_pending_writes"
);
assert!(
txn.pending_commit_pages().unwrap().is_empty(),
"bead_id={BEAD_ID} case=wal_net_zero_eof_allocate_free_has_no_commit_pages"
);
txn.commit(&cx).unwrap();
assert_eq!(
pager.published_snapshot().visible_commit_seq,
seq_before,
"bead_id={BEAD_ID} case=wal_net_zero_eof_allocate_free_keeps_visible_commit_seq"
);
assert_eq!(
frames.lock().unwrap().len(),
frames_before,
"bead_id={BEAD_ID} case=wal_net_zero_eof_allocate_free_appends_no_frames"
);
}
#[test]
fn test_wal_external_refresh_tracks_headerless_interior_commit() {
let (pager1, pager2, frames) = wal_pager_pair_with_shared_backend();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page_two = {
let mut txn = pager1.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
page_two
};
let seq_before = pager1.published_snapshot().visible_commit_seq;
let mut txn = pager1.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page_two, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
let latest_seq = pager1.published_snapshot().visible_commit_seq;
assert!(
latest_seq > seq_before,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_advances_local_seq"
);
assert_eq!(
frames
.lock()
.unwrap()
.iter()
.filter(|(_, _, db_size_if_commit)| *db_size_if_commit > 0)
.count(),
2,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_emits_commit_markers"
);
{
let inner = pager2.inner.lock().unwrap();
assert_eq!(
inner.journal_mode,
JournalMode::Wal,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_follower_in_wal_mode"
);
drop(inner);
let mut wal_guard = pager2.wal_backend.write().unwrap();
let wal = wal_guard
.as_deref_mut()
.expect("WAL backend should stay installed");
assert_eq!(
wal.committed_txn_count(&cx).unwrap(),
2,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_backend_reports_commit_count"
);
}
let reader = pager2.begin(&cx, TransactionMode::ReadOnly).unwrap();
{
let inner = pager2.inner.lock().unwrap();
assert_eq!(
inner.commit_seq, latest_seq,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_refreshes_inner_commit_seq"
);
}
let refreshed = pager2.published_snapshot();
assert_eq!(
refreshed.visible_commit_seq, latest_seq,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_refreshes_visible_seq"
);
assert_eq!(
reader.get_page(&cx, page_two).unwrap().as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=wal_headerless_interior_commit_refreshes_latest_page"
);
}
#[test]
fn test_wal_rollback_does_not_append_frames() {
let (pager, frames) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p1 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p1, &vec![0xDD; ps]).unwrap();
txn.rollback(&cx).unwrap();
assert_eq!(
frames.lock().unwrap().len(),
0,
"bead_id={BEAD_ID} case=wal_rollback_no_frames"
);
}
#[test]
fn test_wal_begin_skips_committed_page1_reload_when_seq_and_file_size_hold() {
let (pager, frames, _, _, read_page_calls) = wal_pager_with_read_tracking();
let cx = Cx::new();
let page1 = {
let inner = pager.inner.lock().unwrap();
let mut page = vec![0_u8; inner.page_size.as_usize()];
let bytes_read = inner.db_file.read(&cx, &mut page, 0).unwrap();
assert_eq!(
bytes_read,
inner.page_size.as_usize(),
"bead_id={BEAD_ID} case=wal_begin_read_tracking_reads_page1"
);
page
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, PageNumber::ONE, &page1).unwrap();
txn.commit(&cx).unwrap();
assert!(
frames
.lock()
.unwrap()
.iter()
.any(|(page_number, _, _)| *page_number == PageNumber::ONE.get()),
"bead_id={BEAD_ID} case=wal_begin_read_tracking_commits_page1_into_wal"
);
let read_page_calls_before_begin = *read_page_calls.lock().unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let read_page_calls_after_begin = *read_page_calls.lock().unwrap();
drop(reader);
assert_eq!(
read_page_calls_after_begin, read_page_calls_before_begin,
"bead_id={BEAD_ID} case=wal_begin_skips_page1_reload_when_commit_seq_and_file_size_match"
);
}
const BEAD_5A1: &str = "bd-2yy6";
#[test]
fn test_page1_database_header_all_fields() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
eprintln!(
"[5A1][test=page1_database_header_all_fields][step=parse] page_len={}",
raw.len()
);
let hdr_bytes: [u8; DATABASE_HEADER_SIZE] = raw[..DATABASE_HEADER_SIZE]
.try_into()
.expect("page 1 must have 100-byte header");
let hdr = DatabaseHeader::from_bytes(&hdr_bytes).expect("header must parse");
assert_eq!(
hdr.page_size,
PageSize::DEFAULT,
"bead_id={BEAD_5A1} case=page_size"
);
assert_eq!(hdr.page_count, 1, "bead_id={BEAD_5A1} case=page_count");
assert_eq!(
hdr.sqlite_version, FRANKENSQLITE_SQLITE_VERSION_NUMBER,
"bead_id={BEAD_5A1} case=sqlite_version"
);
assert_eq!(
hdr.schema_format, 4,
"bead_id={BEAD_5A1} case=schema_format"
);
assert_eq!(
hdr.freelist_trunk, 0,
"bead_id={BEAD_5A1} case=freelist_trunk"
);
assert_eq!(
hdr.freelist_count, 0,
"bead_id={BEAD_5A1} case=freelist_count"
);
assert_eq!(
hdr.schema_cookie, 0,
"bead_id={BEAD_5A1} case=schema_cookie"
);
assert_eq!(
hdr.text_encoding,
fsqlite_types::TextEncoding::Utf8,
"bead_id={BEAD_5A1} case=text_encoding"
);
assert_eq!(hdr.user_version, 0, "bead_id={BEAD_5A1} case=user_version");
assert_eq!(
hdr.application_id, 0,
"bead_id={BEAD_5A1} case=application_id"
);
assert_eq!(
hdr.change_counter, 0,
"bead_id={BEAD_5A1} case=change_counter"
);
assert_eq!(
&raw[..16],
b"SQLite format 3\0",
"bead_id={BEAD_5A1} case=magic_string"
);
assert_eq!(raw[21], 64, "bead_id={BEAD_5A1} case=max_payload_fraction");
assert_eq!(raw[22], 32, "bead_id={BEAD_5A1} case=min_payload_fraction");
assert_eq!(raw[23], 32, "bead_id={BEAD_5A1} case=leaf_payload_fraction");
eprintln!(
"[5A1][test=page1_database_header_all_fields][step=verify] \
page_size={} page_count={} schema_format={} encoding=UTF8 \u{2713}",
hdr.page_size.get(),
hdr.page_count,
hdr.schema_format
);
}
#[test]
fn test_page1_btree_header_is_valid_empty_leaf_table() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let btree_hdr =
BTreePageHeader::parse(&raw, PageSize::DEFAULT, 0, true).expect("btree header");
assert_eq!(
btree_hdr.page_type,
fsqlite_types::BTreePageType::LeafTable,
"bead_id={BEAD_5A1} case=btree_page_type"
);
assert_eq!(
btree_hdr.cell_count, 0,
"bead_id={BEAD_5A1} case=btree_cell_count"
);
assert_eq!(
btree_hdr.cell_content_start,
PageSize::DEFAULT.get(),
"bead_id={BEAD_5A1} case=btree_content_start"
);
assert_eq!(
btree_hdr.first_freeblock, 0,
"bead_id={BEAD_5A1} case=btree_first_freeblock"
);
assert_eq!(
btree_hdr.fragmented_free_bytes, 0,
"bead_id={BEAD_5A1} case=btree_fragmented_free"
);
assert_eq!(
btree_hdr.header_offset, DATABASE_HEADER_SIZE,
"bead_id={BEAD_5A1} case=btree_header_offset"
);
assert!(
btree_hdr.right_most_child.is_none(),
"bead_id={BEAD_5A1} case=leaf_no_child"
);
eprintln!("[5A1][test=page1_btree_header][step=verify] empty_leaf_table valid \u{2713}");
}
#[test]
fn test_page1_rest_is_zeroed() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let btree_header_end = DATABASE_HEADER_SIZE + 8;
let trailing = &raw[btree_header_end..];
let non_zero_count = trailing.iter().filter(|&&b| b != 0).count();
assert_eq!(
non_zero_count, 0,
"bead_id={BEAD_5A1} case=trailing_bytes_zeroed non_zero_count={non_zero_count}"
);
eprintln!(
"[5A1][test=page1_rest_is_zeroed][step=verify] \
trailing_bytes={} all_zero=true \u{2713}",
trailing.len()
);
}
#[test]
fn test_page1_various_page_sizes() {
for &ps_val in &[512u32, 1024, 2048, 4096, 8192, 16384, 32768, 65536] {
let page_size = PageSize::new(ps_val).unwrap();
let vfs = MemoryVfs::new();
let path = PathBuf::from(format!("/test_{ps_val}.db"));
let pager = SimplePager::open(vfs, &path, page_size).unwrap();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
eprintln!(
"[5A1][test=page1_various_page_sizes][step=open] page_size={ps_val} page_len={}",
raw.len()
);
assert_eq!(
raw.len(),
ps_val as usize,
"bead_id={BEAD_5A1} case=page_len ps={ps_val}"
);
let hdr_bytes: [u8; DATABASE_HEADER_SIZE] =
raw[..DATABASE_HEADER_SIZE].try_into().unwrap();
let hdr = DatabaseHeader::from_bytes(&hdr_bytes).unwrap_or_else(|e| {
panic!("bead_id={BEAD_5A1} case=hdr_parse ps={ps_val} err={e}")
});
assert_eq!(
hdr.page_size, page_size,
"bead_id={BEAD_5A1} case=hdr_page_size ps={ps_val}"
);
let btree = BTreePageHeader::parse(&raw, page_size, 0, true).unwrap_or_else(|e| {
panic!("bead_id={BEAD_5A1} case=btree_parse ps={ps_val} err={e}")
});
assert_eq!(
btree.cell_count, 0,
"bead_id={BEAD_5A1} case=empty_cells ps={ps_val}"
);
let expected_content = ps_val;
assert_eq!(
btree.cell_content_start, expected_content,
"bead_id={BEAD_5A1} case=content_start ps={ps_val}"
);
eprintln!(
"[5A1][test=page1_various_page_sizes][step=verify] \
page_size={ps_val} content_start={} \u{2713}",
btree.cell_content_start
);
}
}
#[test]
fn test_write_empty_leaf_table_roundtrip() {
let page_size = PageSize::DEFAULT;
let mut page = vec![0u8; page_size.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut page, 0, page_size.get());
let parsed = BTreePageHeader::parse(&page, page_size, 0, false)
.expect("bead_id=bd-2yy6 written page must parse");
assert_eq!(parsed.page_type, fsqlite_types::BTreePageType::LeafTable);
assert_eq!(parsed.cell_count, 0);
assert_eq!(parsed.first_freeblock, 0);
assert_eq!(parsed.fragmented_free_bytes, 0);
assert_eq!(parsed.cell_content_start, page_size.get());
assert_eq!(parsed.header_offset, 0);
eprintln!(
"[5A1][test=write_empty_leaf_roundtrip][step=verify] \
non_page1 roundtrip \u{2713}"
);
let mut page1 = vec![0u8; page_size.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut page1, DATABASE_HEADER_SIZE, page_size.get());
let hdr = DatabaseHeader {
page_size,
page_count: 1,
sqlite_version: FRANKENSQLITE_SQLITE_VERSION_NUMBER,
..DatabaseHeader::default()
};
let hdr_bytes = hdr.to_bytes().unwrap();
page1[..DATABASE_HEADER_SIZE].copy_from_slice(&hdr_bytes);
let parsed1 = BTreePageHeader::parse(&page1, page_size, 0, true)
.expect("bead_id=bd-2yy6 page1 written page must parse");
assert_eq!(parsed1.page_type, fsqlite_types::BTreePageType::LeafTable);
assert_eq!(parsed1.cell_count, 0);
assert_eq!(parsed1.header_offset, DATABASE_HEADER_SIZE);
eprintln!(
"[5A1][test=write_empty_leaf_roundtrip][step=verify] \
page1 roundtrip \u{2713}"
);
}
#[test]
fn test_write_empty_leaf_table_65536_page_size() {
let page_size = PageSize::new(65536).unwrap();
let mut page = vec![0u8; page_size.as_usize()];
BTreePageHeader::write_empty_leaf_table(&mut page, 0, page_size.get());
assert_eq!(page[5], 0x00);
assert_eq!(page[6], 0x00);
let parsed =
BTreePageHeader::parse(&page, page_size, 0, false).expect("65536 page must parse");
assert_eq!(parsed.cell_content_start, 65536);
eprintln!(
"[5A1][test=write_empty_leaf_65536][step=verify] \
content_start=65536 encoding=0x0000 \u{2713}"
);
}
#[test]
fn test_freelist_leak_on_rollback() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn1.allocate_page(&cx).unwrap();
txn1.write_page(&cx, p, &vec![0xAA; ps]).unwrap();
txn1.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p).unwrap();
txn2.commit(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert_eq!(inner.freelist.len(), 1);
assert_eq!(inner.freelist[0], p);
drop(inner);
}
let mut txn3 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = txn3.allocate_page(&cx).unwrap();
assert_eq!(
p2,
p,
"bead_id={BEAD_ID} case=freelist_reuse p3={} p1={}",
p2.get(),
p.get()
);
txn3.write_page(&cx, p2, &vec![0xBB; ps]).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert!(inner.freelist.is_empty());
drop(inner);
}
txn3.rollback(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.freelist.len(),
1,
"bead_id={BEAD_ID} case=freelist_leak_on_rollback"
);
assert_eq!(inner.freelist[0], p);
drop(inner);
}
}
#[test]
fn test_concurrent_rollback_reclaims_eof_allocations_without_holes() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut concurrent = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let p2 = concurrent.allocate_page(&cx).unwrap();
let p3 = concurrent.allocate_page(&cx).unwrap();
concurrent.write_page(&cx, p2, &vec![0xAA; ps]).unwrap();
concurrent.write_page(&cx, p3, &vec![0xBB; ps]).unwrap();
concurrent.rollback(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&p2) && inner.freelist.contains(&p3),
"bead_id={BEAD_ID} case=concurrent_rollback_restores_eof_pages freelist={:?}",
inner.freelist
);
}
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused1 = txn.allocate_page(&cx).unwrap();
let reused2 = txn.allocate_page(&cx).unwrap();
assert!(
reused1 != reused2 && [p2, p3].contains(&reused1) && [p2, p3].contains(&reused2),
"bead_id={BEAD_ID} case=concurrent_rollback_reuses_eof_pages reused=({}, {}) expected=({}, {})",
reused1.get(),
reused2.get(),
p2.get(),
p3.get()
);
txn.write_page(&cx, reused1, &vec![0xCC; ps]).unwrap();
txn.write_page(&cx, reused2, &vec![0xDD; ps]).unwrap();
txn.commit(&cx).unwrap();
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.db_size, 3,
"bead_id={BEAD_ID} case=concurrent_rollback_does_not_skip_page_numbers"
);
}
#[test]
fn test_concurrent_allocate_ignores_global_freelist_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
let p3 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0x11; ps]).unwrap();
seed.write_page(&cx, p3, &vec![0x22; ps]).unwrap();
seed.commit(&cx).unwrap();
let mut free_txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
free_txn.free_page(&cx, p2).unwrap();
free_txn.commit(&cx).unwrap();
let mut concurrent = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let allocated = concurrent.allocate_page(&cx).unwrap();
assert_eq!(
allocated.get(),
p3.get() + 1,
"bead_id={BEAD_ID} case=concurrent_allocate_must_not_reuse_global_freelist_pages"
);
concurrent.rollback(&cx).unwrap();
}
#[test]
fn test_immediate_allocate_ignores_global_freelist_pages_while_reader_active() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
let p3 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0x11; ps]).unwrap();
seed.write_page(&cx, p3, &vec![0x22; ps]).unwrap();
seed.commit(&cx).unwrap();
let mut free_txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
free_txn.free_page(&cx, p2).unwrap();
free_txn.commit(&cx).unwrap();
let mut reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let snapshot_page = reader.get_page(&cx, p3).unwrap();
assert_eq!(
snapshot_page.as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=reader_snapshot_established_before_writer_allocate"
);
let mut writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let allocated = writer.allocate_page(&cx).unwrap();
assert_eq!(
allocated.get(),
p3.get() + 1,
"bead_id={BEAD_ID} case=immediate_allocate_must_not_reuse_snapshot_pinned_global_freelist_pages"
);
writer.rollback(&cx).unwrap();
reader.commit(&cx).unwrap();
}
#[test]
fn test_commit_keeps_beyond_db_size_freelist_entries_volatile_only() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
let p3 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0x11; ps]).unwrap();
seed.write_page(&cx, p3, &vec![0x22; ps]).unwrap();
seed.commit(&cx).unwrap();
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let p4 = abandoned.allocate_page(&cx).unwrap();
abandoned.write_page(&cx, p4, &vec![0x33; ps]).unwrap();
abandoned.rollback(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&p4),
"bead_id={BEAD_ID} case=aborted_eof_page_retained_in_memory"
);
}
let mut free_txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
free_txn.free_page(&cx, p2).unwrap();
free_txn.commit(&cx).unwrap();
let mut reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let committed_page = reader.get_page(&cx, p3).unwrap();
assert_eq!(
committed_page.as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=durable_refresh_survives_filtered_beyond_db_size_freelist"
);
let page_one = reader.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let hdr_bytes: [u8; DATABASE_HEADER_SIZE] =
page_one[..DATABASE_HEADER_SIZE].try_into().unwrap();
let hdr = DatabaseHeader::from_bytes(&hdr_bytes).unwrap();
assert_eq!(
hdr.freelist_count, 1,
"bead_id={BEAD_ID} case=durable_freelist_header_excludes_volatile_eof_page"
);
reader.commit(&cx).unwrap();
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&p2),
"bead_id={BEAD_ID} case=durable_freelist_keeps_committed_free_page"
);
assert!(
inner.freelist.contains(&p4),
"bead_id={BEAD_ID} case=volatile_freelist_keeps_beyond_db_size_page_for_reuse"
);
}
#[test]
fn test_committed_concurrent_page_lease_pages_are_reused_before_eof_growth() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut seed = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let first = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, first, &vec![0x11; ps]).unwrap();
let second = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, second, &vec![0x22; ps]).unwrap();
assert!(
!seed.page_lease.is_empty(),
"bead_id={BEAD_ID} case=page_lease_test_must_exercise_batch_allocator"
);
let leased_lowest = *seed
.page_lease
.iter()
.min()
.expect("batch allocator should leave unused lease pages");
seed.commit(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&leased_lowest),
"bead_id={BEAD_ID} case=commit_retains_unused_lease_page_in_volatile_freelist"
);
}
let mut reuse = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let reused = reuse.allocate_page(&cx).unwrap();
assert_eq!(
reused, leased_lowest,
"bead_id={BEAD_ID} case=allocator_reuses_volatile_lease_page_before_eof_growth"
);
reuse.rollback(&cx).unwrap();
}
#[test]
fn test_pending_commit_pages_ignore_beyond_db_size_freelist_entries() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/pending_commit_pages_ignore_beyond_db_size.db");
let pager = SimplePager::open(vfs, &path, PageSize::MIN).unwrap();
let cx = Cx::new();
let ps = PageSize::MIN.as_usize();
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
let p3 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0x11; ps]).unwrap();
seed.write_page(&cx, p3, &vec![0x22; ps]).unwrap();
seed.commit(&cx).unwrap();
let overflow_freelist_pages = (ps / 4).saturating_sub(2) + 2;
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let mut abandoned_pages = Vec::with_capacity(overflow_freelist_pages);
for _ in 0..overflow_freelist_pages {
abandoned_pages.push(abandoned.allocate_page(&cx).unwrap());
}
abandoned.rollback(&cx).unwrap();
let first_beyond_db_size_page = abandoned_pages[0];
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
txn.free_page(&cx, p2).unwrap();
let predicted = txn.pending_commit_pages().unwrap();
assert!(
predicted.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=pending_commit_pages_still_include_page_one_rewrite"
);
assert!(
predicted.contains(&p2),
"bead_id={BEAD_ID} case=pending_commit_pages_include_real_durable_trunk"
);
assert!(
!predicted.contains(&first_beyond_db_size_page),
"bead_id={BEAD_ID} case=pending_commit_pages_ignore_eof_only_freelist_pages"
);
txn.rollback(&cx).unwrap();
}
#[test]
fn test_commit_keeps_newly_freed_pages_below_future_db_size_on_durable_freelist() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/commit_keeps_newly_freed_pages.db");
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
let p3 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0x11; ps]).unwrap();
seed.write_page(&cx, p3, &vec![0x22; ps]).unwrap();
seed.commit(&cx).unwrap();
let (p4, p5, p6, p7) = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p4 = txn.allocate_page(&cx).unwrap();
let p5 = txn.allocate_page(&cx).unwrap();
let p6 = txn.allocate_page(&cx).unwrap();
let p7 = txn.allocate_page(&cx).unwrap();
txn.free_page(&cx, p4).unwrap();
txn.free_page(&cx, p5).unwrap();
txn.free_page(&cx, p6).unwrap();
txn.write_page(&cx, p7, &vec![0x77; ps]).unwrap();
let predicted = txn.pending_commit_pages().unwrap();
assert!(
predicted.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=future_db_size_commit_still_rewrites_page_one"
);
assert!(
predicted.contains(&p4),
"bead_id={BEAD_ID} case=future_db_size_commit_includes_new_trunk_page"
);
assert!(
predicted.contains(&p7),
"bead_id={BEAD_ID} case=future_db_size_commit_includes_live_high_page"
);
txn.commit(&cx).unwrap();
(p4, p5, p6, p7)
};
let mut txn_ro = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw = txn_ro.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let hdr_bytes: [u8; DATABASE_HEADER_SIZE] = raw[..DATABASE_HEADER_SIZE].try_into().unwrap();
let hdr = DatabaseHeader::from_bytes(&hdr_bytes).unwrap();
assert_eq!(
hdr.page_count,
p7.get(),
"bead_id={BEAD_ID} case=future_db_size_commit_advances_page_count_to_live_high_page"
);
assert_eq!(
hdr.freelist_count, 3,
"bead_id={BEAD_ID} case=future_db_size_commit_persists_newly_freed_pages"
);
assert_eq!(
hdr.freelist_trunk,
p4.get(),
"bead_id={BEAD_ID} case=future_db_size_commit_uses_first_newly_freed_page_as_trunk"
);
txn_ro.commit(&cx).unwrap();
let reopened = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
{
let inner = reopened.inner.lock().unwrap();
assert_eq!(
inner.db_size,
p7.get(),
"bead_id={BEAD_ID} case=future_db_size_commit_reopen_keeps_page_count"
);
assert_eq!(
inner.freelist.len(),
3,
"bead_id={BEAD_ID} case=future_db_size_commit_reopen_restores_freelist_len"
);
assert!(
inner.freelist.contains(&p4)
&& inner.freelist.contains(&p5)
&& inner.freelist.contains(&p6),
"bead_id={BEAD_ID} case=future_db_size_commit_reopen_restores_newly_freed_pages"
);
}
let mut reuse = reopened.begin(&cx, TransactionMode::Immediate).unwrap();
let mut reused = vec![
reuse.allocate_page(&cx).unwrap(),
reuse.allocate_page(&cx).unwrap(),
reuse.allocate_page(&cx).unwrap(),
];
reused.sort_unstable();
assert_eq!(
reused,
vec![p4, p5, p6],
"bead_id={BEAD_ID} case=future_db_size_commit_reuses_newly_freed_pages_after_reopen"
);
reuse.rollback(&cx).unwrap();
}
#[test]
fn test_concurrent_allocate_reuses_beyond_db_size_freelist_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let p2 = abandoned.allocate_page(&cx).unwrap();
let p3 = abandoned.allocate_page(&cx).unwrap();
abandoned.write_page(&cx, p2, &vec![0x33; ps]).unwrap();
abandoned.write_page(&cx, p3, &vec![0x44; ps]).unwrap();
abandoned.rollback(&cx).unwrap();
let mut concurrent = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let reused1 = concurrent.allocate_page(&cx).unwrap();
let reused2 = concurrent.allocate_page(&cx).unwrap();
assert!(
reused1 != reused2 && [p2, p3].contains(&reused1) && [p2, p3].contains(&reused2),
"bead_id={BEAD_ID} case=concurrent_allocate_reuses_beyond_db_size_freelist_pages reused=({}, {}) expected=({}, {})",
reused1.get(),
reused2.get(),
p2.get(),
p3.get()
);
concurrent.rollback(&cx).unwrap();
}
#[test]
fn test_concurrent_reuse_and_free_beyond_db_size_page_is_net_zero() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let p2 = abandoned.allocate_page(&cx).unwrap();
let p3 = abandoned.allocate_page(&cx).unwrap();
abandoned.write_page(&cx, p2, &vec![0x33; ps]).unwrap();
abandoned.write_page(&cx, p3, &vec![0x44; ps]).unwrap();
abandoned.rollback(&cx).unwrap();
let current_db_size = pager.published_snapshot().db_size;
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let reused = txn.allocate_page(&cx).unwrap();
assert!(
[p2, p3].contains(&reused),
"bead_id={BEAD_ID} case=concurrent_reuse_then_free_picks_eof_only_page reused={}",
reused.get()
);
txn.free_page(&cx, reused).unwrap();
let plan = txn.classify_wal_page_one_write(current_db_size, txn.freelist_metadata_dirty());
assert_eq!(
plan,
WalPageOneWritePlan {
max_written: 0,
page_one_dirty: false,
freelist_metadata_dirty: false,
db_growth: false,
},
"bead_id={BEAD_ID} case=concurrent_reuse_then_free_has_no_wal_page_one_trigger"
);
assert!(
!txn.has_pending_writes(),
"bead_id={BEAD_ID} case=concurrent_reuse_then_free_has_no_pending_writes"
);
assert!(
txn.pending_commit_pages().unwrap().is_empty(),
"bead_id={BEAD_ID} case=concurrent_reuse_then_free_has_no_commit_pages"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_allocate_page_requires_page_one_conflict_tracking_skips_pure_concurrent_eof_allocate() {
let (pager, _) = test_pager();
let cx = Cx::new();
let txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
assert!(
!txn.allocate_page_requires_page_one_conflict_tracking()
.unwrap(),
"bead_id={BEAD_ID} case=concurrent_eof_allocate_does_not_predeclare_page_one_conflict"
);
}
#[test]
fn test_allocate_page_requires_page_one_conflict_tracking_skips_beyond_db_size_reuse() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let reusable_pages = {
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page_one = abandoned.allocate_page(&cx).unwrap();
let page_two = abandoned.allocate_page(&cx).unwrap();
abandoned
.write_page(&cx, page_one, &vec![0x44; ps])
.unwrap();
abandoned
.write_page(&cx, page_two, &vec![0x55; ps])
.unwrap();
abandoned.rollback(&cx).unwrap();
[page_one, page_two]
};
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
assert!(
!txn.allocate_page_requires_page_one_conflict_tracking()
.unwrap(),
"bead_id={BEAD_ID} case=beyond_db_size_reuse_skips_page_one_conflict_tracking"
);
let reused = txn.allocate_page(&cx).unwrap();
assert!(
reusable_pages.contains(&reused),
"bead_id={BEAD_ID} case=allocator_page_one_hook_matches_actual_reuse reused={} expected=({}, {})",
reused.get(),
reusable_pages[0].get(),
reusable_pages[1].get()
);
}
#[test]
fn test_memory_db_allocator_skips_committed_freelist_and_page_one_conflicts() {
let vfs = MemoryVfs::new();
let pager = SimplePager::open(vfs, Path::new("/:memory:"), PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let (page_two, page_three) = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_two = seed.allocate_page(&cx).unwrap();
let page_three = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page_two, &vec![0x11; ps]).unwrap();
seed.write_page(&cx, page_three, &vec![0x22; ps]).unwrap();
seed.commit(&cx).unwrap();
(page_two, page_three)
};
{
let mut free_txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
free_txn.free_page(&cx, page_two).unwrap();
free_txn.commit(&cx).unwrap();
}
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
assert!(
!txn.allocate_page_requires_page_one_conflict_tracking()
.unwrap(),
"bead_id={BEAD_ID} case=memory_db_allocate_skips_page_one_conflict_tracking"
);
let allocated = txn.allocate_page(&cx).unwrap();
let expected = PageNumber::new(page_three.get() + 1).unwrap();
assert_eq!(
allocated,
expected,
"bead_id={BEAD_ID} case=memory_db_allocator_uses_bump_path allocated={} expected={}",
allocated.get(),
expected.get()
);
}
#[test]
fn test_memory_db_allocator_stays_bump_only_after_rollback() {
let vfs = MemoryVfs::new();
let pager = SimplePager::open(vfs, Path::new("/:memory:"), PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let abandoned_pages = {
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page_one = abandoned.allocate_page(&cx).unwrap();
let page_two = abandoned.allocate_page(&cx).unwrap();
abandoned.rollback(&cx).unwrap();
[page_one, page_two]
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
assert!(
!txn.allocate_page_requires_page_one_conflict_tracking()
.unwrap(),
"bead_id={BEAD_ID} case=memory_db_post_rollback_allocate_skips_page_one_conflict_tracking"
);
let allocated = txn.allocate_page(&cx).unwrap();
let expected_next_page = abandoned_pages[0].get() + 1 + PAGE_LEASE_BATCH_SIZE;
let expected = PageNumber::new(expected_next_page).unwrap();
assert!(
!abandoned_pages.contains(&allocated),
"bead_id={BEAD_ID} case=memory_db_allocator_skips_freelist_after_rollback allocated={} abandoned=({}, {})",
allocated.get(),
abandoned_pages[0].get(),
abandoned_pages[1].get()
);
assert_eq!(
allocated,
expected,
"bead_id={BEAD_ID} case=memory_db_allocator_keeps_bump_sequence allocated={} expected={}",
allocated.get(),
expected.get()
);
}
#[test]
fn test_free_page_requires_page_one_conflict_tracking_defers_concurrent_freelist_reconciliation()
{
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let durable_page = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page, &[0xAB; 32]).unwrap();
seed.commit(&cx).unwrap();
page
};
let mut durable_txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
assert!(
!durable_txn
.free_page_requires_page_one_conflict_tracking(durable_page)
.unwrap(),
"bead_id={BEAD_ID} case=concurrent_durable_free_defers_page_one_conflict_tracking"
);
durable_txn.free_page(&cx, durable_page).unwrap();
let durable_predicted = durable_txn.pending_commit_pages().unwrap();
assert!(
durable_predicted.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=concurrent_durable_free_still_puts_page_one_in_pending_commit_surface"
);
durable_txn.rollback(&cx).unwrap();
let reusable_pages = {
let mut abandoned = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page_one = abandoned.allocate_page(&cx).unwrap();
let page_two = abandoned.allocate_page(&cx).unwrap();
abandoned
.write_page(&cx, page_one, &vec![0x55; ps])
.unwrap();
abandoned
.write_page(&cx, page_two, &vec![0x66; ps])
.unwrap();
abandoned.rollback(&cx).unwrap();
[page_one, page_two]
};
let mut net_zero_txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let reused = net_zero_txn.allocate_page(&cx).unwrap();
assert!(
reusable_pages.contains(&reused),
"bead_id={BEAD_ID} case=net_zero_free_reuses_abandoned_page reused={} expected=({}, {})",
reused.get(),
reusable_pages[0].get(),
reusable_pages[1].get()
);
assert!(
!net_zero_txn
.free_page_requires_page_one_conflict_tracking(reused)
.unwrap(),
"bead_id={BEAD_ID} case=net_zero_free_skips_page_one_conflict_tracking"
);
}
#[test]
fn test_concurrent_freelist_no_orphaned_pages_after_sequential_free_commits() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let (p2, p3, p4, p5) = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
let p3 = seed.allocate_page(&cx).unwrap();
let p4 = seed.allocate_page(&cx).unwrap();
let p5 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0x22; ps]).unwrap();
seed.write_page(&cx, p3, &vec![0x33; ps]).unwrap();
seed.write_page(&cx, p4, &vec![0x44; ps]).unwrap();
seed.write_page(&cx, p5, &vec![0x55; ps]).unwrap();
seed.commit(&cx).unwrap();
(p2, p3, p4, p5)
};
{
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn1.free_page(&cx, p2).unwrap();
txn1.commit(&cx).unwrap();
}
{
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&p2),
"bead_id={BEAD_ID} case=freed_page_promoted_to_freelist_after_commit p2={}",
p2.get()
);
}
{
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p3).unwrap();
txn2.commit(&cx).unwrap();
}
{
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&p2),
"bead_id={BEAD_ID} case=p2_still_in_freelist_after_second_commit p2={}",
p2.get()
);
assert!(
inner.freelist.contains(&p3),
"bead_id={BEAD_ID} case=p3_in_freelist_after_commit p3={}",
p3.get()
);
assert!(
!inner.freelist.contains(&p4),
"bead_id={BEAD_ID} case=p4_not_freed p4={}",
p4.get()
);
assert!(
!inner.freelist.contains(&p5),
"bead_id={BEAD_ID} case=p5_not_freed p5={}",
p5.get()
);
}
{
let mut txn3 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused1 = txn3.allocate_page(&cx).unwrap();
let reused2 = txn3.allocate_page(&cx).unwrap();
assert!(
[p2, p3].contains(&reused1) && [p2, p3].contains(&reused2),
"bead_id={BEAD_ID} case=freed_pages_reusable reused1={} reused2={}",
reused1.get(),
reused2.get()
);
txn3.rollback(&cx).unwrap();
}
}
#[test]
fn test_freed_pages_not_in_freelist_before_commit_success() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let p2 = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p2 = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, p2, &vec![0xAA; ps]).unwrap();
seed.commit(&cx).unwrap();
p2
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.free_page(&cx, p2).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert!(
!inner.freelist.contains(&p2),
"bead_id={BEAD_ID} case=freed_page_not_leaked_before_commit p2={}",
p2.get()
);
}
txn.commit(&cx).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert!(
inner.freelist.contains(&p2),
"bead_id={BEAD_ID} case=freed_page_promoted_after_successful_commit p2={}",
p2.get()
);
}
}
#[test]
fn test_write_page_requires_page_one_conflict_tracking_defers_concurrent_growth_to_commit_surface()
{
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page = txn.allocate_page(&cx).unwrap();
assert!(
!txn.write_page_requires_page_one_conflict_tracking(page)
.unwrap(),
"bead_id={BEAD_ID} case=concurrent_growth_write_defers_page_one_conflict_tracking"
);
txn.write_page(&cx, page, &[0x7B; 32]).unwrap();
let predicted = txn.pending_commit_pages().unwrap();
assert!(
predicted.contains(&page),
"bead_id={BEAD_ID} case=concurrent_growth_pending_commit_surface_keeps_high_page"
);
assert!(
predicted.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=concurrent_growth_pending_commit_surface_still_contains_page_one"
);
}
#[test]
fn test_pending_conflict_pages_exclude_synthetic_page_one_for_concurrent_wal_growth() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &[0x7B; 32]).unwrap();
let pending_commit = txn.pending_commit_pages().unwrap();
let pending_conflict = txn.pending_conflict_pages().unwrap();
assert!(
!pending_commit.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=concurrent_wal_growth_commit_surface_excludes_page_one"
);
assert!(
pending_commit.contains(&page),
"bead_id={BEAD_ID} case=concurrent_wal_growth_commit_surface_keeps_data_page"
);
assert!(
!pending_conflict.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=concurrent_wal_growth_conflict_surface_excludes_synthetic_page_one"
);
assert!(
pending_conflict.contains(&page),
"bead_id={BEAD_ID} case=concurrent_wal_growth_conflict_surface_keeps_real_data_page"
);
}
#[test]
fn test_freelist_dirty_after_lease_return_short_circuits_clean_append_growth() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &[0x7D; 32]).unwrap();
let inner = txn.inner.lock().unwrap();
let committed_db_size = txn.committed_db_size_with_inner(&inner);
assert!(
!txn.freelist_metadata_dirty_with_pending_free_pages(&inner, committed_db_size, &[]),
"bead_id={BEAD_ID} case=clean_append_growth_has_no_freelist_metadata_delta"
);
}
#[test]
fn test_freelist_dirty_after_lease_return_skips_volatile_eof_lease_pages() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let first_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, first_page, &vec![0x71; ps]).unwrap();
let second_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, second_page, &vec![0x72; ps]).unwrap();
let inner_arc = Arc::clone(&txn.inner);
let inner = inner_arc.lock().unwrap();
let committed_db_size = txn.committed_db_size_with_inner(&inner);
assert!(
txn.page_lease
.iter()
.all(|page| page.get() > committed_db_size),
"bead_id=bd-wee9a case=volatile_eof_lease_pages_must_be_above_commit_size"
);
let pending_returned_pages = txn.page_lease.clone();
let lease_return_affects_durable_freelist = pending_returned_pages
.iter()
.any(|page| page.get() <= committed_db_size);
assert!(
!lease_return_affects_durable_freelist,
"bead_id=bd-wee9a case=volatile_eof_lease_return_has_no_durable_freelist_effect"
);
assert!(
!txn.freelist_metadata_dirty_with_pending_free_pages(
&inner,
committed_db_size,
&pending_returned_pages,
),
"bead_id=bd-wee9a case=volatile_eof_lease_return_skips_freelist_dirty_work"
);
}
#[test]
fn test_freelist_dirty_after_lease_return_keeps_durable_lease_holes() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let first_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, first_page, &vec![0x81; ps]).unwrap();
let second_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, second_page, &vec![0x82; ps]).unwrap();
let high_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, high_page, &vec![0x83; ps]).unwrap();
assert!(
!txn.page_lease.is_empty(),
"bead_id={BEAD_ID} case=lease_hole_test_must_exercise_batched_allocator"
);
let inner_arc = Arc::clone(&txn.inner);
let inner = inner_arc.lock().unwrap();
let committed_db_size = txn.committed_db_size_with_inner(&inner);
let pending_returned_pages = txn.page_lease.clone();
let lease_return_affects_durable_freelist = pending_returned_pages
.iter()
.any(|page| page.get() <= committed_db_size);
assert!(
lease_return_affects_durable_freelist,
"bead_id={BEAD_ID} case=lease_hole_return_affects_durable_freelist"
);
assert!(
txn.freelist_metadata_dirty_with_pending_free_pages(
&inner,
committed_db_size,
&pending_returned_pages,
),
"bead_id={BEAD_ID} case=returned_lease_holes_remain_durable_freelist_delta"
);
}
#[test]
fn test_concurrent_commit_freelists_unstaged_allocated_eof_holes() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let (unwritten_page, high_page) = {
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let first_page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, first_page, &vec![0x91; ps]).unwrap();
let unwritten_page = txn.allocate_page(&cx).unwrap();
let high_page = txn.allocate_page(&cx).unwrap();
assert!(
unwritten_page.get() < high_page.get(),
"bead_id={BEAD_ID} case=unstaged_allocation_test_needs_page_count_hole"
);
assert!(
!txn.write_set.contains_key(&unwritten_page),
"bead_id={BEAD_ID} case=unwritten_allocated_page_must_not_be_staged"
);
txn.write_page(&cx, high_page, &vec![0x92; ps]).unwrap();
txn.commit(&cx).unwrap();
(unwritten_page, high_page)
};
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.db_size,
high_page.get(),
"bead_id={BEAD_ID} case=high_page_advances_committed_db_size"
);
assert!(
inner.freelist.contains(&unwritten_page),
"bead_id={BEAD_ID} case=unstaged_allocated_page_reusable_in_memory page={}",
unwritten_page.get()
);
let page1 = inner
.read_committed_page_copy(&cx, &pager.wal_backend, PageNumber::ONE)
.unwrap();
let header_bytes: [u8; DATABASE_HEADER_SIZE] =
page1[..DATABASE_HEADER_SIZE].try_into().unwrap();
let header = DatabaseHeader::from_bytes(&header_bytes).unwrap();
let durable_freelist = load_freelist_from_committed_state(
&cx,
&inner,
&pager.wal_backend,
header.page_count,
header.freelist_trunk,
header.freelist_count,
)
.unwrap();
assert!(
durable_freelist.contains(&unwritten_page),
"bead_id={BEAD_ID} case=unstaged_allocated_page_serialized_to_durable_freelist page={} freelist={durable_freelist:?}",
unwritten_page.get()
);
}
#[test]
fn test_commit_conflict_pages_exclude_synthetic_page_one_after_wal_growth_injection() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x7C; ps]).unwrap();
let wal_page1_plan = {
let inner = txn.inner.lock().unwrap();
let committed_db_size = txn.committed_db_size_with_inner(&inner);
let freelist_dirty = txn.freelist_metadata_dirty_with_inner(&inner, committed_db_size);
txn.classify_wal_page_one_write(inner.db_size, freelist_dirty)
};
assert!(
!wal_page1_plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=synthetic_wal_page1_plan_not_direct_page_one_write"
);
assert!(
wal_page1_plan.requires_page_count_advance(),
"bead_id={BEAD_ID} case=synthetic_wal_page1_plan_db_growth"
);
let synthetic_page_one = StagedPage::from_bytes(&txn.pool, &[0xA5; 32]).unwrap();
insert_staged_page(
&mut txn.write_set,
&mut txn.write_pages_sorted,
PageNumber::ONE,
synthetic_page_one,
);
assert!(
txn.write_set.contains_key(&PageNumber::ONE),
"bead_id={BEAD_ID} case=commit_prepare_injected_synthetic_page_one"
);
let conflict_pages = {
let inner = txn.inner.lock().unwrap();
txn.predicted_conflict_pages_for_wal_commit_with_inner(&inner, wal_page1_plan, &[])
};
assert!(
!conflict_pages.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=synthetic_wal_page1_not_cross_process_conflict"
);
assert!(
conflict_pages.contains(&page),
"bead_id={BEAD_ID} case=data_page_remains_cross_process_conflict"
);
}
#[test]
fn test_commit_conflict_pages_exclude_synthetic_page_one_after_freelist_serialization() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let page = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page, &vec![0x8D; ps]).unwrap();
seed.commit(&cx).unwrap();
page
};
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
txn.free_page(&cx, page).unwrap();
let (wal_page1_plan, pending_freed) = {
let mut inner = txn.inner.lock().unwrap();
let committed_db_size = txn.committed_db_size_with_inner(&inner);
let freelist_dirty = txn.freelist_metadata_dirty_with_inner(&inner, committed_db_size);
let wal_page1_plan = txn.classify_wal_page_one_write(inner.db_size, freelist_dirty);
assert!(
freelist_dirty,
"bead_id={BEAD_ID} case=freelist_serialization_regression_requires_dirty_freelist"
);
assert!(
!wal_page1_plan.requires_page_one_rewrite(),
"bead_id={BEAD_ID} case=freelist_serialization_plan_not_direct_page_one_write"
);
let pending_freed = txn.freed_pages.drain(..).collect::<Vec<_>>();
serialize_freelist_to_write_set(
&cx,
&mut inner,
&txn.cache,
&txn.wal_backend,
&txn.pool,
&mut txn.write_set,
&mut txn.write_pages_sorted,
committed_db_size,
&pending_freed,
)
.unwrap();
(wal_page1_plan, pending_freed)
};
assert!(
txn.write_set.contains_key(&PageNumber::ONE),
"bead_id={BEAD_ID} case=freelist_serialization_injected_synthetic_page_one"
);
let conflict_pages = {
let inner = txn.inner.lock().unwrap();
txn.predicted_conflict_pages_for_wal_commit_with_inner(
&inner,
wal_page1_plan,
&pending_freed,
)
};
assert!(
!conflict_pages.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=freelist_synthetic_page1_not_cross_process_conflict"
);
assert!(
conflict_pages.contains(&page),
"bead_id={BEAD_ID} case=freelist_trunk_page_remains_cross_process_conflict"
);
}
#[test]
fn test_commit_conflict_pages_keep_non_trunk_freed_pages_after_drain() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let (first_page, second_page) = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let first_page = seed.allocate_page(&cx).unwrap();
let second_page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, first_page, &vec![0x91; ps]).unwrap();
seed.write_page(&cx, second_page, &vec![0x92; ps]).unwrap();
seed.commit(&cx).unwrap();
(first_page, second_page)
};
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
txn.free_page(&cx, first_page).unwrap();
txn.free_page(&cx, second_page).unwrap();
let (wal_page1_plan, pending_freed) = {
let mut inner = txn.inner.lock().unwrap();
let committed_db_size = txn.committed_db_size_with_inner(&inner);
let freelist_dirty = txn.freelist_metadata_dirty_with_inner(&inner, committed_db_size);
let wal_page1_plan = txn.classify_wal_page_one_write(inner.db_size, freelist_dirty);
let pending_freed = txn.freed_pages.drain(..).collect::<Vec<_>>();
serialize_freelist_to_write_set(
&cx,
&mut inner,
&txn.cache,
&txn.wal_backend,
&txn.pool,
&mut txn.write_set,
&mut txn.write_pages_sorted,
committed_db_size,
&pending_freed,
)
.unwrap();
(wal_page1_plan, pending_freed)
};
let non_trunk_freed_page = pending_freed
.iter()
.copied()
.find(|page| !txn.write_set.contains_key(page))
.expect("at least one freed page should be carried only as freelist metadata");
let conflict_pages = {
let inner = txn.inner.lock().unwrap();
txn.predicted_conflict_pages_for_wal_commit_with_inner(
&inner,
wal_page1_plan,
&pending_freed,
)
};
assert!(
conflict_pages.contains(&first_page),
"bead_id={BEAD_ID} case=first_freed_page_remains_cross_process_conflict"
);
assert!(
conflict_pages.contains(&second_page),
"bead_id={BEAD_ID} case=second_freed_page_remains_cross_process_conflict"
);
assert!(
conflict_pages.contains(&non_trunk_freed_page),
"bead_id={BEAD_ID} case=non_trunk_freed_page_survives_drain"
);
}
#[test]
fn test_pending_conflict_pages_keep_explicit_page_one_write_for_concurrent_wal() {
let (pager, _) = wal_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
txn.write_page(&cx, PageNumber::ONE, &[0x5A; 32]).unwrap();
let pending_conflict = txn.pending_conflict_pages().unwrap();
assert!(
pending_conflict.contains(&PageNumber::ONE),
"bead_id={BEAD_ID} case=explicit_page_one_write_remains_in_conflict_surface"
);
}
#[test]
fn test_write_page_requires_page_one_conflict_tracking_skips_interior_page() {
let (pager, _) = test_pager();
let cx = Cx::new();
let durable_page = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page, &[0xAB; 32]).unwrap();
seed.commit(&cx).unwrap();
page
};
let txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
assert!(
!txn.write_page_requires_page_one_conflict_tracking(durable_page)
.unwrap(),
"bead_id={BEAD_ID} case=interior_page_write_skips_page_one_conflict_tracking"
);
}
#[test]
fn test_concurrent_rollback_to_savepoint_reclaims_eof_allocations() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let base = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, base, &vec![0x11; ps]).unwrap();
txn.savepoint(&cx, "sp").unwrap();
let p3 = txn.allocate_page(&cx).unwrap();
let p4 = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p3, &vec![0x22; ps]).unwrap();
txn.write_page(&cx, p4, &vec![0x33; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "sp").unwrap();
let reused1 = txn.allocate_page(&cx).unwrap();
let reused2 = txn.allocate_page(&cx).unwrap();
assert!(
reused1.get() == 3 && reused2.get() == 4,
"bead_id={BEAD_ID} case=concurrent_savepoint_reuses_eof_pages reused=({}, {}) expected=(3, 4)",
reused1.get(),
reused2.get()
);
txn.write_page(&cx, reused1, &vec![0x44; ps]).unwrap();
txn.write_page(&cx, reused2, &vec![0x55; ps]).unwrap();
txn.commit(&cx).unwrap();
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.db_size, 4,
"bead_id={BEAD_ID} case=concurrent_savepoint_rollback_does_not_skip_page_numbers"
);
}
#[test]
fn test_concurrent_drop_reclaims_eof_allocations_without_holes() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
{
let mut concurrent = pager.begin(&cx, TransactionMode::Concurrent).unwrap();
let p2 = concurrent.allocate_page(&cx).unwrap();
let p3 = concurrent.allocate_page(&cx).unwrap();
concurrent.write_page(&cx, p2, &vec![0x66; ps]).unwrap();
concurrent.write_page(&cx, p3, &vec![0x77; ps]).unwrap();
}
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused1 = txn.allocate_page(&cx).unwrap();
let reused2 = txn.allocate_page(&cx).unwrap();
assert!(
reused1.get() <= 3 && reused2.get() <= 3 && reused1 != reused2,
"bead_id={BEAD_ID} case=concurrent_drop_reuses_abandoned_eof_pages reused=({}, {})",
reused1.get(),
reused2.get()
);
txn.write_page(&cx, reused1, &vec![0x88; ps]).unwrap();
txn.write_page(&cx, reused2, &vec![0x99; ps]).unwrap();
txn.commit(&cx).unwrap();
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.db_size, 3,
"bead_id={BEAD_ID} case=concurrent_drop_does_not_skip_page_numbers"
);
}
#[test]
fn test_freelist_persisted_and_reloaded_on_reopen() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/freelist_persist.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn1.allocate_page(&cx).unwrap();
txn1.write_page(&cx, p, &vec![0xAB; ps]).unwrap();
txn1.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p).unwrap();
txn2.commit(&cx).unwrap();
let txn_ro = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let raw = txn_ro.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
let hdr_bytes: [u8; DATABASE_HEADER_SIZE] = raw[..DATABASE_HEADER_SIZE].try_into().unwrap();
let hdr = DatabaseHeader::from_bytes(&hdr_bytes).unwrap();
assert_eq!(hdr.freelist_count, 1);
assert_eq!(hdr.freelist_trunk, p.get());
let reopened = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
{
let inner = reopened.inner.lock().unwrap();
assert_eq!(inner.freelist.len(), 1);
assert_eq!(inner.freelist[0], p);
}
let mut txn3 = reopened.begin(&cx, TransactionMode::Immediate).unwrap();
let reused = txn3.allocate_page(&cx).unwrap();
assert_eq!(
reused, p,
"reopened pager should reuse persisted freelist page"
);
txn3.commit(&cx).unwrap();
}
#[test]
#[allow(clippy::similar_names, clippy::cast_possible_truncation)]
fn test_cache_eviction_under_pressure() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for i in 0..300u32 {
let p = txn.allocate_page(&cx).unwrap();
pages.push(p);
let byte = (i % 256) as u8;
let data = vec![byte; ps];
txn.write_page(&cx, p, &data).unwrap();
}
txn.commit(&cx).unwrap();
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for (i, &p) in pages.iter().enumerate() {
let data = txn.get_page(&cx, p).unwrap();
let expected_byte = (i % 256) as u8;
assert_eq!(
data.as_ref()[0],
expected_byte,
"bead_id={BEAD_ID} case=cache_pressure page={p}"
);
}
}
const BEAD_INV: &str = "bd-2ttd8.2";
#[test]
fn test_inv_write_set_not_in_freelist_during_txn() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xAA; ps]).unwrap();
txn.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p).unwrap();
assert!(
txn2.freed_pages.contains(&p),
"bead_id={BEAD_INV} inv=freed_page_tracked"
);
assert!(
!txn2.write_set.contains_key(&p),
"bead_id={BEAD_INV} inv=freed_not_in_write_set"
);
txn2.commit(&cx).unwrap();
}
#[test]
fn test_inv_allocated_pages_sequential_and_nonzero() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for _ in 0..10 {
let p = txn.allocate_page(&cx).unwrap();
assert!(p.get() > 0, "bead_id={BEAD_INV} inv=page_nonzero");
assert!(
!pages.contains(&p),
"bead_id={BEAD_INV} inv=page_unique p={p}"
);
pages.push(p);
}
txn.commit(&cx).unwrap();
}
#[test]
fn test_inv_writer_serialization_single_writer() {
let (pager, _) = test_pager();
let cx = Cx::new();
let _w1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let err = pager.begin(&cx, TransactionMode::Immediate);
assert!(
err.is_err(),
"bead_id={BEAD_INV} inv=single_writer_enforced"
);
let err2 = pager.begin(&cx, TransactionMode::Exclusive);
assert!(
err2.is_err(),
"bead_id={BEAD_INV} inv=exclusive_blocked_by_writer"
);
}
#[test]
fn test_inv_writer_released_on_commit() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut w1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
w1.commit(&cx).unwrap();
let _w2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
}
#[test]
fn test_inv_writer_released_on_rollback() {
let (pager, _) = test_pager();
let cx = Cx::new();
let mut w1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
w1.rollback(&cx).unwrap();
let _w2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
}
#[test]
fn test_inv_writer_released_on_drop() {
let (pager, _) = test_pager();
let cx = Cx::new();
{
let _w1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
}
let _w2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
}
#[test]
fn test_inv_commit_persists_all_dirty_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for i in 0..5u8 {
let p = txn.allocate_page(&cx).unwrap();
let mut data = vec![0u8; ps];
data[0] = 0xD0 + i;
data[ps - 1] = i;
txn.write_page(&cx, p, &data).unwrap();
pages.push((p, 0xD0 + i, i));
}
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for (p, first_byte, last_byte) in &pages {
let data = txn2.get_page(&cx, *p).unwrap();
assert_eq!(
data.as_ref()[0],
*first_byte,
"bead_id={BEAD_INV} inv=dirty_page_committed p={p}"
);
assert_eq!(
data.as_ref()[ps - 1],
*last_byte,
"bead_id={BEAD_INV} inv=dirty_page_last_byte p={p}"
);
}
}
#[test]
fn test_inv_rollback_discards_all_dirty_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn1.allocate_page(&cx).unwrap();
txn1.write_page(&cx, p, &vec![0xAA; ps]).unwrap();
txn1.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.write_page(&cx, p, &vec![0xBB; ps]).unwrap();
txn2.rollback(&cx).unwrap();
let txn3 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn3.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0xAA,
"bead_id={BEAD_INV} inv=rollback_preserves_committed"
);
}
#[test]
fn test_inv_savepoint_nested_stack_order() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x01; ps]).unwrap();
txn.savepoint(&cx, "sp1").unwrap();
txn.write_page(&cx, p, &vec![0x02; ps]).unwrap();
txn.savepoint(&cx, "sp2").unwrap();
txn.write_page(&cx, p, &vec![0x03; ps]).unwrap();
txn.savepoint(&cx, "sp3").unwrap();
txn.write_page(&cx, p, &vec![0x04; ps]).unwrap();
txn.rollback_to_savepoint(&cx, "sp2").unwrap();
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0x02,
"bead_id={BEAD_INV} inv=nested_rollback_sp2"
);
let err = txn.rollback_to_savepoint(&cx, "sp3");
assert!(
err.is_err(),
"bead_id={BEAD_INV} inv=sp3_removed_after_rollback_to_sp2"
);
txn.rollback_to_savepoint(&cx, "sp1").unwrap();
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0x01,
"bead_id={BEAD_INV} inv=nested_rollback_sp1"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_inv_savepoint_release_merges_to_parent() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x10; ps]).unwrap();
txn.savepoint(&cx, "outer").unwrap();
txn.write_page(&cx, p, &vec![0x20; ps]).unwrap();
txn.savepoint(&cx, "inner").unwrap();
txn.write_page(&cx, p, &vec![0x30; ps]).unwrap();
txn.release_savepoint(&cx, "inner").unwrap();
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0x30,
"bead_id={BEAD_INV} inv=release_keeps_changes"
);
txn.rollback_to_savepoint(&cx, "outer").unwrap();
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0x10,
"bead_id={BEAD_INV} inv=rollback_outer_after_release_inner"
);
txn.commit(&cx).unwrap();
}
#[test]
fn test_inv_freelist_restored_on_rollback() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn1.allocate_page(&cx).unwrap();
txn1.write_page(&cx, p, &vec![0xAA; ps]).unwrap();
txn1.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p).unwrap();
txn2.commit(&cx).unwrap();
let freelist_before = {
let inner = pager.inner.lock().unwrap();
inner.freelist.clone()
};
assert!(
freelist_before.contains(&p),
"bead_id={BEAD_INV} inv=freed_in_freelist"
);
let mut txn3 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused = txn3.allocate_page(&cx).unwrap();
assert_eq!(reused, p, "should reuse freed page");
txn3.rollback(&cx).unwrap();
let freelist_after = {
let inner = pager.inner.lock().unwrap();
inner.freelist.clone()
};
assert_eq!(
freelist_after, freelist_before,
"bead_id={BEAD_INV} inv=freelist_restored_after_rollback"
);
}
#[test]
fn test_inv_page_identity_read_before_write() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xAA; ps]).unwrap();
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn2.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0xAA,
"bead_id={BEAD_INV} inv=committed_visible"
);
}
#[test]
fn test_inv_write_set_isolation() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn1 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn1.allocate_page(&cx).unwrap();
txn1.write_page(&cx, p, &vec![0x11; ps]).unwrap();
txn1.commit(&cx).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let r_data = reader.get_page(&cx, p).unwrap();
assert_eq!(r_data.as_ref()[0], 0x11);
let mut writer = pager.begin(&cx, TransactionMode::Immediate).unwrap();
writer.write_page(&cx, p, &vec![0x22; ps]).unwrap();
let r_data2 = reader.get_page(&cx, p).unwrap();
assert_eq!(
r_data2.as_ref()[0],
0x11,
"bead_id={BEAD_INV} inv=write_set_isolated_from_readers"
);
writer.commit(&cx).unwrap();
}
#[test]
fn test_inv_db_size_grows_on_allocate_commit() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let initial_size = {
let inner = pager.inner.lock().unwrap();
inner.db_size
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
for _ in 0..5 {
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x00; ps]).unwrap();
}
txn.commit(&cx).unwrap();
let final_size = {
let inner = pager.inner.lock().unwrap();
inner.db_size
};
assert!(
final_size > initial_size,
"bead_id={BEAD_INV} inv=db_size_grows initial={initial_size} final={final_size}"
);
}
#[test]
fn test_inv_db_size_restored_on_rollback() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let size_before = {
let inner = pager.inner.lock().unwrap();
inner.db_size
};
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
for _ in 0..5 {
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x00; ps]).unwrap();
}
txn.rollback(&cx).unwrap();
let size_after = {
let inner = pager.inner.lock().unwrap();
inner.db_size
};
assert_eq!(
size_after, size_before,
"bead_id={BEAD_INV} inv=db_size_restored_on_rollback"
);
}
#[test]
fn test_inv_active_transaction_count() {
let (pager, _) = test_pager();
let cx = Cx::new();
let count_before = {
let inner = pager.inner.lock().unwrap();
inner.active_transactions
};
assert_eq!(count_before, 0, "bead_id={BEAD_INV} inv=initial_zero_txns");
let r1 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let r2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
{
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.active_transactions, 2,
"bead_id={BEAD_INV} inv=two_active_txns"
);
}
drop(r1);
{
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.active_transactions, 1,
"bead_id={BEAD_INV} inv=one_after_drop"
);
}
drop(r2);
{
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.active_transactions, 0,
"bead_id={BEAD_INV} inv=zero_after_all_dropped"
);
}
}
#[test]
fn test_inv_journal_mode_default_delete() {
let (pager, _) = test_pager();
assert_eq!(
pager.journal_mode(),
JournalMode::Delete,
"bead_id={BEAD_INV} inv=default_journal_delete"
);
}
#[test]
fn test_inv_commit_seq_monotonic() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut prev_seq = {
let inner = pager.inner.lock().unwrap();
inner.commit_seq.get()
};
for _ in 0..5 {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x00; ps]).unwrap();
txn.commit(&cx).unwrap();
let seq = {
let inner = pager.inner.lock().unwrap();
inner.commit_seq.get()
};
assert!(
seq >= prev_seq,
"bead_id={BEAD_INV} inv=commit_seq_monotonic seq={seq} prev={prev_seq}"
);
prev_seq = seq;
}
}
const BEAD_E2E: &str = "bd-2ttd8.3";
#[test]
fn test_e2e_sequential_write_read_with_metrics() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
pager.reset_cache_metrics().unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for i in 0..20u32 {
let p = txn.allocate_page(&cx).unwrap();
let mut data = vec![0u8; ps];
data[0] = (i & 0xFF) as u8;
data[1] = ((i >> 8) & 0xFF) as u8;
txn.write_page(&cx, p, &data).unwrap();
pages.push(p);
}
txn.commit(&cx).unwrap();
let post_write = pager.cache_metrics_snapshot().unwrap();
assert!(
post_write.admits > 0,
"bead_id={BEAD_E2E} case=seq_write_admits"
);
pager.reset_cache_metrics().unwrap();
let read_before = read_surface_snapshot(&pager);
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for (i, &p) in pages.iter().enumerate() {
let data = txn2.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
(i & 0xFF) as u8,
"bead_id={BEAD_E2E} case=seq_read_content page={p}"
);
}
let post_read = pager.cache_metrics_snapshot().unwrap();
let read_after = read_surface_snapshot(&pager);
println!(
"DEBUG_METRICS: hits={} misses={} admits={} evictions={} cached={}",
post_read.hits,
post_read.misses,
post_read.admits,
post_read.evictions,
post_read.cached_pages
);
let total_reads = observed_read_total(read_before, read_after);
assert!(
total_reads == 20,
"bead_id={BEAD_E2E} case=seq_read_accesses total={}",
total_reads
);
let hit_rate = observed_read_hit_rate_percent(read_before, read_after);
assert!(
hit_rate > 40.0,
"bead_id={BEAD_E2E} case=seq_read_hit_rate rate={}",
hit_rate
);
}
#[test]
fn test_cache_efficiency_snapshot_matches_raw_cache_metrics() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0xAB; ps]).unwrap();
txn.commit(&cx).unwrap();
pager.reset_cache_metrics().unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let _ = reader.get_page(&cx, page).unwrap();
let raw = pager.cache_metrics_snapshot().unwrap();
let efficiency = pager.cache_efficiency_snapshot().unwrap();
assert_eq!(
efficiency.hits, raw.hits,
"bead_id={BEAD_E2E} case=efficiency_snapshot_hits"
);
assert_eq!(
efficiency.misses, raw.misses,
"bead_id={BEAD_E2E} case=efficiency_snapshot_misses"
);
assert_eq!(
efficiency.evictions, raw.evictions,
"bead_id={BEAD_E2E} case=efficiency_snapshot_evictions"
);
assert_eq!(
efficiency.cached_pages, raw.cached_pages,
"bead_id={BEAD_E2E} case=efficiency_snapshot_cached_pages"
);
assert!(
(efficiency.hit_rate_percent() - raw.hit_rate_percent()).abs() < f64::EPSILON,
"bead_id={BEAD_E2E} case=efficiency_snapshot_hit_rate"
);
}
#[test]
fn test_e2e_cache_pressure_eviction_telemetry() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for i in 0..300u32 {
let p = txn.allocate_page(&cx).unwrap();
let byte = (i % 256) as u8;
txn.write_page(&cx, p, &vec![byte; ps]).unwrap();
pages.push((p, byte));
}
txn.commit(&cx).unwrap();
pager.reset_cache_metrics().unwrap();
let read_before = read_surface_snapshot(&pager);
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for (p, expected) in &pages {
let data = txn2.get_page(&cx, *p).unwrap();
assert_eq!(
data.as_ref()[0],
*expected,
"bead_id={BEAD_E2E} case=pressure_content page={p}"
);
}
let read_after = read_surface_snapshot(&pager);
let total = observed_read_total(read_before, read_after);
assert_eq!(
total, 300,
"bead_id={BEAD_E2E} case=pressure_total_accesses"
);
let hit_rate = observed_read_hit_rate_percent(read_before, read_after);
assert!(
hit_rate > 40.0,
"bead_id={BEAD_E2E} case=pressure_hit_rate rate={}",
hit_rate
);
}
#[test]
fn test_e2e_hot_cold_workload_hit_rate() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for i in 0..50u32 {
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![(i % 256) as u8; ps]).unwrap();
pages.push(p);
}
txn.commit(&cx).unwrap();
let hot = &pages[..5];
pager.reset_cache_metrics().unwrap();
let read_before = read_surface_snapshot(&pager);
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for idx in 0..200usize {
for h in hot {
let _ = txn2.get_page(&cx, *h).unwrap();
}
let cold_idx = (idx * 2) % 45;
let _ = txn2.get_page(&cx, pages[5 + cold_idx]).unwrap();
let _ = txn2.get_page(&cx, pages[5 + (cold_idx + 1) % 45]).unwrap();
}
drop(txn2);
let read_after = read_surface_snapshot(&pager);
let total = observed_read_total(read_before, read_after);
let expected_total_reads = u64::try_from((hot.len() + 2) * 200).unwrap();
assert_eq!(
total, expected_total_reads,
"bead_id={BEAD_E2E} case=hot_cold_total_accesses"
);
let hit_rate = observed_read_hit_rate_percent(read_before, read_after);
assert!(
hit_rate > 40.0,
"bead_id={BEAD_E2E} case=hot_cold_hit_rate rate={}",
hit_rate
);
}
#[test]
fn test_e2e_random_access_pattern_deterministic() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for _ in 0..100u32 {
let p = txn.allocate_page(&cx).unwrap();
let mut data = vec![0u8; ps];
data[..4].copy_from_slice(&p.get().to_le_bytes());
txn.write_page(&cx, p, &data).unwrap();
pages.push(p);
}
txn.commit(&cx).unwrap();
pager.reset_cache_metrics().unwrap();
let read_before = read_surface_snapshot(&pager);
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let mut idx: usize = 0;
for _ in 0..200 {
idx = (13 * idx + 7) % 100;
let p = pages[idx];
let data = txn2.get_page(&cx, p).unwrap();
let stored_pgno = u32::from_le_bytes(data.as_ref()[..4].try_into().unwrap());
assert_eq!(
stored_pgno,
p.get(),
"bead_id={BEAD_E2E} case=random_fingerprint page={p}"
);
}
let read_after = read_surface_snapshot(&pager);
let total = observed_read_total(read_before, read_after);
assert!(total > 0, "bead_id={BEAD_E2E} case=random_total_accesses");
assert_eq!(total, 200, "bead_id={BEAD_E2E} case=random_total_accesses");
let hit_rate = observed_read_hit_rate_percent(read_before, read_after);
assert!(
hit_rate > 40.0,
"bead_id={BEAD_E2E} case=random_hit_rate rate={}",
hit_rate
);
}
#[test]
fn test_e2e_mixed_read_write_workload() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for _ in 0..30u32 {
let p = txn.allocate_page(&cx).unwrap();
let mut data = vec![0u8; ps];
data[..4].copy_from_slice(&p.get().to_le_bytes());
txn.write_page(&cx, p, &data).unwrap();
pages.push(p);
}
txn.commit(&cx).unwrap();
let mut phase_two_reads = 0_u64;
for batch in 0..5u32 {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let batch_read_before = read_surface_snapshot(&pager);
for i in 0..10 {
let idx = ((batch as usize * 3) + i) % pages.len();
let _ = txn.get_page(&cx, pages[idx]).unwrap();
}
let batch_read_after = read_surface_snapshot(&pager);
phase_two_reads = phase_two_reads
.saturating_add(observed_read_total(batch_read_before, batch_read_after));
for i in 0..3 {
let idx = ((batch as usize * 5) + i) % pages.len();
let new_val = ((batch * 10 + i as u32) % 256) as u8;
txn.write_page(&cx, pages[idx], &vec![new_val; ps]).unwrap();
}
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xF0 + batch as u8; ps])
.unwrap();
pages.push(p);
txn.commit(&cx).unwrap();
}
assert!(
phase_two_reads == 50,
"bead_id={BEAD_E2E} case=mixed_total_accesses total={}",
phase_two_reads
);
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for batch in 0..5u32 {
let p = pages[30 + batch as usize];
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0xF0 + batch as u8,
"bead_id={BEAD_E2E} case=mixed_new_page batch={batch}"
);
}
}
#[test]
fn test_e2e_write_overwrite_verify_latest() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x01; ps]).unwrap();
txn.commit(&cx).unwrap();
for version in 2..=11u8 {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, p, &vec![version; ps]).unwrap();
txn.commit(&cx).unwrap();
}
let txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
11,
"bead_id={BEAD_E2E} case=overwrite_latest_version"
);
}
#[test]
fn test_e2e_savepoint_heavy_workload() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::new();
for i in 0..10u8 {
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![i; ps]).unwrap();
pages.push(p);
}
txn.savepoint(&cx, "sp_heavy").unwrap();
for &p in &pages {
txn.write_page(&cx, p, &vec![0xFF; ps]).unwrap();
}
for &p in &pages {
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(data.as_ref()[0], 0xFF);
}
txn.rollback_to_savepoint(&cx, "sp_heavy").unwrap();
for (i, &p) in pages.iter().enumerate() {
let data = txn.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
i as u8,
"bead_id={BEAD_E2E} case=savepoint_heavy_restored page={p}"
);
}
txn.commit(&cx).unwrap();
}
#[test]
fn test_e2e_alloc_free_cycle_no_leak() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let initial_db_size = {
let inner = pager.inner.lock().unwrap();
inner.db_size
};
let mut freed_pages = Vec::new();
for _ in 0..10 {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xCC; ps]).unwrap();
txn.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.free_page(&cx, p).unwrap();
txn2.commit(&cx).unwrap();
freed_pages.push(p);
}
let freelist_len = {
let inner = pager.inner.lock().unwrap();
inner.freelist.len()
};
assert!(
freelist_len > 0,
"bead_id={BEAD_E2E} case=alloc_free_freelist_populated"
);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let reused = txn.allocate_page(&cx).unwrap();
assert!(
freed_pages.contains(&reused),
"bead_id={BEAD_E2E} case=alloc_free_reuse reused={reused}"
);
txn.commit(&cx).unwrap();
let final_db_size = {
let inner = pager.inner.lock().unwrap();
inner.db_size
};
assert_eq!(
final_db_size,
initial_db_size + 1,
"DB size should only grow by 1 page (the one currently allocated)"
);
}
#[test]
fn test_e2e_metrics_monotonic_across_transactions() {
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut prev_total = 0u64;
for round in 0..5u32 {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![round as u8; ps]).unwrap();
txn.commit(&cx).unwrap();
let txn2 = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let _ = txn2.get_page(&cx, p).unwrap();
let snapshot = read_surface_snapshot(&pager);
let total = snapshot
.cache
.total_accesses()
.saturating_add(snapshot.published_hits);
assert!(
total >= prev_total,
"bead_id={BEAD_E2E} case=metrics_monotonic round={round} \
total={} prev={}",
total,
prev_total
);
prev_total = total;
}
}
#[test]
fn test_e2e_journal_recovery_after_crash_simulation() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/crash_sim.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xAA; ps]).unwrap();
txn.commit(&cx).unwrap();
let mut txn2 = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn2.write_page(&cx, p, &vec![0xBB; ps]).unwrap();
drop(txn2);
drop(pager);
let pager2 = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let txn3 = pager2.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = txn3.get_page(&cx, p).unwrap();
assert_eq!(
data.as_ref()[0],
0xAA,
"bead_id={BEAD_E2E} case=journal_recovery_restores_committed"
);
}
#[test]
fn test_published_current_sequence_gen_tracks_snapshot_gen() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let before_snap = pager.published_snapshot();
assert_eq!(
pager.published.current_sequence_gen(),
before_snap.snapshot_gen,
"bead_id={BEAD_ID} case=current_sequence_gen_matches_snapshot_before_commit"
);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x5A; ps]).unwrap();
txn.commit(&cx).unwrap();
let after_snap = pager.published_snapshot();
assert_eq!(
pager.published.current_sequence_gen(),
after_snap.snapshot_gen,
"bead_id={BEAD_ID} case=current_sequence_gen_matches_snapshot_after_commit"
);
assert!(
after_snap.snapshot_gen > before_snap.snapshot_gen,
"bead_id={BEAD_ID} case=current_sequence_gen_advances_after_publish"
);
assert!(
pager.published.current_sequence_gen() % 2 == 0,
"bead_id={BEAD_ID} case=current_sequence_gen_quiescent_value_is_even"
);
}
#[test]
fn test_published_snapshot_monotonic_after_commit() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let before = pager.published_snapshot();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x5A; ps]).unwrap();
txn.commit(&cx).unwrap();
let after = pager.published_snapshot();
assert!(
after.snapshot_gen > before.snapshot_gen,
"bead_id={BEAD_ID} case=publication_snapshot_gen_monotonic"
);
assert!(
after.visible_commit_seq > before.visible_commit_seq,
"bead_id={BEAD_ID} case=publication_commit_seq_monotonic"
);
assert_eq!(
after.db_size,
p.get(),
"bead_id={BEAD_ID} case=publication_db_size_updates"
);
assert_eq!(
after.freelist_count, 0,
"bead_id={BEAD_ID} case=publication_freelist_count_updates"
);
assert!(
!after.checkpoint_active,
"bead_id={BEAD_ID} case=publication_checkpoint_inactive_after_commit"
);
}
#[test]
fn test_commit_batches_publication_writes() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let publication_writes_before_commit = pager.publication_write_count();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x5A; ps]).unwrap();
txn.commit(&cx).unwrap();
assert_eq!(
pager.publication_write_count(),
publication_writes_before_commit + 1,
"bead_id={BEAD_ID} case=commit_batches_publication_writes"
);
}
#[test]
fn test_single_connection_commit_elides_publication_writes() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(1));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let before = pager.published_snapshot();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let publication_writes_before_commit = pager.publication_write_count();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x6B; ps]).unwrap();
txn.commit(&cx).unwrap();
let after = pager.published_snapshot();
assert_eq!(
pager.publication_write_count(),
publication_writes_before_commit,
"bead_id={BEAD_ID} case=single_connection_commit_skips_publication_write"
);
assert!(
after.visible_commit_seq > before.visible_commit_seq,
"bead_id={BEAD_ID} case=single_connection_commit_still_advances_visible_commit_seq"
);
assert_eq!(
after.db_size,
p.get(),
"bead_id={BEAD_ID} case=single_connection_commit_updates_published_db_size"
);
assert_eq!(
after.page_set_size, 0,
"bead_id={BEAD_ID} case=single_connection_commit_keeps_publication_plane_empty"
);
shared_connection_count.store(2, AtomicOrdering::Release);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let publication_writes_before_multi_connection_commit = pager.publication_write_count();
txn.write_page(&cx, p, &vec![0x7C; ps]).unwrap();
txn.commit(&cx).unwrap();
assert_eq!(
pager.publication_write_count(),
publication_writes_before_multi_connection_commit + 1,
"bead_id={BEAD_ID} case=multi_connection_commit_restores_publication_write"
);
}
#[test]
fn test_single_connection_commit_updates_transaction_published_snapshot_hint() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(1));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let before = pager.published_snapshot();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x6B; ps]).unwrap();
txn.commit(&cx).unwrap();
let committed_hint = txn
.published_visible_commit_seq_hint()
.expect("transaction should expose a published commit-seq hint");
let after = pager.published_snapshot();
assert!(
after.visible_commit_seq > before.visible_commit_seq,
"bead_id={BEAD_ID} case=single_connection_commit_advances_visible_commit_seq"
);
assert_eq!(
committed_hint, after.visible_commit_seq,
"bead_id={BEAD_ID} case=single_connection_commit_refreshes_transaction_published_snapshot_hint"
);
}
#[test]
fn test_single_connection_metadata_only_commit_clears_stale_page_plane_before_later_publish() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(2));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let p = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
p
};
shared_connection_count.store(1, AtomicOrdering::Release);
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, p, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
}
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_one = txn.get_page(&cx, PageNumber::ONE).unwrap().into_vec();
txn.write_page(&cx, PageNumber::ONE, &page_one).unwrap();
txn.commit(&cx).unwrap();
}
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, p).unwrap().as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=metadata_only_single_connection_commit_must_not_leave_stale_page_plane_bytes"
);
}
#[test]
fn test_single_connection_read_skips_publication_plane_population() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(1));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let p = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x4D; ps]).unwrap();
txn.commit(&cx).unwrap();
p
};
let published_before = pager.published_snapshot();
let published_hits_before = pager.published_page_hits();
assert_eq!(
published_before.page_set_size, 0,
"bead_id={BEAD_ID} case=single_connection_read_starts_with_empty_publication_plane"
);
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = reader.get_page(&cx, p).unwrap();
assert_eq!(data.as_ref()[0], 0x4D);
let published_after = pager.published_snapshot();
assert_eq!(
published_after.page_set_size, 0,
"bead_id={BEAD_ID} case=single_connection_read_does_not_publish_observed_pages"
);
assert_eq!(
pager.published_page_hits(),
published_hits_before,
"bead_id={BEAD_ID} case=single_connection_read_bypasses_publication_plane_hits"
);
}
#[test]
fn test_single_connection_commit_and_retain_skips_stale_publication_plane_reads() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(2));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let p = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
p
};
shared_connection_count.store(1, AtomicOrdering::Release);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let published_before_retain = pager.published_snapshot();
let publication_writes_before_commit = pager.publication_write_count();
txn.write_page(&cx, p, &vec![0x22; ps]).unwrap();
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_should_retain_writer"
);
assert_eq!(
pager.publication_write_count(),
publication_writes_before_commit,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_skips_publication_write"
);
let published_after_retain = pager.published_snapshot();
assert_eq!(
published_after_retain.page_set_size, 0,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_keeps_publication_plane_empty"
);
assert!(
published_after_retain.visible_commit_seq > published_before_retain.visible_commit_seq,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_still_advances_visible_commit_seq"
);
assert!(
txn.txn_read_cache.borrow().contains_key(&p),
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_keeps_authoritative_txn_read_cache"
);
shared_connection_count.store(2, AtomicOrdering::Release);
let retained_snapshot = pager.committed_snapshot();
assert_eq!(
retained_snapshot.commit_seq, published_after_retain.visible_commit_seq,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_refreshes_committed_snapshot_seq"
);
assert_eq!(
retained_snapshot.db_size, published_after_retain.db_size,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_refreshes_committed_snapshot_db_size"
);
shared_connection_count.store(1, AtomicOrdering::Release);
let mut stale_buf = PageBuf::new(PageSize::DEFAULT);
stale_buf.fill(0x7F);
pager.cache.insert_buffer(p, stale_buf);
assert_eq!(
txn.get_page(&cx, p).unwrap().as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_must_not_read_stale_shared_cache_page"
);
txn.commit(&cx).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, p).unwrap().as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_release_preserves_committed_visibility"
);
}
#[test]
fn test_committed_snapshot_retained_reader_arc_survives_publication_swap() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(2));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let first_page = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x33; ps]).unwrap();
txn.commit(&cx).unwrap();
page
};
let retained = pager.committed_snapshot();
let retained_commit_seq = retained.commit_seq;
let retained_db_size = retained.db_size;
assert_eq!(
Arc::strong_count(&retained),
2,
"bead_id={DB300_E3_3_A_BEAD_ID} case=retained_snapshot_starts_shared_with_publication_slot"
);
let second_page = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x44; ps]).unwrap();
txn.commit(&cx).unwrap();
page
};
let latest = pager.committed_snapshot();
assert!(
latest.commit_seq > retained_commit_seq,
"bead_id={DB300_E3_3_A_BEAD_ID} case=latest_snapshot_advances_after_swap"
);
assert!(
latest.db_size > retained_db_size,
"bead_id={DB300_E3_3_A_BEAD_ID} case=latest_snapshot_reflects_second_allocation"
);
assert_eq!(
retained.commit_seq, retained_commit_seq,
"bead_id={DB300_E3_3_A_BEAD_ID} case=retained_snapshot_commit_seq_remains_stable"
);
assert_eq!(
retained.db_size, retained_db_size,
"bead_id={DB300_E3_3_A_BEAD_ID} case=retained_snapshot_db_size_remains_stable"
);
assert_eq!(
Arc::strong_count(&retained),
1,
"bead_id={DB300_E3_3_A_BEAD_ID} case=old_snapshot_slot_released_after_swap"
);
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, first_page).unwrap().as_ref()[0],
0x33,
"bead_id={DB300_E3_3_A_BEAD_ID} case=first_page_still_visible"
);
assert_eq!(
reader.get_page(&cx, second_page).unwrap().as_ref()[0],
0x44,
"bead_id={DB300_E3_3_A_BEAD_ID} case=second_page_visible_after_swap"
);
}
#[test]
fn test_committed_snapshot_reclamation_stress_bounds_stale_arcs() {
init_publication_test_tracing();
let (pager, _) = test_pager();
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(2)));
const HELD_READER_CLONES: usize = 8;
const PUBLISH_ITERATIONS: u64 = 256;
for generation in 1..=PUBLISH_ITERATIONS {
let held_snapshots = (0..HELD_READER_CLONES)
.map(|_| pager.committed_snapshot())
.collect::<Vec<_>>();
let old_snapshot = Arc::clone(&held_snapshots[0]);
let old_commit_seq = old_snapshot.commit_seq;
assert!(
held_snapshots
.iter()
.all(|snapshot| Arc::ptr_eq(snapshot, &old_snapshot)),
"bead_id={DB300_E3_3_BEAD_ID} case=reclamation_readers_share_pre_swap_slot"
);
assert_eq!(
Arc::strong_count(&old_snapshot),
HELD_READER_CLONES + 2,
"bead_id={DB300_E3_3_BEAD_ID} case=reclamation_pre_swap_slot_refcount"
);
{
let mut inner = pager
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.commit_seq = CommitSeq::new(generation);
inner.db_size = u32::try_from(generation).expect("test generation fits db_size");
pager.publish_committed_snapshot_from_inner(&inner);
}
let latest = pager.committed_snapshot();
assert!(
!Arc::ptr_eq(&old_snapshot, &latest),
"bead_id={DB300_E3_3_BEAD_ID} case=reclamation_publication_slot_swapped"
);
assert!(
latest.commit_seq > old_commit_seq,
"bead_id={DB300_E3_3_BEAD_ID} case=reclamation_latest_snapshot_advances"
);
assert_eq!(
Arc::strong_count(&old_snapshot),
HELD_READER_CLONES + 1,
"bead_id={DB300_E3_3_BEAD_ID} case=reclamation_old_slot_released_after_swap"
);
}
let latest = pager.committed_snapshot();
assert_eq!(
Arc::strong_count(&latest),
2,
"bead_id={DB300_E3_3_BEAD_ID} case=reclamation_no_stale_snapshots_after_readers_drop"
);
}
#[test]
fn test_committed_snapshot_writer_not_starved_by_reader_pressure() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let pager = StdArc::new(pager);
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(2)));
const READERS: usize = 8;
const PUBLISHES: u64 = 1_024;
let stop = StdArc::new(AtomicBool::new(false));
let barrier = StdArc::new(std::sync::Barrier::new(READERS + 1));
let mut reader_handles = Vec::with_capacity(READERS);
for _ in 0..READERS {
let pager = StdArc::clone(&pager);
let stop = StdArc::clone(&stop);
let barrier = StdArc::clone(&barrier);
reader_handles.push(std::thread::spawn(move || {
barrier.wait();
let mut reads = 0_u64;
while !stop.load(AtomicOrdering::Acquire) {
let snapshot = pager.committed_snapshot();
std::hint::black_box(snapshot.commit_seq);
reads += 1;
}
reads
}));
}
barrier.wait();
let started = Instant::now();
for generation in 1..=PUBLISHES {
let mut inner = pager
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.commit_seq = CommitSeq::new(generation);
inner.db_size = u32::try_from(generation).expect("test generation fits db_size");
pager.publish_committed_snapshot_from_inner(&inner);
}
let elapsed = started.elapsed();
stop.store(true, AtomicOrdering::Release);
let total_reader_snapshots = reader_handles
.into_iter()
.map(|handle| handle.join().expect("reader joined"))
.sum::<u64>();
assert!(
total_reader_snapshots > 0,
"bead_id={DB300_E3_3_BEAD_ID} case=starvation_readers_exercised_snapshot_path"
);
assert!(
elapsed < Duration::from_secs(2),
"bead_id={DB300_E3_3_BEAD_ID} case=starvation_writer_completed elapsed={elapsed:?}"
);
let latest = pager.committed_snapshot();
assert_eq!(
latest.commit_seq,
CommitSeq::new(PUBLISHES),
"bead_id={DB300_E3_3_BEAD_ID} case=starvation_latest_publication_visible"
);
}
#[test]
fn test_single_connection_commit_and_retain_rollback_preserves_last_committed_page() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let shared_connection_count = Arc::new(AtomicUsize::new(1));
pager.bind_shared_connection_count(Arc::clone(&shared_connection_count));
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x31; ps]).unwrap();
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_should_retain_writer_before_rollback"
);
txn.write_page(&cx, p, &vec![0x7A; ps]).unwrap();
txn.rollback(&cx).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, p).unwrap().as_ref()[0],
0x31,
"bead_id={BEAD_ID} case=single_connection_commit_and_retain_rollback_keeps_last_committed_page"
);
}
#[test]
fn test_private_memory_commit_and_retain_batches_multiple_logical_commits_without_journal() {
init_publication_test_tracing();
let pager = private_memory_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let journal_path = SimplePager::<MemoryVfs>::journal_path(&pager.db_path);
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(1)));
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_one = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_one, &vec![0x11; ps]).unwrap();
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={BEAD_ID} case=private_memory_first_logical_commit_retained"
);
assert!(
!pager
.vfs
.access(&cx, &journal_path, AccessFlags::EXISTS)
.unwrap(),
"bead_id={BEAD_ID} case=private_memory_first_logical_commit_skips_journal"
);
{
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.active_transactions, 1,
"bead_id={BEAD_ID} case=private_memory_retained_writer_keeps_single_active_txn"
);
assert!(
inner.writer_active,
"bead_id={BEAD_ID} case=private_memory_retained_writer_keeps_writer_slot"
);
}
txn.write_page(&cx, page_one, &vec![0x22; ps]).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_two, &vec![0x33; ps]).unwrap();
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={BEAD_ID} case=private_memory_second_logical_commit_retained"
);
assert!(
!pager
.vfs
.access(&cx, &journal_path, AccessFlags::EXISTS)
.unwrap(),
"bead_id={BEAD_ID} case=private_memory_second_logical_commit_skips_journal"
);
txn.commit(&cx).unwrap();
assert!(
!pager
.vfs
.access(&cx, &journal_path, AccessFlags::EXISTS)
.unwrap(),
"bead_id={BEAD_ID} case=private_memory_release_commit_skips_journal"
);
{
let inner = pager.inner.lock().unwrap();
assert_eq!(
inner.active_transactions, 0,
"bead_id={BEAD_ID} case=private_memory_release_commit_drops_active_txn_count"
);
assert!(
!inner.writer_active,
"bead_id={BEAD_ID} case=private_memory_release_commit_releases_writer_slot"
);
}
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page_one).unwrap().as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=private_memory_batched_commits_keep_latest_page_one"
);
assert_eq!(
reader.get_page(&cx, page_two).unwrap().as_ref()[0],
0x33,
"bead_id={BEAD_ID} case=private_memory_batched_commits_keep_latest_page_two"
);
}
#[test]
fn test_dirty_bitmap_set_check() {
let pager = private_memory_pager();
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(1)));
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page, &vec![0x41; ps]).unwrap();
assert_eq!(
txn.write_pages_sorted,
vec![page],
"bead_id={TRACK_U_BEAD_ID} case=dirty_bitmap_tracks_single_page"
);
assert!(
txn.write_set.contains_key(&page),
"bead_id={TRACK_U_BEAD_ID} case=dirty_bitmap_marks_page_in_write_set"
);
assert!(
!txn.retained_memory_overlay_dirty_pages.contains(&page),
"bead_id={TRACK_U_BEAD_ID} case=retained_overlay_stays_empty_before_retain"
);
track_u_log_counts("dirty_bitmap_set_check", 1, 0, 0);
}
#[test]
fn test_dirty_bitmap_double_write_dedups_wal_entry() {
let (pager, _) = test_pager();
let cx = Cx::new();
let (backend, frames, _begin_calls, _batch_calls) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let page = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page, &sample_page(0x11)).unwrap();
seed.commit(&cx).unwrap();
page
};
frames.lock().unwrap().clear();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page, &sample_page(0x22)).unwrap();
let final_bytes = sample_page(0x33);
txn.write_page(&cx, page, &final_bytes).unwrap();
assert_eq!(
txn.write_pages_sorted,
vec![page],
"bead_id={TRACK_U_BEAD_ID} case=double_write_dedups_dirty_surface"
);
txn.commit(&cx).unwrap();
let written = frames.lock().unwrap().clone();
let page_frames = written
.iter()
.filter(|(page_no, _, _)| *page_no == page.get())
.collect::<Vec<_>>();
assert_eq!(
page_frames.len(),
1,
"bead_id={TRACK_U_BEAD_ID} case=double_write_emits_single_data_frame"
);
assert_eq!(
page_frames[0].1.as_slice(),
final_bytes.as_slice(),
"bead_id={TRACK_U_BEAD_ID} case=double_write_keeps_last_page_image"
);
assert_eq!(
written
.iter()
.filter(|(page_no, _, _)| *page_no == PageNumber::ONE.get())
.count(),
0,
"bead_id={TRACK_U_BEAD_ID} case=double_write_emits_no_synthetic_page_one_frame"
);
track_u_log_counts("dirty_bitmap_double_write", 1, 1, page_frames.len());
}
#[test]
fn test_dirty_bitmap_commit_flushes_all_dirty_pages() {
let (pager, _) = test_pager();
let cx = Cx::new();
let (backend, frames, _begin_calls, _batch_calls) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let pages = {
let mut seed = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::with_capacity(100);
for seed_byte in 0_u8..100 {
let page = seed.allocate_page(&cx).unwrap();
seed.write_page(&cx, page, &sample_page(seed_byte)).unwrap();
pages.push(page);
}
seed.commit(&cx).unwrap();
pages
};
frames.lock().unwrap().clear();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
for (idx, page) in pages.iter().copied().enumerate() {
let seed_byte = u8::try_from(idx).expect("100-page test index fits u8");
txn.write_page(&cx, page, &sample_page(seed_byte.wrapping_add(100)))
.unwrap();
}
assert_eq!(
txn.write_pages_sorted.len(),
pages.len(),
"bead_id={TRACK_U_BEAD_ID} case=dirty_surface_tracks_all_100_pages"
);
txn.commit(&cx).unwrap();
let written = frames.lock().unwrap().clone();
let flushed_pages = written
.iter()
.filter(|(page_no, _, _)| *page_no != PageNumber::ONE.get())
.map(|(page_no, _, _)| *page_no)
.collect::<Vec<_>>();
let expected_pages = pages.iter().map(|page| page.get()).collect::<Vec<_>>();
assert_eq!(
flushed_pages, expected_pages,
"bead_id={TRACK_U_BEAD_ID} case=commit_flushes_every_dirty_page_once"
);
assert_eq!(
written
.iter()
.filter(|(page_no, _, _)| *page_no == PageNumber::ONE.get())
.count(),
0,
"bead_id={TRACK_U_BEAD_ID} case=commit_flush_emits_no_synthetic_page_one_frame"
);
track_u_log_counts(
"dirty_bitmap_commit_flushes_all",
pages.len(),
0,
flushed_pages.len(),
);
}
#[test]
fn test_dirty_bitmap_rollback_clears_retained_overlay() {
let pager = private_memory_pager();
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(1)));
let cx = Cx::new();
let original_one = sample_page(0x31);
let original_two = sample_page(0x32);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_one = txn.allocate_page(&cx).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_one, &original_one).unwrap();
txn.write_page(&cx, page_two, &original_two).unwrap();
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={TRACK_U_BEAD_ID} case=rollback_clear_requires_retained_commit"
);
assert_eq!(
txn.retained_memory_overlay_dirty_pages.len(),
2,
"bead_id={TRACK_U_BEAD_ID} case=retained_overlay_tracks_committed_pages"
);
txn.write_page(&cx, page_one, &sample_page(0x7A)).unwrap();
txn.rollback(&cx).unwrap();
assert!(
txn.retained_memory_overlay_dirty_pages.is_empty(),
"bead_id={TRACK_U_BEAD_ID} case=rollback_clears_retained_overlay_bitmap"
);
assert!(
txn.write_set.is_empty() && txn.write_pages_sorted.is_empty(),
"bead_id={TRACK_U_BEAD_ID} case=rollback_clears_live_dirty_surface"
);
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page_one).unwrap().into_vec(),
original_one,
"bead_id={TRACK_U_BEAD_ID} case=rollback_restores_last_committed_page_one"
);
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
original_two,
"bead_id={TRACK_U_BEAD_ID} case=rollback_restores_last_committed_page_two"
);
track_u_log_counts("dirty_bitmap_rollback_clears", 2, 0, 2);
}
#[test]
fn test_dirty_bitmap_large_10k_pages() {
let pager = private_memory_pager();
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(1)));
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::with_capacity(10_000);
for idx in 0..10_000_usize {
let page = txn.allocate_page(&cx).unwrap();
let fill = u8::try_from(idx % 251).expect("modulo fits u8");
txn.write_page(&cx, page, &vec![fill; ps]).unwrap();
pages.push((page, fill));
}
assert_eq!(
txn.write_pages_sorted.len(),
pages.len(),
"bead_id={TRACK_U_BEAD_ID} case=large_dirty_surface_tracks_10k_pages"
);
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={TRACK_U_BEAD_ID} case=large_dirty_surface_retain_succeeds"
);
assert_eq!(
txn.retained_memory_overlay_dirty_pages.len(),
pages.len(),
"bead_id={TRACK_U_BEAD_ID} case=large_retained_overlay_tracks_10k_pages"
);
txn.rollback(&cx).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
for &(sample_idx, expected_fill) in &[
(0_usize, pages[0].1),
(4_999_usize, pages[4_999].1),
(9_999_usize, pages[9_999].1),
] {
let page = pages[sample_idx].0;
assert_eq!(
reader.get_page(&cx, page).unwrap().as_ref()[0],
expected_fill,
"bead_id={TRACK_U_BEAD_ID} case=large_dirty_surface_preserves_sample sample_idx={sample_idx}"
);
}
track_u_log_counts("dirty_bitmap_large", pages.len(), 0, pages.len());
}
#[test]
fn test_dirty_bitmap_crash_recovery_restores_original_pages() {
let path = PathBuf::from("/track_u_dirty_bitmap_crash_recovery.db");
let journal_path = SimplePager::<DbWriteFailOnceVfs>::journal_path(&path);
let vfs = DbWriteFailOnceVfs::new(path.clone());
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let original_pages = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let mut pages = Vec::with_capacity(32);
for seed_byte in 1_u8..=32 {
let page = txn.allocate_page(&cx).unwrap();
let original = sample_page(seed_byte);
txn.write_page(&cx, page, &original).unwrap();
pages.push((page, original));
}
txn.commit(&cx).unwrap();
pages
};
vfs.arm_after_db_writes(8);
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
for (idx, (page, _)) in original_pages.iter().enumerate() {
let seed_byte = u8::try_from(idx).expect("32-page test index fits u8");
txn.write_page(&cx, *page, &sample_page(seed_byte.wrapping_add(101)))
.unwrap();
}
let err = txn.commit(&cx).unwrap_err();
assert!(
matches!(err, FrankenError::Io(_)),
"bead_id={TRACK_U_BEAD_ID} case=crash_recovery_surfaces_partial_commit_io_error"
);
}
drop(pager);
let reopened = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let reader = reopened.begin(&cx, TransactionMode::ReadOnly).unwrap();
for (page, original) in &original_pages {
assert_eq!(
reader.get_page(&cx, *page).unwrap().as_ref(),
original.as_slice(),
"bead_id={TRACK_U_BEAD_ID} case=crash_recovery_restores_original_page page={}",
page.get()
);
}
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={TRACK_U_BEAD_ID} case=crash_recovery_clears_abandoned_journal"
);
track_u_log_counts("dirty_bitmap_crash_recovery", original_pages.len(), 0, 0);
}
#[test]
fn test_dirty_bitmap_deferred_flush_materializes_retained_overlay_without_duplicates() {
let pager = private_memory_pager();
pager.bind_shared_connection_count(Arc::new(AtomicUsize::new(1)));
let cx = Cx::new();
let original_one = sample_page(0x41);
let original_two = sample_page(0x42);
let updated_one = sample_page(0x51);
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_one = txn.allocate_page(&cx).unwrap();
let page_two = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, page_one, &original_one).unwrap();
txn.write_page(&cx, page_two, &original_two).unwrap();
assert!(
txn.commit_and_retain(&cx).unwrap(),
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_requires_retained_commit"
);
assert_eq!(
txn.retained_memory_overlay_dirty_pages.len(),
2,
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_tracks_retained_overlay"
);
txn.write_page(&cx, page_one, &updated_one).unwrap();
assert_eq!(
txn.write_set_page_numbers(),
vec![page_one],
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_starts_with_live_dirty_page"
);
txn.materialize_retained_memory_overlay_into_write_set()
.unwrap();
assert_eq!(
txn.write_set_page_numbers(),
vec![page_one, page_two],
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_backfills_only_missing_overlay_page"
);
assert_eq!(
txn.write_set.len(),
2,
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_avoids_duplicate_dirty_entries"
);
assert_eq!(
txn.write_set
.get(&page_one)
.expect("page_one staged entry")
.as_page_bytes(),
updated_one.as_slice(),
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_preserves_newest_page_image"
);
assert_eq!(
txn.write_set
.get(&page_two)
.expect("page_two staged entry")
.as_page_bytes(),
original_two.as_slice(),
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_restores_untouched_overlay_page"
);
txn.commit(&cx).unwrap();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page_one).unwrap().into_vec(),
updated_one,
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_commits_latest_page_one"
);
assert_eq!(
reader.get_page(&cx, page_two).unwrap().into_vec(),
original_two,
"bead_id={TRACK_U_BEAD_ID} case=deferred_flush_materialization_commits_restored_overlay_page_two"
);
track_u_log_counts("dirty_bitmap_deferred_flush_materialization", 2, 1, 2);
}
#[test]
fn test_dirty_bitmap_double_write_crash_recovery_restores_original_page() {
let path = PathBuf::from("/track_u_dirty_bitmap_double_write_crash.db");
let journal_path = SimplePager::<DbWriteFailOnceVfs>::journal_path(&path);
let vfs = DbWriteFailOnceVfs::new(path.clone());
let pager = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let (page, original) = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page = txn.allocate_page(&cx).unwrap();
let original = sample_page(0x2A);
txn.write_page(&cx, page, &original).unwrap();
txn.commit(&cx).unwrap();
(page, original)
};
vfs.arm_after_db_writes(1);
{
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, page, &sample_page(0x55)).unwrap();
txn.write_page(&cx, page, &sample_page(0x66)).unwrap();
let err = txn.commit(&cx).unwrap_err();
assert!(
matches!(err, FrankenError::Io(_)),
"bead_id={TRACK_U_BEAD_ID} case=double_write_crash_recovery_surfaces_commit_io_error"
);
}
drop(pager);
let reopened = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let reader = reopened.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, page).unwrap().as_ref(),
original.as_slice(),
"bead_id={TRACK_U_BEAD_ID} case=double_write_crash_recovery_restores_original_page"
);
assert!(
!vfs.access(&cx, &journal_path, AccessFlags::EXISTS).unwrap(),
"bead_id={TRACK_U_BEAD_ID} case=double_write_crash_recovery_clears_abandoned_journal"
);
track_u_log_counts("dirty_bitmap_double_write_crash_recovery", 1, 1, 0);
}
#[test]
fn test_wal_commit_skips_post_commit_cache_admission() {
init_publication_test_tracing();
let cx = Cx::new();
let vfs = MemoryVfs::new();
let db_path = PathBuf::from("/wal_commit_skips_post_commit_cache_admission.db");
let pager = SimplePager::open(vfs, &db_path, PageSize::DEFAULT).unwrap();
let (backend, _frames, _begin_calls, _batch_calls) = MockWalBackend::new();
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let cached_pages_before_commit = pager.cache_metrics_snapshot().unwrap().cached_pages;
let ps = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xAB; ps]).unwrap();
txn.commit(&cx).unwrap();
let cache_after_commit = pager.cache_metrics_snapshot().unwrap();
assert_eq!(
cache_after_commit.cached_pages,
cached_pages_before_commit + 1,
"bead_id={BEAD_ID} case=wal_commit_avoids_post_commit_cache_fanout"
);
let read_before = read_surface_snapshot(&pager);
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
reader.get_page(&cx, p).unwrap().as_ref()[0],
0xAB,
"bead_id={BEAD_ID} case=wal_commit_publication_keeps_read_visibility"
);
let read_after = read_surface_snapshot(&pager);
assert_eq!(
read_after.cache, read_before.cache,
"bead_id={BEAD_ID} case=wal_post_commit_read_skips_cache"
);
assert_eq!(
read_after.published_hits,
read_before.published_hits + 1,
"bead_id={BEAD_ID} case=wal_post_commit_read_hits_publication"
);
}
#[test]
fn test_published_read_hit_does_not_touch_cache_metrics() {
init_publication_test_tracing();
let (pager, _) = test_pager();
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let p = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0xAB; ps]).unwrap();
txn.commit(&cx).unwrap();
p
};
let cache_before = pager.cache_metrics_snapshot().unwrap();
let published_hits_before = pager.published_page_hits();
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let data = reader.get_page(&cx, p).unwrap();
assert_eq!(data.as_ref()[0], 0xAB);
let cache_after = pager.cache_metrics_snapshot().unwrap();
assert_eq!(
cache_after, cache_before,
"bead_id={BEAD_ID} case=publication_hit_skips_cache_metrics"
);
assert_eq!(
pager.published_page_hits(),
published_hits_before + 1,
"bead_id={BEAD_ID} case=publication_hit_counter"
);
}
#[test]
fn test_published_pages_len_tracks_insert_remove_clear() {
let published_pages = PublishedPages::new(0);
let page_two = PageNumber::new(2).unwrap();
let page_three = PageNumber::new(3).unwrap();
assert_eq!(published_pages.len(), 0);
assert!(published_pages.insert(page_two, PageData::from_vec(sample_page(0x22))));
assert_eq!(published_pages.len(), 1);
assert!(!published_pages.insert(page_two, PageData::from_vec(sample_page(0x33))));
assert_eq!(
published_pages.len(),
1,
"bead_id=bd-qrss1 case=replacement_must_not_increment_page_count"
);
assert!(published_pages.insert(page_three, PageData::from_vec(sample_page(0x44))));
assert_eq!(published_pages.len(), 2);
assert!(published_pages.remove(page_two));
assert_eq!(published_pages.len(), 1);
assert!(!published_pages.remove(page_two));
assert_eq!(published_pages.len(), 1);
published_pages.clear();
assert_eq!(published_pages.len(), 0);
}
#[test]
fn test_published_pages_overflow_insert_remove_clear_tracks_count() {
let published_pages = PublishedPages::new(0);
let overflow_page = PageNumber::new(70_000).unwrap();
assert_eq!(published_pages.len(), 0);
assert!(published_pages.insert(overflow_page, PageData::from_vec(sample_page(0x70))));
assert_eq!(
published_pages.get(overflow_page),
Some(PageData::from_vec(sample_page(0x70))),
"bead_id=bd-uvdnk case=overflow_insert_makes_page_visible"
);
assert_eq!(published_pages.len(), 1);
assert!(!published_pages.insert(overflow_page, PageData::from_vec(sample_page(0x71))));
assert_eq!(
published_pages.get(overflow_page),
Some(PageData::from_vec(sample_page(0x71))),
"bead_id=bd-uvdnk case=overflow_replace_keeps_latest_page"
);
assert_eq!(
published_pages.len(),
1,
"bead_id=bd-uvdnk case=overflow_replace_does_not_increment_count"
);
assert!(published_pages.remove(overflow_page));
assert!(published_pages.get(overflow_page).is_none());
assert_eq!(published_pages.len(), 0);
assert!(published_pages.insert(overflow_page, PageData::from_vec(sample_page(0x72))));
published_pages.clear();
assert!(
published_pages.get(overflow_page).is_none(),
"bead_id=bd-uvdnk case=overflow_clear_removes_page"
);
assert_eq!(published_pages.len(), 0);
}
#[test]
#[ignore = "microbench, run with --ignored --nocapture"]
fn bench_concurrent_published_pages_clear_empty_overflow_microbench() {
use std::time::Instant;
const ITERS: usize = 1_000_000;
let overflow = ConcurrentPublishedPages::new();
for _ in 0..ITERS / 10 {
overflow.clear();
}
let start = Instant::now();
for _ in 0..ITERS {
overflow.clear();
}
let elapsed_opt = start.elapsed();
let per_call_opt = elapsed_opt / u32::try_from(ITERS).unwrap();
let start = Instant::now();
for _ in 0..ITERS {
overflow.page_count.store(1, AtomicOrdering::Release);
overflow.clear();
}
let elapsed_base = start.elapsed();
let per_call_base = elapsed_base / u32::try_from(ITERS).unwrap();
let speedup = per_call_base.as_nanos() as f64 / per_call_opt.as_nanos().max(1) as f64;
eprintln!(
"bench_concurrent_published_pages_clear_empty_overflow_microbench: ITERS={ITERS} \
baseline={per_call_base:?} optimized={per_call_opt:?} \
speedup={speedup:.2}x"
);
}
#[test]
fn test_published_pages_insert_batch_and_retain_track_page_count() {
let published_pages = PublishedPages::new(0);
let page_two = PageNumber::new(2).unwrap();
let page_sixty_five = PageNumber::new(65).unwrap();
let page_seventy_thousand = PageNumber::new(70_000).unwrap();
published_pages.insert_batch([
(page_two, PageData::from_vec(sample_page(0x02))),
(page_sixty_five, PageData::from_vec(sample_page(0x41))),
(page_seventy_thousand, PageData::from_vec(sample_page(0x81))),
(page_two, PageData::from_vec(sample_page(0xFF))),
]);
assert_eq!(
published_pages.len(),
3,
"bead_id=bd-qrss1 case=batch_insert_counts_only_new_pages"
);
assert_eq!(
published_pages.get(page_two),
Some(PageData::from_vec(sample_page(0xFF))),
"bead_id=bd-qrss1 case=batch_insert_replaces_existing_page"
);
published_pages.retain(|page_no| page_no.get() >= 65);
assert_eq!(
published_pages.len(),
2,
"bead_id=bd-qrss1 case=retain_updates_atomic_page_count"
);
assert!(published_pages.get(page_two).is_none());
assert!(published_pages.get(page_sixty_five).is_some());
assert!(published_pages.get(page_seventy_thousand).is_some());
}
#[test]
fn test_published_pages_direct_slots_use_small_database_floor() {
let published_pages = PublishedPages::new(1);
assert_eq!(
published_pages.atomic_slot_count(),
ATOMIC_PUBLISHED_MIN_SLOT_COUNT,
"small databases should not allocate the full direct-slot plane"
);
let floor_page =
u32::try_from(ATOMIC_PUBLISHED_MIN_SLOT_COUNT).expect("slot floor fits in u32");
let direct_edge = PageNumber::new(floor_page).unwrap();
let overflow_edge = PageNumber::new(floor_page + 1).unwrap();
assert!(published_pages.insert(direct_edge, PageData::from_vec(sample_page(0xA1))));
assert!(published_pages.insert(overflow_edge, PageData::from_vec(sample_page(0xA2))));
assert_eq!(
published_pages.get(direct_edge),
Some(PageData::from_vec(sample_page(0xA1)))
);
assert_eq!(
published_pages.get(overflow_edge),
Some(PageData::from_vec(sample_page(0xA2)))
);
assert_eq!(published_pages.len(), 2);
}
#[test]
fn test_published_pages_direct_active_slots_track_live_pages() {
let published_pages = PublishedPages::new(1);
let page_one = PageNumber::new(1).unwrap();
let page_two = PageNumber::new(2).unwrap();
assert!(published_pages.insert(page_one, PageData::from_vec(sample_page(0xB1))));
assert_eq!(published_pages.atomic_active_slot_count(), 1);
assert!(!published_pages.insert(page_one, PageData::from_vec(sample_page(0xB2))));
assert_eq!(
published_pages.atomic_active_slot_count(),
1,
"direct replacement must not duplicate the active slot index"
);
assert!(published_pages.insert(page_two, PageData::from_vec(sample_page(0xB3))));
assert_eq!(published_pages.atomic_active_slot_count(), 2);
assert!(published_pages.remove(page_one));
assert_eq!(
published_pages.atomic_active_slot_count(),
1,
"direct remove should drop its active slot index"
);
published_pages.clear();
assert_eq!(published_pages.atomic_active_slot_count(), 0);
assert_eq!(published_pages.len(), 0);
assert!(published_pages.get(page_two).is_none());
}
#[test]
fn test_atomic_published_pages_remove_middle_and_tail_are_consistent() {
let published_pages = PublishedPages::new(16);
let page = |n: u32| PageNumber::new(n).unwrap();
for n in 1..=5 {
assert!(
published_pages.insert(page(n), PageData::from_vec(sample_page(n as u8))),
"fresh insert should succeed for page {n}"
);
}
assert_eq!(published_pages.atomic_active_slot_count(), 5);
assert!(published_pages.remove(page(3)));
assert_eq!(published_pages.atomic_active_slot_count(), 4);
assert!(published_pages.get(page(3)).is_none());
assert!(published_pages.get(page(5)).is_some());
assert!(published_pages.remove(page(1)));
assert_eq!(published_pages.atomic_active_slot_count(), 3);
assert!(published_pages.get(page(1)).is_none());
assert!(published_pages.get(page(4)).is_some());
assert!(published_pages.remove(page(5)));
assert_eq!(published_pages.atomic_active_slot_count(), 2);
assert!(published_pages.get(page(5)).is_none());
assert!(published_pages.get(page(2)).is_some());
assert!(published_pages.get(page(4)).is_some());
assert!(published_pages.remove(page(2)));
assert!(published_pages.remove(page(4)));
assert_eq!(published_pages.atomic_active_slot_count(), 0);
assert_eq!(published_pages.len(), 0);
}
#[test]
fn test_atomic_published_pages_remove_is_o1_vs_linear_scan() {
use std::time::Instant;
const N: u32 = 4_000;
let slot_cap = u32::try_from(PublishedPages::new(N).atomic_slot_count())
.expect("direct slot count fits in u32");
assert!(
N <= slot_cap,
"N must stay within the direct-slot plane for apples-to-apples timing"
);
let pages: Vec<PageNumber> = (1..=N).map(|n| PageNumber::new(n).unwrap()).collect();
let payload = PageData::from_vec(sample_page(0xE4));
{
let warm = PublishedPages::new(N);
for &p in &pages {
let _ = warm.insert(p, payload.clone());
}
for &p in &pages {
let _ = warm.remove(p);
}
}
let published = PublishedPages::new(N);
for &p in &pages {
assert!(published.insert(p, payload.clone()));
}
let t0 = Instant::now();
for &p in &pages {
assert!(published.remove(p));
}
let o1_elapsed = t0.elapsed();
assert_eq!(published.atomic_active_slot_count(), 0);
let mut active_indices: Vec<usize> = (0..N as usize).collect();
let t0 = Instant::now();
for &p in &pages {
let idx = (p.get() - 1) as usize;
let pos = active_indices
.iter()
.position(|&i| i == idx)
.expect("active index must be present");
active_indices.swap_remove(pos);
}
let linear_elapsed = t0.elapsed();
assert!(active_indices.is_empty());
eprintln!(
"atomic_published_pages_remove: O(1) back-pointer = {o1_elapsed:?}, \
reference O(n) linear scan = {linear_elapsed:?} for N={N}"
);
assert!(
o1_elapsed <= linear_elapsed.saturating_mul(4),
"O(1) remove regressed vs pure O(n) reference: o1={o1_elapsed:?} scan={linear_elapsed:?}"
);
}
#[test]
fn test_published_pager_state_new_construction_drop_cost() {
use std::time::Instant;
for _ in 0..4 {
let _ = PublishedPagerState::new(1, CommitSeq::new(0), JournalMode::Wal, 0);
}
const ITERS: usize = 8;
let t0 = Instant::now();
for _ in 0..ITERS {
let state = PublishedPagerState::new(1, CommitSeq::new(0), JournalMode::Wal, 0);
drop(state);
}
let elapsed = t0.elapsed();
let per_pair = elapsed / ITERS as u32;
eprintln!(
"PublishedPagerState::new + drop (small-DB, floor={}): \
{ITERS}x = {elapsed:?} ({per_pair:?} per pair)",
ATOMIC_PUBLISHED_MIN_SLOT_COUNT,
);
assert!(
elapsed < std::time::Duration::from_millis(20 * ITERS as u64),
"PublishedPagerState::new+drop ran much slower than expected: {elapsed:?} for {ITERS} pairs"
);
}
#[test]
fn test_wal_frame_count_read_lock_does_not_block_behind_writer() {
use std::sync::Barrier;
use std::sync::RwLock;
use std::sync::atomic::AtomicUsize;
use std::time::{Duration, Instant};
const READERS: usize = 8;
const HOLD: Duration = Duration::from_millis(20);
let shared = StdArc::new(RwLock::new(AtomicUsize::new(42)));
let barrier = StdArc::new(Barrier::new(READERS + 1));
let writer = {
let shared = StdArc::clone(&shared);
let barrier = StdArc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait();
let _g = shared.write().unwrap();
std::thread::sleep(HOLD);
})
};
let started = Instant::now();
let readers: Vec<_> = (0..READERS)
.map(|_| {
let shared = StdArc::clone(&shared);
let barrier = StdArc::clone(&barrier);
std::thread::spawn(move || {
barrier.wait();
let g = shared.read().unwrap();
std::hint::black_box(g.load(std::sync::atomic::Ordering::Relaxed));
})
})
.collect();
for r in readers {
r.join().expect("reader join");
}
let readers_elapsed = started.elapsed();
writer.join().expect("writer join");
eprintln!(
"wal_frame_count contention: {READERS} concurrent read-lock probes finished \
in {readers_elapsed:?} behind a {HOLD:?} write-hold. \
Stacked-write upper bound would be {READERS}×{HOLD:?} = {:?}.",
HOLD * READERS as u32
);
assert!(
readers_elapsed < HOLD * 3,
"read-lock probes should not stack serially: elapsed={readers_elapsed:?} HOLD={HOLD:?}"
);
}
#[test]
fn test_published_snapshot_page_set_size_tracks_concurrent_page_count() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(7), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_sixty_five = PageNumber::new(65).unwrap();
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(7),
db_size: 65,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_two,
PageData::from_vec(sample_page(0x22)),
);
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 65,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_sixty_five,
PageData::from_vec(sample_page(0x65)),
);
assert_eq!(
published.snapshot().page_set_size,
2,
"bead_id=bd-qrss1 case=publication_plane_page_count_after_inserts"
);
published.publish_remove_page(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(9),
db_size: 65,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_two,
);
assert_eq!(
published.snapshot().page_set_size,
1,
"bead_id=bd-qrss1 case=publication_plane_page_count_after_remove"
);
published.publish_clear_if(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(10),
db_size: 0,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
true,
);
assert_eq!(
published.snapshot().page_set_size,
0,
"bead_id=bd-qrss1 case=publication_plane_page_count_after_clear"
);
}
#[test]
fn test_stale_smaller_commit_publish_does_not_mutate_newer_snapshot() {
init_publication_test_tracing();
let published = PublishedPagerState::new(8, CommitSeq::new(8), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_seven = PageNumber::new(7).unwrap();
let original_page_two = PageData::from_vec(sample_page(0x22));
let original_page_seven = PageData::from_vec(sample_page(0x77));
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_two,
original_page_two.clone(),
);
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_seven,
original_page_seven.clone(),
);
let refreshed_page_two = PageData::from_vec(sample_page(0x99));
let write_set = HashMap::from([(
page_two,
StagedPage::from_page_data(refreshed_page_two.clone()),
)]);
published.publish_commit(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(7),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
&write_set,
);
assert_eq!(
published.try_get_page(page_two),
Some(original_page_two),
"bead_id=bd-wwqen.3 case=stale_smaller_commit_must_not_overwrite_newer_page_bytes"
);
assert_eq!(
published.try_get_page(page_seven),
Some(original_page_seven),
"bead_id=bd-wwqen.3 case=stale_smaller_commit_must_not_evict_newer_pages"
);
assert_eq!(
published.snapshot().db_size,
8,
"bead_id=bd-wwqen.3 case=stale_smaller_commit_preserves_published_db_size"
);
}
#[test]
fn test_publish_commit_sweeps_pages_but_keeps_db_size_monotonic() {
init_publication_test_tracing();
let published = PublishedPagerState::new(8, CommitSeq::new(8), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_seven = PageNumber::new(7).unwrap();
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_seven,
PageData::from_vec(sample_page(0x77)),
);
let refreshed_page_two = PageData::from_vec(sample_page(0x22));
let write_set = HashMap::from([(
page_two,
StagedPage::from_page_data(refreshed_page_two.clone()),
)]);
published.publish_commit(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(9),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
&write_set,
);
assert_eq!(
published.try_get_page(page_two),
Some(refreshed_page_two),
"bead_id=bd-wwqen.3 case=shrink_commit_keeps_written_page_inside_new_db_size"
);
assert!(
published.try_get_page(page_seven).is_none(),
"bead_id=bd-wwqen.3 case=shrink_commit_evicts_pages_above_new_db_size"
);
assert_eq!(
published.snapshot().db_size,
8,
"bead_id=bd-wwqen.3 case=shrink_commit_leaves_published_db_size_monotonic"
);
}
#[test]
fn test_read_page_copy_zero_fills_pages_beyond_db_size_even_if_cache_is_stale() {
let (pager, _) = test_pager();
let cx = Cx::new();
let page_seven = PageNumber::new(7).unwrap();
let mut stale_buf = PageBuf::new(PageSize::DEFAULT);
stale_buf.fill(0x7A);
pager.cache.insert_buffer(page_seven, stale_buf);
let mut inner = pager
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.db_size = 4;
let bytes = inner
.read_page_copy(&cx, &pager.cache, &pager.wal_backend, page_seven)
.expect("read_page_copy should succeed for pages beyond db_size");
assert!(
bytes.iter().all(|byte| *byte == 0),
"bead_id=bd-wwqen.3 case=stale_cached_page_above_db_size_must_zero_fill"
);
}
#[test]
fn test_publish_truncate_checkpoint_updates_published_db_size_on_same_commit_seq() {
init_publication_test_tracing();
let published = PublishedPagerState::new(8, CommitSeq::new(8), JournalMode::Wal, 0);
let cx = Cx::new();
let page_seven = PageNumber::new(7).unwrap();
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_seven,
PageData::from_vec(sample_page(0x77)),
);
published.publish_truncate_checkpoint(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
4,
);
assert!(
published.try_get_page(page_seven).is_none(),
"bead_id=bd-wwqen.3 case=truncate_checkpoint_evicts_pages_above_new_db_size"
);
assert_eq!(
published.snapshot().db_size,
4,
"bead_id=bd-wwqen.3 case=truncate_checkpoint_updates_published_db_size"
);
}
#[test]
fn test_stale_larger_commit_publish_does_not_reopen_newer_shrunken_snapshot() {
init_publication_test_tracing();
let published = PublishedPagerState::new(8, CommitSeq::new(8), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_seven = PageNumber::new(7).unwrap();
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_seven,
PageData::from_vec(sample_page(0x77)),
);
let current_page_two = PageData::from_vec(sample_page(0x22));
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(9),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_two,
current_page_two.clone(),
);
published.publish_truncate_checkpoint(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(9),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
4,
);
let stale_page_two = PageData::from_vec(sample_page(0x99));
let stale_page_seven = PageData::from_vec(sample_page(0x77));
let stale_write_set = HashMap::from([
(page_two, StagedPage::from_page_data(stale_page_two)),
(page_seven, StagedPage::from_page_data(stale_page_seven)),
]);
published.publish_commit(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
&stale_write_set,
);
let after = published.snapshot();
assert_eq!(
after.visible_commit_seq,
CommitSeq::new(9),
"bead_id=bd-wwqen.3 case=stale_larger_commit_must_not_regress_visible_commit_seq"
);
assert_eq!(
after.db_size, 4,
"bead_id=bd-wwqen.3 case=stale_larger_commit_must_not_reopen_shrunken_db_size"
);
assert_eq!(
published.try_get_page(page_two),
Some(current_page_two),
"bead_id=bd-wwqen.3 case=stale_larger_commit_must_not_overwrite_newer_page_bytes"
);
assert!(
published.try_get_page(page_seven).is_none(),
"bead_id=bd-wwqen.3 case=stale_larger_commit_must_not_restore_truncated_pages"
);
}
#[test]
fn test_metadata_only_publish_without_page_clear_advances_snapshot_generation() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(7), JournalMode::Wal, 0);
let cx = Cx::new();
let before = published.snapshot();
let writes_before = published.publication_write_count();
published.publish_single_connection_metadata_update(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 2,
checkpoint_active: true,
},
false,
);
let after = published.snapshot();
assert!(
after.snapshot_gen > before.snapshot_gen,
"bead_id=bd-wwqen.3 case=metadata_only_publish_must_advance_snapshot_generation"
);
assert_eq!(
after.visible_commit_seq,
CommitSeq::new(8),
"bead_id=bd-wwqen.3 case=metadata_only_publish_updates_visible_commit_seq"
);
assert_eq!(
after.freelist_count, 2,
"bead_id=bd-wwqen.3 case=metadata_only_publish_updates_freelist_count"
);
assert!(
after.checkpoint_active,
"bead_id=bd-wwqen.3 case=metadata_only_publish_updates_checkpoint_flag"
);
assert_eq!(
after.page_set_size, 0,
"bead_id=bd-wwqen.3 case=metadata_only_publish_keeps_page_plane_empty"
);
assert_eq!(
published.page_plane_visible_commit_seq(),
before.visible_commit_seq,
"bead_id=bd-wwqen.3 case=metadata_only_publish_keeps_page_plane_horizon_stale"
);
assert_eq!(
published.publication_write_count(),
writes_before,
"bead_id=bd-wwqen.3 case=metadata_only_publish_still_elides_page_plane_write_count"
);
}
#[test]
fn test_metadata_only_publish_without_page_clear_preserves_newer_visible_snapshot() {
init_publication_test_tracing();
let published = PublishedPagerState::new(8, CommitSeq::new(8), JournalMode::Wal, 0);
let cx = Cx::new();
published.publish_single_connection_metadata_update(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 8,
journal_mode: JournalMode::Wal,
freelist_count: 3,
checkpoint_active: true,
},
false,
);
let before = published.snapshot();
published.publish_single_connection_metadata_update(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(7),
db_size: 4,
journal_mode: JournalMode::Delete,
freelist_count: 0,
checkpoint_active: false,
},
false,
);
let after = published.snapshot();
assert_eq!(
after.visible_commit_seq, before.visible_commit_seq,
"bead_id=bd-wwqen.3 case=stale_metadata_only_publish_must_not_regress_visible_commit_seq"
);
assert_eq!(
after.db_size, before.db_size,
"bead_id=bd-wwqen.3 case=stale_metadata_only_publish_must_not_shrink_newer_db_size"
);
assert_eq!(
after.snapshot_gen, before.snapshot_gen,
"bead_id=bd-wwqen.3 case=stale_metadata_only_publish_must_be_a_noop"
);
assert_eq!(
after.journal_mode, before.journal_mode,
"bead_id=bd-wwqen.3 case=stale_metadata_only_publish_must_not_regress_journal_mode"
);
assert_eq!(
after.freelist_count, before.freelist_count,
"bead_id=bd-wwqen.3 case=stale_metadata_only_publish_must_not_regress_freelist_count"
);
assert_eq!(
after.checkpoint_active, before.checkpoint_active,
"bead_id=bd-wwqen.3 case=stale_metadata_only_publish_must_not_regress_checkpoint_flag"
);
}
#[test]
fn test_publish_commit_draining_write_set_publishes_and_empties_staging() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(4), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_three = PageNumber::new(3).unwrap();
let page_two_data = PageData::from_vec(sample_page(0x22));
let page_three_data = PageData::from_vec(sample_page(0x33));
let mut write_set = HashMap::from([
(page_two, StagedPage::from_page_data(page_two_data.clone())),
(
page_three,
StagedPage::from_page_data(page_three_data.clone()),
),
]);
published.publish_commit_draining_write_set(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(5),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
&mut write_set,
);
assert!(
write_set.is_empty(),
"bead_id=bd-autocommit-publish-drain case=publish_drain_consumes_write_set"
);
assert_eq!(
published.try_get_page(page_two),
Some(page_two_data),
"bead_id=bd-autocommit-publish-drain case=publish_drain_keeps_page_two_visible"
);
assert_eq!(
published.try_get_page(page_three),
Some(page_three_data),
"bead_id=bd-autocommit-publish-drain case=publish_drain_keeps_page_three_visible"
);
}
#[test]
fn test_publish_commit_draining_write_set_single_page_publishes_and_empties_staging() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(4), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_two_data = PageData::from_vec(sample_page(0x24));
let mut write_set =
HashMap::from([(page_two, StagedPage::from_page_data(page_two_data.clone()))]);
published.publish_commit_draining_write_set(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(5),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
&mut write_set,
);
assert!(
write_set.is_empty(),
"bead_id=bd-autocommit-publish-drain case=single_page_publish_drain_consumes_write_set"
);
assert_eq!(
published.try_get_page(page_two),
Some(page_two_data),
"bead_id=bd-autocommit-publish-drain case=single_page_publish_drain_keeps_page_visible"
);
}
#[test]
fn test_publish_commit_staged_pages_publishes_drained_pages() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(4), JournalMode::Wal, 0);
let cx = Cx::new();
let page_two = PageNumber::new(2).unwrap();
let page_three = PageNumber::new(3).unwrap();
let staged_pages = vec![
(
page_two,
StagedPage::from_page_data(PageData::from_vec(sample_page(0x42))),
),
(
page_three,
StagedPage::from_page_data(PageData::from_vec(sample_page(0x53))),
),
];
published.publish_commit_staged_pages(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(5),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
staged_pages,
);
assert_eq!(
published.try_get_page(page_two),
Some(PageData::from_vec(sample_page(0x42))),
"bead_id=bd-autocommit-publish-drain case=publish_staged_pages_keeps_page_two_visible"
);
assert_eq!(
published.try_get_page(page_three),
Some(PageData::from_vec(sample_page(0x53))),
"bead_id=bd-autocommit-publish-drain case=publish_staged_pages_keeps_page_three_visible"
);
}
#[test]
fn test_observed_page_publication_populates_snapshot_plane() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(7), JournalMode::Wal, 0);
let cx = Cx::new();
let page_no = PageNumber::new(2).unwrap();
let page = PageData::from_vec(vec![0xAB; PageSize::DEFAULT.as_usize()]);
let published_page = published.publish_observed_page(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(7),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_no,
page.clone(),
);
assert!(
published_page,
"bead_id={BEAD_ID} case=observed_page_publication_applies"
);
assert_eq!(
published.try_get_page(page_no),
Some(page),
"bead_id={BEAD_ID} case=observed_page_visible"
);
assert_eq!(
published.snapshot().visible_commit_seq,
CommitSeq::new(7),
"bead_id={BEAD_ID} case=observed_page_keeps_commit_seq"
);
}
#[test]
fn test_observed_page_publication_skips_stale_commit_regression() {
init_publication_test_tracing();
let published = PublishedPagerState::new(4, CommitSeq::new(7), JournalMode::Wal, 0);
let cx = Cx::new();
let page_no = PageNumber::new(2).unwrap();
let current_page = PageData::from_vec(vec![0xCC; PageSize::DEFAULT.as_usize()]);
published.publish_insert_single(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(8),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_no,
current_page.clone(),
);
let stale_publish_applied = published.publish_observed_page(
&cx,
PublishedPagerUpdate {
visible_commit_seq: CommitSeq::new(7),
db_size: 4,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
},
page_no,
PageData::from_vec(vec![0x11; PageSize::DEFAULT.as_usize()]),
);
assert!(
!stale_publish_applied,
"bead_id={BEAD_ID} case=stale_observed_page_publication_skipped"
);
assert_eq!(
published.snapshot().visible_commit_seq,
CommitSeq::new(8),
"bead_id={BEAD_ID} case=stale_publication_preserves_commit_seq"
);
assert_eq!(
published.try_get_page(page_no),
Some(current_page),
"bead_id={BEAD_ID} case=stale_publication_preserves_page"
);
}
#[test]
fn test_staged_page_owned_publication_reuses_shared_snapshot() {
let expected = sample_page(0x42);
let staged = StagedPage::from_page_data(PageData::from_vec(expected.clone()));
let first = staged.published_page();
let second = staged.published_page();
assert_eq!(
first.as_bytes(),
expected.as_slice(),
"bead_id=bd-db300.10.6 case=owned_publication_keeps_bytes"
);
assert_eq!(
first.as_bytes().as_ptr(),
second.as_bytes().as_ptr(),
"bead_id=bd-db300.10.6 case=owned_publication_reuses_shared_snapshot"
);
}
#[test]
fn test_staged_page_buffered_publication_reuses_shared_snapshot() {
let mut buf = PageBuf::new(PageSize::DEFAULT);
buf.as_mut_slice().fill(0x53);
let staged = StagedPage::from_buf(buf);
let first = staged.published_page();
let second = staged.published_page();
assert!(
first.as_bytes().iter().all(|byte| *byte == 0x53),
"bead_id=bd-db300.10.6 case=buffered_publication_keeps_bytes"
);
assert_eq!(
first.as_bytes().as_ptr(),
second.as_bytes().as_ptr(),
"bead_id=bd-db300.10.6 case=buffered_publication_reuses_shared_snapshot"
);
}
#[test]
fn test_staged_page_unpublishes_before_mutation() {
let pool = PageBufPool::new(PageSize::DEFAULT, 2);
let mut original = sample_page(0x42);
original[24] = 0x11;
let staged = StagedPage::from_page_data(PageData::from_vec(original));
let old_snapshot = staged.published_page();
let mut staged = staged.into_unpublished_for_mutation(&pool);
staged.as_page_bytes_mut()[24] = 0x99;
let final_page = staged.into_published_page();
assert_eq!(
old_snapshot.as_bytes()[24],
0x11,
"bead_id=bd-page1-staged-invariant case=old_snapshot_keeps_original_byte"
);
assert_eq!(
final_page.as_bytes()[24],
0x99,
"bead_id=bd-page1-staged-invariant case=final_publication_uses_mutated_byte"
);
}
#[test]
fn test_write_page_data_short_buffer_is_zero_filled_to_page_size() {
let (pager, _path) = test_pager();
let cx = Cx::new();
let page_size = PageSize::DEFAULT.as_usize();
let page_no = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
txn.write_page_data(&cx, page_no, PageData::from_vec(vec![0xAB; 32]))
.unwrap();
txn.commit(&cx).unwrap();
page_no
};
let reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page = reader.get_page(&cx, page_no).unwrap();
assert_eq!(
page.len(),
page_size,
"bead_id={BEAD_ID} case=short_owned_page_write_preserves_page_size"
);
assert!(
page.as_bytes()[..32].iter().all(|byte| *byte == 0xAB),
"bead_id={BEAD_ID} case=short_owned_page_write_keeps_prefix"
);
assert!(
page.as_bytes()[32..].iter().all(|byte| *byte == 0),
"bead_id={BEAD_ID} case=short_owned_page_write_zero_fills_tail"
);
}
#[test]
fn test_external_refresh_clears_stale_published_pages() {
init_publication_test_tracing();
let vfs = MemoryVfs::new();
let path = PathBuf::from("/published_refresh.db");
let cx = Cx::new();
let ps = PageSize::DEFAULT.as_usize();
let pager1 = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
let pager2 = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let p = {
let mut txn = pager1.begin(&cx, TransactionMode::Immediate).unwrap();
let p = txn.allocate_page(&cx).unwrap();
txn.write_page(&cx, p, &vec![0x11; ps]).unwrap();
txn.commit(&cx).unwrap();
p
};
let reader = pager1.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(reader.get_page(&cx, p).unwrap().as_ref()[0], 0x11);
drop(reader);
let published_before = pager1.published_snapshot();
assert!(
published_before.page_set_size > 0,
"bead_id={BEAD_ID} case=publication_plane_populated_before_refresh"
);
let mut txn = pager2.begin(&cx, TransactionMode::Immediate).unwrap();
txn.write_page(&cx, p, &vec![0x22; ps]).unwrap();
txn.commit(&cx).unwrap();
let refreshed_reader = pager1.begin(&cx, TransactionMode::ReadOnly).unwrap();
let published_after_refresh = pager1.published_snapshot();
assert!(
published_after_refresh.snapshot_gen > published_before.snapshot_gen,
"bead_id={BEAD_ID} case=publication_gen_advances_on_refresh"
);
assert_eq!(
published_after_refresh.visible_commit_seq,
pager2.published_snapshot().visible_commit_seq,
"bead_id={BEAD_ID} case=publication_visible_seq_tracks_external_commit"
);
assert_eq!(
published_after_refresh.page_set_size, 0,
"bead_id={BEAD_ID} case=publication_clears_stale_pages_on_refresh"
);
assert_eq!(
refreshed_reader.get_page(&cx, p).unwrap().as_ref()[0],
0x22,
"bead_id={BEAD_ID} case=publication_refresh_reads_latest_committed_page"
);
}
#[test]
fn test_stale_main_header_recovery_ignores_uncommitted_wal_page1_tail() {
let cx = Cx::new();
let vfs = MemoryVfs::new();
let db_path = PathBuf::from("/stale-main-header-uncommitted-tail.db");
let wal_path = PathBuf::from("/stale-main-header-uncommitted-tail.db-wal");
let page_size = PageSize::DEFAULT;
let valid_header = DatabaseHeader {
page_size,
page_count: 1,
..DatabaseHeader::default()
};
let mut committed_page1 = vec![0_u8; page_size.as_usize()];
committed_page1[..DATABASE_HEADER_SIZE]
.copy_from_slice(&valid_header.to_bytes().expect("valid page-1 header"));
let mut stale_header_bytes = valid_header.to_bytes().expect("base header bytes");
stale_header_bytes[44..48].fill(0);
let stale_error = DatabaseHeader::from_bytes(&stale_header_bytes)
.expect_err("schema format 0 must be treated as a stale main-file header");
let mut uncommitted_page1 = committed_page1.clone();
uncommitted_page1[..DATABASE_HEADER_SIZE].copy_from_slice(&stale_header_bytes);
let open_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
let (file, _) = vfs.open(&cx, Some(&wal_path), open_flags).unwrap();
let mut wal = fsqlite_wal::WalFile::create(
&cx,
file,
page_size.get(),
0,
fsqlite_wal::WalSalts::default(),
)
.unwrap();
wal.append_frame(&cx, PageNumber::ONE.get(), &committed_page1, 1)
.expect("append committed page-1 frame");
wal.append_frame(&cx, PageNumber::ONE.get(), &uncommitted_page1, 0)
.expect("append uncommitted page-1 tail");
wal.close(&cx).unwrap();
let recoverable = stale_main_header_can_be_recovered_from_live_wal(
&cx,
&vfs,
&db_path,
&stale_header_bytes,
&stale_error,
false,
)
.expect("probe stale-header recovery");
assert!(
recoverable,
"recovery must use the committed WAL horizon, not an uncommitted tail frame"
);
}
#[test]
fn test_stale_main_header_recovery_never_falls_back_to_readonly_wal_probe() {
let cx = Cx::new();
let vfs = WalReadonlyFallbackProbeVfs::new();
let db_path = PathBuf::from("/stale-main-header-readonly-fallback.db");
let wal_path = PathBuf::from("/stale-main-header-readonly-fallback.db-wal");
let page_size = PageSize::DEFAULT;
let valid_header = DatabaseHeader {
page_size,
page_count: 1,
..DatabaseHeader::default()
};
let mut stale_header_bytes = valid_header.to_bytes().expect("base header bytes");
stale_header_bytes[44..48].fill(0);
let stale_error = DatabaseHeader::from_bytes(&stale_header_bytes)
.expect_err("schema format 0 must be treated as a stale main-file header");
let open_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
let (mut wal_file, _) = vfs.inner.open(&cx, Some(&wal_path), open_flags).unwrap();
wal_file.close(&cx).unwrap();
let recoverable = stale_main_header_can_be_recovered_from_live_wal(
&cx,
&vfs,
&db_path,
&stale_header_bytes,
&stale_error,
false,
)
.expect("probe stale-header recovery");
assert!(
!recoverable,
"readwrite probe failure must stop recovery instead of retrying READONLY"
);
assert!(
!vfs.readonly_wal_open_attempted(),
"bead_id={BEAD_ID} case=stale_header_recovery_must_not_probe_wal_readonly"
);
}
#[test]
fn test_readonly_stale_main_header_recovery_allows_readonly_wal_probe() {
let cx = Cx::new();
let vfs = WalReadonlyFallbackProbeVfs::new();
let db_path = PathBuf::from("/readonly-stale-main-header-fallback.db");
let wal_path = PathBuf::from("/readonly-stale-main-header-fallback.db-wal");
let page_size = PageSize::DEFAULT;
let valid_header = DatabaseHeader {
page_size,
page_count: 1,
..DatabaseHeader::default()
};
let mut committed_page1 = vec![0_u8; page_size.as_usize()];
committed_page1[..DATABASE_HEADER_SIZE]
.copy_from_slice(&valid_header.to_bytes().expect("valid page-1 header"));
let mut stale_header_bytes = valid_header.to_bytes().expect("base header bytes");
stale_header_bytes[44..48].fill(0);
let stale_error = DatabaseHeader::from_bytes(&stale_header_bytes)
.expect_err("schema format 0 must be treated as a stale main-file header");
let open_flags = VfsOpenFlags::READWRITE | VfsOpenFlags::CREATE | VfsOpenFlags::WAL;
let (file, _) = vfs.inner.open(&cx, Some(&wal_path), open_flags).unwrap();
let mut wal = fsqlite_wal::WalFile::create(
&cx,
file,
page_size.get(),
0,
fsqlite_wal::WalSalts::default(),
)
.unwrap();
wal.append_frame(&cx, PageNumber::ONE.get(), &committed_page1, 1)
.expect("append committed page-1 frame");
wal.close(&cx).unwrap();
let recoverable = stale_main_header_can_be_recovered_from_live_wal(
&cx,
&vfs,
&db_path,
&stale_header_bytes,
&stale_error,
true,
)
.expect("probe stale-header recovery");
assert!(
recoverable,
"read-only startup may validate a live WAL through a READONLY sidecar handle"
);
assert!(
vfs.readonly_wal_open_attempted(),
"read-only recovery should fall back to a READONLY WAL probe"
);
}
#[test]
fn test_set_wal_backend_owned_returns_backend_when_install_is_rejected() {
let (pager, _) = test_pager();
let dropped = Arc::new(Mutex::new(false));
{
let mut inner = pager
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.checkpoint_active = true;
}
let backend = DropAwareWalBackend {
dropped: Arc::clone(&dropped),
};
let (err, backend) = pager
.set_wal_backend_owned(backend)
.expect_err("checkpoint-active pager must reject WAL backend install");
assert!(
matches!(err, FrankenError::Busy),
"unexpected error: {err:?}"
);
assert!(
!*dropped
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
"rejected install must return the backend instead of dropping it"
);
assert!(
!has_wal_backend(&pager.wal_backend).unwrap(),
"failed install must not publish a backend"
);
drop(backend);
assert!(
*dropped
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
"caller should own backend cleanup after a rejected install"
);
}
#[test]
fn test_set_journal_mode_wal_requires_backend_even_when_inner_mode_is_wal() {
let cx = Cx::new();
let (pager, _) = test_pager();
{
let mut inner = pager
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.journal_mode = JournalMode::Wal;
}
let err = pager
.set_journal_mode(&cx, JournalMode::Wal)
.expect_err("WAL mode must require an installed WAL backend");
assert!(
matches!(err, FrankenError::Unsupported),
"unexpected error: {err:?}"
);
assert!(
!has_wal_backend(&pager.wal_backend).unwrap(),
"failed WAL mode confirmation must not publish a backend"
);
}
#[test]
fn test_set_journal_mode_same_rollback_mode_allowed_during_active_transaction() {
let cx = Cx::new();
let (pager, _) = test_pager();
let mut txn = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
assert_eq!(
pager.set_journal_mode(&cx, JournalMode::Delete).unwrap(),
JournalMode::Delete,
"idempotent rollback journal-mode confirmation should not require a mode switch"
);
txn.rollback(&cx).unwrap();
}
fn read_all_vfs_bytes<V: Vfs>(vfs: &V, cx: &Cx, path: &Path) -> Vec<u8> {
let flags = VfsOpenFlags::MAIN_DB | VfsOpenFlags::READWRITE;
let (mut file, _) = vfs.open(cx, Some(path), flags).unwrap();
let size = usize::try_from(file.file_size(cx).unwrap()).unwrap();
let mut out = vec![0_u8; size];
let read = file.read(cx, &mut out, 0).unwrap();
assert_eq!(read, size);
file.close(cx).unwrap();
out
}
#[test]
fn test_copy_database_to_copies_main_db_via_vfs() {
let cx = Cx::new();
let vfs = MemoryVfs::new();
let source_path = PathBuf::from("/copy_source.db");
let target_path = PathBuf::from("/copy_target.db");
let pager = SimplePager::open(vfs.clone(), &source_path, PageSize::DEFAULT).unwrap();
let page_size = PageSize::DEFAULT.as_usize();
let page_no = {
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
let mut page = vec![0xA5; page_size];
page[0] = 0x5A;
page[page_size - 1] = 0xC3;
txn.write_page(&cx, page_no, &page).unwrap();
txn.commit(&cx).unwrap();
page_no
};
pager.copy_database_to(&cx, &target_path).unwrap();
let source_bytes = read_all_vfs_bytes(&vfs, &cx, &source_path);
let target_bytes = read_all_vfs_bytes(&vfs, &cx, &target_path);
assert_eq!(
target_bytes, source_bytes,
"bead_id={BEAD_ID} case=copy_database_to_byte_identical_copy"
);
let copied = SimplePager::open(vfs, &target_path, PageSize::DEFAULT).unwrap();
let reader = copied.begin(&cx, TransactionMode::ReadOnly).unwrap();
let page = reader.get_page(&cx, page_no).unwrap();
assert_eq!(
page.as_ref()[0],
0x5A,
"bead_id={BEAD_ID} case=copy_database_to_reopen_reads_committed_page"
);
assert_eq!(
page.as_ref()[page_size - 1],
0xC3,
"bead_id={BEAD_ID} case=copy_database_to_preserves_page_tail"
);
}
#[test]
fn test_copy_database_to_rejects_existing_target() {
let cx = Cx::new();
let vfs = MemoryVfs::new();
let source_path = PathBuf::from("/copy_existing_source.db");
let target_path = PathBuf::from("/copy_existing_target.db");
let pager = SimplePager::open(vfs.clone(), &source_path, PageSize::DEFAULT).unwrap();
let _target = SimplePager::open(vfs, &target_path, PageSize::DEFAULT).unwrap();
let err = pager.copy_database_to(&cx, &target_path).unwrap_err();
assert!(
matches!(err, FrankenError::CannotOpen { .. }),
"bead_id={BEAD_ID} case=copy_database_to_existing_target_err={err:?}"
);
}
#[test]
fn test_copy_database_to_requires_quiescent_pager() {
let cx = Cx::new();
let vfs = MemoryVfs::new();
let source_path = PathBuf::from("/copy_busy_source.db");
let target_path = PathBuf::from("/copy_busy_target.db");
let pager = SimplePager::open(vfs, &source_path, PageSize::DEFAULT).unwrap();
let _reader = pager.begin(&cx, TransactionMode::ReadOnly).unwrap();
let err = pager.copy_database_to(&cx, &target_path).unwrap_err();
assert!(
matches!(err, FrankenError::Busy),
"bead_id={BEAD_ID} case=copy_database_to_rejects_active_transactions err={err:?}"
);
}
#[test]
fn test_published_snapshot_retries_during_inflight_publication() {
init_publication_test_tracing();
let published = Arc::new(PublishedPagerState::new(
1,
CommitSeq::new(1),
JournalMode::Delete,
0,
));
published.sequence.store(3, AtomicOrdering::Release);
let reader_plane = Arc::clone(&published);
let handle = std::thread::spawn(move || reader_plane.snapshot());
for _ in 0..10_000 {
if published.read_retry_count() > 0 {
break;
}
std::thread::yield_now();
}
assert!(
published.read_retry_count() > 0,
"bead_id={BEAD_ID} case=publication_retry_counter_increments"
);
published
.visible_commit_seq
.store(2, AtomicOrdering::Release);
published.db_size.store(2, AtomicOrdering::Release);
published.journal_mode.store(
encode_journal_mode(JournalMode::Wal),
AtomicOrdering::Release,
);
published.freelist_count.store(1, AtomicOrdering::Release);
published
.checkpoint_active
.store(true, AtomicOrdering::Release);
published.page_set_size.store(0, AtomicOrdering::Release);
published.sequence.store(4, AtomicOrdering::Release);
let snapshot = handle.join().unwrap();
assert_eq!(
snapshot.snapshot_gen, 4,
"bead_id={BEAD_ID} case=publication_retry_returns_new_snapshot"
);
assert_eq!(
snapshot.visible_commit_seq,
CommitSeq::new(2),
"bead_id={BEAD_ID} case=publication_retry_visible_commit_seq"
);
assert_eq!(
snapshot.freelist_count, 1,
"bead_id={BEAD_ID} case=publication_retry_freelist_count"
);
assert!(
snapshot.checkpoint_active,
"bead_id={BEAD_ID} case=publication_retry_checkpoint_flag"
);
}
#[test]
fn test_published_sequence_waiters_wake_on_targeted_transitions() {
init_publication_test_tracing();
let published = Arc::new(PublishedPagerState::new(
1,
CommitSeq::new(1),
JournalMode::Delete,
0,
));
published.sequence.store(4, AtomicOrdering::Release);
let begin_plane = Arc::clone(&published);
let (begin_ready_tx, begin_ready_rx) = std::sync::mpsc::channel();
let (begin_done_tx, begin_done_rx) = std::sync::mpsc::channel();
let begin_waiter = std::thread::spawn(move || {
begin_ready_tx
.send(())
.expect("begin waiter should signal readiness");
begin_plane.wait_for_sequence_change(4, Duration::from_secs(1));
begin_done_tx
.send(())
.expect("begin waiter should signal completion");
});
begin_ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("begin waiter should start");
for _ in 0..10_000 {
if published.sequence_waiters.has_slot(4) {
break;
}
std::thread::yield_now();
}
assert!(
published.sequence_waiters.has_slot(4),
"bead_id={BEAD_ID} case=publication_begin_waiter_registers_targeted_slot"
);
assert!(
begin_done_rx
.recv_timeout(Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=publication_begin_waiter_stays_parked_until_targeted_signal"
);
let begin_sequence = published.sequence.fetch_add(1, AtomicOrdering::AcqRel);
assert_eq!(begin_sequence, 4);
published.signal_sequence_waiters(begin_sequence, "test_begin");
begin_done_rx
.recv_timeout(Duration::from_millis(100))
.expect("publish-begin signal should wake matching waiter");
begin_waiter.join().unwrap();
let complete_plane = Arc::clone(&published);
let (complete_ready_tx, complete_ready_rx) = std::sync::mpsc::channel();
let (complete_done_tx, complete_done_rx) = std::sync::mpsc::channel();
let complete_waiter = std::thread::spawn(move || {
complete_ready_tx
.send(())
.expect("complete waiter should signal readiness");
complete_plane.wait_for_sequence_change(5, Duration::from_secs(1));
complete_done_tx
.send(())
.expect("complete waiter should signal completion");
});
complete_ready_rx
.recv_timeout(Duration::from_secs(1))
.expect("complete waiter should start");
for _ in 0..10_000 {
if published.sequence_waiters.has_slot(5) {
break;
}
std::thread::yield_now();
}
assert!(
published.sequence_waiters.has_slot(5),
"bead_id={BEAD_ID} case=publication_complete_waiter_registers_targeted_slot"
);
assert!(
complete_done_rx
.recv_timeout(Duration::from_millis(20))
.is_err(),
"bead_id={BEAD_ID} case=publication_complete_waiter_stays_parked_until_targeted_signal"
);
let complete_sequence = published.sequence.fetch_add(1, AtomicOrdering::AcqRel);
assert_eq!(complete_sequence, 5);
published.signal_sequence_waiters(complete_sequence, "test_complete");
complete_done_rx
.recv_timeout(Duration::from_millis(100))
.expect("publish-complete signal should wake matching waiter");
complete_waiter.join().unwrap();
}
fn run_parallel_counter_benchmark<F>(
thread_count: usize,
increments_per_thread: usize,
increment: F,
) -> u64
where
F: Fn() + Send + Sync + 'static,
{
let start_barrier = Arc::new(std::sync::Barrier::new(thread_count + 1));
let increment = Arc::new(increment);
let handles: Vec<_> = (0..thread_count)
.map(|_| {
let start_barrier = Arc::clone(&start_barrier);
let increment = Arc::clone(&increment);
std::thread::spawn(move || {
start_barrier.wait();
for _ in 0..increments_per_thread {
increment();
}
})
})
.collect();
start_barrier.wait();
let started = Instant::now();
for handle in handles {
handle.join().expect("counter worker should finish");
}
u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX)
}
#[test]
fn test_published_counter_striping_tracks_parallel_increments() {
let counter = Arc::new(StripedCounter64::new());
let thread_count = 8;
let increments_per_thread = 5_000;
let expected_total =
u64::try_from(thread_count * increments_per_thread).unwrap_or(u64::MAX);
let counter_for_bench = Arc::clone(&counter);
let elapsed_ns =
run_parallel_counter_benchmark(thread_count, increments_per_thread, move || {
counter_for_bench.increment();
});
assert!(
elapsed_ns > 0,
"bead_id={BEAD_ID} case=publication_counter_parallel_elapsed"
);
assert_eq!(
counter.load(),
expected_total,
"bead_id={BEAD_ID} case=publication_counter_parallel_total"
);
}
#[test]
#[ignore = "manual perf evidence for bd-db300.2.3.2"]
fn bench_bd_db300_2_3_2_publication_counter_striping() {
#[derive(Debug)]
struct BaselineCounter(AtomicU64);
impl BaselineCounter {
fn increment(&self) {
self.0.fetch_add(1, AtomicOrdering::Relaxed);
}
fn load(&self) -> u64 {
self.0.load(AtomicOrdering::Acquire)
}
}
let thread_count = std::thread::available_parallelism()
.map_or(4, |parallelism| parallelism.get().clamp(2, 16));
let increments_per_thread = 200_000;
let expected_total =
u64::try_from(thread_count * increments_per_thread).unwrap_or(u64::MAX);
let baseline = Arc::new(BaselineCounter(AtomicU64::new(0)));
let baseline_counter = Arc::clone(&baseline);
let baseline_ns =
run_parallel_counter_benchmark(thread_count, increments_per_thread, move || {
baseline_counter.increment();
});
assert_eq!(
baseline.load(),
expected_total,
"bead_id={BEAD_ID} case=publication_counter_baseline_total"
);
let striped = Arc::new(StripedCounter64::new());
let striped_counter = Arc::clone(&striped);
let striped_ns =
run_parallel_counter_benchmark(thread_count, increments_per_thread, move || {
striped_counter.increment();
});
assert_eq!(
striped.load(),
expected_total,
"bead_id={BEAD_ID} case=publication_counter_striped_total"
);
let speedup_milli = if striped_ns == 0 {
0_u64
} else {
u64::try_from((u128::from(baseline_ns)).saturating_mul(1_000) / u128::from(striped_ns))
.unwrap_or(u64::MAX)
};
println!("BEGIN_BD_DB300_2_3_2_REPORT");
println!(
"{{\"threads\":{thread_count},\"increments_per_thread\":{increments_per_thread},\"baseline_ns\":{baseline_ns},\"striped_ns\":{striped_ns},\"speedup_milli\":{speedup_milli}}}"
);
println!("END_BD_DB300_2_3_2_REPORT");
}
#[test]
fn test_read_page_from_wal_backend_falls_back_to_write_lock_when_pinned_reads_unsupported() {
let wal_backend: SharedWalBackend = new_shared_wal_backend();
let cx = Cx::new();
let page_no = PageNumber::new(1).unwrap();
let result = read_page_from_wal_backend(&wal_backend, &cx, page_no);
assert!(
result.is_err(),
"bead_id=bd-db300.3.8.7 read_page_from_wal_backend should error without backend"
);
let (mock, _frames, _begin, _batch) = MockWalBackend::new();
*wal_backend.write().unwrap() = Some(Box::new(mock));
let result = read_page_from_wal_backend(&wal_backend, &cx, page_no);
assert!(
result.is_ok(),
"bead_id=bd-db300.3.8.7 fallback to write-lock read_page should succeed"
);
assert_eq!(
result.unwrap(),
None,
"bead_id=bd-db300.3.8.7 unwritten page should return None"
);
}
#[test]
fn test_read_page_from_wal_backend_uses_pinned_read_without_write_fallback() {
use crate::traits::WalBackend;
struct PinnedReadBackend {
pinned_calls: Arc<Mutex<usize>>,
fallback_calls: Arc<Mutex<usize>>,
response: Vec<u8>,
}
impl WalBackend for PinnedReadBackend {
fn begin_transaction(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
*self.fallback_calls.lock().unwrap() += 1;
Ok(Some(vec![0xEE]))
}
fn read_page_pinned(
&self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
*self.pinned_calls.lock().unwrap() += 1;
Ok(Some(self.response.clone()))
}
fn supports_pinned_reads(&self) -> bool {
true
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
0
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
let pinned_calls = Arc::new(Mutex::new(0_usize));
let fallback_calls = Arc::new(Mutex::new(0_usize));
let expected = vec![0xAB, 0xCD, 0xEF];
let wal_backend: SharedWalBackend = new_shared_wal_backend();
*wal_backend.write().unwrap() = Some(Box::new(PinnedReadBackend {
pinned_calls: Arc::clone(&pinned_calls),
fallback_calls: Arc::clone(&fallback_calls),
response: expected.clone(),
}));
let cx = Cx::new();
let page_no = PageNumber::new(7).unwrap();
let result = read_page_from_wal_backend(&wal_backend, &cx, page_no).unwrap();
assert_eq!(
result,
Some(expected),
"bead_id=bd-db300.3.8.7 case=wal_read_scope_pinned_read_returns_data_without_fallback"
);
assert_eq!(
*pinned_calls.lock().unwrap(),
1,
"bead_id=bd-db300.3.8.7 case=wal_read_scope_pinned_read_call_count"
);
assert_eq!(
*fallback_calls.lock().unwrap(),
0,
"bead_id=bd-db300.3.8.7 case=wal_read_scope_pinned_read_must_not_take_write_lock_fallback"
);
}
#[test]
fn test_read_page_pinned_error_does_not_fall_back() {
use crate::traits::WalBackend;
struct CorruptPinnedReadBackend;
impl WalBackend for CorruptPinnedReadBackend {
fn begin_transaction(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
panic!(
"bead_id=bd-db300.3.8.7 MUST NOT fall back to read_page \
when read_page_pinned returns a real error"
);
}
fn read_page_pinned(
&self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Err(fsqlite_error::FrankenError::WalCorrupt {
detail: "simulated corruption in pinned read".to_owned(),
})
}
fn supports_pinned_reads(&self) -> bool {
true
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
0
}
fn checkpoint(
&mut self,
_cx: &Cx,
_mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: _mode,
effective_mode: _mode,
})
}
}
let wal_backend: SharedWalBackend = new_shared_wal_backend();
*wal_backend.write().unwrap() = Some(Box::new(CorruptPinnedReadBackend));
let cx = Cx::new();
let page_no = PageNumber::new(1).unwrap();
let result = read_page_from_wal_backend(&wal_backend, &cx, page_no);
assert!(
result.is_err(),
"bead_id=bd-db300.3.8.7 real read_page_pinned error must propagate, not fall back"
);
let err = result.unwrap_err();
assert!(
format!("{err}").contains("corruption"),
"bead_id=bd-db300.3.8.7 error should be the corruption error, got: {err}"
);
}
#[test]
fn test_fused_batch_assembly_preserves_order_and_db_size() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
let batches = vec![
TransactionFrameBatch::new(vec![
FrameSubmission {
page_number: 2,
page_data: vec![0xAA; 4096],
db_size_if_commit: 0, },
FrameSubmission {
page_number: 3,
page_data: vec![0xBB; 4096],
db_size_if_commit: 10, },
]),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 5,
page_data: vec![0xCC; 4096],
db_size_if_commit: 12, }]),
TransactionFrameBatch::new(vec![
FrameSubmission {
page_number: 7,
page_data: vec![0xDD; 4096],
db_size_if_commit: 0, },
FrameSubmission {
page_number: 8,
page_data: vec![0xEE; 4096],
db_size_if_commit: 8, },
]),
];
let current_db_size: u32 = 5;
let (frame_refs, final_db_size) = flatten_group_commit_batches(current_db_size, &batches);
let page_numbers: Vec<u32> = frame_refs.iter().map(|f| f.page_number).collect();
assert_eq!(
page_numbers,
vec![2, 3, 5, 7, 8],
"bd-db300.3.8.6: fused assembly must preserve cross-batch frame order"
);
assert_eq!(frame_refs.len(), 5, "should flatten all 5 frames");
assert_eq!(
final_db_size, 12,
"bd-db300.3.8.6: final_db_size must be max commit size across group"
);
let commit_sizes: Vec<u32> = frame_refs
.iter()
.map(|frame| frame.db_size_if_commit)
.collect();
assert_eq!(
commit_sizes,
vec![0, 0, 0, 0, 12],
"only the final frame should carry the consolidated commit db_size"
);
assert!(
frame_refs.capacity() >= 5,
"Vec should have been pre-sized to avoid realloc"
);
}
#[test]
fn test_group_commit_page_one_headers_promote_to_consolidated_db_size() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
fn page_one_with_page_count(fill: u8, page_count: u32) -> Vec<u8> {
let mut page = vec![fill; 4096];
page[28..32].copy_from_slice(&page_count.to_be_bytes());
page
}
fn page_count(page: &[u8]) -> u32 {
let mut bytes = [0_u8; 4];
bytes.copy_from_slice(&page[28..32]);
u32::from_be_bytes(bytes)
}
let mut batches = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: page_one_with_page_count(0xAA, 12),
db_size_if_commit: 12,
}]),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: page_one_with_page_count(0xBA, 8),
db_size_if_commit: 8,
}]),
];
let final_db_size = group_commit_final_db_size(5, &batches);
assert_eq!(
final_db_size, 12,
"group final db_size should be the max commit marker"
);
assert!(
promote_group_commit_page_one_headers(&mut batches, final_db_size),
"a smaller trailing Page 1 header must be promoted before WAL append"
);
assert_eq!(page_count(&batches[0].frames[0].page_data), 12);
assert_eq!(page_count(&batches[1].frames[0].page_data), 12);
let (frame_refs, flattened_db_size) = flatten_group_commit_batches(5, &batches);
assert_eq!(flattened_db_size, 12);
assert_eq!(
frame_refs
.iter()
.map(|frame| frame.db_size_if_commit)
.collect::<Vec<_>>(),
vec![0, 12],
"the final WAL commit marker and promoted Page 1 header must agree on group db_size"
);
}
#[test]
fn test_group_commit_page_one_header_promotion_never_shrinks_existing_count() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
let mut page = vec![0xCC; 4096];
page[28..32].copy_from_slice(&20_u32.to_be_bytes());
let mut batches = vec![TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: page,
db_size_if_commit: 12,
}])];
assert!(
!promote_group_commit_page_one_headers(&mut batches, 12),
"Page 1 promotion should not shrink a header that already advertises a larger database"
);
let mut bytes = [0_u8; 4];
bytes.copy_from_slice(&batches[0].frames[0].page_data[28..32]);
assert_eq!(u32::from_be_bytes(bytes), 20);
}
#[test]
fn test_group_commit_conflict_detection_reports_only_cross_batch_page_overlaps() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
let batches = vec![
TransactionFrameBatch::new(vec![
FrameSubmission {
page_number: 1,
page_data: vec![0xA0; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 2,
page_data: vec![0xAA; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 2,
page_data: vec![0xAB; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 3,
page_data: vec![0xAC; 4096],
db_size_if_commit: 10,
},
]),
TransactionFrameBatch::new(vec![
FrameSubmission {
page_number: 1,
page_data: vec![0xB0; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 4,
page_data: vec![0xBA; 4096],
db_size_if_commit: 11,
},
]),
TransactionFrameBatch::new(vec![
FrameSubmission {
page_number: 1,
page_data: vec![0xC0; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 3,
page_data: vec![0xCA; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 4,
page_data: vec![0xCB; 4096],
db_size_if_commit: 12,
},
]),
];
assert_eq!(
conflicting_pages_across_group_commit_batches(&batches),
vec![3, 4],
"only pages written by multiple distinct transaction batches should force an epoch retry"
);
}
#[test]
fn test_group_commit_conflict_detection_uses_semantic_conflict_metadata() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
let batches = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 2,
page_data: vec![0xAA; 4096],
db_size_if_commit: 10,
}])
.with_conflict_snapshot(vec![2, 50, 60], None),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 3,
page_data: vec![0xBA; 4096],
db_size_if_commit: 11,
}])
.with_conflict_snapshot(vec![50], None),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 60,
page_data: vec![0xCA; 4096],
db_size_if_commit: 12,
}]),
];
assert_eq!(
conflicting_pages_across_group_commit_batches(&batches),
vec![50, 60],
"group-commit overlap checks must include conflict-only pages such as pending freed pages, not just emitted WAL frames"
);
}
#[test]
fn test_group_commit_conflict_detection_keeps_synthetic_page_one_batching_safe() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
let synthetic_page_one_batches = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: vec![0xAA; 4096],
db_size_if_commit: 10,
}]),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: vec![0xBA; 4096],
db_size_if_commit: 11,
}]),
];
assert_eq!(
conflicting_pages_across_group_commit_batches(&synthetic_page_one_batches),
Vec::<u32>::new(),
"synthetic Page 1 header/count frames remain batchable when no transaction explicitly rewrote Page 1"
);
let explicit_page_one_with_synthetic = vec![
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: vec![0xCA; 4096],
db_size_if_commit: 10,
}])
.with_conflict_snapshot(vec![1], None),
TransactionFrameBatch::new(vec![FrameSubmission {
page_number: 1,
page_data: vec![0xDA; 4096],
db_size_if_commit: 11,
}]),
];
assert_eq!(
conflicting_pages_across_group_commit_batches(&explicit_page_one_with_synthetic),
vec![1],
"an explicit Page 1 rewrite must not batch with another transaction's synthetic Page 1 frame because the synthetic bytes could otherwise overwrite schema/header changes"
);
}
#[test]
fn test_fused_batch_assembly_all_zero_db_size() {
use fsqlite_wal::group_commit::{FrameSubmission, TransactionFrameBatch};
let batches = vec![TransactionFrameBatch::new(vec![
FrameSubmission {
page_number: 2,
page_data: vec![0; 4096],
db_size_if_commit: 0,
},
FrameSubmission {
page_number: 3,
page_data: vec![0; 4096],
db_size_if_commit: 0,
},
])];
let current_db_size: u32 = 7;
let (frame_refs, final_db_size) = flatten_group_commit_batches(current_db_size, &batches);
assert_eq!(
final_db_size, 7,
"bd-db300.3.8.6: all-zero db_size_if_commit must preserve current_db_size"
);
assert!(
frame_refs.iter().all(|frame| frame.db_size_if_commit == 0),
"all-zero inputs must stay non-commit after flattening"
);
}
#[test]
fn test_same_page_write_steals_existing_buffer() {
let (pager, _) = test_pager();
let cx = Cx::new();
let page_size = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
let initial = vec![0x11_u8; page_size];
txn.write_page(&cx, page_no, &initial).unwrap();
let pool_before = txn.pool.metrics_snapshot();
let steals_before = staged_page_overwrite_steals_total();
let iterations = 10_000_u64;
for i in 0..iterations {
let mut data = vec![0_u8; page_size];
data[0] = u8::try_from(i & 0xFF).expect("mask fits u8");
data[1] = u8::try_from((i >> 8) & 0xFF).expect("mask fits u8");
txn.write_page(&cx, page_no, &data).unwrap();
}
let pool_after = txn.pool.metrics_snapshot();
let steals_after = staged_page_overwrite_steals_total();
assert!(
steals_after.saturating_sub(steals_before) >= iterations,
"bead_id=bd-steal-same-page case=all_repeated_writes_stole \
steals_before={steals_before} steals_after={steals_after} \
iterations={iterations}"
);
assert_eq!(
pool_after.page_buffer_pool_misses, pool_before.page_buffer_pool_misses,
"bead_id=bd-steal-same-page case=no_new_pool_misses \
before={} after={}",
pool_before.page_buffer_pool_misses, pool_after.page_buffer_pool_misses
);
assert_eq!(
pool_after.page_buffer_pool_hits, pool_before.page_buffer_pool_hits,
"bead_id=bd-steal-same-page case=no_new_pool_hits \
before={} after={}",
pool_before.page_buffer_pool_hits, pool_after.page_buffer_pool_hits
);
let final_mask = (iterations - 1) & 0xFF;
let read_back = txn.get_page(&cx, page_no).unwrap();
assert_eq!(
u64::from(read_back.as_ref()[0]),
final_mask,
"bead_id=bd-steal-same-page case=last_write_wins_byte0"
);
}
#[test]
fn test_same_page_write_after_publish_does_not_steal() {
let (pager, _) = test_pager();
let cx = Cx::new();
let page_size = PageSize::DEFAULT.as_usize();
let mut txn = pager.begin(&cx, TransactionMode::Immediate).unwrap();
let page_no = txn.allocate_page(&cx).unwrap();
let initial = vec![0xA5_u8; page_size];
txn.write_page(&cx, page_no, &initial).unwrap();
let snapshot = txn.get_page(&cx, page_no).unwrap();
assert_eq!(
snapshot.as_ref()[0],
0xA5,
"bead_id=bd-steal-same-page case=pre_publish_read"
);
let pool_before = txn.pool.metrics_snapshot();
let updated = vec![0x5A_u8; page_size];
txn.write_page(&cx, page_no, &updated).unwrap();
let pool_after = txn.pool.metrics_snapshot();
let new_hits = pool_after.page_buffer_pool_hits - pool_before.page_buffer_pool_hits;
let new_misses = pool_after.page_buffer_pool_misses - pool_before.page_buffer_pool_misses;
assert!(
new_hits + new_misses >= 1,
"bead_id=bd-steal-same-page case=published_forbids_steal \
new_hits={new_hits} new_misses={new_misses}"
);
let fresh = txn.get_page(&cx, page_no).unwrap();
assert_eq!(
fresh.as_ref()[0],
0x5A,
"bead_id=bd-steal-same-page case=post_write_reads_new"
);
assert_eq!(
snapshot.as_ref()[0],
0xA5,
"bead_id=bd-steal-same-page case=captured_snapshot_unchanged"
);
}
#[test]
fn test_split_inner_lock_allows_parallel_prepare() {
use std::time::Duration;
const BEAD: &str = "bd-3wop3.8";
struct SlowWalBackend {
append_calls: SharedCounter,
io_delay: Duration,
}
impl crate::traits::WalBackend for SlowWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
*self.append_calls.lock().unwrap() += 1;
std::thread::sleep(self.io_delay);
let _ = frames;
Ok(())
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
if frames.is_empty() {
return Ok(None);
}
let frame_size =
fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE + frames[0].page_data.len();
let mut frame_bytes = Vec::with_capacity(frame_size * frames.len());
let mut frame_metas = Vec::with_capacity(frames.len());
let mut checksum_transforms = Vec::with_capacity(frames.len());
let mut last_commit: Option<usize> = None;
for (i, frame) in frames.iter().enumerate() {
frame_metas.push(crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
});
checksum_transforms.push(crate::traits::PreparedWalChecksumTransform {
a11: 0,
a12: 0,
a21: 0,
a22: 0,
c1: 0,
c2: 0,
});
let mut header = [0_u8; fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE];
header[0..4].copy_from_slice(&frame.page_number.to_be_bytes());
header[4..8].copy_from_slice(&frame.db_size_if_commit.to_be_bytes());
frame_bytes.extend_from_slice(&header);
frame_bytes.extend_from_slice(frame.page_data);
if frame.db_size_if_commit != 0 {
last_commit = Some(i);
}
}
Ok(Some(crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE,
big_endian_checksum: false,
frame_metas,
checksum_transforms,
frame_bytes,
last_commit_frame_offset: last_commit,
finalized_for: None,
finalized_running_checksum: None,
}))
}
fn append_prepared_frames(
&mut self,
_cx: &Cx,
_prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
*self.append_calls.lock().unwrap() += 1;
std::thread::sleep(self.io_delay);
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
0
}
fn checkpoint(
&mut self,
_cx: &Cx,
mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: mode,
effective_mode: mode,
})
}
}
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let vfs = MemoryVfs::new();
let path = PathBuf::from("/split_lock_parallel_prepare.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let append_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let backend = SlowWalBackend {
append_calls: StdArc::clone(&append_calls),
io_delay: Duration::from_millis(20),
};
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface::default(),
));
let pool = pager.pool.clone();
const WORKERS: u32 = 4;
let barrier = StdArc::new(std::sync::Barrier::new(WORKERS as usize));
let mut handles = Vec::with_capacity(WORKERS as usize);
let wall_start = Instant::now();
for worker_id in 0..WORKERS {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let barrier = StdArc::clone(&barrier);
let handle = std::thread::spawn(move || {
let cx = Cx::new();
barrier.wait();
let page_no = PageNumber::new(2 + worker_id).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_no,
StagedPage::from_bytes(&pool, &sample_page(worker_id as u8)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_no],
&[],
&queue,
)
.expect("group commit succeeded");
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("worker thread joined");
}
let wall_elapsed = wall_start.elapsed();
let serial_budget = Duration::from_millis(20 * u64::from(WORKERS));
assert!(
wall_elapsed < serial_budget,
"bead_id={BEAD} case=parallel_prepare \
wall_elapsed_ms={} serial_budget_ms={} — split lock should \
allow overlapping prepare phases",
wall_elapsed.as_millis(),
serial_budget.as_millis()
);
}
#[test]
fn test_group_commit_batches_frames_fewer_io_ops_than_commits() {
const BEAD: &str = "bd-3wop3.8";
struct CountingWalBackend {
append_calls: SharedCounter,
total_frames: SharedCounter,
}
impl crate::traits::WalBackend for CountingWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
*self.append_calls.lock().unwrap() += 1;
*self.total_frames.lock().unwrap() += frames.len();
Ok(())
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
if frames.is_empty() {
return Ok(None);
}
let frame_size =
fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE + frames[0].page_data.len();
let mut frame_bytes = Vec::with_capacity(frame_size * frames.len());
let mut frame_metas = Vec::with_capacity(frames.len());
let mut checksum_transforms = Vec::with_capacity(frames.len());
let mut last_commit: Option<usize> = None;
for (i, frame) in frames.iter().enumerate() {
frame_metas.push(crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
});
checksum_transforms.push(crate::traits::PreparedWalChecksumTransform {
a11: 0,
a12: 0,
a21: 0,
a22: 0,
c1: 0,
c2: 0,
});
let mut header = [0_u8; fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE];
header[0..4].copy_from_slice(&frame.page_number.to_be_bytes());
header[4..8].copy_from_slice(&frame.db_size_if_commit.to_be_bytes());
frame_bytes.extend_from_slice(&header);
frame_bytes.extend_from_slice(frame.page_data);
if frame.db_size_if_commit != 0 {
last_commit = Some(i);
}
}
Ok(Some(crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE,
big_endian_checksum: false,
frame_metas,
checksum_transforms,
frame_bytes,
last_commit_frame_offset: last_commit,
finalized_for: None,
finalized_running_checksum: None,
}))
}
fn append_prepared_frames(
&mut self,
_cx: &Cx,
prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
*self.append_calls.lock().unwrap() += 1;
*self.total_frames.lock().unwrap() += prepared.frame_count();
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
*self.total_frames.lock().unwrap()
}
fn checkpoint(
&mut self,
_cx: &Cx,
mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: mode,
effective_mode: mode,
})
}
}
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let vfs = MemoryVfs::new();
let path = PathBuf::from("/group_commit_batches_frames.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let append_calls: SharedCounter = StdArc::new(StdMutex::new(0));
let total_frames: SharedCounter = StdArc::new(StdMutex::new(0));
let backend = CountingWalBackend {
append_calls: StdArc::clone(&append_calls),
total_frames: StdArc::clone(&total_frames),
};
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface::default(),
));
let pool = pager.pool.clone();
const WORKERS: u32 = 8;
const ITERS: u32 = 20;
let barrier = StdArc::new(std::sync::Barrier::new(WORKERS as usize));
let mut handles = Vec::with_capacity(WORKERS as usize);
for worker_id in 0..WORKERS {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let barrier = StdArc::clone(&barrier);
let handle = std::thread::spawn(move || {
let cx = Cx::new();
barrier.wait();
for iter in 0..ITERS {
let page_no = PageNumber::new(2 + worker_id * ITERS + iter).unwrap();
let mut write_set = HashMap::new();
write_set.insert(
page_no,
StagedPage::from_bytes(&pool, &sample_page(worker_id as u8)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_no],
&[],
&queue,
)
.expect("group commit succeeded");
}
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("worker joined");
}
let total_commits = (WORKERS * ITERS) as usize;
let io_ops = *append_calls.lock().unwrap();
let frames_written = *total_frames.lock().unwrap();
assert_eq!(
frames_written, total_commits,
"bead_id={BEAD} case=batched_frame_count — every commit frame must be written"
);
assert!(
io_ops < total_commits,
"bead_id={BEAD} case=batched_io_ops \
io_ops={io_ops} total_commits={total_commits} — \
group commit should batch multiple commits into fewer I/O calls"
);
eprintln!(
"INFO bead_id={BEAD} case=batched_io_ops \
io_ops={io_ops} total_commits={total_commits} \
batch_ratio={:.2}x",
total_commits as f64 / io_ops as f64
);
}
#[test]
fn test_sharded_publish_no_reader_blocking() {
const BEAD: &str = "bd-3wop3.8";
let pages = PublishedPages::new(0);
let page_data = |seed: u8| -> PageData {
let buf = vec![seed; PageSize::DEFAULT.as_usize()];
PageData::from_vec(buf)
};
for i in 1..=128_u32 {
let pn = PageNumber::new(i).unwrap();
pages.insert(pn, page_data(i as u8));
}
let pages = StdArc::new(pages);
let barrier = StdArc::new(std::sync::Barrier::new(3));
let reader_done = StdArc::new(AtomicBool::new(false));
let writer_done = StdArc::new(AtomicBool::new(false));
let pages_r = StdArc::clone(&pages);
let barrier_r = StdArc::clone(&barrier);
let reader_done_w = StdArc::clone(&reader_done);
let reader = std::thread::spawn(move || {
barrier_r.wait();
let mut reads = 0_u64;
for _ in 0..1000 {
for i in 1..=64_u32 {
let pn = PageNumber::new(i).unwrap();
if pages_r.get(pn).is_some() {
reads += 1;
}
}
}
reader_done_w.store(true, AtomicOrdering::Release);
reads
});
let pages_w = StdArc::clone(&pages);
let barrier_w = StdArc::clone(&barrier);
let writer_done_w = StdArc::clone(&writer_done);
let writer = std::thread::spawn(move || {
barrier_w.wait();
let mut writes = 0_u64;
for round in 0_u8..200 {
for i in 65..=128_u32 {
let pn = PageNumber::new(i).unwrap();
pages_w.insert(pn, page_data(round.wrapping_add(i as u8)));
writes += 1;
}
}
writer_done_w.store(true, AtomicOrdering::Release);
writes
});
barrier.wait();
let reads = reader.join().expect("reader joined");
let writes = writer.join().expect("writer joined");
assert!(
reads > 0 && writes > 0,
"bead_id={BEAD} case=sharded_publish_concurrent reads={reads} writes={writes}"
);
}
#[test]
fn test_no_data_loss_under_batched_commit() {
const BEAD: &str = "bd-3wop3.8";
struct RecordingWalBackend {
committed_pages: StdArc<StdMutex<HashMap<u32, Vec<u8>>>>,
}
impl crate::traits::WalBackend for RecordingWalBackend {
fn append_frame(
&mut self,
_cx: &Cx,
_page_number: u32,
_page_data: &[u8],
_db_size_if_commit: u32,
) -> fsqlite_error::Result<()> {
Ok(())
}
fn append_frames(
&mut self,
_cx: &Cx,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<()> {
let mut committed = self.committed_pages.lock().unwrap();
for frame in frames {
committed.insert(frame.page_number, frame.page_data.to_vec());
}
Ok(())
}
fn prepare_append_frames(
&self,
frames: &[crate::traits::WalFrameRef<'_>],
) -> fsqlite_error::Result<Option<crate::traits::PreparedWalFrameBatch>> {
if frames.is_empty() {
return Ok(None);
}
let frame_size =
fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE + frames[0].page_data.len();
let mut frame_bytes = Vec::with_capacity(frame_size * frames.len());
let mut frame_metas = Vec::with_capacity(frames.len());
let mut checksum_transforms = Vec::with_capacity(frames.len());
let mut last_commit: Option<usize> = None;
for (i, frame) in frames.iter().enumerate() {
frame_metas.push(crate::traits::PreparedWalFrameMeta {
page_number: frame.page_number,
db_size_if_commit: frame.db_size_if_commit,
});
checksum_transforms.push(crate::traits::PreparedWalChecksumTransform {
a11: 0,
a12: 0,
a21: 0,
a22: 0,
c1: 0,
c2: 0,
});
let mut header = [0_u8; fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE];
header[0..4].copy_from_slice(&frame.page_number.to_be_bytes());
header[4..8].copy_from_slice(&frame.db_size_if_commit.to_be_bytes());
frame_bytes.extend_from_slice(&header);
frame_bytes.extend_from_slice(frame.page_data);
if frame.db_size_if_commit != 0 {
last_commit = Some(i);
}
}
Ok(Some(crate::traits::PreparedWalFrameBatch {
frame_size,
page_data_offset: fsqlite_wal::checksum::WAL_FRAME_HEADER_SIZE,
big_endian_checksum: false,
frame_metas,
checksum_transforms,
frame_bytes,
last_commit_frame_offset: last_commit,
finalized_for: None,
finalized_running_checksum: None,
}))
}
fn append_prepared_frames(
&mut self,
_cx: &Cx,
prepared: &mut crate::traits::PreparedWalFrameBatch,
) -> fsqlite_error::Result<()> {
let mut committed = self.committed_pages.lock().unwrap();
for i in 0..prepared.frame_count() {
let meta = &prepared.frame_metas[i];
let data = prepared.page_data(i).to_vec();
committed.insert(meta.page_number, data);
}
Ok(())
}
fn read_page(
&mut self,
_cx: &Cx,
_page_number: u32,
) -> fsqlite_error::Result<Option<Vec<u8>>> {
Ok(None)
}
fn sync(&mut self, _cx: &Cx) -> fsqlite_error::Result<()> {
Ok(())
}
fn frame_count(&self) -> usize {
self.committed_pages.lock().unwrap().len()
}
fn checkpoint(
&mut self,
_cx: &Cx,
mode: crate::traits::CheckpointMode,
_writer: &mut dyn crate::traits::CheckpointPageWriter,
_backfilled_frames: u32,
_oldest_reader_frame: Option<u32>,
) -> fsqlite_error::Result<crate::traits::CheckpointResult> {
Ok(crate::traits::CheckpointResult {
total_frames: 0,
frames_backfilled: 0,
completed: true,
wal_was_reset: false,
requested_mode: mode,
effective_mode: mode,
})
}
}
let _guard = PARALLEL_WAL_LANE_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let vfs = MemoryVfs::new();
let path = PathBuf::from("/no_data_loss_batched.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
let cx = Cx::new();
let committed_pages: StdArc<StdMutex<HashMap<u32, Vec<u8>>>> =
StdArc::new(StdMutex::new(HashMap::new()));
let backend = RecordingWalBackend {
committed_pages: StdArc::clone(&committed_pages),
};
pager.set_wal_backend(Box::new(backend)).unwrap();
pager.set_journal_mode(&cx, JournalMode::Wal).unwrap();
let inner = Arc::clone(&pager.inner);
let wal_backend = Arc::clone(&pager.wal_backend);
let queue = Arc::new(GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface::default(),
));
let pool = pager.pool.clone();
const WORKERS: u32 = 8;
let barrier = StdArc::new(std::sync::Barrier::new(WORKERS as usize));
let mut handles = Vec::with_capacity(WORKERS as usize);
for worker_id in 0..WORKERS {
let inner = Arc::clone(&inner);
let wal_backend = Arc::clone(&wal_backend);
let queue = Arc::clone(&queue);
let pool = pool.clone();
let barrier = StdArc::clone(&barrier);
let handle = std::thread::spawn(move || {
let cx = Cx::new();
barrier.wait();
let page_no = PageNumber::new(2 + worker_id).unwrap();
let fill = worker_id as u8;
let mut write_set = HashMap::new();
write_set.insert(
page_no,
StagedPage::from_bytes(&pool, &sample_page(fill)).unwrap(),
);
SimpleTransaction::<MemoryVfs>::commit_wal_group_commit(
&cx,
&wal_backend,
&inner,
&write_set,
&[page_no],
&[],
&queue,
)
.expect("commit succeeded");
(page_no.get(), fill)
});
handles.push(handle);
}
let expected: Vec<(u32, u8)> = handles
.into_iter()
.map(|h| h.join().expect("worker joined"))
.collect();
let committed = committed_pages.lock().unwrap();
for (page_no, fill) in &expected {
let data = committed.get(page_no).unwrap_or_else(|| {
panic!(
"bead_id={BEAD} case=data_loss page_no={page_no} — \
page missing from WAL after batched commit"
)
});
assert_eq!(
data[0], *fill,
"bead_id={BEAD} case=data_integrity page_no={page_no} — \
page content mismatch"
);
}
assert!(
committed.len() >= expected.len(),
"bead_id={BEAD} case=committed_count \
committed={} expected={}",
committed.len(),
expected.len()
);
}
#[test]
fn test_combiner_wired_in_production() {
use fsqlite_mvcc::TxnManager;
const BEAD: &str = "bd-3wop3.8";
const THREADS: usize = 8;
const ALLOCS_PER_THREAD: usize = 50;
let mgr = TxnManager::new(1, 1);
let mgr = StdArc::new(mgr);
let barrier = StdArc::new(std::sync::Barrier::new(THREADS));
let mut handles = Vec::with_capacity(THREADS);
for _ in 0..THREADS {
let mgr = StdArc::clone(&mgr);
let barrier = StdArc::clone(&barrier);
let handle = std::thread::spawn(move || {
barrier.wait();
let mut seqs = Vec::with_capacity(ALLOCS_PER_THREAD);
for _ in 0..ALLOCS_PER_THREAD {
seqs.push(mgr.alloc_commit_seq().get());
}
seqs
});
handles.push(handle);
}
let mut all_seqs: Vec<u64> = Vec::with_capacity(THREADS * ALLOCS_PER_THREAD);
for handle in handles {
all_seqs.extend(handle.join().expect("thread joined"));
}
all_seqs.sort_unstable();
for window in all_seqs.windows(2) {
assert_ne!(
window[0], window[1],
"bead_id={BEAD} case=combiner_uniqueness — \
duplicate commit seq {}",
window[0]
);
}
let min = all_seqs[0];
let max = *all_seqs.last().unwrap();
let expected_count = (THREADS * ALLOCS_PER_THREAD) as u64;
assert_eq!(
max - min + 1,
expected_count,
"bead_id={BEAD} case=combiner_contiguous \
min={min} max={max} count={expected_count} — \
sequences must form a dense range"
);
}
#[test]
fn test_group_commit_epoch_maps_bounded_under_sustained_load() {
const BEAD: &str = "bd-vn2ea";
let queue = GroupCommitQueue::with_parallel_wal_control(
GroupCommitConfig::default(),
ParallelWalControlSurface::default(),
);
let total_epochs: u64 = 500;
for epoch in 1..=total_epochs {
let error = FrankenError::Busy;
queue.publish_failed_epoch(epoch, &error, false);
queue.publish_completed_epoch(epoch, false);
}
let failed_len = queue
.failed_epochs
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
assert!(
failed_len <= 128,
"bead_id={BEAD} case=epoch_map_gc \
failed_epochs_len={failed_len} max=128 — \
stale epoch metadata should be pruned"
);
eprintln!(
"INFO bead_id={BEAD} case=epoch_map_gc \
failed_epochs_len={failed_len} total_epochs={total_epochs}"
);
}
#[test]
#[ignore = "requires real benchmark harness with rusqlite reference; run via e2e bench suite"]
fn test_16t_throughput_exceeds_sqlite() {
eprintln!(
"INFO bead_id=bd-3wop3.8 case=16t_throughput_gate \
status=skipped reason=requires_benchmark_harness"
);
}
#[test]
fn test_simple_pager_open_defaults_to_s3_fifo_eviction() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/s3fifo_default_rw.db");
let pager = SimplePager::open(vfs, &path, PageSize::DEFAULT).unwrap();
assert!(
matches!(
pager.cache.eviction_policy(),
crate::page_cache::PageCacheEvictionPolicy::S3Fifo(_)
),
"read-write pager must default to S3-FIFO eviction"
);
}
#[test]
fn test_simple_pager_open_readonly_defaults_to_s3_fifo_eviction() {
let vfs = MemoryVfs::new();
let path = PathBuf::from("/s3fifo_default_ro.db");
{
let _rw = SimplePager::open(vfs.clone(), &path, PageSize::DEFAULT).unwrap();
}
let cx = Cx::new();
let pager = SimplePager::open_readonly_with_cx(&cx, vfs, &path, PageSize::DEFAULT).unwrap();
assert!(
matches!(
pager.cache.eviction_policy(),
crate::page_cache::PageCacheEvictionPolicy::S3Fifo(_)
),
"read-only pager must default to S3-FIFO eviction"
);
}
#[test]
fn pager_commit_profile_snapshot_default_and_debug() {
let def = PagerCommitProfileSnapshot::default();
assert_eq!(def.commit_calls, 0);
assert_eq!(def.phase_a_time_ns, 0);
assert_eq!(def.wal_commit_time_ns, 0);
let copied = def;
assert_eq!(copied, def);
let dbg = format!("{def:?}");
assert!(dbg.contains("PagerCommitProfileSnapshot"));
}
#[test]
fn wal_commit_sync_policy_variants_eq() {
let deferred = WalCommitSyncPolicy::Deferred;
let per_commit = WalCommitSyncPolicy::PerCommit;
assert_ne!(deferred, per_commit);
assert!(!deferred.should_sync_on_commit());
assert!(per_commit.should_sync_on_commit());
let copied = deferred;
assert_eq!(copied, deferred);
let dbg = format!("{per_commit:?}");
assert!(dbg.contains("PerCommit"));
}
#[test]
fn pager_metadata_publication_class_all_variants() {
let variants = [
PagerMetadataPublicationClass::SnapshotSummary,
PagerMetadataPublicationClass::PagePlaneResidency,
PagerMetadataPublicationClass::CertificateDerivedIntent,
];
for (i, v) in variants.iter().enumerate() {
let copied = *v;
assert_eq!(copied, *v);
for (j, w) in variants.iter().enumerate() {
assert_eq!(i == j, v == w);
}
}
assert_eq!(PAGER_METADATA_PUBLICATION_CONTRACTS.len(), 3);
assert_eq!(
PAGER_METADATA_PUBLICATION_CONTRACTS[0].class,
PagerMetadataPublicationClass::SnapshotSummary
);
}
#[test]
fn pager_published_snapshot_debug_clone_copy_eq() {
let snap = PagerPublishedSnapshot {
snapshot_gen: 4,
visible_commit_seq: CommitSeq::new(10),
db_size: 100,
journal_mode: JournalMode::Wal,
freelist_count: 5,
checkpoint_active: false,
page_set_size: 42,
};
let copied = snap;
assert_eq!(copied, snap);
let other = PagerPublishedSnapshot {
db_size: 200,
..snap
};
assert_ne!(snap, other);
let dbg = format!("{snap:?}");
assert!(dbg.contains("PagerPublishedSnapshot"));
}
#[test]
fn pager_committed_snapshot_copy_eq_debug() {
let snap = PagerCommittedSnapshot {
commit_seq: CommitSeq::new(5),
db_size: 100,
journal_mode: JournalMode::Wal,
freelist_count: 3,
checkpoint_active: false,
writer_active: true,
db_file_size_bytes: 409_600,
};
let copied = snap;
assert_eq!(copied, snap);
let other = PagerCommittedSnapshot {
db_size: 200,
..snap
};
assert_ne!(snap, other);
let dbg = format!("{snap:?}");
assert!(dbg.contains("PagerCommittedSnapshot"));
}
#[test]
fn parallel_wal_publication_intent_copy_eq_debug() {
let intent = ParallelWalPublicationIntent {
certificate_epoch: 7,
visible_commit_seq: CommitSeq::new(10),
page_plane_visible_commit_seq: CommitSeq::new(9),
db_size: 50,
journal_mode: JournalMode::Wal,
freelist_count: 0,
checkpoint_active: false,
page_set_size: 12,
};
let copied = intent;
assert_eq!(copied, intent);
let other = ParallelWalPublicationIntent {
certificate_epoch: 8,
..intent
};
assert_ne!(intent, other);
let dbg = format!("{intent:?}");
assert!(dbg.contains("ParallelWalPublicationIntent"));
}
#[test]
fn pager_metadata_publication_contract_copy_eq() {
let c = PAGER_METADATA_PUBLICATION_CONTRACTS[0];
assert_eq!(c.class, PagerMetadataPublicationClass::SnapshotSummary);
assert!(!c.touchpoint.is_empty());
assert!(!c.current_primitive.is_empty());
let copied = c;
assert_eq!(copied, c);
}
#[test]
fn pager_metadata_publication_contracts_cover_all_classes() {
let classes: Vec<_> = PAGER_METADATA_PUBLICATION_CONTRACTS
.iter()
.map(|c| c.class)
.collect();
assert!(classes.contains(&PagerMetadataPublicationClass::SnapshotSummary));
assert!(classes.contains(&PagerMetadataPublicationClass::PagePlaneResidency));
assert!(classes.contains(&PagerMetadataPublicationClass::CertificateDerivedIntent));
assert_eq!(classes.len(), 3);
}
}