1use std::sync::Arc;
4use std::time::Duration;
5
6use rskit_errors::{AppError, AppResult, ErrorCode};
7use rskit_stream::SpawnedTask;
8
9use crate::message::Message;
10use crate::traits::MessageProducer;
11
12#[derive(Debug, Clone)]
14pub struct BatchConfig {
15 pub max_size: usize,
17 pub max_wait: Duration,
19 pub max_bytes: Option<u64>,
22}
23
24impl Default for BatchConfig {
25 fn default() -> Self {
26 Self {
27 max_size: 100,
28 max_wait: Duration::from_secs(5),
29 max_bytes: None,
30 }
31 }
32}
33
34impl BatchConfig {
35 pub fn validate(&self) -> AppResult<()> {
37 if self.max_size == 0 {
38 return Err(AppError::new(
39 ErrorCode::InvalidInput,
40 "batch max_size must be greater than zero",
41 ));
42 }
43 if self.max_wait.is_zero() {
44 return Err(AppError::new(
45 ErrorCode::InvalidInput,
46 "batch max_wait must be greater than zero",
47 ));
48 }
49 Ok(())
50 }
51}
52
53pub struct BatchProducer<T: Send + Sync + Clone + 'static> {
62 producer: Arc<dyn MessageProducer<T>>,
63 config: BatchConfig,
64 buffer: Arc<tokio::sync::Mutex<Vec<Message<T>>>>,
65 flush_task: Arc<tokio::sync::Mutex<Option<SpawnedTask>>>,
66}
67
68impl<T: Send + Sync + Clone + 'static> BatchProducer<T> {
69 pub fn new(
79 producer: Arc<dyn MessageProducer<T>>,
80 _topic: String,
81 config: BatchConfig,
82 ) -> AppResult<Self> {
83 config.validate()?;
84 let buffer: Arc<tokio::sync::Mutex<Vec<Message<T>>>> =
85 Arc::new(tokio::sync::Mutex::new(Vec::new()));
86
87 let flush_buffer = buffer.clone();
88 let flush_producer = producer.clone();
89 let max_wait = config.max_wait;
90
91 let task = SpawnedTask::spawn(move |cancel| async move {
92 let mut interval = tokio::time::interval(max_wait);
93 interval.tick().await;
95 loop {
96 tokio::select! {
97 () = cancel.cancelled() => break,
98 _ = interval.tick() => {
99 let msgs = {
100 let mut buf = flush_buffer.lock().await;
101 std::mem::take(&mut *buf)
102 };
103 if !msgs.is_empty()
104 && let Err(e) = flush_producer.send_batch(msgs).await {
105 ::tracing::error!(error = %e, "batch periodic flush failed");
106 }
107 }
108 }
109 }
110 });
111
112 Ok(Self {
113 producer,
114 config,
115 buffer,
116 flush_task: Arc::new(tokio::sync::Mutex::new(Some(task))),
117 })
118 }
119
120 pub async fn send(&self, msg: Message<T>) -> AppResult<()> {
122 let should_flush = {
123 let mut buf = self.buffer.lock().await;
124 buf.push(msg);
125 buf.len() >= self.config.max_size
126 };
127 if should_flush {
128 self.flush().await?;
129 }
130 Ok(())
131 }
132
133 pub async fn flush(&self) -> AppResult<()> {
135 let msgs = {
136 let mut buf = self.buffer.lock().await;
137 std::mem::take(&mut *buf)
138 };
139 if !msgs.is_empty() {
140 self.producer.send_batch(msgs).await?;
141 }
142 Ok(())
143 }
144
145 pub async fn close(&self) -> AppResult<()> {
147 self.flush().await?;
148 let task = {
149 let mut t = self.flush_task.lock().await;
150 t.take()
151 };
152 if let Some(task) = task {
153 task.shutdown(Duration::from_secs(10)).await;
154 }
155 Ok(())
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use async_trait::async_trait;
163
164 struct RecordingProducer<T: Clone + Send + Sync + 'static> {
166 sent: Arc<tokio::sync::Mutex<Vec<Message<T>>>>,
167 }
168
169 impl<T: Clone + Send + Sync + 'static> RecordingProducer<T> {
170 fn new() -> (Self, Arc<tokio::sync::Mutex<Vec<Message<T>>>>) {
171 let sent = Arc::new(tokio::sync::Mutex::new(Vec::new()));
172 (Self { sent: sent.clone() }, sent)
173 }
174 }
175
176 #[async_trait]
177 impl<T: Clone + Send + Sync + 'static> MessageProducer<T> for RecordingProducer<T> {
178 async fn send(&self, msg: Message<T>) -> AppResult<()> {
179 self.sent.lock().await.push(msg);
180 Ok(())
181 }
182
183 async fn send_batch(&self, msgs: Vec<Message<T>>) -> AppResult<()> {
184 self.sent.lock().await.extend(msgs);
185 Ok(())
186 }
187
188 async fn flush(&self, _timeout: Duration) -> AppResult<()> {
189 Ok(())
190 }
191 }
192
193 #[tokio::test]
194 async fn batch_flush_on_size() {
195 let (producer, sent) = RecordingProducer::<String>::new();
196 let batch = BatchProducer::new(
197 Arc::new(producer),
198 "topic".to_string(),
199 BatchConfig {
200 max_size: 3,
201 max_wait: Duration::from_mins(1),
202 max_bytes: None,
203 },
204 )
205 .unwrap();
206
207 batch
208 .send(Message::new("topic", "a".to_string()))
209 .await
210 .unwrap();
211 batch
212 .send(Message::new("topic", "b".to_string()))
213 .await
214 .unwrap();
215 assert_eq!(sent.lock().await.len(), 0);
216
217 batch
219 .send(Message::new("topic", "c".to_string()))
220 .await
221 .unwrap();
222 assert_eq!(sent.lock().await.len(), 3);
223
224 batch.close().await.unwrap();
225 }
226
227 #[tokio::test(start_paused = true)]
228 async fn batch_flush_on_time() {
229 let (producer, sent) = RecordingProducer::<String>::new();
230 let config = BatchConfig {
231 max_size: 100,
232 max_wait: Duration::from_millis(100),
233 max_bytes: None,
234 };
235 let batch = BatchProducer::new(Arc::new(producer), "topic".to_string(), config).unwrap();
236 tokio::task::yield_now().await;
237
238 batch
239 .send(Message::new("topic", "a".to_string()))
240 .await
241 .unwrap();
242 batch
243 .send(Message::new("topic", "b".to_string()))
244 .await
245 .unwrap();
246 assert_eq!(sent.lock().await.len(), 0);
247
248 tokio::time::advance(Duration::from_millis(101)).await;
249 tokio::task::yield_now().await;
250 tokio::task::yield_now().await;
251
252 assert_eq!(sent.lock().await.len(), 2);
253 batch.close().await.unwrap();
254 }
255
256 #[tokio::test]
257 async fn batch_flush_on_close() {
258 let (producer, sent) = RecordingProducer::<String>::new();
259 let batch = BatchProducer::new(
260 Arc::new(producer),
261 "topic".to_string(),
262 BatchConfig {
263 max_size: 100,
264 max_wait: Duration::from_mins(1),
265 max_bytes: None,
266 },
267 )
268 .unwrap();
269
270 batch
271 .send(Message::new("topic", "a".to_string()))
272 .await
273 .unwrap();
274 batch
275 .send(Message::new("topic", "b".to_string()))
276 .await
277 .unwrap();
278 assert_eq!(sent.lock().await.len(), 0);
279
280 batch.close().await.unwrap();
281 assert_eq!(sent.lock().await.len(), 2);
282 }
283
284 #[test]
285 fn batch_config_rejects_unbounded_zero_values() {
286 assert!(
287 BatchConfig {
288 max_size: 0,
289 ..BatchConfig::default()
290 }
291 .validate()
292 .is_err()
293 );
294 assert!(
295 BatchConfig {
296 max_wait: Duration::ZERO,
297 ..BatchConfig::default()
298 }
299 .validate()
300 .is_err()
301 );
302 }
303}