Skip to main content

cache_mod/
sized.rs

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