Coverage Report

Created: 2026-08-20 02:36

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.43k
    async fn get_or_try_init<E, F, Fut>(&self, f: F) -> Result<(), E>
94
1.43k
    where
95
1.43k
        F: FnOnce() -> Fut,
96
1.43k
        Fut: Future<Output = Result<(), E>>,
97
1.43k
    {
98
1.43k
        if let Some(loader) = &self.loader {
99
1.43k
            loader.get_or_try_init(f).await.
map1.43k
(|&()| ())
100
        } else {
101
            // This is impossible, but we do it anyway.
102
0
            f().await
103
        }
104
1.43k
    }
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
147
    fn complete(mut self, size: MaybeSize) {
119
147
        *self.write_complete_guard = size;
120
147
    }
121
}
122
123
impl Drop for InFlightSlowWriteGuard {
124
148
    fn drop(&mut self) {
125
148
        let Some(store) = self.weak_store.upgrade() else {
126
0
            return;
127
        };
128
148
        let Some(key) = self.key.take() else {
129
0
            return;
130
        };
131
148
        store.in_flight_slow_writes.lock().remove(&key);
132
148
    }
133
}
134
135
impl Drop for LoaderGuard<'_> {
136
1.43k
    fn drop(&mut self) {
137
1.43k
        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.43k
        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.43k
        let mut guard = store.populating_digests.lock();
147
1.43k
        if let std::collections::hash_map::Entry::Occupied(occupied_entry) =
148
1.43k
            guard.entry(self.key.borrow().into_owned())
149
1.43k
            && Arc::ptr_eq(occupied_entry.get(), &loader)
150
        {
151
1.43k
            drop(loader);
152
1.43k
            if Arc::strong_count(occupied_entry.get()) == 1 {
153
1.39k
                // This is the last loader, so remove it.
154
1.39k
                occupied_entry.remove();
155
1.39k
            
}46
156
0
        }
157
1.43k
    }
158
}
159
160
impl FastSlowStore {
161
91
    pub fn new(spec: &FastSlowSpec, fast_store: Store, slow_store: Store) -> Arc<Self> {
162
91
        Arc::new_cyclic(|weak_self| Self {
163
91
            fast_store,
164
91
            fast_direction: spec.fast_direction,
165
91
            slow_store,
166
91
            slow_direction: spec.slow_direction,
167
            // 0 (default) disables the bypass entirely (always dedup).
168
91
            bypass_dedup_threshold_bytes: spec.bypass_dedup_threshold_bytes,
169
91
            weak_self: weak_self.clone(),
170
91
            metrics: FastSlowStoreMetrics::default(),
171
91
            populating_digests: Mutex::new(HashMap::new()),
172
91
            in_flight_slow_writes: Mutex::new(HashMap::new()),
173
91
        })
174
91
    }
175
176
148
    fn register_in_flight_slow_write(&self, key: StoreKey<'_>) -> InFlightSlowWriteGuard {
177
148
        let owned = key.into_owned();
178
148
        let write_complete = Arc::new(tokio::sync::Mutex::new(None));
179
148
        let write_complete_guard = write_complete
180
148
            .clone()
181
148
            .try_lock_owned()
182
148
            .expect("Newly created mutex is locked");
183
148
        self.in_flight_slow_writes
184
148
            .lock()
185
148
            .insert(owned.borrow().into_owned(), write_complete);
186
148
        InFlightSlowWriteGuard {
187
148
            weak_store: self.weak_self.clone(),
188
148
            key: Some(owned),
189
148
            write_complete_guard,
190
148
        }
191
148
    }
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
394
    fn should_bypass_dedup(&self, key: &StoreKey<'_>) -> bool {
204
394
        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
394
    }
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
1
    pub fn get_filesystem_stores(&self) -> Vec<Arc<FilesystemStore>> {
225
1
        let mut stores = Vec::with_capacity(2);
226
2
        for store in 
[&self.fast_store, &self.slow_store]1
{
227
2
            let Some(
filesystem_store1
) = store
228
2
                .downcast_ref::<FilesystemStore>(None)
229
2
                .and_then(FilesystemStore::get_arc)
230
            else {
231
1
                continue;
232
            };
233
1
            if !stores
234
1
                .iter()
235
1
                .any(|existing| 
Arc::ptr_eq0
(
existing0
,
&filesystem_store0
))
236
1
            {
237
1
                stores.push(filesystem_store);
238
1
            
}0
239
        }
240
1
        stores
241
1
    }
242
243
2
    pub fn get_arc(&self) -> Option<Arc<Self>> {
244
2
        self.weak_self.upgrade()
245
2
    }
246
247
1.43k
    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.43k
        let mut is_leader = false;
251
1.43k
        let loader = match self
252
1.43k
            .populating_digests
253
1.43k
            .lock()
254
1.43k
            .entry(key.borrow().into_owned())
255
        {
256
46
            std::collections::hash_map::Entry::Occupied(occupied_entry) => {
257
46
                occupied_entry.get().clone()
258
            }
259
1.39k
            std::collections::hash_map::Entry::Vacant(vacant_entry) => {
260
1.39k
                is_leader = true;
261
1.39k
                vacant_entry.insert(Arc::new(OnceCell::new())).clone()
262
            }
263
        };
264
1.43k
        LoaderGuard {
265
1.43k
            weak_store: self.weak_self.clone(),
266
1.43k
            key,
267
1.43k
            loader: Some(loader),
268
1.43k
            is_leader,
269
1.43k
        }
270
1.43k
    }
