agentic_forge_core/cache/
lru.rs1use 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); 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()); let _ = cache.get(&"miss".into()); let _ = cache.get(&"key".into()); 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 let r1 = cache.get(&"query".into());
198 assert!(r1.is_some());
199 assert_eq!(cache.metrics.hits(), 1);
200
201 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}