Coverage Report

Created: 2026-10-22 22:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-service/src/bytestream_server.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::convert::Into;
16
use core::fmt::{Debug, Formatter};
17
use core::pin::Pin;
18
use core::sync::atomic::{AtomicU64, Ordering};
19
use core::time::Duration;
20
use std::collections::hash_map::Entry;
21
use std::collections::{HashMap, HashSet};
22
use std::sync::Arc;
23
use std::time::{Instant, SystemTime, UNIX_EPOCH};
24
25
use bytes::BytesMut;
26
use futures::future::pending;
27
use futures::stream::unfold;
28
use futures::{Future, Stream, TryFutureExt, try_join};
29
use nativelink_config::cas_server::{ByteStreamConfig, InstanceName, WithInstanceName};
30
use nativelink_error::{Code, Error, ResultExt, make_err, make_input_err};
31
use nativelink_metric::{
32
    MetricFieldData, MetricKind, MetricPublishKnownKindData, MetricsComponent, group, publish,
33
};
34
use nativelink_proto::build::bazel::remote::execution::v2::compressor;
35
use nativelink_proto::google::bytestream::byte_stream_server::{
36
    ByteStream, ByteStreamServer as Server,
37
};
38
use nativelink_proto::google::bytestream::{
39
    QueryWriteStatusRequest, QueryWriteStatusResponse, ReadRequest, ReadResponse, WriteRequest,
40
    WriteResponse,
41
};
42
use nativelink_store::grpc_store::GrpcStore;
43
use nativelink_store::store_manager::StoreManager;
44
use nativelink_util::buf_channel::{
45
    DropCloserReadHalf, DropCloserWriteHalf, make_buf_channel_pair,
46
};
47
use nativelink_util::common::DigestInfo;
48
use nativelink_util::digest_hasher::{
49
    DigestHasherFunc, default_digest_hasher_func, make_ctx_for_hash_func,
50
};
51
use nativelink_util::proto_stream_utils::WriteRequestStreamWrapper;
52
use nativelink_util::resource_info::ResourceInfo;
53
use nativelink_util::spawn;
54
use nativelink_util::store_trait::{Store, StoreLike, StoreOptimizations, UploadSizeInfo};
55
use nativelink_util::task::JoinHandleDropGuard;
56
use opentelemetry::context::FutureExt;
57
use parking_lot::Mutex;
58
use tokio::time::sleep;
59
use tonic::{Request, Response, Status, Streaming};
60
use tracing::{Instrument, Level, debug, error, error_span, info, instrument, trace, warn};
61
62
use crate::wire_compression::RemoteCacheCompressionInstances;
63
64
/// If this value changes update the documentation in the config definition.
65
const DEFAULT_PERSIST_STREAM_ON_DISCONNECT_TIMEOUT: Duration = Duration::from_mins(1);
66
67
/// If this value changes update the documentation in the config definition.
68
const DEFAULT_MAX_BYTES_PER_STREAM: usize = 64 * 1024;
69
70
/// Metrics for `ByteStream` server operations.
71
/// Tracks upload/download activity, throughput, and latency.
72
#[derive(Debug, Default)]
73
pub struct ByteStreamMetrics {
74
    /// Number of currently active uploads (includes idle streams waiting for resume)
75
    pub active_uploads: AtomicU64,
76
    /// Total number of write requests received
77
    pub write_requests_total: AtomicU64,
78
    /// Total number of successful write requests
79
    pub write_requests_success: AtomicU64,
80
    /// Total number of failed write requests
81
    pub write_requests_failure: AtomicU64,
82
    /// Total number of read requests received
83
    pub read_requests_total: AtomicU64,
84
    /// Total number of successful read requests
85
    pub read_requests_success: AtomicU64,
86
    /// Total number of failed read requests
87
    pub read_requests_failure: AtomicU64,
88
    /// Total number of `query_write_status` requests
89
    pub query_write_status_total: AtomicU64,
90
    /// Total bytes written via `ByteStream`
91
    pub bytes_written_total: AtomicU64,
92
    /// Total bytes read via `ByteStream`
93
    pub bytes_read_total: AtomicU64,
94
    /// Sum of write durations in nanoseconds (for average latency calculation)
95
    pub write_duration_ns: AtomicU64,
96
    /// Sum of read durations in nanoseconds (for average latency calculation)
97
    pub read_duration_ns: AtomicU64,
98
    /// Number of UUID collisions detected
99
    pub uuid_collisions: AtomicU64,
100
    /// Number of resumed uploads (client reconnected to existing stream)
101
    pub resumed_uploads: AtomicU64,
102
    /// Number of idle streams that timed out
103
    pub idle_stream_timeouts: AtomicU64,
104
}
105
106
impl MetricsComponent for ByteStreamMetrics {
107
0
    fn publish(
108
0
        &self,
109
0
        _kind: MetricKind,
110
0
        field_metadata: MetricFieldData,
111
0
    ) -> Result<MetricPublishKnownKindData, nativelink_metric::Error> {
112
0
        let _enter = group!(field_metadata.name).entered();
113
114
0
        publish!(
115
0
            "active_uploads",
116
0
            &self.active_uploads,
117
0
            MetricKind::Counter,
118
0
            "Number of currently active uploads"
119
        );
120
0
        publish!(
121
0
            "write_requests_total",
122
0
            &self.write_requests_total,
123
0
            MetricKind::Counter,
124
0
            "Total write requests received"
125
        );
126
0
        publish!(
127
0
            "write_requests_success",
128
0
            &self.write_requests_success,
129
0
            MetricKind::Counter,
130
0
            "Total successful write requests"
131
        );
132
0
        publish!(
133
0
            "write_requests_failure",
134
0
            &self.write_requests_failure,
135
0
            MetricKind::Counter,
136
0
            "Total failed write requests"
137
        );
138
0
        publish!(
139
0
            "read_requests_total",
140
0
            &self.read_requests_total,
141
0
            MetricKind::Counter,
142
0
            "Total read requests received"
143
        );
144
0
        publish!(
145
0
            "read_requests_success",
146
0
            &self.read_requests_success,
147
0
            MetricKind::Counter,
148
0
            "Total successful read requests"
149
        );
150
0
        publish!(
151
0
            "read_requests_failure",
152
0
            &self.read_requests_failure,
153
0
            MetricKind::Counter,
154
0
            "Total failed read requests"
155
        );
156
0
        publish!(
157
0
            "query_write_status_total",
158
0
            &self.query_write_status_total,
159
0
            MetricKind::Counter,
160
0
            "Total query_write_status requests"
161
        );
162
0
        publish!(
163
0
            "bytes_written_total",
164
0
            &self.bytes_written_total,
165
0
            MetricKind::Counter,
166
0
            "Total bytes written via ByteStream"
167
        );
168
0
        publish!(
169
0
            "bytes_read_total",
170
0
            &self.bytes_read_total,
171
0
            MetricKind::Counter,
172
0
            "Total bytes read via ByteStream"
173
        );
174
0
        publish!(
175
0
            "write_duration_ns",
176
0
            &self.write_duration_ns,
177
0
            MetricKind::Counter,
178
0
            "Sum of write durations in nanoseconds"
179
        );
180
0
        publish!(
181
0
            "read_duration_ns",
182
0
            &self.read_duration_ns,
183
0
            MetricKind::Counter,
184
0
            "Sum of read durations in nanoseconds"
185
        );
186
0
        publish!(
187
0
            "uuid_collisions",
188
0
            &self.uuid_collisions,
189
0
            MetricKind::Counter,
190
0
            "Number of UUID collisions detected"
191
        );
192
0
        publish!(
193
0
            "resumed_uploads",
194
0
            &self.resumed_uploads,
195
0
            MetricKind::Counter,
196
0
            "Number of resumed uploads"
197
        );
198
0
        publish!(
199
0
            "idle_stream_timeouts",
200
0
            &self.idle_stream_timeouts,
201
0
            MetricKind::Counter,
202
0
            "Number of idle streams that timed out"
203
        );
204
205
0
        Ok(MetricPublishKnownKindData::Component)
206
0
    }
207
}
208
209
type BytesWrittenAndIdleStream = (Arc<AtomicU64>, Option<IdleStream>);
210
211
/// Type alias for the UUID key used in `active_uploads` `HashMap`.
212
/// Using u128 instead of String reduces memory allocations and improves
213
/// cache locality for `HashMap` operations.
214
type UuidKey = u128;
215
216
/// Parse a UUID string to a u128 for use as a `HashMap` key.
217
/// This avoids heap allocation for String keys and improves `HashMap` performance.
218
/// Falls back to hashing the string if it's not a valid hex UUID.
219
#[inline]
220
48
fn parse_uuid_to_key(uuid_str: &str) -> UuidKey {
221
    // UUIDs are typically 32 hex chars (128 bits) or 36 chars with dashes.
222
    // We'll try to parse as hex first, then fall back to hashing.
223
48
    let clean: String = uuid_str.chars().filter(char::is_ascii_hexdigit).collect();
224
48
    if clean.len() >= 16 {
225
        // Take up to 32 hex chars (128 bits)
226
48
        let hex_str = if clean.len() > 32 {
227
0
            &clean[..32]
228
        } else {
229
48
            &clean
230
        };
231
48
        u128::from_str_radix(hex_str, 16).unwrap_or_else(|_| 
{0
232
            // Hash fallback for non-hex strings
233
            use core::hash::{Hash, Hasher};
234
0
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
235
0
            uuid_str.hash(&mut hasher);
236
0
            u128::from(hasher.finish())
237
0
        })
238
    } else {
239
        // Short strings: use hash
240
        use core::hash::{Hash, Hasher};
241
0
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
242
0
        uuid_str.hash(&mut hasher);
243
0
        u128::from(hasher.finish())
244
    }
245
48
}
246
247
pub struct InstanceInfo {
248
    store: Store,
249
    // Max number of bytes to send on each grpc stream chunk.
250
    max_bytes_per_stream: usize,
251
    /// Active uploads keyed by UUID as u128 for better performance.
252
    /// Using u128 keys instead of String reduces heap allocations
253
    /// and improves `HashMap` lookup performance.
254
    active_uploads: Arc<Mutex<HashMap<UuidKey, BytesWrittenAndIdleStream>>>,
255
    /// How long to keep idle streams before timing them out.
256
    idle_stream_timeout: Duration,
257
    metrics: Arc<ByteStreamMetrics>,
258
    /// Handle to the global sweeper task. Kept alive for the lifetime of the instance.
259
    _sweeper_handle: Arc<JoinHandleDropGuard<()>>,
260
    /// Whether this instance supports Bazel remote cache compression.
261
    remote_cache_compression_enabled: bool,
262
}
263
264
impl Debug for InstanceInfo {
265
0
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
266
0
        f.debug_struct("InstanceInfo")
267
0
            .field("store", &self.store)
268
0
            .field("max_bytes_per_stream", &self.max_bytes_per_stream)
269
0
            .field("active_uploads", &self.active_uploads)
270
0
            .field("idle_stream_timeout", &self.idle_stream_timeout)
271
0
            .field("metrics", &self.metrics)
272
0
            .field(
273
0
                "remote_cache_compression_enabled",
274
0
                &self.remote_cache_compression_enabled,
275
0
            )
276
0
            .finish()
277
0
    }
