finance_query/streaming/
batch.rs1use std::pin::Pin;
10use std::task::{Context, Poll};
11use std::time::Duration;
12
13use futures::stream::Stream;
14use tokio_stream::adapters::ChunksTimeout;
15
16const DEFAULT_MAX_BATCH: usize = 512;
18
19pub struct Batched<S>
28where
29 S: Stream,
30{
31 inner: Pin<Box<ChunksTimeout<S>>>,
32}
33
34impl<S> Batched<S>
35where
36 S: Stream,
37{
38 pub fn new(inner: S, window: Duration, max_items: usize) -> Self {
40 use tokio_stream::StreamExt as _;
42
43 Self {
44 inner: Box::pin(inner.chunks_timeout(max_items.max(1), window)),
47 }
48 }
49}
50
51impl<S> Stream for Batched<S>
52where
53 S: Stream,
54{
55 type Item = Vec<S::Item>;
56
57 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
58 self.inner.as_mut().poll_next(cx)
59 }
60}
61
62pub trait StreamBatchExt: Stream + Sized + Unpin {
83 fn batched(self, window: Duration) -> Batched<Self> {
85 Batched::new(self, window, DEFAULT_MAX_BATCH)
86 }
87
88 fn batched_with_capacity(self, window: Duration, max_items: usize) -> Batched<Self> {
90 Batched::new(self, window, max_items)
91 }
92}
93
94impl<S> StreamBatchExt for S where S: Stream + Sized + Unpin {}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use futures::StreamExt;
100 use tokio::sync::mpsc;
101 use tokio_stream::wrappers::ReceiverStream;
102
103 #[tokio::test]
104 async fn window_coalesces_items_into_one_batch() {
105 let (tx, rx) = mpsc::channel(16);
106 let mut batched = ReceiverStream::new(rx).batched(Duration::from_millis(60));
107
108 for i in 0..5 {
109 tx.send(i).await.unwrap();
110 }
111
112 let batch = tokio::time::timeout(Duration::from_secs(2), batched.next())
113 .await
114 .expect("timed out")
115 .expect("stream ended");
116 assert_eq!(batch, vec![0, 1, 2, 3, 4]);
117 drop(tx);
118 }
119
120 #[tokio::test]
121 async fn max_items_flushes_before_the_window_elapses() {
122 let (tx, rx) = mpsc::channel(16);
123 let mut batched = ReceiverStream::new(rx).batched_with_capacity(Duration::from_secs(30), 2);
124
125 for i in 0..4 {
126 tx.send(i).await.unwrap();
127 }
128
129 let batch = tokio::time::timeout(Duration::from_secs(2), batched.next())
131 .await
132 .expect("timed out")
133 .expect("stream ended");
134 assert_eq!(batch, vec![0, 1]);
135 drop(tx);
136 }
137
138 #[tokio::test]
139 async fn partial_batch_is_flushed_when_the_source_ends() {
140 let (tx, rx) = mpsc::channel(16);
141 let mut batched = ReceiverStream::new(rx).batched(Duration::from_secs(30));
142 tx.send(7).await.unwrap();
143 drop(tx);
144
145 let batch = tokio::time::timeout(Duration::from_secs(2), batched.next())
146 .await
147 .expect("timed out")
148 .expect("stream ended");
149 assert_eq!(batch, vec![7]);
150
151 let end = tokio::time::timeout(Duration::from_secs(2), batched.next())
152 .await
153 .expect("timed out");
154 assert!(end.is_none());
155 }
156
157 #[tokio::test]
158 async fn empty_batches_are_never_emitted() {
159 let (tx, rx) = mpsc::channel::<u8>(1);
160 let mut batched = ReceiverStream::new(rx).batched(Duration::from_millis(20));
161
162 let idle = tokio::time::timeout(Duration::from_millis(150), batched.next()).await;
163 assert!(idle.is_err(), "idle source must not emit empty batches");
164 drop(tx);
165 }
166}