/build/source/nativelink-store/src/grpc_store.rs
Line | Count | Source |
1 | | // Copyright 2024 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::pin::Pin; |
16 | | use core::sync::atomic::{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, |
36 | | }; |
37 | | use nativelink_proto::google::bytestream::byte_stream_client::ByteStreamClient; |
38 | | use nativelink_proto::google::bytestream::{ |
39 | | QueryWriteStatusRequest, QueryWriteStatusResponse, ReadRequest, ReadResponse, WriteRequest, |
40 | | WriteResponse, |
41 | | }; |
42 | | use nativelink_util::buf_channel::{DropCloserReadHalf, DropCloserWriteHalf}; |
43 | | use nativelink_util::common::DigestInfo; |
44 | | use nativelink_util::connection_manager::ConnectionManager; |
45 | | use nativelink_util::digest_hasher::{DigestHasherFunc, default_digest_hasher_func}; |
46 | | use nativelink_util::health_utils::HealthStatusIndicator; |
47 | | use nativelink_util::proto_stream_utils::{ |
48 | | FirstStream, WriteRequestStreamWrapper, WriteState, WriteStateWrapper, |
49 | | }; |
50 | | use nativelink_util::resource_info::ResourceInfo; |
51 | | use nativelink_util::retry::{Retrier, RetryResult}; |
52 | | use nativelink_util::store_trait::{RemoveCallback, StoreDriver, StoreKey, UploadSizeInfo}; |
53 | | use nativelink_util::telemetry::ClientHeaders; |
54 | | use nativelink_util::{background_spawn, default_health_status_indicator, tls_utils}; |
55 | | use opentelemetry::context::Context; |
56 | | use opentelemetry::global; |
57 | | use opentelemetry::propagation::Injector; |
58 | | use parking_lot::Mutex; |
59 | | use prost::Message; |
60 | | use tokio::sync::{Semaphore, oneshot}; |
61 | | use tokio::time::sleep; |
62 | | use tonic::metadata::{Ascii, MetadataKey, MetadataValue}; |
63 | | use tonic::{Code, IntoRequest, Request, Response, Status, Streaming}; |
64 | | use tracing::{error, trace, warn}; |
65 | | use uuid::Uuid; |
66 | | |
67 | | struct TonicMetadataInjector<'a>(&'a mut tonic::metadata::MetadataMap); |
68 | | |
69 | | impl Injector for TonicMetadataInjector<'_> { |
70 | 0 | fn set(&mut self, key: &str, value: String) { |
71 | 0 | if let (Ok(k), Ok(v)) = ( |
72 | 0 | MetadataKey::from_bytes(key.as_bytes()), |
73 | 0 | MetadataValue::try_from(&value), |
74 | 0 | ) { |
75 | 0 | self.0.insert(k, v); |
76 | 0 | } |
77 | 0 | } |
78 | | } |
79 | | |
80 | | /// Adds configured static headers, forwards nominated client request headers, |
81 | | /// and injects the current OpenTelemetry trace context into an outgoing gRPC |
82 | | /// request. |
83 | 14 | fn enrich_request<T>( |
84 | 14 | mut request: Request<T>, |
85 | 14 | headers: &[(MetadataKey<Ascii>, MetadataValue<Ascii>)], |
86 | 14 | forward_headers: &[String], |
87 | 14 | ) -> Request<T> { |
88 | 14 | for (key1 , value1 ) in headers { |
89 | 1 | request.metadata_mut().insert(key.clone(), value.clone()); |
90 | 1 | } |
91 | 14 | if !forward_headers.is_empty() |
92 | 1 | && let Some(client_headers) = Context::current().get::<ClientHeaders>() |
93 | | { |
94 | 1 | for name in forward_headers { |
95 | 1 | if let Some(value) = client_headers.0.get(&name.to_lowercase()) |
96 | 1 | && let (Ok(k), Ok(v)) = ( |
97 | 1 | MetadataKey::from_bytes(name.as_bytes()), |
98 | 1 | MetadataValue::try_from(value.as_str()), |
99 | | ) |
100 | 1 | { |
101 | 1 | request.metadata_mut().insert(k, v); |
102 | 1 | }0 |
103 | | } |
104 | 13 | } |
105 | 14 | global::get_text_map_propagator(|propagator| { |
106 | 14 | propagator.inject(&mut TonicMetadataInjector(request.metadata_mut())); |
107 | 14 | }); |
108 | 14 | request |
109 | 14 | } |
110 | | |
111 | | /// Estimated per-entry protobuf and framing overhead charged against |
112 | | /// `max_batch_bytes`, so that batches of many tiny blobs cannot push a |
113 | | /// `BatchReadBlobs` response over the gRPC message size limit. |
114 | | const BATCH_READ_PER_ENTRY_OVERHEAD_BYTES: u64 = 256; |
115 | | |
116 | | /// A small-blob read waiting to be coalesced into a `BatchReadBlobs` RPC. |
117 | | #[derive(Debug)] |
118 | | struct PendingRead { |
119 | | digest: DigestInfo, |
120 | | digest_function: i32, |
121 | | tx: oneshot::Sender<Result<Bytes, Error>>, |
122 | | } |
123 | | |
124 | | /// The pending small-blob reads plus the total payload bytes they declare. |
125 | | #[derive(Debug, Default)] |
126 | | struct ReadQueue { |
127 | | items: VecDeque<PendingRead>, |
128 | | bytes: u64, |
129 | | } |
130 | | |
131 | | /// State for coalescing small-blob reads into `BatchReadBlobs` RPCs. |
132 | | /// |
133 | | /// This uses a slot-based group commit scheme: callers enqueue their read and |
134 | | /// then try to start a detached dispatcher task by acquiring one of |
135 | | /// `dispatch_slots` semaphore permits. A dispatcher repeatedly drains up to |
136 | | /// `max_batch_bytes` worth of pending reads into a single `BatchReadBlobs` |
137 | | /// request until the queue is empty. This is work-conserving (a read never |
138 | | /// waits while a dispatch slot is free) and uses no timers. |
139 | | #[derive(Debug, MetricsComponent)] |
140 | | struct ReadBatcher { |
141 | | max_blob_size_bytes: u64, |
142 | | max_batch_bytes: u64, |
143 | | max_queued_bytes: u64, |
144 | | queue: Mutex<ReadQueue>, |
145 | | dispatch_slots: Arc<Semaphore>, |
146 | | #[metric(help = "Number of BatchReadBlobs RPCs sent by the read coalescer")] |
147 | | batches_sent: AtomicU64, |
148 | | #[metric(help = "Number of blob reads coalesced into BatchReadBlobs RPCs")] |
149 | | blobs_batched: AtomicU64, |
150 | | #[metric( |
151 | | help = "Number of reads that bypassed batching because the queue byte budget was full" |
152 | | )] |
153 | | queue_bypasses: AtomicU64, |
154 | | #[metric(help = "Number of batched reads that resolved to a per-entry error")] |
155 | | batched_read_errors: AtomicU64, |
156 | | #[metric(help = "Payload bytes currently waiting in the read coalescer queue")] |
157 | | queued_bytes: AtomicU64, |
158 | | } |
159 | | |
160 | | impl ReadBatcher { |
161 | 6 | fn new(config: &GrpcReadBatchingConfig) -> Self { |
162 | 6 | Self { |
163 | 6 | max_blob_size_bytes: config.max_blob_size_bytes, |
164 | 6 | max_batch_bytes: config.max_batch_bytes, |
165 | 6 | max_queued_bytes: config.max_queued_bytes, |
166 | 6 | queue: Mutex::new(ReadQueue::default()), |
167 | 6 | dispatch_slots: Arc::new(Semaphore::new(config.dispatch_slots)), |
168 | 6 | batches_sent: AtomicU64::new(0), |
169 | 6 | blobs_batched: AtomicU64::new(0), |
170 | 6 | queue_bypasses: AtomicU64::new(0), |
171 | 6 | batched_read_errors: AtomicU64::new(0), |
172 | 6 | queued_bytes: AtomicU64::new(0), |
173 | 6 | } |
174 | 6 | } |
175 | | } |
176 | | |
177 | | /// Mirrors the default retryable-code classification used by |
178 | | /// `nativelink_util::retry::Retrier::should_retry`: only codes that are |
179 | | /// always terminal are considered non-retryable. |
180 | 1 | const fn is_retryable_code(code: Code) -> bool { |
181 | 0 | !matches!( |
182 | 1 | code, |
183 | | Code::Ok |
184 | | | Code::InvalidArgument |
185 | | | Code::FailedPrecondition |
186 | | | Code::OutOfRange |
187 | | | Code::Unimplemented |
188 | | | Code::NotFound |
189 | | | Code::AlreadyExists |
190 | | | Code::PermissionDenied |
191 | | | Code::Unauthenticated |
192 | | ) |
193 | 1 | } |
194 | | |
195 | | // This store is usually a pass-through store, but can also be used as a CAS store. Using it as an |
196 | | // AC store has one major side-effect... The has() function may not give the proper size of the |
197 | | // underlying data. This might cause issues if embedded in certain stores. |
198 | | #[derive(Debug, MetricsComponent)] |
199 | | pub struct GrpcStore { |
200 | | #[metric(help = "Instance name for the store")] |
201 | | instance_name: String, |
202 | | store_type: nativelink_config::stores::StoreType, |
203 | | retrier: Retrier, |
204 | | connection_manager: ConnectionManager, |
205 | | /// Per-RPC timeout. `Duration::ZERO` means disabled. |
206 | | rpc_timeout: Duration, |
207 | | use_legacy_resource_names: bool, |
208 | | headers: Vec<(MetadataKey<Ascii>, MetadataValue<Ascii>)>, |
209 | | forward_headers: Vec<String>, |
210 | | /// When configured, coalesces small-blob reads into `BatchReadBlobs` |
211 | | /// RPCs. `None` means reads always use the `ByteStream` `Read` path. |
212 | | #[metric(group = "read_batcher")] |
213 | | read_batcher: Option<ReadBatcher>, |
214 | | /// Used by the read coalescer to hand a strong reference of this store |
215 | | /// to detached dispatcher tasks. |
216 | | weak_self: Weak<Self>, |
217 | | } |
218 | | |
219 | | impl GrpcStore { |
220 | 16 | pub async fn new(spec: &GrpcSpec) -> Result<Arc<Self>, Error> { |
221 | 16 | Self::new_with_jitter(spec, spec.retry.make_jitter_fn()).await |
222 | 16 | } |
223 | | |
224 | 16 | pub async fn new_with_jitter( |
225 | 16 | spec: &GrpcSpec, |
226 | 16 | jitter_fn: Arc<dyn Fn(Duration) -> Duration + Send + Sync>, |
227 | 16 | ) -> Result<Arc<Self>, Error> { |
228 | 0 | error_if!( |
229 | 16 | spec.endpoints.is_empty(), |
230 | | "Expected at least 1 endpoint in GrpcStore" |
231 | | ); |
232 | 16 | let mut endpoints = Vec::with_capacity(spec.endpoints.len()); |
233 | 16 | for endpoint_config in &spec.endpoints { |
234 | 16 | let endpoint = tls_utils::endpoint(endpoint_config).map_err(|e| {0 |
235 | 0 | Error::from_std_err(Code::InvalidArgument, &e) |
236 | 0 | .append("Invalid URI for GrpcStore endpoint") |
237 | 0 | })?; |
238 | 16 | endpoints.push(endpoint); |
239 | | } |
240 | | |
241 | 16 | let rpc_timeout = Duration::from_secs(spec.rpc_timeout_s); |
242 | | |
243 | 16 | let read_batcher15 = match &spec.experimental_read_batching { |
244 | 7 | Some(config) => { |
245 | 0 | error_if!( |
246 | 7 | config.dispatch_slots == 0, |
247 | | "experimental_read_batching.dispatch_slots must be greater than zero" |
248 | | ); |
249 | | // Batched reads share one upstream RPC across many client |
250 | | // requests, so per-client forwarded headers (e.g. credentials) |
251 | | // cannot be attached correctly. |
252 | 6 | error_if!( |
253 | 7 | !spec.forward_headers.is_empty(), |
254 | | "experimental_read_batching is incompatible with forward_headers" |
255 | | ); |
256 | 6 | Some(ReadBatcher::new(config)) |
257 | | } |
258 | 9 | None => None, |
259 | | }; |
260 | | |
261 | 15 | let mut headers = Vec::with_capacity(spec.headers.len()); |
262 | 15 | for (name1 , value1 ) in &spec.headers { |
263 | | // We lowercase keys as HTTP headers are case-insensitive so we should match all cases |
264 | 1 | let key = MetadataKey::from_bytes(name.to_lowercase().as_bytes()).map_err(|_| {0 |
265 | 0 | make_err!(Code::InvalidArgument, "Invalid gRPC metadata key: {name}") |
266 | 0 | })?; |
267 | 1 | let val = MetadataValue::try_from(value.as_str()).map_err(|_| {0 |
268 | 0 | make_err!( |
269 | 0 | Code::InvalidArgument, |
270 | | "Invalid gRPC metadata value for key: {name}" |
271 | | ) |
272 | 0 | })?; |
273 | 1 | headers.push((key, val)); |
274 | | } |
275 | | |
276 | 15 | Ok(Arc::new_cyclic(|weak_self| Self { |
277 | 15 | weak_self: weak_self.clone(), |
278 | 15 | instance_name: spec.instance_name.clone(), |
279 | 15 | store_type: spec.store_type, |
280 | 15 | retrier: Retrier::new( |
281 | 15 | Arc::new(|duration| Box::pin0 (sleep0 (duration0 ))), |
282 | 15 | jitter_fn.clone(), |
283 | 15 | spec.retry.clone(), |
284 | | ), |
285 | 15 | connection_manager: ConnectionManager::new( |
286 | 15 | endpoints, |
287 | 15 | spec.connections_per_endpoint, |
288 | 15 | spec.max_concurrent_requests, |
289 | 15 | spec.retry.clone(), |
290 | 15 | jitter_fn, |
291 | | ), |
292 | 15 | rpc_timeout, |
293 | 15 | use_legacy_resource_names: spec.use_legacy_resource_names, |
294 | 15 | read_batcher, |
295 | 15 | headers, |
296 | | // We lowercase keys as HTTP headers are case-insensitive so we should match all cases |
297 | 15 | forward_headers: spec |
298 | 15 | .forward_headers |
299 | 15 | .iter() |
300 | 15 | .map(|s| s1 .to_lowercase1 ()) |
301 | 15 | .collect(), |
302 | 15 | })) |
303 | 16 | } |
304 | | |
305 | 6 | async fn perform_request<F, Fut, R, I>(&self, input: I, mut request: F) -> Result<R, Error> |
306 | 6 | where |
307 | 6 | F: FnMut(I) -> Fut + Send + Copy, |
308 | 6 | Fut: Future<Output = Result<R, Error>> + Send, |
309 | 6 | R: Send, |
310 | 6 | I: Send + Clone, |
311 | 6 | { |
312 | 6 | self.retrier |
313 | 6 | .retry(unfold(input, move |input| async move { |
314 | 6 | let input_clone = input.clone(); |
315 | | Some(( |
316 | 6 | request(input_clone) |
317 | 6 | .await |
318 | 6 | .map_or_else(RetryResult::Retry, RetryResult::Ok), |
319 | 6 | input, |
320 | | )) |
321 | 12 | })) |
322 | 6 | .await |
323 | 6 | } |
324 | | |
325 | 1 | pub async fn find_missing_blobs( |
326 | 1 | &self, |
327 | 1 | grpc_request: Request<FindMissingBlobsRequest>, |
328 | 1 | ) -> Result<Response<FindMissingBlobsResponse>, Error> { |
329 | 0 | error_if!( |
330 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
331 | | "CAS operation on AC store" |
332 | | ); |
333 | | |
334 | 1 | let mut request = grpc_request.into_inner(); |
335 | | |
336 | | // Some builds (Chromium for example) do lots of empty requests for some reason, so shortcut them |
337 | 1 | if request.blob_digests.is_empty() { |
338 | 1 | return Ok(Response::new(FindMissingBlobsResponse { |
339 | 1 | missing_blob_digests: vec![], |
340 | 1 | })); |
341 | 0 | } |
342 | | |
343 | 0 | request.instance_name.clone_from(&self.instance_name); |
344 | 0 | self.perform_request(request, |request| async move { |
345 | 0 | let channel = self |
346 | 0 | .connection_manager |
347 | 0 | .connection(format!( |
348 | 0 | "find_missing_blobs: ({}) {:?}", |
349 | 0 | request.blob_digests.len(), |
350 | 0 | request.blob_digests |
351 | 0 | )) |
352 | 0 | .await |
353 | 0 | .err_tip(|| "in find_missing_blobs")?; |
354 | 0 | ContentAddressableStorageClient::new(channel) |
355 | 0 | .find_missing_blobs(enrich_request( |
356 | 0 | Request::new(request), |
357 | 0 | &self.headers, |
358 | 0 | &self.forward_headers, |
359 | 0 | )) |
360 | 0 | .await |
361 | 0 | .err_tip(|| "in GrpcStore::find_missing_blobs") |
362 | 0 | }) |
363 | 0 | .await |
364 | 1 | } |
365 | | |
366 | 0 | pub async fn batch_update_blobs( |
367 | 0 | &self, |
368 | 0 | grpc_request: Request<BatchUpdateBlobsRequest>, |
369 | 0 | ) -> Result<Response<BatchUpdateBlobsResponse>, Error> { |
370 | 0 | error_if!( |
371 | 0 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
372 | | "CAS operation on AC store" |
373 | | ); |
374 | | |
375 | 0 | let mut request = grpc_request.into_inner(); |
376 | 0 | request.instance_name.clone_from(&self.instance_name); |
377 | 0 | self.perform_request(request, |request| async move { |
378 | 0 | let channel = self |
379 | 0 | .connection_manager |
380 | 0 | .connection("batch_update_blobs".into()) |
381 | 0 | .await |
382 | 0 | .err_tip(|| "in batch_update_blobs")?; |
383 | 0 | ContentAddressableStorageClient::new(channel) |
384 | 0 | .batch_update_blobs(enrich_request( |
385 | 0 | Request::new(request), |
386 | 0 | &self.headers, |
387 | 0 | &self.forward_headers, |
388 | 0 | )) |
389 | 0 | .await |
390 | 0 | .err_tip(|| "in GrpcStore::batch_update_blobs") |
391 | 0 | }) |
392 | 0 | .await |
393 | 0 | } |
394 | | |
395 | 4 | pub async fn batch_read_blobs( |
396 | 4 | &self, |
397 | 4 | grpc_request: Request<BatchReadBlobsRequest>, |
398 | 4 | ) -> Result<Response<BatchReadBlobsResponse>, Error> { |
399 | 0 | error_if!( |
400 | 4 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
401 | | "CAS operation on AC store" |
402 | | ); |
403 | | |
404 | 4 | let mut request = grpc_request.into_inner(); |
405 | 4 | request.instance_name.clone_from(&self.instance_name); |
406 | 4 | self.perform_request(request, |request| async move { |
407 | 4 | let channel = self |
408 | 4 | .connection_manager |
409 | 4 | .connection("batch_read_blobs".into()) |
410 | 4 | .await |
411 | 4 | .err_tip(|| "in batch_read_blobs")?0 ; |
412 | 4 | ContentAddressableStorageClient::new(channel) |
413 | 4 | .batch_read_blobs(enrich_request( |
414 | 4 | Request::new(request), |
415 | 4 | &self.headers, |
416 | 4 | &self.forward_headers, |
417 | 4 | )) |
418 | 4 | .await |
419 | 4 | .err_tip(|| "in GrpcStore::batch_read_blobs") |
420 | 8 | }) |
421 | 4 | .await |
422 | 4 | } |
423 | | |
424 | | /// Enqueues a small-blob read for coalescing into a `BatchReadBlobs` RPC |
425 | | /// and waits for its result. Returns `None` when the queue is over its |
426 | | /// byte budget, in which case the caller must fall back to the |
427 | | /// `ByteStream` `Read` path. |
428 | 208 | async fn batched_read( |
429 | 208 | &self, |
430 | 208 | batcher: &ReadBatcher, |
431 | 208 | digest: DigestInfo, |
432 | 208 | ) -> Option<Result<Bytes, Error>> { |
433 | | // Capture the digest function from the caller's ambient context now; |
434 | | // the dispatcher runs on a detached task with no such context. |
435 | 208 | let digest_function: i32 = Context::current() |
436 | 208 | .get::<DigestHasherFunc>() |
437 | 208 | .map_or_else(default_digest_hasher_func, |v| *v) |
438 | 208 | .proto_digest_func() |
439 | 208 | .into(); |
440 | 208 | let (tx, rx) = oneshot::channel(); |
441 | | { |
442 | 208 | let mut queue = batcher.queue.lock(); |
443 | | // Admission control. On overflow gracefully degrade to the |
444 | | // stream path instead of blocking. |
445 | 208 | let new_bytes = queue.bytes.saturating_add(digest.size_bytes()); |
446 | 208 | if new_bytes > batcher.max_queued_bytes { |
447 | 0 | batcher.queue_bypasses.fetch_add(1, Ordering::Relaxed); |
448 | 0 | return None; |
449 | 208 | } |
450 | 208 | queue.bytes = new_bytes; |
451 | 208 | batcher.queued_bytes.store(new_bytes, Ordering::Relaxed); |
452 | 208 | queue.items.push_back(PendingRead { |
453 | 208 | digest, |
454 | 208 | digest_function, |
455 | 208 | tx, |
456 | 208 | }); |
457 | | } |
458 | | |
459 | 208 | self.maybe_dispatch_read_batches(batcher); |
460 | | |
461 | 208 | Some(rx.await.unwrap_or_else207 (|_| {0 |
462 | 0 | Err(make_err!( |
463 | 0 | Code::Internal, |
464 | 0 | "Read batch dispatcher dropped result in GrpcStore::batched_read" |
465 | 0 | )) |
466 | 0 | })) |
467 | 207 | } |
468 | | |
469 | | /// Tries to start a read batch dispatcher. This is work-conserving: if a |
470 | | /// dispatch slot is free a dispatcher is started immediately, otherwise |
471 | | /// one of the active dispatchers is responsible for every currently |
472 | | /// queued item. The dispatcher runs as a detached task so that |
473 | | /// cancellation of any individual reader can neither abort an in-flight |
474 | | /// `BatchReadBlobs` RPC nor strand still-queued waiters. |
475 | 208 | fn maybe_dispatch_read_batches(&self, batcher: &ReadBatcher) { |
476 | 208 | let Ok(mut permit4 ) = batcher.dispatch_slots.clone().try_acquire_owned() else { |
477 | 204 | return; |
478 | | }; |
479 | 4 | let Some(store) = self.weak_self.upgrade() else { |
480 | 0 | return; |
481 | | }; |
482 | 4 | background_spawn!("grpc_store_read_batch_dispatch", async move { |
483 | 4 | let Some(batcher) = &store.read_batcher else { |
484 | 0 | return; |
485 | | }; |
486 | | loop { |
487 | 4 | store.dispatch_read_batches(batcher).await; |
488 | 4 | drop(permit); |
489 | | // Items may have been enqueued between the last drain and |
490 | | // the permit release. Re-check so they are not stranded |
491 | | // with no active dispatcher. |
492 | 4 | if batcher.queue.lock().items.is_empty() { |
493 | 4 | return; |
494 | 0 | } |
495 | 0 | match batcher.dispatch_slots.clone().try_acquire_owned() { |
496 | 0 | Ok(new_permit) => permit = new_permit, |
497 | | // Another dispatcher is active and will observe these |
498 | | // items (or re-check after releasing its own permit). |
499 | 0 | Err(_) => return, |
500 | | } |
501 | | } |
502 | 4 | }); |
503 | 208 | } |
504 | | |
505 | | /// Drains the pending read queue, sending one `BatchReadBlobs` RPC per |
506 | | /// drained batch, until the queue is empty. |
507 | 4 | async fn dispatch_read_batches(&self, batcher: &ReadBatcher) { |
508 | | loop { |
509 | 4 | let batch = { |
510 | 8 | let mut queue = batcher.queue.lock(); |
511 | 8 | let Some(head4 ) = queue.items.front() else { |
512 | 4 | return; |
513 | | }; |
514 | | // All digests in one BatchReadBlobsRequest must use the same |
515 | | // digest function. Partition-drain: take items matching the |
516 | | // head's digest function from anywhere in the queue (up to |
517 | | // the batch budget) and keep the rest in relative order. |
518 | 4 | let digest_function = head.digest_function; |
519 | 4 | let mut batch = Vec::new(); |
520 | 4 | let mut batch_bytes = 0u64; |
521 | 4 | let mut rest = VecDeque::with_capacity(queue.items.len()); |
522 | 212 | while let Some(item208 ) = queue.items.pop_front() { |
523 | 208 | let item_cost = item |
524 | 208 | .digest |
525 | 208 | .size_bytes() |
526 | 208 | .saturating_add(BATCH_READ_PER_ENTRY_OVERHEAD_BYTES); |
527 | 208 | if item.digest_function == digest_function |
528 | 208 | && (batch.is_empty() |
529 | 204 | || batch_bytes.saturating_add(item_cost) <= batcher.max_batch_bytes) |
530 | 208 | { |
531 | 208 | batch_bytes = batch_bytes.saturating_add(item_cost); |
532 | 208 | queue.bytes = queue.bytes.saturating_sub(item.digest.size_bytes()); |
533 | 208 | batch.push(item); |
534 | 208 | } else { |
535 | 0 | rest.push_back(item); |
536 | 0 | } |
537 | | } |
538 | 4 | queue.items = rest; |
539 | 4 | batcher.queued_bytes.store(queue.bytes, Ordering::Relaxed); |
540 | 4 | batch |
541 | | }; |
542 | 4 | self.send_read_batch(batcher, batch).await; |
543 | | } |
544 | 4 | } |
545 | | |
546 | | /// Sends one `BatchReadBlobs` RPC for `batch` and demultiplexes the |
547 | | /// per-blob responses back to the waiting readers. One failed item does |
548 | | /// not affect its batch-mates; failure of the whole RPC is broadcast to |
549 | | /// every item in the batch. |
550 | 4 | async fn send_read_batch(&self, batcher: &ReadBatcher, batch: Vec<PendingRead>) { |
551 | 4 | let Some(digest_function) = batch.first().map(|item| item.digest_function) else { |
552 | 0 | return; |
553 | | }; |
554 | | // Servers may dedupe duplicate digests within one request, so group |
555 | | // the waiters per digest and request each digest exactly once, |
556 | | // fanning the (refcounted) data out to every waiter. |
557 | 4 | let batch_len = u64::try_from(batch.len()).unwrap_or(u64::MAX); |
558 | 4 | let mut waiters: HashMap<DigestInfo, Vec<PendingRead>> = HashMap::new(); |
559 | 208 | for item in batch4 { |
560 | 208 | waiters.entry(item.digest).or_default().push(item); |
561 | 208 | } |
562 | 4 | let request = BatchReadBlobsRequest { |
563 | | // batch_read_blobs() overwrites the instance name, so there is |
564 | | // no need to set it here. |
565 | 4 | instance_name: String::new(), |
566 | 207 | digests: waiters4 .keys4 ().map4 (|digest| (*digest).into()).collect4 (), |
567 | 4 | acceptable_compressors: vec![], |
568 | 4 | digest_function, |
569 | | }; |
570 | 4 | batcher.batches_sent.fetch_add(1, Ordering::Relaxed); |
571 | 4 | batcher |
572 | 4 | .blobs_batched |
573 | 4 | .fetch_add(batch_len, Ordering::Relaxed); |
574 | 4 | let response = match self.batch_read_blobs(Request::new(request)).await { |
575 | 4 | Ok(response) => response.into_inner(), |
576 | 0 | Err(err) => { |
577 | | // The whole RPC failed, so every waiter in this batch gets |
578 | | // the error. Waiters may have gone away, ignore send errors. |
579 | 0 | for item in waiters.into_values().flatten() { |
580 | 0 | drop(item.tx.send(Err(err.clone()))); |
581 | 0 | } |
582 | 0 | return; |
583 | | } |
584 | | }; |
585 | 207 | for entry in response.responses4 { |
586 | 207 | let Some(Ok(entry_digest)) = entry.digest.map(DigestInfo::try_from) else { |
587 | 0 | continue; |
588 | | }; |
589 | 207 | let Some(items) = waiters.remove(&entry_digest) else { |
590 | 0 | continue; |
591 | | }; |
592 | 207 | let entry_len = u64::try_from(entry.data.len()).unwrap_or(u64::MAX); |
593 | 207 | let result = if let Some(status1 ) = entry.status.filter(|status| status.code != 0) { |
594 | 1 | Err(Error::from(status) |
595 | 1 | .append("Batch read entry failed in GrpcStore::send_read_batch")) |
596 | 206 | } else if entry.compressor != 0 { |
597 | | // We requested no acceptable compressors, so data must be |
598 | | // returned with the identity compressor. |
599 | 0 | Err(make_err!( |
600 | 0 | Code::Internal, |
601 | 0 | "BatchReadBlobs entry for {entry_digest} used unsupported compressor {}", |
602 | 0 | entry.compressor |
603 | 0 | )) |
604 | 206 | } else if entry_len != entry_digest.size_bytes() { |
605 | 0 | Err(make_err!( |
606 | 0 | Code::Internal, |
607 | 0 | "BatchReadBlobs entry for {entry_digest} returned {entry_len} bytes, expected {}", |
608 | 0 | entry_digest.size_bytes() |
609 | 0 | )) |
610 | | } else { |
611 | 206 | Ok(entry.data) |
612 | | }; |
613 | 207 | if result.is_err() { |
614 | 1 | batcher.batched_read_errors.fetch_add( |
615 | 1 | u64::try_from(items.len()).unwrap_or(u64::MAX), |
616 | 1 | Ordering::Relaxed, |
617 | 1 | ); |
618 | 206 | } |
619 | 208 | for item in items207 { |
620 | 208 | drop(item.tx.send(result.clone())); |
621 | 208 | } |
622 | | } |
623 | | // Any waiter with no matching response entry is missing upstream. |
624 | 4 | for item0 in waiters.into_values().flatten() { |
625 | 0 | batcher.batched_read_errors.fetch_add(1, Ordering::Relaxed); |
626 | 0 | let err = make_err!( |
627 | 0 | Code::NotFound, |
628 | 0 | "Blob {} not found in BatchReadBlobs response", |
629 | 0 | item.digest |
630 | 0 | ); |
631 | 0 | drop(item.tx.send(Err(err))); |
632 | 0 | } |
633 | 4 | } |
634 | | |
635 | 0 | pub async fn get_tree( |
636 | 0 | &self, |
637 | 0 | grpc_request: Request<GetTreeRequest>, |
638 | 0 | ) -> Result<Response<Streaming<GetTreeResponse>>, Error> { |
639 | 0 | error_if!( |
640 | 0 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
641 | | "CAS operation on AC store" |
642 | | ); |
643 | | |
644 | 0 | let mut request = grpc_request.into_inner(); |
645 | 0 | request.instance_name.clone_from(&self.instance_name); |
646 | 0 | self.perform_request(request, |request| async move { |
647 | 0 | let channel = self |
648 | 0 | .connection_manager |
649 | 0 | .connection(format!("get_tree: {:?}", request.root_digest)) |
650 | 0 | .await |
651 | 0 | .err_tip(|| "in get_tree")?; |
652 | 0 | ContentAddressableStorageClient::new(channel) |
653 | 0 | .get_tree(enrich_request( |
654 | 0 | Request::new(request), |
655 | 0 | &self.headers, |
656 | 0 | &self.forward_headers, |
657 | 0 | )) |
658 | 0 | .await |
659 | 0 | .err_tip(|| "in GrpcStore::get_tree") |
660 | 0 | }) |
661 | 0 | .await |
662 | 0 | } |
663 | | |
664 | 1 | pub async fn split_blob( |
665 | 1 | &self, |
666 | 1 | grpc_request: Request<SplitBlobRequest>, |
667 | 1 | ) -> Result<Response<SplitBlobResponse>, Error> { |
668 | 0 | error_if!( |
669 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
670 | | "CAS operation on AC store" |
671 | | ); |
672 | | |
673 | 1 | let mut request = grpc_request.into_inner(); |
674 | 1 | request.instance_name.clone_from(&self.instance_name); |
675 | 1 | self.perform_request(request, |request| async move { |
676 | 1 | let channel = self |
677 | 1 | .connection_manager |
678 | 1 | .connection(format!("split_blob: {:?}", request.blob_digest)) |
679 | 1 | .await |
680 | 1 | .err_tip(|| "in split_blob")?0 ; |
681 | 1 | ContentAddressableStorageClient::new(channel) |
682 | 1 | .split_blob(enrich_request( |
683 | 1 | Request::new(request), |
684 | 1 | &self.headers, |
685 | 1 | &self.forward_headers, |
686 | 1 | )) |
687 | 1 | .await |
688 | 1 | .err_tip(|| "in GrpcStore::split_blob") |
689 | 2 | }) |
690 | 1 | .await |
691 | 1 | } |
692 | | |
693 | 1 | pub async fn splice_blob( |
694 | 1 | &self, |
695 | 1 | grpc_request: Request<SpliceBlobRequest>, |
696 | 1 | ) -> Result<Response<SpliceBlobResponse>, Error> { |
697 | 0 | error_if!( |
698 | 1 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
699 | | "CAS operation on AC store" |
700 | | ); |
701 | | |
702 | 1 | let mut request = grpc_request.into_inner(); |
703 | 1 | request.instance_name.clone_from(&self.instance_name); |
704 | 1 | self.perform_request(request, |request| async move { |
705 | 1 | let channel = self |
706 | 1 | .connection_manager |
707 | 1 | .connection(format!("splice_blob: {:?}", request.blob_digest)) |
708 | 1 | .await |
709 | 1 | .err_tip(|| "in splice_blob")?0 ; |
710 | 1 | ContentAddressableStorageClient::new(channel) |
711 | 1 | .splice_blob(enrich_request( |
712 | 1 | Request::new(request), |
713 | 1 | &self.headers, |
714 | 1 | &self.forward_headers, |
715 | 1 | )) |
716 | 1 | .await |
717 | 1 | .err_tip(|| "in GrpcStore::splice_blob") |
718 | 2 | }) |
719 | 1 | .await |
720 | 1 | } |
721 | | |
722 | 0 | fn get_read_request(&self, mut request: ReadRequest) -> Result<ReadRequest, Error> { |
723 | | const IS_UPLOAD_FALSE: bool = false; |
724 | 0 | let mut resource_info = ResourceInfo::new(&request.resource_name, IS_UPLOAD_FALSE)?; |
725 | 0 | if resource_info.instance_name != self.instance_name { |
726 | 0 | resource_info.instance_name = Cow::Borrowed(&self.instance_name); |
727 | 0 | request.resource_name = resource_info.to_string(IS_UPLOAD_FALSE); |
728 | 0 | } |
729 | 0 | Ok(request) |
730 | 0 | } |
731 | | |
732 | 6 | async fn read_internal( |
733 | 6 | &self, |
734 | 6 | request: ReadRequest, |
735 | 6 | ) -> Result<impl Stream<Item = Result<ReadResponse, Status>> + use<>, Error> { |
736 | 6 | let channel = self |
737 | 6 | .connection_manager |
738 | 6 | .connection(format!("read_internal: {}", request.resource_name)) |
739 | 6 | .await |
740 | 6 | .err_tip(|| "in read_internal")?0 ; |
741 | 6 | let mut response = ByteStreamClient::new(channel) |
742 | 6 | .read(enrich_request( |
743 | 6 | Request::new(request), |
744 | 6 | &self.headers, |
745 | 6 | &self.forward_headers, |
746 | 6 | )) |
747 | 6 | .await |
748 | 6 | .err_tip(|| "in GrpcStore::read")?0 |
749 | 6 | .into_inner(); |
750 | 6 | let first_response = response |
751 | 6 | .message() |
752 | 6 | .await |
753 | 6 | .err_tip(|| "Fetching first chunk in GrpcStore::read()")?0 ; |
754 | 6 | Ok(FirstStream::new(first_response, response)) |
755 | 6 | } |
756 | | |
757 | 0 | pub async fn read<R>( |
758 | 0 | &self, |
759 | 0 | grpc_request: R, |
760 | 0 | ) -> Result<impl Stream<Item = Result<ReadResponse, Status>> + use<R>, Error> |
761 | 0 | where |
762 | 0 | R: IntoRequest<ReadRequest>, |
763 | 0 | { |
764 | 0 | error_if!( |
765 | 0 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
766 | | "CAS operation on AC store" |
767 | | ); |
768 | | |
769 | 0 | let request = self.get_read_request(grpc_request.into_request().into_inner())?; |
770 | 0 | self.perform_request(request, |request| async move { |
771 | 0 | self.read_internal(request).await |
772 | 0 | }) |
773 | 0 | .await |
774 | 0 | } |
775 | | |
776 | 2 | pub async fn write<T, E>( |
777 | 2 | &self, |
778 | 2 | stream: WriteRequestStreamWrapper<T>, |
779 | 2 | ) -> Result<Response<WriteResponse>, Error> |
780 | 2 | where |
781 | 2 | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
782 | 2 | E: Into<Error> + 'static, |
783 | 2 | { |
784 | 0 | error_if!( |
785 | 2 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
786 | | "CAS operation on AC store" |
787 | | ); |
788 | | |
789 | 2 | let local_state = Arc::new(Mutex::new(WriteState::new( |
790 | 2 | self.instance_name.clone(), |
791 | 2 | stream, |
792 | | ))); |
793 | | |
794 | 2 | let write_start = std::time::Instant::now(); |
795 | 2 | let instance_name = self.instance_name.clone(); |
796 | 2 | let rpc_timeout = self.rpc_timeout; |
797 | 2 | trace!( |
798 | | instance_name = %instance_name, |
799 | 2 | rpc_timeout_s = rpc_timeout.as_secs(), |
800 | | "GrpcStore::write: starting ByteStream write", |
801 | | ); |
802 | 2 | let mut attempt: u32 = 0; |
803 | 2 | let result = self |
804 | 2 | .retrier |
805 | 2 | .retry(unfold(local_state, move |local_state| { |
806 | 2 | attempt += 1; |
807 | 2 | let instance_name = instance_name.clone(); |
808 | 2 | async move { |
809 | | // The client write may occur on a separate thread and |
810 | | // therefore in order to share the state with it we have to |
811 | | // wrap it in a Mutex and retrieve it after the write |
812 | | // has completed. There is no way to get the value back |
813 | | // from the client. |
814 | 2 | trace!( |
815 | | instance_name = %instance_name, |
816 | | attempt, |
817 | | "GrpcStore::write: requesting connection from pool", |
818 | | ); |
819 | 2 | let conn_start = std::time::Instant::now(); |
820 | 2 | let rpc_fut = self.connection_manager.connection("write".into()).and_then( |
821 | 2 | |channel| { |
822 | 2 | let conn_elapsed = conn_start.elapsed(); |
823 | 2 | let instance_for_rpc = instance_name.clone(); |
824 | 2 | let conn_elapsed_ms = |
825 | 2 | u64::try_from(conn_elapsed.as_millis()).unwrap_or(u64::MAX); |
826 | 2 | trace!( |
827 | | instance_name = %instance_for_rpc, |
828 | | conn_elapsed_ms, |
829 | | "GrpcStore::write: got connection, starting ByteStream.Write RPC", |
830 | | ); |
831 | 2 | let rpc_start = std::time::Instant::now(); |
832 | 2 | let local_state_for_rpc = local_state.clone(); |
833 | 2 | async move { |
834 | 2 | let res = ByteStreamClient::new(channel) |
835 | 2 | .write(enrich_request( |
836 | 2 | Request::new(WriteStateWrapper::new(local_state_for_rpc)), |
837 | 2 | &self.headers, |
838 | 2 | &self.forward_headers, |
839 | 2 | )) |
840 | 2 | .await |
841 | 2 | .err_tip(|| "in GrpcStore::write"); |
842 | 2 | let rpc_elapsed_ms = u64::try_from(rpc_start.elapsed().as_millis()) |
843 | 2 | .unwrap_or(u64::MAX); |
844 | 2 | trace!( |
845 | | instance_name = %instance_for_rpc, |
846 | | rpc_elapsed_ms, |
847 | 2 | success = res.is_ok(), |
848 | | "GrpcStore::write: ByteStream.Write RPC returned", |
849 | | ); |
850 | 2 | res |
851 | 2 | } |
852 | 2 | }, |
853 | | ); |
854 | | |
855 | 2 | let result = if rpc_timeout > Duration::ZERO { |
856 | 2 | match tokio::time::timeout(rpc_timeout, rpc_fut).await { |
857 | 2 | Ok(res) => res, |
858 | 0 | Err(_elapsed) => { |
859 | 0 | warn!( |
860 | | instance_name = %instance_name, |
861 | | attempt, |
862 | 0 | rpc_timeout_s = rpc_timeout.as_secs(), |
863 | | "GrpcStore::write: per-RPC timeout exceeded, cancelling", |
864 | | ); |
865 | | #[allow(unused_qualifications)] |
866 | 0 | Err(nativelink_error::make_err!( |
867 | 0 | nativelink_error::Code::DeadlineExceeded, |
868 | 0 | "GrpcStore::write RPC timed out after {}s", |
869 | 0 | rpc_timeout.as_secs() |
870 | 0 | )) |
871 | | } |
872 | | } |
873 | | } else { |
874 | 0 | rpc_fut.await |
875 | | }; |
876 | | |
877 | | // Get the state back from StateWrapper, this should be |
878 | | // uncontended since write has returned. |
879 | 2 | let mut local_state_locked = local_state.lock(); |
880 | | |
881 | 2 | let result = local_state_locked |
882 | 2 | .take_read_stream_error() |
883 | 2 | .map(|err| RetryResult::Err(err0 .append0 ("Where read_stream_error was set"))) |
884 | 2 | .unwrap_or_else(|| { |
885 | | // No stream error, handle the original result |
886 | 2 | match result { |
887 | 2 | Ok(response) => RetryResult::Ok(response), |
888 | 0 | Err(ref err) => { |
889 | 0 | warn!( |
890 | | instance_name = %instance_name, |
891 | | attempt, |
892 | | ?err, |
893 | 0 | can_resume = local_state_locked.can_resume(), |
894 | | "GrpcStore::write: RPC failed", |
895 | | ); |
896 | 0 | if local_state_locked.can_resume() { |
897 | 0 | local_state_locked.resume(); |
898 | 0 | RetryResult::Retry(err.clone()) |
899 | | } else { |
900 | 0 | RetryResult::Err( |
901 | 0 | err.clone().append("Retry is not possible"), |
902 | 0 | ) |
903 | | } |
904 | | } |
905 | | } |
906 | 2 | }); |
907 | | |
908 | 2 | drop(local_state_locked); |
909 | 2 | Some((result, local_state)) |
910 | 2 | } |
911 | 2 | })) |
912 | 2 | .await?0 ; |
913 | | |
914 | 2 | let total_elapsed_ms = u64::try_from(write_start.elapsed().as_millis()).unwrap_or(u64::MAX); |
915 | 2 | trace!( |
916 | | instance_name = %self.instance_name, |
917 | | total_elapsed_ms, |
918 | | "GrpcStore::write: completed successfully", |
919 | | ); |
920 | 2 | Ok(result) |
921 | 2 | } |
922 | | |
923 | 0 | pub async fn query_write_status( |
924 | 0 | &self, |
925 | 0 | grpc_request: Request<QueryWriteStatusRequest>, |
926 | 0 | ) -> Result<Response<QueryWriteStatusResponse>, Error> { |
927 | | const IS_UPLOAD_TRUE: bool = true; |
928 | | |
929 | 0 | error_if!( |
930 | 0 | matches!(self.store_type, nativelink_config::stores::StoreType::Ac), |
931 | | "CAS operation on AC store" |
932 | | ); |
933 | | |
934 | 0 | let mut request = grpc_request.into_inner(); |
935 | | |
936 | 0 | let mut request_info = ResourceInfo::new(&request.resource_name, IS_UPLOAD_TRUE)?; |
937 | 0 | if request_info.instance_name != self.instance_name { |
938 | 0 | request_info.instance_name = Cow::Borrowed(&self.instance_name); |
939 | 0 | request.resource_name = request_info.to_string(IS_UPLOAD_TRUE); |
940 | 0 | } |
941 | | |
942 | 0 | self.perform_request(request, |request| async move { |
943 | 0 | let channel = self |
944 | 0 | .connection_manager |
945 | 0 | .connection(format!("query_write_status: {}", request.resource_name)) |
946 | 0 | .await |
947 | 0 | .err_tip(|| "in query_write_status")?; |
948 | 0 | ByteStreamClient::new(channel) |
949 | 0 | .query_write_status(enrich_request( |
950 | 0 | Request::new(request), |
951 | 0 | &self.headers, |
952 | 0 | &self.forward_headers, |
953 | 0 | )) |
954 | 0 | .await |
955 | 0 | .err_tip(|| "in GrpcStore::query_write_status") |
956 | 0 | }) |
957 | 0 | .await |
958 | 0 | } |
959 | | |
960 | 0 | pub async fn get_action_result( |
961 | 0 | &self, |
962 | 0 | grpc_request: Request<GetActionResultRequest>, |
963 | 0 | ) -> Result<Response<ActionResult>, Error> { |
964 | 0 | let mut request = grpc_request.into_inner(); |
965 | 0 | request.instance_name.clone_from(&self.instance_name); |
966 | 0 | self.perform_request(request, |request| async move { |
967 | 0 | let channel = self |
968 | 0 | .connection_manager |
969 | 0 | .connection(format!("get_action_result: {:?}", request.action_digest)) |
970 | 0 | .await |
971 | 0 | .err_tip(|| "in get_action_result")?; |
972 | 0 | ActionCacheClient::new(channel) |
973 | 0 | .get_action_result(enrich_request( |
974 | 0 | Request::new(request), |
975 | 0 | &self.headers, |
976 | 0 | &self.forward_headers, |
977 | 0 | )) |
978 | 0 | .await |
979 | 0 | .err_tip(|| "in GrpcStore::get_action_result") |
980 | 0 | }) |
981 | 0 | .await |
982 | 0 | } |
983 | | |
984 | 0 | pub async fn update_action_result( |
985 | 0 | &self, |
986 | 0 | grpc_request: Request<UpdateActionResultRequest>, |
987 | 0 | ) -> Result<Response<ActionResult>, Error> { |
988 | 0 | let mut request = grpc_request.into_inner(); |
989 | 0 | request.instance_name.clone_from(&self.instance_name); |
990 | 0 | self.perform_request(request, |request| async move { |
991 | 0 | let channel = self |
992 | 0 | .connection_manager |
993 | 0 | .connection(format!("update_action_result: {:?}", request.action_digest)) |
994 | 0 | .await |
995 | 0 | .err_tip(|| "in update_action_result")?; |
996 | 0 | ActionCacheClient::new(channel) |
997 | 0 | .update_action_result(enrich_request( |
998 | 0 | Request::new(request), |
999 | 0 | &self.headers, |
1000 | 0 | &self.forward_headers, |
1001 | 0 | )) |
1002 | 0 | .await |
1003 | 0 | .err_tip(|| "in GrpcStore::update_action_result") |
1004 | 0 | }) |
1005 | 0 | .await |
1006 | 0 | } |
1007 | | |
1008 | 0 | async fn get_action_result_from_digest( |
1009 | 0 | &self, |
1010 | 0 | digest: DigestInfo, |
1011 | 0 | ) -> Result<Response<ActionResult>, Error> { |
1012 | 0 | let action_result_request = GetActionResultRequest { |
1013 | 0 | instance_name: self.instance_name.clone(), |
1014 | 0 | action_digest: Some(digest.into()), |
1015 | | inline_stdout: false, |
1016 | | inline_stderr: false, |
1017 | 0 | inline_output_files: Vec::new(), |
1018 | 0 | digest_function: Context::current() |
1019 | 0 | .get::<DigestHasherFunc>() |
1020 | 0 | .map_or_else(default_digest_hasher_func, |v| *v) |
1021 | 0 | .proto_digest_func() |
1022 | 0 | .into(), |
1023 | | }; |
1024 | 0 | self.get_action_result(Request::new(action_result_request)) |
1025 | 0 | .await |
1026 | 0 | } |
1027 | | |
1028 | 0 | async fn get_action_result_as_part( |
1029 | 0 | &self, |
1030 | 0 | digest: DigestInfo, |
1031 | 0 | writer: &mut DropCloserWriteHalf, |
1032 | 0 | offset: usize, |
1033 | 0 | length: Option<usize>, |
1034 | 0 | ) -> Result<(), Error> { |
1035 | 0 | let action_result = self |
1036 | 0 | .get_action_result_from_digest(digest) |
1037 | 0 | .await |
1038 | 0 | .map(Response::into_inner) |
1039 | 0 | .err_tip(|| "Action result not found")?; |
1040 | | // TODO: Would be better to avoid all the encoding and decoding in this |
1041 | | // file, however there's no way to currently get raw bytes from a |
1042 | | // generated prost request unfortunately. |
1043 | 0 | let mut value = BytesMut::new(); |
1044 | 0 | action_result |
1045 | 0 | .encode(&mut value) |
1046 | 0 | .err_tip(|| "Could not encode upstream action result")?; |
1047 | | |
1048 | 0 | let default_len = value.len() - offset; |
1049 | 0 | let length = length.unwrap_or(default_len).min(default_len); |
1050 | 0 | if length > 0 { |
1051 | 0 | writer |
1052 | 0 | .send(value.freeze().slice(offset..offset + length)) |
1053 | 0 | .await |
1054 | 0 | .err_tip(|| "Failed to write data in grpc store")?; |
1055 | 0 | } |
1056 | 0 | writer |
1057 | 0 | .send_eof() |
1058 | 0 | .err_tip(|| "Failed to write EOF in grpc store get_action_result_as_part")?; |
1059 | 0 | Ok(()) |
1060 | 0 | } |
1061 | | |
1062 | 0 | async fn update_action_result_from_bytes( |
1063 | 0 | &self, |
1064 | 0 | digest: DigestInfo, |
1065 | 0 | mut reader: DropCloserReadHalf, |
1066 | 0 | ) -> Result<u64, Error> { |
1067 | 0 | let bytes = reader.consume(None).await?; |
1068 | 0 | let len = bytes.len() as u64; |
1069 | 0 | let action_result = ActionResult::decode(bytes) |
1070 | 0 | .err_tip(|| "Failed to decode ActionResult in update_action_result_from_bytes")?; |
1071 | 0 | let update_action_request = UpdateActionResultRequest { |
1072 | 0 | instance_name: self.instance_name.clone(), |
1073 | 0 | action_digest: Some(digest.into()), |
1074 | 0 | action_result: Some(action_result), |
1075 | 0 | results_cache_policy: None, |
1076 | 0 | digest_function: Context::current() |
1077 | 0 | .get::<DigestHasherFunc>() |
1078 | 0 | .map_or_else(default_digest_hasher_func, |v| *v) |
1079 | 0 | .proto_digest_func() |
1080 | 0 | .into(), |
1081 | | }; |
1082 | 0 | self.update_action_result(Request::new(update_action_request)) |
1083 | 0 | .await |
1084 | 0 | .map(|_| len) |
1085 | 0 | } |
1086 | | } |
1087 | | |
1088 | | #[async_trait] |
1089 | | impl StoreDriver for GrpcStore { |
1090 | 0 | async fn post_init(self: Arc<Self>) -> Result<(), Error> { |
1091 | | Ok(()) |
1092 | 0 | } |
1093 | | |
1094 | | // NOTE: This function can only be safely used on CAS stores. AC stores may return a size that |
1095 | | // is incorrect. |
1096 | | async fn has_with_results( |
1097 | | self: Pin<&Self>, |
1098 | | keys: &[StoreKey<'_>], |
1099 | | results: &mut [Option<u64>], |
1100 | 0 | ) -> Result<(), Error> { |
1101 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Ac) { |
1102 | | keys.iter() |
1103 | | .zip(results.iter_mut()) |
1104 | 0 | .map(|(key, result)| async move { |
1105 | | // The length of an AC is incorrect, so we don't figure out the |
1106 | | // length, instead the biggest possible result is returned in the |
1107 | | // hope that we detect incorrect usage. |
1108 | 0 | self.get_action_result_from_digest(key.borrow().into_digest()) |
1109 | 0 | .await?; |
1110 | 0 | *result = Some(u64::MAX); |
1111 | 0 | Ok::<_, Error>(()) |
1112 | 0 | }) |
1113 | | .collect::<FuturesUnordered<_>>() |
1114 | 0 | .try_for_each(|()| future::ready(Ok(()))) |
1115 | | .await |
1116 | | .err_tip(|| "Getting upstream action cache entry")?; |
1117 | | return Ok(()); |
1118 | | } |
1119 | | |
1120 | | let missing_blobs_response = self |
1121 | | .find_missing_blobs(Request::new(FindMissingBlobsRequest { |
1122 | | instance_name: self.instance_name.clone(), |
1123 | | blob_digests: keys |
1124 | | .iter() |
1125 | 0 | .map(|k| k.borrow().into_digest().into()) |
1126 | | .collect(), |
1127 | | digest_function: Context::current() |
1128 | | .get::<DigestHasherFunc>() |
1129 | | .map_or_else(default_digest_hasher_func, |v| *v) |
1130 | | .proto_digest_func() |
1131 | | .into(), |
1132 | | })) |
1133 | | .await? |
1134 | | .into_inner(); |
1135 | | |
1136 | | // Since the ordering is not guaranteed above, the matching has to check |
1137 | | // all missing blobs against all entries in the unsorted digest list. |
1138 | | // To optimise this, the missing digests are sorted and then it is |
1139 | | // efficient to perform a binary search for each digest within the |
1140 | | // missing list. |
1141 | | let mut missing_digests = |
1142 | | Vec::with_capacity(missing_blobs_response.missing_blob_digests.len()); |
1143 | | for missing_digest in missing_blobs_response.missing_blob_digests { |
1144 | | missing_digests.push(DigestInfo::try_from(missing_digest)?); |
1145 | | } |
1146 | | missing_digests.sort_unstable(); |
1147 | | for (digest, result) in keys |
1148 | | .iter() |
1149 | 0 | .map(|v| v.borrow().into_digest()) |
1150 | | .zip(results.iter_mut()) |
1151 | | { |
1152 | | match missing_digests.binary_search(&digest) { |
1153 | | Ok(_) => *result = None, |
1154 | | Err(_) => *result = Some(digest.size_bytes()), |
1155 | | } |
1156 | | } |
1157 | | |
1158 | | Ok(()) |
1159 | 0 | } |
1160 | | |
1161 | | async fn update( |
1162 | | self: Pin<&Self>, |
1163 | | key: StoreKey<'_>, |
1164 | | reader: DropCloserReadHalf, |
1165 | | _size_info: UploadSizeInfo, |
1166 | 2 | ) -> Result<u64, Error> { |
1167 | | struct LocalState { |
1168 | | resource_name: String, |
1169 | | reader: DropCloserReadHalf, |
1170 | | did_error: bool, |
1171 | | bytes_received: i64, |
1172 | | } |
1173 | | |
1174 | | let digest = key.into_digest(); |
1175 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Ac) { |
1176 | | return self.update_action_result_from_bytes(digest, reader).await; |
1177 | | } |
1178 | | |
1179 | | let mut buf = Uuid::encode_buffer(); |
1180 | | let resource_name = if self.use_legacy_resource_names { |
1181 | | format!( |
1182 | | "{}/uploads/{}/blobs/{}/{}", |
1183 | | &self.instance_name, |
1184 | | Uuid::new_v4().hyphenated().encode_lower(&mut buf), |
1185 | | digest.packed_hash(), |
1186 | | digest.size_bytes(), |
1187 | | ) |
1188 | | } else { |
1189 | | let digest_function = Context::current() |
1190 | | .get::<DigestHasherFunc>() |
1191 | | .map_or_else(default_digest_hasher_func, |v| *v) |
1192 | | .proto_digest_func() |
1193 | | .as_str_name() |
1194 | | .to_ascii_lowercase(); |
1195 | | format!( |
1196 | | "{}/uploads/{}/blobs/{}/{}/{}", |
1197 | | &self.instance_name, |
1198 | | Uuid::new_v4().hyphenated().encode_lower(&mut buf), |
1199 | | digest_function, |
1200 | | digest.packed_hash(), |
1201 | | digest.size_bytes(), |
1202 | | ) |
1203 | | }; |
1204 | | trace!( |
1205 | | resource_name = %resource_name, |
1206 | | digest_hash = %digest.packed_hash(), |
1207 | | digest_size = digest.size_bytes(), |
1208 | | "GrpcStore::update: starting upload for digest", |
1209 | | ); |
1210 | | let local_state = LocalState { |
1211 | | resource_name, |
1212 | | reader, |
1213 | | did_error: false, |
1214 | | bytes_received: 0, |
1215 | | }; |
1216 | | |
1217 | 4 | let stream = Box::pin(unfold(local_state, |mut local_state| async move { |
1218 | 4 | if local_state.did_error { |
1219 | 0 | error!("GrpcStore::update() polled stream after error was returned"); |
1220 | 0 | return None; |
1221 | 4 | } |
1222 | 4 | let data = match local_state |
1223 | 4 | .reader |
1224 | 4 | .recv() |
1225 | 4 | .await |
1226 | 4 | .err_tip(|| "In GrpcStore::update()") |
1227 | | { |
1228 | 4 | Ok(data) => data, |
1229 | 0 | Err(err) => { |
1230 | 0 | local_state.did_error = true; |
1231 | 0 | return Some((Err(err), local_state)); |
1232 | | } |
1233 | | }; |
1234 | | |
1235 | 4 | let write_offset = local_state.bytes_received; |
1236 | 4 | local_state.bytes_received += data.len() as i64; |
1237 | | |
1238 | 4 | Some(( |
1239 | 4 | Ok(WriteRequest { |
1240 | 4 | resource_name: local_state.resource_name.clone(), |
1241 | 4 | write_offset, |
1242 | 4 | finish_write: data.is_empty(), // EOF is when no data was polled. |
1243 | 4 | data, |
1244 | 4 | }), |
1245 | 4 | local_state, |
1246 | 4 | )) |
1247 | 8 | })); |
1248 | | |
1249 | | self.write( |
1250 | | WriteRequestStreamWrapper::from(stream) |
1251 | | .await |
1252 | | .err_tip(|| "in GrpcStore::update()")?, |
1253 | | ) |
1254 | | .await |
1255 | | .err_tip(|| "in GrpcStore::update()")?; |
1256 | | |
1257 | | Ok(digest.size_bytes()) |
1258 | 2 | } |
1259 | | |
1260 | | async fn get_part( |
1261 | | self: Pin<&Self>, |
1262 | | key: StoreKey<'_>, |
1263 | | writer: &mut DropCloserWriteHalf, |
1264 | | offset: u64, |
1265 | | length: Option<u64>, |
1266 | 214 | ) -> Result<(), Error> { |
1267 | | struct LocalState<'a> { |
1268 | | resource_name: String, |
1269 | | writer: &'a mut DropCloserWriteHalf, |
1270 | | read_offset: i64, |
1271 | | read_limit: i64, |
1272 | | } |
1273 | | |
1274 | | let is_digest_key = matches!(key, StoreKey::Digest(_)); |
1275 | | let digest = key.into_digest(); |
1276 | | if matches!(self.store_type, nativelink_config::stores::StoreType::Ac) { |
1277 | | let offset = usize::try_from(offset).err_tip(|| "Could not convert offset to usize")?; |
1278 | | let length = length |
1279 | 0 | .map(|v| usize::try_from(v).err_tip(|| "Could not convert length to usize")) |
1280 | | .transpose()?; |
1281 | | |
1282 | | return self |
1283 | | .get_action_result_as_part(digest, writer, offset, length) |
1284 | | .await; |
1285 | | } |
1286 | | |
1287 | | // Shortcut for empty blobs. |
1288 | | if digest.size_bytes() == 0 { |
1289 | | return writer.send_eof(); |
1290 | | } |
1291 | | |
1292 | | // When configured, coalesce full reads of small blobs into |
1293 | | // BatchReadBlobs RPCs. `batched_read` returns `None` when the queue |
1294 | | // is over budget, in which case we fall through to the stream path. |
1295 | | if let Some(batcher) = &self.read_batcher |
1296 | | && is_digest_key |
1297 | | && offset == 0 |
1298 | 0 | && length.is_none_or(|len| len >= digest.size_bytes()) |
1299 | | && digest.size_bytes() <= batcher.max_blob_size_bytes |
1300 | | && let Some(result) = self.batched_read(batcher, digest).await |
1301 | | { |
1302 | | match result { |
1303 | | Ok(data) => { |
1304 | | if !data.is_empty() { |
1305 | | writer |
1306 | | .send(data) |
1307 | | .await |
1308 | | .err_tip(|| "Failed to write data in GrpcStore::get_part()")?; |
1309 | | } |
1310 | | return writer |
1311 | | .send_eof() |
1312 | | .err_tip(|| "Failed to send EOF in GrpcStore::get_part()"); |
1313 | | } |
1314 | | // A retryable error falls through to the ByteStream path |
1315 | | // below, which re-enters the full retry machinery. This |
1316 | | // matches the retry behavior reads had before batching. |
1317 | | Err(err) if is_retryable_code(err.code) => { |
1318 | | warn!( |
1319 | | ?err, |
1320 | | "Batched read failed with retryable error, falling back to ByteStream read", |
1321 | | ); |
1322 | | } |
1323 | | Err(err) => return Err(err.append("in GrpcStore::get_part()")), |
1324 | | } |
1325 | | } |
1326 | | |
1327 | | let resource_name = if self.use_legacy_resource_names { |
1328 | | format!( |
1329 | | "{}/blobs/{}/{}", |
1330 | | &self.instance_name, |
1331 | | digest.packed_hash(), |
1332 | | digest.size_bytes(), |
1333 | | ) |
1334 | | } else { |
1335 | | let digest_function = Context::current() |
1336 | | .get::<DigestHasherFunc>() |
1337 | | .map_or_else(default_digest_hasher_func, |v| *v) |
1338 | | .proto_digest_func() |
1339 | | .as_str_name() |
1340 | | .to_ascii_lowercase(); |
1341 | | format!( |
1342 | | "{}/blobs/{}/{}/{}", |
1343 | | &self.instance_name, |
1344 | | digest_function, |
1345 | | digest.packed_hash(), |
1346 | | digest.size_bytes(), |
1347 | | ) |
1348 | | }; |
1349 | | |
1350 | | let local_state = LocalState { |
1351 | | resource_name, |
1352 | | writer, |
1353 | | read_offset: i64::try_from(offset).err_tip(|| "Could not convert offset to i64")?, |
1354 | | read_limit: i64::try_from(length.unwrap_or(0)) |
1355 | | .err_tip(|| "Could not convert length to i64")?, |
1356 | | }; |
1357 | | |
1358 | | self.retrier |
1359 | 6 | .retry(unfold(local_state, move |mut local_state| async move { |
1360 | 6 | let request = ReadRequest { |
1361 | 6 | resource_name: local_state.resource_name.clone(), |
1362 | 6 | read_offset: local_state.read_offset, |
1363 | 6 | read_limit: local_state.read_limit, |
1364 | 6 | }; |
1365 | 6 | let mut stream = match self |
1366 | 6 | .read_internal(request) |
1367 | 6 | .await |
1368 | 6 | .err_tip(|| "in GrpcStore::get_part()") |
1369 | | { |
1370 | 6 | Ok(stream) => stream, |
1371 | 0 | Err(err) => return Some((RetryResult::Retry(err), local_state)), |
1372 | | }; |
1373 | | |
1374 | | loop { |
1375 | 12 | let data = match stream.next().await { |
1376 | | // Create an empty response to represent EOF. |
1377 | 6 | None => Bytes::new(), |
1378 | 6 | Some(Ok(message)) => message.data, |
1379 | 0 | Some(Err(status)) => { |
1380 | 0 | return Some(( |
1381 | 0 | RetryResult::Retry( |
1382 | 0 | Into::<Error>::into(status) |
1383 | 0 | .append("While fetching message in GrpcStore::get_part()"), |
1384 | 0 | ), |
1385 | 0 | local_state, |
1386 | 0 | )); |
1387 | | } |
1388 | | }; |
1389 | 12 | let length = data.len() as i64; |
1390 | | // This is the usual exit from the loop at EOF. |
1391 | 12 | if length == 0 { |
1392 | 6 | let eof_result = local_state |
1393 | 6 | .writer |
1394 | 6 | .send_eof() |
1395 | 6 | .err_tip(|| "Could not send eof in GrpcStore::get_part()") |
1396 | 6 | .map_or_else(RetryResult::Err, RetryResult::Ok); |
1397 | 6 | return Some((eof_result, local_state)); |
1398 | 6 | } |
1399 | | // Forward the data upstream. |
1400 | 6 | if let Err(err0 ) = local_state |
1401 | 6 | .writer |
1402 | 6 | .send(data) |
1403 | 6 | .await |
1404 | 6 | .err_tip(|| "While sending in GrpcStore::get_part()") |
1405 | | { |
1406 | 0 | return Some((RetryResult::Err(err), local_state)); |
1407 | 6 | } |
1408 | 6 | local_state.read_offset += length; |
1409 | | } |
1410 | 12 | })) |
1411 | | .await |
1412 | 214 | } |
1413 | | |
1414 | 2 | fn inner_store(&self, _digest: Option<StoreKey>) -> &dyn StoreDriver { |
1415 | 2 | self |
1416 | 2 | } |
1417 | | |
1418 | 2 | fn as_any<'a>(&'a self) -> &'a (dyn core::any::Any + Sync + Send + 'static) { |
1419 | 2 | self |
1420 | 2 | } |
1421 | | |
1422 | 0 | fn as_any_arc(self: Arc<Self>) -> Arc<dyn core::any::Any + Sync + Send + 'static> { |
1423 | 0 | self |
1424 | 0 | } |
1425 | | |
1426 | 0 | fn register_remove_callback(self: Arc<Self>, _callback: RemoveCallback) -> Result<(), Error> { |
1427 | 0 | Err(Error::new( |
1428 | 0 | Code::Internal, |
1429 | 0 | "gRPC stores are incompatible with removal callbacks".to_string(), |
1430 | 0 | )) |
1431 | 0 | } |
1432 | | } |
1433 | | |
1434 | | default_health_status_indicator!(GrpcStore); |