/build/source/nativelink-util/src/proto_stream_utils.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::fmt::Debug; |
16 | | use core::mem; |
17 | | use core::pin::Pin; |
18 | | use core::task::{Context, Poll}; |
19 | | use std::borrow::Cow; |
20 | | use std::sync::Arc; |
21 | | |
22 | | use futures::{Stream, StreamExt}; |
23 | | use nativelink_error::{Error, ResultExt, error_if, make_input_err}; |
24 | | use nativelink_proto::google::bytestream::{ReadResponse, WriteRequest}; |
25 | | use parking_lot::Mutex; |
26 | | use tonic::{Status, Streaming}; |
27 | | |
28 | | use crate::resource_info::ResourceInfo; |
29 | | |
30 | | pub struct WriteRequestStreamWrapper<T> { |
31 | | pub resource_info: ResourceInfo<'static>, |
32 | | pub bytes_received: usize, |
33 | | stream: T, |
34 | | first_msg: Option<WriteRequest>, |
35 | | pub write_finished: bool, |
36 | | } |
37 | | |
38 | | impl<T> Debug for WriteRequestStreamWrapper<T> { |
39 | 31 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
40 | 31 | f.debug_struct("WriteRequestStreamWrapper") |
41 | 31 | .field("resource_info", &self.resource_info) |
42 | 31 | .field("bytes_received", &self.bytes_received) |
43 | 31 | .field("first_msg", &self.first_msg) |
44 | 31 | .field("write_finished", &self.write_finished) |
45 | 31 | .finish() |
46 | 31 | } |
47 | | } |
48 | | |
49 | | impl<T, E> WriteRequestStreamWrapper<T> |
50 | | where |
51 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin, |
52 | | E: Into<Error>, |
53 | | { |
54 | 73 | pub async fn from(mut stream: T) -> Result<Self, Error> { |
55 | 73 | let first_msg69 = stream |
56 | 73 | .next() |
57 | 73 | .await |
58 | 73 | .err_tip(|| "Error receiving first message in stream")?0 |
59 | 73 | .err_tip(|| "Expected WriteRequest struct in stream (from)")?4 ; |
60 | | |
61 | 69 | let resource_info = ResourceInfo::new(&first_msg.resource_name, true) |
62 | 69 | .err_tip(|| {0 |
63 | 0 | format!( |
64 | | "Could not extract resource info from first message of stream: {}", |
65 | | first_msg.resource_name |
66 | | ) |
67 | 0 | })? |
68 | 69 | .to_owned(); |
69 | | |
70 | 69 | Ok(Self { |
71 | 69 | resource_info, |
72 | 69 | bytes_received: 0, |
73 | 69 | stream, |
74 | 69 | first_msg: Some(first_msg), |
75 | 69 | write_finished: false, |
76 | 69 | }) |
77 | 73 | } |
78 | | |
79 | 68 | pub async fn next(&mut self) -> Option<Result<WriteRequest, Error>> { |
80 | 179 | futures::future::poll_fn68 (|cx| Pin::new(&mut *self).poll_next(cx)).await68 |
81 | 67 | } |
82 | | |
83 | 16 | pub const fn is_first_msg(&self) -> bool { |
84 | 16 | self.first_msg.is_some() |
85 | 16 | } |
86 | | |
87 | | /// Returns whether the first message has `finish_write` set to true. |
88 | | /// This indicates a single-shot upload where all data is in one message. |
89 | 24 | pub fn is_first_msg_complete(&self) -> bool { |
90 | 24 | self.first_msg.as_ref().is_some_and(|msg| msg.finish_write) |
91 | 24 | } |
92 | | |
93 | | /// Returns the `write_offset` of the first message, if it has not been |
94 | | /// consumed yet. A non-zero offset means the client is resuming an upload. |
95 | 72 | pub fn first_msg_write_offset(&self) -> Option<i64> { |
96 | 72 | self.first_msg.as_ref().map(|msg| msg.write_offset) |
97 | 72 | } |
98 | | |
99 | 131 | fn enforce_wire_size_matches_digest_size(&self) -> bool { |
100 | 32 | matches!( |
101 | 131 | self.resource_info.compressor.as_deref(), |
102 | 33 | None | Some("identity") |
103 | | ) |
104 | 131 | } |
105 | | } |
106 | | |
107 | | impl<T, E> Stream for WriteRequestStreamWrapper<T> |
108 | | where |
109 | | E: Into<Error>, |
110 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin, |
111 | | { |
112 | | type Item = Result<WriteRequest, Error>; |
113 | | |
114 | 253 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
115 | | // If the stream said that the previous message was the last one, then |
116 | | // return a stream EOF (i.e. None). |
117 | 253 | if self.write_finished { |
118 | 17 | if self.enforce_wire_size_matches_digest_size() { |
119 | 0 | error_if!( |
120 | 14 | self.bytes_received != self.resource_info.expected_size, |
121 | | "Did not send enough data. Expected {}, but so far received {}", |
122 | 0 | self.resource_info.expected_size, |
123 | 0 | self.bytes_received |
124 | | ); |
125 | 3 | } |
126 | 17 | return Poll::Ready(None); |
127 | 236 | } |
128 | | |
129 | | // Gets the next message, this is either the cached first or a |
130 | | // subsequent message from the wrapped Stream. |
131 | 236 | let maybe_message121 = if let Some(first_msg63 ) = self.first_msg.take() { |
132 | 63 | Ok(first_msg) |
133 | | } else { |
134 | 173 | match Pin::new(&mut self.stream).poll_next(cx) { |
135 | 115 | Poll::Pending => return Poll::Pending, |
136 | 52 | Poll::Ready(Some(maybe_message)) => maybe_message |
137 | 52 | .err_tip(|| format!1 ("Stream error at byte {}", self.bytes_received1 )), |
138 | 6 | Poll::Ready(None) => Err(make_input_err!( |
139 | 6 | "Expected WriteRequest struct in stream (got None)" |
140 | 6 | )), |
141 | | } |
142 | | }; |
143 | | |
144 | | // If we successfully got a message, update our internal state with the |
145 | | // message meta data. |
146 | 121 | Poll::Ready(Some(maybe_message.and_then(|message| {114 |
147 | 114 | self.write_finished = message.finish_write; |
148 | 114 | self.bytes_received += message.data.len(); |
149 | | |
150 | | // Check that we haven't read past the expected end. |
151 | 114 | if self.enforce_wire_size_matches_digest_size() |
152 | 85 | && self.bytes_received > self.resource_info.expected_size |
153 | | { |
154 | 2 | Err(make_input_err!( |
155 | 2 | "Sent too much data. Expected {}, but so far received {}", |
156 | 2 | self.resource_info.expected_size, |
157 | 2 | self.bytes_received |
158 | 2 | )) |
159 | | } else { |
160 | 112 | Ok(message) |
161 | | } |
162 | 114 | }))) |
163 | 253 | } |
164 | | } |
165 | | |
166 | | /// Represents the state of the first response in a `FirstStream`. |
167 | | #[derive(Debug)] |
168 | | pub enum FirstResponseState { |
169 | | /// Contains an optional first response that hasn't been consumed yet. |
170 | | /// A `None` value indicates the first response was EOF. |
171 | | Unused(Option<ReadResponse>), |
172 | | /// Indicates the first response has been consumed and future reads should |
173 | | /// come from the underlying stream. |
174 | | Used, |
175 | | } |
176 | | |
177 | | /// This provides a buffer for the first response from GrpcStore.read in order |
178 | | /// to allow the first read to occur within the retry loop. That means that if |
179 | | /// the connection establishes fine, but reading the first byte of the file |
180 | | /// fails we have the ability to retry before returning to the caller. |
181 | | #[derive(Debug)] |
182 | | pub struct FirstStream { |
183 | | /// The current state of the first response. When in the `Unused` state, |
184 | | /// contains an optional response which could be `None` or an EOF. |
185 | | /// Once consumed, transitions to the `Used` state. |
186 | | state: FirstResponseState, |
187 | | /// The stream to get responses from after the first response is consumed. |
188 | | stream: Streaming<ReadResponse>, |
189 | | } |
190 | | |
191 | | impl FirstStream { |
192 | | /// Creates a new `FirstStream` with the given first response and underlying |
193 | | /// stream. |
194 | 0 | pub const fn new( |
195 | 0 | first_response: Option<ReadResponse>, |
196 | 0 | stream: Streaming<ReadResponse>, |
197 | 0 | ) -> Self { |
198 | 0 | Self { |
199 | 0 | state: FirstResponseState::Unused(first_response), |
200 | 0 | stream, |
201 | 0 | } |
202 | 0 | } |
203 | | } |
204 | | |
205 | | impl Stream for FirstStream { |
206 | | type Item = Result<ReadResponse, Status>; |
207 | | |
208 | 111 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
209 | 111 | match mem::replace(&mut self.state, FirstResponseState::Used) { |
210 | 14 | FirstResponseState::Unused(first_response) => Poll::Ready(first_response.map(Ok)), |
211 | 97 | FirstResponseState::Used => Pin::new(&mut self.stream).poll_next(cx), |
212 | | } |
213 | 111 | } |
214 | | } |
215 | | |
216 | | /// Largest `data` payload in a single `ByteStream` `WriteRequest`, kept under |
217 | | /// tonic's 4MiB default `max_decoding_message_size` with room to spare. |
218 | | pub const MAX_WRITE_REQUEST_DATA_BYTES: usize = 2 * 1024 * 1024; |
219 | | |
220 | | /// This structure wraps all of the information required to perform a write |
221 | | /// request on the `GrpcStore`. It stores the last message retrieved which allows |
222 | | /// the write to resume since the UUID allows upload resume at the server. |
223 | | #[derive(Debug)] |
224 | | pub struct WriteState<T, E> |
225 | | where |
226 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
227 | | E: Into<Error> + 'static, |
228 | | { |
229 | | instance_name: String, |
230 | | read_stream_error: Option<Error>, |
231 | | read_stream: WriteRequestStreamWrapper<T>, |
232 | | // Tonic doesn't appear to report an error until it has taken two messages, |
233 | | // therefore we are required to buffer the last two messages. |
234 | | cached_messages: [Option<WriteRequest>; 2], |
235 | | // When resuming after an error, the previous messages are cloned into this |
236 | | // queue upfront to allow them to be served back. |
237 | | resume_queue: [Option<WriteRequest>; 2], |
238 | | // An optimisation to avoid having to manage resume_queue when it's empty. |
239 | | is_resumed: bool, |
240 | | // Remainder of an incoming frame too large to forward in one message. |
241 | | // Drained before the next frame is read so ordering is preserved. |
242 | | pending: Option<WriteRequest>, |
243 | | // When false, a partially-consumed stream never reports `can_resume()`: |
244 | | // uploads whose server-side protocol cannot accept a replay from a |
245 | | // nonzero offset (REAPI compressed-blobs writes) must fail fast instead |
246 | | // of burning retries on guaranteed-rejected resumes. A stream that has |
247 | | // not yet been consumed can always be retried from the start. |
248 | | resumable: bool, |
249 | | } |
250 | | |
251 | | impl<T, E> WriteState<T, E> |
252 | | where |
253 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
254 | | E: Into<Error> + 'static, |
255 | | { |
256 | 19 | pub const fn new(instance_name: String, read_stream: WriteRequestStreamWrapper<T>) -> Self { |
257 | 19 | Self { |
258 | 19 | instance_name, |
259 | 19 | read_stream_error: None, |
260 | 19 | read_stream, |
261 | 19 | cached_messages: [None, None], |
262 | 19 | resume_queue: [None, None], |
263 | 19 | is_resumed: false, |
264 | 19 | pending: None, |
265 | 19 | resumable: true, |
266 | 19 | } |
267 | 19 | } |
268 | | |
269 | | /// Caps the payload of an outgoing message, stashing anything over the |
270 | | /// limit to be sent as the following message. |
271 | | /// |
272 | | /// A caller can hand us a whole blob in one frame, and forwarding that |
273 | | /// verbatim produces a message the receiver rejects at its 4MiB default. |
274 | | /// Only the final piece carries `finish_write`, and each piece advances |
275 | | /// `write_offset` by what came before it. |
276 | 55 | fn split_oversized(&mut self, mut message: WriteRequest) -> WriteRequest { |
277 | 55 | if message.data.len() <= MAX_WRITE_REQUEST_DATA_BYTES { |
278 | 53 | return message; |
279 | 2 | } |
280 | 2 | let rest = message.data.split_off(MAX_WRITE_REQUEST_DATA_BYTES); |
281 | 2 | let sent_len = i64::try_from(message.data.len()).unwrap_or(i64::MAX); |
282 | 2 | self.pending = Some(WriteRequest { |
283 | 2 | resource_name: message.resource_name.clone(), |
284 | 2 | write_offset: message.write_offset.saturating_add(sent_len), |
285 | 2 | finish_write: message.finish_write, |
286 | 2 | data: rest, |
287 | 2 | }); |
288 | 2 | message.finish_write = false; |
289 | 2 | message |
290 | 55 | } |
291 | | |
292 | | /// Marks this write as non-resumable: once the stream has been partially |
293 | | /// consumed, `can_resume()` reports false so the caller fails fast with |
294 | | /// the original error instead of replaying messages the server-side |
295 | | /// protocol is guaranteed to reject. Retrying an unconsumed stream from |
296 | | /// the start remains allowed. |
297 | 17 | pub const fn set_non_resumable(&mut self) { |
298 | 17 | self.resumable = false; |
299 | 17 | } |
300 | | |
301 | 55 | pub(crate) const fn is_resumable(&self) -> bool { |
302 | 55 | self.resumable |
303 | 55 | } |
304 | | |
305 | 6 | fn push_message(&mut self, message: WriteRequest) { |
306 | 6 | self.cached_messages.swap(0, 1); |
307 | 6 | self.cached_messages[0] = Some(message); |
308 | 6 | } |
309 | | |
310 | 76 | const fn resumed_message(&mut self) -> Option<WriteRequest> { |
311 | 76 | if self.is_resumed { |
312 | | // The resume_queue is a circular buffer, that we have to shift, |
313 | | // since its only got two elements its a trivial swap. |
314 | 0 | self.resume_queue.swap(0, 1); |
315 | 0 | let message = self.resume_queue[0].take(); |
316 | 0 | if message.is_none() { |
317 | 0 | self.is_resumed = false; |
318 | 0 | } |
319 | 0 | message |
320 | | } else { |
321 | 76 | None |
322 | | } |
323 | 76 | } |
324 | | |
325 | 16 | pub const fn can_resume(&self) -> bool { |
326 | 16 | self.read_stream_error.is_none() |
327 | 16 | && ((self.resumable && self.cached_messages[0]0 .is_some0 ()) |
328 | 16 | || self.read_stream.is_first_msg()) |
329 | 16 | } |
330 | | |
331 | 0 | pub fn resume(&mut self) { |
332 | 0 | self.resume_queue.clone_from(&self.cached_messages); |
333 | 0 | self.is_resumed = true; |
334 | 0 | } |
335 | | |
336 | 19 | pub const fn take_read_stream_error(&mut self) -> Option<Error> { |
337 | 19 | self.read_stream_error.take() |
338 | 19 | } |
339 | | } |
340 | | |
341 | | /// A wrapper around `WriteState` to allow it to be reclaimed from the underlying |
342 | | /// write call in the case of failure. |
343 | | #[derive(Debug)] |
344 | | pub struct WriteStateWrapper<T, E> |
345 | | where |
346 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
347 | | E: Into<Error> + 'static, |
348 | | { |
349 | | shared_state: Arc<Mutex<WriteState<T, E>>>, |
350 | | } |
351 | | |
352 | | impl<T, E> WriteStateWrapper<T, E> |
353 | | where |
354 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
355 | | E: Into<Error> + 'static, |
356 | | { |
357 | 19 | pub const fn new(shared_state: Arc<Mutex<WriteState<T, E>>>) -> Self { |
358 | 19 | Self { shared_state } |
359 | 19 | } |
360 | | } |
361 | | |
362 | | impl<T, E> Stream for WriteStateWrapper<T, E> |
363 | | where |
364 | | T: Stream<Item = Result<WriteRequest, E>> + Unpin + Send + 'static, |
365 | | E: Into<Error> + 'static, |
366 | | { |
367 | | type Item = WriteRequest; |
368 | | |
369 | 76 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
370 | | const IS_UPLOAD_TRUE: bool = true; |
371 | | |
372 | | // This should be an uncontended lock since write was called. |
373 | 76 | let mut local_state = self.shared_state.lock(); |
374 | | // If this is the first or second call after a failure and we have |
375 | | // cached messages, then use the cached write requests. |
376 | 76 | let cached_message = local_state.resumed_message(); |
377 | 76 | if cached_message.is_some() { |
378 | 0 | return Poll::Ready(cached_message); |
379 | 76 | } |
380 | | // Finish draining an oversized frame before reading the next one. |
381 | 76 | if let Some(pending2 ) = local_state.pending.take() { |
382 | 2 | let message = local_state.split_oversized(pending); |
383 | 2 | if local_state.is_resumable() { |
384 | 2 | local_state.push_message(message.clone()); |
385 | 2 | }0 |
386 | 2 | return Poll::Ready(Some(message)); |
387 | 74 | } |
388 | | // Read a new write request from the downstream. |
389 | 74 | let Poll::Ready(maybe_message71 ) = Pin::new(&mut local_state.read_stream).poll_next(cx) |
390 | | else { |
391 | 3 | return Poll::Pending; |
392 | | }; |
393 | | // Update the instance name in the write request and forward it on. |
394 | 71 | let result = match maybe_message54 { |
395 | 53 | Some(Ok(mut message)) => { |
396 | 53 | if !message.resource_name.is_empty() { |
397 | | // Replace the instance name in the resource name if it is |
398 | | // different from the instance name in the write state. |
399 | 51 | match ResourceInfo::new(&message.resource_name, IS_UPLOAD_TRUE) { |
400 | 51 | Ok(mut resource_name) => { |
401 | 51 | if resource_name.instance_name != local_state.instance_name { |
402 | 1 | resource_name.instance_name = |
403 | 1 | Cow::Borrowed(&local_state.instance_name); |
404 | 1 | message.resource_name = resource_name.to_string(IS_UPLOAD_TRUE); |
405 | 50 | } |
406 | | } |
407 | 0 | Err(err) => { |
408 | 0 | local_state.read_stream_error = Some(err); |
409 | 0 | return Poll::Ready(None); |
410 | | } |
411 | | } |
412 | 2 | } |
413 | 53 | let message = local_state.split_oversized(message); |
414 | | // Cache the last request in case there is an error to allow |
415 | | // the upload to be resumed. Non-resumable writes skip the |
416 | | // clone: cached messages would never be replayed. |
417 | 53 | if local_state.is_resumable() { |
418 | 4 | local_state.push_message(message.clone()); |
419 | 49 | } |
420 | 53 | Some(message) |
421 | | } |
422 | 1 | Some(Err(err)) => { |
423 | 1 | local_state.read_stream_error = Some(err); |
424 | 1 | None |
425 | | } |
426 | 17 | None => None, |
427 | | }; |
428 | 71 | Poll::Ready(result) |
429 | 76 | } |
430 | | } |