278
}
279
280
struct CompressedUploadGuard {
281
    uuid_key: u128,
282
    active_uploads: Arc<Mutex<HashMap<UuidKey, BytesWrittenAndIdleStream>>>,
283
    metrics: Arc<ByteStreamMetrics>,
284
}
285
286
impl Drop for CompressedUploadGuard {
287
7
    fn drop(&mut self) {
288
7
        self.active_uploads.lock().remove(&self.uuid_key);
289
7
        self.metrics.active_uploads.fetch_sub(1, Ordering::Relaxed);
290
7
    }
291
}
292
293
impl InstanceInfo {
294
7
    fn track_compressed_upload(
295
7
        &self,
296
7
        uuid_key: UuidKey,
297
7
    ) -> (Arc<AtomicU64>, CompressedUploadGuard) {
298
7
        let bytes_received = Arc::new(AtomicU64::new(0));
299
7
        let uuid_key = {
300
7
            let mut active_uploads = self.active_uploads.lock();
301
7
            match active_uploads.entry(uuid_key) {
302
0
                Entry::Occupied(entry) => {
303
                    // Another upload already owns this UUID; rekey instead of
304
                    // clobbering its entry (which would break its QueryWriteStatus
305
                    // visibility and double-decrement the metric on guard drop).
306
                    // Mirrors create_or_join_upload_stream's collision handling.
307
0
                    let original_key = *entry.key();
308
0
                    let unique_key = ByteStreamServer::generate_unique_uuid_key(original_key);
309
0
                    warn!(
310
                        msg = "UUID collision detected on compressed upload, generating unique UUID to prevent conflict",
311
0
                        original_uuid = format!("{original_key:032x}"),
312
0
                        unique_uuid = format!("{unique_key:032x}")
313
                    );
314
0
                    self.metrics.uuid_collisions.fetch_add(1, Ordering::Relaxed);
315
                    // Release the Occupied entry's borrow so we can insert on the same guard.
316
0
                    let _ = entry;
317
0
                    active_uploads.insert(unique_key, (bytes_received.clone(), None));
318
0
                    unique_key
319
                }
320
7
                Entry::Vacant(entry) => {
321
7
                    let key = *entry.key();
322
7
                    entry.insert((bytes_received.clone(), None));
323
7
                    key
324
                }
325
            }
326
        };
327
7
        self.metrics.active_uploads.fetch_add(1, Ordering::Relaxed);
328
329
7
        (
330
7
            bytes_received,
331
7
            CompressedUploadGuard {
332
7
                uuid_key,
333
7
                active_uploads: self.active_uploads.clone(),
334
7
                metrics: self.metrics.clone(),
335
7
            },
336
7
        )
337
7
    }
338
}
339
340
/// Pump compressed `ByteStream` upload chunks into the decoder.
341
///
342
/// Compressed uploads intentionally do not support the identity upload resume
343
/// protocol; offsets are validated against compressed wire bytes.
344
7
async fn process_compressed_client_stream(
345
7
    mut stream: WriteRequestStreamWrapper<impl Stream<Item = Result<WriteRequest, Status>> + Unpin>,
346
7
    mut tx: DropCloserWriteHalf,
347
7
    bytes_received: &Arc<AtomicU64>,
348
7
) -> Result<(), Error> {
349
    loop {
350
15
        match stream.next().await {
351
15
            Some(Ok(write_request)) => {
352
15
                if write_request.write_offset < 0 {
353
0
                    return Err(make_input_err!(
354
0
                        "Invalid negative compressed write offset in write request: {}",
355
0
                        write_request.write_offset
356
0
                    ));
357
15
                }
358
15
                let write_offset = u64::try_from(write_request.write_offset)
359
15
                    .err_tip(|| "Compressed write offset was not convertible to u64")
?0
;
360
15
                let compressed_offset = tx.get_bytes_written();
361
15
                if write_offset != compressed_offset {
362
1
                    return Err(make_input_err!(
363
1
                        "Received out of order compressed data. Got {}, expected {}",
364
1
                        write_offset,
365
1
                        compressed_offset
366
1
                    ));
367
14
                }
368
369
14
                if !write_request.data.is_empty() {
370
13
                    tx.send(write_request.data)
371
13
                        .await
372
13
                        .err_tip(|| "Failed to forward compressed upload data")
?0
;
373
13
                    bytes_received.store(tx.get_bytes_written(), Ordering::Release);
374
1
                }
375
14
                if write_request.finish_write {
376
6
                    tx.send_eof()
377
6
                        .err_tip(|| "Failed to send compressed upload EOF")
?0
;
378
6
                    return Ok(());
379
8
                }
380
            }
381
0
            Some(Err(e)) => {
382
0
                return Err(e);
383
            }
384
            None => {
385
0
                return Err(make_err!(
386
0
                    Code::InvalidArgument,
387
0
                    "Compressed write stream ended without finish_write"
388
0
                ));
389
            }
390
        }
391
    }
392
7
}
393
394
type ReadStream = Pin<Box<dyn Stream<Item = Result<ReadResponse, Status>> + Send + 'static>>;
395
type StoreUpdateFuture = Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'static>>;
396
397
struct StreamState {
398
    uuid: UuidKey,
399
    tx: DropCloserWriteHalf,
400
    store_update_fut: StoreUpdateFuture,
401
    digest_function: DigestHasherFunc,
402
}
403
404
impl Debug for StreamState {
405
0
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
406
0
        f.debug_struct("StreamState")
407
0
            .field("uuid", &format!("{:032x}", self.uuid))
408
0
            .finish()
409
0
    }
