1use dashmap::DashMap;
2use std::sync::{Arc, RwLock};
3use std::time::{Duration, Instant};
4use tracing::{debug, warn};
5
6#[derive(Clone, Debug)]
7pub struct CacheEntry {
8 pub data: serde_json::Value,
9 pub expires_at: Instant,
10 pub last_access: Instant,
11}
12
13#[derive(Debug, Clone)]
14pub struct CacheStats {
15 pub total_requests: u64,
16 pub cache_hits: u64,
17 pub cache_misses: u64,
18 pub hit_ratio: f64,
19 pub cache_size: usize,
20}
21
22#[derive(Clone)]
23pub struct YnabCache {
24 cache: Arc<DashMap<String, CacheEntry>>,
25 stats: Arc<RwLock<CacheStats>>,
26 capacity: usize,
27}
28
29impl YnabCache {
30 pub fn new(capacity: usize) -> Self {
31 Self {
32 cache: Arc::new(DashMap::new()),
33 stats: Arc::new(RwLock::new(CacheStats {
34 total_requests: 0,
35 cache_hits: 0,
36 cache_misses: 0,
37 hit_ratio: 0.0,
38 cache_size: 0,
39 })),
40 capacity,
41 }
42 }
43
44 pub fn get(&self, key: &str) -> Option<serde_json::Value> {
45 if let Ok(mut stats) = self.stats.write() {
47 stats.total_requests += 1;
48 }
49
50 if let Some(mut entry) = self.cache.get_mut(key) {
51 if entry.expires_at > Instant::now() {
52 entry.last_access = Instant::now();
53
54 if let Ok(mut stats) = self.stats.write() {
56 stats.cache_hits += 1;
57 stats.hit_ratio = stats.cache_hits as f64 / stats.total_requests as f64;
58 }
59
60 debug!(cache_key = key, "Cache hit");
61 Some(entry.data.clone())
62 } else {
63 drop(entry); self.cache.remove(key);
66
67 if let Ok(mut stats) = self.stats.write() {
69 stats.cache_misses += 1;
70 stats.cache_size = self.cache.len();
71 stats.hit_ratio = stats.cache_hits as f64 / stats.total_requests as f64;
72 }
73
74 debug!(cache_key = key, "Cache miss (expired)");
75 None
76 }
77 } else {
78 if let Ok(mut stats) = self.stats.write() {
80 stats.cache_misses += 1;
81 stats.hit_ratio = stats.cache_hits as f64 / stats.total_requests as f64;
82 }
83
84 debug!(cache_key = key, "Cache miss (not found)");
85 None
86 }
87 }
88
89 pub fn set(&self, key: String, data: serde_json::Value, ttl: Duration) {
90 if self.cache.len() >= self.capacity {
92 self.evict_expired_or_oldest();
93 }
94
95 let entry = CacheEntry {
96 data,
97 expires_at: Instant::now() + ttl,
98 last_access: Instant::now(),
99 };
100
101 self.cache.insert(key.clone(), entry);
102
103 if let Ok(mut stats) = self.stats.write() {
105 stats.cache_size = self.cache.len();
106 }
107
108 debug!(cache_key = key, ttl_secs = ttl.as_secs(), "Data cached");
109 }
110
111 pub fn clear(&self) {
112 self.cache.clear();
113
114 if let Ok(mut stats) = self.stats.write() {
115 stats.cache_size = 0;
116 }
117
118 debug!("Cache cleared");
119 }
120
121 pub fn get_stats(&self) -> CacheStats {
122 if let Ok(stats) = self.stats.read() {
123 stats.clone()
124 } else {
125 CacheStats {
126 total_requests: 0,
127 cache_hits: 0,
128 cache_misses: 0,
129 hit_ratio: 0.0,
130 cache_size: self.cache.len(),
131 }
132 }
133 }
134
135 pub fn cleanup_expired(&self) -> usize {
136 let now = Instant::now();
137 let mut removed = 0;
138
139 let expired_keys: Vec<String> = self
141 .cache
142 .iter()
143 .filter_map(|entry| {
144 if entry.value().expires_at <= now {
145 Some(entry.key().clone())
146 } else {
147 None
148 }
149 })
150 .collect();
151
152 for key in expired_keys {
154 if self.cache.remove(&key).is_some() {
155 removed += 1;
156 }
157 }
158
159 if removed > 0 {
160 if let Ok(mut stats) = self.stats.write() {
162 stats.cache_size = self.cache.len();
163 }
164
165 debug!(
166 expired_entries = removed,
167 "Cleaned up expired cache entries"
168 );
169 }
170
171 removed
172 }
173
174 fn evict_expired_or_oldest(&self) {
175 if self.cleanup_expired() > 0 {
177 return;
178 }
179
180 if let Some(oldest_key) = self.find_oldest_key() {
182 if self.cache.remove(&oldest_key).is_some() {
183 warn!(
184 cache_key = oldest_key,
185 "Evicted oldest cache entry to make room"
186 );
187
188 if let Ok(mut stats) = self.stats.write() {
189 stats.cache_size = self.cache.len();
190 }
191 }
192 }
193 }
194
195 fn find_oldest_key(&self) -> Option<String> {
196 let mut oldest_key: Option<String> = None;
197 let mut oldest_time = Instant::now();
198
199 for entry in self.cache.iter() {
200 if entry.value().last_access < oldest_time {
201 oldest_time = entry.value().last_access;
202 oldest_key = Some(entry.key().clone());
203 }
204 }
205
206 oldest_key
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use serde_json::json;
214 use std::thread;
215 use std::time::Duration;
216
217 #[test]
218 fn test_cache_creation() {
219 let cache = YnabCache::new(100);
220 let stats = cache.get_stats();
221
222 assert_eq!(stats.total_requests, 0);
223 assert_eq!(stats.cache_hits, 0);
224 assert_eq!(stats.cache_misses, 0);
225 assert_eq!(stats.hit_ratio, 0.0);
226 assert_eq!(stats.cache_size, 0);
227 assert_eq!(cache.capacity, 100);
228 }
229
230 #[test]
231 fn test_cache_entry_creation() {
232 let data = json!({"key": "value"});
233 let now = Instant::now();
234 let entry = CacheEntry {
235 data: data.clone(),
236 expires_at: now + Duration::from_secs(60),
237 last_access: now,
238 };
239
240 assert_eq!(entry.data, data);
241 assert!(entry.expires_at > now);
242 assert_eq!(entry.last_access, now);
243 }
244
245 #[test]
246 fn test_cache_stats_creation() {
247 let stats = CacheStats {
248 total_requests: 10,
249 cache_hits: 8,
250 cache_misses: 2,
251 hit_ratio: 0.8,
252 cache_size: 5,
253 };
254
255 assert_eq!(stats.total_requests, 10);
256 assert_eq!(stats.cache_hits, 8);
257 assert_eq!(stats.cache_misses, 2);
258 assert_eq!(stats.hit_ratio, 0.8);
259 assert_eq!(stats.cache_size, 5);
260 }
261
262 #[test]
263 fn test_cache_set_and_get() {
264 let cache = YnabCache::new(10);
265 let key = "test_key".to_string();
266 let value = json!({"test": "data"});
267
268 cache.set(key.clone(), value.clone(), Duration::from_secs(60));
269
270 let retrieved = cache.get(&key);
271 assert!(retrieved.is_some());
272 assert_eq!(retrieved.unwrap(), value);
273
274 let stats = cache.get_stats();
275 assert_eq!(stats.total_requests, 1);
276 assert_eq!(stats.cache_hits, 1);
277 assert_eq!(stats.cache_misses, 0);
278 assert_eq!(stats.hit_ratio, 1.0);
279 assert_eq!(stats.cache_size, 1);
280 }
281
282 #[test]
283 fn test_cache_miss() {
284 let cache = YnabCache::new(10);
285
286 let result = cache.get("nonexistent_key");
287 assert!(result.is_none());
288
289 let stats = cache.get_stats();
290 assert_eq!(stats.total_requests, 1);
291 assert_eq!(stats.cache_hits, 0);
292 assert_eq!(stats.cache_misses, 1);
293 assert_eq!(stats.hit_ratio, 0.0);
294 assert_eq!(stats.cache_size, 0);
295 }
296
297 #[test]
298 fn test_cache_expiration() {
299 let cache = YnabCache::new(10);
300 let key = "test_key".to_string();
301 let value = json!({"test": "data"});
302
303 cache.set(key.clone(), value.clone(), Duration::from_millis(10));
305
306 let retrieved = cache.get(&key);
308 assert!(retrieved.is_some());
309
310 thread::sleep(Duration::from_millis(20));
312
313 let retrieved = cache.get(&key);
315 assert!(retrieved.is_none());
316
317 let stats = cache.get_stats();
318 assert_eq!(stats.total_requests, 2);
319 assert_eq!(stats.cache_hits, 1);
320 assert_eq!(stats.cache_misses, 1);
321 assert_eq!(stats.hit_ratio, 0.5);
322 assert_eq!(stats.cache_size, 0); }
324
325 #[test]
326 fn test_cache_clear() {
327 let cache = YnabCache::new(10);
328
329 cache.set(
331 "key1".to_string(),
332 json!({"data": 1}),
333 Duration::from_secs(60),
334 );
335 cache.set(
336 "key2".to_string(),
337 json!({"data": 2}),
338 Duration::from_secs(60),
339 );
340 cache.set(
341 "key3".to_string(),
342 json!({"data": 3}),
343 Duration::from_secs(60),
344 );
345
346 let stats_before = cache.get_stats();
347 assert_eq!(stats_before.cache_size, 3);
348
349 cache.clear();
350
351 let stats_after = cache.get_stats();
352 assert_eq!(stats_after.cache_size, 0);
353
354 assert!(cache.get("key1").is_none());
356 assert!(cache.get("key2").is_none());
357 assert!(cache.get("key3").is_none());
358 }
359
360 #[test]
361 fn test_cache_cleanup_expired() {
362 let cache = YnabCache::new(10);
363
364 cache.set(
366 "short_ttl".to_string(),
367 json!({"data": 1}),
368 Duration::from_millis(10),
369 );
370 cache.set(
371 "long_ttl".to_string(),
372 json!({"data": 2}),
373 Duration::from_secs(60),
374 );
375
376 thread::sleep(Duration::from_millis(20));
378
379 let removed = cache.cleanup_expired();
381 assert_eq!(removed, 1);
382
383 assert!(cache.get("long_ttl").is_some());
385 assert!(cache.get("short_ttl").is_none());
386
387 let stats = cache.get_stats();
388 assert_eq!(stats.cache_size, 1);
389 }
390
391 #[test]
392 fn test_cache_capacity_eviction() {
393 let cache = YnabCache::new(2); cache.set(
397 "key1".to_string(),
398 json!({"data": 1}),
399 Duration::from_secs(60),
400 );
401 cache.set(
402 "key2".to_string(),
403 json!({"data": 2}),
404 Duration::from_secs(60),
405 );
406
407 cache.get("key1");
409
410 cache.set(
412 "key3".to_string(),
413 json!({"data": 3}),
414 Duration::from_secs(60),
415 );
416
417 let stats = cache.get_stats();
418 assert_eq!(stats.cache_size, 2);
419
420 assert!(cache.get("key1").is_some());
422 assert!(cache.get("key3").is_some());
423 assert!(cache.get("key2").is_none());
424 }
425
426 #[test]
427 fn test_find_oldest_key() {
428 let cache = YnabCache::new(10);
429
430 cache.set(
432 "first".to_string(),
433 json!({"data": 1}),
434 Duration::from_secs(60),
435 );
436 thread::sleep(Duration::from_millis(1));
437 cache.set(
438 "second".to_string(),
439 json!({"data": 2}),
440 Duration::from_secs(60),
441 );
442 thread::sleep(Duration::from_millis(1));
443 cache.set(
444 "third".to_string(),
445 json!({"data": 3}),
446 Duration::from_secs(60),
447 );
448
449 cache.get("second");
451
452 let oldest_key = cache.find_oldest_key();
453 assert!(oldest_key.is_some());
454 let key = oldest_key.unwrap();
456 assert!(key == "first" || key == "third");
457 }
458
459 #[test]
460 fn test_cache_hit_ratio_calculation() {
461 let cache = YnabCache::new(10);
462
463 cache.set(
464 "key1".to_string(),
465 json!({"data": 1}),
466 Duration::from_secs(60),
467 );
468
469 cache.get("key1");
471 let stats = cache.get_stats();
472 assert_eq!(stats.hit_ratio, 1.0);
473
474 cache.get("nonexistent");
476 let stats = cache.get_stats();
477 assert_eq!(stats.hit_ratio, 0.5);
478
479 cache.get("key1");
481 let stats = cache.get_stats();
482 assert_eq!(stats.total_requests, 3);
483 assert_eq!(stats.cache_hits, 2);
484 assert_eq!(stats.cache_misses, 1);
485 assert!((stats.hit_ratio - 0.6666666666666666).abs() < f64::EPSILON);
486 }
487
488 #[test]
489 fn test_cache_stats_with_lock_failure() {
490 let cache = YnabCache::new(10);
491
492 let stats = cache.get_stats();
495 assert_eq!(stats.cache_size, 0);
496 }
497
498 #[test]
499 fn test_evict_expired_or_oldest_with_expired_entries() {
500 let cache = YnabCache::new(2);
501
502 cache.set(
504 "expired1".to_string(),
505 json!({"data": 1}),
506 Duration::from_millis(10),
507 );
508 cache.set(
509 "expired2".to_string(),
510 json!({"data": 2}),
511 Duration::from_millis(10),
512 );
513
514 thread::sleep(Duration::from_millis(20));
516
517 cache.set(
519 "new_key".to_string(),
520 json!({"data": 3}),
521 Duration::from_secs(60),
522 );
523
524 let stats = cache.get_stats();
525 assert_eq!(stats.cache_size, 1);
526 assert!(cache.get("new_key").is_some());
527 assert!(cache.get("expired1").is_none());
528 assert!(cache.get("expired2").is_none());
529 }
530
531 #[test]
532 fn test_evict_expired_or_oldest_without_expired_entries() {
533 let cache = YnabCache::new(2);
534
535 cache.set(
537 "key1".to_string(),
538 json!({"data": 1}),
539 Duration::from_secs(60),
540 );
541 cache.set(
542 "key2".to_string(),
543 json!({"data": 2}),
544 Duration::from_secs(60),
545 );
546
547 cache.get("key1");
549
550 cache.set(
552 "key3".to_string(),
553 json!({"data": 3}),
554 Duration::from_secs(60),
555 );
556
557 let stats = cache.get_stats();
558 assert_eq!(stats.cache_size, 2);
559 assert!(cache.get("key1").is_some());
560 assert!(cache.get("key3").is_some());
561 assert!(cache.get("key2").is_none());
562 }
563
564 #[test]
565 fn test_multiple_cache_operations() {
566 let cache = YnabCache::new(5);
567
568 cache.set(
570 "a".to_string(),
571 json!({"value": "a"}),
572 Duration::from_secs(60),
573 );
574 cache.set(
575 "b".to_string(),
576 json!({"value": "b"}),
577 Duration::from_millis(50),
578 );
579 cache.set(
580 "c".to_string(),
581 json!({"value": "c"}),
582 Duration::from_secs(60),
583 );
584
585 assert_eq!(cache.get_stats().cache_size, 3);
587
588 assert!(cache.get("a").is_some());
590 assert!(cache.get("nonexistent").is_none());
591 assert!(cache.get("c").is_some());
592
593 thread::sleep(Duration::from_millis(60));
595
596 assert!(cache.get("b").is_none());
598 assert_eq!(cache.get_stats().cache_size, 2);
599
600 let removed = cache.cleanup_expired();
602 assert_eq!(removed, 0); cache.clear();
606 assert_eq!(cache.get_stats().cache_size, 0);
607 }
608
609 #[test]
610 fn test_zero_capacity_cache() {
611 let cache = YnabCache::new(0);
612
613 cache.set(
615 "key".to_string(),
616 json!({"data": "value"}),
617 Duration::from_secs(60),
618 );
619
620 let stats = cache.get_stats();
622 assert!(stats.cache_size <= 1); }
624
625 #[test]
626 fn test_cache_with_large_capacity() {
627 let cache = YnabCache::new(1000);
628
629 for i in 0..100 {
631 cache.set(
632 format!("key_{}", i),
633 json!({"index": i}),
634 Duration::from_secs(60),
635 );
636 }
637
638 let stats = cache.get_stats();
639 assert_eq!(stats.cache_size, 100);
640
641 for i in 0..100 {
643 assert!(cache.get(&format!("key_{}", i)).is_some());
644 }
645
646 let final_stats = cache.get_stats();
647 assert_eq!(final_stats.total_requests, 100);
648 assert_eq!(final_stats.cache_hits, 100);
649 assert_eq!(final_stats.hit_ratio, 1.0);
650 }
651
652 #[test]
653 fn test_cache_entry_debug() {
654 let entry = CacheEntry {
655 data: json!({"test": "value"}),
656 expires_at: Instant::now() + Duration::from_secs(60),
657 last_access: Instant::now(),
658 };
659
660 let debug_str = format!("{:?}", entry);
661 assert!(debug_str.contains("CacheEntry"));
662 assert!(debug_str.contains("data"));
663 assert!(debug_str.contains("expires_at"));
664 assert!(debug_str.contains("last_access"));
665 }
666
667 #[test]
668 fn test_cache_stats_debug() {
669 let stats = CacheStats {
670 total_requests: 10,
671 cache_hits: 8,
672 cache_misses: 2,
673 hit_ratio: 0.8,
674 cache_size: 5,
675 };
676
677 let debug_str = format!("{:?}", stats);
678 assert!(debug_str.contains("CacheStats"));
679 assert!(debug_str.contains("10"));
680 assert!(debug_str.contains("8"));
681 assert!(debug_str.contains("2"));
682 assert!(debug_str.contains("0.8"));
683 assert!(debug_str.contains("5"));
684 }
685
686 #[test]
687 fn test_cache_clone() {
688 let cache = YnabCache::new(10);
689 cache.set(
690 "key".to_string(),
691 json!({"data": "value"}),
692 Duration::from_secs(60),
693 );
694
695 let cloned_cache = cache.clone();
696
697 assert!(cache.get("key").is_some());
699 assert!(cloned_cache.get("key").is_some());
700
701 cloned_cache.set(
703 "key2".to_string(),
704 json!({"data": "value2"}),
705 Duration::from_secs(60),
706 );
707 assert!(cache.get("key2").is_some());
708 }
709
710 #[test]
711 fn test_empty_cache_oldest_key() {
712 let cache = YnabCache::new(10);
713
714 let oldest_key = cache.find_oldest_key();
715 assert!(oldest_key.is_none());
716 }
717
718 #[test]
719 fn test_single_entry_oldest_key() {
720 let cache = YnabCache::new(10);
721 cache.set(
722 "only_key".to_string(),
723 json!({"data": "value"}),
724 Duration::from_secs(60),
725 );
726
727 let oldest_key = cache.find_oldest_key();
728 assert_eq!(oldest_key, Some("only_key".to_string()));
729 }
730}