Coverage Report

Created: 2026-07-30 18:11

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
37
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
37
    let clean: String = uuid_str.chars().filter(char::is_ascii_hexdigit).collect();
224
37
    if clean.len() >= 16 {
225
        // Take up to 32 hex chars (128 bits)
226
37
        let hex_str = if clean.len() > 32 {
227
0
            &clean[..32]
228
        } else {
229
37
            &clean
230
        };
231
37
        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
37
}
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
12
    fn graceful_finish(mut self) {
426
12
        let stream_state = self.stream_state.take().unwrap();
427
12
        self.active_uploads.lock().remove(&stream_state.uuid);
428
        // Decrement active uploads counter on successful completion
429
12
        self.metrics.active_uploads.fetch_sub(1, Ordering::Relaxed);
430
12
    }
431
}
432
433
impl Drop for ActiveStreamGuard {
434
19
    fn drop(&mut self) {
435
19
        let Some(
stream_state7
) = self.stream_state.take() else {
436
12
            return; // If None it means we don't want it put back into an IdleStream.
437
        };
438
7
        let mut active_uploads = self.active_uploads.lock();
439
7
        let uuid = stream_state.uuid; // u128 is Copy, no clone needed
440
7
        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
7
        active_uploads_slot.1 = Some(IdleStream {
451
7
            stream_state,
452
7
            idle_since: Instant::now(),
453
7
        });
454
19
    }
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
4
    fn into_active_stream(
469
4
        self,
470
4
        bytes_received: Arc<AtomicU64>,
471
4
        instance_info: &InstanceInfo,
472
4
    ) -> ActiveStreamGuard {
473
4
        ActiveStreamGuard {
474
4
            stream_state: Some(self.stream_state),
475
4
            bytes_received,
476
4
            active_uploads: instance_info.active_uploads.clone(),
477
4
            metrics: instance_info.metrics.clone(),
478
4
        }
479
4
    }
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
38
    pub fn new(
501
38
        configs: &[WithInstanceName<ByteStreamConfig>],
502
38
        store_manager: &StoreManager,
503
38
        remote_cache_compression_instances: &RemoteCacheCompressionInstances,
504
38
    ) -> Result<Self, Error> {
505
38
        let mut instance_infos: HashMap<String, InstanceInfo> = HashMap::new();
506
38
        for config in configs {
507
38
            let idle_stream_timeout = if config.persist_stream_on_disconnect_timeout_s == 0 {
508
38
                DEFAULT_PERSIST_STREAM_ON_DISCONNECT_TIMEOUT
509
            } else {
510
0
                Duration::from_secs(config.persist_stream_on_disconnect_timeout_s as u64)
511
            };
512
38
            let remote_cache_compression_enabled =
513
38
                remote_cache_compression_instances.enabled_for(&config.instance_name);
514
38
            let _old_value = instance_infos.insert(
515
38
                config.instance_name.clone(),
516
38
                Self::new_with_timeout(
517
38
                    config,
518
38
                    store_manager,
519
38
                    idle_stream_timeout,
520
38
                    remote_cache_compression_enabled,
521
0
                )?,
522
            );
523
        }
524
38
        Ok(Self {
525
38
            instance_infos,
526
38
            reported_digest_function_mismatches: Mutex::new(HashSet::new()),
527
38
        })
528
38
    }
529
530
38
    pub fn new_with_timeout(
531
38
        config: &WithInstanceName<ByteStreamConfig>,
532
38
        store_manager: &StoreManager,
533
38
        idle_stream_timeout: Duration,
534
38
        remote_cache_compression_enabled: bool,
535
38
    ) -> Result<InstanceInfo, Error> {
536
38
        let store = store_manager
537
38
            .get_store(&config.cas_store)
538
38
            .ok_or_else(|| 
make_input_err!0
("'cas_store': '{}' does not exist",
config.cas_store0
))
?0
;
539
38
        let max_bytes_per_stream = if config.max_bytes_per_stream == 0 {
540
7
            DEFAULT_MAX_BYTES_PER_STREAM
541
        } else {
542
31
            config.max_bytes_per_stream
543
        };
544
545
38
        let active_uploads: Arc<Mutex<HashMap<UuidKey, BytesWrittenAndIdleStream>>> =
546
38
            Arc::new(Mutex::new(HashMap::new()));
547
38
        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
38
        let sweeper_active_uploads = Arc::downgrade(&active_uploads);
552
38
        let sweeper_metrics = Arc::downgrade(&metrics);
553
38
        let sweep_interval = idle_stream_timeout / 2; // Check every half-timeout period
554
38
        let sweeper_handle = spawn!("bytestream_idle_stream_sweeper", async move 
{30
555
            loop {
556
30
                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
38
        Ok(InstanceInfo {
601
38
            store,
602
38
            max_bytes_per_stream,
603
38
            active_uploads,
604
38
            idle_stream_timeout,
605
38
            metrics,
606
38
            _sweeper_handle: Arc::new(sweeper_handle),
607
38
            remote_cache_compression_enabled,
608
38
        })
609
38
    }
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
19
    fn create_or_join_upload_stream(
627
19
        &self,
628
19
        uuid_str: &str,
629
19
        instance: &InstanceInfo,
630
19
        digest: DigestInfo,
631
19
        digest_function: DigestHasherFunc,
632
19
    ) -> Result<ActiveStreamGuard, Error> {
633
        // Bind the digest function to the retained store future itself. The
634
        // future can outlive this RPC while an upload is idle, so it must not
635
        // depend on the context of whichever request polls it next.
636
19
        let store_update_context = make_ctx_for_hash_func(digest_function)
?0
;
637
638
        // Parse UUID string to u128 key for efficient HashMap operations
639
19
        let uuid_key = parse_uuid_to_key(uuid_str);
640
641
15
        let (uuid, bytes_received, is_collision) = {
642
19
            let mut active_uploads = instance.active_uploads.lock();
643
19
            match active_uploads.entry(uuid_key) {
644
5
                Entry::Occupied(mut entry) => {
645
5
                    let maybe_idle_stream = entry.get_mut();
646
5
                    if let Some(
idle_stream4
) = maybe_idle_stream.1.as_ref()
647
4
                        && idle_stream.stream_state.digest_function != digest_function
648
                    {
649
0
                        return Err(make_input_err!(
650
0
                            "Cannot resume upload with digest function {} because it started with {}",
651
0
                            digest_function,
652
0
                            idle_stream.stream_state.digest_function,
653
0
                        ));
654
5
                    }
655
5
                    if let Some(
idle_stream4
) = maybe_idle_stream.1.take() {
656
                        // Case 2: Stream exists but is idle, we can resume it
657
4
                        let bytes_received = maybe_idle_stream.0.clone();
658
4
                        info!(
659
                            msg = "Joining existing stream",
660
4
                            uuid = format!("{:032x}", entry.key())
661
                        );
662
                        // Track resumed upload
663
4
                        instance
664
4
                            .metrics
665
4
                            .resumed_uploads
666
4
                            .fetch_add(1, Ordering::Relaxed);
667
4
                        return Ok(idle_stream.into_active_stream(bytes_received, instance));
668
1
                    }
669
                    // Case 3: Stream is active - generate a unique UUID to avoid collision.
670
                    // Using nanosecond timestamp makes collision probability essentially zero.
671
1
                    let original_key = *entry.key();
672
1
                    let unique_key = Self::generate_unique_uuid_key(original_key);
673
1
                    warn!(
674
                        msg = "UUID collision detected, generating unique UUID to prevent conflict",
675
1
                        original_uuid = format!("{:032x}", original_key),
676
1
                        unique_uuid = format!("{:032x}", unique_key)
677
                    );
678
                    // Release the Occupied entry's borrow so we can insert on the same guard.
679
1
                    let _ = entry;
680
1
                    let bytes_received = Arc::new(AtomicU64::new(0));
681
1
                    active_uploads.insert(unique_key, (bytes_received.clone(), None));
682
1
                    (unique_key, bytes_received, true)
683
                }
684
14
                Entry::Vacant(entry) => {
685
                    // Case 1: UUID doesn't exist, create new stream
686
14
                    let bytes_received = Arc::new(AtomicU64::new(0));
687
14
                    let uuid = *entry.key();
688
                    // Our stream is "in use" if the key is in the map, but the value is None.
689
14
                    entry.insert((bytes_received.clone(), None));
690
14
                    (uuid, bytes_received, false)
691
                }
692
            }
693
        };
694
695
        // Track metrics for new upload
696
15
        instance
697
15
            .metrics
698
15
            .active_uploads
699
15
            .fetch_add(1, Ordering::Relaxed);
700
15
        if is_collision {
701
1
            instance
702
1
                .metrics
703
1
                .uuid_collisions
704
1
                .fetch_add(1, Ordering::Relaxed);
705
14
        }
706
707
        // Important: Do not return an error from this point onwards without
708
        // removing the entry from the map, otherwise that UUID becomes
709
        // unusable.
710
711
15
        let (tx, rx) = make_buf_channel_pair();
712
15
        let store = instance.store.clone();
713
15
        let store_update_fut = Box::pin(
714
13
            async move {
715
                // We need to wrap `Store::update()` in a another future because we need to capture
716
                // `store` to ensure its lifetime follows the future and not the caller.
717
13
                store
718
13
                    // Bytestream always uses digest size as the actual byte size.
719
13
                    .update(digest, rx, UploadSizeInfo::ExactSize(digest.size_bytes()))
720
13
                    .await
721
12
                    .map(|_| ())
722
12
            }
723
15
            .with_context(store_update_context),
724
        );
725
15
        Ok(ActiveStreamGuard {
726
15
            stream_state: Some(StreamState {
727
15
                uuid,
728
15
                tx,
729
15
                store_update_fut,
730
15
                digest_function,
731
15
            }),
732
15
            bytes_received,
733
15
            active_uploads: instance.active_uploads.clone(),
734
15
            metrics: instance.metrics.clone(),
735
15
        })
736
19
    }
737
738
8
    async fn inner_read(
739
8
        &self,
740
8
        instance: &InstanceInfo,
741
8
        digest: DigestInfo,
742
8
        read_request: ReadRequest,
743
8
    ) -> Result<impl Stream<Item = Result<ReadResponse, Status>> + Send + use<>, Error> {
744
        struct ReaderState {
745
            max_bytes_per_stream: usize,
746
            rx: DropCloserReadHalf,
747
            maybe_get_part_result: Option<Result<(), Error>>,
748
            get_part_fut: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>,
749
        }
750
751
8
        let read_limit = u64::try_from(read_request.read_limit)
752
8
            .err_tip(|| "Could not convert read_limit to u64")
?0
;
753
754
8
        let (tx, rx) = make_buf_channel_pair();
755
756
8
        let read_limit = if read_limit != 0 {
757
5
            Some(read_limit)
758
        } else {
759
3
            None
760
        };
761
762
        // This allows us to call a destructor when the the object is dropped.
763
8
        let store = instance.store.clone();
764
8
        let state = Some(ReaderState {
765
8
            rx,
766
8
            max_bytes_per_stream: instance.max_bytes_per_stream,
767
8
            maybe_get_part_result: None,
768
8
            get_part_fut: Box::pin(async move {
769
8
                store
770
8
                    .get_part(
771
8
                        digest,
772
8
                        tx,
773
8
                        u64::try_from(read_request.read_offset)
774
8
                            .err_tip(|| "Could not convert read_offset to u64")
?0
,
775
8
                        read_limit,
776
                    )
777
8
                    .await
778
8
            }),
779
        });
780
781
8
        let read_stream_span = error_span!("read_stream");
782
783
9.79k
        Ok(
Box::pin8
(
unfold8
(
state8
, move |state| {
784
9.79k
            async {
785
9.79k
            let mut state = state
?0
; // If None our stream is done.
786
9.79k
            let mut response = ReadResponse::default();
787
            {
788
9.79k
                let consume_fut = state.rx.consume(Some(state.max_bytes_per_stream));
789
9.79k
                tokio::pin!(consume_fut);
790
                loop {
791
9.80k
                    tokio::select! {
792
9.80k
                        
read_result9.79k
= &mut consume_fut => {
793
9.79k
                            match read_result {
794
9.79k
                                Ok(bytes) => {
795
9.79k
                                    if bytes.is_empty() {
796
                                        // EOF.
797
7
                                        return None;
798
9.78k
                                    }
799
9.78k
                                    if bytes.len() > state.max_bytes_per_stream {
800
0
                                        let err = make_err!(Code::Internal, "Returned store size was larger than read size");
801
0
                                        return Some((Err(err.into()), None));
802
9.78k
                                    }
803
9.78k
                                    response.data = bytes;
804
9.78k
                                    trace!(response.data = format!("<redacted len({})>", response.data.len()));
805
9.78k
                                    break;
806
                                }
807
1
                                Err(mut e) => {
808
                                    // We may need to propagate the error from reading the data through first.
809
                                    // For example, the NotFound error will come through `get_part_fut`, and
810
                                    // will not be present in `e`, but we need to ensure we pass NotFound error
811
                                    // code or the client won't know why it failed.
812
1
                                    let get_part_result = if let Some(result) = state.maybe_get_part_result {
813
1
                                        result
814
                                    } else {
815
                                        // This should never be `future::pending()` if maybe_get_part_result is
816
                                        // not set.
817
0
                                        state.get_part_fut.await
818
                                    };
819
1
                                    if let Err(err) = get_part_result {
820
1
                                        e = err.merge(e);
821
1
                                    
}0
822
1
                                    if e.code == Code::NotFound {
823
1
                                        // Trim the error code. Not Found is quite common and we don't want to send a large
824
1
                                        // error (debug) message for something that is common. We resize to just the last
825
1
                                        // message as it will be the most relevant.
826
1
                                        e.messages.truncate(1);
827
1
                                    
}0
828
1
                                    error!(response = ?e);
829
1
                                    return Some((Err(e.into()), None))
830
                                }
831
                            }
832
                        },
833
9.80k
                        
result8
= &mut state.get_part_fut => {
834
8
                            state.maybe_get_part_result = Some(result);
835
8
                            // It is non-deterministic on which future will finish in what order.
836
8
                            // It is also possible that the `state.rx.consume()` call above may not be able to
837
8
                            // respond even though the publishing future is done.
838
8
                            // Because of this we set the writing future to pending so it never finishes.
839
8
                            // The `state.rx.consume()` future will eventually finish and return either the
840
8
                            // data or an error.
841
8
                            // An EOF will terminate the `state.rx.consume()` future, but we are also protected
842
8
                            // because we are dropping the writing future, it will drop the `tx` channel
843
8
                            // which will eventually propagate an error to the `state.rx.consume()` future if
844
8
                            // the EOF was not sent due to some other error.
845
8
                            state.get_part_fut = Box::pin(pending());
846
8
                        },
847
                    }
848
                }
849
            }
850
9.78k
            Some((Ok(response), Some(state)))
851
9.79k
        }.instrument(read_stream_span.clone())
852
9.79k
        })))
853
8
    }
854
855
    // We instrument tracing here as well as below because `stream` has a hash on it
856
    // that is extracted from the first stream message. If we only implemented it below
857
    // we would not have the hash available to us.
858
    #[instrument(
859
        ret(level = Level::DEBUG),
860
        level = Level::ERROR,
861
        skip(self, instance_info),
862
    )]
863
19
    async fn inner_write(
864
19
        &self,
865
19
        instance_info: &InstanceInfo,
866
19
        digest: DigestInfo,
867
19
        digest_function: DigestHasherFunc,
868
19
        stream: WriteRequestStreamWrapper<impl Stream<Item = Result<WriteRequest, Status>> + Unpin>,
869
19
    ) -> Result<Response<WriteResponse>, Error> {
870
19
        async fn process_client_stream(
871
19
            mut stream: WriteRequestStreamWrapper<
872
19
                impl Stream<Item = Result<WriteRequest, Status>> + Unpin,
873
19
            >,
874
19
            tx: &mut DropCloserWriteHalf,
875
19
            outer_bytes_received: &Arc<AtomicU64>,
876
19
            expected_size: u64,
877
19
        ) -> Result<(), Error> {
878
            loop {
879
33
                let 
write_request27
= match stream.next().await {
880
                    // Code path for when client tries to gracefully close the stream.
881
                    // If this happens it means there's a problem with the data sent,
882
                    // because we always close the stream from our end before this point
883
                    // by counting the number of bytes sent from the client. If they send
884
                    // less than the amount they said they were going to send and then
885
                    // close the stream, we know there's a problem.
886
                    None => {
887
0
                        return Err(make_input_err!(
888
0
                            "Client closed stream before sending all data"
889
0
                        ));
890
                    }
891
                    // Code path for client stream error. Probably client disconnect.
892
5
                    Some(Err(err)) => return Err(err),
893
                    // Code path for received chunk of data.
894
27
                    Some(Ok(write_request)) => write_request,
895
                };
896
897
27
                if write_request.write_offset < 0 {
898
0
                    return Err(make_input_err!(
899
0
                        "Invalid negative write offset in write request: {}",
900
0
                        write_request.write_offset
901
0
                    ));
902
27
                }
903
27
                let write_offset = write_request.write_offset as u64;
904
905
                // If we get duplicate data because a client didn't know where
906
                // it left off from, then we can simply skip it.
907
27
                let 
data26
= if write_offset < tx.get_bytes_written() {
908
2
                    if (write_offset + write_request.data.len() as u64) < tx.get_bytes_written() {
909
0
                        if write_request.finish_write {
910
0
                            return Err(make_input_err!(
911
0
                                "Resumed stream finished at {} bytes when we already received {} bytes.",
912
0
                                write_offset + write_request.data.len() as u64,
913
0
                                tx.get_bytes_written()
914
0
                            ));
915
0
                        }
916
0
                        continue;
917
2
                    }
918
2
                    write_request.data.slice(
919
2
                        usize::try_from(tx.get_bytes_written() - write_offset)
920
2
                            .unwrap_or(usize::MAX)..,
921
                    )
922
                } else {
923
25
                    if write_offset != tx.get_bytes_written() {
924
1
                        return Err(make_input_err!(
925
1
                            "Received out of order data. Got {}, expected {}",
926
1
                            write_offset,
927
1
                            tx.get_bytes_written()
928
1
                        ));
929
24
                    }
930
24
                    write_request.data
931
                };
932
933
                // Do not process EOF or weird stuff will happen.
934
26
                if !data.is_empty() {
935
                    // We also need to process the possible EOF branch, so we can't early return.
936
20
                    if let Err(
mut err0
) = tx.send(data).await {
937
0
                        err.code = Code::Internal;
938
0
                        return Err(err);
939
20
                    }
940
20
                    outer_bytes_received.store(tx.get_bytes_written(), Ordering::Release);
941
6
                }
942
943
26
                if expected_size < tx.get_bytes_written() {
944
0
                    return Err(make_input_err!("Received more bytes than expected"));
945
26
                }
946
26
                if write_request.finish_write {
947
                    // Gracefully close our stream.
948
12
                    tx.send_eof()
949
12
                        .err_tip(|| "Failed to send EOF in ByteStream::write")
?0
;
950
12
                    return Ok(());
951
14
                }
952
                // Continue.
953
            }
954
            // Unreachable.
955
18
        }
956
957
        let uuid = stream
958
            .resource_info
959
            .uuid
960
            .as_ref()
961
0
            .ok_or_else(|| make_input_err!("UUID must be set if writing data"))?;
962
        let mut active_stream_guard =
963
            self.create_or_join_upload_stream(uuid, instance_info, digest, digest_function)?;
964
        let expected_size = stream.resource_info.expected_size as u64;
965
966
        let active_stream = active_stream_guard.stream_state.as_mut().unwrap();
967
        try_join!(
968
            process_client_stream(
969
                stream,
970
                &mut active_stream.tx,
971
                &active_stream_guard.bytes_received,
972
                expected_size
973
            ),
974
            (&mut active_stream.store_update_fut)
975
0
                .map_err(|err| { err.append("Error updating inner store") })
976
        )?;
977
978
        // Close our guard and consider the stream no longer active.
979
        active_stream_guard.graceful_finish();
980
981
        Ok(Response::new(WriteResponse {
982
            committed_size: expected_size.try_into().unwrap_or(i64::MAX),
983
        }))
984
18
    }
985
986
    /// Fast-path write that bypasses channel overhead for stores that support direct Bytes updates.
987
    /// This buffers all data in memory and calls `update_oneshot` directly.
988
5
    async fn inner_write_oneshot(
989
5
        &self,
990
5
        instance_info: &InstanceInfo,
991
5
        digest: DigestInfo,
992
5
        digest_function: DigestHasherFunc,
993
5
        mut stream: WriteRequestStreamWrapper<
994
5
            impl Stream<Item = Result<WriteRequest, Status>> + Unpin,
995
5
        >,
996
5
    ) -> Result<Response<WriteResponse>, Error> {
997
5
        let expected_size = stream.resource_info.expected_size as u64;
998
999
        // Pre-allocate buffer for expected size (capped at reasonable limit to prevent DoS)
1000
5
        let capacity =
1001
5
            usize::try_from(expected_size.min(64 * 1024 * 1024)).unwrap_or(64 * 1024 * 1024);
1002
5
        let mut buffer = BytesMut::with_capacity(capacity);
1003
5
        let mut bytes_received: u64 = 0;
1004
1005
        // Collect all data from client stream
1006
        loop {
1007
5
            let 
write_request4
= match stream.next().await {
1008
                None => {
1009
0
                    return Err(make_input_err!(
1010
0
                        "Client closed stream before sending all data"
1011
0
                    ));
1012
                }
1013
1
                Some(Err(err)) => return Err(err),
1014
4
                Some(Ok(write_request)) => write_request,
1015
            };
1016
1017
4
            if write_request.write_offset < 0 {
1018
1
                return Err(make_input_err!(
1019
1
                    "Invalid negative write offset in write request: {}",
1020
1
                    write_request.write_offset
1021
1
                ));
1022
3
            }
1023
3
            let write_offset = write_request.write_offset as u64;
1024
1025
            // Handle duplicate/resumed data
1026
3
            let data = if write_offset < bytes_received {
1027
0
                if (write_offset + write_request.data.len() as u64) < bytes_received {
1028
0
                    if write_request.finish_write {
1029
0
                        return Err(make_input_err!(
1030
0
                            "Resumed stream finished at {} bytes when we already received {} bytes.",
1031
0
                            write_offset + write_request.data.len() as u64,
1032
0
                            bytes_received
1033
0
                        ));
1034
0
                    }
1035
0
                    continue;
1036
0
                }
1037
0
                write_request
1038
0
                    .data
1039
0
                    .slice(usize::try_from(bytes_received - write_offset).unwrap_or(usize::MAX)..)
1040
            } else {
1041
3
                if write_offset != bytes_received {
1042
0
                    return Err(make_input_err!(
1043
0
                        "Received out of order data. Got {}, expected {}",
1044
0
                        write_offset,
1045
0
                        bytes_received
1046
0
                    ));
1047
3
                }
1048
3
                write_request.data
1049
            };
1050
1051
3
            if !data.is_empty() {
1052
2
                buffer.extend_from_slice(&data);
1053
2
                bytes_received += data.len() as u64;
1054
2
            
}1
1055
1056
3
            if expected_size < bytes_received {
1057
0
                return Err(make_input_err!("Received more bytes than expected"));
1058
3
            }
1059
1060
3
            if write_request.finish_write {
1061
3
                break;
1062
0
            }
1063
        }
1064
1065
        // Direct update without channel overhead
1066
3
        let store = instance_info.store.clone();
1067
3
        let store_update_context = make_ctx_for_hash_func(digest_function)
?0
;
1068
3
        store
1069
3
            .update_oneshot(digest, buffer.freeze())
1070
3
            .with_context(store_update_context)
1071
3
            .await
1072
3
            .err_tip(|| "Error in update_oneshot")
?0
;
1073
1074
        // Note: bytes_written_total is updated in the caller (bytestream_write) based on result
1075
1076
3
        Ok(Response::new(WriteResponse {
1077
3
            committed_size: expected_size.try_into().unwrap_or(i64::MAX),
1078
3
        }))
1079
5
    }
1080
1081
    /// Handle a compressed upload: stream compressed wire bytes through the
1082
    /// decoder into the store update stream, validate the decoded size, and
1083
    /// verify the decoded digest.
1084
7
    async fn inner_write_compressed(
1085
7
        &self,
1086
7
        instance: &InstanceInfo,
1087
7
        digest: DigestInfo,
1088
7
        digest_function: DigestHasherFunc,
1089
7
        wire_compressor: compressor::Value,
1090
7
        stream: WriteRequestStreamWrapper<impl Stream<Item = Result<WriteRequest, Status>> + Unpin>,
1091
7
    ) -> Result<Response<WriteResponse>, Error> {
1092
        // Register the upload in active_uploads so QueryWriteStatus can report
1093
        // compressed wire-byte progress while decoding. This mirrors what
1094
        // create_or_join_upload_stream does for uncompressed uploads.
1095
7
        let uuid_str = stream
1096
7
            .resource_info
1097
7
            .uuid
1098
7
            .as_deref()
1099
7
            .ok_or_else(|| 
make_input_err!0
("UUID must be set if writing compressed data"))
?0
;
1100
7
        let uuid_key = parse_uuid_to_key(uuid_str);
1101
7
        let (bytes_received, _guard) = instance.track_compressed_upload(uuid_key);
1102
1103
7
        let (compressed_tx, compressed_rx) = make_buf_channel_pair();
1104
7
        let (decompressed_tx, decompressed_rx) = make_buf_channel_pair();
1105
7
        let store = instance.store.clone();
1106
7
        let store_update_context = make_ctx_for_hash_func(digest_function)
?0
;
1107
7
        let store_update_fut = async move {
1108
7
            store
1109
7
                .update(
1110
7
                    digest,
1111
7
                    decompressed_rx,
1112
7
                    UploadSizeInfo::ExactSize(digest.size_bytes()),
1113
7
                )
1114
7
                .await
1115
7
                .map(|_| ())
1116
7
                .err_tip(|| "Failed to store decompressed data")
1117
7
        }
1118
7
        .with_context(store_update_context);
1119
        // Plain async future: decode progresses at the pace of the client
1120
        // upload and the store write without occupying a blocking-pool thread
1121
        // for the stream's lifetime.
1122
7
        let decode_fut = crate::wire_compression::stream_decode_compressed_upload(
1123
7
            compressed_rx,
1124
7
            wire_compressor,
1125
7
            digest,
1126
7
            digest_function,
1127
7
            decompressed_tx,
1128
        );
1129
7
        let client_stream_fut =
1130
7
            process_compressed_client_stream(stream, compressed_tx, &bytes_received);
1131
7
        let (client_stream_result, decode_result, store_update_result) =
1132
7
            tokio::join!(client_stream_fut, decode_fut, store_update_fut);
1133
1134
7
        if let Err(
err1
) = &client_stream_result
1135
1
            && err.code == Code::InvalidArgument
1136
        {
1137
1
            return Err(err.clone());
1138
6
        }
1139
6
        if let Err(
err1
) = &decode_result
1140
1
            && err.code == Code::InvalidArgument
1141
        {
1142
1
            return Err(err.clone());
1143
5
        }
1144
5
        let mut upload_error = store_update_result.err();
1145
5
        if let Err(
err0
) = decode_result {
1146
0
            upload_error = Some(match upload_error {
1147
0
                Some(existing) => existing.merge(err),
1148
0
                None => err,
1149
            });
1150
5
        }
1151
5
        if let Err(
err0
) = client_stream_result {
1152
0
            upload_error = Some(match upload_error {
1153
0
                Some(existing) => existing.merge(err),
1154
0
                None => err,
1155
            });
1156
5
        }
1157
5
        if let Some(
err0
) = upload_error {
1158
0
            return Err(err);
1159
5
        }
1160
1161
5
        let committed_size = i64::try_from(bytes_received.load(Ordering::Acquire))
1162
5
            .err_tip(|| "Compressed upload size was not convertible to i64")
?0
;
1163
5
        Ok(Response::new(WriteResponse { committed_size }))
1164
7
    }
1165
1166
    /// Read a blob from the store, compress it with the given wire compressor,
1167
    /// and return it as a stream of chunked `ReadResponse`s.
1168
5
    async fn inner_read_compressed(
1169
5
        &self,
1170
5
        instance: &InstanceInfo,
1171
5
        digest: DigestInfo,
1172
5
        wire_compressor: compressor::Value,
1173
5
        read_request: ReadRequest,
1174
5
    ) -> Result<ReadStream, Error> {
1175
        struct ReaderState {
1176
            max_bytes_per_stream: usize,
1177
            rx: DropCloserReadHalf,
1178
            maybe_get_part_result: Option<Result<(), Error>>,
1179
            maybe_encode_result: Option<Result<(), Error>>,
1180
            get_part_fut: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>,
1181
            encode_fut: Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>,
1182
        }
1183
1184
        impl ReaderState {
1185
4
            async fn finish(mut self, mut error: Option<Error>) -> Option<Error> {
1186
4
                let encode_result = if let Some(result) = self.maybe_encode_result.take() {
1187
4
                    result
1188
                } else {
1189
0
                    self.encode_fut.await
1190
                };
1191
4
                if let Err(
err0
) = encode_result {
1192
0
                    error = Some(match error {
1193
0
                        Some(existing) => err.merge(existing),
1194
0
                        None => err,
1195
                    });
1196
4
                }
1197
1198
4
                let get_part_result = if let Some(result) = self.maybe_get_part_result.take() {
1199
4
                    result
1200
                } else {
1201
0
                    self.get_part_fut.await
1202
                };
1203
4
                if let Err(
err0
) = get_part_result {
1204
0
                    error = Some(match error {
1205
0
                        Some(existing) => err.merge(existing),
1206
0
                        None => err,
1207
                    });
1208
4
                }
1209
1210
4
                error
1211
4
            }
1212
        }
1213
1214
5
        if read_request.read_limit != 0 {
1215
1
            return Err(make_input_err!(
1216
1
                "read_limit must be 0 when reading compressed blobs"
1217
1
            ));
1218
4
        }
1219
4
        if read_request.read_offset < 0 {
1220
0
            return Err(make_input_err!(
1221
0
                "read_offset must be non-negative when reading compressed blobs"
1222
0
            ));
1223
4
        }
1224
4
        let read_offset = u64::try_from(read_request.read_offset)
1225
4
            .err_tip(|| "Could not convert read_offset to u64")
?0
;
1226
1227
4
        let (raw_tx, raw_rx) = make_buf_channel_pair();
1228
4
        let (compressed_tx, compressed_rx) = make_buf_channel_pair();
1229
1230
4
        let store = instance.store.clone();
1231
4
        let get_part_fut = Box::pin(async move {
1232
4
            store
1233
4
                .get_part(digest, raw_tx, read_offset, None)
1234
4
                .await
1235
4
                .err_tip(|| "Failed to read blob for wire compression")
1236
4
        });
1237
        // The encode runs as a plain async future: it must not occupy a
1238
        // blocking-pool thread for the stream's lifetime, because it only
1239
        // progresses at the client's drain rate. Dropping the returned
1240
        // stream drops this future, which tears the encode down exactly
1241
        // like the previous task-abort-on-drop did.
1242
4
        let encode_fut = Box::pin(crate::wire_compression::stream_encode_compressed_download(
1243
4
            raw_rx,
1244
4
            wire_compressor,
1245
            crate::wire_compression::ZSTD_COMPRESSION_LEVEL,
1246
4
            compressed_tx,
1247
        ));
1248
1249
4
        let state = Some(ReaderState {
1250
4
            max_bytes_per_stream: instance.max_bytes_per_stream,
1251
4
            rx: compressed_rx,
1252
4
            maybe_get_part_result: None,
1253
4
            maybe_encode_result: None,
1254
4
            get_part_fut,
1255
4
            encode_fut,
1256
4
        });
1257
1258
67
        Ok(
Box::pin4
(
unfold4
(
state4
, move |state| async {
1259
            enum ReadStep {
1260
                Response(ReadResponse),
1261
                Finish(Option<Error>),
1262
            }
1263
1264
67
            let mut state = state
?0
;
1265
67
            let step = {
1266
67
                let mut response = ReadResponse::default();
1267
67
                let consume_fut = state.rx.consume(Some(state.max_bytes_per_stream));
1268
67
                tokio::pin!(consume_fut);
1269
                loop {
1270
75
                    tokio::select! {
1271
75
                        
read_result67
= &mut consume_fut => {
1272
67
                            match read_result {
1273
67
                                Ok(bytes) => {
1274
67
                                    if bytes.is_empty() {
1275
4
                                        break ReadStep::Finish(None);
1276
63
                                    }
1277
63
                                    if bytes.len() > state.max_bytes_per_stream {
1278
0
                                        let err = make_err!(Code::Internal, "Returned compressed size was larger than read size");
1279
0
                                        break ReadStep::Finish(Some(err));
1280
63
                                    }
1281
63
                                    response.data = bytes;
1282
63
                                    trace!(
1283
63
                                        response.data = format!("<redacted len({})>", response.data.len())
1284
                                    );
1285
63
                                    break ReadStep::Response(response);
1286
                                }
1287
0
                                Err(e) => {
1288
0
                                    break ReadStep::Finish(Some(e));
1289
                                }
1290
                            }
1291
                        }
1292
75
                        
get_part_result4
= &mut state.get_part_fut, if state.maybe_get_part_result.is_none() => {
1293
4
                            state.maybe_get_part_result = Some(get_part_result);
1294
4
                        }
1295
75
                        
encode_result4
= &mut state.encode_fut, if state.maybe_encode_result.is_none() => {
1296
4
                            state.maybe_encode_result = Some(encode_result);
1297
4
                        }
1298
                    }
1299
                }
1300
            };
1301
1302
67
            match step {
1303
63
                ReadStep::Response(response) => Some((Ok(response), Some(state))),
1304
4
                ReadStep::Finish(error) => {
1305
4
                    state.finish(error).await.map(|mut err| 
{0
1306
0
                        if err.code == Code::NotFound {
1307
0
                            // Trim common NotFound details to match the identity read path.
1308
0
                            err.messages.truncate(1);
1309
0
                        }
1310
0
                        error!(response = ?err);
1311
0
                        (Err(err.into()), None)
1312
0
                    })
1313
                }
1314
            }
1315
134
        })))
1316
5
    }
1317
1318
4
    async fn inner_query_write_status(
1319
4
        &self,
1320
4
        query_request: &QueryWriteStatusRequest,
1321
4
    ) -> Result<Response<QueryWriteStatusResponse>, Error> {
1322
4
        let mut resource_info = ResourceInfo::new(&query_request.resource_name, true)
?0
;
1323
1324
4
        let instance = self
1325
4
            .instance_infos
1326
4
            .get(resource_info.instance_name.as_ref())
1327
4
            .err_tip(|| 
{0
1328
0
                format!(
1329
                    "'instance_name' not configured for '{}'",
1330
0
                    &resource_info.instance_name
1331
                )
1332
0
            })?;
1333
4
        let store_clone = instance.store.clone();
1334
1335
4
        let digest = DigestInfo::try_new(resource_info.hash.as_ref(), resource_info.expected_size)
?0
;
1336
1337
        // If we are a GrpcStore we shortcut here, as this is a special store.
1338
4
        if let Some(
grpc_store0
) = store_clone.downcast_ref::<GrpcStore>(Some(digest.into())) {
1339
0
            return grpc_store
1340
0
                .query_write_status(Request::new(query_request.clone()))
1341
0
                .await;
1342
4
        }
1343
1344
4
        let uuid_str = resource_info
1345
4
            .uuid
1346
4
            .take()
1347
4
            .ok_or_else(|| 
make_input_err!0
("UUID must be set if querying write status"))
?0
;
1348
4
        let uuid_key = parse_uuid_to_key(&uuid_str);
1349
1350
        {
1351
4
            let active_uploads = instance.active_uploads.lock();
1352
4
            if let Some((
received_bytes2
,
_maybe_idle_stream2
)) = active_uploads.get(&uuid_key) {
1353
2
                return Ok(Response::new(QueryWriteStatusResponse {
1354
2
                    committed_size: received_bytes
1355
2
                        .load(Ordering::Acquire)
1356
2
                        .try_into()
1357
2
                        .unwrap_or(i64::MAX),
1358
2
                    // If we are in the active_uploads map, but the value is None,
1359
2
                    // it means the stream is not complete.
1360
2
                    complete: false,
1361
2
                }));
1362
2
            }
1363
        }
1364
1365
2
        let has_fut = store_clone.has(digest);
1366
2
        let Some(
item_size1
) = has_fut.await.err_tip(|| "Failed to call .has() on store")
?0
else {
1367
            // We lie here and say that the stream needs to start over, even though
1368
            // it was never started. This can happen when the client disconnects
1369
            // before sending the first payload, but the client thinks it did send
1370
            // the payload.
1371
1
            return Ok(Response::new(QueryWriteStatusResponse {
1372
1
                committed_size: 0,
1373
1
                complete: false,
1374
1
            }));
1375
        };
1376
1
        Ok(Response::new(QueryWriteStatusResponse {
1377
1
            committed_size: item_size.try_into().unwrap_or(i64::MAX),
1378
1
            complete: true,
1379
1
        }))
1380
4
    }
1381
}
1382
1383
#[tonic::async_trait]
1384
impl ByteStream for ByteStreamServer {
1385
    type ReadStream = ReadStream;
1386
1387
    #[instrument(
1388
        err,
1389
        level = Level::ERROR,
1390
        skip_all,
1391
        fields(request = ?grpc_request.get_ref())
1392
    )]
