Skip to main content

shared_state/
ttl.rs

1//! Time-to-live cache backed by DashMap.
2
3use std::hash::Hash;
4use std::time::{Duration, Instant};
5use dashmap::DashMap;
6
7/// A cache entry with expiration time.
8struct CacheEntry<V> {
9    value: V,
10    expires_at: Instant,
11}
12
13/// A concurrent cache with per-entry TTL eviction.
14///
15/// Entries are automatically considered expired after their TTL elapses.
16/// Expired entries are lazily evicted on access.
17pub struct TtlCache<K, V> {
18    inner: DashMap<K, CacheEntry<V>>,
19    ttl: Duration,
20}
21
22impl<K: Eq + Hash + Clone, V: Clone> TtlCache<K, V> {
23    /// Create a new cache with the given TTL for all entries.
24    pub fn new(ttl: Duration) -> Self {
25        Self {
26            inner: DashMap::new(),
27            ttl,
28        }
29    }
30
31    /// Get a value if it exists and has not expired.
32    pub fn get(&self, key: &K) -> Option<V> {
33        let entry = self.inner.get(key)?;
34        if entry.expires_at > Instant::now() {
35            Some(entry.value.clone())
36        } else {
37            drop(entry);
38            self.inner.remove(key);
39            None
40        }
41    }
42
43    /// Insert a value with the configured TTL.
44    pub fn insert(&self, key: K, value: V) {
45        self.inner.insert(
46            key,
47            CacheEntry {
48                value,
49                expires_at: Instant::now() + self.ttl,
50            },
51        );
52    }
53
54    /// Remove an entry.
55    pub fn remove(&self, key: &K) -> Option<V> {
56        self.inner.remove(key).map(|e| e.1.value)
57    }
58
59    /// Remove all expired entries.
60    pub fn cleanup(&self) {
61        let now = Instant::now();
62        self.inner.retain(|_, entry| entry.expires_at > now);
63    }
64
65    /// Return the number of entries (including expired ones).
66    pub fn len(&self) -> usize {
67        self.inner.len()
68    }
69
70    /// Return true if the cache is empty.
71    pub fn is_empty(&self) -> bool {
72        self.inner.is_empty()
73    }
74}
75
76impl<K: Eq + Hash + Clone, V: Clone> Default for TtlCache<K, V> {
77    fn default() -> Self {
78        Self::new(Duration::from_secs(300))
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::time::Duration;
86
87    #[test]
88    fn insert_and_get() {
89        let cache = TtlCache::new(Duration::from_secs(60));
90        cache.insert("key1", 42);
91        assert_eq!(cache.get(&"key1"), Some(42));
92    }
93
94    #[test]
95    fn expired_entry_returns_none() {
96        let cache = TtlCache::new(Duration::from_millis(1));
97        cache.insert("key1", 42);
98        std::thread::sleep(Duration::from_millis(5));
99        assert_eq!(cache.get(&"key1"), None);
100    }
101
102    #[test]
103    fn remove_entry() {
104        let cache = TtlCache::new(Duration::from_secs(60));
105        cache.insert("key1", 42);
106        assert_eq!(cache.remove(&"key1"), Some(42));
107        assert_eq!(cache.get(&"key1"), None);
108    }
109
110    #[test]
111    fn cleanup_removes_expired() {
112        let cache = TtlCache::new(Duration::from_millis(1));
113        cache.insert("key1", 1);
114        cache.insert("key2", 2);
115        std::thread::sleep(Duration::from_millis(5));
116        cache.cleanup();
117        assert_eq!(cache.len(), 0);
118    }
119
120    #[test]
121    fn len_and_is_empty() {
122        let cache = TtlCache::new(Duration::from_secs(60));
123        assert!(cache.is_empty());
124        cache.insert("key1", 42);
125        assert_eq!(cache.len(), 1);
126        assert!(!cache.is_empty());
127    }
128}