410
}
411
412
/// If a stream is in this state, it will automatically be put back into an `IdleStream` and
413
/// placed back into the `active_uploads` map as an `IdleStream` after it is dropped.
414
/// To prevent it from being put back into an `IdleStream` you must call `.graceful_finish()`.
415
struct ActiveStreamGuard {
416
    stream_state: Option<StreamState>,
417
    bytes_received: Arc<AtomicU64>,
418
    active_uploads: Arc<Mutex<HashMap<UuidKey, BytesWrittenAndIdleStream>>>,
419
    metrics: Arc<ByteStreamMetrics>,
420
}
421
422
impl ActiveStreamGuard {
423
    /// Consumes the guard. The stream will be considered "finished", will
424
    /// remove it from the `active_uploads`.
425
13
    fn graceful_finish(mut self) {
426
13
        let stream_state = self.stream_state.take().unwrap();
427
13
        self.active_uploads.lock().remove(&stream_state.uuid);
428
        // Decrement active uploads counter on successful completion
429
13
        self.metrics.active_uploads.fetch_sub(1, Ordering::Relaxed);
430
13
    }
431
}
432
433
impl Drop for ActiveStreamGuard {
434
23
    fn drop(&mut self) {
435
23
        let Some(
stream_state10
) = self.stream_state.take() else {
436
13
            return; // If None it means we don't want it put back into an IdleStream.
437
        };
438
10
        let mut active_uploads = self.active_uploads.lock();
439
10
        let uuid = stream_state.uuid; // u128 is Copy, no clone needed
440
10
        let Some(active_uploads_slot) = active_uploads.get_mut(&uuid) else {
441
0
            error!(
442
                err = "Failed to find active upload. This should never happen.",
443
0
                uuid = format!("{:032x}", uuid),
444
            );
445
0
            return;
446
        };
447
        // Mark stream as idle with current timestamp.
448
        // The global sweeper will clean it up after idle_stream_timeout.
449
        // This avoids spawning a task per stream, reducing overhead from O(n) to O(1).
450
10
        active_uploads_slot.1 = Some(IdleStream {
451
10
            stream_state,
452
10
            idle_since: Instant::now(),
453
10
        });
454
23
    }
455
}
456
457
/// Represents a stream that is in the "idle" state. this means it is not currently being used
458
/// by a client. If it is not used within a certain amount of time it will be removed from the
459
/// `active_uploads` map automatically by the global sweeper task.
460
#[derive(Debug)]
461
struct IdleStream {
462
    stream_state: StreamState,
463
    /// When this stream became idle. Used by the global sweeper to determine expiration.
464
    idle_since: Instant,
465
}
466
467
impl IdleStream {
468
5
    fn into_active_stream(
469
5
        self,
470
5
        bytes_received: Arc<AtomicU64>,
471
5
        instance_info: &InstanceInfo,
472
5
    ) -> ActiveStreamGuard {
473
5
        ActiveStreamGuard {
474
5
            stream_state: Some(self.stream_state),
475
5
            bytes_received,
476
5
            active_uploads: instance_info.active_uploads.clone(),
477
5
            metrics: instance_info.metrics.clone(),
478
5
        }
479
5
    }
480
}
481
482
#[derive(Debug)]
483
pub struct ByteStreamServer {
484
    instance_infos: HashMap<InstanceName, InstanceInfo>,
485
    reported_digest_function_mismatches: Mutex<HashSet<(DigestHasherFunc, DigestHasherFunc)>>,
486
}
487
488
impl ByteStreamServer {
489
    /// Generate a unique UUID key by `XOR`ing the base key with a nanosecond timestamp.
490
    /// This ensures virtually zero collision probability while being O(1).
491
1
    fn generate_unique_uuid_key(base_key: UuidKey) -> UuidKey {
492
1
        let timestamp = SystemTime::now()
493
1
            .duration_since(UNIX_EPOCH)
494
1
            .unwrap_or_default()
495
1
            .as_nanos();
496
        // XOR with timestamp to create unique key
497
1
        base_key ^ timestamp
498
1
    }
499
500
43
    pub fn new(
501
43
        configs: &[WithInstanceName<ByteStreamConfig>],
502
43
        store_manager: &StoreManager,
503
43
        remote_cache_compression_instances: &RemoteCacheCompressionInstances,
504
43
    ) -> Result<Self, Error> {
505
43
        let mut instance_infos: HashMap<String, InstanceInfo> = HashMap::new();
506
43
        for config in configs {
507
43
            let idle_stream_timeout = if config.persist_stream_on_disconnect_timeout_s == 0 {
508
43
                DEFAULT_PERSIST_STREAM_ON_DISCONNECT_TIMEOUT
509
            } else {
510
0
                Duration::from_secs(config.persist_stream_on_disconnect_timeout_s as u64)
511
            };
512
43
            let remote_cache_compression_enabled =
513
43
                remote_cache_compression_instances.enabled_for(&config.instance_name);
514
43
            let _old_value = instance_infos.insert(
515
43
                config.instance_name.clone(),
516
43
                Self::new_with_timeout(
517
43
                    config,
518
43
                    store_manager,
519
43
                    idle_stream_timeout,
520
43
                    remote_cache_compression_enabled,
521
0
                )?,
522
            );
523
        }
524
43
        Ok(Self {
525
43
            instance_infos,
526
43
            reported_digest_function_mismatches: Mutex::new(HashSet::new()),
527
43
        })
528
43
    }
529
530
43
    pub fn new_with_timeout(
531
43
        config: &WithInstanceName<ByteStreamConfig>,
532
43
        store_manager: &StoreManager,
533
43
        idle_stream_timeout: Duration,
534
43
        remote_cache_compression_enabled: bool,
535
43
    ) -> Result<InstanceInfo, Error> {
536
43
        let store = store_manager
537
43
            .get_store(&config.cas_store)
538
43
            .ok_or_else(|| 
make_input_err!0
("'cas_store': '{}' does not exist",
config.cas_store0
))
?0
;
539
43
        let max_bytes_per_stream = if config.max_bytes_per_stream == 0 {
540
7
            DEFAULT_MAX_BYTES_PER_STREAM
541
        } else {
542
36
            config.max_bytes_per_stream
543
        };
544
545
43
        let active_uploads: Arc<Mutex<HashMap<UuidKey, BytesWrittenAndIdleStream>>> =
546
43
            Arc::new(Mutex::new(HashMap::new()));
547
43
        let metrics = Arc::new(ByteStreamMetrics::default());
548
549
        // Spawn a single global sweeper task that periodically cleans up expired idle streams.
550
        // This replaces per-stream timeout tasks, reducing task spawn overhead from O(n) to O(1).
551
43
        let sweeper_active_uploads = Arc::downgrade(&active_uploads);
552
43
        let sweeper_metrics = Arc::downgrade(&metrics);
553
43
        let sweep_interval = idle_stream_timeout / 2; // Check every half-timeout period
554
43
        let sweeper_handle = spawn!("bytestream_idle_stream_sweeper", async move 
{35
555
            loop {
556
35
                sleep(sweep_interval).await;
557
558
0
                let Some(active_uploads) = sweeper_active_uploads.upgrade() else {
559
                    // InstanceInfo has been dropped, exit the sweeper
560
0
                    break;
561
                };
562
0
                let metrics = sweeper_metrics.upgrade();
563
564
0
                let now = Instant::now();
565
0
                let mut expired_count = 0u64;
566
567
                // Lock and sweep expired entries
568
                {
569
0
                    let mut uploads = active_uploads.lock();
570
0
                    uploads.retain(|uuid, (_, maybe_idle)| {
571
0
                        if let Some(idle_stream) = maybe_idle
572
0
                            && now.duration_since(idle_stream.idle_since) >= idle_stream_timeout
573
                        {
574
0
                            info!(
575
                                msg = "Sweeping expired idle stream",
576
0
                                uuid = format!("{:032x}", uuid)
577
                            );
578
0
                            expired_count += 1;
579
0
                            return false; // Remove this entry
580
0
                        }
581
0
                        true // Keep this entry
582
0
                    });
583
                }
584
585
                // Update metrics outside the lock
586
0
                if expired_count > 0 {
587
0
                    if let Some(m) = &metrics {
588
0
                        m.idle_stream_timeouts
589
0
                            .fetch_add(expired_count, Ordering::Relaxed);
590
0
                        m.active_uploads.fetch_sub(expired_count, Ordering::Relaxed);
591
0
                    }
592
0
                    trace!(
593
                        msg = "Sweeper cleaned up expired streams",
594
                        count = expired_count
595
                    );
596
0
                }
597
            }
598
0
        });
599
600
43
        Ok(InstanceInfo {
601
43
            store,
602
43
            max_bytes_per_stream,
603
43
            active_uploads,
604
43
            idle_stream_timeout,
605
43
            metrics,
606
43
            _sweeper_handle: Arc::new(sweeper_handle),
607
43
            remote_cache_compression_enabled,
608
43
        })
609
43
    }
610
611
7
    pub fn into_service(self) -> Server<Self> {
612
7
        Server::new(self)
613
7
    }
614
615
    /// Creates or joins an upload stream for the given UUID.
616
    ///
617
    /// This function handles three scenarios:
618
    /// 1. UUID doesn't exist - creates a new upload stream
619
    /// 2. UUID exists but is idle - resumes the existing stream
620
    /// 3. UUID exists and is active - generates a unique UUID by appending a nanosecond
621
    ///    timestamp to avoid collision, then creates a new stream with that UUID
622
    ///
623
    /// The nanosecond timestamp ensures virtually zero probability of collision since
624
    /// two concurrent uploads would need to both collide on the original UUID AND
625
    /// generate the unique UUID in the exact same nanosecond.
626
24
    fn create_or_join_upload_stream(
627
24
        &self,
628
24
        uuid_str: &str,
629
24
        instance: &InstanceInfo,
630
24
        digest: DigestInfo,
631
24
        digest_function: DigestHasherFunc,
632
24
        write_offset: i64,
633
24
    ) -> Result<ActiveStreamGuard, Error> {
634
        // Bind the digest function to the retained store future itself. The
635
        // future can outlive this RPC while an upload is idle, so it must not
636
        // depend on the context of whichever request polls it next.
637
24
        let store_update_context = make_ctx_for_hash_func(digest_function)
?0
;
638
639
        // Parse UUID string to u128 key for efficient HashMap operations
640
24
        let uuid_key = parse_uuid_to_key(uuid_str);
641
642
18
        let (uuid, bytes_received, is_collision) = {
643
24
            let mut active_uploads = instance.active_uploads.lock();
644
24
            match active_uploads.entry(uuid_key) {
645
7
                Entry::Occupied(mut entry) => {
646
7
                    let maybe_idle_stream = entry.get_mut();
647
7
                    if let Some(
idle_stream5
) = maybe_idle_stream.1.as_ref()
648
5
                        && idle_stream.stream_state.digest_function != digest_function
649
                    {
650
0
                        return Err(make_input_err!(
651
0
                            "Cannot resume upload with digest function {} because it started with {}",
652
0
                            digest_function,
653
0
                            idle_stream.stream_state.digest_function,
654
0
                        ));
655
7
                    }
656
7
                    if let Some(
idle_stream5
) = maybe_idle_stream.1.take() {
657
                        // Case 2: Stream exists but is idle, we can resume it
658
5
                        let bytes_received = maybe_idle_stream.0.clone();
659
5
                        info!(
660
                            msg = "Joining existing stream",
661
5
                            uuid = format!("{:032x}", entry.key())
662
                        );
663
                        // Track resumed upload
664
5
                        instance
665
5
                            .metrics
666
5
                            .resumed_uploads
667
5
                            .fetch_add(1, Ordering::Relaxed);
668
5
                        return Ok(idle_stream.into_active_stream(bytes_received, instance));
669
2
                    }
670
                    // Case 3a: Stream is active and the client is resuming (non-zero
671
                    // offset). Only the owner of this UUID resumes it: the client has
672
                    // given up on its previous stream (deadline, dropped connection) but
673
                    // we have not observed that stream ending yet. A fresh UUID would
674
                    // expect offset 0 and reject the resume as out of order, so ask the
675
                    // client to retry; by then the old stream is idle and the resume
676
                    // joins it (Case 2).
677
2
                    if write_offset > 0 {
678
1
                        return Err(make_err!(
679
1
                            Code::Unavailable,
680
1
                            "Upload {uuid_str} is still active on the server; retry resuming at offset {write_offset}"
681
1
                        ));
682
1
                    }
683
                    // Case 3b: Stream is active - generate a unique UUID to avoid collision.
684
                    // Using nanosecond timestamp makes collision probability essentially zero.
685
1
                    let original_key = *entry.key();
686
1
                    let unique_key = Self::generate_unique_uuid_key(original_key);
687
1
                    warn!(
688
                        msg = "UUID collision detected, generating unique UUID to prevent conflict",
689
1
                        original_uuid = format!("{:032x}", original_key),
690
1
                        unique_uuid = format!("{:032x}", unique_key)
691
                    );
692
                    // Release the Occupied entry's borrow so we can insert on the same guard.
693
1
                    let _ = entry;
694
1
                    let bytes_received = Arc::new(AtomicU64::new(0));
695
1
                    active_uploads.insert(unique_key, (bytes_received.clone(), None));
696
1
                    (unique_key, bytes_received, true)
697
                }
698
17
                Entry::Vacant(entry) => {
699
                    // Case 1: UUID doesn't exist, create new stream
700
17
                    let bytes_received = Arc::new(AtomicU64::new(0));
701
17
                    let uuid = *entry.key();
702
                    // Our stream is "in use" if the key is in the map, but the value is None.
703
17
                    entry.insert((bytes_received.clone(), None));
704
17
                    (uuid, bytes_received, false)
705
                }
706
            }
707
        };
708
709
        // Track metrics for new upload
710
18
        instance
711
18
            .metrics
712
18
            .active_uploads
713
18
            .fetch_add(1, Ordering::Relaxed);
714
18
        if is_collision {
715
1
            instance
716
1
                .metrics
717
1
                .uuid_collisions
718
1
                .fetch_add(1, Ordering::Relaxed);
719
17
        }
720
721
        // Important: Do not return an error from this point onwards without
722
        // removing the entry from the map, otherwise that UUID becomes
723
        // unusable.
724
725
18
        let (tx, rx) = make_buf_channel_pair();
726
18
        let store = instance.store.clone();
727
18
        let store_update_fut = Box::pin(
728
14
            async move {
729
                // We need to wrap `Store::update()` in a another future because we need to capture
730
                // `store` to ensure its lifetime follows the future and not the caller.
731
14
                store
732
14
                    // Bytestream always uses digest size as the actual byte size.
733
14
                    .update(digest, rx, UploadSizeInfo::ExactSize(digest.size_bytes()))
734
14
                    .await
735
13
                    .map(|_| ())
736
13
            }
737
18
            .with_context(store_update_context),
738
        );
739
18
        Ok(ActiveStreamGuard {
740
18
            stream_state: Some(StreamState {
741
18
                uuid,
742
18
                tx,
743
18
                store_update_fut,
744
18
                digest_function,
745
18
            }),
746
18
            bytes_received,
747
18
            active_uploads: instance.active_uploads.clone(),
748
18
            metrics: instance.metrics.clone(),
749
18
        })
750
24
    }
751
752
8
    async fn inner_read(
753
8
        &self,
754
8
        instance: &InstanceInfo,
755
8
        digest: DigestInfo,
756
8
        read_request: ReadRequest,
757
8
    ) -> Result<impl Stream<Item = Result<ReadResponse, Status>> + Send + use<>, Error> {
758
        struct ReaderState {
759
            max_bytes_per_stream: usize,
760
            rx: DropCloserReadHalf,
761
            maybe_get_part_result: Option<Result<(), Error>>,
762
            get_part_fut: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>,
763
        }
764
765
8
        let read_limit = u64::try_from(read_request.read_limit)
766
8
            .err_tip(|| "Could not convert read_limit to u64")
?0
;
767
768
8
        let (tx, rx) = make_buf_channel_pair();
769
770
8
        let read_limit = if read_limit != 0 {
771
5
            Some(read_limit)
772
        } else {
773
3
            None
774
        };
775
776
        // This allows us to call a destructor when the the object is dropped.
777
8
        let store = instance.store.clone();
778
8
        let state = Some(ReaderState {
779
8
            rx,
780
8
            max_bytes_per_stream: instance.max_bytes_per_stream,
781
8
            maybe_get_part_result: None,
782
8
            get_part_fut: Box::pin(async move {
783
8
                store
784
8
                    .get_part(
785
8
                        digest,
786
8
                        tx,
787
8
                        u64::try_from(read_request.read_offset)
788
8
                            .err_tip(|| "Could not convert read_offset to u64")
?0
,
789
8
                        read_limit,
790
                    )
791
8
                    .await
792
8
            }),
793
        });
794
795
8
        let read_stream_span = error_span!("read_stream");
796
797
9.79k
        Ok(
Box::pin8
(
unfold8
(
state8
, move |state| {
798
9.79k
            async {
799
9.79k
            let mut state = state
?0
; // If None our stream is done.
800
9.79k
            let mut response = ReadResponse::default();
801
            {
802
9.79k
                let consume_fut = state.rx.consume(Some(state.max_bytes_per_stream));
803
9.79k
                tokio::pin!(consume_fut);
804
                loop {
805
9.80k
                    tokio::select! {
806
9.80k
                        
read_result9.79k
= &mut consume_fut => {
807
9.79k
                            match read_result {
808
9.79k
                                Ok(bytes) => {
809
9.79k
                                    if bytes.is_empty() {
810
                                        // EOF.
811
7
                                        return None;
812
9.78k
                                    }
813
9.78k
                                    if bytes.len() > state.max_bytes_per_stream {
814
0
                                        let err = make_err!(Code::Internal, "Returned store size was larger than read size");
815
0
                                        return Some((Err(err.into()), None));
816
9.78k
                                    }
817
9.78k
                                    response.data = bytes;
818
9.78k
                                    trace!(response.data = format!("<redacted len({})>", response.data.len()));
819
9.78k
                                    break;
820
                                }
821
1
                                Err(mut e) => {
822
                                    // We may need to propagate the error from reading the data through first.
823
                                    // For example, the NotFound error will come through `get_part_fut`, and
824
                                    // will not be present in `e`, but we need to ensure we pass NotFound error
825
                                    // code or the client won't know why it failed.
826
1
                                    let get_part_result = if let Some(result) = state.maybe_get_part_result {
827
1
                                        result
828
                                    } else {
829
                                        // This should never be `future::pending()` if maybe_get_part_result is
830
                                        // not set.
831
0
                                        state.get_part_fut.await
832
                                    };
833
1
                                    if let Err(err) = get_part_result {
834
1
                                        e = err.merge(e);
835
1
                                    
}0
836
1
                                    if e.code == Code::NotFound {
837
1
                                        // Trim the error code. Not Found is quite common and we don't want to send a large
838
1
                                        // error (debug) message for something that is common. We resize to just the last
839
1
                                        // message as it will be the most relevant.
840
1
                                        e.messages.truncate(1);
841
1
                                    
}0
842
1
                                    error!(response = ?e);
843
1
                                    return Some((Err(e.into()), None))
844
                                }
845
                            }
846
                        },
847
9.80k
                        
result8
= &mut state.get_part_fut => {
848
8
                            state.maybe_get_part_result = Some(result);
849
8
                            // It is non-deterministic on which future will finish in what order.
850
8
                            // It is also possible that the `state.rx.consume()` call above may not be able to
851
8
                            // respond even though the publishing future is done.
852
8
                            // Because of this we set the writing future to pending so it never finishes.
853
8
                            // The `state.rx.consume()` future will eventually finish and return either the
854
8
                            // data or an error.
855
8
                            // An EOF will terminate the `state.rx.consume()` future, but we are also protected
856
8
                            // because we are dropping the writing future, it will drop the `tx` channel
857
8
                            // which will eventually propagate an error to the `state.rx.consume()` future if
858
8
                            // the EOF was not sent due to some other error.
859
8
                            state.get_part_fut = Box::pin(pending());
860
8
                        },
861
                    }
862
                }
863
            }
864
9.78k
            Some((Ok(response), Some(state)))
865
9.79k
        }.instrument(read_stream_span.clone())
866
9.79k
        })))
867
8
    }
868
869
    // We instrument tracing here as well as below because `stream` has a hash on it
870
    // that is extracted from the first stream message. If we only implemented it below
871
    // we would not have the hash available to us.
872
    #[instrument(
873
        ret(level = Level::DEBUG),
874
        level = Level::ERROR,
875
        skip(self, instance_info),
876
    )]
