Coverage Report

Created: 2026-11-05 16:55

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