Skip to main content

oxistore_cache/
lfu.rs

1/// O(1) Least-Frequently-Used (LFU) cache.
2///
3/// Implements the constant-time LFU algorithm from:
4///   Shah, K., Mitra, A. & Matani, D. (2010). An O(1) algorithm for
5///   implementing the LFU cache eviction scheme. Technical Report.
6///
7/// ## Data structures
8///
9/// - `key_to_entry`: `HashMap<K, (freq, CacheEntry<V>)>` — maps each key to
10///   its current frequency count and cached value.
11/// - `freq_to_keys`: `HashMap<u64, LinkedHashMap<K, ()>>` — maps each
12///   frequency to a FIFO-ordered set of keys at that frequency.  Within a
13///   frequency bucket, the *front* is the oldest (to be evicted first).
14/// - `min_freq`: the smallest frequency present in the cache; used to locate
15///   the eviction candidate in O(1).
16///
17/// ## Complexity
18///
19/// All operations (`get`, `put`, eviction) run in O(1) amortized time.
20use std::collections::HashMap;
21use std::hash::Hash;
22
23use hashlink::LinkedHashMap;
24
25use crate::{Cache, CacheEntry};
26
27/// An LFU cache with a fixed capacity and optional per-entry TTL.
28///
29/// Entries with equal frequency are evicted in FIFO order (the entry inserted
30/// earlier at that frequency is evicted first).
31///
32/// # Type parameters
33///
34/// - `K`: key type — must be `Eq + Hash + Clone`.
35/// - `V`: value type.
36pub struct LfuCache<K, V> {
37    cap: usize,
38    min_freq: u64,
39    /// key -> (frequency, entry)
40    key_to_entry: HashMap<K, (u64, CacheEntry<V>), ahash::RandomState>,
41    /// frequency -> FIFO-ordered key set (front = oldest = next to evict)
42    freq_to_keys: HashMap<u64, LinkedHashMap<K, (), ahash::RandomState>, ahash::RandomState>,
43}
44
45impl<K, V> LfuCache<K, V>
46where
47    K: Eq + Hash + Clone,
48{
49    /// Create a new `LfuCache` with the given capacity.
50    ///
51    /// A capacity of `0` is valid; every insert will immediately be evicted on
52    /// the next insert.
53    #[must_use]
54    pub fn new(cap: usize) -> Self {
55        LfuCache {
56            cap,
57            min_freq: 0,
58            key_to_entry: HashMap::with_hasher(ahash::RandomState::default()),
59            freq_to_keys: HashMap::with_hasher(ahash::RandomState::default()),
60        }
61    }
62
63    /// Increment the frequency of an existing key and update bookkeeping.
64    ///
65    /// This is called both on `get` (after a hit) and on `put` (when updating
66    /// an existing key).  The caller must ensure `key` is present in
67    /// `key_to_entry`.
68    fn increment_freq(&mut self, key: &K) {
69        let (freq, _entry) = match self.key_to_entry.get_mut(key) {
70            Some(pair) => pair,
71            None => return,
72        };
73        let old_freq = *freq;
74        let new_freq = old_freq + 1;
75        *freq = new_freq;
76
77        // Remove from old frequency bucket.
78        if let Some(bucket) = self.freq_to_keys.get_mut(&old_freq) {
79            bucket.remove(key);
80            if bucket.is_empty() {
81                self.freq_to_keys.remove(&old_freq);
82                // If we just emptied the min-freq bucket, advance min_freq.
83                if old_freq == self.min_freq {
84                    self.min_freq = new_freq;
85                }
86            }
87        }
88
89        // Insert into new frequency bucket at the back (MRU within frequency).
90        self.freq_to_keys
91            .entry(new_freq)
92            .or_insert_with(|| LinkedHashMap::with_hasher(ahash::RandomState::default()))
93            .insert(key.clone(), ());
94    }
95
96    /// Evict the entry with the lowest frequency (FIFO within that frequency).
97    fn evict(&mut self) -> Option<V> {
98        let bucket = self.freq_to_keys.get_mut(&self.min_freq)?;
99        // pop_front gives us the oldest key at this frequency.
100        let (evict_key, ()) = bucket.pop_front()?;
101        if bucket.is_empty() {
102            self.freq_to_keys.remove(&self.min_freq);
103        }
104        let (_freq, entry) = self.key_to_entry.remove(&evict_key)?;
105        Some(entry.value)
106    }
107
108    /// Internal put: inserts or updates a key with a pre-built `CacheEntry`.
109    fn insert_entry(&mut self, key: K, entry: CacheEntry<V>) -> Option<V> {
110        if self.cap == 0 {
111            return Some(entry.value);
112        }
113
114        // Key already present: update value + TTL and increment frequency.
115        if self.key_to_entry.contains_key(&key) {
116            let pair = self.key_to_entry.get_mut(&key).expect("confirmed present");
117            pair.1 = entry;
118            self.increment_freq(&key);
119            return None;
120        }
121
122        // New key: evict if at capacity.
123        let evicted = if self.key_to_entry.len() >= self.cap {
124            self.evict()
125        } else {
126            None
127        };
128
129        // Insert with frequency = 1.
130        self.key_to_entry.insert(key.clone(), (1, entry));
131        self.freq_to_keys
132            .entry(1)
133            .or_insert_with(|| LinkedHashMap::with_hasher(ahash::RandomState::default()))
134            .insert(key, ());
135        self.min_freq = 1;
136
137        evicted
138    }
139
140    /// Return the value for `key`, incrementing its frequency.
141    ///
142    /// If the entry has expired (TTL), it is removed and `None` is returned
143    /// without updating frequency or recency.
144    pub fn get(&mut self, key: &K) -> Option<&V> {
145        // Check expiry first.
146        let expired = self
147            .key_to_entry
148            .get(key)
149            .map(|(_, e)| e.is_expired())
150            .unwrap_or(false);
151
152        if expired {
153            // Remove from key_to_entry and freq_to_keys.
154            if let Some((freq, _entry)) = self.key_to_entry.remove(key) {
155                if let Some(bucket) = self.freq_to_keys.get_mut(&freq) {
156                    bucket.remove(key);
157                    if bucket.is_empty() {
158                        self.freq_to_keys.remove(&freq);
159                    }
160                }
161            }
162            return None;
163        }
164
165        if !self.key_to_entry.contains_key(key) {
166            return None;
167        }
168
169        self.increment_freq(key);
170        self.key_to_entry.get(key).map(|(_, e)| &e.value)
171    }
172
173    /// Insert or update `key` -> `value` (no TTL).
174    pub fn put(&mut self, key: K, value: V) -> Option<V> {
175        self.insert_entry(key, CacheEntry::new(value))
176    }
177
178    /// Insert or update `key` -> `value` with a TTL.
179    pub fn put_with_ttl(&mut self, key: K, value: V, ttl: std::time::Duration) -> Option<V> {
180        self.insert_entry(key, CacheEntry::with_ttl(value, ttl))
181    }
182
183    /// Number of entries currently in the cache.
184    #[must_use]
185    pub fn len(&self) -> usize {
186        self.key_to_entry.len()
187    }
188
189    /// Return `true` if the cache is empty.
190    #[must_use]
191    pub fn is_empty(&self) -> bool {
192        self.key_to_entry.is_empty()
193    }
194
195    /// Cache capacity.
196    #[must_use]
197    pub fn cap(&self) -> usize {
198        self.cap
199    }
200
201    /// Read a value without updating frequency or recency.
202    ///
203    /// Returns `None` if absent or expired.
204    #[must_use]
205    pub fn peek(&self, key: &K) -> Option<&V> {
206        self.key_to_entry.get(key).and_then(
207            |(_, e)| {
208                if e.is_expired() {
209                    None
210                } else {
211                    Some(&e.value)
212                }
213            },
214        )
215    }
216
217    /// Return `true` if `key` is present and not expired.
218    #[must_use]
219    pub fn contains_key(&self, key: &K) -> bool {
220        self.key_to_entry
221            .get(key)
222            .map(|(_, e)| !e.is_expired())
223            .unwrap_or(false)
224    }
225
226    /// Remove the entry for `key`, returning its value.
227    pub fn remove(&mut self, key: &K) -> Option<V> {
228        if let Some((freq, entry)) = self.key_to_entry.remove(key) {
229            if let Some(bucket) = self.freq_to_keys.get_mut(&freq) {
230                bucket.remove(key);
231                if bucket.is_empty() {
232                    self.freq_to_keys.remove(&freq);
233                }
234            }
235            Some(entry.value)
236        } else {
237            None
238        }
239    }
240
241    /// Remove all entries.
242    pub fn clear(&mut self) {
243        self.key_to_entry.clear();
244        self.freq_to_keys.clear();
245        self.min_freq = 0;
246    }
247
248    /// Dynamically resize the cache capacity.
249    ///
250    /// If `new_cap < current len`, entries at `min_freq` are evicted first
251    /// (FIFO within that frequency), then the next lowest frequency, etc.
252    pub fn resize(&mut self, new_cap: usize) {
253        self.cap = new_cap;
254        while self.cap > 0 && self.key_to_entry.len() > self.cap {
255            self.evict();
256        }
257    }
258}
259
260impl<K, V> Cache<K, V> for LfuCache<K, V>
261where
262    K: Eq + Hash + Clone,
263{
264    fn get(&mut self, key: &K) -> Option<&V> {
265        LfuCache::get(self, key)
266    }
267
268    fn put(&mut self, key: K, value: V) -> Option<V> {
269        LfuCache::put(self, key, value)
270    }
271
272    fn put_with_ttl(&mut self, key: K, value: V, ttl: std::time::Duration) -> Option<V> {
273        LfuCache::put_with_ttl(self, key, value, ttl)
274    }
275
276    fn len(&self) -> usize {
277        self.key_to_entry.len()
278    }
279
280    fn cap(&self) -> usize {
281        self.cap
282    }
283
284    fn remove(&mut self, key: &K) -> Option<V> {
285        LfuCache::remove(self, key)
286    }
287
288    fn clear(&mut self) {
289        LfuCache::clear(self);
290    }
291
292    fn peek(&self, key: &K) -> Option<&V> {
293        LfuCache::peek(self, key)
294    }
295
296    fn contains_key(&self, key: &K) -> bool {
297        LfuCache::contains_key(self, key)
298    }
299
300    fn resize(&mut self, new_cap: usize) {
301        LfuCache::resize(self, new_cap);
302    }
303}