877
24
    async fn inner_write(
878
24
        &self,
879
24
        instance_info: &InstanceInfo,
880
24
        digest: DigestInfo,
881
24
        digest_function: DigestHasherFunc,
882
24
        stream: WriteRequestStreamWrapper<impl Stream<Item = Result<WriteRequest, Status>> + Unpin>,
883
24
    ) -> Result<Response<WriteResponse>, Error> {
884
23
        async fn process_client_stream(
885
23
            mut stream: WriteRequestStreamWrapper<
886
23
                impl Stream<Item = Result<WriteRequest, Status>> + Unpin,
887
23
            >,
888
23
            tx: &mut DropCloserWriteHalf,
889
23
            outer_bytes_received: &Arc<AtomicU64>,
890
23
            expected_size: u64,
891
23
        ) -> Result<(), Error> {
892
            loop {
893
38
                let 
write_request31
= match stream.next().await {
894
                    // Code path for when client tries to gracefully close the stream.
895
                    // If this happens it means there's a problem with the data sent,
896
                    // because we always close the stream from our end before this point
897
                    // by counting the number of bytes sent from the client. If they send
898
                    // less than the amount they said they were going to send and then
899
                    // close the stream, we know there's a problem.
900
                    None => {
901
0
                        return Err(make_input_err!(
902
0
                            "Client closed stream before sending all data"
903
0
                        ));
904
                    }
905
                    // Code path for client stream error. Probably client disconnect.
906
6
                    Some(Err(err)) => return Err(err),
907
                    // Code path for received chunk of data.
908
31
                    Some(Ok(write_request)) => write_request,
909
                };
910
911
31
                if write_request.write_offset < 0 {
912
0
                    return Err(make_input_err!(
913
0
                        "Invalid negative write offset in write request: {}",
914
0
                        write_request.write_offset
915
0
                    ));
916
31
                }
917
31
                let write_offset = write_request.write_offset as u64;
918
919
                // If we get duplicate data because a client didn't know where
920
                // it left off from, then we can simply skip it.
921
31
                let 
data30
= if write_offset < tx.get_bytes_written() {
922
2
                    if (write_offset + write_request.data.len() as u64) < tx.get_bytes_written() {
923
0
                        if write_request.finish_write {
924
0
                            return Err(make_input_err!(
925
0
                                "Resumed stream finished at {} bytes when we already received {} bytes.",
926
0
                                write_offset + write_request.data.len() as u64,
927
0
                                tx.get_bytes_written()
928
0
                            ));
929
0
                        }
930
0
                        continue;
931
2
                    }
932
2
                    write_request.data.slice(
933
2
                        usize::try_from(tx.get_bytes_written() - write_offset)
934
2
                            .unwrap_or(usize::MAX)..,
935
                    )
936
                } else {
937
29
                    if write_offset != tx.get_bytes_written() {
938
1
                        return Err(make_input_err!(
939
1
                            "Received out of order data. Got {}, expected {}",
940
1
                            write_offset,
941
1
                            tx.get_bytes_written()
942
1
                        ));
943
28
                    }
944
28
                    write_request.data
945
                };
946
947
                // Do not process EOF or weird stuff will happen.
948
30
                if !data.is_empty() {
949
                    // We also need to process the possible EOF branch, so we can't early return.
950
23
                    if let Err(
mut err0
) = tx.send(data).await {
951
0
                        err.code = Code::Internal;
952
0
                        return Err(err);
953
23
                    }
954
23
                    outer_bytes_received.store(tx.get_bytes_written(), Ordering::Release);
955
7
                }
956
957
30
                if expected_size < tx.get_bytes_written() {
958
0
                    return Err(make_input_err!("Received more bytes than expected"));
959
30
                }
960
30
                if write_request.finish_write {
961
                    // Gracefully close our stream.
962
                    // REAPI requires the completed upload to match its digest:
963
                    // https://github.com/bazelbuild/remote-apis/blob/main/build/bazel/remote/execution/v2/remote_execution.proto
964
                    // Validate before EOF, which authorizes the store to commit.
965
15
                    UploadSizeInfo::ExactSize(expected_size)
966
15
                        .validate_size(tx.get_bytes_written())
?2
;
967
13
                    tx.send_eof()
968
13
                        .err_tip(|| "Failed to send EOF in ByteStream::write")
?0
;
969
13
                    return Ok(());
970
15
                }
971
                // Continue.
972
            }
973
            // Unreachable.
974
22
        }
975
976
        let uuid = stream
977
            .resource_info
978
            .uuid
979
            .as_ref()
980
0
            .ok_or_else(|| make_input_err!("UUID must be set if writing data"))?;
981
        let write_offset = stream.first_msg_write_offset().unwrap_or(0);
982
        let mut active_stream_guard = self.create_or_join_upload_stream(
983
            uuid,
984
            instance_info,
985
            digest,
986
            digest_function,
987
            write_offset,
988
        )?;
989
        let expected_size = stream.resource_info.expected_size as u64;
990
991
        let active_stream = active_stream_guard.stream_state.as_mut().unwrap();
992
        try_join!(
993
            process_client_stream(
994
                stream,
995
                &mut active_stream.tx,
996
                &active_stream_guard.bytes_received,
997
                expected_size
998
            ),
999
            (&mut active_stream.store_update_fut)
1000
0
                .map_err(|err| { err.append("Error updating inner store") })
1001
        )?;
1002
1003
        // Close our guard and consider the stream no longer active.
1004
        active_stream_guard.graceful_finish();
1005
1006
        Ok(Response::new(WriteResponse {
1007
            committed_size: expected_size.try_into().unwrap_or(i64::MAX),
1008
        }))
1009
23
    }
