Skip to main content

cache_mod/
sized.rs

1//! Byte-bound cache — arena-backed reference implementation.
2
3use core::hash::Hash;
4use std::collections::HashMap;
5use std::sync::Mutex;
6
7use crate::cache::Cache;
8use crate::error::CacheError;
9use crate::util::MutexExt;
10
11/// A cache bounded by **total byte-weight** rather than entry count.
12///
13/// Each value is weighed by a user-supplied `fn(&V) -> usize` at insert
14/// time. The cache evicts least-recently-accessed entries until the new
15/// entry fits within `max_weight`.
16///
17/// An entry whose own weight exceeds `max_weight` is silently rejected:
18/// the insert returns `None` and the value is dropped. (No cache could
19/// honour such a request.)
20///
21/// 0.6.0 implementation: arena-backed doubly-linked list with O(1) promote
22/// and O(1) eviction (eviction may loop until enough weight is reclaimed).
23/// The lock-strategy upgrade lands in 0.7.0 without changing this public surface.
24///
25/// # Choice of unit
26///
27/// `usize` is unitless — the weigher decides. Common choices:
28///
29/// - `|v: &Vec<u8>| v.len()` — track payload bytes
30/// - `|v: &String| v.len() + std::mem::size_of::<String>()` — include header
31/// - `|_: &T| std::mem::size_of::<T>()` — fixed-size approximation
32///
33/// The weigher is a plain function pointer, not a closure — captured
34/// state would force `Box<dyn Fn>`-style indirection on every weigh call.
35/// If your weighing logic needs state, hoist it into the value itself.
36///
37/// # Capacity reporting
38///
39/// [`capacity`](Cache::capacity) on `SizedCache` returns `max_weight`, not
40/// entry count. Use [`total_weight`](SizedCache::total_weight) to query
41/// the dynamic in-use weight and [`max_weight`](SizedCache::max_weight)
42/// as the explicit alias.
43///
44/// # Example
45///
46/// ```
47/// use cache_mod::{Cache, SizedCache};
48///
49/// fn weigh(payload: &Vec<u8>) -> usize { payload.len() }
50///
51/// let cache: SizedCache<&'static str, Vec<u8>> =
52///     SizedCache::new(1024, weigh).expect("max_weight > 0");
53///
54/// cache.insert("small", vec![0u8; 64]);
55/// assert_eq!(cache.total_weight(), 64);
56/// ```
57pub struct SizedCache<K, V> {
58    max_weight: usize,
59    weigher: fn(&V) -> usize,
60    inner: Mutex<Inner<K, V>>,
61}
62
63struct Node<K, V> {
64    key: K,
65    value: V,
66    weight: usize,
67    prev: Option<usize>,
68    next: Option<usize>,
69}
70
71struct Inner<K, V> {
72    nodes: Vec<Option<Node<K, V>>>,
73    free: Vec<usize>,
74    head: Option<usize>,
75    tail: Option<usize>,
76    map: HashMap<K, usize>,
77    total_weight: usize,
78}
79
80impl<K, V> Inner<K, V>
81where
82    K: Eq + Hash + Clone,
83{
84    fn new() -> Self {
85        Self {
86            nodes: Vec::new(),
87            free: Vec::new(),
88            head: None,
89            tail: None,
90            map: HashMap::new(),
91            total_weight: 0,
92        }
93    }
94
95    fn alloc(&mut self, node: Node<K, V>) -> usize {
96        if let Some(idx) = self.free.pop() {
97            self.nodes[idx] = Some(node);
98            idx
99        } else {
100            self.nodes.push(Some(node));
101            self.nodes.len() - 1
102        }
103    }
104
105    fn dealloc(&mut self, idx: usize) -> Node<K, V> {
106        let node = self.nodes[idx]
107            .take()
108            .unwrap_or_else(|| unreachable!("arena slot must be occupied"));
109        self.free.push(idx);
110        node
111    }
112
113    fn unlink(&mut self, idx: usize) {
114        let (prev, next) = {
115            let n = self.nodes[idx]
116                .as_ref()
117                .unwrap_or_else(|| unreachable!("unlink target must be occupied"));
118            (n.prev, n.next)
119        };
120        match prev {
121            Some(p) => {
122                self.nodes[p]
123                    .as_mut()
124                    .unwrap_or_else(|| unreachable!())
125                    .next = next
126            }
127            None => self.head = next,
128        }
129        match next {
130            Some(n) => {
131                self.nodes[n]
132                    .as_mut()
133                    .unwrap_or_else(|| unreachable!())
134                    .prev = prev
135            }
136            None => self.tail = prev,
137        }
138        if let Some(n) = self.nodes[idx].as_mut() {
139            n.prev = None;
140            n.next = None;
141        }
142    }
143
144    fn push_front(&mut self, idx: usize) {
145        let old_head = self.head;
146        if let Some(n) = self.nodes[idx].as_mut() {
147            n.prev = None;
148            n.next = old_head;
149        }
150        if let Some(h) = old_head {
151            if let Some(n) = self.nodes[h].as_mut() {
152                n.prev = Some(idx);
153            }
154        } else {
155            self.tail = Some(idx);
156        }
157        self.head = Some(idx);
158    }
159
160    fn promote(&mut self, idx: usize) {
161        if self.head == Some(idx) {
162            return;
163        }
164        self.unlink(idx);
165        self.push_front(idx);
166    }
167
168    /// Pop the LRU tail and return the evicted node's (key, weight).
169    fn evict_tail(&mut self) -> Option<(K, usize)> {
170        let tail_idx = self.tail?;
171        self.unlink(tail_idx);
172        let node = self.dealloc(tail_idx);
173        self.total_weight = self.total_weight.saturating_sub(node.weight);
174        Some((node.key, node.weight))
175    }
176}
177
178impl<K, V> SizedCache<K, V>
179where
180    K: Eq + Hash + Clone,
181    V: Clone,
182{
183    /// Creates a cache with the given byte-weight ceiling.
184    ///
185    /// Returns [`CacheError::InvalidCapacity`] if `max_weight == 0`.
186    ///
187    /// # Example
188    ///
189    /// ```
190    /// use cache_mod::SizedCache;
191    ///
192    /// fn weigh(s: &String) -> usize { s.len() }
193    ///
194    /// let cache: SizedCache<u32, String> =
195    ///     SizedCache::new(4096, weigh).expect("max_weight > 0");
196    /// ```
197    pub fn new(max_weight: usize, weigher: fn(&V) -> usize) -> Result<Self, CacheError> {
198        if max_weight == 0 {
199            return Err(CacheError::InvalidCapacity);
200        }
201        Ok(Self {
202            max_weight,
203            weigher,
204            inner: Mutex::new(Inner::new()),
205        })
206    }
207
208    /// The configured byte-weight ceiling.
209    pub fn max_weight(&self) -> usize {
210        self.max_weight
211    }
212
213    /// Current total weight of all cached entries.
214    pub fn total_weight(&self) -> usize {
215        self.inner.lock_recover().total_weight
216    }
217}
218
219impl<K, V> Cache<K, V> for SizedCache<K, V>
220where
221    K: Eq + Hash + Clone,
222    V: Clone,
223{
224    fn get(&self, key: &K) -> Option<V> {
225        let mut inner = self.inner.lock_recover();
226        let idx = *inner.map.get(key)?;
227        inner.promote(idx);
228        inner.nodes[idx].as_ref().map(|n| n.value.clone())
229    }
230
231    fn insert(&self, key: K, value: V) -> Option<V> {
232        let new_weight = (self.weigher)(&value);
233        // Reject values that cannot possibly fit.
234        if new_weight > self.max_weight {
235            return None;
236        }
237
238        let mut inner = self.inner.lock_recover();
239
240        // Live update: replace value, fix up weight delta, promote.
241        if let Some(&idx) = inner.map.get(&key) {
242            let (old_value, old_weight) = inner.nodes[idx]
243                .as_mut()
244                .map(|n| {
245                    let ov = core::mem::replace(&mut n.value, value);
246                    let ow = n.weight;
247                    n.weight = new_weight;
248                    (ov, ow)
249                })
250                .unwrap_or_else(|| unreachable!("mapped index must be occupied"));
251            inner.total_weight = inner
252                .total_weight
253                .saturating_add(new_weight)
254                .saturating_sub(old_weight);
255            inner.promote(idx);
256            // Live update may have overshot — evict from LRU until fit.
257            while inner.total_weight > self.max_weight {
258                if inner.evict_tail().is_none() {
259                    break;
260                }
261            }
262            // Pop the updated key from any map entry left dangling if it
263            // was evicted. (Shouldn't happen — we promoted it to head — but
264            // be defensive.)
265            return Some(old_value);
266        }
267
268        // New entry — evict from the LRU end until the projected total fits.
269        while inner.total_weight.saturating_add(new_weight) > self.max_weight {
270            match inner.evict_tail() {
271                Some((evicted_key, _)) => {
272                    let _ = inner.map.remove(&evicted_key);
273                }
274                None => break,
275            }
276        }
277
278        let idx = inner.alloc(Node {
279            key: key.clone(),
280            value,
281            weight: new_weight,
282            prev: None,
283            next: None,
284        });
285        inner.push_front(idx);
286        let _ = inner.map.insert(key, idx);
287        inner.total_weight = inner.total_weight.saturating_add(new_weight);
288        None
289    }
290
291    fn remove(&self, key: &K) -> Option<V> {
292        let mut inner = self.inner.lock_recover();
293        let idx = inner.map.remove(key)?;
294        inner.unlink(idx);
295        let node = inner.dealloc(idx);
296        inner.total_weight = inner.total_weight.saturating_sub(node.weight);
297        Some(node.value)
298    }
299
300    fn contains_key(&self, key: &K) -> bool {
301        self.inner.lock_recover().map.contains_key(key)
302    }
303
304    fn len(&self) -> usize {
305        self.inner.lock_recover().map.len()
306    }
307
308    fn clear(&self) {
309        let mut inner = self.inner.lock_recover();
310        inner.nodes.clear();
311        inner.free.clear();
312        inner.head = None;
313        inner.tail = None;
314        inner.map.clear();
315        inner.total_weight = 0;
316    }
317
318    /// For `SizedCache`, capacity is the configured `max_weight` — see the
319    /// type-level docs.
320    fn capacity(&self) -> usize {
321        self.max_weight
322    }
323}