plain_cache/
cache.rs

1use crate::Stats;
2use parking_lot::{Mutex, RwLock};
3use shard::Shard;
4use std::borrow::Borrow;
5use std::hash::{BuildHasher, Hash};
6use std::num::NonZero;
7use std::time::Instant;
8use std::{cmp, thread};
9
10mod entry;
11mod fixed_size_hash_table;
12mod ring_buffer;
13mod shard;
14pub(crate) mod stats;
15
16pub(crate) type RandomState = ahash::RandomState;
17
18/// Highly performant, thread-safe cache with a focus on simplicity.
19///
20/// It implements the S3-FIFO eviction algorithm as specified in
21/// [FIFO Queues are All You Need for Cache Eviction](https://dl.acm.org/doi/pdf/10.1145/3600006.3613147).
22/// The cache is divided into multiple shards to reduce contention during concurrent access. This
23/// crate does not use any unsafe code.
24///
25/// Wrap the cache in a [`std::sync::Arc`] to share it between threads. Both reads and writes only
26/// require shared references to the cache.
27#[derive(Debug)]
28pub struct Cache<K, V, S = RandomState> {
29    hash_builder: S,
30    shards: Vec<RwLock<Shard<K, V, S>>>,
31    metrics_last_accessed: Mutex<Instant>,
32}
33
34impl<K, V> Cache<K, V, RandomState>
35where
36    K: Clone + Eq + Hash,
37    V: Clone,
38{
39    /// Creates a new cache with at least the specified capacity.
40    ///
41    /// The actual capacity may be slightly higher due to sharding and rounding.
42    pub fn with_capacity(capacity: usize) -> Cache<K, V, RandomState> {
43        Cache::with_capacity_and_hasher(capacity, Default::default())
44    }
45}
46
47impl<K, V, S> Cache<K, V, S>
48where
49    K: Clone + Eq + Hash,
50    V: Clone,
51    S: BuildHasher,
52{
53    /// Inserts a key-value pair into the cache.
54    ///
55    /// If the cache did not have this key present, [`None`] is returned.
56    ///
57    /// If the cache did have this key present, the value is updated, and the old value is returned.
58    pub fn insert(&self, key: K, value: V) -> Option<V> {
59        let hash = self.hash_builder.hash_one(&key);
60        let shard_lock = self.get_shard(hash)?;
61
62        let mut shard = shard_lock.write();
63        shard.insert(key, value)
64    }
65
66    /// Returns the value corresponding to the key.
67    ///
68    /// This method clones the value when returning the item. Consider wrapping your values in
69    /// [`std::sync::Arc`] if cloning is too expensive for you use-case.
70    pub fn get<Q>(&self, key: &Q) -> Option<V>
71    where
72        K: Borrow<Q>,
73        Q: ?Sized + Hash + Eq,
74    {
75        let hash = self.hash_builder.hash_one(key);
76        let shard_lock = self.get_shard(hash)?;
77
78        let shard = shard_lock.read();
79        shard.get(key)
80    }
81
82    fn get_shard(&self, hash: u64) -> Option<&RwLock<Shard<K, V, S>>> {
83        let shard_idx = hash as usize % (cmp::max(self.shards.len(), 2) - 1);
84        self.shards.get(shard_idx)
85    }
86}
87
88impl<K, V, S> Cache<K, V, S>
89where
90    K: Clone + Eq + Hash,
91    V: Clone,
92    S: Clone + BuildHasher,
93{
94    /// Creates a new cache with the at least the specified capacity, using `hasher` to hash the
95    /// keys.
96    ///
97    /// The actual capacity may be slightly higher due to sharding and rounding.
98    pub fn with_capacity_and_hasher(capacity: usize, hash_builder: S) -> Cache<K, V, S> {
99        let available_parallelism = thread::available_parallelism()
100            .map(NonZero::get)
101            .unwrap_or(1);
102
103        let number_of_shards = cmp::min(available_parallelism * 4, capacity);
104
105        let mut shards = Vec::with_capacity(number_of_shards);
106
107        let metrics_last_accessed = Mutex::new(Instant::now());
108
109        if number_of_shards == 0 {
110            return Self {
111                hash_builder,
112                shards,
113                metrics_last_accessed,
114            };
115        }
116
117        let capacity_per_shard = capacity.div_ceil(number_of_shards);
118
119        for _ in 0..number_of_shards {
120            let shard = Shard::with_capacity_and_hasher(capacity_per_shard, hash_builder.clone());
121            shards.push(RwLock::new(shard))
122        }
123
124        Self {
125            hash_builder,
126            shards,
127            metrics_last_accessed,
128        }
129    }
130}
131
132impl<K, V, S> Cache<K, V, S> {
133    /// Returns cache performance statistics and resets the internal counters.
134    ///
135    /// This method provides metrics about cache performance since the last call to `stats()`.
136    /// After returning the statistics, all internal counters are reset to zero.
137    ///
138    /// # Examples
139    ///
140    /// Basic usage:
141    /// ```
142    /// use plain_cache::Cache;
143    ///
144    /// let cache = Cache::with_capacity(100);
145    /// cache.insert("key1", "value1");
146    /// cache.get("key1"); // hit
147    /// cache.get("key2"); // miss
148    ///
149    /// let stats = cache.stats();
150    /// println!("Hits: {}, Misses: {}", stats.hit_count, stats.miss_count);
151    /// ```
152    ///
153    /// Calculating hit rate:
154    /// ```
155    /// use plain_cache::Cache;
156    ///
157    /// let cache = Cache::with_capacity(100);
158    /// // ... perform cache operations ...
159    ///
160    /// let stats = cache.stats();
161    /// let total_requests = stats.hit_count + stats.miss_count;
162    /// if total_requests > 0 {
163    ///     let hit_rate = stats.hit_count as f64 / total_requests as f64;
164    ///     println!("Hit rate: {:.2}%", hit_rate * 100.0);
165    /// }
166    /// ```
167    ///
168    /// # Note
169    ///
170    /// The counters are reset after each call to `stats()`, so each call returns
171    /// statistics for the period since the previous call. This allows for periodic
172    /// monitoring of cache performance.
173    pub fn stats(&self) -> Stats {
174        let mut stats = Stats::default();
175
176        let millis_elapsed = {
177            let mut guard = self.metrics_last_accessed.lock();
178            let millis_elapsed = guard.elapsed().as_millis();
179            *guard = Instant::now();
180            millis_elapsed
181        };
182
183        stats.millis_elapsed = millis_elapsed;
184
185        for shard in &self.shards {
186            let shard = shard.read();
187            stats.hit_count += shard.hit_count();
188            stats.miss_count += shard.miss_count();
189            stats.eviction_count += shard.eviction_count();
190            shard.reset_counters();
191        }
192
193        stats
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use std::sync::Arc;
201    use std::thread;
202
203    #[test]
204    fn it_inserts_and_gets_basic_values() {
205        // given
206        let cache = Cache::with_capacity(100);
207
208        // when
209        cache.insert("key1", "value1");
210
211        // then
212        assert_eq!(cache.get("key1"), Some("value1"));
213        assert_eq!(cache.get("key2"), None);
214    }
215
216    #[test]
217    fn it_updates_existing_value() {
218        // given
219        let cache = Cache::with_capacity(100);
220        cache.insert("key1", "value1");
221
222        // when
223        let old_value = cache.insert("key1", "new_value");
224
225        // then
226        assert_eq!(old_value, Some("value1"));
227        assert_eq!(cache.get("key1"), Some("new_value"));
228    }
229
230    #[test]
231    fn it_handles_zero_capacity() {
232        // given
233        let cache = Cache::with_capacity(0);
234
235        // when
236        cache.insert("key1", "value1");
237
238        // then
239        assert_eq!(cache.get("key1"), None);
240    }
241
242    #[test]
243    fn it_handles_one_capacity() {
244        // given
245        let cache = Cache::with_capacity(1);
246
247        // when
248        cache.insert("key1", "value1");
249
250        // then
251        assert_eq!(cache.get("key1"), Some("value1"));
252        assert_eq!(cache.get("key2"), None);
253    }
254
255    #[test]
256    fn it_works_with_custom_hasher() {
257        // given
258        use std::collections::hash_map::RandomState;
259        let cache = Cache::with_capacity_and_hasher(100, RandomState::new());
260
261        // when
262        cache.insert("key1", "value1");
263
264        // then
265        assert_eq!(cache.get("key1"), Some("value1"));
266    }
267
268    #[test]
269    fn it_is_thread_safe() {
270        // given
271        let cache: Arc<Cache<String, String>> = Arc::new(Cache::with_capacity(1_000));
272        let mut handles = vec![];
273
274        // when
275        for i in 0..5 {
276            let cache_clone = Arc::clone(&cache);
277            let key = format!("key{}", i);
278            let value = format!("value{}", i);
279            let handle = thread::spawn(move || {
280                cache_clone.insert(key.clone(), value.clone());
281                assert_eq!(cache_clone.get(&key), Some(value));
282            });
283            handles.push(handle);
284        }
285
286        // Wait for all threads to complete
287        for handle in handles {
288            handle.join().unwrap();
289        }
290
291        // then
292        for i in 0..5 {
293            let key = format!("key{}", i);
294            let value = format!("value{}", i);
295            assert_eq!(cache.get(&key), Some(value));
296        }
297    }
298
299    #[test]
300    fn it_respects_capacity_limits() {
301        // given
302        let cache = Cache::with_capacity(2);
303
304        // when
305        cache.insert("key1", "value1");
306        cache.insert("key2", "value2");
307        cache.insert("key3", "value3");
308        cache.insert("key4", "value4");
309
310        // then
311        assert_eq!(cache.get("key1"), None);
312    }
313
314    #[test]
315    fn it_returns_and_resets_stats() {
316        // given
317        let cache = Cache::with_capacity(1_000);
318
319        // when
320        for i in 0..10 {
321            cache.insert(i, i);
322        }
323
324        // 5 hits
325        for i in 0..5 {
326            cache.get(&i);
327        }
328
329        // 5 misses
330        for i in 10..15 {
331            cache.get(&i);
332        }
333
334        // then
335        let stats = cache.stats();
336        assert_eq!(stats.hit_count, 5);
337        assert_eq!(stats.miss_count, 5);
338
339        let stats = cache.stats();
340        assert_eq!(stats.hit_count, 0);
341        assert_eq!(stats.miss_count, 0);
342    }
343}