1010
1011
    /// Fast-path write that bypasses channel overhead for stores that support direct Bytes updates.
1012
    /// This buffers all data in memory and calls `update_oneshot` directly.
1013
7
    async fn inner_write_oneshot(
1014
7
        &self,
1015
7
        instance_info: &InstanceInfo,
1016
7
        digest: DigestInfo,
1017
7
        digest_function: DigestHasherFunc,
1018
7
        mut stream: WriteRequestStreamWrapper<
1019
7
            impl Stream<Item = Result<WriteRequest, Status>> + Unpin,
1020
7
        >,
1021
7
    ) -> Result<Response<WriteResponse>, Error> {
1022
7
        let expected_size = stream.resource_info.expected_size as u64;
1023
1024
        // Pre-allocate buffer for expected size (capped at reasonable limit to prevent DoS)
1025
7
        let capacity =
1026
7
            usize::try_from(expected_size.min(64 * 1024 * 1024)).unwrap_or(64 * 1024 * 1024);
1027
7
        let mut buffer = BytesMut::with_capacity(capacity);
1028
7
        let mut bytes_received: u64 = 0;
1029
1030
        // Collect all data from client stream
1031
        loop {
1032
7
            let 
write_request6
= match stream.next().await {
1033
                None => {
1034
0
                    return Err(make_input_err!(
1035
0
                        "Client closed stream before sending all data"
1036
0
                    ));
1037
                }
1038
1
                Some(Err(err)) => return Err(err),
1039
6
                Some(Ok(write_request)) => write_request,
1040
            };
1041
1042
6
            if write_request.write_offset < 0 {
1043
1
                return Err(make_input_err!(
1044
1
                    "Invalid negative write offset in write request: {}",
1045
1
                    write_request.write_offset
1046
1
                ));
1047
5
            }
1048
5
            let write_offset = write_request.write_offset as u64;
1049
1050
            // Handle duplicate/resumed data
1051
5
            let data = if write_offset < bytes_received {
1052
0
                if (write_offset + write_request.data.len() as u64) < bytes_received {
1053
0
                    if write_request.finish_write {
1054
0
                        return Err(make_input_err!(
1055
0
                            "Resumed stream finished at {} bytes when we already received {} bytes.",
1056
0
                            write_offset + write_request.data.len() as u64,
1057
0
                            bytes_received
1058
0
                        ));
1059
0
                    }
1060
0
                    continue;
1061
0
                }
1062
0
                write_request
1063
0
                    .data
1064
0
                    .slice(usize::try_from(bytes_received - write_offset).unwrap_or(usize::MAX)..)
1065
            } else {
1066
5
                if write_offset != bytes_received {
1067
0
                    return Err(make_input_err!(
1068
0
                        "Received out of order data. Got {}, expected {}",
1069
0
                        write_offset,
1070
0
                        bytes_received
1071
0
                    ));
1072
5
                }
1073
5
                write_request.data
1074
            };
1075
1076
5
            if !data.is_empty() {
1077
3
                buffer.extend_from_slice(&data);
1078
3
                bytes_received += data.len() as u64;
1079
3
            
}2
1080
1081
5
            if expected_size < bytes_received {
1082
0
                return Err(make_input_err!("Received more bytes than expected"));
1083
5
            }
1084
1085
5
            if write_request.finish_write {
1086
5
                break;
1087
0
            }
1088
        }
1089
1090
        // Direct update without channel overhead
1091
5
        UploadSizeInfo::ExactSize(expected_size).validate_size(bytes_received)
?2
;
1092
3
        let store = instance_info.store.clone();
1093
3
        let store_update_context = make_ctx_for_hash_func(digest_function)
?0
;
1094
3
        store
1095
3
            .update_oneshot(digest, buffer.freeze())
1096
3
            .with_context(store_update_context)
1097
3
            .await
1098
3
            .err_tip(|| "Error in update_oneshot")
?0
;
1099
1100
        // Note: bytes_written_total is updated in the caller (bytestream_write) based on result
1101
1102
3
        Ok(Response::new(WriteResponse {
1103
3
            committed_size: expected_size.try_into().unwrap_or(i64::MAX),
1104
3
        }))
1105
7
    }
