Coverage Report

Created: 2026-09-18 20:40

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