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}