1106
1107
    /// Handle a compressed upload: stream compressed wire bytes through the
1108
    /// decoder into the store update stream, validate the decoded size, and
1109
    /// verify the decoded digest.
1110
7
    async fn inner_write_compressed(
1111
7
        &self,
1112
7
        instance: &InstanceInfo,
1113
7
        digest: DigestInfo,
1114
7
        digest_function: DigestHasherFunc,
1115
7
        wire_compressor: compressor::Value,
1116
7
        stream: WriteRequestStreamWrapper<impl Stream<Item = Result<WriteRequest, Status>> + Unpin>,
1117
7
    ) -> Result<Response<WriteResponse>, Error> {
1118
        // Register the upload in active_uploads so QueryWriteStatus can report
1119
        // compressed wire-byte progress while decoding. This mirrors what
1120
        // create_or_join_upload_stream does for uncompressed uploads.
1121
7
        let uuid_str = stream
1122
7
            .resource_info
1123
7
            .uuid
1124
7
            .as_deref()
1125
7
            .ok_or_else(|| 
make_input_err!0
("UUID must be set if writing compressed data"))
?0
;
1126
7
        let uuid_key = parse_uuid_to_key(uuid_str);
1127
7
        let (bytes_received, _guard) = instance.track_compressed_upload(uuid_key);
1128
1129
7
        let (compressed_tx, compressed_rx) = make_buf_channel_pair();
1130
7
        let (decompressed_tx, decompressed_rx) = make_buf_channel_pair();
1131
7
        let store = instance.store.clone();
1132
7
        let store_update_context = make_ctx_for_hash_func(digest_function)
?0
;
1133
7
        let store_update_fut = async move {
1134
7
            store
1135
7
                .update(
1136
7
                    digest,
1137
7
                    decompressed_rx,
1138
7
                    UploadSizeInfo::ExactSize(digest.size_bytes()),
1139
7
                )
1140
7
                .await
1141
7
                .map(|_| ())
1142
7
                .err_tip(|| "Failed to store decompressed data")
1143
7
        }
1144
7
        .with_context(store_update_context);
1145
        // Plain async future: decode progresses at the pace of the client
1146
        // upload and the store write without occupying a blocking-pool thread
1147
        // for the stream's lifetime.
1148
7
        let decode_fut = crate::wire_compression::stream_decode_compressed_upload(
1149
7
            compressed_rx,
1150
7
            wire_compressor,
1151
7
            digest,
1152
7
            digest_function,
1153
7
            decompressed_tx,
1154
        );
1155
7
        let client_stream_fut =
1156
7
            process_compressed_client_stream(stream, compressed_tx, &bytes_received);
1157
7
        let (client_stream_result, decode_result, store_update_result) =
1158
7
            tokio::join!(client_stream_fut, decode_fut, store_update_fut);
1159
1160
7
        if let Err(
err1
) = &client_stream_result
1161
1
            && err.code == Code::InvalidArgument
1162
        {
1163
1
            return Err(err.clone());
1164
6
        }
1165
6
        if let Err(
err1
) = &decode_result
1166
1
            && err.code == Code::InvalidArgument
1167
        {
1168
1
            return Err(err.clone());
1169
5
        }
1170
5
        let mut upload_error = store_update_result.err();
1171
5
        if let Err(
err0
) = decode_result {
1172
0
            upload_error = Some(match upload_error {
1173
0
                Some(existing) => existing.merge(err),
1174
0
                None => err,
1175
            });
1176
5
        }
1177
5
        if let Err(
err0
) = client_stream_result {
1178
0
            upload_error = Some(match upload_error {
1179
0
                Some(existing) => existing.merge(err),
1180
0
                None => err,
1181
            });
1182
5
        }
1183
5
        if let Some(
err0
) = upload_error {
1184
0
            return Err(err);
1185
5
        }
1186
1187
5
        let committed_size = i64::try_from(bytes_received.load(Ordering::Acquire))
1188
5
            .err_tip(|| "Compressed upload size was not convertible to i64")
?0
;
1189
5
        Ok(Response::new(WriteResponse { committed_size }))
1190
7
    }
1191
1192
    /// Read a blob from the store, compress it with the given wire compressor,
1193
    /// and return it as a stream of chunked `ReadResponse`s.
1194
5
    async fn inner_read_compressed(
1195
5
        &self,
1196
5
        instance: &InstanceInfo,
1197
5
        digest: DigestInfo,
1198
5
        wire_compressor: compressor::Value,
1199
5
        read_request: ReadRequest,
1200
5
    ) -> Result<ReadStream, Error> {
1201
        struct ReaderState {
1202
            max_bytes_per_stream: usize,
1203
            rx: DropCloserReadHalf,
1204
            maybe_get_part_result: Option<Result<(), Error>>,
1205
            maybe_encode_result: Option<Result<(), Error>>,
1206
            get_part_fut: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>,
1207
            encode_fut: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>,
1208
        }
1209
1210
        impl ReaderState {
1211
4
            async fn finish(mut self, mut error: Option<Error>) -> Option<Error> {
1212
4
                let encode_result = if let Some(result) = self.maybe_encode_result.take() {
1213
4
                    result
1214
                } else {
1215
0
                    self.encode_fut.await
1216
                };
1217
4
                if let Err(
err0
) = encode_result {
1218
0
                    error = Some(match error {
1219
0
                        Some(existing) => err.merge(existing),
1220
0
                        None => err,
1221
                    });
1222
4
                }
1223
1224
4
                let get_part_result = if let Some(result) = self.maybe_get_part_result.take() {
1225
4
                    result
1226
                } else {
1227
0
                    self.get_part_fut.await
1228
                };
1229
4
                if let Err(
err0
) = get_part_result {
1230
0
                    error = Some(match error {
1231
0
                        Some(existing) => err.merge(existing),
1232
0
                        None => err,
1233
                    });
1234
4
                }
1235
1236
4
                error
1237
4
            }
1238
        }
1239
1240
5
        if read_request.read_limit != 0 {
1241
1
            return Err(make_input_err!(
1242
1
                "read_limit must be 0 when reading compressed blobs"
1243
1
            ));
1244
4
        }
1245
4
        if read_request.read_offset < 0 {
1246
0
            return Err(make_input_err!(
1247
0
                "read_offset must be non-negative when reading compressed blobs"
1248
0
            ));
1249
4
        }
1250
4
        let read_offset = u64::try_from(read_request.read_offset)
1251
4
            .err_tip(|| "Could not convert read_offset to u64")
?0
;
1252
1253
4
        let (raw_tx, raw_rx) = make_buf_channel_pair();
1254
4
        let (compressed_tx, compressed_rx) = make_buf_channel_pair();
1255
1256
4
        let store = instance.store.clone();
1257
4
        let get_part_fut = Box::pin(async move {
1258
4
            store
1259
4
                .get_part(digest, raw_tx, read_offset, None)
1260
4
                .await
1261
4
                .err_tip(|| "Failed to read blob for wire compression")
1262
4
        });
1263
        // The encode runs as a plain async future: it must not occupy a
1264
        // blocking-pool thread for the stream's lifetime, because it only
1265
        // progresses at the client's drain rate. Dropping the returned
1266
        // stream drops this future, which tears the encode down exactly
1267
        // like the previous task-abort-on-drop did.
1268
4
        let encode_fut = Box::pin(crate::wire_compression::stream_encode_compressed_download(
1269
4
            raw_rx,
1270
4
            wire_compressor,
1271
            crate::wire_compression::ZSTD_COMPRESSION_LEVEL,
1272
4
            compressed_tx,
1273
        ));
1274
1275
4
        let state = Some(ReaderState {
1276
4
            max_bytes_per_stream: instance.max_bytes_per_stream,
1277
4
            rx: compressed_rx,
1278
4
            maybe_get_part_result: None,
1279
4
            maybe_encode_result: None,
1280
4
            get_part_fut,
1281
4
            encode_fut,
1282
4
        });
1283
1284
67
        Ok(
Box::pin4
(
unfold4
(
state4
, move |state| async {
1285
            enum ReadStep {
1286
                Response(ReadResponse),
1287
                Finish(Option<Error>),
1288
            }
1289
1290
67
            let mut state = state
?0
;
1291
67
            let step = {
1292
67
                let mut response = ReadResponse::default();
1293
67
                let consume_fut = state.rx.consume(Some(state.max_bytes_per_stream));
1294
67
                tokio::pin!(consume_fut);
1295
                loop {
1296
75
                    tokio::select! {
1297
75
                        
read_result67
= &mut consume_fut => {
1298
67
                            match read_result {
1299
67
                                Ok(bytes) => {
1300
67
                                    if bytes.is_empty() {
1301
4
                                        break ReadStep::Finish(None);
1302
63
                                    }
1303
63
                                    if bytes.len() > state.max_bytes_per_stream {
1304
0
                                        let err = make_err!(Code::Internal, "Returned compressed size was larger than read size");
1305
0
                                        break ReadStep::Finish(Some(err));
1306
63
                                    }
1307
63
                                    response.data = bytes;
1308
63
                                    trace!(
1309
63
                                        response.data = format!("<redacted len({})>", response.data.len())
1310
                                    );
1311
63
                                    break ReadStep::Response(response);
1312
                                }
1313
0
                                Err(e) => {
1314
0
                                    break ReadStep::Finish(Some(e));
1315
                                }
1316
                            }
1317
                        }
1318
75
                        
get_part_result4
= &mut state.get_part_fut, if state.maybe_get_part_result.is_none() => {
1319
4
                            state.maybe_get_part_result = Some(get_part_result);
1320
4
                        }
1321
75
                        
encode_result4
= &mut state.encode_fut, if state.maybe_encode_result.is_none() => {
1322
4
                            state.maybe_encode_result = Some(encode_result);
1323
4
                        }
1324
                    }
1325
                }
1326
            };
1327
1328
67
            match step {
1329
63
                ReadStep::Response(response) => Some((Ok(response), Some(state))),
1330
4
                ReadStep::Finish(error) => {
1331
4
                    state.finish(error).await.map(|mut err| 
{0
1332
0
                        if err.code == Code::NotFound {
1333
0
                            // Trim common NotFound details to match the identity read path.
1334
0
                            err.messages.truncate(1);
1335
0
                        }
1336
0
                        error!(response = ?err);
1337
0
                        (Err(err.into()), None)
1338
0
                    })
1339
                }
1340
            }
1341
134
        })))
1342
5
    }
