1use std::collections::HashMap;
7use std::hash::Hash;
8use std::sync::RwLock;
9use std::time::{Duration, Instant};
10
11use tracing::{debug, warn};
12
13#[derive(Debug, Clone)]
15struct CacheEntry<V> {
16 value: V,
17 inserted_at: Instant,
18 ttl: Duration,
19}
20
21impl<V> CacheEntry<V> {
22 fn new(value: V, ttl: Duration) -> Self {
24 Self {
25 value,
26 inserted_at: Instant::now(),
27 ttl,
28 }
29 }
30
31 fn is_expired(&self) -> bool {
33 self.inserted_at.elapsed() > self.ttl
34 }
35
36 fn is_stale(&self) -> bool {
39 self.inserted_at.elapsed() > (self.ttl * 3 / 4)
40 }
41
42 fn age(&self) -> Duration {
44 self.inserted_at.elapsed()
45 }
46}
47
48pub struct TtlCache<K, V> {
72 entries: RwLock<HashMap<K, CacheEntry<V>>>,
73 default_ttl: Duration,
74 max_capacity: usize,
77}
78
79const DEFAULT_MAX_CAPACITY: usize = 1024;
81
82impl<K, V> TtlCache<K, V>
83where
84 K: Eq + Hash + Clone + std::fmt::Debug,
85 V: Clone,
86{
87 pub fn new(default_ttl: Duration) -> Self {
89 Self {
90 entries: RwLock::new(HashMap::new()),
91 default_ttl,
92 max_capacity: DEFAULT_MAX_CAPACITY,
93 }
94 }
95
96 pub fn with_max_capacity(default_ttl: Duration, max_capacity: usize) -> Self {
98 Self {
99 entries: RwLock::new(HashMap::new()),
100 default_ttl,
101 max_capacity,
102 }
103 }
104
105 pub fn get(&self, key: &K) -> Option<V> {
110 let entries = match self.entries.read() {
111 Ok(guard) => guard,
112 Err(poisoned) => {
113 warn!("Cache read lock poisoned, recovering");
114 poisoned.into_inner()
115 }
116 };
117 let entry = entries.get(key)?;
118
119 if entry.is_expired() {
120 debug!(
121 hit = false,
122 ?key,
123 age_secs = entry.age().as_secs(),
124 "cache lookup (expired)"
125 );
126 None
127 } else {
128 debug!(hit = true, ?key, "cache lookup");
129 Some(entry.value.clone())
130 }
131 }
132
133 pub fn get_stale(&self, key: &K) -> Option<V> {
138 let entries = match self.entries.read() {
139 Ok(guard) => guard,
140 Err(poisoned) => {
141 warn!("Cache read lock poisoned, recovering");
142 poisoned.into_inner()
143 }
144 };
145 entries.get(key).map(|entry| {
146 if entry.is_expired() {
147 debug!(
148 ?key,
149 age_secs = entry.age().as_secs(),
150 "Serving stale cache entry"
151 );
152 }
153 entry.value.clone()
154 })
155 }
156
157 pub fn needs_refresh(&self, key: &K) -> bool {
161 let entries = match self.entries.read() {
162 Ok(guard) => guard,
163 Err(poisoned) => {
164 warn!("Cache read lock poisoned, recovering");
165 poisoned.into_inner()
166 }
167 };
168
169 entries.get(key).is_some_and(|entry| entry.is_stale())
170 }
171
172 pub fn insert(&self, key: K, value: V) {
174 self.insert_with_ttl(key, value, self.default_ttl);
175 }
176
177 pub fn insert_with_ttl(&self, key: K, value: V, ttl: Duration) {
182 let mut entries = match self.entries.write() {
183 Ok(guard) => guard,
184 Err(poisoned) => {
185 warn!("Cache write lock poisoned, recovering");
186 poisoned.into_inner()
187 }
188 };
189
190 if entries.len() >= self.max_capacity && !entries.contains_key(&key) {
192 let before = entries.len();
194 entries.retain(|_, entry| !entry.is_expired());
195 let removed = before - entries.len();
196 if removed > 0 {
197 debug!(removed, "Evicted expired entries to make room");
198 }
199
200 if entries.len() >= self.max_capacity {
202 if let Some(oldest_key) = entries
203 .iter()
204 .max_by_key(|(_, entry)| entry.age())
205 .map(|(k, _)| k.clone())
206 {
207 entries.remove(&oldest_key);
208 debug!(?oldest_key, "Evicted oldest entry to make room");
209 }
210 }
211 }
212
213 debug!(?key, ttl_secs = ttl.as_secs(), "Inserting cache entry");
214 entries.insert(key, CacheEntry::new(value, ttl));
215 }
216
217 pub fn remove(&self, key: &K) -> Option<V> {
219 let mut entries = match self.entries.write() {
220 Ok(guard) => guard,
221 Err(poisoned) => {
222 warn!("Cache write lock poisoned, recovering");
223 poisoned.into_inner()
224 }
225 };
226 entries.remove(key).map(|e| e.value)
227 }
228
229 pub fn cleanup(&self) {
233 let mut entries = match self.entries.write() {
234 Ok(guard) => guard,
235 Err(poisoned) => {
236 warn!("Cache write lock poisoned, recovering");
237 poisoned.into_inner()
238 }
239 };
240 let before = entries.len();
241 entries.retain(|_, entry| !entry.is_expired());
242 let removed = before - entries.len();
243 if removed > 0 {
244 debug!(removed, remaining = entries.len(), "Cache cleanup complete");
245 }
246 }
247
248 pub fn len(&self) -> usize {
250 match self.entries.read() {
251 Ok(entries) => entries.len(),
252 Err(poisoned) => {
253 warn!("Cache read lock poisoned, recovering");
254 poisoned.into_inner().len()
255 }
256 }
257 }
258
259 pub fn is_empty(&self) -> bool {
261 self.len() == 0
262 }
263
264 pub fn clear(&self) {
266 let mut entries = match self.entries.write() {
267 Ok(guard) => guard,
268 Err(poisoned) => {
269 warn!("Cache write lock poisoned, recovering");
270 poisoned.into_inner()
271 }
272 };
273 entries.clear();
274 }
275}
276
277pub struct SingleValueCache<V> {
282 entry: RwLock<Option<CacheEntry<V>>>,
283 ttl: Duration,
284}
285
286impl<V: Clone> SingleValueCache<V> {
287 pub fn new(ttl: Duration) -> Self {
289 Self {
290 entry: RwLock::new(None),
291 ttl,
292 }
293 }
294
295 pub fn get(&self) -> Option<V> {
297 let guard = match self.entry.read() {
298 Ok(guard) => guard,
299 Err(poisoned) => {
300 warn!("SingleValueCache read lock poisoned, recovering");
301 poisoned.into_inner()
302 }
303 };
304 let entry = guard.as_ref()?;
305
306 if entry.is_expired() {
307 None
308 } else {
309 Some(entry.value.clone())
310 }
311 }
312
313 pub fn get_stale(&self) -> Option<V> {
315 let guard = match self.entry.read() {
316 Ok(guard) => guard,
317 Err(poisoned) => {
318 warn!("SingleValueCache read lock poisoned, recovering");
319 poisoned.into_inner()
320 }
321 };
322 guard.as_ref().map(|e| e.value.clone())
323 }
324
325 pub fn needs_refresh(&self) -> bool {
327 let guard = match self.entry.read() {
328 Ok(guard) => guard,
329 Err(poisoned) => {
330 warn!("SingleValueCache read lock poisoned, recovering");
331 poisoned.into_inner()
332 }
333 };
334
335 match guard.as_ref() {
336 Some(e) => e.is_stale(),
337 None => true,
338 }
339 }
340
341 pub fn has_value(&self) -> bool {
343 let guard = match self.entry.read() {
344 Ok(guard) => guard,
345 Err(poisoned) => {
346 warn!("SingleValueCache read lock poisoned, recovering");
347 poisoned.into_inner()
348 }
349 };
350 guard.is_some()
351 }
352
353 pub fn set(&self, value: V) {
355 let mut guard = match self.entry.write() {
356 Ok(guard) => guard,
357 Err(poisoned) => {
358 warn!("SingleValueCache write lock poisoned, recovering");
359 poisoned.into_inner()
360 }
361 };
362 *guard = Some(CacheEntry::new(value, self.ttl));
363 }
364
365 pub fn clear(&self) {
367 let mut guard = match self.entry.write() {
368 Ok(guard) => guard,
369 Err(poisoned) => {
370 warn!("SingleValueCache write lock poisoned, recovering");
371 poisoned.into_inner()
372 }
373 };
374 *guard = None;
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_cache_insert_and_get() {
384 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
385
386 cache.insert("key".to_string(), "value".to_string());
387
388 assert_eq!(cache.get(&"key".to_string()), Some("value".to_string()));
389 }
390
391 #[test]
392 fn test_cache_get_missing_key() {
393 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
394
395 assert_eq!(cache.get(&"missing".to_string()), None);
396 }
397
398 #[test]
399 fn test_cache_expiration() {
400 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(10));
401
402 cache.insert("key".to_string(), "value".to_string());
403 assert_eq!(cache.get(&"key".to_string()), Some("value".to_string()));
404
405 std::thread::sleep(Duration::from_millis(20));
407
408 assert_eq!(cache.get(&"key".to_string()), None);
409 }
410
411 #[test]
412 fn test_cache_get_stale_after_expiration() {
413 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(10));
414
415 cache.insert("key".to_string(), "value".to_string());
416
417 std::thread::sleep(Duration::from_millis(20));
419
420 assert_eq!(cache.get(&"key".to_string()), None);
422 assert_eq!(
424 cache.get_stale(&"key".to_string()),
425 Some("value".to_string())
426 );
427 }
428
429 #[test]
430 fn test_cache_remove() {
431 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
432
433 cache.insert("key".to_string(), "value".to_string());
434 assert!(cache.get(&"key".to_string()).is_some());
435
436 cache.remove(&"key".to_string());
437 assert!(cache.get(&"key".to_string()).is_none());
438 }
439
440 #[test]
441 fn test_cache_cleanup() {
442 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(10));
443
444 cache.insert("key1".to_string(), "value1".to_string());
445 cache.insert("key2".to_string(), "value2".to_string());
446
447 std::thread::sleep(Duration::from_millis(20));
449
450 cache.insert_with_ttl(
452 "key3".to_string(),
453 "value3".to_string(),
454 Duration::from_secs(3600),
455 );
456
457 assert_eq!(cache.len(), 3);
458
459 cache.cleanup();
460
461 assert_eq!(cache.len(), 1);
463 assert_eq!(cache.get(&"key3".to_string()), Some("value3".to_string()));
464 }
465
466 #[test]
467 fn test_cache_clear() {
468 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
469
470 cache.insert("key1".to_string(), "value1".to_string());
471 cache.insert("key2".to_string(), "value2".to_string());
472
473 assert_eq!(cache.len(), 2);
474
475 cache.clear();
476
477 assert_eq!(cache.len(), 0);
478 assert!(cache.is_empty());
479 }
480
481 #[test]
482 fn test_single_value_cache() {
483 let cache: SingleValueCache<String> = SingleValueCache::new(Duration::from_secs(3600));
484
485 assert!(!cache.has_value());
486 assert!(cache.get().is_none());
487
488 cache.set("value".to_string());
489
490 assert!(cache.has_value());
491 assert_eq!(cache.get(), Some("value".to_string()));
492 }
493
494 #[test]
495 fn test_single_value_cache_expiration() {
496 let cache: SingleValueCache<String> = SingleValueCache::new(Duration::from_millis(10));
497
498 cache.set("value".to_string());
499 assert_eq!(cache.get(), Some("value".to_string()));
500
501 std::thread::sleep(Duration::from_millis(20));
503
504 assert!(cache.get().is_none());
505 assert_eq!(cache.get_stale(), Some("value".to_string()));
507 }
508
509 #[test]
510 fn test_needs_refresh() {
511 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(1000));
514
515 cache.insert("key".to_string(), "value".to_string());
516
517 assert!(!cache.needs_refresh(&"key".to_string()));
519
520 std::thread::sleep(Duration::from_millis(800));
522
523 assert!(cache.needs_refresh(&"key".to_string()));
525
526 assert!(cache.get(&"key".to_string()).is_some());
528 }
529}