/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::NotFound3 => { |
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); |