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 ?key,
122 age_secs = entry.age().as_secs(),
123 "Cache entry expired"
124 );
125 None
126 } else {
127 Some(entry.value.clone())
128 }
129 }
130
131 pub fn get_stale(&self, key: &K) -> Option<V> {
136 let entries = match self.entries.read() {
137 Ok(guard) => guard,
138 Err(poisoned) => {
139 warn!("Cache read lock poisoned, recovering");
140 poisoned.into_inner()
141 }
142 };
143 entries.get(key).map(|entry| {
144 if entry.is_expired() {
145 debug!(
146 ?key,
147 age_secs = entry.age().as_secs(),
148 "Serving stale cache entry"
149 );
150 }
151 entry.value.clone()
152 })
153 }
154
155 pub fn needs_refresh(&self, key: &K) -> bool {
159 let entries = match self.entries.read() {
160 Ok(guard) => guard,
161 Err(poisoned) => {
162 warn!("Cache read lock poisoned, recovering");
163 poisoned.into_inner()
164 }
165 };
166
167 entries.get(key).is_some_and(|entry| entry.is_stale())
168 }
169
170 pub fn insert(&self, key: K, value: V) {
172 self.insert_with_ttl(key, value, self.default_ttl);
173 }
174
175 pub fn insert_with_ttl(&self, key: K, value: V, ttl: Duration) {
180 let mut entries = match self.entries.write() {
181 Ok(guard) => guard,
182 Err(poisoned) => {
183 warn!("Cache write lock poisoned, recovering");
184 poisoned.into_inner()
185 }
186 };
187
188 if entries.len() >= self.max_capacity && !entries.contains_key(&key) {
190 let before = entries.len();
192 entries.retain(|_, entry| !entry.is_expired());
193 let removed = before - entries.len();
194 if removed > 0 {
195 debug!(removed, "Evicted expired entries to make room");
196 }
197
198 if entries.len() >= self.max_capacity {
200 if let Some(oldest_key) = entries
201 .iter()
202 .max_by_key(|(_, entry)| entry.age())
203 .map(|(k, _)| k.clone())
204 {
205 entries.remove(&oldest_key);
206 debug!(?oldest_key, "Evicted oldest entry to make room");
207 }
208 }
209 }
210
211 debug!(?key, ttl_secs = ttl.as_secs(), "Inserting cache entry");
212 entries.insert(key, CacheEntry::new(value, ttl));
213 }
214
215 pub fn remove(&self, key: &K) -> Option<V> {
217 let mut entries = match self.entries.write() {
218 Ok(guard) => guard,
219 Err(poisoned) => {
220 warn!("Cache write lock poisoned, recovering");
221 poisoned.into_inner()
222 }
223 };
224 entries.remove(key).map(|e| e.value)
225 }
226
227 pub fn cleanup(&self) {
231 let mut entries = match self.entries.write() {
232 Ok(guard) => guard,
233 Err(poisoned) => {
234 warn!("Cache write lock poisoned, recovering");
235 poisoned.into_inner()
236 }
237 };
238 let before = entries.len();
239 entries.retain(|_, entry| !entry.is_expired());
240 let removed = before - entries.len();
241 if removed > 0 {
242 debug!(removed, remaining = entries.len(), "Cache cleanup complete");
243 }
244 }
245
246 pub fn len(&self) -> usize {
248 match self.entries.read() {
249 Ok(entries) => entries.len(),
250 Err(poisoned) => {
251 warn!("Cache read lock poisoned, recovering");
252 poisoned.into_inner().len()
253 }
254 }
255 }
256
257 pub fn is_empty(&self) -> bool {
259 self.len() == 0
260 }
261
262 pub fn clear(&self) {
264 let mut entries = match self.entries.write() {
265 Ok(guard) => guard,
266 Err(poisoned) => {
267 warn!("Cache write lock poisoned, recovering");
268 poisoned.into_inner()
269 }
270 };
271 entries.clear();
272 }
273}
274
275pub struct SingleValueCache<V> {
280 entry: RwLock<Option<CacheEntry<V>>>,
281 ttl: Duration,
282}
283
284impl<V: Clone> SingleValueCache<V> {
285 pub fn new(ttl: Duration) -> Self {
287 Self {
288 entry: RwLock::new(None),
289 ttl,
290 }
291 }
292
293 pub fn get(&self) -> Option<V> {
295 let guard = match self.entry.read() {
296 Ok(guard) => guard,
297 Err(poisoned) => {
298 warn!("SingleValueCache read lock poisoned, recovering");
299 poisoned.into_inner()
300 }
301 };
302 let entry = guard.as_ref()?;
303
304 if entry.is_expired() {
305 None
306 } else {
307 Some(entry.value.clone())
308 }
309 }
310
311 pub fn get_stale(&self) -> Option<V> {
313 let guard = match self.entry.read() {
314 Ok(guard) => guard,
315 Err(poisoned) => {
316 warn!("SingleValueCache read lock poisoned, recovering");
317 poisoned.into_inner()
318 }
319 };
320 guard.as_ref().map(|e| e.value.clone())
321 }
322
323 pub fn needs_refresh(&self) -> bool {
325 let guard = match self.entry.read() {
326 Ok(guard) => guard,
327 Err(poisoned) => {
328 warn!("SingleValueCache read lock poisoned, recovering");
329 poisoned.into_inner()
330 }
331 };
332
333 match guard.as_ref() {
334 Some(e) => e.is_stale(),
335 None => true,
336 }
337 }
338
339 pub fn has_value(&self) -> bool {
341 let guard = match self.entry.read() {
342 Ok(guard) => guard,
343 Err(poisoned) => {
344 warn!("SingleValueCache read lock poisoned, recovering");
345 poisoned.into_inner()
346 }
347 };
348 guard.is_some()
349 }
350
351 pub fn set(&self, value: V) {
353 let mut guard = match self.entry.write() {
354 Ok(guard) => guard,
355 Err(poisoned) => {
356 warn!("SingleValueCache write lock poisoned, recovering");
357 poisoned.into_inner()
358 }
359 };
360 *guard = Some(CacheEntry::new(value, self.ttl));
361 }
362
363 pub fn clear(&self) {
365 let mut guard = match self.entry.write() {
366 Ok(guard) => guard,
367 Err(poisoned) => {
368 warn!("SingleValueCache write lock poisoned, recovering");
369 poisoned.into_inner()
370 }
371 };
372 *guard = None;
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_cache_insert_and_get() {
382 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
383
384 cache.insert("key".to_string(), "value".to_string());
385
386 assert_eq!(cache.get(&"key".to_string()), Some("value".to_string()));
387 }
388
389 #[test]
390 fn test_cache_get_missing_key() {
391 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
392
393 assert_eq!(cache.get(&"missing".to_string()), None);
394 }
395
396 #[test]
397 fn test_cache_expiration() {
398 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(10));
399
400 cache.insert("key".to_string(), "value".to_string());
401 assert_eq!(cache.get(&"key".to_string()), Some("value".to_string()));
402
403 std::thread::sleep(Duration::from_millis(20));
405
406 assert_eq!(cache.get(&"key".to_string()), None);
407 }
408
409 #[test]
410 fn test_cache_get_stale_after_expiration() {
411 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(10));
412
413 cache.insert("key".to_string(), "value".to_string());
414
415 std::thread::sleep(Duration::from_millis(20));
417
418 assert_eq!(cache.get(&"key".to_string()), None);
420 assert_eq!(
422 cache.get_stale(&"key".to_string()),
423 Some("value".to_string())
424 );
425 }
426
427 #[test]
428 fn test_cache_remove() {
429 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
430
431 cache.insert("key".to_string(), "value".to_string());
432 assert!(cache.get(&"key".to_string()).is_some());
433
434 cache.remove(&"key".to_string());
435 assert!(cache.get(&"key".to_string()).is_none());
436 }
437
438 #[test]
439 fn test_cache_cleanup() {
440 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(10));
441
442 cache.insert("key1".to_string(), "value1".to_string());
443 cache.insert("key2".to_string(), "value2".to_string());
444
445 std::thread::sleep(Duration::from_millis(20));
447
448 cache.insert_with_ttl(
450 "key3".to_string(),
451 "value3".to_string(),
452 Duration::from_secs(3600),
453 );
454
455 assert_eq!(cache.len(), 3);
456
457 cache.cleanup();
458
459 assert_eq!(cache.len(), 1);
461 assert_eq!(cache.get(&"key3".to_string()), Some("value3".to_string()));
462 }
463
464 #[test]
465 fn test_cache_clear() {
466 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_secs(3600));
467
468 cache.insert("key1".to_string(), "value1".to_string());
469 cache.insert("key2".to_string(), "value2".to_string());
470
471 assert_eq!(cache.len(), 2);
472
473 cache.clear();
474
475 assert_eq!(cache.len(), 0);
476 assert!(cache.is_empty());
477 }
478
479 #[test]
480 fn test_single_value_cache() {
481 let cache: SingleValueCache<String> = SingleValueCache::new(Duration::from_secs(3600));
482
483 assert!(!cache.has_value());
484 assert!(cache.get().is_none());
485
486 cache.set("value".to_string());
487
488 assert!(cache.has_value());
489 assert_eq!(cache.get(), Some("value".to_string()));
490 }
491
492 #[test]
493 fn test_single_value_cache_expiration() {
494 let cache: SingleValueCache<String> = SingleValueCache::new(Duration::from_millis(10));
495
496 cache.set("value".to_string());
497 assert_eq!(cache.get(), Some("value".to_string()));
498
499 std::thread::sleep(Duration::from_millis(20));
501
502 assert!(cache.get().is_none());
503 assert_eq!(cache.get_stale(), Some("value".to_string()));
505 }
506
507 #[test]
508 fn test_needs_refresh() {
509 let cache: TtlCache<String, String> = TtlCache::new(Duration::from_millis(1000));
512
513 cache.insert("key".to_string(), "value".to_string());
514
515 assert!(!cache.needs_refresh(&"key".to_string()));
517
518 std::thread::sleep(Duration::from_millis(800));
520
521 assert!(cache.needs_refresh(&"key".to_string()));
523
524 assert!(cache.get(&"key".to_string()).is_some());
526 }
527}