use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use lru::LruCache;
use parking_lot::RwLock;
use serde::Serialize;
pub const DEFAULT_CACHE_CAPACITY: usize = 8192;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub entries: usize,
}
pub struct AdjacencyCache {
inner: RwLock<LruCache<i64, Arc<Vec<i64>>>>,
hits: AtomicU64,
misses: AtomicU64,
}
impl Default for AdjacencyCache {
fn default() -> Self {
Self::new()
}
}
impl AdjacencyCache {
pub fn new() -> Self {
Self::with_capacity(NonZeroUsize::new(DEFAULT_CACHE_CAPACITY).expect("capacity > 0"))
}
pub fn with_capacity(capacity: NonZeroUsize) -> Self {
Self {
inner: RwLock::new(LruCache::new(capacity)),
hits: AtomicU64::new(0),
misses: AtomicU64::new(0),
}
}
pub fn get(&self, key: i64) -> Option<Arc<Vec<i64>>> {
let hit = self.inner.read().peek(&key).map(Arc::clone);
if hit.is_some() {
self.hits.fetch_add(1, Ordering::Relaxed);
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
}
hit
}
pub fn insert(&self, key: i64, value: Vec<i64>) -> Arc<Vec<i64>> {
let arc = Arc::new(value);
let _evicted = self.inner.write().put(key, Arc::clone(&arc));
arc
}
pub fn clear(&self) {
self.inner.write().clear();
self.hits.store(0, Ordering::Relaxed);
self.misses.store(0, Ordering::Relaxed);
}
pub fn remove(&self, key: i64) {
let _removed = self.inner.write().pop(&key);
}
pub fn stats(&self) -> CacheStats {
let entries = self.inner.read().len();
CacheStats {
hits: self.hits.load(Ordering::Relaxed),
misses: self.misses.load(Ordering::Relaxed),
entries,
}
}
pub fn inner(&self) -> std::collections::HashMap<i64, Vec<i64>> {
self.inner
.read()
.iter()
.map(|(k, v)| (*k, (**v).clone()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hit_returns_arc_without_copying() {
let cache = AdjacencyCache::new();
assert!(cache.get(1).is_none());
let stored = cache.insert(1, vec![2, 3, 4]);
let got = cache.get(1).expect("hit after insert");
assert!(Arc::ptr_eq(&got, &stored) || got.as_slice() == stored.as_slice());
assert_eq!(got.as_slice(), &[2, 3, 4]);
}
#[test]
fn clear_resets_state_and_counters() {
let cache = AdjacencyCache::new();
cache.insert(1, vec![2]);
let _ = cache.get(1); let _ = cache.get(2); cache.clear();
let stats = cache.stats();
assert_eq!(stats.entries, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert!(cache.get(1).is_none());
}
#[test]
fn capacity_bounds_entries_with_eviction() {
let cache = AdjacencyCache::with_capacity(NonZeroUsize::new(2).unwrap());
cache.insert(1, vec![10]);
cache.insert(2, vec![20]);
cache.insert(3, vec![30]); assert!(cache.get(1).is_none(), "oldest-inserted entry was evicted");
assert!(cache.get(2).is_some());
assert!(cache.get(3).is_some());
assert_eq!(cache.stats().entries, 2);
}
#[test]
fn inner_snapshot_clones_all_entries() {
let cache = AdjacencyCache::new();
cache.insert(1, vec![2, 3]);
cache.insert(4, vec![5]);
let snap = cache.inner();
assert_eq!(snap.len(), 2);
let mut got: Vec<i64> = snap[&1].clone();
got.sort_unstable();
assert_eq!(got, vec![2, 3]);
}
#[test]
fn remove_drops_single_entry() {
let cache = AdjacencyCache::new();
cache.insert(1, vec![2]);
cache.insert(3, vec![4]);
cache.remove(1);
assert!(cache.get(1).is_none());
assert!(cache.get(3).is_some());
}
}