Coverage Report

Created: 2026-07-16 22:05

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