Coverage Report

Created: 2026-10-18 12:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-store/src/fast_slow_store.rs
Line
Count
Source
1
// Copyright 2024-2025 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::BorrowMut;
16
use core::cmp::{max, min};
17
use core::ops::Range;
18
use core::pin::Pin;
19
use core::sync::atomic::{AtomicU64, Ordering};
20
use core::time::Duration;
21
use std::collections::HashMap;
22
use std::ffi::OsString;
23
use std::sync::{Arc, Weak};
24
25
use async_trait::async_trait;
26
use futures::stream::{FuturesUnordered, StreamExt};
27
use futures::{FutureExt, join, try_join};
28
use nativelink_config::stores::{FastSlowSpec, StoreDirection};
29
use nativelink_error::{Code, Error, ErrorContext, ResultExt, make_err};
30
use nativelink_metric::MetricsComponent;
31
use nativelink_util::buf_channel::{
32
    DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair,
33
};
34
use nativelink_util::fs;
35
use nativelink_util::health_utils::{HealthStatusIndicator, default_health_status_indicator};
36
use nativelink_util::metrics::{record_store_tier_io, record_store_tier_read};
37
use nativelink_util::store_trait::{
38
    RemoveCallback, Store, StoreDriver, StoreKey, StoreLike, StoreOptimizations, UploadSizeInfo,
39
    slow_update_store_with_file,
40
};
41
use parking_lot::Mutex;
42
use tokio::sync::OnceCell;
43
use tracing::{debug, info, trace, warn};
44
45
use crate::filesystem_store::FilesystemStore;
46
47
// TODO(palfrey) This store needs to be evaluated for more efficient memory usage,
48
// there are many copies happening internally.
49
50
type Loader = Arc<OnceCell<()>>;
51
type MaybeSize = Option<u64>;
52
53
// TODO(palfrey) We should consider copying the data in the background to allow the
54
// client to hang up while the data is buffered. An alternative is to possibly make a
55
// "BufferedStore" that could be placed on the "slow" store that would hang up early
56
// if data is in the buffer.
57
#[derive(Debug, MetricsComponent)]
58
pub struct FastSlowStore {
59
    #[metric(group = "fast_store")]
60
    fast_store: Store,
61
    fast_direction: StoreDirection,
62
    #[metric(group = "slow_store")]
63
    slow_store: Store,
64
    slow_direction: StoreDirection,
65
    /// See [`FastSlowSpec::bypass_dedup_threshold_bytes`].
66
    bypass_dedup_threshold_bytes: u64,
67
    weak_self: Weak<Self>,
68
    #[metric]
69
    metrics: FastSlowStoreMetrics,
70
    // De-duplicate requests for the fast store, only the first streams, others
71
    // are blocked.  This may feel like it's causing a slow down of tasks, but
72
    // actually it's faster because we're not downloading the file multiple
73
    // times are doing loads of duplicate IO.
74
    populating_digests: Mutex<HashMap<StoreKey<'static>, Loader>>,
75
    // Tracks keys whose slow-store write is currently in flight, along with
76
    // the best-known size. Consulted by `has_with_results` so that a
77
    // concurrent writer that has not yet finished pushing to the slow store
78
    // is still visible to a concurrent existence check, preventing redundant
79
    // duplicate uploads of the same blob.
80
    in_flight_slow_writes: Mutex<HashMap<StoreKey<'static>, Arc<tokio::sync::Mutex<MaybeSize>>>>,
81
}
82
83
// This guard ensures that the populating_digests is cleared even if the future
84
// is dropped, it is cancel safe.
85
struct LoaderGuard<'a> {
86
    weak_store: Weak<FastSlowStore>,
87
    key: StoreKey<'a>,
88
    loader: Option<Loader>,
89
    is_leader: bool,
90
}
91
92
impl LoaderGuard<'_> {
93
1.64k
    async fn get_or_try_init<E, F, Fut>(&self, f: F) -> Result<(), E>
94
1.64k
    where
95
1.64k
        F: FnOnce() -> Fut,
96
1.64k
        Fut: Future<Output = Result<(), E>>,
97
1.64k
    {
98
1.64k
        if let Some(loader) = &self.loader {
99
1.64k
            loader.get_or_try_init(f).await.
map1.64k
(|&()| ())
100
        } else {
101
            // This is impossible, but we do it anyway.
102
0
            f().await
103
        }
104
1.64k
    }
105
}
106
107
/// Cancel-safe RAII guard that removes an entry from
108
/// `FastSlowStore::in_flight_slow_writes` when dropped. This ensures the
109
/// map does not leak entries if the surrounding `update()` future is
110
/// cancelled before the slow-store write completes.
111
struct InFlightSlowWriteGuard {
112
    weak_store: Weak<FastSlowStore>,
113
    key: Option<StoreKey<'static>>,
114
    write_complete_guard: tokio::sync::OwnedMutexGuard<MaybeSize>,
115
}
116
117
impl InFlightSlowWriteGuard {
118
543
    fn complete(mut self, size: MaybeSize) {
119
543
        *self.write_complete_guard = size;
120
543
    }
121
}
122
123
impl Drop for InFlightSlowWriteGuard {
124
546
    fn drop(&mut self) {
125
546
        let Some(store) = self.weak_store.upgrade() else {
126
0
            return;
127
        };
128
546
        let Some(key) = self.key.take() else {
129
0
            return;
130
        };
131
546
        store.in_flight_slow_writes.lock().remove(&key);
132
546
    }
133
}
134
135
impl Drop for LoaderGuard<'_> {
136
1.64k
    fn drop(&mut self) {
137
1.64k
        let Some(store) = self.weak_store.upgrade() else {
138
            // The store has already gone away, nothing to remove from.
139
0
            return;
140
        };
141
1.64k
        let Some(loader) = self.loader.take() else {
142
            // This should never happen, but we do it to be safe.
143
0
            return;
144
        };
145
146
1.64k
        let mut guard = store.populating_digests.lock();
147
1.64k
        if let std::collections::hash_map::Entry::Occupied(occupied_entry) =
148
1.64k
            guard.entry(self.key.borrow().into_owned())
149
1.64k
            && Arc::ptr_eq(occupied_entry.get(), &loader)
150
        {
151
1.64k
            drop(loader);
152
1.64k
            if Arc::strong_count(occupied_entry.get()) == 1 {
153
1.59k
                // This is the last loader, so remove it.
154
1.59k
                occupied_entry.remove();
155
1.59k
            
}53
156
0
        }
157
1.64k
    }
