/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 | | } |