/rust/registry/src/index.crates.io-1949cf8c6b5b557f/quick_cache-0.6.21/src/sync.rs
Line | Count | Source |
1 | | use std::{ |
2 | | future::Future, |
3 | | hash::{BuildHasher, Hash}, |
4 | | hint::unreachable_unchecked, |
5 | | time::Duration, |
6 | | }; |
7 | | |
8 | | use crate::{ |
9 | | linked_slab::Token, |
10 | | options::{Options, OptionsBuilder}, |
11 | | shard::{CacheShard, InsertStrategy}, |
12 | | shim::rw_lock::RwLock, |
13 | | sync_placeholder::SharedPlaceholder, |
14 | | DefaultHashBuilder, Equivalent, Lifecycle, MemoryUsed, UnitWeighter, Weighter, |
15 | | }; |
16 | | |
17 | | use crate::shard::EntryOrPlaceholder; |
18 | | pub use crate::sync_placeholder::{EntryAction, EntryResult, GuardResult, PlaceholderGuard}; |
19 | | use crate::sync_placeholder::{JoinFuture, JoinResult}; |
20 | | |
21 | | /// A concurrent cache |
22 | | /// |
23 | | /// The concurrent cache is internally composed of equally sized shards, each of which is independently |
24 | | /// synchronized. This allows for low contention when multiple threads are accessing the cache but limits the |
25 | | /// maximum weight capacity of each shard. |
26 | | /// |
27 | | /// # Value |
28 | | /// Cache values are cloned when fetched. Users should wrap their values with `Arc<_>` |
29 | | /// if necessary to avoid expensive clone operations. If interior mutability is required |
30 | | /// `Arc<Mutex<_>>` or `Arc<RwLock<_>>` can also be used. |
31 | | /// |
32 | | /// # Thread Safety and Concurrency |
33 | | /// The cache instance can be wrapped with an `Arc` (or equivalent) and shared between threads. |
34 | | /// All methods are accessible via non-mut references so no further synchronization (e.g. Mutex) is needed. |
35 | | pub struct Cache< |
36 | | Key, |
37 | | Val, |
38 | | We = UnitWeighter, |
39 | | B = DefaultHashBuilder, |
40 | | L = DefaultLifecycle<Key, Val>, |
41 | | > { |
42 | | hash_builder: B, |
43 | | shards: Box<[RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>]>, |
44 | | shards_mask: u64, |
45 | | lifecycle: L, |
46 | | } |
47 | | |
48 | | impl<Key: Eq + Hash, Val: Clone> Cache<Key, Val> { |
49 | | /// Creates a new cache with holds up to `items_capacity` items (approximately). |
50 | 0 | pub fn new(items_capacity: usize) -> Self { |
51 | 0 | Self::with( |
52 | 0 | items_capacity, |
53 | 0 | items_capacity as u64, |
54 | 0 | Default::default(), |
55 | 0 | Default::default(), |
56 | 0 | Default::default(), |
57 | | ) |
58 | 0 | } Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::new Unexecuted instantiation: <quick_cache::sync::Cache<_, _>>::new |
59 | | } |
60 | | |
61 | | impl<Key: Eq + Hash, Val: Clone, We: Weighter<Key, Val> + Clone> Cache<Key, Val, We> { |
62 | 26.2k | pub fn with_weighter( |
63 | 26.2k | estimated_items_capacity: usize, |
64 | 26.2k | weight_capacity: u64, |
65 | 26.2k | weighter: We, |
66 | 26.2k | ) -> Self { |
67 | 26.2k | Self::with( |
68 | 26.2k | estimated_items_capacity, |
69 | 26.2k | weight_capacity, |
70 | 26.2k | weighter, |
71 | 26.2k | Default::default(), |
72 | 26.2k | Default::default(), |
73 | | ) |
74 | 26.2k | } <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::with_weighter Line | Count | Source | 62 | 26.2k | pub fn with_weighter( | 63 | 26.2k | estimated_items_capacity: usize, | 64 | 26.2k | weight_capacity: u64, | 65 | 26.2k | weighter: We, | 66 | 26.2k | ) -> Self { | 67 | 26.2k | Self::with( | 68 | 26.2k | estimated_items_capacity, | 69 | 26.2k | weight_capacity, | 70 | 26.2k | weighter, | 71 | 26.2k | Default::default(), | 72 | 26.2k | Default::default(), | 73 | | ) | 74 | 26.2k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _>>::with_weighter |
75 | | } |
76 | | |
77 | | impl< |
78 | | Key: Eq + Hash, |
79 | | Val: Clone, |
80 | | We: Weighter<Key, Val> + Clone, |
81 | | B: BuildHasher + Clone, |
82 | | L: Lifecycle<Key, Val> + Clone, |
83 | | > Cache<Key, Val, We, B, L> |
84 | | { |
85 | | /// Creates a new cache that can hold up to `weight_capacity` in weight. |
86 | | /// `estimated_items_capacity` is the estimated number of items the cache is expected to hold, |
87 | | /// roughly equivalent to `weight_capacity / average item weight`. |
88 | 52.5k | pub fn with( |
89 | 52.5k | estimated_items_capacity: usize, |
90 | 52.5k | weight_capacity: u64, |
91 | 52.5k | weighter: We, |
92 | 52.5k | hash_builder: B, |
93 | 52.5k | lifecycle: L, |
94 | 52.5k | ) -> Self { |
95 | 52.5k | Self::with_options( |
96 | 52.5k | OptionsBuilder::new() |
97 | 52.5k | .estimated_items_capacity(estimated_items_capacity) |
98 | 52.5k | .weight_capacity(weight_capacity) |
99 | 52.5k | .build() |
100 | 52.5k | .unwrap(), |
101 | 52.5k | weighter, |
102 | 52.5k | hash_builder, |
103 | 52.5k | lifecycle, |
104 | | ) |
105 | 52.5k | } Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::with <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::with Line | Count | Source | 88 | 26.2k | pub fn with( | 89 | 26.2k | estimated_items_capacity: usize, | 90 | 26.2k | weight_capacity: u64, | 91 | 26.2k | weighter: We, | 92 | 26.2k | hash_builder: B, | 93 | 26.2k | lifecycle: L, | 94 | 26.2k | ) -> Self { | 95 | 26.2k | Self::with_options( | 96 | 26.2k | OptionsBuilder::new() | 97 | 26.2k | .estimated_items_capacity(estimated_items_capacity) | 98 | 26.2k | .weight_capacity(weight_capacity) | 99 | 26.2k | .build() | 100 | 26.2k | .unwrap(), | 101 | 26.2k | weighter, | 102 | 26.2k | hash_builder, | 103 | 26.2k | lifecycle, | 104 | | ) | 105 | 26.2k | } |
<quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::with Line | Count | Source | 88 | 26.2k | pub fn with( | 89 | 26.2k | estimated_items_capacity: usize, | 90 | 26.2k | weight_capacity: u64, | 91 | 26.2k | weighter: We, | 92 | 26.2k | hash_builder: B, | 93 | 26.2k | lifecycle: L, | 94 | 26.2k | ) -> Self { | 95 | 26.2k | Self::with_options( | 96 | 26.2k | OptionsBuilder::new() | 97 | 26.2k | .estimated_items_capacity(estimated_items_capacity) | 98 | 26.2k | .weight_capacity(weight_capacity) | 99 | 26.2k | .build() | 100 | 26.2k | .unwrap(), | 101 | 26.2k | weighter, | 102 | 26.2k | hash_builder, | 103 | 26.2k | lifecycle, | 104 | | ) | 105 | 26.2k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::with |
106 | | |
107 | | /// Constructs a cache based on [OptionsBuilder]. |
108 | | /// |
109 | | /// # Example |
110 | | /// |
111 | | /// ```rust |
112 | | /// use quick_cache::{sync::{Cache, DefaultLifecycle}, OptionsBuilder, UnitWeighter, DefaultHashBuilder}; |
113 | | /// |
114 | | /// Cache::<(String, u64), String>::with_options( |
115 | | /// OptionsBuilder::new() |
116 | | /// .estimated_items_capacity(10000) |
117 | | /// .weight_capacity(10000) |
118 | | /// .build() |
119 | | /// .unwrap(), |
120 | | /// UnitWeighter, |
121 | | /// DefaultHashBuilder::default(), |
122 | | /// DefaultLifecycle::default(), |
123 | | /// ); |
124 | | /// ``` |
125 | 479k | pub fn with_options(options: Options, weighter: We, hash_builder: B, lifecycle: L) -> Self { |
126 | 479k | let mut num_shards = options.shards.next_power_of_two() as u64; |
127 | 479k | let estimated_items_capacity = options.estimated_items_capacity as u64; |
128 | 479k | let weight_capacity = options.weight_capacity; |
129 | 479k | let mut shard_items_cap = |
130 | 479k | estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; |
131 | 479k | let mut shard_weight_cap = |
132 | 479k | options.weight_capacity.saturating_add(num_shards - 1) / num_shards; |
133 | | // try to make each shard hold at least 32 items |
134 | 532k | while shard_items_cap < 32 && num_shards > 1 { |
135 | 52.5k | num_shards /= 2; |
136 | 52.5k | shard_items_cap = estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; |
137 | 52.5k | shard_weight_cap = weight_capacity.saturating_add(num_shards - 1) / num_shards; |
138 | 52.5k | } |
139 | 479k | let shards = (0..num_shards) |
140 | 7.97M | .map(|_| { |
141 | 7.97M | RwLock::new(CacheShard::new( |
142 | 7.97M | options.hot_allocation, |
143 | 7.97M | options.ghost_allocation, |
144 | 7.97M | shard_items_cap as usize, |
145 | 7.97M | shard_weight_cap, |
146 | 7.97M | weighter.clone(), |
147 | 7.97M | hash_builder.clone(), |
148 | 7.97M | lifecycle.clone(), |
149 | | )) |
150 | 7.97M | }) Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::with_options::{closure#0}<quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::with_options::{closure#0}Line | Count | Source | 140 | 3.36M | .map(|_| { | 141 | 3.36M | RwLock::new(CacheShard::new( | 142 | 3.36M | options.hot_allocation, | 143 | 3.36M | options.ghost_allocation, | 144 | 3.36M | shard_items_cap as usize, | 145 | 3.36M | shard_weight_cap, | 146 | 3.36M | weighter.clone(), | 147 | 3.36M | hash_builder.clone(), | 148 | 3.36M | lifecycle.clone(), | 149 | | )) | 150 | 3.36M | }) |
<quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::with_options::{closure#0}Line | Count | Source | 140 | 3.36M | .map(|_| { | 141 | 3.36M | RwLock::new(CacheShard::new( | 142 | 3.36M | options.hot_allocation, | 143 | 3.36M | options.ghost_allocation, | 144 | 3.36M | shard_items_cap as usize, | 145 | 3.36M | shard_weight_cap, | 146 | 3.36M | weighter.clone(), | 147 | 3.36M | hash_builder.clone(), | 148 | 3.36M | lifecycle.clone(), | 149 | | )) | 150 | 3.36M | }) |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::with_options::{closure#0}Line | Count | Source | 140 | 841k | .map(|_| { | 141 | 841k | RwLock::new(CacheShard::new( | 142 | 841k | options.hot_allocation, | 143 | 841k | options.ghost_allocation, | 144 | 841k | shard_items_cap as usize, | 145 | 841k | shard_weight_cap, | 146 | 841k | weighter.clone(), | 147 | 841k | hash_builder.clone(), | 148 | 841k | lifecycle.clone(), | 149 | | )) | 150 | 841k | }) |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::with_options::{closure#0}Line | Count | Source | 140 | 400k | .map(|_| { | 141 | 400k | RwLock::new(CacheShard::new( | 142 | 400k | options.hot_allocation, | 143 | 400k | options.ghost_allocation, | 144 | 400k | shard_items_cap as usize, | 145 | 400k | shard_weight_cap, | 146 | 400k | weighter.clone(), | 147 | 400k | hash_builder.clone(), | 148 | 400k | lifecycle.clone(), | 149 | | )) | 150 | 400k | }) |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::with_options::{closure#0} |
151 | 479k | .collect::<Vec<_>>(); |
152 | 479k | Self { |
153 | 479k | shards: shards.into_boxed_slice(), |
154 | 479k | hash_builder, |
155 | 479k | shards_mask: num_shards - 1, |
156 | 479k | lifecycle, |
157 | 479k | } |
158 | 479k | } Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::with_options <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::with_options Line | Count | Source | 125 | 26.2k | pub fn with_options(options: Options, weighter: We, hash_builder: B, lifecycle: L) -> Self { | 126 | 26.2k | let mut num_shards = options.shards.next_power_of_two() as u64; | 127 | 26.2k | let estimated_items_capacity = options.estimated_items_capacity as u64; | 128 | 26.2k | let weight_capacity = options.weight_capacity; | 129 | 26.2k | let mut shard_items_cap = | 130 | 26.2k | estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 131 | 26.2k | let mut shard_weight_cap = | 132 | 26.2k | options.weight_capacity.saturating_add(num_shards - 1) / num_shards; | 133 | | // try to make each shard hold at least 32 items | 134 | 26.2k | while shard_items_cap < 32 && num_shards > 1 { | 135 | 0 | num_shards /= 2; | 136 | 0 | shard_items_cap = estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 137 | 0 | shard_weight_cap = weight_capacity.saturating_add(num_shards - 1) / num_shards; | 138 | 0 | } | 139 | 26.2k | let shards = (0..num_shards) | 140 | 26.2k | .map(|_| { | 141 | | RwLock::new(CacheShard::new( | 142 | | options.hot_allocation, | 143 | | options.ghost_allocation, | 144 | | shard_items_cap as usize, | 145 | | shard_weight_cap, | 146 | | weighter.clone(), | 147 | | hash_builder.clone(), | 148 | | lifecycle.clone(), | 149 | | )) | 150 | | }) | 151 | 26.2k | .collect::<Vec<_>>(); | 152 | 26.2k | Self { | 153 | 26.2k | shards: shards.into_boxed_slice(), | 154 | 26.2k | hash_builder, | 155 | 26.2k | shards_mask: num_shards - 1, | 156 | 26.2k | lifecycle, | 157 | 26.2k | } | 158 | 26.2k | } |
<quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::with_options Line | Count | Source | 125 | 26.2k | pub fn with_options(options: Options, weighter: We, hash_builder: B, lifecycle: L) -> Self { | 126 | 26.2k | let mut num_shards = options.shards.next_power_of_two() as u64; | 127 | 26.2k | let estimated_items_capacity = options.estimated_items_capacity as u64; | 128 | 26.2k | let weight_capacity = options.weight_capacity; | 129 | 26.2k | let mut shard_items_cap = | 130 | 26.2k | estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 131 | 26.2k | let mut shard_weight_cap = | 132 | 26.2k | options.weight_capacity.saturating_add(num_shards - 1) / num_shards; | 133 | | // try to make each shard hold at least 32 items | 134 | 26.2k | while shard_items_cap < 32 && num_shards > 1 { | 135 | 0 | num_shards /= 2; | 136 | 0 | shard_items_cap = estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 137 | 0 | shard_weight_cap = weight_capacity.saturating_add(num_shards - 1) / num_shards; | 138 | 0 | } | 139 | 26.2k | let shards = (0..num_shards) | 140 | 26.2k | .map(|_| { | 141 | | RwLock::new(CacheShard::new( | 142 | | options.hot_allocation, | 143 | | options.ghost_allocation, | 144 | | shard_items_cap as usize, | 145 | | shard_weight_cap, | 146 | | weighter.clone(), | 147 | | hash_builder.clone(), | 148 | | lifecycle.clone(), | 149 | | )) | 150 | | }) | 151 | 26.2k | .collect::<Vec<_>>(); | 152 | 26.2k | Self { | 153 | 26.2k | shards: shards.into_boxed_slice(), | 154 | 26.2k | hash_builder, | 155 | 26.2k | shards_mask: num_shards - 1, | 156 | 26.2k | lifecycle, | 157 | 26.2k | } | 158 | 26.2k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::with_options Line | Count | Source | 125 | 26.2k | pub fn with_options(options: Options, weighter: We, hash_builder: B, lifecycle: L) -> Self { | 126 | 26.2k | let mut num_shards = options.shards.next_power_of_two() as u64; | 127 | 26.2k | let estimated_items_capacity = options.estimated_items_capacity as u64; | 128 | 26.2k | let weight_capacity = options.weight_capacity; | 129 | 26.2k | let mut shard_items_cap = | 130 | 26.2k | estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 131 | 26.2k | let mut shard_weight_cap = | 132 | 26.2k | options.weight_capacity.saturating_add(num_shards - 1) / num_shards; | 133 | | // try to make each shard hold at least 32 items | 134 | 78.8k | while shard_items_cap < 32 && num_shards > 1 { | 135 | 52.5k | num_shards /= 2; | 136 | 52.5k | shard_items_cap = estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 137 | 52.5k | shard_weight_cap = weight_capacity.saturating_add(num_shards - 1) / num_shards; | 138 | 52.5k | } | 139 | 26.2k | let shards = (0..num_shards) | 140 | 26.2k | .map(|_| { | 141 | | RwLock::new(CacheShard::new( | 142 | | options.hot_allocation, | 143 | | options.ghost_allocation, | 144 | | shard_items_cap as usize, | 145 | | shard_weight_cap, | 146 | | weighter.clone(), | 147 | | hash_builder.clone(), | 148 | | lifecycle.clone(), | 149 | | )) | 150 | | }) | 151 | 26.2k | .collect::<Vec<_>>(); | 152 | 26.2k | Self { | 153 | 26.2k | shards: shards.into_boxed_slice(), | 154 | 26.2k | hash_builder, | 155 | 26.2k | shards_mask: num_shards - 1, | 156 | 26.2k | lifecycle, | 157 | 26.2k | } | 158 | 26.2k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::with_options Line | Count | Source | 125 | 400k | pub fn with_options(options: Options, weighter: We, hash_builder: B, lifecycle: L) -> Self { | 126 | 400k | let mut num_shards = options.shards.next_power_of_two() as u64; | 127 | 400k | let estimated_items_capacity = options.estimated_items_capacity as u64; | 128 | 400k | let weight_capacity = options.weight_capacity; | 129 | 400k | let mut shard_items_cap = | 130 | 400k | estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 131 | 400k | let mut shard_weight_cap = | 132 | 400k | options.weight_capacity.saturating_add(num_shards - 1) / num_shards; | 133 | | // try to make each shard hold at least 32 items | 134 | 400k | while shard_items_cap < 32 && num_shards > 1 { | 135 | 0 | num_shards /= 2; | 136 | 0 | shard_items_cap = estimated_items_capacity.saturating_add(num_shards - 1) / num_shards; | 137 | 0 | shard_weight_cap = weight_capacity.saturating_add(num_shards - 1) / num_shards; | 138 | 0 | } | 139 | 400k | let shards = (0..num_shards) | 140 | 400k | .map(|_| { | 141 | | RwLock::new(CacheShard::new( | 142 | | options.hot_allocation, | 143 | | options.ghost_allocation, | 144 | | shard_items_cap as usize, | 145 | | shard_weight_cap, | 146 | | weighter.clone(), | 147 | | hash_builder.clone(), | 148 | | lifecycle.clone(), | 149 | | )) | 150 | | }) | 151 | 400k | .collect::<Vec<_>>(); | 152 | 400k | Self { | 153 | 400k | shards: shards.into_boxed_slice(), | 154 | 400k | hash_builder, | 155 | 400k | shards_mask: num_shards - 1, | 156 | 400k | lifecycle, | 157 | 400k | } | 158 | 400k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::with_options |
159 | | |
160 | | #[cfg(fuzzing)] |
161 | 0 | pub fn validate(&self) { |
162 | 0 | for s in &*self.shards { |
163 | 0 | s.read().validate(false) |
164 | | } |
165 | 0 | } |
166 | | |
167 | | /// Returns whether the cache is empty |
168 | 0 | pub fn is_empty(&self) -> bool { |
169 | 0 | self.shards.iter().all(|s| s.read().len() == 0) |
170 | 0 | } |
171 | | |
172 | | /// Returns the number of cached items |
173 | 0 | pub fn len(&self) -> usize { |
174 | 0 | self.shards.iter().map(|s| s.read().len()).sum() |
175 | 0 | } |
176 | | |
177 | | /// Returns the total weight of cached items |
178 | 0 | pub fn weight(&self) -> u64 { |
179 | 0 | self.shards.iter().map(|s| s.read().weight()).sum() |
180 | 0 | } |
181 | | |
182 | | /// Returns the _total_ maximum weight capacity of cached items. |
183 | | /// Note that the cache may be composed of multiple shards and each shard has its own maximum weight capacity, |
184 | | /// see [`Self::shard_capacity`]. |
185 | 0 | pub fn capacity(&self) -> u64 { |
186 | 0 | self.shards.iter().map(|s| s.read().capacity()).sum() |
187 | 0 | } |
188 | | |
189 | | /// Returns the maximum weight capacity of each shard. |
190 | 0 | pub fn shard_capacity(&self) -> u64 { |
191 | 0 | self.shards[0].read().capacity() |
192 | 0 | } |
193 | | |
194 | | /// Returns the number of shards. |
195 | 0 | pub fn num_shards(&self) -> usize { |
196 | 0 | self.shards.len() |
197 | 0 | } |
198 | | |
199 | | /// Returns the number of misses |
200 | | #[cfg(feature = "stats")] |
201 | | pub fn misses(&self) -> u64 { |
202 | | self.shards.iter().map(|s| s.read().misses()).sum() |
203 | | } |
204 | | |
205 | | /// Returns the number of hits |
206 | | #[cfg(feature = "stats")] |
207 | | pub fn hits(&self) -> u64 { |
208 | | self.shards.iter().map(|s| s.read().hits()).sum() |
209 | | } |
210 | | |
211 | | #[inline] |
212 | 1.27M | fn shard_for<Q>( |
213 | 1.27M | &self, |
214 | 1.27M | key: &Q, |
215 | 1.27M | ) -> Option<( |
216 | 1.27M | &RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>, |
217 | 1.27M | u64, |
218 | 1.27M | )> |
219 | 1.27M | where |
220 | 1.27M | Q: Hash + Equivalent<Key> + ?Sized, |
221 | | { |
222 | 1.27M | let hash = self.hash_builder.hash_one(key); |
223 | | // When choosing the shard, rotate the hash bits usize::BITS / 2 so that we |
224 | | // give preference to the bits in the middle of the hash. |
225 | | // Internally hashbrown uses the lower bits for start of probing + the 7 highest, |
226 | | // so by picking something else we improve the real entropy available to each hashbrown shard. |
227 | 1.27M | let shard_idx = (hash.rotate_right(usize::BITS / 2) & self.shards_mask) as usize; |
228 | 1.27M | self.shards.get(shard_idx).map(|s| (s, hash)) Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::shard_for::<str>::{closure#0}Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::shard_for::<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey>::{closure#0}Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::shard_for::<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey>::{closure#0}<quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::ds::key::Key>::{closure#0}Line | Count | Source | 228 | 18.9k | self.shards.get(shard_idx).map(|s| (s, hash)) |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::ds::lookup::Lookup>::{closure#0}Line | Count | Source | 228 | 169k | self.shards.get(shard_idx).map(|s| (s, hash)) |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::tx::key::Key>::{closure#0}Line | Count | Source | 228 | 327k | self.shards.get(shard_idx).map(|s| (s, hash)) |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::tx::lookup::Lookup>::{closure#0}Line | Count | Source | 228 | 756k | self.shards.get(shard_idx).map(|s| (s, hash)) |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::shard_for::<_>::{closure#0} |
229 | 1.27M | } Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::shard_for::<str> Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::shard_for::<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey> Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::shard_for::<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey> <quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::ds::key::Key> Line | Count | Source | 212 | 18.9k | fn shard_for<Q>( | 213 | 18.9k | &self, | 214 | 18.9k | key: &Q, | 215 | 18.9k | ) -> Option<( | 216 | 18.9k | &RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>, | 217 | 18.9k | u64, | 218 | 18.9k | )> | 219 | 18.9k | where | 220 | 18.9k | Q: Hash + Equivalent<Key> + ?Sized, | 221 | | { | 222 | 18.9k | let hash = self.hash_builder.hash_one(key); | 223 | | // When choosing the shard, rotate the hash bits usize::BITS / 2 so that we | 224 | | // give preference to the bits in the middle of the hash. | 225 | | // Internally hashbrown uses the lower bits for start of probing + the 7 highest, | 226 | | // so by picking something else we improve the real entropy available to each hashbrown shard. | 227 | 18.9k | let shard_idx = (hash.rotate_right(usize::BITS / 2) & self.shards_mask) as usize; | 228 | 18.9k | self.shards.get(shard_idx).map(|s| (s, hash)) | 229 | 18.9k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::ds::lookup::Lookup> Line | Count | Source | 212 | 169k | fn shard_for<Q>( | 213 | 169k | &self, | 214 | 169k | key: &Q, | 215 | 169k | ) -> Option<( | 216 | 169k | &RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>, | 217 | 169k | u64, | 218 | 169k | )> | 219 | 169k | where | 220 | 169k | Q: Hash + Equivalent<Key> + ?Sized, | 221 | | { | 222 | 169k | let hash = self.hash_builder.hash_one(key); | 223 | | // When choosing the shard, rotate the hash bits usize::BITS / 2 so that we | 224 | | // give preference to the bits in the middle of the hash. | 225 | | // Internally hashbrown uses the lower bits for start of probing + the 7 highest, | 226 | | // so by picking something else we improve the real entropy available to each hashbrown shard. | 227 | 169k | let shard_idx = (hash.rotate_right(usize::BITS / 2) & self.shards_mask) as usize; | 228 | 169k | self.shards.get(shard_idx).map(|s| (s, hash)) | 229 | 169k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::tx::key::Key> Line | Count | Source | 212 | 327k | fn shard_for<Q>( | 213 | 327k | &self, | 214 | 327k | key: &Q, | 215 | 327k | ) -> Option<( | 216 | 327k | &RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>, | 217 | 327k | u64, | 218 | 327k | )> | 219 | 327k | where | 220 | 327k | Q: Hash + Equivalent<Key> + ?Sized, | 221 | | { | 222 | 327k | let hash = self.hash_builder.hash_one(key); | 223 | | // When choosing the shard, rotate the hash bits usize::BITS / 2 so that we | 224 | | // give preference to the bits in the middle of the hash. | 225 | | // Internally hashbrown uses the lower bits for start of probing + the 7 highest, | 226 | | // so by picking something else we improve the real entropy available to each hashbrown shard. | 227 | 327k | let shard_idx = (hash.rotate_right(usize::BITS / 2) & self.shards_mask) as usize; | 228 | 327k | self.shards.get(shard_idx).map(|s| (s, hash)) | 229 | 327k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::shard_for::<surrealdb_core::kvs::cache::tx::lookup::Lookup> Line | Count | Source | 212 | 756k | fn shard_for<Q>( | 213 | 756k | &self, | 214 | 756k | key: &Q, | 215 | 756k | ) -> Option<( | 216 | 756k | &RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>, | 217 | 756k | u64, | 218 | 756k | )> | 219 | 756k | where | 220 | 756k | Q: Hash + Equivalent<Key> + ?Sized, | 221 | | { | 222 | 756k | let hash = self.hash_builder.hash_one(key); | 223 | | // When choosing the shard, rotate the hash bits usize::BITS / 2 so that we | 224 | | // give preference to the bits in the middle of the hash. | 225 | | // Internally hashbrown uses the lower bits for start of probing + the 7 highest, | 226 | | // so by picking something else we improve the real entropy available to each hashbrown shard. | 227 | 756k | let shard_idx = (hash.rotate_right(usize::BITS / 2) & self.shards_mask) as usize; | 228 | 756k | self.shards.get(shard_idx).map(|s| (s, hash)) | 229 | 756k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::shard_for::<_> |
230 | | |
231 | | /// Reserve additional space for `additional` entries. |
232 | | /// Note that this is counted in entries, and is not weighted. |
233 | 0 | pub fn reserve(&self, additional: usize) { |
234 | 0 | let additional_per_shard = |
235 | 0 | additional.saturating_add(self.shards.len() - 1) / self.shards.len(); |
236 | 0 | for s in &*self.shards { |
237 | 0 | s.write().reserve(additional_per_shard); |
238 | 0 | } |
239 | 0 | } |
240 | | |
241 | | /// Checks if a key exists in the cache. |
242 | 0 | pub fn contains_key<Q>(&self, key: &Q) -> bool |
243 | 0 | where |
244 | 0 | Q: Hash + Equivalent<Key> + ?Sized, |
245 | | { |
246 | 0 | self.shard_for(key) |
247 | 0 | .is_some_and(|(shard, hash)| shard.read().contains(hash, key)) |
248 | 0 | } |
249 | | |
250 | | /// Fetches an item from the cache whose key is `key`. |
251 | 871k | pub fn get<Q>(&self, key: &Q) -> Option<Val> |
252 | 871k | where |
253 | 871k | Q: Hash + Equivalent<Key> + ?Sized, |
254 | | { |
255 | 871k | let (shard, hash) = self.shard_for(key)?; |
256 | 871k | shard.read().get(hash, key).cloned() |
257 | 871k | } Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::get::<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey> Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::get::<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey> <quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::get::<surrealdb_core::kvs::cache::ds::lookup::Lookup> Line | Count | Source | 251 | 169k | pub fn get<Q>(&self, key: &Q) -> Option<Val> | 252 | 169k | where | 253 | 169k | Q: Hash + Equivalent<Key> + ?Sized, | 254 | | { | 255 | 169k | let (shard, hash) = self.shard_for(key)?; | 256 | 169k | shard.read().get(hash, key).cloned() | 257 | 169k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::get::<surrealdb_core::kvs::cache::tx::lookup::Lookup> Line | Count | Source | 251 | 702k | pub fn get<Q>(&self, key: &Q) -> Option<Val> | 252 | 702k | where | 253 | 702k | Q: Hash + Equivalent<Key> + ?Sized, | 254 | | { | 255 | 702k | let (shard, hash) = self.shard_for(key)?; | 256 | 702k | shard.read().get(hash, key).cloned() | 257 | 702k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::get::<_> |
258 | | |
259 | | /// Peeks an item from the cache whose key is `key`. |
260 | | /// Contrary to gets, peeks don't alter the key "hotness". |
261 | 0 | pub fn peek<Q>(&self, key: &Q) -> Option<Val> |
262 | 0 | where |
263 | 0 | Q: Hash + Equivalent<Key> + ?Sized, |
264 | | { |
265 | 0 | let (shard, hash) = self.shard_for(key)?; |
266 | 0 | shard.read().peek(hash, key).cloned() |
267 | 0 | } |
268 | | |
269 | | /// Remove an item from the cache whose key is `key`. |
270 | | /// Returns the removed entry, if any. |
271 | 54.3k | pub fn remove<Q>(&self, key: &Q) -> Option<(Key, Val)> |
272 | 54.3k | where |
273 | 54.3k | Q: Hash + Equivalent<Key> + ?Sized, |
274 | | { |
275 | 54.3k | let (shard, hash) = self.shard_for(key).unwrap(); |
276 | 54.3k | shard.write().remove(hash, key) |
277 | 54.3k | } Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::remove::<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey> Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::remove::<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey> <quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::remove::<surrealdb_core::kvs::cache::tx::lookup::Lookup> Line | Count | Source | 271 | 54.3k | pub fn remove<Q>(&self, key: &Q) -> Option<(Key, Val)> | 272 | 54.3k | where | 273 | 54.3k | Q: Hash + Equivalent<Key> + ?Sized, | 274 | | { | 275 | 54.3k | let (shard, hash) = self.shard_for(key).unwrap(); | 276 | 54.3k | shard.write().remove(hash, key) | 277 | 54.3k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::remove::<_> |
278 | | |
279 | | /// Remove an item from the cache whose key is `key` if `f(&value)` returns `true` for that entry. |
280 | | /// Compared to peek and remove, this method guarantees that no new value was inserted in-between. |
281 | | /// |
282 | | /// Returns the removed entry, if any. |
283 | 0 | pub fn remove_if<Q, F>(&self, key: &Q, f: F) -> Option<(Key, Val)> |
284 | 0 | where |
285 | 0 | Q: Hash + Equivalent<Key> + ?Sized, |
286 | 0 | F: FnOnce(&Val) -> bool, |
287 | | { |
288 | 0 | let (shard, hash) = self.shard_for(key).unwrap(); |
289 | 0 | shard.write().remove_if(hash, key, f) |
290 | 0 | } |
291 | | |
292 | | /// Inserts an item in the cache, but _only_ if an entry with key `key` already exists. |
293 | | /// If `soft` is set, the replace operation won't affect the "hotness" of the entry, |
294 | | /// even if the value is replaced. |
295 | | /// |
296 | | /// Returns `Ok` if the entry was admitted and `Err(_)` if it wasn't. |
297 | 0 | pub fn replace(&self, key: Key, value: Val, soft: bool) -> Result<(), (Key, Val)> { |
298 | 0 | let lcs = self.replace_with_lifecycle(key, value, soft)?; |
299 | 0 | self.lifecycle.end_request(lcs); |
300 | 0 | Ok(()) |
301 | 0 | } |
302 | | |
303 | | /// Inserts an item in the cache, but _only_ if an entry with key `key` already exists. |
304 | | /// If `soft` is set, the replace operation won't affect the "hotness" of the entry, |
305 | | /// even if the value is replaced. |
306 | | /// |
307 | | /// Returns `Ok` if the entry was admitted and `Err(_)` if it wasn't. |
308 | 0 | pub fn replace_with_lifecycle( |
309 | 0 | &self, |
310 | 0 | key: Key, |
311 | 0 | value: Val, |
312 | 0 | soft: bool, |
313 | 0 | ) -> Result<L::RequestState, (Key, Val)> { |
314 | 0 | let mut lcs = self.lifecycle.begin_request(); |
315 | 0 | let (shard, hash) = self.shard_for(&key).unwrap(); |
316 | 0 | shard |
317 | 0 | .write() |
318 | 0 | .insert(&mut lcs, hash, key, value, InsertStrategy::Replace { soft })?; |
319 | 0 | Ok(lcs) |
320 | 0 | } |
321 | | |
322 | | /// Retains only the items specified by the predicate. |
323 | | /// In other words, remove all items for which `f(&key, &value)` returns `false`. The |
324 | | /// elements are visited in arbitrary order. |
325 | 0 | pub fn retain<F>(&self, f: F) |
326 | 0 | where |
327 | 0 | F: Fn(&Key, &Val) -> bool, |
328 | | { |
329 | 0 | for s in self.shards.iter() { |
330 | 0 | s.write().retain(&f); |
331 | 0 | } |
332 | 0 | } Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::retain::<<surrealdb_core::idx::trees::diskann::cache::DiskAnnCache>::remove_index::{closure#0}::{closure#0}>Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::retain::<_> |
333 | | |
334 | | /// Inserts an item in the cache with key `key`. |
335 | 346k | pub fn insert(&self, key: Key, value: Val) { |
336 | 346k | let lcs = self.insert_with_lifecycle(key, value); |
337 | 346k | self.lifecycle.end_request(lcs); |
338 | 346k | } Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::insert Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::insert <quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::insert Line | Count | Source | 335 | 18.9k | pub fn insert(&self, key: Key, value: Val) { | 336 | 18.9k | let lcs = self.insert_with_lifecycle(key, value); | 337 | 18.9k | self.lifecycle.end_request(lcs); | 338 | 18.9k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::insert Line | Count | Source | 335 | 327k | pub fn insert(&self, key: Key, value: Val) { | 336 | 327k | let lcs = self.insert_with_lifecycle(key, value); | 337 | 327k | self.lifecycle.end_request(lcs); | 338 | 327k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::insert |
339 | | |
340 | | /// Inserts an item in the cache with key `key`. |
341 | 346k | pub fn insert_with_lifecycle(&self, key: Key, value: Val) -> L::RequestState { |
342 | 346k | let mut lcs = self.lifecycle.begin_request(); |
343 | 346k | let (shard, hash) = self.shard_for(&key).unwrap(); |
344 | 346k | let result = shard |
345 | 346k | .write() |
346 | 346k | .insert(&mut lcs, hash, key, value, InsertStrategy::Insert); |
347 | | // result cannot err with the Insert strategy |
348 | 346k | debug_assert!(result.is_ok()); |
349 | 346k | lcs |
350 | 346k | } Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::hnsw::cache::HnswCacheKey, surrealdb_core::idx::trees::hnsw::cache::HnswCacheValue, surrealdb_core::idx::trees::hnsw::cache::HnswCacheWeighter, ahash::random_state::RandomState, surrealdb_core::idx::trees::hnsw::cache::HnswCacheLifecycle>>::insert_with_lifecycle Unexecuted instantiation: <quick_cache::sync::Cache<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheWeighter>>::insert_with_lifecycle <quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::insert_with_lifecycle Line | Count | Source | 341 | 18.9k | pub fn insert_with_lifecycle(&self, key: Key, value: Val) -> L::RequestState { | 342 | 18.9k | let mut lcs = self.lifecycle.begin_request(); | 343 | 18.9k | let (shard, hash) = self.shard_for(&key).unwrap(); | 344 | 18.9k | let result = shard | 345 | 18.9k | .write() | 346 | 18.9k | .insert(&mut lcs, hash, key, value, InsertStrategy::Insert); | 347 | | // result cannot err with the Insert strategy | 348 | 18.9k | debug_assert!(result.is_ok()); | 349 | 18.9k | lcs | 350 | 18.9k | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::insert_with_lifecycle Line | Count | Source | 341 | 327k | pub fn insert_with_lifecycle(&self, key: Key, value: Val) -> L::RequestState { | 342 | 327k | let mut lcs = self.lifecycle.begin_request(); | 343 | 327k | let (shard, hash) = self.shard_for(&key).unwrap(); | 344 | 327k | let result = shard | 345 | 327k | .write() | 346 | 327k | .insert(&mut lcs, hash, key, value, InsertStrategy::Insert); | 347 | | // result cannot err with the Insert strategy | 348 | 327k | debug_assert!(result.is_ok()); | 349 | 327k | lcs | 350 | 327k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::insert_with_lifecycle |
351 | | |
352 | | /// Clear all items from the cache |
353 | 15.3k | pub fn clear(&self) { |
354 | 17.2k | for s in self.shards.iter() { |
355 | 17.2k | s.write().clear(); |
356 | 17.2k | } |
357 | 15.3k | } <quick_cache::sync::Cache<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry, surrealdb_core::kvs::cache::ds::weight::Weight>>::clear Line | Count | Source | 353 | 61 | pub fn clear(&self) { | 354 | 1.95k | for s in self.shards.iter() { | 355 | 1.95k | s.write().clear(); | 356 | 1.95k | } | 357 | 61 | } |
<quick_cache::sync::Cache<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry, surrealdb_core::kvs::cache::tx::weight::Weight>>::clear Line | Count | Source | 353 | 15.3k | pub fn clear(&self) { | 354 | 15.3k | for s in self.shards.iter() { | 355 | 15.3k | s.write().clear(); | 356 | 15.3k | } | 357 | 15.3k | } |
Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::clear |
358 | | |
359 | | /// Iterates over the items in the cache returning cloned key value pairs. |
360 | | /// |
361 | | /// The iterator is guaranteed to yield all items in the cache at the time of creation |
362 | | /// provided that they are not removed or evicted from the cache while iterating. |
363 | | /// The iterator may also yield items added to the cache after the iterator is created. |
364 | 0 | pub fn iter(&self) -> Iter<'_, Key, Val, We, B, L> |
365 | 0 | where |
366 | 0 | Key: Clone, |
367 | | { |
368 | 0 | Iter { |
369 | 0 | shards: &self.shards, |
370 | 0 | current_shard: 0, |
371 | 0 | last: None, |
372 | 0 | } |
373 | 0 | } |
374 | | |
375 | | /// Drains items from the cache. |
376 | | /// |
377 | | /// The iterator is guaranteed to drain all items in the cache at the time of creation |
378 | | /// provided that they are not removed or evicted from the cache while draining. |
379 | | /// The iterator may also drain items added to the cache after the iterator is created. |
380 | | /// Due to the above, the cache may not be empty after the iterator is fully consumed |
381 | | /// if items are added to the cache while draining. |
382 | | /// |
383 | | /// Note that dropping the iterator will _not_ finish the draining process, unlike other |
384 | | /// drain methods. |
385 | 0 | pub fn drain(&self) -> Drain<'_, Key, Val, We, B, L> { |
386 | 0 | Drain { |
387 | 0 | shards: &self.shards, |
388 | 0 | current_shard: 0, |
389 | 0 | last: None, |
390 | 0 | } |
391 | 0 | } |
392 | | |
393 | | /// Sets the cache to a new weight capacity. |
394 | | /// |
395 | | /// This will adjust the weight capacity of each shard proportionally. |
396 | | /// If the new capacity is smaller than the current weight, items will be evicted |
397 | | /// to bring the cache within the new limit. |
398 | 0 | pub fn set_capacity(&self, new_weight_capacity: u64) { |
399 | 0 | let shard_weight_cap = new_weight_capacity.saturating_add(self.shards.len() as u64 - 1) |
400 | 0 | / self.shards.len() as u64; |
401 | 0 | for shard in &*self.shards { |
402 | 0 | shard.write().set_capacity(shard_weight_cap); |
403 | 0 | } |
404 | 0 | } |
405 | | |
406 | | /// Gets an item from the cache with key `key` . |
407 | | /// |
408 | | /// If the corresponding value isn't present in the cache, this function returns a guard |
409 | | /// that can be used to insert the value once it's computed. |
410 | | /// While the returned guard is alive, other calls with the same key using the |
411 | | /// `get_value_or_guard` or `get_or_insert` family of functions will wait until the guard |
412 | | /// is dropped or the value is inserted. |
413 | | /// |
414 | | /// A `None` `timeout` means waiting forever. |
415 | | /// A `Some(<zero>)` timeout will return a Timeout error immediately if the value is not present |
416 | | /// and a guard is alive elsewhere. |
417 | 0 | pub fn get_value_or_guard<Q>( |
418 | 0 | &self, |
419 | 0 | key: &Q, |
420 | 0 | timeout: Option<Duration>, |
421 | 0 | ) -> GuardResult<'_, Key, Val, We, B, L> |
422 | 0 | where |
423 | 0 | Q: Hash + Equivalent<Key> + ToOwned<Owned = Key> + ?Sized, |
424 | | { |
425 | 0 | let (shard, hash) = self.shard_for(key).unwrap(); |
426 | 0 | if let Some(v) = shard.read().get(hash, key) { |
427 | 0 | return GuardResult::Value(v.clone()); |
428 | 0 | } |
429 | 0 | PlaceholderGuard::join(&self.lifecycle, shard, hash, key, timeout) |
430 | 0 | } Unexecuted instantiation: <quick_cache::sync::Cache<alloc::string::String, regex::regex::string::Regex>>::get_value_or_guard::<str> Unexecuted instantiation: <quick_cache::sync::Cache<_, _, _, _, _>>::get_value_or_guard::<_> |
431 | | |
432 | | /// Gets or inserts an item in the cache with key `key`. |
433 | | /// |
434 | | /// See also `get_value_or_guard` and `get_value_or_guard_async`. |
435 | 0 | pub fn get_or_insert_with<Q, E>( |
436 | 0 | &self, |
437 | 0 | key: &Q, |
438 | 0 | with: impl FnOnce() -> Result<Val, E>, |
439 | 0 | ) -> Result<Val, E> |
440 | 0 | where |
441 | 0 | Q: Hash + Equivalent<Key> + ToOwned<Owned = Key> + ?Sized, |
442 | | { |
443 | 0 | match self.get_value_or_guard(key, None) { |
444 | 0 | GuardResult::Value(v) => Ok(v), |
445 | 0 | GuardResult::Guard(g) => { |
446 | 0 | let v = with()?; |
447 | 0 | let _ = g.insert(v.clone()); |
448 | 0 | Ok(v) |
449 | | } |
450 | 0 | GuardResult::Timeout => unsafe { unreachable_unchecked() }, |
451 | | } |
452 | 0 | } |
453 | | |
454 | | /// Gets an item from the cache with key `key`. |
455 | | /// |
456 | | /// If the corresponding value isn't present in the cache, this function returns a guard |
457 | | /// that can be used to insert the value once it's computed. |
458 | | /// While the returned guard is alive, other calls with the same key using the |
459 | | /// `get_value_or_guard` or `get_or_insert` family of functions will wait until the guard |
460 | | /// is dropped or the value is inserted. |
461 | 0 | pub async fn get_value_or_guard_async<'a, Q>( |
462 | 0 | &'a self, |
463 | 0 | key: &Q, |
464 | 0 | ) -> Result<Val, PlaceholderGuard<'a, Key, Val, We, B, L>> |
465 | 0 | where |
466 | 0 | Q: Hash + Equivalent<Key> + ToOwned<Owned = Key> + ?Sized, |
467 | 0 | { |
468 | 0 | let (shard, hash) = self.shard_for(key).unwrap(); |
469 | | loop { |
470 | 0 | if let Some(v) = shard.read().get(hash, key) { |
471 | 0 | return Ok(v.clone()); |
472 | 0 | } |
473 | 0 | match JoinFuture::new(&self.lifecycle, shard, hash, key).await { |
474 | 0 | JoinResult::Filled(Some(shared)) => { |
475 | | // SAFETY: Filled means the value was set by the loader. |
476 | 0 | return Ok(unsafe { shared.value().unwrap_unchecked().clone() }); |
477 | | } |
478 | 0 | JoinResult::Filled(None) => continue, |
479 | 0 | JoinResult::Guard(g) => return Err(g), |
480 | 0 | JoinResult::Timeout => unsafe { unreachable_unchecked() }, |
481 | | } |
482 | | } |
483 | 0 | } |
484 | | |
485 | | /// Gets or inserts an item in the cache with key `key`. |
486 | 0 | pub async fn get_or_insert_async<Q, E>( |
487 | 0 | &self, |
488 | 0 | key: &Q, |
489 | 0 | with: impl Future<Output = Result<Val, E>>, |
490 | 0 | ) -> Result<Val, E> |
491 | 0 | where |
492 | 0 | Q: Hash + Equivalent<Key> + ToOwned<Owned = Key> + ?Sized, |
493 | 0 | { |
494 | 0 | match self.get_value_or_guard_async(key).await { |
495 | 0 | Ok(v) => Ok(v), |
496 | 0 | Err(g) => { |
497 | 0 | let v = with.await?; |
498 | 0 | let _ = g.insert(v.clone()); |
499 | 0 | Ok(v) |
500 | | } |
501 | | } |
502 | 0 | } |
503 | | |
504 | | /// Atomically accesses an existing entry, or gets a guard for insertion. |
505 | | /// |
506 | | /// If a value exists for `key`, `on_occupied` is called with a mutable reference |
507 | | /// to the key and value. The callback returns an [`EntryAction`] to decide what to do: |
508 | | /// - [`EntryAction::Retain`]`(T)` — keep the entry, return `T`. |
509 | | /// Weight is recalculated after the callback returns. |
510 | | /// - [`EntryAction::Remove`] — remove the entry from the cache. |
511 | | /// - [`EntryAction::ReplaceWithGuard`] — remove the entry and get a guard for re-insertion. |
512 | | /// |
513 | | /// If no value exists, a [`PlaceholderGuard`] is returned for inserting a new value. |
514 | | /// If another thread is already loading this key, waits up to `timeout` for the value |
515 | | /// to arrive, then calls `on_occupied` on the result. |
516 | | /// |
517 | | /// A `None` `timeout` means waiting forever. |
518 | | /// A `Some(<zero>)` timeout will return a Timeout immediately if a guard is alive elsewhere. |
519 | | /// |
520 | | /// The callback is `FnOnce` and runs **at most once**. |
521 | | /// |
522 | | /// # Performance |
523 | | /// |
524 | | /// Always acquires a **write lock** on the shard. For read-only lookups where |
525 | | /// contention matters, prefer [`get`](Self::get), [`get_value_or_guard`](Self::get_value_or_guard) |
526 | | /// or similar. |
527 | | /// |
528 | | /// The callback runs under the shard write lock — keep it short to avoid blocking |
529 | | /// other operations on the same shard. **Do not** call back into the cache from the |
530 | | /// callback, as this will deadlock when the same shard is accessed. |
531 | | /// |
532 | | /// # Panics |
533 | | /// |
534 | | /// If the callback panics, weight accounting is automatically corrected. |
535 | | /// However, any partial mutation to the value will remain. |
536 | | /// |
537 | | /// # Examples |
538 | | /// |
539 | | /// ``` |
540 | | /// use quick_cache::sync::{Cache, EntryAction, EntryResult}; |
541 | | /// |
542 | | /// let cache: Cache<String, u64> = Cache::new(5); |
543 | | /// cache.insert("counter".to_string(), 0); |
544 | | /// |
545 | | /// // Mutate in place: increment a counter |
546 | | /// let result = cache.entry("counter", None, |_k, v| { |
547 | | /// *v += 1; |
548 | | /// EntryAction::Retain(*v) |
549 | | /// }); |
550 | | /// assert!(matches!(result, EntryResult::Retained(1))); |
551 | | /// assert_eq!(cache.get("counter"), Some(1)); |
552 | | /// ``` |
553 | 0 | pub fn entry<Q, T>( |
554 | 0 | &self, |
555 | 0 | key: &Q, |
556 | 0 | timeout: Option<Duration>, |
557 | 0 | on_occupied: impl FnOnce(&Key, &mut Val) -> EntryAction<T>, |
558 | 0 | ) -> EntryResult<'_, Key, Val, We, B, L, T> |
559 | 0 | where |
560 | 0 | Q: Hash + Equivalent<Key> + ToOwned<Owned = Key> + ?Sized, |
561 | | { |
562 | 0 | let (shard, hash) = self.shard_for(key).unwrap(); |
563 | | // Wrap FnOnce in Option so we can pass &mut FnMut to entry_or_placeholder |
564 | | // in a loop. The loop retries only on ExistingPlaceholder (another thread is |
565 | | // loading), which does not invoke the callback — so the Option is still Some |
566 | | // on retry and the callback runs at most once. |
567 | 0 | let mut on_occupied = Some(on_occupied); |
568 | 0 | let mut callback = |k: &Key, v: &mut Val| on_occupied.take().unwrap()(k, v); |
569 | 0 | let mut deadline = timeout.map(Ok); |
570 | | |
571 | | loop { |
572 | 0 | let mut shard_guard = shard.write(); |
573 | 0 | match shard_guard.entry_or_placeholder(hash, key, &mut callback) { |
574 | 0 | EntryOrPlaceholder::Kept(t) => return EntryResult::Retained(t), |
575 | 0 | EntryOrPlaceholder::Removed(k, v) => return EntryResult::Removed(k, v), |
576 | 0 | EntryOrPlaceholder::Replaced(shared, old_val) => { |
577 | 0 | drop(shard_guard); |
578 | 0 | return EntryResult::Replaced( |
579 | 0 | PlaceholderGuard::start_loading(&self.lifecycle, shard, shared), |
580 | 0 | old_val, |
581 | 0 | ); |
582 | | } |
583 | 0 | EntryOrPlaceholder::NewPlaceholder(shared) => { |
584 | 0 | drop(shard_guard); |
585 | 0 | return EntryResult::Vacant(PlaceholderGuard::start_loading( |
586 | 0 | &self.lifecycle, |
587 | 0 | shard, |
588 | 0 | shared, |
589 | 0 | )); |
590 | | } |
591 | 0 | EntryOrPlaceholder::ExistingPlaceholder(shared) => { |
592 | 0 | match PlaceholderGuard::wait_for_placeholder( |
593 | 0 | &self.lifecycle, |
594 | 0 | shard, |
595 | 0 | shard_guard, |
596 | 0 | shared, |
597 | 0 | deadline.as_mut(), |
598 | 0 | ) { |
599 | 0 | JoinResult::Filled(_) => continue, |
600 | 0 | JoinResult::Guard(g) => return EntryResult::Vacant(g), |
601 | 0 | JoinResult::Timeout => return EntryResult::Timeout, |
602 | | } |
603 | | } |
604 | | } |
605 | | } |
606 | 0 | } |
607 | | |
608 | | /// Async version of [`Self::entry`]. |
609 | | /// |
610 | | /// Atomically accesses an existing entry, or gets a guard for insertion. |
611 | | /// If another task is already loading this key, waits asynchronously for the value. |
612 | | /// |
613 | | /// See [`entry`](Self::entry) for full documentation. |
614 | 0 | pub async fn entry_async<'a, Q, T>( |
615 | 0 | &'a self, |
616 | 0 | key: &Q, |
617 | 0 | on_occupied: impl FnOnce(&Key, &mut Val) -> EntryAction<T>, |
618 | 0 | ) -> EntryResult<'a, Key, Val, We, B, L, T> |
619 | 0 | where |
620 | 0 | Q: Hash + Equivalent<Key> + ToOwned<Owned = Key> + ?Sized, |
621 | 0 | { |
622 | 0 | let (shard, hash) = self.shard_for(key).unwrap(); |
623 | | // See entry() for explanation of the Option::take pattern. |
624 | 0 | let mut on_occupied = Some(on_occupied); |
625 | 0 | let mut callback = |k: &Key, v: &mut Val| on_occupied.take().unwrap()(k, v); |
626 | | |
627 | | loop { |
628 | | // Scope the write guard so it doesn't appear in the async state machine, |
629 | | // which would make the future !Send. |
630 | 0 | let result = { |
631 | 0 | let mut shard_guard = shard.write(); |
632 | 0 | match shard_guard.entry_or_placeholder(hash, key, &mut callback) { |
633 | 0 | EntryOrPlaceholder::Kept(t) => Ok(EntryResult::Retained(t)), |
634 | 0 | EntryOrPlaceholder::Removed(k, v) => Ok(EntryResult::Removed(k, v)), |
635 | 0 | EntryOrPlaceholder::Replaced(shared, old_val) => { |
636 | 0 | drop(shard_guard); |
637 | 0 | Ok(EntryResult::Replaced( |
638 | 0 | PlaceholderGuard::start_loading(&self.lifecycle, shard, shared), |
639 | 0 | old_val, |
640 | 0 | )) |
641 | | } |
642 | 0 | EntryOrPlaceholder::NewPlaceholder(shared) => { |
643 | 0 | drop(shard_guard); |
644 | 0 | Ok(EntryResult::Vacant(PlaceholderGuard::start_loading( |
645 | 0 | &self.lifecycle, |
646 | 0 | shard, |
647 | 0 | shared, |
648 | 0 | ))) |
649 | | } |
650 | 0 | EntryOrPlaceholder::ExistingPlaceholder(_) => Err(()), |
651 | | } |
652 | | }; |
653 | 0 | match result { |
654 | 0 | Ok(entry_result) => return entry_result, |
655 | 0 | Err(()) => match JoinFuture::new(&self.lifecycle, shard, hash, key).await { |
656 | 0 | JoinResult::Filled(_) => continue, |
657 | 0 | JoinResult::Guard(g) => return EntryResult::Vacant(g), |
658 | 0 | JoinResult::Timeout => unsafe { unreachable_unchecked() }, |
659 | | }, |
660 | | } |
661 | | } |
662 | 0 | } |
663 | | |
664 | | /// Get total memory used by cache data structures |
665 | | /// |
666 | | /// It should be noted that if cache key or value is some type like `Vec<T>`, |
667 | | /// the memory allocated in the heap will not be counted. |
668 | 0 | pub fn memory_used(&self) -> MemoryUsed { |
669 | 0 | let mut total = MemoryUsed { entries: 0, map: 0 }; |
670 | 0 | self.shards.iter().for_each(|shard| { |
671 | 0 | let shard_memory = shard.read().memory_used(); |
672 | 0 | total.entries += shard_memory.entries; |
673 | 0 | total.map += shard_memory.map; |
674 | 0 | }); |
675 | 0 | total |
676 | 0 | } |
677 | | } |
678 | | |
679 | | impl<Key, Val, We, B, L> std::fmt::Debug for Cache<Key, Val, We, B, L> { |
680 | 0 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
681 | 0 | f.debug_struct("Cache").finish_non_exhaustive() |
682 | 0 | } |
683 | | } |
684 | | |
685 | | /// Iterator over the items in the cache. |
686 | | /// |
687 | | /// See [`Cache::iter`] for more details. |
688 | | pub struct Iter<'a, Key, Val, We, B, L> { |
689 | | shards: &'a [RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>], |
690 | | current_shard: usize, |
691 | | last: Option<Token>, |
692 | | } |
693 | | |
694 | | impl<Key, Val, We, B, L> Iterator for Iter<'_, Key, Val, We, B, L> |
695 | | where |
696 | | Key: Clone, |
697 | | Val: Clone, |
698 | | { |
699 | | type Item = (Key, Val); |
700 | | |
701 | 0 | fn next(&mut self) -> Option<Self::Item> { |
702 | 0 | while self.current_shard < self.shards.len() { |
703 | 0 | let shard = &self.shards[self.current_shard]; |
704 | 0 | let lock = shard.read(); |
705 | 0 | if let Some((new_last, key, val)) = lock.iter_from(self.last).next() { |
706 | 0 | self.last = Some(new_last); |
707 | 0 | return Some((key.clone(), val.clone())); |
708 | 0 | } |
709 | 0 | self.last = None; |
710 | 0 | self.current_shard += 1; |
711 | | } |
712 | 0 | None |
713 | 0 | } |
714 | | } |
715 | | |
716 | | impl<Key, Val, We, B, L> std::fmt::Debug for Iter<'_, Key, Val, We, B, L> { |
717 | 0 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
718 | 0 | f.debug_struct("Iter").finish_non_exhaustive() |
719 | 0 | } |
720 | | } |
721 | | |
722 | | /// Draining iterator for the items in the cache. |
723 | | /// |
724 | | /// See [`Cache::drain`] for more details. |
725 | | pub struct Drain<'a, Key, Val, We, B, L> { |
726 | | shards: &'a [RwLock<CacheShard<Key, Val, We, B, L, SharedPlaceholder<Val>>>], |
727 | | current_shard: usize, |
728 | | last: Option<Token>, |
729 | | } |
730 | | |
731 | | impl<Key, Val, We, B, L> Iterator for Drain<'_, Key, Val, We, B, L> |
732 | | where |
733 | | Key: Hash + Eq, |
734 | | We: Weighter<Key, Val>, |
735 | | B: BuildHasher, |
736 | | L: Lifecycle<Key, Val>, |
737 | | { |
738 | | type Item = (Key, Val); |
739 | | |
740 | 0 | fn next(&mut self) -> Option<Self::Item> { |
741 | 0 | while self.current_shard < self.shards.len() { |
742 | 0 | let shard = &self.shards[self.current_shard]; |
743 | 0 | let mut lock = shard.write(); |
744 | 0 | if let Some((new_last, key, value)) = lock.remove_next(self.last) { |
745 | 0 | self.last = Some(new_last); |
746 | 0 | return Some((key, value)); |
747 | 0 | } |
748 | 0 | self.last = None; |
749 | 0 | self.current_shard += 1; |
750 | | } |
751 | 0 | None |
752 | 0 | } |
753 | | } |
754 | | |
755 | | impl<Key, Val, We, B, L> std::fmt::Debug for Drain<'_, Key, Val, We, B, L> { |
756 | 0 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
757 | 0 | f.debug_struct("Drain").finish_non_exhaustive() |
758 | 0 | } |
759 | | } |
760 | | |
761 | | /// Default `Lifecycle` for a sync cache. |
762 | | /// |
763 | | /// Stashes up to two evicted items for dropping them outside the cache locks. |
764 | | pub struct DefaultLifecycle<Key, Val>(std::marker::PhantomData<(Key, Val)>); |
765 | | |
766 | | impl<Key, Val> std::fmt::Debug for DefaultLifecycle<Key, Val> { |
767 | 0 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
768 | 0 | f.debug_tuple("DefaultLifecycle").finish() |
769 | 0 | } |
770 | | } |
771 | | |
772 | | impl<Key, Val> Default for DefaultLifecycle<Key, Val> { |
773 | | #[inline] |
774 | 453k | fn default() -> Self { |
775 | 453k | Self(Default::default()) |
776 | 453k | } Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<alloc::string::String, regex::regex::string::Regex> as core::default::Default>::default <quick_cache::sync::DefaultLifecycle<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue> as core::default::Default>::default Line | Count | Source | 774 | 26.2k | fn default() -> Self { | 775 | 26.2k | Self(Default::default()) | 776 | 26.2k | } |
<quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry> as core::default::Default>::default Line | Count | Source | 774 | 26.2k | fn default() -> Self { | 775 | 26.2k | Self(Default::default()) | 776 | 26.2k | } |
<quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry> as core::default::Default>::default Line | Count | Source | 774 | 400k | fn default() -> Self { | 775 | 400k | Self(Default::default()) | 776 | 400k | } |
Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<_, _> as core::default::Default>::default |
777 | | } |
778 | | impl<Key, Val> Clone for DefaultLifecycle<Key, Val> { |
779 | | #[inline] |
780 | 4.60M | fn clone(&self) -> Self { |
781 | 4.60M | Self(Default::default()) |
782 | 4.60M | } Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<alloc::string::String, regex::regex::string::Regex> as core::clone::Clone>::clone <quick_cache::sync::DefaultLifecycle<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue> as core::clone::Clone>::clone Line | Count | Source | 780 | 3.36M | fn clone(&self) -> Self { | 781 | 3.36M | Self(Default::default()) | 782 | 3.36M | } |
<quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry> as core::clone::Clone>::clone Line | Count | Source | 780 | 841k | fn clone(&self) -> Self { | 781 | 841k | Self(Default::default()) | 782 | 841k | } |
<quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry> as core::clone::Clone>::clone Line | Count | Source | 780 | 400k | fn clone(&self) -> Self { | 781 | 400k | Self(Default::default()) | 782 | 400k | } |
Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<_, _> as core::clone::Clone>::clone |
783 | | } |
784 | | |
785 | | impl<Key, Val> Lifecycle<Key, Val> for DefaultLifecycle<Key, Val> { |
786 | | // Why two items? |
787 | | // Because assuming the cache has roughly similarly weighted items, |
788 | | // we can expect that at one or two items will be evicted per request |
789 | | // in most cases. And we want to avoid introducing any extra |
790 | | // overhead (e.g. a vector) for this default lifecycle. |
791 | | type RequestState = [Option<(Key, Val)>; 2]; |
792 | | |
793 | | #[inline] |
794 | 346k | fn begin_request(&self) -> Self::RequestState { |
795 | 346k | [None, None] |
796 | 346k | } Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<alloc::string::String, regex::regex::string::Regex> as quick_cache::Lifecycle<alloc::string::String, regex::regex::string::Regex>>::begin_request Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue> as quick_cache::Lifecycle<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue>>::begin_request <quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry> as quick_cache::Lifecycle<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry>>::begin_request Line | Count | Source | 794 | 18.9k | fn begin_request(&self) -> Self::RequestState { | 795 | 18.9k | [None, None] | 796 | 18.9k | } |
<quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry> as quick_cache::Lifecycle<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry>>::begin_request Line | Count | Source | 794 | 327k | fn begin_request(&self) -> Self::RequestState { | 795 | 327k | [None, None] | 796 | 327k | } |
Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<_, _> as quick_cache::Lifecycle<_, _>>::begin_request |
797 | | |
798 | | #[inline] |
799 | 29.2k | fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) { |
800 | 29.2k | if std::mem::needs_drop::<(Key, Val)>() { |
801 | 29.2k | if state[0].is_none() { |
802 | 29.2k | state[0] = Some((key, val)); |
803 | 29.2k | } else if state[1].is_none() { |
804 | 0 | state[1] = Some((key, val)); |
805 | 0 | } |
806 | 0 | } |
807 | 29.2k | } Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<alloc::string::String, regex::regex::string::Regex> as quick_cache::Lifecycle<alloc::string::String, regex::regex::string::Regex>>::on_evict Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue> as quick_cache::Lifecycle<surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheKey, surrealdb_core::idx::trees::diskann::cache::DiskAnnCacheValue>>::on_evict <quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry> as quick_cache::Lifecycle<surrealdb_core::kvs::cache::ds::key::Key, surrealdb_core::kvs::cache::ds::entry::Entry>>::on_evict Line | Count | Source | 799 | 430 | fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) { | 800 | 430 | if std::mem::needs_drop::<(Key, Val)>() { | 801 | 430 | if state[0].is_none() { | 802 | 430 | state[0] = Some((key, val)); | 803 | 430 | } else if state[1].is_none() { | 804 | 0 | state[1] = Some((key, val)); | 805 | 0 | } | 806 | 0 | } | 807 | 430 | } |
<quick_cache::sync::DefaultLifecycle<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry> as quick_cache::Lifecycle<surrealdb_core::kvs::cache::tx::key::Key, surrealdb_core::kvs::cache::tx::entry::Entry>>::on_evict Line | Count | Source | 799 | 28.7k | fn on_evict(&self, state: &mut Self::RequestState, key: Key, val: Val) { | 800 | 28.7k | if std::mem::needs_drop::<(Key, Val)>() { | 801 | 28.7k | if state[0].is_none() { | 802 | 28.7k | state[0] = Some((key, val)); | 803 | 28.7k | } else if state[1].is_none() { | 804 | 0 | state[1] = Some((key, val)); | 805 | 0 | } | 806 | 0 | } | 807 | 28.7k | } |
Unexecuted instantiation: <quick_cache::sync::DefaultLifecycle<_, _> as quick_cache::Lifecycle<_, _>>::on_evict |
808 | | } |
809 | | |
810 | | #[cfg(test)] |
811 | | mod tests { |
812 | | use super::*; |
813 | | use std::{ |
814 | | sync::{Arc, Barrier}, |
815 | | thread, |
816 | | }; |
817 | | |
818 | | #[test] |
819 | | #[cfg_attr(miri, ignore)] |
820 | | fn test_multiple_threads() { |
821 | | const N_THREAD_PAIRS: usize = 8; |
822 | | const N_ROUNDS: usize = 1_000; |
823 | | const ITEMS_PER_THREAD: usize = 1_000; |
824 | | let mut threads = Vec::new(); |
825 | | let barrier = Arc::new(Barrier::new(N_THREAD_PAIRS * 2)); |
826 | | let cache = Arc::new(Cache::new(N_THREAD_PAIRS * ITEMS_PER_THREAD / 10)); |
827 | | for t in 0..N_THREAD_PAIRS { |
828 | | let barrier = barrier.clone(); |
829 | | let cache = cache.clone(); |
830 | | let handle = thread::spawn(move || { |
831 | | let start = ITEMS_PER_THREAD * t; |
832 | | barrier.wait(); |
833 | | for _round in 0..N_ROUNDS { |
834 | | for i in start..start + ITEMS_PER_THREAD { |
835 | | cache.insert(i, i); |
836 | | } |
837 | | } |
838 | | }); |
839 | | threads.push(handle); |
840 | | } |
841 | | for t in 0..N_THREAD_PAIRS { |
842 | | let barrier = barrier.clone(); |
843 | | let cache = cache.clone(); |
844 | | let handle = thread::spawn(move || { |
845 | | let start = ITEMS_PER_THREAD * t; |
846 | | barrier.wait(); |
847 | | for _round in 0..N_ROUNDS { |
848 | | for i in start..start + ITEMS_PER_THREAD { |
849 | | if let Some(cached) = cache.get(&i) { |
850 | | assert_eq!(cached, i); |
851 | | } |
852 | | } |
853 | | } |
854 | | }); |
855 | | threads.push(handle); |
856 | | } |
857 | | for t in threads { |
858 | | t.join().unwrap(); |
859 | | } |
860 | | } |
861 | | |
862 | | #[test] |
863 | | fn test_iter() { |
864 | | let capacity = if cfg!(miri) { 100 } else { 100000 }; |
865 | | let options = OptionsBuilder::new() |
866 | | .estimated_items_capacity(capacity) |
867 | | .weight_capacity(capacity as u64) |
868 | | .shards(2) |
869 | | .build() |
870 | | .unwrap(); |
871 | | let cache = Cache::with_options( |
872 | | options, |
873 | | UnitWeighter, |
874 | | DefaultHashBuilder::default(), |
875 | | DefaultLifecycle::default(), |
876 | | ); |
877 | | let items = capacity / 2; |
878 | | for i in 0..items { |
879 | | cache.insert(i, i); |
880 | | } |
881 | | assert_eq!(cache.len(), items); |
882 | | let mut iter_collected = cache.iter().collect::<Vec<_>>(); |
883 | | assert_eq!(iter_collected.len(), items); |
884 | | iter_collected.sort(); |
885 | | for (i, v) in iter_collected.into_iter().enumerate() { |
886 | | assert_eq!((i, i), v); |
887 | | } |
888 | | } |
889 | | |
890 | | #[test] |
891 | | fn test_drain() { |
892 | | let capacity = if cfg!(miri) { 100 } else { 100000 }; |
893 | | let options = OptionsBuilder::new() |
894 | | .estimated_items_capacity(capacity) |
895 | | .weight_capacity(capacity as u64) |
896 | | .shards(2) |
897 | | .build() |
898 | | .unwrap(); |
899 | | let cache = Cache::with_options( |
900 | | options, |
901 | | UnitWeighter, |
902 | | DefaultHashBuilder::default(), |
903 | | DefaultLifecycle::default(), |
904 | | ); |
905 | | let items = capacity / 2; |
906 | | for i in 0..items { |
907 | | cache.insert(i, i); |
908 | | } |
909 | | assert_eq!(cache.len(), items); |
910 | | let mut drain_collected = cache.drain().collect::<Vec<_>>(); |
911 | | assert_eq!(cache.len(), 0); |
912 | | assert_eq!(drain_collected.len(), items); |
913 | | drain_collected.sort(); |
914 | | for (i, v) in drain_collected.into_iter().enumerate() { |
915 | | assert_eq!((i, i), v); |
916 | | } |
917 | | } |
918 | | |
919 | | #[test] |
920 | | fn test_set_capacity() { |
921 | | let cache = Cache::new(100); |
922 | | for i in 0..80 { |
923 | | cache.insert(i, i); |
924 | | } |
925 | | let initial_len = cache.len(); |
926 | | assert!(initial_len <= 80); |
927 | | |
928 | | // Set to smaller capacity |
929 | | cache.set_capacity(50); |
930 | | assert!(cache.len() <= 50); |
931 | | assert!(cache.weight() <= 50); |
932 | | |
933 | | // Set to larger capacity |
934 | | cache.set_capacity(200); |
935 | | assert_eq!(cache.capacity(), 200); |
936 | | |
937 | | // Insert more items |
938 | | for i in 100..180 { |
939 | | cache.insert(i, i); |
940 | | } |
941 | | assert!(cache.len() <= 180); |
942 | | assert!(cache.weight() <= 200); |
943 | | } |
944 | | |
945 | | #[test] |
946 | | fn test_remove_if() { |
947 | | let cache = Cache::new(100); |
948 | | |
949 | | // Insert test data |
950 | | cache.insert(1, 10); |
951 | | cache.insert(2, 20); |
952 | | cache.insert(3, 30); |
953 | | |
954 | | // Test removing with predicate that returns true |
955 | | let removed = cache.remove_if(&2, |v| *v == 20); |
956 | | assert_eq!(removed, Some((2, 20))); |
957 | | assert_eq!(cache.get(&2), None); |
958 | | |
959 | | // Test removing with predicate that returns false |
960 | | let not_removed = cache.remove_if(&3, |v| *v == 999); |
961 | | assert_eq!(not_removed, None); |
962 | | assert_eq!(cache.get(&3), Some(30)); |
963 | | |
964 | | // Test removing non-existent key |
965 | | let not_found = cache.remove_if(&999, |_| true); |
966 | | assert_eq!(not_found, None); |
967 | | } |
968 | | |
969 | | /// Tests all basic entry actions: Retain, Remove, ReplaceWithGuard, Vacant, mutate+Retain |
970 | | #[test] |
971 | | fn test_entry_actions() { |
972 | | let cache = Cache::new(100); |
973 | | cache.insert(1, 10); |
974 | | cache.insert(2, 20); |
975 | | |
976 | | // Retain returns the value via callback, entry stays |
977 | | let result = cache.entry(&1, None, |_k, v| EntryAction::Retain(*v)); |
978 | | assert!(matches!(result, EntryResult::Retained(10))); |
979 | | assert_eq!(cache.get(&1), Some(10)); |
980 | | |
981 | | // Mutate in place via Retain |
982 | | let result = cache.entry(&1, None, |_k, v| { |
983 | | *v += 5; |
984 | | EntryAction::Retain(()) |
985 | | }); |
986 | | assert!(matches!(result, EntryResult::Retained(()))); |
987 | | assert_eq!(cache.get(&1), Some(15)); |
988 | | |
989 | | // Remove |
990 | | let result = cache.entry(&1, None, |_k, _v| EntryAction::<()>::Remove); |
991 | | assert!(matches!(result, EntryResult::Removed(1, 15))); |
992 | | assert_eq!(cache.get(&1), None); |
993 | | |
994 | | // Remove then re-enter same key → Vacant |
995 | | let result = cache.entry(&1, None, |_k, v| EntryAction::Retain(*v)); |
996 | | match result { |
997 | | EntryResult::Vacant(g) => { |
998 | | let _ = g.insert(99); |
999 | | assert_eq!(cache.get(&1), Some(99)); |
1000 | | } |
1001 | | _ => panic!("expected Vacant for removed key"), |
1002 | | } |
1003 | | |
1004 | | // ReplaceWithGuard: capture old value, get guard, insert new |
1005 | | let mut old_val = 0; |
1006 | | let result = cache.entry(&2, None, |_k, v| { |
1007 | | old_val = *v; |
1008 | | EntryAction::<()>::ReplaceWithGuard |
1009 | | }); |
1010 | | assert_eq!(old_val, 20); |
1011 | | match result { |
1012 | | EntryResult::Replaced(g, old) => { |
1013 | | assert_eq!(old, 20); |
1014 | | let _ = g.insert(old_val + 100); |
1015 | | assert_eq!(cache.get(&2), Some(120)); |
1016 | | } |
1017 | | _ => panic!("expected Replaced"), |
1018 | | } |
1019 | | |
1020 | | // ReplaceWithGuard then abandon guard → entry gone |
1021 | | let result = cache.entry(&2, None, |_k, _v| EntryAction::<()>::ReplaceWithGuard); |
1022 | | match result { |
1023 | | EntryResult::Replaced(g, _old) => { |
1024 | | drop(g); |
1025 | | assert_eq!(cache.get(&2), None); |
1026 | | } |
1027 | | _ => panic!("expected Replaced"), |
1028 | | } |
1029 | | |
1030 | | // Vacant key → guard |
1031 | | let result = cache.entry(&3, None, |_k, v| EntryAction::Retain(*v)); |
1032 | | match result { |
1033 | | EntryResult::Vacant(g) => { |
1034 | | let _ = g.insert(30); |
1035 | | assert_eq!(cache.get(&3), Some(30)); |
1036 | | } |
1037 | | _ => panic!("expected Vacant"), |
1038 | | } |
1039 | | } |
1040 | | |
1041 | | /// Tests weight tracking across all entry actions using a string-length weighter |
1042 | | #[test] |
1043 | | fn test_entry_weight_tracking() { |
1044 | | #[derive(Clone)] |
1045 | | struct StringWeighter; |
1046 | | impl crate::Weighter<u64, String> for StringWeighter { |
1047 | | fn weight(&self, _key: &u64, val: &String) -> u64 { |
1048 | | val.len() as u64 |
1049 | | } |
1050 | | } |
1051 | | |
1052 | | let cache = Cache::with_weighter(100, 100_000, StringWeighter); |
1053 | | cache.insert(1, "hello".to_string()); |
1054 | | cache.insert(2, "world".to_string()); |
1055 | | assert_eq!(cache.weight(), 10); |
1056 | | |
1057 | | // Retain without mutation — weight unchanged |
1058 | | let result = cache.entry(&1, None, |_k, _v| EntryAction::Retain(())); |
1059 | | assert!(matches!(result, EntryResult::Retained(()))); |
1060 | | assert_eq!(cache.weight(), 10); |
1061 | | |
1062 | | // Mutate to longer string — weight increases |
1063 | | let result = cache.entry(&1, None, |_k, v| { |
1064 | | v.push_str(" world"); |
1065 | | EntryAction::Retain(()) |
1066 | | }); |
1067 | | assert!(matches!(result, EntryResult::Retained(()))); |
1068 | | assert_eq!(cache.weight(), 16); // "hello world" (11) + "world" (5) |
1069 | | assert_eq!(cache.get(&1).unwrap(), "hello world"); |
1070 | | |
1071 | | // Mutate to empty string — weight to zero, entry stays |
1072 | | let result = cache.entry(&1, None, |_k, v| { |
1073 | | v.clear(); |
1074 | | EntryAction::Retain(()) |
1075 | | }); |
1076 | | assert!(matches!(result, EntryResult::Retained(()))); |
1077 | | assert_eq!(cache.weight(), 5); // "" (0) + "world" (5) |
1078 | | assert_eq!(cache.get(&1).unwrap(), ""); |
1079 | | |
1080 | | // Remove — weight decremented |
1081 | | let result = cache.entry(&2, None, |_k, _v| EntryAction::<()>::Remove); |
1082 | | assert!(matches!(result, EntryResult::Removed(2, _))); |
1083 | | assert_eq!(cache.weight(), 0); |
1084 | | assert_eq!(cache.len(), 1); |
1085 | | |
1086 | | // ReplaceWithGuard — old weight gone, new weight after insert |
1087 | | cache.insert(3, "hello".to_string()); |
1088 | | assert_eq!(cache.weight(), 5); |
1089 | | let result = cache.entry(&3, None, |_k, _v| EntryAction::<()>::ReplaceWithGuard); |
1090 | | match result { |
1091 | | EntryResult::Replaced(g, _old) => { |
1092 | | assert_eq!(cache.weight(), 0); |
1093 | | let _ = g.insert("hello world!!".to_string()); |
1094 | | assert_eq!(cache.weight(), 13); |
1095 | | } |
1096 | | _ => panic!("expected Replaced"), |
1097 | | } |
1098 | | } |
1099 | | |
1100 | | /// Tests eviction and zero-capacity edge cases |
1101 | | #[test] |
1102 | | fn test_entry_eviction() { |
1103 | | // Cache with capacity for ~2 items — insert 3rd triggers eviction |
1104 | | let cache = Cache::new(2); |
1105 | | cache.insert(1, 10); |
1106 | | cache.insert(2, 20); |
1107 | | assert_eq!(cache.len(), 2); |
1108 | | |
1109 | | let result = cache.entry(&3, None, |_k, v| EntryAction::Retain(*v)); |
1110 | | match result { |
1111 | | EntryResult::Vacant(g) => { |
1112 | | let _ = g.insert(30); |
1113 | | assert!(cache.len() <= 2); |
1114 | | assert_eq!(cache.get(&3), Some(30)); |
1115 | | } |
1116 | | _ => panic!("expected Vacant"), |
1117 | | } |
1118 | | |
1119 | | // Zero-capacity cache — insert evicts immediately |
1120 | | let cache = Cache::new(0); |
1121 | | let result = cache.entry(&1, None, |_k, v| EntryAction::Retain(*v)); |
1122 | | match result { |
1123 | | EntryResult::Vacant(g) => { |
1124 | | let _ = g.insert(10); |
1125 | | assert_eq!(cache.get(&1), None); |
1126 | | } |
1127 | | _ => panic!("expected Vacant"), |
1128 | | } |
1129 | | } |
1130 | | |
1131 | | /// Tests entry() waiting on existing placeholder: value arrives, guard abandoned |
1132 | | #[test] |
1133 | | #[cfg_attr(miri, ignore)] |
1134 | | fn test_entry_concurrent_placeholder_wait() { |
1135 | | let cache = Arc::new(Cache::new(100)); |
1136 | | let barrier = Arc::new(Barrier::new(2)); |
1137 | | |
1138 | | // Thread holds guard, inserts after delay |
1139 | | let cache2 = cache.clone(); |
1140 | | let barrier2 = barrier.clone(); |
1141 | | let handle = thread::spawn(move || match cache2.get_value_or_guard(&1, None) { |
1142 | | GuardResult::Guard(g) => { |
1143 | | barrier2.wait(); |
1144 | | std::thread::sleep(Duration::from_millis(50)); |
1145 | | let _ = g.insert(42); |
1146 | | } |
1147 | | _ => panic!("expected guard"), |
1148 | | }); |
1149 | | |
1150 | | barrier.wait(); |
1151 | | let result = cache.entry(&1, None, |_k, v| EntryAction::Retain(*v)); |
1152 | | assert!(matches!(result, EntryResult::Retained(42))); |
1153 | | handle.join().unwrap(); |
1154 | | } |
1155 | | |
1156 | | /// Tests entry() getting guard when placeholder loader abandons |
1157 | | #[test] |
1158 | | #[cfg_attr(miri, ignore)] |
1159 | | fn test_entry_concurrent_placeholder_guard_abandoned() { |
1160 | | let cache = Arc::new(Cache::new(100)); |
1161 | | let barrier = Arc::new(Barrier::new(2)); |
1162 | | |
1163 | | let cache2 = cache.clone(); |
1164 | | let barrier2 = barrier.clone(); |
1165 | | let handle = thread::spawn(move || match cache2.get_value_or_guard(&1, None) { |
1166 | | GuardResult::Guard(g) => { |
1167 | | barrier2.wait(); |
1168 | | std::thread::sleep(Duration::from_millis(50)); |
1169 | | drop(g); |
1170 | | } |
1171 | | _ => panic!("expected guard"), |
1172 | | }); |
1173 | | |
1174 | | barrier.wait(); |
1175 | | let result = cache.entry(&1, None, |_k, v| EntryAction::Retain(*v)); |
1176 | | match result { |
1177 | | EntryResult::Vacant(g) => { |
1178 | | let _ = g.insert(99); |
1179 | | assert_eq!(cache.get(&1), Some(99)); |
1180 | | } |
1181 | | _ => panic!("expected Vacant after abandoned placeholder"), |
1182 | | } |
1183 | | handle.join().unwrap(); |
1184 | | } |
1185 | | |
1186 | | /// Tests zero and nonzero timeouts |
1187 | | #[test] |
1188 | | #[cfg_attr(miri, ignore)] |
1189 | | fn test_entry_timeout() { |
1190 | | let cache = Cache::new(100); |
1191 | | |
1192 | | // Zero timeout — immediate Timeout when placeholder exists |
1193 | | let guard = match cache.get_value_or_guard(&1, None) { |
1194 | | GuardResult::Guard(g) => g, |
1195 | | _ => panic!("expected guard"), |
1196 | | }; |
1197 | | let result = cache.entry(&1, Some(Duration::ZERO), |_k, v| EntryAction::Retain(*v)); |
1198 | | assert!(matches!(result, EntryResult::Timeout)); |
1199 | | let _ = guard.insert(1); |
1200 | | |
1201 | | // Nonzero timeout — guard held longer than timeout |
1202 | | let cache = Arc::new(Cache::new(100)); |
1203 | | let barrier = Arc::new(Barrier::new(2)); |
1204 | | let cache2 = cache.clone(); |
1205 | | let barrier2 = barrier.clone(); |
1206 | | let holder = thread::spawn(move || { |
1207 | | let guard = match cache2.get_value_or_guard(&1, None) { |
1208 | | GuardResult::Guard(g) => g, |
1209 | | _ => panic!("expected guard"), |
1210 | | }; |
1211 | | barrier2.wait(); |
1212 | | std::thread::sleep(Duration::from_millis(200)); |
1213 | | let _ = guard.insert(1); |
1214 | | }); |
1215 | | |
1216 | | barrier.wait(); |
1217 | | let result = cache.entry(&1, Some(Duration::from_millis(50)), |_k, v| { |
1218 | | EntryAction::Retain(*v) |
1219 | | }); |
1220 | | assert!(matches!(result, EntryResult::Timeout)); |
1221 | | holder.join().unwrap(); |
1222 | | } |
1223 | | |
1224 | | /// Tests multiple waiters all receiving the value |
1225 | | #[test] |
1226 | | #[cfg_attr(miri, ignore)] |
1227 | | fn test_entry_concurrent_multiple_waiters() { |
1228 | | let cache = Arc::new(Cache::new(100)); |
1229 | | let barrier = Arc::new(Barrier::new(4)); // 1 loader + 3 waiters |
1230 | | |
1231 | | let cache1 = cache.clone(); |
1232 | | let barrier1 = barrier.clone(); |
1233 | | let loader = thread::spawn(move || match cache1.get_value_or_guard(&1, None) { |
1234 | | GuardResult::Guard(g) => { |
1235 | | barrier1.wait(); |
1236 | | std::thread::sleep(Duration::from_millis(50)); |
1237 | | let _ = g.insert(42); |
1238 | | } |
1239 | | _ => panic!("expected guard"), |
1240 | | }); |
1241 | | |
1242 | | let mut waiters = Vec::new(); |
1243 | | for _ in 0..3 { |
1244 | | let cache_c = cache.clone(); |
1245 | | let barrier_c = barrier.clone(); |
1246 | | waiters.push(thread::spawn(move || { |
1247 | | barrier_c.wait(); |
1248 | | let result = cache_c.entry(&1, None, |_k, v| EntryAction::Retain(*v)); |
1249 | | match result { |
1250 | | EntryResult::Retained(v) => v, |
1251 | | _ => panic!("expected Value"), |
1252 | | } |
1253 | | })); |
1254 | | } |
1255 | | |
1256 | | loader.join().unwrap(); |
1257 | | for w in waiters { |
1258 | | assert_eq!(w.join().unwrap(), 42); |
1259 | | } |
1260 | | } |
1261 | | |
1262 | | /// Tests ReplaceWithGuard and Remove actions after waiting for a placeholder |
1263 | | #[test] |
1264 | | #[cfg_attr(miri, ignore)] |
1265 | | fn test_entry_concurrent_action_after_wait() { |
1266 | | // ReplaceWithGuard after wait |
1267 | | let cache = Arc::new(Cache::new(100)); |
1268 | | let barrier = Arc::new(Barrier::new(2)); |
1269 | | |
1270 | | let cache1 = cache.clone(); |
1271 | | let barrier1 = barrier.clone(); |
1272 | | let loader = thread::spawn(move || match cache1.get_value_or_guard(&1, None) { |
1273 | | GuardResult::Guard(g) => { |
1274 | | barrier1.wait(); |
1275 | | std::thread::sleep(Duration::from_millis(50)); |
1276 | | let _ = g.insert(42); |
1277 | | } |
1278 | | _ => panic!("expected guard"), |
1279 | | }); |
1280 | | |
1281 | | barrier.wait(); |
1282 | | let result = cache.entry(&1, None, |_k, _v| EntryAction::<()>::ReplaceWithGuard); |
1283 | | match result { |
1284 | | EntryResult::Replaced(g, old) => { |
1285 | | assert_eq!(old, 42); |
1286 | | let _ = g.insert(100); |
1287 | | assert_eq!(cache.get(&1), Some(100)); |
1288 | | } |
1289 | | _ => panic!("expected Replaced"), |
1290 | | } |
1291 | | loader.join().unwrap(); |
1292 | | |
1293 | | // Remove after wait |
1294 | | let cache = Arc::new(Cache::new(100)); |
1295 | | let barrier = Arc::new(Barrier::new(2)); |
1296 | | |
1297 | | let cache1 = cache.clone(); |
1298 | | let barrier1 = barrier.clone(); |
1299 | | let loader = thread::spawn(move || match cache1.get_value_or_guard(&1, None) { |
1300 | | GuardResult::Guard(g) => { |
1301 | | barrier1.wait(); |
1302 | | std::thread::sleep(Duration::from_millis(50)); |
1303 | | let _ = g.insert(42); |
1304 | | } |
1305 | | _ => panic!("expected guard"), |
1306 | | }); |
1307 | | |
1308 | | barrier.wait(); |
1309 | | let result = cache.entry(&1, None, |_k, _v| EntryAction::<()>::Remove); |
1310 | | assert!(matches!(result, EntryResult::Removed(1, 42))); |
1311 | | assert_eq!(cache.get(&1), None); |
1312 | | loader.join().unwrap(); |
1313 | | } |
1314 | | |
1315 | | /// Multi-thread stress test for entry() |
1316 | | #[test] |
1317 | | #[cfg_attr(miri, ignore)] |
1318 | | fn test_entry_concurrent_stress() { |
1319 | | const N_THREADS: usize = 8; |
1320 | | const N_KEYS: usize = 50; |
1321 | | const N_OPS: usize = 500; |
1322 | | |
1323 | | let cache = Arc::new(Cache::new(1000)); |
1324 | | let barrier = Arc::new(Barrier::new(N_THREADS)); |
1325 | | |
1326 | | let mut handles = Vec::new(); |
1327 | | for t in 0..N_THREADS { |
1328 | | let cache = cache.clone(); |
1329 | | let barrier = barrier.clone(); |
1330 | | handles.push(thread::spawn(move || { |
1331 | | barrier.wait(); |
1332 | | for i in 0..N_OPS { |
1333 | | let key = (t * N_OPS + i) % N_KEYS; |
1334 | | let result = cache.entry(&key, Some(Duration::from_millis(10)), |_k, v| { |
1335 | | EntryAction::Retain(*v) |
1336 | | }); |
1337 | | match result { |
1338 | | EntryResult::Retained(_) => {} |
1339 | | EntryResult::Vacant(g) => { |
1340 | | let _ = g.insert(key * 10); |
1341 | | } |
1342 | | EntryResult::Replaced(g, _) => { |
1343 | | let _ = g.insert(key * 10); |
1344 | | } |
1345 | | EntryResult::Timeout => {} |
1346 | | EntryResult::Removed(_, _) => {} |
1347 | | } |
1348 | | } |
1349 | | })); |
1350 | | } |
1351 | | |
1352 | | for h in handles { |
1353 | | h.join().unwrap(); |
1354 | | } |
1355 | | |
1356 | | assert!(cache.len() <= N_KEYS); |
1357 | | for key in 0..N_KEYS { |
1358 | | if let Some(v) = cache.get(&key) { |
1359 | | assert_eq!(v, key * 10); |
1360 | | } |
1361 | | } |
1362 | | } |
1363 | | |
1364 | | // --- Async tests --- |
1365 | | |
1366 | | /// Tests all basic async entry actions in one test |
1367 | | #[tokio::test] |
1368 | | async fn test_entry_async_actions() { |
1369 | | let cache = Cache::new(100); |
1370 | | cache.insert(1, 10); |
1371 | | cache.insert(2, 20); |
1372 | | |
1373 | | // Retain |
1374 | | let result = cache.entry_async(&1, |_k, v| EntryAction::Retain(*v)).await; |
1375 | | assert!(matches!(result, EntryResult::Retained(10))); |
1376 | | assert_eq!(cache.get(&1), Some(10)); |
1377 | | |
1378 | | // Remove |
1379 | | let result = cache |
1380 | | .entry_async(&1, |_k, _v| EntryAction::<()>::Remove) |
1381 | | .await; |
1382 | | assert!(matches!(result, EntryResult::Removed(1, 10))); |
1383 | | assert_eq!(cache.get(&1), None); |
1384 | | |
1385 | | // ReplaceWithGuard |
1386 | | let result = cache |
1387 | | .entry_async(&2, |_k, _v| EntryAction::<()>::ReplaceWithGuard) |
1388 | | .await; |
1389 | | match result { |
1390 | | EntryResult::Replaced(g, old) => { |
1391 | | assert_eq!(old, 20); |
1392 | | let _ = g.insert(42); |
1393 | | assert_eq!(cache.get(&2), Some(42)); |
1394 | | } |
1395 | | _ => panic!("expected Replaced"), |
1396 | | } |
1397 | | |
1398 | | // Vacant |
1399 | | let result = cache.entry_async(&3, |_k, v| EntryAction::Retain(*v)).await; |
1400 | | match result { |
1401 | | EntryResult::Vacant(g) => { |
1402 | | let _ = g.insert(99); |
1403 | | assert_eq!(cache.get(&3), Some(99)); |
1404 | | } |
1405 | | _ => panic!("expected Vacant"), |
1406 | | } |
1407 | | } |
1408 | | |
1409 | | /// Tests async entry waiting on placeholder: value arrives, guard abandoned |
1410 | | #[tokio::test(flavor = "multi_thread")] |
1411 | | async fn test_entry_async_concurrent_wait() { |
1412 | | let cache = Arc::new(Cache::new(100)); |
1413 | | let barrier = Arc::new(Barrier::new(2)); |
1414 | | |
1415 | | let cache1 = cache.clone(); |
1416 | | let barrier1 = barrier.clone(); |
1417 | | let holder = thread::spawn(move || { |
1418 | | let guard = match cache1.get_value_or_guard(&1, None) { |
1419 | | GuardResult::Guard(g) => g, |
1420 | | _ => panic!("expected guard"), |
1421 | | }; |
1422 | | barrier1.wait(); |
1423 | | std::thread::sleep(Duration::from_millis(50)); |
1424 | | let _ = guard.insert(42); |
1425 | | }); |
1426 | | |
1427 | | barrier.wait(); |
1428 | | let result = cache.entry_async(&1, |_k, v| EntryAction::Retain(*v)).await; |
1429 | | assert!(matches!(result, EntryResult::Retained(42))); |
1430 | | holder.join().unwrap(); |
1431 | | } |
1432 | | |
1433 | | /// Tests async entry getting guard when placeholder loader abandons |
1434 | | #[tokio::test(flavor = "multi_thread")] |
1435 | | async fn test_entry_async_concurrent_guard_abandoned() { |
1436 | | let cache = Arc::new(Cache::new(100)); |
1437 | | let barrier = Arc::new(Barrier::new(2)); |
1438 | | |
1439 | | let cache1 = cache.clone(); |
1440 | | let barrier1 = barrier.clone(); |
1441 | | let holder = thread::spawn(move || { |
1442 | | let guard = match cache1.get_value_or_guard(&1, None) { |
1443 | | GuardResult::Guard(g) => g, |
1444 | | _ => panic!("expected guard"), |
1445 | | }; |
1446 | | barrier1.wait(); |
1447 | | std::thread::sleep(Duration::from_millis(50)); |
1448 | | drop(guard); |
1449 | | }); |
1450 | | |
1451 | | barrier.wait(); |
1452 | | let result = cache.entry_async(&1, |_k, v| EntryAction::Retain(*v)).await; |
1453 | | match result { |
1454 | | EntryResult::Vacant(g) => { |
1455 | | let _ = g.insert(99); |
1456 | | } |
1457 | | _ => panic!("expected Vacant after abandoned placeholder"), |
1458 | | } |
1459 | | assert_eq!(cache.get(&1), Some(99)); |
1460 | | holder.join().unwrap(); |
1461 | | } |
1462 | | |
1463 | | /// Multi-task async stress test |
1464 | | #[tokio::test(flavor = "multi_thread")] |
1465 | | #[cfg_attr(miri, ignore)] |
1466 | | async fn test_entry_async_concurrent_stress() { |
1467 | | const N_TASKS: usize = 16; |
1468 | | const N_KEYS: usize = 50; |
1469 | | const N_OPS: usize = 200; |
1470 | | |
1471 | | let cache = Arc::new(Cache::new(1000)); |
1472 | | let barrier = Arc::new(tokio::sync::Barrier::new(N_TASKS)); |
1473 | | |
1474 | | let mut handles = Vec::new(); |
1475 | | for t in 0..N_TASKS { |
1476 | | let cache = cache.clone(); |
1477 | | let barrier = barrier.clone(); |
1478 | | handles.push(tokio::spawn(async move { |
1479 | | barrier.wait().await; |
1480 | | for i in 0..N_OPS { |
1481 | | let key = (t * N_OPS + i) % N_KEYS; |
1482 | | // Use get_or_insert_async instead of entry_async to avoid |
1483 | | // lifetime issues with tokio::spawn (entry_async borrows &self) |
1484 | | let _ = cache |
1485 | | .get_or_insert_async(&key, async { Ok::<_, ()>(key * 10) }) |
1486 | | .await; |
1487 | | } |
1488 | | })); |
1489 | | } |
1490 | | |
1491 | | for h in handles { |
1492 | | h.await.unwrap(); |
1493 | | } |
1494 | | |
1495 | | assert!(cache.len() <= N_KEYS); |
1496 | | for key in 0..N_KEYS { |
1497 | | if let Some(v) = cache.get(&key) { |
1498 | | assert_eq!(v, key * 10); |
1499 | | } |
1500 | | } |
1501 | | } |
1502 | | } |