158
}
159
160
impl FastSlowStore {
161
139
    pub fn new(spec: &FastSlowSpec, fast_store: Store, slow_store: Store) -> Arc<Self> {
162
139
        Arc::new_cyclic(|weak_self| Self {
163
139
            fast_store,
164
139
            fast_direction: spec.fast_direction,
165
139
            slow_store,
166
139
            slow_direction: spec.slow_direction,
167
            // 0 (default) disables the bypass entirely (always dedup).
168
139
            bypass_dedup_threshold_bytes: spec.bypass_dedup_threshold_bytes,
169
139
            weak_self: weak_self.clone(),
170
139
            metrics: FastSlowStoreMetrics::default(),
171
139
            populating_digests: Mutex::new(HashMap::new()),
172
139
            in_flight_slow_writes: Mutex::new(HashMap::new()),
173
139
        })
174
139
    }
175
176
546
    fn register_in_flight_slow_write(&self, key: StoreKey<'_>) -> InFlightSlowWriteGuard {
177
546
        let owned = key.into_owned();
178
546
        let write_complete = Arc::new(tokio::sync::Mutex::new(None));
179
546
        let write_complete_guard = write_complete
180
546
            .clone()
181
546
            .try_lock_owned()
182
546
            .expect("Newly created mutex is locked");
183
546
        self.in_flight_slow_writes
184
546
            .lock()
185
546
            .insert(owned.borrow().into_owned(), write_complete);
186
546
        InFlightSlowWriteGuard {
187
546
            weak_store: self.weak_self.clone(),
188
546
            key: Some(owned),
189
546
            write_complete_guard,
190
546
        }
191
546
    }
192
193
    /// Digest size in bytes, or `None` for non-digest keys.
194
28
    const fn digest_size_bytes(key: &StoreKey<'_>) -> Option<u64> {
195
28
        match key {
196
28
            StoreKey::Digest(d) => Some(d.size_bytes()),
197
0
            StoreKey::Str(_) => None,
198
        }
199
28
    }
200
201
    /// Whether a read should skip dedup and hit the slow store directly. A
202
    /// threshold of 0 disables the bypass so every read goes through dedup.
203
447
    fn should_bypass_dedup(&self, key: &StoreKey<'_>) -> bool {
204
447
        self.bypass_dedup_threshold_bytes != 0
205
28
            && Self::digest_size_bytes(key)
206
28
                .is_some_and(|size| size >= self.bypass_dedup_threshold_bytes)
207
447
    }
208
209
0
    pub const fn fast_store(&self) -> &Store {
210
0
        &self.fast_store
211
0
    }
212
213
0
    pub const fn slow_store(&self) -> &Store {
214
0
        &self.slow_store
215
0
    }
216
217
    /// Returns the filesystem-backed tiers that can evict a digest.
218
    ///
219
    /// The worker's fast store is normally a `FilesystemStore`, while the slow
220
    /// store may be one behind a `RefStore`. `Store::downcast_ref` follows
221
    /// wrappers that delegate `inner_store`; transforming wrappers intentionally
222
    /// keep themselves visible because their digest-to-bytes mapping may differ,
223
    /// so they are not recursively inspected here.
224
9
    pub fn get_filesystem_stores(&self) -> Vec<Arc<FilesystemStore>> {
225
9
        let mut stores = Vec::with_capacity(2);
226
18
        for store in 
[&self.fast_store, &self.slow_store]9
{
227
18
            let Some(
filesystem_store9
) = store
228
18
                .downcast_ref::<FilesystemStore>(None)
229
18
                .and_then(FilesystemStore::get_arc)
230
            else {
231
9
                continue;
232
            };
233
9
            if !stores
234
9
                .iter()
235
9
                .any(|existing| 
Arc::ptr_eq0
(
existing0
,
&filesystem_store0
))
236
9
            {
237
9
                stores.push(filesystem_store);
238
9
            
}0
239
        }
240
9
        stores
241
9
    }
242
243
2
    pub fn get_arc(&self) -> Option<Arc<Self>> {
244
2
        self.weak_self.upgrade()
245
2
    }
246
247
1.64k
    fn get_loader<'a>(&self, key: StoreKey<'a>) -> LoaderGuard<'a> {
248
        // Get a single loader instance that's used to populate the fast store
249
        // for this digest.  If another request comes in then it's de-duplicated.
250
1.64k
        let mut is_leader = false;
251
1.64k
        let loader = match self
252
1.64k
            .populating_digests
253
1.64k
            .lock()
254
1.64k
            .entry(key.borrow().into_owned())
255
        {
256
53
            std::collections::hash_map::Entry::Occupied(occupied_entry) => {
257
53
                occupied_entry.get().clone()
258
            }
259
1.59k
            std::collections::hash_map::Entry::Vacant(vacant_entry) => {
260
1.59k
                is_leader = true;
261
1.59k
                vacant_entry.insert(Arc::new(OnceCell::new())).clone()
262
            }
263
        };
264
1.64k
        LoaderGuard {
265
1.64k
            weak_store: self.weak_self.clone(),
266
1.64k
            key,
267
1.64k
            loader: Some(loader),
268
1.64k
            is_leader,
269
1.64k
        }
