use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
use tracing::{debug, warn};
use crate::connection::AeroConnection;
use crate::error::AeroError;
#[derive(Debug, Clone)]
pub struct ProducerConfig {
pub bootstrap_servers: Vec<String>,
pub token: Option<String>,
pub max_retries: usize,
pub initial_backoff: Duration,
pub max_backoff: Duration,
pub backoff_multiplier: f64,
pub connect_timeout: Duration,
pub request_timeout: Duration,
}
impl Default for ProducerConfig {
fn default() -> Self {
Self {
bootstrap_servers: vec!["127.0.0.1:9091".to_string()],
token: None,
max_retries: 5,
initial_backoff: Duration::from_millis(50),
max_backoff: Duration::from_millis(2000),
backoff_multiplier: 2.0,
connect_timeout: Duration::from_secs(5),
request_timeout: Duration::from_secs(10),
}
}
}
impl ProducerConfig {
pub fn builder() -> ProducerConfigBuilder {
ProducerConfigBuilder::default()
}
}
#[derive(Default)]
pub struct ProducerConfigBuilder {
config: ProducerConfig,
}
impl ProducerConfigBuilder {
pub fn bootstrap_servers(mut self, servers: Vec<String>) -> Self {
self.config.bootstrap_servers = servers;
self
}
pub fn bootstrap_server(mut self, server: impl Into<String>) -> Self {
self.config.bootstrap_servers = vec![server.into()];
self
}
pub fn token(mut self, token: impl Into<String>) -> Self {
self.config.token = Some(token.into());
self
}
pub fn max_retries(mut self, retries: usize) -> Self {
self.config.max_retries = retries;
self
}
pub fn initial_backoff(mut self, backoff: Duration) -> Self {
self.config.initial_backoff = backoff;
self
}
pub fn max_backoff(mut self, backoff: Duration) -> Self {
self.config.max_backoff = backoff;
self
}
pub fn backoff_multiplier(mut self, mult: f64) -> Self {
self.config.backoff_multiplier = mult;
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.config.connect_timeout = timeout;
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.config.request_timeout = timeout;
self
}
pub fn build(self) -> Result<ProducerConfig, AeroError> {
if self.config.bootstrap_servers.is_empty() {
return Err(AeroError::InvalidConfiguration(
"bootstrap_servers cannot be empty".into(),
));
}
Ok(self.config)
}
}
fn compute_backoff(
attempt: usize,
initial: Duration,
max: Duration,
multiplier: f64,
) -> Duration {
let exponent = (attempt.saturating_sub(1)) as i32;
let base_ms = (initial.as_millis() as f64) * multiplier.powi(exponent);
let capped_ms = base_ms.min(max.as_millis() as f64);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
let jitter = 0.75 + ((nanos % 500) as f64 / 1000.0);
Duration::from_millis((capped_ms * jitter).max(1.0) as u64)
}
#[derive(Clone)]
pub struct AeroProducer {
config: Arc<ProducerConfig>,
connection: Arc<RwLock<Option<AeroConnection>>>,
}
impl AeroProducer {
pub fn new(config: ProducerConfig) -> Self {
Self {
config: Arc::new(config),
connection: Arc::new(RwLock::new(None)),
}
}
pub fn connect(addr: impl Into<String>) -> Self {
let config = ProducerConfig::builder()
.bootstrap_server(addr)
.build()
.expect("default config valid");
Self::new(config)
}
async fn get_or_connect(&self) -> Result<AeroConnection, AeroError> {
{
let read = self.connection.read().await;
if let Some(conn) = &*read {
if conn.is_alive() {
return Ok(conn.clone());
}
}
}
let mut write = self.connection.write().await;
if let Some(conn) = &*write {
if conn.is_alive() {
return Ok(conn.clone());
}
}
let mut last_err = AeroError::Disconnected;
for server in &self.config.bootstrap_servers {
match AeroConnection::connect(
server,
self.config.token.as_deref(),
self.config.connect_timeout,
)
.await
{
Ok(conn) => {
*write = Some(conn.clone());
return Ok(conn);
}
Err(e) => {
warn!(broker = %server, error = %e, "Failed connecting to broker");
last_err = e;
}
}
}
Err(last_err)
}
pub async fn send(
&self,
topic: &str,
partition: u32,
payload: &[u8],
) -> Result<u64, AeroError> {
let mut attempts = 0;
loop {
attempts += 1;
let conn = match self.get_or_connect().await {
Ok(c) => c,
Err(e) => {
if attempts > self.config.max_retries {
return Err(e);
}
let backoff = compute_backoff(
attempts,
self.config.initial_backoff,
self.config.max_backoff,
self.config.backoff_multiplier,
);
debug!(attempt = attempts, backoff = ?backoff, "Connection retry backoff");
tokio::time::sleep(backoff).await;
continue;
}
};
let send_fut = conn.produce(topic, partition, payload);
let send_res = tokio::time::timeout(self.config.request_timeout, send_fut).await;
match send_res {
Ok(Ok(offset)) => return Ok(offset),
Ok(Err(err)) => {
match &err {
AeroError::AuthFailed(_) | AeroError::OutOfOrder => return Err(err),
_ => {}
}
conn.mark_broken();
if attempts > self.config.max_retries {
return Err(err);
}
let backoff = compute_backoff(
attempts,
self.config.initial_backoff,
self.config.max_backoff,
self.config.backoff_multiplier,
);
debug!(attempt = attempts, error = %err, backoff = ?backoff, "Produce retry");
tokio::time::sleep(backoff).await;
}
Err(_) => {
conn.mark_broken();
if attempts > self.config.max_retries {
return Err(AeroError::Timeout("Produce request timed out".into()));
}
let backoff = compute_backoff(
attempts,
self.config.initial_backoff,
self.config.max_backoff,
self.config.backoff_multiplier,
);
tokio::time::sleep(backoff).await;
}
}
}
}
}