Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/existence_cache_store.rs
Line
Count
Source
1
// Copyright 2024 Trace Machina, Inc. 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::pin::Pin;
16
use std::borrow::Cow;
17
use std::sync::{Arc, Weak};
18
use std::time::SystemTime;
19
20
use async_trait::async_trait;
21
use futures::StreamExt;
22
use futures::stream::FuturesUnordered;
23
use nativelink_config::stores::{EvictionPolicy, ExistenceCacheSpec};
24
use nativelink_error::{Code, Error, ResultExt, error_if};
25
use nativelink_metric::MetricsComponent;
26
use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf};
27
use nativelink_util::common::DigestInfo;
28
use nativelink_util::evicting_map::{EvictingMap, LenEntry};
29
use nativelink_util::health_utils::{HealthStatus, HealthStatusIndicator};
30
use nativelink_util::instant_wrapper::InstantWrapper;
31
use nativelink_util::store_trait::{
32
    RemoveCallback, RemoveItemCallback, Store, StoreDriver, StoreKey, StoreLike, UploadSizeInfo,
33
};
34
use parking_lot::Mutex;
35
use tracing::{debug, trace};
36
37
#[derive(Clone, Debug)]
38
struct ExistenceItem(u64);
39
40
impl LenEntry for ExistenceItem {
41
    #[inline]
42
76
    fn len(&self) -> u64 {
43
76
        self.0
44
76
    }
45
46
    #[inline]
47
0
    fn is_empty(&self) -> bool {
48
0
        false
49
0
    }
50
}
51
52
#[derive(Debug, MetricsComponent)]
53
pub struct ExistenceCacheStore<I: InstantWrapper> {
54
    #[metric(group = "inner_store")]
55
    inner_store: Store,
56
    existence_cache: EvictingMap<DigestInfo, DigestInfo, ExistenceItem, I>,
57
58
    // We need to pause them temporarily when inserting into the inner store
59
    // as if it immediately expires them, we should only apply the remove callbacks
60
    // afterwards. If this is None, we're not pausing; if it's Some it's the location to
61
    // store them in temporarily
62
    pause_remove_callbacks: Mutex<Option<Vec<StoreKey<'static>>>>,
63
}
64
65
impl ExistenceCacheStore<SystemTime> {
66
11
    pub fn new(spec: &ExistenceCacheSpec, inner_store: Store) -> Arc<Self> {
67
11
        Self::new_with_time(spec, inner_store, SystemTime::now())
68
11
    }
69
}
70
71
impl<I: InstantWrapper> RemoveItemCallback for ExistenceCacheStore<I> {
72
6
    fn callback<'a>(
73
6
        &'a self,
74
6
        store_key: StoreKey<'a>,
75
6
    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
76
6
        debug!(?store_key, "Removing item from cache due to callback");
77
6
        let digest = store_key.borrow().into_digest();
78
6
        Box::pin(async move {
79
6
            let deleted_key = self.existence_cache.remove(&digest).await;
80
6
            if !deleted_key {
81
                // The subscription delivers every key event on the server,
82
                // most of them for keys this cache never held.
83
0
                debug!(?store_key, "Key removed below the cache was not cached");
84
6
            }
85
6
        })
86
6
    }
87
}
88
89
#[derive(Debug)]
90
struct ExistenceCacheCallback<I: InstantWrapper> {
91
    cache: Weak<ExistenceCacheStore<I>>,
92
}
93
94
impl<I: InstantWrapper> RemoveItemCallback for ExistenceCacheCallback<I> {
95
6
    fn callback<'a>(
96
6
        &'a self,
97
6
        store_key: StoreKey<'a>,
98
6
    ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
99
6
        let cache = self.cache.upgrade();
100
6
        if let Some(local_cache) = cache {
101
6
            if let Some(
callbacks1
) = local_cache.pause_remove_callbacks.lock().as_mut() {
102
1
                callbacks.push(store_key.into_owned());
103
1
            } else {
104
5
                let store_key = store_key.into_owned();
105
5
                return Box::pin(async move {
106
5
                    local_cache.callback(store_key).await;
107
5
                });
108
            }
109
        } else {
110
0
            debug!("Cache dropped, so not doing callback");
111
        }
112
1
        Box::pin(async {})
113
6
    }