271
272
1.39k
    async fn populate_and_maybe_stream(
273
1.39k
        self: Pin<&Self>,
274
1.39k
        key: StoreKey<'_>,
275
1.39k
        maybe_writer: Option<&mut DropCloserWriteHalf>,
276
1.39k
        offset: u64,
277
1.39k
        length: Option<u64>,
278
1.39k
    ) -> Result<(), Error> {
279
1.39k
        let 
reader_stream_size1.38k
= if self
280
1.39k
            .slow_store
281
1.39k
            .inner_store(Some(key.borrow()))
282
1.39k
            .optimized_for(StoreOptimizations::LazyExistenceOnSync)
283
        {
284
2
            trace!(
285
                %key,
286
2
                store_name = %self.slow_store.inner_store(Some(key.borrow())).get_name(),
287
                "Skipping .has() check due to LazyExistenceOnSync optimization"
288
            );
289
2
            UploadSizeInfo::MaxSize(u64::MAX)
290
        } else {
291
1.38k
            UploadSizeInfo::ExactSize(self
292
1.38k
                    .slow_store
293
1.38k
                    .has(key.borrow())
294
1.38k
                    .await
295
1.38k
                    .err_tip(|| "Failed to run has() on slow store")
?0
296
1.38k
                    .ok_or_else(|| 
{4
297
4
                        let err = make_err!(
298
4
                            Code::NotFound,
299
                            "Object {} not found in either fast or slow store. \
300
                                If using multiple workers, ensure all workers share the same CAS storage path.",
301
4
                            key.as_str()
302
                        );
303
4
                        if let StoreKey::Digest(d) = key.borrow() {
304
4
                            err.with_context(ErrorContext::MissingDigest {
305
4
                                hash: d.packed_hash().to_string(),
306
4
                                size: d.size_bytes().try_into().unwrap_or(i64::MAX),
307
4
                            })
308
                        } else {
309
0
                            err
310
                        }
311
4
                    })?
312
            )
313
        };
314
315
1.38k
        let send_range = offset..length.map_or(u64::MAX, |length| 
length212
+
offset212
);
316
1.38k
        let mut bytes_received: u64 = 0;
317
1.38k
        let mut counted_hit = false;
318
319
1.38k
        let (mut fast_tx, fast_rx) = make_buf_channel_pair();
320
1.38k
        let (slow_tx, mut slow_rx) = make_buf_channel_pair();
321
1.38k
        let data_stream_fut = async move {
322
1.38k
            let mut maybe_writer_pin = maybe_writer.map(Pin::new);
323
            loop {
324
2.76k
                let 
output_buf2.76k
= slow_rx
325
2.76k
                    .recv()
326
2.76k
                    .await
327
2.76k
                    .err_tip(|| "Failed to read data data buffer from slow store")
?2
;
328
2.76k
                if output_buf.is_empty() {
329
                    // Write out our EOF.
330
                    // We are dropped as soon as we send_eof to writer_pin, so
331
                    // we wait until we've finished all of our joins to do that.
332
1.38k
                    let fast_res = fast_tx.send_eof();
333
1.38k
                    return Ok::<_, Error>((fast_res, maybe_writer_pin));
334
1.37k
                }
335
336
1.37k
                if !counted_hit {
337
1.37k
                    self.metrics
338
1.37k
                        .slow_store_hit_count
339
1.37k
                        .fetch_add(1, Ordering::Acquire);
340
1.37k
                    record_store_tier_read("slow", "hit");
341
1.37k
                    counted_hit = true;
342
1.37k
                
}0
343
344
1.37k
                let output_buf_len = u64::try_from(output_buf.len())
345
1.37k
                    .err_tip(|| "Could not output_buf.len() to u64")
?0
;
346
1.37k
                self.metrics
347
1.37k
                    .slow_store_downloaded_bytes
348
1.37k
                    .fetch_add(output_buf_len, Ordering::Acquire);
349
1.37k
                record_store_tier_io("slow", "read", output_buf_len);
350
351
1.37k
                let writer_fut = Self::calculate_range(
352
1.37k
                    &(bytes_received..bytes_received + output_buf_len),
353
1.37k
                    &send_range,
354
0
                )?
355
1.37k
                .zip(maybe_writer_pin.as_mut())
356
1.37k
                .map_or_else(
357
1.05k
                    || futures::future::ready(Ok(())).left_future(),
358
325
                    |(range, writer_pin)| writer_pin.send(output_buf.slice(range)).right_future(),
359
                );
360
361
1.37k
                bytes_received += output_buf_len;
362
363
1.37k
                let (fast_tx_res, writer_res) = join!(fast_tx.send(output_buf), writer_fut);
364
1.37k
                fast_tx_res.err_tip(|| "Failed to write to fast store in fast_slow store")
?0
;
365
1.37k
                writer_res.err_tip(|| "Failed to write result to writer in fast_slow store")
?0
;
366
            }
367
1.38k
        };
368
369
1.38k
        let slow_store_fut = self.slow_store.get(key.borrow(), slow_tx);
370
1.38k
        let fast_store_fut = self
371
1.38k
            .fast_store
372
1.38k
            .update(key.borrow(), fast_rx, reader_stream_size);
373
374
1.38k
        let (data_stream_res, slow_res, fast_res) =
375
1.38k
            join!(data_stream_fut, slow_store_fut, fast_store_fut);
376
1.38k
        match data_stream_res {
377
1.38k
            Ok((fast_eof_res, maybe_writer_pin)) =>
378
            // Sending the EOF will drop us almost immediately in bytestream_server
379
            // so we perform it as the very last action in this method.
380
            {
381
1.38k
                fast_eof_res.merge(fast_res).merge(slow_res).merge(
382
1.38k
                    if let Some(
mut writer_pin331
) = maybe_writer_pin {
383
331
                        writer_pin.send_eof()
384
                    } else {
385
1.05k
                        Ok(())
386
                    },
387
                )
388
            }
389
2
            Err(err) => match slow_res {
390
2
                Err(
slow_err1
) if slow_err.code == Code::NotFoun
d1
=>
Err(slow_err)1
,
391
1
                _ => fast_res.merge(slow_res).merge(Err(err)),
392
            },
393
        }
394
1.39k
    }
