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}