114
}
115
116
impl<I: InstantWrapper> ExistenceCacheStore<I> {
117
13
    pub fn new_with_time(
118
13
        spec: &ExistenceCacheSpec,
119
13
        inner_store: Store,
120
13
        anchor_time: I,
121
13
    ) -> Arc<Self> {
122
13
        let empty_policy = EvictionPolicy::default();
123
13
        let eviction_policy = spec.eviction_policy.as_ref().unwrap_or(&empty_policy);
124
13
        let existence_cache_store = Arc::new(Self {
125
13
            inner_store,
126
13
            existence_cache: EvictingMap::new(eviction_policy, anchor_time),
127
13
            pause_remove_callbacks: Mutex::new(None),
128
13
        });
129
13
        let other_ref = Arc::downgrade(&existence_cache_store);
130
13
        existence_cache_store
131
13
            .inner_store
132
13
            .register_remove_callback(Arc::new(ExistenceCacheCallback { cache: other_ref }))
133
13
            .expect("Register remove callback should work");
134
13
        existence_cache_store
135
13
    }
136
137
8
    pub async fn exists_in_cache(&self, digest: &DigestInfo) -> bool {
138
8
        let mut results = [None];
139
8
        self.existence_cache
140
8
            .sizes_for_keys([digest], &mut results[..], true /* peek */)
141
8
            .await;
142
8
        results[0].is_some()
143
8
    }
144
145
2
    pub async fn remove_from_cache(&self, digest: &DigestInfo) {
146
2
        self.existence_cache.remove(digest).await;
147
2
    }
148
149
12
    async fn inner_has_with_results(
150
12
        self: Pin<&Self>,
151
12
        keys: &[DigestInfo],
152
12
        results: &mut [Option<u64>],
153
12
    ) -> Result<(), Error> {
154
12
        self.existence_cache
155
12
            .sizes_for_keys(keys, results, true /* peek */)
156
12
            .await;
157
158
12
        let not_cached_keys: Vec<_> = keys
159
12
            .iter()
160
12
            .zip(results.iter())
161
12
            .filter_map(|(digest, result)| result.map_or_else(|| Some(
digest9
.
into9
()), |_| None))
162
12
            .collect();
163
164
        // Hot path optimization when all keys are cached.
165
12
        if not_cached_keys.is_empty() {
166
3
            return Ok(());
167
9
        }
168
169
        // Now query only the items not found in the cache.
170
9
        let mut inner_results = vec![None; not_cached_keys.len()];
171
9
        self.inner_store
172
9
            .has_with_results(&not_cached_keys, &mut inner_results)
173
9
            .await
174
9
            .err_tip(|| "In ExistenceCacheStore::inner_has_with_results")
?0
;
175
176
        // Insert found from previous query into our cache.
177
        {
178
            // Note: Sadly due to some weird lifetime issues we need to collect here, but
179
            // in theory we don't actually need to collect.
180
9
            let inserts = not_cached_keys
181
9
                .iter()
182
9
                .zip(inner_results.iter())
183
9
                .filter_map(|(key, result)| {
184
9
                    result.map(|size| (
key4
.
borrow4
().
into_digest4
(),
ExistenceItem(size)4
))
185
9
                })
186
9
                .collect::<Vec<_>>();
187
9
            drop(self.existence_cache.insert_many(inserts).await);
188
        }
189
190
        // Merge the results from the cache and the query.
191
        {
192
9
            let mut inner_results_iter = inner_results.into_iter();
193
            // We know at this point that any None in results was queried and will have
194
            // a result in inner_results_iter, so use this knowledge to fill in the results.
195
9
            for result in results.iter_mut() {
196
9
                if result.is_none() {
197
9
                    *result = inner_results_iter
198
9
                        .next()
199
9
                        .expect("has_with_results returned less results than expected");
200
9
                
}0
201
            }
202
            // Ensure that there was no logic error by ensuring our iterator is not empty.
203
0
            error_if!(
204
9
                inner_results_iter.next().is_some(),
205
                "has_with_results returned more results than expected"
206
            );
207
        }
208
209
9
        Ok(())
210
12
    }
211
}
212
213
#[async_trait]
214
impl<I: InstantWrapper> StoreDriver for ExistenceCacheStore<I> {
215
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
216
        self.inner_store.clone().into_inner().post_init().await?;
217
        Ok(())
218
0
    }
