1use hashbrown::{HashMap, HashSet};
7use std::collections::VecDeque;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::RwLock;
11
12#[derive(Debug)]
15struct CacheEntry<V> {
16 value: Arc<V>,
17 inserted_at: Instant,
18 accessed_at: Instant,
19 access_count: u64,
20}
21
22#[derive(Debug)]
26struct CacheState<V> {
27 entries: HashMap<String, CacheEntry<V>>,
28 access_order: VecDeque<String>,
29}
30
31impl<V> CacheEntry<V> {
32 #[inline]
33 fn is_expired(&self, ttl: Duration) -> bool {
34 self.inserted_at.elapsed() > ttl
35 }
36
37 #[inline]
38 fn update_access(&mut self) {
39 self.accessed_at = Instant::now();
40 self.access_count += 1;
41 }
42}
43
44#[derive(Clone, Copy, Debug, Default)]
46pub struct CacheStats {
47 pub hits: u64,
49 pub misses: u64,
51 pub evictions: u64,
53 pub expirations: u64,
55}
56
57impl CacheStats {
58 #[inline]
60 pub fn hit_rate(&self) -> f64 {
61 let total = self.hits + self.misses;
62 if total == 0 {
63 0.0
64 } else {
65 (self.hits as f64 / total as f64) * 100.0
66 }
67 }
68}
69
70#[async_trait::async_trait]
72pub trait CacheObserver: Send + Sync {
73 async fn on_hit(&self, key: &str, access_count: u64);
74 async fn on_miss(&self, key: &str);
75 async fn on_evict(&self, key: &str, reason: EvictionReason);
76}
77
78#[derive(Clone, Debug, Copy, PartialEq, Eq)]
80pub enum EvictionReason {
81 Capacity,
82 Expired,
83 Manual,
84}
85
86pub struct NoopObserver;
88
89#[async_trait::async_trait]
90impl CacheObserver for NoopObserver {
91 async fn on_hit(&self, _: &str, _: u64) {}
92 async fn on_miss(&self, _: &str) {}
93 async fn on_evict(&self, _: &str, _: EvictionReason) {}
94}
95
96pub struct LruCache<V> {
98 capacity: usize,
100 ttl: Duration,
102 state: Arc<RwLock<CacheState<V>>>,
104 stats: Arc<RwLock<CacheStats>>,
106 observer: Arc<dyn CacheObserver>,
108}
109
110impl<V: Send + Sync> LruCache<V> {
111 pub fn new(capacity: usize, ttl: Duration) -> Self {
113 Self::with_observer(capacity, ttl, Arc::new(NoopObserver))
114 }
115
116 pub fn with_observer(capacity: usize, ttl: Duration, observer: Arc<dyn CacheObserver>) -> Self {
118 Self {
119 capacity,
120 ttl,
121 state: Arc::new(RwLock::new(CacheState {
122 entries: HashMap::new(),
123 access_order: VecDeque::new(),
124 })),
125 stats: Arc::new(RwLock::new(CacheStats::default())),
126 observer,
127 }
128 }
129
130 pub async fn get(&self, key: &str) -> Option<Arc<V>> {
132 enum GetOutcome<V> {
134 Hit { value: Arc<V>, access_count: u64 },
135 Miss,
136 Expired,
137 }
138
139 let outcome = {
140 let mut state = self.state.write().await;
141
142 if let Some(entry) = state.entries.get(key) {
144 if entry.is_expired(self.ttl) {
145 state.entries.remove(key);
146 state.access_order.retain(|k| k != key);
147 GetOutcome::Expired
148 } else {
149 let value = Arc::clone(&entry.value);
151 let access_count = entry.access_count;
152
153 if let Some(entry) = state.entries.get_mut(key) {
155 entry.update_access();
156 }
157
158 state.access_order.retain(|k| k != key);
160 state.access_order.push_back(key.to_string());
161
162 GetOutcome::Hit { value, access_count }
163 }
164 } else {
165 GetOutcome::Miss
166 }
167 };
168
169 match outcome {
171 GetOutcome::Hit { value, access_count } => {
172 let mut stats = self.stats.write().await;
173 stats.hits += 1;
174 self.observer.on_hit(key, access_count).await;
175 Some(value)
176 }
177 GetOutcome::Expired => {
178 let mut stats = self.stats.write().await;
179 stats.expirations += 1;
180 stats.misses += 1;
181 self.observer.on_evict(key, EvictionReason::Expired).await;
182 None
183 }
184 GetOutcome::Miss => {
185 let mut stats = self.stats.write().await;
186 stats.misses += 1;
187 self.observer.on_miss(key).await;
188 None
189 }
190 }
191 }
192
193 pub async fn get_owned(&self, key: &str) -> Option<V>
195 where
196 V: Clone,
197 {
198 self.get(key).await.map(|arc| V::clone(&arc))
199 }
200
201 pub async fn get_arc(&self, key: &str) -> Option<Arc<V>> {
203 self.get(key).await
204 }
205
206 pub async fn insert(&self, key: String, value: V) {
208 self.insert_arc(key, Arc::new(value)).await;
209 }
210
211 pub async fn insert_arc(&self, key: String, value: Arc<V>) {
213 let capacity_evicted = {
214 let mut state = self.state.write().await;
215 let mut evicted: Option<String> = None;
216
217 if state.entries.len() >= self.capacity && !state.entries.contains_key(&key) {
219 if let Some(lru_key) = state.access_order.pop_front() {
220 state.entries.remove(&lru_key);
221 evicted = Some(lru_key);
222 }
223 }
224
225 let entry = CacheEntry {
226 value: Arc::clone(&value),
227 inserted_at: Instant::now(),
228 accessed_at: Instant::now(),
229 access_count: 0,
230 };
231
232 state.entries.insert(key.clone(), entry);
233 state.access_order.retain(|existing| existing != &key);
234 state.access_order.push_back(key);
235 evicted
236 };
237
238 if let Some(evicted_key) = capacity_evicted {
240 self.observer.on_evict(&evicted_key, EvictionReason::Capacity).await;
241 let mut stats = self.stats.write().await;
242 stats.evictions += 1;
243 }
244 }
245
246 pub async fn remove(&self, key: &str) -> Option<Arc<V>> {
248 let removed = {
249 let mut state = self.state.write().await;
250 state.access_order.retain(|k| k != key);
251 state.entries.remove(key).map(|e| e.value)
252 };
253
254 if removed.is_some() {
255 self.observer.on_evict(key, EvictionReason::Manual).await;
256 }
257 removed
258 }
259
260 pub async fn clear(&self) {
262 let mut state = self.state.write().await;
263 state.entries.clear();
264 state.access_order.clear();
265 let mut stats = self.stats.write().await;
266 *stats = CacheStats::default();
267 }
268
269 pub async fn stats(&self) -> CacheStats {
271 *self.stats.read().await
272 }
273
274 pub async fn len(&self) -> usize {
276 self.state.read().await.entries.len()
277 }
278
279 pub async fn is_empty(&self) -> bool {
281 self.state.read().await.entries.is_empty()
282 }
283
284 pub async fn keys(&self) -> Vec<String> {
286 let state = self.state.read().await;
287 state
288 .entries
289 .iter()
290 .filter(|(_, entry)| !entry.is_expired(self.ttl))
291 .map(|(k, _)| k.clone())
292 .collect()
293 }
294
295 pub async fn prune_expired(&self) {
297 let expired = {
298 let mut state = self.state.write().await;
299
300 let mut expired = Vec::new();
301 state.entries.retain(|key, entry| {
302 let keep = !entry.is_expired(self.ttl);
303 if !keep {
304 expired.push(key.clone());
305 }
306 keep
307 });
308
309 if !expired.is_empty() {
310 let expired_set: HashSet<_> = expired.iter().cloned().collect();
311 state.access_order.retain(|k| !expired_set.contains(k));
312 }
313
314 expired
315 };
316
317 if expired.is_empty() {
318 return;
319 }
320
321 for key in &expired {
322 self.observer.on_evict(key, EvictionReason::Expired).await;
323 }
324
325 let mut stats = self.stats.write().await;
326 stats.expirations += expired.len() as u64;
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[tokio::test]
335 async fn test_basic_operations() {
336 let cache: LruCache<String> = LruCache::new(3, Duration::from_secs(60));
337
338 cache.insert_arc("a".into(), Arc::new("value_a".into())).await;
339 cache.insert_arc("b".into(), Arc::new("value_b".into())).await;
340
341 assert_eq!(cache.get("a").await.map(|v| (*v).clone()), Some("value_a".into()));
342 assert_eq!(cache.get("b").await.map(|v| (*v).clone()), Some("value_b".into()));
343 assert_eq!(cache.get("c").await, None);
344 }
345
346 #[tokio::test]
347 async fn test_capacity_eviction() {
348 let cache: LruCache<i32> = LruCache::new(2, Duration::from_secs(60));
349
350 cache.insert("a".into(), 1).await;
351 cache.insert("b".into(), 2).await;
352 cache.insert("c".into(), 3).await; assert_eq!(cache.get("a").await, None);
355 assert_eq!(cache.get("b").await.map(|v| *v), Some(2));
356 assert_eq!(cache.get("c").await.map(|v| *v), Some(3));
357 }
358
359 #[tokio::test]
360 async fn test_ttl_expiration() {
361 let cache: LruCache<String> = LruCache::new(10, Duration::from_millis(50));
362
363 cache.insert_arc("a".into(), Arc::new("value".into())).await;
364 assert_eq!(cache.get("a").await.map(|v| (*v).clone()), Some("value".into()));
365
366 tokio::time::sleep(Duration::from_millis(100)).await;
367 assert_eq!(cache.get("a").await, None);
368 }
369
370 #[tokio::test]
371 async fn test_stats() {
372 let cache: LruCache<String> = LruCache::new(10, Duration::from_secs(60));
373
374 cache.insert_arc("a".into(), Arc::new("value".into())).await;
375 cache.get("a").await; cache.get("b").await; let stats = cache.stats().await;
379 assert_eq!(stats.hits, 1);
380 assert_eq!(stats.misses, 1);
381 }
382
383 #[tokio::test]
384 async fn test_prune_expired() {
385 let cache: LruCache<i32> = LruCache::new(10, Duration::from_millis(50));
386
387 cache.insert("a".into(), 1).await;
388 cache.insert("b".into(), 2).await;
389 tokio::time::sleep(Duration::from_millis(100)).await;
390 cache.prune_expired().await;
391
392 assert_eq!(cache.len().await, 0);
393 }
394
395 #[tokio::test]
396 async fn insert_arc_avoids_clone() {
397 let cache = LruCache::new(2, Duration::from_secs(60));
398 let v = Arc::new(42);
399 cache.insert_arc("k1".to_string(), Arc::clone(&v)).await;
400 let got = cache.get("k1").await;
401 assert_eq!(got.unwrap().as_ref(), &42);
402 }
403}