Skip to main content

objectstore_service/
stream.rs

1//! Stream types and buffering utilities for object data.
2//!
3//! Data flows in streams to keep memory consumption low. Two distinct types
4//! cover the two directions of data flow:
5//!
6//! - [`ClientStream`] — incoming data from a client PUT request body. Uses
7//!   [`ClientError`] as the error type so backends can distinguish a broken
8//!   client connection from a backend I/O failure (400 vs 500).
9//! - [`PayloadStream`] — outgoing data returned from
10//!   [`Backend::get_object`](crate::backend::common::Backend::get_object).
11
12use std::error::Error;
13use std::fmt;
14use std::io;
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18use bytes::{Bytes, BytesMut};
19use futures_util::stream::BoxStream;
20use futures_util::{Stream, StreamExt, TryStreamExt};
21
22/// Outgoing byte stream returned by [`Backend::get_object`](crate::backend::common::Backend::get_object).
23///
24/// Use [`single`] to construct a single-chunk `PayloadStream` from an owned value.
25pub type PayloadStream = BoxStream<'static, io::Result<Bytes>>;
26
27/// Error originating from a client-supplied input stream.
28///
29/// Wraps any error yielded by a [`ClientStream`] (the incoming HTTP request body).
30/// Backends receive this via [`ClientStream`] and can detect it with
31/// [`unpack_client_error`] to return a 4xx response rather than treating
32/// it as a 5xx backend failure.
33#[derive(Clone, Debug)]
34pub struct ClientError(Arc<dyn Error + Send + Sync + 'static>);
35
36impl ClientError {
37    /// Creates a new [`ClientError`] wrapping `err`.
38    pub fn new<E>(err: E) -> Self
39    where
40        E: Error + Send + Sync + 'static,
41    {
42        Self(Arc::new(err))
43    }
44}
45
46impl fmt::Display for ClientError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        self.0.fmt(f)
49    }
50}
51
52impl Error for ClientError {
53    fn source(&self) -> Option<&(dyn Error + 'static)> {
54        self.0.source()
55    }
56}
57
58/// Required by [`tokio_util::io::StreamReader`] in the local filesystem backend.
59impl From<ClientError> for io::Error {
60    fn from(err: ClientError) -> Self {
61        io::Error::other(err)
62    }
63}
64
65/// Incoming byte stream from a client PUT request body.
66///
67/// Uses [`ClientError`] as the error type so that a dropped or interrupted
68/// client connection is distinguishable from a backend I/O failure. Backends
69/// that detect a [`ClientError`] (via [`unpack_client_error`]) can surface it
70/// as [`ClientStream`](crate::error::ErrorKind::ClientStream), which the server
71/// maps to HTTP 400 rather than 500.
72///
73/// Use [`single`] to construct a single-chunk `ClientStream` from an owned value.
74pub type ClientStream = BoxStream<'static, Result<Bytes, ClientError>>;
75
76/// Walks the source chain of `err` looking for a [`ClientError`].
77///
78/// At each step, two locations are checked:
79///
80/// - **Direct**: the error itself is a `ClientError`.
81/// - **Packed in `io::Error`**: the error is an `io::Error` whose custom inner
82///   value is a `ClientError`.
83///
84/// Use this in `put_object` implementations to reclassify body-stream errors
85/// as [`ClientStream`](crate::error::ErrorKind::ClientStream) instead of an
86/// opaque server error.
87pub fn unpack_client_error<E>(err: &E) -> Option<ClientError>
88where
89    E: Error + 'static,
90{
91    let mut source = Some(err as &(dyn Error + 'static));
92
93    while let Some(s) = source {
94        // The client error may be wrapped as custom `io::Error`, in which case it cannot be
95        // discovered by iterating `sources`.
96        let target = match s.downcast_ref::<io::Error>().and_then(|e| e.get_ref()) {
97            Some(inner) => inner,
98            None => s,
99        };
100
101        if let Some(client_error) = target.downcast_ref::<ClientError>() {
102            return Some(client_error.clone());
103        }
104
105        source = s.source();
106    }
107    None
108}
109
110#[derive(Debug, Default)]
111enum ChunkedBytesState {
112    #[default]
113    Empty,
114    Single(Bytes),
115    Multi(BytesMut),
116}
117
118/// Lazy stream buffer that avoids copying single chunks.
119///
120/// Tracks three internal states:
121/// - **Empty** — no allocation, no data.
122/// - **Single** — first chunk stored by move (zero-copy).
123/// - **Multi** — allocated on second `push`, coalesces all chunks.
124#[derive(Debug)]
125pub(crate) struct ChunkedBytes {
126    state: ChunkedBytesState,
127    capacity: usize,
128}
129
130impl ChunkedBytes {
131    /// Creates a new buffer with the given capacity hint.
132    ///
133    /// The capacity is used when transitioning from Single to Multi state
134    /// and is exposed via [`capacity()`](Self::capacity) for use as a buffer limit.
135    pub fn new(capacity: usize) -> Self {
136        Self {
137            state: ChunkedBytesState::Empty,
138            capacity,
139        }
140    }
141
142    /// Appends a chunk to the buffer.
143    ///
144    /// Empty→Single stores by move (zero-copy). Single→Multi allocates and
145    /// copies both chunks. Multi extends the existing allocation.
146    pub fn push(&mut self, chunk: Bytes) {
147        if chunk.is_empty() {
148            return;
149        }
150        self.state = match std::mem::take(&mut self.state) {
151            ChunkedBytesState::Empty => ChunkedBytesState::Single(chunk),
152            ChunkedBytesState::Single(first) => {
153                let capacity = self.capacity.max(first.len() + chunk.len());
154                let mut buf = BytesMut::with_capacity(capacity);
155                buf.extend_from_slice(&first);
156                buf.extend_from_slice(&chunk);
157                ChunkedBytesState::Multi(buf)
158            }
159            ChunkedBytesState::Multi(mut buf) => {
160                buf.extend_from_slice(&chunk);
161                ChunkedBytesState::Multi(buf)
162            }
163        };
164    }
165
166    /// Returns the configured capacity (used as the buffer limit).
167    pub fn capacity(&self) -> usize {
168        self.capacity
169    }
170
171    /// Returns the total number of buffered bytes.
172    pub fn len(&self) -> usize {
173        match &self.state {
174            ChunkedBytesState::Empty => 0,
175            ChunkedBytesState::Single(b) => b.len(),
176            ChunkedBytesState::Multi(b) => b.len(),
177        }
178    }
179
180    /// Returns `true` if no bytes have been buffered.
181    #[cfg(test)]
182    pub fn is_empty(&self) -> bool {
183        self.len() == 0
184    }
185
186    /// Consumes the buffer and returns the data as a single `Bytes`.
187    ///
188    /// Single-chunk data is returned by move (zero-copy). Multi-chunk data
189    /// is frozen from the internal `BytesMut`.
190    pub fn into_bytes(self) -> Bytes {
191        match self.state {
192            ChunkedBytesState::Empty => Bytes::new(),
193            ChunkedBytesState::Single(b) => b,
194            ChunkedBytesState::Multi(b) => b.freeze(),
195        }
196    }
197}
198
199/// Reads up to `limit` bytes from a stream to support size-based routing decisions.
200///
201/// Constructed via the async [`SizedPeek::new`], which reads eagerly so that
202/// [`is_exhausted()`](Self::is_exhausted) is always valid.
203/// The full stream (buffered prefix plus any unconsumed remainder) is recovered
204/// with [`into_stream()`](Self::into_stream).
205///
206/// The buffer never exceeds `limit` bytes: the chunk that would cause overflow
207/// is held separately and re-emitted by `into_stream` without copying.
208pub(crate) struct SizedPeek<S> {
209    buffer: ChunkedBytes,
210    /// The first chunk that exceeded the limit; `None` when the stream was exhausted.
211    pending: Option<Bytes>,
212    /// `None` when the stream was fully consumed within the limit.
213    stream: Option<S>,
214}
215
216impl<S> SizedPeek<S> {
217    /// Returns `true` if the stream was fully consumed within the peek limit.
218    pub fn is_exhausted(&self) -> bool {
219        self.pending.is_none()
220    }
221
222    /// Returns the number of bytes held in the buffer (at most `limit`).
223    #[cfg(test)]
224    pub fn len(&self) -> usize {
225        self.buffer.len()
226    }
227}
228
229impl<S, E> SizedPeek<S>
230where
231    S: Stream<Item = Result<Bytes, E>> + Unpin,
232{
233    /// Reads from `stream` into an internal buffer until `limit` bytes are accumulated
234    /// or the stream ends.
235    ///
236    /// Uses strictly-greater-than comparison: a stream of exactly `limit` bytes is
237    /// considered exhausted.
238    pub async fn new(mut stream: S, limit: usize) -> Result<Self, E> {
239        let mut buffer = ChunkedBytes::new(limit);
240
241        while let Some(chunk) = stream.try_next().await? {
242            if buffer.len() + chunk.len() > buffer.capacity() {
243                return Ok(Self {
244                    buffer,
245                    pending: Some(chunk),
246                    stream: Some(stream),
247                });
248            }
249            buffer.push(chunk);
250        }
251
252        Ok(Self {
253            buffer,
254            pending: None,
255            stream: None,
256        })
257    }
258
259    /// Consumes self and returns all bytes as a single [`Bytes`].
260    ///
261    /// If the peek limit was exceeded, drains the remaining stream before
262    /// returning. Always correct regardless of [`is_exhausted`](Self::is_exhausted).
263    pub async fn into_bytes(mut self) -> Result<Bytes, E> {
264        if let Some(pending) = self.pending.take() {
265            self.buffer.push(pending);
266        }
267
268        if let Some(mut stream) = self.stream.take() {
269            while let Some(chunk) = stream.try_next().await? {
270                self.buffer.push(chunk);
271            }
272        }
273
274        Ok(self.buffer.into_bytes())
275    }
276}
277
278impl<S, E> SizedPeek<S>
279where
280    S: Stream<Item = Result<Bytes, E>>,
281{
282    /// Consumes self and returns a stream that yields the buffered prefix first,
283    /// then any remaining data from the original stream.
284    pub fn into_stream(self) -> impl Stream<Item = Result<Bytes, E>> {
285        let leading = [self.buffer.into_bytes(), self.pending.unwrap_or_default()]
286            .into_iter()
287            .filter(|b| !b.is_empty())
288            .map(Ok);
289
290        let tail = futures_util::stream::iter(self.stream).flatten();
291        futures_util::stream::iter(leading).chain(tail)
292    }
293}
294
295/// Creates a single-chunk stream that yields `contents` as one item.
296pub fn single<E: Send + 'static>(
297    contents: impl Into<Bytes>,
298) -> BoxStream<'static, Result<Bytes, E>> {
299    futures_util::stream::once(std::future::ready(Ok(contents.into()))).boxed()
300}
301
302/// Wraps a stream to count the total bytes yielded by successful chunks.
303///
304/// Returns the shared counter and the wrapped stream. The counter is incremented
305/// as the stream is consumed, so read it only after the stream is exhausted.
306pub fn counting_stream<S, E>(stream: S) -> (Arc<AtomicU64>, impl Stream<Item = Result<Bytes, E>>)
307where
308    S: Stream<Item = Result<Bytes, E>>,
309{
310    let counter = Arc::new(AtomicU64::new(0));
311
312    (
313        counter.clone(),
314        stream.inspect(move |res| {
315            if let Ok(chunk) = res {
316                counter.fetch_add(chunk.len() as u64, Ordering::Relaxed);
317            }
318        }),
319    )
320}
321
322/// Collects a stream of `Bytes` chunks into a `Vec<u8>`.
323#[cfg(test)]
324pub(crate) async fn read_to_vec<S, E>(mut stream: S) -> crate::error::Result<Vec<u8>>
325where
326    S: Stream<Item = Result<Bytes, E>> + Unpin,
327    E: Into<crate::error::Error>,
328{
329    let mut payload = Vec::new();
330    while let Some(result) = stream.next().await {
331        let chunk = result.map_err(Into::into)?;
332        payload.extend(&chunk);
333    }
334    Ok(payload)
335}
336
337#[cfg(test)]
338mod tests {
339    use futures_util::stream;
340
341    use super::*;
342
343    // --- ChunkedBytes tests ---
344
345    #[test]
346    fn empty_into_bytes() {
347        let buf = ChunkedBytes::new(1024);
348        assert!(buf.is_empty());
349        assert_eq!(buf.len(), 0);
350        assert_eq!(buf.into_bytes().len(), 0);
351    }
352
353    #[test]
354    fn single_chunk_zero_copy() {
355        let original = Bytes::from_static(b"hello world");
356        let ptr = original.as_ptr();
357
358        let mut buf = ChunkedBytes::new(1024);
359        buf.push(original);
360
361        assert_eq!(buf.len(), 11);
362        assert!(!buf.is_empty());
363
364        let result = buf.into_bytes();
365        assert_eq!(result.as_ref(), b"hello world");
366        // Pointer equality proves zero-copy.
367        assert_eq!(result.as_ptr(), ptr);
368    }
369
370    #[test]
371    fn multi_chunk_coalesces() {
372        let mut buf = ChunkedBytes::new(1024);
373        buf.push(Bytes::from_static(b"hello "));
374        buf.push(Bytes::from_static(b"world"));
375
376        assert_eq!(buf.len(), 11);
377        assert_eq!(buf.into_bytes().as_ref(), b"hello world");
378    }
379
380    #[test]
381    fn push_empty_chunk_is_noop() {
382        let original = Bytes::from_static(b"data");
383        let ptr = original.as_ptr();
384
385        let mut buf = ChunkedBytes::new(1024);
386        buf.push(Bytes::new());
387        assert!(buf.is_empty());
388
389        buf.push(original);
390        buf.push(Bytes::new());
391        // Still in Single state — empty pushes don't trigger Multi.
392        let result = buf.into_bytes();
393        assert_eq!(result.as_ptr(), ptr);
394    }
395
396    #[test]
397    fn capacity_is_preserved() {
398        let buf = ChunkedBytes::new(42);
399        assert_eq!(buf.capacity(), 42);
400    }
401
402    // --- SizedPeek tests ---
403
404    #[tokio::test]
405    async fn exhausted_when_stream_fits() {
406        let s = stream::once(std::future::ready(Ok::<_, io::Error>(Bytes::from_static(
407            b"small payload",
408        ))));
409
410        let peeked = SizedPeek::new(s, 1024).await.unwrap();
411
412        assert!(peeked.is_exhausted());
413        assert_eq!(peeked.len(), 13);
414    }
415
416    #[tokio::test]
417    async fn remaining_when_stream_exceeds_limit() {
418        let chunks: Vec<io::Result<Bytes>> = vec![
419            Ok(Bytes::from(vec![0u8; 600])),
420            Ok(Bytes::from(vec![1u8; 600])),
421        ];
422
423        let peeked = SizedPeek::new(stream::iter(chunks), 1000).await.unwrap();
424
425        assert!(!peeked.is_exhausted());
426        // Only the first 600-byte chunk fits in the buffer; the second is pending.
427        assert_eq!(peeked.len(), 600);
428    }
429
430    #[tokio::test]
431    async fn into_stream_reassembles_data() {
432        // "aaa" fits (3 ≤ 5), "bbb" overflows (3+3=6 > 5) → pending, "ccc" stays in stream.
433        let chunks: Vec<io::Result<Bytes>> = vec![
434            Ok(Bytes::from_static(b"aaa")),
435            Ok(Bytes::from_static(b"bbb")),
436            Ok(Bytes::from_static(b"ccc")),
437        ];
438
439        let peeked = SizedPeek::new(stream::iter(chunks), 5).await.unwrap();
440        assert!(!peeked.is_exhausted());
441
442        let output = read_to_vec(peeked.into_stream()).await.unwrap();
443        assert_eq!(output, b"aaabbbccc");
444    }
445
446    #[tokio::test]
447    async fn into_stream_single_chunk_zero_copy() {
448        let original = Bytes::from_static(b"zero-copy roundtrip");
449        let ptr = original.as_ptr();
450        let s = stream::once(std::future::ready(Ok::<_, io::Error>(original)));
451
452        let peeked = SizedPeek::new(s, 1024).await.unwrap();
453        let out = peeked.into_stream();
454        futures_util::pin_mut!(out);
455        let first = out.try_next().await.unwrap().unwrap();
456        assert_eq!(first.as_ptr(), ptr);
457    }
458
459    #[tokio::test]
460    async fn oversized_single_chunk_goes_to_pending() {
461        // A single chunk larger than the limit never enters the buffer.
462        let big = Bytes::from(vec![0u8; 2000]);
463        let ptr = big.as_ptr();
464        let s = stream::once(std::future::ready(Ok::<_, io::Error>(big)));
465
466        let peeked = SizedPeek::new(s, 1000).await.unwrap();
467        assert!(!peeked.is_exhausted());
468        assert_eq!(peeked.len(), 0); // nothing in buffer
469
470        let out = peeked.into_stream();
471        futures_util::pin_mut!(out);
472        let first = out.try_next().await.unwrap().unwrap();
473        // The pending chunk is emitted by move — same pointer, no copy.
474        assert_eq!(first.as_ptr(), ptr);
475        assert_eq!(first.len(), 2000);
476    }
477
478    #[tokio::test]
479    async fn error_propagation() {
480        let chunks: Vec<io::Result<Bytes>> = vec![
481            Ok(Bytes::from_static(b"ok")),
482            Err(io::Error::new(io::ErrorKind::BrokenPipe, "broken")),
483        ];
484
485        let result = SizedPeek::new(stream::iter(chunks), 1024).await;
486        assert!(result.is_err());
487    }
488}