219
220
    async fn has_with_results(
221
        self: Pin<&Self>,
222
        digests: &[StoreKey<'_>],
223
        results: &mut [Option<u64>],
224
12
    ) -> Result<(), Error> {
225
        // TODO(palfrey) This is a bit of a hack to get around the lifetime issues with the
226
        // existence_cache. We need to convert the digests to owned values to be able to
227
        // insert them into the cache. In theory it should be able to elide this conversion
228
        // but it seems to be a bit tricky to get right.
229
        let digests: Vec<_> = digests
230
            .iter()
231
12
            .map(|key| key.borrow().into_digest())
232
            .collect();
233
        self.inner_has_with_results(&digests, results).await
234
12
    }
235
236
    async fn update(
237
        self: Pin<&Self>,
238
        key: StoreKey<'_>,
239
        reader: DropCloserReadHalf,
240
        size_info: UploadSizeInfo,
241
10
    ) -> Result<u64, Error> {
242
        let digest = key.into_digest();
243
        // Every upload reaches the inner store, whatever this cache believes.
244
        // A client only uploads what it was told is missing, so the write is
245
        // never wasted in the normal flow; and the one time the cache is
246
        // wrong (the tier under it lost the blob and the remove callback did
247
        // not arrive), discarding the upload is what turns a stale answer
248
        // into a blob nothing holds. The entry is refreshed on success.
249
        {
250
            let mut locked_callbacks = self.pause_remove_callbacks.lock();
251
            if locked_callbacks.is_none() {
252
                locked_callbacks.replace(vec![]);
253
            }
254
        }
255
        trace!(?digest, "Inserting into inner cache");
256
        let result = self.inner_store.update(digest, reader, size_info).await;
257
        if let Ok(size) = &result {
258
            trace!(?digest, "Inserting into existence cache");
259
            let _ = self
260
                .existence_cache
261
                .insert(digest, ExistenceItem(*size))
262
                .await;
263
        }
264
        {
265
            let maybe_keys = self.pause_remove_callbacks.lock().take();
266
            if let Some(keys) = maybe_keys {
267
                let mut callbacks: FuturesUnordered<_> = keys
268
                    .into_iter()
269
1
                    .map(|store_key| self.callback(store_key))
270
                    .collect();
271
                while callbacks.next().await.is_some() {}
272
            }
273
        }
274
        result
275
10
    }
276
277
    async fn get_part(
278
        self: Pin<&Self>,
279
        key: StoreKey<'_>,
280
        writer: &mut DropCloserWriteHalf,
281
        offset: u64,
282
        length: Option<u64>,
283
3
    ) -> Result<(), Error> {
284
        let digest = key.into_digest();
285
        let result = self
286
            .inner_store
287
            .get_part(digest, writer, offset, length)
288
            .await;
289
        match &result {
290
            Ok(()) => {
291
                let _ = self
292
                    .existence_cache
293
                    .insert(digest, ExistenceItem(digest.size_bytes()))
294
                    .await;
295
            }
296
            // The store below no longer holds a blob we may be vouching
297
            // for (deleted from the object store, say, which sends no
298
            // removal event). Stop vouching now rather than at the TTL:
299
            // a client told the blob is present skips uploading it, and
300
            // its retry would ask us again and be told the same.
301
            Err(err) if err.code == Code::NotFound => {
302
                if self.existence_cache.remove(&digest).await {
303
                    debug!(
304
                        ?digest,
305
                        "Read found no blob; dropped it from the existence cache"
306
                    );
307
                }
308
            }
309
            Err(_) => {}
310
        }
311
        result
312
3
    }
313
314
1
    fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver {
315
1
        self
316
1
    }
317
318
1
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
319
1
        self
320
1
    }
321
322
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
323
0
        self
324
0
    }
325
326
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
327
0
        self.inner_store.register_remove_callback(callback)
328
0
    }
329
}
330
331
#[async_trait]
332
impl<I: InstantWrapper> HealthStatusIndicator for ExistenceCacheStore<I> {
333
0
    fn get_name(&self) -> &'static str {
334
0
        "ExistenceCacheStore"
335
0
    }
336
337
0
    async fn check_health(&self, namespace: Cow<'static, str>) -> HealthStatus {
338
        StoreDriver::check_health(Pin::new(self), namespace).await
339
0
    }
340
}