Skip to main content

cache_mod/
lfu.rs

1//! Least-Frequently-Used (LFU) cache.
2
3use core::hash::Hash;
4use std::collections::HashMap;
5use std::num::NonZeroUsize;
6use std::sync::{Mutex, MutexGuard};
7
8use crate::cache::Cache;
9use crate::error::CacheError;
10
11/// A bounded, thread-safe LFU cache.
12///
13/// Each entry carries a counter that is incremented on every [`get`](Cache::get)
14/// or [`insert`](Cache::insert) of an already-present key. On overflow, the
15/// entry with the **lowest counter** is evicted; ties are broken in favour of
16/// evicting the **least-recently-accessed** entry.
17///
18/// [`contains_key`](Cache::contains_key) is a query and does not increment
19/// the counter or touch access order, per the [`Cache`] contract.
20///
21/// This is the 0.3.0 reference implementation: correct and `&self`-everywhere,
22/// `Mutex`-guarded. Eviction is O(n) — a scan for the minimum on overflow.
23/// An O(1) bucket-based implementation lands in 0.5.0 without changing this
24/// public surface.
25///
26/// # Example
27///
28/// ```
29/// use cache_mod::{Cache, LfuCache};
30///
31/// let cache: LfuCache<&'static str, u32> = LfuCache::new(2).expect("capacity > 0");
32///
33/// cache.insert("a", 1);
34/// cache.insert("b", 2);
35///
36/// // Bump "a"'s frequency above "b"'s.
37/// assert_eq!(cache.get(&"a"), Some(1));
38/// assert_eq!(cache.get(&"a"), Some(1));
39///
40/// // Inserting "c" should evict "b" (lowest counter).
41/// cache.insert("c", 3);
42/// assert_eq!(cache.get(&"b"), None);
43/// assert_eq!(cache.get(&"a"), Some(1));
44/// assert_eq!(cache.get(&"c"), Some(3));
45/// ```
46pub struct LfuCache<K, V> {
47    capacity: NonZeroUsize,
48    inner: Mutex<Inner<K, V>>,
49}
50
51struct Entry<V> {
52    value: V,
53    /// Number of accesses (`get` + `insert`-of-existing-key) since insertion.
54    count: u64,
55    /// Monotonic access marker; updated on every access. Lower = older.
56    /// Tie-break secondary criterion when multiple entries share `count`.
57    last_access: u64,
58}
59
60struct Inner<K, V> {
61    map: HashMap<K, Entry<V>>,
62    /// Monotonic counter used to stamp `Entry::last_access`. Wraps with
63    /// `wrapping_add`; long-running caches see one collision per 2^64 ops,
64    /// which is acceptable because tie-breaking is a best-effort secondary
65    /// criterion already.
66    clock: u64,
67}
68
69impl<K, V> LfuCache<K, V>
70where
71    K: Eq + Hash + Clone,
72    V: Clone,
73{
74    /// Creates a cache with the given capacity.
75    ///
76    /// Returns [`CacheError::InvalidCapacity`] if `capacity == 0`.
77    ///
78    /// # Example
79    ///
80    /// ```
81    /// use cache_mod::LfuCache;
82    ///
83    /// let cache: LfuCache<String, u32> = LfuCache::new(128).expect("capacity > 0");
84    /// ```
85    pub fn new(capacity: usize) -> Result<Self, CacheError> {
86        let cap = NonZeroUsize::new(capacity).ok_or(CacheError::InvalidCapacity)?;
87        Ok(Self::with_capacity(cap))
88    }
89
90    /// Creates a cache with the given non-zero capacity. Infallible.
91    ///
92    /// # Example
93    ///
94    /// ```
95    /// use std::num::NonZeroUsize;
96    /// use cache_mod::LfuCache;
97    ///
98    /// let cap = NonZeroUsize::new(64).expect("64 != 0");
99    /// let cache: LfuCache<String, u32> = LfuCache::with_capacity(cap);
100    /// ```
101    pub fn with_capacity(capacity: NonZeroUsize) -> Self {
102        let cap = capacity.get();
103        Self {
104            capacity,
105            inner: Mutex::new(Inner {
106                map: HashMap::with_capacity(cap),
107                clock: 0,
108            }),
109        }
110    }
111
112    fn lock_inner(&self) -> MutexGuard<'_, Inner<K, V>> {
113        match self.inner.lock() {
114            Ok(guard) => guard,
115            Err(poisoned) => poisoned.into_inner(),
116        }
117    }
118}
119
120impl<K, V> Cache<K, V> for LfuCache<K, V>
121where
122    K: Eq + Hash + Clone,
123    V: Clone,
124{
125    fn get(&self, key: &K) -> Option<V> {
126        let mut inner = self.lock_inner();
127        inner.clock = inner.clock.wrapping_add(1);
128        let now = inner.clock;
129        let entry = inner.map.get_mut(key)?;
130        entry.count = entry.count.saturating_add(1);
131        entry.last_access = now;
132        Some(entry.value.clone())
133    }
134
135    fn insert(&self, key: K, value: V) -> Option<V> {
136        let mut inner = self.lock_inner();
137        inner.clock = inner.clock.wrapping_add(1);
138        let now = inner.clock;
139
140        if let Some(existing) = inner.map.get_mut(&key) {
141            let old = core::mem::replace(&mut existing.value, value);
142            existing.count = existing.count.saturating_add(1);
143            existing.last_access = now;
144            return Some(old);
145        }
146
147        // New key — evict if at capacity.
148        if inner.map.len() >= self.capacity.get() {
149            if let Some(victim) = find_victim(&inner.map) {
150                let _ = inner.map.remove(&victim);
151            }
152        }
153
154        let _ = inner.map.insert(
155            key,
156            Entry {
157                value,
158                count: 1,
159                last_access: now,
160            },
161        );
162        None
163    }
164
165    fn remove(&self, key: &K) -> Option<V> {
166        let mut inner = self.lock_inner();
167        inner.map.remove(key).map(|e| e.value)
168    }
169
170    fn contains_key(&self, key: &K) -> bool {
171        self.lock_inner().map.contains_key(key)
172    }
173
174    fn len(&self) -> usize {
175        self.lock_inner().map.len()
176    }
177
178    fn clear(&self) {
179        let mut inner = self.lock_inner();
180        inner.map.clear();
181        inner.clock = 0;
182    }
183
184    fn capacity(&self) -> usize {
185        self.capacity.get()
186    }
187}
188
189/// Eviction target: minimum `count`, ties broken by minimum `last_access`
190/// (least-recently-accessed).
191fn find_victim<K, V>(map: &HashMap<K, Entry<V>>) -> Option<K>
192where
193    K: Clone,
194{
195    map.iter()
196        .min_by(|(_, a), (_, b)| {
197            a.count
198                .cmp(&b.count)
199                .then(a.last_access.cmp(&b.last_access))
200        })
201        .map(|(k, _)| k.clone())
202}