395
396
    /// Ensure our fast store is populated. This should be kept as a low
397
    /// cost function. Since the data itself is shared and not copied it should be fairly
398
    /// low cost to just discard the data, but does cost a few mutex locks while
399
    /// streaming.
400
1.24k
    
pub async fn populate_fast_store(&self, key: StoreKey<'_>) -> Result<(), Error>0
{
401
1.24k
        let maybe_size_info = self
402
1.24k
            .fast_store
403
1.24k
            .has(key.borrow())
404
1.24k
            .await
405
1.24k
            .err_tip(|| "While querying in populate_fast_store")
?0
;
406
1.24k
        if maybe_size_info.is_some() {
407
183
            return Ok(());
408
1.06k
        }
409
410
        // If the fast store is noop or read only or update only then this is an error.
411
1.06k
        if self
412
1.06k
            .fast_store
413
1.06k
            .inner_store(Some(key.borrow()))
414
1.06k
            .optimized_for(StoreOptimizations::NoopUpdates)
415
1.06k
            || self.fast_direction == StoreDirection::ReadOnly
416
1.06k
            || self.fast_direction == StoreDirection::Update
417
        {
418
0
            return Err(make_err!(
419
0
                Code::Internal,
420
0
                "Attempt to populate fast store that is read only or noop"
421
0
            ));
422
1.06k
        }
423
424
1.06k
        self.get_loader(key.borrow())
425
1.06k
            .get_or_try_init(|| 
{1.05k
426
1.05k
                Pin::new(self).populate_and_maybe_stream(key.borrow(), None, 0, None)
427
1.05k
            })
428
1.06k
            .await
429
1.06k
            .err_tip(|| "Failed to populate()")
430
1.24k
    }
431
432
    /// Returns the range of bytes that should be sent given a slice bounds
433
    /// offset so the output range maps the `received_range.start` to 0.
434
    // TODO(palfrey) This should be put into utils, as this logic is used
435
    // elsewhere in the code.
436
1.38k
    pub fn calculate_range(
437
1.38k
        received_range: &Range<u64>,
438
1.38k
        send_range: &Range<u64>,
439
1.38k
    ) -> Result<Option<Range<usize>>, Error> {
440
        // Protect against subtraction overflow.
441
1.38k
        if received_range.start >= received_range.end {
442
0
            return Ok(None);
443
1.38k
        }
444
445
1.38k
        let start = max(received_range.start, send_range.start);
446
1.38k
        let end = min(received_range.end, send_range.end);
447
1.38k
        if received_range.contains(&start) && 
received_range1.38k
.
contains1.38k
(
&(end - 1)1.38k
) {
448
            // Offset both to the start of the received_range.
449
1.38k
            let calculated_range_start = usize::try_from(start - received_range.start)
450
1.38k
                .err_tip(|| "Could not convert (start - received_range.start) to usize")
?0
;
451
1.38k
            let calculated_range_end = usize::try_from(end - received_range.start)
452
1.38k
                .err_tip(|| "Could not convert (end - received_range.start) to usize")
?0
;
453
1.38k
            Ok(Some(calculated_range_start..calculated_range_end))
454
        } else {
455
4
            Ok(None)
456
        }
457
1.38k
    }
458
}
459
460
#[async_trait]
461
impl StoreDriver for FastSlowStore {
462
0
    async fn post_init(self: Arc<Self>) -> Result<(), Error> {
463
        try_join!(
464
            self.fast_store.clone().into_inner().post_init(),
465
            self.slow_store.clone().into_inner().post_init()
466
        )?;
467
        Ok(())
468
0
    }
469
470
    async fn has_with_results(
471
        self: Pin<&Self>,
472
        key: &[StoreKey<'_>],
473
        results: &mut [Option<u64>],
474
27
    ) -> Result<(), Error> {
475
        // If our slow store is a noop store, it'll always return a 404,
476
        // so only check the fast store in such case.
477
        let slow_store = self.slow_store.inner_store::<StoreKey<'_>>(None);
478
        if slow_store.optimized_for(StoreOptimizations::NoopDownloads) {
479
            return self.fast_store.has_with_results(key, results).await;
480
        }
481
482
        // Check with the slow store first.
483
        self.slow_store.has_with_results(key, results).await?;
484
485
        // Check for any in-flight requests to the slow store next.
486
        let mut in_flight_futs = FuturesUnordered::new();
487
        {
488
            let in_flight = self.in_flight_slow_writes.lock();
489
            if !in_flight.is_empty() {
490
                for (i, (k, result)) in key.iter().zip(results.iter_mut()).enumerate() {
491
                    if result.is_none() {
492
                        let owned = k.borrow().into_owned();
493
                        if let Some(maybe_size) = in_flight.get(&owned) {
494
                            let maybe_size = maybe_size.clone();
495
3
                            in_flight_futs.push(async move { (i, *maybe_size.lock().await) });
496
                        }
497
                    }
498
                }
499
            }
500
        }
501
        while let Some((i, size)) = in_flight_futs.next().await {
502
            results[i] = size;
503
        }
504
505
        // NOTE: We intentionally *NEVER* check the fast store, this is to
506
        // ensure that we re-upload data to the slow store if it only exists
507
        // in the fast store.  This does not affect workers as they do not
508
        // check existence through `has` and instead go direct to loading the
509
        // data which bypasses the check and will load from the fast store if
510
        // it does not exist in the slow store.
511
512
        Ok(())
513
27
    }
514
515
    async fn update(
516
        self: Pin<&Self>,
517
        key: StoreKey<'_>,
518
        mut reader: DropCloserReadHalf,
519
        size_info: UploadSizeInfo,
520
137
    ) -> Result<u64, Error> {
521
        // If either one of our stores is a noop store, bypass the multiplexing
522
        // and just use the store that is not a noop store.
523
        let ignore_slow = self
524
            .slow_store
525
            .inner_store(Some(key.borrow()))
526
            .optimized_for(StoreOptimizations::NoopUpdates)
527
            || self.slow_direction == StoreDirection::ReadOnly
528
            || self.slow_direction == StoreDirection::Get;
529
        let ignore_fast = self
530
            .fast_store
531
            .inner_store(Some(key.borrow()))
532
            .optimized_for(StoreOptimizations::NoopUpdates)
533
            || self.fast_direction == StoreDirection::ReadOnly
534
            || self.fast_direction == StoreDirection::Get;
535
        if ignore_slow && ignore_fast {
536
            // We need to drain the reader to avoid the writer complaining that we dropped
537
            // the connection prematurely.
538
            return reader.drain().await.err_tip(|| "In FastFlowStore::update");
539
        }
540
        if ignore_slow {
541
            return self.fast_store.update(key, reader, size_info).await;
542
        }
543
        let slow_in_flight_guard = self.register_in_flight_slow_write(key.borrow());
544
        if ignore_fast {
545
            let result = self
546
                .slow_store
547
                .update(key.borrow(), reader, size_info)
548
                .await;
549
            if let Ok(size) = &result {
550
                slow_in_flight_guard.complete(Some(*size));
551
            }
552
            return result;
553
        }
554
555
        let (mut fast_tx, fast_rx) = make_buf_channel_pair();
556
        let (mut slow_tx, slow_rx) = make_buf_channel_pair();
557
558
        let key_debug = format!("{key:?}");
559
        trace!(
560
            key = %key_debug,
561
            "FastSlowStore::update: starting dual-store upload",
562
        );
563
        let update_start = std::time::Instant::now();
564
565
131
        let data_stream_fut = async move {
566
131
            let mut bytes_sent: u64 = 0;
567
            loop {
568
214
                let buffer = reader
569
214
                    .recv()
570
214
                    .await
571
214
                    .err_tip(|| "Failed to read buffer in fastslow store")
?0
;
572
214
                if buffer.is_empty() {
573
                    // EOF received.
574
131
                    fast_tx.send_eof().err_tip(
575
                        || "Failed to write eof to fast store in fast_slow store update",
576
0
                    )?;
577
131
                    slow_tx
578
131
                        .send_eof()
579
131
                        .err_tip(|| "Failed to write eof to writer in fast_slow store update")
?0
;
580
131
                    debug!(
581
                        total_bytes = bytes_sent,
582
                        "FastSlowStore::update: data_stream sent EOF to both stores",
583
                    );
584
131
                    return Result::<u64, Error>::Ok(bytes_sent);
585
83
                }
586
587
83
                let chunk_len = buffer.len();
588
83
                let send_start = std::time::Instant::now();
589
83
                let (fast_result, slow_result) =
590
83
                    join!(fast_tx.send(buffer.clone()), slow_tx.send(buffer));
591
83
                let send_elapsed = send_start.elapsed();
592
83
                if send_elapsed.as_secs() >= 5 {
593
0
                    warn!(
594
                        chunk_len,
595
0
                        send_elapsed_ms = send_elapsed.as_millis(),
596
                        total_bytes = bytes_sent,
597
                        "FastSlowStore::update: channel send stalled (>5s). A downstream store may be hanging",
598
                    );
599
83
                }
600
83
                bytes_sent += u64::try_from(chunk_len).unwrap_or(u64::MAX);
601
83
                fast_result
602
83
                    .map_err(|e| 
{0
603
0
                        make_err!(
604
0
                            Code::Internal,
605
                            "Failed to send message to fast_store in fast_slow_store {:?}",
606
                            e
607
                        )
608
0
                    })
609
83
                    .merge(slow_result.map_err(|e| 
{0
610
0
                        make_err!(
611
0
                            Code::Internal,
612
                            "Failed to send message to slow_store in fast_slow store {:?}",
613
                            e
614
                        )
615
0
                    }))?;
616
            }
617
131
        };
618
619
        let fast_store_fut = self.fast_store.update(key.borrow(), fast_rx, size_info);
620
        let slow_store_fut = self.slow_store.update(key.borrow(), slow_rx, size_info);
621
622
        let (data_stream_res, fast_res, slow_res) =
623
            join!(data_stream_fut, fast_store_fut, slow_store_fut);
624
625
        if let Ok(size) = slow_res {
626
            slow_in_flight_guard.complete(Some(size));
627
        } else {
628
            drop(slow_in_flight_guard);
629
        }
630
631
        let total_elapsed = update_start.elapsed();
632
        if data_stream_res.is_err() || fast_res.is_err() || slow_res.is_err() {
633
            let all_not_found = [&data_stream_res, &fast_res, &slow_res]
634
                .iter()
635
0
                .all(|r| match r {
636
0
                    Ok(_size) => true,
637
0
                    Err(e) => e.code == Code::NotFound,
638
0
                });
639
            if all_not_found {
640
                info!(
641
                    key = %key_debug,
642
                    elapsed_ms = total_elapsed.as_millis(),
643
                    data_stream_ok = data_stream_res.is_ok(),
644
                    fast_store_ok = fast_res.is_ok(),
645
                    slow_store_ok = slow_res.is_ok(),
646
                    "FastSlowStore::update: completed with NotFound error(s)",
647
                );
648
            } else {
649
                warn!(
650
                    key = %key_debug,
651
                    elapsed_ms = total_elapsed.as_millis(),
652
                    data_stream_ok = data_stream_res.is_ok(),
653
                    fast_store_ok = fast_res.is_ok(),
654
                    slow_store_ok = slow_res.is_ok(),
655
                    "FastSlowStore::update: completed with error(s)",
656
                );
657
            }
658
        } else {
659
            trace!(
660
                key = %key_debug,
661
                elapsed_ms = total_elapsed.as_millis(),
662
                "FastSlowStore::update: completed successfully",
663
            );
664
        }
665
        data_stream_res.merge(fast_res).merge(slow_res)
666
137
    }
667
668
    /// `FastSlowStore` has optimizations for dealing with files.
669
0
    fn optimized_for(&self, optimization: StoreOptimizations) -> bool {
670
0
        optimization == StoreOptimizations::FileUpdates
671
0
    }
672
673
    /// Optimized variation to consume the file if one of the stores is a
674
    /// filesystem store. This makes the operation a move instead of a copy
675
    /// dramatically increasing performance for large files.
676
    async fn update_with_whole_file(
677
        self: Pin<&Self>,
678
        key: StoreKey<'_>,
679
        path: OsString,
680
        mut file: fs::FileSlot,
681
        upload_size: UploadSizeInfo,
682
13
    ) -> Result<(u64, Option<fs::FileSlot>), Error> {
683
        trace!(
684
            key = ?key,
685
            ?upload_size,
686
            "FastSlowStore::update_with_whole_file: starting",
687
        );
688
        if self
689
            .fast_store
690
            .optimized_for(StoreOptimizations::FileUpdates)
691
        {
692
            if !self
693
                .slow_store
694
                .inner_store(Some(key.borrow()))
695
                .optimized_for(StoreOptimizations::NoopUpdates)
696
                && self.slow_direction != StoreDirection::ReadOnly
697
                && self.slow_direction != StoreDirection::Get
698
            {
699
                trace!("FastSlowStore::update_with_whole_file: uploading to slow_store");
700
                let slow_start = std::time::Instant::now();
701
                let slow_in_flight_guard = self.register_in_flight_slow_write(key.borrow());
702
                let size = slow_update_store_with_file(
703
                    self.slow_store.as_store_driver_pin(),
704
                    key.borrow(),
705
                    &mut file,
706
                    upload_size,
707
                )
708
                .await
709
                .err_tip(|| "In FastSlowStore::update_with_whole_file slow_store")?;
710
                slow_in_flight_guard.complete(Some(size));
711
                trace!(
712
                    elapsed_ms = slow_start.elapsed().as_millis(),
713
                    "FastSlowStore::update_with_whole_file: slow_store upload completed",
714
                );
715
            }
716
            if self.fast_direction == StoreDirection::ReadOnly
717
                || self.fast_direction == StoreDirection::Get
718
            {
719
                let file_size = match upload_size {
720
                    UploadSizeInfo::ExactSize(size) => size,
721
                    UploadSizeInfo::MaxSize(_) => file
722
                        .as_ref()
723
                        .metadata()
724
                        .await
725
0
                        .err_tip(|| format!("While reading metadata for {}", path.display()))?
726
                        .len(),
727
                };
728
                return Ok((file_size, Some(file)));
729
            }
730
            return self
731
                .fast_store
732
                .update_with_whole_file(key, path, file, upload_size)
733
                .await;
734
        }
735
736
        if self
737
            .slow_store
738
            .optimized_for(StoreOptimizations::FileUpdates)
739
        {
740
            let ignore_fast = self
741
                .fast_store
742
                .inner_store(Some(key.borrow()))
743
                .optimized_for(StoreOptimizations::NoopUpdates)
744
                || self.fast_direction == StoreDirection::ReadOnly
745
                || self.fast_direction == StoreDirection::Get;
746
            let maybe_size = if ignore_fast {
747
                None
748
            } else {
749
                Some(
750
                    slow_update_store_with_file(
751
                        self.fast_store.as_store_driver_pin(),
752
                        key.borrow(),
753
                        &mut file,
754
                        upload_size,
755
                    )
756
                    .await
757
                    .err_tip(|| "In FastSlowStore::update_with_whole_file fast_store")?,
758
                )
759
            };
760
            let ignore_slow = self.slow_direction == StoreDirection::ReadOnly
761
                || self.slow_direction == StoreDirection::Get;
762
            if ignore_slow {
763
                let size = match maybe_size {
764
                    Some(size) => size,
765
                    None => match upload_size {
766
                        UploadSizeInfo::ExactSize(size) => size,
767
                        UploadSizeInfo::MaxSize(_) => file
768
                            .as_ref()
769
                            .metadata()
770
                            .await
771
0
                            .err_tip(|| format!("While reading metadata for {}", path.display()))?
772
                            .len(),
773
                    },
774
                };
775
                return Ok((size, Some(file)));
776
            }
777
            let slow_in_flight_guard: InFlightSlowWriteGuard =
778
                self.register_in_flight_slow_write(key.borrow());
779
            let (size, maybe_file_slot) = self
780
                .slow_store
781
                .update_with_whole_file(key.borrow(), path, file, upload_size)
782
                .await?;
783
            slow_in_flight_guard.complete(Some(size));
784
            return Ok((size, maybe_file_slot));
785
        }
786
787
        let size = slow_update_store_with_file(self, key, &mut file, upload_size)
788
            .await
789
            .err_tip(|| "In FastSlowStore::update_with_whole_file")?;
790
        Ok((size, Some(file)))
791
13
    }
792
793
    async fn get_part(
794
        self: Pin<&Self>,
795
        key: StoreKey<'_>,
796
        writer: &mut DropCloserWriteHalf,
797
        offset: u64,
798
        length: Option<u64>,
799
607
    ) -> Result<(), Error> {
800
        // `has()` can report a stale map entry whose file is gone, so
801
        // get_part may still return NotFound; fall through to the slow
802
        // store unless we have already streamed bytes to the caller.
803
        // One existence check, reused: this is the hot read path and `has()`
804
        // on a filesystem fast store is a syscall.
805
        let in_fast_store = self.fast_store.has(key.borrow()).await?.is_some();
806
        if !in_fast_store {
807
            record_store_tier_read("fast", "miss");
808
        }
809
        if in_fast_store {
810
            let bytes_before = writer.get_bytes_written();
811
            match self
812
                .fast_store
813
                .get_part(key.borrow(), writer.borrow_mut(), offset, length)
814
                .await
815
            {
816
                Ok(()) => {
817
                    self.metrics
818
                        .fast_store_hit_count
819
                        .fetch_add(1, Ordering::Acquire);
820
                    self.metrics
821
                        .fast_store_downloaded_bytes
822
                        .fetch_add(writer.get_bytes_written(), Ordering::Acquire);
823
                    record_store_tier_read("fast", "hit");
824
                    record_store_tier_io("fast", "read", writer.get_bytes_written());
825
                    return Ok(());
826
                }
827
                Err(e)
828
                    if e.code == Code::NotFound && writer.get_bytes_written() == bytes_before =>
829
                {
830
                    self.metrics
831
                        .fast_store_stale_map_falls_through
832
                        .fetch_add(1, Ordering::Acquire);
833
                    record_store_tier_read("fast", "stale");
834
                    warn!(%key, ?e, "Stale fast-store map entry; falling through to slow store");
835
                    // fall through to populate path
836
                }
837
                Err(e) => return Err(e),
838
            }
839
        }
840
841
        // If the fast store is noop or read only or update only then bypass it.
842
        if self
843
            .fast_store
844
            .inner_store(Some(key.borrow()))
845
            .optimized_for(StoreOptimizations::NoopUpdates)
846
            || self.fast_direction == StoreDirection::ReadOnly
847
            || self.fast_direction == StoreDirection::Update
848
        {
849
            self.metrics
850
                .slow_store_hit_count
851
                .fetch_add(1, Ordering::Acquire);
852
            self.slow_store
853
                .get_part(key, writer.borrow_mut(), offset, length)
854
                .await?;
855
            self.metrics
856
                .slow_store_downloaded_bytes
857
                .fetch_add(writer.get_bytes_written(), Ordering::Acquire);
858
            record_store_tier_read("slow", "hit");
859
            record_store_tier_io("slow", "read", writer.get_bytes_written());
860
            return Ok(());
861
        }
862
863
        // Huge blobs: dedup is a net loss (followers time out anyway and
864
        // the fast tier is evicted), so read straight from the slow store.
865
        if self.should_bypass_dedup(&key) {
866
            self.metrics
867
                .huge_blob_dedup_bypasses
868
                .fetch_add(1, Ordering::Acquire);
869
            self.metrics
870
                .slow_store_hit_count
871
                .fetch_add(1, Ordering::Acquire);
872
            debug!(%key, threshold_bytes = self.bypass_dedup_threshold_bytes, "Bypassing dedup for huge blob");
873
            self.slow_store
874
                .get_part(key, writer.borrow_mut(), offset, length)
875
                .await
876
                .err_tip(|| "In FastSlowStore::get_part huge-blob bypass")?;
877
            self.metrics
878
                .slow_store_downloaded_bytes
879
                .fetch_add(writer.get_bytes_written(), Ordering::Acquire);
880
            record_store_tier_read("slow", "hit");
881
            record_store_tier_io("slow", "read", writer.get_bytes_written());
882
            return Ok(());
883
        }
884
885
        let mut writer = Some(writer);
886
887
        // Drive the dedup loader. Two distinct paths:
888
        //
889
        //   * Leader (created the OnceCell entry): runs `populate` with
890
        //     OUR `writer`, streaming directly to the caller while
891
        //     filling the fast cache. No timeout: a multi-GB blob
892
        //     legitimately takes minutes to stream, and cancelling our
893
        //     own populate would propagate the failure to every other
894
        //     reader of this digest.
895
        //
896
        //   * Follower: bound the wait so a wedged leader does not pin
897
        //     us until the upstream `gRPC` deadline fires. The follower
898
        //     closure passes `None` for `writer`. This is critical: if
899
        //     the OnceCell ever promotes our follower closure to leader
900
        //     (because the original leader's future was dropped), and
901
        //     our `tokio::time::timeout` then cancels it, *no* caller
902
        //     `writer` was ever moved into the populate stream, so no
903
        //     `gRPC` sender is dropped without EOF. The follower then
904
        //     re-enters `get_part` below and reads from the now-warm
905
        //     fast cache, OR falls back to the slow store on timeout.
906
        let needs_slow_store_fallback: bool = {
907
            let loader_guard = self.get_loader(key.borrow());
908
            let is_leader = loader_guard.is_leader;
909
            if is_leader {
910
                loader_guard
911
337
                    .get_or_try_init(|| {
912
337
                        self.populate_and_maybe_stream(key.borrow(), writer.take(), offset, length)
913
337
                    })
914
                    .await?;
915
                false
916
            } else {
917
0
                let load_fut = loader_guard.get_or_try_init(|| {
918
0
                    self.populate_and_maybe_stream(key.borrow(), None, offset, length)
919
0
                });
920
                match tokio::time::timeout(LEADER_WAIT_TIMEOUT, load_fut).await {
921
                    Ok(result) => {
922
                        result?;
923
                        false
924
                    }
925
                    Err(_elapsed) => {
926
                        self.metrics
927
                            .leader_wait_timeouts
928
                            .fetch_add(1, Ordering::Acquire);
929
                        warn!(
930
                            %key,
931
                            timeout_secs = LEADER_WAIT_TIMEOUT.as_secs(),
932
                            "FastSlowStore::get_part: leader-wait exceeded timeout, bypassing dedup and reading slow store directly",
933
                        );
934
                        true
935
                    }
936
                }
937
            }
938
        };
939
940
        if needs_slow_store_fallback && let Some(writer) = writer.take() {
941
            return self
942
                .slow_store
943
                .get_part(key, writer, offset, length)
944
                .await
945
                .err_tip(
946
                    || "In FastSlowStore::get_part slow_store fallback after leader-wait timeout",
947
                );
948
        }
949
950
        // If we didn't stream then re-enter which will stream from the fast
951
        // store, or retry the download.  We should not get in a loop here
952
        // because OnceCell has the good sense to retry for all callers so in
953
        // order to get here the fast store will have been populated.  There's
954
        // an outside chance it was evicted, but that's slim.
955
        if let Some(writer) = writer.take() {
956
            self.get_part(key, writer, offset, length).await
957
        } else {
958
            // This was the thread that did the streaming already, lucky duck.
959
            Ok(())
960
        }
961
607
    }
962
963
2
    fn inner_store(&self, _key: Option<StoreKey>) -> &dyn StoreDriver {
964
2
        self
965
2
    }
966
967
2
    fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) {
968
2
        self
969
2
    }
970
971
0
    fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> {
972
0
        self
973
0
    }
974
975
0
    fn register_remove_callback(self: Arc<Self>, callback: RemoveCallback) -> Result<(), Error> {
976
0
        self.fast_store.register_remove_callback(callback.clone())?;
977
0
        self.slow_store.register_remove_callback(callback)?;
978
0
        Ok(())
979
0
    }
980
}
981
982
#[derive(Debug, Default, MetricsComponent)]
983
struct FastSlowStoreMetrics {
984
    #[metric(help = "Hit count for the fast store")]
985
    fast_store_hit_count: AtomicU64,
986
    #[metric(help = "Downloaded bytes from the fast store")]
987
    fast_store_downloaded_bytes: AtomicU64,
988
    #[metric(help = "Hit count for the slow store")]
989
    slow_store_hit_count: AtomicU64,
990
    #[metric(help = "Downloaded bytes from the slow store")]
991
    slow_store_downloaded_bytes: AtomicU64,
992
    #[metric(
993
        help = "Number of times a follower bypassed the populating-digests dedup because the leader exceeded LEADER_WAIT_TIMEOUT"
994
    )]
995
    leader_wait_timeouts: AtomicU64,
996
    #[metric(help = "get_part calls that bypassed dedup for huge blobs")]
997
    huge_blob_dedup_bypasses: AtomicU64,
998
    #[metric(help = "Stale fast-store map entries that fell through to the slow store")]
999
    fast_store_stale_map_falls_through: AtomicU64,
1000
}
1001
1002
/// Maximum time a follower will wait on the leader-populator before
1003
/// bypassing the dedup map and reading directly from the slow store.
1004
///
1005
/// Without this bound a single wedged populator would block every
1006
/// concurrent reader of the same digest until each one's own `gRPC`
1007
/// deadline fired (e.g. Bazel's `--remote_timeout`), turning a
1008
/// single slow read into a fan-out of `DEADLINE_EXCEEDED` errors.
1009
const LEADER_WAIT_TIMEOUT: Duration = Duration::from_mins(1);
1010
1011
default_health_status_indicator!(FastSlowStore);