1343
1344
6
    async fn inner_query_write_status(
1345
6
        &self,
1346
6
        query_request: &QueryWriteStatusRequest,
1347
6
    ) -> Result<Response<QueryWriteStatusResponse>, Error> {
1348
6
        let mut resource_info = ResourceInfo::new(&query_request.resource_name, true)
?0
;
1349
1350
6
        let instance = self
1351
6
            .instance_infos
1352
6
            .get(resource_info.instance_name.as_ref())
1353
6
            .err_tip(|| 
{0
1354
0
                format!(
1355
                    "'instance_name' not configured for '{}'",
1356
                    resource_info.instance_name
1357
                )
1358
0
            })?;
1359
6
        let store_clone = instance.store.clone();
1360
1361
6
        let digest = DigestInfo::try_new(resource_info.hash.as_ref(), resource_info.expected_size)
?0
;
1362
1363
        // If we are a GrpcStore we shortcut here, as this is a special store.
1364
6
        if let Some(
grpc_store0
) = store_clone.downcast_ref::<GrpcStore>(Some(digest.into())) {
1365
0
            return grpc_store
1366
0
                .query_write_status(Request::new(query_request.clone()))
1367
0
                .await;
1368
6
        }
1369
1370
6
        let uuid_str = resource_info
1371
6
            .uuid
1372
6
            .take()
1373
6
            .ok_or_else(|| 
make_input_err!0
("UUID must be set if querying write status"))
?0
;
1374
6
        let uuid_key = parse_uuid_to_key(&uuid_str);
1375
1376
        {
1377
6
            let active_uploads = instance.active_uploads.lock();
1378
6
            if let Some((
received_bytes4
,
_maybe_idle_stream4
)) = active_uploads.get(&uuid_key) {
1379
4
                return Ok(Response::new(QueryWriteStatusResponse {
1380
4
                    committed_size: received_bytes
1381
4
                        .load(Ordering::Acquire)
1382
4
                        .try_into()
1383
4
                        .unwrap_or(i64::MAX),
1384
4
                    // If we are in the active_uploads map, but the value is None,
1385
4
                    // it means the stream is not complete.
1386
4
                    complete: false,
1387
4
                }));
1388
2
            }
1389
        }
1390
1391
2
        let has_fut = store_clone.has(digest);
1392
2
        let Some(
item_size1
) = has_fut.await.err_tip(|| "Failed to call .has() on store")
?0
else {
1393
            // We lie here and say that the stream needs to start over, even though
1394
            // it was never started. This can happen when the client disconnects
1395
            // before sending the first payload, but the client thinks it did send
1396
            // the payload.
1397
1
            return Ok(Response::new(QueryWriteStatusResponse {
1398
1
                committed_size: 0,
1399
1
                complete: false,
1400
1
            }));
1401
        };
1402
1
        Ok(Response::new(QueryWriteStatusResponse {
1403
1
            committed_size: item_size.try_into().unwrap_or(i64::MAX),
1404
1
            complete: true,
1405
1
        }))
1406
6
    }
1407
}
1408
1409
#[tonic::async_trait]
1410
impl ByteStream for ByteStreamServer {
1411
    type ReadStream = ReadStream;
1412
1413
    #[instrument(
1414
        err,
1415
        level = Level::ERROR,
1416
        skip_all,
1417
        fields(request = ?grpc_request.get_ref())
1418
    )]
1419
    async fn read(
1420
        &self,
1421
        grpc_request: Request<ReadRequest>,
1422
    ) -> Result<Response<Self::ReadStream>, Status> {
1423
        let start_time = Instant::now();
1424
1425
        let read_request = grpc_request.into_inner();
1426
        let resource_name = read_request.resource_name.clone();
1427
        let resource_info = ResourceInfo::new(&resource_name, false)?;
1428
        let instance_name = resource_info.instance_name.as_ref();
1429
        let expected_size = resource_info.expected_size as u64;
1430
        let instance = self
1431
            .instance_infos
1432
            .get(instance_name)
1433
0
            .err_tip(|| format!("'instance_name' not configured for '{instance_name}'"))?;
1434
1435
        trace!(
1436
            resource_name,
1437
            instance_name, expected_size, "Starting bytestream request"
1438
        );
1439
1440
        // Track read request
1441
        instance
1442
            .metrics
1443
            .read_requests_total
1444
            .fetch_add(1, Ordering::Relaxed);
1445
1446
        let store = instance.store.clone();
1447
1448
        let digest = DigestInfo::try_new(resource_info.hash.as_ref(), resource_info.expected_size)?;
1449
1450
        // If we are a GrpcStore we shortcut here, as this is a special store.
1451
        if let Some(grpc_store) = store.downcast_ref::<GrpcStore>(Some(digest.into())) {
1452
            let stream = Box::pin(grpc_store.read(Request::new(read_request)).await?);
1453
            return Ok(Response::new(stream));
1454
        }
1455
1456
        let digest_function = resource_info.digest_function.as_deref().map_or_else(
1457
10
            || Ok(default_digest_hasher_func()),
1458
            DigestHasherFunc::try_from,
1459
        )?;
1460
1461
        // Determine if the client requested wire-compressed data via compressed-blobs URI.
1462
        let wire_compressor = crate::wire_compression::resolve_wire_compressor(
1463
            resource_info.compressor.as_deref(),
1464
            instance.remote_cache_compression_enabled,
1465
        )?;
1466
1467
        let resp = if wire_compressor == compressor::Value::Identity {
1468
            // Uncompressed path — use the existing streaming read.
1469
            self.inner_read(instance, digest, read_request)
1470
                .instrument(error_span!("bytestream_read"))
1471
                .with_context(
1472
                    make_ctx_for_hash_func(digest_function)
1473
                        .err_tip(|| "In BytestreamServer::read")?,
1474
                )
1475
                .await
1476
                .err_tip(|| "In ByteStreamServer::read")
1477
8
                .map(|stream| -> Response<Self::ReadStream> { Response::new(Box::pin(stream)) })
1478
        } else {
1479
            // Compressed path — stream the requested raw range through zstd.
1480
            self.inner_read_compressed(instance, digest, wire_compressor, read_request)
1481
                .instrument(error_span!("bytestream_read_compressed"))
1482
                .with_context(
1483
                    make_ctx_for_hash_func(digest_function)
1484
                        .err_tip(|| "In BytestreamServer::read_compressed")?,
1485
                )
1486
                .await
1487
                .err_tip(|| "In ByteStreamServer::read_compressed")
1488
4
                .map(|stream| -> Response<Self::ReadStream> { Response::new(Box::pin(stream)) })
1489
        };
1490
1491
        // Track metrics based on result
1492
        let elapsed = start_time.elapsed();
1493
        #[allow(clippy::cast_possible_truncation)]
1494
        let elapsed_ns = elapsed.as_nanos() as u64;
1495
        instance
1496
            .metrics
1497
            .read_duration_ns
1498
            .fetch_add(elapsed_ns, Ordering::Relaxed);
1499
1500
        trace!(
1501
            ?elapsed,
1502
            resource_name, instance_name, expected_size, "Completed bytestream request"
1503
        );
1504
1505
        match &resp {
1506
            Ok(_) => {
1507
                instance
1508
                    .metrics
1509
                    .read_requests_success
1510
                    .fetch_add(1, Ordering::Relaxed);
1511
                instance
1512
                    .metrics
1513
                    .bytes_read_total
1514
                    .fetch_add(expected_size, Ordering::Relaxed);
1515
                debug!(return = "Ok(<stream>)");
1516
            }
1517
            Err(_) => {
1518
                instance
1519
                    .metrics
1520
                    .read_requests_failure
1521
                    .fetch_add(1, Ordering::Relaxed);
1522
            }
1523
        }
1524
1525
        resp.map_err(Into::into)
1526
    }
