Skip to main content

aerostream_client/
client.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4
5use tokio::sync::RwLock;
6use tracing::debug;
7
8use crate::connection::AeroConnection;
9use crate::consumer::{AeroConsumer, ConsumerConfig};
10use crate::error::AeroError;
11use crate::producer::{AeroProducer, ProducerConfig};
12
13/// Global configuration options for the AeroStream client.
14#[derive(Debug, Clone)]
15pub struct ClientConfig {
16    /// Broker endpoint addresses.
17    pub bootstrap_servers: Vec<String>,
18    /// Optional authentication token.
19    pub token: Option<String>,
20    /// Connection timeout.
21    pub connect_timeout: Duration,
22}
23
24impl Default for ClientConfig {
25    fn default() -> Self {
26        Self {
27            bootstrap_servers: vec!["127.0.0.1:9091".to_string()],
28            token: None,
29            connect_timeout: Duration::from_secs(5),
30        }
31    }
32}
33
34impl ClientConfig {
35    /// Create a fluent builder for ClientConfig.
36    pub fn builder() -> ClientConfigBuilder {
37        ClientConfigBuilder::default()
38    }
39}
40
41/// Fluent builder for constructing `ClientConfig` instances.
42#[derive(Default)]
43pub struct ClientConfigBuilder {
44    config: ClientConfig,
45}
46
47impl ClientConfigBuilder {
48    /// Set the list of bootstrap broker endpoints.
49    pub fn bootstrap_servers(mut self, servers: Vec<String>) -> Self {
50        self.config.bootstrap_servers = servers;
51        self
52    }
53
54    /// Add a single bootstrap broker endpoint.
55    pub fn bootstrap_server(mut self, server: impl Into<String>) -> Self {
56        self.config.bootstrap_servers = vec![server.into()];
57        self
58    }
59
60    /// Configure authentication bearer token.
61    pub fn token(mut self, token: impl Into<String>) -> Self {
62        self.config.token = Some(token.into());
63        self
64    }
65
66    /// Set connection timeout duration.
67    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
68        self.config.connect_timeout = timeout;
69        self
70    }
71
72    /// Validate and construct ClientConfig.
73    pub fn build(self) -> Result<ClientConfig, AeroError> {
74        if self.config.bootstrap_servers.is_empty() {
75            return Err(AeroError::InvalidConfiguration(
76                "bootstrap_servers cannot be empty".into(),
77            ));
78        }
79        Ok(self.config)
80    }
81}
82
83/// High-level client managing connection pools and orchestrating producers and consumers.
84#[derive(Clone)]
85pub struct AeroClient {
86    config: Arc<ClientConfig>,
87    pool: Arc<RwLock<HashMap<String, AeroConnection>>>,
88    default_producer: Arc<RwLock<Option<AeroProducer>>>,
89}
90
91impl AeroClient {
92    /// Initialize a new client with the provided configuration.
93    pub fn new(config: ClientConfig) -> Self {
94        Self {
95            config: Arc::new(config),
96            pool: Arc::new(RwLock::new(HashMap::new())),
97            default_producer: Arc::new(RwLock::new(None)),
98        }
99    }
100
101    /// Connect using default configuration for a single broker endpoint.
102    pub fn connect(addr: impl Into<String>) -> Result<Self, AeroError> {
103        let config = ClientConfig::builder().bootstrap_server(addr).build()?;
104        Ok(Self::new(config))
105    }
106
107    /// Retrieve an existing pooled connection or establish a new connection.
108    pub async fn get_connection(&self, broker_addr: &str) -> Result<AeroConnection, AeroError> {
109        {
110            let read = self.pool.read().await;
111            if let Some(conn) = read.get(broker_addr) {
112                if conn.is_alive() {
113                    return Ok(conn.clone());
114                }
115            }
116        }
117
118        let mut write = self.pool.write().await;
119        if let Some(conn) = write.get(broker_addr) {
120            if conn.is_alive() {
121                return Ok(conn.clone());
122            }
123        }
124
125        debug!(addr = %broker_addr, "Establishing new pooled connection");
126        let conn = AeroConnection::connect(
127            broker_addr,
128            self.config.token.as_deref(),
129            self.config.connect_timeout,
130        )
131        .await?;
132
133        write.insert(broker_addr.to_string(), conn.clone());
134        Ok(conn)
135    }
136
137    /// Create an `AeroProducer` inheriting the client's bootstrap servers and auth token.
138    pub fn producer(&self) -> AeroProducer {
139        let config = ProducerConfig::builder()
140            .bootstrap_servers(self.config.bootstrap_servers.clone())
141            .token(self.config.token.clone().unwrap_or_default())
142            .connect_timeout(self.config.connect_timeout)
143            .build()
144            .expect("valid producer configuration");
145        AeroProducer::new(config)
146    }
147
148    /// Create an `AeroProducer` with customized configuration.
149    pub fn producer_with_config(&self, config: ProducerConfig) -> AeroProducer {
150        AeroProducer::new(config)
151    }
152
153    /// Create an `AeroConsumer` for a topic and partition.
154    pub fn consumer(&self, topic: impl Into<String>, partition: u32) -> AeroConsumer {
155        let config = ConsumerConfig::builder()
156            .bootstrap_servers(self.config.bootstrap_servers.clone())
157            .token(self.config.token.clone().unwrap_or_default())
158            .topic(topic)
159            .partition(partition)
160            .connect_timeout(self.config.connect_timeout)
161            .build()
162            .expect("valid consumer configuration");
163        AeroConsumer::new(config)
164    }
165
166    /// Create an `AeroConsumer` with customized configuration.
167    pub fn consumer_with_config(&self, config: ConsumerConfig) -> AeroConsumer {
168        AeroConsumer::new(config)
169    }
170
171    /// Publish a message directly through the client's shared producer.
172    pub async fn publish(
173        &self,
174        topic: &str,
175        partition: u32,
176        payload: &[u8],
177    ) -> Result<u64, AeroError> {
178        let producer = {
179            let read = self.default_producer.read().await;
180            if let Some(p) = &*read {
181                p.clone()
182            } else {
183                drop(read);
184                let mut write = self.default_producer.write().await;
185                if let Some(p) = &*write {
186                    p.clone()
187                } else {
188                    let p = self.producer();
189                    *write = Some(p.clone());
190                    p
191                }
192            }
193        };
194
195        producer.send(topic, partition, payload).await
196    }
197}