1393
    async fn read(
1394
        &self,
1395
        grpc_request: Request<ReadRequest>,
1396
    ) -> Result<Response<Self::ReadStream>, Status> {
1397
        let start_time = Instant::now();
1398
1399
        let read_request = grpc_request.into_inner();
1400
        let resource_name = read_request.resource_name.clone();
1401
        let resource_info = ResourceInfo::new(&resource_name, false)?;
1402
        let instance_name = resource_info.instance_name.as_ref();
1403
        let expected_size = resource_info.expected_size as u64;
1404
        let instance = self
1405
            .instance_infos
1406
            .get(instance_name)
1407
0
            .err_tip(|| format!("'instance_name' not configured for '{instance_name}'"))?;
1408
1409
        trace!(
1410
            resource_name,
1411
            instance_name, expected_size, "Starting bytestream request"
1412
        );
1413
1414
        // Track read request
1415
        instance
1416
            .metrics
1417
            .read_requests_total
1418
            .fetch_add(1, Ordering::Relaxed);
1419
1420
        let store = instance.store.clone();
1421
1422
        let digest = DigestInfo::try_new(resource_info.hash.as_ref(), resource_info.expected_size)?;
1423
1424
        // If we are a GrpcStore we shortcut here, as this is a special store.
1425
        if let Some(grpc_store) = store.downcast_ref::<GrpcStore>(Some(digest.into())) {
1426
            let stream = Box::pin(grpc_store.read(Request::new(read_request)).await?);
1427
            return Ok(Response::new(stream));
1428
        }
1429
1430
        let digest_function = resource_info.digest_function.as_deref().map_or_else(
1431
10
            || Ok(default_digest_hasher_func()),
1432
            DigestHasherFunc::try_from,
1433
        )?;
1434
1435
        // Determine if the client requested wire-compressed data via compressed-blobs URI.
1436
        let wire_compressor = crate::wire_compression::resolve_wire_compressor(
1437
            resource_info.compressor.as_deref(),
1438
            instance.remote_cache_compression_enabled,
1439
        )?;
1440
1441
        let resp = if wire_compressor == compressor::Value::Identity {
1442
            // Uncompressed path — use the existing streaming read.
1443
            self.inner_read(instance, digest, read_request)
1444
                .instrument(error_span!("bytestream_read"))
1445
                .with_context(
1446
                    make_ctx_for_hash_func(digest_function)
1447
                        .err_tip(|| "In BytestreamServer::read")?,
1448
                )
1449
                .await
1450
                .err_tip(|| "In ByteStreamServer::read")
1451
8
                .map(|stream| -> Response<Self::ReadStream> { Response::new(Box::pin(stream)) })
1452
        } else {
1453
            // Compressed path — stream the requested raw range through zstd.
1454
            self.inner_read_compressed(instance, digest, wire_compressor, read_request)
1455
                .instrument(error_span!("bytestream_read_compressed"))
1456
                .with_context(
1457
                    make_ctx_for_hash_func(digest_function)
1458
                        .err_tip(|| "In BytestreamServer::read_compressed")?,
1459
                )
1460
                .await
1461
                .err_tip(|| "In ByteStreamServer::read_compressed")
1462
4
                .map(|stream| -> Response<Self::ReadStream> { Response::new(Box::pin(stream)) })
1463
        };
1464
1465
        // Track metrics based on result
1466
        let elapsed = start_time.elapsed();
1467
        #[allow(clippy::cast_possible_truncation)]
1468
        let elapsed_ns = elapsed.as_nanos() as u64;
1469
        instance
1470
            .metrics
1471
            .read_duration_ns
1472
            .fetch_add(elapsed_ns, Ordering::Relaxed);
1473
1474
        trace!(
1475
            ?elapsed,
1476
            resource_name, instance_name, expected_size, "Completed bytestream request"
1477
        );
1478
1479
        match &resp {
1480
            Ok(_) => {
1481
                instance
1482
                    .metrics
1483
                    .read_requests_success
1484
                    .fetch_add(1, Ordering::Relaxed);
1485
                instance
1486
                    .metrics
1487
                    .bytes_read_total
1488
                    .fetch_add(expected_size, Ordering::Relaxed);
1489
                debug!(return = "Ok(<stream>)");
1490
            }
1491
            Err(_) => {
1492
                instance
1493
                    .metrics
1494
                    .read_requests_failure
1495
                    .fetch_add(1, Ordering::Relaxed);
1496
            }
1497
        }
1498
1499
        resp.map_err(Into::into)
1500
    }
