mod admission;
mod cached_blob;
mod guid_hash;
mod mutation;
mod residency;
mod telemetry;
mod write_delta;
use std::collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, Weak};
use dashmap::DashMap;
use crate::api::stats::{StoreStats, VacuumStats};
use guid_hash::GuidBuildHasher;
use crate::api::errors::{Error, Result};
use crate::engine;
use crate::layout::{BlobGuid, HEADER_SIZE, PAGE_SIZE};
use crate::store::{BlobFrameRef, PAGE_4K};
use super::blob_store::{AlignedBlobBuf, BlobStore};
use super::read_index::{ReadIndex, ReadIndexCache, ReadIndexStamp, ReadPageCache};
use admission::TinyLFU;
pub use cached_blob::{BlobWriteGuard, CachedBlob};
use mutation::{
bookkeeping_shard_idx, pop_candidate_batch, CandidateKind, MutationState, BOOKKEEPING_SHARDS,
};
use residency::RouteResidency;
use telemetry::Telemetry;
use write_delta::{DeltaEntry, DeltaOp, WriteDelta};
pub(crate) use write_delta::DeltaEntry as WriteDeltaEntry;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WriteDeltaKeyState {
Put { seq: u64 },
Delete,
}
pub const STRUCTURAL_SEQ: u64 = u64::MAX;
#[derive(Default)]
struct SnapshotState {
live: BTreeMap<u64, SnapshotRoot>,
cow_pending: HashMap<BlobGuid, Vec<(BlobGuid, u64)>>,
orphans: Vec<(BlobGuid, u64)>,
structural_pending: HashMap<BlobGuid, HashSet<BlobGuid>>,
structural_orphans: HashSet<BlobGuid>,
retired_orphans: VecDeque<BlobGuid>,
retired_orphan_set: HashSet<BlobGuid>,
}
struct SnapshotRoot {
guid: BlobGuid,
pin: Weak<CachedBlob>,
}
pub(crate) struct SnapshotLease {
store: Arc<BufferManager>,
epoch: u64,
root_guid: BlobGuid,
root_pin: Option<Arc<CachedBlob>>,
}
impl SnapshotLease {
pub(crate) fn new(
store: Arc<BufferManager>,
epoch: u64,
root_guid: BlobGuid,
root_pin: Arc<CachedBlob>,
) -> Arc<Self> {
Arc::new(Self {
store,
epoch,
root_guid,
root_pin: Some(root_pin),
})
}
}
impl Drop for SnapshotLease {
fn drop(&mut self) {
self.store.retire_snapshot(self.epoch);
drop(self.root_pin.take());
self.store.discard_snapshot_root(self.root_guid);
}
}
pub(crate) struct PinnedSnapshotRoot {
store: Arc<BufferManager>,
guid: BlobGuid,
pin: Option<Arc<CachedBlob>>,
}
impl PinnedSnapshotRoot {
#[must_use]
pub(crate) fn guid(&self) -> BlobGuid {
self.guid
}
}
impl Drop for PinnedSnapshotRoot {
fn drop(&mut self) {
drop(self.pin.take());
self.store.discard_snapshot_root(self.guid);
}
}
pub(crate) struct WriteThroughEntry {
pub(crate) guid: BlobGuid,
pub(crate) bytes: AlignedBlobBuf,
pub(crate) expected_seq: u64,
pub(crate) content_version: Option<u64>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WriteThroughStatus {
Written,
Stale,
}
pub(crate) struct WriteThroughBatchReport {
pub(crate) statuses: Vec<WriteThroughStatus>,
}
#[derive(Clone, Copy)]
pub(crate) struct DirtySnapshotEntry {
pub(crate) guid: BlobGuid,
pub(crate) expected_seq: u64,
pub(crate) content_version: u64,
}
#[derive(Clone, Copy)]
enum PinAccess {
Point,
Scan,
Silent,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum ReadIndexPolicy {
PointRead,
Liveness,
}
const READ_AUX_CACHE_MIN_BYTES: usize = 2 * 1024 * 1024;
const READ_AUX_CACHE_MAX_BYTES: usize = 256 * 1024 * 1024;
const READ_AUX_CACHE_ENABLE_AT_BYTES: usize = 16 * 1024 * 1024;
const READ_INDEX_DIRECTORY_PROBE_BYTES: usize = PAGE_4K as usize;
#[derive(Clone, Copy, Debug)]
struct CacheBudget {
blob_slots: usize,
read_page_bytes: usize,
read_index_bytes: usize,
}
impl CacheBudget {
fn memory(total_blob_slots: usize) -> Self {
Self {
blob_slots: total_blob_slots.max(1),
read_page_bytes: 0,
read_index_bytes: 0,
}
}
fn file(total_blob_slots: usize) -> Self {
let total_blob_slots = total_blob_slots.max(1);
let total_bytes = total_blob_slots.saturating_mul(PAGE_SIZE as usize);
let read_aux_bytes = if total_bytes < READ_AUX_CACHE_ENABLE_AT_BYTES {
0
} else {
(total_bytes / 2).clamp(READ_AUX_CACHE_MIN_BYTES, READ_AUX_CACHE_MAX_BYTES)
};
let read_index_bytes = read_aux_bytes * 7 / 8;
let read_page_bytes = read_aux_bytes - read_index_bytes;
let blob_bytes = total_bytes.saturating_sub(read_aux_bytes);
Self {
blob_slots: (blob_bytes / PAGE_SIZE as usize).max(1),
read_page_bytes,
read_index_bytes,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct BufferStats {
pub(crate) dirty_count: usize,
pub(crate) pending_delete_count: usize,
pub(crate) gc_orphan_backlog_count: usize,
pub(crate) gc_reclaimed_count: u64,
pub(crate) gc_last_full_sweep_deferred_count: usize,
pub(crate) read_index_token_count: usize,
pub(crate) read_index_cache_entries: usize,
pub(crate) read_index_cache_bytes: usize,
pub(crate) read_index_cache_budget_bytes: usize,
pub(crate) read_page_cache_entries: usize,
pub(crate) read_page_cache_bytes: usize,
pub(crate) read_page_cache_ghost_entries: usize,
pub(crate) read_page_cache_budget_bytes: usize,
pub(crate) cache_hits: u64,
pub(crate) cache_misses: u64,
pub(crate) full_blob_reads: u64,
pub(crate) full_blob_read_bytes: u64,
pub(crate) point_full_blob_reads: u64,
pub(crate) scan_full_blob_reads: u64,
pub(crate) silent_full_blob_reads: u64,
pub(crate) read_page_hits: u64,
pub(crate) read_page_misses: u64,
pub(crate) read_index_cache_hits: u64,
pub(crate) read_index_cache_misses: u64,
pub(crate) read_index_loads: u64,
pub(crate) read_index_dir_read_bytes: u64,
pub(crate) read_index_bucket_reads: u64,
pub(crate) read_index_bucket_read_bytes: u64,
pub(crate) read_index_inline_hits: u64,
pub(crate) read_index_value_hits: u64,
pub(crate) read_index_value_read_bytes: u64,
pub(crate) read_index_offset_hits: u64,
pub(crate) read_index_negative_hits: u64,
pub(crate) read_index_crossing_hits: u64,
pub(crate) read_index_unknowns: u64,
pub(crate) optimistic_restarts: u64,
pub(crate) range_restarts: u64,
pub(crate) walker_ops: u64,
pub(crate) walker_blob_hops: u64,
pub(crate) max_blob_hops: u64,
pub(crate) max_cross_blob_depth: u64,
pub(crate) spillovers: u64,
pub(crate) merges: u64,
pub(crate) route_resident_count: usize,
pub(crate) route_resident_demotions: u64,
pub(crate) cache_evictions: u64,
pub(crate) eviction_skips_protected: u64,
pub(crate) eviction_skips_route_resident: u64,
pub(crate) admission_protects: u64,
pub(crate) write_delta_count: usize,
pub(crate) store: StoreStats,
}
pub(crate) struct GcSweepOutcome {
pub(crate) freed: usize,
pub(crate) complete: bool,
}
pub struct BufferManager {
store: Arc<dyn BlobStore>,
alloc_uninit: Arc<dyn Fn() -> AlignedBlobBuf + Send + Sync>,
capacity: usize,
read_pages: ReadPageCache,
read_indexes: ReadIndexCache,
read_index_tokens: DashMap<BlobGuid, AtomicU64, GuidBuildHasher>,
read_index_token_clock: AtomicU64,
cache: DashMap<BlobGuid, Arc<CachedBlob>, GuidBuildHasher>,
admission: TinyLFU,
route_resident: RouteResidency,
mutation: [Mutex<MutationState>; BOOKKEEPING_SHARDS],
write_delta: WriteDelta,
write_delta_flush: Mutex<()>,
checkpoint_io: Mutex<()>,
delete_fence_total: AtomicUsize,
compact_candidate_cursor: AtomicUsize,
merge_candidate_cursor: AtomicUsize,
compact_candidate_total: AtomicUsize,
merge_candidate_total: AtomicUsize,
clock: AtomicU64,
telemetry: Telemetry,
current_epoch: AtomicU64,
fork_barrier: AtomicU64,
snapshots: Mutex<SnapshotState>,
gc_epoch: AtomicU64,
physical_gc: Mutex<()>,
gc_reclaimed_count: AtomicU64,
gc_last_full_sweep_deferred_count: AtomicUsize,
checkpoint_waker: Mutex<Option<std::thread::Thread>>,
}
struct GcEpochGuard<'a> {
_serial: MutexGuard<'a, ()>,
epoch: &'a AtomicU64,
}
#[cfg(test)]
struct ResidentPinBarrier {
entered: std::sync::Barrier,
release: std::sync::Barrier,
}
#[cfg(test)]
impl ResidentPinBarrier {
fn new() -> Self {
Self {
entered: std::sync::Barrier::new(2),
release: std::sync::Barrier::new(2),
}
}
}
#[cfg(test)]
thread_local! {
static RESIDENT_PIN_BARRIER: std::cell::RefCell<Option<Arc<ResidentPinBarrier>>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
fn set_resident_pin_barrier_for_current_thread(barrier: Arc<ResidentPinBarrier>) {
RESIDENT_PIN_BARRIER.with(|slot| *slot.borrow_mut() = Some(barrier));
}
#[cfg(test)]
fn pause_resident_pin_after_fence_check() {
let barrier = RESIDENT_PIN_BARRIER.with(|slot| slot.borrow_mut().take());
if let Some(barrier) = barrier {
barrier.entered.wait();
barrier.release.wait();
}
}
impl Drop for GcEpochGuard<'_> {
fn drop(&mut self) {
let previous = self.epoch.fetch_add(1, Ordering::AcqRel);
debug_assert_eq!(previous & 1, 1, "GC epoch must be active on guard drop");
}
}
impl BufferManager {
#[must_use]
pub(crate) fn current_epoch(&self) -> u64 {
self.current_epoch.load(Ordering::Acquire)
}
pub(crate) fn set_current_epoch(&self, epoch: u64) {
self.current_epoch.store(epoch.max(1), Ordering::Release);
}
#[must_use]
pub(crate) fn fork_barrier(&self) -> u64 {
self.fork_barrier.load(Ordering::Acquire)
}
#[must_use]
pub(crate) fn gc_read_epoch(&self) -> u64 {
self.gc_epoch.load(Ordering::Acquire)
}
#[must_use]
pub(crate) fn gc_stable_read_epoch(&self) -> u64 {
loop {
let epoch = self.gc_read_epoch();
if epoch & 1 == 0 {
return epoch;
}
std::thread::yield_now();
}
}
#[must_use]
pub(crate) fn gc_raced_since(&self, captured: u64) -> bool {
captured & 1 != 0 || self.gc_epoch.load(Ordering::Acquire) != captured
}
pub(crate) fn with_stable_gc_epoch<R>(
&self,
captured: u64,
publish: impl FnOnce() -> R,
) -> Option<R> {
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
(!self.gc_raced_since(captured)).then(publish)
}
#[must_use]
pub(crate) fn gc_epoch_still_stable(&self, captured: u64) -> bool {
self.with_stable_gc_epoch(captured, || ()).is_some()
}
fn begin_gc_epoch(&self) -> GcEpochGuard<'_> {
let serial = self.physical_gc.lock().expect("physical GC lock poisoned");
let previous = self.gc_epoch.fetch_add(1, Ordering::AcqRel);
debug_assert_eq!(previous & 1, 0, "physical GC passes must be serialized");
GcEpochGuard {
_serial: serial,
epoch: &self.gc_epoch,
}
}
pub(crate) fn fork_frame(
&self,
src_bytes: &[u8],
new_guid: BlobGuid,
seq: u64,
) -> Result<Arc<CachedBlob>> {
let mut buf = self.alloc_blob_buf_zeroed();
buf.as_mut_slice().copy_from_slice(src_bytes);
crate::layout::set_frame_blob_guid(buf.as_mut_slice(), new_guid);
self.install_new_blob(new_guid, buf, seq);
self.pin(new_guid)
}
pub(crate) fn install_snapshot_root(
&self,
new_guid: BlobGuid,
src: &CachedBlob,
) -> Arc<CachedBlob> {
let guard = src.read();
let mut buf = self.alloc_blob_buf_zeroed();
buf.as_mut_slice().copy_from_slice(guard.as_slice());
crate::layout::set_frame_blob_guid(buf.as_mut_slice(), new_guid);
self.insert_owned_into_cache(new_guid, buf, PinAccess::Silent)
}
pub(crate) fn register_snapshot(
&self,
root_guid: BlobGuid,
root_pin: &Arc<CachedBlob>,
) -> Result<u64> {
let mut snaps = self.snapshots.lock().expect("snapshot registry poisoned");
let epoch = self
.current_epoch
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| {
(current < u64::MAX - 1).then_some(current + 1)
})
.map_err(|_| Error::SnapshotEpochExhausted)?;
snaps.live.insert(
epoch,
SnapshotRoot {
guid: root_guid,
pin: Arc::downgrade(root_pin),
},
);
self.fork_barrier.store(epoch, Ordering::Release);
Ok(epoch)
}
pub(crate) fn stage_cow_reclaim(
&self,
parent_guid: BlobGuid,
guid: BlobGuid,
created_epoch: u64,
) {
let mut snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
let pending = snapshots.cow_pending.entry(parent_guid).or_default();
if !pending.iter().any(|(candidate, _)| *candidate == guid) {
pending.push((guid, created_epoch));
}
}
pub(crate) fn stage_structural_reclaim(&self, parent_guid: BlobGuid, child_guid: BlobGuid) {
let mut snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
snapshots
.structural_pending
.entry(parent_guid)
.or_default()
.insert(child_guid);
}
fn publish_parent_orphans(&self, parent_guid: BlobGuid) {
let wake = {
let mut snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
let cow = snapshots
.cow_pending
.remove(&parent_guid)
.unwrap_or_default();
let structural = snapshots
.structural_pending
.remove(&parent_guid)
.unwrap_or_default();
let barrier = snapshots.live.keys().next_back().copied().unwrap_or(0);
let mut wake = false;
for (guid, created_epoch) in cow {
if barrier != 0 && created_epoch <= barrier {
snapshots.orphans.push((guid, created_epoch));
} else if snapshots.retired_orphan_set.insert(guid) {
snapshots.retired_orphans.push_back(guid);
wake = true;
}
}
for guid in structural {
if snapshots.live.is_empty() {
if snapshots.retired_orphan_set.insert(guid) {
snapshots.retired_orphans.push_back(guid);
wake = true;
}
} else {
snapshots.structural_orphans.insert(guid);
}
}
wake
};
if wake {
self.wake_checkpointer();
}
}
pub(crate) fn retire_snapshot(&self, epoch: u64) {
let (root, wake) = {
let mut snaps = self.snapshots.lock().expect("snapshot registry poisoned");
let root = snaps.live.remove(&epoch);
let barrier = snaps.live.keys().next_back().copied().unwrap_or(0);
self.fork_barrier.store(barrier, Ordering::Release);
let mut active = Vec::with_capacity(snaps.orphans.len());
for (guid, created_epoch) in std::mem::take(&mut snaps.orphans) {
if barrier != 0 && created_epoch <= barrier {
active.push((guid, created_epoch));
} else if snaps.retired_orphan_set.insert(guid) {
snaps.retired_orphans.push_back(guid);
}
}
snaps.orphans = active;
if barrier == 0 {
let structural = std::mem::take(&mut snaps.structural_orphans);
for guid in structural {
if snaps.retired_orphan_set.insert(guid) {
snaps.retired_orphans.push_back(guid);
}
}
}
(root, !snaps.retired_orphans.is_empty())
};
if let Some(root) = root {
self.discard_snapshot_root(root.guid);
}
if wake {
self.wake_checkpointer();
}
}
pub(crate) fn discard_snapshot_root(&self, guid: BlobGuid) {
let _ = self.evict_reclaimable_blob(guid);
}
fn evict_reclaimable_blob(&self, guid: BlobGuid) -> bool {
{
let state = self.mutation_shard(guid).lock().unwrap();
if state.is_protected_or_pending(&guid) {
return false;
}
}
if let Some((_, entry)) = self.cache.remove_if(&guid, |_, entry| {
if Arc::strong_count(entry) > 1 {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
!state.is_protected_or_pending(&guid)
}) {
entry.clear_dirty_hint();
} else if self.cache.contains_key(&guid) {
return false;
}
self.route_resident.remove(guid);
let mut state = self.mutation_shard(guid).lock().unwrap();
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.decrement_candidate_totals(removed);
true
}
pub(crate) fn blob_is_pinned(&self, guid: BlobGuid) -> bool {
self.cache
.get(&guid)
.is_some_and(|entry| Arc::strong_count(entry.value()) > 1)
}
fn reclaim_unreachable_blob(&self, guid: BlobGuid) -> Result<bool> {
{
let mut state = self.mutation_shard(guid).lock().unwrap();
if state.is_protected_or_pending(&guid) {
return Ok(false);
}
state.gc_deleting.insert(guid);
self.delete_fence_total.fetch_add(1, Ordering::AcqRel);
}
let result = (|| {
self.invalidate_indexed_reads(guid);
if let Some((_, entry)) = self.cache.remove_if(&guid, |_, entry| {
if Arc::strong_count(entry) > 1 {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
!state.is_protected(&guid)
&& !state.pending_deletes.contains_key(&guid)
&& !state.deleting.contains_key(&guid)
}) {
entry.clear_dirty_hint();
} else if self.cache.contains_key(&guid) {
return Ok(false);
}
self.route_resident.remove(guid);
let mut state = self.mutation_shard(guid).lock().unwrap();
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.decrement_candidate_totals(removed);
self.store.delete_blob(guid)?;
Ok(true)
})();
let mut state = self.mutation_shard(guid).lock().unwrap();
if state.gc_deleting.remove(&guid) {
self.delete_fence_total.fetch_sub(1, Ordering::AcqRel);
}
drop(state);
self.remove_read_index_token_if_unreachable(guid);
result
}
pub(crate) fn snapshot_roots_pinned(self: &Arc<Self>) -> Result<Vec<PinnedSnapshotRoot>> {
self.snapshots
.lock()
.expect("snapshot registry poisoned")
.live
.values()
.map(|root| {
root.pin
.upgrade()
.map(|pin| PinnedSnapshotRoot {
store: Arc::clone(self),
guid: root.guid,
pin: Some(pin),
})
.ok_or(Error::Internal("live snapshot registry lost its root pin"))
})
.collect()
}
#[cfg(test)]
pub(crate) fn gc_sweep_unreachable(&self, reachable: &HashSet<BlobGuid>) -> Result<usize> {
self.gc_sweep_unreachable_bounded(reachable, usize::MAX)
.map(|outcome| outcome.freed)
}
pub(crate) fn gc_sweep_unreachable_with_canonical(
&self,
reachable: &HashSet<BlobGuid>,
canonical_reachable: &HashSet<BlobGuid>,
) -> Result<usize> {
self.gc_sweep_unreachable_with_canonical_bounded(reachable, canonical_reachable, usize::MAX)
.map(|outcome| outcome.freed)
}
#[cfg(test)]
pub(crate) fn gc_sweep_unreachable_bounded(
&self,
reachable: &HashSet<BlobGuid>,
limit: usize,
) -> Result<GcSweepOutcome> {
self.gc_sweep_unreachable_with_canonical_bounded(reachable, reachable, limit)
}
pub(crate) fn gc_sweep_unreachable_with_canonical_bounded(
&self,
reachable: &HashSet<BlobGuid>,
canonical_reachable: &HashSet<BlobGuid>,
limit: usize,
) -> Result<GcSweepOutcome> {
debug_assert!(canonical_reachable.is_subset(reachable));
let _epoch = self.begin_gc_epoch();
let mut freed = 0usize;
let mut deferred = 0usize;
let mut reclaimed = HashSet::new();
let present = self.store.list_blobs()?;
for guid in present.iter().copied() {
if reachable.contains(&guid) {
continue;
}
if freed >= limit {
deferred = deferred.saturating_add(1);
continue;
}
if self.reclaim_unreachable_blob(guid)? {
freed += 1;
reclaimed.insert(guid);
} else {
deferred = deferred.saturating_add(1);
}
}
if freed != 0 {
self.store.flush()?;
self.gc_reclaimed_count
.fetch_add(freed as u64, Ordering::Relaxed);
}
self.gc_last_full_sweep_deferred_count
.store(deferred, Ordering::Relaxed);
let present: HashSet<_> = present.into_iter().collect();
let mut snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
snapshots.retired_orphans.retain(|guid| {
present.contains(guid)
&& !reclaimed.contains(guid)
&& !canonical_reachable.contains(guid)
});
snapshots.retired_orphan_set = snapshots.retired_orphans.iter().copied().collect();
Ok(GcSweepOutcome {
freed,
complete: deferred == 0,
})
}
pub(crate) fn reclaim_retired_orphans_bounded(&self, limit: usize) -> Result<usize> {
let candidates = self.take_retired_orphans_bounded(limit);
self.reclaim_retired_orphan_batch(candidates)
}
pub(crate) fn take_retired_orphans_bounded(&self, limit: usize) -> Vec<BlobGuid> {
let mut snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
let take = limit.min(snapshots.retired_orphans.len());
let mut candidates = Vec::with_capacity(take);
for _ in 0..take {
let guid = snapshots
.retired_orphans
.pop_front()
.expect("bounded retired orphan count");
snapshots.retired_orphan_set.remove(&guid);
candidates.push(guid);
}
candidates
}
pub(crate) fn reclaim_retired_orphan_batch(&self, candidates: Vec<BlobGuid>) -> Result<usize> {
if candidates.is_empty() {
return Ok(0);
}
let _epoch = self.begin_gc_epoch();
let mut freed = 0usize;
let mut retry = Vec::new();
for (index, guid) in candidates.iter().copied().enumerate() {
match self.store.has_blob(guid) {
Ok(true) => {}
Ok(false) => continue,
Err(error) => {
retry.extend(candidates[index..].iter().copied());
self.restore_retired_orphans(retry);
return Err(error);
}
}
match self.reclaim_unreachable_blob(guid) {
Ok(true) => freed += 1,
Ok(false) => retry.push(guid),
Err(error) => {
retry.extend(candidates[index..].iter().copied());
self.restore_retired_orphans(retry);
return Err(error);
}
}
}
if freed != 0 {
if let Err(error) = self.store.flush() {
self.restore_retired_orphans(candidates);
return Err(error);
}
self.gc_reclaimed_count
.fetch_add(freed as u64, Ordering::Relaxed);
}
self.restore_retired_orphans(retry);
Ok(freed)
}
fn restore_retired_orphans(&self, guids: impl IntoIterator<Item = BlobGuid>) {
let mut snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
for guid in guids {
if snapshots.retired_orphan_set.insert(guid) {
snapshots.retired_orphans.push_back(guid);
}
}
}
#[must_use]
pub(crate) fn gc_orphan_backlog_count(&self) -> usize {
let snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
snapshots.cow_pending.values().map(Vec::len).sum::<usize>()
+ snapshots.orphans.len()
+ snapshots
.structural_pending
.values()
.map(HashSet::len)
.sum::<usize>()
+ snapshots.structural_orphans.len()
+ snapshots.retired_orphans.len()
}
pub(crate) fn orphan_staging_count(&self) -> usize {
let snapshots = self.snapshots.lock().expect("snapshot registry poisoned");
snapshots.cow_pending.values().map(Vec::len).sum::<usize>()
+ snapshots
.structural_pending
.values()
.map(HashSet::len)
.sum::<usize>()
}
pub(crate) fn register_checkpoint_waker(&self, thread: std::thread::Thread) {
*self
.checkpoint_waker
.lock()
.expect("checkpoint waker lock poisoned") = Some(thread);
}
pub(crate) fn clear_checkpoint_waker(&self) {
self.checkpoint_waker
.lock()
.expect("checkpoint waker lock poisoned")
.take();
}
fn wake_checkpointer(&self) {
if let Some(thread) = self
.checkpoint_waker
.lock()
.expect("checkpoint waker lock poisoned")
.as_ref()
{
thread.unpark();
}
}
pub(crate) fn vacuum_storage(&self) -> Result<VacuumStats> {
self.store.vacuum()
}
}
impl BufferManager {
#[must_use]
pub fn new(store: Arc<dyn BlobStore>, capacity: usize) -> Self {
Self::new_with_budget_and_uninit_allocator(store, CacheBudget::memory(capacity), || {
unsafe { AlignedBlobBuf::uninit() }
})
}
#[must_use]
pub(crate) fn new_file<F>(store: Arc<dyn BlobStore>, capacity: usize, alloc_uninit: F) -> Self
where
F: Fn() -> AlignedBlobBuf + Send + Sync + 'static,
{
Self::new_with_budget_and_uninit_allocator(store, CacheBudget::file(capacity), alloc_uninit)
}
fn new_with_budget_and_uninit_allocator<F>(
store: Arc<dyn BlobStore>,
budget: CacheBudget,
alloc_uninit: F,
) -> Self
where
F: Fn() -> AlignedBlobBuf + Send + Sync + 'static,
{
let capacity = budget.blob_slots.max(1);
Self {
store,
alloc_uninit: Arc::new(alloc_uninit),
capacity,
read_pages: ReadPageCache::new(budget.read_page_bytes),
read_indexes: ReadIndexCache::new(budget.read_index_bytes),
read_index_tokens: DashMap::with_hasher(GuidBuildHasher),
read_index_token_clock: AtomicU64::new(1),
cache: DashMap::with_hasher(GuidBuildHasher),
admission: TinyLFU::new(),
route_resident: RouteResidency::new(capacity),
mutation: std::array::from_fn(|_| Mutex::new(MutationState::default())),
write_delta: WriteDelta::default(),
write_delta_flush: Mutex::new(()),
checkpoint_io: Mutex::new(()),
delete_fence_total: AtomicUsize::new(0),
compact_candidate_cursor: AtomicUsize::new(0),
merge_candidate_cursor: AtomicUsize::new(0),
compact_candidate_total: AtomicUsize::new(0),
merge_candidate_total: AtomicUsize::new(0),
clock: AtomicU64::new(1),
telemetry: Telemetry::default(),
current_epoch: AtomicU64::new(1),
fork_barrier: AtomicU64::new(0),
snapshots: Mutex::new(SnapshotState::default()),
gc_epoch: AtomicU64::new(0),
physical_gc: Mutex::new(()),
gc_reclaimed_count: AtomicU64::new(0),
gc_last_full_sweep_deferred_count: AtomicUsize::new(0),
checkpoint_waker: Mutex::new(None),
}
}
fn alloc_blob_buf_uninit(&self) -> AlignedBlobBuf {
(self.alloc_uninit)()
}
pub(crate) fn enter_checkpoint_io(&self) -> MutexGuard<'_, ()> {
self.checkpoint_io
.lock()
.expect("checkpoint I/O lock poisoned")
}
pub(crate) fn clock_tick(&self) -> u64 {
self.clock.load(Ordering::Relaxed)
}
pub(crate) fn cache_excess(&self) -> usize {
self.cache.len().saturating_sub(self.capacity)
}
pub(crate) fn route_resident_count(&self) -> usize {
self.route_resident.len()
}
pub(crate) fn mark_route_resident(&self, guid: BlobGuid) {
let tick = self.clock.fetch_add(1, Ordering::Relaxed);
for _ in 0..self.route_resident.mark(guid, tick) {
self.telemetry.note_route_resident_demotion();
}
}
fn is_route_resident(&self, guid: BlobGuid) -> bool {
self.route_resident.contains(guid)
}
pub(crate) fn snapshot_entries(&self) -> Vec<(BlobGuid, Arc<CachedBlob>)> {
self.cache
.iter()
.map(|kv| (*kv.key(), Arc::clone(kv.value())))
.collect()
}
fn decrement_candidate_totals(&self, removed: (bool, bool)) {
if removed.0 {
self.compact_candidate_total.fetch_sub(1, Ordering::Relaxed);
}
if removed.1 {
self.merge_candidate_total.fetch_sub(1, Ordering::Relaxed);
}
}
pub(crate) fn try_evict_cold(&self, guid: BlobGuid) -> bool {
if self.is_route_resident(guid) {
self.telemetry.note_eviction_skip_route_resident();
return false;
}
{
let state = self.mutation_shard(guid).lock().unwrap();
if state.is_protected_or_pending(&guid) {
self.telemetry.note_eviction_skip_protected();
return false;
}
}
let removed = self
.cache
.remove_if(&guid, |_, entry| {
if self.is_route_resident(guid) {
self.telemetry.note_eviction_skip_route_resident();
return false;
}
if Arc::strong_count(entry) > 1 {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
let removable = !state.is_protected_or_pending(&guid);
if !removable {
self.telemetry.note_eviction_skip_protected();
}
removable
})
.is_some();
if removed {
self.telemetry.note_cache_eviction();
}
removed
}
#[must_use]
pub(crate) fn cached_count(&self) -> usize {
self.cache.len()
}
pub(crate) fn stats(&self) -> BufferStats {
let read_index_cache = self.read_indexes.snapshot();
let read_page_cache = self.read_pages.snapshot();
BufferStats {
dirty_count: self.dirty_count(),
pending_delete_count: self.pending_delete_count(),
gc_orphan_backlog_count: self.gc_orphan_backlog_count(),
gc_reclaimed_count: self.gc_reclaimed_count.load(Ordering::Relaxed),
gc_last_full_sweep_deferred_count: self
.gc_last_full_sweep_deferred_count
.load(Ordering::Relaxed),
read_index_token_count: self.read_index_tokens.len(),
read_index_cache_entries: read_index_cache.entries,
read_index_cache_bytes: read_index_cache.bytes,
read_index_cache_budget_bytes: read_index_cache.budget_bytes,
read_page_cache_entries: read_page_cache.entries,
read_page_cache_bytes: read_page_cache.bytes,
read_page_cache_ghost_entries: read_page_cache.ghost_entries,
read_page_cache_budget_bytes: read_page_cache.budget_bytes,
cache_hits: self.telemetry.cache_hits(),
cache_misses: self.telemetry.cache_misses(),
full_blob_reads: self.telemetry.full_blob_reads(),
full_blob_read_bytes: self.telemetry.full_blob_reads() * PAGE_SIZE as u64,
point_full_blob_reads: self.telemetry.point_full_blob_reads(),
scan_full_blob_reads: self.telemetry.scan_full_blob_reads(),
silent_full_blob_reads: self.telemetry.silent_full_blob_reads(),
read_page_hits: self.telemetry.read_page_hits(),
read_page_misses: self.telemetry.read_page_misses(),
read_index_cache_hits: self.telemetry.read_index_cache_hits(),
read_index_cache_misses: self.telemetry.read_index_cache_misses(),
read_index_loads: self.telemetry.read_index_loads(),
read_index_dir_read_bytes: self.telemetry.read_index_dir_read_bytes(),
read_index_bucket_reads: self.telemetry.read_index_bucket_reads(),
read_index_bucket_read_bytes: self.telemetry.read_index_bucket_read_bytes(),
read_index_inline_hits: self.telemetry.read_index_inline_hits(),
read_index_value_hits: self.telemetry.read_index_value_hits(),
read_index_value_read_bytes: self.telemetry.read_index_value_read_bytes(),
read_index_offset_hits: self.telemetry.read_index_offset_hits(),
read_index_negative_hits: self.telemetry.read_index_negative_hits(),
read_index_crossing_hits: self.telemetry.read_index_crossing_hits(),
read_index_unknowns: self.telemetry.read_index_unknowns(),
optimistic_restarts: self.telemetry.optimistic_restarts(),
range_restarts: self.telemetry.range_restarts(),
walker_ops: self.telemetry.walker_ops(),
walker_blob_hops: self.telemetry.walker_blob_hops(),
max_blob_hops: self.telemetry.max_blob_hops(),
max_cross_blob_depth: self.telemetry.max_cross_blob_depth(),
spillovers: self.telemetry.spillover_count(),
merges: self.telemetry.merge_count(),
route_resident_count: self.route_resident_count(),
route_resident_demotions: self.telemetry.route_resident_demotions(),
cache_evictions: self.telemetry.cache_evictions(),
eviction_skips_protected: self.telemetry.eviction_skips_protected(),
eviction_skips_route_resident: self.telemetry.eviction_skips_route_resident(),
admission_protects: self.telemetry.admission_protects(),
write_delta_count: self.write_delta.len(),
store: self.store.store_stats(),
}
}
pub(crate) fn note_optimistic_restart(&self) {
self.telemetry.note_optimistic_restart();
}
pub(crate) fn note_range_restart(&self) {
self.telemetry.note_range_restart();
}
pub(crate) fn note_walker_blob_hops(&self, hops: u64, max_cross_blob_depth: usize) {
self.telemetry
.note_walker_blob_hops(hops, max_cross_blob_depth);
}
pub(crate) fn note_spillover(&self) {
self.telemetry.note_spillover();
}
pub(crate) fn note_merges(&self, merged: u64) {
self.telemetry.note_merges(merged);
}
fn get_cached_with_access(&self, guid: BlobGuid, access: PinAccess) -> Option<Arc<CachedBlob>> {
let Some(entry) = self.cache.get(&guid) else {
if !matches!(access, PinAccess::Silent) {
self.telemetry.note_cache_miss();
}
if matches!(access, PinAccess::Point) {
self.admission.record(guid);
}
return None;
};
let arc = Arc::clone(entry.value());
drop(entry);
match access {
PinAccess::Point => {
self.admission.record(guid);
let tick = self.clock.fetch_add(1, Ordering::Relaxed);
arc.last_touched.store(tick, Ordering::Relaxed);
self.telemetry.note_cache_hit();
}
PinAccess::Scan => {
self.telemetry.note_cache_hit();
}
PinAccess::Silent => {}
}
Some(arc)
}
fn mutation_shard(&self, guid: BlobGuid) -> &Mutex<MutationState> {
&self.mutation[bookkeeping_shard_idx(&guid)]
}
fn is_pending_delete(&self, guid: BlobGuid) -> bool {
if self.delete_fence_total.load(Ordering::Acquire) == 0 {
return false;
}
self.mutation_shard(guid)
.lock()
.unwrap()
.has_delete_fence(&guid)
}
pub(crate) fn has_delete_fence(&self, guid: BlobGuid) -> bool {
self.is_pending_delete(guid)
}
fn pending_delete_not_found(guid: BlobGuid) -> Error {
Error::BlobStoreIo(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("blob {:02x?} is pending delete", &guid[..4]),
))
}
fn insert_owned_into_cache(
&self,
guid: BlobGuid,
contents: AlignedBlobBuf,
access: PinAccess,
) -> Arc<CachedBlob> {
let hot_tick =
matches!(access, PinAccess::Point).then(|| self.clock.fetch_add(1, Ordering::Relaxed));
let inserted = self.cache.entry(guid).or_insert_with(|| {
let entry = Arc::new(CachedBlob::new(contents));
entry
.last_touched
.store(hot_tick.unwrap_or(0), Ordering::Relaxed);
entry
});
let entry = Arc::clone(inserted.value());
if let Some(tick) = hot_tick {
entry.last_touched.store(tick, Ordering::Relaxed);
}
drop(inserted);
const RETRY_BUDGET: u32 = 8;
let mut retries_left = RETRY_BUDGET;
let mut entry_spins = self.cache.len();
while self.cache.len() > self.capacity {
let evicted = match access {
PinAccess::Point => self.try_evict_for_point_insert(guid),
PinAccess::Scan | PinAccess::Silent => self.try_evict_scan_cold(),
};
if evicted {
entry_spins = self.cache.len();
continue;
}
if retries_left == 0 || entry_spins == 0 {
break;
}
std::thread::yield_now();
retries_left -= 1;
entry_spins = entry_spins.saturating_sub(1);
}
entry
}
fn try_evict_for_point_insert(&self, candidate: BlobGuid) -> bool {
self.try_evict_until(
u64::MAX,
Some((candidate, self.admission.estimate(candidate))),
)
}
fn try_evict_scan_cold(&self) -> bool {
self.try_evict_until(0, None)
}
fn try_evict_until(&self, max_last_touched: u64, candidate: Option<(BlobGuid, u8)>) -> bool {
let protected_snap: std::collections::HashSet<BlobGuid> = {
let mut out = std::collections::HashSet::new();
for shard in &self.mutation {
let state = shard.lock().unwrap();
out.extend(state.dirty.keys().copied());
out.extend(state.flushing.keys().copied());
out.extend(state.pending_deletes.keys().copied());
}
out
};
let mut victim: Option<(BlobGuid, u8, u64)> = None;
for kv in &self.cache {
if Arc::strong_count(kv.value()) > 1 {
continue;
}
let guid = *kv.key();
if protected_snap.contains(&guid) {
self.telemetry.note_eviction_skip_protected();
continue;
}
if self.is_route_resident(guid) {
self.telemetry.note_eviction_skip_route_resident();
continue;
}
let tick = kv.value().last_touched.load(Ordering::Relaxed);
if tick > max_last_touched {
continue;
}
let freq = if candidate.is_some() {
self.admission.estimate(guid)
} else {
0
};
match victim {
None => victim = Some((guid, freq, tick)),
Some((_, vfreq, vtick)) if (freq, tick) < (vfreq, vtick) => {
victim = Some((guid, freq, tick));
}
_ => {}
}
}
if let (Some((candidate_guid, candidate_freq)), Some((victim_guid, victim_freq, _))) =
(candidate, victim)
{
if victim_guid != candidate_guid && victim_freq > candidate_freq {
self.telemetry.note_admission_protect();
return false;
}
}
if let Some((guid, _, _)) = victim {
let removed = self
.cache
.remove_if(&guid, |_, e| {
if self.is_route_resident(guid) {
self.telemetry.note_eviction_skip_route_resident();
return false;
}
if Arc::strong_count(e) > 1 {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
let removable = !state.is_protected_or_pending(&guid);
if !removable {
self.telemetry.note_eviction_skip_protected();
}
removable
})
.is_some();
if removed {
self.telemetry.note_cache_eviction();
}
return removed;
}
false
}
#[cfg(test)]
fn evict_from_cache(&self, guid: BlobGuid) -> bool {
{
let state = self.mutation_shard(guid).lock().unwrap();
if state.checkpoint_owned_or_pending(&guid) {
return false;
}
}
if let Some((_, entry)) = self.cache.remove_if(&guid, |_, entry| {
if Arc::strong_count(entry) > 1 {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
!state.checkpoint_owned_or_pending(&guid)
}) {
entry.clear_dirty_hint();
} else if self.cache.contains_key(&guid) {
return false;
}
self.route_resident.remove(guid);
let mut state = self.mutation_shard(guid).lock().unwrap();
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.decrement_candidate_totals(removed);
true
}
#[cfg(test)]
pub(crate) fn evict_from_cache_for_test(&self, guid: BlobGuid) -> bool {
self.evict_from_cache(guid)
}
pub fn pin(&self, guid: BlobGuid) -> Result<Arc<CachedBlob>> {
self.pin_with_access(guid, PinAccess::Point)
}
pub(crate) fn pin_cached(&self, guid: BlobGuid) -> Result<Option<Arc<CachedBlob>>> {
self.pin_resident_stable(guid, PinAccess::Point)
}
pub(crate) fn pin_scan(&self, guid: BlobGuid) -> Result<Arc<CachedBlob>> {
self.pin_with_access(guid, PinAccess::Scan)
}
pub(crate) fn pin_scan_many(&self, guids: &[BlobGuid]) -> Vec<Option<Arc<CachedBlob>>> {
let mut out: Vec<Option<Arc<CachedBlob>>> = Vec::with_capacity(guids.len());
let mut miss_guids: Vec<BlobGuid> = Vec::new();
let mut miss_slots: Vec<usize> = Vec::new();
for &guid in guids {
match self.pin_resident_stable(guid, PinAccess::Scan) {
Ok(Some(entry)) => {
out.push(Some(entry));
continue;
}
Err(_) => {
out.push(None);
continue;
}
Ok(None) => {}
}
out.push(None);
miss_slots.push(out.len() - 1);
miss_guids.push(guid);
}
if miss_guids.is_empty() {
return out;
}
let gc_epoch = self.gc_stable_read_epoch();
let mut bufs: Vec<AlignedBlobBuf> = (0..miss_guids.len())
.map(|_| self.alloc_blob_buf_uninit())
.collect();
let results = self.store.read_blobs(&miss_guids, &mut bufs);
for (i, (buf, res)) in bufs.into_iter().zip(results).enumerate() {
if res.is_err() {
continue;
}
self.note_full_blob_read(PinAccess::Scan);
let guid = miss_guids[i];
out[miss_slots[i]] = self
.insert_loaded_after_gc(guid, buf, PinAccess::Scan, gc_epoch)
.ok()
.flatten();
}
out
}
pub fn pin_silent(&self, guid: BlobGuid) -> Result<Arc<CachedBlob>> {
self.pin_with_access(guid, PinAccess::Silent)
}
fn pin_with_access(&self, guid: BlobGuid, access: PinAccess) -> Result<Arc<CachedBlob>> {
loop {
if let Some(entry) = self.pin_resident_stable(guid, access)? {
return Ok(entry);
}
let gc_epoch = self.gc_stable_read_epoch();
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
let mut scratch = self.alloc_blob_buf_uninit();
let read = self.store.read_blob(guid, &mut scratch);
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
if self.gc_raced_since(gc_epoch) {
continue;
}
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
read?;
self.note_full_blob_read(access);
return Ok(self.insert_owned_into_cache(guid, scratch, access));
}
}
fn pin_resident_stable(
&self,
guid: BlobGuid,
access: PinAccess,
) -> Result<Option<Arc<CachedBlob>>> {
loop {
let gc_epoch = self.gc_stable_read_epoch();
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
#[cfg(test)]
pause_resident_pin_after_fence_check();
let entry = self.get_cached_with_access(guid, access);
if self.gc_raced_since(gc_epoch) {
continue;
}
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
return Ok(entry);
}
}
fn insert_loaded_after_gc(
&self,
guid: BlobGuid,
contents: AlignedBlobBuf,
access: PinAccess,
captured_gc_epoch: u64,
) -> Result<Option<Arc<CachedBlob>>> {
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
if self.gc_raced_since(captured_gc_epoch) {
return Ok(None);
}
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
Ok(Some(self.insert_owned_into_cache(guid, contents, access)))
}
pub(crate) fn indexed_read_eligible(&self, guid: BlobGuid) -> bool {
self.read_index_eligible(guid, ReadIndexPolicy::PointRead)
}
pub(crate) fn read_blob_range(
&self,
guid: BlobGuid,
byte_offset: u64,
dst: &mut [u8],
) -> Result<()> {
loop {
let gc_epoch = self.gc_stable_read_epoch();
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
let read = self.store.read_blob_range(guid, byte_offset, dst);
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
if self.gc_raced_since(gc_epoch) {
continue;
}
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
return read;
}
}
pub(crate) fn read_page_cached(&self, guid: BlobGuid, page: u16, dst: &mut [u8]) -> bool {
if self.read_pages.fill(guid, page, dst) {
self.telemetry.note_read_page_hit();
true
} else {
self.telemetry.note_read_page_miss();
false
}
}
pub(crate) fn read_page_store(&self, guid: BlobGuid, page: u16, src: &[u8]) {
self.read_pages.put(guid, page, src);
}
pub(crate) fn read_leaf_page_store(&self, guid: BlobGuid, page: u16, src: &[u8]) {
self.read_pages.put_after_second_touch(guid, page, src);
}
pub(crate) fn read_index(&self, guid: BlobGuid) -> Option<(Arc<ReadIndex>, u64)> {
self.read_index_load(guid, ReadIndexPolicy::PointRead)
}
pub(crate) fn read_index_for_liveness(&self, guid: BlobGuid) -> Option<(Arc<ReadIndex>, u64)> {
self.read_index_load(guid, ReadIndexPolicy::Liveness)
}
fn read_index_load(
&self,
guid: BlobGuid,
policy: ReadIndexPolicy,
) -> Option<(Arc<ReadIndex>, u64)> {
if !self.read_index_eligible(guid, policy) {
return None;
}
if let Some(index) = self.read_indexes.get(guid) {
self.telemetry.note_read_index_cache_hit();
return self.read_index_token(guid).map(|token| (index, token));
}
self.telemetry.note_read_index_cache_miss();
let token = self.ensure_read_index_token(guid);
let mut bytes = vec![0; READ_INDEX_DIRECTORY_PROBE_BYTES];
if !self.store.read_index_range(guid, 0, &mut bytes).ok()? {
return None;
}
let directory_len = ReadIndex::directory_len(&bytes).ok()?;
let read_bytes = if directory_len > bytes.len() {
let mut rest = vec![0; directory_len - bytes.len()];
if !self
.store
.read_index_range(guid, bytes.len() as u64, &mut rest)
.ok()?
{
return None;
}
bytes.extend_from_slice(&rest);
bytes.len() as u64
} else {
bytes.truncate(directory_len);
READ_INDEX_DIRECTORY_PROBE_BYTES as u64
};
let index = ReadIndex::decode_directory(bytes).ok()?;
if !self.read_index_stamp_matches(guid, &index).ok()? {
self.read_indexes.invalidate(guid);
return None;
}
if self.read_index_token(guid) != Some(token) || !self.read_index_eligible(guid, policy) {
return None;
}
self.telemetry.note_read_index_load(read_bytes);
Some((self.read_indexes.insert(guid, index), token))
}
pub(crate) fn read_index_bucket(
&self,
guid: BlobGuid,
index: &ReadIndex,
user_key: &[u8],
dst: &mut Vec<u8>,
) -> Option<()> {
let (off, len) = index.bucket_range(user_key)?;
dst.resize(len as usize, 0);
self.telemetry.note_read_index_bucket_read(u64::from(len));
if len != 0
&& !self
.store
.read_index_range(guid, u64::from(off), dst.as_mut_slice())
.ok()?
{
return None;
}
Some(())
}
pub(crate) fn read_value_segment_range(
&self,
guid: BlobGuid,
byte_offset: u64,
dst: &mut [u8],
) -> Option<()> {
if !self
.store
.read_value_segment_range(guid, byte_offset, dst)
.ok()?
{
return None;
}
Some(())
}
pub(crate) fn note_read_index_inline_hit(&self) {
self.telemetry.note_read_index_inline_hit();
}
pub(crate) fn note_read_index_value_hit(&self, bytes: u64) {
self.telemetry.note_read_index_value_hit(bytes);
}
pub(crate) fn note_read_index_offset_hit(&self) {
self.telemetry.note_read_index_offset_hit();
}
pub(crate) fn note_read_index_negative_hit(&self) {
self.telemetry.note_read_index_negative_hit();
}
pub(crate) fn note_read_index_crossing_hit(&self) {
self.telemetry.note_read_index_crossing_hit();
}
pub(crate) fn note_read_index_unknown(&self) {
self.telemetry.note_read_index_unknown();
}
fn invalidate_indexed_reads(&self, guid: BlobGuid) {
self.bump_read_index_token(guid);
self.read_pages.invalidate(guid);
self.read_indexes.invalidate(guid);
}
pub(crate) fn read_index_token_valid(&self, guid: BlobGuid, token: u64) -> bool {
self.indexed_read_eligible(guid) && self.read_index_token(guid) == Some(token)
}
pub(crate) fn read_index_liveness_token_valid(&self, guid: BlobGuid, token: u64) -> bool {
self.read_index_eligible(guid, ReadIndexPolicy::Liveness)
&& self.read_index_token(guid) == Some(token)
}
fn read_index_eligible(&self, guid: BlobGuid, policy: ReadIndexPolicy) -> bool {
if self.store.needs_flush() || self.is_pending_delete(guid) {
return false;
}
if !self.store.has_blob(guid).unwrap_or(false) {
return false;
}
if policy == ReadIndexPolicy::PointRead && self.cache.contains_key(&guid) {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
!state.is_protected_or_pending(&guid)
}
fn read_index_token(&self, guid: BlobGuid) -> Option<u64> {
self.read_index_tokens
.get(&guid)
.map(|entry| entry.load(Ordering::Acquire))
}
fn ensure_read_index_token(&self, guid: BlobGuid) -> u64 {
if let Some(entry) = self.read_index_tokens.get(&guid) {
return entry.load(Ordering::Acquire);
}
let token = self.next_read_index_token();
let entry = self
.read_index_tokens
.entry(guid)
.or_insert_with(|| AtomicU64::new(token));
entry.load(Ordering::Acquire)
}
fn bump_read_index_token(&self, guid: BlobGuid) {
let token = self.next_read_index_token();
self.read_index_tokens
.entry(guid)
.or_insert_with(|| AtomicU64::new(token))
.store(token, Ordering::Release);
}
fn next_read_index_token(&self) -> u64 {
self.read_index_token_clock.fetch_add(1, Ordering::AcqRel) + 1
}
fn remove_read_index_token_if_unreachable(&self, guid: BlobGuid) {
if self.cache.contains_key(&guid) {
return;
}
{
let state = self.mutation_shard(guid).lock().unwrap();
if state.is_protected_or_pending(&guid) {
return;
}
}
if self.store.has_blob(guid).unwrap_or(true) {
return;
}
self.read_pages.invalidate(guid);
self.read_indexes.invalidate(guid);
self.read_index_tokens.remove(&guid);
}
fn read_index_stamp_matches(&self, guid: BlobGuid, index: &ReadIndex) -> Result<bool> {
let mut scratch = AlignedBlobBuf::zeroed();
self.store
.read_blob_range(guid, 0, &mut scratch.as_mut_slice()[..HEADER_SIZE as usize])?;
let frame = BlobFrameRef::wrap(scratch.as_slice());
Ok(ReadIndexStamp::new(frame.header()) == index.stamp())
}
fn note_full_blob_read(&self, access: PinAccess) {
match access {
PinAccess::Point => self.telemetry.note_point_full_blob_read(),
PinAccess::Scan => self.telemetry.note_scan_full_blob_read(),
PinAccess::Silent => self.telemetry.note_silent_full_blob_read(),
}
}
pub(crate) fn stage_write_delta_put(
&self,
tree_id: u64,
root_guid: BlobGuid,
key: &[u8],
value: &[u8],
seq: u64,
creates_key: bool,
) {
self.write_delta
.stage_put(tree_id, root_guid, key, value, seq, creates_key);
}
pub(crate) fn stage_write_delta_delete(
&self,
tree_id: u64,
root_guid: BlobGuid,
key: &[u8],
seq: u64,
) {
self.write_delta.stage_delete(tree_id, root_guid, key, seq);
}
pub(crate) fn lookup_write_delta(&self, tree_id: u64, key: &[u8]) -> Option<DeltaEntry> {
self.write_delta.get(tree_id, key)
}
pub(crate) fn write_delta_count(&self) -> usize {
self.write_delta.len()
}
pub(crate) fn write_delta_count_for_tree(&self, tree_id: u64) -> usize {
self.write_delta.tree_len(tree_id)
}
pub(crate) fn write_delta_key_set_count_for_tree(&self, tree_id: u64) -> usize {
self.write_delta.tree_key_set_len(tree_id)
}
pub(crate) fn write_delta_key_state(
&self,
tree_id: u64,
key: &[u8],
) -> Option<WriteDeltaKeyState> {
self.write_delta.get(tree_id, key).map(|entry| match entry {
DeltaEntry::Put { seq, .. } => WriteDeltaKeyState::Put { seq },
DeltaEntry::Delete { .. } => WriteDeltaKeyState::Delete,
})
}
pub(crate) fn flush_write_deltas(&self) -> Result<()> {
let _flush = self.write_delta_flush.lock().unwrap();
let ops = self.write_delta.begin_flush_all();
self.apply_write_delta_ops(ops)
}
pub(crate) fn flush_write_deltas_for_tree(&self, tree_id: u64) -> Result<()> {
let _flush = self.write_delta_flush.lock().unwrap();
let ops = self.write_delta.begin_flush_tree(tree_id);
self.apply_write_delta_ops(ops)
}
fn apply_write_delta_ops(&self, ops: Vec<DeltaOp>) -> Result<()> {
if ops.is_empty() {
return Ok(());
}
if let Err(e) = self.apply_write_delta_ops_inner(&ops) {
self.write_delta.abort_flush(ops);
return Err(e);
}
self.write_delta.finish_flush(&ops);
Ok(())
}
fn apply_write_delta_ops_inner(&self, ops: &[DeltaOp]) -> Result<()> {
let mut root_pins: HashMap<BlobGuid, Arc<CachedBlob>> = HashMap::new();
let mut i = 0usize;
while i < ops.len() {
let op = &ops[i];
let root_pin = match root_pins.entry(op.root_guid) {
Entry::Occupied(entry) => Arc::clone(entry.get()),
Entry::Vacant(entry) => Arc::clone(entry.insert(self.pin(op.root_guid)?)),
};
match &op.entry {
DeltaEntry::Put { .. } => {
let mut end = i + 1;
while end < ops.len()
&& ops[end].tree_id == op.tree_id
&& ops[end].root_guid == op.root_guid
&& matches!(ops[end].entry, DeltaEntry::Put { .. })
{
end += 1;
}
self.apply_write_delta_put_run(op.root_guid, &root_pin, &ops[i..end])?;
i = end;
}
DeltaEntry::Delete { seq } => {
let outcome = engine::erase_multi(
self,
&root_pin,
None,
engine::SearchKey::user(&op.key),
*seq,
)?;
if outcome.mutated && outcome.root_dirty {
self.mark_dirty_cached(op.root_guid, *seq, root_pin.as_ref());
}
i += 1;
}
}
}
Ok(())
}
fn apply_write_delta_put_run(
&self,
root_guid: BlobGuid,
root_pin: &Arc<CachedBlob>,
ops: &[DeltaOp],
) -> Result<()> {
let mut items = Vec::with_capacity(ops.len());
for op in ops {
let DeltaEntry::Put { value, seq, .. } = &op.entry else {
return Err(Error::Internal("write-delta put run contained non-put op"));
};
items.push(engine::InsertBatchItem::new(
engine::SearchKey::user(&op.key),
value,
*seq,
engine::InsertCondition::Always,
));
}
let mut applied = 0usize;
while applied < items.len() {
let outcome =
engine::insert_multi_batch_conditional(self, root_pin, None, &items[applied..])?;
if outcome.applied == 0 {
return Err(Error::Internal("write-delta batch flush made no progress"));
}
if outcome.root_dirty {
let dirty_seq = items[applied..applied + outcome.applied]
.iter()
.map(|item| item.seq)
.min()
.unwrap_or(u64::MAX);
self.mark_dirty_cached(root_guid, dirty_seq, root_pin.as_ref());
}
applied += outcome.applied;
}
Ok(())
}
pub fn mark_dirty(&self, guid: BlobGuid, seq: u64) {
let cached = self.get_cached_with_access(guid, PinAccess::Silent);
if self.mark_dirty_with_hint(guid, seq, cached.as_deref()) {
self.publish_parent_orphans(guid);
}
}
pub(crate) fn mark_dirty_cached(&self, guid: BlobGuid, seq: u64, entry: &CachedBlob) {
if self.mark_dirty_with_hint(guid, seq, Some(entry)) {
self.publish_parent_orphans(guid);
}
}
fn mark_dirty_with_hint(&self, guid: BlobGuid, seq: u64, cached: Option<&CachedBlob>) -> bool {
let Some(cached) = cached else {
return false;
};
self.invalidate_indexed_reads(guid);
let hint_covers_seq = !cached.dirty_hint_needs_map_publish(seq);
let mut state = self.mutation_shard(guid).lock().unwrap();
if state.has_logical_delete_fence(&guid) {
cached.clear_dirty_hint();
return false;
}
if hint_covers_seq && matches!(state.dirty.get(&guid), Some(cur) if *cur <= seq) {
return true;
}
if hint_covers_seq {
cached.clear_dirty_hint();
let _ = cached.dirty_hint_needs_map_publish(seq);
}
state
.dirty
.entry(guid)
.and_modify(|cur| *cur = (*cur).min(seq))
.or_insert(seq);
true
}
#[must_use]
pub fn snapshot_dirty(&self) -> HashMap<BlobGuid, u64> {
let mut out = HashMap::new();
for shard in &self.mutation {
let (claimed, logically_deleted) = {
let mut state = shard.lock().unwrap();
let snap = std::mem::take(&mut state.dirty);
let mut claimed = Vec::new();
let mut logically_deleted = Vec::new();
for (guid, seq) in snap {
if state.has_logical_delete_fence(&guid) {
logically_deleted.push(guid);
} else if state.gc_deleting.contains(&guid) {
state.dirty.insert(guid, seq);
} else {
state.add_flushing(guid);
claimed.push((guid, seq));
}
}
(claimed, logically_deleted)
};
for guid in logically_deleted {
if let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) {
entry.clear_dirty_hint();
}
}
for (guid, mut seq) in claimed {
if let Some(hinted_seq) = self
.get_cached_with_access(guid, PinAccess::Silent)
.and_then(|entry| entry.take_dirty_hint())
{
seq = seq.min(hinted_seq);
}
out.insert(guid, seq);
}
}
out
}
pub(crate) fn snapshot_dirty_versions(
&self,
snap: &HashMap<BlobGuid, u64>,
) -> Result<Vec<DirtySnapshotEntry>> {
let mut out = Vec::with_capacity(snap.len());
for (&guid, &seq) in snap {
let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) else {
return Err(Error::Internal(
"snapshot_dirty_versions: dirty entry lost cache image",
));
};
out.push(DirtySnapshotEntry {
guid,
expected_seq: seq,
content_version: entry.content_version(),
});
}
Ok(out)
}
pub fn restore_dirty(&self, entries: HashMap<BlobGuid, u64>) {
if entries.is_empty() {
return;
}
for (guid, t) in entries {
let cached = self.get_cached_with_access(guid, PinAccess::Silent);
if let Some(entry) = &cached {
let _ = entry.dirty_hint_needs_map_publish(t);
}
let mut state = self.mutation_shard(guid).lock().unwrap();
if state.has_logical_delete_fence(&guid) {
if let Some(entry) = cached {
entry.clear_dirty_hint();
}
state.remove_one_flushing(&guid);
continue;
}
state.remove_one_flushing(&guid);
state
.dirty
.entry(guid)
.and_modify(|cur| *cur = (*cur).min(t))
.or_insert(t);
}
}
#[must_use]
pub fn dirty_count(&self) -> usize {
self.mutation
.iter()
.map(|shard| shard.lock().unwrap().dirty.len())
.sum()
}
#[must_use]
pub(crate) fn flushing_count(&self) -> usize {
self.mutation
.iter()
.map(|shard| shard.lock().unwrap().flushing.values().sum::<usize>())
.sum()
}
pub fn mark_for_delete(&self, guid: BlobGuid, seq: u64) {
assert_ne!(
seq, STRUCTURAL_SEQ,
"STRUCTURAL_SEQ requires parent-scoped stage_structural_reclaim"
);
let mut state = self.mutation_shard(guid).lock().unwrap();
if let Some(seq_ref) = state.deleting.get_mut(&guid) {
*seq_ref = (*seq_ref).min(seq);
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.route_resident.remove(guid);
self.decrement_candidate_totals(removed);
return;
}
match state.pending_deletes.entry(guid) {
Entry::Occupied(mut entry) => {
let cur = entry.get_mut();
*cur = (*cur).min(seq);
}
Entry::Vacant(entry) => {
entry.insert(seq);
self.delete_fence_total.fetch_add(1, Ordering::AcqRel);
}
}
let keep_cached_for_flushing = state.flushing.contains_key(&guid);
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.route_resident.remove(guid);
self.decrement_candidate_totals(removed);
if keep_cached_for_flushing {
if let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) {
entry.clear_dirty_hint();
}
} else if let Some((_, entry)) = self
.cache
.remove_if(&guid, |_, entry| Arc::strong_count(entry) == 1)
{
entry.clear_dirty_hint();
} else if let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) {
entry.clear_dirty_hint();
}
}
#[must_use]
pub fn snapshot_pending_deletes(&self) -> HashMap<BlobGuid, u64> {
let mut out = HashMap::new();
for shard in &self.mutation {
let mut state = shard.lock().unwrap();
let pending = std::mem::take(&mut state.pending_deletes);
for (guid, seq) in &pending {
state
.deleting
.entry(*guid)
.and_modify(|cur| *cur = (*cur).min(*seq))
.or_insert(*seq);
}
out.extend(pending);
}
out
}
pub fn restore_pending_deletes(&self, entries: HashMap<BlobGuid, u64>) {
if entries.is_empty() {
return;
}
for (g, t) in entries {
let mut state = self.mutation_shard(g).lock().unwrap();
let mut seq = t;
let had_logical_fence = state.has_logical_delete_fence(&g);
if let Some(claimed) = state.deleting.remove(&g) {
seq = seq.min(claimed);
}
match state.pending_deletes.entry(g) {
Entry::Occupied(mut entry) => {
let cur = entry.get_mut();
*cur = (*cur).min(seq);
}
Entry::Vacant(entry) => {
entry.insert(seq);
if !had_logical_fence {
self.delete_fence_total.fetch_add(1, Ordering::AcqRel);
}
}
}
}
}
#[must_use]
pub fn pending_delete_count(&self) -> usize {
self.delete_fence_total.load(Ordering::Acquire)
}
pub(crate) fn note_compaction_candidate(&self, guid: BlobGuid) {
let mut state = self.mutation_shard(guid).lock().unwrap();
if !state.has_delete_fence(&guid) && state.compact_candidates.insert(guid) {
self.compact_candidate_total.fetch_add(1, Ordering::Relaxed);
}
}
pub(crate) fn note_merge_candidate(&self, guid: BlobGuid) {
let mut state = self.mutation_shard(guid).lock().unwrap();
if !state.has_delete_fence(&guid) && state.merge_candidates.insert(guid) {
self.merge_candidate_total.fetch_add(1, Ordering::Relaxed);
}
}
#[must_use]
pub(crate) fn pop_compaction_candidates(&self, limit: usize) -> Vec<BlobGuid> {
pop_candidate_batch(
&self.mutation,
&self.compact_candidate_cursor,
&self.compact_candidate_total,
CandidateKind::Compact,
limit,
)
}
#[must_use]
pub(crate) fn pop_merge_candidates(&self, limit: usize) -> Vec<BlobGuid> {
pop_candidate_batch(
&self.mutation,
&self.merge_candidate_cursor,
&self.merge_candidate_total,
CandidateKind::Merge,
limit,
)
}
#[must_use]
pub(crate) fn compaction_candidate_count(&self) -> usize {
self.compact_candidate_total.load(Ordering::Relaxed)
}
#[must_use]
pub(crate) fn merge_candidate_count(&self) -> usize {
self.merge_candidate_total.load(Ordering::Relaxed)
}
pub(crate) fn execute_pending_delete(&self, guid: BlobGuid, seq: u64) -> Result<bool> {
if seq == STRUCTURAL_SEQ {
return Err(Error::Internal(
"STRUCTURAL_SEQ bypassed parent-scoped staging",
));
}
{
let state = self.mutation_shard(guid).lock().unwrap();
if state.is_protected(&guid) {
return Ok(false);
}
}
if let Some((_, entry)) = self.cache.remove_if(&guid, |_, entry| {
if Arc::strong_count(entry) > 1 {
return false;
}
let state = self.mutation_shard(guid).lock().unwrap();
!state.is_protected(&guid)
}) {
entry.clear_dirty_hint();
} else if self.cache.contains_key(&guid) {
return Ok(false);
}
self.store.delete_blob(guid)?;
self.route_resident.remove(guid);
self.finish_pending_delete(guid);
self.remove_read_index_token_if_unreachable(guid);
Ok(true)
}
fn finish_pending_delete(&self, guid: BlobGuid) {
let mut state = self.mutation_shard(guid).lock().unwrap();
let had_claim = state.deleting.remove(&guid).is_some();
if had_claim && !state.pending_deletes.contains_key(&guid) {
self.delete_fence_total.fetch_sub(1, Ordering::AcqRel);
}
}
pub(crate) fn store_has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.store.has_blob(guid)
}
pub(crate) fn store_blob_guids(&self) -> Result<Vec<BlobGuid>> {
self.store.list_blobs()
}
pub(crate) fn store_has_durable_blob(&self, guid: BlobGuid) -> Result<bool> {
if !self.store.has_blob(guid)? {
return Ok(false);
}
Ok(!self.store.needs_flush())
}
pub(crate) fn has_unflushed_blob(&self, guid: BlobGuid) -> bool {
let state = self.mutation_shard(guid).lock().unwrap();
state.dirty.contains_key(&guid) || state.flushing.contains_key(&guid)
}
pub(crate) fn snapshot_bytes_if_version(
&self,
guid: BlobGuid,
content_version: u64,
) -> Result<Option<AlignedBlobBuf>> {
let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) else {
return Err(Error::Internal(
"snapshot_bytes_if_version: dirty entry lost cache image",
));
};
let buf = entry.read();
if entry.content_version() != content_version {
return Ok(None);
}
let mut out = self.alloc_blob_buf_uninit();
out.as_mut_slice().copy_from_slice(buf.as_slice());
Ok(Some(out))
}
#[must_use]
pub(crate) fn alloc_blob_buf_zeroed(&self) -> AlignedBlobBuf {
self.store.alloc_blob_buf_zeroed()
}
pub(crate) fn write_through_batch(
&self,
entries: &[WriteThroughEntry],
) -> Result<WriteThroughBatchReport> {
if entries.is_empty() {
return Ok(WriteThroughBatchReport {
statuses: Vec::new(),
});
}
let mut statuses = vec![WriteThroughStatus::Stale; entries.len()];
let write_indices: Vec<_> = entries
.iter()
.enumerate()
.filter_map(|(idx, entry)| match self.write_snapshot_is_current(entry) {
Ok(true) => Some(Ok(idx)),
Ok(false) => None,
Err(e) => Some(Err(e)),
})
.collect::<Result<Vec<_>>>()?;
let writes: Vec<_> = write_indices
.iter()
.map(|idx| (entries[*idx].guid, &entries[*idx].bytes))
.collect();
if !writes.is_empty() {
self.store.write_blobs_with_data_sync(&writes)?;
}
for idx in &write_indices {
let entry = &entries[*idx];
self.invalidate_indexed_reads(entry.guid);
self.try_publish_read_index(entry.guid, &entry.bytes);
}
for idx in write_indices {
let entry = &entries[idx];
if self.retire_write_through(entry.guid, entry.expected_seq, entry.content_version)? {
statuses[idx] = WriteThroughStatus::Written;
}
}
Ok(WriteThroughBatchReport { statuses })
}
fn write_snapshot_is_current(&self, entry: &WriteThroughEntry) -> Result<bool> {
let Some(version) = entry.content_version else {
return Ok(true);
};
let Some(cached) = self.get_cached_with_access(entry.guid, PinAccess::Silent) else {
return Err(Error::Internal(
"write_through_batch: flushing entry lost cache image",
));
};
Ok(cached.validate_content_version(version))
}
fn retire_write_through(
&self,
guid: BlobGuid,
expected_seq: u64,
content_version: Option<u64>,
) -> Result<bool> {
let cached =
if content_version.is_some() {
Some(self.get_cached_with_access(guid, PinAccess::Silent).ok_or(
Error::Internal("retire_write_through: flushing entry lost cache image"),
)?)
} else {
None
};
let mut state = self.mutation_shard(guid).lock().unwrap();
if let (Some(expected), Some(cached)) = (content_version, cached.as_ref()) {
if !cached.validate_content_version(expected) {
return Ok(false);
}
}
if expected_seq != STRUCTURAL_SEQ {
if let std::collections::hash_map::Entry::Occupied(e) = state.dirty.entry(guid) {
if *e.get() <= expected_seq {
e.remove();
}
}
}
state.remove_one_flushing(&guid);
let still_dirty = state.dirty.contains_key(&guid) || state.flushing.contains_key(&guid);
drop(state);
if !still_dirty {
if let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) {
entry.clear_dirty_hint();
}
}
Ok(true)
}
fn try_publish_read_index(&self, guid: BlobGuid, bytes: &AlignedBlobBuf) {
let frame = BlobFrameRef::wrap(bytes.as_slice());
let Ok(build) = ReadIndex::build(frame) else {
return;
};
if self
.store
.publish_read_index(guid, &build.index, &build.values)
.is_err()
{
self.read_indexes.invalidate(guid);
return;
}
let Ok(directory_len) = ReadIndex::directory_len(&build.index[..ReadIndex::HEADER_LEN])
else {
self.read_indexes.invalidate(guid);
return;
};
let Ok(index) = ReadIndex::decode_directory(build.index[..directory_len].to_vec()) else {
self.read_indexes.invalidate(guid);
return;
};
let _ = self.read_indexes.insert(guid, index);
}
pub(crate) fn flush_inner(&self) -> Result<()> {
self.store.flush()
}
fn write_blob_through_locked(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
loop {
let _stable = self.gc_stable_read_epoch();
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
if let Some(entry) = self.get_cached_with_access(guid, PinAccess::Silent) {
let mut cached = entry.write();
let captured_version = entry.content_version();
let _epoch = self.begin_gc_epoch();
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
self.invalidate_indexed_reads(guid);
self.store.write_blob(guid, src)?;
debug_assert_eq!(entry.content_version(), captured_version);
cached.as_mut_slice().copy_from_slice(src.as_slice());
entry.clear_dirty_hint();
let mut state = self.mutation_shard(guid).lock().unwrap();
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.decrement_candidate_totals(removed);
return Ok(());
}
let _epoch = self.begin_gc_epoch();
if self.cache.contains_key(&guid) {
continue;
}
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
self.invalidate_indexed_reads(guid);
self.store.write_blob(guid, src)?;
let mut state = self.mutation_shard(guid).lock().unwrap();
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.decrement_candidate_totals(removed);
return Ok(());
}
}
fn delete_blob_through_locked(&self, guid: BlobGuid) -> Result<()> {
let _epoch = self.begin_gc_epoch();
let claimed_dirty = {
let mut state = self.mutation_shard(guid).lock().unwrap();
if state.checkpoint_owned_or_pending(&guid) {
return Err(Error::Internal(
"delete_blob: checkpoint or delete fence owns blob",
));
}
let claimed_dirty = state.dirty.remove(&guid);
state.gc_deleting.insert(guid);
self.delete_fence_total.fetch_add(1, Ordering::AcqRel);
claimed_dirty
};
let detached_entry = self
.cache
.remove_if(&guid, |_, entry| Arc::strong_count(entry) == 1);
let cache_claim_failed = detached_entry.is_none() && self.cache.contains_key(&guid);
let result = (|| {
if cache_claim_failed {
return Err(Error::Internal(
"delete_blob: protected cache image cannot be evicted",
));
}
self.invalidate_indexed_reads(guid);
self.store.delete_blob(guid)?;
if let Some((_, entry)) = &detached_entry {
entry.clear_dirty_hint();
}
self.route_resident.remove(guid);
let mut state = self.mutation_shard(guid).lock().unwrap();
state.remove_unclaimed_dirty(&guid);
let removed = state.remove_maintenance_candidates(&guid);
drop(state);
self.decrement_candidate_totals(removed);
Ok(())
})();
let logical_delete_before_reinsert = if result.is_err() {
self.mutation_shard(guid)
.lock()
.unwrap()
.has_logical_delete_fence(&guid)
} else {
false
};
if result.is_err() && !logical_delete_before_reinsert {
if let Some((_, entry)) = &detached_entry {
self.cache.entry(guid).or_insert_with(|| Arc::clone(entry));
}
}
let claimed_entry = if result.is_err() {
detached_entry
.as_ref()
.map(|(_, entry)| Arc::clone(entry))
.or_else(|| self.get_cached_with_access(guid, PinAccess::Silent))
} else {
None
};
let mut state = self.mutation_shard(guid).lock().unwrap();
if state.gc_deleting.remove(&guid) {
self.delete_fence_total.fetch_sub(1, Ordering::AcqRel);
}
let logical_delete_owned = state.has_logical_delete_fence(&guid);
if result.is_err() {
if let (Some(seq), false) = (claimed_dirty, logical_delete_owned) {
if let Some(entry) = &claimed_entry {
let _ = entry.dirty_hint_needs_map_publish(seq);
}
state
.dirty
.entry(guid)
.and_modify(|current| *current = (*current).min(seq))
.or_insert(seq);
} else if logical_delete_owned {
if let Some(entry) = &claimed_entry {
entry.clear_dirty_hint();
}
}
}
drop(state);
if result.is_ok() {
self.remove_read_index_token_if_unreachable(guid);
}
result
}
pub(crate) fn install_new_blob(&self, guid: BlobGuid, mut bytes: AlignedBlobBuf, seq: u64) {
self.invalidate_indexed_reads(guid);
crate::layout::set_frame_created_epoch(
bytes.as_mut_slice(),
self.current_epoch.load(Ordering::Acquire),
);
let tick = self.clock.fetch_add(1, Ordering::Relaxed);
let entry = Arc::new(CachedBlob::new(bytes));
entry.last_touched.store(tick, Ordering::Relaxed);
self.cache.insert(guid, Arc::clone(&entry));
let _ = entry.dirty_hint_needs_map_publish(seq);
let mut state = self.mutation_shard(guid).lock().unwrap();
state
.dirty
.entry(guid)
.and_modify(|cur| *cur = (*cur).min(seq))
.or_insert(seq);
drop(entry);
}
}
impl BlobStore for BufferManager {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
let pin = self.pin(guid)?;
let bytes = pin.read();
dst.as_mut_slice().copy_from_slice(bytes.as_slice());
Ok(())
}
fn read_blob_range(&self, guid: BlobGuid, byte_offset: u64, dst: &mut [u8]) -> Result<()> {
let pin = self.pin(guid)?;
let bytes = pin.read();
let start = usize::try_from(byte_offset).map_err(|_| {
Error::BlobStoreIo(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"blob range offset exceeds addressable memory",
))
})?;
let end = start.checked_add(dst.len()).ok_or_else(|| {
Error::BlobStoreIo(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"blob range end overflow",
))
})?;
let src = bytes.as_slice().get(start..end).ok_or_else(|| {
Error::BlobStoreIo(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"blob range exceeds frame",
))
})?;
dst.copy_from_slice(src);
Ok(())
}
fn read_index_range(&self, guid: BlobGuid, byte_offset: u64, dst: &mut [u8]) -> Result<bool> {
loop {
let gc_epoch = self.gc_stable_read_epoch();
if !self.read_index_eligible(guid, ReadIndexPolicy::PointRead) {
return Ok(false);
}
let read = self.store.read_index_range(guid, byte_offset, dst);
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
if self.gc_raced_since(gc_epoch) {
continue;
}
if !self.read_index_eligible(guid, ReadIndexPolicy::PointRead) {
return Ok(false);
}
return read;
}
}
fn read_value_segment_range(
&self,
guid: BlobGuid,
byte_offset: u64,
dst: &mut [u8],
) -> Result<bool> {
loop {
let gc_epoch = self.gc_stable_read_epoch();
if !self.read_index_eligible(guid, ReadIndexPolicy::PointRead) {
return Ok(false);
}
let read = self.store.read_value_segment_range(guid, byte_offset, dst);
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
if self.gc_raced_since(gc_epoch) {
continue;
}
if !self.read_index_eligible(guid, ReadIndexPolicy::PointRead) {
return Ok(false);
}
return read;
}
}
fn publish_read_index(&self, guid: BlobGuid, bytes: &[u8], values: &[u8]) -> Result<()> {
let _checkpoint_io = self.enter_checkpoint_io();
let _epoch = self.begin_gc_epoch();
if self.is_pending_delete(guid) {
return Err(Self::pending_delete_not_found(guid));
}
self.invalidate_indexed_reads(guid);
self.store.publish_read_index(guid, bytes, values)
}
fn delete_read_index(&self, guid: BlobGuid) -> Result<()> {
let _checkpoint_io = self.enter_checkpoint_io();
let _epoch = self.begin_gc_epoch();
self.invalidate_indexed_reads(guid);
self.store.delete_read_index(guid)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
let _checkpoint_io = self.enter_checkpoint_io();
self.write_blob_through_locked(guid, src)
}
fn write_blobs(&self, writes: &[(BlobGuid, &AlignedBlobBuf)]) -> Result<()> {
let _checkpoint_io = self.enter_checkpoint_io();
for (guid, src) in writes {
self.write_blob_through_locked(*guid, src)?;
}
Ok(())
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
let _checkpoint_io = self.enter_checkpoint_io();
self.delete_blob_through_locked(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
self.store.list_blobs()
}
fn flush(&self) -> Result<()> {
let _checkpoint_io = self.enter_checkpoint_io();
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
self.store.flush()
}
fn needs_flush(&self) -> bool {
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
self.store.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
if self.is_pending_delete(guid) {
return Ok(false);
}
if self.cache.contains_key(&guid) {
return Ok(true);
}
self.store.has_blob(guid)
}
fn store_stats(&self) -> StoreStats {
let _gc = self.physical_gc.lock().expect("physical GC lock poisoned");
self.store.store_stats()
}
fn vacuum(&self) -> Result<VacuumStats> {
let _checkpoint_io = self.enter_checkpoint_io();
let _epoch = self.begin_gc_epoch();
self.store.vacuum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::blob_store::{FileBlobStore, MemoryBlobStore};
use crate::store::{BlobFrame, PAGE_4K};
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::sync::Barrier;
fn make_buf(byte_at_100: u8) -> AlignedBlobBuf {
let mut b = AlignedBlobBuf::zeroed();
b.as_mut_slice()[100] = byte_at_100;
b
}
fn snapshot_current_bytes(bm: &BufferManager, guid: BlobGuid) -> AlignedBlobBuf {
let pin = bm.pin(guid).expect("test blob must be pinnable");
bm.snapshot_bytes_if_version(guid, pin.content_version())
.expect("test snapshot must keep its cache image")
.expect("test snapshot version must stay current")
}
fn persist_all_dirty_for_test(bm: &BufferManager) {
let dirty = bm.snapshot_dirty();
let versioned = bm.snapshot_dirty_versions(&dirty).unwrap();
let entries: Vec<_> = versioned
.into_iter()
.map(|snapshot| WriteThroughEntry {
guid: snapshot.guid,
bytes: bm
.snapshot_bytes_if_version(snapshot.guid, snapshot.content_version)
.unwrap()
.unwrap(),
expected_seq: snapshot.expected_seq,
content_version: Some(snapshot.content_version),
})
.collect();
let _checkpoint_io = bm.enter_checkpoint_io();
let report = bm.write_through_batch(&entries).unwrap();
assert!(report
.statuses
.iter()
.all(|status| *status == WriteThroughStatus::Written),);
bm.flush_inner().unwrap();
}
struct FlushPendingStore {
inner: MemoryBlobStore,
pending: AtomicBool,
}
impl FlushPendingStore {
fn new() -> Self {
Self {
inner: MemoryBlobStore::new(),
pending: AtomicBool::new(false),
}
}
}
impl BlobStore for FlushPendingStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn read_blobs(&self, guids: &[BlobGuid], dsts: &mut [AlignedBlobBuf]) -> Vec<Result<()>> {
self.inner.read_blobs(guids, dsts)
}
fn read_blob_range(&self, guid: BlobGuid, byte_offset: u64, dst: &mut [u8]) -> Result<()> {
self.inner.read_blob_range(guid, byte_offset, dst)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
self.pending.store(true, Ordering::Release);
self.inner.write_blob(guid, src)
}
fn write_blobs(&self, writes: &[(BlobGuid, &AlignedBlobBuf)]) -> Result<()> {
self.pending.store(true, Ordering::Release);
self.inner.write_blobs(writes)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.pending.store(true, Ordering::Release);
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()?;
self.pending.store(false, Ordering::Release);
Ok(())
}
fn needs_flush(&self) -> bool {
self.pending.load(Ordering::Acquire) || self.inner.needs_flush()
}
}
struct BlockingReadStore {
inner: MemoryBlobStore,
block_once: AtomicBool,
entered: Barrier,
release: Barrier,
}
impl BlockingReadStore {
fn new(inner: MemoryBlobStore) -> Self {
Self {
inner,
block_once: AtomicBool::new(true),
entered: Barrier::new(2),
release: Barrier::new(2),
}
}
}
struct BlockingWriteStore {
inner: MemoryBlobStore,
block_once: AtomicBool,
entered: Barrier,
release: Barrier,
}
impl BlockingWriteStore {
fn new(inner: MemoryBlobStore) -> Self {
Self {
inner,
block_once: AtomicBool::new(true),
entered: Barrier::new(2),
release: Barrier::new(2),
}
}
}
impl BlobStore for BlockingWriteStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
self.inner.write_blob(guid, src)
}
fn write_blobs_with_data_sync(&self, writes: &[(BlobGuid, &AlignedBlobBuf)]) -> Result<()> {
if self.block_once.swap(false, Ordering::AcqRel) {
self.entered.wait();
self.release.wait();
}
self.inner.write_blobs(writes)?;
self.inner.flush()
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.inner.has_blob(guid)
}
}
impl BlobStore for BlockingReadStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)?;
if self.block_once.swap(false, Ordering::AcqRel) {
self.entered.wait();
self.release.wait();
}
Ok(())
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
self.inner.write_blob(guid, src)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
}
struct BlockingTraitWriteStore {
inner: MemoryBlobStore,
block_once: AtomicBool,
entered: Barrier,
release: Barrier,
}
impl BlockingTraitWriteStore {
fn new(inner: MemoryBlobStore) -> Self {
Self {
inner,
block_once: AtomicBool::new(true),
entered: Barrier::new(2),
release: Barrier::new(2),
}
}
}
impl BlobStore for BlockingTraitWriteStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
if self.block_once.swap(false, Ordering::AcqRel) {
self.entered.wait();
self.release.wait();
}
self.inner.write_blob(guid, src)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.inner.has_blob(guid)
}
}
struct FailingMutationStore {
inner: MemoryBlobStore,
fail_writes: AtomicBool,
fail_deletes: AtomicBool,
block_delete_once: AtomicBool,
delete_entered: Barrier,
delete_release: Barrier,
}
impl FailingMutationStore {
fn new(inner: MemoryBlobStore) -> Self {
Self {
inner,
fail_writes: AtomicBool::new(false),
fail_deletes: AtomicBool::new(false),
block_delete_once: AtomicBool::new(false),
delete_entered: Barrier::new(2),
delete_release: Barrier::new(2),
}
}
}
impl BlobStore for FailingMutationStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
if self.fail_writes.load(Ordering::Acquire) {
return Err(Error::BlobStoreIo(std::io::Error::other(
"injected write failure",
)));
}
self.inner.write_blob(guid, src)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
if self.block_delete_once.swap(false, Ordering::AcqRel) {
self.delete_entered.wait();
self.delete_release.wait();
}
if self.fail_deletes.load(Ordering::Acquire) {
return Err(Error::BlobStoreIo(std::io::Error::other(
"injected delete failure",
)));
}
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.inner.has_blob(guid)
}
}
struct FailNthWriteStore {
inner: MemoryBlobStore,
fail_on: usize,
writes: AtomicUsize,
}
struct FailAfterDeleteStore {
inner: MemoryBlobStore,
fail_once: AtomicBool,
}
impl FailAfterDeleteStore {
fn new(inner: MemoryBlobStore) -> Self {
Self {
inner,
fail_once: AtomicBool::new(true),
}
}
}
impl BlobStore for FailAfterDeleteStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
self.inner.write_blob(guid, src)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_blob(guid)?;
if self.fail_once.swap(false, Ordering::AcqRel) {
return Err(Error::BlobStoreIo(std::io::Error::other(
"injected post-delete failure",
)));
}
Ok(())
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.inner.has_blob(guid)
}
}
impl FailNthWriteStore {
fn new(inner: MemoryBlobStore, fail_on: usize) -> Self {
Self {
inner,
fail_on,
writes: AtomicUsize::new(0),
}
}
}
impl BlobStore for FailNthWriteStore {
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
let ordinal = self.writes.fetch_add(1, Ordering::AcqRel) + 1;
if ordinal == self.fail_on {
return Err(Error::BlobStoreIo(std::io::Error::other(
"injected batch-prefix failure",
)));
}
self.inner.write_blob(guid, src)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.inner.has_blob(guid)
}
}
#[test]
fn cold_pin_does_not_reinsert_blob_after_gc_fence_retires() {
let guid = [0xA1; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(7)).unwrap();
let store = Arc::new(BlockingReadStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
let reader_bm = Arc::clone(&bm);
let reader = std::thread::spawn(move || reader_bm.pin(guid));
store.entered.wait();
let outcome = bm
.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.unwrap();
assert_eq!(outcome.freed, 1);
assert!(outcome.complete);
store.release.wait();
assert!(reader.join().unwrap().is_err());
assert_eq!(bm.cached_count(), 0, "stale read must not create a zombie");
assert!(!store.has_blob(guid).unwrap());
}
#[test]
fn cold_pin_retries_after_unrelated_gc_epoch_change() {
let target = [0xA6; 16];
let unreachable = [0xA7; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(target, &make_buf(7)).unwrap();
inner.write_blob(unreachable, &make_buf(8)).unwrap();
let store = Arc::new(BlockingReadStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
let reader_bm = Arc::clone(&bm);
let reader = std::thread::spawn(move || reader_bm.pin(target));
store.entered.wait();
let reachable = HashSet::from([target]);
let outcome = bm
.gc_sweep_unreachable_bounded(&reachable, usize::MAX)
.unwrap();
assert_eq!(outcome.freed, 1);
assert!(outcome.complete);
store.release.wait();
let pin = reader
.join()
.unwrap()
.expect("unrelated GC must be retried inside the cold pin");
assert_eq!(pin.read().as_slice()[100], 7);
assert!(store.has_blob(target).unwrap());
assert!(!store.has_blob(unreachable).unwrap());
}
#[test]
fn trait_full_read_does_not_publish_gc_zombie() {
let guid = [0xB1; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(7)).unwrap();
let store = Arc::new(BlockingReadStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
let reader_bm = Arc::clone(&bm);
let reader = std::thread::spawn(move || {
let mut dst = AlignedBlobBuf::zeroed();
BlobStore::read_blob(reader_bm.as_ref(), guid, &mut dst)
});
store.entered.wait();
let outcome = bm
.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.unwrap();
assert_eq!(outcome.freed, 1);
store.release.wait();
assert!(reader.join().unwrap().is_err());
assert_eq!(bm.cached_count(), 0);
assert!(!store.has_blob(guid).unwrap());
}
#[test]
fn trait_range_read_does_not_publish_gc_zombie() {
let guid = [0xB2; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(7)).unwrap();
let store = Arc::new(BlockingReadStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
let reader_bm = Arc::clone(&bm);
let reader = std::thread::spawn(move || {
let mut dst = [0u8; 8];
BlobStore::read_blob_range(reader_bm.as_ref(), guid, 96, &mut dst)
});
store.entered.wait();
let outcome = bm
.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.unwrap();
assert_eq!(outcome.freed, 1);
store.release.wait();
assert!(reader.join().unwrap().is_err());
assert_eq!(bm.cached_count(), 0);
assert!(!store.has_blob(guid).unwrap());
}
#[test]
fn trait_range_read_observes_dirty_cache_image() {
let guid = [0xB3; 16];
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let pin = bm.pin(guid).unwrap();
{
let mut bytes = pin.write();
bytes.as_mut_slice()[100] = 9;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let mut dst = [0u8; 1];
BlobStore::read_blob_range(&bm, guid, 100, &mut dst).unwrap();
assert_eq!(dst, [9]);
}
#[test]
fn trait_write_serializes_with_gc_epoch() {
let guid = [0xB4; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(BlockingTraitWriteStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
drop(bm.pin(guid).unwrap());
let writer_bm = Arc::clone(&bm);
let writer = std::thread::spawn(move || {
BlobStore::write_blob(writer_bm.as_ref(), guid, &make_buf(9))
});
store.entered.wait();
let gc_bm = Arc::clone(&bm);
let (done_tx, done_rx) = std::sync::mpsc::sync_channel(1);
let gc = std::thread::spawn(move || {
let result = gc_bm.gc_sweep_unreachable_bounded(&HashSet::from([guid]), usize::MAX);
done_tx.send(result).unwrap();
});
assert!(
done_rx
.recv_timeout(std::time::Duration::from_millis(100))
.is_err(),
"GC must wait while the public write owns its odd physical epoch",
);
store.release.wait();
writer.join().unwrap().unwrap();
let outcome = done_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap()
.unwrap();
gc.join().unwrap();
assert_eq!(outcome.freed, 0);
let mut stored = AlignedBlobBuf::zeroed();
store.read_blob(guid, &mut stored).unwrap();
assert_eq!(stored.as_slice()[100], 9);
assert_eq!(bm.pin(guid).unwrap().read().as_slice()[100], 9);
}
#[test]
fn trait_write_failure_preserves_cached_bytes_and_dirty_debt() {
let guid = [0xB5; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(FailingMutationStore::new(inner));
let bm = BufferManager::new(store.clone(), 4);
let pin = bm.pin(guid).unwrap();
{
let mut bytes = pin.write();
bytes.as_mut_slice()[100] = 7;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
drop(pin);
store.fail_writes.store(true, Ordering::Release);
assert!(BlobStore::write_blob(&bm, guid, &make_buf(9)).is_err());
assert_eq!(bm.pin(guid).unwrap().read().as_slice()[100], 7);
assert_eq!(bm.dirty_count(), 1);
let mut stored = AlignedBlobBuf::zeroed();
store.inner.read_blob(guid, &mut stored).unwrap();
assert_eq!(stored.as_slice()[100], 1);
}
#[test]
fn trait_delete_failure_restores_planner_hidden_dirty_and_reopens_latest_bytes() {
let guid = [0xB6; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(FailingMutationStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
let pin = bm.pin(guid).unwrap();
{
let mut bytes = pin.write();
bytes.as_mut_slice()[100] = 7;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
drop(pin);
store.fail_deletes.store(true, Ordering::Release);
store.block_delete_once.store(true, Ordering::Release);
let delete_bm = Arc::clone(&bm);
let delete = std::thread::spawn(move || BlobStore::delete_blob(delete_bm.as_ref(), guid));
store.delete_entered.wait();
assert!(
bm.snapshot_dirty().is_empty(),
"the public delete must hide its claimed dirty row from a planner",
);
store.delete_release.wait();
assert!(delete.join().unwrap().is_err());
assert_eq!(bm.pin(guid).unwrap().read().as_slice()[100], 7);
assert_eq!(bm.dirty_count(), 1);
assert_eq!(bm.pending_delete_count(), 0);
assert!(store.inner.has_blob(guid).unwrap());
persist_all_dirty_for_test(&bm);
drop(bm);
let store_dyn: Arc<dyn BlobStore> = store;
let reopened = BufferManager::new(store_dyn, 4);
assert_eq!(reopened.pin(guid).unwrap().read().as_slice()[100], 7);
}
#[test]
fn trait_delete_failure_defers_to_concurrent_logical_delete() {
let guid = [0xB9; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(FailingMutationStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
let pin = bm.pin(guid).unwrap();
{
let mut bytes = pin.write();
bytes.as_mut_slice()[100] = 7;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
drop(pin);
store.fail_deletes.store(true, Ordering::Release);
store.block_delete_once.store(true, Ordering::Release);
let delete_bm = Arc::clone(&bm);
let delete = std::thread::spawn(move || BlobStore::delete_blob(delete_bm.as_ref(), guid));
store.delete_entered.wait();
bm.mark_for_delete(guid, 20);
assert_eq!(
bm.pending_delete_count(),
2,
"generic and logical delete fences must be counted independently",
);
store.delete_release.wait();
assert!(delete.join().unwrap().is_err());
assert_eq!(bm.dirty_count(), 0, "logical deletion owns the old bytes");
assert_eq!(bm.pending_delete_count(), 1);
let pending = bm.snapshot_pending_deletes();
assert_eq!(pending.get(&guid), Some(&20));
bm.restore_pending_deletes(pending);
assert_eq!(bm.pending_delete_count(), 1);
assert!(store.inner.has_blob(guid).unwrap());
}
#[test]
fn restored_logical_delete_counts_independently_from_transient_gc() {
let guid = [0xBE; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(FailingMutationStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
drop(bm.pin(guid).unwrap());
store.fail_deletes.store(true, Ordering::Release);
store.block_delete_once.store(true, Ordering::Release);
let delete_bm = Arc::clone(&bm);
let transient_delete =
std::thread::spawn(move || BlobStore::delete_blob(delete_bm.as_ref(), guid));
store.delete_entered.wait();
assert_eq!(bm.pending_delete_count(), 1);
bm.restore_pending_deletes(HashMap::from([(guid, 17)]));
assert_eq!(
bm.pending_delete_count(),
2,
"logical and transient fences require separate references",
);
store.delete_release.wait();
assert!(transient_delete.join().unwrap().is_err());
assert_eq!(bm.pending_delete_count(), 1);
let claimed = bm.snapshot_pending_deletes();
assert_eq!(claimed.get(&guid), Some(&17));
store.fail_deletes.store(false, Ordering::Release);
assert!(bm.execute_pending_delete(guid, 17).unwrap());
assert_eq!(bm.pending_delete_count(), 0);
}
#[test]
fn trait_delete_success_never_returns_detached_resident_pin() {
let guid = [0xBA; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(FailingMutationStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
drop(bm.pin(guid).unwrap());
let pin_barrier = Arc::new(ResidentPinBarrier::new());
let reader_bm = Arc::clone(&bm);
let reader_barrier = Arc::clone(&pin_barrier);
let reader = std::thread::spawn(move || {
set_resident_pin_barrier_for_current_thread(reader_barrier);
reader_bm.pin(guid)
});
pin_barrier.entered.wait();
store.block_delete_once.store(true, Ordering::Release);
let delete_bm = Arc::clone(&bm);
let delete = std::thread::spawn(move || BlobStore::delete_blob(delete_bm.as_ref(), guid));
store.delete_entered.wait();
pin_barrier.release.wait();
store.delete_release.wait();
delete.join().unwrap().unwrap();
assert!(
reader.join().unwrap().is_err(),
"a reader that passed the first fence check must not return the detached Arc",
);
assert!(!store.inner.has_blob(guid).unwrap());
assert_eq!(bm.cached_count(), 0);
}
#[test]
fn trait_delete_failure_reinstalls_resident_before_reader_retry() {
let guid = [0xBB; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(3)).unwrap();
let store = Arc::new(FailingMutationStore::new(inner));
let bm = Arc::new(BufferManager::new(store.clone(), 4));
drop(bm.pin(guid).unwrap());
let pin_barrier = Arc::new(ResidentPinBarrier::new());
let reader_bm = Arc::clone(&bm);
let reader_barrier = Arc::clone(&pin_barrier);
let reader = std::thread::spawn(move || {
set_resident_pin_barrier_for_current_thread(reader_barrier);
reader_bm.pin(guid)
});
pin_barrier.entered.wait();
store.fail_deletes.store(true, Ordering::Release);
store.block_delete_once.store(true, Ordering::Release);
let delete_bm = Arc::clone(&bm);
let delete = std::thread::spawn(move || BlobStore::delete_blob(delete_bm.as_ref(), guid));
store.delete_entered.wait();
pin_barrier.release.wait();
store.delete_release.wait();
assert!(delete.join().unwrap().is_err());
let pin = reader
.join()
.unwrap()
.expect("failed delete must make the reinstated resident visible on retry");
assert_eq!(pin.read().as_slice()[100], 3);
assert!(store.inner.has_blob(guid).unwrap());
}
#[test]
fn trait_delete_protected_writer_keeps_gc_fence_dirty_debt_through_reopen() {
let guid = [0xBC; 16];
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = Arc::new(BufferManager::new(inner.clone(), 4));
let pin = bm.pin(guid).unwrap();
{
let mut bytes = pin.write();
bytes.as_mut_slice()[100] = 7;
}
let cache_ref = bm.cache.get(&guid).unwrap();
let delete_bm = Arc::clone(&bm);
let delete = std::thread::spawn(move || BlobStore::delete_blob(delete_bm.as_ref(), guid));
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while bm.pending_delete_count() == 0 {
assert!(std::time::Instant::now() < deadline, "delete fence timeout");
std::thread::yield_now();
}
bm.mark_dirty_cached(guid, 30, pin.as_ref());
assert!(
bm.snapshot_dirty().is_empty(),
"planner must leave transient-GC dirty rows in place",
);
assert_eq!(bm.dirty_count(), 1);
drop(cache_ref);
assert!(delete.join().unwrap().is_err());
drop(pin);
assert_eq!(bm.pending_delete_count(), 0);
assert_eq!(bm.dirty_count(), 1);
persist_all_dirty_for_test(&bm);
drop(bm);
let reopened = BufferManager::new(inner, 4);
assert_eq!(reopened.pin(guid).unwrap().read().as_slice()[100], 7);
}
#[test]
fn reachability_gc_protected_writer_keeps_dirty_debt_through_reopen() {
let guid = [0xBD; 16];
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = Arc::new(BufferManager::new(inner.clone(), 4));
let pin = bm.pin(guid).unwrap();
let cache_ref = bm.cache.get(&guid).unwrap();
let gc_bm = Arc::clone(&bm);
let gc = std::thread::spawn(move || {
gc_bm.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
});
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while bm.pending_delete_count() == 0 {
assert!(std::time::Instant::now() < deadline, "GC fence timeout");
std::thread::yield_now();
}
{
let mut bytes = pin.write();
bytes.as_mut_slice()[100] = 9;
}
bm.mark_dirty_cached(guid, 31, pin.as_ref());
assert!(
bm.snapshot_dirty().is_empty(),
"planner must not claim a writer protected by transient GC",
);
assert_eq!(bm.dirty_count(), 1);
drop(cache_ref);
let outcome = gc.join().unwrap().unwrap();
assert_eq!(outcome.freed, 0);
assert!(!outcome.complete);
assert_eq!(bm.pending_delete_count(), 0);
assert_eq!(bm.cached_count(), 1);
assert!(inner.has_blob(guid).unwrap());
assert_eq!(bm.dirty_count(), 1);
drop(pin);
persist_all_dirty_for_test(&bm);
drop(bm);
let reopened = BufferManager::new(inner, 4);
assert_eq!(reopened.pin(guid).unwrap().read().as_slice()[100], 9);
}
#[test]
fn trait_batch_write_keeps_successful_prefix_cache_store_consistent() {
let first = [0xB7; 16];
let second = [0xB8; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(first, &make_buf(1)).unwrap();
inner.write_blob(second, &make_buf(2)).unwrap();
let store = Arc::new(FailNthWriteStore::new(inner, 2));
let bm = BufferManager::new(store.clone(), 4);
drop(bm.pin(first).unwrap());
drop(bm.pin(second).unwrap());
let first_new = make_buf(7);
let second_new = make_buf(8);
assert!(
BlobStore::write_blobs(&bm, &[(first, &first_new), (second, &second_new)]).is_err()
);
assert_eq!(bm.pin(first).unwrap().read().as_slice()[100], 7);
assert_eq!(bm.pin(second).unwrap().read().as_slice()[100], 2);
let mut stored = AlignedBlobBuf::zeroed();
store.inner.read_blob(first, &mut stored).unwrap();
assert_eq!(stored.as_slice()[100], 7);
store.inner.read_blob(second, &mut stored).unwrap();
assert_eq!(stored.as_slice()[100], 2);
}
#[test]
fn concurrent_gc_passes_keep_generation_serial_and_even() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xA2; 16], &make_buf(1)).unwrap();
inner.write_blob([0xA3; 16], &make_buf(2)).unwrap();
let bm = Arc::new(BufferManager::new(inner.clone(), 4));
let start = Arc::new(Barrier::new(3));
let mut workers = Vec::new();
for _ in 0..2 {
let bm = Arc::clone(&bm);
let start = Arc::clone(&start);
workers.push(std::thread::spawn(move || {
start.wait();
bm.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.unwrap()
.freed
}));
}
start.wait();
let total: usize = workers
.into_iter()
.map(|worker| worker.join().unwrap())
.sum();
assert_eq!(total, 2);
assert_eq!(bm.gc_read_epoch(), 4);
assert_eq!(bm.gc_read_epoch() & 1, 0);
assert!(inner.list_blobs().unwrap().is_empty());
}
#[test]
fn bounded_gc_reports_incomplete_until_all_candidates_are_visited() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xA8; 16], &make_buf(1)).unwrap();
inner.write_blob([0xA9; 16], &make_buf(2)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let first = bm.gc_sweep_unreachable_bounded(&HashSet::new(), 1).unwrap();
assert_eq!(first.freed, 1);
assert!(!first.complete);
assert_eq!(inner.list_blobs().unwrap().len(), 1);
assert_eq!(bm.stats().gc_last_full_sweep_deferred_count, 1);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 0);
assert_eq!(
bm.stats().gc_last_full_sweep_deferred_count,
1,
"an empty exact-reclaim batch must not clear full-sweep debt",
);
let second = bm.gc_sweep_unreachable_bounded(&HashSet::new(), 1).unwrap();
assert_eq!(second.freed, 1);
assert!(second.complete);
assert!(inner.list_blobs().unwrap().is_empty());
assert_eq!(bm.stats().gc_last_full_sweep_deferred_count, 0);
}
#[test]
fn gc_releases_read_index_tokens_after_delete_fence_retires() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for suffix in 0..64u8 {
let mut guid = [0xAB; 16];
guid[15] = suffix;
inner.write_blob(guid, &make_buf(suffix)).unwrap();
}
let bm = BufferManager::new(inner.clone(), 8);
let outcome = bm
.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.unwrap();
assert_eq!(outcome.freed, 64);
assert!(outcome.complete);
assert!(inner.list_blobs().unwrap().is_empty());
assert_eq!(
bm.read_index_tokens.len(),
0,
"successful GC must not retain per-GUID indexed-read tokens",
);
}
#[test]
fn gc_post_delete_error_still_releases_unreachable_index_token() {
let guid = [0xAC; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(7)).unwrap();
let store = Arc::new(FailAfterDeleteStore::new(inner));
let bm = BufferManager::new(store.clone(), 4);
drop(bm.pin(guid).unwrap());
let token = bm.ensure_read_index_token(guid);
assert_eq!(bm.read_index_tokens.len(), 1);
assert!(bm
.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.is_err());
assert!(!store.inner.has_blob(guid).unwrap());
assert_eq!(bm.cached_count(), 0);
assert_eq!(bm.pending_delete_count(), 0);
assert_eq!(bm.read_index_tokens.len(), 0);
assert!(!bm.read_index_token_valid(guid, token));
let retry = bm
.gc_sweep_unreachable_bounded(&HashSet::new(), usize::MAX)
.unwrap();
assert_eq!(retry.freed, 0);
assert!(retry.complete);
}
#[test]
fn deferred_fifo_skips_pinned_head_then_reclaims_later_candidates() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let pinned_guid = [0xA4; 16];
let free_guid = [0xA5; 16];
inner.write_blob(pinned_guid, &make_buf(1)).unwrap();
inner.write_blob(free_guid, &make_buf(2)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let pin = bm.pin(pinned_guid).unwrap();
bm.restore_retired_orphans([pinned_guid, free_guid]);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 0);
assert_eq!(bm.gc_orphan_backlog_count(), 2);
assert_eq!(bm.stats().gc_last_full_sweep_deferred_count, 0);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 1);
assert!(!inner.has_blob(free_guid).unwrap());
assert!(inner.has_blob(pinned_guid).unwrap());
assert_eq!(bm.gc_orphan_backlog_count(), 1);
assert_eq!(
bm.stats().gc_last_full_sweep_deferred_count,
0,
"exact reclaim must not overwrite the full-sweep gauge",
);
drop(pin);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 1);
assert_eq!(bm.gc_orphan_backlog_count(), 0);
assert_eq!(bm.stats().gc_last_full_sweep_deferred_count, 0);
assert!(!inner.has_blob(pinned_guid).unwrap());
}
struct CountingReadIndexStore {
inner: FileBlobStore,
index_reads: AtomicUsize,
}
impl CountingReadIndexStore {
fn open(path: &std::path::Path) -> Result<Self> {
Ok(Self {
inner: FileBlobStore::open(path)?,
index_reads: AtomicUsize::new(0),
})
}
fn index_reads(&self) -> usize {
self.index_reads.load(Ordering::Acquire)
}
fn reset_index_reads(&self) {
self.index_reads.store(0, Ordering::Release);
}
}
impl BlobStore for CountingReadIndexStore {
fn alloc_blob_buf_zeroed(&self) -> AlignedBlobBuf {
self.inner.alloc_blob_buf_zeroed()
}
fn read_blob(&self, guid: BlobGuid, dst: &mut AlignedBlobBuf) -> Result<()> {
self.inner.read_blob(guid, dst)
}
fn read_blobs(&self, guids: &[BlobGuid], dsts: &mut [AlignedBlobBuf]) -> Vec<Result<()>> {
self.inner.read_blobs(guids, dsts)
}
fn read_blob_range(&self, guid: BlobGuid, byte_offset: u64, dst: &mut [u8]) -> Result<()> {
self.inner.read_blob_range(guid, byte_offset, dst)
}
fn read_index_range(
&self,
guid: BlobGuid,
byte_offset: u64,
dst: &mut [u8],
) -> Result<bool> {
self.index_reads.fetch_add(1, Ordering::AcqRel);
self.inner.read_index_range(guid, byte_offset, dst)
}
fn read_value_segment_range(
&self,
guid: BlobGuid,
byte_offset: u64,
dst: &mut [u8],
) -> Result<bool> {
self.inner.read_value_segment_range(guid, byte_offset, dst)
}
fn publish_read_index(&self, guid: BlobGuid, bytes: &[u8], values: &[u8]) -> Result<()> {
self.inner.publish_read_index(guid, bytes, values)
}
fn delete_read_index(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_read_index(guid)
}
fn write_blob(&self, guid: BlobGuid, src: &AlignedBlobBuf) -> Result<()> {
self.inner.write_blob(guid, src)
}
fn write_blobs(&self, writes: &[(BlobGuid, &AlignedBlobBuf)]) -> Result<()> {
self.inner.write_blobs(writes)
}
fn write_blobs_with_data_sync(&self, writes: &[(BlobGuid, &AlignedBlobBuf)]) -> Result<()> {
self.inner.write_blobs_with_data_sync(writes)
}
fn delete_blob(&self, guid: BlobGuid) -> Result<()> {
self.inner.delete_blob(guid)
}
fn list_blobs(&self) -> Result<Vec<BlobGuid>> {
self.inner.list_blobs()
}
fn flush(&self) -> Result<()> {
self.inner.flush()
}
fn needs_flush(&self) -> bool {
self.inner.needs_flush()
}
fn has_blob(&self, guid: BlobGuid) -> Result<bool> {
self.inner.has_blob(guid)
}
fn store_stats(&self) -> StoreStats {
self.inner.store_stats()
}
}
#[test]
fn file_cache_budget_splits_cold_aux_inside_total() {
let budget = CacheBudget::file(256);
assert_eq!(budget.read_page_bytes, 8 * 1024 * 1024);
assert_eq!(budget.read_index_bytes, 56 * 1024 * 1024);
assert_eq!(budget.blob_slots, 128);
}
#[test]
fn small_file_cache_budget_preserves_blob_slots() {
let budget = CacheBudget::file(16);
assert_eq!(budget.read_page_bytes, 0);
assert_eq!(budget.read_index_bytes, 0);
assert_eq!(budget.blob_slots, 16);
}
#[test]
fn memory_cache_budget_disables_cold_aux() {
let budget = CacheBudget::memory(256);
assert_eq!(budget.read_page_bytes, 0);
assert_eq!(budget.read_index_bytes, 0);
assert_eq!(budget.blob_slots, 256);
}
#[test]
fn indexed_read_eligible_waits_for_store_flush() {
let guid = [0xCE; 16];
let inner = Arc::new(FlushPendingStore::new());
inner.write_blob(guid, &make_buf(7)).unwrap();
let store: Arc<dyn BlobStore> = inner.clone();
let bm = BufferManager::new_file(store, 128, AlignedBlobBuf::zeroed);
assert!(
!bm.indexed_read_eligible(guid),
"partial indexed reads must not bypass a store with unflushed data or manifest state",
);
inner.flush().unwrap();
assert!(
bm.indexed_read_eligible(guid),
"once the store is durable and the blob is not cached/protected, indexed reads may proceed",
);
}
#[test]
fn read_index_tokens_are_removed_after_final_delete() {
let guid = [0xC1; 16];
let inner = Arc::new(MemoryBlobStore::new());
inner.write_blob(guid, &make_buf(7)).unwrap();
let store: Arc<dyn BlobStore> = inner.clone();
let bm = BufferManager::new(store, 4);
let token = bm.ensure_read_index_token(guid);
assert!(bm.read_index_token_valid(guid, token));
assert_eq!(bm.stats().read_index_token_count, 1);
bm.delete_blob(guid).unwrap();
assert_eq!(bm.stats().read_index_token_count, 0);
assert!(!bm.read_index_token_valid(guid, token));
}
#[test]
fn reintroduced_guid_gets_fresh_read_index_token() {
let guid = [0xC2; 16];
let inner = Arc::new(MemoryBlobStore::new());
inner.write_blob(guid, &make_buf(1)).unwrap();
let store: Arc<dyn BlobStore> = inner.clone();
let bm = BufferManager::new(store, 4);
let first = bm.ensure_read_index_token(guid);
bm.delete_blob(guid).unwrap();
inner.write_blob(guid, &make_buf(2)).unwrap();
let second = bm.ensure_read_index_token(guid);
assert_ne!(first, second);
assert!(!bm.read_index_token_valid(guid, first));
assert!(bm.read_index_token_valid(guid, second));
}
#[test]
fn read_caches_after_first_load() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xAB; 16], &make_buf(7)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
assert_eq!(bm.cached_count(), 0);
let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob([0xAB; 16], &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], 7);
assert_eq!(bm.cached_count(), 1);
bm.read_blob([0xAB; 16], &mut dst).unwrap();
assert_eq!(bm.cached_count(), 1);
}
#[test]
fn pin_scan_many_returns_each_blob_in_order_and_none_for_missing() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..10u8 {
inner.write_blob([i; 16], &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 64);
let mut guids: Vec<BlobGuid> = (0..10u8).map(|i| [i; 16]).collect();
guids.insert(5, [0xFF; 16]);
let pins = bm.pin_scan_many(&guids);
assert_eq!(pins.len(), guids.len());
for (g, pin) in guids.iter().zip(&pins) {
if *g == [0xFF; 16] {
assert!(pin.is_none(), "missing guid must map to None");
} else {
let pin = pin.as_ref().expect("present guid must be pinned");
assert_eq!(pin.read().as_slice()[100], g[0]);
}
}
}
#[test]
fn pin_miss_is_not_counted_as_a_hit() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0xCD; 16];
inner.write_blob(guid, &make_buf(9)).unwrap();
let bm = BufferManager::new(inner, 4);
let first = bm.pin(guid).unwrap();
assert_eq!(first.read().as_slice()[100], 9);
drop(first);
let stats = bm.stats();
assert_eq!(stats.cache_misses, 1);
assert_eq!(stats.cache_hits, 0);
let second = bm.pin(guid).unwrap();
assert_eq!(second.read().as_slice()[100], 9);
let stats = bm.stats();
assert_eq!(stats.cache_misses, 1);
assert_eq!(stats.cache_hits, 1);
assert_eq!(stats.full_blob_reads, 1);
assert_eq!(stats.full_blob_read_bytes, PAGE_SIZE as u64);
}
#[test]
fn full_blob_reads_are_classified_by_access_path() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..3u8 {
let mut guid = [0u8; 16];
guid[0] = i;
inner.write_blob(guid, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 4);
drop(bm.pin([0; 16]).unwrap());
let mut scan = [0u8; 16];
scan[0] = 1;
drop(bm.pin_scan(scan).unwrap());
let mut silent = [0u8; 16];
silent[0] = 2;
drop(bm.pin_silent(silent).unwrap());
let stats = bm.stats();
assert_eq!(stats.full_blob_reads, 3);
assert_eq!(stats.full_blob_read_bytes, 3 * PAGE_SIZE as u64);
assert_eq!(stats.point_full_blob_reads, 1);
assert_eq!(stats.scan_full_blob_reads, 1);
assert_eq!(stats.silent_full_blob_reads, 1);
assert_eq!(
stats.cache_misses, 2,
"silent miss does not count as a public cache miss"
);
assert_eq!(stats.cache_hits, 0);
drop(bm.pin([0; 16]).unwrap());
let stats = bm.stats();
assert_eq!(
stats.full_blob_reads, 3,
"cache hits must not count as store reads"
);
assert_eq!(stats.cache_hits, 1);
}
#[test]
fn scan_misses_do_not_evict_hot_point_blob() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..5u8 {
let mut guid = [0u8; 16];
guid[0] = i;
inner.write_blob(guid, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 2);
let hot = [0u8; 16];
drop(bm.pin(hot).unwrap());
for i in 1..5u8 {
let mut guid = [0u8; 16];
guid[0] = i;
drop(bm.pin_scan(guid).unwrap());
}
assert_eq!(bm.cached_count(), 2);
assert!(
bm.cache.contains_key(&hot),
"scan-loaded blobs must stay colder than point-read blobs",
);
}
#[test]
fn scan_miss_may_overshoot_instead_of_evicting_only_hot_blob() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..2u8 {
let mut guid = [0u8; 16];
guid[0] = i;
inner.write_blob(guid, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 1);
let hot = [0u8; 16];
let mut scan = [0u8; 16];
scan[0] = 1;
drop(bm.pin(hot).unwrap());
drop(bm.pin_scan(scan).unwrap());
assert!(
bm.cache.contains_key(&hot),
"scan miss must not evict the only point-hot blob",
);
assert_eq!(
bm.cached_count(),
2,
"scan access may briefly exceed capacity to avoid hot-cache pollution",
);
}
#[test]
fn scan_hits_do_not_refresh_recency() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..3u8 {
let mut guid = [0u8; 16];
guid[0] = i;
inner.write_blob(guid, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 2);
let first = [0u8; 16];
let mut second = [0u8; 16];
second[0] = 1;
let mut third = [0u8; 16];
third[0] = 2;
drop(bm.pin(first).unwrap());
drop(bm.pin(second).unwrap());
drop(bm.pin_scan(first).unwrap());
drop(bm.pin(third).unwrap());
assert!(
!bm.cache.contains_key(&first),
"a scan hit must not make the oldest point blob look hot",
);
assert!(bm.cache.contains_key(&second));
assert!(bm.cache.contains_key(&third));
}
#[test]
fn frequency_aware_eviction_stays_at_capacity() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..10u8 {
let mut g = [0u8; 16];
g[0] = i;
inner.write_blob(g, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 4);
for i in 0..10u8 {
let mut g = [0u8; 16];
g[0] = i;
let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob(g, &mut dst).unwrap();
}
assert_eq!(
bm.cached_count(),
4,
"cache must shrink to capacity after over-fill",
);
let mut g_last = [0u8; 16];
g_last[0] = 9;
let mut g_first = [0u8; 16];
g_first[0] = 0;
assert!(bm.cache.contains_key(&g_last));
assert!(!bm.cache.contains_key(&g_first));
}
#[test]
fn tinylfu_keeps_frequent_point_blob_against_one_hit_stream() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..12u8 {
let mut g = [0u8; 16];
g[0] = i;
inner.write_blob(g, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 2);
let hot = [0u8; 16];
for _ in 0..8 {
drop(bm.pin(hot).unwrap());
}
for i in 1..12u8 {
let mut cold = [0u8; 16];
cold[0] = i;
drop(bm.pin(cold).unwrap());
assert!(
bm.cache.contains_key(&hot),
"frequent point blob should survive one-hit stream pressure",
);
assert!(
bm.cached_count() <= 2,
"unprotected one-hit blobs should be reclaimed immediately",
);
}
}
#[test]
fn route_resident_anchor_survives_inline_eviction() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..9u8 {
let mut g = [0u8; 16];
g[0] = i;
inner.write_blob(g, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 8);
let anchor = [0u8; 16];
let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob(anchor, &mut dst).unwrap();
bm.mark_route_resident(anchor);
for i in 1..9u8 {
let mut g = [0u8; 16];
g[0] = i;
bm.read_blob(g, &mut dst).unwrap();
}
assert_eq!(bm.cached_count(), 8);
assert!(bm.cache.contains_key(&anchor));
assert!(bm.is_route_resident(anchor));
let mut first_non_route = [0u8; 16];
first_non_route[0] = 1;
assert!(
!bm.cache.contains_key(&first_non_route),
"oldest non-route blob should be evicted first",
);
}
#[test]
fn route_resident_tier_demotes_old_anchors_at_budget() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(inner, 4);
bm.mark_route_resident([1; 16]);
bm.mark_route_resident([2; 16]);
assert_eq!(bm.route_resident_count(), 1);
assert_eq!(bm.stats().route_resident_demotions, 1);
assert!(!bm.is_route_resident([1; 16]));
assert!(bm.is_route_resident([2; 16]));
}
#[test]
fn inline_eviction_skips_dirty_entries() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..3u8 {
let mut g = [0u8; 16];
g[0] = i;
inner.write_blob(g, &make_buf(i)).unwrap();
}
let bm = BufferManager::new(inner, 2);
let g_a = {
let mut g = [0u8; 16];
g[0] = 0;
g
};
let g_b = {
let mut g = [0u8; 16];
g[0] = 1;
g
};
let g_c = {
let mut g = [0u8; 16];
g[0] = 2;
g
};
{
let _pin = bm.pin(g_a).unwrap();
}
bm.mark_dirty(g_a, 10);
assert_eq!(bm.dirty_count(), 1);
assert!(bm.cache.contains_key(&g_a));
{
let _pin = bm.pin(g_b).unwrap();
}
assert!(bm.cache.contains_key(&g_a));
assert!(bm.cache.contains_key(&g_b));
{
let _pin = bm.pin(g_c).unwrap();
}
assert!(
bm.cache.contains_key(&g_a),
"dirty entry A's cache image must survive inline eviction",
);
assert!(
bm.cache.contains_key(&g_c),
"newly-pinned C must be in cache",
);
assert!(
!bm.cache.contains_key(&g_b),
"B (clean, no pin) should have been evicted in A's stead",
);
assert_eq!(
bm.dirty_count(),
1,
"dirty bookkeeping must not be touched by eviction",
);
let _ = snapshot_current_bytes(&bm, g_a);
}
#[test]
fn maintenance_candidates_are_unique_and_fifo_budgeted() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(inner, 4);
let mut buckets = vec![Vec::<BlobGuid>::new(); BOOKKEEPING_SHARDS];
for i in 0..=u8::MAX {
let mut g = [0u8; 16];
g[0] = i;
buckets[bookkeeping_shard_idx(&g)].push(g);
}
let same_shard = buckets.into_iter().find(|b| b.len() >= 3).unwrap();
let a = same_shard[0];
let b = same_shard[1];
let c = same_shard[2];
bm.note_compaction_candidate(a);
bm.note_compaction_candidate(b);
bm.note_compaction_candidate(a);
bm.note_compaction_candidate(c);
assert_eq!(bm.compaction_candidate_count(), 3);
assert_eq!(bm.pop_compaction_candidates(2), vec![a, b]);
assert_eq!(bm.compaction_candidate_count(), 1);
bm.note_compaction_candidate(a);
assert_eq!(bm.pop_compaction_candidates(8), vec![c, a]);
assert_eq!(bm.compaction_candidate_count(), 0);
}
#[test]
fn maintenance_candidate_drain_rotates_across_shards() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(inner, 4);
let mut by_shard = [None::<BlobGuid>; BOOKKEEPING_SHARDS];
let mut counter = 0u32;
while by_shard.iter().any(Option::is_none) {
assert!(
counter < 100_000,
"test helper could not cover every bookkeeping shard"
);
let mut guid = [0u8; 16];
guid[0..4].copy_from_slice(&counter.to_le_bytes());
let shard = bookkeeping_shard_idx(&guid);
by_shard[shard].get_or_insert(guid);
counter += 1;
}
for guid in by_shard.iter().flatten() {
bm.note_compaction_candidate(*guid);
}
for expected_shard in 0..4 {
let batch = bm.pop_compaction_candidates(1);
assert_eq!(batch.len(), 1);
assert_eq!(bookkeeping_shard_idx(&batch[0]), expected_shard);
}
}
#[test]
fn write_through_propagates_to_inner_store() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(inner.clone(), 4);
bm.write_blob([0xCD; 16], &make_buf(0x42)).unwrap();
assert!(inner.has_blob([0xCD; 16]).unwrap());
let mut dst = AlignedBlobBuf::zeroed();
inner.read_blob([0xCD; 16], &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], 0x42);
}
#[test]
fn write_through_updates_cache_if_present() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xEF; 16], &make_buf(1)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob([0xEF; 16], &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], 1);
bm.write_blob([0xEF; 16], &make_buf(99)).unwrap();
bm.read_blob([0xEF; 16], &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], 99);
}
#[test]
fn delete_evicts_from_cache_and_inner() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0x33; 16], &make_buf(5)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob([0x33; 16], &mut dst).unwrap();
assert_eq!(bm.cached_count(), 1);
bm.delete_blob([0x33; 16]).unwrap();
assert_eq!(bm.cached_count(), 0);
assert!(!inner.has_blob([0x33; 16]).unwrap());
assert!(!bm.has_blob([0x33; 16]).unwrap());
}
#[test]
fn pending_delete_hides_blob_until_checkpoint_delete_applies() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0x44; 16], &make_buf(7)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let _pin = bm.pin([0x44; 16]).unwrap();
assert!(bm.has_blob([0x44; 16]).unwrap());
bm.mark_dirty([0x44; 16], 10);
bm.mark_for_delete([0x44; 16], 11);
assert!(inner.has_blob([0x44; 16]).unwrap());
assert!(!bm.has_blob([0x44; 16]).unwrap());
assert!(
bm.pin([0x44; 16]).is_err(),
"pending-delete child must not be reloaded from store"
);
bm.mark_dirty([0x44; 16], 12);
let mut restore = HashMap::new();
restore.insert([0x44; 16], 13);
bm.restore_dirty(restore);
assert_eq!(bm.dirty_count(), 0);
assert_eq!(bm.pending_delete_count(), 1);
}
#[test]
fn pending_delete_count_tracks_snapshot_and_restore() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(inner, 4);
let guid = [0x55; 16];
bm.mark_for_delete(guid, 20);
bm.mark_for_delete(guid, 10);
assert_eq!(bm.pending_delete_count(), 1);
let pending = bm.snapshot_pending_deletes();
assert_eq!(pending.get(&guid), Some(&10));
assert_eq!(
bm.pending_delete_count(),
1,
"claimed deletes remain fenced while the I/O worker owns them",
);
bm.restore_pending_deletes(pending);
assert_eq!(bm.pending_delete_count(), 1);
let pending = bm.snapshot_pending_deletes();
assert_eq!(pending.get(&guid), Some(&10));
assert_eq!(bm.pending_delete_count(), 1);
}
#[test]
fn claimed_pending_delete_still_hides_blob_from_stale_pins() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5A; 16];
inner.write_blob(guid, &make_buf(7)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
bm.mark_for_delete(guid, 10);
let pending = bm.snapshot_pending_deletes();
assert_eq!(pending.get(&guid), Some(&10));
assert!(inner.has_blob(guid).unwrap());
assert!(!bm.has_blob(guid).unwrap());
assert!(
bm.pin(guid).is_err(),
"a claimed delete must keep stale walkers from reloading the blob",
);
let mut dst = AlignedBlobBuf::zeroed();
assert!(
bm.read_blob(guid, &mut dst).is_err(),
"BlobStore reads must obey the same delete fence as pin()",
);
bm.mark_dirty(guid, 11);
assert_eq!(bm.dirty_count(), 0);
assert!(bm.write_blob(guid, &make_buf(9)).is_err());
assert!(bm.delete_blob(guid).is_err());
assert!(bm.execute_pending_delete(guid, 10).unwrap());
assert_eq!(bm.pending_delete_count(), 0);
assert!(!inner.has_blob(guid).unwrap());
}
#[test]
fn structural_detach_waits_for_parent_dirty_before_fifo() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5B; 16];
let parent_guid = [0x5A; 16];
inner.write_blob(guid, &make_buf(7)).unwrap();
inner.write_blob(parent_guid, &make_buf(8)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let parent = bm.pin(parent_guid).unwrap();
bm.stage_structural_reclaim(parent_guid, guid);
assert_eq!(bm.pending_delete_count(), 0);
assert_eq!(bm.gc_orphan_backlog_count(), 1);
assert_eq!(bm.orphan_staging_count(), 1);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 0);
bm.mark_dirty_cached(parent_guid, STRUCTURAL_SEQ, parent.as_ref());
assert_eq!(bm.orphan_staging_count(), 0);
assert!(
inner.has_blob(guid).unwrap(),
"structural detach must not unlink before parent dirty publication"
);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 1);
assert_eq!(bm.gc_orphan_backlog_count(), 0);
assert!(!inner.has_blob(guid).unwrap());
}
#[test]
fn structural_orphan_stays_visible_until_last_snapshot_lease_retires() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5D; 16];
let root_guid = [0x5E; 16];
inner.write_blob(guid, &make_buf(7)).unwrap();
inner.write_blob(root_guid, &make_buf(8)).unwrap();
let bm = Arc::new(BufferManager::new(inner.clone(), 4));
let root_pin = bm.pin(root_guid).unwrap();
let epoch = bm.register_snapshot(root_guid, &root_pin).unwrap();
bm.stage_structural_reclaim(root_guid, guid);
bm.mark_dirty_cached(root_guid, STRUCTURAL_SEQ, root_pin.as_ref());
assert_eq!(bm.pending_delete_count(), 0);
assert_eq!(bm.gc_orphan_backlog_count(), 1);
assert_eq!(bm.orphan_staging_count(), 0);
assert_eq!(
bm.pin(guid).unwrap().read().as_slice()[100],
7,
"structural detach must not fence a snapshot's first lazy child pin"
);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 0);
assert!(inner.has_blob(guid).unwrap());
bm.retire_snapshot(epoch);
assert_eq!(bm.gc_orphan_backlog_count(), 1);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 1);
assert!(!inner.has_blob(guid).unwrap());
}
#[test]
fn snapshot_epoch_exhaustion_never_wraps_or_mutates_live_registry() {
let root_guid = [0x63; 16];
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob(root_guid, &make_buf(1)).unwrap();
let bm = Arc::new(BufferManager::new(inner, 4));
let root = bm.pin(root_guid).unwrap();
bm.set_current_epoch(u64::MAX - 2);
let epoch = bm.register_snapshot(root_guid, &root).unwrap();
assert_eq!(epoch, u64::MAX - 2);
assert_eq!(bm.current_epoch(), u64::MAX - 1);
assert_eq!(bm.fork_barrier(), u64::MAX - 2);
assert_eq!(bm.snapshots.lock().unwrap().live.len(), 1);
let cached_before = bm.cached_count();
let dirty_before = bm.dirty_count();
let error = bm.register_snapshot(root_guid, &root).unwrap_err();
assert!(matches!(error, Error::SnapshotEpochExhausted));
assert_eq!(bm.current_epoch(), u64::MAX - 1);
assert_eq!(bm.fork_barrier(), u64::MAX - 2);
assert_eq!(bm.snapshots.lock().unwrap().live.len(), 1);
assert_eq!(bm.cached_count(), cached_before);
assert_eq!(bm.dirty_count(), dirty_before);
bm.retire_snapshot(epoch);
assert_eq!(bm.fork_barrier(), 0);
assert_eq!(bm.current_epoch(), u64::MAX - 1);
}
#[test]
fn structural_last_lease_drop_before_parent_dirty_stays_staged() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let child_guid = [0x5F; 16];
let parent_guid = [0x60; 16];
inner.write_blob(child_guid, &make_buf(7)).unwrap();
inner.write_blob(parent_guid, &make_buf(8)).unwrap();
let bm = Arc::new(BufferManager::new(inner.clone(), 4));
let parent = bm.pin(parent_guid).unwrap();
let epoch = bm.register_snapshot(parent_guid, &parent).unwrap();
bm.stage_structural_reclaim(parent_guid, child_guid);
bm.retire_snapshot(epoch);
assert_eq!(bm.orphan_staging_count(), 1);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 0);
assert!(inner.has_blob(child_guid).unwrap());
bm.mark_dirty_cached(parent_guid, STRUCTURAL_SEQ, parent.as_ref());
assert_eq!(bm.orphan_staging_count(), 0);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 1);
assert!(!inner.has_blob(child_guid).unwrap());
}
#[test]
fn cow_epoch_zero_last_lease_drop_before_parent_dirty_stays_staged() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let child_guid = [0x61; 16];
let parent_guid = [0x62; 16];
inner.write_blob(child_guid, &make_buf(7)).unwrap();
inner.write_blob(parent_guid, &make_buf(8)).unwrap();
let bm = Arc::new(BufferManager::new(inner.clone(), 4));
let parent = bm.pin(parent_guid).unwrap();
let epoch = bm.register_snapshot(parent_guid, &parent).unwrap();
bm.stage_cow_reclaim(parent_guid, child_guid, 0);
bm.retire_snapshot(epoch);
assert_eq!(bm.orphan_staging_count(), 1);
assert_eq!(bm.gc_orphan_backlog_count(), 1);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 0);
assert!(inner.has_blob(child_guid).unwrap());
bm.mark_dirty_cached(parent_guid, 9, parent.as_ref());
assert_eq!(bm.orphan_staging_count(), 0);
assert_eq!(bm.reclaim_retired_orphans_bounded(1).unwrap(), 1);
assert!(!inner.has_blob(child_guid).unwrap());
}
#[test]
fn pending_delete_defers_until_existing_pin_drops() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5C; 16];
inner.write_blob(guid, &make_buf(7)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let pin = bm.pin(guid).unwrap();
bm.mark_for_delete(guid, 10);
let pending = bm.snapshot_pending_deletes();
assert!(
!bm.execute_pending_delete(guid, 10).unwrap(),
"delete must wait while an old walker still holds a cached blob pin",
);
bm.restore_pending_deletes(pending);
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x66;
}
bm.mark_dirty_cached(guid, 11, pin.as_ref());
assert_eq!(
bm.dirty_count(),
0,
"existing pins must not publish orphan dirty state while delete-fenced",
);
drop(pin);
let pending = bm.snapshot_pending_deletes();
assert_eq!(pending.get(&guid), Some(&10));
assert!(bm.execute_pending_delete(guid, 10).unwrap());
assert_eq!(bm.pending_delete_count(), 0);
assert!(!inner.has_blob(guid).unwrap());
}
#[test]
fn has_blob_fast_path_avoids_inner_when_cached() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0x77; 16], &make_buf(11)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob([0x77; 16], &mut dst).unwrap();
assert!(bm.has_blob([0x77; 16]).unwrap());
assert!(!bm.has_blob([0x88; 16]).unwrap());
}
#[test]
fn mark_dirty_keeps_lowest_seq() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x01; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin(guid).unwrap();
bm.mark_dirty(guid, 50);
bm.mark_dirty(guid, 30);
bm.mark_dirty(guid, 99);
assert_eq!(bm.dirty_count(), 1);
let snap = bm.snapshot_dirty();
assert_eq!(snap[&guid], 30);
}
#[test]
fn mark_dirty_without_cache_image_does_not_publish_orphan() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0xAB; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
bm.mark_dirty(guid, 10);
assert!(
bm.snapshot_dirty().is_empty(),
"dirty map must not contain an entry without a cache image",
);
}
#[test]
fn cached_dirty_hint_resets_after_snapshot() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0xD1; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin(guid).unwrap();
bm.mark_dirty(guid, 10);
bm.mark_dirty(guid, 20);
let snap = bm.snapshot_dirty();
assert_eq!(snap[&guid], 10);
assert_eq!(bm.dirty_count(), 0);
bm.mark_dirty(guid, 30);
let next = bm.snapshot_dirty();
assert_eq!(
next[&guid], 30,
"mark_dirty after snapshot must publish a fresh dirty entry",
);
}
#[test]
fn stale_dirty_hint_cannot_skip_dirty_map_publish() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0xD3; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let pin = bm.pin(guid).unwrap();
assert!(pin.dirty_hint_needs_map_publish(10));
bm.mark_dirty(guid, 20);
let snap = bm.snapshot_dirty();
assert_eq!(
snap[&guid], 20,
"a stale hint without a dirty-map entry must not hide a fresh write",
);
}
#[test]
fn cached_dirty_hint_preserves_lower_restored_seq() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0xD2; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin(guid).unwrap();
let mut restored = HashMap::new();
restored.insert(guid, 40);
bm.restore_dirty(restored);
bm.mark_dirty(guid, 90);
let snap = bm.snapshot_dirty();
assert_eq!(
snap[&guid], 40,
"duplicate higher seq must be covered by restored low-watermark",
);
bm.restore_dirty(snap);
bm.mark_dirty(guid, 20);
let lowered = bm.snapshot_dirty();
assert_eq!(
lowered[&guid], 20,
"lower seq must still update the dirty low-watermark",
);
}
#[test]
fn snapshot_dirty_drains_atomically() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for guid in [[0x01; 16], [0x02; 16], [0x03; 16]] {
inner.write_blob(guid, &make_buf(1)).unwrap();
}
let bm = BufferManager::new(inner, 4);
let _p1 = bm.pin([0x01; 16]).unwrap();
let _p2 = bm.pin([0x02; 16]).unwrap();
bm.mark_dirty([0x01; 16], 10);
bm.mark_dirty([0x02; 16], 20);
let snap = bm.snapshot_dirty();
assert_eq!(snap.len(), 2);
assert_eq!(snap[&[0x01; 16]], 10);
assert_eq!(snap[&[0x02; 16]], 20);
assert_eq!(bm.dirty_count(), 0);
let _p3 = bm.pin([0x03; 16]).unwrap();
bm.mark_dirty([0x03; 16], 99);
assert_eq!(bm.dirty_count(), 1);
let next = bm.snapshot_dirty();
assert_eq!(next[&[0x03; 16]], 99);
}
#[test]
fn snapshot_dirty_drains_every_bookkeeping_shard() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(Arc::clone(&inner), BOOKKEEPING_SHARDS);
let mut guids: [Option<BlobGuid>; BOOKKEEPING_SHARDS] = [None; BOOKKEEPING_SHARDS];
for i in 0..20_000u64 {
let mut guid = [0u8; 16];
guid[0..8].copy_from_slice(&i.to_le_bytes());
guid[8..16].copy_from_slice(&i.wrapping_mul(0x9E37_79B9_7F4A_7C15).to_le_bytes());
let shard = bookkeeping_shard_idx(&guid);
guids[shard].get_or_insert(guid);
if guids.iter().all(Option::is_some) {
break;
}
}
assert!(
guids.iter().all(Option::is_some),
"test generator should hit every bookkeeping shard"
);
for (shard, guid) in guids.iter().enumerate() {
let guid = guid.expect("filled");
inner.write_blob(guid, &make_buf(1)).unwrap();
let _pin = bm.pin(guid).unwrap();
bm.mark_dirty(guid, shard as u64 + 1);
}
let snap = bm.snapshot_dirty();
assert_eq!(snap.len(), BOOKKEEPING_SHARDS);
assert_eq!(bm.dirty_count(), 0);
for (shard, guid) in guids.iter().enumerate() {
assert_eq!(snap[&guid.expect("filled")], shard as u64 + 1);
}
}
#[test]
fn snapshot_dirty_protects_flushing_entry_from_eviction() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x55; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 1);
{
let pin = bm.pin(guid).unwrap();
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0xAB;
}
bm.mark_dirty(guid, 42);
let snap = bm.snapshot_dirty();
assert_eq!(snap[&guid], 42);
assert_eq!(
bm.dirty_count(),
0,
"snapshot drains the live dirty map for racing writers",
);
assert!(
!bm.try_evict_cold(guid),
"checkpoint-owned flushing entries must stay cached until write-through",
);
let bytes = snapshot_current_bytes(&bm, guid);
assert_eq!(bytes.as_slice()[123], 0xAB);
bm.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 42,
content_version: None,
}])
.unwrap();
assert!(
bm.try_evict_cold(guid),
"successful write-through releases flushing protection",
);
}
#[test]
fn cow_reclaim_does_not_drop_flushing_cache_image() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x56; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0xAA;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
assert_eq!(snap[&guid], 10);
drop(pin);
bm.discard_snapshot_root(guid);
let version = bm.snapshot_dirty_versions(&snap).unwrap()[0].content_version;
let bytes = bm
.snapshot_bytes_if_version(guid, version)
.unwrap()
.expect("COW reclaim must not drop checkpoint-owned bytes");
assert_eq!(bytes.as_slice()[123], 0xAA);
assert!(inner.has_blob(guid).unwrap());
}
#[test]
fn cow_reclaim_does_not_drop_pinned_cache_image() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x56; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
bm.discard_snapshot_root(guid);
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0xBB;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let version = bm.snapshot_dirty_versions(&snap).unwrap()[0].content_version;
let bytes = bm
.snapshot_bytes_if_version(guid, version)
.unwrap()
.expect("pinned dirty image must stay reachable through cache");
assert_eq!(bytes.as_slice()[123], 0xBB);
}
#[test]
fn snapshot_bytes_if_version_rejects_stale_blob_image() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x56; 16];
inner.write_blob(guid, &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 1);
let pin = bm.pin(guid).unwrap();
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let versioned = bm.snapshot_dirty_versions(&snap).unwrap();
assert_eq!(versioned.len(), 1);
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0xEE;
}
assert!(
bm.snapshot_bytes_if_version(guid, versioned[0].content_version)
.unwrap()
.is_none(),
"checkpoint clone must reject bytes after a newer blob mutation"
);
let bytes = bm
.snapshot_bytes_if_version(guid, pin.content_version())
.unwrap()
.expect("current version should clone");
assert_eq!(bytes.as_slice()[123], 0xEE);
}
#[test]
fn write_through_rejects_stale_snapshot_bytes() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x57; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x11;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let versioned = bm.snapshot_dirty_versions(&snap).unwrap();
let stale_version = versioned[0].content_version;
let stale_bytes = bm
.snapshot_bytes_if_version(guid, stale_version)
.unwrap()
.expect("snapshot bytes should clone while version still matches");
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0xEE;
}
bm.mark_dirty_cached(guid, 20, pin.as_ref());
let report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes: stale_bytes,
expected_seq: 10,
content_version: Some(stale_version),
}])
.unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Stale]);
assert_eq!(
bm.snapshot_dirty()[&guid],
20,
"newer writer entry must survive stale write-through retirement",
);
let mut stored = AlignedBlobBuf::zeroed();
inner.read_blob(guid, &mut stored).unwrap();
assert_eq!(
stored.as_slice()[123],
0,
"stale checkpoint bytes must not overwrite the store"
);
}
#[test]
fn overlapping_checkpoint_epochs_keep_cache_image_protected() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x58; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner, 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x11;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let first = bm.snapshot_dirty();
assert_eq!(bm.flushing_count(), 1);
let first_version = bm.snapshot_dirty_versions(&first).unwrap()[0].content_version;
let first_bytes = bm
.snapshot_bytes_if_version(guid, first_version)
.unwrap()
.unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x22;
}
bm.mark_dirty_cached(guid, 20, pin.as_ref());
let second = bm.snapshot_dirty();
assert_eq!(bm.flushing_count(), 2);
let second_version = bm.snapshot_dirty_versions(&second).unwrap()[0].content_version;
let second_bytes = bm
.snapshot_bytes_if_version(guid, second_version)
.unwrap()
.unwrap();
drop(pin);
let first_report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes: first_bytes,
expected_seq: 10,
content_version: Some(first_version),
}])
.unwrap();
assert_eq!(first_report.statuses, vec![WriteThroughStatus::Stale]);
bm.restore_dirty(first);
assert!(
!bm.try_evict_cold(guid),
"second in-flight epoch must keep the blob cached after first retire",
);
assert_eq!(bm.flushing_count(), 1);
let second_report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes: second_bytes,
expected_seq: 20,
content_version: Some(second_version),
}])
.unwrap();
assert_eq!(second_report.statuses, vec![WriteThroughStatus::Written]);
assert!(
bm.try_evict_cold(guid),
"last in-flight epoch can release eviction protection",
);
assert_eq!(bm.flushing_count(), 0);
}
#[test]
fn pending_delete_preserves_in_flight_checkpoint_image() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x59; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x33;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let version = bm.snapshot_dirty_versions(&snap).unwrap()[0].content_version;
let bytes = bm
.snapshot_bytes_if_version(guid, version)
.unwrap()
.unwrap();
drop(pin);
bm.mark_for_delete(guid, 20);
assert_eq!(
bm.flushing_count(),
1,
"a pending delete must not retire an in-flight checkpoint epoch",
);
assert!(
bm.cache.contains_key(&guid),
"a pending delete must keep the cache image needed by write-through validation",
);
let report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 10,
content_version: Some(version),
}])
.unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Written]);
assert_eq!(bm.flushing_count(), 0);
assert_eq!(bm.pending_delete_count(), 1);
assert!(
bm.pin(guid).is_err(),
"pending delete must still hide the blob"
);
let mut stored = AlignedBlobBuf::zeroed();
inner.read_blob(guid, &mut stored).unwrap();
assert_eq!(
stored.as_slice()[123],
0x33,
"checkpoint write-through must preserve the durable image until delete applies",
);
}
#[test]
fn execute_pending_delete_defers_while_blob_is_flushing() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5B; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x44;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let version = bm.snapshot_dirty_versions(&snap).unwrap()[0].content_version;
let bytes = bm
.snapshot_bytes_if_version(guid, version)
.unwrap()
.unwrap();
drop(pin);
bm.mark_for_delete(guid, 20);
let pending = bm.snapshot_pending_deletes();
assert_eq!(pending.get(&guid), Some(&20));
assert!(
!bm.execute_pending_delete(guid, 20).unwrap(),
"delete must wait for the in-flight checkpoint image to retire",
);
assert!(inner.has_blob(guid).unwrap());
bm.restore_pending_deletes(pending);
assert_eq!(bm.pending_delete_count(), 1);
let report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 10,
content_version: Some(version),
}])
.unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Written]);
let pending = bm.snapshot_pending_deletes();
assert!(bm.execute_pending_delete(guid, 20).unwrap());
assert!(!inner.has_blob(guid).unwrap());
assert_eq!(bm.pending_delete_count(), 0);
assert_eq!(pending.get(&guid), Some(&20));
}
#[test]
fn write_through_does_not_clear_in_flight_checkpoint_owner() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5C; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x55;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let version = bm.snapshot_dirty_versions(&snap).unwrap()[0].content_version;
let bytes = bm
.snapshot_bytes_if_version(guid, version)
.unwrap()
.unwrap();
drop(pin);
bm.write_blob(guid, &make_buf(0x66)).unwrap();
assert_eq!(
bm.flushing_count(),
1,
"direct write-through must not retire another checkpoint epoch",
);
assert!(
bm.cache.contains_key(&guid),
"direct write-through must keep the image required by version validation",
);
let report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 10,
content_version: Some(version),
}])
.unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Stale]);
}
#[test]
fn delete_blob_rejects_in_flight_checkpoint_owner() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0x5D; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 1);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[123] = 0x77;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let version = bm.snapshot_dirty_versions(&snap).unwrap()[0].content_version;
let bytes = bm
.snapshot_bytes_if_version(guid, version)
.unwrap()
.unwrap();
drop(pin);
assert!(bm.delete_blob(guid).is_err());
assert_eq!(bm.flushing_count(), 1);
assert!(bm.cache.contains_key(&guid));
assert!(inner.has_blob(guid).unwrap());
let report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 10,
content_version: Some(version),
}])
.unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Written]);
}
#[test]
fn restore_dirty_merges_keeping_min() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for guid in [[0x01; 16], [0x02; 16], [0x03; 16]] {
inner.write_blob(guid, &make_buf(1)).unwrap();
}
let bm = BufferManager::new(inner, 4);
let _p1 = bm.pin([0x01; 16]).unwrap();
let _p2 = bm.pin([0x02; 16]).unwrap();
let _p3 = bm.pin([0x03; 16]).unwrap();
let mut snap = HashMap::new();
snap.insert([0x01; 16], 10);
snap.insert([0x02; 16], 20);
bm.mark_dirty([0x01; 16], 50);
bm.mark_dirty([0x03; 16], 5);
bm.restore_dirty(snap);
assert_eq!(bm.dirty_count(), 3);
let live = bm.snapshot_dirty();
assert_eq!(live[&[0x01; 16]], 10);
assert_eq!(live[&[0x02; 16]], 20);
assert_eq!(live[&[0x03; 16]], 5);
}
#[test]
fn write_through_keeps_racing_writer_dirty_entry() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xAA; 16], &make_buf(0)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin([0xAA; 16]).unwrap();
bm.mark_dirty([0xAA; 16], 200);
let snap_bytes = snapshot_current_bytes(&bm, [0xAA; 16]);
bm.write_through_batch(&[WriteThroughEntry {
guid: [0xAA; 16],
bytes: snap_bytes,
expected_seq: 50,
content_version: None,
}])
.unwrap();
assert_eq!(
bm.dirty_count(),
1,
"write-through must not stomp a racing newer-seq entry",
);
let live = bm.snapshot_dirty();
assert_eq!(live[&[0xAA; 16]], 200, "racing writer's seq survives");
}
#[test]
fn write_through_keeps_racing_structural_dirty_entry() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xA5; 16], &make_buf(0)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin([0xA5; 16]).unwrap();
bm.mark_dirty([0xA5; 16], STRUCTURAL_SEQ);
let snap_bytes = snapshot_current_bytes(&bm, [0xA5; 16]);
bm.write_through_batch(&[WriteThroughEntry {
guid: [0xA5; 16],
bytes: snap_bytes,
expected_seq: STRUCTURAL_SEQ,
content_version: None,
}])
.unwrap();
assert_eq!(
bm.dirty_count(),
1,
"structural sentinel equality is not enough to retire a racing entry",
);
let live = bm.snapshot_dirty();
assert_eq!(live[&[0xA5; 16]], STRUCTURAL_SEQ);
}
#[test]
fn write_through_retires_clean_snapshot() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0xBB; 16], &make_buf(0)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin([0xBB; 16]).unwrap();
bm.mark_dirty([0xBB; 16], 42);
let snap_bytes = snapshot_current_bytes(&bm, [0xBB; 16]);
bm.write_through_batch(&[WriteThroughEntry {
guid: [0xBB; 16],
bytes: snap_bytes,
expected_seq: 42,
content_version: None,
}])
.unwrap();
assert_eq!(bm.dirty_count(), 0);
}
#[test]
fn write_through_batch_retires_clean_snapshots() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let g1 = [0xB1; 16];
let g2 = [0xB2; 16];
inner.write_blob(g1, &make_buf(0)).unwrap();
inner.write_blob(g2, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner.clone(), 4);
for (guid, byte) in [(g1, 11), (g2, 22)] {
let pin = bm.pin(guid).unwrap();
let mut guard = pin.write();
guard.as_mut_slice()[100] = byte;
bm.mark_dirty(guid, u64::from(byte));
}
let snap = bm.snapshot_dirty();
let entries: Vec<_> = snap
.iter()
.map(|(guid, expected_seq)| WriteThroughEntry {
guid: *guid,
bytes: snapshot_current_bytes(&bm, *guid),
expected_seq: *expected_seq,
content_version: None,
})
.collect();
bm.write_through_batch(&entries).unwrap();
assert_eq!(bm.dirty_count(), 0);
let mut dst = AlignedBlobBuf::zeroed();
inner.read_blob(g1, &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], 11);
inner.read_blob(g2, &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], 22);
}
#[test]
fn write_through_batch_invalidates_indexed_read_cache_before_retire() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let guid = [0xBC; 16];
inner.write_blob(guid, &make_buf(0)).unwrap();
let bm = BufferManager::new_file(inner.clone(), 128, AlignedBlobBuf::zeroed);
let old_page = [0xA5; PAGE_4K as usize];
let mut dst = [0u8; PAGE_4K as usize];
bm.read_page_store(guid, 0, &old_page);
assert!(bm.read_page_cached(guid, 0, &mut dst));
assert_eq!(dst, old_page);
let pin = bm.pin(guid).unwrap();
{
let mut guard = pin.write();
guard.as_mut_slice()[100] = 0x77;
}
bm.mark_dirty_cached(guid, 10, pin.as_ref());
let snap = bm.snapshot_dirty();
let bytes = snapshot_current_bytes(&bm, guid);
bm.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: snap[&guid],
content_version: None,
}])
.unwrap();
assert!(
!bm.read_page_cached(guid, 0, &mut dst),
"checkpointed bytes must retire stale indexed navigation pages"
);
}
#[test]
fn write_through_batch_publishes_read_index() {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(crate::store::blob_store::FileBlobStore::open(dir.path()).unwrap());
let store_dyn: Arc<dyn BlobStore> = store.clone();
let bm = BufferManager::new_file(store_dyn, 128, AlignedBlobBuf::zeroed);
let guid = [0xBD; 16];
let mut bytes = AlignedBlobBuf::zeroed();
BlobFrame::init(bytes.as_mut_slice(), guid).unwrap();
bm.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 1,
content_version: None,
}])
.unwrap();
let mut index_bytes = vec![0; ReadIndex::HEADER_LEN];
assert!(
store.read_index_range(guid, 0, &mut index_bytes).unwrap(),
"checkpoint write-through should publish read index"
);
let directory_len = ReadIndex::directory_len(&index_bytes)
.expect("published read index header should parse");
if directory_len > ReadIndex::HEADER_LEN {
let mut rest = vec![0; directory_len - ReadIndex::HEADER_LEN];
assert!(
store
.read_index_range(guid, ReadIndex::HEADER_LEN as u64, &mut rest)
.unwrap(),
"published read index directory should be readable"
);
index_bytes.extend_from_slice(&rest);
}
ReadIndex::decode_directory(index_bytes).expect("published read index should parse");
}
#[test]
fn read_index_load_reads_small_directory_in_one_probe() {
let dir = tempfile::tempdir().unwrap();
let store = Arc::new(CountingReadIndexStore::open(dir.path()).unwrap());
let store_dyn: Arc<dyn BlobStore> = store.clone();
let bm = BufferManager::new_file(store_dyn, 128, AlignedBlobBuf::zeroed);
let guid = [0xBE; 16];
let mut bytes = AlignedBlobBuf::zeroed();
BlobFrame::init(bytes.as_mut_slice(), guid).unwrap();
bm.write_through_batch(&[WriteThroughEntry {
guid,
bytes,
expected_seq: 1,
content_version: None,
}])
.unwrap();
store.flush().unwrap();
let mut index_bytes = vec![0; ReadIndex::HEADER_LEN];
assert!(store.read_index_range(guid, 0, &mut index_bytes).unwrap());
ReadIndex::directory_len(&index_bytes).expect("published read-index header parses");
store.reset_index_reads();
let store_dyn: Arc<dyn BlobStore> = store.clone();
let bm = BufferManager::new_file(store_dyn, 128, AlignedBlobBuf::zeroed);
assert!(bm.indexed_read_eligible(guid));
let mut probe = vec![0; READ_INDEX_DIRECTORY_PROBE_BYTES];
assert!(store.read_index_range(guid, 0, &mut probe).unwrap());
let directory_len =
ReadIndex::directory_len(&probe).expect("probe should parse read-index header");
probe.truncate(directory_len);
let index = ReadIndex::decode_directory(probe).expect("probe should decode directory");
assert!(
bm.read_index_stamp_matches(guid, &index).unwrap(),
"probe-decoded read index should match the blob header"
);
store.reset_index_reads();
assert!(bm.read_index(guid).is_some());
assert_eq!(
store.index_reads(),
1,
"small read-index directories fit in the first 4 KiB probe"
);
}
#[test]
fn write_through_batch_keeps_racing_writer_dirty_entry() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let g1 = [0xC1; 16];
let g2 = [0xC2; 16];
inner.write_blob(g1, &make_buf(0)).unwrap();
inner.write_blob(g2, &make_buf(0)).unwrap();
let bm = BufferManager::new(inner, 4);
let _ = bm.pin(g1).unwrap();
let _ = bm.pin(g2).unwrap();
bm.mark_dirty(g1, 50);
bm.mark_dirty(g2, 60);
let snap = bm.snapshot_dirty();
bm.mark_dirty(g1, 200);
let entries: Vec<_> = snap
.iter()
.map(|(guid, expected_seq)| WriteThroughEntry {
guid: *guid,
bytes: snapshot_current_bytes(&bm, *guid),
expected_seq: *expected_seq,
content_version: None,
})
.collect();
bm.write_through_batch(&entries).unwrap();
let live = bm.snapshot_dirty();
assert_eq!(live.len(), 1);
assert_eq!(live[&g1], 200);
}
#[test]
fn write_through_revalidates_version_after_store_io() {
let guid = [0xC3; 16];
let inner = MemoryBlobStore::new();
inner.write_blob(guid, &make_buf(1)).unwrap();
let store = Arc::new(BlockingWriteStore::new(inner));
let store_dyn: Arc<dyn BlobStore> = store.clone();
let bm = Arc::new(BufferManager::new(store_dyn, 4));
let pin = bm.pin(guid).unwrap();
bm.mark_dirty(guid, 100);
let first_dirty = bm.snapshot_dirty();
let first_version = bm.snapshot_dirty_versions(&first_dirty).unwrap()[0].content_version;
let first_bytes = bm
.snapshot_bytes_if_version(guid, first_version)
.unwrap()
.unwrap();
let write_bm = Arc::clone(&bm);
let writer = std::thread::spawn(move || {
write_bm.write_through_batch(&[WriteThroughEntry {
guid,
bytes: first_bytes,
expected_seq: 100,
content_version: Some(first_version),
}])
});
store.entered.wait();
{
let mut frame = pin.write();
frame.as_mut_slice()[100] = 2;
}
bm.mark_dirty_cached(guid, 50, pin.as_ref());
store.release.wait();
let report = writer.join().unwrap().unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Stale]);
bm.restore_dirty(first_dirty);
let retained = bm.snapshot_dirty();
assert_eq!(retained.get(&guid), Some(&50));
bm.restore_dirty(retained);
let second_dirty = bm.snapshot_dirty();
let second_version = bm.snapshot_dirty_versions(&second_dirty).unwrap()[0].content_version;
let second_bytes = bm
.snapshot_bytes_if_version(guid, second_version)
.unwrap()
.unwrap();
let report = bm
.write_through_batch(&[WriteThroughEntry {
guid,
bytes: second_bytes,
expected_seq: second_dirty[&guid],
content_version: Some(second_version),
}])
.unwrap();
assert_eq!(report.statuses, vec![WriteThroughStatus::Written]);
assert_eq!(bm.dirty_count(), 0);
assert_eq!(bm.flushing_count(), 0);
drop(pin);
drop(bm);
let store_dyn: Arc<dyn BlobStore> = store;
let reopened = BufferManager::new(store_dyn, 4);
assert_eq!(
reopened.pin(guid).unwrap().read().as_slice()[100],
2,
"the racing writer must survive the retry and reopen",
);
}
#[test]
fn write_blob_through_trait_clears_dirty() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0x88; 16], &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let _pin = bm.pin([0x88; 16]).unwrap();
bm.mark_dirty([0x88; 16], 100);
assert_eq!(bm.dirty_count(), 1);
BlobStore::write_blob(&bm, [0x88; 16], &make_buf(9)).unwrap();
assert_eq!(bm.dirty_count(), 0);
}
#[test]
fn delete_blob_drops_dirty_entry() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
inner.write_blob([0x99; 16], &make_buf(1)).unwrap();
let bm = BufferManager::new(inner, 4);
let _ = bm.pin([0x99; 16]).unwrap();
bm.mark_dirty([0x99; 16], 7);
assert_eq!(bm.dirty_count(), 1);
BlobStore::delete_blob(&bm, [0x99; 16]).unwrap();
assert_eq!(
bm.dirty_count(),
0,
"deleted blobs must not linger as flush candidates"
);
}
#[test]
fn install_new_blob_caches_and_marks_dirty_without_store_write() {
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
let bm = BufferManager::new(Arc::clone(&inner), 4);
let new_guid = [0xCC; 16];
let mut bytes = AlignedBlobBuf::zeroed();
bytes.as_mut_slice()[200] = 0x77;
bm.install_new_blob(new_guid, bytes, 42);
assert_eq!(bm.cached_count(), 1);
assert_eq!(bm.dirty_count(), 1);
let snap = bm.snapshot_dirty();
assert_eq!(snap[&new_guid], 42);
bm.restore_dirty(snap);
assert!(
!inner.has_blob(new_guid).unwrap(),
"install_new_blob must defer the store write to the checkpoint round",
);
let pin = bm.pin(new_guid).unwrap();
let guard = pin.read();
assert_eq!(guard.as_slice()[200], 0x77);
drop(guard);
drop(pin);
let snap = bm.snapshot_dirty();
let bytes = snapshot_current_bytes(&bm, new_guid);
bm.write_through_batch(&[WriteThroughEntry {
guid: new_guid,
bytes,
expected_seq: snap[&new_guid],
content_version: None,
}])
.unwrap();
bm.flush_inner().unwrap();
assert_eq!(bm.dirty_count(), 0);
assert!(inner.has_blob(new_guid).unwrap());
let mut dst = AlignedBlobBuf::zeroed();
inner.read_blob(new_guid, &mut dst).unwrap();
assert_eq!(dst.as_slice()[200], 0x77);
}
#[test]
fn concurrent_reads_on_different_blobs_progress() {
use std::thread;
let inner: Arc<dyn BlobStore> = Arc::new(MemoryBlobStore::new());
for i in 0..16u8 {
let mut g = [0u8; 16];
g[0] = i;
inner.write_blob(g, &make_buf(i)).unwrap();
}
let bm = Arc::new(BufferManager::new(inner, 16));
let handles: Vec<_> = (0..8u8)
.map(|t| {
let bm = bm.clone();
thread::spawn(move || {
for _ in 0..50 {
let mut g = [0u8; 16];
g[0] = t * 2; let mut dst = AlignedBlobBuf::zeroed();
bm.read_blob(g, &mut dst).unwrap();
assert_eq!(dst.as_slice()[100], t * 2);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(bm.cached_count(), 8);
}
}