Skip to main content

pubhubs/misc/
stream_ext.rs

1//! Tools for dealing with streams
2
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use futures::stream::Stream;
7
8/// Extension trait for [`Stream`]s
9pub trait StreamExt: Stream + Sized {
10    /// Yields items from the current, first, stream until an item from the `other`,
11    /// second, stream becomes available.  At that point the first stream is dropped,
12    /// and only items from the second stream are yielded.
13    fn until_overridden_by<Other: Stream<Item = Self::Item>>(
14        self,
15        other: Other,
16    ) -> UntilOverriddenBy<Self, Other>;
17
18    /// Like [`futures::stream::Fuse`], but can be 'tripped' to cut the stream short.
19    fn breaker(self) -> Breaker<Self>;
20
21    /// Relays this stream over a channel so that a `!Send` stream — e.g. one tied to a single-threaded
22    /// runtime — can be consumed from anywhere a `Send + Sync` stream is required.
23    ///
24    /// A background *pump* task forwards each item over the channel; this method spawns it with
25    /// [`tokio::task::spawn_local`], so it must be called from within a [`tokio::task::LocalSet`].
26    /// `capacity` bounds how many items may buffer ahead of a slow consumer.
27    ///
28    /// Each source item is yielded as `Ok`.  If the pump or source is dropped before the source is
29    /// exhausted (e.g. its runtime is torn down mid-relay), the final item is `Err(`[`Truncated`]`)` —
30    /// so a stream cut short can never be mistaken for a clean end.  See also [`SyncStream::new`],
31    /// which hands back the pump for you to drive yourself.
32    fn sync(self, capacity: std::num::NonZero<usize>) -> SyncStream<Result<Self::Item, Truncated>>
33    where
34        Self: 'static,
35        Self::Item: Send + 'static;
36}
37
38impl<S: Stream> StreamExt for S {
39    fn until_overridden_by<Other: Stream<Item = Self::Item>>(
40        self,
41        other: Other,
42    ) -> UntilOverriddenBy<Self, Other> {
43        UntilOverriddenBy {
44            a: self.breaker(),
45            b: other.breaker(),
46        }
47    }
48
49    fn breaker(self) -> Breaker<Self> {
50        Breaker { inner: Some(self) }
51    }
52
53    fn sync(self, capacity: std::num::NonZero<usize>) -> SyncStream<Result<Self::Item, Truncated>>
54    where
55        Self: 'static,
56        Self::Item: Send + 'static,
57    {
58        let (pump, stream) = SyncStream::new(self, capacity);
59        tokio::task::spawn_local(pump);
60        stream
61    }
62}
63
64pin_project_lite::pin_project! {
65/// Return type of [`StreamExt::breaker`]
66pub struct Breaker<S : Stream>{
67#[pin]
68inner: Option<S>
69}
70}
71
72impl<S: Stream> Stream for Breaker<S> {
73    type Item = S::Item;
74
75    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
76        let Some(s) = self.as_mut().project().inner.as_pin_mut() else {
77            return Poll::Ready(None);
78        };
79
80        let result = s.poll_next(cx);
81
82        if matches!(result, Poll::Ready(None)) {
83            self.trip();
84        }
85
86        result
87    }
88}
89
90impl<S: Stream> futures::stream::FusedStream for Breaker<S> {
91    fn is_terminated(&self) -> bool {
92        self.inner.is_none()
93    }
94}
95
96impl<S: Stream> Breaker<S> {
97    /// Drop the underlying stream (if it was not already);
98    /// [`Stream::poll_next`] will return `Poll::Ready(None)` from this point onwards.
99    pub fn trip(self: Pin<&mut Self>) {
100        self.project().inner.set(None)
101    }
102}
103
104pin_project_lite::pin_project! {
105/// Return type of [`StreamExt::until_overridden_by`].
106pub struct UntilOverriddenBy<A, B>
107where
108    A: Stream,
109    B: Stream<Item = A::Item>,
110{
111    #[pin]
112    a: Breaker<A>,
113    #[pin]
114    b: Breaker<B>,
115}
116}
117
118impl<A, B> Stream for UntilOverriddenBy<A, B>
119where
120    A: Stream,
121    B: Stream<Item = A::Item>,
122{
123    type Item = A::Item;
124
125    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
126        let mut this = self.project();
127
128        // As soon as b yields an item it overrides a for good: drop a (and its remaining items) and
129        // yield b's item.  Both fields are `Breaker`s, so re-polling either after it has finished is
130        // safe and cheap — a finished breaker returns `Ready(None)` again without touching its inner
131        // stream — which is what lets us poll unconditionally below.
132        let from_b = this.b.as_mut().poll_next(cx);
133        if let Poll::Ready(Some(_)) = from_b {
134            this.a.as_mut().trip();
135            return from_b;
136        }
137
138        // b produced nothing; yield from a while it still has items.  After an override a's breaker is
139        // tripped, so this just returns `Ready(None)` — meaning no item from a is ever returned after
140        // an item from b.
141        let from_a = this.a.as_mut().poll_next(cx);
142        if let Poll::Ready(Some(_)) = from_a {
143            return from_a;
144        }
145
146        // Neither produced an item.  End only once *both* breakers are exhausted; while either is still
147        // pending it may yet yield (b may override), so wait.  (A breaker reporting `Ready(None)` does
148        // not mean its underlying stream ran out — a's is tripped on override while it may still have
149        // had items.)  The polls above registered the wakers we need.
150        match (from_a, from_b) {
151            (Poll::Ready(None), Poll::Ready(None)) => Poll::Ready(None),
152            _ => Poll::Pending,
153        }
154    }
155}
156
157impl<A: Stream, B: Stream<Item = A::Item>> futures::stream::FusedStream
158    for UntilOverriddenBy<A, B>
159{
160    fn is_terminated(&self) -> bool {
161        self.a.is_terminated() && self.b.is_terminated()
162    }
163}
164
165/// Marks that a [`SyncStream`] was cut short: its pump (or source) was dropped before the source
166/// finished, so the `Ok` items before it are only a prefix of the source.  By contract it is the final
167/// item the stream yields — nothing follows it.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub struct Truncated;
170
171impl std::fmt::Display for Truncated {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.write_str("the stream was truncated: its pump was dropped before the source finished")
174    }
175}
176
177impl std::error::Error for Truncated {}
178
179/// A `Send + Sync` stream of `T`, fed over a channel by a *pump* (a future) running on the source's
180/// thread — letting a `!Send` source stream be consumed where `Send`/`Sync` is required.
181///
182/// Its constructors, [`SyncStream::new`] and [`StreamExt::sync`], relay a source stream and set
183/// `T = Result<SourceItem, `[`Truncated`]`>`: each source item is yielded as `Ok`, and a final
184/// `Err(Truncated)` appears if the pump or source is dropped before the source is exhausted — so a
185/// stream cut short can't be mistaken for a clean end.
186pub struct SyncStream<T> {
187    receiver: tokio::sync::mpsc::Receiver<T>,
188}
189
190impl<U> SyncStream<Result<U, Truncated>> {
191    /// Relays `source` into a `Send + Sync` [`SyncStream`], returning it together with the *pump*: a
192    /// future that forwards each source item (as `Ok`) over a channel bounded to `capacity`, so a slow
193    /// consumer exerts backpressure rather than letting the pump buffer the whole source.
194    ///
195    /// The pump must be driven on the thread where `source` lives — typically with
196    /// [`tokio::task::spawn_local`].  Until it is driven the stream yields nothing.  If the pump or
197    /// source is dropped before `source` is exhausted — including if the pump is never driven at all —
198    /// the stream's final item is `Err(`[`Truncated`]`)`; on a clean finish only `Ok` items appear.
199    #[must_use = "drive the returned pump (e.g. with spawn_local); dropping it undriven makes the stream yield only Err(Truncated)"]
200    pub fn new<S>(
201        source: S,
202        capacity: std::num::NonZero<usize>,
203    ) -> (impl std::future::Future<Output = ()>, Self)
204    where
205        S: Stream<Item = U> + 'static,
206        U: Send + 'static,
207    {
208        // One extra slot, reserved up front, lets a dropped pump always deliver the `Truncated` marker
209        // even when the channel is full.
210        let (sender, receiver) = tokio::sync::mpsc::channel(capacity.get().saturating_add(1));
211
212        // Arm a drop bomb — before the pump is even polled — that delivers `Truncated` over the
213        // reserved slot if the pump is dropped before the source is exhausted.  Every clean exit
214        // defuses it; either way its permit (and the sender clone it holds) is released, so the channel
215        // can close and the receiver sees the end.
216        let mut truncate_on_drop = crate::misc::drop_ext::Bomb::new({
217            let permit = sender.clone().try_reserve_owned().ok();
218            move || {
219                if let Some(permit) = permit {
220                    permit.send(Err(Truncated));
221                }
222            }
223        });
224
225        let pump = async move {
226            use futures::StreamExt as _;
227
228            let mut source = std::pin::pin!(source);
229            while let Some(item) = source.next().await {
230                // `send` only errors once the consumer (this `SyncStream`) has been dropped — there is
231                // then no one left to tell, so we stop without marking truncation.
232                if sender.send(Ok(item)).await.is_err() {
233                    truncate_on_drop.defuse();
234                    return;
235                }
236            }
237            // The source is exhausted: a clean end.
238            truncate_on_drop.defuse();
239        };
240
241        (pump, Self { receiver })
242    }
243}
244
245// This forward to `poll_recv` makes `SyncStream` a hand-rolled `tokio_stream::wrappers::ReceiverStream`.
246// We keep the newtype deliberately: it stays a documented named type in signatures (the natural home for
247// the `Truncated` contract above) and avoids enabling tokio-stream's `sync` feature, at the cost of these
248// few lines — a forward to a stable API that needs no maintenance.
249impl<T> Stream for SyncStream<T> {
250    type Item = T;
251
252    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<T>> {
253        self.get_mut().receiver.poll_recv(cx)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use futures::StreamExt as _;
261
262    fn chan(
263        items: &[i32],
264    ) -> (
265        futures::channel::mpsc::UnboundedSender<i32>,
266        impl Stream<Item = i32>,
267    ) {
268        let (tx, rx) = futures::channel::mpsc::unbounded();
269        for &item in items {
270            tx.unbounded_send(item).unwrap();
271        }
272        (tx, rx)
273    }
274
275    /// B overrides A after A yields; the remaining item in A is dropped
276    #[test]
277    fn b_overrides_a() {
278        let (a_tx, a) = chan(&[1, 2, 3]); // 3 will be overridden; B starts pending
279        let (b_tx, b) = chan(&[]);
280        let mut s = a.until_overridden_by(b);
281
282        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(1));
283        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(2));
284
285        b_tx.unbounded_send(10).unwrap();
286        drop(b_tx);
287
288        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(10));
289        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), None);
290
291        assert!(a_tx.unbounded_send(99).is_err()); // A (and its buffered 3) was dropped
292    }
293
294    /// After b overrides a, a *pending* gap from b must not end the stream.  (The bug: it polled the
295    /// now-tripped a, got `Ready(None)`, and ended early — dropping b's later items.)
296    #[test]
297    fn b_keeps_overriding_across_a_pending_gap() {
298        let (a_tx, a) = chan(&[1, 2]); // 2 will be dropped on override
299        let (b_tx, b) = chan(&[]);
300        let mut s = a.until_overridden_by(b);
301
302        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(1));
303
304        b_tx.unbounded_send(10).unwrap(); // b overrides a
305        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(10));
306
307        // b is alive but has nothing yet: the stream must wait for it, not end.
308        tokio_test::assert_pending!(tokio_test::task::spawn(s.next()).poll());
309
310        b_tx.unbounded_send(20).unwrap();
311        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(20));
312
313        drop(b_tx);
314        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), None);
315
316        assert!(a_tx.unbounded_send(99).is_err()); // a was dropped on override
317    }
318
319    /// B ends without overriding; A is not dropped and continues
320    #[test]
321    fn b_ends_without_overriding() {
322        let (a_tx, a) = chan(&[]);
323        let (b_tx, b) = chan(&[]);
324        drop(b_tx); // B ends immediately
325        let mut s = a.until_overridden_by(b);
326
327        a_tx.unbounded_send(1).unwrap();
328        a_tx.unbounded_send(2).unwrap();
329        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(1));
330        tokio_test::assert_ready_eq!(tokio_test::task::spawn(s.next()).poll(), Some(2));
331        assert!(a_tx.unbounded_send(99).is_ok()); // A was not dropped
332    }
333
334    /// Both pending — stream is pending
335    #[test]
336    fn both_pending() {
337        let (_a_tx, a) = chan(&[]);
338        let (_b_tx, b) = chan(&[]);
339        let mut s = a.until_overridden_by(b);
340        tokio_test::assert_pending!(tokio_test::task::spawn(s.next()).poll());
341    }
342
343    /// `sync` bridges a `!Send` source into a `Send + Sync` stream, relaying every item as `Ok`.
344    #[tokio::test]
345    async fn sync_relays_a_non_send_source() {
346        fn assert_send_sync<T: Send + Sync>(_: &T) {}
347
348        tokio::task::LocalSet::new()
349            .run_until(async {
350                // A `!Send` source: the `map` closure captures an `Rc`.
351                let shared = std::rc::Rc::new(vec![10, 20, 30]);
352                let mut synced = futures::stream::iter(0..shared.len())
353                    .map(move |i| shared[i])
354                    .sync(std::num::NonZero::new(4).unwrap());
355                assert_send_sync(&synced); // it crossed the `!Send` boundary
356
357                let mut got = Vec::new();
358                while let Some(item) = synced.next().await {
359                    got.push(item.expect("the source was not truncated"));
360                }
361                assert_eq!(got, [10, 20, 30]);
362            })
363            .await;
364    }
365
366    /// A pump dropped before it is ever driven yields `Err(Truncated)` and nothing else.
367    #[tokio::test]
368    async fn an_undriven_dropped_pump_truncates() {
369        let source = futures::stream::iter([1, 2, 3]);
370        let (pump, mut synced) = SyncStream::new(source, std::num::NonZero::new(4).unwrap());
371        drop(pump);
372        assert_eq!(synced.next().await, Some(Err(Truncated)));
373        assert_eq!(synced.next().await, None);
374    }
375
376    /// Aborting the pump mid-stream delivers the items already sent, then exactly one final
377    /// `Err(Truncated)`.
378    #[tokio::test]
379    async fn aborting_mid_stream_truncates_after_the_buffered_items() {
380        let source = futures::stream::iter(0..1000); // far more than we will drain
381        let (pump, mut synced) = SyncStream::new(source, std::num::NonZero::new(4).unwrap());
382        let pump = tokio::spawn(pump);
383
384        assert_eq!(synced.next().await, Some(Ok(0)));
385        assert_eq!(synced.next().await, Some(Ok(1)));
386        pump.abort();
387
388        let mut last = None;
389        let mut errors = 0;
390        while let Some(item) = synced.next().await {
391            if item.is_err() {
392                errors += 1;
393            }
394            last = Some(item);
395        }
396        assert_eq!(last, Some(Err(Truncated)));
397        assert_eq!(errors, 1);
398    }
399
400    /// A dropped consumer makes the pump stop (its `send` fails); it exits without marking truncation,
401    /// since no one is left to read it.
402    #[tokio::test]
403    async fn pump_exits_when_the_consumer_is_dropped() {
404        let source = futures::stream::iter(0..1000);
405        let (pump, mut synced) = SyncStream::new(source, std::num::NonZero::new(4).unwrap());
406        let pump = tokio::spawn(pump);
407
408        assert_eq!(synced.next().await, Some(Ok(0)));
409        assert_eq!(synced.next().await, Some(Ok(1)));
410        drop(synced);
411
412        tokio::time::timeout(std::time::Duration::from_secs(1), pump)
413            .await
414            .expect("the pump should exit after the consumer is dropped")
415            .expect("the pump task should not panic");
416    }
417}