agentic_evolve_core/cache/
lru.rs1use std::collections::HashMap;
4use std::hash::Hash;
5use std::sync::RwLock;
6use std::time::{Duration, Instant};
7
8use serde::{Deserialize, Serialize};
9
10use super::metrics::CacheMetrics;
11
12struct CacheEntry<V> {
14 value: V,
15 inserted_at: Instant,
16 last_accessed: Instant,
17}
18
19pub struct LruCache<K, V> {
23 entries: RwLock<HashMap<K, CacheEntry<V>>>,
24 max_size: usize,
25 ttl: Duration,
26 metrics: CacheMetrics,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct LruCacheConfig {
32 pub max_size: usize,
33 pub ttl_secs: u64,
34}
35
36impl Default for LruCacheConfig {
37 fn default() -> Self {
38 Self {
39 max_size: 1024,
40 ttl_secs: 300,
41 }
42 }
43}
44
45impl<K, V> LruCache<K, V>
46where
47 K: Eq + Hash + Clone,
48 V: Clone,
49{
50 pub fn new(max_size: usize, ttl: Duration) -> Self {
52 Self {
53 entries: RwLock::new(HashMap::new()),
54 max_size,
55 ttl,
56 metrics: CacheMetrics::new(),
57 }
58 }
59
60 pub fn from_config(config: &LruCacheConfig) -> Self {
62 Self::new(config.max_size, Duration::from_secs(config.ttl_secs))
63 }
64
65 pub fn get(&self, key: &K) -> Option<V> {
67 let mut map = self.entries.write().unwrap();
68 match map.get_mut(key) {
69 Some(entry) if entry.inserted_at.elapsed() < self.ttl => {
70 entry.last_accessed = Instant::now();
71 self.metrics.record_hit();
72 Some(entry.value.clone())
73 }
74 Some(_) => {
75 map.remove(key);
77 self.metrics.record_eviction();
78 self.metrics.record_miss();
79 self.metrics.set_size(map.len());
80 None
81 }
82 None => {
83 self.metrics.record_miss();
84 None
85 }
86 }
87 }
88
89 pub fn insert(&self, key: K, value: V) {
91 let mut map = self.entries.write().unwrap();
92
93 self.evict_expired(&mut map);
95
96 if map.len() >= self.max_size && !map.contains_key(&key) {
98 self.evict_lru(&mut map);
99 }
100
101 let now = Instant::now();
102 map.insert(
103 key,
104 CacheEntry {
105 value,
106 inserted_at: now,
107 last_accessed: now,
108 },
109 );
110 self.metrics.set_size(map.len());
111 }
112
113 pub fn invalidate(&self, key: &K) -> bool {
115 let mut map = self.entries.write().unwrap();
116 let removed = map.remove(key).is_some();
117 if removed {
118 self.metrics.record_eviction();
119 }
120 self.metrics.set_size(map.len());
121 removed
122 }
123
124 pub fn clear(&self) {
126 let mut map = self.entries.write().unwrap();
127 let count = map.len();
128 map.clear();
129 for _ in 0..count {
130 self.metrics.record_eviction();
131 }
132 self.metrics.set_size(0);
133 }
134
135 pub fn contains(&self, key: &K) -> bool {
137 let map = self.entries.read().unwrap();
138 match map.get(key) {
139 Some(entry) => entry.inserted_at.elapsed() < self.ttl,
140 None => false,
141 }
142 }
143
144 pub fn len(&self) -> usize {
146 self.entries.read().unwrap().len()
147 }
148
149 pub fn is_empty(&self) -> bool {
151 self.len() == 0
152 }
153
154 pub fn metrics(&self) -> &CacheMetrics {
156 &self.metrics
157 }
158
159 fn evict_expired(&self, map: &mut HashMap<K, CacheEntry<V>>) {
162 let expired: Vec<K> = map
163 .iter()
164 .filter(|(_, e)| e.inserted_at.elapsed() >= self.ttl)
165 .map(|(k, _)| k.clone())
166 .collect();
167 for k in expired {
168 map.remove(&k);
169 self.metrics.record_eviction();
170 }
171 }
172
173 fn evict_lru(&self, map: &mut HashMap<K, CacheEntry<V>>) {
174 if let Some(lru_key) = map
175 .iter()
176 .min_by_key(|(_, e)| e.last_accessed)
177 .map(|(k, _)| k.clone())
178 {
179 map.remove(&lru_key);
180 self.metrics.record_eviction();
181 }
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use std::thread;
189
190 #[test]
191 fn insert_and_get() {
192 let cache = LruCache::new(10, Duration::from_secs(60));
193 cache.insert("a", 1);
194 assert_eq!(cache.get(&"a"), Some(1));
195 }
196
197 #[test]
198 fn missing_key_returns_none() {
199 let cache: LruCache<&str, i32> = LruCache::new(10, Duration::from_secs(60));
200 assert_eq!(cache.get(&"missing"), None);
201 }
202
203 #[test]
204 fn ttl_expiration() {
205 let cache = LruCache::new(10, Duration::from_millis(50));
206 cache.insert("x", 42);
207 assert_eq!(cache.get(&"x"), Some(42));
208 thread::sleep(Duration::from_millis(60));
209 assert_eq!(cache.get(&"x"), None);
210 }
211
212 #[test]
213 fn evict_lru_on_full() {
214 let cache = LruCache::new(2, Duration::from_secs(60));
215 cache.insert("a", 1);
216 cache.insert("b", 2);
217 let _ = cache.get(&"a");
219 cache.insert("c", 3);
220 assert_eq!(cache.get(&"b"), None);
222 assert_eq!(cache.get(&"a"), Some(1));
223 assert_eq!(cache.get(&"c"), Some(3));
224 }
225
226 #[test]
227 fn invalidate_removes_key() {
228 let cache = LruCache::new(10, Duration::from_secs(60));
229 cache.insert("k", 99);
230 assert!(cache.invalidate(&"k"));
231 assert_eq!(cache.get(&"k"), None);
232 }
233
234 #[test]
235 fn clear_empties_cache() {
236 let cache = LruCache::new(10, Duration::from_secs(60));
237 cache.insert("a", 1);
238 cache.insert("b", 2);
239 cache.clear();
240 assert!(cache.is_empty());
241 }
242
243 #[test]
244 fn contains_respects_ttl() {
245 let cache = LruCache::new(10, Duration::from_millis(50));
246 cache.insert("k", 1);
247 assert!(cache.contains(&"k"));
248 thread::sleep(Duration::from_millis(60));
249 assert!(!cache.contains(&"k"));
250 }
251
252 #[test]
253 fn metrics_track_hits_and_misses() {
254 let cache = LruCache::new(10, Duration::from_secs(60));
255 cache.insert("a", 1);
256 let _ = cache.get(&"a"); let _ = cache.get(&"b"); assert_eq!(cache.metrics().hit_count(), 1);
259 assert_eq!(cache.metrics().miss_count(), 1);
260 }
261
262 #[test]
263 fn from_config_works() {
264 let config = LruCacheConfig {
265 max_size: 5,
266 ttl_secs: 120,
267 };
268 let cache: LruCache<String, String> = LruCache::from_config(&config);
269 assert_eq!(cache.max_size, 5);
270 }
271
272 #[test]
273 fn len_tracks_insertions() {
274 let cache = LruCache::new(10, Duration::from_secs(60));
275 assert_eq!(cache.len(), 0);
276 cache.insert("a", 1);
277 assert_eq!(cache.len(), 1);
278 cache.insert("b", 2);
279 assert_eq!(cache.len(), 2);
280 }
281}