use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use crossbeam_epoch::{self as epoch, Atomic, Guard, Owned};
use crossbeam_utils::CachePadded;
use parking_lot::{Mutex, RwLock};
use crate::Result;
mod sync {
#[cfg(all(test, loom))]
pub(super) use loom::sync::atomic::{fence, AtomicU64, AtomicU8};
#[cfg(not(all(test, loom)))]
pub(super) use std::sync::atomic::{fence, AtomicU64, AtomicU8};
#[inline]
pub(super) fn spin() {
#[cfg(all(test, loom))]
loom::thread::yield_now();
#[cfg(not(all(test, loom)))]
std::hint::spin_loop();
}
}
use sync::{fence, AtomicU64, AtomicU8};
const SHARDS: usize = 64;
const SHARD_BITS: u32 = 6;
const SHARD_MASK: u64 = (SHARDS as u64) - 1;
const INITIAL_SHARD_CAPACITY: usize = 16;
const GROWTH_NUM: usize = 3;
const GROWTH_DENOM: usize = 4;
const STATE_EMPTY: u8 = 0;
const STATE_OCCUPIED: u8 = 1;
const STATE_TOMBSTONE: u8 = 2;
const STATE_OVERFLOW: u8 = 3;
pub(crate) type KeyHash = u64;
#[derive(Clone, Copy)]
pub(crate) struct KeyHasher {
k0: u64,
k1: u64,
k2: u64,
}
impl std::fmt::Debug for KeyHasher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KeyHasher").finish_non_exhaustive()
}
}
#[inline(always)]
fn folded_mul(a: u64, b: u64) -> u64 {
let product = u128::from(a).wrapping_mul(u128::from(b));
(product as u64) ^ ((product >> 64) as u64)
}
#[inline(always)]
fn read_u64_le(bytes: &[u8]) -> u64 {
let mut word = [0_u8; 8];
word.copy_from_slice(&bytes[..8]);
u64::from_le_bytes(word)
}
#[inline(always)]
fn read_u32_le(bytes: &[u8]) -> u64 {
let mut word = [0_u8; 4];
word.copy_from_slice(&bytes[..4]);
u64::from(u32::from_le_bytes(word))
}
impl KeyHasher {
const C0: u64 = 0x243f_6a88_85a3_08d3;
const C1: u64 = 0x5851_f42d_4c95_7f2d;
#[must_use]
pub(crate) fn random() -> Self {
use std::hash::BuildHasher;
let state = std::collections::hash_map::RandomState::new();
Self {
k0: state.hash_one(0x6b30_u64),
k1: state.hash_one(0x6b31_u64) | 1,
k2: state.hash_one(0x6b32_u64) | 1,
}
}
#[cfg(all(test, not(loom)))]
#[must_use]
pub(crate) const fn with_secrets(k0: u64, k1: u64, k2: u64) -> Self {
Self { k0, k1, k2 }
}
#[inline]
#[must_use]
pub(crate) fn hash(&self, key: &[u8]) -> KeyHash {
let len = key.len();
let mut state = self.k0 ^ (len as u64).wrapping_mul(Self::C0);
let mut bytes = key;
while bytes.len() > 16 {
let a = read_u64_le(bytes);
let b = read_u64_le(&bytes[8..]);
state = folded_mul(a ^ self.k1, b ^ self.k2 ^ state);
bytes = &bytes[16..];
}
let rest = bytes.len();
let (a, b) = if rest >= 8 {
(read_u64_le(bytes), read_u64_le(&bytes[rest - 8..]))
} else if rest >= 4 {
(read_u32_le(bytes), read_u32_le(&bytes[rest - 4..]))
} else if rest > 0 {
let lo = u64::from(bytes[0]);
let mid = u64::from(bytes[rest / 2]) << 8;
let hi = u64::from(bytes[rest - 1]) << 16;
(lo | mid | hi, 0)
} else {
(0, 0)
};
state = folded_mul(a ^ self.k1, b ^ self.k2 ^ state);
folded_mul(state ^ Self::C1, self.k1 ^ Self::C0)
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum KeyCheck {
Same,
Other(Vec<u8>),
Unreadable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SlotSnapshot {
state: u8,
hash: u64,
offset: u64,
}
#[repr(C)]
struct AtomicSlot {
seq: AtomicU64,
state: AtomicU8,
hash: AtomicU64,
offset: AtomicU64,
}
impl AtomicSlot {
fn empty() -> Self {
Self {
seq: AtomicU64::new(0),
state: AtomicU8::new(STATE_EMPTY),
hash: AtomicU64::new(0),
offset: AtomicU64::new(0),
}
}
#[inline]
fn read(&self) -> SlotSnapshot {
loop {
let s0 = self.seq.load(Ordering::Acquire);
if s0 & 1 == 1 {
sync::spin();
continue;
}
let state = self.state.load(Ordering::Relaxed);
let hash = self.hash.load(Ordering::Relaxed);
let offset = self.offset.load(Ordering::Relaxed);
fence(Ordering::Acquire);
let s1 = self.seq.load(Ordering::Relaxed);
if s0 == s1 {
return SlotSnapshot {
state,
hash,
offset,
};
}
sync::spin();
}
}
#[inline]
fn write(&self, state: u8, hash: u64, offset: u64) {
let seq = self.seq.load(Ordering::Relaxed);
debug_assert_eq!(seq & 1, 0, "seqlock writer re-entered");
self.seq.store(seq.wrapping_add(1), Ordering::Relaxed);
fence(Ordering::Release);
self.state.store(state, Ordering::Relaxed);
self.hash.store(hash, Ordering::Relaxed);
self.offset.store(offset, Ordering::Relaxed);
self.seq.store(seq.wrapping_add(2), Ordering::Release);
}
}
struct Table {
slots: Box<[AtomicSlot]>,
mask: usize,
}
impl Table {
fn with_capacity(capacity: usize) -> Self {
debug_assert!(capacity.is_power_of_two());
let slots: Box<[AtomicSlot]> = (0..capacity).map(|_| AtomicSlot::empty()).collect();
Self {
slots,
mask: capacity - 1,
}
}
#[inline]
fn capacity(&self) -> usize {
self.mask + 1
}
#[inline]
fn home(&self, hash: u64) -> usize {
((hash >> SHARD_BITS) as usize) & self.mask
}
#[inline]
fn next(&self, idx: usize) -> usize {
(idx + 1) & self.mask
}
fn insert_unique(&self, state: u8, hash: u64, offset: u64) {
let mut idx = self.home(hash);
for _ in 0..self.capacity() {
let slot = &self.slots[idx];
if slot.state.load(Ordering::Relaxed) == STATE_EMPTY {
slot.write(state, hash, offset);
return;
}
idx = self.next(idx);
}
debug_assert!(false, "insert_unique: table full during rebuild");
}
}
type OverflowMap = HashMap<u64, Vec<(Box<[u8]>, u64)>>;
#[derive(Debug, Default)]
struct WriterState {
occupied: usize,
tombstones: usize,
}
struct ReadSide {
table: Atomic<Table>,
}
struct WriteSide {
writer: Mutex<WriterState>,
live: AtomicUsize,
}
struct Shard {
read: CachePadded<ReadSide>,
write: CachePadded<WriteSide>,
overflow: RwLock<OverflowMap>,
}
impl Shard {
fn new() -> Self {
Self {
read: CachePadded::new(ReadSide {
table: Atomic::new(Table::with_capacity(INITIAL_SHARD_CAPACITY)),
}),
write: CachePadded::new(WriteSide {
writer: Mutex::new(WriterState::default()),
live: AtomicUsize::new(0),
}),
overflow: RwLock::new(OverflowMap::new()),
}
}
#[inline]
fn table<'g>(&self, guard: &'g Guard) -> &'g Table {
let shared = self.read.table.load(Ordering::Acquire, guard);
unsafe { shared.deref() }
}
#[inline]
unsafe fn writer_table(&self) -> &Table {
let guard = unsafe { epoch::unprotected() };
let shared = self.read.table.load(Ordering::Acquire, guard);
unsafe { shared.deref() }
}
fn get(&self, hash: u64, key: &[u8]) -> Option<u64> {
let guard = epoch::pin();
let table = self.table(&guard);
let mut idx = table.home(hash);
for _ in 0..table.capacity() {
let snap = table.slots[idx].read();
match snap.state {
STATE_EMPTY => return None,
STATE_OCCUPIED if snap.hash == hash => return Some(snap.offset),
STATE_OVERFLOW if snap.hash == hash => return self.overflow_get(hash, key),
_ => {}
}
idx = table.next(idx);
}
None
}
fn overflow_get(&self, hash: u64, key: &[u8]) -> Option<u64> {
let overflow = self.overflow.read();
overflow
.get(&hash)?
.iter()
.find(|(k, _)| k.as_ref() == key)
.map(|(_, off)| *off)
}
fn replace<F>(&self, hash: u64, key: &[u8], offset: u64, resolve: &mut F) -> Result<Option<u64>>
where
F: FnMut(u64, &[u8]) -> Result<KeyCheck>,
{
let mut state = self.write.writer.lock();
loop {
let table = unsafe { self.writer_table() };
if let Some(prev) = self.replace_in(table, &mut state, hash, key, offset, resolve)? {
return Ok(prev);
}
self.rebuild(&mut state);
}
}
fn replace_in<F>(
&self,
table: &Table,
state: &mut WriterState,
hash: u64,
key: &[u8],
offset: u64,
resolve: &mut F,
) -> Result<Option<Option<u64>>>
where
F: FnMut(u64, &[u8]) -> Result<KeyCheck>,
{
let mut idx = table.home(hash);
let mut reusable: Option<usize> = None;
let mut empty: Option<usize> = None;
for _ in 0..table.capacity() {
let snap = table.slots[idx].read();
match snap.state {
STATE_EMPTY => {
empty = Some(idx);
break;
}
STATE_TOMBSTONE => {
let _ = reusable.get_or_insert(idx);
}
STATE_OCCUPIED if snap.hash == hash => {
return match resolve(snap.offset, key)? {
KeyCheck::Same | KeyCheck::Unreadable => {
table.slots[idx].write(STATE_OCCUPIED, hash, offset);
Ok(Some(Some(snap.offset)))
}
KeyCheck::Other(existing_key) => {
self.promote_to_overflow(
&table.slots[idx],
hash,
existing_key,
snap.offset,
key,
offset,
);
Ok(Some(None))
}
};
}
STATE_OVERFLOW if snap.hash == hash => {
return Ok(Some(self.overflow_upsert(hash, key, offset)));
}
_ => {}
}
idx = table.next(idx);
}
#[cfg(test)]
test_hooks::before_claim();
let target = match (reusable, empty) {
(Some(tombstone), _) => {
state.tombstones -= 1;
tombstone
}
(None, Some(free)) => {
let used = state.occupied + state.tombstones + 1;
if used * GROWTH_DENOM > table.capacity() * GROWTH_NUM {
return Ok(None);
}
free
}
(None, None) => return Ok(None),
};
table.slots[target].write(STATE_OCCUPIED, hash, offset);
state.occupied += 1;
let _ = self.write.live.fetch_add(1, Ordering::Release);
Ok(Some(None))
}
fn promote_to_overflow(
&self,
slot: &AtomicSlot,
hash: u64,
existing_key: Vec<u8>,
existing_offset: u64,
key: &[u8],
offset: u64,
) {
let added = {
let mut overflow = self.overflow.write();
let entries = overflow.entry(hash).or_default();
let _ = upsert_entry(entries, &existing_key, existing_offset);
upsert_entry(entries, key, offset).is_none()
};
slot.write(STATE_OVERFLOW, hash, 0);
if added {
let _ = self.write.live.fetch_add(1, Ordering::Release);
}
}
fn overflow_upsert(&self, hash: u64, key: &[u8], offset: u64) -> Option<u64> {
let mut overflow = self.overflow.write();
let entries = overflow.entry(hash).or_default();
let prev = upsert_entry(entries, key, offset);
if prev.is_none() {
let _ = self.write.live.fetch_add(1, Ordering::Release);
}
prev
}
fn remove_where<F>(&self, hash: u64, key: &[u8], mut matches: F) -> Result<Option<u64>>
where
F: FnMut(u64) -> Result<bool>,
{
let mut state = self.write.writer.lock();
let table = unsafe { self.writer_table() };
let mut idx = table.home(hash);
for _ in 0..table.capacity() {
let snap = table.slots[idx].read();
match snap.state {
STATE_EMPTY => return Ok(None),
STATE_OCCUPIED if snap.hash == hash => {
if !matches(snap.offset)? {
return Ok(None);
}
table.slots[idx].write(STATE_TOMBSTONE, 0, 0);
state.occupied -= 1;
state.tombstones += 1;
let _ = self.write.live.fetch_sub(1, Ordering::Release);
return Ok(Some(snap.offset));
}
STATE_OVERFLOW if snap.hash == hash => {
let mut overflow = self.overflow.write();
let Some(entries) = overflow.get_mut(&hash) else {
return Ok(None);
};
let Some(pos) = entries.iter().position(|(k, _)| k.as_ref() == key) else {
return Ok(None);
};
let current = entries[pos].1;
if !matches(current)? {
return Ok(None);
}
let _ = entries.swap_remove(pos);
let _ = self.write.live.fetch_sub(1, Ordering::Release);
if entries.is_empty() {
let _ = overflow.remove(&hash);
drop(overflow);
table.slots[idx].write(STATE_TOMBSTONE, 0, 0);
state.occupied -= 1;
state.tombstones += 1;
}
return Ok(Some(current));
}
_ => {}
}
idx = table.next(idx);
}
Ok(None)
}
fn rebuild(&self, state: &mut WriterState) {
let guard = epoch::pin();
let old = self.table(&guard);
let capacity = old.capacity();
let new_capacity = if (state.occupied + 1) * 2 > capacity {
capacity * 2
} else {
capacity
};
let fresh = Table::with_capacity(new_capacity);
for slot in old.slots.iter() {
let snap = slot.read();
if snap.state == STATE_OCCUPIED || snap.state == STATE_OVERFLOW {
fresh.insert_unique(snap.state, snap.hash, snap.offset);
}
}
state.tombstones = 0;
self.publish(fresh, &guard);
}
fn publish(&self, table: Table, guard: &Guard) {
let previous = self
.read
.table
.swap(Owned::new(table), Ordering::AcqRel, guard);
unsafe { guard.defer_destroy(previous) };
guard.flush();
}
#[cfg(all(test, not(loom)))]
fn clear(&self) {
let mut state = self.write.writer.lock();
let guard = epoch::pin();
self.publish(Table::with_capacity(INITIAL_SHARD_CAPACITY), &guard);
self.overflow.write().clear();
*state = WriterState::default();
self.write.live.store(0, Ordering::Release);
}
fn collect_offsets(&self, out: &mut Vec<u64>) {
let guard = epoch::pin();
let table = self.table(&guard);
for slot in table.slots.iter() {
let snap = slot.read();
if snap.state == STATE_OCCUPIED {
out.push(snap.offset);
}
}
drop(guard);
for entries in self.overflow.read().values() {
out.extend(entries.iter().map(|(_, off)| *off));
}
}
fn live(&self) -> usize {
self.write.live.load(Ordering::Acquire)
}
}
impl Drop for Shard {
fn drop(&mut self) {
let table = std::mem::replace(&mut self.read.table, Atomic::null());
drop(unsafe { table.into_owned() });
}
}
fn upsert_entry(entries: &mut Vec<(Box<[u8]>, u64)>, key: &[u8], offset: u64) -> Option<u64> {
if let Some(entry) = entries.iter_mut().find(|(k, _)| k.as_ref() == key) {
return Some(std::mem::replace(&mut entry.1, offset));
}
entries.push((key.into(), offset));
None
}
pub(crate) struct Index {
shards: Box<[Shard; SHARDS]>,
}
impl std::fmt::Debug for Index {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Index")
.field("shards", &SHARDS)
.field("len", &self.len())
.finish()
}
}
impl Default for Index {
fn default() -> Self {
Self::new()
}
}
impl Index {
#[must_use]
pub(crate) fn new() -> Self {
Self {
shards: Box::new(std::array::from_fn(|_| Shard::new())),
}
}
#[inline]
fn shard(&self, hash: KeyHash) -> &Shard {
&self.shards[(hash & SHARD_MASK) as usize]
}
pub(crate) fn get(&self, hash: KeyHash, key: &[u8]) -> Result<Option<u64>> {
Ok(self.shard(hash).get(hash, key))
}
pub(crate) fn replace<F>(
&self,
hash: KeyHash,
key: &[u8],
offset: u64,
mut resolve: F,
) -> Result<Option<u64>>
where
F: FnMut(u64, &[u8]) -> Result<KeyCheck>,
{
self.shard(hash).replace(hash, key, offset, &mut resolve)
}
pub(crate) fn remove<F>(&self, hash: KeyHash, key: &[u8], mut resolve: F) -> Result<Option<u64>>
where
F: FnMut(u64, &[u8]) -> Result<KeyCheck>,
{
self.shard(hash).remove_where(hash, key, |offset| {
Ok(!matches!(resolve(offset, key)?, KeyCheck::Other(_)))
})
}
pub(crate) fn remove_if_offset(&self, hash: KeyHash, key: &[u8], expected: u64) -> bool {
matches!(
self.shard(hash)
.remove_where(hash, key, |offset| Ok(offset == expected)),
Ok(Some(_))
)
}
#[must_use]
pub(crate) fn len(&self) -> usize {
self.shards.iter().map(Shard::live).sum()
}
#[cfg(all(test, not(loom)))]
pub(crate) fn clear(&self) -> Result<()> {
for shard in self.shards.iter() {
shard.clear();
}
Ok(())
}
pub(crate) fn collect_offsets(&self) -> Result<Vec<u64>> {
let mut out = Vec::with_capacity(self.len());
for shard in self.shards.iter() {
shard.collect_offsets(&mut out);
}
Ok(out)
}
}
#[cfg(test)]
mod test_hooks {
use std::cell::Cell;
thread_local! {
static PAUSE_BEFORE_CLAIM: Cell<bool> = const { Cell::new(false) };
}
#[cfg(not(loom))]
pub(super) fn set_pause_before_claim(on: bool) {
PAUSE_BEFORE_CLAIM.with(|cell| cell.set(on));
}
pub(super) fn before_claim() {
if PAUSE_BEFORE_CLAIM.with(Cell::get) {
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
use std::sync::{Arc, Barrier};
const HASHER: KeyHasher = KeyHasher::with_secrets(1, 3, 5);
fn h(key: &[u8]) -> u64 {
HASHER.hash(key)
}
fn journal<'a>(keys: &'a [&'a [u8]]) -> impl FnMut(u64, &[u8]) -> Result<KeyCheck> + 'a {
move |offset, key| {
let stored = keys[offset as usize];
Ok(if stored == key {
KeyCheck::Same
} else {
KeyCheck::Other(stored.to_vec())
})
}
}
fn unreadable(_offset: u64, _key: &[u8]) -> Result<KeyCheck> {
Ok(KeyCheck::Unreadable)
}
fn race_rounds() -> usize {
if cfg!(miri) {
return 4;
}
std::env::var("EMDB_RACE_ROUNDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(2_000)
}
#[test]
fn test_index_insert_then_get_round_trips() {
let idx = Index::new();
let hash = h(b"alpha");
assert_eq!(idx.replace(hash, b"alpha", 7, unreadable).unwrap(), None);
assert_eq!(idx.get(hash, b"alpha").unwrap(), Some(7));
assert_eq!(idx.len(), 1);
}
#[test]
fn test_index_get_missing_returns_none() {
let idx = Index::new();
assert_eq!(idx.get(h(b"nope"), b"nope").unwrap(), None);
assert_eq!(idx.len(), 0);
}
#[test]
fn test_index_overwrite_returns_previous_offset() {
let keys: [&[u8]; 2] = [b"alpha", b"alpha"];
let idx = Index::new();
let hash = h(b"alpha");
let _ = idx.replace(hash, b"alpha", 0, journal(&keys)).unwrap();
assert_eq!(
idx.replace(hash, b"alpha", 1, journal(&keys)).unwrap(),
Some(0)
);
assert_eq!(idx.get(hash, b"alpha").unwrap(), Some(1));
assert_eq!(idx.len(), 1);
}
#[test]
fn test_index_remove_drops_entry_and_reuses_tombstone() {
let keys: [&[u8]; 2] = [b"alpha", b"alpha"];
let idx = Index::new();
let hash = h(b"alpha");
let _ = idx.replace(hash, b"alpha", 0, journal(&keys)).unwrap();
assert_eq!(idx.remove(hash, b"alpha", journal(&keys)).unwrap(), Some(0));
assert_eq!(idx.get(hash, b"alpha").unwrap(), None);
assert_eq!(idx.len(), 0);
assert_eq!(
idx.replace(hash, b"alpha", 1, journal(&keys)).unwrap(),
None
);
assert_eq!(idx.get(hash, b"alpha").unwrap(), Some(1));
}
#[test]
fn test_index_new_index_allocates_small_tables() {
let idx = Index::new();
let guard = epoch::pin();
let slots: usize = idx.shards.iter().map(|s| s.table(&guard).capacity()).sum();
assert_eq!(slots, SHARDS * INITIAL_SHARD_CAPACITY);
assert!(slots * std::mem::size_of::<AtomicSlot>() <= 32 * 1024);
}
#[test]
fn test_index_remove_of_colliding_absent_key_keeps_entry() {
let keys: [&[u8]; 1] = [b"first"];
let idx = Index::new();
let _ = idx.replace(42, b"first", 0, journal(&keys)).unwrap();
assert_eq!(idx.remove(42, b"other", journal(&keys)).unwrap(), None);
assert_eq!(idx.get(42, b"first").unwrap(), Some(0));
}
#[test]
fn test_index_remove_if_offset_only_matches_current() {
let idx = Index::new();
let hash = h(b"k");
let _ = idx.replace(hash, b"k", 5, unreadable).unwrap();
assert!(!idx.remove_if_offset(hash, b"k", 4));
assert_eq!(idx.get(hash, b"k").unwrap(), Some(5));
assert!(idx.remove_if_offset(hash, b"k", 5));
assert_eq!(idx.get(hash, b"k").unwrap(), None);
assert!(!idx.remove_if_offset(hash, b"k", 5));
}
#[test]
fn test_index_hash_collision_disambiguates_by_key() {
let keys: [&[u8]; 3] = [b"first", b"second", b"third"];
let idx = Index::new();
let _ = idx.replace(42, b"first", 0, journal(&keys)).unwrap();
let _ = idx.replace(42, b"second", 1, journal(&keys)).unwrap();
assert_eq!(idx.get(42, b"first").unwrap(), Some(0));
assert_eq!(idx.get(42, b"second").unwrap(), Some(1));
assert_eq!(idx.get(42, b"third").unwrap(), None);
assert_eq!(idx.len(), 2);
assert_eq!(
idx.replace(42, b"second", 1, journal(&keys)).unwrap(),
Some(1)
);
assert_eq!(idx.len(), 2);
assert_eq!(idx.remove(42, b"first", journal(&keys)).unwrap(), Some(0));
assert_eq!(idx.remove(42, b"second", journal(&keys)).unwrap(), Some(1));
assert_eq!(idx.len(), 0);
assert_eq!(idx.get(42, b"second").unwrap(), None);
assert_eq!(idx.replace(42, b"third", 2, journal(&keys)).unwrap(), None);
assert_eq!(idx.get(42, b"third").unwrap(), Some(2));
}
#[test]
fn test_index_overflow_migration_never_duplicates_keys() {
let keys: [&[u8]; 2] = [b"a", b"b"];
let idx = Index::new();
let _ = idx.replace(9, b"a", 0, journal(&keys)).unwrap();
let _ = idx.replace(9, b"b", 1, journal(&keys)).unwrap();
let _ = idx.remove(9, b"b", journal(&keys)).unwrap();
let _ = idx.remove(9, b"a", journal(&keys)).unwrap();
let _ = idx.replace(9, b"a", 0, journal(&keys)).unwrap();
let _ = idx.replace(9, b"b", 1, journal(&keys)).unwrap();
assert_eq!(idx.len(), 2);
let mut offsets = idx.collect_offsets().unwrap();
offsets.sort_unstable();
assert_eq!(offsets, vec![0, 1]);
}
#[test]
fn test_index_len_reflects_total_entries_across_shards() {
let idx = Index::new();
for i in 0_u64..200 {
let key = format!("k{i:04}");
let _ = idx
.replace(h(key.as_bytes()), key.as_bytes(), i, unreadable)
.unwrap();
}
assert_eq!(idx.len(), 200);
assert_eq!(idx.collect_offsets().unwrap().len(), 200);
}
#[test]
fn test_index_clear_empties_every_shard() {
let idx = Index::new();
for i in 0_u64..50 {
let key = format!("k{i}");
let _ = idx
.replace(h(key.as_bytes()), key.as_bytes(), i, unreadable)
.unwrap();
}
idx.clear().unwrap();
assert_eq!(idx.len(), 0);
assert!(idx.collect_offsets().unwrap().is_empty());
assert_eq!(idx.get(h(b"k1"), b"k1").unwrap(), None);
}
#[test]
fn test_index_growth_preserves_entries() {
let idx = Index::new();
let count = if cfg!(miri) { 300 } else { 5_000 };
for i in 0..count as u64 {
let key = format!("k{i:06}");
let _ = idx
.replace(i << SHARD_BITS, key.as_bytes(), i, unreadable)
.unwrap();
}
for i in 0..count as u64 {
let key = format!("k{i:06}");
assert_eq!(idx.get(i << SHARD_BITS, key.as_bytes()).unwrap(), Some(i));
}
assert_eq!(idx.len(), count);
}
#[test]
fn test_index_delete_heavy_workload_rehashes_without_growing() {
let idx = Index::new();
for round in 0_u64..if cfg!(miri) { 3 } else { 20 } {
for i in 0_u64..500 {
let hash = (round * 1000 + i) << SHARD_BITS;
let _ = idx.replace(hash, b"k", i, unreadable).unwrap();
let _ = idx.remove_if_offset(hash, b"k", i);
}
}
assert_eq!(idx.len(), 0);
let guard = epoch::pin();
assert_eq!(
idx.shards[0].table(&guard).capacity(),
INITIAL_SHARD_CAPACITY
);
}
#[test]
#[cfg_attr(miri, ignore = "200 K inserts; too slow under Miri")]
fn test_index_load_factor_uses_every_home_slot() {
let idx = Index::new();
let hasher = KeyHasher::random();
let n = 200_000_usize;
let hashes: Vec<u64> = (0..n)
.map(|i| {
let key = format!("user:{i:08}");
let hash = hasher.hash(key.as_bytes());
let _ = idx
.replace(hash, key.as_bytes(), i as u64, unreadable)
.unwrap();
hash
})
.collect();
let guard = epoch::pin();
let mut probes = 0_usize;
for &hash in &hashes {
let table = idx.shard(hash).table(&guard);
let mut slot = table.home(hash);
let mut steps = 1;
while table.slots[slot].read().hash != hash {
slot = table.next(slot);
steps += 1;
}
probes += steps;
}
let average = probes as f64 / n as f64;
assert!(average < 2.0, "average hit probe length {average}");
for shard in idx.shards.iter() {
let table = shard.table(&guard);
assert!(shard.live() * 4 <= table.capacity() * 3);
}
}
#[test]
fn test_key_hasher_is_deterministic_per_instance() {
let hasher = KeyHasher::random();
assert_eq!(hasher.hash(b"deterministic"), hasher.hash(b"deterministic"));
assert_ne!(hasher.hash(b"deterministic"), hasher.hash(b"different"));
assert_ne!(hasher.hash(b""), hasher.hash(b"\0"));
assert_ne!(hasher.hash(b"ab"), hasher.hash(b"ab\0"));
}
#[test]
fn test_key_hasher_secrets_change_the_hash() {
let a = KeyHasher::random();
let b = KeyHasher::random();
let same = (0..64_u32)
.filter(|i| a.hash(&i.to_le_bytes()) == b.hash(&i.to_le_bytes()))
.count();
assert_eq!(same, 0);
}
#[test]
#[cfg_attr(miri, ignore = "hash-quality check, no concurrency; slow under Miri")]
fn test_key_hasher_has_no_collisions_on_structured_keys() {
let hasher = KeyHasher::random();
let mut by_hash: HashMap<u64, Vec<u8>> = HashMap::new();
for len in [0_usize, 1, 3, 4, 7, 8, 12, 15, 16, 17, 31, 32, 33, 64] {
for i in 0..2_000_u32 {
let mut key = vec![b'#'; len];
for (dst, src) in key.iter_mut().zip(i.to_le_bytes()) {
*dst = src;
}
if len >= 8 {
key[len - 4..].copy_from_slice(&i.to_be_bytes());
}
if let Some(previous) = by_hash.insert(hasher.hash(&key), key.clone()) {
assert_eq!(previous, key, "distinct keys collided");
}
}
}
}
fn legacy_colliding_key(w1: u64) -> [u8; 16] {
const P1: u64 = 0xa076_1d64_78bd_642f;
const P2: u64 = 0xe703_7ed1_a0b4_28db;
fn inverse(a: u64) -> u64 {
let mut x = a;
for _ in 0..6 {
x = x.wrapping_mul(2_u64.wrapping_sub(a.wrapping_mul(x)));
}
x
}
let need = 0_u64.wrapping_sub(w1.wrapping_mul(P1)).rotate_right(27);
let w2 = need.wrapping_mul(inverse(P2));
let mut key = [0_u8; 16];
key[..8].copy_from_slice(&w1.to_le_bytes());
key[8..].copy_from_slice(&w2.to_le_bytes());
key
}
#[test]
#[cfg_attr(miri, ignore = "hash-quality check, no concurrency; slow under Miri")]
fn test_key_hasher_resists_precomputed_collisions() {
let hasher = KeyHasher::random();
let hashes: std::collections::HashSet<u64> = (1..=20_000_u64)
.map(|w1| hasher.hash(&legacy_colliding_key(w1)))
.collect();
assert!(hashes.len() > 19_990, "{} distinct hashes", hashes.len());
}
#[test]
fn test_index_duplicate_slot_race_is_impossible() {
let hx: u64 = 0x1000_0000_0000_0000;
let hk: u64 = 0x2000_0000_0000_0000;
let keys: [&[u8]; 3] = [b"X", b"K", b"K"];
let idx = Arc::new(Index::new());
let _ = idx.replace(hx, b"X", 0, journal(&keys)).unwrap();
let racer = {
let idx = Arc::clone(&idx);
std::thread::spawn(move || {
let keys: [&[u8]; 3] = [b"X", b"K", b"K"];
test_hooks::set_pause_before_claim(true);
let _ = idx.replace(hk, b"K", 1, journal(&keys)).unwrap();
})
};
std::thread::sleep(std::time::Duration::from_millis(10));
let _ = idx.remove(hx, b"X", journal(&keys)).unwrap();
let _ = idx.replace(hk, b"K", 2, journal(&keys)).unwrap();
racer.join().unwrap();
assert_eq!(idx.len(), 1);
assert!(idx.remove(hk, b"K", journal(&keys)).unwrap().is_some());
assert_eq!(idx.get(hk, b"K").unwrap(), None, "K resurrected");
}
#[test]
fn test_index_duplicate_slot_race_stress() {
let rounds = race_rounds();
let hx: u64 = 0x1000_0000_0000_0000;
let hk: u64 = 0x2000_0000_0000_0000;
let idx = Arc::new(Index::new());
for _ in 0..rounds {
let keys: [&[u8]; 3] = [b"X", b"K", b"K"];
let _ = idx.replace(hx, b"X", 0, journal(&keys)).unwrap();
let barrier = Arc::new(Barrier::new(3));
let mut handles = Vec::new();
{
let (idx, barrier) = (Arc::clone(&idx), Arc::clone(&barrier));
handles.push(std::thread::spawn(move || {
let keys: [&[u8]; 3] = [b"X", b"K", b"K"];
let _ = barrier.wait();
let _ = idx.remove(hx, b"X", journal(&keys)).unwrap();
}));
}
for offset in [1_u64, 2] {
let (idx, barrier) = (Arc::clone(&idx), Arc::clone(&barrier));
handles.push(std::thread::spawn(move || {
let keys: [&[u8]; 3] = [b"X", b"K", b"K"];
let _ = barrier.wait();
let _ = idx.replace(hk, b"K", offset, journal(&keys)).unwrap();
}));
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(idx.len(), 1);
let _ = idx.remove(hk, b"K", journal(&keys)).unwrap();
assert_eq!(idx.get(hk, b"K").unwrap(), None, "K resurrected");
idx.clear().unwrap();
}
}
#[test]
fn test_index_concurrent_collisions_keep_overflow_consistent() {
let rounds = race_rounds();
let keys: Arc<Vec<Vec<u8>>> =
Arc::new(vec![b"k0".to_vec(), b"k1".to_vec(), b"k2".to_vec()]);
let resolver = |keys: Arc<Vec<Vec<u8>>>| {
move |offset: u64, key: &[u8]| -> Result<KeyCheck> {
let stored = &keys[offset as usize];
Ok(if stored.as_slice() == key {
KeyCheck::Same
} else {
KeyCheck::Other(stored.clone())
})
}
};
for _ in 0..rounds {
let idx = Arc::new(Index::new());
let _ = idx
.replace(77, &keys[0], 0, resolver(Arc::clone(&keys)))
.unwrap();
let barrier = Arc::new(Barrier::new(2));
let handles: Vec<_> = [1_usize, 2]
.into_iter()
.map(|i| {
let (idx, barrier, keys) =
(Arc::clone(&idx), Arc::clone(&barrier), Arc::clone(&keys));
std::thread::spawn(move || {
let _ = barrier.wait();
let key = keys[i].clone();
let _ = idx.replace(77, &key, i as u64, resolver(keys)).unwrap();
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
for (i, key) in keys.iter().enumerate() {
assert_eq!(idx.get(77, key).unwrap(), Some(i as u64));
}
assert_eq!(idx.len(), 3);
for key in keys.iter() {
assert!(idx
.remove(77, key, resolver(Arc::clone(&keys)))
.unwrap()
.is_some());
}
assert!(keys.iter().all(|k| idx.get(77, k).unwrap().is_none()));
assert_eq!(idx.len(), 0);
}
}
#[test]
fn test_index_readers_see_consistent_state_during_writes() {
let (rounds, keys) = if cfg!(miri) { (3, 60) } else { (30, 4_000) };
let idx = Arc::new(Index::new());
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let writer = {
let (idx, stop) = (Arc::clone(&idx), Arc::clone(&stop));
std::thread::spawn(move || {
for round in 0_u64..rounds {
for i in 0_u64..keys {
let hash = h(&i.to_le_bytes());
let _ = idx
.replace(hash, &i.to_le_bytes(), i * 1_000 + round, unreadable)
.unwrap();
}
for i in (0_u64..keys).step_by(3) {
let hash = h(&i.to_le_bytes());
let _ = idx.remove(hash, &i.to_le_bytes(), unreadable).unwrap();
}
}
stop.store(true, std::sync::atomic::Ordering::Release);
})
};
let readers: Vec<_> = (0..3)
.map(|_| {
let (idx, stop) = (Arc::clone(&idx), Arc::clone(&stop));
std::thread::spawn(move || {
while !stop.load(std::sync::atomic::Ordering::Acquire) {
for i in 0_u64..keys {
let hash = h(&i.to_le_bytes());
if let Some(offset) = idx.get(hash, &i.to_le_bytes()).unwrap() {
assert_eq!(offset / 1_000, i, "foreign offset");
}
}
}
})
})
.collect();
writer.join().unwrap();
for reader in readers {
reader.join().unwrap();
}
}
#[test]
#[ignore = "measurement, not a check"]
fn probe_length_report() {
let hasher = KeyHasher::random();
for &n in &[10_000_usize, 200_000, 1_000_000] {
let idx = Index::new();
let hashes: Vec<u64> = (0..n)
.map(|i| {
let key = format!("user:{i:08}");
let hash = hasher.hash(key.as_bytes());
let _ = idx
.replace(hash, key.as_bytes(), i as u64, unreadable)
.unwrap();
hash
})
.collect();
let guard = epoch::pin();
let probe = |hash: u64| {
let table = idx.shard(hash).table(&guard);
let mut slot = table.home(hash);
let mut steps = 1_usize;
loop {
let snap = table.slots[slot].read();
if snap.state == STATE_EMPTY || snap.hash == hash {
return steps;
}
slot = table.next(slot);
steps += 1;
}
};
let hits: Vec<usize> = hashes.iter().map(|&x| probe(x)).collect();
let misses: usize = (0..10_000_usize)
.map(|i| probe(hasher.hash(format!("absent:{i:08}").as_bytes())))
.sum();
let table = idx.shards[0].table(&guard);
eprintln!(
"n={n:>8} shard0 cap={:>6} load={:.3} avg_hit={:.2} max_hit={} avg_miss={:.2}",
table.capacity(),
idx.shards[0].live() as f64 / table.capacity() as f64,
hits.iter().sum::<usize>() as f64 / n as f64,
hits.iter().max().copied().unwrap_or(0),
misses as f64 / 10_000.0
);
}
}
}
#[cfg(all(test, loom))]
mod loom_model {
use super::*;
const OLD: SlotSnapshot = SlotSnapshot {
state: STATE_EMPTY,
hash: 0,
offset: 0,
};
#[test]
fn loom_seqlock_reader_never_sees_a_torn_slot() {
loom::model(|| {
let slot = loom::sync::Arc::new(AtomicSlot::empty());
let writer = {
let slot = slot.clone();
loom::thread::spawn(move || slot.write(STATE_OCCUPIED, 7, 7))
};
let snap = slot.read();
let new = SlotSnapshot {
state: STATE_OCCUPIED,
hash: 7,
offset: 7,
};
assert!(snap == OLD || snap == new, "torn read: {snap:?}");
writer.join().unwrap();
});
}
#[test]
fn loom_seqlock_two_updates_reader_sees_one_of_three_states() {
loom::model(|| {
let slot = loom::sync::Arc::new(AtomicSlot::empty());
let writer = {
let slot = slot.clone();
loom::thread::spawn(move || {
slot.write(STATE_OCCUPIED, 1, 1);
slot.write(STATE_TOMBSTONE, 2, 2);
})
};
let snap = slot.read();
assert_eq!(snap.hash, snap.offset, "torn read: {snap:?}");
writer.join().unwrap();
});
}
}