Skip to main content

http_streams_core/
buffer.rs

1//! Coalescing small encoded chunks into larger ones.
2//!
3//! Without this, a format whose items are small (JSON Lines of short objects, say) emits one
4//! HTTP frame per item, which is dreadful wire efficiency. It matters on the sending side in
5//! both directions: a client uploading a stream has exactly the same problem as a server
6//! returning one.
7//!
8//! Core yields [`Bytes`]; wrapping them in whatever frame type the HTTP layer wants is the
9//! binding crate's job.
10
11use bytes::{Bytes, BytesMut};
12use futures::stream::{Stream, StreamExt};
13
14/// Coalesce every `count` items that are ready at the same time into one chunk.
15///
16/// Uses readiness rather than a fixed count, so a slow producer is never held back waiting for
17/// a batch to fill.
18pub fn buffer_ready_items<'b, S, E>(
19    stream: S,
20    count: usize,
21) -> impl Stream<Item = Result<Bytes, E>> + Send + 'b
22where
23    S: Stream<Item = Result<Bytes, E>> + Send + 'b,
24    E: 'b,
25{
26    stream.ready_chunks(count).map(|chunks| {
27        let mut buf = BytesMut::new();
28        for chunk in chunks {
29            buf.extend_from_slice(&chunk?);
30        }
31        Ok(buf.freeze())
32    })
33}
34
35/// Coalesce output into chunks of at least `size` bytes.
36///
37/// The final chunk is whatever is left over, and may be shorter.
38pub fn buffer_bytes<'b, S, E>(
39    stream: S,
40    size: usize,
41) -> impl Stream<Item = Result<Bytes, E>> + Send + 'b
42where
43    S: Stream<Item = Result<Bytes, E>> + Send + 'b,
44    // Unlike the combinators that only observe, this one holds errors in its scan state, so
45    // they must be `Send` for the resulting stream to be.
46    E: Send + 'b,
47{
48    // A zero size would make the drain loop below spin forever, since `split_to(0)` never
49    // shrinks the buffer. This is a public function, so the guard belongs here rather than in
50    // every caller.
51    let size = size.max(1);
52
53    // An empty chunk appended to the end is the signal to flush whatever is still buffered.
54    // `Bytes::new()` cannot occur otherwise, because empty encoder output is dropped upstream.
55    let stream = stream.chain(futures::stream::once(futures::future::ready(Ok(
56        Bytes::new(),
57    ))));
58
59    stream
60        .scan(
61            (BytesMut::with_capacity(size), false),
62            move |(current_buffer, errored), maybe_bytes| {
63                futures::future::ready(if *errored {
64                    None
65                } else {
66                    match maybe_bytes {
67                        // The flush marker. Emitting unconditionally would append an empty
68                        // chunk whenever the input divided evenly into `size`, or was empty.
69                        Ok(bytes) if bytes.is_empty() => {
70                            if current_buffer.is_empty() {
71                                Some(Vec::new())
72                            } else {
73                                Some(vec![Ok(current_buffer.split().freeze())])
74                            }
75                        }
76                        Ok(bytes) => {
77                            let mut chunks = Vec::new();
78                            current_buffer.extend_from_slice(&bytes);
79                            while current_buffer.len() >= size {
80                                chunks.push(Ok(current_buffer.split_to(size).freeze()));
81                            }
82                            Some(chunks)
83                        }
84                        // Propagate the error instead of ending the stream: returning `None`
85                        // here would make a failure indistinguishable from a clean EOF, and
86                        // the receiver would silently accept a truncated body. Buffered bytes
87                        // are dropped and the stream stops, so no data can follow the error
88                        // via the trailing flush marker above.
89                        Err(e) => {
90                            *errored = true;
91                            current_buffer.clear();
92                            Some(vec![Err(e)])
93                        }
94                    }
95                })
96            },
97        )
98        .flat_map(futures::stream::iter)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::error::{StreamError, StreamErrorKind};
105
106    async fn collect(s: impl Stream<Item = Result<Bytes, StreamError>>) -> (Vec<Vec<u8>>, usize) {
107        let items: Vec<_> = Box::pin(s).collect().await;
108        let errors = items.iter().filter(|i| i.is_err()).count();
109        let chunks = items.into_iter().flatten().map(|b| b.to_vec()).collect();
110        (chunks, errors)
111    }
112
113    fn source(parts: Vec<&'static str>) -> impl Stream<Item = Result<Bytes, StreamError>> {
114        futures::stream::iter(
115            parts
116                .into_iter()
117                .map(|p| Ok(Bytes::from_static(p.as_bytes()))),
118        )
119    }
120
121    #[tokio::test]
122    async fn buffers_to_the_requested_size() {
123        let (chunks, errors) = collect(buffer_bytes(source(vec!["ab", "cd", "ef", "g"]), 3)).await;
124        assert_eq!(errors, 0);
125        assert_eq!(
126            chunks,
127            vec![b"abc".to_vec(), b"def".to_vec(), b"g".to_vec()]
128        );
129    }
130
131    #[tokio::test]
132    async fn flushes_a_short_tail() {
133        let (chunks, _) = collect(buffer_bytes(source(vec!["ab"]), 100)).await;
134        assert_eq!(chunks, vec![b"ab".to_vec()]);
135    }
136
137    /// The regression: an error must not look like a clean end of body.
138    #[tokio::test]
139    async fn an_error_stops_the_stream_and_is_visible() {
140        let parts = vec![
141            Ok(Bytes::from_static(b"ab")),
142            Err(StreamError::new(StreamErrorKind::CodecError, None, None)),
143            Ok(Bytes::from_static(b"cd")),
144        ];
145        let (chunks, errors) = collect(buffer_bytes(futures::stream::iter(parts), 100)).await;
146
147        assert_eq!(errors, 1, "the error must be yielded, not swallowed");
148        assert!(
149            chunks.is_empty(),
150            "buffered bytes are dropped, so no data can follow the error"
151        );
152    }
153
154    #[tokio::test]
155    async fn ready_items_coalesce() {
156        let (chunks, errors) =
157            collect(buffer_ready_items(source(vec!["a", "b", "c", "d", "e"]), 2)).await;
158        assert_eq!(errors, 0);
159        assert_eq!(
160            chunks.concat(),
161            b"abcde".to_vec(),
162            "coalescing must not change the bytes"
163        );
164        assert!(chunks.len() < 5, "chunks must actually be coalesced");
165    }
166
167    /// The flush marker used to be emitted unconditionally, appending a zero-length chunk
168    /// whenever the input happened to divide evenly into `size`.
169    #[tokio::test]
170    async fn an_exact_multiple_emits_no_empty_tail() {
171        let (chunks, errors) = collect(buffer_bytes(source(vec!["abc", "def"]), 3)).await;
172        assert_eq!(errors, 0);
173        assert_eq!(chunks, vec![b"abc".to_vec(), b"def".to_vec()]);
174        assert!(
175            chunks.iter().all(|c| !c.is_empty()),
176            "no zero-length chunk may be emitted"
177        );
178    }
179
180    #[tokio::test]
181    async fn an_empty_source_emits_nothing() {
182        let empty = futures::stream::iter(Vec::<Result<Bytes, StreamError>>::new());
183        let (chunks, errors) = collect(buffer_bytes(empty, 8)).await;
184        assert_eq!(errors, 0);
185        assert!(chunks.is_empty(), "an empty body must produce no chunks");
186    }
187
188    /// A zero size would spin forever: `split_to(0)` never shrinks the buffer. These are public
189    /// functions, so a caller can pass one.
190    #[tokio::test]
191    async fn a_zero_size_does_not_hang() {
192        let buffered = buffer_bytes(source(vec!["ab", "cd"]), 0);
193        let (chunks, errors) =
194            tokio::time::timeout(std::time::Duration::from_secs(5), collect(buffered))
195                .await
196                .expect("a zero size must not loop forever");
197
198        assert_eq!(errors, 0);
199        assert_eq!(chunks.concat(), b"abcd".to_vec());
200    }
201}