use crate::error::LaserError;
use crate::laser::Laser;
use iggy::prelude::{HeaderKey, HeaderValue, IggyMessage};
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{Mutex, Notify};
pub const DEFAULT_MAX_RECORDS: usize = 512;
pub const DEFAULT_MAX_BYTES: usize = 1024 * 1024;
pub const DEFAULT_LINGER: Duration = Duration::from_millis(5);
pub const MIN_LINGER: Duration = Duration::from_millis(1);
pub struct BatchingProducerBuilder {
laser: Laser,
stream: String,
topic: String,
partition_key: Option<String>,
max_records: usize,
max_bytes: usize,
linger: Duration,
}
impl BatchingProducerBuilder {
pub(crate) fn new(laser: Laser, stream: String, topic: String) -> Self {
Self {
laser,
stream,
topic,
partition_key: None,
max_records: DEFAULT_MAX_RECORDS,
max_bytes: DEFAULT_MAX_BYTES,
linger: DEFAULT_LINGER,
}
}
#[must_use]
pub fn max_records(mut self, n: usize) -> Self {
self.max_records = n.max(1);
self
}
#[must_use]
pub fn max_bytes(mut self, payload: usize) -> Self {
self.max_bytes = payload.max(1);
self
}
#[must_use]
pub fn linger(mut self, linger: Duration) -> Self {
self.linger = linger;
self
}
#[must_use]
pub fn partition_key(mut self, key: impl Into<String>) -> Self {
self.partition_key = Some(key.into());
self
}
pub fn build(self) -> BatchingProducer {
let inner = Arc::new(Inner {
laser: self.laser,
stream: self.stream,
topic: self.topic,
partition_key: self.partition_key,
max_records: self.max_records,
max_bytes: self.max_bytes,
queue: Mutex::new(Queue::default()),
});
let shutdown = Arc::new(Notify::new());
let timer = {
let inner = Arc::clone(&inner);
let shutdown = Arc::clone(&shutdown);
let linger = self.linger.max(MIN_LINGER);
tokio::spawn(async move {
let mut ticker = tokio::time::interval(linger);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
_ = ticker.tick() => {
if let Err(error) = inner.flush().await {
tracing::warn!(%error, "linger flush failed (error: {error})");
}
}
_ = shutdown.notified() => {
if let Err(error) = inner.flush().await {
tracing::warn!(%error, "shutdown flush failed (error: {error})");
}
break;
}
}
}
})
};
BatchingProducer {
inner,
timer: Some(timer),
shutdown,
}
}
}
pub struct BatchingProducer {
inner: Arc<Inner>,
timer: Option<tokio::task::JoinHandle<()>>,
shutdown: Arc<Notify>,
}
#[derive(Default)]
struct Queue {
messages: Vec<IggyMessage>,
payload: usize,
}
struct Inner {
laser: Laser,
stream: String,
topic: String,
partition_key: Option<String>,
max_records: usize,
max_bytes: usize,
queue: Mutex<Queue>,
}
impl BatchingProducer {
pub async fn send(
&self,
payload: impl Into<Vec<u8>>,
headers: BTreeMap<HeaderKey, HeaderValue>,
) -> Result<(), LaserError> {
let payload = payload.into();
let message = IggyMessage::builder()
.payload(payload.into())
.user_headers(headers)
.build()?;
let flush_now = {
let mut queue = self.inner.queue.lock().await;
queue.payload += message.payload.len();
queue.messages.push(message);
queue.messages.len() >= self.inner.max_records || queue.payload >= self.inner.max_bytes
};
if flush_now {
self.inner.flush().await?;
}
Ok(())
}
pub async fn flush(&self) -> Result<(), LaserError> {
self.inner.flush().await
}
pub async fn close(mut self) -> Result<(), LaserError> {
self.shutdown.notify_one();
if let Some(timer) = self.timer.take() {
let _ = timer.await;
}
self.inner.flush().await
}
}
impl Inner {
async fn flush(&self) -> Result<(), LaserError> {
let batch = {
let mut queue = self.queue.lock().await;
if queue.messages.is_empty() {
return Ok(());
}
queue.payload = 0;
std::mem::take(&mut queue.messages)
};
self.laser
.send_batch_on(
&self.stream,
&self.topic,
batch,
self.partition_key.as_deref(),
)
.await
}
}
impl Drop for BatchingProducer {
fn drop(&mut self) {
if self.timer.take().is_some() {
self.shutdown.notify_one();
}
}
}