Skip to main content

oxistore_cache/
builder.rs

1//! Cache builder — ergonomic constructor for all cache policies.
2//!
3//! [`CacheBuilder`] lets callers configure a cache through a fluent API and
4//! then construct the desired implementation.  Optional fields (`max_bytes`,
5//! `n_shards`) are used by the wrapper types but are not required for basic
6//! policy caches.
7
8use crate::bounded::BoundedCache;
9use crate::sharded::ShardedCache;
10use crate::{ArcCache, LfuCache, LruCache, WTinyLfuCache};
11
12/// The eviction policy to use.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum CachePolicy {
15    /// Least-Recently-Used — classic recency-based eviction.
16    Lru,
17    /// Adaptive Replacement Cache — balances recency and frequency.
18    Arc,
19    /// Least-Frequently-Used — O(1) frequency-based eviction.
20    Lfu,
21    /// Window TinyLFU — near-optimal admission policy with CMS frequency estimation.
22    WTinyLfu,
23}
24
25/// Builder for cache instances.
26///
27/// # Examples
28///
29/// ```rust
30/// use oxistore_cache::builder::{CacheBuilder, CachePolicy};
31///
32/// let lru = CacheBuilder::new(128)
33///     .policy(CachePolicy::Lru)
34///     .build_lru();
35///
36/// let arc = CacheBuilder::new(256)
37///     .policy(CachePolicy::Arc)
38///     .build_arc();
39/// ```
40#[derive(Debug, Clone)]
41pub struct CacheBuilder {
42    /// Number of cache entries (item count, not bytes).
43    capacity: usize,
44    /// Eviction policy selection (informational; concrete build methods ignore it).
45    policy: CachePolicy,
46    /// Optional byte-budget cap for `BoundedCache`.
47    max_bytes: Option<usize>,
48    /// Optional shard count for `ShardedCache` (must be power of 2).
49    n_shards: Option<usize>,
50}
51
52impl CacheBuilder {
53    /// Create a builder with the given entry-count capacity.
54    #[must_use]
55    pub fn new(capacity: usize) -> Self {
56        CacheBuilder {
57            capacity,
58            policy: CachePolicy::Lru,
59            max_bytes: None,
60            n_shards: None,
61        }
62    }
63
64    /// Set the eviction policy.
65    #[must_use]
66    pub fn policy(mut self, p: CachePolicy) -> Self {
67        self.policy = p;
68        self
69    }
70
71    /// Set a byte-budget limit (used by [`CacheBuilder::build_bounded_lru`]).
72    #[must_use]
73    pub fn max_bytes(mut self, b: usize) -> Self {
74        self.max_bytes = Some(b);
75        self
76    }
77
78    /// Set the number of shards (used by [`CacheBuilder::build_sharded`]).
79    ///
80    /// Must be a power of two.
81    #[must_use]
82    pub fn n_shards(mut self, n: usize) -> Self {
83        self.n_shards = Some(n);
84        self
85    }
86
87    /// Build a [`LruCache`] with the configured capacity.
88    #[must_use]
89    pub fn build_lru(self) -> LruCache<Vec<u8>, Vec<u8>> {
90        LruCache::new(self.capacity)
91    }
92
93    /// Build an [`ArcCache`] with the configured capacity.
94    #[must_use]
95    pub fn build_arc(self) -> ArcCache<Vec<u8>, Vec<u8>> {
96        ArcCache::new(self.capacity)
97    }
98
99    /// Build an [`LfuCache`] with the configured capacity.
100    #[must_use]
101    pub fn build_lfu(self) -> LfuCache<Vec<u8>, Vec<u8>> {
102        LfuCache::new(self.capacity)
103    }
104
105    /// Build a [`WTinyLfuCache`] with the configured capacity.
106    #[must_use]
107    pub fn build_wtinylfu(self) -> WTinyLfuCache<Vec<u8>, Vec<u8>> {
108        WTinyLfuCache::new(self.capacity)
109    }
110
111    /// Build a [`BoundedCache`] wrapping an LRU inner cache.
112    ///
113    /// Uses `max_bytes` if set; otherwise defaults to `capacity * 64` (a rough
114    /// 64-byte average per entry).
115    #[must_use]
116    pub fn build_bounded_lru(self) -> BoundedCache<LruCache<Vec<u8>, Vec<u8>>> {
117        let max_bytes = self.max_bytes.unwrap_or(self.capacity * 64);
118        BoundedCache::new(LruCache::new(self.capacity), max_bytes)
119    }
120
121    /// Build a [`ShardedCache`] with LRU shards.
122    ///
123    /// Uses `n_shards` if set; otherwise defaults to 8.
124    /// The capacity is split evenly across shards (`capacity / n_shards`).
125    ///
126    /// # Panics
127    ///
128    /// Panics if the resolved `n_shards` is not a power of two or is zero.
129    #[must_use]
130    pub fn build_sharded(self) -> ShardedCache {
131        let n = self.n_shards.unwrap_or(8);
132        let shard_cap = (self.capacity / n).max(1);
133        ShardedCache::new(n, shard_cap)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::Cache;
141
142    #[test]
143    fn builder_lru() {
144        let mut cache = CacheBuilder::new(4).policy(CachePolicy::Lru).build_lru();
145        cache.put(b"k".to_vec(), b"v".to_vec());
146        assert_eq!(cache.get(&b"k".to_vec()), Some(&b"v".to_vec()));
147        assert_eq!(cache.cap(), 4);
148    }
149
150    #[test]
151    fn builder_arc() {
152        let mut cache = CacheBuilder::new(4).policy(CachePolicy::Arc).build_arc();
153        cache.put(b"k".to_vec(), b"v".to_vec());
154        assert_eq!(cache.get(&b"k".to_vec()), Some(&b"v".to_vec()));
155        assert_eq!(cache.cap(), 4);
156    }
157
158    #[test]
159    fn builder_lfu() {
160        let mut cache = CacheBuilder::new(4).policy(CachePolicy::Lfu).build_lfu();
161        cache.put(b"k".to_vec(), b"v".to_vec());
162        assert_eq!(cache.get(&b"k".to_vec()), Some(&b"v".to_vec()));
163        assert_eq!(cache.cap(), 4);
164    }
165
166    #[test]
167    fn builder_wtinylfu() {
168        let mut cache = CacheBuilder::new(10)
169            .policy(CachePolicy::WTinyLfu)
170            .build_wtinylfu();
171        cache.put(b"k".to_vec(), b"v".to_vec());
172        assert_eq!(cache.cap(), 10);
173    }
174
175    #[test]
176    fn builder_bounded_default_budget() {
177        let cache = CacheBuilder::new(8).build_bounded_lru();
178        assert_eq!(cache.max_bytes(), 8 * 64);
179    }
180
181    #[test]
182    fn builder_bounded_explicit_budget() {
183        let mut cache = CacheBuilder::new(100).max_bytes(50).build_bounded_lru();
184        assert_eq!(cache.max_bytes(), 50);
185        // Insert entries that together are at most 50 bytes.
186        cache.put(b"key1".to_vec(), b"val1".to_vec()); // 8 bytes
187        assert!(cache.current_bytes() <= 50);
188    }
189
190    #[test]
191    fn builder_sharded_default() {
192        let cache = CacheBuilder::new(64).build_sharded();
193        assert_eq!(cache.n_shards(), 8);
194    }
195
196    #[test]
197    fn builder_sharded_custom_shards() {
198        let cache = CacheBuilder::new(32).n_shards(4).build_sharded();
199        assert_eq!(cache.n_shards(), 4);
200        assert_eq!(cache.shard_cap(), 8); // 32 / 4
201    }
202
203    #[test]
204    fn builder_each_policy_usable() {
205        // Smoke-test: each policy type can be constructed and used.
206        let mut lru = CacheBuilder::new(4).build_lru();
207        lru.put(b"a".to_vec(), b"1".to_vec());
208
209        let mut arc = CacheBuilder::new(4).build_arc();
210        arc.put(b"a".to_vec(), b"1".to_vec());
211
212        let mut lfu = CacheBuilder::new(4).build_lfu();
213        lfu.put(b"a".to_vec(), b"1".to_vec());
214
215        let mut wtlfu = CacheBuilder::new(10).build_wtinylfu();
216        wtlfu.put(b"a".to_vec(), b"1".to_vec());
217
218        let mut bounded = CacheBuilder::new(4).max_bytes(200).build_bounded_lru();
219        bounded.put(b"a".to_vec(), b"1".to_vec());
220
221        let sharded = CacheBuilder::new(16).n_shards(4).build_sharded();
222        sharded.put(b"a".to_vec(), b"1".to_vec());
223        assert_eq!(sharded.get(b"a"), Some(b"1".to_vec()));
224    }
225}