/build/source/nativelink-util/src/evicting_map.rs
Line | Count | Source |
1 | | // Copyright 2024 The NativeLink Authors. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::borrow::Borrow; |
16 | | use core::cmp::Eq; |
17 | | use core::fmt::{Debug, Display}; |
18 | | use core::future::Future; |
19 | | use core::hash::Hash; |
20 | | use core::marker::PhantomData; |
21 | | use core::ops::RangeBounds; |
22 | | use core::pin::Pin; |
23 | | use std::collections::{BTreeSet, HashMap}; |
24 | | use std::sync::{Arc, OnceLock}; |
25 | | |
26 | | use futures::StreamExt; |
27 | | use futures::stream::FuturesUnordered; |
28 | | use lru::LruCache; |
29 | | use nativelink_config::stores::EvictionPolicy; |
30 | | use nativelink_metric::MetricsComponent; |
31 | | use opentelemetry::KeyValue; |
32 | | use parking_lot::Mutex; |
33 | | use serde::{Deserialize, Serialize}; |
34 | | use tracing::{debug, info}; |
35 | | |
36 | | use crate::instant_wrapper::InstantWrapper; |
37 | | use crate::metrics::{record_cache_entries_delta, saturating_i64}; |
38 | | use crate::metrics_utils::{Counter, CounterWithTime}; |
39 | | |
40 | | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)] |
41 | | pub struct SerializedLRU<K> { |
42 | | pub data: Vec<(K, i32)>, |
43 | | pub anchor_time: u64, |
44 | | } |
45 | | |
46 | | #[derive(Debug)] |
47 | | struct EvictionItem<T: LenEntry + Debug> { |
48 | | seconds_since_anchor: i32, |
49 | | data: T, |
50 | | } |
51 | | |
52 | | pub trait LenEntry: 'static { |
53 | | /// Length of referenced data. |
54 | | fn len(&self) -> u64; |
55 | | |
56 | | /// Returns `true` if `self` has zero length. |
57 | | fn is_empty(&self) -> bool; |
58 | | |
59 | | /// This will be called when object is removed from map. |
60 | | /// Note: There may still be a reference to it held somewhere else, which |
61 | | /// is why it can't be mutable. This is a good place to mark the item |
62 | | /// to be deleted and then in the Drop call actually do the deleting. |
63 | | /// This will ensure nowhere else in the program still holds a reference |
64 | | /// to this object. |
65 | | /// You should not rely only on the Drop trait. Doing so might result in the |
66 | | /// program safely shutting down and calling the Drop method on each object, |
67 | | /// which if you are deleting items you may not want to do. |
68 | | /// It is undefined behavior to have `unref()` called more than once. |
69 | | /// Runs outside the map lock. Another task may insert a replacement for |
70 | | /// the same key before this call finishes; cleanup must only affect the |
71 | | /// removed entry's resources. |
72 | | #[inline] |
73 | 403 | fn unref(&self) -> impl Future<Output = ()> + Send { |
74 | 403 | core::future::ready(()) |
75 | 403 | } |
76 | | } |
77 | | |
78 | | impl<T: LenEntry + Send + Sync> LenEntry for Arc<T> { |
79 | | #[inline] |
80 | 5.09k | fn len(&self) -> u64 { |
81 | 5.09k | T::len(self.as_ref()) |
82 | 5.09k | } |
83 | | |
84 | | #[inline] |
85 | 0 | fn is_empty(&self) -> bool { |
86 | 0 | T::is_empty(self.as_ref()) |
87 | 0 | } |
88 | | |
89 | | #[inline] |
90 | 44 | async fn unref(&self) { |
91 | 44 | self.as_ref().unref().await; |
92 | 44 | } |
93 | | } |
94 | | |
95 | | // Callback to be called when the EvictingMap removes an item |
96 | | // either via eviction or direct deletion. This will be called with |
97 | | // whatever key type the EvictingMap uses. |
98 | | pub trait RemoveItemCallback<Q>: Debug + Send + Sync { |
99 | | fn callback(&self, store_key: &Q) -> Pin<Box<dyn Future<Output = ()> + Send>>; |
100 | | } |
101 | | |
102 | | #[derive(Debug, MetricsComponent)] |
103 | | struct State< |
104 | | K: Ord + Hash + Eq + Clone + Debug + Send + Borrow<Q>, |
105 | | Q: Ord + Hash + Eq + Debug, |
106 | | T: LenEntry + Debug + Send, |
107 | | C: RemoveItemCallback<Q>, |
108 | | > { |
109 | | lru: LruCache<K, EvictionItem<T>>, |
110 | | btree: Option<BTreeSet<K>>, |
111 | | /// A mirror of `lru` that contains only currently unleased keys. |
112 | | /// Keeping this index in LRU order lets eviction visit candidates without |
113 | | /// scanning entries that an active operation has pinned. |
114 | | evictable_lru: LruCache<K, ()>, |
115 | | /// Reference counts for keys that must not be evicted while an active |
116 | | /// operation is using them. A key can be leased before it is inserted, |
117 | | /// which closes the admission-to-insert race for cache populations. |
118 | | leases: HashMap<K, u32>, |
119 | | #[metric(help = "Total size of all items in the store")] |
120 | | sum_store_size: u64, |
121 | | #[metric(help = "Number of items currently leased and not evictable")] |
122 | | leased_items: u64, |
123 | | #[metric(help = "Number of bytes currently leased and not evictable")] |
124 | | leased_bytes: u64, |
125 | | |
126 | | #[metric(help = "Number of bytes evicted from the store")] |
127 | | evicted_bytes: Counter, |
128 | | #[metric(help = "Number of items evicted from the store")] |
129 | | evicted_items: CounterWithTime, |
130 | | #[metric(help = "Number of bytes replaced in the store")] |
131 | | replaced_bytes: Counter, |
132 | | #[metric(help = "Number of items replaced in the store")] |
133 | | replaced_items: CounterWithTime, |
134 | | #[metric(help = "Number of bytes inserted into the store since it was created")] |
135 | | lifetime_inserted_bytes: Counter, |
136 | | |
137 | | _key_type: PhantomData<Q>, |
138 | | remove_callbacks: Vec<C>, |
139 | | /// Size and entry-count changes waiting to be reported as `cache.size` and |
140 | | /// `cache.entries`. Recording to `OpenTelemetry` costs far more than the |
141 | | /// plain atomic counters beside it, so the mutation paths only do integer |
142 | | /// arithmetic here and the totals are flushed once the lock is released. |
143 | | /// A flush that is missed only delays the next update, it never loses a |
144 | | /// change. |
145 | | pending_size_delta: i64, |
146 | | pending_entries_delta: i64, |
147 | | } |
148 | | |
149 | | type RemoveFuture = Pin<Box<dyn Future<Output = ()> + Send>>; |
150 | | |
151 | | impl< |
152 | | K: Ord + Hash + Eq + Clone + Debug + Send + Sync + Borrow<Q>, |
153 | | Q: Ord + Hash + Eq + Debug + Sync, |
154 | | T: LenEntry + Debug + Sync + Send, |
155 | | C: RemoveItemCallback<Q>, |
156 | | > State<K, Q, T, C> |
157 | | { |
158 | 46.1k | fn is_leased(&self, key: &Q) -> bool { |
159 | 46.1k | self.leases.contains_key(key) |
160 | 46.1k | } |
161 | | |
162 | | /// Keep candidate recency aligned with a normal read from `lru`. |
163 | | /// Returns whether the key is leased so callers do not repeat the lookup. |
164 | 26.1k | fn touch_evictable(&mut self, key: &Q) -> bool { |
165 | 26.1k | if self.is_leased(key) { |
166 | 23 | return true; |
167 | 26.1k | } |
168 | 26.1k | self.evictable_lru.get(key); |
169 | 26.1k | false |
170 | 26.1k | } |
171 | | |
172 | | /// Add a newly written key to the candidate index unless it is reserved |
173 | | /// by a lease that was acquired before insertion. |
174 | 9.73k | fn insert_evictable(&mut self, key: K) { |
175 | 9.73k | if !self.is_leased(key.borrow()) { |
176 | 9.72k | self.evictable_lru.put(key, ()); |
177 | 9.72k | }10 |
178 | 9.73k | } |
179 | | |
180 | 13 | fn lease(&mut self, key: K) { |
181 | 13 | if let Some(lease_count2 ) = self.leases.get_mut(key.borrow()) { |
182 | 2 | *lease_count = lease_count.checked_add(1).expect("lease count overflow"); |
183 | 2 | return; |
184 | 11 | } |
185 | | |
186 | 11 | self.evictable_lru.pop(key.borrow()); |
187 | 11 | if let Some(entry1 ) = self.lru.peek(key.borrow()) { |
188 | 1 | self.leased_bytes += entry.data.len(); |
189 | 10 | } |
190 | 11 | self.leased_items += 1; |
191 | 11 | self.leases.insert(key, 1); |
192 | 13 | } |
193 | | |
194 | | /// Release one reference and return the owned key when the lease ends. |
195 | 11 | fn release_lease(&mut self, key: &Q) -> Option<K> { |
196 | 11 | let lease_count = self.leases.get_mut(key)?0 ; |
197 | 11 | if *lease_count > 1 { |
198 | 2 | *lease_count -= 1; |
199 | 2 | return None; |
200 | 9 | } |
201 | | |
202 | 9 | let leased_key = self |
203 | 9 | .leases |
204 | 9 | .remove_entry(key) |
205 | 9 | .expect("Lease must exist when its final reference is released") |
206 | 9 | .0; |
207 | 9 | self.leased_items -= 1; |
208 | 9 | if let Some(entry8 ) = self.lru.peek(leased_key.borrow()) { |
209 | 8 | self.leased_bytes -= entry.data.len(); |
210 | 8 | }1 |
211 | 9 | Some(leased_key) |
212 | 11 | } |
213 | | |
214 | | /// Re-enter a resident key as the newest eviction candidate. Its age is |
215 | | /// intentionally preserved, so a long lease does not extend the TTL. |
216 | 9 | fn reinsert_evictable(&mut self, key: K) { |
217 | 9 | if self.lru.peek(key.borrow()).is_some() { |
218 | 8 | self.evictable_lru.put(key, ()); |
219 | 8 | }1 |
220 | 9 | } |
221 | | |
222 | 19.5k | fn peek_evictable(&self) -> Option<&EvictionItem<T>> { |
223 | 19.5k | let (key19.5k , ()) = self.evictable_lru.peek_lru()?19 ; |
224 | 19.5k | Some( |
225 | 19.5k | self.lru |
226 | 19.5k | .peek(key.borrow()) |
227 | 19.5k | .expect("Evictable LRU key must be resident in the main LRU"), |
228 | 19.5k | ) |
229 | 19.5k | } |
230 | | |
231 | 42 | fn pop_evictable(&mut self) -> Option<(K, EvictionItem<T>)> { |
232 | 42 | let (candidate_key, ()) = self.evictable_lru.pop_lru()?0 ; |
233 | 42 | let (key, entry) = self |
234 | 42 | .lru |
235 | 42 | .pop_entry(candidate_key.borrow()) |
236 | 42 | .expect("Evictable LRU key must be resident in the main LRU"); |
237 | 42 | Some((key, entry)) |
238 | 42 | } |
239 | | |
240 | | /// Removes an item from the cache and returns the data for deferred cleanup. |
241 | | /// The caller is responsible for calling `unref()` on the returned data outside of the lock. |
242 | | #[must_use] |
243 | 443 | fn remove( |
244 | 443 | &mut self, |
245 | 443 | key: &Q, |
246 | 443 | eviction_item: &EvictionItem<T>, |
247 | 443 | replaced: bool, |
248 | 443 | ) -> (T, Vec<RemoveFuture>) |
249 | 443 | where |
250 | 443 | T: Clone, |
251 | | { |
252 | 443 | if let Some(btree1 ) = &mut self.btree { |
253 | 1 | btree.remove(key); |
254 | 442 | } |
255 | | // Keep every auxiliary resident index in sync with the main LRU. |
256 | 443 | self.evictable_lru.pop(key); |
257 | 443 | if self.is_leased(key) { |
258 | 1 | self.leased_bytes -= eviction_item.data.len(); |
259 | 442 | } |
260 | 443 | self.sum_store_size -= eviction_item.data.len(); |
261 | 443 | self.pending_size_delta -= saturating_i64(eviction_item.data.len()); |
262 | 443 | self.pending_entries_delta -= 1; |
263 | 443 | if replaced { |
264 | 370 | self.replaced_items.inc(); |
265 | 370 | self.replaced_bytes.add(eviction_item.data.len()); |
266 | 370 | } else { |
267 | 73 | self.evicted_items.inc(); |
268 | 73 | self.evicted_bytes.add(eviction_item.data.len()); |
269 | 73 | } |
270 | | |
271 | 443 | let callbacks = self |
272 | 443 | .remove_callbacks |
273 | 443 | .iter() |
274 | 443 | .map(|callback| callback37 .callback37 (key37 )) |
275 | 443 | .collect(); |
276 | | |
277 | | // Return the data for deferred unref outside of lock |
278 | 443 | (eviction_item.data.clone(), callbacks) |
279 | 443 | } |
280 | | |
281 | | /// Inserts a new item into the cache. If the key already exists, the old item is returned |
282 | | /// for deferred cleanup. |
283 | | #[must_use] |
284 | 9.73k | fn put(&mut self, key: &K, eviction_item: EvictionItem<T>) -> Option<(T, Vec<RemoveFuture>)> |
285 | 9.73k | where |
286 | 9.73k | K: Clone, |
287 | 9.73k | T: Clone, |
288 | | { |
289 | 9.73k | let is_leased = self.is_leased(key.borrow()); |
290 | 9.73k | let new_item_size = eviction_item.data.len(); |
291 | 9.73k | let replaced_item = self |
292 | 9.73k | .lru |
293 | 9.73k | .put(key.clone(), eviction_item) |
294 | 9.73k | .map(|old_item| self370 .remove370 (key.borrow()370 , &old_item370 , true)); |
295 | | |
296 | 9.73k | if is_leased { |
297 | 10 | self.leased_bytes += new_item_size; |
298 | 9.72k | } |
299 | | // `remove()` drops the old key from both indexes. Reinsert it after |
300 | | // replacement so an unleased write is MRU in both LRU indexes. |
301 | 9.73k | self.insert_evictable(key.clone()); |
302 | 9.73k | if let Some(btree2 ) = &mut self.btree { |
303 | 2 | btree.insert(key.clone()); |
304 | 9.73k | } |
305 | | |
306 | 9.73k | replaced_item |
307 | 9.73k | } |
308 | | |
309 | 138 | fn add_remove_callback(&mut self, callback: C) { |
310 | 138 | self.remove_callbacks.push(callback); |
311 | 138 | } |
312 | | } |
313 | | |
314 | | #[derive(Debug, Clone, Copy)] |
315 | | pub struct NoopRemove; |
316 | | |
317 | | impl<Q> RemoveItemCallback<Q> for NoopRemove { |
318 | 0 | fn callback(&self, _store_key: &Q) -> Pin<Box<dyn Future<Output = ()> + Send>> { |
319 | 0 | Box::pin(async {}) |
320 | 0 | } |
321 | | } |
322 | | |
323 | | #[derive(Debug, MetricsComponent)] |
324 | | pub struct EvictingMap< |
325 | | K: Ord + Hash + Eq + Clone + Debug + Send + Borrow<Q>, |
326 | | Q: Ord + Hash + Eq + Debug, |
327 | | T: LenEntry + Debug + Send, |
328 | | I: InstantWrapper, |
329 | | C: RemoveItemCallback<Q> = NoopRemove, |
330 | | > { |
331 | | #[metric] |
332 | | state: Mutex<State<K, Q, T, C>>, |
333 | | anchor_time: I, |
334 | | #[metric(help = "Maximum size of the store in bytes")] |
335 | | max_bytes: u64, |
336 | | #[metric(help = "Number of bytes to evict when the store is full")] |
337 | | evict_bytes: u64, |
338 | | #[metric(help = "Maximum number of seconds to keep an item in the store")] |
339 | | max_seconds: i32, |
340 | | #[metric(help = "Maximum number of items to keep in the store")] |
341 | | max_count: u64, |
342 | | /// Attributes to report `cache.size` and `cache.entries` under. Unset until |
343 | | /// a `cache_metrics` wrapper enables it, which is what keeps those two |
344 | | /// instruments off by default. |
345 | | cache_size_attrs: OnceLock<Vec<KeyValue>>, |
346 | | } |
347 | | |
348 | | // debugging helper used mostly to get a snapshot of what eviction threshold might be causing issues |
349 | | #[derive(Debug, Copy, Clone)] |
350 | | pub struct EvictionSnapshot { |
351 | | max_bytes: u64, |
352 | | current_bytes: u64, |
353 | | max_items: u64, |
354 | | current_items: usize, |
355 | | max_seconds: i32, |
356 | | } |
357 | | |
358 | | impl Display for EvictionSnapshot { |
359 | 4 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
360 | 4 | if self.max_bytes != 0 { |
361 | 2 | write!( |
362 | 2 | f, |
363 | | "Bytes: {} of {} ({:.3}%); ", |
364 | | self.current_bytes, |
365 | | self.max_bytes, |
366 | 2 | (self.current_bytes as f64 * 100.0) / (self.max_bytes as f64) |
367 | 0 | )?; |
368 | | } else { |
369 | 2 | write!(f, "Bytes: {} of unlimited; ", self.current_bytes)?0 ; |
370 | | } |
371 | 4 | if self.max_items != 0 { |
372 | 2 | write!( |
373 | 2 | f, |
374 | | "Items: {} of {} ({:.3}%); ", |
375 | | self.current_items, |
376 | | self.max_items, |
377 | 2 | (self.current_items as f64 * 100.0) / (self.max_items as f64) |
378 | 0 | )?; |
379 | | } else { |
380 | 2 | write!(f, "Items: {} of unlimited; ", self.current_items)?0 ; |
381 | | } |
382 | 4 | if self.max_seconds > 0 { |
383 | 1 | write!(f, "Timeout: {}s", self.max_seconds)?0 ; |
384 | | } else { |
385 | 3 | write!(f, "Timeout: unlimited")?0 ; |
386 | | } |
387 | 4 | Ok(()) |
388 | 4 | } |
389 | | } |
390 | | |
391 | | impl<K, Q, T, I, C> EvictingMap<K, Q, T, I, C> |
392 | | where |
393 | | K: Ord + Hash + Eq + Clone + Debug + Send + Sync + Borrow<Q>, |
394 | | Q: Ord + Hash + Eq + Debug + Sync, |
395 | | T: LenEntry + Debug + Clone + Send + Sync, |
396 | | I: InstantWrapper, |
397 | | C: RemoveItemCallback<Q>, |
398 | | { |
399 | 694 | pub fn new(config: &EvictionPolicy, anchor_time: I) -> Self { |
400 | 694 | Self { |
401 | 694 | // We use unbounded because if we use the bounded version we can't call the delete |
402 | 694 | // function on the LenEntry properly. |
403 | 694 | state: Mutex::new(State { |
404 | 694 | lru: LruCache::unbounded(), |
405 | 694 | btree: None, |
406 | 694 | evictable_lru: LruCache::unbounded(), |
407 | 694 | leases: HashMap::new(), |
408 | 694 | sum_store_size: 0, |
409 | 694 | leased_items: 0, |
410 | 694 | leased_bytes: 0, |
411 | 694 | evicted_bytes: Counter::default(), |
412 | 694 | evicted_items: CounterWithTime::default(), |
413 | 694 | replaced_bytes: Counter::default(), |
414 | 694 | replaced_items: CounterWithTime::default(), |
415 | 694 | lifetime_inserted_bytes: Counter::default(), |
416 | 694 | _key_type: PhantomData, |
417 | 694 | remove_callbacks: Vec::new(), |
418 | 694 | pending_size_delta: 0, |
419 | 694 | pending_entries_delta: 0, |
420 | 694 | }), |
421 | 694 | anchor_time, |
422 | 694 | max_bytes: config.max_bytes as u64, |
423 | 694 | evict_bytes: config.evict_bytes as u64, |
424 | 694 | max_seconds: config.max_seconds.try_into().unwrap_or(i32::MAX), |
425 | 694 | max_count: config.max_count, |
426 | 694 | cache_size_attrs: OnceLock::new(), |
427 | 694 | } |
428 | 694 | } |
429 | | |
430 | | /// Reports this map's size and entry count as `cache.size` and |
431 | | /// `cache.entries` under `attrs`, starting from what it already holds. |
432 | | /// Only the first call takes effect, so a map is never counted twice. |
433 | 4 | pub fn enable_cache_size_metrics(&self, attrs: Vec<KeyValue>) { |
434 | 4 | if self.cache_size_attrs.set(attrs).is_err() { |
435 | 1 | return; |
436 | 3 | } |
437 | | // Nothing has been reported yet, so the first flush is the map's |
438 | | // current totals. Overwrite rather than add: the mutations that filled |
439 | | // the map accumulated deltas too, and counting those as well would |
440 | | // report everything already resident twice. |
441 | 3 | { |
442 | 3 | let mut state = self.state.lock(); |
443 | 3 | state.pending_size_delta = saturating_i64(state.sum_store_size); |
444 | 3 | state.pending_entries_delta = i64::try_from(state.lru.len()).unwrap_or(i64::MAX); |
445 | 3 | } |
446 | 3 | self.flush_cache_size_metrics(); |
447 | 4 | } |
448 | | |
449 | | /// Records the size and entry-count changes accumulated under the lock. |
450 | | /// |
451 | | /// Call after releasing the state lock: reporting to `OpenTelemetry` is |
452 | | /// much costlier than the counters updated inside it, and the state lock is |
453 | | /// contended across the whole store. |
454 | 24.4k | fn flush_cache_size_metrics(&self) { |
455 | 24.4k | let Some(attrs10 ) = self.cache_size_attrs.get() else { |
456 | 24.3k | return; |
457 | | }; |
458 | 10 | let (size_delta, entries_delta) = { |
459 | 10 | let mut state = self.state.lock(); |
460 | 10 | ( |
461 | 10 | core::mem::take(&mut state.pending_size_delta), |
462 | 10 | core::mem::take(&mut state.pending_entries_delta), |
463 | 10 | ) |
464 | 10 | }; |
465 | 10 | if size_delta == 0 && entries_delta == 04 { |
466 | 4 | return; |
467 | 6 | } |
468 | 6 | record_cache_entries_delta(size_delta, entries_delta, attrs); |
469 | 24.4k | } |
470 | | |
471 | | // Only used for tests |
472 | 1 | pub fn enable_filtering(&self) { |
473 | 1 | let mut state = self.state.lock(); |
474 | 1 | if state.btree.is_none() { |
475 | 1 | Self::rebuild_btree_index(&mut state); |
476 | 1 | }0 |
477 | 1 | } |
478 | | |
479 | 3 | fn rebuild_btree_index(state: &mut State<K, Q, T, C>) { |
480 | 3 | state.btree = Some(state.lru.iter().map(|(k, _)| k).cloned().collect()); |
481 | 3 | } |
482 | | |
483 | | /// Run the `handler` function on each key-value pair that matches the `prefix_range` |
484 | | /// and return the number of items that were processed. |
485 | | /// The `handler` function should return `true` to continue processing the next item |
486 | | /// or `false` to stop processing. |
487 | 12 | pub fn range<F>(&self, prefix_range: impl RangeBounds<Q> + Send, mut handler: F) -> u64 |
488 | 12 | where |
489 | 12 | F: FnMut(&K, &T) -> bool + Send, |
490 | 12 | K: Ord, |
491 | | { |
492 | 12 | let mut state = self.state.lock(); |
493 | 12 | let btree = if let Some(ref btree10 ) = state.btree { |
494 | 10 | btree |
495 | | } else { |
496 | 2 | Self::rebuild_btree_index(&mut state); |
497 | 2 | state.btree.as_ref().unwrap() |
498 | | }; |
499 | 12 | let mut continue_count = 0; |
500 | 23 | for key in btree12 .range12 (prefix_range12 ) { |
501 | 23 | let value = &state.lru.peek(key.borrow()).unwrap().data; |
502 | 23 | let should_continue = handler(key, value); |
503 | 23 | if !should_continue { |
504 | 0 | break; |
505 | 23 | } |
506 | 23 | continue_count += 1; |
507 | | } |
508 | 12 | continue_count |
509 | 12 | } |
510 | | |
511 | | /// Returns the number of key-value pairs that are currently in the the cache. |
512 | | /// Function is not for production code paths. |
513 | 31 | pub fn len_for_test(&self) -> usize { |
514 | 31 | self.state.lock().lru.len() |
515 | 31 | } |
516 | | |
517 | 22.7k | fn should_evict( |
518 | 22.7k | &self, |
519 | 22.7k | lru_len: usize, |
520 | 22.7k | peek_entry: &EvictionItem<T>, |
521 | 22.7k | sum_store_size: u64, |
522 | 22.7k | max_bytes: u64, |
523 | 22.7k | ) -> bool { |
524 | 22.7k | let is_over_size = max_bytes != 0 && sum_store_size >= max_bytes12.9k ; |
525 | | |
526 | 22.7k | let elapsed_seconds = self.elapsed_seconds(); |
527 | 22.7k | let evict_older_than_seconds = elapsed_seconds.saturating_sub(self.max_seconds); |
528 | 22.7k | let old_item_exists = |
529 | 22.7k | self.max_seconds != 0 && peek_entry.seconds_since_anchor < evict_older_than_seconds147 ; |
530 | | |
531 | 22.7k | let is_over_count = |
532 | 22.7k | self.max_count != 0 && u64::try_from228 (lru_len228 ).unwrap_or(u64::MAX) > self.max_count; |
533 | | |
534 | 22.7k | is_over_size || old_item_exists22.7k || is_over_count22.7k |
535 | 22.7k | } |
536 | | |
537 | 45.1k | fn elapsed_seconds(&self) -> i32 { |
538 | 45.1k | i32::try_from(self.anchor_time.elapsed().as_secs()).unwrap_or(i32::MAX) |
539 | 45.1k | } |
540 | | |
541 | | // Gets a debugging snapshot of the state of the map. It's inevitably out of date by the time it gets sent to the user |
542 | | // but does provide a momentary glimpse into possible issues e.g. if current_bytes is close to max_bytes |
543 | 4 | pub fn get_snapshot(&self) -> EvictionSnapshot { |
544 | 4 | let state = self.state.lock(); |
545 | 4 | EvictionSnapshot { |
546 | 4 | max_bytes: self.max_bytes, |
547 | 4 | current_bytes: state.sum_store_size, |
548 | 4 | max_items: self.max_count, |
549 | 4 | current_items: state.lru.len(), |
550 | 4 | max_seconds: self.max_seconds, |
551 | 4 | } |
552 | 4 | } |
553 | | |
554 | | #[must_use] |
555 | 9.77k | fn evict_items(&self, state: &mut State<K, Q, T, C>) -> (Vec<T>, Vec<RemoveFuture>) { |
556 | 9.77k | let Some(first_entry9.76k ) = state.peek_evictable() else { |
557 | 9 | if !state.lru.is_empty() { |
558 | 9 | debug!( |
559 | 9 | resident_items = state.lru.len(), |
560 | 9 | evictable_items = state.evictable_lru.len(), |
561 | 9 | lease_keys = state.leases.len(), |
562 | | leased_items = state.leased_items, |
563 | | leased_bytes = state.leased_bytes, |
564 | | resident_bytes = state.sum_store_size, |
565 | | "Eviction requested, but every resident entry is leased", |
566 | | ); |
567 | 0 | } |
568 | 9 | return (Vec::new(), Vec::new()); |
569 | | }; |
570 | | |
571 | | // Preserve the configured low-watermark behavior: once pressure is |
572 | | // detected, keep evicting until `evict_bytes` is reclaimed. |
573 | 9.76k | let max_bytes = if self.max_bytes != 0 |
574 | 21 | && self.evict_bytes != 0 |
575 | 4 | && self.should_evict( |
576 | 4 | state.lru.len(), |
577 | 4 | first_entry, |
578 | 4 | state.sum_store_size, |
579 | 4 | self.max_bytes, |
580 | | ) { |
581 | 1 | self.max_bytes.saturating_sub(self.evict_bytes) |
582 | | } else { |
583 | 9.76k | self.max_bytes |
584 | | }; |
585 | | |
586 | 9.76k | let mut items_to_unref = Vec::new(); |
587 | 9.76k | let mut removal_futures = Vec::new(); |
588 | | |
589 | | // Pop the candidate index directly instead of collecting and cloning |
590 | | // all victim keys. Both indexes are protected by the same lock. |
591 | 9.80k | while let Some(entry9.79k ) = state.peek_evictable() { |
592 | 9.79k | if !self.should_evict(state.lru.len(), entry, state.sum_store_size, max_bytes) { |
593 | 9.75k | break; |
594 | 42 | } |
595 | | |
596 | 42 | let (key, eviction_item) = state |
597 | 42 | .pop_evictable() |
598 | 42 | .expect("Evictable LRU key disappeared while state was locked"); |
599 | 42 | debug!(?key, "Evicting"); |
600 | 42 | let (data, futures) = state.remove(key.borrow(), &eviction_item, false); |
601 | 42 | items_to_unref.push(data); |
602 | 42 | removal_futures.extend(futures); |
603 | | } |
604 | | |
605 | 9.76k | (items_to_unref, removal_futures) |
606 | 9.77k | } |
607 | | |
608 | | /// Return the size of a `key`, if not found `None` is returned. |
609 | 46 | pub async fn size_for_key(&self, key: &Q) -> Option<u64> { |
610 | 46 | let mut results = [None]; |
611 | 46 | self.sizes_for_keys([key], &mut results[..], false).await; |
612 | 46 | results[0] |
613 | 46 | } |
614 | | |
615 | | /// Return the sizes of a collection of `keys`. Expects `results` collection |
616 | | /// to be provided for storing the resulting key sizes. Each index value in |
617 | | /// `keys` maps directly to the size value for the key in `results`. |
618 | | /// If no key is found in the internal map, `None` is filled in its place. |
619 | | /// If `peek` is set to `true`, the items are not promoted to the front of the |
620 | | /// LRU cache. Note: peek may still evict, but won't promote. |
621 | 4.14k | pub async fn sizes_for_keys<It, R>(&self, keys: It, results: &mut [Option<u64>], peek: bool) |
622 | 4.14k | where |
623 | 4.14k | It: IntoIterator<Item = R> + Send, |
624 | 4.14k | // Note: It's not enough to have the inserts themselves be Send. The |
625 | 4.14k | // returned iterator should be Send as well. |
626 | 4.14k | <It as IntoIterator>::IntoIter: Send, |
627 | 4.14k | // This may look strange, but what we are doing is saying: |
628 | 4.14k | // * `K` must be able to borrow `Q` |
629 | 4.14k | // * `R` (the input stream item type) must also be able to borrow `Q` |
630 | 4.14k | // Note: That K and R do not need to be the same type, they just both need |
631 | 4.14k | // to be able to borrow a `Q`. |
632 | 4.14k | R: Borrow<Q> + Send, |
633 | 4.14k | { |
634 | 4.14k | let (removal_futures, data_to_unref) = { |
635 | 4.14k | let mut state = self.state.lock(); |
636 | | |
637 | 4.14k | let lru_len = state.lru.len(); |
638 | 4.14k | let mut data_to_unref = Vec::new(); |
639 | 4.14k | let mut removal_futures = Vec::new(); |
640 | 4.19k | for (key, result) in keys4.14k .into_iter4.14k ().zip4.14k (results4.14k .iter_mut4.14k ()) { |
641 | 4.19k | let is_leased = if peek { |
642 | 17 | state.is_leased(key.borrow()) |
643 | | } else { |
644 | 4.18k | state.touch_evictable(key.borrow()) |
645 | | }; |
646 | 4.19k | let maybe_entry = if peek { |
647 | 17 | state.lru.peek_mut(key.borrow()) |
648 | | } else { |
649 | 4.18k | state.lru.get_mut(key.borrow()) |
650 | | }; |
651 | 4.19k | match maybe_entry { |
652 | 2.46k | Some(entry) => { |
653 | | // Note: We need to check eviction because the item might be expired |
654 | | // based on the current time. In such case, we remove the item while |
655 | | // we are here. |
656 | 2.46k | if !is_leased && self2.45k .should_evict2.45k (lru_len2.45k , entry2.45k , 0, u64::MAX) { |
657 | 1 | *result = None; |
658 | 1 | if let Some((key, eviction_item)) = state.lru.pop_entry(key.borrow()) { |
659 | 1 | info!(?key, "Item expired, evicting"); |
660 | 1 | let (data, futures) = |
661 | 1 | state.remove(key.borrow(), &eviction_item, false); |
662 | | // Store data for later unref - we can't drop state here as we're still iterating |
663 | 1 | data_to_unref.push(data); |
664 | 1 | removal_futures.extend(futures); |
665 | 0 | } |
666 | | } else { |
667 | 2.46k | if !peek { |
668 | 2.45k | entry.seconds_since_anchor = self.elapsed_seconds(); |
669 | 2.45k | }4 |
670 | 2.46k | *result = Some(entry.data.len()); |
671 | | } |
672 | | } |
673 | 1.73k | None => *result = None, |
674 | | } |
675 | | } |
676 | 4.14k | (removal_futures, data_to_unref) |
677 | | }; |
678 | | |
679 | | // Perform the async callbacks outside of the lock |
680 | 4.14k | self.flush_cache_size_metrics(); |
681 | 4.14k | let mut callbacks: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
682 | 4.14k | while callbacks.next().await.is_some() {}0 |
683 | 4.14k | let mut callbacks: FuturesUnordered<_> = |
684 | 4.14k | data_to_unref.iter().map(LenEntry::unref).collect(); |
685 | 4.14k | while callbacks.next().await.is_some() {}1 |
686 | 4.14k | } |
687 | | |
688 | | /// Fires the registered remove callbacks for `key` without touching the map. |
689 | | /// A store uses this to invalidate downstream listeners (e.g. an |
690 | | /// `ExistenceCacheStore`) when a write is rejected outright and never |
691 | | /// inserted — mirroring the callbacks an insert-then-immediate-evict would |
692 | | /// otherwise have fired. It only *notifies*; it does not remove anything from |
693 | | /// the map (there is nothing to remove). A no-op when no callbacks are |
694 | | /// registered. |
695 | 3 | pub async fn fire_remove_callbacks(&self, key: &Q) { |
696 | 3 | let mut callbacks: FuturesUnordered<_> = { |
697 | 3 | let state = self.state.lock(); |
698 | 3 | state |
699 | 3 | .remove_callbacks |
700 | 3 | .iter() |
701 | 3 | .map(|callback| callback2 .callback2 (key2 )) |
702 | 3 | .collect() |
703 | | }; |
704 | 5 | while callbacks.next().await.is_some() {}2 |
705 | 3 | } |
706 | | |
707 | | /// Returns the value for `key` if present and not expired, refreshing |
708 | | /// its LRU/atime position. If the entry is present but TTL- or |
709 | | /// count-expired, it is reaped and `None` is returned. |
710 | | /// |
711 | | /// A read never cascades into other entries — only the queried key is |
712 | | /// ever touched. Global eviction (size/count overflow trim) runs on |
713 | | /// inserts; it is not driven by reads, since `sum_store_size` cannot |
714 | | /// grow without an insert. |
715 | 12.2k | pub async fn get(&self, key: &Q) -> Option<T> { |
716 | | // Lazily reap *only* the requested entry if it is itself expired; |
717 | | // leave the rest for inserts (which already run the global eviction |
718 | | // loop). |
719 | 10.4k | let (data, expired_data, removal_futures) = { |
720 | 12.2k | let mut state = self.state.lock(); |
721 | 12.2k | let lru_len = state.lru.len(); |
722 | 12.2k | let is_leased = state.touch_evictable(key.borrow()); |
723 | 12.2k | let entry10.4k = state.lru.get_mut(key.borrow())?1.77k ; |
724 | | // Pass `sum_store_size=0` and `max_bytes=u64::MAX` so we only |
725 | | // consult TTL / count predicates — never the global byte budget. |
726 | | // Mirrors the per-key reap path in `sizes_for_keys`. |
727 | 10.4k | if !is_leased && self10.4k .should_evict10.4k (lru_len10.4k , entry10.4k , 0, u64::MAX) { |
728 | 3 | let (popped_key, eviction_item) = state |
729 | 3 | .lru |
730 | 3 | .pop_entry(key.borrow()) |
731 | 3 | .expect("entry was just observed via get_mut"); |
732 | 3 | info!(?popped_key, "Item expired, evicting"); |
733 | 3 | let (data, futures) = state.remove(popped_key.borrow(), &eviction_item, false); |
734 | 3 | (None, Some(data), futures) |
735 | | } else { |
736 | 10.4k | entry.seconds_since_anchor = self.elapsed_seconds(); |
737 | 10.4k | (Some(entry.data.clone()), None, Vec::new()) |
738 | | } |
739 | | }; |
740 | | |
741 | | // Drain remove_callbacks and unref the reaped entry outside the lock. |
742 | 10.4k | self.flush_cache_size_metrics(); |
743 | 10.4k | let mut callbacks: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
744 | 10.4k | while callbacks.next().await.is_some() {}0 |
745 | 10.4k | if let Some(d3 ) = expired_data { |
746 | 3 | d.unref().await; |
747 | 10.4k | } |
748 | | |
749 | 10.4k | data |
750 | 12.2k | } |
751 | | |
752 | | /// Returns the replaced item if any. |
753 | 7.73k | pub async fn insert(&self, key: K, data: T) -> Option<T> |
754 | 7.73k | where |
755 | 7.73k | K: 'static, |
756 | 7.73k | { |
757 | 7.73k | self.insert_with_time(key, data, self.elapsed_seconds()) |
758 | 7.73k | .await |
759 | 7.73k | } |
760 | | |
761 | | /// Same as `insert()`, but allows for a conditional to be applied to the |
762 | | /// entry before insertion in an atomic fashion. |
763 | 1.76k | pub async fn insert_if<F>(&self, key: K, data: T, cond: F) -> (bool, Option<T>) |
764 | 1.76k | where |
765 | 1.76k | F: FnOnce(&T, &T) -> bool + Send, |
766 | 1.76k | { |
767 | 1.76k | self.insert_with_time_if(key, data, cond, self.elapsed_seconds()) |
768 | 1.76k | .await |
769 | 1.76k | } |
770 | | |
771 | | /// Returns the replaced item if any. |
772 | 7.73k | pub async fn insert_with_time(&self, key: K, data: T, seconds_since_anchor: i32) -> Option<T> { |
773 | 7.73k | self.insert_with_time_if(key, data, |_, _| true, seconds_since_anchor) |
774 | 7.73k | .await |
775 | | .1 |
776 | 7.73k | } |
777 | | |
778 | | /// Conditional insertion with an explicit timestamp. The predicate runs |
779 | | /// under the map lock; a rejected value is left for the caller to clean up. |
780 | 9.73k | pub async fn insert_with_time_if<F>( |
781 | 9.73k | &self, |
782 | 9.73k | key: K, |
783 | 9.73k | data: T, |
784 | 9.73k | cond: F, |
785 | 9.73k | seconds_since_anchor: i32, |
786 | 9.73k | ) -> (bool, Option<T>) |
787 | 9.73k | where |
788 | 9.73k | F: FnOnce(&T, &T) -> bool + Send, |
789 | 9.73k | { |
790 | 9.73k | let (items_to_unref, removal_futures) = { |
791 | 9.73k | let mut state = self.state.lock(); |
792 | | |
793 | 9.73k | state.touch_evictable(key.borrow()); |
794 | 9.73k | if let Some(old_entry372 ) = state.lru.get(key.borrow()) |
795 | 372 | && !cond(&old_entry.data, &data) |
796 | | { |
797 | 2 | return (false, None); |
798 | 9.73k | } |
799 | | |
800 | 9.73k | self.inner_insert_many(&mut state, [(key, data)], seconds_since_anchor) |
801 | | }; |
802 | | |
803 | 9.73k | self.flush_cache_size_metrics(); |
804 | 9.73k | let mut futures: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
805 | 9.76k | while futures.next().await.is_some() {}27 |
806 | | |
807 | | // Unref items outside of lock |
808 | 9.73k | let futures: FuturesUnordered<_> = items_to_unref |
809 | 9.73k | .into_iter() |
810 | 9.73k | .map(|item| async move {408 |
811 | 408 | item.unref().await; |
812 | 408 | item |
813 | 816 | }) |
814 | 9.73k | .collect(); |
815 | 9.73k | (true, futures.collect::<Vec<_>>().await.into_iter().next()) |
816 | 9.73k | } |
817 | | |
818 | | /// Same as `insert()`, but optimized for multiple inserts. |
819 | | /// Returns the replaced items if any. |
820 | 11 | pub async fn insert_many<It>(&self, inserts: It) -> Vec<T> |
821 | 11 | where |
822 | 11 | It: IntoIterator<Item = (K, T)> + Send, |
823 | 11 | // Note: It's not enough to have the inserts themselves be Send. The |
824 | 11 | // returned iterator should be Send as well. |
825 | 11 | <It as IntoIterator>::IntoIter: Send, |
826 | 11 | K: 'static, |
827 | 11 | { |
828 | 11 | let mut inserts = inserts.into_iter().peekable(); |
829 | | // Shortcut for cases where there are no inserts, so we don't need to lock. |
830 | 11 | if inserts.peek().is_none() { |
831 | 5 | return Vec::new(); |
832 | 6 | } |
833 | | |
834 | 6 | let (items_to_unref, removal_futures) = { |
835 | 6 | let mut state = self.state.lock(); |
836 | 6 | self.inner_insert_many(&mut state, inserts, self.elapsed_seconds()) |
837 | 6 | }; |
838 | | |
839 | 6 | self.flush_cache_size_metrics(); |
840 | 6 | let mut futures: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
841 | 6 | while futures.next().await.is_some() {}0 |
842 | | |
843 | | // Unref items outside of lock |
844 | 6 | items_to_unref |
845 | 6 | .into_iter() |
846 | 6 | .map(|item| async move {0 |
847 | 0 | item.unref().await; |
848 | 0 | item |
849 | 0 | }) |
850 | 6 | .collect::<FuturesUnordered<_>>() |
851 | 6 | .collect::<Vec<_>>() |
852 | 6 | .await |
853 | 11 | } |
854 | | |
855 | | /// Lease a key so automatic size, count, and expiry eviction cannot remove it. |
856 | | /// |
857 | | /// Leasing before the value is inserted is intentional: an action can |
858 | | /// reserve a digest before populating the fast tier, so insertion and |
859 | | /// eviction cannot race with the start of input materialization. Explicit |
860 | | /// `remove` and `remove_if` calls still remove leased entries; those paths |
861 | | /// are used for deliberate deletion and filesystem self-healing. |
862 | 13 | pub fn lease_key(&self, key: K) { |
863 | 13 | self.state.lock().lease(key); |
864 | 13 | } |
865 | | |
866 | | /// Release one lease and trim any entries that were retained while the |
867 | | /// lease was active. |
868 | 5 | pub async fn release_key(&self, key: &Q) { |
869 | 5 | self.release_keys([key]).await; |
870 | 5 | } |
871 | | |
872 | | /// Release a batch of leases and trim retained entries once. |
873 | | /// |
874 | | /// Action input leases commonly contain thousands of digests. Batching |
875 | | /// avoids rescanning the LRU and running deferred cleanup once per digest |
876 | | /// during action teardown. |
877 | 8 | pub async fn release_keys<It, R>(&self, keys: It) |
878 | 8 | where |
879 | 8 | It: IntoIterator<Item = R> + Send, |
880 | 8 | <It as IntoIterator>::IntoIter: Send, |
881 | 8 | R: Borrow<Q> + Send, |
882 | 8 | { |
883 | 8 | let (items_to_unref, removal_futures) = { |
884 | 8 | let mut state = self.state.lock(); |
885 | 8 | let mut released_any = false; |
886 | 11 | for key in keys8 { |
887 | 11 | if let Some(leased_key9 ) = state.release_lease(key.borrow()) { |
888 | 9 | // A released key re-enters as MRU, giving an actively used |
889 | 9 | // input a fair chance to remain cached after its action. |
890 | 9 | state.reinsert_evictable(leased_key); |
891 | 9 | released_any = true; |
892 | 9 | }2 |
893 | | } |
894 | 8 | if released_any { |
895 | 7 | self.evict_items(&mut state) |
896 | | } else { |
897 | 1 | (Vec::new(), Vec::new()) |
898 | | } |
899 | | }; |
900 | | |
901 | 8 | self.flush_cache_size_metrics(); |
902 | 8 | let mut futures: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
903 | 9 | while futures.next().await.is_some() {}1 |
904 | | |
905 | 8 | let mut futures: FuturesUnordered<_> = items_to_unref.iter().map(LenEntry::unref).collect(); |
906 | 11 | while futures.next().await.is_some() {}3 |
907 | 8 | } |
908 | | |
909 | 9.73k | fn inner_insert_many<It>( |
910 | 9.73k | &self, |
911 | 9.73k | state: &mut State<K, Q, T, C>, |
912 | 9.73k | inserts: It, |
913 | 9.73k | seconds_since_anchor: i32, |
914 | 9.73k | ) -> (Vec<T>, Vec<RemoveFuture>) |
915 | 9.73k | where |
916 | 9.73k | It: IntoIterator<Item = (K, T)> + Send, |
917 | 9.73k | // Note: It's not enough to have the inserts themselves be Send. The |
918 | 9.73k | // returned iterator should be Send as well. |
919 | 9.73k | <It as IntoIterator>::IntoIter: Send, |
920 | | { |
921 | 9.73k | let mut replaced_items = Vec::new(); |
922 | 9.73k | let mut removal_futures = Vec::new(); |
923 | 9.73k | for (key, data) in inserts { |
924 | 9.73k | let new_item_size = data.len(); |
925 | 9.73k | let eviction_item = EvictionItem { |
926 | 9.73k | seconds_since_anchor, |
927 | 9.73k | data, |
928 | 9.73k | }; |
929 | | |
930 | 9.73k | if let Some((old_item370 , futures370 )) = state.put(&key, eviction_item) { |
931 | 370 | removal_futures.extend(futures); |
932 | 370 | debug!(?key, "Evicting old item"); |
933 | 370 | replaced_items.push(old_item); |
934 | 9.36k | } |
935 | 9.73k | state.sum_store_size += new_item_size; |
936 | 9.73k | state.lifetime_inserted_bytes.add(new_item_size); |
937 | 9.73k | state.pending_size_delta += saturating_i64(new_item_size); |
938 | 9.73k | state.pending_entries_delta += 1; |
939 | | } |
940 | | |
941 | | // Perform eviction after all insertions |
942 | 9.73k | let (items_to_unref, futures) = self.evict_items(state); |
943 | 9.73k | removal_futures.extend(futures); |
944 | | |
945 | | // Note: We cannot drop the state lock here since we're borrowing it, |
946 | | // but the caller will handle unreffing these items after releasing the lock |
947 | 9.73k | replaced_items.extend(items_to_unref); |
948 | | |
949 | 9.73k | (replaced_items, removal_futures) |
950 | 9.73k | } |
951 | | |
952 | 20 | pub async fn remove(&self, key: &Q) -> bool { |
953 | 20 | let (items_to_unref, removed_item, removal_futures) = { |
954 | 20 | let mut state = self.state.lock(); |
955 | | |
956 | | // First perform eviction |
957 | 20 | let (evicted_items, mut removal_futures) = self.evict_items(&mut *state); |
958 | | |
959 | | // Then try to remove the requested item |
960 | 20 | let removed = if let Some(entry19 ) = state.lru.pop(key.borrow()) { |
961 | 19 | let (removed_item, more_removal_futures) = state.remove(key, &entry, false); |
962 | 19 | removal_futures.extend(more_removal_futures); |
963 | 19 | Some(removed_item) |
964 | | } else { |
965 | 1 | None |
966 | | }; |
967 | | |
968 | 20 | (evicted_items, removed, removal_futures) |
969 | | }; |
970 | | |
971 | 20 | self.flush_cache_size_metrics(); |
972 | 20 | let mut callbacks: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
973 | 21 | while callbacks.next().await.is_some() {}1 |
974 | | |
975 | | // Unref evicted items outside of lock |
976 | 20 | let mut callbacks: FuturesUnordered<_> = |
977 | 20 | items_to_unref.iter().map(LenEntry::unref).collect(); |
978 | 21 | while callbacks.next().await.is_some() {}1 |
979 | | |
980 | | // Unref removed item if any |
981 | 20 | if let Some(item19 ) = removed_item { |
982 | 19 | debug!(?key, "Evicting (direct remove)"); |
983 | 19 | item.unref().await; |
984 | 19 | return true; |
985 | 1 | } |
986 | | |
987 | 1 | false |
988 | 20 | } |
989 | | |
990 | | /// Same as `remove()`, but allows for a conditional to be applied to the |
991 | | /// entry before removal in an atomic fashion. |
992 | 10 | pub async fn remove_if<F>(&self, key: &Q, cond: F) -> bool |
993 | 10 | where |
994 | 10 | F: FnOnce(&T) -> bool + Send, |
995 | 10 | { |
996 | 8 | let (evicted_items, removal_futures, removed_item) = { |
997 | 10 | let mut state = self.state.lock(); |
998 | 10 | state.touch_evictable(key.borrow()); |
999 | 10 | let Some(entry) = state.lru.get(key.borrow()) else { |
1000 | 0 | return false; |
1001 | | }; |
1002 | 10 | if !cond(&entry.data) { |
1003 | 2 | return false; |
1004 | 8 | } |
1005 | | |
1006 | | // First perform eviction |
1007 | 8 | let (evicted_items, mut removal_futures) = self.evict_items(&mut state); |
1008 | | |
1009 | | // Then try to remove the requested item |
1010 | 8 | let removed_item = if let Some(entry) = state.lru.pop(key.borrow()) { |
1011 | 8 | let (item, more_removal_futures) = state.remove(key, &entry, false); |
1012 | 8 | removal_futures.extend(more_removal_futures); |
1013 | 8 | Some(item) |
1014 | | } else { |
1015 | 0 | None |
1016 | | }; |
1017 | | |
1018 | 8 | (evicted_items, removal_futures, removed_item) |
1019 | | }; |
1020 | | |
1021 | | // Perform the async callbacks outside of the lock |
1022 | 8 | self.flush_cache_size_metrics(); |
1023 | 8 | let mut removal_futures: FuturesUnordered<_> = removal_futures.into_iter().collect(); |
1024 | 16 | while removal_futures.next().await.is_some() {}8 |
1025 | | |
1026 | | // Unref evicted items |
1027 | 8 | let mut callbacks: FuturesUnordered<_> = |
1028 | 8 | evicted_items.iter().map(LenEntry::unref).collect(); |
1029 | 8 | while callbacks.next().await.is_some() {}0 |
1030 | | |
1031 | | // Unref removed item if any |
1032 | 8 | if let Some(item) = removed_item { |
1033 | 8 | debug!(?key, "Evicting (conditional remove)"); |
1034 | 8 | item.unref().await; |
1035 | 8 | true |
1036 | | } else { |
1037 | 0 | false |
1038 | | } |
1039 | 10 | } |
1040 | | |
1041 | 138 | pub fn add_remove_callback(&self, callback: C) { |
1042 | 138 | self.state.lock().add_remove_callback(callback); |
1043 | 138 | } |
1044 | | } |