Coverage Report

Created: 2026-09-18 20:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}