use std::sync::{Arc, Weak};
use std::time::Duration;
use tracing::info;
use crate::auth::AuthConfig;
use crate::error::{KrafkaError, Result};
use crate::metadata::{ClusterMetadata, MetadataRecoveryStrategy};
use crate::metrics::{Metrics, MetricsSource};
use crate::network::{
ConnectionConfig, ConnectionPool, ProxyConfig, TransportConfig, TransportConfigBuilder,
};
#[derive(Clone)]
pub struct Kafka {
inner: Arc<KafkaInner>,
}
struct KafkaInner {
pool: Arc<ConnectionPool>,
metadata: Arc<ClusterMetadata>,
client_id: String,
request_timeout: Duration,
clients: parking_lot::Mutex<Vec<Weak<MetricsSource>>>,
}
impl std::fmt::Debug for Kafka {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Kafka")
.field("client_id", &self.inner.client_id)
.field("connections", &self.inner.pool.len())
.finish_non_exhaustive()
}
}
impl Kafka {
pub fn builder(bootstrap_servers: impl Into<String>) -> KafkaBuilder {
KafkaBuilder {
bootstrap_servers: bootstrap_servers.into(),
client_id: "krafka".to_string(),
security: None,
request_timeout: Duration::from_secs(30),
connect_timeout: crate::network::DEFAULT_CONNECT_TIMEOUT,
metadata_max_age: Duration::from_secs(300),
metadata_recovery_strategy: MetadataRecoveryStrategy::Rebootstrap,
metadata_recovery_rebootstrap_trigger: Duration::from_secs(300),
metadata_topic_cache_ttl: Some(Duration::from_secs(300)),
allow_auto_create_topics: false,
transport: TransportConfig::builder(),
#[cfg(feature = "test-broker")]
connector: None,
}
}
pub fn producer(&self) -> crate::producer::ProducerBuilder {
crate::producer::ProducerBuilder::new(self.clone())
}
pub fn consumer(&self, group_id: impl Into<String>) -> crate::consumer::ConsumerBuilder {
crate::consumer::ConsumerBuilder::new(self.clone(), Some(group_id.into()))
}
pub fn consumer_without_group(&self) -> crate::consumer::ConsumerBuilder {
crate::consumer::ConsumerBuilder::new(self.clone(), None)
}
pub fn share_consumer(
&self,
group_id: impl Into<String>,
) -> crate::share_consumer::ShareConsumerBuilder {
crate::share_consumer::ShareConsumerBuilder::new(self.clone(), group_id.into())
}
pub fn admin(&self) -> crate::admin::AdminClient {
crate::admin::AdminClient::new(self.clone())
}
pub async fn refresh_tls(&self) -> Result<()> {
self.inner.pool.refresh_tls().await
}
pub fn update_seed_brokers(&self, servers: Vec<String>) -> Result<()> {
self.inner.metadata.update_seed_brokers(servers)
}
pub async fn rebootstrap(&self) {
self.inner.metadata.rebootstrap().await;
}
pub fn metrics(&self) -> Metrics {
let mut total = Metrics::default();
self.inner
.clients
.lock()
.retain(|client| match client.upgrade() {
Some(source) => {
total.add_client(&source.client_counters());
true
}
None => false,
});
total.connections = self.inner.pool.metrics();
total
}
pub(crate) fn register_metrics(&self, source: &Arc<MetricsSource>) {
let mut clients = self.inner.clients.lock();
clients.retain(|client| client.strong_count() > 0);
clients.push(Arc::downgrade(source));
}
pub(crate) fn pool(&self) -> &Arc<ConnectionPool> {
&self.inner.pool
}
pub(crate) fn metadata(&self) -> &Arc<ClusterMetadata> {
&self.inner.metadata
}
pub(crate) fn client_id(&self) -> &str {
&self.inner.client_id
}
pub(crate) fn request_timeout(&self) -> Duration {
self.inner.request_timeout
}
#[cfg(test)]
pub(crate) fn detached() -> Self {
let pool = Arc::new(ConnectionPool::new(ConnectionConfig::default()));
let metadata = Arc::new(ClusterMetadata::new(
vec!["localhost:9092".to_string()],
Arc::clone(&pool),
Duration::from_secs(300),
));
Self {
inner: Arc::new(KafkaInner {
pool,
metadata,
client_id: "krafka".to_string(),
request_timeout: Duration::from_secs(30),
clients: parking_lot::Mutex::default(),
}),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct CloseOptions {
pub(crate) timeout: Option<Duration>,
pub(crate) group_membership_operation: GroupMembershipOperation,
}
impl CloseOptions {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn group_membership_operation(mut self, operation: GroupMembershipOperation) -> Self {
self.group_membership_operation = operation;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum GroupMembershipOperation {
#[default]
Default,
LeaveGroup,
RemainInGroup,
}
#[must_use = "builders do nothing until .connect().await is called"]
pub struct KafkaBuilder {
bootstrap_servers: String,
client_id: String,
security: Option<AuthConfig>,
request_timeout: Duration,
connect_timeout: Duration,
metadata_max_age: Duration,
metadata_recovery_strategy: MetadataRecoveryStrategy,
metadata_recovery_rebootstrap_trigger: Duration,
metadata_topic_cache_ttl: Option<Duration>,
allow_auto_create_topics: bool,
transport: TransportConfigBuilder,
#[cfg(feature = "test-broker")]
connector: Option<crate::network::connector::Connector>,
}
impl std::fmt::Debug for KafkaBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("KafkaBuilder")
.field("bootstrap_servers", &self.bootstrap_servers)
.field("client_id", &self.client_id)
.finish_non_exhaustive()
}
}
impl KafkaBuilder {
pub fn client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = client_id.into();
self
}
pub fn security(mut self, security: AuthConfig) -> Self {
self.security = Some(security);
self
}
pub fn request_timeout(mut self, timeout: Duration) -> Self {
self.request_timeout = timeout;
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn metadata_max_age(mut self, age: Duration) -> Self {
self.metadata_max_age = age;
self
}
pub fn metadata_recovery_strategy(mut self, strategy: MetadataRecoveryStrategy) -> Self {
self.metadata_recovery_strategy = strategy;
self
}
pub fn metadata_recovery_rebootstrap_trigger(mut self, duration: Duration) -> Self {
self.metadata_recovery_rebootstrap_trigger = duration;
self
}
pub fn metadata_topic_cache_ttl(mut self, ttl: Option<Duration>) -> Self {
self.metadata_topic_cache_ttl = ttl;
self
}
pub fn allow_auto_create_topics(mut self, allow: bool) -> Self {
self.allow_auto_create_topics = allow;
self
}
pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
self.transport = self.transport.proxy(proxy);
self
}
pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
self.transport = self.transport.tcp_nodelay(enabled);
self
}
pub fn tcp_keepalive(mut self, interval: Option<Duration>) -> Self {
self.transport = self.transport.tcp_keepalive(interval);
self
}
pub fn max_response_size(mut self, bytes: usize) -> Self {
self.transport = self.transport.max_response_size(bytes);
self
}
pub fn max_in_flight_requests(mut self, max: usize) -> Self {
self.transport = self.transport.max_in_flight_requests(max);
self
}
pub fn socket_send_buffer(mut self, bytes: Option<usize>) -> Self {
self.transport = self.transport.socket_send_buffer(bytes);
self
}
pub fn socket_receive_buffer(mut self, bytes: Option<usize>) -> Self {
self.transport = self.transport.socket_receive_buffer(bytes);
self
}
pub fn connection_attempt_delay(mut self, delay: Duration) -> Self {
self.transport = self.transport.connection_attempt_delay(delay);
self
}
pub fn connections_max_idle(mut self, max_idle: Option<Duration>) -> Self {
self.transport = self.transport.connections_max_idle(max_idle);
self
}
pub fn max_connections(mut self, limit: Option<usize>) -> Self {
self.transport = self.transport.max_connections(limit);
self
}
pub fn tls_reload_interval(mut self, interval: Option<Duration>) -> Self {
self.transport = self.transport.tls_reload_interval(interval);
self
}
#[cfg(feature = "test-broker")]
pub(crate) fn connector(mut self, connector: crate::network::connector::Connector) -> Self {
self.connector = Some(connector);
self
}
pub async fn connect(self) -> Result<Kafka> {
if self.bootstrap_servers.trim().is_empty() {
return Err(KrafkaError::config("bootstrap_servers is required"));
}
if self.client_id.len() > i16::MAX as usize {
return Err(KrafkaError::config(format!(
"client_id is {} bytes, exceeding the Kafka wire limit of {}",
self.client_id.len(),
i16::MAX
)));
}
if let Some(security) = &self.security {
security.validate()?;
}
let transport = self.transport.build()?;
let bootstrap_servers = crate::util::parse_bootstrap_servers(&self.bootstrap_servers)?;
let mut pool_config = transport.apply(
ConnectionConfig::builder()
.client_id(&self.client_id)
.request_timeout(self.request_timeout)
.connect_timeout(self.connect_timeout),
);
if let Some(security) = self.security {
pool_config = pool_config.auth(security);
}
let mut pool_config = pool_config.build()?;
#[cfg(feature = "test-broker")]
{
pool_config.connector = self.connector;
}
pool_config.init_tls().await?;
let pool = transport.build_pool(pool_config);
let metadata =
ClusterMetadata::new(bootstrap_servers, Arc::clone(&pool), self.metadata_max_age)
.with_recovery_strategy(self.metadata_recovery_strategy)
.with_rebootstrap_trigger(self.metadata_recovery_rebootstrap_trigger)
.with_auto_create_topics(self.allow_auto_create_topics);
let metadata = Arc::new(match self.metadata_topic_cache_ttl {
Some(ttl) => metadata.with_topic_cache_ttl(ttl),
None => metadata.with_topic_cache_ttl_disabled(),
});
metadata.refresh().await?;
info!(
bootstrap_servers = %self.bootstrap_servers,
brokers = metadata.brokers().len(),
"connected"
);
Ok(Kafka {
inner: Arc::new(KafkaInner {
pool,
metadata,
client_id: self.client_id,
request_timeout: self.request_timeout,
clients: parking_lot::Mutex::default(),
}),
})
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[tokio::test]
async fn an_empty_bootstrap_list_is_a_config_error() {
let err = Kafka::builder("").connect().await.unwrap_err();
assert!(matches!(err, KrafkaError::Config { .. }), "{err:?}");
assert!(err.to_string().contains("bootstrap_servers"), "{err}");
}
#[tokio::test]
async fn empty_sasl_credentials_are_a_config_error_naming_the_setting() {
let err = Kafka::builder("127.0.0.1:1")
.security(AuthConfig::sasl_plain("", "secret"))
.connect()
.await
.unwrap_err();
assert!(matches!(err, KrafkaError::Config { .. }), "{err:?}");
assert!(err.to_string().contains("username"), "{err}");
}
#[tokio::test]
async fn an_invalid_transport_setting_is_a_config_error() {
let err = Kafka::builder("127.0.0.1:1")
.max_in_flight_requests(0)
.connect()
.await
.unwrap_err();
assert!(err.to_string().contains("max_in_flight_requests"), "{err}");
}
#[tokio::test]
async fn a_request_timeout_below_connect_timeout_names_the_setting() {
let err = Kafka::builder("127.0.0.1:1")
.request_timeout(Duration::from_secs(2))
.connect()
.await
.unwrap_err();
assert!(err.to_string().contains("connect_timeout"), "{err}");
let err = Kafka::builder("127.0.0.1:1")
.request_timeout(Duration::from_secs(2))
.connect_timeout(Duration::from_secs(2))
.connect()
.await
.unwrap_err();
assert!(!err.to_string().contains("connect_timeout"), "{err}");
}
#[tokio::test]
async fn an_unreachable_cluster_fails_with_network_or_timeout() {
let err = Kafka::builder("127.0.0.1:1")
.request_timeout(Duration::from_secs(2))
.connect_timeout(Duration::from_secs(1))
.connect()
.await
.unwrap_err();
assert!(
matches!(err, KrafkaError::Network(_) | KrafkaError::Timeout { .. }),
"{err:?}"
);
}
}