1use std::time::Duration;
2
3use crate::cache::{DefaultLifecycle, EntryStatus, Lifecycle};
4
5pub trait Ttl {
14 fn remaining(&self) -> Duration;
16}
17
18#[derive(Debug, Clone, Copy, Default)]
25pub struct TtlLifecycle<L = DefaultLifecycle> {
26 inner: L,
27}
28
29impl TtlLifecycle<DefaultLifecycle> {
30 pub fn new() -> Self {
31 Self {
32 inner: DefaultLifecycle,
33 }
34 }
35}
36
37impl<L> TtlLifecycle<L> {
38 pub fn with_inner(inner: L) -> Self {
39 Self { inner }
40 }
41}
42
43impl<K, V, L> Lifecycle<K, V> for TtlLifecycle<L>
44where
45 V: Ttl,
46 L: Lifecycle<K, V>,
47{
48 fn on_eviction(&mut self, key: K, value: V) {
49 self.inner.on_eviction(key, value);
50 }
51
52 fn evaluate(&self, key: &K, value: &V) -> EntryStatus {
53 if value.remaining() == Duration::ZERO {
54 EntryStatus::Evict
55 } else {
56 self.inner.evaluate(key, value)
57 }
58 }
59}
60
61#[cfg(test)]
62mod test {
63 use std::hash::RandomState;
64 use std::sync::{Arc, Mutex};
65 use std::time::{Duration, Instant};
66
67 use super::*;
68 use crate::Cache;
69
70 #[derive(Debug, Clone)]
71 struct Expiring<V> {
72 value: V,
73 expires_at: Instant,
74 }
75
76 impl<V> Ttl for Expiring<V> {
77 fn remaining(&self) -> Duration {
78 self.expires_at.saturating_duration_since(Instant::now())
79 }
80 }
81
82 #[test]
83 fn fresh_entries_are_returned() {
84 let mut cache: Cache<String, Expiring<String>, RandomState, crate::One, TtlLifecycle> =
85 Cache::new_with_lifecycle(RandomState::new(), 100, TtlLifecycle::new());
86 cache.put(
87 "k".to_string(),
88 Expiring {
89 value: "v".to_string(),
90 expires_at: Instant::now() + Duration::from_secs(60),
91 },
92 );
93 assert_eq!(cache.get("k").map(|e| e.value.as_str()), Some("v"));
94 }
95
96 #[test]
97 fn expired_entries_report_a_miss_on_get() {
98 let mut cache: Cache<String, Expiring<String>, RandomState, crate::One, TtlLifecycle> =
99 Cache::new_with_lifecycle(RandomState::new(), 100, TtlLifecycle::new());
100 cache.put(
101 "k".to_string(),
102 Expiring {
103 value: "v".to_string(),
104 expires_at: Instant::now(),
105 },
106 );
107 assert!(cache.get("k").is_none());
109 }
110
111 #[test]
112 fn expired_entries_are_evicted_when_the_hand_reaches_them() {
113 let evicted: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
114
115 #[derive(Clone)]
116 struct Recorder(Arc<Mutex<Vec<String>>>);
117 impl Lifecycle<String, Expiring<String>> for Recorder {
118 fn on_eviction(&mut self, key: String, _value: Expiring<String>) {
119 self.0.lock().expect("lock poison").push(key);
120 }
121 }
122
123 let lifecycle = TtlLifecycle::with_inner(Recorder(evicted.clone()));
124 let mut cache: Cache<
125 String,
126 Expiring<String>,
127 RandomState,
128 crate::One,
129 TtlLifecycle<Recorder>,
130 > = Cache::new_with_lifecycle(RandomState::new(), 100, lifecycle);
131
132 cache.put(
133 "stale".to_string(),
134 Expiring {
135 value: "x".to_string(),
136 expires_at: Instant::now(),
137 },
138 );
139 cache.put(
142 "fresh".to_string(),
143 Expiring {
144 value: "y".to_string(),
145 expires_at: Instant::now() + Duration::from_secs(60),
146 },
147 );
148
149 assert_eq!(
150 evicted.lock().expect("lock poison").as_slice(),
151 &["stale".to_string()]
152 );
153 assert!(cache.get("stale").is_none());
154 assert!(cache.get("fresh").is_some());
155 }
156}