270
1.64k
    }
271
272
1.59k
    async fn populate_and_maybe_stream(
273
1.59k
        self: Pin<&Self>,
274
1.59k
        key: StoreKey<'_>,
275
1.59k
        maybe_writer: Option<&mut DropCloserWriteHalf>,
276
1.59k
        offset: u64,
277
1.59k
        length: Option<u64>,
278
1.59k
    ) -> Result<(), Error> {
279
        // A `NotFound` for a digest key carries the digest as typed
280
        // context, so the worker can report it as the missing input and the
281
        // client re-uploads exactly that blob; see `ErrorContext`.
282
7
        fn name_missing_digest(err: Error, key: &StoreKey<'_>) -> Error {
283
7
            if let StoreKey::Digest(d) = key {
284
7
                err.with_context(ErrorContext::MissingDigest {
285
7
                    hash: d.packed_hash().to_string(),
286
7
                    size: d.size_bytes().try_into().unwrap_or(i64::MAX),
287
7
                })
288
            } else {
289
0
                err
290
            }
291
7
        }
292
1.59k
        let 
reader_stream_size1.58k
= if self
293
1.59k
            .slow_store
294
1.59k
            .inner_store(Some(key.borrow()))
295
1.59k
            .optimized_for(StoreOptimizations::LazyExistenceOnSync)
296
        {
297
2
            trace!(
298
                %key,
299
2
                store_name = %self.slow_store.inner_store(Some(key.borrow())).get_name(),
300
                "Skipping .has() check due to LazyExistenceOnSync optimization"
301
            );
302
2
            UploadSizeInfo::MaxSize(u64::MAX)
303
        } else {
304
1.59k
            UploadSizeInfo::ExactSize(self
305
1.59k
                    .slow_store
306
1.59k
                    .has(key.borrow())
307
1.59k
                    .await
308
1.59k
                    .err_tip(|| "Failed to run has() on slow store")
?0
309
1.59k
                    .ok_or_else(|| 
{4
310
4
                        name_missing_digest(
311
4
                            make_err!(
312
4
                                Code::NotFound,
313
                                "Object {} not found in either fast or slow store. \
314
                                    If using multiple workers, ensure all workers share the same CAS storage path.",
315
4
                                key.as_str()
316
                            ),
317
4
                            &key,
318
                        )
319
4
                    })?
320
            )
321
        };
322
323
1.58k
        let send_range = offset..length.map_or(u64::MAX, |length| 
length260
+
offset260
);
324
1.58k
        let mut bytes_received: u64 = 0;
325
1.58k
        let mut counted_hit = false;
326
327
1.58k
        let (mut fast_tx, fast_rx) = make_buf_channel_pair();
328
1.58k
        let (slow_tx, mut slow_rx) = make_buf_channel_pair();
329
1.58k
        let data_stream_fut = async move {
330
1.58k
            let mut maybe_writer_pin = maybe_writer.map(Pin::new);
331
            loop {
332
3.15k
                let 
output_buf3.13k
= slow_rx
333
3.15k
                    .recv()
334
3.15k
                    .await
335
3.15k
                    .err_tip(|| "Failed to read data data buffer from slow store")
?15
;
336
3.13k
                if output_buf.is_empty() {
337
                    // Write out our EOF.
338
                    // We are dropped as soon as we send_eof to writer_pin, so
339
                    // we wait until we've finished all of our joins to do that.
340
1.57k
                    let fast_res = fast_tx.send_eof();
341
1.57k
                    return Ok::<_, Error>((fast_res, maybe_writer_pin));
342
1.56k
                }
343
344
1.56k
                if !counted_hit {
345
1.56k
                    self.metrics
346
1.56k
                        .slow_store_hit_count
347
1.56k
                        .fetch_add(1, Ordering::Acquire);
348
1.56k
                    record_store_tier_read("slow", "hit");
349
1.56k
                    counted_hit = true;
350
1.56k
                
}0
351
352
1.56k
                let output_buf_len = u64::try_from(output_buf.len())
353
1.56k
                    .err_tip(|| "Could not output_buf.len() to u64")
?0
;
354
1.56k
                self.metrics
355
1.56k
                    .slow_store_downloaded_bytes
356
1.56k
                    .fetch_add(output_buf_len, Ordering::Acquire);
357
1.56k
                record_store_tier_io("slow", "read", output_buf_len);
358
359
1.56k
                let writer_fut = Self::calculate_range(
360
1.56k
                    &(bytes_received..bytes_received + output_buf_len),
361
1.56k
                    &send_range,
362
0
                )?
363
1.56k
                .zip(maybe_writer_pin.as_mut())
364
1.56k
                .map_or_else(
365
1.19k
                    || futures::future::ready(Ok(())).left_future(),
366
376
                    |(range, writer_pin)| writer_pin.send(output_buf.slice(range)).right_future(),
367
                );
368
369
1.56k
                bytes_received += output_buf_len;
370
371
1.56k
                let (fast_tx_res, writer_res) = join!(fast_tx.send(output_buf), writer_fut);
372
1.56k
                fast_tx_res.err_tip(|| "Failed to write to fast store in fast_slow store")
?0
;
373
1.56k
                writer_res.err_tip(|| "Failed to write result to writer in fast_slow store")
?0
;
374
            }
375
1.58k
        };
376
377
1.58k
        let slow_store_fut = self.slow_store.get(key.borrow(), slow_tx);
378
1.58k
        let fast_store_fut = self
379
1.58k
            .fast_store
380
1.58k
            .update(key.borrow(), fast_rx, reader_stream_size);
381
382
1.58k
        let (data_stream_res, slow_res, fast_res) =
383
1.58k
            join!(data_stream_fut, slow_store_fut, fast_store_fut);
384
1.58k
        match data_stream_res {
385
1.57k
            Ok((fast_eof_res, maybe_writer_pin)) =>
386
            // Sending the EOF will drop us almost immediately in bytestream_server
387
            // so we perform it as the very last action in this method.
388
            {
389
1.57k
                fast_eof_res.merge(fast_res).merge(slow_res).merge(
390
1.57k
                    if let Some(
mut writer_pin380
) = maybe_writer_pin {
391
380
                        writer_pin.send_eof()
392
                    } else {
393
1.19k
                        Ok(())
394
                    },
395
                )
396
            }
397
15
            Err(err) => match slow_res {
398
                // The slow store lost the blob under the read (an evicting
399
                // tier behind a gRPC store, say): the same missing input as
400
                // a blob that was never there.
401
15
                Err(
slow_err3
) if slow_err.code == Code::NotFoun
d3
=> {
402
3
                    Err(name_missing_digest(slow_err, &key))
403
                }
404
12
                _ => fast_res.merge(slow_res).merge(Err(err)),
405
            },
406
        }
407
1.58k
    }