1527
1528
    #[instrument(
1529
        err,
1530
        level = Level::ERROR,
1531
        skip_all,
1532
        fields(request = ?grpc_request.get_ref())
1533
    )]
1534
    async fn write(
1535
        &self,
1536
        grpc_request: Request<Streaming<WriteRequest>>,
1537
    ) -> Result<Response<WriteResponse>, Status> {
1538
        let start_time = Instant::now();
1539
1540
        let request = grpc_request.into_inner();
1541
        let stream = WriteRequestStreamWrapper::from(request)
1542
            .await
1543
            .err_tip(|| "Could not unwrap first stream message")
1544
            .map_err(Into::<Status>::into)?;
1545
1546
        let instance_name = stream.resource_info.instance_name.as_ref();
1547
        let expected_size = stream.resource_info.expected_size as u64;
1548
        let instance = self
1549
            .instance_infos
1550
            .get(instance_name)
1551
0
            .err_tip(|| format!("'instance_name' not configured for '{instance_name}'"))?;
1552
1553
        // Track write request
1554
        instance
1555
            .metrics
1556
            .write_requests_total
1557
            .fetch_add(1, Ordering::Relaxed);
1558
1559
        let store = instance.store.clone();
1560
1561
        let digest = DigestInfo::try_new(
1562
            &stream.resource_info.hash,
1563
            stream.resource_info.expected_size,
1564
        )
1565
        .err_tip(|| "Invalid digest input in ByteStream::write")?;
1566
1567
        // If we are a GrpcStore we shortcut here, as this is a special store.
1568
        if let Some(grpc_store) = store.downcast_ref::<GrpcStore>(Some(digest.into())) {
1569
            let resp = grpc_store.write(stream).await.map_err(Into::into);
1570
            return resp;
1571
        }
1572
1573
        let default_digest_function = default_digest_hasher_func();
1574
        let digest_function = match stream.resource_info.digest_function.as_deref() {
1575
            Some(value) => {
1576
                let digest_function = DigestHasherFunc::try_from(value)?;
1577
                if digest_function != default_digest_function
1578
                    && self
1579
                        .reported_digest_function_mismatches
1580
                        .lock()
1581
                        .insert((digest_function, default_digest_function))
1582
                {
1583
                    warn!(
1584
                        client_digest_function = %digest_function,
1585
                        server_default_digest_function = %default_digest_function,
1586
                        "ByteStream client declared a digest function that differs from the server default; the client-declared function will be used for this upload, but clients using different digest functions generate different cache keys and will not share cache hits; configure global.default_digest_hash_function and all clients to use the same digest function"
1587
                    );
1588
                }
1589
                digest_function
1590
            }
1591
            None => default_digest_function,
1592
        };
1593
1594
        // Determine if the client is sending wire-compressed data via compressed-blobs URI.
1595
        let wire_compressor = crate::wire_compression::resolve_wire_compressor(
1596
            stream.resource_info.compressor.as_deref(),
1597
            instance.remote_cache_compression_enabled,
1598
        )?;
1599
1600
        // For compressed uploads, stream compressed wire bytes through the
1601
        // decoder and store the resulting raw bytes.
1602
        if wire_compressor != compressor::Value::Identity {
1603
            let result = self
1604
                .inner_write_compressed(instance, digest, digest_function, wire_compressor, stream)
1605
                .instrument(error_span!("bytestream_write_compressed"))
1606
                .await
1607
                .err_tip(|| "In ByteStreamServer::write_compressed");
1608
1609
            // Track metrics based on result
1610
            #[allow(clippy::cast_possible_truncation)]
1611
            let elapsed_ns = start_time.elapsed().as_nanos() as u64;
1612
            instance
1613
                .metrics
1614
                .write_duration_ns
1615
                .fetch_add(elapsed_ns, Ordering::Relaxed);
1616
1617
            match &result {
1618
                Ok(_) => {
1619
                    instance
1620
                        .metrics
1621
                        .write_requests_success
1622
                        .fetch_add(1, Ordering::Relaxed);
1623
                    instance
1624
                        .metrics
1625
                        .bytes_written_total
1626
                        .fetch_add(expected_size, Ordering::Relaxed);
1627
                }
1628
                Err(_) => {
1629
                    instance
1630
                        .metrics
1631
                        .write_requests_failure
1632
                        .fetch_add(1, Ordering::Relaxed);
1633
                }
1634
            }
1635
1636
            return result.map_err(Into::into);
1637
        }
1638
1639
        // Check if store supports direct oneshot updates (bypasses channel overhead).
1640
        // Use fast-path only when:
1641
        // 1. Store supports oneshot optimization
1642
        // 2. UUID is provided
1643
        // 3. Size is under 64MB (memory safety)
1644
        // 4. This is a NEW upload (UUID not already in active_uploads)
1645
        // 5. The first message has finish_write=true (single-shot upload)
1646
        //
1647
        // The oneshot path cannot be used for multi-message streams because:
1648
        // - QueryWriteStatus won't work (no progress tracking)
1649
        // - Resumed streams won't work (no partial progress)
1650
        let use_oneshot = if store.optimized_for(StoreOptimizations::SubscribesToUpdateOneshot)
1651
            && expected_size <= 64 * 1024 * 1024
1652
            && let Some(ref resource_uuid) = stream.resource_info.uuid
1653
        {
1654
            // Check if first message completes the upload (single-shot)
1655
            let is_single_shot = stream.is_first_msg_complete();
1656
1657
            if is_single_shot {
1658
                let uuid_key = parse_uuid_to_key(resource_uuid);
1659
                // Only use oneshot if this UUID is not already being tracked
1660
                !instance.active_uploads.lock().contains_key(&uuid_key)
1661
            } else {
1662
                false
1663
            }
1664
        } else {
1665
            false
1666
        };
1667
1668
        let result = if use_oneshot {
1669
            self.inner_write_oneshot(instance, digest, digest_function, stream)
1670
                .instrument(error_span!("bytestream_write_oneshot"))
1671
                .await
1672
                .err_tip(|| "In ByteStreamServer::write (oneshot)")
1673
        } else {
1674
            self.inner_write(instance, digest, digest_function, stream)
1675
                .instrument(error_span!("bytestream_write"))
1676
                .await
1677
                .err_tip(|| "In ByteStreamServer::write")
1678
        };
1679
1680
        // Track metrics based on result
1681
        #[allow(clippy::cast_possible_truncation)]
1682
        let elapsed_ns = start_time.elapsed().as_nanos() as u64;
1683
        instance
1684
            .metrics
1685
            .write_duration_ns
1686
            .fetch_add(elapsed_ns, Ordering::Relaxed);
1687
1688
        match &result {
1689
            Ok(_) => {
1690
                instance
1691
                    .metrics
1692
                    .write_requests_success
1693
                    .fetch_add(1, Ordering::Relaxed);
1694
                instance
1695
                    .metrics
1696
                    .bytes_written_total
1697
                    .fetch_add(expected_size, Ordering::Relaxed);
1698
            }
1699
            Err(_) => {
1700
                instance
1701
                    .metrics
1702
                    .write_requests_failure
1703
                    .fetch_add(1, Ordering::Relaxed);
1704
            }
1705
        }
1706
1707
        result.map_err(Into::into)
1708
    }
1709
1710
    #[instrument(
1711
        err,
1712
        ret(level = Level::INFO),
1713
        level = Level::ERROR,
1714
        skip_all,
1715
        fields(request = ?grpc_request.get_ref())
1716
    )]
1717
    async fn query_write_status(
1718
        &self,
1719
        grpc_request: Request<QueryWriteStatusRequest>,
1720
    ) -> Result<Response<QueryWriteStatusResponse>, Status> {
1721
        let request = grpc_request.into_inner();
1722
1723
        // Track query_write_status request - we need to parse the resource name to get the instance
1724
        if let Ok(resource_info) = ResourceInfo::new(&request.resource_name, true)
1725
            && let Some(instance) = self
1726
                .instance_infos
1727
                .get(resource_info.instance_name.as_ref())
1728
        {
1729
            instance
1730
                .metrics
1731
                .query_write_status_total
1732
                .fetch_add(1, Ordering::Relaxed);
1733
        }
1734
1735
        self.inner_query_write_status(&request)
1736
            .await
1737
            .err_tip(|| "Failed on query_write_status() command")
1738
            .map_err(Into::into)
1739
    }
1740
}