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}