use futures::Stream;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::sync::mpsc::Receiver;
use tokio::task::JoinHandle;
pub const DEFAULT_BLOCKING_CHANNEL_CAPACITY: usize = 1024;
pub fn stream_blocking_iter<Init, Iter, Item, Error>(
capacity: usize,
init: Init,
) -> impl Stream<Item = Result<Item, Error>> + Send + Unpin + 'static
where
Init: FnOnce() -> Result<Iter, Error> + Send + 'static,
Iter: Iterator<Item = Result<Item, Error>>,
Item: Send + 'static,
Error: Send + 'static,
{
assert!(
capacity > 0,
"stream_blocking_iter requires a non-zero channel capacity"
);
let (tx, rx) = tokio::sync::mpsc::channel(capacity);
let handle = tokio::task::spawn_blocking(move || {
let iter = match init() {
Ok(iter) => iter,
Err(error) => {
let _ = tx.blocking_send(Err(error));
return;
}
};
for item in iter {
if tx.blocking_send(item).is_err() {
break;
}
}
});
BlockingIterStream {
rx,
handle: Some(handle),
}
}
struct BlockingIterStream<Item, Error> {
rx: Receiver<Result<Item, Error>>,
handle: Option<JoinHandle<()>>,
}
impl<Item, Error> Stream for BlockingIterStream<Item, Error> {
type Item = Result<Item, Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match this.rx.poll_recv(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(Some(item)) => return Poll::Ready(Some(item)),
Poll::Ready(None) => {}
}
let Some(handle) = this.handle.as_mut() else {
return Poll::Ready(None);
};
let result = std::task::ready!(Pin::new(handle).poll(cx));
this.handle = None;
match result {
Ok(()) => Poll::Ready(None),
Err(error) if error.is_panic() => std::panic::resume_unwind(error.into_panic()),
Err(_) => Poll::Ready(None),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use futures::StreamExt;
use std::{
io::{Error, ErrorKind},
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
fn ok_iter(count: usize) -> Result<std::vec::IntoIter<Result<usize, Error>>, Error> {
Ok((0..count).map(Ok).collect::<Vec<_>>().into_iter())
}
#[tokio::test]
async fn forwards_every_item_in_order() {
let stream = stream_blocking_iter(4, || ok_iter(50));
let items = stream.map(|item| item.unwrap()).collect::<Vec<_>>().await;
assert_eq!(items, (0..50).collect::<Vec<_>>());
}
#[tokio::test]
async fn an_init_failure_becomes_a_single_err_item() {
let stream = stream_blocking_iter(4, || {
Err::<std::vec::IntoIter<Result<usize, Error>>, _>(Error::new(
ErrorKind::NotFound,
"no such artifact",
))
});
let items = stream.collect::<Vec<_>>().await;
assert_eq!(items.len(), 1);
assert_eq!(items[0].as_ref().unwrap_err().kind(), ErrorKind::NotFound);
}
#[tokio::test]
async fn a_mid_stream_failure_is_surfaced_not_skipped() {
let stream = stream_blocking_iter(4, || {
Ok(vec![
Ok(1_usize),
Err(Error::new(ErrorKind::InvalidData, "bad row")),
Ok(3),
]
.into_iter())
});
let items = stream.collect::<Vec<_>>().await;
assert_eq!(items.len(), 3);
assert!(items[1].is_err());
}
#[tokio::test]
#[should_panic(expected = "corrupt page header")]
async fn a_panic_in_the_iterator_reaches_the_consumer() {
let stream = stream_blocking_iter(4, || {
Ok((0..10).map(|index: usize| {
assert!(index < 3, "corrupt page header");
Ok::<usize, Error>(index)
}))
});
let _items = stream.collect::<Vec<_>>().await;
}
#[tokio::test]
#[should_panic(expected = "unreadable footer")]
async fn a_panic_in_init_reaches_the_consumer() {
let stream = stream_blocking_iter(4, || {
panic!("unreadable footer");
#[allow(unreachable_code)] ok_iter(0)
});
let _items = stream.collect::<Vec<_>>().await;
}
#[tokio::test]
async fn items_before_a_panic_are_still_yielded() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&delivered);
let collect = std::panic::AssertUnwindSafe(async move {
let mut stream = Box::pin(stream_blocking_iter(4, || {
Ok((0..10).map(|index: usize| {
assert!(index < 3, "boom");
Ok::<usize, Error>(index)
}))
}));
while let Some(item) = stream.next().await {
sink.lock().unwrap().push(item.unwrap());
}
});
let outcome = futures::FutureExt::catch_unwind(collect).await;
assert!(outcome.is_err(), "the panic must not be swallowed");
assert_eq!(
*delivered.lock().unwrap(),
vec![0, 1, 2],
"every item decoded before the panic is real data and must still reach the consumer"
);
}
#[tokio::test]
async fn a_normal_end_of_stream_does_not_panic() {
let mut stream = Box::pin(stream_blocking_iter(4, || ok_iter(3)));
for expected in 0..3 {
assert_eq!(stream.next().await.unwrap().unwrap(), expected);
}
assert!(stream.next().await.is_none());
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn the_producer_is_bounded_by_the_channel_capacity() {
const CAPACITY: usize = 2;
let produced = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&produced);
let mut stream = Box::pin(stream_blocking_iter(CAPACITY, move || {
Ok((0..10_000).map(move |index| {
counter.fetch_add(1, Ordering::SeqCst);
Ok::<usize, Error>(index)
}))
}));
assert_eq!(stream.next().await.unwrap().unwrap(), 0);
tokio::task::yield_now().await;
let produced = produced.load(Ordering::SeqCst);
assert!(
produced <= CAPACITY + 2,
"producer ran {produced} items ahead of a {CAPACITY}-item channel"
);
}
#[tokio::test]
async fn dropping_the_stream_stops_the_producer() {
const ITEMS: usize = 10_000;
let produced = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&produced);
let mut stream = Box::pin(stream_blocking_iter(2, move || {
Ok((0..ITEMS).map(move |index| {
counter.fetch_add(1, Ordering::SeqCst);
Ok::<usize, Error>(index)
}))
}));
assert!(stream.next().await.is_some());
drop(stream);
let mut settled = produced.load(Ordering::SeqCst);
let mut stopped = false;
for _ in 0..50 {
tokio::time::sleep(Duration::from_millis(10)).await;
let current = produced.load(Ordering::SeqCst);
if current == settled {
stopped = true;
break;
}
settled = current;
}
assert!(
stopped,
"the producer was still running 500ms after the stream was dropped ({settled} items)"
);
assert!(
settled < ITEMS,
"the producer ran to completion ({settled} items) despite the stream being dropped"
);
}
}