1use std::collections::HashMap;
24
25#[derive(Debug, Clone)]
29pub struct TtlEntry<V> {
30 pub value: V,
32 pub inserted_at_secs: u64,
34 pub ttl_secs: u64,
36}
37
38impl<V> TtlEntry<V> {
39 #[inline]
41 pub fn new(value: V, inserted_at_secs: u64, ttl_secs: u64) -> Self {
42 Self {
43 value,
44 inserted_at_secs,
45 ttl_secs,
46 }
47 }
48
49 #[inline]
53 pub fn is_expired(&self, now_secs: u64) -> bool {
54 if self.ttl_secs == 0 {
55 return false;
56 }
57 now_secs >= self.inserted_at_secs.saturating_add(self.ttl_secs)
58 }
59
60 #[inline]
64 pub fn remaining_secs(&self, now_secs: u64) -> u64 {
65 if self.ttl_secs == 0 {
66 return u64::MAX;
67 }
68 let deadline = self.inserted_at_secs.saturating_add(self.ttl_secs);
69 deadline.saturating_sub(now_secs)
70 }
71}
72
73#[derive(Debug, Clone, Default)]
77pub struct TtlCacheStats {
78 pub total_entries: usize,
81 pub expired_entries: usize,
83 pub hit_count: u64,
85 pub miss_count: u64,
87}
88
89pub struct TtlCache<K, V>
104where
105 K: Eq + std::hash::Hash + Clone,
106{
107 capacity: usize,
108 default_ttl_secs: u64,
109 entries: HashMap<K, TtlEntry<V>>,
111 insertion_order: Vec<K>,
113 hit_count: u64,
114 miss_count: u64,
115}
116
117impl<K, V> TtlCache<K, V>
118where
119 K: Eq + std::hash::Hash + Clone,
120{
121 pub fn new(capacity: usize, default_ttl_secs: u64) -> Self {
133 assert!(capacity > 0, "TtlCache capacity must be non-zero");
134 Self {
135 capacity,
136 default_ttl_secs,
137 entries: HashMap::with_capacity(capacity.min(1024)),
138 insertion_order: Vec::with_capacity(capacity.min(1024)),
139 hit_count: 0,
140 miss_count: 0,
141 }
142 }
143
144 pub fn insert(&mut self, key: K, value: V, now_secs: u64) {
149 self.insert_with_ttl(key, value, self.default_ttl_secs, now_secs);
150 }
151
152 pub fn insert_with_ttl(&mut self, key: K, value: V, ttl_secs: u64, now_secs: u64) {
156 if self.entries.contains_key(&key) {
158 self.insertion_order.retain(|k| k != &key);
159 } else if self.entries.len() >= self.capacity {
160 self.evict_oldest();
162 }
163
164 let entry = TtlEntry::new(value, now_secs, ttl_secs);
165 self.entries.insert(key.clone(), entry);
166 self.insertion_order.push(key);
167 }
168
169 pub fn get(&mut self, key: &K, now_secs: u64) -> Option<&V> {
177 match self.entries.get(key) {
178 None => {
179 self.miss_count += 1;
180 None
181 }
182 Some(entry) if entry.is_expired(now_secs) => {
183 self.miss_count += 1;
184 None
185 }
186 Some(entry) => {
187 self.hit_count += 1;
188 Some(&entry.value)
189 }
190 }
191 }
192
193 pub fn remove(&mut self, key: &K) -> bool {
197 if self.entries.remove(key).is_some() {
198 self.insertion_order.retain(|k| k != key);
199 true
200 } else {
201 false
202 }
203 }
204
205 pub fn remove_expired(&mut self, now_secs: u64) -> usize {
207 let expired_keys: Vec<K> = self
208 .entries
209 .iter()
210 .filter(|(_, e)| e.is_expired(now_secs))
211 .map(|(k, _)| k.clone())
212 .collect();
213
214 let count = expired_keys.len();
215 for k in &expired_keys {
216 self.entries.remove(k);
217 }
218 self.insertion_order.retain(|k| !expired_keys.contains(k));
219 count
220 }
221
222 pub fn len(&self, now_secs: u64) -> usize {
224 self.entries
225 .values()
226 .filter(|e| !e.is_expired(now_secs))
227 .count()
228 }
229
230 pub fn is_empty(&self, now_secs: u64) -> bool {
232 self.len(now_secs) == 0
233 }
234
235 pub fn stats(&self, now_secs: u64) -> TtlCacheStats {
237 let total = self.entries.len();
238 let expired = self
239 .entries
240 .values()
241 .filter(|e| e.is_expired(now_secs))
242 .count();
243 TtlCacheStats {
244 total_entries: total,
245 expired_entries: expired,
246 hit_count: self.hit_count,
247 miss_count: self.miss_count,
248 }
249 }
250
251 #[inline]
253 pub fn capacity(&self) -> usize {
254 self.capacity
255 }
256
257 fn evict_oldest(&mut self) {
261 if self.insertion_order.is_empty() {
262 return;
263 }
264 let oldest = self.insertion_order.remove(0);
265 self.entries.remove(&oldest);
266 }
267}
268
269#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_insert_and_get_within_ttl() {
277 let mut cache: TtlCache<&str, u32> = TtlCache::new(10, 60);
278 cache.insert("a", 42, 1000);
279 assert_eq!(cache.get(&"a", 1059), Some(&42));
280 }
281
282 #[test]
283 fn test_expired_entry_returns_none() {
284 let mut cache: TtlCache<&str, u32> = TtlCache::new(10, 30);
285 cache.insert("b", 99, 1000);
286 assert_eq!(cache.get(&"b", 1030), None);
288 assert_eq!(cache.get(&"b", 1100), None);
290 }
291
292 #[test]
293 fn test_entry_at_expiry_boundary() {
294 let mut cache: TtlCache<&str, u32> = TtlCache::new(10, 10);
295 cache.insert("c", 7, 500);
296 assert_eq!(cache.get(&"c", 509), Some(&7)); assert_eq!(cache.get(&"c", 510), None); }
299
300 #[test]
301 fn test_remove_expired_count() {
302 let mut cache: TtlCache<u32, &str> = TtlCache::new(20, 5);
303 for i in 0..10 {
304 cache.insert(i, "v", 0);
305 }
306 let purged = cache.remove_expired(5);
308 assert_eq!(purged, 10);
309 assert_eq!(cache.len(5), 0);
310 }
311
312 #[test]
313 fn test_remove_expired_partial() {
314 let mut cache: TtlCache<u32, u32> = TtlCache::new(20, 100);
315 for i in 0..5 {
317 cache.insert_with_ttl(i, i * 10, 10, 0);
318 }
319 for i in 5..10 {
321 cache.insert_with_ttl(i, i * 10, 1000, 0);
322 }
323 let purged = cache.remove_expired(11);
324 assert_eq!(purged, 5);
325 assert_eq!(cache.len(11), 5);
326 }
327
328 #[test]
329 fn test_per_entry_ttl_override() {
330 let mut cache: TtlCache<&str, u32> = TtlCache::new(10, 60);
331 cache.insert_with_ttl("short", 1, 5, 1000);
332 cache.insert("long", 2, 1000); assert_eq!(cache.get(&"short", 1006), None); assert_eq!(cache.get(&"long", 1059), Some(&2)); }
336
337 #[test]
338 fn test_stats_hit_miss_expired() {
339 let mut cache: TtlCache<u32, u32> = TtlCache::new(10, 50);
340 cache.insert(1, 10, 0); cache.insert(2, 20, 0); let _ = cache.get(&1, 10); let _ = cache.get(&3, 10); let _ = cache.get(&2, 51); let s = cache.stats(51);
349 assert_eq!(s.hit_count, 1);
350 assert_eq!(s.miss_count, 2);
351 assert_eq!(s.expired_entries, 2);
353 }
354
355 #[test]
356 fn test_capacity_eviction_fifo() {
357 let mut cache: TtlCache<u32, u32> = TtlCache::new(3, 3600);
358 cache.insert(1, 1, 0);
359 cache.insert(2, 2, 0);
360 cache.insert(3, 3, 0);
361 cache.insert(4, 4, 0);
363 assert_eq!(cache.get(&1, 0), None);
364 assert_eq!(cache.get(&2, 0), Some(&2));
365 assert_eq!(cache.get(&3, 0), Some(&3));
366 assert_eq!(cache.get(&4, 0), Some(&4));
367 }
368
369 #[test]
370 fn test_zero_ttl_never_expires() {
371 let mut cache: TtlCache<&str, u32> = TtlCache::new(10, 0);
372 cache.insert("immortal", 55, 0);
373 assert_eq!(cache.get(&"immortal", u64::MAX / 2), Some(&55));
375 }
376
377 #[test]
378 fn test_overwrite_existing_key() {
379 let mut cache: TtlCache<&str, u32> = TtlCache::new(5, 100);
380 cache.insert("k", 1, 0);
381 cache.insert("k", 2, 0); assert_eq!(cache.get(&"k", 0), Some(&2));
383 assert_eq!(cache.len(0), 1);
385 }
386
387 #[test]
388 fn test_remove_entry() {
389 let mut cache: TtlCache<u32, u32> = TtlCache::new(10, 100);
390 cache.insert(7, 77, 0);
391 assert!(cache.remove(&7));
392 assert!(!cache.remove(&7)); assert_eq!(cache.get(&7, 0), None);
394 }
395
396 #[test]
397 fn test_is_empty() {
398 let mut cache: TtlCache<u32, u32> = TtlCache::new(4, 10);
399 assert!(cache.is_empty(0));
400 cache.insert(1, 1, 0);
401 assert!(!cache.is_empty(0));
402 assert!(cache.is_empty(11)); }
404}