1use crate::error::{LaserError, PublishFailure};
2use crate::laser::Laser;
3use iggy::prelude::{HeaderKey, HeaderValue, IggyMessage};
4use std::collections::BTreeMap;
5use std::sync::Arc;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::time::Duration;
8use tokio::sync::{Mutex, Notify};
9
10pub const DEFAULT_MAX_RECORDS: usize = 512;
12pub const DEFAULT_MAX_BYTES: usize = 1024 * 1024;
14pub const DEFAULT_LINGER: Duration = Duration::from_millis(5);
16pub const MIN_LINGER: Duration = Duration::from_millis(1);
20
21pub struct BatchingProducerBuilder {
26 laser: Laser,
27 stream: String,
28 topic: String,
29 partition_key: Option<String>,
30 max_records: usize,
31 max_bytes: usize,
32 linger: Duration,
33}
34
35impl BatchingProducerBuilder {
36 pub(crate) fn new(laser: Laser, stream: String, topic: String) -> Self {
37 Self {
38 laser,
39 stream,
40 topic,
41 partition_key: None,
42 max_records: DEFAULT_MAX_RECORDS,
43 max_bytes: DEFAULT_MAX_BYTES,
44 linger: DEFAULT_LINGER,
45 }
46 }
47
48 #[must_use]
50 pub fn max_records(mut self, n: usize) -> Self {
51 self.max_records = n.max(1);
52 self
53 }
54
55 #[must_use]
57 pub fn max_bytes(mut self, payload: usize) -> Self {
58 self.max_bytes = payload.max(1);
59 self
60 }
61
62 #[must_use]
65 pub fn linger(mut self, linger: Duration) -> Self {
66 self.linger = linger;
67 self
68 }
69
70 #[must_use]
76 pub fn partition_key(mut self, key: impl Into<String>) -> Self {
77 self.partition_key = Some(key.into());
78 self
79 }
80
81 pub fn build(self) -> BatchingProducer {
83 let inner = Arc::new(Inner {
84 laser: self.laser,
85 stream: self.stream,
86 topic: self.topic,
87 partition_key: self.partition_key,
88 max_records: self.max_records,
89 max_bytes: self.max_bytes,
90 queue: Mutex::new(Queue::default()),
91 kept: Mutex::new(None),
92 failed: AtomicBool::new(false),
93 });
94 let shutdown = Arc::new(Notify::new());
95 let timer = {
96 let inner = Arc::clone(&inner);
97 let shutdown = Arc::clone(&shutdown);
98 let linger = self.linger.max(MIN_LINGER);
99 tokio::spawn(async move {
100 let mut ticker = tokio::time::interval(linger);
101 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
102 loop {
103 tokio::select! {
108 _ = ticker.tick() => inner.flush_on_timer().await,
109 _ = shutdown.notified() => {
110 inner.flush_on_timer().await;
111 break;
112 }
113 }
114 }
115 })
116 };
117 BatchingProducer {
118 inner,
119 timer: Some(timer),
120 shutdown,
121 }
122 }
123}
124
125pub struct BatchingProducer {
142 inner: Arc<Inner>,
143 timer: Option<tokio::task::JoinHandle<()>>,
144 shutdown: Arc<Notify>,
145}
146
147#[derive(Default)]
148struct Queue {
149 messages: Vec<IggyMessage>,
150 payload: usize,
151}
152
153#[derive(Clone, Copy)]
154enum Trigger {
155 Send,
156 Timer,
157 Caller,
158}
159
160struct Inner {
161 laser: Laser,
162 stream: String,
163 topic: String,
164 partition_key: Option<String>,
165 max_records: usize,
166 max_bytes: usize,
167 queue: Mutex<Queue>,
168 kept: Mutex<Option<LaserError>>,
171 failed: AtomicBool,
174}
175
176impl BatchingProducer {
177 pub async fn send(
184 &self,
185 payload: impl Into<Vec<u8>>,
186 headers: BTreeMap<HeaderKey, HeaderValue>,
187 ) -> Result<(), LaserError> {
188 let payload = payload.into();
189 let message = IggyMessage::builder()
190 .payload(payload.into())
191 .user_headers(headers)
192 .build()?;
193 if let Some(kept) = self.inner.take_kept().await {
194 return Err(refuse(kept, message, &self.inner.stream, &self.inner.topic));
195 }
196 let flush_now = {
197 let mut queue = self.inner.queue.lock().await;
198 queue.payload += message.payload.len();
199 queue.messages.push(message);
200 queue.messages.len() >= self.inner.max_records || queue.payload >= self.inner.max_bytes
201 };
202 if flush_now {
203 self.inner.flush(Trigger::Send).await?;
204 }
205 Ok(())
206 }
207
208 pub async fn flush(&self) -> Result<(), LaserError> {
211 self.inner.flush(Trigger::Caller).await
212 }
213
214 pub async fn close(mut self) -> Result<(), LaserError> {
220 self.shutdown.notify_one();
221 if let Some(timer) = self.timer.take() {
222 let _ = timer.await;
223 }
224 self.inner.flush(Trigger::Caller).await
225 }
226}
227
228impl Inner {
229 async fn flush(&self, trigger: Trigger) -> Result<(), LaserError> {
230 let mut kept = self.kept.lock().await;
231 let batch = {
232 let mut queue = self.queue.lock().await;
233 queue.payload = 0;
234 std::mem::take(&mut queue.messages)
235 };
236 let sent = if batch.is_empty() {
237 Ok(())
238 } else {
239 self.laser
240 .send_batch_on(
241 &self.stream,
242 &self.topic,
243 batch,
244 self.partition_key.as_deref(),
245 )
246 .await
247 .map(|_| ())
248 };
249 let settled = settle(trigger, &mut kept, sent);
250 self.failed.store(kept.is_some(), Ordering::Release);
251 settled
252 }
253
254 async fn flush_on_timer(&self) {
255 let _ = self.flush(Trigger::Timer).await;
257 }
258
259 async fn take_kept(&self) -> Option<LaserError> {
261 if !self.failed.load(Ordering::Acquire) {
262 return None;
263 }
264 let mut kept = self.kept.lock().await;
265 self.failed.store(false, Ordering::Release);
266 kept.take()
267 }
268}
269
270fn settle(
274 trigger: Trigger,
275 kept: &mut Option<LaserError>,
276 sent: Result<(), LaserError>,
277) -> Result<(), LaserError> {
278 match trigger {
279 Trigger::Send => sent,
280 Trigger::Timer => {
281 if let Err(error) = sent {
282 tracing::warn!(%error, "linger flush failed");
283 *kept = Some(match kept.take() {
284 Some(earlier) => merge(earlier, error),
285 None => error,
286 });
287 }
288 Ok(())
289 }
290 Trigger::Caller => match (kept.take(), sent) {
291 (None, sent) => sent,
292 (Some(earlier), Ok(())) => Err(earlier),
293 (Some(earlier), Err(error)) => Err(merge(earlier, error)),
294 },
295 }
296}
297
298fn refuse(kept: LaserError, record: IggyMessage, stream: &str, topic: &str) -> LaserError {
302 match kept {
303 LaserError::PublishFailed(mut failure) => {
304 failure.unconfirmed.push(record);
305 LaserError::PublishFailed(failure)
306 }
307 source => LaserError::PublishFailed(Box::new(PublishFailure {
308 source,
309 stream: stream.to_owned(),
310 topic: topic.to_owned(),
311 committed: Vec::new(),
312 unconfirmed: vec![record],
313 })),
314 }
315}
316
317fn merge(earlier: LaserError, later: LaserError) -> LaserError {
320 match (earlier, later) {
321 (LaserError::PublishFailed(mut first), LaserError::PublishFailed(next)) => {
322 let next = *next;
323 first.committed.extend(next.committed);
324 first.unconfirmed.extend(next.unconfirmed);
325 LaserError::PublishFailed(first)
326 }
327 (earlier, later) => {
328 tracing::warn!(error = %later, "a later batch flush failed while an earlier failure was kept");
329 earlier
330 }
331 }
332}
333
334impl Drop for BatchingProducer {
335 fn drop(&mut self) {
336 if self.timer.take().is_some() {
341 self.shutdown.notify_one();
342 }
343 }
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 fn producer() -> BatchingProducer {
351 let laser = Laser::from_client(iggy::prelude::IggyClient::default());
352 BatchingProducerBuilder::new(laser, "fleet".into(), "readings".into()).build()
353 }
354
355 fn publish_failure(payloads: &[&'static str]) -> LaserError {
356 LaserError::PublishFailed(Box::new(PublishFailure {
357 source: LaserError::Invalid("append refused".into()),
358 stream: "fleet".into(),
359 topic: "readings".into(),
360 committed: Vec::new(),
361 unconfirmed: payloads
362 .iter()
363 .map(|payload| {
364 IggyMessage::builder()
365 .payload((*payload).into())
366 .build()
367 .expect("a test message should build")
368 })
369 .collect(),
370 }))
371 }
372
373 fn unconfirmed(error: &LaserError) -> Vec<&[u8]> {
374 match error {
375 LaserError::PublishFailed(failure) => failure
376 .unconfirmed
377 .iter()
378 .map(|message| message.payload.as_ref())
379 .collect(),
380 other => panic!("expected a publish failure, got {other:?}"),
381 }
382 }
383
384 #[tokio::test]
385 async fn given_a_kept_timer_failure_when_flushing_then_should_report_it_once() {
386 let producer = producer();
387 *producer.inner.kept.lock().await = Some(LaserError::Invalid("failed batch".into()));
388 producer.inner.flush_on_timer().await;
389 assert!(matches!(
390 producer.flush().await,
391 Err(LaserError::Invalid(_))
392 ));
393 producer
394 .close()
395 .await
396 .expect("the reported failure is not repeated");
397 }
398
399 #[tokio::test]
400 async fn given_a_kept_timer_failure_when_sending_then_should_refuse_the_record_and_report_both()
401 {
402 let producer = producer();
403 *producer.inner.kept.lock().await = Some(publish_failure(&["a"]));
404 producer.inner.flush_on_timer().await;
405 let error = producer
406 .send("b", BTreeMap::new())
407 .await
408 .expect_err("the sender hears the kept failure");
409 assert_eq!(unconfirmed(&error), [b"a".as_slice(), b"b"]);
410 assert!(
411 producer.inner.queue.lock().await.messages.is_empty(),
412 "the refused record is not queued"
413 );
414 producer
415 .close()
416 .await
417 .expect("the reported failure is not repeated");
418 }
419
420 #[tokio::test]
421 async fn given_a_kept_failure_without_records_when_sending_then_should_list_the_refused_record()
422 {
423 let producer = producer();
424 *producer.inner.kept.lock().await = Some(LaserError::Invalid("failed batch".into()));
425 producer.inner.flush_on_timer().await;
426 let error = producer
427 .send("b", BTreeMap::new())
428 .await
429 .expect_err("the sender hears the kept failure");
430 assert_eq!(unconfirmed(&error), [b"b".as_slice()]);
431 let LaserError::PublishFailed(failure) = error else {
432 panic!("a publish failure")
433 };
434 assert!(matches!(failure.source, LaserError::Invalid(_)));
435 assert_eq!(
436 (failure.stream.as_str(), failure.topic.as_str()),
437 ("fleet", "readings")
438 );
439 }
440
441 #[tokio::test]
442 async fn given_a_kept_timer_failure_when_closing_then_should_not_report_success() {
443 let producer = producer();
444 *producer.inner.kept.lock().await = Some(LaserError::Invalid("failed batch".into()));
445 assert!(matches!(
446 producer.close().await,
447 Err(LaserError::Invalid(_))
448 ));
449 }
450
451 #[test]
452 fn given_a_failed_timer_flush_when_settled_then_should_keep_it_and_report_success() {
453 let mut kept = None;
454 settle(Trigger::Timer, &mut kept, Err(publish_failure(&["a"])))
455 .expect("the timer keeps the failure");
456 assert_eq!(
457 unconfirmed(kept.as_ref().expect("the failure is kept")),
458 [b"a"]
459 );
460 }
461
462 #[test]
463 fn given_two_failed_timer_flushes_when_settled_then_should_keep_the_records_of_both() {
464 let mut kept = None;
465 settle(Trigger::Timer, &mut kept, Err(publish_failure(&["a"])))
466 .expect("the timer keeps the first failure");
467 settle(Trigger::Timer, &mut kept, Err(publish_failure(&["b", "c"])))
468 .expect("the timer keeps the second failure");
469 assert_eq!(
470 unconfirmed(kept.as_ref().expect("the failures are kept")),
471 [b"a".as_slice(), b"b", b"c"]
472 );
473 }
474
475 #[test]
476 fn given_a_kept_failure_when_a_sender_flush_succeeds_then_should_not_report_it_to_the_sender() {
477 let mut kept = Some(publish_failure(&["a"]));
478 settle(Trigger::Send, &mut kept, Ok(())).expect("the sender hears only its own flush");
479 assert!(kept.is_some());
480 }
481
482 #[test]
483 fn given_a_kept_failure_when_the_caller_flush_also_fails_then_should_report_both_once() {
484 let mut kept = Some(publish_failure(&["a"]));
485 let error = settle(Trigger::Caller, &mut kept, Err(publish_failure(&["b"])))
486 .expect_err("the caller hears both failures");
487 assert_eq!(unconfirmed(&error), [b"a".as_slice(), b"b"]);
488 assert!(kept.is_none());
489 }
490
491 #[test]
492 fn given_a_kept_failure_when_the_caller_flush_succeeds_then_should_report_the_kept_one() {
493 let mut kept = Some(publish_failure(&["a"]));
494 let error = settle(Trigger::Caller, &mut kept, Ok(()))
495 .expect_err("the caller hears the kept failure");
496 assert_eq!(unconfirmed(&error), [b"a".as_slice()]);
497 assert!(kept.is_none());
498 }
499}