use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use super::eviction::LruEviction;
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub evictions: u64,
}
struct CacheInner<V> {
map: HashMap<u64, V>,
eviction: LruEviction,
stats: CacheStats,
schema_version: u64,
}
pub struct PlanCache<V: Clone> {
inner: Arc<RwLock<CacheInner<V>>>,
}
impl<V: Clone> PlanCache<V> {
pub fn new(capacity: usize) -> Self {
Self {
inner: Arc::new(RwLock::new(CacheInner {
map: HashMap::new(),
eviction: LruEviction::new(capacity),
stats: CacheStats {
hits: 0,
misses: 0,
evictions: 0,
},
schema_version: 0,
})),
}
}
pub fn get(&self, key: u64) -> Option<V> {
let mut inner = self.inner.write();
if let Some(val) = inner.map.get(&key).cloned() {
inner.eviction.on_access(key);
inner.stats.hits += 1;
Some(val)
} else {
inner.stats.misses += 1;
None
}
}
pub fn insert(&self, key: u64, value: V) {
let mut inner = self.inner.write();
if let Some(evict_key) = inner.eviction.on_insert(key) {
inner.map.remove(&evict_key);
inner.stats.evictions += 1;
}
inner.map.insert(key, value);
}
pub fn invalidate_all(&self) {
let mut inner = self.inner.write();
inner.map.clear();
inner.eviction = LruEviction::new(inner.eviction.capacity());
inner.schema_version += 1;
}
pub fn stats(&self) -> (u64, u64, u64) {
let inner = self.inner.read();
(inner.stats.hits, inner.stats.misses, inner.stats.evictions)
}
pub fn len(&self) -> usize {
self.inner.read().map.len()
}
pub fn is_empty(&self) -> bool {
self.inner.read().map.is_empty()
}
pub fn schema_version(&self) -> u64 {
self.inner.read().schema_version
}
}
impl<V: Clone> Clone for PlanCache<V> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hit_and_miss_counters() {
let cache: PlanCache<String> = PlanCache::new(10);
cache.insert(1, "a".into());
assert!(cache.get(1).is_some());
assert!(cache.get(2).is_none());
let (hits, misses, _) = cache.stats();
assert_eq!(hits, 1);
assert_eq!(misses, 1);
}
#[test]
fn capacity_evicts_lru() {
let cache: PlanCache<u32> = PlanCache::new(3);
cache.insert(1, 10);
cache.insert(2, 20);
cache.insert(3, 30);
cache.insert(4, 40); assert!(cache.get(1).is_none(), "key 1 should be evicted");
assert_eq!(cache.get(4), Some(40));
}
#[test]
fn invalidate_all_clears_and_bumps_version() {
let cache: PlanCache<u32> = PlanCache::new(10);
cache.insert(1, 100);
assert_eq!(cache.schema_version(), 0);
cache.invalidate_all();
assert!(cache.get(1).is_none());
assert_eq!(cache.schema_version(), 1);
}
#[test]
fn clone_shares_backing_store() {
let cache: PlanCache<u32> = PlanCache::new(10);
let clone = cache.clone();
cache.insert(99, 42);
assert_eq!(clone.get(99), Some(42));
}
#[test]
fn is_empty_initially() {
let cache: PlanCache<u32> = PlanCache::new(10);
assert!(cache.is_empty());
cache.insert(1, 1);
assert!(!cache.is_empty());
}
}