use std::mem::size_of;
use std::ops::Bound;
use std::sync::{Arc, OnceLock, Weak};
use crate::Key;
use crate::batch::Applied;
use crate::byte_view::ByteView;
use crate::cache::{BlockCache, BlockKey};
use crate::compaction::{CompactionIndex, compact_shard};
use crate::config::Config;
use crate::ctx::{Ctx, Family, Held, Owned, ShardGuards};
use crate::decoded_cache::DecodedCache;
use crate::disk_loc::DiskLoc;
use crate::engine::Engine;
use crate::error::{DbError, DbResult};
use crate::hook::{NoHook, WriteHook};
use crate::io::aligned_buf::AlignedBuf;
use crate::map_index::{Dir, dir_from};
use crate::recovery::recover_var_tree;
use crate::scope::DirRangeIter;
use crate::shard::ShardInner;
use crate::skiplist::iter::{SkipKeyIter, SkipKeyKeys};
use crate::skiplist::node::{SkipNode, VarNode, random_height};
use crate::skiplist::{BackFinger, InsertResult, SkipList};
use crate::sync::MutexGuard;
use crate::sync::{self};
use crate::ttl::{SweepOutcome, TTL_RETRY_BACKOFF_MS, TtlSlots};
use crate::upsert::Upsert;
use crate::value_cache::{ValueCache, ValueKey};
use crate::watch::{WatchEventInner, WatchWakeBatch};
enum VarSweepAction<K: Key> {
Delete(K, Option<ByteView>, u64),
Reschedule(K, u64),
Drop,
Retry(K),
}
pub(crate) enum VarHookNew<'v> {
Borrowed(&'v [u8]),
Owned(ByteView),
}
impl VarHookNew<'_> {
pub(crate) fn as_slice(&self) -> &[u8] {
match self {
Self::Borrowed(s) => s,
Self::Owned(v) => v.as_ref(),
}
}
pub(crate) fn into_owned(self) -> ByteView {
match self {
Self::Borrowed(s) => ByteView::from(s),
Self::Owned(v) => v,
}
}
}
const MAX_STALE_RETRIES: usize = 3;
#[cfg(test)]
thread_local! {
static UPDATE_MANY_FAIL_AFTER: std::cell::Cell<usize> = const { std::cell::Cell::new(usize::MAX) };
}
#[cfg(test)]
pub(crate) struct UpdateManyFailAfter(usize);
#[cfg(test)]
impl UpdateManyFailAfter {
pub(crate) fn after(applied: usize) -> Self {
let previous = UPDATE_MANY_FAIL_AFTER.with(|remaining| {
let previous = remaining.get();
remaining.set(applied);
previous
});
Self(previous)
}
}
#[cfg(test)]
impl Drop for UpdateManyFailAfter {
fn drop(&mut self) {
UPDATE_MANY_FAIL_AFTER.with(|remaining| remaining.set(self.0));
}
}
#[cfg(test)]
thread_local! {
pub(crate) static TEST_DISK_VALUE_READS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
pub(crate) static TEST_LOCKED_VALUE_READS: std::cell::Cell<usize> =
const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn test_disk_value_reads() -> usize {
TEST_DISK_VALUE_READS.with(|c| c.get())
}
#[cfg(test)]
pub(crate) fn test_locked_value_reads() -> usize {
TEST_LOCKED_VALUE_READS.with(|c| c.get())
}
#[cfg(test)]
thread_local! {
static COUNT_LIVE_KEYS_FAULT: std::cell::RefCell<Option<(usize, std::io::ErrorKind, String)>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
pub(crate) struct CountLiveKeysFaultGuard;
#[cfg(test)]
impl Drop for CountLiveKeysFaultGuard {
fn drop(&mut self) {
COUNT_LIVE_KEYS_FAULT.with(|fault| {
*fault.borrow_mut() = None;
});
}
}
#[cfg(test)]
pub(crate) fn fail_count_live_keys_after(
keys_counted: usize,
error: std::io::Error,
) -> CountLiveKeysFaultGuard {
COUNT_LIVE_KEYS_FAULT.with(|fault| {
let mut fault = fault.borrow_mut();
assert!(
fault.is_none(),
"a count-live-keys fault is already registered"
);
*fault = Some((keys_counted, error.kind(), error.to_string()));
});
CountLiveKeysFaultGuard
}
#[cfg(test)]
fn injected_count_live_keys_fault(keys_counted: u64) -> Option<DbError> {
COUNT_LIVE_KEYS_FAULT.with(|fault| {
let fault = fault.borrow();
let (trigger_at, kind, message) = fault.as_ref()?;
(keys_counted == *trigger_at as u64)
.then(|| DbError::Io(std::io::Error::new(*kind, message.clone())))
})
}
enum UpdateOutcome {
Missing,
Unchanged(ByteView),
Updated { old: ByteView, new: ByteView },
}
pub struct VarTree<K: Key, H: WriteHook<K> = NoHook> {
index: SkipList<VarNode<K>>,
engine: Engine,
block_cache: BlockCache,
value_cache: ValueCache,
compaction_threshold: f64,
shard_prefix_bits: usize,
reversed: bool,
pub(crate) hook: H,
tx_self: OnceLock<Weak<Self>>,
ttl: TtlSlots<K>,
pub(crate) counter_dirty: OnceLock<crate::counter_dirty::CounterDirty>,
}
impl<K: Key> VarTree<K> {
#[doc = include_str!("../docs/exclusive-access.md")]
pub fn open(path: impl AsRef<std::path::Path>, config: Config) -> DbResult<Self> {
Self::open_hooked(path, config, NoHook)
}
}
impl<K: Key, H: WriteHook<K>> VarTree<K, H> {
pub(crate) fn floor_recovered_gsns(&self, gsn_high: u64, shard_gsn_high: &[u64]) {
self.engine.gsn().fetch_max(
gsn_high.saturating_add(1),
std::sync::atomic::Ordering::Relaxed,
);
for (shard_id, high) in shard_gsn_high.iter().copied().enumerate() {
if let Some(shard) = self.engine.shards().get(shard_id) {
shard.set_durable_recovered_gsn(shard.durable_recovered_gsn().max(high));
}
}
}
#[doc = include_str!("../docs/exclusive-access.md")]
pub fn open_hooked(
path: impl AsRef<std::path::Path>,
config: Config,
hook: H,
) -> DbResult<Self> {
#[cfg(feature = "armour")]
if H::EMITS {
return Err(DbError::ChangeStreamRequiresManagedOpen);
}
let config = config.with_resolved_hints(true);
if config.var_cache_warn_needed() {
tracing::warn!(
"Var collection opened with no cache (block_cache=0 and value_cache=0): every get hits disk"
);
}
if config.app_cache_without_direct_io() {
tracing::warn!(
"Var collection has an app cache enabled (block_cache/value_cache) but direct_io is off: \
values are cached twice (app cache + OS page cache). Enable direct_io to avoid redundant \
RAM — exception: keeping the app cache for plaintext caching under encryption."
);
}
Self::open_inner(path, config, hook)
}
pub(crate) fn open_inner(
path: impl AsRef<std::path::Path>,
config: Config,
hook: H,
) -> DbResult<Self> {
let compaction_threshold = config.compaction_threshold;
let shard_prefix_bits = config.shard_prefix_bits;
let reversed = config.reversed;
let block_cache = BlockCache::new(&config.block_cache);
let value_cache = ValueCache::new(&config.value_cache);
let engine = Engine::open(path, config)?;
let tree = Self {
index: SkipList::new(reversed),
engine,
block_cache,
value_cache,
compaction_threshold,
shard_prefix_bits,
reversed,
hook,
tx_self: OnceLock::new(),
ttl: TtlSlots::new(),
counter_dirty: OnceLock::new(),
};
let shard_dirs = tree.engine.shard_dirs();
let shard_dir_refs = Engine::shard_dir_refs(&shard_dirs);
let shard_ids = tree.engine.shard_ids();
let hints = tree.engine.hints();
let outcome = recover_var_tree::<K>(
&shard_dir_refs,
&shard_ids,
tree.index(),
hints,
#[cfg(feature = "encryption")]
tree.engine.shard_ciphers(),
)?;
for tail in &outcome.active_tails {
tree.engine.shards()[tail.shard_idx].apply_recovery_tail(tail)?;
}
for (shard_idx, dead) in outcome.shard_dead_bytes {
tree.engine.shards()[shard_idx].install_dead_bytes(dead);
}
for (shard_idx, file_seqs) in outcome.shard_file_seqs {
tree.engine.shards()[shard_idx].install_recovery_file_seqs(file_seqs);
}
for (shard_idx, max_gsn) in &outcome.shard_max_gsns {
tree.engine.shards()[*shard_idx].set_durable_recovered_gsn(*max_gsn);
}
let max_gsn = outcome.max_gsn;
tree.engine
.gsn()
.fetch_max(max_gsn + 1, std::sync::atomic::Ordering::Relaxed);
if hints {
for shard in tree.engine.shards().iter() {
shard.set_key_len(size_of::<K>());
}
}
tracing::info!(
key_size = size_of::<K>(),
entries = tree.len(),
"var_tree recovered"
);
Ok(tree)
}
pub fn clean_shutdown(&self) -> DbResult<()> {
if self.engine.hints() {
self.sync_hints()?;
}
self.engine.close()
}
pub fn close(self) -> DbResult<()> {
self.clean_shutdown()
}
pub fn flush_buffers(&self) -> DbResult<()> {
self.engine.flush_buffers()
}
pub fn dirty_shards(&self) -> usize {
self.engine.dirty_shards()
}
pub(crate) fn durable_flush(&self) -> DbResult<()> {
self.engine.flush()
}
pub fn config(&self) -> &Config {
self.engine.config()
}
}
impl<K: Key, H: WriteHook<K>> CompactionIndex<K> for VarTree<K, H> {
fn update_if_match(&self, key: &K, old_loc: DiskLoc, new_loc: DiskLoc) -> bool {
let guard = self.index.collector().enter();
if let Some(node) = self.index.get(key.as_bytes(), &guard)
&& node.read_loc() == old_loc
{
node.write_loc(new_loc);
return true;
}
false
}
fn invalidate_blocks(&self, shard_id: u8, file_id: u32, total_bytes: u64) {
self.block_cache
.invalidate_file(shard_id, file_id, total_bytes);
}
fn contains_key(&self, key: &K) -> bool {
self.contains(key)
}
fn is_live(&self, _shard_id: u8, key: &K, loc: DiskLoc) -> bool {
let guard = self.index.collector().enter();
self.index
.get(key.as_bytes(), &guard)
.is_some_and(|node| node.read_loc() == loc)
}
}
impl<K: Key, H: WriteHook<K>> Family for VarTree<K, H> {
type Locked<'a>
= MutexGuard<'a, ShardInner>
where
Self: 'a;
type Event<'v> = (K, Option<ByteView>, Option<VarHookNew<'v>>);
type Owned = (K, Option<ByteView>, Option<ByteView>);
#[inline]
fn lock_shard(&self, sid: usize) -> Self::Locked<'_> {
self.engine.shards()[sid].lock()
}
fn should_sync<'a>(locked: &Self::Locked<'a>) -> bool
where
Self: 'a,
{
locked.should_sync()
}
fn sync(&self, sid: usize) -> DbResult<()> {
#[cfg(feature = "ctx-gates")]
crate::ctx_gates_sync::record_sync_call();
self.engine.shards()[sid].flush()
}
fn fire(&self, ev: Self::Event<'_>) {
let (k, old, new) = ev;
self.hook
.on_write(&k, old.as_deref(), new.as_ref().map(|n| n.as_slice()));
}
fn own(ev: Self::Event<'_>) -> Self::Owned {
let (k, old, new) = ev;
(k, old, new.map(VarHookNew::into_owned))
}
fn fire_owned(&self, ev: &Self::Owned) {
let (k, old, new) = ev;
self.hook.on_write(k, old.as_deref(), new.as_deref());
}
fn publish_watch(&self, ev: &Self::Event<'_>, active: bool, wakes: &mut WatchWakeBatch) {
if !H::WATCHES {
return;
}
let Some(channel) = self.hook.watch_channel() else {
return;
};
if !active {
return;
}
let (key, old, new) = ev;
let old_existed = old.is_some();
let old = if self.hook.watch_old() {
old.as_deref()
.map(|bytes| bytes.to_vec().into_boxed_slice())
} else {
None
};
let new = new
.as_ref()
.map(|bytes| bytes.as_slice().to_vec().into_boxed_slice());
channel.publish(
std::sync::Arc::new(WatchEventInner::from_write(
*key,
old,
new,
self.hook.watch_old(),
old_existed,
)),
wakes,
);
}
const WATCHES: bool = H::WATCHES;
fn watch_enter(&self) -> crate::watch::WatchProducer<'_> {
if H::WATCHES {
self.hook
.watch_channel()
.and_then(|channel| channel.enter_producer())
.unwrap_or_else(crate::watch::WatchProducer::inactive)
} else {
crate::watch::WatchProducer::inactive()
}
}
fn apply_projection_for_event(&self, ev: &Self::Event<'_>) -> DbResult<()> {
let (k, old, new) = ev;
let new_bytes = new.as_ref().map(|n| n.as_slice());
self.apply_projection_for_bytes(k, old.as_deref(), new_bytes)
}
fn apply_projection_for_owned(&self, ev: &Self::Owned) -> DbResult<()> {
let (k, old, new) = ev;
self.apply_projection_for_bytes(k, old.as_deref(), new.as_deref())
}
}
impl<K: Key, H: WriteHook<K>> VarTree<K, H> {
#[doc(hidden)]
pub fn build_ttl_index(&self) -> DbResult<()> {
if !H::EXPIRES {
return Ok(());
}
let n = self.engine.shards().len();
let slots = self.ttl.install_and_clear(n);
let _guard = self.index.collector().enter();
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
let mut skipped = 0usize;
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
let sid = self.shard_for(&node.key);
let locked = self.engine.shards()[sid].lock();
if let Ok(bytes) = self.read_value_locked_result(&node.read_loc(), sid as u8, &locked) {
let deadline = self.hook.deadline(&node.key, &bytes);
sync::lock(&slots[sid]).record(&node.key, deadline);
} else {
tracing::warn!(
key = ?node.key.as_bytes(),
"ttl cold build: row omitted from index — value unreadable until rewrite or rebuild"
);
skipped += 1;
}
}
if skipped > 0 {
tracing::warn!(
skipped,
"ttl cold build finished with rows omitted from index — sweep will not see them until rewrite or rebuild"
);
}
Ok(())
}
fn record_ttl(&self, shard_id: usize, key: &K, expires_at: Option<u64>) {
if !H::EXPIRES {
return;
}
self.ttl.record(shard_id, key, expires_at);
}
fn deadline_for_bytes(&self, key: &K, value: &[u8]) -> Option<u64> {
if H::EXPIRES {
self.hook.deadline(key, value)
} else {
None
}
}
#[doc(hidden)]
pub fn ttl_debug_len(&self) -> usize {
self.ttl.debug_len()
}
#[doc(hidden)]
pub fn ttl_debug_deadline(&self, key: &K) -> Option<u64> {
self.ttl.debug_deadline(self.shard_for(key), key)
}
#[doc(hidden)]
pub fn ttl_debug_record(&self, key: &K, deadline: Option<u64>) {
self.ttl.record(self.shard_for(key), key, deadline);
}
#[doc(hidden)]
pub fn ttl_debug_inject_stale(&self, key: &K, deadline: u64) {
self.ttl
.debug_inject_stale(self.shard_for(key), deadline, *key);
}
pub fn sweep_expired(&self, now_ms: u64, budget: usize) -> DbResult<SweepOutcome> {
if !H::EXPIRES {
return Ok(SweepOutcome::default());
}
let Some(slots) = self.ttl.get() else {
return Ok(SweepOutcome::default());
};
let Some(_pass) = self.ttl.begin_pass() else {
return Ok(SweepOutcome::default());
};
let shard_count = slots.len();
let mut remaining = budget;
let mut outcome = SweepOutcome::default();
let guard = self.index.collector().enter();
while remaining > 0 && self.ttl.any_due(now_ms) {
let sid = self.ttl.next_shard(shard_count);
let mut shard_events: Vec<(K, Option<ByteView>, Option<ByteView>)> = Vec::new();
let mut wakes = WatchWakeBatch::default();
let mut locked = self.lock_shard(sid);
let producer = self.watch_enter();
let active = producer.active();
let context_active = H::WATCHES.then(|| crate::watch::ContextActiveGuard::set(active));
let mut idx = sync::lock(&slots[sid]);
while remaining > 0 {
let Some((candidate_dl, key)) = idx.pop_due(now_ms) else {
break;
};
remaining -= 1;
let action = match self.index.get(key.as_bytes(), &guard) {
Some(node) => {
let disk = node.read_loc();
match self.read_value_locked_result(&disk, sid as u8, &locked) {
Ok(bytes) => {
match crate::ttl::catch_deadline(|| {
self.hook.deadline(&key, &bytes)
}) {
Some(Some(dl)) if dl <= now_ms => {
VarSweepAction::Delete(key, Some(bytes), candidate_dl)
}
Some(Some(dl)) => VarSweepAction::Reschedule(key, dl),
Some(None) => {
match crate::ttl::catch_deadline(|| {
self.hook.ttl_decode_failed(&bytes)
}) {
Some(false) => VarSweepAction::Drop,
_ => VarSweepAction::Retry(key),
}
}
None => VarSweepAction::Retry(key),
}
}
Err(_) => VarSweepAction::Retry(key),
}
}
None => VarSweepAction::Drop,
};
match action {
VarSweepAction::Delete(key, stage2_bytes, candidate_dl) => {
drop(idx);
match self.delete_locked(sid, &mut locked, &guard, &key) {
Ok(existed) => {
idx = sync::lock(&slots[sid]);
if existed {
let old = if H::NEEDS_OLD_VALUE && (H::NEEDS_WRITE || active) {
stage2_bytes
} else {
None
};
let event = (key, old.clone(), None);
self.publish_watch(&event, active, &mut wakes);
if H::NEEDS_WRITE {
shard_events.push((key, old, None));
}
outcome.deleted += 1;
}
}
Err(e) => {
sync::lock(&slots[sid]).record(&key, Some(candidate_dl));
let needs_sync = Self::should_sync(&locked);
locked.release();
crate::ttl::log_sweep_failures(None, &outcome);
drop(context_active);
let _ = crate::projection::replay_owned_events(
self,
&shard_events,
needs_sync,
sid,
);
<Self as Family>::wake_watches(wakes);
return Err(e);
}
}
}
VarSweepAction::Reschedule(key, dl) => {
idx.record(&key, Some(dl));
}
VarSweepAction::Retry(key) => {
outcome.failed += 1;
idx.record(&key, Some(now_ms + TTL_RETRY_BACKOFF_MS));
}
VarSweepAction::Drop => {}
}
}
drop(idx);
let needs_sync = Self::should_sync(&locked);
locked.release();
drop(context_active);
let replay =
crate::projection::replay_owned_events(self, &shard_events, needs_sync, sid);
<Self as Family>::wake_watches(wakes);
replay?;
}
drop(guard);
outcome.more = self.ttl.any_due(now_ms);
crate::ttl::log_sweep_failures(None, &outcome);
Ok(outcome)
}
pub fn set_ttl_sweep(&self, enabled: bool) -> DbResult<()> {
if H::EXPIRES {
self.ttl.set_enabled(enabled)
} else {
Ok(())
}
}
pub fn mark_replication_follower(&self) {
if H::EXPIRES {
self.ttl.mark_replication_follower();
}
}
pub fn rebuild_ttl_index(&self) -> DbResult<()> {
if !H::EXPIRES {
return Err(DbError::Client("collection has no TTL policy"));
}
self.build_ttl_index()?;
self.ttl.clear_rebuild_requirement();
Ok(())
}
}
impl<K: Key, H: WriteHook<K>> VarTree<K, H> {
pub fn compact(&self) -> DbResult<usize> {
let mut total_compacted = 0;
for shard in self.engine.shards().iter() {
total_compacted += compact_shard(shard, self, self.compaction_threshold)?;
}
Ok(total_compacted)
}
pub fn get(&self, key: &K) -> Option<ByteView> {
metrics::counter!("armdb.ops", "op" => "get", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.get");
let guard = self.index.collector().enter();
let node = match self.index.get(key.as_bytes(), &guard) {
Some(n) => n,
None => {
#[cfg(feature = "hot-path-tracing")]
tracing::error!(
"VarTree get error: index.get returned None for key {:?}",
key.as_bytes()
);
return None;
}
};
self.read_value_cached(node, &guard)
}
pub fn get_or_err(&self, key: &K) -> DbResult<ByteView> {
let guard = self.index.collector().enter();
let node = self
.index
.get(key.as_bytes(), &guard)
.ok_or(DbError::KeyNotFound)?;
self.read_value_cached_result(node, &guard)
}
pub fn try_get(&self, key: &K) -> DbResult<Option<ByteView>> {
let guard = self.index.collector().enter();
match self.index.get(key.as_bytes(), &guard) {
Some(node) => self.read_value_cached_result(node, &guard).map(Some),
None => Ok(None),
}
}
pub fn try_first(&self) -> DbResult<Option<(K, ByteView)>> {
let guard = self.index.collector().enter();
let mut ptr = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
while !ptr.is_null() {
let node = unsafe { &*ptr };
if !node.is_marked() {
return self
.read_value_cached_result(node, &guard)
.map(|v| Some((node.key, v)));
}
ptr = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
}
Ok(None)
}
pub fn try_last(&self) -> DbResult<Option<(K, ByteView)>> {
let guard = self.index.collector().enter();
let mut ptr = self.index.find_last(&guard);
while !ptr.is_null() {
let node = unsafe { &*ptr };
if !node.is_marked() {
return self
.read_value_cached_result(node, &guard)
.map(|v| Some((node.key, v)));
}
ptr = self.index.find_last_lt(node.key_bytes(), &guard);
}
Ok(None)
}
pub fn put(&self, key: &K, value: &[u8]) -> DbResult<bool> {
let expires_at = self.deadline_for_bytes(key, value);
self.put_with_deadline(key, value, expires_at)
}
pub(crate) fn put_with_deadline(
&self,
key: &K,
value: &[u8],
expires_at: Option<u64>,
) -> DbResult<bool> {
metrics::counter!("armdb.ops", "op" => "put", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.put");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let guard = self.index.collector().enter();
let old_value = if (H::NEEDS_OLD_VALUE
&& (H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active())))
|| H::CLAIMS
{
if let Some(node) = self.index.get(key.as_bytes(), &guard) {
let disk = node.read_loc();
Some(self.read_value_locked_result(&disk, shard_id as u8, locked)?)
} else {
None
}
} else {
None
};
self.claim_guard_bytes(key, old_value.as_deref(), Some(value))?;
let existed = self.put_locked(shard_id, locked, &guard, key, value, expires_at)?;
let ev = if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((*key, old_value, Some(VarHookNew::Borrowed(value))))
} else {
None
};
*ev_out = ev;
Ok(existed)
})
}
pub fn insert(&self, key: &K, value: &[u8]) -> DbResult<()> {
metrics::counter!("armdb.ops", "op" => "insert", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.insert");
let expires_at = self.deadline_for_bytes(key, value);
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let guard = self.index.collector().enter();
if (H::EMITS || (H::CLAIMS && self.hook.claim_targets().is_some()))
&& self.index.get(key.as_bytes(), &guard).is_none()
{
self.claim_guard_bytes(key, None, Some(value))?;
}
self.insert_locked(shard_id, locked, &guard, key, value, expires_at)?;
let ev = if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((*key, None, Some(VarHookNew::Borrowed(value))))
} else {
None
};
*ev_out = ev;
Ok(())
})
}
pub fn delete(&self, key: &K) -> DbResult<bool> {
metrics::counter!("armdb.ops", "op" => "delete", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.delete");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let guard = self.index.collector().enter();
let old_value = if (H::NEEDS_OLD_VALUE
&& (H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active())))
|| H::CLAIMS
{
if let Some(node) = self.index.get(key.as_bytes(), &guard) {
let disk = node.read_loc();
Some(self.read_value_locked_result(&disk, shard_id as u8, locked)?)
} else {
None
}
} else {
None
};
self.claim_guard_bytes(key, old_value.as_deref(), None)?;
let existed = self.delete_locked(shard_id, locked, &guard, key)?;
let ev =
if existed && (H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active())) {
Some((*key, old_value, None))
} else {
None
};
*ev_out = ev;
Ok(existed)
})
}
pub fn atomic<R>(
&self,
shard_key: &K,
f: impl FnOnce(&mut VarShard<'_, K, H>) -> DbResult<R>,
) -> DbResult<R> {
let _atomic_depth = crate::AtomicDepthGuard::try_enter()?;
let shard_id = self.shard_for(shard_key);
let locked = self.lock_shard(shard_id);
let guard = self.index.collector().enter();
let mut shard = VarShard {
tree: self,
held: Held {
locked,
sid: shard_id,
events: Vec::new(),
watch_wakes: WatchWakeBatch::default(),
publish_watches: true,
},
guard,
};
let result = f(&mut shard);
let VarShard { held, guard, .. } = shard;
let Held {
locked,
events,
sid,
watch_wakes,
..
} = held;
let needs_sync = Self::should_sync(&locked);
locked.release();
let result =
crate::projection::finish_atomic_teardown(self, sid, result, needs_sync, &events);
drop(guard);
<Self as Family>::wake_watches(watch_wakes);
result
}
pub(crate) fn mark_counter_dirty(&self) -> DbResult<()> {
match self.counter_dirty.get() {
Some(d) => d.mark(),
None => Ok(()),
}
}
fn apply_projection_for_bytes(
&self,
key: &K,
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<()> {
if !H::PROJECTS && !H::COUNTS {
return Ok(());
}
let (max_rows, max_bytes) = self.effective_projection_limits();
let mut plan = match (H::PROJECTS, self.hook.projection_targets()) {
(true, Some(targets)) => crate::projection::diff_projection_bytes(
&self.hook, targets, max_rows, max_bytes, key, old, new,
)?,
_ => crate::projection::PendingProjection::default(),
};
if H::COUNTS
&& let Some(counter_targets) = self.hook.counter_targets()
{
let deltas = crate::counter::diff_counters_bytes(
&self.hook,
counter_targets,
max_rows,
max_bytes,
key,
old,
new,
)?;
if !deltas.is_empty() {
plan.set_deltas(deltas, counter_targets.clone());
}
}
plan.apply_or_incomplete()
}
fn put_locked(
&self,
shard_id: usize,
inner: &mut ShardInner,
guard: &seize::LocalGuard<'_>,
key: &K,
value: &[u8],
expires_at: Option<u64>,
) -> DbResult<bool> {
let (disk_loc, _gsn) = inner.append_entry(shard_id as u8, key.as_bytes(), value, false)?;
if let Some(existing) = self.index.get(key.as_bytes(), guard) {
let old_disk = existing.read_loc();
inner.add_dead_bytes(
old_disk.file_id,
crate::entry::entry_size(size_of::<K>(), old_disk.len),
);
existing.write_loc(disk_loc);
self.record_ttl(shard_id, key, expires_at);
return Ok(true);
}
let height = random_height();
let node_ptr = VarNode::alloc(*key, disk_loc, height);
match self.index.insert(node_ptr, guard) {
InsertResult::Inserted => {
self.record_ttl(shard_id, key, expires_at);
Ok(false)
}
InsertResult::Exists(existing) => {
let old_disk = existing.read_loc();
inner.add_dead_bytes(
old_disk.file_id,
crate::entry::entry_size(size_of::<K>(), old_disk.len),
);
existing.write_loc(disk_loc);
self.record_ttl(shard_id, key, expires_at);
unsafe {
VarNode::<K>::dealloc_node(node_ptr);
}
Ok(true)
}
}
}
fn insert_locked(
&self,
shard_id: usize,
inner: &mut ShardInner,
guard: &seize::LocalGuard<'_>,
key: &K,
value: &[u8],
expires_at: Option<u64>,
) -> DbResult<()> {
if self.index.get(key.as_bytes(), guard).is_some() {
return Err(DbError::KeyExists);
}
let (disk_loc, _gsn) = inner.append_entry(shard_id as u8, key.as_bytes(), value, false)?;
let height = random_height();
let node_ptr = VarNode::alloc(*key, disk_loc, height);
match self.index.insert(node_ptr, guard) {
InsertResult::Inserted => {
self.record_ttl(shard_id, key, expires_at);
Ok(())
}
InsertResult::Exists(_existing) => {
inner.add_dead_bytes(
disk_loc.file_id,
crate::entry::entry_size(size_of::<K>(), disk_loc.len),
);
unsafe { VarNode::<K>::dealloc_node(node_ptr) };
Err(DbError::KeyExists)
}
}
}
fn delete_locked(
&self,
shard_id: usize,
inner: &mut ShardInner,
guard: &seize::LocalGuard<'_>,
key: &K,
) -> DbResult<bool> {
#[cfg(test)]
if crate::durability::ttl_delete_should_fail() {
return Err(DbError::Internal("test: TTL delete injected failure"));
}
if self.index.get(key.as_bytes(), guard).is_none() {
return Ok(false);
}
inner.append_entry(shard_id as u8, key.as_bytes(), &[], true)?;
let removed = self.index.remove(key.as_bytes(), guard);
if let Some(node_ptr) = removed {
let disk = unsafe { &*node_ptr }.read_loc();
inner.add_dead_bytes(
disk.file_id,
crate::entry::entry_size(size_of::<K>(), disk.len),
);
}
if removed.is_some() {
self.record_ttl(shard_id, key, None);
}
Ok(removed.is_some())
}
pub fn contains(&self, key: &K) -> bool {
let guard = self.index.collector().enter();
self.index.get(key.as_bytes(), &guard).is_some()
}
pub fn entry_len(&self, key: &K) -> Option<u32> {
let guard = self.index.collector().enter();
self.index
.get(key.as_bytes(), &guard)
.map(|node| node.read_loc().len)
}
pub fn first(&self) -> Option<(K, ByteView)> {
let guard = self.index.collector().enter();
let mut ptr = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
while !ptr.is_null() {
let node = unsafe { &*ptr };
if !node.is_marked()
&& let Some(v) = self.read_value_cached(node, &guard)
{
return Some((node.key, v));
}
ptr = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
}
None
}
pub fn last(&self) -> Option<(K, ByteView)> {
self.iter().next_back()
}
fn resolve_front(&self, bound: &Bound<&K>, guard: &seize::LocalGuard<'_>) -> *mut VarNode<K> {
match bound {
Bound::Included(k) => self.index.find_first_ge(k.as_bytes(), guard),
Bound::Excluded(k) => {
let ge = self.index.find_first_ge(k.as_bytes(), guard);
if !ge.is_null()
&& !unsafe { &*ge }.is_marked()
&& unsafe { &*ge }.key_bytes() == k.as_bytes()
{
crate::skiplist::strip_mark(unsafe {
(*ge).tower(0).load(std::sync::atomic::Ordering::Acquire)
})
} else {
ge
}
}
Bound::Unbounded => crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
}),
}
}
fn prefix_bounds(&self, prefix: &[u8]) -> (K, Bound<K>) {
if self.reversed {
let mut search = K::zeroed();
search.as_bytes_mut().fill(0xFF);
search.as_bytes_mut()[..prefix.len()].copy_from_slice(prefix);
let mut end_key = K::zeroed();
end_key.as_bytes_mut()[..prefix.len()].copy_from_slice(prefix);
(search, Bound::Included(end_key))
} else {
let mut search = K::zeroed();
search.as_bytes_mut()[..prefix.len()].copy_from_slice(prefix);
let end = crate::scope::key_math::prefix_to_end_bound::<K>(prefix);
(search, end)
}
}
pub fn prefix_iter(&self, prefix: &[u8]) -> VarIter<'_, K, H> {
let guard = self.index.collector().enter();
let (search_key, end) = self.prefix_bounds(prefix);
let front = self.index.find_first_ge(search_key.as_bytes(), &guard);
VarIter {
tree: self,
front,
back: None,
back_finger: BackFinger::empty(),
last_front: None,
last_back: None,
end,
start: Bound::Included(search_key),
reversed: self.reversed,
done: false,
_guard: guard,
}
}
pub fn iter(&self) -> VarIter<'_, K, H> {
let guard = self.index.collector().enter();
let front = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
VarIter {
tree: self,
front,
back: None,
back_finger: BackFinger::empty(),
last_front: None,
last_back: None,
end: Bound::Unbounded,
start: Bound::Unbounded,
reversed: self.reversed,
done: false,
_guard: guard,
}
}
pub fn range(&self, start: &K, end: &K) -> VarIter<'_, K, H> {
self.range_bounds(Bound::Included(start), Bound::Excluded(end))
}
pub fn range_bounds(&self, start: Bound<&K>, end: Bound<&K>) -> VarIter<'_, K, H> {
let guard = self.index.collector().enter();
if self.reversed {
let front = self.resolve_front(&end, &guard);
VarIter {
tree: self,
front,
back: None,
back_finger: BackFinger::empty(),
last_front: None,
last_back: None,
end: bound_owned(&start),
start: bound_owned(&end),
reversed: true,
done: false,
_guard: guard,
}
} else {
let front = self.resolve_front(&start, &guard);
VarIter {
tree: self,
front,
back: None,
back_finger: BackFinger::empty(),
last_front: None,
last_back: None,
end: bound_owned(&end),
start: bound_owned(&start),
reversed: false,
done: false,
_guard: guard,
}
}
}
pub(crate) fn keys_range_bounds_dir(
&self,
start: Bound<&K>,
end: Bound<&K>,
dir: Dir,
) -> SkipKeyKeys<'_, K, VarNode<K>> {
let guard = self.index.collector().enter();
let against = dir_from(self.reversed) != dir;
let (front, iter_end, iter_start, storage_reversed) = if against {
if self.reversed {
(
std::ptr::null_mut(),
bound_owned(&start),
bound_owned(&end),
true,
)
} else {
(
std::ptr::null_mut(),
bound_owned(&end),
bound_owned(&start),
false,
)
}
} else if self.reversed {
let front = self.resolve_front(&end, &guard);
(front, bound_owned(&start), bound_owned(&end), true)
} else {
let front = self.resolve_front(&start, &guard);
(front, bound_owned(&end), bound_owned(&start), false)
};
SkipKeyKeys::new(SkipKeyIter::bounded_dir(
&self.index,
iter_start,
iter_end,
front,
against,
storage_reversed,
guard,
))
}
pub(crate) fn range_bounds_dir(
&self,
start: Bound<&K>,
end: Bound<&K>,
dir: Dir,
) -> DirRangeIter<VarIter<'_, K, H>> {
let iter = self.range_bounds(start, end);
DirRangeIter::from_storage_match(iter, dir == dir_from(self.reversed))
}
pub(crate) fn try_range_bounds(
&self,
start: Bound<&K>,
end: Bound<&K>,
) -> TryVarIter<'_, K, H> {
TryVarIter {
inner: self.range_bounds(start, end),
failed: false,
}
}
pub(crate) fn try_range_bounds_dir(
&self,
start: Bound<&K>,
end: Bound<&K>,
dir: Dir,
) -> crate::scope::DirRangeIter<TryVarIter<'_, K, H>> {
crate::scope::DirRangeIter::from_storage_match(
self.try_range_bounds(start, end),
dir == dir_from(self.reversed),
)
}
pub(crate) fn reversed(&self) -> bool {
self.reversed
}
pub fn scope<const N: usize>(
&self,
key: &K,
) -> crate::scope::Scope<'_, crate::scope::TreeScan<&Self>, N> {
crate::scope::tree_scope::<_, _, N>(self, key, dir_from(self.reversed()))
}
pub fn len(&self) -> usize {
self.index.len()
}
pub(crate) fn try_count_live_keys(&self) -> DbResult<u64> {
let guard = self.index.collector().enter();
let mut count = 0u64;
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
#[cfg(test)]
if let Some(error) = injected_count_live_keys_fault(count) {
return Err(error);
}
count += 1;
}
drop(guard);
Ok(count)
}
pub fn is_empty(&self) -> bool {
self.index.is_empty()
}
pub fn sync_hints(&self) -> DbResult<()> {
for shard in self.engine.shards().iter() {
shard.write_active_hint(size_of::<K>())?;
}
Ok(())
}
pub fn warmup(&self) -> DbResult<()> {
use std::collections::BTreeSet;
let guard = self.index.collector().enter();
let mut blocks: BTreeSet<(u8, u32, u64)> = BTreeSet::new();
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
let disk = node.read_loc();
if disk.is_value_cache_routed() {
continue;
}
let shard_id = self.shard_for(&node.key) as u8;
let block_offset = disk.offset as u64 & !4095;
blocks.insert((shard_id, disk.file_id, block_offset));
if (disk.offset & 4095) as usize + disk.len as usize > 4096 {
blocks.insert((shard_id, disk.file_id, block_offset + 4096));
}
}
drop(guard);
for (shard_id, file_id, block_offset) in &blocks {
let key = BlockKey {
shard_id: *shard_id,
file_id: *file_id,
block_offset: *block_offset,
};
if self.block_cache.get(&key).is_some() {
continue;
}
let shard = &self.engine.shards()[*shard_id as usize];
let (buf, is_full_block) = shard.read_block(*file_id, *block_offset)?;
if is_full_block {
self.block_cache.insert(key, Arc::new(buf));
}
}
Ok(())
}
pub(crate) fn index(&self) -> &SkipList<VarNode<K>> {
&self.index
}
pub fn migrate(
&self,
f: impl Fn(&K, &[u8]) -> crate::MigrateAction<ByteView>,
) -> DbResult<usize> {
self.migrate_inner(f, true)
}
pub(crate) fn migrate_inner(
&self,
f: impl Fn(&K, &[u8]) -> crate::MigrateAction<ByteView>,
fire_init: bool,
) -> DbResult<usize> {
use crate::MigrateAction;
let guard = self.index.collector().enter();
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
let mut count = 0;
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
let value = match self.read_value_cached_result(node, &guard) {
Ok(v) => v,
Err(e) => {
tracing::error!(
key = ?node.key.as_bytes(),
error = %e,
"var_tree migrate: value read failed — aborting migration step"
);
return Err(e);
}
};
match f(&node.key, &value) {
MigrateAction::Keep => {
if fire_init && H::NEEDS_INIT {
self.hook.on_init(&node.key, &value);
}
}
MigrateAction::Update(new_value) => {
let shard_id = self.shard_for(&node.key);
{
let mut inner = self.engine.shards()[shard_id].lock();
self.claim_guard_bytes(&node.key, Some(&value), Some(&new_value))?;
let expires_at = self.deadline_for_bytes(&node.key, &new_value);
self.put_locked(
shard_id, &mut inner, &guard, &node.key, &new_value, expires_at,
)?;
}
if fire_init && H::NEEDS_INIT {
self.hook.on_init(&node.key, &new_value);
}
count += 1;
}
MigrateAction::Delete => {
let shard_id = self.shard_for(&node.key);
let mut inner = self.engine.shards()[shard_id].lock();
self.claim_guard_bytes(&node.key, Some(&value), None)?;
self.delete_locked(shard_id, &mut inner, &guard, &node.key)?;
count += 1;
}
}
}
tracing::info!(mutations = count, "var_tree migration complete");
Ok(count)
}
pub(crate) fn replay_init(&self) {
if !H::NEEDS_INIT {
return;
}
let guard = self.index.collector().enter();
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
let mut count = 0usize;
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
let value = match self.read_value_cached(node, &guard) {
Some(v) => v,
None => {
tracing::warn!(
key = ?node.key.as_bytes(),
"var_tree replay_init: skipping entry — value read failed"
);
continue;
}
};
self.hook.on_init(&node.key, &value);
count += 1;
}
tracing::debug!(replayed = count, "var_tree replay_init complete");
}
pub(crate) fn effective_projection_limits(&self) -> (usize, usize) {
let cfg = self.config();
let (hook_rows, hook_bytes) = self.hook.projection_limits();
(
hook_rows.min(cfg.max_projected_rows),
hook_bytes.min(cfg.max_projected_bytes),
)
}
#[allow(clippy::collapsible_if)]
#[inline]
fn claim_guard_bytes(&self, key: &K, old: Option<&[u8]>, new: Option<&[u8]>) -> DbResult<()> {
#[cfg(feature = "armour")]
if H::EMITS && !crate::txn_depth::in_stream_apply() {
if let Some(targets) = self.hook.stream_targets() {
let collection = self
.counter_dirty
.get()
.map(crate::counter_dirty::CounterDirty::name)
.unwrap_or("");
return Err(DbError::ChangeStreamRequiresTx {
collection,
queue: targets.first_name(),
});
}
}
if !H::CLAIMS {
return Ok(());
}
if self.hook.claim_targets().is_none() {
return Ok(());
}
let collection = self
.counter_dirty
.get()
.map(crate::counter_dirty::CounterDirty::name)
.unwrap_or("");
crate::unique::guard_nontx_write_bytes(
collection,
&self.hook,
self.effective_projection_limits(),
key,
old,
new,
)
}
pub(crate) fn reconcile_projections(&self) -> DbResult<()> {
if !H::PROJECTS {
return Ok(());
}
let Some(targets) = self.hook.projection_targets() else {
return Ok(());
};
let (max_rows, max_bytes) = self.effective_projection_limits();
crate::projection::reconcile_carrier_projections(
targets,
|emit| {
let guard = self.index.collector().enter();
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
let mut err: Option<DbError> = None;
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
if err.is_some() {
break;
}
let value = match crate::projection::reconcile_value_read_fault()
.and_then(|()| self.read_value_cached_result(node, &guard))
{
Ok(v) => v,
Err(e) => {
err = Some(e);
break;
}
};
let mut sink =
crate::projection::ProjectionSink::new(targets, max_rows, max_bytes);
match self
.hook
.project(&node.key, None, Some(value.as_ref()), &mut sink)
.and_then(|()| {
for row in sink.rows() {
emit(row.target, &row.key, &row.value)?;
}
Ok(())
}) {
Ok(()) => {}
Err(e) => err = Some(e),
}
}
err.map_or(Ok(()), Err)
},
true,
)
}
pub(crate) fn reconcile_counters(&self) -> DbResult<u64> {
if !H::COUNTS {
return Ok(0);
}
let Some(targets) = self.hook.counter_targets() else {
return Ok(0);
};
let (max_rows, max_bytes) = self.effective_projection_limits();
crate::counter::recount(
|k: &K, v: &[u8], sink| self.hook.counter_project(k, None, Some(v), sink),
targets,
(max_rows, max_bytes),
&mut |visit| {
let guard = self.index.collector().enter();
let mut current = crate::skiplist::strip_mark(unsafe {
(*self.index.head_ptr())
.tower(0)
.load(std::sync::atomic::Ordering::Acquire)
});
let mut err: Option<DbError> = None;
while !current.is_null() {
let node = unsafe { &*current };
current = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if node.is_marked() {
continue;
}
if err.is_some() {
break;
}
let value = match crate::projection::reconcile_value_read_fault()
.and_then(|()| self.read_value_cached_result(node, &guard))
{
Ok(v) => v,
Err(e) => {
err = Some(e);
break;
}
};
if let Err(e) = visit(&node.key, value.as_ref()) {
err = Some(e);
}
}
err.map_or(Ok(()), Err)
},
)
}
pub(crate) fn counter_target_names(&self) -> Vec<&'static str> {
self.hook
.counter_targets()
.map(|t| t.all().map(|m| m.collection_name()).collect())
.unwrap_or_default()
}
fn read_value_cached_inner(&self, disk: &DiskLoc, shard_id: u8) -> DbResult<ByteView> {
let len = disk.len as usize;
let start = (disk.offset & 4095) as usize;
if start + len > 8192 {
{
let shard = &self.engine.shards()[shard_id as usize];
let inner = shard.lock();
if inner.active.file_id == disk.file_id {
if let Some(bytes) = inner.write_buf.read(disk.offset as u64, len) {
return Ok(ByteView::new(bytes));
}
if inner.straddles_write_buf(disk) {
return Ok(ByteView::from_vec(
inner.read_straddling_value_locked(disk)?,
));
}
}
}
let shard = &self.engine.shards()[shard_id as usize];
#[cfg(test)]
TEST_DISK_VALUE_READS.with(|c| c.set(c.get() + 1));
return Ok(ByteView::from_vec(shard.read_value(
disk.file_id,
disk.offset,
len,
)?));
}
let block_offset = disk.offset as u64 & !4095;
let cache_key = BlockKey {
shard_id,
file_id: disk.file_id,
block_offset,
};
if let Some(block) = self.block_cache.get(&cache_key) {
metrics::counter!("armdb.block_cache.hit").increment(1);
if start + len > 4096 {
let shard = &self.engine.shards()[shard_id as usize];
let inner = shard.lock();
if inner.active.file_id == disk.file_id && inner.straddles_write_buf(disk) {
return Ok(ByteView::from_vec(
inner.read_straddling_value_locked(disk)?,
));
}
}
return Self::extract_from_block(&block, start, len, || {
self.get_or_read_block(shard_id, disk.file_id, block_offset + 4096)
});
}
{
let shard = &self.engine.shards()[shard_id as usize];
let inner = shard.lock();
if inner.active.file_id == disk.file_id {
if let Some(bytes) = inner.write_buf.read(disk.offset as u64, len) {
return Ok(ByteView::new(bytes));
}
if inner.straddles_write_buf(disk) {
return Ok(ByteView::from_vec(
inner.read_straddling_value_locked(disk)?,
));
}
}
}
metrics::counter!("armdb.block_cache.miss").increment(1);
#[cfg(test)]
TEST_DISK_VALUE_READS.with(|c| c.set(c.get() + 1));
let block = self.get_or_read_block(shard_id, disk.file_id, block_offset)?;
Self::extract_from_block(&block, start, len, || {
self.get_or_read_block(shard_id, disk.file_id, block_offset + 4096)
})
}
fn read_value_cached_result(
&self,
node: &VarNode<K>,
_guard: &seize::LocalGuard<'_>,
) -> DbResult<ByteView> {
let shard_id = self.shard_for(&node.key) as u8;
for _ in 0..MAX_STALE_RETRIES {
let disk = node.read_loc();
let is_large = disk.is_value_cache_routed();
if is_large {
let vkey = ValueKey {
shard_id,
file_id: disk.file_id,
offset: disk.offset,
};
if let Some(v) = self.value_cache.get(&vkey) {
let now = node.read_loc();
if now.file_id == disk.file_id && now.offset == disk.offset {
metrics::counter!("armdb.value_cache.hit").increment(1);
return Ok(v);
}
continue; }
metrics::counter!("armdb.value_cache.miss").increment(1);
match self.read_value_cached_inner(&disk, shard_id) {
Ok(v) => {
self.value_cache.insert(vkey, v.clone());
return Ok(v);
}
Err(DbError::StaleDiskLoc) => {
metrics::counter!("armdb.read.stale_retry", "tree" => "var_tree")
.increment(1);
continue;
}
Err(e) => return Err(e),
}
}
match self.read_value_cached_inner(&disk, shard_id) {
Ok(v) => return Ok(v),
Err(DbError::StaleDiskLoc) => {
metrics::counter!("armdb.read.stale_retry", "tree" => "var_tree").increment(1);
continue;
}
Err(e) => return Err(e),
}
}
Err(DbError::StaleDiskLoc)
}
pub(crate) fn read_decoded<T>(
&self,
node: &VarNode<K>,
_guard: &seize::LocalGuard<'_>,
cache: &DecodedCache<T>,
decode: impl Fn(&ByteView) -> DbResult<T>,
) -> DbResult<Arc<T>> {
let shard_id = self.shard_for(&node.key) as u8;
for _ in 0..MAX_STALE_RETRIES {
let disk = node.read_loc();
let vkey = ValueKey {
shard_id,
file_id: disk.file_id,
offset: disk.offset,
};
if let Some(v) = cache.get(&vkey) {
let now = node.read_loc();
if now.file_id == disk.file_id && now.offset == disk.offset {
metrics::counter!("armdb.decoded_cache.hit").increment(1);
return Ok(v);
}
continue; }
if cache.is_enabled() {
metrics::counter!("armdb.decoded_cache.miss").increment(1);
}
let routed = disk.is_value_cache_routed();
let bytes = if routed && let Some(v) = self.value_cache.get(&vkey) {
let now = node.read_loc();
if now.file_id != disk.file_id || now.offset != disk.offset {
continue;
}
metrics::counter!("armdb.value_cache.hit").increment(1);
v
} else {
if routed {
metrics::counter!("armdb.value_cache.miss").increment(1);
}
match self.read_value_cached_inner(&disk, shard_id) {
Ok(v) => {
if routed {
self.value_cache.insert(vkey, v.clone());
}
v
}
Err(DbError::StaleDiskLoc) => {
metrics::counter!("armdb.read.stale_retry", "tree" => "var_tree")
.increment(1);
continue;
}
Err(e) => return Err(e),
}
};
let value = Arc::new(decode(&bytes)?);
cache.insert(vkey, Arc::clone(&value), disk.len);
return Ok(value);
}
Err(DbError::StaleDiskLoc)
}
pub(crate) fn try_get_decoded<T>(
&self,
key: &K,
cache: &DecodedCache<T>,
decode: impl Fn(&ByteView) -> DbResult<T>,
) -> DbResult<Option<Arc<T>>> {
metrics::counter!("armdb.ops", "op" => "get_shared", "tree" => "var_tree").increment(1);
let guard = self.index.collector().enter();
match self.index.get(key.as_bytes(), &guard) {
Some(node) => self.read_decoded(node, &guard, cache, decode).map(Some),
None => Ok(None),
}
}
fn read_value_cached(
&self,
node: &VarNode<K>,
guard: &seize::LocalGuard<'_>,
) -> Option<ByteView> {
let result = self.read_value_cached_result(node, guard);
#[cfg(feature = "hot-path-tracing")]
if let Err(ref _e) = result {
tracing::error!("VarTree read_value_cached error: {:?}", _e);
}
result.ok()
}
fn extract_from_block(
block: &AlignedBuf,
start: usize,
len: usize,
next_block: impl FnOnce() -> DbResult<Arc<AlignedBuf>>,
) -> DbResult<ByteView> {
debug_assert!(
start + len <= 8192,
"extract_from_block supports at most 2 blocks (8192 bytes)"
);
if start + len <= 4096 {
Ok(ByteView::new(&block[start..start + len]))
} else {
let next = next_block()?;
let first_part = &block[start..];
let second_len = len - first_part.len();
let mut combined = Vec::with_capacity(len);
combined.extend_from_slice(first_part);
combined.extend_from_slice(&next[..second_len]);
Ok(ByteView::from_vec(combined))
}
}
fn get_or_read_block(
&self,
shard_id: u8,
file_id: u32,
block_offset: u64,
) -> DbResult<Arc<AlignedBuf>> {
let key = BlockKey {
shard_id,
file_id,
block_offset,
};
if let Some(cached) = self.block_cache.get(&key) {
return Ok(cached);
}
let shard = &self.engine.shards()[shard_id as usize];
let (buf, is_full_block) = shard.read_block(file_id, block_offset)?;
let arc = Arc::new(buf);
if is_full_block {
self.block_cache.insert(key, arc.clone());
}
Ok(arc)
}
pub fn cas(&self, key: &K, expected: &[u8], new_value: &[u8]) -> DbResult<()> {
metrics::counter!("armdb.ops", "op" => "cas", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.cas");
let expires_at = self.deadline_for_bytes(key, new_value);
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let guard = self.index.collector().enter();
let existing = self
.index
.get(key.as_bytes(), &guard)
.ok_or(DbError::KeyNotFound)?;
let disk = existing.read_loc();
let current = self.read_value_locked_result(&disk, shard_id as u8, locked)?;
if current.as_ref() != expected {
return Err(DbError::CasMismatch);
}
self.claim_guard_bytes(key, Some(current.as_ref()), Some(new_value))?;
let (new_disk_loc, _gsn) =
locked.append_entry(shard_id as u8, key.as_bytes(), new_value, false)?;
let old_disk = existing.read_loc();
locked.add_dead_bytes(
old_disk.file_id,
crate::entry::entry_size(size_of::<K>(), old_disk.len),
);
existing.write_loc(new_disk_loc);
self.record_ttl(shard_id, key, expires_at);
let ev = if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((
*key,
if H::NEEDS_OLD_VALUE {
Some(current)
} else {
None
},
Some(VarHookNew::Borrowed(new_value)),
))
} else {
None
};
*ev_out = ev;
Ok(())
})
}
pub fn compare_delete(&self, key: &K, expected: &[u8]) -> DbResult<()> {
metrics::counter!("armdb.ops", "op" => "compare_delete", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.compare_delete");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let guard = self.index.collector().enter();
let existing = self
.index
.get(key.as_bytes(), &guard)
.ok_or(DbError::KeyNotFound)?;
let disk = existing.read_loc();
let current = self.read_value_locked_result(&disk, shard_id as u8, locked)?;
if current.as_ref() != expected {
return Err(DbError::CasMismatch);
}
self.claim_guard_bytes(key, Some(current.as_ref()), None)?;
let removed = self.delete_locked(shard_id, locked, &guard, key)?;
debug_assert!(
removed,
"compare_delete: key must exist after a successful match"
);
let ev = if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((
*key,
if H::NEEDS_OLD_VALUE {
Some(current)
} else {
None
},
None,
))
} else {
None
};
*ev_out = ev;
Ok(())
})
}
pub fn update(&self, key: &K, f: impl FnOnce(&[u8]) -> ByteView) -> DbResult<Option<ByteView>> {
self.update_inner(key, f, false)
}
pub fn fetch_update(
&self,
key: &K,
f: impl FnOnce(&[u8]) -> ByteView,
) -> DbResult<Option<ByteView>> {
self.update_inner(key, f, true)
}
fn try_update_locked(
&self,
shard_id: usize,
inner: &mut ShardInner,
guard: &seize::LocalGuard<'_>,
key: &K,
f: impl FnOnce(&[u8]) -> DbResult<Option<ByteView>>,
) -> DbResult<UpdateOutcome> {
let existing = match self.index.get(key.as_bytes(), guard) {
Some(n) => n,
None => return Ok(UpdateOutcome::Missing),
};
let disk = existing.read_loc();
let current = self.read_value_locked_result(&disk, shard_id as u8, inner)?;
let new_value = match f(¤t)? {
Some(v) => v,
None => return Ok(UpdateOutcome::Unchanged(current)),
};
self.claim_guard_bytes(key, Some(current.as_ref()), Some(&new_value))?;
let (new_disk_loc, _gsn) =
inner.append_entry(shard_id as u8, key.as_bytes(), &new_value, false)?;
let old_disk = existing.read_loc();
inner.add_dead_bytes(
old_disk.file_id,
crate::entry::entry_size(size_of::<K>(), old_disk.len),
);
existing.write_loc(new_disk_loc);
self.record_ttl(shard_id, key, self.deadline_for_bytes(key, &new_value));
Ok(UpdateOutcome::Updated {
old: current,
new: new_value,
})
}
pub(crate) fn try_update_inner(
&self,
key: &K,
f: impl FnOnce(&[u8]) -> DbResult<Option<ByteView>>,
return_old: bool,
) -> DbResult<Option<ByteView>> {
metrics::counter!("armdb.ops", "op" => "update", "tree" => "var_tree").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("var_tree.update");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let guard = self.index.collector().enter();
match self.try_update_locked(shard_id, locked, &guard, key, f)? {
UpdateOutcome::Missing => Ok(None),
UpdateOutcome::Unchanged(current) => Ok(Some(current)),
UpdateOutcome::Updated { old, new } => {
let (result, ev) =
if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
let event_old = if H::NEEDS_OLD_VALUE {
Some(old.clone())
} else {
None
};
let result = Some(if return_old { old } else { new.clone() });
(
result,
Some((*key, event_old, Some(VarHookNew::Owned(new)))),
)
} else {
(Some(if return_old { old } else { new }), None)
};
{
*ev_out = ev;
Ok(result)
}
}
}
})
}
fn update_inner(
&self,
key: &K,
f: impl FnOnce(&[u8]) -> ByteView,
return_old: bool,
) -> DbResult<Option<ByteView>> {
self.try_update_inner(key, |bytes| Ok(Some(f(bytes))), return_old)
}
fn read_value_locked_result(
&self,
disk: &DiskLoc,
shard_id: u8,
inner: &ShardInner,
) -> DbResult<ByteView> {
#[cfg(test)]
TEST_LOCKED_VALUE_READS.with(|c| c.set(c.get() + 1));
let len = disk.len as usize;
if inner.active.file_id == disk.file_id {
if let Some(bytes) = inner.write_buf.read(disk.offset as u64, len) {
return Ok(ByteView::new(bytes));
}
if inner.straddles_write_buf(disk) {
return Ok(ByteView::from_vec(
inner.read_straddling_value_locked(disk)?,
));
}
}
let block_offset = disk.offset as u64 & !4095;
let start = (disk.offset & 4095) as usize;
if start + len <= 4096 {
let cache_key = BlockKey {
shard_id,
file_id: disk.file_id,
block_offset,
};
if let Some(block) = self.block_cache.get(&cache_key) {
return Ok(ByteView::new(&block[start..start + len]));
}
let (buf, is_full_block) = inner.read_block_locked(disk.file_id, block_offset)?;
let arc = Arc::new(buf);
if is_full_block {
self.block_cache.insert(cache_key, arc.clone());
}
return Ok(ByteView::new(&arc[start..start + len]));
}
let bytes = inner.read_value_from_disk_locked(disk)?;
Ok(ByteView::new(&bytes))
}
pub fn shard_for(&self, key: &K) -> usize {
crate::shard_for_key(key, self.shard_prefix_bits, self.engine.shards().len())
}
pub(crate) fn name(&self) -> &'static str {
""
}
pub(crate) fn typ_hash(&self) -> u64 {
0
}
pub(crate) fn shard_count(&self) -> usize {
self.engine.shards().len()
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn bind_tx_self(arc: &Arc<Self>) {
let _ = arc.tx_self.set(Arc::downgrade(arc));
}
pub(crate) fn tx_arc(&self) -> DbResult<Arc<Self>> {
self.tx_self
.get()
.and_then(Weak::upgrade)
.ok_or(DbError::Internal(
"VarTree::bind_tx_self must be called before transaction use",
))
}
pub(crate) fn tx_install_projection_metadata(&self, db: &crate::armour::Db) -> DbResult<()> {
if H::PROJECTS
&& let Some(targets) = self.hook.projection_targets()
{
db.tx_register_projection_targets(targets)?;
}
if H::COUNTS
&& let Some(targets) = self.hook.counter_targets()
{
db.tx_register_counter_targets(targets)?;
}
if H::CLAIMS
&& let Some(targets) = self.hook.claim_targets()
{
db.tx_register_claim_targets(targets)?;
}
if H::EMITS
&& let Some(targets) = self.hook.stream_targets()
{
db.tx_register_stream_targets(targets)?;
}
Ok(())
}
pub(crate) fn tx_preplan_projection_targets(
&self,
) -> DbResult<Vec<Arc<dyn crate::txn::participant::TxParticipant>>> {
let mut out = Vec::new();
if H::PROJECTS
&& let Some(targets) = self.hook.projection_targets()
{
out.extend(
targets
.erased_targets()
.iter()
.filter_map(|h| targets.resolve_tx_participant(h.name)),
);
}
if H::COUNTS
&& let Some(targets) = self.hook.counter_targets()
{
out.extend(targets.all().map(|m| m.as_tx_participant()));
}
if H::CLAIMS
&& let Some(targets) = self.hook.claim_targets()
{
out.extend(targets.all().map(|t| t.as_tx_participant()));
}
if H::EMITS
&& let Some(targets) = self.hook.stream_targets()
{
out.extend(targets.tx_participants());
}
Ok(out)
}
pub(crate) fn tx_preplan_stream_targets(
&self,
) -> Vec<Arc<dyn crate::txn::participant::TxParticipant>> {
self.hook
.stream_targets()
.map(|t| t.tx_participants())
.unwrap_or_default()
}
pub(crate) fn tx_prepare_streams(
&self,
) -> DbResult<Vec<crate::queue::retention::ProducerPermit>> {
self.hook
.stream_targets()
.map_or(Ok(Vec::new()), crate::QueueTargets::prepare_streams)
}
pub(crate) fn tx_stream_writes(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<crate::txn::participant::TxStreamWrites> {
if !H::EMITS {
return Ok(crate::txn::participant::TxStreamWrites::empty());
}
let Some(targets) = self.hook.stream_targets() else {
return Ok(crate::txn::participant::TxStreamWrites::empty());
};
let key = K::from_bytes(key);
let (max_rows, max_bytes) = self.effective_projection_limits();
let mut sink = crate::EmitSink::new(targets, max_rows, max_bytes);
self.hook.stream_project(&key, old, new, &mut sink)?;
targets.lower(&sink)
}
pub(crate) fn tx_project_writes(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<crate::txn::participant::TxDerivedWrite>> {
if !H::PROJECTS {
return Ok(Vec::new());
}
let Some(targets) = self.hook.projection_targets() else {
return Ok(Vec::new());
};
let key_typed = K::from_bytes(key);
let (max_rows, max_bytes) = self.effective_projection_limits();
let plan = crate::projection::diff_projection_bytes(
&self.hook, targets, max_rows, max_bytes, &key_typed, old, new,
)?;
plan.into_tx_derived(targets)
}
pub(crate) fn tx_counter_deltas_for(
&self,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<crate::txn::participant::TxCounterDelta>> {
if !H::COUNTS {
return Ok(Vec::new());
}
let Some(targets) = self.hook.counter_targets() else {
return Ok(Vec::new());
};
let key_typed = K::from_bytes(key);
let (max_rows, max_bytes) = self.effective_projection_limits();
let deltas = crate::counter::diff_counters_bytes(
&self.hook, targets, max_rows, max_bytes, &key_typed, old, new,
)?;
crate::counter::into_tx_counter_deltas(targets, deltas)
}
pub(crate) fn tx_claims_for(
&self,
name: &'static str,
key: &[u8],
old: Option<&[u8]>,
new: Option<&[u8]>,
) -> DbResult<Vec<crate::txn::participant::ClaimOp>> {
if !H::CLAIMS {
return Ok(Vec::new());
}
let Some(targets) = self.hook.claim_targets() else {
return Ok(Vec::new());
};
let key_typed = K::from_bytes(key);
let (max_rows, max_bytes) = self.effective_projection_limits();
let delta = crate::unique::claim_delta_bytes(
&self.hook, targets, max_rows, max_bytes, &key_typed, old, new,
)?;
crate::unique::into_tx_claim_ops(targets, name, key, delta)
}
pub(crate) fn tx_shard(&self, sid: usize) -> VarShard<'_, K, H> {
let locked = self.lock_shard(sid);
let guard = self.index.collector().enter();
VarShard {
tree: self,
held: Held {
locked,
sid,
events: Vec::new(),
watch_wakes: WatchWakeBatch::default(),
publish_watches: true,
},
guard,
}
}
pub(crate) fn tx_release_shard(
shard: VarShard<'_, K, H>,
) -> (
Vec<<Self as Family>::Owned>,
WatchWakeBatch,
seize::LocalGuard<'_>,
) {
let VarShard { held, guard, .. } = shard;
let Held {
locked,
events,
watch_wakes,
..
} = held;
locked.release();
(events, watch_wakes, guard)
}
pub(crate) fn tx_reserve_gsns(&self, count: usize) -> u64 {
self.engine
.gsn()
.fetch_add(count as u64, std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn tx_encrypted(&self) -> bool {
#[cfg(feature = "encryption")]
{
self.engine.config().encryption_key.is_some()
}
#[cfg(not(feature = "encryption"))]
{
false
}
}
pub(crate) fn tx_durable_recovered_gsn(&self, sid: usize) -> u64 {
self.engine.shards()[sid].durable_recovered_gsn()
}
pub fn get_many(&self, keys: &[K]) -> Vec<Option<ByteView>> {
let n = keys.len();
let mut out: Vec<Option<ByteView>> = (0..n).map(|_| None).collect();
if n == 0 {
return out;
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
let ord = keys[a].as_bytes().cmp(keys[b].as_bytes());
if self.reversed { ord.reverse() } else { ord }
});
let guard = self.index.collector().enter();
let mut finger = self.index.finger();
for &i in &order {
let kb = keys[i].as_bytes();
if let Some(node) = self.index.finger_seek(&mut finger, kb, &guard)
&& node.key_bytes() == kb
{
out[i] = self.read_value_cached(node, &guard);
}
}
out
}
pub(crate) fn get_many_decoded<T>(
&self,
keys: &[K],
cache: &DecodedCache<T>,
decode: impl Fn(&ByteView) -> DbResult<T>,
) -> Vec<Option<Arc<T>>> {
let n = keys.len();
let mut out: Vec<Option<Arc<T>>> = (0..n).map(|_| None).collect();
if n == 0 {
return out;
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
let ord = keys[a].as_bytes().cmp(keys[b].as_bytes());
if self.reversed { ord.reverse() } else { ord }
});
let guard = self.index.collector().enter();
let mut finger = self.index.finger();
for &i in &order {
let kb = keys[i].as_bytes();
if let Some(node) = self.index.finger_seek(&mut finger, kb, &guard)
&& node.key_bytes() == kb
{
out[i] = self.read_decoded(node, &guard, cache, &decode).ok();
}
}
out
}
pub fn update_many<P>(
&self,
items: Vec<(K, P)>,
f: impl Fn(&K, Option<&[u8]>, &P) -> Upsert<ByteView>,
) -> DbResult<Vec<(K, Applied<ByteView>)>> {
let n = items.len();
let mut by_shard: std::collections::BTreeMap<usize, Vec<usize>> =
std::collections::BTreeMap::new();
for (i, (k, _)) in items.iter().enumerate() {
by_shard.entry(self.shard_for(k)).or_default().push(i);
}
let mut out: Vec<Option<(K, Applied<ByteView>)>> = (0..n).map(|_| None).collect();
let mut events: Vec<(K, Option<ByteView>, Option<ByteView>)> = Vec::new();
let mut wakes = WatchWakeBatch::default();
let mut err: Option<DbError> = None;
'shards: for (shard_id, idxs) in by_shard {
let mut inner = self.engine.shards()[shard_id].lock();
let producer = self.watch_enter();
let active = producer.active();
let context_active = H::WATCHES.then(|| crate::watch::ContextActiveGuard::set(active));
let guard = self.index.collector().enter();
for &i in &idxs {
#[cfg(test)]
if UPDATE_MANY_FAIL_AFTER.with(|remaining| match remaining.get() {
0 => true,
usize::MAX => false,
value => {
remaining.set(value - 1);
false
}
}) {
err = Some(DbError::Internal("test: update_many injected failure"));
break;
}
let (k, p) = &items[i];
let cur: Option<ByteView> = match self.index.get(k.as_bytes(), &guard) {
Some(node) => {
let disk = node.read_loc();
match self.read_value_locked_result(&disk, shard_id as u8, &inner) {
Ok(v) => Some(v),
Err(e) => {
err = Some(e);
break;
}
}
}
None => None,
};
let decision = f(k, cur.as_deref(), p);
let guard_result = match &decision {
Upsert::Put(v) => self.claim_guard_bytes(k, cur.as_deref(), Some(v)),
Upsert::Delete => self.claim_guard_bytes(k, cur.as_deref(), None),
Upsert::Keep => Ok(()),
};
if let Err(e) = guard_result {
err = Some(e);
break;
}
let applied = match decision {
Upsert::Put(v) => {
let expires_at = self.deadline_for_bytes(k, &v);
match self.put_locked(shard_id, &mut inner, &guard, k, &v, expires_at) {
Ok(_existed) => {
if H::NEEDS_WRITE || active {
let event =
(*k, cur.clone(), Some(VarHookNew::Owned(v.clone())));
self.publish_watch(&event, active, &mut wakes);
if H::NEEDS_WRITE {
events.push((*k, cur.clone(), Some(v.clone())));
}
}
Applied::Written { old: cur, new: v }
}
Err(e) => {
err = Some(e);
break;
}
}
}
Upsert::Keep => Applied::Kept,
Upsert::Delete => {
match self.delete_locked(shard_id, &mut inner, &guard, k) {
Ok(true) => {
let old = cur.expect("existing key has an old value");
if H::NEEDS_WRITE || active {
let event = (*k, Some(old.clone()), None);
self.publish_watch(&event, active, &mut wakes);
if H::NEEDS_WRITE {
events.push((*k, Some(old.clone()), None));
}
}
Applied::Deleted(old)
}
Ok(false) => Applied::Kept, Err(e) => {
err = Some(e);
break;
}
}
}
};
out[i] = Some((*k, applied));
}
drop(inner);
drop(guard);
drop(context_active);
if err.is_some() {
break 'shards;
}
}
if H::NEEDS_WRITE {
for (k, old, new) in &events {
self.hook.on_write(k, old.as_deref(), new.as_deref());
}
}
<Self as Family>::wake_watches(wakes);
if let Some(e) = err {
return Err(e);
}
Ok(out
.into_iter()
.map(|slot| slot.expect("every index assigned on the success path"))
.collect())
}
}
#[cfg(feature = "replication")]
impl<K: Key, H: WriteHook<K>> crate::replication::ReplicationTarget for VarTree<K, H> {
fn apply_entry(
&self,
_shard_inner: &mut crate::shard::ShardInner,
_shard_id: u8,
file_id: u32,
entry_offset: u64,
header: &crate::entry::EntryHeader,
key: &[u8],
_value: &[u8],
) -> DbResult<crate::replication::ApplyOutcome> {
use crate::replication::ApplyOutcome;
let key: K = K::from_bytes(key);
let value_offset =
entry_offset + size_of::<crate::entry::EntryHeader>() as u64 + size_of::<K>() as u64;
let disk = DiskLoc::new(file_id, value_offset as u32, header.value_len);
if header.is_tombstone() {
let guard = self.index.collector().enter();
let removed = self.index.remove(key.as_bytes(), &guard);
match removed {
Some(node_ptr) => {
let old_disk = unsafe { &*node_ptr }.read_loc();
Ok(ApplyOutcome::TombstoneRemoved(old_disk))
}
None => Ok(ApplyOutcome::Inserted), }
} else {
let guard = self.index.collector().enter();
let height = random_height();
let node_ptr = VarNode::alloc(key, disk, height);
match self.index.insert(node_ptr, &guard) {
InsertResult::Inserted => Ok(ApplyOutcome::Inserted),
InsertResult::Exists(existing) => {
let old_disk = existing.read_loc();
existing.write_loc(disk);
unsafe {
VarNode::<K>::dealloc_node(node_ptr);
}
Ok(ApplyOutcome::Replaced(old_disk))
}
}
}
}
fn try_apply_entry(
&self,
shard_inner: &mut crate::shard::ShardInner,
shard_id: u8,
file_id: u32,
entry_offset: u64,
header: &crate::entry::EntryHeader,
raw_after_header: &[u8],
) -> DbResult<crate::replication::ApplyOutcome> {
use crate::replication::ApplyOutcome;
if raw_after_header.len() < size_of::<K>() + header.value_len as usize {
return Ok(ApplyOutcome::NotMatched);
}
let key = &raw_after_header[..size_of::<K>()];
let value = &raw_after_header[size_of::<K>()..size_of::<K>() + header.value_len as usize];
let crc = crate::entry::compute_crc32(header.gsn, header.value_len, key, value);
if crc != header.crc32 {
return Ok(ApplyOutcome::NotMatched);
}
self.apply_entry(
shard_inner,
shard_id,
file_id,
entry_offset,
header,
key,
value,
)
}
fn key_len(&self) -> usize {
size_of::<K>()
}
fn mark_replication_follower(&self) {
VarTree::mark_replication_follower(self);
}
}
#[cfg(feature = "replication")]
impl<K: Key, H: WriteHook<K>> VarTree<K, H> {
pub fn start_replication_server(
&self,
bind_addr: std::net::SocketAddr,
signal: crate::shutdown::ShutdownSignal,
) -> crate::error::DbResult<crate::replication::ReplicationServer> {
let consumers = self.install_replication_producers()?;
crate::replication::ReplicationServer::start(
bind_addr,
self.engine.shards().clone(),
consumers,
self.engine.config().max_file_size,
signal,
)
}
pub fn start_replication_server_with_options(
&self,
bind_addr: std::net::SocketAddr,
signal: crate::shutdown::ShutdownSignal,
options: crate::replication::ReplicationServerOptions,
) -> crate::error::DbResult<crate::replication::ReplicationServer> {
let consumers = self.install_replication_producers()?;
crate::replication::ReplicationServer::start_with_options(
bind_addr,
self.engine.shards().clone(),
consumers,
self.engine.config().max_file_size,
signal,
options,
)
}
fn install_replication_producers(
&self,
) -> crate::error::DbResult<Vec<rtrb::Consumer<crate::replication::ReplicationEntry>>> {
const SPSC_CAPACITY: usize = 4096;
let shards = self.engine.shards();
let mut consumers = Vec::with_capacity(shards.len());
for shard in shards.iter() {
let (p, c) = rtrb::RingBuffer::new(SPSC_CAPACITY);
shard.set_replication_producer(p);
consumers.push(c);
}
Ok(consumers)
}
pub fn start_replication_client(
&self,
leader_addr: std::net::SocketAddr,
registry: std::sync::Arc<crate::replication::ReplicationRegistry>,
signal: crate::shutdown::ShutdownSignal,
) -> crate::error::DbResult<crate::replication::ReplicationClient> {
crate::replication::ReplicationClient::start(
leader_addr,
self.engine.shards().clone(),
registry,
size_of::<K>() as u16,
signal,
)
}
pub fn start_replication_client_with_options(
&self,
leader_addr: std::net::SocketAddr,
registry: std::sync::Arc<crate::replication::ReplicationRegistry>,
signal: crate::shutdown::ShutdownSignal,
options: crate::replication::ReplicationClientOptions,
) -> crate::error::DbResult<crate::replication::ReplicationClient> {
crate::replication::ReplicationClient::start_with_options(
leader_addr,
self.engine.shards().clone(),
registry,
size_of::<K>() as u16,
signal,
options,
)
}
}
#[cfg(feature = "replication")]
impl<K, H> VarTree<K, H>
where
K: Key + Send + Sync + 'static,
H: WriteHook<K> + Send + Sync + 'static,
{
pub fn as_replication_target(
self: &std::sync::Arc<Self>,
) -> Box<dyn crate::replication::ReplicationTarget> {
Box::new(std::sync::Arc::clone(self))
}
}
pub struct VarShard<'a, K: Key, H: WriteHook<K> = NoHook> {
tree: &'a VarTree<K, H>,
pub(crate) held: Held<'a, VarTree<K, H>>,
guard: seize::LocalGuard<'a>,
}
impl<K: Key, H: WriteHook<K>> VarShard<'_, K, H> {
pub fn put(&mut self, key: &K, value: &[u8]) -> DbResult<bool> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
let guard = &self.guard;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
let old = if (H::NEEDS_OLD_VALUE
&& (H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active())))
|| H::CLAIMS
{
if let Some(node) = tree.index.get(key.as_bytes(), guard) {
let disk = node.read_loc();
Some(tree.read_value_locked_result(&disk, sid as u8, locked)?)
} else {
None
}
} else {
None
};
tree.claim_guard_bytes(key, old.as_deref(), Some(value))?;
let expires_at = tree.deadline_for_bytes(key, value);
let existed = tree.put_locked(sid, locked, guard, key, value, expires_at)?;
let ev = if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((*key, old, Some(VarHookNew::Borrowed(value))))
} else {
None
};
*ev_out = ev;
Ok(existed)
})
}
pub fn insert(&mut self, key: &K, value: &[u8]) -> DbResult<()> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
let guard = &self.guard;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
if (H::EMITS || (H::CLAIMS && tree.hook.claim_targets().is_some()))
&& tree.index.get(key.as_bytes(), guard).is_none()
{
tree.claim_guard_bytes(key, None, Some(value))?;
}
let expires_at = tree.deadline_for_bytes(key, value);
tree.insert_locked(sid, locked, guard, key, value, expires_at)?;
let ev = if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((*key, None, Some(VarHookNew::Borrowed(value))))
} else {
None
};
*ev_out = ev;
Ok(())
})
}
pub fn delete(&mut self, key: &K) -> DbResult<bool> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
let guard = &self.guard;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
let old = if (H::NEEDS_OLD_VALUE
&& (H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active())))
|| H::CLAIMS
{
if let Some(node) = tree.index.get(key.as_bytes(), guard) {
let disk = node.read_loc();
Some(tree.read_value_locked_result(&disk, sid as u8, locked)?)
} else {
None
}
} else {
None
};
tree.claim_guard_bytes(key, old.as_deref(), None)?;
let existed = tree.delete_locked(sid, locked, guard, key)?;
let ev =
if existed && (H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active())) {
Some((*key, old, None))
} else {
None
};
*ev_out = ev;
Ok(existed)
})
}
pub fn try_get(&self, key: &K) -> DbResult<Option<ByteView>> {
self.check_shard(key)?;
match self.tree.index.get(key.as_bytes(), &self.guard) {
Some(node) => {
let disk = node.read_loc();
let view = self.tree.read_value_locked_result(
&disk,
self.held.sid as u8,
&self.held.locked,
)?;
Ok(Some(view))
}
None => Ok(None),
}
}
pub fn get_or_err(&self, key: &K) -> DbResult<ByteView> {
self.check_shard(key)?;
let node = self
.tree
.index
.get(key.as_bytes(), &self.guard)
.ok_or(DbError::KeyNotFound)?;
let disk = node.read_loc();
self.tree
.read_value_locked_result(&disk, self.held.sid as u8, &self.held.locked)
}
pub fn try_contains(&self, key: &K) -> DbResult<bool> {
self.check_shard(key)?;
Ok(self.tree.index.get(key.as_bytes(), &self.guard).is_some())
}
pub fn update(
&mut self,
key: &K,
f: impl FnOnce(&[u8]) -> ByteView,
) -> DbResult<Option<ByteView>> {
self.update_inner(key, |b| Ok(Some(f(b))), false)
}
pub fn fetch_update(
&mut self,
key: &K,
f: impl FnOnce(&[u8]) -> ByteView,
) -> DbResult<Option<ByteView>> {
self.update_inner(key, |b| Ok(Some(f(b))), true)
}
pub(crate) fn update_inner(
&mut self,
key: &K,
f: impl FnOnce(&[u8]) -> DbResult<Option<ByteView>>,
return_old: bool,
) -> DbResult<Option<ByteView>> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
let guard = &self.guard;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
match tree.try_update_locked(sid, locked, guard, key, f)? {
UpdateOutcome::Missing => Ok(None),
UpdateOutcome::Unchanged(current) => Ok(Some(current)),
UpdateOutcome::Updated { old, new } => {
let (result, ev) =
if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
let event_old = if H::NEEDS_OLD_VALUE {
Some(old.clone())
} else {
None
};
let result = Some(if return_old { old } else { new.clone() });
(
result,
Some((*key, event_old, Some(VarHookNew::Owned(new)))),
)
} else {
(Some(if return_old { old } else { new }), None)
};
{
*ev_out = ev;
Ok(result)
}
}
}
})
}
fn check_shard(&self, key: &K) -> DbResult<()> {
if self.tree.shard_for(key) != self.held.sid {
return Err(DbError::ShardMismatch);
}
Ok(())
}
}
impl<K: Key, H: WriteHook<K>> VarShard<'_, K, H> {
pub(crate) fn tx_arm_gsn(&mut self, gsn: u64) {
self.held.locked.arm_gsn(gsn);
}
pub(crate) fn tx_clear_armed_gsn(&mut self) {
self.held.locked.clear_armed_gsn();
}
pub(crate) fn tx_fence(&mut self, gsn: u64) -> u64 {
self.held.locked.fence(gsn)
}
pub(crate) fn tx_buffered_events(&self) -> usize {
self.held.events.len()
}
}
fn bound_owned<K: Copy>(b: &Bound<&K>) -> Bound<K> {
match b {
Bound::Included(k) => Bound::Included(**k),
Bound::Excluded(k) => Bound::Excluded(**k),
Bound::Unbounded => Bound::Unbounded,
}
}
pub struct VarIter<'a, K: Key, H: WriteHook<K> = NoHook> {
tree: &'a VarTree<K, H>,
front: *mut VarNode<K>,
back: Option<*mut VarNode<K>>,
back_finger: BackFinger<VarNode<K>>,
last_front: Option<K>,
last_back: Option<K>,
end: Bound<K>,
start: Bound<K>,
reversed: bool,
done: bool,
_guard: seize::LocalGuard<'a>,
}
impl<K: Key, H: WriteHook<K>> Iterator for VarIter<'_, K, H> {
type Item = (K, ByteView);
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.done || self.front.is_null() {
return None;
}
let node = unsafe { &*self.front };
let converged = self.back.is_some_and(|back| std::ptr::eq(self.front, back));
self.front = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if converged {
self.done = true;
}
if node.is_marked() {
if converged {
return None;
}
continue;
}
if self.crossed_back(&node.key) {
self.done = true;
return None;
}
if !self.check_end(&node.key) {
self.done = true;
return None;
}
match self.tree.read_value_cached(node, &self._guard) {
Some(value) => {
self.last_front = Some(node.key);
return Some((node.key, value));
}
None => {
if converged {
return None;
}
continue;
}
}
}
}
}
impl<K: Key, H: WriteHook<K>> DoubleEndedIterator for VarIter<'_, K, H> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.back.is_none() {
self.back = Some(self.resolve_back());
if self.front.is_null() {
self.done = true;
}
}
loop {
let back = self.back.unwrap_or(std::ptr::null_mut());
if self.done || back.is_null() {
return None;
}
let node = unsafe { &*back };
let key = node.key;
let converged = std::ptr::eq(self.front, back);
self.back = Some(self.tree.index().back_step(
&mut self.back_finger,
key.as_bytes(),
&self._guard,
));
if converged {
self.done = true;
}
if node.is_marked() {
if converged {
return None;
}
continue;
}
if self.crossed_front(&key) {
self.done = true;
return None;
}
if !self.check_start(&key) {
self.done = true;
return None;
}
match self.tree.read_value_cached(node, &self._guard) {
Some(value) => {
self.last_back = Some(key);
return Some((key, value));
}
None => {
if converged {
return None;
}
continue;
}
}
}
}
}
impl<K: Key, H: WriteHook<K>> VarIter<'_, K, H> {
fn resolve_back(&mut self) -> *mut VarNode<K> {
let index = self.tree.index();
match &self.end {
Bound::Unbounded => index.back_seek_last(&mut self.back_finger, &self._guard),
Bound::Excluded(k) => {
index.back_step(&mut self.back_finger, k.as_bytes(), &self._guard)
}
Bound::Included(k) => {
let ge = index.find_first_ge(k.as_bytes(), &self._guard);
if !ge.is_null()
&& !unsafe { &*ge }.is_marked()
&& unsafe { &*ge }.key_bytes() == k.as_bytes()
{
ge
} else {
index.back_step(&mut self.back_finger, k.as_bytes(), &self._guard)
}
}
}
}
#[inline(always)]
fn crossed_back(&self, key: &K) -> bool {
self.last_back.as_ref().is_some_and(|last| {
if self.reversed {
key.as_bytes() <= last.as_bytes()
} else {
key.as_bytes() >= last.as_bytes()
}
})
}
#[inline(always)]
fn crossed_front(&self, key: &K) -> bool {
self.last_front.as_ref().is_some_and(|last| {
if self.reversed {
key.as_bytes() >= last.as_bytes()
} else {
key.as_bytes() <= last.as_bytes()
}
})
}
#[inline(always)]
fn check_end(&self, key: &K) -> bool {
match &self.end {
Bound::Unbounded => true,
Bound::Excluded(end) => {
if self.reversed {
key.as_bytes() > end.as_bytes()
} else {
key.as_bytes() < end.as_bytes()
}
}
Bound::Included(end) => {
if self.reversed {
key.as_bytes() >= end.as_bytes()
} else {
key.as_bytes() <= end.as_bytes()
}
}
}
}
#[inline(always)]
fn check_start(&self, key: &K) -> bool {
match &self.start {
Bound::Unbounded => true,
Bound::Excluded(s) => {
if self.reversed {
key.as_bytes() < s.as_bytes()
} else {
key.as_bytes() > s.as_bytes()
}
}
Bound::Included(s) => {
if self.reversed {
key.as_bytes() <= s.as_bytes()
} else {
key.as_bytes() >= s.as_bytes()
}
}
}
}
pub fn collect_keys(&mut self) -> Vec<K> {
self.map(|(k, _)| k).collect()
}
pub fn collect_entries(&mut self) -> Vec<(K, ByteView)> {
self.collect()
}
pub(crate) fn try_next_raw(&mut self) -> DbResult<Option<(K, ByteView)>> {
loop {
if self.done || self.front.is_null() {
return Ok(None);
}
let node = unsafe { &*self.front };
let converged = self.back.is_some_and(|back| std::ptr::eq(self.front, back));
self.front = crate::skiplist::strip_mark(
node.tower(0).load(std::sync::atomic::Ordering::Acquire),
);
if converged {
self.done = true;
}
if node.is_marked() {
if converged {
return Ok(None);
}
continue;
}
if !self.check_end(&node.key) {
self.done = true;
return Ok(None);
}
return self
.tree
.read_value_cached_result(node, &self._guard)
.map(|value| Some((node.key, value)));
}
}
pub(crate) fn try_next_raw_back(&mut self) -> DbResult<Option<(K, ByteView)>> {
if self.back.is_none() {
self.back = Some(self.resolve_back());
if self.front.is_null() {
self.done = true;
}
}
loop {
let back = self.back.unwrap_or(std::ptr::null_mut());
if self.done || back.is_null() {
return Ok(None);
}
let node = unsafe { &*back };
let key = node.key;
let converged = std::ptr::eq(self.front, back);
self.back = Some(self.tree.index().back_step(
&mut self.back_finger,
key.as_bytes(),
&self._guard,
));
if converged {
self.done = true;
}
if node.is_marked() {
if converged {
return Ok(None);
}
continue;
}
if !self.check_start(&key) {
self.done = true;
return Ok(None);
}
return self
.tree
.read_value_cached_result(node, &self._guard)
.map(|value| Some((key, value)));
}
}
}
#[doc(hidden)]
pub struct TryVarIter<'a, K: Key, H: WriteHook<K> = NoHook> {
inner: VarIter<'a, K, H>,
failed: bool,
}
impl<K: Key, H: WriteHook<K>> Iterator for TryVarIter<'_, K, H> {
type Item = DbResult<(K, ByteView)>;
fn next(&mut self) -> Option<Self::Item> {
if self.failed {
return None;
}
match self.inner.try_next_raw() {
Ok(Some(row)) => Some(Ok(row)),
Ok(None) => None,
Err(error) => {
self.failed = true;
Some(Err(error))
}
}
}
}
impl<K: Key, H: WriteHook<K>> DoubleEndedIterator for TryVarIter<'_, K, H> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.failed {
return None;
}
match self.inner.try_next_raw_back() {
Ok(Some(row)) => Some(Ok(row)),
Ok(None) => None,
Err(error) => {
self.failed = true;
Some(Err(error))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use crate::compaction::compact_shard;
use tempfile::tempdir;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
#[derive(Default)]
struct CountingHook<const NEEDS_INIT: bool, const NEEDS_OLD: bool> {
writes: AtomicUsize,
writes_with_old: AtomicUsize,
inits: AtomicUsize,
last_write_new: crate::sync::Mutex<Option<Vec<u8>>>,
last_init_value: crate::sync::Mutex<Option<Vec<u8>>>,
}
impl<const NEEDS_INIT: bool, const NEEDS_OLD: bool> WriteHook<[u8; 8]>
for CountingHook<NEEDS_INIT, NEEDS_OLD>
{
const NEEDS_OLD_VALUE: bool = NEEDS_OLD;
const NEEDS_INIT: bool = NEEDS_INIT;
fn on_write(&self, _key: &[u8; 8], old: Option<&[u8]>, new: Option<&[u8]>) {
self.writes.fetch_add(1, AtomicOrdering::Relaxed);
if old.is_some() {
self.writes_with_old.fetch_add(1, AtomicOrdering::Relaxed);
}
*crate::sync::lock(&self.last_write_new) = new.map(<[u8]>::to_vec);
}
fn on_init(&self, _key: &[u8; 8], value: &[u8]) {
self.inits.fetch_add(1, AtomicOrdering::Relaxed);
*crate::sync::lock(&self.last_init_value) = Some(value.to_vec());
}
}
fn open_test_tree(dir: &std::path::Path) -> VarTree<[u8; 8]> {
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 8192;
cfg.write_buffer_size = 8192;
cfg.compaction_threshold = 0.0;
VarTree::open(dir, cfg).expect("open test tree")
}
fn open_test_tree_hooked<const NEEDS_INIT: bool, const NEEDS_OLD: bool>(
dir: &std::path::Path,
hook: CountingHook<NEEDS_INIT, NEEDS_OLD>,
) -> VarTree<[u8; 8], CountingHook<NEEDS_INIT, NEEDS_OLD>> {
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 8192;
cfg.write_buffer_size = 8192;
cfg.compaction_threshold = 0.0;
VarTree::open_hooked(dir, cfg, hook).expect("open hooked test tree")
}
#[test]
fn strict_range_bounds_dir_surfaces_stale_and_terminates() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 1u64.to_be_bytes();
tree.put(&key, b"payload").unwrap();
let shard = &tree.engine.shards()[tree.shard_for(&key)];
let mut inner = shard.lock();
inner.immutable = Vec::new();
inner.active.file_id = u32::MAX;
drop(inner);
let mut rows = tree.try_range_bounds_dir(Bound::Unbounded, Bound::Unbounded, Dir::Asc);
assert!(matches!(rows.next(), Some(Err(DbError::StaleDiskLoc))));
assert!(
rows.next().is_none(),
"strict iterator must terminate after an error"
);
}
#[test]
fn strict_range_bounds_back_matches_ordinary_valid_data_sequence() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.reversed = false;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).expect("open test tree");
for value in [10u64, 20, 30, 40] {
tree.put(&value.to_be_bytes(), &value.to_be_bytes())
.expect("seed row");
}
let start = 10u64.to_be_bytes();
let end = 40u64.to_be_bytes();
let mut ordinary = tree.range_bounds(Bound::Included(&start), Bound::Excluded(&end));
let ordinary_keys: Vec<_> =
std::iter::from_fn(|| ordinary.next_back().map(|(key, _)| key)).collect();
let mut strict = tree.try_range_bounds(Bound::Included(&start), Bound::Excluded(&end));
let mut strict_keys = Vec::new();
while let Some((key, _)) = strict.inner.try_next_raw_back().expect("strict row") {
strict_keys.push(key);
}
assert_eq!(
ordinary_keys,
vec![
30u64.to_be_bytes(),
20u64.to_be_bytes(),
10u64.to_be_bytes()
]
);
assert_eq!(strict_keys, ordinary_keys);
}
#[test]
fn var_compaction_reclaims_dead_bytes_under_overwrite_churn() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 64 * 1024;
cfg.write_buffer_size = 16 * 1024;
cfg.compaction_threshold = 0.30;
let max_file_size = cfg.max_file_size;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).unwrap();
const N: u64 = 500;
const M: u64 = 40;
let entry_sz: u64 = 16 + 8 + 64;
let live_set = N * entry_sz;
let total_written = N * (M + 1) * entry_sz;
for k in 0..N {
tree.put(&k.to_be_bytes(), &[0u8; 64]).unwrap();
}
for round in 1..=M {
let mut v = [0u8; 64];
v[0] = round as u8;
for k in 0..N {
tree.put(&k.to_be_bytes(), &v).unwrap();
}
}
tree.flush_buffers().unwrap();
let shard = &tree.engine.shards()[0];
let disk = || {
let inner = shard.lock();
inner.active.write_offset + inner.immutable.iter().map(|f| f.total_bytes).sum::<u64>()
};
assert!(disk() > total_written / 2, "expected dead-byte buildup");
for _ in 0..200 {
let before = disk();
tree.compact().unwrap();
if disk() == before {
break;
}
}
assert!(
disk() <= live_set + max_file_size,
"VarTree compaction failed to reclaim: disk={} live_set={live_set}",
disk()
);
let mut v = [0u8; 64];
v[0] = M as u8;
assert_eq!(tree.get(&0u64.to_be_bytes()).as_deref(), Some(&v[..]));
assert_eq!(tree.get(&(N - 1).to_be_bytes()).as_deref(), Some(&v[..]));
assert_eq!(tree.len(), N as usize);
}
fn put_until_compactable(tree: &VarTree<[u8; 8]>, key: [u8; 8]) -> DiskLoc {
tree.put(&key, &[0u8; 256]).expect("first put");
let snap = {
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
node.read_loc()
};
for i in 1..65u8 {
tree.put(&key, &[i; 256]).expect("put");
}
tree.put(&key, b"final-value-payload-XX")
.expect("final put");
snap
}
use crate::config::CacheConfig;
use crate::decoded_cache::DecodedCache;
fn decoded_cache_on() -> DecodedCache<Vec<u8>> {
DecodedCache::new(&CacheConfig {
max_size: 1 << 20,
estimated_items: 64,
})
}
fn counting_decode(counter: &AtomicUsize) -> impl Fn(&ByteView) -> DbResult<Vec<u8>> + '_ {
move |b| {
counter.fetch_add(1, AtomicOrdering::Relaxed);
Ok(b.as_bytes().to_vec())
}
}
#[test]
fn read_decoded_hit_does_not_decode() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 3u64.to_be_bytes();
tree.put(&key, b"payload").unwrap();
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let first = tree
.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("first read");
let second = tree
.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("second read");
assert_eq!(first.as_slice(), b"payload");
assert_eq!(second.as_slice(), b"payload");
assert_eq!(
decodes.load(AtomicOrdering::Relaxed),
1,
"the second read must be served from the decoded cache"
);
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn read_decoded_disabled_cache_decodes_every_time() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 4u64.to_be_bytes();
tree.put(&key, b"payload").unwrap();
let cache: DecodedCache<Vec<u8>> = DecodedCache::new(&CacheConfig {
max_size: 0,
estimated_items: 0,
});
let decodes = AtomicUsize::new(0);
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
for _ in 0..3 {
let v = tree
.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("read");
assert_eq!(v.as_slice(), b"payload");
}
assert_eq!(decodes.load(AtomicOrdering::Relaxed), 3);
}
#[test]
fn read_decoded_sees_a_write_and_decodes_again() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 5u64.to_be_bytes();
tree.put(&key, b"v1").unwrap();
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let held = {
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
tree.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("first read")
};
assert_eq!(held.as_slice(), b"v1");
tree.put(&key, b"v2").unwrap();
let after = {
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
tree.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("second read")
};
assert_eq!(after.as_slice(), b"v2", "a write must not be served stale");
assert_eq!(decodes.load(AtomicOrdering::Relaxed), 2);
assert_eq!(held.as_slice(), b"v1");
}
#[test]
fn read_decoded_correct_after_compaction() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 11u64.to_be_bytes();
let _snap = put_until_compactable(&tree, key);
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("seal active file");
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
{
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
tree.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("pre-compaction read");
}
let shard = &tree.engine.shards()[0];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let v = tree
.read_decoded(node, &guard, &cache, counting_decode(&decodes))
.expect("post-compaction read");
assert_eq!(v.as_slice(), b"final-value-payload-XX");
assert_eq!(
decodes.load(AtomicOrdering::Relaxed),
2,
"compaction moves the value, so the post-compaction read is a cold miss"
);
}
#[test]
fn read_decoded_covers_both_byte_routes() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
let tree = VarTree::open(dir.path(), cfg).expect("open test tree");
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let small_key = 21u64.to_be_bytes();
tree.put(&small_key, &[7u8; 64]).unwrap();
let big_key = 22u64.to_be_bytes();
tree.put(&big_key, &vec![9u8; 12 * 1024]).unwrap();
let guard = tree.index.collector().enter();
let small_node = tree
.index
.get(small_key.as_bytes(), &guard)
.expect("indexed");
assert!(
!small_node.read_loc().is_value_cache_routed(),
"test setup: the small value must take the block-cache route"
);
let small = tree
.read_decoded(small_node, &guard, &cache, counting_decode(&decodes))
.expect("small read");
assert_eq!(small.as_slice(), &vec![7u8; 64][..]);
let big_node = tree.index.get(big_key.as_bytes(), &guard).expect("indexed");
assert!(
big_node.read_loc().is_value_cache_routed(),
"test setup: the large value must take the value-cache route"
);
let big = tree
.read_decoded(big_node, &guard, &cache, counting_decode(&decodes))
.expect("big read");
assert_eq!(big.as_slice(), &vec![9u8; 12 * 1024][..]);
let before = decodes.load(AtomicOrdering::Relaxed);
tree.read_decoded(small_node, &guard, &cache, counting_decode(&decodes))
.expect("small re-read");
tree.read_decoded(big_node, &guard, &cache, counting_decode(&decodes))
.expect("big re-read");
assert_eq!(decodes.load(AtomicOrdering::Relaxed), before);
}
#[test]
fn try_get_decoded_absent_key_is_none() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let missing = tree
.try_get_decoded(&99u64.to_be_bytes(), &cache, counting_decode(&decodes))
.expect("absent key is not an error");
assert!(missing.is_none());
assert_eq!(decodes.load(AtomicOrdering::Relaxed), 0);
}
#[test]
fn try_get_decoded_absent_after_delete_redecodes_on_reinsert() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 41u64.to_be_bytes();
tree.put(&key, b"v1").unwrap();
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let v1 = tree
.try_get_decoded(&key, &cache, counting_decode(&decodes))
.unwrap()
.unwrap();
assert_eq!(v1.as_slice(), b"v1");
assert!(tree.delete(&key).unwrap());
assert!(
tree.try_get_decoded(&key, &cache, counting_decode(&decodes))
.unwrap()
.is_none()
);
tree.put(&key, b"v2").unwrap();
let before = decodes.load(AtomicOrdering::Relaxed);
let v2 = tree
.try_get_decoded(&key, &cache, counting_decode(&decodes))
.unwrap()
.unwrap();
assert_eq!(v2.as_slice(), b"v2");
assert_eq!(
decodes.load(AtomicOrdering::Relaxed) - before,
1,
"re-insert must decode again"
);
}
#[test]
fn read_decoded_returns_stale_after_retry_limit() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 51u64.to_be_bytes();
tree.put(&key, b"payload").expect("put");
let guard = tree.index.collector().enter();
assert!(tree.index.get(key.as_bytes(), &guard).is_some());
drop(guard);
{
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let mut inner = shard.lock();
inner.immutable = Vec::new();
inner.active.file_id = u32::MAX;
}
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let guard = tree.index.collector().enter();
let node = tree
.index
.get(key.as_bytes(), &guard)
.expect("still indexed");
match tree.read_decoded(node, &guard, &cache, counting_decode(&decodes)) {
Err(DbError::StaleDiskLoc) => {}
Ok(v) => panic!("expected StaleDiskLoc, got Ok({:?})", v.as_slice()),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
}
#[test]
fn try_get_decoded_propagates_decode_errors_and_caches_nothing() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 31u64.to_be_bytes();
tree.put(&key, b"whatever").unwrap();
let cache = decoded_cache_on();
let attempts = AtomicUsize::new(0);
let failing = |_b: &ByteView| -> DbResult<Vec<u8>> {
attempts.fetch_add(1, AtomicOrdering::Relaxed);
Err(DbError::CorruptedEntry { offset: 0 })
};
for _ in 0..2 {
assert!(matches!(
tree.try_get_decoded(&key, &cache, failing),
Err(DbError::CorruptedEntry { .. })
));
}
assert_eq!(
attempts.load(AtomicOrdering::Relaxed),
2,
"a failed decode must not be cached"
);
}
#[test]
fn get_many_decoded_is_aligned_and_lossy() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
for i in [1u64, 2, 3] {
tree.put(&i.to_be_bytes(), format!("v{i}").as_bytes())
.unwrap();
}
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let keys = [
3u64.to_be_bytes(),
99u64.to_be_bytes(),
1u64.to_be_bytes(),
2u64.to_be_bytes(),
];
let out = tree.get_many_decoded(&keys, &cache, counting_decode(&decodes));
assert_eq!(out.len(), 4);
assert_eq!(out[0].as_deref().map(Vec::as_slice), Some(&b"v3"[..]));
assert!(out[1].is_none(), "absent key must be None at its own index");
assert_eq!(out[2].as_deref().map(Vec::as_slice), Some(&b"v1"[..]));
assert_eq!(out[3].as_deref().map(Vec::as_slice), Some(&b"v2"[..]));
assert_eq!(decodes.load(AtomicOrdering::Relaxed), 3);
}
#[test]
fn get_many_decoded_second_pass_hits_the_cache() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
for i in 0..8u64 {
tree.put(&i.to_be_bytes(), format!("v{i}").as_bytes())
.unwrap();
}
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let keys: Vec<[u8; 8]> = (0..8u64).map(u64::to_be_bytes).collect();
let first = tree.get_many_decoded(&keys, &cache, counting_decode(&decodes));
assert_eq!(first.iter().filter(|v| v.is_some()).count(), 8);
assert_eq!(decodes.load(AtomicOrdering::Relaxed), 8);
let second = tree.get_many_decoded(&keys, &cache, counting_decode(&decodes));
assert_eq!(second.iter().filter(|v| v.is_some()).count(), 8);
assert_eq!(
decodes.load(AtomicOrdering::Relaxed),
8,
"a fully cached batch must not decode again"
);
}
#[test]
fn get_many_decoded_empty_input() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let cache = decoded_cache_on();
let decodes = AtomicUsize::new(0);
let out = tree.get_many_decoded(&[], &cache, counting_decode(&decodes));
assert!(out.is_empty());
}
#[test]
fn get_many_decoded_decode_error_is_lossy_and_does_not_cache_failure() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let good_key = 1u64.to_be_bytes();
let bad_key = 2u64.to_be_bytes();
tree.put(&good_key, b"ok").unwrap();
tree.put(&bad_key, b"bad-bytes").unwrap();
let cache = decoded_cache_on();
let attempts = AtomicUsize::new(0);
let decode = |b: &ByteView| -> DbResult<Vec<u8>> {
if b.as_bytes() == b"bad-bytes" {
attempts.fetch_add(1, AtomicOrdering::Relaxed);
return Err(DbError::CorruptedEntry { offset: 0 });
}
Ok(b.as_bytes().to_vec())
};
let keys = [bad_key, good_key, 99u64.to_be_bytes()];
let out = tree.get_many_decoded(&keys, &cache, decode);
assert_eq!(out.len(), 3);
assert!(out[0].is_none(), "decode error must map to None");
assert_eq!(out[1].as_deref().map(Vec::as_slice), Some(&b"ok"[..]));
assert!(out[2].is_none(), "absent key must be None");
assert_eq!(attempts.load(AtomicOrdering::Relaxed), 1);
let before = attempts.load(AtomicOrdering::Relaxed);
let again = tree.get_many_decoded(&keys, &cache, decode);
assert_eq!(again[1].as_deref().map(Vec::as_slice), Some(&b"ok"[..]));
assert!(again[0].is_none());
assert_eq!(
attempts.load(AtomicOrdering::Relaxed) - before,
1,
"a failed decode must not be cached; the bad key retries on the next batch"
);
}
#[test]
fn put_reports_whether_key_existed() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 42u64.to_be_bytes();
assert!(!tree.put(&key, b"v1").expect("put"));
assert!(tree.put(&key, b"v2").expect("put"));
assert!(tree.put(&key, b"v3").expect("put"));
tree.flush_buffers().expect("flush");
assert!(tree.put(&key, b"v4").expect("put"));
assert!(!tree.put(&7u64.to_be_bytes(), b"x").expect("put"));
}
#[test]
fn warmup_threads_correct_shard_across_shards() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
for i in 0..256u64 {
tree.put(&i.to_be_bytes(), &[i as u8; 64]).expect("put");
}
tree.flush_buffers().expect("flush");
tree.warmup().expect("warmup");
for i in 0..256u64 {
assert_eq!(
tree.get(&i.to_be_bytes()).as_deref(),
Some(&[i as u8; 64][..])
);
}
}
#[test]
fn read_value_cached_inner_returns_stale_after_compaction() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 7u64.to_be_bytes();
let snap = put_until_compactable(&tree, key);
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
match tree.read_value_cached_inner(&snap, shard_id) {
Err(DbError::StaleDiskLoc) => {}
Ok(v) => panic!("expected StaleDiskLoc, got Ok({:?})", v.as_bytes()),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
}
#[test]
fn read_value_cached_returns_some_after_compaction() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 11u64.to_be_bytes();
let _snap = put_until_compactable(&tree, key);
let shard = &tree.engine.shards()[0];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let v = tree
.read_value_cached(node, &guard)
.expect("post-compaction value must be readable");
assert_eq!(v.as_bytes(), b"final-value-payload-XX");
}
#[test]
fn get_during_compaction_returns_some() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 13u64.to_be_bytes();
let _snap = put_until_compactable(&tree, key);
let shard = &tree.engine.shards()[0];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
let v = tree.get(&key).expect("post-compaction get");
assert_eq!(v.as_bytes(), b"final-value-payload-XX");
}
#[test]
fn iter_during_compaction_yields_all_live_keys() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
for k in 1u64..=3 {
put_until_compactable(&tree, k.to_be_bytes());
}
let shard = &tree.engine.shards()[0];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
let collected: std::collections::BTreeMap<[u8; 8], Vec<u8>> = tree
.iter()
.map(|(k, v)| (k, v.as_bytes().to_vec()))
.collect();
assert_eq!(collected.len(), 3);
for k in 1u64..=3 {
let bytes = collected
.get(&k.to_be_bytes())
.expect("every original key must remain");
assert_eq!(bytes.as_slice(), b"final-value-payload-XX");
}
}
#[test]
fn get_or_read_block_returns_stale_for_unknown_file_id() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
match tree.get_or_read_block(0, 9999, 0) {
Err(DbError::StaleDiskLoc) => {}
Ok(_) => panic!("expected StaleDiskLoc, got Ok"),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
}
#[test]
fn extract_from_block_propagates_next_block_error() {
let block = AlignedBuf::zeroed(4096);
let start = 4090;
let len = 32;
let result: DbResult<ByteView> =
VarTree::<[u8; 8]>::extract_from_block(&block, start, len, || {
Err(DbError::StaleDiskLoc)
});
match result {
Err(DbError::StaleDiskLoc) => {}
Ok(_) => panic!("expected StaleDiskLoc, got Ok"),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
}
#[test]
fn extract_from_block_multi_block_first_cached_second_stale() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 21u64.to_be_bytes();
let value = vec![0xCDu8; 4073];
tree.put(&key, &value).expect("initial put");
let snap = {
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
node.read_loc()
};
for i in 0..65u8 {
tree.put(&key, &[i; 256]).expect("overwrite");
}
tree.put(&key, b"final").expect("final");
let shard = &tree.engine.shards()[0];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
let block_offset = snap.offset as u64 & !4095;
let shard_id = tree.shard_for(&key) as u8;
let first_block = AlignedBuf::zeroed(4096);
let cache_key = BlockKey {
shard_id,
file_id: snap.file_id,
block_offset,
};
tree.block_cache.insert(cache_key, Arc::new(first_block));
match tree.read_value_cached_inner(&snap, shard_id) {
Err(DbError::StaleDiskLoc) => {}
Ok(_) => panic!("expected StaleDiskLoc from second block, got Ok"),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
}
#[test]
fn retry_limit_returns_none_on_persistent_stale() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 99u64.to_be_bytes();
tree.put(&key, b"payload").expect("put");
let guard = tree.index.collector().enter();
assert!(tree.index.get(key.as_bytes(), &guard).is_some());
drop(guard);
{
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let mut inner = shard.lock();
inner.immutable = Vec::new();
inner.active.file_id = u32::MAX;
}
let guard = tree.index.collector().enter();
let node = tree
.index
.get(key.as_bytes(), &guard)
.expect("still indexed");
assert!(
tree.read_value_cached(node, &guard).is_none(),
"MAX_STALE_RETRIES must terminate the loop and return None"
);
}
#[test]
fn var_tree_replay_init_fires_on_init_per_live_key_raw() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, false>(dir.path(), CountingHook::default());
for i in 0u64..5 {
tree.put(&i.to_be_bytes(), &[i as u8; 16]).expect("put");
}
tree.delete(&3u64.to_be_bytes()).expect("delete");
tree.hook.writes.store(0, AtomicOrdering::Relaxed);
tree.hook.inits.store(0, AtomicOrdering::Relaxed);
tree.replay_init();
assert_eq!(
tree.hook.inits.load(AtomicOrdering::Relaxed),
4,
"4 live keys"
);
assert_eq!(
tree.hook.writes.load(AtomicOrdering::Relaxed),
0,
"no on_write"
);
}
#[test]
fn var_tree_replay_init_no_hook_is_noop() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
for i in 0u64..3 {
tree.put(&i.to_be_bytes(), &[i as u8; 8]).expect("put");
}
tree.replay_init();
assert!(tree.get(&0u64.to_be_bytes()).is_some());
}
#[test]
fn var_tree_migrate_keep_fires_on_init_not_on_write_raw() {
use crate::MigrateAction;
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, false>(dir.path(), CountingHook::default());
for i in 0u64..4 {
tree.put(&i.to_be_bytes(), &[i as u8; 16]).expect("put");
}
tree.hook.writes.store(0, AtomicOrdering::Relaxed);
tree.hook.inits.store(0, AtomicOrdering::Relaxed);
let mutated = tree.migrate(|_, _| MigrateAction::Keep).expect("migrate");
assert_eq!(mutated, 0);
assert_eq!(
tree.hook.inits.load(AtomicOrdering::Relaxed),
4,
"4 keeps -> 4 on_init"
);
assert_eq!(
tree.hook.writes.load(AtomicOrdering::Relaxed),
0,
"Keep must not fire on_write"
);
}
#[test]
fn var_tree_migrate_update_fires_on_init_with_new_value_raw() {
use crate::MigrateAction;
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, false>(dir.path(), CountingHook::default());
let key = 42u64.to_be_bytes();
tree.put(&key, b"old-value").expect("put");
tree.hook.writes.store(0, AtomicOrdering::Relaxed);
tree.hook.inits.store(0, AtomicOrdering::Relaxed);
let new = ByteView::new(b"new-value");
let mutated = tree
.migrate(move |_, _| MigrateAction::Update(new.clone()))
.expect("migrate");
assert_eq!(mutated, 1);
assert_eq!(tree.hook.inits.load(AtomicOrdering::Relaxed), 1);
assert_eq!(
crate::sync::lock(&tree.hook.last_init_value).as_deref(),
Some(b"new-value".as_ref()),
"on_init must receive the NEW value"
);
assert_eq!(
tree.hook.writes.load(AtomicOrdering::Relaxed),
0,
"Update must NOT fire on_write (was double-firing through self.put)"
);
assert_eq!(tree.get(&key).unwrap().as_bytes(), b"new-value");
}
#[test]
fn var_tree_migrate_delete_fires_no_hooks_raw() {
use crate::MigrateAction;
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, false>(dir.path(), CountingHook::default());
let key = 7u64.to_be_bytes();
tree.put(&key, b"x").expect("put");
tree.hook.writes.store(0, AtomicOrdering::Relaxed);
tree.hook.inits.store(0, AtomicOrdering::Relaxed);
let mutated = tree.migrate(|_, _| MigrateAction::Delete).expect("migrate");
assert_eq!(mutated, 1);
assert_eq!(tree.hook.inits.load(AtomicOrdering::Relaxed), 0);
assert_eq!(tree.hook.writes.load(AtomicOrdering::Relaxed), 0);
assert!(tree.get(&key).is_none());
}
#[test]
fn var_tree_migrate_no_init_hook_is_silent_for_keep_and_update() {
use crate::MigrateAction;
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<false, false>(dir.path(), CountingHook::default());
for i in 0u64..3 {
tree.put(&i.to_be_bytes(), &[i as u8; 16]).expect("put");
}
tree.hook.writes.store(0, AtomicOrdering::Relaxed);
tree.hook.inits.store(0, AtomicOrdering::Relaxed);
tree.migrate(|_, _| MigrateAction::Keep)
.expect("migrate keep");
assert_eq!(
tree.hook.inits.load(AtomicOrdering::Relaxed),
0,
"Keep with NEEDS_INIT=false"
);
assert_eq!(tree.hook.writes.load(AtomicOrdering::Relaxed), 0);
let new = ByteView::new(b"new");
tree.migrate(move |_, _| MigrateAction::Update(new.clone()))
.expect("migrate update");
assert_eq!(
tree.hook.inits.load(AtomicOrdering::Relaxed),
0,
"Update with NEEDS_INIT=false"
);
assert_eq!(
tree.hook.writes.load(AtomicOrdering::Relaxed),
0,
"Update must not fire on_write either"
);
}
#[test]
fn var_tree_public_put_still_fires_on_write_once() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, false>(dir.path(), CountingHook::default());
tree.put(&1u64.to_be_bytes(), b"v").expect("put");
assert_eq!(tree.hook.writes.load(AtomicOrdering::Relaxed), 1);
}
#[test]
fn var_tree_atomic_fires_hooks() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, true>(dir.path(), CountingHook::default());
let key = 1u64.to_be_bytes();
tree.atomic(&key, |shard| {
shard.put(&key, b"a")?; shard.delete(&key)?; Ok(())
})
.expect("atomic");
assert_eq!(tree.hook.writes.load(AtomicOrdering::Relaxed), 2);
assert_eq!(tree.hook.writes_with_old.load(AtomicOrdering::Relaxed), 1); }
#[test]
fn var_tree_atomic_fires_for_applied_on_err() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, true>(dir.path(), CountingHook::default());
let key = 1u64.to_be_bytes();
let r: DbResult<()> = tree.atomic(&key, |shard| {
shard.put(&key, b"x")?;
Err(DbError::KeyNotFound)
});
assert!(r.is_err());
assert_eq!(tree.hook.writes.load(AtomicOrdering::Relaxed), 1);
}
#[test]
fn var_tree_atomic_same_key_old_from_disk() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<false, true>(dir.path(), CountingHook::default());
let key = 1u64.to_be_bytes();
tree.atomic(&key, |shard| {
shard.put(&key, b"first")?;
shard.put(&key, b"second")?;
Ok(())
})
.expect("atomic");
assert_eq!(tree.hook.writes.load(AtomicOrdering::Relaxed), 2);
assert_eq!(tree.hook.writes_with_old.load(AtomicOrdering::Relaxed), 1); assert_eq!(
crate::sync::lock(&tree.hook.last_write_new).clone(),
Some(b"second".to_vec())
);
}
#[test]
fn var_tree_shard_put_returns_existed() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, true>(dir.path(), CountingHook::default());
let key = 1u64.to_be_bytes();
let (fresh, overwrite) = tree
.atomic(&key, |shard| {
let fresh = shard.put(&key, b"a")?; let overwrite = shard.put(&key, b"b")?; Ok((fresh, overwrite))
})
.expect("atomic");
assert!(!fresh, "fresh insert must return false");
assert!(overwrite, "overwrite must return true");
}
#[test]
fn compare_delete_match_mismatch_absent() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let k = [1u8; 8];
tree.put(&k, b"hello").unwrap();
assert!(matches!(
tree.compare_delete(&k, b"WRONG"),
Err(DbError::CasMismatch)
));
assert_eq!(tree.get(&k).as_deref(), Some(&b"hello"[..]));
assert!(tree.compare_delete(&k, b"hello").is_ok());
assert!(tree.get(&k).is_none());
assert!(matches!(
tree.compare_delete(&k, b"hello"),
Err(DbError::KeyNotFound)
));
}
#[test]
fn compare_delete_hook_needs_old_true_gets_old_value() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<false, true>(dir.path(), CountingHook::default());
let k = [2u8; 8];
tree.put(&k, b"payload").unwrap();
let with_old_before = tree.hook.writes_with_old.load(AtomicOrdering::Relaxed);
tree.compare_delete(&k, b"payload").unwrap();
assert_eq!(
tree.hook.writes_with_old.load(AtomicOrdering::Relaxed) - with_old_before,
1
);
assert_eq!(*crate::sync::lock(&tree.hook.last_write_new), None);
}
#[test]
fn compare_delete_hook_needs_old_false_gets_none() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<false, false>(dir.path(), CountingHook::default());
let k = [3u8; 8];
tree.put(&k, b"payload").unwrap();
let writes_before = tree.hook.writes.load(AtomicOrdering::Relaxed);
let with_old_before = tree.hook.writes_with_old.load(AtomicOrdering::Relaxed);
tree.compare_delete(&k, b"payload").unwrap();
assert_eq!(
tree.hook.writes.load(AtomicOrdering::Relaxed) - writes_before,
1
);
assert_eq!(
tree.hook.writes_with_old.load(AtomicOrdering::Relaxed) - with_old_before,
0
);
}
#[test]
fn read_value_locked_result_ok_from_write_buf() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 1u64.to_be_bytes();
let payload = b"in-write-buffer-value";
tree.put(&key, payload).expect("put");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let disk = node.read_loc();
drop(guard);
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let inner = shard.lock();
let v = tree
.read_value_locked_result(&disk, shard_id, &inner)
.expect("write-buf read must succeed");
assert_eq!(v.as_bytes(), payload);
}
#[test]
fn read_value_locked_result_ok_from_disk_immutable() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 2u64.to_be_bytes();
let payload = b"single-block-immutable";
tree.put(&key, payload).expect("put");
for i in 100u64..135 {
tree.put(&i.to_be_bytes(), &[i as u8; 256])
.expect("rotator");
}
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let disk = node.read_loc();
drop(guard);
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let inner = shard.lock();
assert_ne!(
disk.file_id, inner.active.file_id,
"test setup failed: key=2 entry is still in the active file's write buffer",
);
let v = tree
.read_value_locked_result(&disk, shard_id, &inner)
.expect("disk read must succeed");
assert_eq!(v.as_bytes(), payload);
}
#[test]
fn read_value_locked_result_propagates_stale_disk_loc() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
let key = 3u64.to_be_bytes();
let snap = put_until_compactable(&tree, key);
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
let inner = shard.lock();
match tree.read_value_locked_result(&snap, shard_id, &inner) {
Err(DbError::StaleDiskLoc) => {}
Ok(v) => panic!("expected StaleDiskLoc, got Ok({:?})", v.as_bytes()),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
}
#[test]
fn large_value_with_first_block_cached_uses_fallback() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.block_cache.max_size = 1 << 20;
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open");
let key = 42u64.to_be_bytes();
let payload: Vec<u8> = (0..20_000u32).map(|i| i as u8).collect();
tree.put(&key, &payload).expect("put large");
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let disk = node.read_loc();
drop(guard);
let start = (disk.offset & 4095) as usize;
assert!(
start + disk.len as usize > 8192,
"test precondition: large value must span >2 blocks",
);
let block_offset = disk.offset as u64 & !4095;
let shard_id = tree.shard_for(&key) as u8;
let cache_key = BlockKey {
shard_id,
file_id: disk.file_id,
block_offset,
};
tree.block_cache
.insert(cache_key, Arc::new(AlignedBuf::zeroed(4096)));
assert!(
tree.block_cache.get(&cache_key).is_some(),
"cache must be enabled for warm-cache scenario",
);
let v = tree
.read_value_cached_inner(&disk, shard_id)
.expect("large value must read via locked fallback");
assert_eq!(v.as_bytes(), payload.as_slice());
}
#[test]
fn extract_from_block_single_block() {
let mut block = AlignedBuf::zeroed(4096);
for (i, byte) in block.iter_mut().enumerate() {
*byte = i as u8;
}
let v = VarTree::<[u8; 8]>::extract_from_block(&block, 100, 50, || {
panic!("next_block must not be called for single-block reads")
})
.expect("ok");
let expected: Vec<u8> = (100u8..150u8).collect();
assert_eq!(v.as_bytes(), expected.as_slice());
}
#[test]
fn extract_from_block_two_blocks_exact() {
let mut first = AlignedBuf::zeroed(4096);
for byte in first.iter_mut() {
*byte = 0xAA;
}
let mut second = AlignedBuf::zeroed(4096);
for byte in second.iter_mut() {
*byte = 0xBB;
}
let v = VarTree::<[u8; 8]>::extract_from_block(&first, 4095, 4097, || Ok(Arc::new(second)))
.expect("ok");
let bytes = v.as_bytes();
assert_eq!(bytes.len(), 4097);
assert_eq!(bytes[0], 0xAA);
assert_eq!(bytes[1], 0xBB);
assert_eq!(bytes[4096], 0xBB);
}
#[test]
fn extract_from_block_two_blocks_partial() {
let mut first = AlignedBuf::zeroed(4096);
for byte in first.iter_mut() {
*byte = 0x11;
}
let mut second = AlignedBuf::zeroed(4096);
for byte in second.iter_mut() {
*byte = 0x22;
}
let v = VarTree::<[u8; 8]>::extract_from_block(&first, 4000, 200, || Ok(Arc::new(second)))
.expect("ok");
let bytes = v.as_bytes();
assert_eq!(bytes.len(), 200);
assert!(bytes[..96].iter().all(|b| *b == 0x11));
assert!(bytes[96..].iter().all(|b| *b == 0x22));
}
fn open_large_value_tree(dir: &std::path::Path) -> VarTree<[u8; 8]> {
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
VarTree::open(dir, cfg).expect("open large-value test tree")
}
fn build_large_payload(seed: u8) -> Vec<u8> {
(0..50_000u32)
.map(|i| (i as u8).wrapping_add(seed))
.collect()
}
fn open_value_cache_tree(dir: &std::path::Path) -> VarTree<[u8; 8]> {
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.value_cache.max_size = 4 << 20; VarTree::open(dir, cfg).expect("open value-cache test tree")
}
#[test]
fn large_value_miss_then_cached() {
let dir = tempdir().unwrap();
let tree = open_value_cache_tree(dir.path());
let key = 1u64.to_be_bytes();
let payload = build_large_payload(0x33);
tree.put(&key, &payload).unwrap();
assert_eq!(tree.get(&key).unwrap().as_bytes(), &payload[..]);
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).unwrap();
let disk = node.read_loc();
let vkey = crate::value_cache::ValueKey {
shard_id: 0,
file_id: disk.file_id,
offset: disk.offset,
};
assert!(
tree.value_cache.get(&vkey).is_some(),
"value cached after read"
);
drop(guard);
assert_eq!(tree.get(&key).unwrap().as_bytes(), &payload[..]);
}
#[test]
fn large_value_read_then_edit_returns_new() {
let dir = tempdir().unwrap();
let tree = open_value_cache_tree(dir.path());
let key = 7u64.to_be_bytes();
let v1 = build_large_payload(0x11);
let v2 = build_large_payload(0x22);
tree.put(&key, &v1).unwrap();
assert_eq!(tree.get(&key).unwrap().as_bytes(), &v1[..]); tree.put(&key, &v2).unwrap(); assert_eq!(tree.get(&key).unwrap().as_bytes(), &v2[..]);
}
#[test]
fn large_value_read_then_compact_returns_live() {
let dir = tempdir().unwrap();
let tree = open_value_cache_tree(dir.path());
let key = 102u64.to_be_bytes();
let payload = build_large_payload(0x42);
tree.put(&key, &payload).unwrap();
assert_eq!(tree.get(&key).unwrap().as_bytes(), &payload[..]);
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
for i in 1..20u8 {
tree.put(&key, &[i; 256]).unwrap();
}
tree.put(&key, b"live-after-compaction").unwrap();
let shard = &tree.engine.shards()[0];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
assert_eq!(tree.get(&key).unwrap().as_bytes(), b"live-after-compaction");
}
#[test]
fn large_value_disabled_cache_reads_correctly() {
let dir = tempdir().unwrap();
let tree = open_large_value_tree(dir.path()); let key = 3u64.to_be_bytes();
let payload = build_large_payload(0x55);
tree.put(&key, &payload).unwrap();
assert_eq!(tree.get(&key).unwrap().as_bytes(), &payload[..]);
assert_eq!(tree.get(&key).unwrap().as_bytes(), &payload[..]);
}
#[test]
fn large_value_read_via_locked_fallback() {
let dir = tempdir().unwrap();
let tree = open_large_value_tree(dir.path());
let key = 100u64.to_be_bytes();
let payload = build_large_payload(0);
tree.put(&key, &payload).expect("put large");
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let v = tree
.read_value_cached(node, &guard)
.expect("read must succeed via locked fallback");
assert_eq!(v.as_bytes(), payload.as_slice());
}
#[cfg(feature = "encryption")]
#[test]
fn get_or_err_and_try_get_surface_read_fault() {
use crate::test_faults::{big_value, corrupt_tags};
let dir = tempfile::tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.encryption_key = Some([7u8; 32]);
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open enc");
let key = 1u64.to_be_bytes();
tree.put(&key, &big_value(0xAB)).expect("put");
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
corrupt_tags(&tree.engine.shard_dirs()[0]);
assert!(tree.get(&key).is_none());
assert!(matches!(
tree.get_or_err(&key),
Err(DbError::EncryptionError(_))
));
assert!(matches!(
tree.try_get(&key),
Err(DbError::EncryptionError(_))
));
let absent = 999u64.to_be_bytes();
assert!(matches!(
tree.get_or_err(&absent),
Err(DbError::KeyNotFound)
));
assert!(matches!(tree.try_get(&absent), Ok(None)));
}
#[cfg(feature = "encryption")]
#[test]
fn mutators_surface_read_fault_not_keynotfound() {
use crate::test_faults::{big_value, corrupt_tags};
let dir = tempfile::tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.encryption_key = Some([7u8; 32]);
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open enc");
let key = 1u64.to_be_bytes();
tree.put(&key, &big_value(0xAB)).unwrap();
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
corrupt_tags(&tree.engine.shard_dirs()[0]);
assert!(matches!(
tree.cas(&key, b"x", b"y"),
Err(DbError::EncryptionError(_))
));
assert!(matches!(
tree.compare_delete(&key, b"x"),
Err(DbError::EncryptionError(_))
));
assert!(matches!(
tree.update(&key, |_| ByteView::new(b"z")),
Err(DbError::EncryptionError(_))
));
assert!(matches!(
tree.fetch_update(&key, |_| ByteView::new(b"z")),
Err(DbError::EncryptionError(_))
));
}
#[cfg(feature = "encryption")]
#[test]
fn first_skips_unreadable_boundary_try_first_surfaces_it() {
use crate::test_faults::{big_value, corrupt_tags};
let dir = tempfile::tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.encryption_key = Some([7u8; 32]);
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open enc");
let k1 = 1u64.to_be_bytes();
let k2 = 2u64.to_be_bytes();
tree.put(&k1, &big_value(0x11)).unwrap();
tree.put(&k2, &big_value(0x22)).unwrap();
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
corrupt_tags(&tree.engine.shard_dirs()[0]);
assert!(tree.first().is_none());
assert_eq!(
tree.first().map(|(k, _)| k),
tree.iter().next().map(|(k, _)| k)
);
assert!(matches!(tree.try_first(), Err(DbError::EncryptionError(_))));
assert!(matches!(tree.try_last(), Err(DbError::EncryptionError(_))));
}
#[cfg(feature = "encryption")]
#[test]
fn large_value_read_encrypted() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.encryption_key = Some([7u8; 32]);
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open enc");
let key = 101u64.to_be_bytes();
let payload = build_large_payload(0xAB);
tree.put(&key, &payload).expect("put encrypted large");
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let v = tree
.read_value_cached(node, &guard)
.expect("encrypted large read must succeed via pread_value_encrypted");
assert_eq!(v.as_bytes(), payload.as_slice());
}
#[test]
fn large_value_stale_disk_loc_deterministic() {
let dir = tempdir().unwrap();
let tree = open_large_value_tree(dir.path());
let key = 102u64.to_be_bytes();
let payload = build_large_payload(0x42);
tree.put(&key, &payload).expect("first large put");
let snap = {
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
node.read_loc()
};
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate after large put");
for i in 1..20u8 {
tree.put(&key, &[i; 256]).expect("overwrite");
}
tree.put(&key, b"live-after-compaction").expect("final put");
let shard_id = tree.shard_for(&key) as u8;
let shard = &tree.engine.shards()[shard_id as usize];
let _ = compact_shard(shard, &tree, 0.0).expect("compaction");
match tree.read_value_cached_inner(&snap, shard_id) {
Err(DbError::StaleDiskLoc) => {}
Ok(v) => panic!("expected StaleDiskLoc, got Ok({:?})", v.as_bytes()),
Err(e) => panic!("expected StaleDiskLoc, got Err({e})"),
}
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let v = tree
.read_value_cached(node, &guard)
.expect("public path must retry and return live value");
assert_eq!(v.as_bytes(), b"live-after-compaction");
}
#[test]
fn read_value_locked_result_ok_from_cache_single_block() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 8192;
cfg.write_buffer_size = 8192;
cfg.compaction_threshold = 0.0;
cfg.block_cache.max_size = 1 << 20;
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open");
let key = 9u64.to_be_bytes();
let payload = b"small-single-block-value";
tree.put(&key, payload).expect("put");
for i in 100u64..135 {
tree.put(&i.to_be_bytes(), &[i as u8; 256])
.expect("rotator");
}
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let disk = node.read_loc();
drop(guard);
let start = (disk.offset & 4095) as usize;
let len = disk.len as usize;
assert!(
start + len <= 4096,
"test precondition: value must fit in a single block",
);
let block_offset = disk.offset as u64 & !4095;
let shard_id = tree.shard_for(&key) as u8;
let cache_key = BlockKey {
shard_id,
file_id: disk.file_id,
block_offset,
};
let block = tree
.get_or_read_block(shard_id, disk.file_id, block_offset)
.expect("read block");
tree.block_cache.insert(cache_key, block);
assert!(
tree.block_cache.get(&cache_key).is_some(),
"cache must contain the block for step 2 to fire",
);
let shard = &tree.engine.shards()[shard_id as usize];
let inner = shard.lock();
assert_ne!(
disk.file_id, inner.active.file_id,
"test setup failed: key entry is still in the active write buffer",
);
let v = tree
.read_value_locked_result(&disk, shard_id, &inner)
.expect("cache-hit read must succeed");
assert_eq!(v.as_bytes(), payload);
}
#[test]
fn var_tree_get_with_file_id_above_u16() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.block_cache.max_size = 1 << 20;
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open");
let shard = &tree.engine.shards()[0];
shard.set_next_file_id(70_000);
shard.rotate_active_for_test(8).expect("first rotate");
assert!(
shard.active_file_id() >= 70_000,
"active_file_id should be >= 70_000 after rotation"
);
let key = 42u64.to_be_bytes();
let value = vec![0xC3u8; 512];
tree.put(&key, &value).expect("put");
{
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).expect("indexed");
let disk = node.read_loc();
assert!(
disk.file_id > u16::MAX as u32,
"DiskLoc.file_id must be above u16::MAX, got {}",
disk.file_id,
);
}
shard.flush().expect("flush");
shard.rotate_active_for_test(8).expect("second rotate");
let got = tree.get(&key).expect("get must return Some");
assert_eq!(got.as_bytes(), value.as_slice());
}
#[test]
fn recovery_handles_file_id_above_u16() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.block_cache.max_size = 1 << 20;
{
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg.clone()).expect("open A");
let shard = &tree.engine.shards()[0];
shard.set_next_file_id(70_000);
shard
.rotate_active_for_test(8)
.expect("rotate to file_id 70_000");
assert!(
shard.active_file_id() >= 70_000,
"active_file_id should be >= 70_000 after rotation"
);
for i in 0u64..4 {
let key = i.to_be_bytes();
let value = vec![i as u8; 200];
tree.put(&key, &value).expect("put phase A");
}
tree.close().expect("close phase A");
}
{
let tree: VarTree<[u8; 8]> = VarTree::open(dir.path(), cfg).expect("open B");
assert_eq!(tree.len(), 4, "all 4 entries must survive recovery");
for i in 0u64..4 {
let key = i.to_be_bytes();
let expected = vec![i as u8; 200];
let got = tree
.get(&key)
.unwrap_or_else(|| panic!("key {i} not found after recovery"));
assert_eq!(
got.as_bytes(),
expected.as_slice(),
"value mismatch for key {i} after recovery"
);
}
let shard = &tree.engine.shards()[0];
let max_fid = shard.file_ids().into_iter().max().expect("non-empty");
assert!(
max_fid > u16::MAX as u32,
"max file_id should exceed u16::MAX after recovery (got {})",
max_fid
);
}
}
#[test]
fn warmup_skips_large_values_block_cache() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 128 * 1024;
cfg.write_buffer_size = 128 * 1024;
cfg.compaction_threshold = 0.0;
cfg.block_cache.max_size = 1 << 20; let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).unwrap();
let key = 5u64.to_be_bytes();
let payload = build_large_payload(0x42); tree.put(&key, &payload).unwrap();
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).unwrap();
let disk = node.read_loc();
assert!(
disk.is_value_cache_routed(),
"test precondition: value must be large"
);
drop(guard);
tree.warmup().unwrap();
let guard = tree.index.collector().enter();
let node = tree.index.get(key.as_bytes(), &guard).unwrap();
let disk = node.read_loc();
let bkey = BlockKey {
shard_id: 0,
file_id: disk.file_id,
block_offset: disk.offset as u64 & !4095,
};
assert!(
tree.block_cache.get(&bkey).is_none(),
"warmup must not cache a large value's first block"
);
}
#[test]
fn warmup_loads_both_blocks_of_a_straddling_value() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 1 << 20;
cfg.write_buffer_size = 1 << 20;
cfg.compaction_threshold = 0.0;
cfg.block_cache.max_size = 1 << 20;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).unwrap();
tree.put(&0u64.to_be_bytes(), &vec![0x11u8; 3000]).unwrap();
tree.put(&1u64.to_be_bytes(), &vec![0x5Au8; 1500]).unwrap();
tree.put(&2u64.to_be_bytes(), &vec![0x77u8; 30_000])
.unwrap();
tree.engine.shards()[0]
.rotate_active_for_test(8)
.expect("rotate");
let guard = tree.index.collector().enter();
let straddler = tree
.index
.get(1u64.to_be_bytes().as_bytes(), &guard)
.unwrap()
.read_loc();
let start = (straddler.offset & 4095) as usize;
assert!(
!straddler.is_value_cache_routed() && start + straddler.len as usize > 4096,
"test precondition: value must straddle a block boundary and stay block-cache routed \
(start={start}, len={})",
straddler.len
);
let tail = tree
.index
.get(2u64.to_be_bytes().as_bytes(), &guard)
.unwrap()
.read_loc();
assert!(
tail.is_value_cache_routed(),
"test precondition: the tail value must be value-cache routed so warmup skips it"
);
drop(guard);
tree.warmup().unwrap();
let block_offset = straddler.offset as u64 & !4095;
let bkey = |off| BlockKey {
shard_id: 0,
file_id: straddler.file_id,
block_offset: off,
};
assert!(
tree.block_cache.get(&bkey(block_offset)).is_some(),
"warmup must cache the value's first block"
);
assert!(
tree.block_cache.get(&bkey(block_offset + 4096)).is_some(),
"warmup must also cache the block the value spills into"
);
}
#[test]
fn var_get_many_lossy_aligned() {
let dir = tempdir().unwrap();
let cfg = Config::test();
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).unwrap();
tree.put(&1u64.to_be_bytes(), b"alpha").unwrap();
tree.put(&2u64.to_be_bytes(), b"beta").unwrap();
let keys: Vec<[u8; 8]> = vec![2u64.to_be_bytes(), 9u64.to_be_bytes(), 1u64.to_be_bytes()];
let got = tree.get_many(&keys);
assert_eq!(got.len(), 3);
assert_eq!(got[0].as_deref(), Some(&b"beta"[..]));
assert_eq!(got[1], None);
assert_eq!(got[2].as_deref(), Some(&b"alpha"[..]));
}
#[test]
fn var_get_many_large_value_cache_path() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.value_cache.max_size = 4 << 20;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).unwrap();
let big = vec![7u8; 16 * 1024]; tree.put(&1u64.to_be_bytes(), &big).unwrap();
let got1 = tree.get_many(&[1u64.to_be_bytes()]);
assert_eq!(got1[0].as_deref(), Some(&big[..]));
let got2 = tree.get_many(&[1u64.to_be_bytes()]);
assert_eq!(got2[0].as_deref(), Some(&big[..]));
}
#[test]
fn var_update_many_set_keep_delete() {
use crate::{Applied, Upsert};
let dir = tempdir().unwrap();
let cfg = Config::test();
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).unwrap();
tree.put(&1u64.to_be_bytes(), b"old").unwrap();
tree.put(&2u64.to_be_bytes(), b"gone").unwrap();
let items: Vec<([u8; 8], &[u8])> = vec![
(1u64.to_be_bytes(), b"new"),
(2u64.to_be_bytes(), b""), (3u64.to_be_bytes(), b"ins"), (4u64.to_be_bytes(), b""), ];
let out = tree
.update_many(items, |k, _cur, p| {
let k2 = 2u64.to_be_bytes();
let k4 = 4u64.to_be_bytes();
if *k == k2 || *k == k4 {
Upsert::Delete
} else {
Upsert::Put(ByteView::new(p))
}
})
.unwrap();
assert!(matches!(out[0].1, Applied::Written { .. }));
assert!(matches!(out[1].1, Applied::Deleted(_)));
assert!(matches!(out[2].1, Applied::Written { old: None, .. }));
assert_eq!(out[3].1, Applied::Kept);
assert_eq!(tree.get(&1u64.to_be_bytes()).as_deref(), Some(&b"new"[..]));
assert!(tree.get(&2u64.to_be_bytes()).is_none());
assert_eq!(tree.get(&3u64.to_be_bytes()).as_deref(), Some(&b"ins"[..]));
}
#[test]
fn var_tree_shard_update_and_fetch_update() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, true>(dir.path(), CountingHook::default());
let key = 1u64.to_be_bytes();
tree.put(&key, b"a").unwrap();
let (upd, fetched, missing) = tree
.atomic(&key, |shard| {
let upd = shard.update(&key, |old| {
let mut v = old.to_vec();
v.push(b'b');
ByteView::from(v.as_slice())
})?;
let fetched = shard.fetch_update(&key, |old| {
let mut v = old.to_vec();
v.push(b'c');
ByteView::from(v.as_slice())
})?;
let missing = shard.update(&2u64.to_be_bytes(), |old| ByteView::from(old))?;
Ok((upd, fetched, missing))
})
.expect("atomic");
assert_eq!(upd.as_deref(), Some(&b"ab"[..])); assert_eq!(fetched.as_deref(), Some(&b"ab"[..])); assert!(missing.is_none());
assert_eq!(tree.get(&key).as_deref(), Some(&b"abc"[..]));
}
#[test]
fn var_tree_atomic_update_fires_hook() {
let dir = tempdir().unwrap();
let tree = open_test_tree_hooked::<true, true>(dir.path(), CountingHook::default());
let key = 1u64.to_be_bytes();
tree.atomic(&key, |shard| {
shard.put(&key, b"init")?; shard.update(&key, |old| {
let mut v = old.to_vec();
v.push(b'!');
ByteView::from(v.as_slice())
})?; Ok(())
})
.expect("atomic");
assert_eq!(
tree.hook.writes.load(AtomicOrdering::Relaxed),
2,
"hook must fire once for put and once for update"
);
assert_eq!(
tree.hook.writes_with_old.load(AtomicOrdering::Relaxed),
1,
"only the update event carries an old value"
);
assert_eq!(tree.get(&key).as_deref(), Some(&b"init!"[..]));
}
#[test]
fn straddle_read_2block_direct_io_returns_full_value() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 1 << 20;
cfg.write_buffer_size = 8192;
cfg.direct_io = true;
cfg.compaction_threshold = 0.0;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).expect("open");
let v1: Vec<u8> = (0..3000).map(|i| ((i % 251) + 1) as u8).collect();
let v2: Vec<u8> = (0..3000).map(|i| ((i % 241) + 2) as u8).collect();
let k1 = 1u64.to_be_bytes();
let k2 = 2u64.to_be_bytes();
tree.put(&k1, &v1).expect("put v1");
tree.put(&k2, &v2).expect("put v2");
tree.flush_buffers().expect("flush");
assert_eq!(
tree.get(&k2).as_deref(),
Some(&v2[..]),
"straddling value must read back byte-for-byte (tail was zeroed before the fix)"
);
assert_eq!(tree.get(&k1).as_deref(), Some(&v1[..]));
}
#[test]
fn straddle_read_large_value_direct_io_try_get_ok() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 1 << 20;
cfg.write_buffer_size = 16 * 1024;
cfg.direct_io = true;
cfg.compaction_threshold = 0.0;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).expect("open");
let v1: Vec<u8> = (0..3000).map(|i| ((i % 251) + 1) as u8).collect();
let v2: Vec<u8> = (0..9000).map(|i| ((i % 239) + 3) as u8).collect();
let k1 = 1u64.to_be_bytes();
let k2 = 2u64.to_be_bytes();
tree.put(&k1, &v1).expect("put v1");
tree.put(&k2, &v2).expect("put v2");
tree.flush_buffers().expect("flush");
let got = tree
.try_get(&k2)
.expect("try_get must not error for a live key");
assert_eq!(
got.as_deref(),
Some(&v2[..]),
"straddling large value must read back byte-for-byte"
);
}
#[cfg(feature = "encryption")]
#[test]
fn straddle_read_2block_encrypted_returns_full_value() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 1 << 20;
cfg.write_buffer_size = 8192;
cfg.encryption_key = Some([7u8; 32]);
cfg.compaction_threshold = 0.0;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).expect("open enc");
let v1: Vec<u8> = (0..3000).map(|i| ((i % 251) + 1) as u8).collect();
let v2: Vec<u8> = (0..3000).map(|i| ((i % 241) + 2) as u8).collect();
let k1 = 1u64.to_be_bytes();
let k2 = 2u64.to_be_bytes();
tree.put(&k1, &v1).expect("put v1");
tree.put(&k2, &v2).expect("put v2");
tree.flush_buffers().expect("flush");
let got = tree
.try_get(&k2)
.expect("encrypted straddle read must not error");
assert_eq!(got.as_deref(), Some(&v2[..]));
assert_eq!(tree.get(&k1).as_deref(), Some(&v1[..]));
}
#[test]
fn straddle_read_2block_cache_hit_returns_full_value() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 1 << 20;
cfg.write_buffer_size = 8192;
cfg.direct_io = true;
cfg.block_cache.max_size = 1 << 20;
cfg.compaction_threshold = 0.0;
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).expect("open");
let v1: Vec<u8> = (0..3000).map(|i| ((i % 251) + 1) as u8).collect();
let v2: Vec<u8> = (0..3000).map(|i| ((i % 241) + 2) as u8).collect();
let k1 = 1u64.to_be_bytes();
let k2 = 2u64.to_be_bytes();
tree.put(&k1, &v1).expect("put v1");
tree.put(&k2, &v2).expect("put v2");
tree.flush_buffers().expect("flush");
let shard_id = tree.shard_for(&k2) as u8;
let key0 = BlockKey {
shard_id,
file_id: 1,
block_offset: 0,
};
let (blk0, _is_full) = tree.engine.shards()[shard_id as usize]
.read_block(1, 0)
.expect("read block 0");
tree.block_cache.insert(key0, Arc::new(blk0));
assert_eq!(
tree.get(&k2).as_deref(),
Some(&v2[..]),
"straddling value via cache-hit path must read back byte-for-byte (tail was zeroed before the fix)"
);
}
fn settled_block_cfg(direct_io: bool) -> Config {
let mut cfg = Config::test();
cfg.shard_count = 1;
cfg.max_file_size = 1 << 20;
cfg.write_buffer_size = 8192;
cfg.direct_io = direct_io;
cfg.block_cache.max_size = 1 << 20;
cfg.compaction_threshold = 0.0;
cfg
}
fn active_file_id(tree: &VarTree<[u8; 8]>) -> u32 {
tree.engine.shards()[0].lock().active.file_id
}
#[allow(clippy::type_complexity)]
fn put_two_and_flush(tree: &VarTree<[u8; 8]>) -> ([u8; 8], Vec<u8>, [u8; 8], Vec<u8>) {
let v1: Vec<u8> = (0..3000).map(|i| ((i % 251) + 1) as u8).collect();
let v2: Vec<u8> = (0..3000).map(|i| ((i % 241) + 2) as u8).collect();
let k1 = 1u64.to_be_bytes();
let k2 = 2u64.to_be_bytes();
tree.put(&k1, &v1).expect("put v1");
tree.put(&k2, &v2).expect("put v2");
tree.flush_buffers().expect("flush");
(k1, v1, k2, v2)
}
#[test]
fn settled_active_block_is_cached_and_served_without_disk_reads() {
let dir = tempdir().unwrap();
let tree = VarTree::<[u8; 8]>::open(dir.path(), settled_block_cfg(true)).expect("open");
let (k1, v1, _, _) = put_two_and_flush(&tree);
let key0 = BlockKey {
shard_id: 0,
file_id: active_file_id(&tree),
block_offset: 0,
};
assert!(
tree.block_cache.get(&key0).is_none(),
"nothing cached before the first read"
);
let before = test_disk_value_reads();
assert_eq!(tree.get(&k1).as_deref(), Some(&v1[..]));
assert_eq!(test_disk_value_reads(), before + 1, "first read misses");
assert!(
tree.block_cache.get(&key0).is_some(),
"block 0 is fully below base_offset and must be cached even though the file is active"
);
assert_eq!(tree.get(&k1).as_deref(), Some(&v1[..]));
assert_eq!(
test_disk_value_reads(),
before + 1,
"second read must hit the block cache"
);
}
#[test]
fn active_block_containing_base_offset_is_not_cached() {
let dir = tempdir().unwrap();
let tree = VarTree::<[u8; 8]>::open(dir.path(), settled_block_cfg(true)).expect("open");
let (_, _, k2, v2) = put_two_and_flush(&tree);
let file_id = active_file_id(&tree);
assert_eq!(
tree.engine.shards()[0].lock().write_buf.base_offset(),
4096,
"non-force page-granular flush leaves base_offset at the page boundary"
);
let _ = tree
.get_or_read_block(0, file_id, 4096)
.expect("read block 1");
let key1 = BlockKey {
shard_id: 0,
file_id,
block_offset: 4096,
};
assert!(
tree.block_cache.get(&key1).is_none(),
"block 1 straddles base_offset and must not be cached"
);
assert_eq!(tree.get(&k2).as_deref(), Some(&v2[..]));
}
#[test]
fn settled_active_block_is_cached_in_byte_granular_mode() {
let dir = tempdir().unwrap();
let tree = VarTree::<[u8; 8]>::open(dir.path(), settled_block_cfg(false)).expect("open");
let (k1, v1, _, _) = put_two_and_flush(&tree);
let file_id = active_file_id(&tree);
let base = tree.engine.shards()[0].lock().write_buf.base_offset();
assert!(
(4096..8192).contains(&base),
"byte-granular flush must leave base_offset mid-block, got {base}"
);
let before = test_disk_value_reads();
assert_eq!(tree.get(&k1).as_deref(), Some(&v1[..]));
assert_eq!(tree.get(&k1).as_deref(), Some(&v1[..]));
assert_eq!(
test_disk_value_reads(),
before + 1,
"second read must hit the block cache"
);
let key1 = BlockKey {
shard_id: 0,
file_id,
block_offset: 4096,
};
let _ = tree
.get_or_read_block(0, file_id, 4096)
.expect("read block 1");
assert!(
tree.block_cache.get(&key1).is_none(),
"block 1 contains base_offset"
);
}
#[test]
fn reopened_active_file_serves_second_pass_from_block_cache() {
let dir = tempdir().unwrap();
let value = vec![0xAB; 200];
{
let tree = VarTree::<[u8; 8]>::open(dir.path(), settled_block_cfg(true)).expect("open");
for i in 0..200u64 {
tree.put(&i.to_be_bytes(), &value).expect("put");
}
tree.close().expect("close");
}
let tree = VarTree::<[u8; 8]>::open(dir.path(), settled_block_cfg(true)).expect("reopen");
assert_eq!(
tree.engine.shards()[0].file_ids().len(),
1,
"no rotation happened"
);
let first_pass_start = test_disk_value_reads();
for i in 0..200u64 {
assert_eq!(tree.get(&i.to_be_bytes()).as_deref(), Some(&value[..]));
}
assert!(
test_disk_value_reads() > first_pass_start,
"cold pass reads disk"
);
let second_pass_start = test_disk_value_reads();
for i in 0..200u64 {
assert_eq!(tree.get(&i.to_be_bytes()).as_deref(), Some(&value[..]));
}
assert_eq!(
test_disk_value_reads(),
second_pass_start,
"warm pass over the active file must be served entirely from the block cache"
);
}
#[cfg(feature = "encryption")]
#[test]
fn settled_active_block_is_cached_encrypted() {
let dir = tempdir().unwrap();
let mut cfg = settled_block_cfg(false);
cfg.encryption_key = Some([7u8; 32]);
let tree = VarTree::<[u8; 8]>::open(dir.path(), cfg).expect("open enc");
let (k1, v1, k2, v2) = put_two_and_flush(&tree);
let file_id = active_file_id(&tree);
let before = test_disk_value_reads();
assert_eq!(tree.try_get(&k1).expect("get").as_deref(), Some(&v1[..]));
assert_eq!(tree.try_get(&k1).expect("get").as_deref(), Some(&v1[..]));
assert_eq!(
test_disk_value_reads(),
before + 1,
"second read must hit the block cache"
);
let key1 = BlockKey {
shard_id: 0,
file_id,
block_offset: 4096,
};
assert!(
tree.block_cache.get(&key1).is_none(),
"unsealed page is not cached"
);
assert_eq!(tree.try_get(&k2).expect("get").as_deref(), Some(&v2[..]));
}
#[test]
fn keys_range_bounds_dir_matches_iter_keys_without_value_reads() {
let dir = tempdir().unwrap();
let tree = open_test_tree(dir.path());
for i in [1u8, 5, 10] {
let mut k = [0u8; 8];
k[7] = i;
tree.put(&k, &[i; 64]).unwrap();
}
let from_iter: Vec<_> = tree.iter().map(|(k, _)| k).collect();
let from_keys: Vec<_> = tree
.keys_range_bounds_dir(Bound::Unbounded, Bound::Unbounded, dir_from(tree.reversed))
.collect();
assert_eq!(from_iter, from_keys);
}
}