use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use futures::stream::Stream;
use tokio_stream::adapters::ChunksTimeout;
const DEFAULT_MAX_BATCH: usize = 512;
pub struct Batched<S>
where
S: Stream,
{
inner: Pin<Box<ChunksTimeout<S>>>,
}
impl<S> Batched<S>
where
S: Stream,
{
pub fn new(inner: S, window: Duration, max_items: usize) -> Self {
use tokio_stream::StreamExt as _;
Self {
inner: Box::pin(inner.chunks_timeout(max_items.max(1), window)),
}
}
}
impl<S> Stream for Batched<S>
where
S: Stream,
{
type Item = Vec<S::Item>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.inner.as_mut().poll_next(cx)
}
}
pub trait StreamBatchExt: Stream + Sized + Unpin {
fn batched(self, window: Duration) -> Batched<Self> {
Batched::new(self, window, DEFAULT_MAX_BATCH)
}
fn batched_with_capacity(self, window: Duration, max_items: usize) -> Batched<Self> {
Batched::new(self, window, max_items)
}
}
impl<S> StreamBatchExt for S where S: Stream + Sized + Unpin {}
#[cfg(test)]
mod tests {
use super::*;
use futures::StreamExt;
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
#[tokio::test]
async fn window_coalesces_items_into_one_batch() {
let (tx, rx) = mpsc::channel(16);
let mut batched = ReceiverStream::new(rx).batched(Duration::from_millis(60));
for i in 0..5 {
tx.send(i).await.unwrap();
}
let batch = tokio::time::timeout(Duration::from_secs(2), batched.next())
.await
.expect("timed out")
.expect("stream ended");
assert_eq!(batch, vec![0, 1, 2, 3, 4]);
drop(tx);
}
#[tokio::test]
async fn max_items_flushes_before_the_window_elapses() {
let (tx, rx) = mpsc::channel(16);
let mut batched = ReceiverStream::new(rx).batched_with_capacity(Duration::from_secs(30), 2);
for i in 0..4 {
tx.send(i).await.unwrap();
}
let batch = tokio::time::timeout(Duration::from_secs(2), batched.next())
.await
.expect("timed out")
.expect("stream ended");
assert_eq!(batch, vec![0, 1]);
drop(tx);
}
#[tokio::test]
async fn partial_batch_is_flushed_when_the_source_ends() {
let (tx, rx) = mpsc::channel(16);
let mut batched = ReceiverStream::new(rx).batched(Duration::from_secs(30));
tx.send(7).await.unwrap();
drop(tx);
let batch = tokio::time::timeout(Duration::from_secs(2), batched.next())
.await
.expect("timed out")
.expect("stream ended");
assert_eq!(batch, vec![7]);
let end = tokio::time::timeout(Duration::from_secs(2), batched.next())
.await
.expect("timed out");
assert!(end.is_none());
}
#[tokio::test]
async fn empty_batches_are_never_emitted() {
let (tx, rx) = mpsc::channel::<u8>(1);
let mut batched = ReceiverStream::new(rx).batched(Duration::from_millis(20));
let idle = tokio::time::timeout(Duration::from_millis(150), batched.next()).await;
assert!(idle.is_err(), "idle source must not emit empty batches");
drop(tx);
}
}