/build/source/nativelink-store/src/grpc_store.rs
Line | Count | Source |
1 | | // Copyright 2024 Trace Machina, Inc. All rights reserved. |
2 | | // |
3 | | // Licensed under the Functional Source License, Version 1.1, Apache 2.0 Future License (the "License"); |
4 | | // you may not use this file except in compliance with the License. |
5 | | // You may obtain a copy of the License at |
6 | | // |
7 | | // See LICENSE file for details |
8 | | // |
9 | | // Unless required by applicable law or agreed to in writing, software |
10 | | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | | // See the License for the specific language governing permissions and |
13 | | // limitations under the License. |
14 | | |
15 | | use core::pin::Pin; |
16 | | use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; |
17 | | use core::time::Duration; |
18 | | use std::borrow::Cow; |
19 | | use std::collections::{HashMap, VecDeque}; |
20 | | use std::sync::{Arc, Weak}; |
21 | | |
22 | | use async_trait::async_trait; |
23 | | use bytes::{Bytes, BytesMut}; |
24 | | use futures::stream::{FuturesUnordered, unfold}; |
25 | | use futures::{Future, Stream, StreamExt, TryFutureExt, TryStreamExt, future}; |
26 | | use nativelink_config::stores::{GrpcReadBatchingConfig, GrpcSpec}; |
27 | | use nativelink_error::{Error, ResultExt, error_if, make_err}; |
28 | | use nativelink_metric::MetricsComponent; |
29 | | use nativelink_proto::build::bazel::remote::execution::v2::action_cache_client::ActionCacheClient; |
30 | | use nativelink_proto::build::bazel::remote::execution::v2::content_addressable_storage_client::ContentAddressableStorageClient; |
31 | | use nativelink_proto::build::bazel::remote::execution::v2::{ |
32 | | ActionResult, BatchReadBlobsRequest, BatchReadBlobsResponse, BatchUpdateBlobsRequest, |
33 | | BatchUpdateBlobsResponse, FindMissingBlobsRequest, FindMissingBlobsResponse, |
34 | | GetActionResultRequest, GetTreeRequest, GetTreeResponse, SpliceBlobRequest, SpliceBlobResponse, |
35 | | SplitBlobRequest, SplitBlobResponse, UpdateActionResultRequest, batch_update_blobs_request, |
36 | | compressor, |
37 | | }; |
38 | | use nativelink_proto::google::bytestream::byte_stream_client::ByteStreamClient; |
39 | | use nativelink_proto::google::bytestream::{ |
40 | | QueryWriteStatusRequest, QueryWriteStatusResponse, ReadRequest, ReadResponse, WriteRequest, |
41 | | WriteResponse, |
42 | | }; |
43 | | use nativelink_util::buf_channel::{ |
44 | | DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair, |
45 | | }; |
46 | | use nativelink_util::common::DigestInfo; |
47 | | use nativelink_util::connection_manager::ConnectionManager; |
48 | | use nativelink_util::digest_hasher::{DigestHasherFunc, default_digest_hasher_func}; |
49 | | use nativelink_util::health_utils::HealthStatusIndicator; |
50 | | use nativelink_util::proto_stream_utils::{ |
51 | | FirstStream, MAX_WRITE_REQUEST_DATA_BYTES, WriteRequestStreamWrapper, WriteState, |
52 | | WriteStateWrapper, |
53 | | }; |
54 | | use nativelink_util::resource_info::ResourceInfo; |
55 | | use nativelink_util::retry::{Retrier, RetryResult}; |
56 | | use nativelink_util::store_trait::{RemoveCallback, StoreDriver, StoreKey, UploadSizeInfo}; |
57 | | use nativelink_util::telemetry::{forward_client_headers, inject_context}; |
58 | | use nativelink_util::wire_compression::{ |
59 | | stream_decode_compressed_upload, stream_encode_compressed_upload_from_reader, |
60 | | }; |
61 | | use nativelink_util::{background_spawn, default_health_status_indicator, tls_utils}; |
62 | | use opentelemetry::context::Context; |
63 | | use parking_lot::Mutex; |
64 | | use prost::Message; |
65 | | use tokio::sync::{Semaphore, oneshot}; |
66 | | use tokio::time::sleep; |
67 | | use tonic::metadata::{Ascii, MetadataKey, MetadataValue}; |
68 | | use tonic::{Code, IntoRequest, Request, Response, Status, Streaming}; |
69 | | use tracing::{debug, error, trace, warn}; |
70 | | use uuid::Uuid; |
71 | | |
72 | | /// Adds configured static headers, forwards nominated client request headers, |
73 | | /// and injects the current OpenTelemetry trace context into an outgoing gRPC |
74 | | /// request. |
75 | 53 | fn enrich_request<T>( |
76 | 53 | mut request: Request<T>, |
77 | 53 | headers: &[(MetadataKey<Ascii>, MetadataValue<Ascii>)], |
78 | 53 | forward_headers: &[String], |
79 | 53 | ) -> Request<T> { |
80 | 53 | for (key1 , value1 ) in headers { |
81 | 1 | request.metadata_mut().insert(key.clone(), value.clone()); |
82 | 1 | } |
83 | 53 | forward_client_headers(&mut request, forward_headers); |
84 | 53 | inject_context(&mut request); |
85 | 53 | request |
86 | 53 | } |
87 | | |
88 | | /// Minimum blob size for wire compression when |
89 | | /// `experimental_remote_cache_compression` is enabled. Below this, zstd |
90 | | /// framing overhead and per-blob CPU outweigh the wire savings (small blobs |
91 | | /// are the batching paths' domain). |
92 | | const WIRE_COMPRESSION_MIN_SIZE_BYTES: u64 = 64 * 1024; |
93 | | |
94 | | /// Zstd level for `GrpcStore`'s own compressed transfers. Level 1: measured on |
95 | | /// artifact-shaped corpora, higher levels bought no meaningful wire reduction |
96 | | /// while encoding slower, and internal hops are throughput-sensitive. |
97 | | const WIRE_COMPRESSION_ZSTD_LEVEL: i32 = 1; |
98 | | |
99 | | /// Estimated per-entry protobuf and framing overhead charged against |
100 | | /// `max_batch_bytes`, so that batches of many tiny blobs cannot push a |
101 | | /// `BatchReadBlobs` response over the gRPC message size limit. |
102 | | const BATCH_READ_PER_ENTRY_OVERHEAD_BYTES: u64 = 256; |
103 | | |
104 | | /// A small-blob read waiting to be coalesced into a `BatchReadBlobs` RPC. |
105 | | #[derive(Debug)] |
106 | | struct PendingRead { |
107 | | digest: DigestInfo, |
108 | | digest_function: i32, |
109 | | tx: oneshot::Sender<Result<Bytes, Error>>, |
110 | | } |
111 | | |
112 | | /// The pending small-blob reads plus the total payload bytes they declare. |
113 | | #[derive(Debug, Default)] |
114 | | struct ReadQueue { |
115 | | items: VecDeque<PendingRead>, |
116 | | bytes: u64, |
117 | | } |
118 | | |
119 | | /// State for coalescing small-blob reads into `BatchReadBlobs` RPCs. |
120 | | /// |
121 | | /// This uses a slot-based group commit scheme: callers enqueue their read and |
122 | | /// then try to start a detached dispatcher task by acquiring one of |
123 | | /// `dispatch_slots` semaphore permits. A dispatcher repeatedly drains up to |
124 | | /// `max_batch_bytes` worth of pending reads into a single `BatchReadBlobs` |
125 | | /// request until the queue is empty. This is work-conserving (a read never |
126 | | /// waits while a dispatch slot is free) and uses no timers. |
127 | | #[derive(Debug, MetricsComponent)] |
128 | | struct ReadBatcher { |
129 | | max_blob_size_bytes: u64, |
130 | | max_batch_bytes: u64, |
131 | | max_queued_bytes: u64, |
132 | | queue: Mutex<ReadQueue>, |
133 | | dispatch_slots: Arc<Semaphore>, |
134 | | #[metric(help = "Number of BatchReadBlobs RPCs sent by the read coalescer")] |
135 | | batches_sent: AtomicU64, |
136 | | #[metric(help = "Number of blob reads coalesced into BatchReadBlobs RPCs")] |
137 | | blobs_batched: AtomicU64, |
138 | | #[metric( |
139 | | help = "Number of reads that bypassed batching because the queue byte budget was full" |
140 | | )] |
141 | | queue_bypasses: AtomicU64, |
142 | | #[metric(help = "Number of batched reads that resolved to a per-entry error")] |
143 | | batched_read_errors: AtomicU64, |
144 | | #[metric(help = "Payload bytes currently waiting in the read coalescer queue")] |
145 | | queued_bytes: AtomicU64, |
146 | | } |
147 | | |
148 | | impl ReadBatcher { |
149 | 6 | fn new(config: &GrpcReadBatchingConfig) -> Self { |
150 | 6 | Self { |
151 | 6 | max_blob_size_bytes: config.max_blob_size_bytes, |
152 | 6 | max_batch_bytes: config.max_batch_bytes, |
153 | 6 | max_queued_bytes: config.max_queued_bytes, |
154 | 6 | queue: Mutex::new(ReadQueue::default()), |
155 | 6 | dispatch_slots: Arc::new(Semaphore::new(config.dispatch_slots)), |
156 | 6 | batches_sent: AtomicU64::new(0), |
157 | 6 | blobs_batched: AtomicU64::new(0), |
158 | 6 | queue_bypasses: AtomicU64::new(0), |
159 | 6 | batched_read_errors: AtomicU64::new(0), |
160 | 6 | queued_bytes: AtomicU64::new(0), |
161 | 6 | } |
162 | 6 | } |
163 | | } |
164 | | |
165 | | /// Mirrors the default retryable-code classification used by |
166 | | /// `nativelink_util::retry::Retrier::should_retry`: only codes that are |
167 | | /// always terminal are considered non-retryable. |
168 | 10 | const fn is_retryable_code(code: Code) -> bool { |
169 | 6 | !matches!( |
170 | 10 | code, |
171 | | Code::Ok |
172 | | | Code::InvalidArgument |
173 | | | Code::FailedPrecondition |
174 | | | Code::OutOfRange |
175 | | | Code::Unimplemented |
176 | | | Code::NotFound |
177 | | | Code::AlreadyExists |
178 | | | Code::PermissionDenied |
179 | | | Code::Unauthenticated |
180 | | ) |
181 | 10 | } |
182 | | |
183 | | // This store is usually a pass-through store, but can also be used as a CAS store. Using it as an |
184 | | // AC store has one major side-effect... The has() function may not give the proper size of the |
185 | | // underlying data. This might cause issues if embedded in certain stores. |
186 | | /// Starting capacity for the stitched-together batch response. Only a hint: |
187 | | /// most batches are one chunk, and the vec grows if not. |
188 | | const MAX_BATCH_UPDATE_ENTRIES_HINT: usize = 256; |
189 | | |
190 | | /// Groups blobs so no single `BatchUpdateBlobs` RPC carries more than |
191 | | /// `MAX_WRITE_REQUEST_DATA_BYTES` of payload. |
192 | | /// |
193 | | /// An entry larger than the cap on its own still goes out alone. Nothing can |
194 | | /// be done for it here, since a batch entry cannot be split across messages, |
195 | | /// and REAPI already tells clients to use `ByteStream` for blobs that size. |
196 | 1 | fn split_batch_update_requests( |
197 | 1 | requests: Vec<batch_update_blobs_request::Request>, |
198 | 1 | ) -> Vec<Vec<batch_update_blobs_request::Request>> { |
199 | 1 | let mut chunks = Vec::new(); |
200 | 1 | let mut current: Vec<batch_update_blobs_request::Request> = Vec::new(); |
201 | 1 | let mut current_bytes = 0usize; |
202 | | |
203 | 12 | for request in requests1 { |
204 | 12 | let len = request.data.len(); |
205 | 12 | if !current.is_empty() && current_bytes11 .saturating_add(len) > MAX_WRITE_REQUEST_DATA_BYTES { |
206 | 5 | chunks.push(core::mem::take(&mut current)); |
207 | 5 | current_bytes = 0; |
208 | 7 | } |
209 | 12 | current_bytes = current_bytes.saturating_add(len); |
210 | 12 | current.push(request); |
211 | | } |
212 | | // An empty batch still needs one RPC: the caller expects the upstream's |
213 | | // answer to an empty request, not a synthesised one. |
214 | 1 | if !current.is_empty() || chunks0 .is_empty0 () { |
215 | 1 | chunks.push(current); |
216 | 1 | }0 |
217 | 1 | chunks |
218 | 1 | } |
219 | | |
220 | | #[derive(Debug, MetricsComponent)] |
221 | | pub struct GrpcStore { |
222 | | #[metric(help = "Instance name for the store")] |
223 | | instance_name: String, |
224 | | store_type: nativelink_config::stores::StoreType, |
225 | | retrier: Retrier, |
226 | | connection_manager: ConnectionManager, |
227 | | /// Per-RPC timeout. `Duration::ZERO` means disabled. |
228 | | rpc_timeout: Duration, |
229 | | use_legacy_resource_names: bool, |
230 | | headers: Vec<(MetadataKey<Ascii>, MetadataValue<Ascii>)>, |
231 | | forward_headers: Vec<String>, |
232 | | /// When configured, coalesces small-blob reads into `BatchReadBlobs` |
233 | | /// RPCs. `None` means reads always use the `ByteStream` `Read` path. |
234 | | #[metric(group = "read_batcher")] |
235 | | read_batcher: Option<ReadBatcher>, |
236 | | remote_cache_compression_enabled: bool, |
237 | | /// Used by the read coalescer to hand a strong reference of this store |
238 | | /// to detached dispatcher tasks. |
239 | | weak_self: Weak<Self>, |
240 | | } |
241 | | |
242 | | impl GrpcStore { |
243 | 48 | pub fn new(spec: &GrpcSpec) -> Result<Arc<Self>, Error> { |
244 | 48 | Self::new_with_jitter(spec, spec.retry.make_jitter_fn()) |
245 | 48 | } |
246 | | |
247 | 48 | pub fn new_with_jitter( |
248 | 48 | spec: &GrpcSpec, |
249 | 48 | jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>, |
250 | 48 | ) -> Result<Arc<Self>, Error> { |
251 | 0 | error_if!( |
252 | 44 | matches!( |
253 | 48 | spec.store_type, |
254 | | nativelink_config::stores::StoreType::EventSink |
255 | 4 | ) && spec.experimental_read_batching.is_some(), |
256 | | "An event sink cannot batch reads" |
257 | | ); |
258 | 0 | error_if!( |
259 | 48 | spec.endpoints.is_empty(), |
260 | | "Expected at least 1 endpoint in GrpcStore" |
261 | | ); |
262 | 48 | let mut endpoints = Vec::with_capacity(spec.endpoints.len()); |
263 | 48 | for endpoint_config in &spec.endpoints { |
264 | 48 | let endpoint = tls_utils::endpoint(endpoint_config).map_err(|e| {0 |
265 | 0 | Error::from_std_err(Code::InvalidArgument, &e) |
266 | 0 | .append("Invalid URI for GrpcStore endpoint") |
267 | 0 | })?; |
268 | 48 | endpoints.push(endpoint); |
269 | | } |
270 | | |
271 | 48 | let rpc_timeout = Duration::from_secs(spec.rpc_timeout_s); |
272 | | |
273 | 48 | let read_batcher47 = match &spec.experimental_read_batching { |
274 | 7 | Some(config) => { |
275 | 0 | error_if!( |
276 | 7 | config.dispatch_slots == 0, |
277 | | "experimental_read_batching.dispatch_slots must be greater than zero" |
278 | | ); |
279 | | // Batched reads share one upstream RPC across many client |
280 | | // requests, so per-client forwarded headers (e.g. credentials) |
281 | | // cannot be attached correctly. |
282 | 6 | error_if!( |
283 | 7 | !spec.forward_headers.is_empty(), |
284 | | "experimental_read_batching is incompatible with forward_headers" |
285 | | ); |
286 | 6 | Some(ReadBatcher::new(config)) |
287 | | } |
288 | 41 | None => None, |
289 | | }; |
290 | | |
291 | 47 | let mut headers = Vec::with_capacity(spec.headers.len()); |
292 | 47 | for (name1 , value1 ) in &spec.headers { |
293 | | // We lowercase keys as HTTP headers are case-insensitive so we should match all cases |
294 | 1 | let key = MetadataKey::from_bytes(name.to_lowercase().as_bytes()).map_err(|_| {0 |
295 | 0 | make_err!(Code::InvalidArgument, "Invalid gRPC metadata key: {name}") |
296 | 0 | })?; |
297 | 1 | let val = MetadataValue::try_from(value.as_str()).map_err(|_| {0 |
298 | 0 | make_err!( |
299 | 0 | Code::InvalidArgument, |
300 | | "Invalid gRPC metadata value for key: {name}" |
301 | | ) |
302 | 0 | })?; |
303 | 1 | headers.push((key, val)); |
304 | | } |
305 | | |
306 | 47 | Ok(Arc::new_cyclic(|weak_self| Self { |
307 | 47 | weak_self: weak_self.clone(), |
308 | 47 | instance_name: spec.instance_name.clone(), |
309 | 47 | store_type: spec.store_type, |
310 | 47 | retrier: Retrier::new( |
311 | 47 | Arc::new(|duration| Box::pin2 (sleep2 (duration2 ))), |
312 | 47 | jitter_fn.clone(), |
313 | 47 | spec.retry.clone(), |
314 | | ) |
315 | 47 | .with_client("grpc_store"), |
316 | 47 | connection_manager: ConnectionManager::new( |
317 | 47 | endpoints, |
318 | 47 | spec.connections_per_endpoint, |
319 | 47 | spec.max_concurrent_requests, |
320 | 47 | spec.retry.clone(), |
321 | 47 | jitter_fn, |
322 | | ), |
323 | 47 | rpc_timeout, |
324 | 47 | use_legacy_resource_names: spec.use_legacy_resource_names, |
325 | 47 | read_batcher, |
326 | 47 | remote_cache_compression_enabled: spec.remote_cache_compression_enabled(), |
327 | 47 | headers, |
328 | | // We lowercase keys as HTTP headers are case-insensitive so we should match all cases |
329 | 47 | forward_headers: spec |
330 | 47 | .forward_headers |
331 | 47 | .iter() |
332 | 47 | .map(|s| s1 .to_lowercase1 ()) |
333 | 47 | .collect(), |
334 | 47 | })) |
335 | 48 | } |
336 | | |
337 | 14 | async fn perform_request<F, Fut, R, I>(&self, input: I, mut request: F) -> Result<R, Error> |
338 | 14 | where |
339 | 14 | F: FnMut(I) -> Fut + Send + Copy, |
340 | 14 | Fut: Future<Output = Result<R, Error>> + Send, |
341 | 14 | R: Send, |
342 | 14 | I: Send + Clone, |
343 | 14 | { |
344 | 14 | self.retrier |
345 | 14 | .retry(unfold(input, move |input| async move { |
346 | 14 | let input_clone = input.clone(); |
347 | | Some(( |
348 | 14 | request(input_clone) |
349 | 14 | .await |
350 | 14 | .map_or_else(RetryResult::Retry, RetryResult::Ok), |
351 | 14 | input, |
352 | | )) |
353 | 28 | })) |
354 | 14 | .await |
355 | 14 | } |
356 | | |
357 | 1 | pub async fn find_missing_blobs( |
358 | 1 | &self, |
359 | 1 | grpc_request: Request<FindMissingBlobsRequest>, |
360 | 1 | ) -> Result<Response<FindMissingBlobsResponse>, Error> { |
361 | 0 | error_if!( |
362 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
363 | | "CAS operation on AC store" |
364 | | ); |
365 | | |
366 | 1 | let mut request = grpc_request.into_inner(); |
367 | | |
368 | | // Some builds (Chromium for example) do lots of empty requests for some reason, so shortcut them |
369 | 1 | if request.blob_digests.is_empty() { |
370 | 1 | return Ok(Response::new(FindMissingBlobsResponse { |
371 | 1 | missing_blob_digests: vec![], |
372 | 1 | })); |
373 | 0 | } |
374 | | |
375 | 0 | request.instance_name.clone_from(&self.instance_name); |
376 | 0 | self.perform_request(request, |request| async move { |
377 | 0 | let channel = self |
378 | 0 | .connection_manager |
379 | 0 | .connection(format!( |
380 | 0 | "find_missing_blobs: ({}) {:?}", |
381 | 0 | request.blob_digests.len(), |
382 | 0 | request.blob_digests |
383 | 0 | )) |
384 | 0 | .await |
385 | 0 | .err_tip(|| "in find_missing_blobs")?; |
386 | 0 | ContentAddressableStorageClient::new(channel) |
387 | 0 | .find_missing_blobs(enrich_request( |
388 | 0 | Request::new(request), |
389 | 0 | &self.headers, |
390 | 0 | &self.forward_headers, |
391 | 0 | )) |
392 | 0 | .await |
393 | 0 | .err_tip(|| "in GrpcStore::find_missing_blobs") |
394 | 0 | }) |
395 | 0 | .await |
396 | 1 | } |
397 | | |
398 | 1 | pub async fn batch_update_blobs( |
399 | 1 | &self, |
400 | 1 | grpc_request: Request<BatchUpdateBlobsRequest>, |
401 | 1 | ) -> Result<Response<BatchUpdateBlobsResponse>, Error> { |
402 | 0 | error_if!( |
403 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
404 | | "CAS operation on AC store" |
405 | | ); |
406 | | |
407 | 1 | let mut request = grpc_request.into_inner(); |
408 | 1 | request.instance_name.clone_from(&self.instance_name); |
409 | | |
410 | | // A batch is one message, so an oversized one cannot be chunked the |
411 | | // way a ByteStream write can; it has to become several RPCs. Split on |
412 | | // the accumulated payload and stitch the responses back together in |
413 | | // request order, which is what the caller matches results by. |
414 | 1 | let mut responses = |
415 | 1 | Vec::with_capacity(request.requests.len().min(MAX_BATCH_UPDATE_ENTRIES_HINT)); |
416 | 6 | for chunk in split_batch_update_requests1 (core::mem::take1 (&mut request.requests1 )) { |
417 | 6 | let chunk_request = BatchUpdateBlobsRequest { |
418 | 6 | instance_name: request.instance_name.clone(), |
419 | 6 | requests: chunk, |
420 | 6 | digest_function: request.digest_function, |
421 | 6 | }; |
422 | 6 | let response = self |
423 | 6 | .perform_request(chunk_request, |chunk_request| async move { |
424 | 6 | let channel = self |
425 | 6 | .connection_manager |
426 | 6 | .connection("batch_update_blobs".into()) |
427 | 6 | .await |
428 | 6 | .err_tip(|| "in batch_update_blobs")?0 ; |
429 | 6 | ContentAddressableStorageClient::new(channel) |
430 | 6 | .batch_update_blobs(enrich_request( |
431 | 6 | Request::new(chunk_request), |
432 | 6 | &self.headers, |
433 | 6 | &self.forward_headers, |
434 | 6 | )) |
435 | 6 | .await |
436 | 6 | .err_tip(|| "in GrpcStore::batch_update_blobs") |
437 | 12 | }) |
438 | 6 | .await?0 ; |
439 | 6 | responses.extend(response.into_inner().responses); |
440 | | } |
441 | | |
442 | 1 | Ok(Response::new(BatchUpdateBlobsResponse { responses })) |
443 | 1 | } |
444 | | |
445 | 4 | pub async fn batch_read_blobs( |
446 | 4 | &self, |
447 | 4 | grpc_request: Request<BatchReadBlobsRequest>, |
448 | 4 | ) -> Result<Response<BatchReadBlobsResponse>, Error> { |
449 | 0 | error_if!( |
450 | 4 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
451 | | "CAS operation on AC store" |
452 | | ); |
453 | | |
454 | 4 | let mut request = grpc_request.into_inner(); |
455 | 4 | request.instance_name.clone_from(&self.instance_name); |
456 | 4 | self.perform_request(request, |request| async move { |
457 | 4 | let channel = self |
458 | 4 | .connection_manager |
459 | 4 | .connection("batch_read_blobs".into()) |
460 | 4 | .await |
461 | 4 | .err_tip(|| "in batch_read_blobs")?0 ; |
462 | 4 | ContentAddressableStorageClient::new(channel) |
463 | 4 | .batch_read_blobs(enrich_request( |
464 | 4 | Request::new(request), |
465 | 4 | &self.headers, |
466 | 4 | &self.forward_headers, |
467 | 4 | )) |
468 | 4 | .await |
469 | 4 | .err_tip(|| "in GrpcStore::batch_read_blobs") |
470 | 8 | }) |
471 | 4 | .await |
472 | 4 | } |
473 | | |
474 | | /// Enqueues a small-blob read for coalescing into a `BatchReadBlobs` RPC |
475 | | /// and waits for its result. Returns `None` when the queue is over its |
476 | | /// byte budget, in which case the caller must fall back to the |
477 | | /// `ByteStream` `Read` path. |
478 | 208 | async fn batched_read( |
479 | 208 | &self, |
480 | 208 | batcher: &ReadBatcher, |
481 | 208 | digest: DigestInfo, |
482 | 208 | ) -> Option<Result<Bytes, Error>> { |
483 | | // Capture the digest function from the caller's ambient context now; |
484 | | // the dispatcher runs on a detached task with no such context. |
485 | 208 | let digest_function: i32 = Context::current() |
486 | 208 | .get::<DigestHasherFunc>() |
487 | 208 | .map_or_else(default_digest_hasher_func, |v| *v) |
488 | 208 | .proto_digest_func() |
489 | 208 | .into(); |
490 | 208 | let (tx, rx) = oneshot::channel(); |
491 | | { |
492 | 208 | let mut queue = batcher.queue.lock(); |
493 | | // Admission control. On overflow gracefully degrade to the |
494 | | // stream path instead of blocking. |
495 | 208 | let new_bytes = queue.bytes.saturating_add(digest.size_bytes()); |
496 | 208 | if new_bytes > batcher.max_queued_bytes { |
497 | 0 | batcher.queue_bypasses.fetch_add(1, Ordering::Relaxed); |
498 | 0 | return None; |
499 | 208 | } |
500 | 208 | queue.bytes = new_bytes; |
501 | 208 | batcher.queued_bytes.store(new_bytes, Ordering::Relaxed); |
502 | 208 | queue.items.push_back(PendingRead { |
503 | 208 | digest, |
504 | 208 | digest_function, |
505 | 208 | tx, |
506 | 208 | }); |
507 | | } |
508 | | |
509 | 208 | self.maybe_dispatch_read_batches(batcher); |
510 | | |
511 | 208 | Some(rx.await.unwrap_or_else207 (|_| {0 |
512 | 0 | Err(make_err!( |
513 | 0 | Code::Internal, |
514 | 0 | "Read batch dispatcher dropped result in GrpcStore::batched_read" |
515 | 0 | )) |
516 | 0 | })) |
517 | 207 | } |
518 | | |
519 | | /// Tries to start a read batch dispatcher. This is work-conserving: if a |
520 | | /// dispatch slot is free a dispatcher is started immediately, otherwise |
521 | | /// one of the active dispatchers is responsible for every currently |
522 | | /// queued item. The dispatcher runs as a detached task so that |
523 | | /// cancellation of any individual reader can neither abort an in-flight |
524 | | /// `BatchReadBlobs` RPC nor strand still-queued waiters. |
525 | 208 | fn maybe_dispatch_read_batches(&self, batcher: &ReadBatcher) { |
526 | 208 | let Ok(mut permit4 ) = batcher.dispatch_slots.clone().try_acquire_owned() else { |
527 | 204 | return; |
528 | | }; |
529 | 4 | let Some(store) = self.weak_self.upgrade() else { |
530 | 0 | return; |
531 | | }; |
532 | 4 | background_spawn!("grpc_store_read_batch_dispatch", async move { |
533 | 4 | let Some(batcher) = &store.read_batcher else { |
534 | 0 | return; |
535 | | }; |
536 | | loop { |
537 | 4 | store.dispatch_read_batches(batcher).await; |
538 | 4 | drop(permit); |
539 | | // Items may have been enqueued between the last drain and |
540 | | // the permit release. Re-check so they are not stranded |
541 | | // with no active dispatcher. |
542 | 4 | if batcher.queue.lock().items.is_empty() { |
543 | 4 | return; |
544 | 0 | } |
545 | 0 | match batcher.dispatch_slots.clone().try_acquire_owned() { |
546 | 0 | Ok(new_permit) => permit = new_permit, |
547 | | // Another dispatcher is active and will observe these |
548 | | // items (or re-check after releasing its own permit). |
549 | 0 | Err(_) => return, |
550 | | } |
551 | | } |
552 | 4 | }); |
553 | 208 | } |
554 | | |
555 | | /// Drains the pending read queue, sending one `BatchReadBlobs` RPC per |
556 | | /// drained batch, until the queue is empty. |
557 | 4 | async fn dispatch_read_batches(&self, batcher: &ReadBatcher) { |
558 | | loop { |
559 | 4 | let batch = { |
560 | 8 | let mut queue = batcher.queue.lock(); |
561 | 8 | let Some(head4 ) = queue.items.front() else { |
562 | 4 | return; |
563 | | }; |
564 | | // All digests in one BatchReadBlobsRequest must use the same |
565 | | // digest function. Partition-drain: take items matching the |
566 | | // head's digest function from anywhere in the queue (up to |
567 | | // the batch budget) and keep the rest in relative order. |
568 | 4 | let digest_function = head.digest_function; |
569 | 4 | let mut batch = Vec::new(); |
570 | 4 | let mut batch_bytes = 0u64; |
571 | 4 | let mut rest = VecDeque::with_capacity(queue.items.len()); |
572 | 212 | while let Some(item208 ) = queue.items.pop_front() { |
573 | 208 | let item_cost = item |
574 | 208 | .digest |
575 | 208 | .size_bytes() |
576 | 208 | .saturating_add(BATCH_READ_PER_ENTRY_OVERHEAD_BYTES); |
577 | 208 | if item.digest_function == digest_function |
578 | 208 | && (batch.is_empty() |
579 | 204 | || batch_bytes.saturating_add(item_cost) <= batcher.max_batch_bytes) |
580 | 208 | { |
581 | 208 | batch_bytes = batch_bytes.saturating_add(item_cost); |
582 | 208 | queue.bytes = queue.bytes.saturating_sub(item.digest.size_bytes()); |
583 | 208 | batch.push(item); |
584 | 208 | } else { |
585 | 0 | rest.push_back(item); |
586 | 0 | } |
587 | | } |
588 | 4 | queue.items = rest; |
589 | 4 | batcher.queued_bytes.store(queue.bytes, Ordering::Relaxed); |
590 | 4 | batch |
591 | | }; |
592 | 4 | self.send_read_batch(batcher, batch).await; |
593 | | } |
594 | 4 | } |
595 | | |
596 | | /// Sends one `BatchReadBlobs` RPC for `batch` and demultiplexes the |
597 | | /// per-blob responses back to the waiting readers. One failed item does |
598 | | /// not affect its batch-mates; failure of the whole RPC is broadcast to |
599 | | /// every item in the batch. |
600 | 4 | async fn send_read_batch(&self, batcher: &ReadBatcher, batch: Vec<PendingRead>) { |
601 | 4 | let Some(digest_function) = batch.first().map(|item| item.digest_function) else { |
602 | 0 | return; |
603 | | }; |
604 | | // Servers may dedupe duplicate digests within one request, so group |
605 | | // the waiters per digest and request each digest exactly once, |
606 | | // fanning the (refcounted) data out to every waiter. |
607 | 4 | let batch_len = u64::try_from(batch.len()).unwrap_or(u64::MAX); |
608 | 4 | let mut waiters: HashMap<DigestInfo, Vec<PendingRead>> = HashMap::new(); |
609 | 208 | for item in batch4 { |
610 | 208 | waiters.entry(item.digest).or_default().push(item); |
611 | 208 | } |
612 | 4 | let request = BatchReadBlobsRequest { |
613 | | // batch_read_blobs() overwrites the instance name, so there is |
614 | | // no need to set it here. |
615 | 4 | instance_name: String::new(), |
616 | 207 | digests: waiters4 .keys4 ().map4 (|digest| (*digest).into()).collect4 (), |
617 | 4 | acceptable_compressors: vec![], |
618 | 4 | digest_function, |
619 | | }; |
620 | 4 | batcher.batches_sent.fetch_add(1, Ordering::Relaxed); |
621 | 4 | batcher |
622 | 4 | .blobs_batched |
623 | 4 | .fetch_add(batch_len, Ordering::Relaxed); |
624 | 4 | let response = match self.batch_read_blobs(Request::new(request)).await { |
625 | 4 | Ok(response) => response.into_inner(), |
626 | 0 | Err(err) => { |
627 | | // The whole RPC failed, so every waiter in this batch gets |
628 | | // the error. Waiters may have gone away, ignore send errors. |
629 | 0 | for item in waiters.into_values().flatten() { |
630 | 0 | drop(item.tx.send(Err(err.clone()))); |
631 | 0 | } |
632 | 0 | return; |
633 | | } |
634 | | }; |
635 | 207 | for entry in response.responses4 { |
636 | 207 | let Some(Ok(entry_digest)) = entry.digest.map(DigestInfo::try_from) else { |
637 | 0 | continue; |
638 | | }; |
639 | 207 | let Some(items) = waiters.remove(&entry_digest) else { |
640 | 0 | continue; |
641 | | }; |
642 | 207 | let entry_len = u64::try_from(entry.data.len()).unwrap_or(u64::MAX); |
643 | 207 | let result = if let Some(status1 ) = entry.status.filter(|status| status.code != 0) { |
644 | 1 | Err(Error::from(status) |
645 | 1 | .append("Batch read entry failed in GrpcStore::send_read_batch")) |
646 | 206 | } else if entry.compressor != 0 { |
647 | | // We requested no acceptable compressors, so data must be |
648 | | // returned with the identity compressor. |
649 | 0 | Err(make_err!( |
650 | 0 | Code::Internal, |
651 | 0 | "BatchReadBlobs entry for {entry_digest} used unsupported compressor {}", |
652 | 0 | entry.compressor |
653 | 0 | )) |
654 | 206 | } else if entry_len != entry_digest.size_bytes() { |
655 | 0 | Err(make_err!( |
656 | 0 | Code::Internal, |
657 | 0 | "BatchReadBlobs entry for {entry_digest} returned {entry_len} bytes, expected {}", |
658 | 0 | entry_digest.size_bytes() |
659 | 0 | )) |
660 | | } else { |
661 | 206 | Ok(entry.data) |
662 | | }; |
663 | 207 | if result.is_err() { |
664 | 1 | batcher.batched_read_errors.fetch_add( |
665 | 1 | u64::try_from(items.len()).unwrap_or(u64::MAX), |
666 | 1 | Ordering::Relaxed, |
667 | 1 | ); |
668 | 206 | } |
669 | 208 | for item in items207 { |
670 | 208 | drop(item.tx.send(result.clone())); |
671 | 208 | } |
672 | | } |
673 | | // Any waiter with no matching response entry is missing upstream. |
674 | 4 | for item0 in waiters.into_values().flatten() { |
675 | 0 | batcher.batched_read_errors.fetch_add(1, Ordering::Relaxed); |
676 | 0 | let err = make_err!( |
677 | 0 | Code::NotFound, |
678 | 0 | "Blob {} not found in BatchReadBlobs response", |
679 | 0 | item.digest |
680 | 0 | ); |
681 | 0 | drop(item.tx.send(Err(err))); |
682 | 0 | } |
683 | 4 | } |
684 | | |
685 | 0 | pub async fn get_tree( |
686 | 0 | &self, |
687 | 0 | grpc_request: Request<GetTreeRequest>, |
688 | 2 | ) -> Result<Response<Streaming<GetTreeResponse>>, Error> { |
689 | 0 | error_if!( |
690 | 2 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
691 | | "CAS operation on AC store" |
692 | | ); |
693 | | |
694 | 2 | let mut request = grpc_request.into_inner(); |
695 | 2 | request.instance_name.clone_from(&self.instance_name); |
696 | 2 | self.perform_request(request, |request| async move { |
697 | 2 | let channel = self |
698 | 2 | .connection_manager |
699 | 2 | .connection(format!("get_tree: {:?}", request.root_digest)) |
700 | 2 | .await |
701 | 2 | .err_tip(|| "in get_tree")?0 ; |
702 | 2 | ContentAddressableStorageClient::new(channel) |
703 | 2 | .get_tree(enrich_request( |
704 | 2 | Request::new(request), |
705 | 2 | &self.headers, |
706 | 2 | &self.forward_headers, |
707 | 2 | )) |
708 | 2 | .await |
709 | 2 | .err_tip(|| "in GrpcStore::get_tree") |
710 | 4 | }) |
711 | 2 | .await |
712 | 2 | } |
713 | | |
714 | 1 | pub async fn split_blob( |
715 | 1 | &self, |
716 | 1 | grpc_request: Request<SplitBlobRequest>, |
717 | 1 | ) -> Result<Response<SplitBlobResponse>, Error> { |
718 | 0 | error_if!( |
719 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
720 | | "CAS operation on AC store" |
721 | | ); |
722 | | |
723 | 1 | let mut request = grpc_request.into_inner(); |
724 | 1 | request.instance_name.clone_from(&self.instance_name); |
725 | 1 | self.perform_request(request, |request| async move { |
726 | 1 | let channel = self |
727 | 1 | .connection_manager |
728 | 1 | .connection(format!("split_blob: {:?}", request.blob_digest)) |
729 | 1 | .await |
730 | 1 | .err_tip(|| "in split_blob")?0 ; |
731 | 1 | ContentAddressableStorageClient::new(channel) |
732 | 1 | .split_blob(enrich_request( |
733 | 1 | Request::new(request), |
734 | 1 | &self.headers, |
735 | 1 | &self.forward_headers, |
736 | 1 | )) |
737 | 1 | .await |
738 | 1 | .err_tip(|| "in GrpcStore::split_blob") |
739 | 2 | }) |
740 | 1 | .await |
741 | 1 | } |
742 | | |
743 | 1 | pub async fn splice_blob( |
744 | 1 | &self, |
745 | 1 | grpc_request: Request<SpliceBlobRequest>, |
746 | 1 | ) -> Result<Response<SpliceBlobResponse>, Error> { |
747 | 0 | error_if!( |
748 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
749 | | "CAS operation on AC store" |
750 | | ); |
751 | | |
752 | 1 | let mut request = grpc_request.into_inner(); |
753 | 1 | request.instance_name.clone_from(&self.instance_name); |
754 | 1 | self.perform_request(request, |request| async move { |
755 | 1 | let channel = self |
756 | 1 | .connection_manager |
757 | 1 | .connection(format!("splice_blob: {:?}", request.blob_digest)) |
758 | 1 | .await |
759 | 1 | .err_tip(|| "in splice_blob")?0 ; |
760 | 1 | ContentAddressableStorageClient::new(channel) |
761 | 1 | .splice_blob(enrich_request( |
762 | 1 | Request::new(request), |
763 | 1 | &self.headers, |
764 | 1 | &self.forward_headers, |
765 | 1 | )) |
766 | 1 | .await |
767 | 1 | .err_tip(|| "in GrpcStore::splice_blob") |
768 | 2 | }) |
769 | 1 | .await |
770 | 1 | } |
771 | | |
772 | 0 | fn get_read_request(&self, mut request: ReadRequest) -> Result<ReadRequest, Error> { |
773 | | const IS_UPLOAD_FALSE: bool = false; |
774 | 0 | let mut resource_info = ResourceInfo::new(&request.resource_name, IS_UPLOAD_FALSE)?; |
775 | 0 | if resource_info.instance_name != self.instance_name { |
776 | 0 | resource_info.instance_name = Cow::Borrowed(&self.instance_name); |
777 | 0 | request.resource_name = resource_info.to_string(IS_UPLOAD_FALSE); |
778 | 0 | } |
779 | 0 | Ok(request) |
780 | 0 | } |
781 | | |
782 | 15 | async fn read_internal( |
783 | 15 | &self, |
784 | 15 | request: ReadRequest, |
785 | 15 | ) -> Result<impl Stream<Item = Result<ReadResponse, Status>> + use<>, Error> { |
786 | 15 | let channel = self |
787 | 15 | .connection_manager |
788 | 15 | .connection(format!("read_internal: {}", request.resource_name)) |
789 | 15 | .await |
790 | 15 | .err_tip(|| "in read_internal")?0 ; |
791 | 15 | let mut response = ByteStreamClient::new(channel) |
792 | 15 | .read(enrich_request( |
793 | 15 | Request::new(request), |
794 | 15 | &self.headers, |
795 | 15 | &self.forward_headers, |
796 | 15 | )) |
797 | 15 | .await |
798 | 15 | .err_tip(|| "in GrpcStore::read")?0 |
799 | 15 | .into_inner(); |
800 | 15 | let first_response14 = response |
801 | 15 | .message() |
802 | 15 | .await |
803 | 15 | .err_tip(|| "Fetching first chunk in GrpcStore::read()")?1 ; |
804 | 14 | Ok(FirstStream::new(first_response, response)) |
805 | 15 | } |
806 | | |
807 | 0 | pub async fn read<R>( |
808 | 0 | &self, |
809 | 0 | grpc_request: R, |
810 | 0 | ) -> Result<impl Stream<Item = Result<ReadResponse, Status>> + use<R>, Error> |
811 | 0 | where |
812 | 0 | R: IntoRequest<ReadRequest>, |
813 | 0 | { |
814 | 0 | error_if!( |
815 | 0 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
816 | | "CAS operation on AC store" |
817 | | ); |
818 | | |
819 | 0 | let request = self.get_read_request(grpc_request.into_request().into_inner())?; |
820 | 0 | self.perform_request(request, |request| async move { |
821 | 0 | self.read_internal(request).await |
822 | 0 | }) |
823 | 0 | .await |
824 | 0 | } |
825 | | |
826 | 1 | pub async fn write<T, E>( |
827 | 1 | &self, |
828 | 1 | stream: WriteRequestStreamWrapper<T>, |
829 | 1 | ) -> Result<Response<WriteResponse>, Error> |
830 | 1 | where |
831 | 1 | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
832 | 1 | E: Into<Error> + 'static, |
833 | 1 | { |
834 | | const RESUMABLE: bool = true; |
835 | 1 | self.write_internal(stream, RESUMABLE).await |
836 | 1 | } |
837 | | |
838 | | /// Writes one event to an event sink. A sink takes an upload only from |
839 | | /// offset zero, so where `write` resumes a retry from its last cached |
840 | | /// messages (mid-event, for an event over one `WriteRequest`), this |
841 | | /// resends the whole event on every attempt. Events are bounded, so the |
842 | | /// event is held in memory for its retries. |
843 | 4 | async fn update_event( |
844 | 4 | &self, |
845 | 4 | resource_name: String, |
846 | 4 | mut reader: DropCloserReadHalf, |
847 | 4 | digest: DigestInfo, |
848 | 4 | ) -> Result<u64, Error> { |
849 | 4 | let size = digest.size_bytes(); |
850 | 4 | let data = reader |
851 | 4 | .consume(Some( |
852 | 4 | usize::try_from(size).err_tip(|| "Event size in GrpcStore::update_event")?0 , |
853 | | )) |
854 | 4 | .await |
855 | 4 | .err_tip(|| "In GrpcStore::update_event")?0 ; |
856 | 4 | let response = self |
857 | 4 | .retrier |
858 | 6 | .retry4 (unfold4 (()4 , move |()| { |
859 | 6 | let mut requests: Vec<WriteRequest> = data |
860 | 6 | .chunks(MAX_WRITE_REQUEST_DATA_BYTES) |
861 | 12 | .scan6 (0usize, |offset, chunk| { |
862 | 12 | let start = *offset; |
863 | 12 | *offset += chunk.len(); |
864 | 12 | Some(WriteRequest { |
865 | 12 | resource_name: resource_name.clone(), |
866 | 12 | write_offset: i64::try_from(start).unwrap_or(i64::MAX), |
867 | 12 | finish_write: false, |
868 | 12 | data: data.slice(start..*offset), |
869 | 12 | }) |
870 | 12 | }) |
871 | 6 | .collect(); |
872 | 6 | requests.push(WriteRequest { |
873 | 6 | resource_name: resource_name.clone(), |
874 | 6 | write_offset: i64::try_from(data.len()).unwrap_or(i64::MAX), |
875 | 6 | finish_write: true, |
876 | 6 | data: Bytes::new(), |
877 | 6 | }); |
878 | 6 | async move { |
879 | 6 | let rpc = self |
880 | 6 | .connection_manager |
881 | 6 | .connection("update_event".into()) |
882 | 6 | .and_then(|channel| async move { |
883 | 6 | ByteStreamClient::new(channel) |
884 | 6 | .write(enrich_request( |
885 | 6 | Request::new(futures::stream::iter(requests)), |
886 | 6 | &self.headers, |
887 | 6 | &self.forward_headers, |
888 | 6 | )) |
889 | 6 | .await |
890 | 6 | .err_tip(|| "in GrpcStore::update_event") |
891 | 12 | }); |
892 | 6 | let result = if self.rpc_timeout > Duration::ZERO { |
893 | 6 | tokio::time::timeout(self.rpc_timeout, rpc) |
894 | 6 | .await |
895 | 6 | .unwrap_or_else(|_elapsed| {0 |
896 | 0 | Err(make_err!( |
897 | 0 | Code::DeadlineExceeded, |
898 | 0 | "GrpcStore::update_event RPC timed out after {}s", |
899 | 0 | self.rpc_timeout.as_secs() |
900 | 0 | )) |
901 | 0 | }) |
902 | | } else { |
903 | 0 | rpc.await |
904 | | }; |
905 | 6 | if let Err(err2 ) = &result { |
906 | 2 | warn!(instance_name = %self.instance_name, ?err, "GrpcStore::update_event: RPC failed; a retry resends the whole event"); |
907 | 4 | } |
908 | 6 | Some((result.map_or_else(RetryResult::Retry, RetryResult::Ok), ())) |
909 | 6 | } |
910 | 6 | })) |
911 | 4 | .await?0 ; |
912 | 4 | if u64::try_from(response.get_ref().committed_size).ok() != Some(size) { |
913 | 1 | return Err(make_err!( |
914 | 1 | Code::DataLoss, |
915 | 1 | "Event sink acknowledged {} bytes; expected {}", |
916 | 1 | response.get_ref().committed_size, |
917 | 1 | size |
918 | 1 | )); |
919 | 3 | } |
920 | 3 | Ok(size) |
921 | 4 | } |
922 | | |
923 | 18 | async fn write_internal<T, E>( |
924 | 18 | &self, |
925 | 18 | stream: WriteRequestStreamWrapper<T>, |
926 | 18 | resumable: bool, |
927 | 18 | ) -> Result<Response<WriteResponse>, Error> |
928 | 18 | where |
929 | 18 | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
930 | 18 | E: Into<Error> + 'static, |
931 | 18 | { |
932 | 0 | error_if!( |
933 | 18 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
934 | | "CAS operation on AC store" |
935 | | ); |
936 | | |
937 | 18 | let mut write_state = WriteState::new(self.instance_name.clone(), stream); |
938 | 18 | if !resumable { |
939 | 17 | write_state.set_non_resumable(); |
940 | 17 | }1 |
941 | 18 | let local_state = Arc::new(Mutex::new(write_state)); |
942 | | |
943 | 18 | let write_start = std::time::Instant::now(); |
944 | 18 | let instance_name = self.instance_name.clone(); |
945 | 18 | let rpc_timeout = self.rpc_timeout; |
946 | 18 | trace!( |
947 | | instance_name = %instance_name, |
948 | 18 | rpc_timeout_s = rpc_timeout.as_secs(), |
949 | | "GrpcStore::write: starting ByteStream write", |
950 | | ); |
951 | 18 | let mut attempt: u32 = 0; |
952 | 18 | let result9 = self |
953 | 18 | .retrier |
954 | 18 | .retry(unfold(local_state, move |local_state| { |
955 | 18 | attempt += 1; |
956 | 18 | let instance_name = instance_name.clone(); |
957 | 18 | async move { |
958 | | // The client write may occur on a separate thread and |
959 | | // therefore in order to share the state with it we have to |
960 | | // wrap it in a Mutex and retrieve it after the write |
961 | | // has completed. There is no way to get the value back |
962 | | // from the client. |
963 | 18 | trace!( |
964 | | instance_name = %instance_name, |
965 | | attempt, |
966 | | "GrpcStore::write: requesting connection from pool", |
967 | | ); |
968 | 18 | let conn_start = std::time::Instant::now(); |
969 | 18 | let rpc_fut = self.connection_manager.connection("write".into()).and_then( |
970 | 18 | |channel| { |
971 | 18 | let conn_elapsed = conn_start.elapsed(); |
972 | 18 | let instance_for_rpc = instance_name.clone(); |
973 | 18 | let conn_elapsed_ms = |
974 | 18 | u64::try_from(conn_elapsed.as_millis()).unwrap_or(u64::MAX); |
975 | 18 | trace!( |
976 | | instance_name = %instance_for_rpc, |
977 | | conn_elapsed_ms, |
978 | | "GrpcStore::write: got connection, starting ByteStream.Write RPC", |
979 | | ); |
980 | 18 | let rpc_start = std::time::Instant::now(); |
981 | 18 | let local_state_for_rpc = local_state.clone(); |
982 | 18 | async move { |
983 | 18 | let res = ByteStreamClient::new(channel) |
984 | 18 | .write(enrich_request( |
985 | 18 | Request::new(WriteStateWrapper::new(local_state_for_rpc)), |
986 | 18 | &self.headers, |
987 | 18 | &self.forward_headers, |
988 | 18 | )) |
989 | 18 | .await |
990 | 18 | .err_tip(|| "in GrpcStore::write"); |
991 | 18 | let rpc_elapsed_ms = u64::try_from(rpc_start.elapsed().as_millis()) |
992 | 18 | .unwrap_or(u64::MAX); |
993 | 18 | trace!( |
994 | | instance_name = %instance_for_rpc, |
995 | | rpc_elapsed_ms, |
996 | 18 | success = res.is_ok(), |
997 | | "GrpcStore::write: ByteStream.Write RPC returned", |
998 | | ); |
999 | 18 | res |
1000 | 18 | } |
1001 | 18 | }, |
1002 | | ); |
1003 | | |
1004 | 18 | let result = if rpc_timeout > Duration::ZERO { |
1005 | 18 | match tokio::time::timeout(rpc_timeout, rpc_fut).await { |
1006 | 18 | Ok(res) => res, |
1007 | 0 | Err(_elapsed) => { |
1008 | 0 | warn!( |
1009 | | instance_name = %instance_name, |
1010 | | attempt, |
1011 | 0 | rpc_timeout_s = rpc_timeout.as_secs(), |
1012 | | "GrpcStore::write: per-RPC timeout exceeded, cancelling", |
1013 | | ); |
1014 | | #[allow(unused_qualifications)] |
1015 | 0 | Err(nativelink_error::make_err!( |
1016 | 0 | nativelink_error::Code::DeadlineExceeded, |
1017 | 0 | "GrpcStore::write RPC timed out after {}s", |
1018 | 0 | rpc_timeout.as_secs() |
1019 | 0 | )) |
1020 | | } |
1021 | | } |
1022 | | } else { |
1023 | 0 | rpc_fut.await |
1024 | | }; |
1025 | | |
1026 | | // Get the state back from StateWrapper, this should be |
1027 | | // uncontended since write has returned. |
1028 | 18 | let mut local_state_locked = local_state.lock(); |
1029 | | |
1030 | 18 | let result = local_state_locked |
1031 | 18 | .take_read_stream_error() |
1032 | 18 | .map(|err| RetryResult::Err(err1 .append1 ("Where read_stream_error was set"))) |
1033 | 18 | .unwrap_or_else(|| {17 |
1034 | | // No stream error, handle the original result |
1035 | 17 | match result { |
1036 | 9 | Ok(response) => RetryResult::Ok(response), |
1037 | 8 | Err(ref err) => { |
1038 | 8 | warn!( |
1039 | | instance_name = %instance_name, |
1040 | | attempt, |
1041 | | ?err, |
1042 | 8 | can_resume = local_state_locked.can_resume(), |
1043 | | "GrpcStore::write: RPC failed", |
1044 | | ); |
1045 | 8 | if local_state_locked.can_resume() { |
1046 | 0 | local_state_locked.resume(); |
1047 | 0 | RetryResult::Retry(err.clone()) |
1048 | | } else { |
1049 | 8 | RetryResult::Err( |
1050 | 8 | err.clone().append("Retry is not possible"), |
1051 | 8 | ) |
1052 | | } |
1053 | | } |
1054 | | } |
1055 | 17 | }); |
1056 | | |
1057 | 18 | drop(local_state_locked); |
1058 | 18 | Some((result, local_state)) |
1059 | 18 | } |
1060 | 18 | })) |
1061 | 18 | .await?9 ; |
1062 | | |
1063 | 9 | let total_elapsed_ms = u64::try_from(write_start.elapsed().as_millis()).unwrap_or(u64::MAX); |
1064 | 9 | trace!( |
1065 | | instance_name = %self.instance_name, |
1066 | | total_elapsed_ms, |
1067 | | "GrpcStore::write: completed successfully", |
1068 | | ); |
1069 | 9 | Ok(result) |
1070 | 18 | } |
1071 | | |
1072 | 0 | pub async fn query_write_status( |
1073 | 0 | &self, |
1074 | 0 | grpc_request: Request<QueryWriteStatusRequest>, |
1075 | 0 | ) -> Result<Response<QueryWriteStatusResponse>, Error> { |
1076 | | const IS_UPLOAD_TRUE: bool = true; |
1077 | | |
1078 | 0 | error_if!( |
1079 | 0 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
1080 | | "CAS operation on AC store" |
1081 | | ); |
1082 | | |
1083 | 0 | let mut request = grpc_request.into_inner(); |
1084 | | |
1085 | 0 | let mut request_info = ResourceInfo::new(&request.resource_name, IS_UPLOAD_TRUE)?; |
1086 | 0 | if request_info.instance_name != self.instance_name { |
1087 | 0 | request_info.instance_name = Cow::Borrowed(&self.instance_name); |
1088 | 0 | request.resource_name = request_info.to_string(IS_UPLOAD_TRUE); |
1089 | 0 | } |
1090 | | |
1091 | 0 | self.perform_request(request, |request| async move { |
1092 | 0 | let channel = self |
1093 | 0 | .connection_manager |
1094 | 0 | .connection(format!("query_write_status: {}", request.resource_name)) |
1095 | 0 | .await |
1096 | 0 | .err_tip(|| "in query_write_status")?; |
1097 | 0 | ByteStreamClient::new(channel) |
1098 | 0 | .query_write_status(enrich_request( |
1099 | 0 | Request::new(request), |
1100 | 0 | &self.headers, |
1101 | 0 | &self.forward_headers, |
1102 | 0 | )) |
1103 | 0 | .await |
1104 | 0 | .err_tip(|| "in GrpcStore::query_write_status") |
1105 | 0 | }) |
1106 | 0 | .await |
1107 | 0 | } |
1108 | | |
1109 | 0 | pub async fn get_action_result( |
1110 | 0 | &self, |
1111 | 0 | grpc_request: Request<GetActionResultRequest>, |
1112 | 0 | ) -> Result<Response<ActionResult>, Error> { |
1113 | 0 | let mut request = grpc_request.into_inner(); |
1114 | 0 | request.instance_name.clone_from(&self.instance_name); |
1115 | 0 | self.perform_request(request, |request| async move { |
1116 | 0 | let channel = self |
1117 | 0 | .connection_manager |
1118 | 0 | .connection(format!("get_action_result: {:?}", request.action_digest)) |
1119 | 0 | .await |
1120 | 0 | .err_tip(|| "in get_action_result")?; |
1121 | 0 | ActionCacheClient::new(channel) |
1122 | 0 | .get_action_result(enrich_request( |
1123 | 0 | Request::new(request), |
1124 | 0 | &self.headers, |
1125 | 0 | &self.forward_headers, |
1126 | 0 | )) |
1127 | 0 | .await |
1128 | 0 | .err_tip(|| "in GrpcStore::get_action_result") |
1129 | 0 | }) |
1130 | 0 | .await |
1131 | 0 | } |
1132 | | |
1133 | 0 | pub async fn update_action_result( |
1134 | 0 | &self, |
1135 | 0 | grpc_request: Request<UpdateActionResultRequest>, |
1136 | 0 | ) -> Result<Response<ActionResult>, Error> { |
1137 | 0 | let mut request = grpc_request.into_inner(); |
1138 | 0 | request.instance_name.clone_from(&self.instance_name); |
1139 | 0 | self.perform_request(request, |request| async move { |
1140 | 0 | let channel = self |
1141 | 0 | .connection_manager |
1142 | 0 | .connection(format!("update_action_result: {:?}", request.action_digest)) |
1143 | 0 | .await |
1144 | 0 | .err_tip(|| "in update_action_result")?; |
1145 | 0 | ActionCacheClient::new(channel) |
1146 | 0 | .update_action_result(enrich_request( |
1147 | 0 | Request::new(request), |
1148 | 0 | &self.headers, |
1149 | 0 | &self.forward_headers, |
1150 | 0 | )) |
1151 | 0 | .await |
1152 | 0 | .err_tip(|| "in GrpcStore::update_action_result") |
1153 | 0 | }) |
1154 | 0 | .await |
1155 | 0 | } |
1156 | | |
1157 | 0 | async fn get_action_result_from_digest( |
1158 | 0 | &self, |
1159 | 0 | digest: DigestInfo, |
1160 | 0 | ) -> Result<Response<ActionResult>, Error> { |
1161 | 0 | let action_result_request = GetActionResultRequest { |
1162 | 0 | instance_name: self.instance_name.clone(), |
1163 | 0 | action_digest: Some(digest.into()), |
1164 | | inline_stdout: false, |
1165 | | inline_stderr: false, |
1166 | 0 | inline_output_files: Vec::new(), |
1167 | 0 | digest_function: Context::current() |
1168 | 0 | .get::<DigestHasherFunc>() |
1169 | 0 | .map_or_else(default_digest_hasher_func, |v| *v) |
1170 | 0 | .proto_digest_func() |
1171 | 0 | .into(), |
1172 | | }; |
1173 | 0 | self.get_action_result(Request::new(action_result_request)) |
1174 | 0 | .await |
1175 | 0 | } |
1176 | | |
1177 | 0 | async fn get_action_result_as_part( |
1178 | 0 | &self, |
1179 | 0 | digest: DigestInfo, |
1180 | 0 | writer: &mut DropCloserWriteHalf, |
1181 | 0 | offset: usize, |
1182 | 0 | length: Option<usize>, |
1183 | 0 | ) -> Result<(), Error> { |
1184 | 0 | let action_result = self |
1185 | 0 | .get_action_result_from_digest(digest) |
1186 | 0 | .await |
1187 | 0 | .map(Response::into_inner) |
1188 | 0 | .err_tip(|| "Action result not found")?; |
1189 | | // TODO: Would be better to avoid all the encoding and decoding in this |
1190 | | // file, however there's no way to currently get raw bytes from a |
1191 | | // generated prost request unfortunately. |
1192 | 0 | let mut value = BytesMut::new(); |
1193 | 0 | action_result |
1194 | 0 | .encode(&mut value) |
1195 | 0 | .err_tip(|| "Could not encode upstream action result")?; |
1196 | | |
1197 | 0 | let default_len = value.len() - offset; |
1198 | 0 | let length = length.unwrap_or(default_len).min(default_len); |
1199 | 0 | if length > 0 { |
1200 | 0 | writer |
1201 | 0 | .send(value.freeze().slice(offset..offset + length)) |
1202 | 0 | .await |
1203 | 0 | .err_tip(|| "Failed to write data in grpc store")?; |
1204 | 0 | } |
1205 | 0 | writer |
1206 | 0 | .send_eof() |
1207 | 0 | .err_tip(|| "Failed to write EOF in grpc store get_action_result_as_part")?; |
1208 | 0 | Ok(()) |
1209 | 0 | } |
1210 | | |
1211 | 0 | async fn update_action_result_from_bytes( |
1212 | 0 | &self, |
1213 | 0 | digest: DigestInfo, |
1214 | 0 | mut reader: DropCloserReadHalf, |
1215 | 0 | ) -> Result<u64, Error> { |
1216 | 0 | let bytes = reader.consume(None).await?; |
1217 | 0 | let len = bytes.len() as u64; |
1218 | 0 | let action_result = ActionResult::decode(bytes) |
1219 | 0 | .err_tip(|| "Failed to decode ActionResult in update_action_result_from_bytes")?; |
1220 | 0 | let update_action_request = UpdateActionResultRequest { |
1221 | 0 | instance_name: self.instance_name.clone(), |
1222 | 0 | action_digest: Some(digest.into()), |
1223 | 0 | action_result: Some(action_result), |
1224 | 0 | results_cache_policy: None, |
1225 | 0 | digest_function: Context::current() |
1226 | 0 | .get::<DigestHasherFunc>() |
1227 | 0 | .map_or_else(default_digest_hasher_func, |v| *v) |
1228 | 0 | .proto_digest_func() |
1229 | 0 | .into(), |
1230 | | }; |
1231 | 0 | self.update_action_result(Request::new(update_action_request)) |
1232 | 0 | .await |
1233 | 0 | .map(|_| len) |
1234 | 0 | } |
1235 | | |
1236 | | /// Uploads `digest` as a REAPI `compressed-blobs/zstd` write: the raw |
1237 | | /// bytes from `reader` are zstd-encoded on the fly and streamed with |
1238 | | /// compressed write offsets. Used when |
1239 | | /// `experimental_remote_cache_compression` is enabled and the blob meets |
1240 | | /// the size threshold. |
1241 | 6 | async fn update_compressed( |
1242 | 6 | self: Pin<&Self>, |
1243 | 6 | digest: DigestInfo, |
1244 | 6 | reader: DropCloserReadHalf, |
1245 | 6 | size_info: UploadSizeInfo, |
1246 | 6 | ) -> Result<u64, Error> { |
1247 | | enum UploadCompletion { |
1248 | | Write(Result<(), Error>), |
1249 | | Encode(Result<(), Error>, Result<(), Error>), |
1250 | | } |
1251 | | |
1252 | | // Compressed writes are NON-resumable: the server-side protocol |
1253 | | // rejects replays from a nonzero compressed offset, so a mid-stream |
1254 | | // failure must surface immediately instead of burning the retry |
1255 | | // budget on guaranteed-rejected resumes. |
1256 | | const NON_RESUMABLE: bool = false; |
1257 | | |
1258 | | struct LocalState { |
1259 | | resource_name: String, |
1260 | | compressed_rx: DropCloserReadHalf, |
1261 | | did_error: bool, |
1262 | | bytes_received: i64, |
1263 | | } |
1264 | | |
1265 | 6 | let mut buf = Uuid::encode_buffer(); |
1266 | 6 | let uuid = Uuid::new_v4().hyphenated().encode_lower(&mut buf); |
1267 | 6 | let resource_name = if self.use_legacy_resource_names { |
1268 | 0 | format!( |
1269 | | "{}/uploads/{}/compressed-blobs/zstd/{}/{}", |
1270 | 0 | self.instance_name, |
1271 | | uuid, |
1272 | 0 | digest.packed_hash(), |
1273 | 0 | digest.size_bytes(), |
1274 | | ) |
1275 | | } else { |
1276 | 6 | let digest_function = Context::current() |
1277 | 6 | .get::<DigestHasherFunc>() |
1278 | 6 | .map_or_else(default_digest_hasher_func, |v| *v) |
1279 | 6 | .proto_digest_func() |
1280 | 6 | .as_str_name() |
1281 | 6 | .to_ascii_lowercase(); |
1282 | 6 | format!( |
1283 | | "{}/uploads/{}/compressed-blobs/zstd/{}/{}/{}", |
1284 | 6 | self.instance_name, |
1285 | | uuid, |
1286 | | digest_function, |
1287 | 6 | digest.packed_hash(), |
1288 | 6 | digest.size_bytes(), |
1289 | | ) |
1290 | | }; |
1291 | | |
1292 | 6 | let (compressed_tx, compressed_rx) = make_buf_channel_pair(); |
1293 | 6 | let mut reader = reader; |
1294 | 6 | let encode_fut = stream_encode_compressed_upload_from_reader( |
1295 | 6 | &mut reader, |
1296 | 6 | compressor::Value::Zstd, |
1297 | | WIRE_COMPRESSION_ZSTD_LEVEL, |
1298 | 6 | compressed_tx, |
1299 | 6 | size_info, |
1300 | | ); |
1301 | | |
1302 | 6 | let local_state = LocalState { |
1303 | 6 | resource_name, |
1304 | 6 | compressed_rx, |
1305 | 6 | did_error: false, |
1306 | 6 | bytes_received: 0, |
1307 | 6 | }; |
1308 | 17 | let stream6 = Box::pin6 (unfold6 (local_state6 , |mut local_state| async move { |
1309 | 17 | if local_state.did_error { |
1310 | 0 | error!("GrpcStore::update_compressed() polled stream after error was returned"); |
1311 | 0 | return None; |
1312 | 17 | } |
1313 | 17 | let data14 = match local_state |
1314 | 17 | .compressed_rx |
1315 | 17 | .recv() |
1316 | 17 | .await |
1317 | 16 | .err_tip(|| "In GrpcStore::update_compressed()") |
1318 | | { |
1319 | 14 | Ok(data) => data, |
1320 | 2 | Err(err) => { |
1321 | 2 | local_state.did_error = true; |
1322 | 2 | return Some((Err(err), local_state)); |
1323 | | } |
1324 | | }; |
1325 | 14 | let write_offset = local_state.bytes_received; |
1326 | 14 | local_state.bytes_received += data.len().try_into().unwrap_or(i64::MAX); |
1327 | 14 | Some(( |
1328 | 14 | Ok(WriteRequest { |
1329 | 14 | resource_name: local_state.resource_name.clone(), |
1330 | 14 | write_offset, |
1331 | 14 | finish_write: data.is_empty(), // EOF is when no data was polled. |
1332 | 14 | data, |
1333 | 14 | }), |
1334 | 14 | local_state, |
1335 | 14 | )) |
1336 | 33 | })); |
1337 | | |
1338 | | // The encoder must be driven concurrently with the RPC: the request |
1339 | | // stream's first message is the encoder's first output chunk. |
1340 | 6 | let write_fut = async { |
1341 | 6 | self.write_internal4 ( |
1342 | 6 | WriteRequestStreamWrapper::from(stream) |
1343 | 6 | .await |
1344 | 6 | .err_tip(|| "in GrpcStore::update_compressed()")?2 , |
1345 | | NON_RESUMABLE, |
1346 | | ) |
1347 | 4 | .await |
1348 | 4 | .map(|_| ()) |
1349 | 4 | .err_tip(|| "in GrpcStore::update_compressed()") |
1350 | 6 | }; |
1351 | 6 | let completion = async { |
1352 | 6 | let write_fut = Box::pin(write_fut); |
1353 | 6 | let encode_fut = Box::pin(encode_fut); |
1354 | 6 | match future::select(write_fut, encode_fut).await { |
1355 | 1 | future::Either::Left((write_result, encode_fut)) => { |
1356 | 1 | drop(encode_fut); |
1357 | 1 | UploadCompletion::Write(write_result) |
1358 | | } |
1359 | 5 | future::Either::Right((encode_result, write_fut)) => { |
1360 | 5 | UploadCompletion::Encode(encode_result, write_fut.await) |
1361 | | } |
1362 | | } |
1363 | 6 | } |
1364 | 6 | .await; |
1365 | 6 | match completion { |
1366 | 1 | UploadCompletion::Write(write_result) => { |
1367 | 1 | write_result?0 ; |
1368 | | // The server settled the write before consuming the whole |
1369 | | // stream (REAPI early completion of a duplicate upload). Do |
1370 | | // not await the encoder: it may be blocked on a stalled |
1371 | | // producer. Cancel it, then drain the raw reader in the |
1372 | | // background so the producer can finish without observing a |
1373 | | // broken pipe from a successful upload. |
1374 | 1 | background_spawn!("grpc_store_compressed_upload_drain", async move { |
1375 | 1 | if let Err(err0 ) = reader.drain().await { |
1376 | 0 | debug!( |
1377 | | ?err, |
1378 | | "Compressed upload reader drain failed after early completion" |
1379 | | ); |
1380 | 0 | } |
1381 | 0 | }); |
1382 | | } |
1383 | 5 | UploadCompletion::Encode(encode_result, write_result) => { |
1384 | 5 | write_result?4 ; |
1385 | | // An encode error with a successful write means the server |
1386 | | // finished without consuming the whole stream; the upload |
1387 | | // itself succeeded. |
1388 | 1 | if let Err(err0 ) = encode_result { |
1389 | 0 | debug!( |
1390 | | ?err, |
1391 | | "Compressed upload encoder ended early after successful write" |
1392 | | ); |
1393 | | // The encoder stopped early, most likely because the |
1394 | | // server completed the write while its response stream |
1395 | | // was still being finalized. It has already released its |
1396 | | // borrow of `reader`, so drain any raw input the producer |
1397 | | // still has in flight. On a clean encode the reader is |
1398 | | // already at EOF and draining would be a no-op, so the |
1399 | | // happy path spawns nothing. |
1400 | 0 | background_spawn!("grpc_store_compressed_upload_drain", async move { |
1401 | 0 | if let Err(err) = reader.drain().await { |
1402 | 0 | debug!( |
1403 | | ?err, |
1404 | | "Compressed upload reader drain failed after early completion" |
1405 | | ); |
1406 | 0 | } |
1407 | 0 | }); |
1408 | 1 | } |
1409 | | } |
1410 | | } |
1411 | 2 | Ok(digest.size_bytes()) |
1412 | 6 | } |
1413 | | |
1414 | | /// Reads all of `digest` as a REAPI `compressed-blobs/zstd` read, |
1415 | | /// streaming decode into `writer` with size and digest verification at |
1416 | | /// EOF. Returns `Ok(None)` on success. On a retryable transport failure |
1417 | | /// before any decoded bytes were forwarded, it returns `Ok(Some(0))` so |
1418 | | /// the caller can restart through the identity path. Failures after any |
1419 | | /// output, and other terminal errors (including decode/digest mismatches), |
1420 | | /// propagate as `Err`. |
1421 | 5 | async fn get_part_compressed( |
1422 | 5 | self: Pin<&Self>, |
1423 | 5 | digest: DigestInfo, |
1424 | 5 | writer: &mut DropCloserWriteHalf, |
1425 | 5 | ) -> Result<Option<u64>, Error> { |
1426 | | #[derive(Debug)] |
1427 | | enum CompressedReadStage { |
1428 | | Feed, |
1429 | | Decode, |
1430 | | Pump, |
1431 | | } |
1432 | | |
1433 | 5 | let resource_name = if self.use_legacy_resource_names { |
1434 | 0 | format!( |
1435 | | "{}/compressed-blobs/zstd/{}/{}", |
1436 | 0 | self.instance_name, |
1437 | 0 | digest.packed_hash(), |
1438 | 0 | digest.size_bytes(), |
1439 | | ) |
1440 | | } else { |
1441 | 5 | let digest_function = Context::current() |
1442 | 5 | .get::<DigestHasherFunc>() |
1443 | 5 | .map_or_else(default_digest_hasher_func, |v| *v) |
1444 | 5 | .proto_digest_func() |
1445 | 5 | .as_str_name() |
1446 | 5 | .to_ascii_lowercase(); |
1447 | 5 | format!( |
1448 | | "{}/compressed-blobs/zstd/{}/{}/{}", |
1449 | 5 | self.instance_name, |
1450 | | digest_function, |
1451 | 5 | digest.packed_hash(), |
1452 | 5 | digest.size_bytes(), |
1453 | | ) |
1454 | | }; |
1455 | | |
1456 | 5 | let mut stream4 = match self |
1457 | 5 | .read_internal(ReadRequest { |
1458 | 5 | resource_name, |
1459 | 5 | read_offset: 0, |
1460 | 5 | read_limit: 0, |
1461 | 5 | }) |
1462 | 5 | .await |
1463 | | { |
1464 | 4 | Ok(stream) => stream, |
1465 | 1 | Err(err) if is_retryable_code(err.code) => { |
1466 | 1 | warn!( |
1467 | | ?err, |
1468 | | "Compressed read failed to start, falling back to identity read" |
1469 | | ); |
1470 | 1 | return Ok(Some(0)); |
1471 | | } |
1472 | 0 | Err(err) => return Err(err.append("in GrpcStore::get_part_compressed()")), |
1473 | | }; |
1474 | | |
1475 | 4 | let digest_function = Context::current() |
1476 | 4 | .get::<DigestHasherFunc>() |
1477 | 4 | .map_or_else(default_digest_hasher_func, |v| *v); |
1478 | 4 | let (mut compressed_tx, compressed_rx) = make_buf_channel_pair(); |
1479 | 4 | let (decoded_tx, mut decoded_rx) = make_buf_channel_pair(); |
1480 | 4 | let decode_fut = stream_decode_compressed_upload( |
1481 | 4 | compressed_rx, |
1482 | 4 | compressor::Value::Zstd, |
1483 | 4 | digest, |
1484 | 4 | digest_function, |
1485 | 4 | decoded_tx, |
1486 | | ); |
1487 | 4 | let feed_fut = async { |
1488 | | loop { |
1489 | 13 | match stream.next().await { |
1490 | | None => { |
1491 | | // A send_eof failure means the decoder already |
1492 | | // settled and dropped its receiver; its result is |
1493 | | // authoritative, so this is not a feed error. |
1494 | 2 | drop(compressed_tx.send_eof()); |
1495 | 2 | return Ok(()); |
1496 | | } |
1497 | 9 | Some(Ok(message)) => { |
1498 | | // Empty chunks are legal on the wire but are the EOF |
1499 | | // marker in buf_channel; skip them. |
1500 | 9 | if !message.data.is_empty() |
1501 | 9 | && compressed_tx.send(message.data).await.is_err() |
1502 | | { |
1503 | | // The decoder stopped consuming (it settled or |
1504 | | // aborted on bad data). Its result decides the |
1505 | | // outcome; reporting a feed error here would |
1506 | | // misclassify a decoder-detected data error as |
1507 | | // retryable transport fallout. |
1508 | 0 | return Ok(()); |
1509 | 9 | } |
1510 | | } |
1511 | 1 | Some(Err(status)) => return Err(Into::<Error>::into(status)), |
1512 | | } |
1513 | | } |
1514 | 3 | }; |
1515 | 4 | let forwarded = AtomicU64::new(0); |
1516 | | // Set once the decoder's EOF has been forwarded. The decoder only |
1517 | | // sends EOF after the whole blob passed its size and digest checks |
1518 | | // (and `buf_channel` reports a sender dropped without EOF as an error |
1519 | | // rather than as EOF), so this is a positive signal that the download |
1520 | | // completed and was verified. |
1521 | 4 | let download_complete = AtomicBool::new(false); |
1522 | 4 | let pump_fut = async { |
1523 | | loop { |
1524 | 76 | let chunk74 = decoded_rx |
1525 | 76 | .recv() |
1526 | 76 | .await |
1527 | 74 | .err_tip(|| "in GrpcStore::get_part_compressed()")?0 ; |
1528 | 74 | if chunk.is_empty() { |
1529 | 1 | writer |
1530 | 1 | .send_eof() |
1531 | 1 | .err_tip(|| "in GrpcStore::get_part_compressed()")?0 ; |
1532 | 1 | download_complete.store(true, Ordering::Relaxed); |
1533 | 1 | return Ok(()); |
1534 | 73 | } |
1535 | 73 | let chunk_len = chunk.len() as u64; |
1536 | 73 | writer |
1537 | 73 | .send(chunk) |
1538 | 73 | .await |
1539 | 73 | .err_tip(|| "in GrpcStore::get_part_compressed()")?1 ; |
1540 | | // Only bytes accepted by the downstream writer are eligible |
1541 | | // to influence retry policy. |
1542 | 72 | forwarded.fetch_add(chunk_len, Ordering::Relaxed); |
1543 | | } |
1544 | 2 | }; |
1545 | | |
1546 | 4 | let result = tokio::try_join!( |
1547 | 4 | async { |
1548 | 4 | feed_fut |
1549 | 4 | .await |
1550 | 3 | .map_err(|err| (CompressedReadStage::Feed1 , err1 )) |
1551 | 3 | }, |
1552 | 4 | async { |
1553 | 4 | decode_fut |
1554 | 4 | .await |
1555 | 2 | .map_err(|err| (CompressedReadStage::Decode1 , err1 )) |
1556 | 2 | }, |
1557 | 4 | async { |
1558 | 4 | pump_fut |
1559 | 4 | .await |
1560 | 2 | .map_err(|err| (CompressedReadStage::Pump1 , err1 )) |
1561 | 2 | }, |
1562 | | ); |
1563 | | |
1564 | 1 | match result { |
1565 | 1 | Ok(((), (), ())) => Ok(None), |
1566 | | // The blob was fully delivered and verified before this error |
1567 | | // happened (for example a transport failure while the response |
1568 | | // trailer was being finalized). Falling back would re-read a |
1569 | | // range that has already been written, into a closed writer. |
1570 | 3 | Err((stage0 , err0 )) if download_complete.load(Ordering::Relaxed)0 => { |
1571 | 0 | debug!( |
1572 | | ?stage, |
1573 | | ?err, |
1574 | | "Compressed read completed and verified before a late stage error" |
1575 | | ); |
1576 | 0 | Ok(None) |
1577 | | } |
1578 | 1 | Err((CompressedReadStage::Decode, err)) if err.code == Code::InvalidArgument => { |
1579 | 1 | Err(err.append("in GrpcStore::get_part_compressed()")) |
1580 | | } |
1581 | | // Pump failures are local delivery failures: either the |
1582 | | // downstream consumer went away or the decoded channel closed |
1583 | | // without EOF. An identity re-read helps in neither case, so |
1584 | | // never fall back — `buf_channel` reports a broken receiver as |
1585 | | // `Internal`, which `is_retryable_code` would otherwise classify |
1586 | | // as retryable. |
1587 | 1 | Err((CompressedReadStage::Pump, err)) => { |
1588 | 1 | Err(err.append("in GrpcStore::get_part_compressed()")) |
1589 | | } |
1590 | 1 | Err((CompressedReadStage::Feed, err0 )) if !is_retryable_code(err.code)0 => { |
1591 | 0 | Err(err.append("in GrpcStore::get_part_compressed()")) |
1592 | | } |
1593 | | // A clean identity restart is only safe before unverified decoded |
1594 | | // bytes have been exposed to the downstream consumer. |
1595 | 1 | Err((stage0 , err0 )) if forwarded.load(Ordering::Relaxed) == 00 => { |
1596 | 0 | warn!( |
1597 | | ?stage, |
1598 | | ?err, |
1599 | | "Compressed read interrupted before forwarding data, falling back to \ |
1600 | | identity read" |
1601 | | ); |
1602 | 0 | Ok(Some(0)) |
1603 | | } |
1604 | 1 | Err((stage, err)) => { |
1605 | 1 | debug!( |
1606 | | ?stage, |
1607 | | ?err, |
1608 | 1 | forwarded = forwarded.load(Ordering::Relaxed), |
1609 | | "Compressed read interrupted after forwarding unverified data" |
1610 | | ); |
1611 | 1 | Err(err.append( |
1612 | 1 | "in GrpcStore::get_part_compressed(): refusing identity fallback after \ |
1613 | 1 | forwarding unverified decoded data", |
1614 | 1 | )) |
1615 | | } |
1616 | | } |
1617 | 5 | } |
1618 | | } |
1619 | | |
1620 | | #[async_trait] |
1621 | | impl StoreDriver for GrpcStore { |
1622 | 0 | async fn post_init(self: Arc<Self>) -> Result<(), Error> { |
1623 | | Ok(()) |
1624 | 0 | } |
1625 | | |
1626 | | // NOTE: This function can only be safely used on CAS stores. AC stores may return a size that |
1627 | | // is incorrect. |
1628 | | async fn has_with_results( |
1629 | | self: Pin<&Self>, |
1630 | | keys: &[StoreKey<'_>], |
1631 | | results: &mut [Option<u64>], |
1632 | 1 | ) -> Result<(), Error> { |
1633 | | if matches!( |
1634 | | self.store_type, |
1635 | | nativelink_config::stores::StoreType::EventSink |
1636 | | ) { |
1637 | | return Err(make_err!( |
1638 | | Code::Unimplemented, |
1639 | | "Event sinks do not support existence checks" |
1640 | | )); |
1641 | | } |
1642 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Ac) { |
1643 | | keys.iter() |
1644 | | .zip(results.iter_mut()) |
1645 | 0 | .map(|(key, result)| async move { |
1646 | | // The length of an AC is incorrect, so we don't figure out the |
1647 | | // length, instead the biggest possible result is returned in the |
1648 | | // hope that we detect incorrect usage. |
1649 | 0 | self.get_action_result_from_digest(key.borrow().into_digest()) |
1650 | 0 | .await?; |
1651 | 0 | *result = Some(u64::MAX); |
1652 | 0 | Ok::<_, Error>(()) |
1653 | 0 | }) |
1654 | | .collect::<FuturesUnordered<_>>() |
1655 | 0 | .try_for_each(|()| future::ready(Ok(()))) |
1656 | | .await |
1657 | | .err_tip(|| "Getting upstream action cache entry")?; |
1658 | | return Ok(()); |
1659 | | } |
1660 | | |
1661 | | let missing_blobs_response = self |
1662 | | .find_missing_blobs(Request::new(FindMissingBlobsRequest { |
1663 | | instance_name: self.instance_name.clone(), |
1664 | | blob_digests: keys |
1665 | | .iter() |
1666 | 0 | .map(|k| k.borrow().into_digest().into()) |
1667 | | .collect(), |
1668 | | digest_function: Context::current() |
1669 | | .get::<DigestHasherFunc>() |
1670 | | .map_or_else(default_digest_hasher_func, |v| *v) |
1671 | | .proto_digest_func() |
1672 | | .into(), |
1673 | | })) |
1674 | | .await? |
1675 | | .into_inner(); |
1676 | | |
1677 | | // Since the ordering is not guaranteed above, the matching has to check |
1678 | | // all missing blobs against all entries in the unsorted digest list. |
1679 | | // To optimise this, the missing digests are sorted and then it is |
1680 | | // efficient to perform a binary search for each digest within the |
1681 | | // missing list. |
1682 | | let mut missing_digests = |
1683 | | Vec::with_capacity(missing_blobs_response.missing_blob_digests.len()); |
1684 | | for missing_digest in missing_blobs_response.missing_blob_digests { |
1685 | | missing_digests.push(DigestInfo::try_from(missing_digest)?); |
1686 | | } |
1687 | | missing_digests.sort_unstable(); |
1688 | | for (digest, result) in keys |
1689 | | .iter() |
1690 | 0 | .map(|v| v.borrow().into_digest()) |
1691 | | .zip(results.iter_mut()) |
1692 | | { |
1693 | | match missing_digests.binary_search(&digest) { |
1694 | | Ok(_) => *result = None, |
1695 | | Err(_) => *result = Some(digest.size_bytes()), |
1696 | | } |
1697 | | } |
1698 | | |
1699 | | Ok(()) |
1700 | 1 | } |
1701 | | |
1702 | | async fn update( |
1703 | | self: Pin<&Self>, |
1704 | | key: StoreKey<'_>, |
1705 | | mut reader: DropCloserReadHalf, |
1706 | | mut size_info: UploadSizeInfo, |
1707 | 30 | ) -> Result<u64, Error> { |
1708 | | const NON_RESUMABLE: bool = false; |
1709 | | |
1710 | | struct LocalState { |
1711 | | resource_name: String, |
1712 | | reader: DropCloserReadHalf, |
1713 | | did_error: bool, |
1714 | | bytes_received: i64, |
1715 | | /// Remainder of a buffer too large to send as one `WriteRequest`. |
1716 | | pending: Bytes, |
1717 | | size_info: UploadSizeInfo, |
1718 | | } |
1719 | | |
1720 | | let is_digest_key = matches!(key, StoreKey::Digest(_)); |
1721 | | let mut digest = key.into_digest(); |
1722 | | if matches!( |
1723 | | self.store_type, |
1724 | | nativelink_config::stores::StoreType::EventSink |
1725 | | ) { |
1726 | | let UploadSizeInfo::ExactSize(size) = size_info else { |
1727 | | return Err(make_err!( |
1728 | | Code::InvalidArgument, |
1729 | | "Event sinks require an exact upload size" |
1730 | | )); |
1731 | | }; |
1732 | | if is_digest_key { |
1733 | | return Err(make_err!( |
1734 | | Code::InvalidArgument, |
1735 | | "Event sinks require string keys" |
1736 | | )); |
1737 | | } |
1738 | | // String-key digests normally encode the key's length. Event bytes |
1739 | | // have a different length and must pass the stream size validator. |
1740 | | digest = DigestInfo::try_new(&digest.packed_hash().to_string(), size)?; |
1741 | | } |
1742 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Ac) { |
1743 | | return self.update_action_result_from_bytes(digest, reader).await; |
1744 | | } |
1745 | | |
1746 | | // This boundary knows it is writing CAS content, unlike generic stores |
1747 | | // that also hold AC values or encoded data under unrelated digest keys. |
1748 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Cas) && is_digest_key { |
1749 | | size_info.validate_size(digest.size_bytes())?; |
1750 | | size_info = UploadSizeInfo::ExactSize(digest.size_bytes()); |
1751 | | } |
1752 | | |
1753 | | // REAPI defines empty blobs as always available, even without an upload. |
1754 | | // https://github.com/bazelbuild/remote-apis/blob/main/build/bazel/remote/execution/v2/remote_execution.proto |
1755 | | // Only CAS digest keys qualify; AC values and event records still need writes. |
1756 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Cas) |
1757 | | && is_digest_key |
1758 | | && crate::cas_utils::is_zero_digest(digest) |
1759 | | { |
1760 | | let size = reader.drain().await?; |
1761 | | UploadSizeInfo::ExactSize(0).validate_size(size)?; |
1762 | | size_info.validate_size(size)?; |
1763 | | return Ok(0); |
1764 | | } |
1765 | | |
1766 | | // Only real digest keys may take the compressed path: for a string key |
1767 | | // `into_digest()` hashes the key itself, so the remote's mandatory |
1768 | | // uncompressed-digest verification would reject the upload. |
1769 | | if self.remote_cache_compression_enabled |
1770 | | && is_digest_key |
1771 | | && digest.size_bytes() >= WIRE_COMPRESSION_MIN_SIZE_BYTES |
1772 | | { |
1773 | | return self.update_compressed(digest, reader, size_info).await; |
1774 | | } |
1775 | | |
1776 | | let mut buf = Uuid::encode_buffer(); |
1777 | | let resource_name = if self.use_legacy_resource_names { |
1778 | | format!( |
1779 | | "{}/uploads/{}/blobs/{}/{}", |
1780 | | self.instance_name, |
1781 | | Uuid::new_v4().hyphenated().encode_lower(&mut buf), |
1782 | | digest.packed_hash(), |
1783 | | digest.size_bytes(), |
1784 | | ) |
1785 | | } else { |
1786 | | let digest_function = Context::current() |
1787 | | .get::<DigestHasherFunc>() |
1788 | | .map_or_else(default_digest_hasher_func, |v| *v) |
1789 | | .proto_digest_func() |
1790 | | .as_str_name() |
1791 | | .to_ascii_lowercase(); |
1792 | | format!( |
1793 | | "{}/uploads/{}/blobs/{}/{}/{}", |
1794 | | self.instance_name, |
1795 | | Uuid::new_v4().hyphenated().encode_lower(&mut buf), |
1796 | | digest_function, |
1797 | | digest.packed_hash(), |
1798 | | digest.size_bytes(), |
1799 | | ) |
1800 | | }; |
1801 | | trace!( |
1802 | | resource_name = %resource_name, |
1803 | | digest_hash = %digest.packed_hash(), |
1804 | | digest_size = digest.size_bytes(), |
1805 | | "GrpcStore::update: starting upload for digest", |
1806 | | ); |
1807 | | if matches!( |
1808 | | self.store_type, |
1809 | | nativelink_config::stores::StoreType::EventSink |
1810 | | ) { |
1811 | | return self.update_event(resource_name, reader, digest).await; |
1812 | | } |
1813 | | let local_state = LocalState { |
1814 | | resource_name, |
1815 | | reader, |
1816 | | did_error: false, |
1817 | | bytes_received: 0, |
1818 | | pending: Bytes::new(), |
1819 | | size_info, |
1820 | | }; |
1821 | | |
1822 | 37 | let stream = Box::pin(unfold(local_state, |mut local_state| async move { |
1823 | 37 | if local_state.did_error { |
1824 | 0 | error!("GrpcStore::update() polled stream after error was returned"); |
1825 | 0 | return None; |
1826 | 37 | } |
1827 | | // Drain any remainder before reading more. |
1828 | 37 | let data = if local_state.pending.is_empty() { |
1829 | 27 | match local_state |
1830 | 27 | .reader |
1831 | 27 | .recv() |
1832 | 27 | .await |
1833 | 27 | .err_tip(|| "In GrpcStore::update()") |
1834 | | { |
1835 | 27 | Ok(data) => data, |
1836 | 0 | Err(err) => { |
1837 | 0 | local_state.did_error = true; |
1838 | 0 | return Some((Err(err), local_state)); |
1839 | | } |
1840 | | } |
1841 | | } else { |
1842 | 10 | core::mem::take(&mut local_state.pending) |
1843 | | }; |
1844 | | |
1845 | | // `update_oneshot` writes a whole blob in one go, and a message |
1846 | | // over the receiver's decode limit is rejected outright rather |
1847 | | // than degrading, so split rather than forwarding it as-is. |
1848 | 37 | let data = if data.len() > MAX_WRITE_REQUEST_DATA_BYTES { |
1849 | 10 | let rest = data.slice(MAX_WRITE_REQUEST_DATA_BYTES..); |
1850 | 10 | local_state.pending = rest; |
1851 | 10 | data.slice(..MAX_WRITE_REQUEST_DATA_BYTES) |
1852 | | } else { |
1853 | 27 | data |
1854 | | }; |
1855 | | |
1856 | 37 | let write_offset = local_state.bytes_received; |
1857 | 37 | local_state.bytes_received += data.len().try_into().unwrap_or(i64::MAX); |
1858 | 37 | if data.is_empty() |
1859 | 14 | && let Err(err2 ) = local_state |
1860 | 14 | .size_info |
1861 | 14 | .validate_size(u64::try_from(local_state.bytes_received).unwrap_or(u64::MAX)) |
1862 | | { |
1863 | 2 | local_state.did_error = true; |
1864 | 2 | return Some((Err(err), local_state)); |
1865 | 35 | } |
1866 | | |
1867 | 35 | Some(( |
1868 | 35 | Ok(WriteRequest { |
1869 | 35 | resource_name: local_state.resource_name.clone(), |
1870 | 35 | write_offset, |
1871 | 35 | // EOF is when no data was polled. A split always leaves a |
1872 | 35 | // non-empty remainder, so this cannot fire early. |
1873 | 35 | finish_write: data.is_empty(), |
1874 | 35 | data, |
1875 | 35 | }), |
1876 | 35 | local_state, |
1877 | 35 | )) |
1878 | 74 | })); |
1879 | | |
1880 | | // A store upload cannot replay bytes already consumed from its reader. |
1881 | | // The upstream may have lost the upload entirely, so replaying just the |
1882 | | // last two messages at nonzero offsets is not a safe resume strategy. |
1883 | | self.write_internal( |
1884 | | WriteRequestStreamWrapper::from(stream) |
1885 | | .await |
1886 | | .err_tip(|| "in GrpcStore::update()")?, |
1887 | | NON_RESUMABLE, |
1888 | | ) |
1889 | | .await |
1890 | 7 | .map_err(|mut err| { |
1891 | 7 | if is_retryable_code(err.code) { |
1892 | 4 | let upstream_code = err.code; |
1893 | 4 | err = err.append(format!( |
1894 | 4 | "Upstream upload failed with {upstream_code:?}; retry the full blob from offset zero" |
1895 | 4 | )); |
1896 | 4 | err.code = Code::Unavailable; |
1897 | 4 | }3 |
1898 | 7 | err |
1899 | 7 | }) |
1900 | | .err_tip(|| "in GrpcStore::update()")?; |
1901 | | |
1902 | | Ok(digest.size_bytes()) |
1903 | 30 | } |
1904 | | |
1905 | | async fn get_part( |
1906 | | self: Pin<&Self>, |
1907 | | key: StoreKey<'_>, |
1908 | | writer: &mut DropCloserWriteHalf, |
1909 | | offset: u64, |
1910 | | length: Option<u64>, |
1911 | 223 | ) -> Result<(), Error> { |
1912 | | struct LocalState<'a> { |
1913 | | resource_name: String, |
1914 | | writer: &'a mut DropCloserWriteHalf, |
1915 | | read_offset: i64, |
1916 | | read_limit: i64, |
1917 | | } |
1918 | | |
1919 | | if matches!( |
1920 | | self.store_type, |
1921 | | nativelink_config::stores::StoreType::EventSink |
1922 | | ) { |
1923 | | return Err(make_err!( |
1924 | | Code::Unimplemented, |
1925 | | "Event sinks do not support reads" |
1926 | | )); |
1927 | | } |
1928 | | |
1929 | | let is_digest_key = matches!(key, StoreKey::Digest(_)); |
1930 | | let digest = key.into_digest(); |
1931 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Ac) { |
1932 | | let offset = usize::try_from(offset).err_tip(|| "Could not convert offset to usize")?; |
1933 | | let length = length |
1934 | 0 | .map(|v| usize::try_from(v).err_tip(|| "Could not convert length to usize")) |
1935 | | .transpose()?; |
1936 | | |
1937 | | return self |
1938 | | .get_action_result_as_part(digest, writer, offset, length) |
1939 | | .await; |
1940 | | } |
1941 | | |
1942 | | // Shortcut for empty blobs. |
1943 | | if digest.size_bytes() == 0 { |
1944 | | return writer.send_eof(); |
1945 | | } |
1946 | | |
1947 | | // When configured, coalesce full reads of small blobs into |
1948 | | // BatchReadBlobs RPCs. `batched_read` returns `None` when the queue |
1949 | | // is over budget, in which case we fall through to the stream path. |
1950 | | if let Some(batcher) = &self.read_batcher |
1951 | | && is_digest_key |
1952 | | && offset == 0 |
1953 | 0 | && length.is_none_or(|len| len >= digest.size_bytes()) |
1954 | | && digest.size_bytes() <= batcher.max_blob_size_bytes |
1955 | | && let Some(result) = self.batched_read(batcher, digest).await |
1956 | | { |
1957 | | match result { |
1958 | | Ok(data) => { |
1959 | | if !data.is_empty() { |
1960 | | writer |
1961 | | .send(data) |
1962 | | .await |
1963 | | .err_tip(|| "Failed to write data in GrpcStore::get_part()")?; |
1964 | | } |
1965 | | return writer |
1966 | | .send_eof() |
1967 | | .err_tip(|| "Failed to send EOF in GrpcStore::get_part()"); |
1968 | | } |
1969 | | // A retryable error falls through to the ByteStream path |
1970 | | // below, which re-enters the full retry machinery. This |
1971 | | // matches the retry behavior reads had before batching. |
1972 | | Err(err) if is_retryable_code(err.code) => { |
1973 | | warn!( |
1974 | | ?err, |
1975 | | "Batched read failed with retryable error, falling back to ByteStream read", |
1976 | | ); |
1977 | | } |
1978 | | Err(err) => return Err(err.append("in GrpcStore::get_part()")), |
1979 | | } |
1980 | | } |
1981 | | |
1982 | | let mut length = length; |
1983 | | if self.remote_cache_compression_enabled |
1984 | | && is_digest_key |
1985 | | && offset == 0 |
1986 | 0 | && length.is_none_or(|len| len >= digest.size_bytes()) |
1987 | | && digest.size_bytes() >= WIRE_COMPRESSION_MIN_SIZE_BYTES |
1988 | | { |
1989 | | match self.get_part_compressed(digest, writer).await? { |
1990 | | None => return Ok(()), |
1991 | | // Retryable failures before any compressed output restart |
1992 | | // through identity. Partial compressed reads are terminal and |
1993 | | // must never reach this branch with a nonzero offset. |
1994 | | Some(0) => { |
1995 | | length = Some(digest.size_bytes()); |
1996 | | } |
1997 | | Some(forwarded) => { |
1998 | | return Err(make_err!( |
1999 | | Code::Internal, |
2000 | | "Compressed identity fallback returned unsafe nonzero offset {}", |
2001 | | forwarded |
2002 | | )); |
2003 | | } |
2004 | | } |
2005 | | } |
2006 | | |
2007 | | let resource_name = if self.use_legacy_resource_names { |
2008 | | format!( |
2009 | | "{}/blobs/{}/{}", |
2010 | | self.instance_name, |
2011 | | digest.packed_hash(), |
2012 | | digest.size_bytes(), |
2013 | | ) |
2014 | | } else { |
2015 | | let digest_function = Context::current() |
2016 | | .get::<DigestHasherFunc>() |
2017 | | .map_or_else(default_digest_hasher_func, |v| *v) |
2018 | | .proto_digest_func() |
2019 | | .as_str_name() |
2020 | | .to_ascii_lowercase(); |
2021 | | format!( |
2022 | | "{}/blobs/{}/{}/{}", |
2023 | | self.instance_name, |
2024 | | digest_function, |
2025 | | digest.packed_hash(), |
2026 | | digest.size_bytes(), |
2027 | | ) |
2028 | | }; |
2029 | | |
2030 | | let local_state = LocalState { |
2031 | | resource_name, |
2032 | | writer, |
2033 | | read_offset: i64::try_from(offset).err_tip(|| "Could not convert offset to i64")?, |
2034 | | read_limit: i64::try_from(length.unwrap_or(0)) |
2035 | | .err_tip(|| "Could not convert length to i64")?, |
2036 | | }; |
2037 | | |
2038 | | self.retrier |
2039 | 10 | .retry(unfold(local_state, move |mut local_state| async move { |
2040 | 10 | let request = ReadRequest { |
2041 | 10 | resource_name: local_state.resource_name.clone(), |
2042 | 10 | read_offset: local_state.read_offset, |
2043 | 10 | read_limit: local_state.read_limit, |
2044 | 10 | }; |
2045 | 10 | let mut stream = match self |
2046 | 10 | .read_internal(request) |
2047 | 10 | .await |
2048 | 10 | .err_tip(|| "in GrpcStore::get_part()") |
2049 | | { |
2050 | 10 | Ok(stream) => stream, |
2051 | 0 | Err(err) => return Some((RetryResult::Retry(err), local_state)), |
2052 | | }; |
2053 | | |
2054 | | loop { |
2055 | 50 | let data = match stream.next().await { |
2056 | | // Create an empty response to represent EOF. |
2057 | 10 | None => Bytes::new(), |
2058 | 40 | Some(Ok(message)) => message.data, |
2059 | 0 | Some(Err(status)) => { |
2060 | 0 | return Some(( |
2061 | 0 | RetryResult::Retry( |
2062 | 0 | Into::<Error>::into(status) |
2063 | 0 | .append("While fetching message in GrpcStore::get_part()"), |
2064 | 0 | ), |
2065 | 0 | local_state, |
2066 | 0 | )); |
2067 | | } |
2068 | | }; |
2069 | 50 | let length = data.len().try_into().unwrap_or(i64::MAX); |
2070 | | |
2071 | | // This is the usual exit from the loop at EOF. |
2072 | 50 | if length == 0 { |
2073 | 10 | let eof_result = local_state |
2074 | 10 | .writer |
2075 | 10 | .send_eof() |
2076 | 10 | .err_tip(|| "Could not send eof in GrpcStore::get_part()") |
2077 | 10 | .map_or_else(RetryResult::Err, RetryResult::Ok); |
2078 | 10 | return Some((eof_result, local_state)); |
2079 | 40 | } |
2080 | | // Forward the data upstream. |
2081 | 40 | if let Err(err0 ) = local_state |
2082 | 40 | .writer |
2083 | 40 | .send(data) |
2084 | 40 | .await |
2085 | 40 | .err_tip(|| "While sending in GrpcStore::get_part()") |
2086 | | { |
2087 | 0 | return Some((RetryResult::Err(err), local_state)); |
2088 | 40 | } |
2089 | 40 | local_state.read_offset += length; |
2090 | | } |
2091 | 20 | })) |
2092 | | .await |
2093 | 223 | } |
2094 | | |
2095 | 3 | fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver { |
2096 | 3 | self |
2097 | 3 | } |
2098 | | |
2099 | 3 | fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) { |
2100 | 3 | self |
2101 | 3 | } |
2102 | | |
2103 | 0 | fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> { |
2104 | 0 | self |
2105 | 0 | } |
2106 | | |
2107 | 0 | fn register_remove_callback(self: Arc<Self>, _callback: RemoveCallback) -> Result<(), Error> { |
2108 | 0 | Err(Error::new( |
2109 | 0 | Code::Internal, |
2110 | 0 | "gRPC stores are incompatible with removal callbacks".to_string(), |
2111 | 0 | )) |
2112 | 0 | } |
2113 | | } |
2114 | | |
2115 | | default_health_status_indicator!(GrpcStore); |