408
409
    /// Ensure our fast store is populated. This should be kept as a low
410
    /// cost function. Since the data itself is shared and not copied it should be fairly
411
    /// low cost to just discard the data, but does cost a few mutex locks while
412
    /// streaming.
413
1.39k
    
pub async fn populate_fast_store(&self, key: StoreKey<'_>) -> Result<(), Error>0
{
414
1.39k
        let maybe_size_info = self
415
1.39k
            .fast_store
416
1.39k
            .has(key.borrow())
417
1.39k
            .await
418
1.39k
            .err_tip(|| "While querying in populate_fast_store")
?0
;
419
1.39k
        if maybe_size_info.is_some() {
420
177
            return Ok(());
421
1.21k
        }
422
423
        // If the fast store is noop or read only or update only then this is an error.
424
1.21k
        if self
425
1.21k
            .fast_store
426
1.21k
            .inner_store(Some(key.borrow()))
427
1.21k
            .optimized_for(StoreOptimizations::NoopUpdates)
428
1.21k
            || self.fast_direction == StoreDirection::ReadOnly
429
1.21k
            || self.fast_direction == StoreDirection::Update
430
        {
431
0
            return Err(make_err!(
432
0
                Code::Internal,
433
0
                "Attempt to populate fast store that is read only or noop"
434
0
            ));
435
1.21k
        }
436
437
1.21k
        self.get_loader(key.borrow())
438
1.21k
            .get_or_try_init(|| 
{1.20k
439
1.20k
                Pin::new(self).populate_and_maybe_stream(key.borrow(), None, 0, None)
440
1.20k
            })
441
1.21k
            .await
442
1.21k
            .err_tip(|| "Failed to populate()")
443
1.39k
    }
444
445
    /// Returns the range of bytes that should be sent given a slice bounds
446
    /// offset so the output range maps the `received_range.start` to 0.
447
    // TODO(palfrey) This should be put into utils, as this logic is used
448
    // elsewhere in the code.
449
1.57k
    pub fn calculate_range(
450
1.57k
        received_range: &Range<u64>,
451
1.57k
        send_range: &Range<u64>,
452
1.57k
    ) -> Result<Option<Range<usize>>, Error> {
453
        // Protect against subtraction overflow.
454
1.57k
        if received_range.start >= received_range.end {
455
0
            return Ok(None);
456
1.57k
        }
457
458
1.57k
        let start = max(received_range.start, send_range.start);
459
1.57k
        let end = min(received_range.end, send_range.end);
460
1.57k
        if received_range.contains(&start) && 
received_range1.57k
.
contains1.57k
(
&(end - 1)1.57k
) {
461
            // Offset both to the start of the received_range.
462
1.57k
            let calculated_range_start = usize::try_from(start - received_range.start)
463
1.57k
                .err_tip(|| "Could not convert (start - received_range.start) to usize")
?0
;
464
1.57k
            let calculated_range_end = usize::try_from(end - received_range.start)
465
1.57k
                .err_tip(|| "Could not convert (end - received_range.start) to usize")
?0
;
466
1.57k
            Ok(Some(calculated_range_start..calculated_range_end))
467
        } else {
468
4
            Ok(None)
469
        }
470
1.57k
    }
471
}
472
473
#[async_trait]
474
impl StoreDriver for FastSlowStore {
475
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
476
        try_join!(
477
            self.fast_store.clone().into_inner().post_init(),
478
            self.slow_store.clone().into_inner().post_init()
479
        )?;
480
        Ok(())
481
0
    }
482
483
    /// Both tiers drop their copy: the fast one is the cache this store
484
    /// keeps, the slow one may be a cache of its own.
485
0
    async fn remove(self: Pin<&Self>, key: StoreKey<'_>) -> Result<bool, Error> {
486
        let fast = self
487
            .fast_store
488
            .as_store_driver_pin()
489
            .remove(key.borrow())
490
            .await?;
491
        let slow = self.slow_store.as_store_driver_pin().remove(key).await?;
492
        Ok(fast || slow)
493
0
    }
