Skip to main content

agentic_forge_core/cache/
lru.rs

1//! Generic LRU cache with TTL.
2
3use super::metrics::CacheMetrics;
4use std::collections::HashMap;
5use std::hash::Hash;
6use std::sync::RwLock;
7use std::time::{Duration, Instant};
8
9struct CacheEntry<V> {
10    value: V,
11    inserted_at: Instant,
12    last_accessed: Instant,
13    access_count: u64,
14}
15
16pub struct Cache<K, V> {
17    entries: RwLock<HashMap<K, CacheEntry<V>>>,
18    max_size: usize,
19    ttl: Duration,
20    pub metrics: CacheMetrics,
21}
22
23impl<K: Hash + Eq + Clone, V: Clone> Cache<K, V> {
24    pub fn new(max_size: usize, ttl: Duration) -> Self {
25        Self {
26            entries: RwLock::new(HashMap::new()),
27            max_size,
28            ttl,
29            metrics: CacheMetrics::new(),
30        }
31    }
32
33    pub fn get(&self, key: &K) -> Option<V> {
34        let mut entries = self.entries.write().unwrap();
35        if let Some(entry) = entries.get_mut(key) {
36            if entry.inserted_at.elapsed() < self.ttl {
37                entry.last_accessed = Instant::now();
38                entry.access_count += 1;
39                self.metrics.record_hit();
40                return Some(entry.value.clone());
41            } else {
42                entries.remove(key);
43                self.metrics.record_eviction();
44            }
45        }
46        self.metrics.record_miss();
47        None
48    }
49
50    pub fn insert(&self, key: K, value: V) {
51        let mut entries = self.entries.write().unwrap();
52        if entries.len() >= self.max_size {
53            self.evict_lru(&mut entries);
54        }
55        entries.insert(
56            key,
57            CacheEntry {
58                value,
59                inserted_at: Instant::now(),
60                last_accessed: Instant::now(),
61                access_count: 0,
62            },
63        );
64        self.metrics.set_size(entries.len());
65    }
66
67    pub fn invalidate(&self, key: &K) -> bool {
68        let mut entries = self.entries.write().unwrap();
69        let removed = entries.remove(key).is_some();
70        self.metrics.set_size(entries.len());
71        removed
72    }
73
74    pub fn clear(&self) {
75        let mut entries = self.entries.write().unwrap();
76        entries.clear();
77        self.metrics.set_size(0);
78    }
79
80    pub fn len(&self) -> usize {
81        self.entries.read().unwrap().len()
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.entries.read().unwrap().is_empty()
86    }
87
88    pub fn contains(&self, key: &K) -> bool {
89        let entries = self.entries.read().unwrap();
90        if let Some(entry) = entries.get(key) {
91            entry.inserted_at.elapsed() < self.ttl
92        } else {
93            false
94        }
95    }
96
97    fn evict_lru(&self, entries: &mut HashMap<K, CacheEntry<V>>) {
98        if let Some(lru_key) = entries
99            .iter()
100            .min_by_key(|(_, e)| e.last_accessed)
101            .map(|(k, _)| k.clone())
102        {
103            entries.remove(&lru_key);
104            self.metrics.record_eviction();
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_cache_insert_and_get() {
115        let cache: Cache<String, String> = Cache::new(10, Duration::from_secs(60));
116        cache.insert("key".into(), "value".into());
117        assert_eq!(cache.get(&"key".into()), Some("value".into()));
118    }
119
120    #[test]
121    fn test_cache_miss() {
122        let cache: Cache<String, String> = Cache::new(10, Duration::from_secs(60));
123        assert_eq!(cache.get(&"missing".into()), None);
124    }
125
126    #[test]
127    fn test_cache_ttl_expiry() {
128        let cache: Cache<String, String> = Cache::new(10, Duration::from_millis(1));
129        cache.insert("key".into(), "value".into());
130        std::thread::sleep(Duration::from_millis(10));
131        assert_eq!(cache.get(&"key".into()), None);
132    }
133
134    #[test]
135    fn test_cache_eviction_on_full() {
136        let cache: Cache<u32, u32> = Cache::new(3, Duration::from_secs(60));
137        cache.insert(1, 10);
138        cache.insert(2, 20);
139        cache.insert(3, 30);
140        cache.insert(4, 40); // Should evict LRU
141        assert_eq!(cache.len(), 3);
142    }
143
144    #[test]
145    fn test_cache_invalidate() {
146        let cache: Cache<String, String> = Cache::new(10, Duration::from_secs(60));
147        cache.insert("key".into(), "value".into());
148        assert!(cache.invalidate(&"key".into()));
149        assert_eq!(cache.get(&"key".into()), None);
150    }
151
152    #[test]
153    fn test_cache_clear() {
154        let cache: Cache<u32, u32> = Cache::new(10, Duration::from_secs(60));
155        for i in 0..5 {
156            cache.insert(i, i * 10);
157        }
158        cache.clear();
159        assert!(cache.is_empty());
160    }
161
162    #[test]
163    fn test_cache_contains() {
164        let cache: Cache<String, String> = Cache::new(10, Duration::from_secs(60));
165        cache.insert("k".into(), "v".into());
166        assert!(cache.contains(&"k".into()));
167        assert!(!cache.contains(&"missing".into()));
168    }
169
170    #[test]
171    fn test_cache_metrics_hit_miss() {
172        let cache: Cache<String, String> = Cache::new(10, Duration::from_secs(60));
173        cache.insert("key".into(), "value".into());
174        let _ = cache.get(&"key".into()); // hit
175        let _ = cache.get(&"miss".into()); // miss
176        let _ = cache.get(&"key".into()); // hit
177        assert_eq!(cache.metrics.hits(), 2);
178        assert_eq!(cache.metrics.misses(), 1);
179        assert!(cache.metrics.hit_rate() > 0.6);
180    }
181
182    #[test]
183    fn test_cache_overwrite_same_key() {
184        let cache: Cache<String, String> = Cache::new(10, Duration::from_secs(60));
185        cache.insert("k".into(), "v1".into());
186        cache.insert("k".into(), "v2".into());
187        assert_eq!(cache.get(&"k".into()), Some("v2".into()));
188    }
189
190    #[test]
191    fn test_cache_second_query_is_cache_hit() {
192        let cache: Cache<String, Vec<u8>> = Cache::new(100, Duration::from_secs(60));
193        let big_data = vec![0u8; 10_000];
194        cache.insert("query".into(), big_data.clone());
195
196        // First get = hit
197        let r1 = cache.get(&"query".into());
198        assert!(r1.is_some());
199        assert_eq!(cache.metrics.hits(), 1);
200
201        // Second get = still hit
202        let r2 = cache.get(&"query".into());
203        assert!(r2.is_some());
204        assert_eq!(cache.metrics.hits(), 2);
205        assert_eq!(cache.metrics.misses(), 0);
206    }
207}