use std::hash::Hash;
use std::mem::size_of;
use std::ops::{Bound, ControlFlow};
use std::sync::{Arc, OnceLock, Weak};
use crate::Key;
use crate::compaction::{CompactionIndex, compact_shard};
use crate::config::Config;
use crate::const_tree::ConstNew;
use crate::ctx::{Ctx, Family, Held, Owned, ShardGuards};
use crate::disk_loc::DiskLoc;
use crate::durability::{Bitcask, Durability, DurabilityInner};
use crate::engine::Engine;
use crate::error::{DbError, DbResult};
use crate::hook::{NoHook, WriteHook};
use crate::key::Location;
use crate::map_index::{
Dir, KeyPage, MapIterView, Page, ShardIndex, ShardSource, dir_from, key_index_batch,
};
use crate::recovery::recover_const_map;
use crate::sync::{self, Mutex, MutexGuard};
use crate::ttl::{SweepOutcome, TtlSlots};
use crate::watch::{WatchEventInner, WatchWakeBatch};
enum ConstMapSweepAction<K: Key> {
Delete(K, u64),
Reschedule(K, u64),
Drop,
Retry(K),
}
pub(crate) struct MapEntry<const V: usize, L: Location> {
pub(crate) loc: L,
pub(crate) value: [u8; V],
}
pub struct ConstMap<
K: Key + Send + Sync + Hash + Eq,
const V: usize,
H: WriteHook<K> = NoHook,
D: Durability = Bitcask,
> {
indexes: Vec<Mutex<ShardIndex<K, MapEntry<V, D::Loc>>>>,
durability: D,
shard_prefix_bits: usize,
pub(crate) hook: H,
iterable: bool,
reversed: bool,
tx_self: OnceLock<Weak<Self>>,
ttl: TtlSlots<K>,
pub(crate) counter_dirty: OnceLock<crate::counter_dirty::CounterDirty>,
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>, D: Durability>
ConstMap<K, V, H, D>
{
pub(crate) fn watch_channel(&self) -> Option<&std::sync::Arc<crate::watch::WatchChannel<K>>> {
self.hook.watch_channel()
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize> ConstMap<K, V, NoHook, Bitcask> {
#[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 + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>> ConstMap<K, V, H, Bitcask> {
pub(crate) fn floor_recovered_gsns(&self, gsn_high: u64, shard_gsn_high: &[u64]) {
self.durability.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.durability.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(false);
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 iterable = config.iterable;
let reversed = config.reversed;
let engine = Engine::open(path, config)?;
let shard_count = engine.shards().len();
let mut indexes = Vec::with_capacity(shard_count);
for _ in 0..shard_count {
indexes.push(Mutex::new(ShardIndex::new(false)));
}
let durability = Bitcask {
engine,
compaction_threshold,
};
let map = Self {
indexes,
durability,
shard_prefix_bits,
hook,
iterable,
reversed,
tx_self: OnceLock::new(),
ttl: TtlSlots::new(),
counter_dirty: OnceLock::new(),
};
let shard_dirs = map.durability.engine.shard_dirs();
let shard_dir_refs = Engine::shard_dir_refs(&shard_dirs);
let shard_ids = map.durability.engine.shard_ids();
let hints = map.durability.engine.hints();
let outcome = recover_const_map::<K, V>(
&shard_dir_refs,
&shard_ids,
map.indexes(),
hints,
#[cfg(feature = "encryption")]
map.durability.engine.shard_ciphers(),
)?;
for tail in &outcome.active_tails {
map.durability.engine.shards()[tail.shard_idx].apply_recovery_tail(tail)?;
}
for (shard_idx, dead) in outcome.shard_dead_bytes {
map.durability.engine.shards()[shard_idx].install_dead_bytes(dead);
}
for (shard_idx, file_seqs) in outcome.shard_file_seqs {
map.durability.engine.shards()[shard_idx].install_recovery_file_seqs(file_seqs);
}
for (shard_idx, max_gsn) in &outcome.shard_max_gsns {
map.durability.engine.shards()[*shard_idx].set_durable_recovered_gsn(*max_gsn);
}
let max_gsn = outcome.max_gsn;
map.durability
.engine
.gsn()
.fetch_max(max_gsn + 1, std::sync::atomic::Ordering::Relaxed);
if hints {
for shard in map.durability.engine.shards().iter() {
shard.set_key_len(size_of::<K>());
}
}
if iterable {
for idx in &map.indexes {
sync::lock(idx).set_iterable_and_rebuild();
}
}
tracing::info!(
key_size = size_of::<K>(),
V,
entries = map.len(),
"const_map recovered"
);
Ok(map)
}
pub fn clean_shutdown(&self) -> DbResult<()> {
self.flush_buffers()?;
if self.durability.engine.hints() {
self.sync_hints()?;
}
self.durability.close()
}
pub fn close(self) -> DbResult<()> {
self.clean_shutdown()
}
pub fn flush_buffers(&self) -> DbResult<()> {
self.durability.engine.flush_buffers()
}
pub(crate) fn durable_flush(&self) -> DbResult<()> {
self.flush_buffers()?;
self.durability.flush()
}
pub fn config(&self) -> &Config {
self.durability.engine.config()
}
pub(crate) fn tx_reserve_gsns(&self, count: usize) -> u64 {
self.durability
.engine
.gsn()
.fetch_add(count as u64, std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn tx_encrypted(&self) -> bool {
#[cfg(feature = "encryption")]
{
self.durability.engine.config().encryption_key.is_some()
}
#[cfg(not(feature = "encryption"))]
{
false
}
}
pub(crate) fn tx_durable_recovered_gsn(&self, sid: usize) -> u64 {
self.durability.engine.shards()[sid].durable_recovered_gsn()
}
pub fn compact(&self) -> DbResult<usize> {
let mut total_compacted = 0;
for shard in self.durability.engine.shards().iter() {
total_compacted += compact_shard(shard, self, self.durability.compaction_threshold)?;
}
Ok(total_compacted)
}
pub fn sync_hints(&self) -> DbResult<()> {
for shard in self.durability.engine.shards().iter() {
shard.write_active_hint(size_of::<K>())?;
}
Ok(())
}
pub(crate) fn indexes(&self) -> &[Mutex<ShardIndex<K, MapEntry<V, DiskLoc>>>] {
&self.indexes
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>> CompactionIndex<K>
for ConstMap<K, V, H, Bitcask>
{
fn update_if_match(&self, key: &K, old_loc: DiskLoc, new_loc: DiskLoc) -> bool {
let mut index = sync::lock(&self.indexes[self.shard_for(key)]);
if let Some(entry) = index.get_mut(key)
&& entry.loc == old_loc
{
entry.loc = new_loc;
return true;
}
false
}
fn contains_key(&self, key: &K) -> bool {
self.contains(key)
}
fn is_live(&self, shard_id: u8, key: &K, loc: DiskLoc) -> bool {
let index = sync::lock(&self.indexes[shard_id as usize]);
index.get(key).is_some_and(|e| e.loc == loc)
}
}
use crate::durability::Fixed;
use crate::fixed::config::FixedConfig;
impl<K: Key + Send + Sync + Hash + Eq, const V: usize> ConstMap<K, V, NoHook, Fixed> {
#[doc = include_str!("../docs/exclusive-access.md")]
pub fn open(path: impl AsRef<std::path::Path>, config: FixedConfig) -> DbResult<Self> {
Self::open_fixed_inner(path, config, NoHook)
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>> ConstMap<K, V, H, Fixed> {
#[doc = include_str!("../docs/exclusive-access.md")]
pub fn open_with_hook(
path: impl AsRef<std::path::Path>,
config: FixedConfig,
hook: H,
) -> DbResult<Self> {
#[cfg(feature = "armour")]
if H::EMITS {
return Err(DbError::ChangeStreamRequiresManagedOpen);
}
Self::open_fixed_inner(path, config, hook)
}
pub(crate) fn open_fixed_inner(
path: impl AsRef<std::path::Path>,
config: FixedConfig,
hook: H,
) -> DbResult<Self> {
let shard_prefix_bits = config.shard_prefix_bits;
let iterable = config.iterable;
let reversed = config.reversed;
let dur = Fixed::open(path, config, std::mem::size_of::<K>(), V)?;
let shard_count = dur.shard_count();
let mut indexes = Vec::with_capacity(shard_count);
for _ in 0..shard_count {
indexes.push(Mutex::new(ShardIndex::new(false)));
}
let total_recovered = dur.recover_entries(|shard_idx| {
let mut guard = sync::lock(&indexes[shard_idx]);
move |key_bytes: &[u8], value_bytes: &[u8], slot_id: u32| -> Option<u32> {
let key = K::from_bytes(key_bytes);
let mut value = [0u8; V];
value.copy_from_slice(value_bytes);
guard
.upsert(
key,
MapEntry {
loc: slot_id,
value,
},
)
.map(|prev| prev.loc)
}
})?;
tracing::info!(
key_size = std::mem::size_of::<K>(),
V,
entries = total_recovered,
"fixed_map recovered"
);
if iterable {
for idx in &indexes {
sync::lock(idx).set_iterable_and_rebuild();
}
}
Ok(Self {
indexes,
durability: dur,
shard_prefix_bits,
hook,
iterable,
reversed,
tx_self: OnceLock::new(),
ttl: TtlSlots::new(),
counter_dirty: OnceLock::new(),
})
}
pub fn clean_shutdown(&self) -> DbResult<()> {
self.durability.close()
}
pub fn close(self) -> DbResult<()> {
self.clean_shutdown()
}
pub(crate) fn tx_reserve_fixed(
&self,
shard: &mut ConstMapShard<'_, K, V, H, Fixed>,
key: &[u8],
delete: bool,
) -> DbResult<(u32, u32)> {
let k = K::from_bytes(key);
let existing = shard.held.locked.1.get(&k).map(|entry| entry.loc);
if delete && existing.is_none() {
return Ok((crate::fixed::slot::NO_SLOT, 0));
}
shard.held.locked.0.reserve_slot(existing, delete)
}
pub(crate) fn tx_arm_slot(
shard: &mut ConstMapShard<'_, K, V, H, Fixed>,
slot_id: u32,
meta: u32,
fresh: bool,
) {
shard.held.locked.0.arm_slot(slot_id, meta, fresh);
}
pub(crate) fn tx_clear_armed_slot(shard: &mut ConstMapShard<'_, K, V, H, Fixed>) {
shard.held.locked.0.clear_armed_slot();
}
pub(crate) fn tx_release_slot(
shard: &mut ConstMapShard<'_, K, V, H, Fixed>,
slot_id: u32,
fresh: bool,
) {
shard.held.locked.0.release_slot_reservation(slot_id, fresh);
}
#[allow(dead_code)] pub(crate) fn tx_fixed_slot_state(
shard: &ConstMapShard<'_, K, V, H, Fixed>,
slot_id: u32,
) -> DbResult<crate::fixed::shard::SlotState> {
shard.held.locked.0.slot_state(slot_id)
}
pub(crate) fn tx_fixed_slot_is_free(
shard: &ConstMapShard<'_, K, V, H, Fixed>,
slot_id: u32,
) -> DbResult<bool> {
let state = shard.held.locked.0.slot_state(slot_id)?;
Ok(crate::fixed::slot::status_of(state.meta) != crate::fixed::slot::STATUS_OCCUPIED)
}
pub(crate) fn tx_fixed_index_slot(
shard: &ConstMapShard<'_, K, V, H, Fixed>,
key: &[u8],
) -> Option<u32> {
let k = K::from_bytes(key);
shard.held.locked.1.get(&k).map(|entry| entry.loc)
}
pub(crate) fn tx_encrypted(&self) -> bool {
false
}
#[doc(hidden)]
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) fn tx_debug_slot_state(
&self,
sid: usize,
slot_id: u32,
) -> DbResult<crate::fixed::shard::SlotState> {
let locked = self.durability.lock_shard(sid);
locked.slot_state(slot_id)
}
#[doc(hidden)]
#[cfg(any(test, feature = "test-faults"))]
pub fn tx_debug_force_slot_meta(&self, sid: usize, slot_id: u32, meta: u32) {
let mut locked = self.durability.lock_shard(sid);
let _ = locked.force_slot_meta(slot_id, meta);
}
}
#[cfg(feature = "replication")]
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>> ConstMap<K, V, H, Fixed> {
pub(crate) fn fixed_durability(&self) -> &Fixed {
&self.durability
}
pub fn fixed_engine_access(
&self,
) -> std::sync::Arc<dyn crate::fixed_replication::FixedEngineAccess> {
self.durability.engine.clone()
}
pub(crate) fn get_slot_id(&self, key: &K) -> Option<u32> {
let index = sync::lock(&self.indexes[self.shard_for(key)]);
index.get(key).map(|e| e.loc)
}
pub(crate) fn remove_key_if_slot_matches(&self, key: &K, slot_id: u32) -> bool {
let mut index = sync::lock(&self.indexes[self.shard_for(key)]);
match index.get(key) {
Some(entry) if entry.loc == slot_id => {
index.remove(key);
true
}
_ => false,
}
}
pub(crate) fn upsert_replicated(&self, key: &K, value: [u8; V], slot_id: u32) {
let mut index = sync::lock(&self.indexes[self.shard_for(key)]);
index.upsert(
*key,
MapEntry {
loc: slot_id,
value,
},
);
}
}
type ConstMapWriteEvent<'v, K, const V: usize> = (K, Option<[u8; V]>, Option<ConstNew<'v, V>>);
pub(crate) type ConstMapOwnedEvent<K, const V: usize> = (K, Option<[u8; V]>, Option<[u8; V]>);
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>, D: Durability> Family
for ConstMap<K, V, H, D>
{
type Locked<'a>
= (
MutexGuard<'a, D::Inner>,
MutexGuard<'a, ShardIndex<K, MapEntry<V, D::Loc>>>,
)
where
Self: 'a;
type Event<'v> = ConstMapWriteEvent<'v, K, V>;
type Owned = ConstMapOwnedEvent<K, V>;
#[inline]
fn lock_shard(&self, sid: usize) -> Self::Locked<'_> {
let inner = self.durability.lock_shard(sid);
let index = sync::lock(&self.indexes[sid]);
(inner, index)
}
fn should_sync<'a>(locked: &Self::Locked<'a>) -> bool
where
Self: 'a,
{
locked.0.should_sync()
}
fn sync(&self, sid: usize) -> DbResult<()> {
self.durability.lock_shard(sid).sync()
}
fn fire(&self, ev: Self::Event<'_>) {
let (k, old, new) = ev;
let new = match &new {
Some(ConstNew::Borrowed(v)) => Some(&v[..]),
Some(ConstNew::Owned(v)) => Some(&v[..]),
None => None,
};
self.hook.on_write(&k, old.as_ref().map(|v| &v[..]), new);
}
fn own(ev: Self::Event<'_>) -> Self::Owned {
let (k, old, new) = ev;
let new = new.map(|n| match n {
ConstNew::Borrowed(v) => *v,
ConstNew::Owned(v) => v,
});
(k, old, new)
}
fn fire_owned(&self, ev: &Self::Owned) {
let (k, old, new) = ev;
self.hook.on_write(
k,
old.as_ref().map(|v| &v[..]),
new.as_ref().map(|v| &v[..]),
);
}
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 = self
.hook
.watch_old()
.then(|| old.map(|value| value.to_vec().into_boxed_slice()))
.flatten();
let new = new.as_ref().map(|value| match value {
ConstNew::Borrowed(value) => value.to_vec().into_boxed_slice(),
ConstNew::Owned(value) => value.to_vec().into_boxed_slice(),
});
channel.publish(
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| match n {
ConstNew::Borrowed(v) => &v[..],
ConstNew::Owned(v) => &v[..],
});
self.apply_projection_for_bytes(k, old.as_ref().map(|v| &v[..]), 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_ref().map(|v| &v[..]),
new.as_ref().map(|v| &v[..]),
)
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>, D: Durability>
ConstMap<K, V, H, D>
{
#[doc(hidden)]
pub fn build_ttl_index(&self) -> DbResult<()> {
if !H::EXPIRES {
return Ok(());
}
let n = self.durability.shard_count();
let slots = self.ttl.install_and_clear(n);
for (sid, slot) in slots.iter().enumerate() {
let _shard = self.durability.lock_shard(sid);
let index = sync::lock(&self.indexes[sid]);
for (key, entry) in index.iter() {
let deadline = self.hook.deadline(key, &entry.value[..]);
sync::lock(slot).record(key, deadline);
}
}
Ok(())
}
#[inline]
fn deadline_for(&self, key: &K, value: &[u8; V]) -> Option<u64> {
if H::EXPIRES {
self.hook.deadline(key, &value[..])
} else {
None
}
}
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);
}
#[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)
}
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();
while remaining > 0 && self.ttl.any_due(now_ms) {
let sid = self.ttl.next_shard(shard_count);
let mut shard_events = 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 locked.1.get(&key) {
Some(entry) => {
match crate::ttl::catch_deadline(|| {
self.hook.deadline(&key, &entry.value[..])
}) {
Some(Some(dl)) if dl <= now_ms => {
ConstMapSweepAction::Delete(key, candidate_dl)
}
Some(Some(dl)) => ConstMapSweepAction::Reschedule(key, dl),
Some(None) => ConstMapSweepAction::Drop,
None => ConstMapSweepAction::Retry(key),
}
}
None => ConstMapSweepAction::Drop,
};
match action {
ConstMapSweepAction::Delete(key, candidate_dl) => {
drop(idx);
let (inner, index) = &mut locked;
match self.delete_locked(sid, inner, index, &key) {
Ok(old) => {
if let Some(old) = old {
if let Some(event) =
self.defer_write_event(&key, Some(old), None)
{
self.publish_watch(&event, active, &mut wakes);
shard_events.push(Self::own(event));
}
outcome.deleted += 1;
}
idx = sync::lock(&slots[sid]);
}
Err(e) => {
sync::lock(&slots[sid]).record(&key, Some(candidate_dl));
crate::ttl::log_sweep_failures(None, &outcome);
let needs_sync = Self::should_sync(&locked);
locked.release();
drop(context_active);
let _ = crate::projection::replay_owned_events(
self,
&shard_events,
needs_sync,
sid,
);
wakes.drain();
return Err(e);
}
}
}
ConstMapSweepAction::Reschedule(key, dl) => {
idx.record(&key, Some(dl));
}
ConstMapSweepAction::Retry(key) => {
outcome.failed += 1;
idx.record(&key, Some(now_ms + crate::ttl::TTL_RETRY_BACKOFF_MS));
}
ConstMapSweepAction::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);
wakes.drain();
replay?;
}
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(())
}
pub(crate) fn reversed(&self) -> bool {
self.reversed
}
pub fn get(&self, key: &K) -> Option<[u8; V]> {
metrics::counter!("armdb.ops", "op" => "get", "tree" => "const_map").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.get");
let index = sync::lock(&self.indexes[self.shard_for(key)]);
index.get(key).map(|e| e.value)
}
pub fn get_or_err(&self, key: &K) -> DbResult<[u8; V]> {
self.get(key).ok_or(DbError::KeyNotFound)
}
#[inline]
fn defer_write_event<'v>(
&self,
key: &K,
old: Option<[u8; V]>,
new: Option<[u8; V]>,
) -> Option<ConstMapWriteEvent<'v, K, V>> {
if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((*key, old, new.map(ConstNew::Owned)))
} else {
None
}
}
#[inline]
fn defer_borrowed_event<'v>(
&self,
key: &K,
old: Option<[u8; V]>,
new: Option<&'v [u8; V]>,
) -> Option<ConstMapWriteEvent<'v, K, V>> {
if H::NEEDS_WRITE || (H::WATCHES && crate::watch::context_active()) {
Some((*key, old, new.map(ConstNew::Borrowed)))
} else {
None
}
}
pub fn put(&self, key: &K, value: &[u8; V]) -> DbResult<Option<[u8; V]>> {
metrics::counter!("armdb.ops", "op" => "put", "tree" => "const_map").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.put");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let (inner, index) = locked;
let old = self.put_locked(shard_id, &mut *inner, index, key, value)?;
*ev_out = self.defer_borrowed_event(key, old, Some(value));
Ok(old)
})
}
pub fn insert(&self, key: &K, value: &[u8; V]) -> DbResult<()> {
metrics::counter!("armdb.ops", "op" => "insert", "tree" => "const_map").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.insert");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let (inner, index) = locked;
self.insert_locked(shard_id, &mut *inner, index, key, value)?;
*ev_out = self.defer_borrowed_event(key, None, Some(value));
Ok(())
})
}
pub fn delete(&self, key: &K) -> DbResult<Option<[u8; V]>> {
metrics::counter!("armdb.ops", "op" => "delete", "tree" => "const_map").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.delete");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let (inner, index) = locked;
let old = self.delete_locked(shard_id, &mut *inner, index, key)?;
let ev = match &old {
Some(o) => self.defer_write_event(key, Some(*o), None),
None => None,
};
*ev_out = ev;
Ok(old)
})
}
pub fn cas(&self, key: &K, expected: &[u8; V], new_value: &[u8; V]) -> DbResult<()> {
metrics::counter!("armdb.ops", "op" => "cas", "tree" => "const_map").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.cas");
let expires_at = self.deadline_for(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 (inner, index) = locked;
let entry = index.get(key).ok_or(DbError::KeyNotFound)?;
if entry.value != *expected {
return Err(DbError::CasMismatch);
}
self.claim_guard_bytes(key, Some(&entry.value[..]), Some(&new_value[..]))?;
let old_loc = entry.loc;
let new_loc = inner.write_update(shard_id as u8, old_loc, key.as_bytes(), new_value)?;
index.upsert(
*key,
MapEntry {
loc: new_loc,
value: *new_value,
},
);
self.record_ttl(shard_id, key, expires_at);
*ev_out = self.defer_borrowed_event(key, Some(*expected), Some(new_value));
Ok(())
})
}
pub fn compare_delete(&self, key: &K, expected: &[u8; V]) -> DbResult<()> {
metrics::counter!("armdb.ops", "op" => "compare_delete", "tree" => "const_map")
.increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.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 (inner, index) = locked;
let entry = index.get(key).ok_or(DbError::KeyNotFound)?;
if entry.value != *expected {
return Err(DbError::CasMismatch);
}
let old_value = entry.value;
let old_loc = entry.loc;
self.claim_guard_bytes(key, Some(&old_value[..]), None)?;
inner.write_tombstone(shard_id as u8, old_loc, key.as_bytes())?;
index.remove(key);
self.record_ttl(shard_id, key, None);
*ev_out = self.defer_write_event(key, Some(old_value), None);
Ok(())
})
}
pub fn update(
&self,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
) -> DbResult<Option<[u8; V]>> {
self.update_inner(key, f, false)
}
pub fn fetch_update(
&self,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
) -> DbResult<Option<[u8; V]>> {
self.update_inner(key, f, true)
}
fn update_inner(
&self,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
return_old: bool,
) -> DbResult<Option<[u8; V]>> {
metrics::counter!("armdb.ops", "op" => "update", "tree" => "const_map").increment(1);
#[cfg(feature = "hot-path-tracing")]
tracing::trace!("const_map.update");
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, ev_out| {
let (inner, index) = locked;
let (old_value, new_value) =
match self.update_locked(shard_id, &mut *inner, index, key, f)? {
Some(pair) => pair,
None => return Ok(None),
};
let result = Some(if return_old { old_value } else { new_value });
*ev_out = self.defer_write_event(key, Some(old_value), Some(new_value));
Ok(result)
})
}
pub fn contains(&self, key: &K) -> bool {
let index = sync::lock(&self.indexes[self.shard_for(key)]);
index.contains_key(key)
}
pub fn len(&self) -> usize {
self.indexes.iter().map(|m| sync::lock(m).len()).sum()
}
pub fn is_empty(&self) -> bool {
self.indexes.iter().all(|m| sync::lock(m).is_empty())
}
fn put_no_hook(&self, key: &K, value: &[u8; V]) -> DbResult<Option<[u8; V]>> {
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, _ev_out| {
let (inner, index) = locked;
let old = self.put_locked(shard_id, &mut *inner, index, key, value)?;
Ok(old)
})
}
fn delete_no_hook(&self, key: &K) -> DbResult<Option<[u8; V]>> {
let shard_id = self.shard_for(key);
let mut ctx = Owned;
Ctx::with_shard(&mut ctx, self, shard_id, |locked, _ev_out| {
let (inner, index) = locked;
let old = self.delete_locked(shard_id, &mut *inner, index, key)?;
Ok(old)
})
}
pub fn atomic<R>(
&self,
shard_key: &K,
f: impl FnOnce(&mut ConstMapShard<'_, K, V, H, D>) -> DbResult<R>,
) -> DbResult<R> {
let _atomic_depth = crate::AtomicDepthGuard::try_enter()?;
let shard_id = self.shard_for(shard_key);
let mut shard = self.tx_shard(shard_id);
let result = f(&mut shard);
let needs_sync = Self::should_sync(&shard.held.locked);
let (events, wakes) = Self::tx_release_shard(shard);
let result =
crate::projection::finish_atomic_teardown(self, shard_id, result, needs_sync, &events);
wakes.drain();
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()
}
pub(crate) fn shard_count(&self) -> usize {
self.durability.shard_count()
}
pub(crate) fn name(&self) -> &'static str {
""
}
pub(crate) fn typ_hash(&self) -> u64 {
0
}
#[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(
"ConstMap::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) -> ConstMapShard<'_, K, V, H, D> {
ConstMapShard {
tree: self,
held: Held {
locked: self.lock_shard(sid),
sid,
events: Vec::new(),
watch_wakes: crate::watch::WatchWakeBatch::default(),
publish_watches: true,
},
}
}
pub(crate) fn tx_release_shard(
shard: ConstMapShard<'_, K, V, H, D>,
) -> (Vec<ConstMapOwnedEvent<K, V>>, crate::watch::WatchWakeBatch) {
let ConstMapShard { held, .. } = shard;
let Held {
locked,
events,
watch_wakes,
..
} = held;
locked.release();
(events, watch_wakes)
}
pub fn upsert<U: crate::IntoUpsert<[u8; V]>>(
&self,
key: &K,
f: impl FnOnce(Option<&[u8; V]>) -> U,
) -> DbResult<U::Out> {
self.atomic(key, |s| crate::upsert::upsert_via_held(s, key, f))
}
fn put_locked(
&self,
shard_id: usize,
inner: &mut D::Inner,
index: &mut ShardIndex<K, MapEntry<V, D::Loc>>,
key: &K,
value: &[u8; V],
) -> DbResult<Option<[u8; V]>> {
let expires_at = self.deadline_for(key, value);
if let Some(existing) = index.get(key) {
let old_value = existing.value;
self.claim_guard_bytes(key, Some(&old_value[..]), Some(&value[..]))?;
let old_loc = existing.loc;
let new_loc = inner.write_update(shard_id as u8, old_loc, key.as_bytes(), value)?;
index.upsert(
*key,
MapEntry {
loc: new_loc,
value: *value,
},
);
self.record_ttl(shard_id, key, expires_at);
return Ok(Some(old_value));
}
self.claim_guard_bytes(key, None, Some(&value[..]))?;
let loc = inner.write_new(shard_id as u8, key.as_bytes(), value)?;
index.upsert(*key, MapEntry { loc, value: *value });
self.record_ttl(shard_id, key, expires_at);
Ok(None)
}
fn update_locked(
&self,
shard_id: usize,
inner: &mut D::Inner,
index: &mut ShardIndex<K, MapEntry<V, D::Loc>>,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
) -> DbResult<Option<([u8; V], [u8; V])>> {
let entry = match index.get(key) {
Some(e) => e,
None => return Ok(None),
};
let old_value = entry.value;
let old_loc = entry.loc;
let new_value = f(&old_value);
self.claim_guard_bytes(key, Some(&old_value[..]), Some(&new_value[..]))?;
let expires_at = self.deadline_for(key, &new_value);
let new_loc = inner.write_update(shard_id as u8, old_loc, key.as_bytes(), &new_value)?;
index.upsert(
*key,
MapEntry {
loc: new_loc,
value: new_value,
},
);
self.record_ttl(shard_id, key, expires_at);
Ok(Some((old_value, new_value)))
}
fn insert_locked(
&self,
shard_id: usize,
inner: &mut D::Inner,
index: &mut ShardIndex<K, MapEntry<V, D::Loc>>,
key: &K,
value: &[u8; V],
) -> DbResult<()> {
if index.contains_key(key) {
return Err(DbError::KeyExists);
}
self.claim_guard_bytes(key, None, Some(&value[..]))?;
let expires_at = self.deadline_for(key, value);
let loc = inner.write_new(shard_id as u8, key.as_bytes(), value)?;
index.upsert(*key, MapEntry { loc, value: *value });
self.record_ttl(shard_id, key, expires_at);
Ok(())
}
fn delete_locked(
&self,
shard_id: usize,
inner: &mut D::Inner,
index: &mut ShardIndex<K, MapEntry<V, D::Loc>>,
key: &K,
) -> DbResult<Option<[u8; V]>> {
let old = match index.get(key) {
Some(old) => old,
None => return Ok(None),
};
self.claim_guard_bytes(key, Some(&old.value[..]), None)?;
inner.write_tombstone(shard_id as u8, old.loc, key.as_bytes())?;
let value = old.value;
index.remove(key);
self.record_ttl(shard_id, key, None);
Ok(Some(value))
}
pub fn shard_for(&self, key: &K) -> usize {
if self.shard_prefix_bits == 0 || self.shard_prefix_bits >= size_of::<K>() * 8 {
let hash = xxhash_rust::xxh3::xxh3_64(key.as_bytes());
return (hash as usize) % self.durability.shard_count();
}
let full_bytes = self.shard_prefix_bits / 8;
let extra_bits = self.shard_prefix_bits % 8;
let hash = if extra_bits == 0 {
xxhash_rust::xxh3::xxh3_64(&key.as_bytes()[..full_bytes])
} else {
let mut buf = K::zeroed();
buf.as_bytes_mut()[..full_bytes].copy_from_slice(&key.as_bytes()[..full_bytes]);
let mask = !((1u8 << (8 - extra_bits)) - 1);
buf.as_bytes_mut()[full_bytes] = key.as_bytes()[full_bytes] & mask;
xxhash_rust::xxh3::xxh3_64(&buf.as_bytes()[..full_bytes + 1])
};
(hash as usize) % self.durability.shard_count()
}
pub fn flush(&self) -> DbResult<()> {
self.durability.flush()
}
pub fn iter_view(&self) -> Option<ConstMapIterView<'_, K, V, H, D>> {
self.iterable.then(|| ConstMapIterView {
inner: MapIterView::new(self, dir_from(self.reversed())),
})
}
pub fn scope<const N: usize>(
&self,
key: &K,
) -> Option<crate::scope::Scope<'_, crate::scope::MapScan<&Self>, N>> {
crate::scope::map_scope::<_, _, N>(self, key, dir_from(self.reversed()), self.iterable)
}
pub fn for_each(&self, mut f: impl FnMut(&K, &[u8; V])) {
for i in 0..self.durability.shard_count() {
let entries: Vec<(K, [u8; V])> = {
let index = sync::lock(&self.indexes[i]);
index.iter().map(|(k, e)| (*k, e.value)).collect()
};
for (key, value) in entries {
f(&key, &value);
}
}
}
pub(crate) fn for_each_while(
&self,
mut f: impl FnMut(&K, &[u8; V]) -> ControlFlow<()>,
) -> ControlFlow<()> {
for i in 0..self.durability.shard_count() {
let entries: Vec<(K, [u8; V])> = {
let index = sync::lock(&self.indexes[i]);
index.iter().map(|(k, e)| (*k, e.value)).collect()
};
for (key, value) in entries {
if let ControlFlow::Break(()) = f(&key, &value) {
return ControlFlow::Break(());
}
}
}
ControlFlow::Continue(())
}
pub fn migrate(
&self,
f: impl Fn(&K, &[u8; V]) -> crate::MigrateAction<[u8; V]>,
) -> DbResult<usize> {
self.migrate_inner(f, true)
}
pub(crate) fn migrate_inner(
&self,
f: impl Fn(&K, &[u8; V]) -> crate::MigrateAction<[u8; V]>,
fire_init: bool,
) -> DbResult<usize> {
use crate::MigrateAction;
let mut count = 0;
for i in 0..self.durability.shard_count() {
let entries: Vec<(K, [u8; V])> = {
let index = sync::lock(&self.indexes[i]);
index.iter().map(|(k, e)| (*k, e.value)).collect()
};
for (key, value) in entries {
match f(&key, &value) {
MigrateAction::Keep => {
if fire_init && H::NEEDS_INIT {
self.hook.on_init(&key, &value[..]);
}
}
MigrateAction::Update(new_value) => {
self.put_no_hook(&key, &new_value)?;
if fire_init && H::NEEDS_INIT {
self.hook.on_init(&key, &new_value[..]);
}
count += 1;
}
MigrateAction::Delete => {
self.delete_no_hook(&key)?;
count += 1;
}
}
}
}
tracing::info!(mutations = count, "const_map migration complete");
Ok(count)
}
pub(crate) fn replay_init(&self) {
if !H::NEEDS_INIT {
return;
}
for shard in &self.indexes {
let entries: Vec<(K, [u8; V])> = {
let index = sync::lock(shard);
index.iter().map(|(k, e)| (*k, e.value)).collect()
};
for (key, value) in entries {
self.hook.on_init(&key, &value[..]);
}
}
}
pub(crate) fn effective_projection_limits(&self) -> (usize, usize) {
let (hook_rows, hook_bytes) = self.hook.projection_limits();
(
hook_rows.min(self.durability.max_projected_rows()),
hook_bytes.min(self.durability.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 mut err: Option<DbError> = None;
self.for_each(|k, value| {
if err.is_some() {
return;
}
let mut sink =
crate::projection::ProjectionSink::new(targets, max_rows, max_bytes);
match self
.hook
.project(k, None, Some(&value[..]), &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 mut err: Option<DbError> = None;
self.for_each(|k, value| {
if err.is_some() {
return;
}
if let Err(e) = visit(k, &value[..]) {
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()
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>, D: Durability> ShardSource
for ConstMap<K, V, H, D>
{
type Key = K;
type Val = [u8; V];
fn shard_count(&self) -> usize {
self.durability.shard_count()
}
fn next_batch(
&self,
shard: usize,
after: Option<&K>,
dir: Dir,
n: usize,
range_start: Bound<crate::map_index::OrdKey<K>>,
range_end: Bound<crate::map_index::OrdKey<K>>,
) -> Vec<(K, [u8; V])> {
let index = sync::lock(&self.indexes[shard]);
let Some(ki) = index.key_index() else {
return Vec::new();
};
let keys = key_index_batch(ki, after, dir, n, range_start, range_end);
let mut out = Vec::with_capacity(keys.len());
for k in keys {
if let Some(e) = index.get(&k) {
out.push((k, e.value));
}
}
out
}
fn next_keys_batch(
&self,
shard: usize,
after: Option<&K>,
dir: Dir,
n: usize,
range_start: Bound<crate::map_index::OrdKey<K>>,
range_end: Bound<crate::map_index::OrdKey<K>>,
) -> Vec<K> {
let index = sync::lock(&self.indexes[shard]);
let Some(ki) = index.key_index() else {
return Vec::new();
};
key_index_batch(ki, after, dir, n, range_start, range_end)
}
}
pub struct ConstMapIterView<'a, K, const V: usize, H = NoHook, D = Bitcask>
where
K: Key + Send + Sync + Hash + Eq,
H: WriteHook<K>,
D: Durability,
{
inner: MapIterView<'a, ConstMap<K, V, H, D>>,
}
impl<K, const V: usize, H, D> ConstMapIterView<'_, K, V, H, D>
where
K: Key + Send + Sync + Hash + Eq,
H: WriteHook<K>,
D: Durability,
{
pub fn iter(&self) -> impl Iterator<Item = (K, [u8; V])> + '_ {
self.inner.iter()
}
pub fn range(&self, start: &K, end: &K) -> impl Iterator<Item = (K, [u8; V])> + '_ {
self.inner.range(start, end)
}
pub fn range_bounds(
&self,
start: Bound<&K>,
end: Bound<&K>,
) -> impl Iterator<Item = (K, [u8; V])> + '_ {
self.inner.range_bounds(start, end)
}
pub fn prefix_iter(&self, prefix: &[u8]) -> impl Iterator<Item = (K, [u8; V])> + '_ {
self.inner.prefix_iter(prefix)
}
pub fn paginate(&self, after: Option<&K>, limit: usize) -> Page<K, [u8; V]> {
self.inner.paginate(after, limit)
}
pub fn keys(&self) -> impl Iterator<Item = K> + '_ {
self.inner.keys()
}
pub fn keys_range(&self, start: &K, end: &K) -> impl Iterator<Item = K> + '_ {
self.inner.keys_range(start, end)
}
pub fn keys_range_bounds(
&self,
start: Bound<&K>,
end: Bound<&K>,
) -> impl Iterator<Item = K> + '_ {
self.inner.keys_range_bounds(start, end)
}
pub fn keys_prefix(&self, prefix: &[u8]) -> impl Iterator<Item = K> + '_ {
self.inner.keys_prefix(prefix)
}
pub fn keys_paginate(&self, after: Option<&K>, limit: usize) -> KeyPage<K> {
self.inner.keys_paginate(after, limit)
}
pub fn retain(&self, mut f: impl FnMut(&K, &[u8; V]) -> bool) -> DbResult<()> {
for (key, value) in self.inner.iter() {
if f(&key, &value) {
continue;
}
match self.inner.src().compare_delete(&key, &value) {
Ok(()) => {}
Err(DbError::CasMismatch) | Err(DbError::KeyNotFound) => {
tracing::debug!("retain: compare_delete skipped (concurrent change)");
}
Err(e) => return Err(e),
}
}
Ok(())
}
}
#[cfg(feature = "replication")]
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>>
crate::replication::ReplicationTarget for ConstMap<K, V, H, Bitcask>
{
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::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 old = sync::lock(&self.indexes[self.shard_for(&key)]).remove(&key);
match old {
Some(old_entry) => Ok(ApplyOutcome::TombstoneRemoved(old_entry.loc)),
None => Ok(ApplyOutcome::Inserted), }
} else {
let value: [u8; V] = value.try_into().map_err(|_| DbError::CorruptedEntry {
offset: entry_offset,
})?;
let old = sync::lock(&self.indexes[self.shard_for(&key)])
.upsert(key, MapEntry { loc: disk, value });
match old {
Some(old_entry) => Ok(ApplyOutcome::Replaced(old_entry.loc)),
None => Ok(ApplyOutcome::Inserted),
}
}
}
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;
let value_len = header.value_len as usize;
if raw_after_header.len() < size_of::<K>() + value_len {
return Ok(ApplyOutcome::NotMatched);
}
let key = &raw_after_header[..size_of::<K>()];
let value = &raw_after_header[size_of::<K>()..size_of::<K>() + value_len];
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) {
ConstMap::mark_replication_follower(self);
}
}
pub struct ConstMapShard<
'a,
K: Key + Send + Sync + Hash + Eq,
const V: usize,
H: WriteHook<K> = NoHook,
D: Durability = Bitcask,
> {
tree: &'a ConstMap<K, V, H, D>,
pub(crate) held: Held<'a, ConstMap<K, V, H, D>>,
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>, D: Durability>
ConstMapShard<'_, K, V, H, D>
{
pub fn put(&mut self, key: &K, value: &[u8; V]) -> DbResult<Option<[u8; V]>> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
let (inner, index) = locked;
let old = tree.put_locked(sid, &mut *inner, index, key, value)?;
*ev_out = tree.defer_write_event(key, old, Some(*value));
Ok(old)
})
}
pub fn insert(&mut self, key: &K, value: &[u8; V]) -> DbResult<()> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
let (inner, index) = locked;
tree.insert_locked(sid, &mut *inner, index, key, value)?;
*ev_out = tree.defer_write_event(key, None, Some(*value));
Ok(())
})
}
pub fn delete(&mut self, key: &K) -> DbResult<Option<[u8; V]>> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
let (inner, index) = locked;
let old = tree.delete_locked(sid, &mut *inner, index, key)?;
let ev = match &old {
Some(o) => tree.defer_write_event(key, Some(*o), None),
None => None,
};
*ev_out = ev;
Ok(old)
})
}
pub fn update(
&mut self,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
) -> DbResult<Option<[u8; V]>> {
self.update_inner(key, f, false)
}
pub fn fetch_update(
&mut self,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
) -> DbResult<Option<[u8; V]>> {
self.update_inner(key, f, true)
}
pub fn upsert<U: crate::IntoUpsert<[u8; V]>>(
&mut self,
key: &K,
f: impl FnOnce(Option<&[u8; V]>) -> U,
) -> DbResult<U::Out> {
crate::upsert::upsert_via_held(self, key, f)
}
fn update_inner(
&mut self,
key: &K,
f: impl FnOnce(&[u8; V]) -> [u8; V],
return_old: bool,
) -> DbResult<Option<[u8; V]>> {
self.check_shard(key)?;
let tree = self.tree;
let sid = self.held.sid;
Ctx::with_shard(&mut self.held, tree, sid, |locked, ev_out| {
let (inner, index) = locked;
let (old_value, new_value) =
match tree.update_locked(sid, &mut *inner, index, key, f)? {
Some(pair) => pair,
None => return Ok(None),
};
let result = Some(if return_old { old_value } else { new_value });
*ev_out = tree.defer_write_event(key, Some(old_value), Some(new_value));
Ok(result)
})
}
pub fn try_get(&self, key: &K) -> DbResult<Option<[u8; V]>> {
self.check_shard(key)?;
Ok(self.held.locked.1.get(key).map(|e| e.value))
}
pub fn get_or_err(&self, key: &K) -> DbResult<[u8; V]> {
self.try_get(key)?.ok_or(DbError::KeyNotFound)
}
pub fn try_contains(&self, key: &K) -> DbResult<bool> {
self.check_shard(key)?;
Ok(self.held.locked.1.contains_key(key))
}
fn check_shard(&self, key: &K) -> DbResult<()> {
if self.tree.shard_for(key) != self.held.sid {
return Err(DbError::ShardMismatch);
}
Ok(())
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>>
ConstMapShard<'_, K, V, H, Bitcask>
{
pub(crate) fn tx_arm_gsn(&mut self, gsn: u64) {
self.held.locked.0.arm_gsn(gsn);
}
pub(crate) fn tx_clear_armed_gsn(&mut self) {
self.held.locked.0.clear_armed_gsn();
}
pub(crate) fn tx_fence(&mut self, gsn: u64) -> u64 {
self.held.locked.0.fence(gsn)
}
pub(crate) fn tx_buffered_events(&self) -> usize {
self.held.events.len()
}
}
impl<K: Key + Send + Sync + Hash + Eq, const V: usize, H: WriteHook<K>>
ConstMapShard<'_, K, V, H, Fixed>
{
pub(crate) fn tx_buffered_events(&self) -> usize {
self.held.events.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use crate::hook::WriteHook;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering, Ordering as AtomicOrdering};
use std::sync::mpsc;
use std::time::Duration;
use tempfile::tempdir;
#[derive(Default)]
struct RecHook {
writes: AtomicUsize,
#[allow(clippy::type_complexity)]
seq: crate::sync::Mutex<Vec<(u64, Option<Vec<u8>>, Option<Vec<u8>>)>>,
}
impl WriteHook<[u8; 8]> for RecHook {
const NEEDS_OLD_VALUE: bool = true;
fn on_write(&self, key: &[u8; 8], old: Option<&[u8]>, new: Option<&[u8]>) {
self.writes.fetch_add(1, AtomicOrdering::Relaxed);
crate::sync::lock(&self.seq).push((
u64::from_be_bytes(*key),
old.map(<[u8]>::to_vec),
new.map(<[u8]>::to_vec),
));
}
}
fn open_map_hooked(dir: &std::path::Path, hook: RecHook) -> ConstMap<[u8; 8], 4, RecHook> {
let mut cfg = Config::test();
cfg.shard_count = 1;
ConstMap::open_hooked(dir, cfg, hook).expect("open hooked")
}
#[test]
fn iter_view_none_unless_iterable() {
let dir = tempfile::tempdir().unwrap();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), Config::test()).unwrap();
assert!(m.iter_view().is_none());
}
#[test]
fn non_iterable_map_scope_is_none() {
let dir = tempfile::tempdir().unwrap();
let m = ConstMap::<[u8; 8], 1>::open(dir.path(), Config::test()).unwrap();
assert!(m.scope::<0>(&[0u8; 8]).is_none());
}
#[test]
fn iter_desc_by_default_across_shards() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(4)
.hints(true)
.iterable(true)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for k in [[1u8, 0], [3, 0], [2, 0], [5, 0], [4, 0]] {
m.insert(&k, &[7]).unwrap();
}
let got: Vec<[u8; 2]> = m.iter_view().unwrap().iter().map(|(k, _)| k).collect();
assert_eq!(got, [[5, 0], [4, 0], [3, 0], [2, 0], [1, 0]]);
}
#[test]
fn paginate_covers_keyset_once_desc() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(3)
.hints(true)
.iterable(true)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for i in 1u8..=10 {
m.insert(&[i, 0], &[i]).unwrap();
}
let view = m.iter_view().unwrap();
let mut seen = Vec::new();
let mut after: Option<[u8; 2]> = None;
loop {
let page = view.paginate(after.as_ref(), 3);
if page.items.is_empty() {
break;
}
seen.extend(page.items.iter().map(|(k, _)| *k));
after = page.next;
if after.is_none() {
break;
}
}
let mut sorted = seen.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), 10, "every key exactly once");
assert_eq!(seen.first().unwrap(), &[10, 0]);
}
#[test]
fn range_and_prefix_bounds() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.reversed(false)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for i in 1u8..=5 {
m.insert(&[i, 0], &[i]).unwrap();
}
let v = m.iter_view().unwrap();
let r: Vec<_> = v.range(&[2, 0], &[4, 0]).map(|(k, _)| k).collect();
assert_eq!(r, [[2, 0], [3, 0]]);
let p: Vec<[u8; 2]> = v.prefix_iter(&[3]).map(|(k, _)| k).collect();
assert_eq!(p, [[3, 0]]);
}
#[test]
fn degenerate_range_is_empty_not_panic() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.reversed(false)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for i in 1u8..=5 {
m.insert(&[i, 0], &[i]).unwrap();
}
let v = m.iter_view().unwrap();
assert_eq!(
v.range_bounds(Bound::Excluded(&[3, 0]), Bound::Excluded(&[3, 0]))
.count(),
0
);
assert_eq!(
v.keys_range_bounds(Bound::Included(&[4, 0]), Bound::Included(&[2, 0]))
.count(),
0
);
}
#[test]
fn keys_only_matches_iter_keys_and_is_ordered() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(3)
.hints(true)
.iterable(true)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for i in 1u8..=6 {
m.insert(&[i, 0], &[i]).unwrap();
}
let v = m.iter_view().unwrap();
let from_keys: Vec<[u8; 2]> = v.keys().collect();
let from_iter: Vec<[u8; 2]> = v.iter().map(|(k, _)| k).collect();
assert_eq!(from_keys, from_iter, "keys() order must match iter() keys");
assert_eq!(
from_keys,
[[6, 0], [5, 0], [4, 0], [3, 0], [2, 0], [1, 0]],
"DESC by default"
);
}
#[test]
fn keys_range_and_prefix() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.reversed(false)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for i in 1u8..=5 {
m.insert(&[i, 0], &[i]).unwrap();
}
let v = m.iter_view().unwrap();
let r: Vec<[u8; 2]> = v.keys_range(&[2, 0], &[4, 0]).collect();
assert_eq!(r, [[2, 0], [3, 0]]);
let p: Vec<[u8; 2]> = v.keys_prefix(&[3]).collect();
assert_eq!(p, [[3, 0]]);
}
#[test]
fn keys_paginate_covers_keyset_once_desc() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(3)
.hints(true)
.iterable(true)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
for i in 1u8..=10 {
m.insert(&[i, 0], &[i]).unwrap();
}
let v = m.iter_view().unwrap();
let mut seen = Vec::new();
let mut after: Option<[u8; 2]> = None;
loop {
let page = v.keys_paginate(after.as_ref(), 3);
if page.keys.is_empty() {
break;
}
seen.extend(page.keys.iter().copied());
after = page.next;
if after.is_none() {
break;
}
}
let mut sorted = seen.clone();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), 10, "every key exactly once");
assert_eq!(
seen.first().unwrap(),
&[10, 0],
"DESC starts at the max key"
);
}
#[cfg(feature = "replication")]
#[test]
fn replication_apply_maintains_iterable_companion_fixed() {
let dir = tempfile::tempdir().unwrap();
let mut cfg = crate::FixedConfig::test();
cfg.iterable = true;
let m = crate::FixedMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
m.upsert_replicated(&[1, 0], [10], 0);
m.upsert_replicated(&[2, 0], [20], 1);
m.upsert_replicated(&[3, 0], [30], 2);
let keys: Vec<[u8; 2]> = m.iter_view().unwrap().keys().collect();
assert_eq!(
keys,
[[3, 0], [2, 0], [1, 0]],
"all replicated keys present (Fixed is DESC)"
);
let slot = m.get_slot_id(&[2, 0]).unwrap();
assert!(m.remove_key_if_slot_matches(&[2, 0], slot));
let keys: Vec<[u8; 2]> = m.iter_view().unwrap().keys().collect();
assert_eq!(
keys,
[[3, 0], [1, 0]],
"replicated tombstone removed from companion"
);
}
#[test]
fn iter_reflects_delete() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
m.insert(&[1, 0], &[1]).unwrap();
m.insert(&[2, 0], &[2]).unwrap();
m.delete(&[1, 0]).unwrap();
let got: Vec<[u8; 2]> = m.iter_view().unwrap().iter().map(|(k, _)| k).collect();
assert_eq!(got, [[2, 0]]);
}
#[test]
fn const_map_atomic_fires_hooks_in_order() {
let dir = tempfile::tempdir().unwrap();
let map = open_map_hooked(dir.path(), RecHook::default());
let k = 7u64.to_be_bytes();
map.atomic(&k, |s| {
s.put(&k, &[1, 1, 1, 1])?;
s.put(&k, &[2, 2, 2, 2])?;
s.delete(&k)?;
Ok(())
})
.expect("atomic");
assert_eq!(map.hook.writes.load(AtomicOrdering::Relaxed), 3);
let seq = crate::sync::lock(&map.hook.seq).clone();
assert_eq!(seq[0], (7, None, Some(vec![1, 1, 1, 1])));
assert_eq!(seq[1], (7, Some(vec![1, 1, 1, 1]), Some(vec![2, 2, 2, 2])));
assert_eq!(seq[2], (7, Some(vec![2, 2, 2, 2]), None));
}
#[test]
fn const_map_atomic_fires_for_applied_on_err() {
let dir = tempfile::tempdir().unwrap();
let map = open_map_hooked(dir.path(), RecHook::default());
let k = 1u64.to_be_bytes();
let r: DbResult<()> = map.atomic(&k, |s| {
s.put(&k, &[9, 9, 9, 9])?;
Err(DbError::KeyNotFound)
});
assert!(r.is_err());
assert_eq!(map.hook.writes.load(AtomicOrdering::Relaxed), 1);
}
fn fixed_sync_fail_cfg() -> crate::FixedConfig {
crate::FixedConfig {
shard_count: 1,
grow_step: 64,
sync_batch_size: 1,
..crate::FixedConfig::test()
}
}
#[test]
fn fetch_update_hook_fires_even_when_sync_fails() {
use crate::durability::Fixed;
use crate::fixed::shard::FAIL_NEXT_SYNC;
let dir = tempdir().unwrap();
let map = ConstMap::<[u8; 8], 8, RecHook, Fixed>::open_with_hook(
dir.path(),
fixed_sync_fail_cfg(),
RecHook::default(),
)
.unwrap();
map.put(&1u64.to_be_bytes(), &1u64.to_be_bytes()).unwrap();
let baseline = map.hook.writes.load(AtomicOrdering::Relaxed);
assert_eq!(baseline, 1);
FAIL_NEXT_SYNC.store(true, Ordering::SeqCst);
let res = map.fetch_update(&1u64.to_be_bytes(), |_| 9u64.to_be_bytes());
assert!(res.is_err(), "injected sync failure must propagate");
assert_eq!(
map.hook.writes.load(AtomicOrdering::Relaxed),
baseline + 1,
"fetch_update on_write must fire despite sync error"
);
}
#[test]
fn const_map_atomic_fires_hooks_fixedstore_sync_seam() {
let dir = tempdir().unwrap();
let fixed_cfg = crate::FixedConfig {
shard_count: 1,
grow_step: 64,
sync_batch_size: 1,
..crate::FixedConfig::test()
};
let map = crate::FixedMap::<[u8; 8], 4, RecHook>::open_with_hook(
dir.path(),
fixed_cfg,
RecHook::default(),
)
.expect("open fixed hooked");
let k = 7u64.to_be_bytes();
map.atomic(&k, |s| {
s.put(&k, &[1, 1, 1, 1])?;
s.put(&k, &[2, 2, 2, 2])?;
s.delete(&k)?;
Ok(())
})
.expect("atomic");
assert_eq!(map.hook.writes.load(AtomicOrdering::Relaxed), 3);
let seq = crate::sync::lock(&map.hook.seq).clone();
assert_eq!(seq[0], (7, None, Some(vec![1, 1, 1, 1])));
assert_eq!(seq[1], (7, Some(vec![1, 1, 1, 1]), Some(vec![2, 2, 2, 2])));
assert_eq!(seq[2], (7, Some(vec![2, 2, 2, 2]), None));
}
#[allow(clippy::type_complexity)]
struct ReplayInitReenterHook<const V: usize> {
target: sync::Mutex<Option<std::sync::Arc<ConstMap<[u8; 8], V, Self>>>>,
entered: AtomicBool,
}
impl<const V: usize> WriteHook<[u8; 8]> for ReplayInitReenterHook<V> {
const NEEDS_OLD_VALUE: bool = false;
const NEEDS_INIT: bool = true;
const NEEDS_WRITE: bool = false;
fn on_write(&self, _key: &[u8; 8], _old: Option<&[u8]>, _new: Option<&[u8]>) {}
fn on_init(&self, key: &[u8; 8], _value: &[u8]) {
self.entered.store(true, Ordering::Release);
let map = sync::lock(&self.target)
.as_ref()
.expect("hook target must be installed")
.clone();
let _ = map.get(key);
}
}
#[test]
fn hook_lifecycle_replay_init_reentry_const_map() {
let dir = tempdir().unwrap();
let hook = ReplayInitReenterHook::<8> {
target: sync::Mutex::new(None),
entered: AtomicBool::new(false),
};
let map = std::sync::Arc::new(
ConstMap::<[u8; 8], 8, ReplayInitReenterHook<8>>::open_hooked(
dir.path(),
Config::test(),
hook,
)
.unwrap(),
);
*sync::lock(&map.hook.target) = Some(std::sync::Arc::clone(&map));
let key = 1u64.to_be_bytes();
map.put(&key, &[0u8; 8]).unwrap();
let (tx, rx) = mpsc::sync_channel(1);
let map2 = std::sync::Arc::clone(&map);
std::thread::spawn(move || {
map2.replay_init();
let _ = tx.send(());
});
rx.recv_timeout(Duration::from_secs(2))
.expect("replay_init deadlocked — on_init called with index lock held");
assert!(map.hook.entered.load(Ordering::Acquire));
}
#[test]
fn retain_deletes_and_persists() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.build();
{
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg.clone()).unwrap();
for i in 1u8..=6 {
m.insert(&[i, 0], &[i]).unwrap();
}
m.iter_view()
.unwrap()
.retain(|_k, v| v[0] % 2 == 0)
.unwrap();
let got: Vec<_> = m.iter_view().unwrap().iter().map(|(k, _)| k[0]).collect();
let mut g = got.clone();
g.sort();
assert_eq!(g, [2, 4, 6]);
m.close().unwrap();
}
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
let got: Vec<_> = m.iter_view().unwrap().iter().map(|(k, _)| k[0]).collect();
let mut g = got.clone();
g.sort();
assert_eq!(g, [2, 4, 6]);
}
#[test]
fn retain_compare_delete_skips_changed_value() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.build();
let m = std::sync::Arc::new(ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap());
m.insert(&[1, 0], &[1]).unwrap();
let m2 = std::sync::Arc::clone(&m);
m.iter_view()
.unwrap()
.retain(|k, _v| {
if k == &[1, 0] {
m2.put(k, &[9]).unwrap();
return false;
}
true
})
.unwrap();
assert_eq!(m.get(&[1, 0]), Some([9]));
}
#[test]
fn const_map_shard_update_and_fetch_update() {
let dir = tempdir().unwrap();
let mut cfg = Config::test();
cfg.shard_count = 1;
let map = ConstMap::<[u8; 8], 4>::open(dir.path(), cfg).unwrap();
let key = 1u64.to_be_bytes();
map.put(&key, &[10u8; 4]).unwrap();
let (upd, fetched, missing) = map
.atomic(&key, |shard| {
let upd = shard.update(&key, |old| {
let mut v = *old;
v[0] = v[0].wrapping_add(1);
v
})?;
let fetched = shard.fetch_update(&key, |old| {
let mut v = *old;
v[0] = v[0].wrapping_add(1);
v
})?;
let missing = shard.update(&2u64.to_be_bytes(), |_| [0u8; 4])?;
Ok((upd, fetched, missing))
})
.expect("atomic");
assert_eq!(upd, Some([11, 10, 10, 10]));
assert_eq!(fetched, Some([11, 10, 10, 10]));
assert_eq!(missing, None);
assert_eq!(map.get(&key), Some([12, 10, 10, 10]));
}
#[test]
fn const_map_atomic_update_fires_hook_with_old_new() {
let dir = tempdir().unwrap();
let map = open_map_hooked(dir.path(), RecHook::default());
let key = 1u64.to_be_bytes();
map.put(&key, &[10u8; 4]).unwrap();
assert_eq!(map.hook.writes.load(AtomicOrdering::Relaxed), 1);
map.atomic(&key, |shard| {
shard.update(&key, |old| {
let mut v = *old;
v[0] = v[0].wrapping_add(5);
v
})
})
.unwrap();
assert_eq!(map.hook.writes.load(AtomicOrdering::Relaxed), 2);
assert_eq!(map.get(&key), Some([15, 10, 10, 10]));
}
#[test]
fn compare_delete_updates_companion() {
let dir = tempfile::tempdir().unwrap();
let cfg = Config::balanced()
.shard_count(2)
.hints(true)
.iterable(true)
.build();
let m = ConstMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
m.insert(&[1, 0], &[9]).unwrap();
m.compare_delete(&[1, 0], &[9]).unwrap();
assert_eq!(m.iter_view().unwrap().iter().count(), 0);
}
#[test]
fn fixed_map_iteration_direction() {
let dir = tempdir().unwrap();
let cfg = crate::FixedConfig {
iterable: true,
..crate::FixedConfig::test()
};
let m = crate::FixedMap::<[u8; 8], 8>::open(dir.path(), cfg).unwrap();
for i in 1u64..=3 {
m.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
let desc: Vec<u64> = m
.iter_view()
.unwrap()
.keys()
.map(u64::from_be_bytes)
.collect();
assert_eq!(desc, [3, 2, 1], "FixedMap default DESC");
let dir2 = tempdir().unwrap();
let cfg2 = crate::FixedConfig {
iterable: true,
reversed: false,
..crate::FixedConfig::test()
};
let m2 = crate::FixedMap::<[u8; 8], 8>::open(dir2.path(), cfg2).unwrap();
for i in 1u64..=3 {
m2.put(&i.to_be_bytes(), &i.to_be_bytes()).unwrap();
}
let asc: Vec<u64> = m2
.iter_view()
.unwrap()
.keys()
.map(u64::from_be_bytes)
.collect();
assert_eq!(asc, [1, 2, 3], "FixedMap reversed=false ASC");
}
#[test]
fn compare_delete_fixed_store_match_mismatch_absent() {
let dir = tempdir().unwrap();
let map =
crate::FixedMap::<[u8; 8], 8>::open(dir.path(), crate::FixedConfig::test()).unwrap();
let key = 1u64.to_be_bytes();
let val = 42u64.to_be_bytes();
let other = 99u64.to_be_bytes();
map.put(&key, &val).unwrap();
assert!(matches!(
map.compare_delete(&key, &other),
Err(DbError::CasMismatch)
));
assert_eq!(map.get(&key), Some(val));
assert!(map.compare_delete(&key, &val).is_ok());
assert_eq!(map.get(&key), None);
assert!(matches!(
map.compare_delete(&key, &val),
Err(DbError::KeyNotFound)
));
}
#[test]
fn compare_delete_bitcask_match_mismatch_absent() {
let dir = tempdir().unwrap();
let map = ConstMap::<[u8; 8], 8>::open(dir.path(), Config::test()).unwrap();
let key = 7u64.to_be_bytes();
let val = 7u64.to_be_bytes();
let other = 8u64.to_be_bytes();
map.put(&key, &val).unwrap();
assert!(matches!(
map.compare_delete(&key, &other),
Err(DbError::CasMismatch)
));
assert_eq!(map.get(&key), Some(val));
assert!(map.compare_delete(&key, &val).is_ok());
assert_eq!(map.get(&key), None);
assert!(matches!(
map.compare_delete(&key, &val),
Err(DbError::KeyNotFound)
));
}
#[test]
fn fixed_map_reopen_iterable_recovers_all() {
let dir = tempfile::tempdir().unwrap();
let mut cfg = crate::FixedConfig::test(); cfg.iterable = true;
{
let m = crate::FixedMap::<[u8; 2], 1>::open(dir.path(), cfg.clone()).unwrap();
m.insert(&[1, 0], &[10]).unwrap();
m.insert(&[2, 0], &[20]).unwrap();
m.insert(&[3, 0], &[30]).unwrap();
m.close().unwrap(); }
let m = crate::FixedMap::<[u8; 2], 1>::open(dir.path(), cfg).unwrap();
let keys: Vec<[u8; 2]> = m.iter_view().unwrap().keys().collect();
assert_eq!(keys, [[3, 0], [2, 0], [1, 0]], "DESC, all keys recovered");
assert_eq!(m.get(&[2, 0]).unwrap(), [20]);
}
#[test]
fn fixed_map_reopen_dirty_recovers_all() {
let dir = tempfile::tempdir().unwrap();
let cfg = crate::FixedConfig::test();
{
let m = crate::FixedMap::<[u8; 8], 1>::open(dir.path(), cfg.clone()).unwrap();
for i in 0..50u64 {
m.insert(&i.to_be_bytes(), &[(i & 0xff) as u8]).unwrap();
}
}
let m = crate::FixedMap::<[u8; 8], 1>::open(dir.path(), cfg).unwrap();
for i in 0..50u64 {
assert_eq!(
m.get(&i.to_be_bytes()).unwrap(),
[(i & 0xff) as u8],
"key {i}"
);
}
}
#[test]
fn fixed_map_recovery_dedups_duplicate_key() {
use crate::durability::Fixed;
let key = [7u8; 8];
let v1 = [0xAAu8; 8];
let v2 = [0xBBu8; 8];
let cfg = crate::FixedConfig {
shard_count: 1,
grow_step: 64,
..crate::FixedConfig::test()
};
let dir = tempfile::tempdir().unwrap();
{
let fixed = Fixed::open(dir.path(), cfg.clone(), 8, 8).unwrap();
let mut inner = fixed.lock_shard(0);
let a = inner.alloc_slot().unwrap();
inner.write_slot(a, &key, &v1).unwrap();
let b = inner.alloc_slot().unwrap();
inner.write_slot(b, &key, &v2).unwrap();
assert_ne!(a, b, "two distinct slots hold the same key");
inner.sync().unwrap();
}
let m = crate::FixedMap::<[u8; 8], 8>::open(dir.path(), cfg).unwrap();
assert_eq!(m.len(), 1, "exactly one index entry after dedup");
assert_eq!(m.get(&key), Some(v2), "last-scanned slot (higher id) wins");
let inner = m.durability.lock_shard(0);
assert_eq!(
inner.bitmap.occupied() as usize,
m.len(),
"bitmap.occupied() == len() (no leaked slot)"
);
assert_eq!(inner.bitmap.occupied(), 1);
assert_eq!(
crate::fixed::slot::status_of(inner.versions[0]),
crate::fixed::slot::STATUS_FREE,
"losing slot 0 freed in versions"
);
assert!(!inner.bitmap.is_set(0), "losing slot 0 cleared in bitmap");
assert_eq!(
crate::fixed::slot::status_of(inner.versions[1]),
crate::fixed::slot::STATUS_OCCUPIED,
"winning slot 1 kept OCCUPIED"
);
}
}