494
495
    async fn has_with_results(
496
        self: Pin<&Self>,
497
        key: &[StoreKey<'_>],
498
        results: &mut [Option<u64>],
499
292
    ) -> Result<(), Error> {
500
        // If our slow store is a noop store, it'll always return a 404,
501
        // so only check the fast store in such case.
502
        let slow_store = self.slow_store.inner_store::<StoreKey<'_>>(None);
503
        if slow_store.optimized_for(StoreOptimizations::NoopDownloads) {
504
            return self.fast_store.has_with_results(key, results).await;
505
        }
506
507
        // Check with the slow store first.
508
        self.slow_store.has_with_results(key, results).await?;
509
510
        // Check for any in-flight requests to the slow store next.
511
        let mut in_flight_futs = FuturesUnordered::new();
512
        {
513
            let in_flight = self.in_flight_slow_writes.lock();
514
            if !in_flight.is_empty() {
515
                for (i, (k, result)) in key.iter().zip(results.iter_mut()).enumerate() {
516
                    if result.is_none() {
517
                        let owned = k.borrow().into_owned();
518
                        if let Some(maybe_size) = in_flight.get(&owned) {
519
                            let maybe_size = maybe_size.clone();
520
3
                            in_flight_futs.push(async move { (i, *maybe_size.lock().await) });
521
                        }
522
                    }
523
                }
524
            }
525
        }
526
        while let Some((i, size)) = in_flight_futs.next().await {
527
            results[i] = size;
528
        }
529
530
        // NOTE: We intentionally *NEVER* check the fast store, this is to
531
        // ensure that we re-upload data to the slow store if it only exists
532
        // in the fast store.  This does not affect workers as they do not
533
        // check existence through `has` and instead go direct to loading the
534
        // data which bypasses the check and will load from the fast store if
535
        // it does not exist in the slow store.
536
537
        Ok(())
538
292
    }
539
540
    async fn update(
541
        self: Pin<&Self>,
542
        key: StoreKey<'_>,
543
        mut reader: DropCloserReadHalf,
544
        size_info: UploadSizeInfo,
545
275
    ) -> Result<u64, Error> {
546
        // If either one of our stores is a noop store, bypass the multiplexing
547
        // and just use the store that is not a noop store.
548
        let ignore_slow = self
549
            .slow_store
550
            .inner_store(Some(key.borrow()))
551
            .optimized_for(StoreOptimizations::NoopUpdates)
552
            || self.slow_direction == StoreDirection::ReadOnly
553
            || self.slow_direction == StoreDirection::Get;
554
        let ignore_fast = self
555
            .fast_store
556
            .inner_store(Some(key.borrow()))
557
            .optimized_for(StoreOptimizations::NoopUpdates)
558
            || self.fast_direction == StoreDirection::ReadOnly
559
            || self.fast_direction == StoreDirection::Get;
560
        if ignore_slow && ignore_fast {
561
            // We need to drain the reader to avoid the writer complaining that we dropped
562
            // the connection prematurely.
563
            return reader.drain().await.err_tip(|| "In FastFlowStore::update");
564
        }
565
        if ignore_slow {
566
            return self.fast_store.update(key, reader, size_info).await;
567
        }
568
        let slow_in_flight_guard = self.register_in_flight_slow_write(key.borrow());
569
        if ignore_fast {
570
            let result = self
571
                .slow_store
572
                .update(key.borrow(), reader, size_info)
573
                .await;
574
            if let Ok(size) = &result {
575
                slow_in_flight_guard.complete(Some(*size));
576
            }
577
            return result;
578
        }
579
580
        let (mut fast_tx, fast_rx) = make_buf_channel_pair();
581
        let (mut slow_tx, slow_rx) = make_buf_channel_pair();
582
583
        let key_debug = format!("{key:?}");
584
        trace!(
585
            key = %key_debug,
586
            "FastSlowStore::update: starting dual-store upload",
587
        );
588
        let update_start = std::time::Instant::now();
589
590
269
        let data_stream_fut = async move {
591
269
            let mut bytes_sent: u64 = 0;
592
            loop {
593
433
                let buffer = reader
594
433
                    .recv()
595
433
                    .await
596
433
                    .err_tip(|| "Failed to read buffer in fastslow store")
?0
;
597
433
                if buffer.is_empty() {
598
                    // EOF received.
599
269
                    fast_tx.send_eof().err_tip(
600
                        || "Failed to write eof to fast store in fast_slow store update",
601
0
                    )?;
602
269
                    slow_tx
603
269
                        .send_eof()
604
269
                        .err_tip(|| "Failed to write eof to writer in fast_slow store update")
?0
;
605
269
                    debug!(
606
                        total_bytes = bytes_sent,
607
                        "FastSlowStore::update: data_stream sent EOF to both stores",
608
                    );
609
269
                    return Result::<u64, Error>::Ok(bytes_sent);
610
164
                }
611
612
164
                let chunk_len = buffer.len();
613
164
                let send_start = std::time::Instant::now();
614
164
                let (fast_result, slow_result) =
615
164
                    join!(fast_tx.send(buffer.clone()), slow_tx.send(buffer));
616
164
                let send_elapsed = send_start.elapsed();
617
164
                if send_elapsed.as_secs() >= 5 {
618
0
                    warn!(
619
                        chunk_len,
620
0
                        send_elapsed_ms = send_elapsed.as_millis(),
621
                        total_bytes = bytes_sent,
622
                        "FastSlowStore::update: channel send stalled (>5s). A downstream store may be hanging",
623
                    );
624
164
                }
625
164
                bytes_sent += u64::try_from(chunk_len).unwrap_or(u64::MAX);
626
164
                fast_result
627
164
                    .map_err(|e| 
{0
628
0
                        make_err!(
629
0
                            Code::Internal,
630
                            "Failed to send message to fast_store in fast_slow_store {:?}",
631
                            e
632
                        )
633
0
                    })
634
164
                    .merge(slow_result.map_err(|e| 
{0
635
0
                        make_err!(
636
0
                            Code::Internal,
637
                            "Failed to send message to slow_store in fast_slow store {:?}",
638
                            e
639
                        )
640
0
                    }))?;
641
            }
642
269
        };
643
644
        let fast_store_fut = self.fast_store.update(key.borrow(), fast_rx, size_info);
645
        let slow_store_fut = self.slow_store.update(key.borrow(), slow_rx, size_info);
646
647
        let (data_stream_res, fast_res, slow_res) =
648
            join!(data_stream_fut, fast_store_fut, slow_store_fut);
649
650
        if let Ok(size) = slow_res {
651
            slow_in_flight_guard.complete(Some(size));
652
        } else {
653
            drop(slow_in_flight_guard);
654
        }
655
656
        let total_elapsed = update_start.elapsed();
657
        if data_stream_res.is_err() || fast_res.is_err() || slow_res.is_err() {
658
            let all_not_found = [&data_stream_res, &fast_res, &slow_res]
659
                .iter()
660
0
                .all(|r| match r {
661
0
                    Ok(_size) => true,
662
0
                    Err(e) => e.code == Code::NotFound,
663
0
                });
664
            if all_not_found {
665
                info!(
666
                    key = %key_debug,
667
                    elapsed_ms = total_elapsed.as_millis(),
668
                    data_stream_ok = data_stream_res.is_ok(),
669
                    fast_store_ok = fast_res.is_ok(),
670
                    slow_store_ok = slow_res.is_ok(),
671
                    "FastSlowStore::update: completed with NotFound error(s)",
672
                );
673
            } else {
674
                warn!(
675
                    key = %key_debug,
676
                    elapsed_ms = total_elapsed.as_millis(),
677
                    data_stream_ok = data_stream_res.is_ok(),
678
                    fast_store_ok = fast_res.is_ok(),
679
                    slow_store_ok = slow_res.is_ok(),
680
                    "FastSlowStore::update: completed with error(s)",
681
                );
682
            }
683
        } else {
684
            trace!(
685
                key = %key_debug,
686
                elapsed_ms = total_elapsed.as_millis(),
687
                "FastSlowStore::update: completed successfully",
688
            );
689
        }
690
        data_stream_res.merge(fast_res).merge(slow_res)
691
275
    }
692
693
    /// `FastSlowStore` has optimizations for dealing with files.
694
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
695
0
        optimization == StoreOptimizations::FileUpdates
696
0
    }
