Coverage Report

Created: 2026-11-13 23:29

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}