use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::debug;
use crate::connection::AeroConnection;
use crate::consumer::{AeroConsumer, ConsumerConfig};
use crate::error::AeroError;
use crate::producer::{AeroProducer, ProducerConfig};
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub bootstrap_servers: Vec<String>,
pub token: Option<String>,
pub connect_timeout: Duration,
}
impl Default for ClientConfig {
fn default() -> Self {
Self {
bootstrap_servers: vec!["127.0.0.1:9091".to_string()],
token: None,
connect_timeout: Duration::from_secs(5),
}
}
}
impl ClientConfig {
pub fn builder() -> ClientConfigBuilder {
ClientConfigBuilder::default()
}
}
#[derive(Default)]
pub struct ClientConfigBuilder {
config: ClientConfig,
}
impl ClientConfigBuilder {
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 connect_timeout(mut self, timeout: Duration) -> Self {
self.config.connect_timeout = timeout;
self
}
pub fn build(self) -> Result<ClientConfig, AeroError> {
if self.config.bootstrap_servers.is_empty() {
return Err(AeroError::InvalidConfiguration(
"bootstrap_servers cannot be empty".into(),
));
}
Ok(self.config)
}
}
#[derive(Clone)]
pub struct AeroClient {
config: Arc<ClientConfig>,
pool: Arc<RwLock<HashMap<String, AeroConnection>>>,
default_producer: Arc<RwLock<Option<AeroProducer>>>,
}
impl AeroClient {
pub fn new(config: ClientConfig) -> Self {
Self {
config: Arc::new(config),
pool: Arc::new(RwLock::new(HashMap::new())),
default_producer: Arc::new(RwLock::new(None)),
}
}
pub fn connect(addr: impl Into<String>) -> Result<Self, AeroError> {
let config = ClientConfig::builder().bootstrap_server(addr).build()?;
Ok(Self::new(config))
}
pub async fn get_connection(&self, broker_addr: &str) -> Result<AeroConnection, AeroError> {
{
let read = self.pool.read().await;
if let Some(conn) = read.get(broker_addr) {
if conn.is_alive() {
return Ok(conn.clone());
}
}
}
let mut write = self.pool.write().await;
if let Some(conn) = write.get(broker_addr) {
if conn.is_alive() {
return Ok(conn.clone());
}
}
debug!(addr = %broker_addr, "Establishing new pooled connection");
let conn = AeroConnection::connect(
broker_addr,
self.config.token.as_deref(),
self.config.connect_timeout,
)
.await?;
write.insert(broker_addr.to_string(), conn.clone());
Ok(conn)
}
pub fn producer(&self) -> AeroProducer {
let config = ProducerConfig::builder()
.bootstrap_servers(self.config.bootstrap_servers.clone())
.token(self.config.token.clone().unwrap_or_default())
.connect_timeout(self.config.connect_timeout)
.build()
.expect("valid producer configuration");
AeroProducer::new(config)
}
pub fn producer_with_config(&self, config: ProducerConfig) -> AeroProducer {
AeroProducer::new(config)
}
pub fn consumer(&self, topic: impl Into<String>, partition: u32) -> AeroConsumer {
let config = ConsumerConfig::builder()
.bootstrap_servers(self.config.bootstrap_servers.clone())
.token(self.config.token.clone().unwrap_or_default())
.topic(topic)
.partition(partition)
.connect_timeout(self.config.connect_timeout)
.build()
.expect("valid consumer configuration");
AeroConsumer::new(config)
}
pub fn consumer_with_config(&self, config: ConsumerConfig) -> AeroConsumer {
AeroConsumer::new(config)
}
pub async fn publish(
&self,
topic: &str,
partition: u32,
payload: &[u8],
) -> Result<u64, AeroError> {
let producer = {
let read = self.default_producer.read().await;
if let Some(p) = &*read {
p.clone()
} else {
drop(read);
let mut write = self.default_producer.write().await;
if let Some(p) = &*write {
p.clone()
} else {
let p = self.producer();
*write = Some(p.clone());
p
}
}
};
producer.send(topic, partition, payload).await
}
}