697
698
    /// Optimized variation to consume the file if one of the stores is a
699
    /// filesystem store. This makes the operation a move instead of a copy
700
    /// dramatically increasing performance for large files.
701
    async fn update_with_whole_file(
702
        self: Pin<&Self>,
703
        key: StoreKey<'_>,
704
        path: OsString,
705
        mut file: fs::FileSlot,
706
        upload_size: UploadSizeInfo,
707
273
    ) -> Result<(u64, Option<fs::FileSlot>), Error> {
708
        trace!(
709
            key = ?key,
710
            ?upload_size,
711
            "FastSlowStore::update_with_whole_file: starting",
712
        );
713
        if self
714
            .fast_store
715
            .optimized_for(StoreOptimizations::FileUpdates)
716
        {
717
            if !self
718
                .slow_store
719
                .inner_store(Some(key.borrow()))
720
                .optimized_for(StoreOptimizations::NoopUpdates)
721
                && self.slow_direction != StoreDirection::ReadOnly
722
                && self.slow_direction != StoreDirection::Get
723
            {
724
                trace!("FastSlowStore::update_with_whole_file: uploading to slow_store");
725
                let slow_start = std::time::Instant::now();
726
                let slow_in_flight_guard = self.register_in_flight_slow_write(key.borrow());
727
                let size = slow_update_store_with_file(
728
                    self.slow_store.as_store_driver_pin(),
729
                    key.borrow(),
730
                    &mut file,
731
                    upload_size,
732
                )
733
                .await
734
                .err_tip(|| "In FastSlowStore::update_with_whole_file slow_store")?;
735
                slow_in_flight_guard.complete(Some(size));
736
                trace!(
737
                    elapsed_ms = slow_start.elapsed().as_millis(),
738
                    "FastSlowStore::update_with_whole_file: slow_store upload completed",
739
                );
740
            }
741
            if self.fast_direction == StoreDirection::ReadOnly
742
                || self.fast_direction == StoreDirection::Get
743
            {
744
                let file_size = match upload_size {
745
                    UploadSizeInfo::ExactSize(size) => size,
746
                    UploadSizeInfo::MaxSize(_) => file
747
                        .as_ref()
748
                        .metadata()
749
                        .await
750
0
                        .err_tip(|| format!("While reading metadata for {}", path.display()))?
751
                        .len(),
752
                };
753
                return Ok((file_size, Some(file)));
754
            }
755
            return self
756
                .fast_store
757
                .update_with_whole_file(key, path, file, upload_size)
758
                .await;
759
        }
760
761
        if self
762
            .slow_store
763
            .optimized_for(StoreOptimizations::FileUpdates)
764
        {
765
            let ignore_fast = self
766
                .fast_store
767
                .inner_store(Some(key.borrow()))
768
                .optimized_for(StoreOptimizations::NoopUpdates)
769
                || self.fast_direction == StoreDirection::ReadOnly
770
                || self.fast_direction == StoreDirection::Get;
771
            let maybe_size = if ignore_fast {
772
                None
773
            } else {
774
                Some(
775
                    slow_update_store_with_file(
776
                        self.fast_store.as_store_driver_pin(),
777
                        key.borrow(),
778
                        &mut file,
779
                        upload_size,
780
                    )
781
                    .await
782
                    .err_tip(|| "In FastSlowStore::update_with_whole_file fast_store")?,
783
                )
784
            };
785
            let ignore_slow = self.slow_direction == StoreDirection::ReadOnly
786
                || self.slow_direction == StoreDirection::Get;
787
            if ignore_slow {
788
                let size = match maybe_size {
789
                    Some(size) => size,
790
                    None => match upload_size {
791
                        UploadSizeInfo::ExactSize(size) => size,
792
                        UploadSizeInfo::MaxSize(_) => file
793
                            .as_ref()
794
                            .metadata()
795
                            .await
796
0
                            .err_tip(|| format!("While reading metadata for {}", path.display()))?
797
                            .len(),
798
                    },
799
                };
800
                return Ok((size, Some(file)));
801
            }
802
            let slow_in_flight_guard: InFlightSlowWriteGuard =
803
                self.register_in_flight_slow_write(key.borrow());
804
            let (size, maybe_file_slot) = self
805
                .slow_store
806
                .update_with_whole_file(key.borrow(), path, file, upload_size)
807
                .await?;
808
            slow_in_flight_guard.complete(Some(size));
809
            return Ok((size, maybe_file_slot));
810
        }
811
812
        let size = slow_update_store_with_file(self, key, &mut file, upload_size)
813
            .await
814
            .err_tip(|| "In FastSlowStore::update_with_whole_file")?;
815
        Ok((size, Some(file)))
816
273
    }
