use crate::engine::hash::compute_hash;
use crate::sync::ConcurrentPulseMap;
use crate::{PulseKey, PulseValue};
const NUM_SHARDS: usize = 16;
pub struct ShardedPulseMap<K: PulseKey, V: PulseValue> {
shards: Box<[ConcurrentPulseMap<K, V>; NUM_SHARDS]>,
}
impl<K: PulseKey, V: PulseValue> ShardedPulseMap<K, V> {
pub fn new(buckets_per_shard: usize) -> Self {
Self {
shards: Box::new(core::array::from_fn(|_| {
ConcurrentPulseMap::new(buckets_per_shard)
})),
}
}
pub fn with_auto_resize(buckets_per_shard: usize) -> Self {
Self {
shards: Box::new(core::array::from_fn(|_| {
ConcurrentPulseMap::with_auto_resize(buckets_per_shard)
})),
}
}
#[inline]
fn shard_for(key_bytes: &[u8]) -> usize {
(compute_hash(key_bytes).h1 >> 14) as usize & (NUM_SHARDS - 1)
}
pub fn insert(&self, key: K, value: V) {
let idx = key.with_key_bytes(Self::shard_for);
self.shards[idx].insert(key, value);
}
pub fn insert_ttl(&self, key: K, value: V, ttl: u64) {
let idx = key.with_key_bytes(Self::shard_for);
self.shards[idx].insert_ttl(key, value, ttl);
}
pub fn get(&self, key: &K) -> Option<V> {
let idx = key.with_key_bytes(Self::shard_for);
self.shards[idx].get(key)
}
pub fn peek(&self, key: &K) -> Option<V> {
let idx = key.with_key_bytes(Self::shard_for);
self.shards[idx].peek(key)
}
#[inline]
pub fn contains_key(&self, key: &K) -> bool {
self.peek(key).is_some()
}
pub fn remove(&self, key: &K) -> bool {
let idx = key.with_key_bytes(Self::shard_for);
self.shards[idx].remove(key)
}
pub fn resize_all(&self, new_buckets_per_shard: usize) {
for shard in self.shards.iter() {
shard.resize(new_buckets_per_shard);
}
}
pub fn set_ttl(&self, ttl: u64) {
for shard in self.shards.iter() {
shard.set_ttl(ttl);
}
}
#[inline]
pub fn get_ttl(&self) -> u64 {
self.shards[0].get_ttl()
}
pub fn current_epoch(&self) -> u64 {
self.shards
.iter()
.map(|s| s.current_epoch())
.max()
.unwrap_or(0)
}
pub fn len(&self) -> usize {
self.shards.iter().map(|s| s.len()).sum()
}
pub fn is_empty(&self) -> bool {
self.shards.iter().all(|s| s.is_empty())
}
pub fn capacity(&self) -> usize {
self.shards.iter().map(|s| s.capacity()).sum()
}
pub fn load_factor(&self) -> f64 {
let cap = self.capacity();
if cap == 0 {
0.0
} else {
self.len() as f64 / cap as f64
}
}
pub fn eviction_count(&self) -> usize {
self.shards.iter().map(|s| s.eviction_count()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn test_sharded_basic_insert_get() {
let map = ShardedPulseMap::<u32, u32>::new(16384);
for i in 0u32..1000 {
map.insert(i, i * 2);
}
for i in 0u32..1000 {
assert_eq!(map.get(&i), Some(i * 2));
}
assert_eq!(map.len(), 1000);
assert!(map.remove(&500));
assert_eq!(map.get(&500), None);
assert_eq!(map.len(), 999);
}
#[test]
fn test_sharded_concurrent_4thread() {
let map = Arc::new(ShardedPulseMap::<u32, u32>::new(16384));
let handles: Vec<_> = (0..4u32)
.map(|t| {
let m = map.clone();
thread::spawn(move || {
for i in 0..10_000u32 {
m.insert(t * 10_000 + i, i);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(map.len(), 40_000);
assert_eq!(map.get(&15_000), Some(5_000));
}
#[test]
fn test_sharded_resize_all() {
let map = ShardedPulseMap::<u32, u32>::new(1024);
for i in 0u32..500 {
map.insert(i, i);
}
let before = map.capacity();
map.resize_all(2048);
assert!(map.capacity() > before);
for i in 0u32..500 {
assert_eq!(map.get(&i), Some(i));
}
}
#[test]
fn test_sharded_ttl_propagation() {
let map = ShardedPulseMap::<u32, u32>::new(64);
map.set_ttl(100);
assert_eq!(map.get_ttl(), 100);
map.insert(1, 1);
assert!(map.current_epoch() >= 1);
map.set_ttl(0);
assert_eq!(map.get_ttl(), 0);
}
#[test]
fn test_sharded_len_sum_of_shards() {
let map = ShardedPulseMap::<u32, u32>::new(64);
assert!(map.is_empty());
for i in 0u32..100 {
map.insert(i, i);
}
let shard_sum: usize = map.shards.iter().map(|s| s.len()).sum();
assert_eq!(map.len(), shard_sum);
assert_eq!(map.len(), 100);
let used = map.shards.iter().filter(|s| !s.is_empty()).count();
assert!(used > 1, "all keys landed in {used} shard(s)");
}
}