1use crossbeam_queue::SegQueue;
10use dashmap::DashMap;
11use moka::policy::EvictionPolicy;
12use moka::sync::Cache;
13use parking_lot::Mutex;
14use std::collections::VecDeque;
15use std::fmt;
16use std::hash::Hash;
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum CachePolicyKind {
23 #[default]
25 Lru,
26 TinyLfu,
28 Hybrid,
30}
31
32impl CachePolicyKind {
33 #[must_use]
35 pub fn parse(value: &str) -> Option<Self> {
36 match value.trim().to_ascii_lowercase().as_str() {
37 "lru" => Some(Self::Lru),
38 "tiny_lfu" | "tinylfu" | "lfu" => Some(Self::TinyLfu),
39 "hybrid" | "window_lfu" | "windowed_lfu" => Some(Self::Hybrid),
40 _ => None,
41 }
42 }
43
44 #[must_use]
46 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::Lru => "lru",
49 Self::TinyLfu => "tiny_lfu",
50 Self::Hybrid => "hybrid",
51 }
52 }
53}
54
55impl fmt::Display for CachePolicyKind {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(self.as_str())
58 }
59}
60
61#[derive(Debug, Clone, Copy)]
63pub struct CachePolicyMetrics {
64 pub kind: CachePolicyKind,
66 pub lfu_rejects: usize,
68 pub hot_evictions: usize,
70 pub cold_evictions: usize,
72 pub protected_hits: usize,
74}
75
76impl CachePolicyMetrics {
77 #[must_use]
79 pub const fn with_kind(kind: CachePolicyKind) -> Self {
80 Self {
81 kind,
82 lfu_rejects: 0,
83 hot_evictions: 0,
84 cold_evictions: 0,
85 protected_hits: 0,
86 }
87 }
88}
89
90impl Default for CachePolicyMetrics {
91 fn default() -> Self {
92 Self::with_kind(CachePolicyKind::Lru)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum CacheAdmission {
99 Accepted,
101 Rejected,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum CacheEvictionKind {
108 Hot,
110 Cold,
112}
113
114#[derive(Debug, Clone)]
116pub struct CachePolicyEviction<K> {
117 pub key: K,
119 pub kind: CacheEvictionKind,
121}
122
123pub trait CachePolicy<K>: Send + Sync
125where
126 K: Eq + Hash + Clone + Send + Sync + 'static,
127{
128 fn kind(&self) -> CachePolicyKind;
130
131 fn admit(&self, key: &K, weight_bytes: u64) -> CacheAdmission;
133
134 fn record_hit(&self, key: &K) -> bool;
136
137 fn invalidate(&self, key: &K);
139
140 fn drain_evictions(&self) -> Vec<CachePolicyEviction<K>>;
142
143 fn reset(&self);
145
146 fn stats(&self) -> CachePolicyMetrics;
148}
149
150#[derive(Debug, Clone, Copy)]
152pub struct CachePolicyConfig {
153 pub kind: CachePolicyKind,
155 pub max_bytes: u64,
157 pub window_ratio: f32,
159}
160
161impl CachePolicyConfig {
162 #[must_use]
164 pub fn new(kind: CachePolicyKind, max_bytes: u64, window_ratio: f32) -> Self {
165 Self {
166 kind,
167 max_bytes,
168 window_ratio,
169 }
170 }
171}
172
173#[must_use]
175pub fn build_cache_policy<K>(config: &CachePolicyConfig) -> Arc<dyn CachePolicy<K>>
176where
177 K: Eq + Hash + Clone + Send + Sync + 'static,
178{
179 match config.kind {
180 CachePolicyKind::Lru => Arc::new(LruPolicy::new()),
181 CachePolicyKind::TinyLfu | CachePolicyKind::Hybrid => Arc::new(TinyLfuPolicy::new(config)),
182 }
183}
184
185struct LruPolicy;
187
188impl LruPolicy {
189 fn new() -> Self {
190 Self
191 }
192}
193
194impl<K> CachePolicy<K> for LruPolicy
195where
196 K: Eq + Hash + Clone + Send + Sync + 'static,
197{
198 fn kind(&self) -> CachePolicyKind {
199 CachePolicyKind::Lru
200 }
201
202 fn admit(&self, _key: &K, _weight_bytes: u64) -> CacheAdmission {
203 CacheAdmission::Accepted
204 }
205
206 fn record_hit(&self, _key: &K) -> bool {
207 false
208 }
209
210 fn invalidate(&self, _key: &K) {}
211
212 fn drain_evictions(&self) -> Vec<CachePolicyEviction<K>> {
213 Vec::new()
214 }
215
216 fn reset(&self) {}
217
218 fn stats(&self) -> CachePolicyMetrics {
219 CachePolicyMetrics::with_kind(CachePolicyKind::Lru)
220 }
221}
222
223#[derive(Debug)]
225struct PolicyVictim<K> {
226 key: K,
227}
228
229#[derive(Debug)]
230struct ProtectedEntry {
231 weight: u32,
232 hits: AtomicU32,
233 protected: AtomicBool,
234}
235
236impl ProtectedEntry {
237 fn new(weight: u32) -> Self {
238 Self {
239 weight,
240 hits: AtomicU32::new(0),
241 protected: AtomicBool::new(false),
242 }
243 }
244}
245
246#[derive(Default)]
247struct PolicyMetricCounters {
248 lfu_rejects: AtomicU64,
249 hot_evictions: AtomicU64,
250 cold_evictions: AtomicU64,
251 protected_hits: AtomicU64,
252}
253
254impl PolicyMetricCounters {
255 fn snapshot(&self, kind: CachePolicyKind) -> CachePolicyMetrics {
256 fn u64_to_usize(value: u64) -> usize {
257 usize::try_from(value).unwrap_or(usize::MAX)
258 }
259
260 CachePolicyMetrics {
261 kind,
262 lfu_rejects: u64_to_usize(self.lfu_rejects.load(Ordering::Relaxed)),
263 hot_evictions: u64_to_usize(self.hot_evictions.load(Ordering::Relaxed)),
264 cold_evictions: u64_to_usize(self.cold_evictions.load(Ordering::Relaxed)),
265 protected_hits: u64_to_usize(self.protected_hits.load(Ordering::Relaxed)),
266 }
267 }
268
269 fn reset(&self) {
270 self.lfu_rejects.store(0, Ordering::Relaxed);
271 self.hot_evictions.store(0, Ordering::Relaxed);
272 self.cold_evictions.store(0, Ordering::Relaxed);
273 self.protected_hits.store(0, Ordering::Relaxed);
274 }
275}
276
277struct TinyLfuPolicy<K>
278where
279 K: Eq + Hash + Clone + Send + Sync + 'static,
280{
281 kind: CachePolicyKind,
282 cache: Cache<K, u32>,
283 victims: Arc<SegQueue<PolicyVictim<K>>>,
284 protected: DashMap<K, Arc<ProtectedEntry>>,
285 protected_order: Mutex<VecDeque<K>>,
286 protected_budget: u64,
287 protected_bytes: AtomicU64,
288 metrics: PolicyMetricCounters,
289 promotion_threshold: u32,
290}
291
292impl<K> TinyLfuPolicy<K>
293where
294 K: Eq + Hash + Clone + Send + Sync + 'static,
295{
296 fn new(config: &CachePolicyConfig) -> Self {
297 let victims = Arc::new(SegQueue::<PolicyVictim<K>>::new());
298 let victims_clone = Arc::clone(&victims);
299 let max_bytes = config.max_bytes.max(1);
300 let eviction_policy = match config.kind {
301 CachePolicyKind::Lru => EvictionPolicy::lru(),
302 CachePolicyKind::TinyLfu | CachePolicyKind::Hybrid => EvictionPolicy::tiny_lfu(),
303 };
304
305 let cache = Cache::builder()
306 .max_capacity(max_bytes)
307 .eviction_policy(eviction_policy)
308 .weigher(|_, weight: &u32| *weight)
309 .eviction_listener(move |key: Arc<K>, _weight: u32, cause| {
310 if cause.was_evicted() {
311 victims_clone.push(PolicyVictim {
312 key: (*key).clone(),
313 });
314 }
315 })
316 .build();
317
318 let window_ratio = clamp_ratio(config.window_ratio);
319 let protected_budget = scale_u64_by_ratio(max_bytes, window_ratio).max(1);
320
321 Self {
322 kind: config.kind,
323 cache,
324 victims,
325 protected: DashMap::new(),
326 protected_order: Mutex::new(VecDeque::new()),
327 protected_budget,
328 protected_bytes: AtomicU64::new(0),
329 metrics: PolicyMetricCounters::default(),
330 promotion_threshold: 3,
331 }
332 }
333
334 fn clamp_weight(weight_bytes: u64) -> u32 {
335 u32::try_from(weight_bytes).unwrap_or(u32::MAX).max(1)
336 }
337
338 fn promote(&self, key: K, entry: &ProtectedEntry) {
339 if entry.protected.swap(true, Ordering::Relaxed) {
340 return;
341 }
342
343 self.protected_bytes
344 .fetch_add(u64::from(entry.weight), Ordering::Relaxed);
345 self.protected_order.lock().push_back(key);
346 self.rebalance_protected_budget();
347 }
348
349 fn demote_key(&self, key: &K) {
350 if let Some(entry) = self.protected.get(key)
351 && entry.protected.swap(false, Ordering::Relaxed)
352 {
353 self.protected_bytes
354 .fetch_sub(u64::from(entry.weight), Ordering::Relaxed);
355 }
356 }
357
358 fn rebalance_protected_budget(&self) {
359 while self.protected_bytes.load(Ordering::Relaxed) > self.protected_budget {
360 let Some(next) = self.protected_order.lock().pop_front() else {
361 break;
362 };
363 self.demote_key(&next);
364 }
365 }
366
367 fn handle_eviction(&self, victim: PolicyVictim<K>) -> CachePolicyEviction<K> {
368 if let Some((_, entry)) = self.protected.remove(&victim.key) {
369 if entry.protected.swap(false, Ordering::Relaxed) {
370 self.metrics.hot_evictions.fetch_add(1, Ordering::Relaxed);
371 self.protected_bytes
372 .fetch_sub(u64::from(entry.weight), Ordering::Relaxed);
373 CachePolicyEviction {
374 key: victim.key,
375 kind: CacheEvictionKind::Hot,
376 }
377 } else {
378 self.metrics.cold_evictions.fetch_add(1, Ordering::Relaxed);
379 CachePolicyEviction {
380 key: victim.key,
381 kind: CacheEvictionKind::Cold,
382 }
383 }
384 } else {
385 self.metrics.cold_evictions.fetch_add(1, Ordering::Relaxed);
386 CachePolicyEviction {
387 key: victim.key,
388 kind: CacheEvictionKind::Cold,
389 }
390 }
391 }
392}
393
394impl<K> CachePolicy<K> for TinyLfuPolicy<K>
395where
396 K: Eq + Hash + Clone + Send + Sync + 'static,
397{
398 fn kind(&self) -> CachePolicyKind {
399 self.kind
400 }
401
402 fn admit(&self, key: &K, weight_bytes: u64) -> CacheAdmission {
403 let weight = Self::clamp_weight(weight_bytes);
404 self.cache.insert(key.clone(), weight);
405 self.cache.run_pending_tasks();
406
407 self.protected
408 .entry(key.clone())
409 .or_insert_with(|| Arc::new(ProtectedEntry::new(weight)));
410
411 if self.cache.contains_key(key) {
412 CacheAdmission::Accepted
413 } else {
414 self.metrics.lfu_rejects.fetch_add(1, Ordering::Relaxed);
415 self.protected.remove(key);
416 CacheAdmission::Rejected
417 }
418 }
419
420 fn record_hit(&self, key: &K) -> bool {
421 let _ = self.cache.get(key);
422 if let Some(entry) = self.protected.get(key) {
423 let hits = entry.hits.fetch_add(1, Ordering::Relaxed) + 1;
424 if hits >= self.promotion_threshold {
425 self.promote(key.clone(), &entry);
426 }
427 if entry.protected.load(Ordering::Relaxed) {
428 self.metrics.protected_hits.fetch_add(1, Ordering::Relaxed);
429 return true;
430 }
431 }
432 false
433 }
434
435 fn invalidate(&self, key: &K) {
436 if let Some((_, entry)) = self.protected.remove(key)
437 && entry.protected.swap(false, Ordering::Relaxed)
438 {
439 self.protected_bytes
440 .fetch_sub(u64::from(entry.weight), Ordering::Relaxed);
441 }
442 self.cache.invalidate(key);
443 self.cache.run_pending_tasks();
444 }
445
446 fn drain_evictions(&self) -> Vec<CachePolicyEviction<K>> {
447 self.cache.run_pending_tasks();
448 let mut victims = Vec::new();
449 while let Some(event) = self.victims.pop() {
450 victims.push(self.handle_eviction(event));
451 }
452 victims
453 }
454
455 fn reset(&self) {
456 self.cache.invalidate_all();
457 self.cache.run_pending_tasks();
458 while self.victims.pop().is_some() {}
459 self.protected.clear();
460 self.protected_order.lock().clear();
461 self.protected_bytes.store(0, Ordering::Relaxed);
462 self.metrics.reset();
463 }
464
465 fn stats(&self) -> CachePolicyMetrics {
466 self.metrics.snapshot(self.kind())
467 }
468}
469
470fn clamp_ratio(ratio: f32) -> f32 {
471 if ratio.is_nan() || !ratio.is_finite() {
472 0.20
473 } else {
474 ratio.clamp(0.05, 0.95)
475 }
476}
477
478fn scale_u64_by_ratio(value: u64, ratio: f32) -> u64 {
479 let scaled = {
480 #[allow(clippy::cast_precision_loss)]
481 {
482 (value as f64) * f64::from(ratio)
483 }
484 };
485 if !scaled.is_finite() || scaled <= 0.0 {
486 return 0;
487 }
488 let capped = {
489 #[allow(clippy::cast_precision_loss)]
490 {
491 scaled.min(u64::MAX as f64)
492 }
493 };
494 #[allow(
495 clippy::cast_possible_truncation,
496 clippy::cast_precision_loss,
497 clippy::cast_sign_loss
498 )]
499 {
500 capped.round() as u64
501 }
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
510 fn test_cache_policy_kind_parse_lru() {
511 assert_eq!(CachePolicyKind::parse("lru"), Some(CachePolicyKind::Lru));
512 assert_eq!(CachePolicyKind::parse("LRU"), Some(CachePolicyKind::Lru));
513 assert_eq!(
514 CachePolicyKind::parse(" lru "),
515 Some(CachePolicyKind::Lru)
516 );
517 }
518
519 #[test]
520 fn test_cache_policy_kind_parse_tiny_lfu() {
521 assert_eq!(
522 CachePolicyKind::parse("tiny_lfu"),
523 Some(CachePolicyKind::TinyLfu)
524 );
525 assert_eq!(
526 CachePolicyKind::parse("tinylfu"),
527 Some(CachePolicyKind::TinyLfu)
528 );
529 assert_eq!(
530 CachePolicyKind::parse("lfu"),
531 Some(CachePolicyKind::TinyLfu)
532 );
533 assert_eq!(
534 CachePolicyKind::parse("LFU"),
535 Some(CachePolicyKind::TinyLfu)
536 );
537 }
538
539 #[test]
540 fn test_cache_policy_kind_parse_hybrid() {
541 assert_eq!(
542 CachePolicyKind::parse("hybrid"),
543 Some(CachePolicyKind::Hybrid)
544 );
545 assert_eq!(
546 CachePolicyKind::parse("window_lfu"),
547 Some(CachePolicyKind::Hybrid)
548 );
549 assert_eq!(
550 CachePolicyKind::parse("windowed_lfu"),
551 Some(CachePolicyKind::Hybrid)
552 );
553 assert_eq!(
554 CachePolicyKind::parse("HYBRID"),
555 Some(CachePolicyKind::Hybrid)
556 );
557 }
558
559 #[test]
560 fn test_cache_policy_kind_parse_invalid() {
561 assert_eq!(CachePolicyKind::parse("unknown"), None);
562 assert_eq!(CachePolicyKind::parse(""), None);
563 assert_eq!(CachePolicyKind::parse("fifo"), None);
564 }
565
566 #[test]
567 fn test_cache_policy_kind_as_str() {
568 assert_eq!(CachePolicyKind::Lru.as_str(), "lru");
569 assert_eq!(CachePolicyKind::TinyLfu.as_str(), "tiny_lfu");
570 assert_eq!(CachePolicyKind::Hybrid.as_str(), "hybrid");
571 }
572
573 #[test]
574 fn test_cache_policy_kind_default() {
575 assert_eq!(CachePolicyKind::default(), CachePolicyKind::Lru);
576 }
577
578 #[test]
579 fn test_cache_policy_kind_display() {
580 assert_eq!(format!("{}", CachePolicyKind::Lru), "lru");
581 assert_eq!(format!("{}", CachePolicyKind::TinyLfu), "tiny_lfu");
582 assert_eq!(format!("{}", CachePolicyKind::Hybrid), "hybrid");
583 }
584
585 #[test]
586 fn test_cache_policy_kind_eq() {
587 assert_eq!(CachePolicyKind::Lru, CachePolicyKind::Lru);
588 assert_ne!(CachePolicyKind::Lru, CachePolicyKind::TinyLfu);
589 }
590
591 #[test]
592 fn test_cache_policy_kind_clone() {
593 let kind = CachePolicyKind::TinyLfu;
594 let cloned = kind;
595 assert_eq!(kind, cloned);
596 }
597
598 #[test]
600 fn test_cache_policy_metrics_with_kind() {
601 let metrics = CachePolicyMetrics::with_kind(CachePolicyKind::TinyLfu);
602 assert_eq!(metrics.kind, CachePolicyKind::TinyLfu);
603 assert_eq!(metrics.lfu_rejects, 0);
604 assert_eq!(metrics.hot_evictions, 0);
605 assert_eq!(metrics.cold_evictions, 0);
606 assert_eq!(metrics.protected_hits, 0);
607 }
608
609 #[test]
610 fn test_cache_policy_metrics_default() {
611 let metrics = CachePolicyMetrics::default();
612 assert_eq!(metrics.kind, CachePolicyKind::Lru);
613 assert_eq!(metrics.lfu_rejects, 0);
614 }
615
616 #[test]
618 fn test_cache_admission_eq() {
619 assert_eq!(CacheAdmission::Accepted, CacheAdmission::Accepted);
620 assert_ne!(CacheAdmission::Accepted, CacheAdmission::Rejected);
621 }
622
623 #[test]
625 fn test_cache_eviction_kind_eq() {
626 assert_eq!(CacheEvictionKind::Hot, CacheEvictionKind::Hot);
627 assert_ne!(CacheEvictionKind::Hot, CacheEvictionKind::Cold);
628 }
629
630 #[test]
632 fn test_cache_policy_config_new() {
633 let config = CachePolicyConfig::new(CachePolicyKind::TinyLfu, 1024 * 1024, 0.2);
634 assert_eq!(config.kind, CachePolicyKind::TinyLfu);
635 assert_eq!(config.max_bytes, 1024 * 1024);
636 assert!((config.window_ratio - 0.2).abs() < f32::EPSILON);
637 }
638
639 #[test]
641 fn test_clamp_ratio_normal() {
642 assert!((clamp_ratio(0.5) - 0.5).abs() < f32::EPSILON);
643 assert!((clamp_ratio(0.2) - 0.2).abs() < f32::EPSILON);
644 }
645
646 #[test]
647 fn test_clamp_ratio_too_low() {
648 assert!((clamp_ratio(0.01) - 0.05).abs() < f32::EPSILON);
650 assert!((clamp_ratio(0.0) - 0.05).abs() < f32::EPSILON);
651 }
652
653 #[test]
654 fn test_clamp_ratio_too_high() {
655 assert!((clamp_ratio(0.99) - 0.95).abs() < f32::EPSILON);
657 assert!((clamp_ratio(1.0) - 0.95).abs() < f32::EPSILON);
658 }
659
660 #[test]
661 fn test_clamp_ratio_nan() {
662 assert!((clamp_ratio(f32::NAN) - 0.20).abs() < f32::EPSILON);
664 }
665
666 #[test]
667 fn test_clamp_ratio_infinity() {
668 assert!((clamp_ratio(f32::INFINITY) - 0.20).abs() < f32::EPSILON);
670 assert!((clamp_ratio(f32::NEG_INFINITY) - 0.20).abs() < f32::EPSILON);
671 }
672
673 #[test]
675 fn test_scale_u64_by_ratio_normal() {
676 assert_eq!(scale_u64_by_ratio(1000, 0.5), 500);
677 assert_eq!(scale_u64_by_ratio(100, 0.1), 10);
678 }
679
680 #[test]
681 fn test_scale_u64_by_ratio_zero_value() {
682 assert_eq!(scale_u64_by_ratio(0, 0.5), 0);
683 }
684
685 #[test]
686 fn test_scale_u64_by_ratio_zero_ratio() {
687 assert_eq!(scale_u64_by_ratio(1000, 0.0), 0);
688 }
689
690 #[test]
691 fn test_scale_u64_by_ratio_negative_ratio() {
692 assert_eq!(scale_u64_by_ratio(1000, -0.5), 0);
694 }
695
696 #[test]
697 fn test_scale_u64_by_ratio_nan() {
698 assert_eq!(scale_u64_by_ratio(1000, f32::NAN), 0);
700 }
701
702 #[test]
703 fn test_scale_u64_by_ratio_rounding() {
704 assert_eq!(scale_u64_by_ratio(1000, 0.33), 330);
706 assert_eq!(scale_u64_by_ratio(1000, 0.335), 335);
708 }
709
710 #[test]
712 fn test_lru_policy_kind() {
713 let policy: Arc<dyn CachePolicy<String>> =
714 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::Lru, 1024, 0.2));
715 assert_eq!(policy.kind(), CachePolicyKind::Lru);
716 }
717
718 #[test]
719 fn test_lru_policy_always_admits() {
720 let policy: Arc<dyn CachePolicy<String>> =
721 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::Lru, 1024, 0.2));
722 assert_eq!(
723 policy.admit(&"key".to_string(), 100),
724 CacheAdmission::Accepted
725 );
726 }
727
728 #[test]
729 fn test_lru_policy_record_hit_returns_false() {
730 let policy: Arc<dyn CachePolicy<String>> =
731 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::Lru, 1024, 0.2));
732 assert!(!policy.record_hit(&"key".to_string()));
734 }
735
736 #[test]
737 fn test_lru_policy_drain_evictions_empty() {
738 let policy: Arc<dyn CachePolicy<String>> =
739 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::Lru, 1024, 0.2));
740 assert!(policy.drain_evictions().is_empty());
741 }
742
743 #[test]
744 fn test_lru_policy_stats() {
745 let policy: Arc<dyn CachePolicy<String>> =
746 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::Lru, 1024, 0.2));
747 let stats = policy.stats();
748 assert_eq!(stats.kind, CachePolicyKind::Lru);
749 assert_eq!(stats.lfu_rejects, 0);
750 }
751
752 #[test]
754 fn test_tiny_lfu_policy_kind() {
755 let policy: Arc<dyn CachePolicy<String>> =
756 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::TinyLfu, 1024, 0.2));
757 assert_eq!(policy.kind(), CachePolicyKind::TinyLfu);
758 }
759
760 #[test]
761 fn test_hybrid_policy_kind() {
762 let policy: Arc<dyn CachePolicy<String>> =
763 build_cache_policy(&CachePolicyConfig::new(CachePolicyKind::Hybrid, 1024, 0.2));
764 assert_eq!(policy.kind(), CachePolicyKind::Hybrid);
765 }
766
767 #[test]
768 fn test_tiny_lfu_policy_admit_and_stats() {
769 let policy: Arc<dyn CachePolicy<String>> = build_cache_policy(&CachePolicyConfig::new(
770 CachePolicyKind::TinyLfu,
771 10240,
772 0.2,
773 ));
774
775 let result = policy.admit(&"test_key".to_string(), 100);
777 assert!(result == CacheAdmission::Accepted || result == CacheAdmission::Rejected);
779
780 let stats = policy.stats();
782 assert_eq!(stats.kind, CachePolicyKind::TinyLfu);
783 }
784
785 #[test]
786 fn test_tiny_lfu_policy_reset() {
787 let policy: Arc<dyn CachePolicy<String>> = build_cache_policy(&CachePolicyConfig::new(
788 CachePolicyKind::TinyLfu,
789 10240,
790 0.2,
791 ));
792
793 policy.admit(&"key1".to_string(), 100);
794 policy.admit(&"key2".to_string(), 100);
795
796 policy.reset();
798
799 let stats = policy.stats();
800 assert_eq!(stats.lfu_rejects, 0);
801 assert_eq!(stats.hot_evictions, 0);
802 assert_eq!(stats.cold_evictions, 0);
803 assert_eq!(stats.protected_hits, 0);
804 }
805
806 #[test]
807 fn test_tiny_lfu_policy_invalidate() {
808 let policy: Arc<dyn CachePolicy<String>> = build_cache_policy(&CachePolicyConfig::new(
809 CachePolicyKind::TinyLfu,
810 10240,
811 0.2,
812 ));
813
814 let key = "test_key".to_string();
815 policy.admit(&key, 100);
816 policy.invalidate(&key);
817 }
819
820 #[test]
822 fn test_build_cache_policy_lru() {
823 let config = CachePolicyConfig::new(CachePolicyKind::Lru, 1024, 0.2);
824 let policy: Arc<dyn CachePolicy<String>> = build_cache_policy(&config);
825 assert_eq!(policy.kind(), CachePolicyKind::Lru);
826 }
827
828 #[test]
829 fn test_build_cache_policy_tiny_lfu() {
830 let config = CachePolicyConfig::new(CachePolicyKind::TinyLfu, 1024, 0.2);
831 let policy: Arc<dyn CachePolicy<String>> = build_cache_policy(&config);
832 assert_eq!(policy.kind(), CachePolicyKind::TinyLfu);
833 }
834
835 #[test]
836 fn test_build_cache_policy_hybrid() {
837 let config = CachePolicyConfig::new(CachePolicyKind::Hybrid, 1024, 0.2);
838 let policy: Arc<dyn CachePolicy<String>> = build_cache_policy(&config);
839 assert_eq!(policy.kind(), CachePolicyKind::Hybrid);
840 }
841
842 #[test]
844 fn test_policy_metric_counters_snapshot() {
845 let counters = PolicyMetricCounters::default();
846 counters.lfu_rejects.store(5, Ordering::Relaxed);
847 counters.hot_evictions.store(3, Ordering::Relaxed);
848 counters.cold_evictions.store(10, Ordering::Relaxed);
849 counters.protected_hits.store(100, Ordering::Relaxed);
850
851 let snapshot = counters.snapshot(CachePolicyKind::TinyLfu);
852 assert_eq!(snapshot.kind, CachePolicyKind::TinyLfu);
853 assert_eq!(snapshot.lfu_rejects, 5);
854 assert_eq!(snapshot.hot_evictions, 3);
855 assert_eq!(snapshot.cold_evictions, 10);
856 assert_eq!(snapshot.protected_hits, 100);
857 }
858
859 #[test]
860 fn test_policy_metric_counters_reset() {
861 let counters = PolicyMetricCounters::default();
862 counters.lfu_rejects.store(5, Ordering::Relaxed);
863 counters.reset();
864 assert_eq!(counters.lfu_rejects.load(Ordering::Relaxed), 0);
865 }
866
867 #[test]
869 fn test_cache_policy_eviction_clone() {
870 let eviction = CachePolicyEviction {
871 key: "test".to_string(),
872 kind: CacheEvictionKind::Hot,
873 };
874 let cloned = eviction.clone();
875 assert_eq!(cloned.key, "test");
876 assert_eq!(cloned.kind, CacheEvictionKind::Hot);
877 }
878
879 #[test]
881 fn test_protected_entry_new() {
882 let entry = ProtectedEntry::new(100);
883 assert_eq!(entry.weight, 100);
884 assert_eq!(entry.hits.load(Ordering::Relaxed), 0);
885 assert!(!entry.protected.load(Ordering::Relaxed));
886 }
887}