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