1501
1502
    #[instrument(
1503
        err,
1504
        level = Level::ERROR,
1505
        skip_all,
1506
        fields(request = ?grpc_request.get_ref())
1507
    )]
1508
    async fn write(
1509
        &self,
1510
        grpc_request: Request<Streaming<WriteRequest>>,
1511
    ) -> Result<Response<WriteResponse>, Status> {
1512
        let start_time = Instant::now();
1513
1514
        let request = grpc_request.into_inner();
1515
        let stream = WriteRequestStreamWrapper::from(request)
1516
            .await
1517
            .err_tip(|| "Could not unwrap first stream message")
1518
            .map_err(Into::<Status>::into)?;
1519
1520
        let instance_name = stream.resource_info.instance_name.as_ref();
1521
        let expected_size = stream.resource_info.expected_size as u64;
1522
        let instance = self
1523
            .instance_infos
1524
            .get(instance_name)
1525
0
            .err_tip(|| format!("'instance_name' not configured for '{instance_name}'"))?;
1526
1527
        // Track write request
1528
        instance
1529
            .metrics
1530
            .write_requests_total
1531
            .fetch_add(1, Ordering::Relaxed);
1532
1533
        let store = instance.store.clone();
1534
1535
        let digest = DigestInfo::try_new(
1536
            &stream.resource_info.hash,
1537
            stream.resource_info.expected_size,
1538
        )
1539
        .err_tip(|| "Invalid digest input in ByteStream::write")?;
1540
1541
        // If we are a GrpcStore we shortcut here, as this is a special store.
1542
        if let Some(grpc_store) = store.downcast_ref::<GrpcStore>(Some(digest.into())) {
1543
            let resp = grpc_store.write(stream).await.map_err(Into::into);
1544
            return resp;
1545
        }
1546
1547
        let default_digest_function = default_digest_hasher_func();
1548
        let digest_function = match stream.resource_info.digest_function.as_deref() {
1549
            Some(value) => {
1550
                let digest_function = DigestHasherFunc::try_from(value)?;
1551
                if digest_function != default_digest_function
1552
                    && self
1553
                        .reported_digest_function_mismatches
1554
                        .lock()
1555
                        .insert((digest_function, default_digest_function))
1556
                {
1557
                    warn!(
1558
                        client_digest_function = %digest_function,
1559
                        server_default_digest_function = %default_digest_function,
1560
                        "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"
1561
                    );
1562
                }
1563
                digest_function
1564
            }
1565
            None => default_digest_function,
1566
        };
1567
1568
        // Determine if the client is sending wire-compressed data via compressed-blobs URI.
1569
        let wire_compressor = crate::wire_compression::resolve_wire_compressor(
1570
            stream.resource_info.compressor.as_deref(),
1571
            instance.remote_cache_compression_enabled,
1572
        )?;
1573
1574
        // For compressed uploads, stream compressed wire bytes through the
1575
        // decoder and store the resulting raw bytes.
1576
        if wire_compressor != compressor::Value::Identity {
1577
            let result = self
1578
                .inner_write_compressed(instance, digest, digest_function, wire_compressor, stream)
1579
                .instrument(error_span!("bytestream_write_compressed"))
1580
                .await
1581
                .err_tip(|| "In ByteStreamServer::write_compressed");
1582
1583
            // Track metrics based on result
1584
            #[allow(clippy::cast_possible_truncation)]
1585
            let elapsed_ns = start_time.elapsed().as_nanos() as u64;
1586
            instance
1587
                .metrics
1588
                .write_duration_ns
1589
                .fetch_add(elapsed_ns, Ordering::Relaxed);
1590
1591
            match &result {
1592
                Ok(_) => {
1593
                    instance
1594
                        .metrics
1595
                        .write_requests_success
1596
                        .fetch_add(1, Ordering::Relaxed);
1597
                    instance
1598
                        .metrics
1599
                        .bytes_written_total
1600
                        .fetch_add(expected_size, Ordering::Relaxed);
1601
                }
1602
                Err(_) => {
1603
                    instance
1604
                        .metrics
1605
                        .write_requests_failure
1606
                        .fetch_add(1, Ordering::Relaxed);
1607
                }
1608
            }
1609
1610
            return result.map_err(Into::into);
1611
        }
1612
1613
        // Check if store supports direct oneshot updates (bypasses channel overhead).
1614
        // Use fast-path only when:
1615
        // 1. Store supports oneshot optimization
1616
        // 2. UUID is provided
1617
        // 3. Size is under 64MB (memory safety)
1618
        // 4. This is a NEW upload (UUID not already in active_uploads)
1619
        // 5. The first message has finish_write=true (single-shot upload)
1620
        //
1621
        // The oneshot path cannot be used for multi-message streams because:
1622
        // - QueryWriteStatus won't work (no progress tracking)
1623
        // - Resumed streams won't work (no partial progress)
1624
        let use_oneshot = if store.optimized_for(StoreOptimizations::SubscribesToUpdateOneshot)
1625
            && expected_size <= 64 * 1024 * 1024
1626
            && let Some(ref resource_uuid) = stream.resource_info.uuid
1627
        {
1628
            // Check if first message completes the upload (single-shot)
1629
            let is_single_shot = stream.is_first_msg_complete();
1630
1631
            if is_single_shot {
1632
                let uuid_key = parse_uuid_to_key(resource_uuid);
1633
                // Only use oneshot if this UUID is not already being tracked
1634
                !instance.active_uploads.lock().contains_key(&uuid_key)
1635
            } else {
1636
                false
1637
            }
1638
        } else {
1639
            false
1640
        };
1641
1642
        let result = if use_oneshot {
1643
            self.inner_write_oneshot(instance, digest, digest_function, stream)
1644
                .instrument(error_span!("bytestream_write_oneshot"))
1645
                .await
1646
                .err_tip(|| "In ByteStreamServer::write (oneshot)")
1647
        } else {
1648
            self.inner_write(instance, digest, digest_function, stream)
1649
                .instrument(error_span!("bytestream_write"))
1650
                .await
1651
                .err_tip(|| "In ByteStreamServer::write")
1652
        };
1653
1654
        // Track metrics based on result
1655
        #[allow(clippy::cast_possible_truncation)]
1656
        let elapsed_ns = start_time.elapsed().as_nanos() as u64;
1657
        instance
1658
            .metrics
1659
            .write_duration_ns
1660
            .fetch_add(elapsed_ns, Ordering::Relaxed);
1661
1662
        match &result {
1663
            Ok(_) => {
1664
                instance
1665
                    .metrics
1666
                    .write_requests_success
1667
                    .fetch_add(1, Ordering::Relaxed);
1668
                instance
1669
                    .metrics
1670
                    .bytes_written_total
1671
                    .fetch_add(expected_size, Ordering::Relaxed);
1672
            }
1673
            Err(_) => {
1674
                instance
1675
                    .metrics
1676
                    .write_requests_failure
1677
                    .fetch_add(1, Ordering::Relaxed);
1678
            }
1679
        }
1680
1681
        result.map_err(Into::into)
1682
    }
1683
1684
    #[instrument(
1685
        err,
1686
        ret(level = Level::INFO),
1687
        level = Level::ERROR,
1688
        skip_all,
1689
        fields(request = ?grpc_request.get_ref())
1690
    )]
1691
    async fn query_write_status(
1692
        &self,
1693
        grpc_request: Request<QueryWriteStatusRequest>,
1694
    ) -> Result<Response<QueryWriteStatusResponse>, Status> {
1695
        let request = grpc_request.into_inner();
1696
1697
        // Track query_write_status request - we need to parse the resource name to get the instance
1698
        if let Ok(resource_info) = ResourceInfo::new(&request.resource_name, true)
1699
            && let Some(instance) = self
1700
                .instance_infos
1701
                .get(resource_info.instance_name.as_ref())
1702
        {
1703
            instance
1704
                .metrics
1705
                .query_write_status_total
1706
                .fetch_add(1, Ordering::Relaxed);
1707
        }
1708
1709
        self.inner_query_write_status(&request)
1710
            .await
1711
            .err_tip(|| "Failed on query_write_status() command")
1712
            .map_err(Into::into)
1713
    }
1714
}