hirofa_utils/
cache.rs

1use linked_hash_map::LinkedHashMap;
2use std::ops::{Div, Sub};
3use std::time::{Duration, Instant};
4
5pub trait CacheIFace<K: std::cmp::Eq, O> {
6    fn invalidate_all(&mut self);
7    fn invalidate_stale(&mut self);
8    fn opt(&mut self, key: &K) -> Option<&O>;
9    fn opt_mut(&mut self, key: &K) -> Option<&mut O>;
10    fn opt_no_touch(&self, key: &K) -> Option<&O>;
11    fn get(&mut self, key: &K) -> Option<&O>;
12    fn get_mut(&mut self, key: &K) -> Option<&mut O>;
13    fn contains_key(&self, key: &K) -> bool;
14    fn invalidate(&mut self, key: &K);
15    fn insert(&mut self, key: K, item: O);
16}
17
18struct CacheEntry<O> {
19    item: O,
20    last_used: Instant,
21}
22
23pub struct Cache<K: std::cmp::Eq + std::hash::Hash, O> {
24    // on every get remove and add (oldest items come first)
25    entries: LinkedHashMap<K, CacheEntry<O>>,
26    #[allow(clippy::type_complexity)]
27    producer: Box<dyn Fn(&K) -> Option<O> + Send>,
28    max_inactive_time: Duration,
29    inactive_resolution: Duration,
30    max_size: usize,
31}
32
33impl<K: std::cmp::Eq + std::hash::Hash, O> Cache<K, O> {
34    pub fn new<P>(producer: P, max_inactive_time: Duration, max_size: usize) -> Self
35    where
36        P: Fn(&K) -> Option<O> + Send + 'static,
37    {
38        let inactive_resolution = max_inactive_time.div(10);
39        Cache {
40            entries: LinkedHashMap::new(),
41            producer: Box::new(producer),
42            max_inactive_time,
43            inactive_resolution,
44            max_size,
45        }
46    }
47    pub fn len(&self) -> usize {
48        self.entries.len()
49    }
50    pub fn is_empty(&self) -> bool {
51        self.entries.is_empty()
52    }
53}
54
55impl<K: std::cmp::Eq + std::hash::Hash + Clone, O> CacheIFace<K, O> for Cache<K, O> {
56    fn invalidate_all(&mut self) {
57        self.entries.clear();
58    }
59
60    fn invalidate_stale(&mut self) {
61        let now = Instant::now();
62        let max_age = now.sub(self.max_inactive_time);
63
64        loop {
65            let front_opt: Option<(&K, &CacheEntry<O>)> = self.entries.front();
66            if let Some(entry) = front_opt {
67                let e = entry.1;
68                if e.last_used.lt(&max_age) {
69                    let _drop_entry = self.entries.pop_front();
70                } else {
71                    break;
72                }
73            } else {
74                break;
75            }
76        }
77    }
78
79    fn opt(&mut self, key: &K) -> Option<&O> {
80        let entry_opt = self.entries.get_mut(key);
81        if let Some(e) = entry_opt {
82            let now = Instant::now();
83            // check if the entry falls outside the resolution , prevents entries being reinserted on every get
84            if e.last_used.lt(&now.sub(self.inactive_resolution)) {
85                let mut removed_entry = self.entries.remove(key).unwrap();
86                removed_entry.last_used = now;
87                self.entries.insert(key.clone(), removed_entry);
88            }
89            self.entries.get(key).map(|i| &i.item)
90        } else {
91            None
92        }
93    }
94
95    fn opt_mut(&mut self, key: &K) -> Option<&mut O> {
96        let entry_opt = self.entries.get_mut(key);
97        if let Some(e) = entry_opt {
98            let now = Instant::now();
99            // check if the entry falls outside the resolution , prevents entries being reinserted on every get
100            if e.last_used.lt(&now.sub(self.inactive_resolution)) {
101                let mut removed_entry = self.entries.remove(key).unwrap();
102                removed_entry.last_used = now;
103                self.entries.insert(key.clone(), removed_entry);
104            }
105            self.entries.get_mut(key).map(|i| &mut i.item)
106        } else {
107            None
108        }
109    }
110
111    fn opt_no_touch(&self, key: &K) -> Option<&O> {
112        self.entries.get(key).map(|e| &e.item)
113    }
114
115    fn get(&mut self, key: &K) -> Option<&O> {
116        self.invalidate_stale();
117        if !self.contains_key(key) {
118            let item_opt = (*self.producer)(key);
119            if let Some(item) = item_opt {
120                self.insert(key.clone(), item);
121            }
122        }
123        self.opt(key)
124    }
125
126    fn get_mut(&mut self, key: &K) -> Option<&mut O> {
127        self.invalidate_stale();
128        if !self.contains_key(key) {
129            let item_opt = (*self.producer)(key);
130            if let Some(item) = item_opt {
131                self.insert(key.clone(), item);
132            }
133        }
134        self.opt_mut(key)
135    }
136    fn contains_key(&self, key: &K) -> bool {
137        self.entries.contains_key(key)
138    }
139
140    fn invalidate(&mut self, key: &K) {
141        self.entries.remove(key);
142    }
143
144    fn insert(&mut self, key: K, item: O) {
145        let entry = CacheEntry {
146            item,
147            last_used: Instant::now(),
148        };
149        self.entries.insert(key, entry);
150        while self.entries.len() > self.max_size {
151            let _drop_entry = self.entries.pop_front();
152        }
153    }
154}
155
156#[cfg(test)]
157pub mod tests {
158    use crate::cache::{Cache, CacheIFace};
159    use std::time::Duration;
160
161    fn test_send<S: Send>(_sendable: &S) {
162        //
163    }
164
165    #[test]
166    fn test_cache() {
167        let producer = |key: &&str| Some(format!("entry: {key}"));
168        let mut cache: Cache<&str, String> = Cache::new(producer, Duration::from_secs(2), 10);
169
170        test_send(&cache);
171
172        let _one = cache.get(&"a");
173        let _two = cache.get(&"b");
174        let three = cache.get(&"c");
175        assert_eq!(three.expect("c not found").as_str(), "entry: c");
176
177        assert_eq!(3, cache.len());
178
179        std::thread::sleep(Duration::from_secs(3));
180        cache.invalidate_stale();
181
182        assert_eq!(0, cache.len());
183
184        for x in vec![
185            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
186        ] {
187            let _ = cache.get(&x);
188        }
189
190        assert_eq!(10, cache.len());
191    }
192}