817
818
    async fn get_part(
819
        self: Pin<&Self>,
820
        key: StoreKey<'_>,
821
        writer: &mut DropCloserWriteHalf,
822
        offset: u64,
823
        length: Option<u64>,
824
997
    ) -> Result<(), Error> {
825
        // `has()` can report a stale map entry whose file is gone, so
826
        // get_part may still return NotFound; fall through to the slow
827
        // store. With bytes already streamed to the caller that is a resume:
828
        // a digest names an immutable blob, so what the fast tier sent is a
829
        // correct prefix and the slow store serves the rest from there (a
830
        // Redis tier evicting the key under a read is the live case). Any
831
        // other key could hold different content in each tier, so a short
832
        // read there stays an error.
833
        // One existence check, reused: this is the hot read path and `has()`
834
        // on a filesystem fast store is a syscall.
835
        let mut offset = offset;
836
        let mut length = length;
837
        let in_fast_store = self.fast_store.has(key.borrow()).await?.is_some();
838
        if !in_fast_store {
839
            record_store_tier_read("fast", "miss");
840
        }
841
        if in_fast_store {
842
            let bytes_before = writer.get_bytes_written();
843
            match self
844
                .fast_store
845
                .get_part(key.borrow(), writer.borrow_mut(), offset, length)
846
                .await
847
            {
848
                Ok(()) => {
849
                    self.metrics
850
                        .fast_store_hit_count
851
                        .fetch_add(1, Ordering::Acquire);
852
                    self.metrics
853
                        .fast_store_downloaded_bytes
854
                        .fetch_add(writer.get_bytes_written(), Ordering::Acquire);
855
                    record_store_tier_read("fast", "hit");
856
                    record_store_tier_io("fast", "read", writer.get_bytes_written());
857
                    return Ok(());
858
                }
859
                Err(e)
860
                    if e.code == Code::NotFound && writer.get_bytes_written() == bytes_before =>
861
                {
862
                    self.metrics
863
                        .fast_store_stale_map_falls_through
864
                        .fetch_add(1, Ordering::Acquire);
865
                    record_store_tier_read("fast", "stale");
866
                    warn!(%key, ?e, "Stale fast-store map entry; falling through to slow store");
867
                    // fall through to populate path
868
                }
869
                Err(e) if e.code == Code::NotFound && matches!(key, StoreKey::Digest(_)) => {
870
                    let sent = writer.get_bytes_written() - bytes_before;
871
                    if let Some(wanted) = length {
872
                        let rest = wanted.saturating_sub(sent);
873
                        if rest == 0 {
874
                            return Ok(());
875
                        }
876
                        length = Some(rest);
877
                    }
878
                    offset += sent;
879
                    self.metrics
880
                        .fast_store_short_reads_resumed
881
                        .fetch_add(1, Ordering::Acquire);
882
                    record_store_tier_read("fast", "short");
883
                    warn!(
884
                        %key,
885
                        sent,
886
                        ?e,
887
                        "Fast store ended short; resuming from the slow store"
888
                    );
889
                    // fall through to populate path, from the next byte
890
                }
891
                Err(e) => return Err(e),
892
            }
893
        }
894
895
        // If the fast store is noop or read only or update only then bypass it.
896
        if self
897
            .fast_store
898
            .inner_store(Some(key.borrow()))
899
            .optimized_for(StoreOptimizations::NoopUpdates)
900
            || self.fast_direction == StoreDirection::ReadOnly
901
            || self.fast_direction == StoreDirection::Update
902
        {
903
            self.metrics
904
                .slow_store_hit_count
905
                .fetch_add(1, Ordering::Acquire);
906
            self.slow_store
907
                .get_part(key, writer.borrow_mut(), offset, length)
908
                .await?;
909
            self.metrics
910
                .slow_store_downloaded_bytes
911
                .fetch_add(writer.get_bytes_written(), Ordering::Acquire);
912
            record_store_tier_read("slow", "hit");
913
            record_store_tier_io("slow", "read", writer.get_bytes_written());
914
            return Ok(());
915
        }
916
917
        // Huge blobs: dedup is a net loss (followers time out anyway and
918
        // the fast tier is evicted), so read straight from the slow store.
919
        if self.should_bypass_dedup(&key) {
920
            self.metrics
921
                .huge_blob_dedup_bypasses
922
                .fetch_add(1, Ordering::Acquire);
923
            self.metrics
924
                .slow_store_hit_count
925
                .fetch_add(1, Ordering::Acquire);
926
            debug!(%key, threshold_bytes = self.bypass_dedup_threshold_bytes, "Bypassing dedup for huge blob");
927
            self.slow_store
928
                .get_part(key, writer.borrow_mut(), offset, length)
929
                .await
930
                .err_tip(|| "In FastSlowStore::get_part huge-blob bypass")?;
931
            self.metrics
932
                .slow_store_downloaded_bytes
933
                .fetch_add(writer.get_bytes_written(), Ordering::Acquire);
934
            record_store_tier_read("slow", "hit");
935
            record_store_tier_io("slow", "read", writer.get_bytes_written());
936
            return Ok(());
937
        }
938
939
        let mut writer = Some(writer);
940
941
        // Drive the dedup loader. Two distinct paths:
942
        //
943
        //   * Leader (created the OnceCell entry): runs `populate` with
944
        //     OUR `writer`, streaming directly to the caller while
945
        //     filling the fast cache. No timeout: a multi-GB blob
946
        //     legitimately takes minutes to stream, and cancelling our
947
        //     own populate would propagate the failure to every other
948
        //     reader of this digest.
949
        //
950
        //   * Follower: bound the wait so a wedged leader does not pin
951
        //     us until the upstream `gRPC` deadline fires. The follower
952
        //     closure passes `None` for `writer`. This is critical: if
953
        //     the OnceCell ever promotes our follower closure to leader
954
        //     (because the original leader's future was dropped), and
955
        //     our `tokio::time::timeout` then cancels it, *no* caller
956
        //     `writer` was ever moved into the populate stream, so no
957
        //     `gRPC` sender is dropped without EOF. The follower then
958
        //     re-enters `get_part` below and reads from the now-warm
959
        //     fast cache, OR falls back to the slow store on timeout.
960
        let needs_slow_store_fallback: bool = {
961
            let loader_guard = self.get_loader(key.borrow());
962
            let is_leader = loader_guard.is_leader;
963
            if is_leader {
964
                loader_guard
965
388
                    .get_or_try_init(|| {
966
388
                        self.populate_and_maybe_stream(key.borrow(), writer.take(), offset, length)
967
388
                    })
968
                    .await?;
969
                false
970
            } else {
971
0
                let load_fut = loader_guard.get_or_try_init(|| {
972
0
                    self.populate_and_maybe_stream(key.borrow(), None, offset, length)
973
0
                });
974
                match tokio::time::timeout(LEADER_WAIT_TIMEOUT, load_fut).await {
975
                    Ok(result) => {
976
                        result?;
977
                        false
978
                    }
979
                    Err(_elapsed) => {
980
                        self.metrics
981
                            .leader_wait_timeouts
982
                            .fetch_add(1, Ordering::Acquire);
983
                        warn!(
984
                            %key,
985
                            timeout_secs = LEADER_WAIT_TIMEOUT.as_secs(),
986
                            "FastSlowStore::get_part: leader-wait exceeded timeout, bypassing dedup and reading slow store directly",
987
                        );
988
                        true
989
                    }
990
                }
991
            }
992
        };
993
994
        if needs_slow_store_fallback && let Some(writer) = writer.take() {
995
            return self
996
                .slow_store
997
                .get_part(key, writer, offset, length)
998
                .await
999
                .err_tip(
1000
                    || "In FastSlowStore::get_part slow_store fallback after leader-wait timeout",
1001
                );
1002
        }
1003
1004
        // If we didn't stream then re-enter which will stream from the fast
1005
        // store, or retry the download.  We should not get in a loop here
1006
        // because OnceCell has the good sense to retry for all callers so in
1007
        // order to get here the fast store will have been populated.  There's
1008
        // an outside chance it was evicted, but that's slim.
1009
        if let Some(writer) = writer.take() {
1010
            self.get_part(key, writer, offset, length).await
1011
        } else {
1012
            // This was the thread that did the streaming already, lucky duck.
1013
            Ok(())
1014
        }
1015
997
    }
