use std::fmt::{Debug, Display, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::Relaxed;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use async_io::Timer;
use futures::Stream;
#[derive(Debug)]
pub struct ItemTimeoutErr {
pub previous_instant: Instant,
}
impl Display for ItemTimeoutErr {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
<Self as Debug>::fmt(self, f)
}
}
impl std::error::Error for ItemTimeoutErr {}
pub trait StreamExtCloseOnItemTimeout: Stream + Sized {
fn close_stream_on_item_timeout(
self,
timeout: Duration,
) -> StreamWithItemTimeout<Self> {
StreamWithItemTimeout::new(self, timeout)
}
}
impl<S: Stream> StreamExtCloseOnItemTimeout for S {}
pub struct StreamWithItemTimeout<UpstreamType>
where
UpstreamType: Stream,
{
upstream: UpstreamType,
timeout: Duration,
timer: async_io::Timer,
timedout: AtomicBool,
}
impl<UpstreamType> StreamWithItemTimeout<UpstreamType>
where
UpstreamType: Stream,
{
pub fn new(upstream: UpstreamType, timeout: Duration) -> Self {
StreamWithItemTimeout {
upstream,
timeout,
timer: Timer::after(timeout),
timedout: AtomicBool::new(false),
}
}
}
impl<UpstreamType, ItemType> Stream for StreamWithItemTimeout<UpstreamType>
where
UpstreamType: Stream<Item = ItemType> + Unpin,
{
type Item = Result<ItemType, ItemTimeoutErr>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.timedout.load(Relaxed) {
return Poll::Ready(None)
}
let timeout = self.timeout;
match Pin::new(&mut self.upstream).poll_next(cx) {
Poll::Ready(Some(item)) => {
_ = std::mem::replace(&mut self.timer, Timer::after(timeout));
Poll::Ready(Some(Ok(item)))
},
Poll::Ready(None) => {
Poll::Ready(None)
}
Poll::Pending => {
match Pin::new(&mut self.timer).poll(cx) {
Poll::Pending => Poll::Pending, Poll::Ready(instant) => {
self.timedout.store(true, Relaxed);
Poll::Ready(Some(Err(ItemTimeoutErr { previous_instant: instant })))
},
}
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use futures::{SinkExt, StreamExt};
#[tokio::test]
async fn basic_timeout_requirements() {
let (mut tx, rx) = futures::channel::mpsc::channel(0);
let mut out_stream = rx
.boxed()
.close_stream_on_item_timeout(Duration::from_millis(100));
_ = tokio::spawn(async move {
for i in 0..15 {
tokio::time::sleep(Duration::from_millis(((i as f64)*10.1) as u64)).await;
tx.send(i).await.expect("Error sending an element");
}
});
for expected_item in 0..=9 {
let observed_item = out_stream.next().await
.unwrap_or_else(|| panic!("Stream ended prematurely at #{expected_item}"))
.unwrap_or_else(|err| panic!("Timeout happened prematurely at #{expected_item}: {err}"));
assert_eq!(observed_item, expected_item, "Received item is wrong");
}
let observed_timeout_result = out_stream.next().await
.expect("Stream ended prematurely -- without yielding the Timeout error");
assert!(observed_timeout_result.is_err(), "item of value '10' was yielded without timing out. Yielded result: {observed_timeout_result:?}");
assert!(out_stream.next().await.is_none(), "Stream did not end after a timeout was detected");
}
#[tokio::test]
async fn timeout_before_first_element() {
const TIMEOUT: Duration = Duration::from_millis(100);
let (_tx, rx) = futures::channel::mpsc::channel::<()>(0);
let mut out_stream = rx
.boxed()
.close_stream_on_item_timeout(TIMEOUT);
let stopwatcher = Instant::now();
let observed_result = out_stream.next().await
.expect("Stream ended prematurely -- without yielding the Timeout error");
assert!(observed_result.is_err(), "an item was yielded without timing out. Yielded result: {observed_result:?}");
let elapsed_time = stopwatcher.elapsed();
assert!((TIMEOUT.as_secs_f64() - elapsed_time.as_secs_f64()).abs() < 1e-3, "The Timeout error did not happen at the right time");
assert!(out_stream.next().await.is_none(), "Stream did not end after a timeout was detected");
}
#[tokio::test]
async fn regular_stream_usage() {
let (mut tx, rx) = futures::channel::mpsc::channel(0);
let mut out_stream = rx
.boxed()
.close_stream_on_item_timeout(Duration::from_millis(100));
_ = tokio::spawn(async move {
for i in 0..15 {
tokio::time::sleep(Duration::from_millis((((i % 10) as f64)*10.1) as u64)).await;
tx.send(i).await.expect("Error sending an element");
}
tx.close_channel();
});
for expected_item in 0..15 {
let observed_item = out_stream.next().await
.unwrap_or_else(|| panic!("Stream ended prematurely at #{expected_item}"))
.unwrap_or_else(|err| panic!("Timeout happened prematurely at #{expected_item}: {err}"));
assert_eq!(observed_item, expected_item, "Received item is wrong");
}
assert!(out_stream.next().await.is_none(), "Sanity check failed: Stream did not end");
}
}