light_cache/policy/
ttl.rs

1use std::{
2    fmt::Debug,
3    hash::{BuildHasher, Hash},
4    marker::PhantomData,
5    time::{Duration, Instant},
6};
7
8use parking_lot::{Mutex, MutexGuard};
9
10use super::{
11    linked_arena::{LinkedArena, LinkedNode},
12    Policy, Prune,
13};
14use crate::LightCache;
15
16/// A simple time-to-live policy that removes only expired keys when the ttl is exceeded
17pub struct TtlPolicy<K, V> {
18    inner: Mutex<TtlPolicyInner<K>>,
19    /// borrow chcker complains and requires fully qualified syntax without this which is annoying
20    phantom: PhantomData<V>,
21}
22
23pub struct TtlPolicyInner<K> {
24    ttl: Duration,
25    arena: LinkedArena<K, TtlNode<K>>,
26}
27
28impl<K, V> TtlPolicy<K, V> {
29    pub fn new(ttl: Duration) -> Self {
30        TtlPolicy {
31            inner: Mutex::new(TtlPolicyInner {
32                ttl,
33                arena: LinkedArena::new(),
34            }),
35            phantom: PhantomData,
36        }
37    }
38}
39
40impl<K, V> Policy<K, V> for TtlPolicy<K, V>
41where
42    K: Copy + Eq + Hash,
43    V: Clone + Sync,
44{
45    type Inner = TtlPolicyInner<K>;
46
47    #[inline]
48    fn lock_inner(&self) -> MutexGuard<'_, Self::Inner> {
49        self.inner.lock()
50    }
51
52    fn get<S: BuildHasher>(&self, key: &K, cache: &LightCache<K, V, S, Self>) -> Option<V> {
53        let _lock = self.lock_and_prune(cache);
54
55        cache.get_no_policy(key)
56    }
57
58    fn insert<S: BuildHasher>(&self, key: K, value: V, cache: &LightCache<K, V, S, Self>) -> Option<V> {
59        {
60            let mut inner = self.lock_and_prune(cache);
61
62            // were updating the value, so lets reset the creation time
63            if let Some((idx, node)) = inner.arena.get_node_mut(&key) {
64                node.creation = Instant::now();
65
66                inner.arena.move_to_head(idx);
67            } else {
68                inner.arena.insert_head(key);
69            }
70        }
71
72        cache.insert_no_policy(key, value)
73    }
74
75    fn remove<S: BuildHasher>(&self, key: &K, cache: &LightCache<K, V, S, Self>) -> Option<V> {
76        {
77            let mut inner = self.lock_inner();
78
79            inner.prune(cache);
80            inner.arena.remove_item(key);
81        }
82
83        cache.remove_no_policy(key)
84    }
85}
86
87impl<K, V> Prune<K, V, TtlPolicy<K, V>> for TtlPolicyInner<K>
88where
89    K: Copy + Eq + Hash,
90    V: Clone + Sync,
91{
92    fn prune<S: BuildHasher>(&mut self, cache: &LightCache<K, V, S, TtlPolicy<K, V>>) {
93        while let Some((idx, tail)) = self.arena.tail() {
94            if tail.should_evict(self.ttl) {
95                let (_, n) = self.arena.remove(idx);
96                cache.remove_no_policy(n.item());
97            } else {
98                break;
99            }
100        }
101    }
102}
103
104struct TtlNode<K> {
105    key: K,
106    creation: Instant,
107    parent: Option<usize>,
108    child: Option<usize>,
109}
110
111impl<K> LinkedNode<K> for TtlNode<K>
112where
113    K: Copy + Eq + Hash,
114{
115    fn new(key: K, parent: Option<usize>, child: Option<usize>) -> Self {
116        TtlNode {
117            key,
118            creation: Instant::now(),
119            parent,
120            child,
121        }
122    }
123
124    fn item(&self) -> &K {
125        &self.key
126    }
127
128    fn prev(&self) -> Option<usize> {
129        self.parent
130    }
131
132    fn next(&self) -> Option<usize> {
133        self.child
134    }
135
136    fn set_prev(&mut self, parent: Option<usize>) {
137        self.parent = parent;
138    }
139
140    fn set_next(&mut self, child: Option<usize>) {
141        self.child = child;
142    }
143}
144
145impl<K> Debug for TtlNode<K> {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("TtlNode")
148            .field("creation", &self.creation)
149            .field("parent", &self.parent)
150            .field("child", &self.child)
151            .finish()
152    }
153}
154
155impl<K> TtlNode<K> {
156    fn duration_since_creation(&self) -> Duration {
157        Instant::now().duration_since(self.creation)
158    }
159
160    fn should_evict(&self, ttl: Duration) -> bool {
161        self.duration_since_creation() > ttl
162    }
163}
164
165#[cfg(test)]
166mod test {
167    use hashbrown::hash_map::DefaultHashBuilder;
168
169    use super::*;
170    use std::time::Duration;
171
172    fn duration_seconds(seconds: u64) -> Duration {
173        Duration::from_secs(seconds)
174    }
175
176    fn sleep_seconds(seconds: u64) {
177        std::thread::sleep(duration_seconds(seconds));
178    }
179
180    fn insert_n<S>(cache: &LightCache<i32, i32, S, TtlPolicy<i32, i32>>, n: usize)
181    where
182        S: BuildHasher,
183    {
184        for i in 0..n {
185            cache.insert(i as i32, i as i32);
186        }
187    }
188
189    fn cache<K, V>(ttl: Duration) -> LightCache<K, V, DefaultHashBuilder, TtlPolicy<K, V>>
190    where
191        K: Copy + Eq + Hash,
192        V: Clone + Sync,
193    {
194        LightCache::from_parts(TtlPolicy::new(ttl), Default::default())
195    }
196
197    #[test]
198    /// Insert 5 keys, wait until expiry and insert 2 more keys
199    /// this will remove items from the fron tof the cache
200    fn test_basic_scenario_1() {
201        let cache = cache::<i32, i32>(duration_seconds(1));
202
203        insert_n(&cache, 5);
204
205        sleep_seconds(2);
206
207        insert_n(&cache, 2);
208
209        // 1 should be removed by now
210        assert_eq!(cache.len(), 2);
211        let policy = cache.policy().lock_inner();
212
213        assert_eq!(policy.arena.nodes.len(), 2);
214        assert_eq!(policy.arena.head, Some(1));
215        assert_eq!(policy.arena.tail, Some(0));
216    }
217
218    #[test]
219    fn test_basic_scenario_2() {
220        let cache = cache::<i32, i32>(duration_seconds(1));
221
222        insert_n(&cache, 10);
223
224        cache.remove(&0);
225
226        sleep_seconds(2);
227
228        insert_n(&cache, 2);
229
230        assert_eq!(cache.len(), 2);
231    }
232}