Skip to main content

memory_budget/
resizable.rs

1//! Contract every cache that wants to participate in the
2//! [`MemoryBudget`](crate::MemoryBudget) reapportionment must satisfy.
3
4/// Minimal interface a memory-budget-aware cache must expose.
5///
6/// All methods are `&self` because caches use interior mutability (a
7/// `Mutex<LruCache<...>>` or a `moka::sync::Cache`). The budget holds
8/// `Weak<dyn Resizable>` handles so a dropped cache is silently
9/// removed from the next tick.
10pub trait Resizable: Send + Sync {
11    /// Human-readable label for log lines and metrics.
12    fn name(&self) -> &str;
13
14    /// Current bytes held by the cache.
15    fn current_bytes(&self) -> u64;
16
17    /// Configured soft cap. Honoured eagerly (eviction-on-insert) and
18    /// at each tick by [`Resizable::set_max_bytes`].
19    fn max_bytes(&self) -> u64;
20
21    /// Update the soft cap. The cache may either evict synchronously
22    /// (small caches) or schedule eviction over the next few ops
23    /// (large caches). Either is acceptable as long as
24    /// [`Resizable::current_bytes`] converges to `new` within one
25    /// budget tick.
26    fn set_max_bytes(&self, new: u64);
27
28    /// Stats sample used by the reapportionment policy.
29    fn stats(&self) -> ResizableStats;
30}
31
32/// Point-in-time stats sample from a registered cache.
33///
34/// All counters are monotonic-since-construction. The policy compares
35/// the delta between consecutive snapshots, so the absolute values
36/// only need to be consistent within a single sample.
37#[derive(Clone, Copy, Debug, Default)]
38pub struct ResizableStats {
39    /// Successful lookups served from the cache.
40    pub hits: u64,
41    /// Lookups that missed and required falling back to the inner store.
42    pub misses: u64,
43    /// Total evictions over the cache's lifetime.
44    pub evictions: u64,
45}
46
47impl ResizableStats {
48    /// Fraction of `hits + misses` served from the cache. `None`
49    /// when no lookups have occurred (avoids division by zero and a
50    /// misleading 0.0 reading on idle caches).
51    #[must_use]
52    pub fn hit_rate(&self) -> Option<f64> {
53        let total = self.hits.checked_add(self.misses)?;
54        if total == 0 {
55            return None;
56        }
57        Some(self.hits as f64 / total as f64)
58    }
59}