1016
1017
2
    fn inner_store(&self, _key: Option<StoreKey>) -> &dyn StoreDriver {
1018
2
        self
1019
2
    }
1020
1021
2
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
1022
2
        self
1023
2
    }
1024
1025
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
1026
0
        self
1027
0
    }
1028
1029
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
1030
0
        self.fast_store.register_remove_callback(callback.clone())?;
1031
0
        self.slow_store.register_remove_callback(callback)?;
1032
0
        Ok(())
1033
0
    }
1034
}
1035
1036
#[derive(Debug, Default, MetricsComponent)]
1037
struct FastSlowStoreMetrics {
1038
    #[metric(help = "Hit count for the fast store")]
1039
    fast_store_hit_count: AtomicU64,
1040
    #[metric(help = "Downloaded bytes from the fast store")]
1041
    fast_store_downloaded_bytes: AtomicU64,
1042
    #[metric(help = "Hit count for the slow store")]
1043
    slow_store_hit_count: AtomicU64,
1044
    #[metric(help = "Downloaded bytes from the slow store")]
1045
    slow_store_downloaded_bytes: AtomicU64,
1046
    #[metric(
1047
        help = "Number of times a follower bypassed the populating-digests dedup because the leader exceeded LEADER_WAIT_TIMEOUT"
1048
    )]
1049
    leader_wait_timeouts: AtomicU64,
1050
    #[metric(help = "get_part calls that bypassed dedup for huge blobs")]
1051
    huge_blob_dedup_bypasses: AtomicU64,
1052
    #[metric(help = "Stale fast-store map entries that fell through to the slow store")]
1053
    fast_store_stale_map_falls_through: AtomicU64,
1054
    #[metric(
1055
        help = "Reads of a digest the fast store ended short, resumed from the slow store at the byte it stopped"
1056
    )]
1057
    fast_store_short_reads_resumed: AtomicU64,
1058
}
1059
1060
/// Maximum time a follower will wait on the leader-populator before
1061
/// bypassing the dedup map and reading directly from the slow store.
1062
///
1063
/// Without this bound a single wedged populator would block every
1064
/// concurrent reader of the same digest until each one's own `gRPC`
1065
/// deadline fired (e.g. Bazel's `--remote_timeout`), turning a
1066
/// single slow read into a fan-out of `DEADLINE_EXCEEDED` errors.
1067
const LEADER_WAIT_TIMEOUT: Duration = Duration::from_mins(1);
1068
1069
default_health_status_indicator!(FastSlowStore);