Skip to main content

quick_cache/
lib.rs

1//! Lightweight, high performance concurrent cache. It allows very fast access to the cached items
2//! with little overhead compared to a plain concurrent hash table. No allocations are ever performed
3//! unless the cache internal state table needs growing (which will eventually stabilize).
4//!
5//! # Eviction policy
6//!
7//! The current eviction policy is a modified version of the Clock-PRO algorithm, very similar to the
8//! later published S3-FIFO algorithm. It's "scan resistent" and provides high hit rates,
9//! significantly better than a LRU eviction policy and comparable to other state-of-the art algorithms
10//! like W-TinyLFU.
11//!
12//! # Thread safety and Concurrency
13//!
14//! Both `sync` (thread-safe) and `unsync` (non thread-safe) implementations are provided. The latter
15//! offers slightly better performance when thread safety is not required.
16//!
17//! # Equivalent keys
18//!
19//! The cache uses the [`Equivalent`](https://docs.rs/equivalent/1.0.1/equivalent/trait.Equivalent.html) trait
20//! for gets/removals. It can help work around the `Borrow` limitations.
21//! For example, if the cache key is a tuple `(K, Q)`, you wouldn't be able to access such keys without
22//! building a `&(K, Q)` and thus potentially cloning `K` and/or `Q`.
23//!
24//! # User defined weight
25//!
26//! By implementing the [Weighter] trait the user can define different weights for each cache entry.
27//!
28//! # Atomic operations
29//!
30//! By using the `get_or_insert` or `get_value_or_guard` family of functions (both sync and async variants
31//! are available, they can be mix and matched) the user can coordinate the insertion of entries, so only
32//! one value is "computed" and inserted after a cache miss.
33//!
34//! The `entry` family of functions provide a closure-based API for atomically
35//! inspecting and acting on existing entries (keep, remove, or replace) while also coordinating
36//! insertion on cache misses.
37//!
38//! # Lifecycle hooks
39//!
40//! A user can optionally provide a custom [Lifecycle] implementation to hook into the lifecycle of cache entries.
41//!
42//! Example use cases:
43//! * item pinning, so even if the item occupies weight but isn't allowed to be evicted
44//! * send evicted items to a channel, achieving the equivalent to an eviction listener feature.
45//! * zero out item weights so they are left in the cache instead of evicted.
46//!
47//! # Approximate memory usage
48//!
49//! The memory overhead per entry is `21` bytes.
50//!
51//! The memory usage of the cache data structures can be estimated as:
52//! `(size_of::<K>() + size_of::<V>() + 21) * (length * 1.5).next_power_of_two()`
53//!
54//! Actual memory usage may vary depending on the cache options and the key and value types, which can have external
55//! allocations (e.g. `String`, `Vec`, etc.). The above formula only accounts for the cache's data structures.
56//!
57//! The `1.5` value in the formula above results from `1 + G`, where `G` is the configured ghost allocation specified
58//! in [`OptionsBuilder::ghost_allocation`], which is `0.5` by default.
59//!
60//! # Hasher
61//!
62//! By default the crate uses a fast non-cryptographic hasher (currently [foldhash](https://crates.io/crates/foldhash))
63//! exposed through the opaque [`DefaultHashBuilder`] type and enabled via the `custom-hasher` feature (on by
64//! default). The concrete algorithm is an implementation detail and may change between releases. If the
65//! `custom-hasher` feature is disabled the crate falls back to the std lib implementation instead (currently
66//! Siphash13). Note that a custom hasher can also be provided via the cache's `B` type parameter if desirable.
67//!
68//! # Synchronization primitives
69//!
70//! By default the crate uses [parking_lot](https://crates.io/crates/parking_lot), which is enabled (by default) via
71//! a crate feature with the same name. If `parking_lot` is disabled and `sharded-lock` is enabled, the crate uses
72//! [`crossbeam_utils::sync::ShardedLock`](https://docs.rs/crossbeam-utils/latest/crossbeam_utils/sync/struct.ShardedLock.html)
73//! from [crossbeam-utils](https://crates.io/crates/crossbeam-utils) instead. If both are disabled the crate defaults
74//! to the std lib implementation. The `parking_lot` and `sharded-lock` features are mutually exclusive.
75//!
76//! # Cargo Features
77//!
78//! | Feature | Default | Description |
79//! |---------|---------|-------------|
80//! | `custom-hasher` | ✓ | Use the crate's bundled fast non-cryptographic default hasher (currently [foldhash](https://crates.io/crates/foldhash)) behind the opaque [`DefaultHashBuilder`]. When disabled, falls back to std lib's `RandomState` (currently SipHash-1-3). |
81//! | `parking_lot` | ✓ | Use [parking_lot](https://crates.io/crates/parking_lot) for synchronization primitives. Mutually exclusive with `sharded-lock`. |
82//! | `sharded-lock` | | Use [`crossbeam_utils::sync::ShardedLock`](https://docs.rs/crossbeam-utils/latest/crossbeam_utils/sync/struct.ShardedLock.html) for synchronization primitives. Mutually exclusive with `parking_lot`. |
83//! | `shuttle` | | Enable [shuttle](https://crates.io/crates/shuttle) testing support for concurrency testing. |
84//! | `stats` | | Enable cache statistics tracking via the `hits()`, `misses()`, and per-item `item_stats()` methods. Overhead: adds an 8-byte per-item access counter (`AtomicU64`) to each resident item — raising per-entry memory by up to 8 bytes, depending on layout — and performs two atomic increments per cache hit and one per miss. |
85#![allow(clippy::type_complexity)]
86#![cfg_attr(docsrs, feature(doc_cfg))]
87
88#[cfg(all(feature = "parking_lot", feature = "sharded-lock"))]
89compile_error!("features `parking_lot` and `sharded-lock` are mutually exclusive");
90
91#[cfg(not(fuzzing))]
92mod linked_slab;
93#[cfg(fuzzing)]
94pub mod linked_slab;
95mod options;
96#[cfg(not(feature = "shuttle"))]
97mod rw_lock;
98mod shard;
99mod shim;
100/// Concurrent cache variants that can be used from multiple threads.
101pub mod sync;
102mod sync_placeholder;
103/// Non-concurrent cache variants.
104pub mod unsync;
105pub use equivalent::Equivalent;
106
107#[cfg(all(test, feature = "shuttle"))]
108mod shuttle_tests;
109
110pub use options::{Options, OptionsBuilder};
111
112#[cfg(feature = "custom-hasher")]
113type DefaultHasherImpl = foldhash::quality::RandomState;
114#[cfg(not(feature = "custom-hasher"))]
115type DefaultHasherImpl = std::collections::hash_map::RandomState;
116
117/// The default [`BuildHasher`](std::hash::BuildHasher) used by the cache.
118///
119/// The hashing algorithm behind this type is an implementation detail and may
120/// change in any release. Name this type — don't rely on the concrete hasher
121/// it wraps. The underlying hasher is selected by the `custom-hasher` feature
122/// (a fast non-cryptographic hasher when enabled, the std library's
123/// `RandomState` otherwise).
124#[derive(Clone, Default)]
125pub struct DefaultHashBuilder(DefaultHasherImpl);
126
127impl std::hash::BuildHasher for DefaultHashBuilder {
128    type Hasher = <DefaultHasherImpl as std::hash::BuildHasher>::Hasher;
129
130    #[inline]
131    fn build_hasher(&self) -> Self::Hasher {
132        self.0.build_hasher()
133    }
134}
135
136impl std::fmt::Debug for DefaultHashBuilder {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        f.debug_struct("DefaultHashBuilder").finish_non_exhaustive()
139    }
140}
141
142/// Defines the weight of a cache entry.
143///
144/// # Example
145///
146/// ```
147/// use quick_cache::{sync::Cache, Weighter};
148///
149/// #[derive(Clone)]
150/// struct StringWeighter;
151///
152/// impl Weighter<u64, String> for StringWeighter {
153///     fn weight(&self, _key: &u64, val: &String) -> u64 {
154///         // Be cautious about zero weights!
155///         val.len() as u64
156///     }
157/// }
158///
159/// let cache = Cache::with_weighter(100, 100_000, StringWeighter);
160/// cache.insert(1, "1".to_string());
161/// ```
162pub trait Weighter<Key, Val> {
163    /// Returns the weight of the cache item.
164    ///
165    /// For performance reasons, this function should be trivially cheap as
166    /// it's called during the cache eviction routine.
167    /// If weight is expensive to calculate, consider caching it alongside the value.
168    ///
169    /// Zero (0) weight items are allowed and will be ignored when looking for eviction
170    /// candidates. Such items can only be manually removed or overwritten.
171    ///
172    /// Note that it's undefined behavior for a cache item to change its weight.
173    /// The only exception to this is when Lifecycle::before_evict is called.
174    ///
175    /// It's also undefined behavior in release mode if the summing of weights overflows,
176    /// although this is unlikely to be a problem in practice.
177    fn weight(&self, key: &Key, val: &Val) -> u64;
178}
179
180/// Each cache entry weights exactly `1` unit of weight.
181#[derive(Debug, Clone, Default)]
182pub struct UnitWeighter;
183
184impl<Key, Val> Weighter<Key, Val> for UnitWeighter {
185    #[inline]
186    fn weight(&self, _key: &Key, _val: &Val) -> u64 {
187        1
188    }
189}
190
191/// Hooks into the lifetime of the cache items.
192///
193/// The functions should be small and very fast, otherwise the cache performance might be negatively affected.
194///
195/// # Request state
196///
197/// Operations that may evict items thread a [`RequestState`](Lifecycle::RequestState)
198/// through the eviction hooks. It is a per-request accumulator: the cache constructs a
199/// fresh one via [`Default`], the `on_evict*`/`before_evict` hooks record into it, and it
200/// is finalized by its own [`Drop`] (which, for example, releases evicted items _after_
201/// the shard lock is dropped).
202///
203/// The `_with_lifecycle` cache methods take `&mut RequestState`, letting a caller drive
204/// several operations against a single state — e.g. to batch the eviction work or inspect
205/// evicted items before dropping it:
206///
207/// ```ignore
208/// let mut lcs = Default::default();
209/// cache.insert_with_lifecycle(k1, v1, &mut lcs);
210/// cache.insert_with_lifecycle(k2, v2, &mut lcs);
211/// // inspect `lcs` here if desired; evicted items are released when it drops
212/// ```
213pub trait Lifecycle<Key, Val> {
214    /// Per-request accumulator threaded through the eviction hooks.
215    ///
216    /// Constructed via [`Default`] at the start of each request and finalized by its
217    /// [`Drop`]. Keep it cheap to create and drop.
218    type RequestState: Default;
219
220    /// Returns whether the item is pinned. Items that are pinned can't be evicted.
221    /// Note that a pinned item can still be replaced with get_mut, insert, replace and similar APIs.
222    ///
223    /// Compared to zero (0) weight items, pinned items still consume (non-zero) weight even if they can't
224    /// be evicted. Furthermore, zero (0) weight items are separated from the other entries, which allows
225    /// having a large number of them without impacting performance, but moving them in/out or the evictable
226    /// section has a small cost. Pinning on the other hand doesn't separate entries, so during eviction
227    /// the cache may visit pinned entries but will ignore them.
228    #[allow(unused_variables)]
229    #[inline]
230    fn is_pinned(&self, key: &Key, val: &Val) -> bool {
231        false
232    }
233
234    /// Called when a cache item is about to be evicted.
235    /// Note that value replacement (e.g. insertions for the same key) won't call this method.
236    ///
237    /// This is the only time the item can change its weight. If the item weight becomes zero (0) it
238    /// will be left in the cache, otherwise it'll still be removed. Zero (0) weight items aren't evictable
239    /// and are kept separated from the other items so it's possible to have a large number of them without
240    /// negatively affecting eviction performance.
241    #[allow(unused_variables)]
242    #[inline]
243    fn before_evict(&self, state: &mut Self::RequestState, key: &Key, val: &mut Val) {}
244
245    /// Called when an item is evicted.
246    ///
247    /// To distinguish evictions from the hot vs cold queues, override
248    /// [`Lifecycle::on_evict_hot`] and/or [`Lifecycle::on_evict_cold`] instead;
249    /// they default to delegating here.
250    ///
251    /// If none of `on_evict`, `on_evict_hot`, or `on_evict_cold` is overridden,
252    /// eviction notifications are silently dropped.
253    ///
254    /// Note: items that are rejected without ever being admitted to the cache
255    /// (oversized inserts and oversized placeholder values) are routed through
256    /// [`Lifecycle::on_evict_cold`], which by default reaches this method.
257    #[allow(unused_variables)]
258    #[inline]
259    fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) {}
260
261    /// Called when an item is evicted from the cold queue.
262    ///
263    /// By default delegates to [`Lifecycle::on_evict`].
264    ///
265    /// Note: items that are rejected without ever being admitted to the cache
266    /// (oversized inserts and oversized placeholder values) are also reported
267    /// via this method.
268    #[inline]
269    fn on_evict_cold(&self, state: &mut Self::RequestState, key: Key, val: Val) {
270        self.on_evict(state, key, val)
271    }
272
273    /// Called when an item is evicted from the hot queue.
274    ///
275    /// By default delegates to [`Lifecycle::on_evict`].
276    ///
277    /// Note: rejected (never-admitted) items are reported via
278    /// [`Lifecycle::on_evict_cold`], not this method.
279    #[inline]
280    fn on_evict_hot(&self, state: &mut Self::RequestState, key: Key, val: Val) {
281        self.on_evict(state, key, val)
282    }
283}
284
285/// The memory used by the cache
286///
287/// This struct exposes some implementation details, may change in the future
288#[non_exhaustive]
289#[derive(Debug, Copy, Clone)]
290pub struct MemoryUsed {
291    pub entries: usize,
292    pub map: usize,
293}
294
295impl MemoryUsed {
296    pub fn total(&self) -> usize {
297        self.entries + self.map
298    }
299}
300
301/// Per-item statistics returned by `item_stats`.
302///
303/// Only available with the `stats` feature enabled. Enabling that feature adds an
304/// 8-byte per-item access counter to each resident item (raising per-entry memory
305/// by up to 8 bytes, depending on layout) and performs two atomic increments per
306/// cache hit and one per miss.
307#[cfg(feature = "stats")]
308#[non_exhaustive]
309#[derive(Debug, Copy, Clone, PartialEq, Eq)]
310pub struct ItemStats {
311    /// Number of times the item has been accessed (read) since it became resident.
312    ///
313    /// Incremented on every cache hit (`get`/`get_mut`/`get_value_or_guard`/`entry`).
314    /// Unlike the internal eviction counter, this is monotonic per residency and is
315    /// not bounded by the eviction policy. It resets to zero if the slot is reused for
316    /// a new value (e.g. after eviction and re-insertion).
317    pub access_count: u64,
318}
319
320#[cfg(test)]
321mod tests {
322    use std::{
323        hash::Hash,
324        sync::{atomic::AtomicUsize, Arc},
325        time::Duration,
326    };
327
328    use super::*;
329    #[derive(Clone)]
330    struct StringWeighter;
331
332    impl Weighter<u64, String> for StringWeighter {
333        fn weight(&self, _key: &u64, val: &String) -> u64 {
334            val.len() as u64
335        }
336    }
337
338    #[test]
339    fn test_new() {
340        sync::Cache::<(u64, u64), u64>::new(0);
341        sync::Cache::<(u64, u64), u64>::new(1);
342        sync::Cache::<(u64, u64), u64>::new(2);
343        sync::Cache::<(u64, u64), u64>::new(3);
344        sync::Cache::<(u64, u64), u64>::new(usize::MAX);
345        sync::Cache::<u64, u64>::new(0);
346        sync::Cache::<u64, u64>::new(1);
347        sync::Cache::<u64, u64>::new(2);
348        sync::Cache::<u64, u64>::new(3);
349        sync::Cache::<u64, u64>::new(usize::MAX);
350    }
351
352    #[test]
353    fn test_capacity_one() {
354        // Sync cache with capacity 1 should be able to hold one item
355        let cache = sync::Cache::<u64, u64>::new(1);
356        cache.insert(1, 10);
357        assert_eq!(cache.get(&1), Some(10));
358        // Inserting a second key should evict the first
359        cache.insert(2, 20);
360        assert_eq!(cache.get(&2), Some(20));
361        assert_eq!(cache.get(&1), None);
362
363        // Unsync cache with capacity 1 should also work
364        let mut cache = unsync::Cache::<u64, u64>::new(1);
365        cache.insert(1, 10);
366        assert_eq!(cache.get(&1), Some(&10));
367        cache.insert(2, 20);
368        assert_eq!(cache.get(&2), Some(&20));
369        assert_eq!(cache.get(&1), None);
370
371        // Capacity 0 should store nothing
372        let cache = sync::Cache::<u64, u64>::new(0);
373        cache.insert(1, 10);
374        assert_eq!(cache.get(&1), None);
375    }
376
377    #[test]
378    fn test_custom_cost() {
379        let cache = sync::Cache::with_weighter(100, 100_000, StringWeighter);
380        cache.insert(1, "1".to_string());
381        cache.insert(54, "54".to_string());
382        cache.insert(1000, "1000".to_string());
383        assert_eq!(cache.get(&1000).unwrap(), "1000");
384    }
385
386    #[test]
387    fn test_change_get_mut_change_weight() {
388        let mut cache = unsync::Cache::with_weighter(100, 100_000, StringWeighter);
389        cache.insert(1, "1".to_string());
390        assert_eq!(cache.get(&1).unwrap(), "1");
391        assert_eq!(cache.weight(), 1);
392        let _old = {
393            cache
394                .get_mut(&1)
395                .map(|mut v| std::mem::replace(&mut *v, "11".to_string()))
396        };
397        let _old = {
398            cache
399                .get_mut(&1)
400                .map(|mut v| std::mem::replace(&mut *v, "".to_string()))
401        };
402        assert_eq!(cache.get(&1).unwrap(), "");
403        assert_eq!(cache.weight(), 0);
404        cache.validate(false);
405    }
406
407    #[derive(Debug, Hash)]
408    pub struct Pair<A, B>(pub A, pub B);
409
410    impl<A, B, C, D> PartialEq<(A, B)> for Pair<C, D>
411    where
412        C: PartialEq<A>,
413        D: PartialEq<B>,
414    {
415        fn eq(&self, rhs: &(A, B)) -> bool {
416            self.0 == rhs.0 && self.1 == rhs.1
417        }
418    }
419
420    impl<A, B, X> Equivalent<X> for Pair<A, B>
421    where
422        Pair<A, B>: PartialEq<X>,
423        A: Hash + Eq,
424        B: Hash + Eq,
425    {
426        fn equivalent(&self, other: &X) -> bool {
427            *self == *other
428        }
429    }
430
431    #[test]
432    fn test_equivalent() {
433        let mut cache = unsync::Cache::new(5);
434        cache.insert(("square".to_string(), 2022), "blue".to_string());
435        cache.insert(("square".to_string(), 2023), "black".to_string());
436        assert_eq!(cache.get(&Pair("square", 2022)).unwrap(), "blue");
437    }
438
439    #[test]
440    fn test_borrow_keys() {
441        let cache = sync::Cache::<(Vec<u8>, Vec<u8>), u64>::new(0);
442        cache.get(&Pair(&b""[..], &b""[..]));
443        let cache = sync::Cache::<(String, String), u64>::new(0);
444        cache.get(&Pair("", ""));
445    }
446
447    #[test]
448    #[cfg_attr(miri, ignore)]
449    fn test_get_or_insert() {
450        use rand::prelude::*;
451        for _i in 0..2000 {
452            dbg!(_i);
453            let mut entered = AtomicUsize::default();
454            let cache = sync::Cache::<(u64, u64), u64>::new(100);
455            const THREADS: usize = 100;
456            let wg = std::sync::Barrier::new(THREADS);
457            let solve_at = rand::rng().random_range(0..THREADS);
458            std::thread::scope(|s| {
459                for _ in 0..THREADS {
460                    s.spawn(|| {
461                        wg.wait();
462                        let result = cache.get_or_insert_with(&(1, 1), || {
463                            let before = entered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
464                            if before == solve_at {
465                                Ok(1)
466                            } else {
467                                Err(())
468                            }
469                        });
470                        assert!(matches!(result, Ok(1) | Err(())));
471                    });
472                }
473            });
474            assert_eq!(*entered.get_mut(), solve_at + 1);
475        }
476    }
477
478    #[test]
479    fn test_get_or_insert_unsync() {
480        let mut cache = unsync::Cache::<u64, u64>::new(100);
481        let guard = cache.get_ref_or_guard(&0).unwrap_err();
482        guard.insert(0);
483        assert_eq!(cache.get_ref_or_guard(&0).ok().copied(), Some(0));
484        let guard = cache.get_mut_or_guard(&1).err().unwrap();
485        guard.insert(1);
486        let v = *cache.get_mut_or_guard(&1).ok().unwrap().unwrap();
487        assert_eq!(v, 1);
488        let result = cache.get_or_insert_with::<_, ()>(&0, || panic!());
489        assert_eq!(result, Ok(Some(&0)));
490        let result = cache.get_or_insert_with::<_, ()>(&1, || panic!());
491        assert_eq!(result, Ok(Some(&1)));
492        let result = cache.get_or_insert_with::<_, ()>(&3, || Ok(3));
493        assert_eq!(result, Ok(Some(&3)));
494        let result = cache.get_or_insert_with::<_, ()>(&4, || Err(()));
495        assert_eq!(result, Err(()));
496    }
497
498    #[tokio::test]
499    async fn test_get_or_insert_sync() {
500        use crate::sync::*;
501        let cache = sync::Cache::<u64, u64>::new(100);
502        let GuardResult::Guard(guard) = cache.get_value_or_guard(&0, None) else {
503            panic!();
504        };
505        guard.insert(0).unwrap();
506        let GuardResult::Value(v) = cache.get_value_or_guard(&0, None) else {
507            panic!();
508        };
509        assert_eq!(v, 0);
510        let Err(guard) = cache.get_value_or_guard_async(&1).await else {
511            panic!();
512        };
513        guard.insert(1).unwrap();
514        let Ok(v) = cache.get_value_or_guard_async(&1).await else {
515            panic!();
516        };
517        assert_eq!(v, 1);
518
519        let result = cache.get_or_insert_with::<_, ()>(&0, || panic!());
520        assert_eq!(result, Ok(0));
521        let result = cache.get_or_insert_with::<_, ()>(&3, || Ok(3));
522        assert_eq!(result, Ok(3));
523        let result = cache.get_or_insert_with::<_, ()>(&4, || Err(()));
524        assert_eq!(result, Err(()));
525        let result = cache
526            .get_or_insert_async::<_, ()>(&0, async { panic!() })
527            .await;
528        assert_eq!(result, Ok(0));
529        let result = cache
530            .get_or_insert_async::<_, ()>(&4, async { Err(()) })
531            .await;
532        assert_eq!(result, Err(()));
533        let result = cache
534            .get_or_insert_async::<_, ()>(&4, async { Ok(4) })
535            .await;
536        assert_eq!(result, Ok(4));
537    }
538
539    #[test]
540    fn test_retain_unsync() {
541        let mut cache = unsync::Cache::<u64, u64>::new(100);
542        let ranges = 0..10;
543        for i in ranges.clone() {
544            let guard = cache.get_ref_or_guard(&i).unwrap_err();
545            guard.insert(i);
546            assert_eq!(cache.get_ref_or_guard(&i).ok().copied(), Some(i));
547        }
548        let small = 3;
549        cache.retain(|&key, &val| val > small && key > small);
550        for i in ranges.clone() {
551            let actual = cache.get(&i);
552            if i > small {
553                assert!(actual.is_some());
554                assert_eq!(*actual.unwrap(), i);
555            } else {
556                assert!(actual.is_none());
557            }
558        }
559        let big = 7;
560        cache.retain(|&key, &val| val < big && key < big);
561        for i in ranges {
562            let actual = cache.get(&i);
563            if i > small && i < big {
564                assert!(actual.is_some());
565                assert_eq!(*actual.unwrap(), i);
566            } else {
567                assert!(actual.is_none());
568            }
569        }
570    }
571
572    #[tokio::test]
573    async fn test_retain_sync() {
574        use crate::sync::*;
575        let cache = Cache::<u64, u64>::new(100);
576        let ranges = 0..10;
577        for i in ranges.clone() {
578            let GuardResult::Guard(guard) = cache.get_value_or_guard(&i, None) else {
579                panic!();
580            };
581            guard.insert(i).unwrap();
582            let GuardResult::Value(v) = cache.get_value_or_guard(&i, None) else {
583                panic!();
584            };
585            assert_eq!(v, i);
586        }
587        let small = 4;
588        cache.retain(|&key, &val| val > small && key > small);
589        for i in ranges.clone() {
590            let actual = cache.get(&i);
591            if i > small {
592                assert!(actual.is_some());
593                assert_eq!(actual.unwrap(), i);
594            } else {
595                assert!(actual.is_none());
596            }
597        }
598        let big = 8;
599        cache.retain(|&key, &val| val < big && key < big);
600        for i in ranges {
601            let actual = cache.get(&i);
602            if i > small && i < big {
603                assert!(actual.is_some());
604                assert_eq!(actual.unwrap(), i);
605            } else {
606                assert!(actual.is_none());
607            }
608        }
609    }
610
611    #[test]
612    #[cfg_attr(miri, ignore)]
613    fn test_value_or_guard() {
614        use crate::sync::*;
615        use rand::prelude::*;
616        for _i in 0..2000 {
617            dbg!(_i);
618            let mut entered = AtomicUsize::default();
619            let cache = sync::Cache::<(u64, u64), u64>::new(100);
620            const THREADS: usize = 100;
621            let wg = std::sync::Barrier::new(THREADS);
622            let solve_at = rand::rng().random_range(0..THREADS);
623            std::thread::scope(|s| {
624                for _ in 0..THREADS {
625                    s.spawn(|| {
626                        wg.wait();
627                        loop {
628                            match cache.get_value_or_guard(&(1, 1), Some(Duration::from_millis(1)))
629                            {
630                                GuardResult::Value(v) => assert_eq!(v, 1),
631                                GuardResult::Guard(g) => {
632                                    let before =
633                                        entered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
634                                    if before == solve_at {
635                                        g.insert(1).unwrap();
636                                    }
637                                }
638                                GuardResult::Timeout => continue,
639                            }
640                            break;
641                        }
642                    });
643                }
644            });
645            assert_eq!(*entered.get_mut(), solve_at + 1);
646        }
647    }
648
649    #[tokio::test(flavor = "multi_thread")]
650    #[cfg_attr(miri, ignore)]
651    async fn test_get_or_insert_async() {
652        use rand::prelude::*;
653        for _i in 0..5000 {
654            dbg!(_i);
655            let entered = Arc::new(AtomicUsize::default());
656            let cache = Arc::new(sync::Cache::<(u64, u64), u64>::new(100));
657            const TASKS: usize = 100;
658            let wg = Arc::new(tokio::sync::Barrier::new(TASKS));
659            let solve_at = rand::rng().random_range(0..TASKS);
660            let mut tasks = Vec::new();
661            for _ in 0..TASKS {
662                let cache = cache.clone();
663                let wg = wg.clone();
664                let entered = entered.clone();
665                let task = tokio::spawn(async move {
666                    wg.wait().await;
667                    let result = cache
668                        .get_or_insert_async(&(1, 1), async {
669                            let before = entered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
670                            if before == solve_at {
671                                Ok(1)
672                            } else {
673                                Err(())
674                            }
675                        })
676                        .await;
677                    assert!(matches!(result, Ok(1) | Err(())));
678                });
679                tasks.push(task);
680            }
681            for task in tasks {
682                task.await.unwrap();
683            }
684            assert_eq!(cache.get(&(1, 1)), Some(1));
685            assert_eq!(
686                entered.load(std::sync::atomic::Ordering::Relaxed),
687                solve_at + 1
688            );
689        }
690    }
691
692    #[tokio::test(flavor = "multi_thread")]
693    #[cfg_attr(miri, ignore)]
694    async fn test_value_or_guard_async() {
695        use rand::prelude::*;
696        for _i in 0..5000 {
697            dbg!(_i);
698            let entered = Arc::new(AtomicUsize::default());
699            let cache = Arc::new(sync::Cache::<(u64, u64), u64>::new(100));
700            const TASKS: usize = 100;
701            let wg = Arc::new(tokio::sync::Barrier::new(TASKS));
702            let solve_at = rand::rng().random_range(0..TASKS);
703            let mut tasks = Vec::new();
704            for _ in 0..TASKS {
705                let cache = cache.clone();
706                let wg = wg.clone();
707                let entered = entered.clone();
708                let task = tokio::spawn(async move {
709                    wg.wait().await;
710                    loop {
711                        match tokio::time::timeout(
712                            Duration::from_millis(1),
713                            cache.get_value_or_guard_async(&(1, 1)),
714                        )
715                        .await
716                        {
717                            Ok(Ok(r)) => assert_eq!(r, 1),
718                            Ok(Err(g)) => {
719                                let before =
720                                    entered.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
721                                if before == solve_at {
722                                    g.insert(1).unwrap();
723                                }
724                            }
725                            Err(_) => continue,
726                        }
727                        break;
728                    }
729                });
730                tasks.push(task);
731            }
732            for task in tasks {
733                task.await.unwrap();
734            }
735            assert_eq!(cache.get(&(1, 1)), Some(1));
736            assert_eq!(
737                entered.load(std::sync::atomic::Ordering::Relaxed),
738                solve_at + 1
739            );
740        }
741    }
742}