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    /// cache.insert("key1", "value1");
159    /// cache.get("key1"); // hit
160    /// cache.get("key2"); // miss
161    ///
162    /// let stats = cache.stats();
163    /// let total_requests = stats.hit_count + stats.miss_count;
164    /// if total_requests > 0 {
165    ///     let hit_rate = stats.hit_count as f64 / total_requests as f64;
166    ///     println!("Hit rate: {:.2}%", hit_rate * 100.0);
167    /// }
168    /// ```
169    ///
170    /// # Note
171    ///
172    /// The counters are reset after each call to `stats()`, so each call returns
173    /// statistics for the period since the previous call. This allows for periodic
174    /// monitoring of cache performance.
175    pub fn stats(&self) -> Stats {
176        let mut stats = Stats::default();
177
178        let millis_elapsed = {
179            let mut guard = self.metrics_last_accessed.lock();
180            let millis_elapsed = guard.elapsed().as_millis();
181            *guard = Instant::now();
182            millis_elapsed
183        };
184
185        stats.millis_elapsed = millis_elapsed;
186
187        for shard in &self.shards {
188            let shard = shard.read();
189            stats.hit_count += shard.hit_count();
190            stats.miss_count += shard.miss_count();
191            stats.eviction_count += shard.eviction_count();
192            shard.reset_counters();
193        }
194
195        stats
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use std::sync::Arc;
203    use std::thread;
204
205    #[test]
206    fn it_inserts_and_gets_basic_values() {
207        // given
208        let cache = Cache::with_capacity(100);
209
210        // when
211        cache.insert("key1", "value1");
212
213        // then
214        assert_eq!(cache.get("key1"), Some("value1"));
215        assert_eq!(cache.get("key2"), None);
216    }
217
218    #[test]
219    fn it_updates_existing_value() {
220        // given
221        let cache = Cache::with_capacity(100);
222        cache.insert("key1", "value1");
223
224        // when
225        let old_value = cache.insert("key1", "new_value");
226
227        // then
228        assert_eq!(old_value, Some("value1"));
229        assert_eq!(cache.get("key1"), Some("new_value"));
230    }
231
232    #[test]
233    fn it_handles_zero_capacity() {
234        // given
235        let cache = Cache::with_capacity(0);
236
237        // when
238        cache.insert("key1", "value1");
239
240        // then
241        assert_eq!(cache.get("key1"), None);
242    }
243
244    #[test]
245    fn it_handles_one_capacity() {
246        // given
247        let cache = Cache::with_capacity(1);
248
249        // when
250        cache.insert("key1", "value1");
251
252        // then
253        assert_eq!(cache.get("key1"), Some("value1"));
254        assert_eq!(cache.get("key2"), None);
255    }
256
257    #[test]
258    fn it_works_with_custom_hasher() {
259        // given
260        use std::collections::hash_map::RandomState;
261        let cache = Cache::with_capacity_and_hasher(100, RandomState::new());
262
263        // when
264        cache.insert("key1", "value1");
265
266        // then
267        assert_eq!(cache.get("key1"), Some("value1"));
268    }
269
270    #[test]
271    fn it_is_thread_safe() {
272        // given
273        let cache: Arc<Cache<String, String>> = Arc::new(Cache::with_capacity(1_000));
274        let mut handles = vec![];
275
276        // when
277        for i in 0..5 {
278            let cache_clone = Arc::clone(&cache);
279            let key = format!("key{}", i);
280            let value = format!("value{}", i);
281            let handle = thread::spawn(move || {
282                cache_clone.insert(key.clone(), value.clone());
283                assert_eq!(cache_clone.get(&key), Some(value));
284            });
285            handles.push(handle);
286        }
287
288        // Wait for all threads to complete
289        for handle in handles {
290            handle.join().unwrap();
291        }
292
293        // then
294        for i in 0..5 {
295            let key = format!("key{}", i);
296            let value = format!("value{}", i);
297            assert_eq!(cache.get(&key), Some(value));
298        }
299    }
300
301    #[test]
302    fn it_respects_capacity_limits() {
303        // given
304        let cache = Cache::with_capacity(2);
305
306        // when
307        cache.insert("key1", "value1");
308        cache.insert("key2", "value2");
309        cache.insert("key3", "value3");
310        cache.insert("key4", "value4");
311
312        // then
313        assert_eq!(cache.get("key1"), None);
314    }
315
316    #[test]
317    fn it_returns_and_resets_stats() {
318        // given
319        let cache = Cache::with_capacity(1_000);
320
321        // when
322        for i in 0..10 {
323            cache.insert(i, i);
324        }
325
326        // 5 hits
327        for i in 0..5 {
328            cache.get(&i);
329        }
330
331        // 5 misses
332        for i in 10..15 {
333            cache.get(&i);
334        }
335
336        // then
337        let stats = cache.stats();
338        assert_eq!(stats.hit_count, 5);
339        assert_eq!(stats.miss_count, 5);
340
341        let stats = cache.stats();
342        assert_eq!(stats.hit_count, 0);
343        assert_eq!(stats.miss_count, 0);
344    }
345}