use std::borrow::Borrow;
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash};
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
const SHARD_COUNT: usize = 16;
pub(crate) struct ShardedLruCache<K: Hash + Eq, V> {
shards: Box<[Mutex<lru::LruCache<K, V>>]>,
hasher: RandomState,
epoch: AtomicU64,
}
impl<K: Hash + Eq, V: Clone> ShardedLruCache<K, V> {
pub(crate) fn new(total_capacity: NonZeroUsize) -> Self {
let per_shard =
NonZeroUsize::new((total_capacity.get() / SHARD_COUNT).max(1)).unwrap_or(NonZeroUsize::MIN);
let shards: Vec<Mutex<lru::LruCache<K, V>>> = (0..SHARD_COUNT)
.map(|_| Mutex::new(lru::LruCache::new(per_shard)))
.collect();
Self {
shards: shards.into_boxed_slice(),
hasher: RandomState::new(),
epoch: AtomicU64::new(0),
}
}
#[allow(clippy::expect_used)] fn shard_for<Q>(&self, key: &Q) -> &Mutex<lru::LruCache<K, V>>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let idx = (self.hasher.hash_one(key) as usize) & (SHARD_COUNT - 1);
self.shards.get(idx).expect("masked shard index in range")
}
fn lock_shard<'a>(
shard: &'a Mutex<lru::LruCache<K, V>>,
) -> std::sync::MutexGuard<'a, lru::LruCache<K, V>> {
match shard.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
pub(crate) fn get<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
Self::lock_shard(self.shard_for(key)).get(key).cloned()
}
pub(crate) fn put(&self, key: K, value: V) {
Self::lock_shard(self.shard_for(&key)).put(key, value);
}
pub(crate) fn contains<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
Self::lock_shard(self.shard_for(key)).contains(key)
}
pub(crate) fn clear(&self) {
self.epoch.fetch_add(1, Ordering::Release);
for shard in self.shards.iter() {
Self::lock_shard(shard).clear();
}
}
pub(crate) fn epoch(&self) -> u64 {
self.epoch.load(Ordering::Acquire)
}
pub(crate) fn is_empty(&self) -> bool {
self.shards.iter().all(|shard| Self::lock_shard(shard).is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cache(capacity: usize) -> ShardedLruCache<String, u64> {
ShardedLruCache::new(NonZeroUsize::new(capacity).unwrap())
}
#[test]
fn get_put_contains_roundtrip() {
let c = cache(128);
assert!(c.is_empty());
assert_eq!(c.get("k1"), None);
c.put("k1".to_string(), 7);
assert_eq!(c.get("k1"), Some(7));
assert!(c.contains("k1"));
assert!(!c.contains("k2"));
assert!(!c.is_empty());
}
#[test]
fn clear_wipes_every_shard() {
let c = cache(256);
for i in 0..200u64 {
c.put(format!("key-{i}"), i);
}
assert!(!c.is_empty());
c.clear();
assert!(c.is_empty());
for i in 0..200u64 {
assert_eq!(c.get(&format!("key-{i}")), None);
}
}
#[test]
fn per_shard_lru_evicts_within_shard_only() {
let c = cache(16);
for i in 0..1000u64 {
c.put(format!("key-{i}"), i);
}
let live: usize = (0..1000u64)
.filter(|i| c.contains(&format!("key-{i}")))
.count();
assert!(live <= 16, "live={live} exceeds shard capacity");
assert!(live >= 1);
}
#[test]
fn recency_update_on_get() {
let c = cache(32);
c.put("hot".to_string(), 1);
for i in 0..500u64 {
assert_eq!(c.get("hot"), Some(1), "hot key evicted at i={i}");
c.put(format!("cold-{i}"), i);
assert_eq!(c.get("hot"), Some(1), "hot key evicted after cold-{i}");
}
}
#[test]
fn concurrent_smoke() {
let c = std::sync::Arc::new(cache(256));
std::thread::scope(|s| {
for t in 0..8usize {
let c = std::sync::Arc::clone(&c);
s.spawn(move || {
for i in 0..10_000u64 {
let key = format!("k-{}", (i + t as u64 * 17) % 64);
c.put(key.clone(), i);
let _ = c.get(&key);
if i % 1000 == 0 {
c.clear();
}
}
});
}
});
}
}