Coverage Report

Created: 2026-11-07 21:17

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