use std::collections::{HashMap, VecDeque};
use std::hash::Hash;
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: usize,
pub misses: usize,
pub size: usize,
}
impl CacheStats {
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
pub struct LruCache<K, V> {
capacity: usize,
map: HashMap<K, V>,
order: VecDeque<K>,
hits: usize,
misses: usize,
}
impl<K: Hash + Eq + Clone, V> LruCache<K, V> {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
map: HashMap::new(),
order: VecDeque::with_capacity(capacity),
hits: 0,
misses: 0,
}
}
pub fn get(&mut self, key: &K) -> Option<&V> {
if self.map.contains_key(key) {
self.hits += 1;
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
self.order.push_front(key.clone());
}
self.map.get(key)
} else {
self.misses += 1;
None
}
}
pub fn put(&mut self, key: K, value: V) {
if self.map.contains_key(&key) {
if let Some(pos) = self.order.iter().position(|k| k == &key) {
self.order.remove(pos);
}
} else if self.order.len() >= self.capacity {
if let Some(old) = self.order.pop_back() {
self.map.remove(&old);
}
}
let key_clone = key.clone();
self.order.push_front(key_clone);
self.map.insert(key, value);
}
pub fn invalidate(&mut self, key: &K) {
self.map.remove(key);
if let Some(pos) = self.order.iter().position(|k| k == key) {
self.order.remove(pos);
}
}
pub fn clear(&mut self) {
self.map.clear();
self.order.clear();
self.hits = 0;
self.misses = 0;
}
pub fn _len(&self) -> usize {
self.map.len()
}
pub fn _is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn stats(&self) -> CacheStats {
CacheStats {
hits: self.hits,
misses: self.misses,
size: self.map.len(),
}
}
pub fn _hit_rate(&self) -> f64 {
self.stats().hit_rate()
}
}
pub type FileNodeCache = LruCache<String, crate::graph::schema::FileNode>;
#[expect(dead_code)] pub type SymbolCache = LruCache<String, Vec<crate::ingest::SymbolFact>>;
pub struct ThreadSafeCache<K, V> {
inner: parking_lot::Mutex<LruCache<K, V>>,
}
impl<K: Hash + Eq + Clone, V> ThreadSafeCache<K, V> {
pub fn new(capacity: usize) -> Self {
Self {
inner: parking_lot::Mutex::new(LruCache::new(capacity)),
}
}
pub fn get_cloned(&self, key: &K) -> Option<V>
where
V: Clone,
{
let mut guard = self.inner.lock();
guard.get(key).cloned()
}
pub fn put(&self, key: K, value: V) {
let mut guard = self.inner.lock();
guard.put(key, value);
}
pub fn invalidate(&self, key: &K) {
let mut guard = self.inner.lock();
guard.invalidate(key);
}
pub fn clear(&self) {
let mut guard = self.inner.lock();
guard.clear();
}
pub fn stats(&self) -> CacheStats {
let guard = self.inner.lock();
guard.stats()
}
}
unsafe impl<K: Send, V: Send> Send for ThreadSafeCache<K, V> {}
unsafe impl<K: Send + Sync, V: Send + Sync> Sync for ThreadSafeCache<K, V> {}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct EntityCacheKey(pub i64);
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct NameCacheKey(pub String);
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub struct ExpandCacheKey(pub i64);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lru_cache_basic_operations() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
assert_eq!(cache.get(&"a".to_string()), None);
assert_eq!(cache._len(), 0);
assert!(cache._is_empty());
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
assert_eq!(cache.get(&"a".to_string()), Some(&1));
assert_eq!(cache.get(&"b".to_string()), Some(&2));
assert_eq!(cache._len(), 2);
assert!(!cache._is_empty());
}
#[test]
fn test_lru_cache_eviction() {
let mut cache: LruCache<String, i32> = LruCache::new(2);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.get(&"a".to_string());
cache.put("c".to_string(), 3);
assert_eq!(cache.get(&"a".to_string()), Some(&1));
assert_eq!(cache.get(&"b".to_string()), None); assert_eq!(cache.get(&"c".to_string()), Some(&3));
assert_eq!(cache._len(), 2);
}
#[test]
fn test_lru_cache_update_existing() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.put("c".to_string(), 3);
cache.put("a".to_string(), 10);
cache.put("d".to_string(), 4);
assert_eq!(cache.get(&"a".to_string()), Some(&10));
assert_eq!(cache.get(&"b".to_string()), None); assert_eq!(cache.get(&"c".to_string()), Some(&3));
assert_eq!(cache.get(&"d".to_string()), Some(&4));
assert_eq!(cache._len(), 3);
}
#[test]
fn test_lru_cache_invalidate() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.put("c".to_string(), 3);
cache.invalidate(&"b".to_string());
assert_eq!(cache.get(&"a".to_string()), Some(&1));
assert_eq!(cache.get(&"b".to_string()), None);
assert_eq!(cache.get(&"c".to_string()), Some(&3));
assert_eq!(cache._len(), 2);
}
#[test]
fn test_lru_cache_clear() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.clear();
assert_eq!(cache._len(), 0);
assert!(cache._is_empty());
assert_eq!(cache.get(&"a".to_string()), None);
}
#[test]
fn test_cache_stats() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.get(&"a".to_string()); cache.get(&"b".to_string()); cache.get(&"c".to_string());
let stats = cache.stats();
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert_eq!(stats.size, 2);
let hit_rate = cache._hit_rate();
assert!((hit_rate - 2.0 / 3.0).abs() < f64::EPSILON);
}
#[test]
fn test_cache_hit_rate_empty() {
let cache: LruCache<String, i32> = LruCache::new(3);
assert_eq!(cache._hit_rate(), 0.0);
}
#[test]
fn test_cache_hit_rate_all_hits() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
cache.put("a".to_string(), 1);
cache.get(&"a".to_string());
cache.get(&"a".to_string());
assert_eq!(cache._hit_rate(), 1.0);
}
#[test]
fn test_cache_hit_rate_all_misses() {
let mut cache: LruCache<String, i32> = LruCache::new(3);
cache.get(&"a".to_string());
cache.get(&"b".to_string());
assert_eq!(cache._hit_rate(), 0.0);
}
#[test]
fn test_thread_safe_cache_basic() {
let cache: ThreadSafeCache<String, i32> = ThreadSafeCache::new(3);
assert_eq!(cache.get_cloned(&"a".to_string()), None);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
assert_eq!(cache.get_cloned(&"a".to_string()), Some(1));
assert_eq!(cache.get_cloned(&"b".to_string()), Some(2));
let stats = cache.stats();
assert_eq!(stats.size, 2);
}
#[test]
fn test_thread_safe_cache_eviction() {
let cache: ThreadSafeCache<String, i32> = ThreadSafeCache::new(2);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.get_cloned(&"a".to_string());
cache.put("c".to_string(), 3);
assert_eq!(cache.get_cloned(&"a".to_string()), Some(1));
assert_eq!(cache.get_cloned(&"b".to_string()), None);
assert_eq!(cache.get_cloned(&"c".to_string()), Some(3));
}
#[test]
fn test_thread_safe_cache_invalidate() {
let cache: ThreadSafeCache<String, i32> = ThreadSafeCache::new(3);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.invalidate(&"a".to_string());
assert_eq!(cache.get_cloned(&"a".to_string()), None);
assert_eq!(cache.get_cloned(&"b".to_string()), Some(2));
assert_eq!(cache.stats().size, 1);
}
#[test]
fn test_thread_safe_cache_clear() {
let cache: ThreadSafeCache<String, i32> = ThreadSafeCache::new(3);
cache.put("a".to_string(), 1);
cache.put("b".to_string(), 2);
cache.clear();
assert_eq!(cache.stats().size, 0);
assert_eq!(cache.get_cloned(&"a".to_string()), None);
}
#[test]
fn test_thread_safe_cache_concurrent_access() {
use std::sync::Arc;
use std::thread;
let cache = Arc::new(ThreadSafeCache::<String, i32>::new(100));
let mut handles = Vec::new();
for i in 0..4 {
let c = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let key = format!("key_{}", i);
c.put(key.clone(), i);
assert_eq!(c.get_cloned(&key), Some(i));
}));
}
for h in handles {
h.join().unwrap();
}
assert_eq!(cache.stats().size, 4);
}
}