Coverage Report

Created: 2026-07-21 15:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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);