use std::collections::HashMap;
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use arc_swap::ArcSwap;
use kafka_conn::protocol::StrBytes;
use kafka_conn::protocol::messages::metadata_request::MetadataRequestTopic;
use kafka_conn::protocol::messages::{FindCoordinatorRequest, MetadataRequest, TopicName};
use kafka_conn::{ApiKey, Connection, ConnectionConfig, Error, ErrorCode, Result, Rpc};
use crate::pool::BrokerPool;
use crate::retry::RetryPolicy;
use crate::routing::{BrokerSelector, CoordinatorKind, Routing, routing};
use crate::snapshot::{BrokerInfo, MetadataSnapshot, PartitionInfo, TopicId, TopicInfo};
#[derive(Debug, Clone)]
pub struct ClusterConfig {
pub connection: ConnectionConfig,
pub retry: RetryPolicy,
pub refresh_interval: Duration,
pub max_staleness: Duration,
}
impl Default for ClusterConfig {
fn default() -> Self {
Self {
connection: ConnectionConfig::default(),
retry: RetryPolicy::default(),
refresh_interval: Duration::from_secs(30),
max_staleness: Duration::from_secs(300),
}
}
}
#[derive(Debug, Clone)]
pub struct Cluster {
inner: Arc<Inner>,
}
#[derive(Debug)]
struct Inner {
pool: BrokerPool,
config: ClusterConfig,
snapshot: ArcSwap<MetadataSnapshot>,
coordinators: Mutex<HashMap<(CoordinatorKind, String), i32>>,
}
impl Cluster {
pub async fn connect(
bootstrap: impl IntoIterator<Item = impl Into<String>>,
config: ClusterConfig,
) -> Result<Self> {
let pool = BrokerPool::new(bootstrap, config.connection.clone(), config.retry);
let cluster = Cluster {
inner: Arc::new(Inner {
pool,
config,
snapshot: ArcSwap::from_pointee(MetadataSnapshot::empty()),
coordinators: Mutex::new(HashMap::new()),
}),
};
cluster.refresh().await?;
cluster.spawn_refresh_task();
Ok(cluster)
}
pub fn snapshot(&self) -> Arc<MetadataSnapshot> {
self.inner.snapshot.load_full()
}
pub fn pool(&self) -> &BrokerPool {
&self.inner.pool
}
pub async fn negotiated_for<R: Rpc>(&self) -> Result<i16> {
self.inner.pool.any().await?.negotiated_for::<R>()
}
pub async fn refresh(&self) -> Result<Arc<MetadataSnapshot>> {
let connection = self.inner.pool.any().await?;
let response = connection.send(all_topics_request(&connection)).await?;
let snapshot = Arc::new(decode_metadata(response));
self.install(snapshot.clone());
Ok(snapshot)
}
pub async fn refresh_topics(&self, topics: &[&str]) -> Result<Arc<MetadataSnapshot>> {
if topics.is_empty() {
return Ok(self.snapshot());
}
let connection = self.inner.pool.any().await?;
let response = connection.send(topics_request(topics)).await?;
let fresh = decode_metadata(response);
let merged = Arc::new(self.snapshot().with_topics_merged(fresh.topics().to_vec()));
self.install(merged.clone());
Ok(merged)
}
pub async fn refresh_if_stale(&self) -> Result<Arc<MetadataSnapshot>> {
let snapshot = self.snapshot();
if snapshot.age() < self.inner.config.max_staleness && !snapshot.brokers().is_empty() {
return Ok(snapshot);
}
self.refresh().await
}
pub async fn leader_for(&self, topic: &str, partition: i32) -> Result<i32> {
if let Some(leader) = self.snapshot().leader_for(topic, partition) {
return Ok(leader);
}
let snapshot = self.refresh_topics(&[topic]).await?;
snapshot.leader_for(topic, partition).ok_or_else(|| {
match snapshot.topic(topic).and_then(|t| t.error) {
Some(code) => Error::from_code(code, Some(format!("topic {topic}"))),
None => Error::from_code(
ErrorCode::LeaderNotAvailable,
Some(format!("{topic}-{partition}")),
),
}
})
}
pub async fn coordinator_for(&self, group: &str) -> Result<i32> {
self.coordinator(CoordinatorKind::Group, group).await
}
pub async fn coordinator(&self, kind: CoordinatorKind, key: &str) -> Result<i32> {
let policy = self.inner.config.retry;
let started = std::time::Instant::now();
let mut attempt = 1;
loop {
let delay = policy.delay(attempt);
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
let error = match self.coordinator_once(kind, key).await {
Ok(node) => return Ok(node),
Err(error) => error,
};
let budget_left = if error.needs_coordinator_refresh() {
started.elapsed() < policy.coordinator_timeout
} else {
policy.should_retry(attempt)
};
if !error.retriable() || !budget_left {
return Err(error);
}
tracing::debug!(?kind, key, attempt, %error, "retrying FindCoordinator");
attempt = attempt.saturating_add(1);
}
}
async fn coordinator_once(&self, kind: CoordinatorKind, key: &str) -> Result<i32> {
let cache_key = (kind, key.to_owned());
if let Some(node) = self
.inner
.coordinators
.lock()
.ok()
.and_then(|map| map.get(&cache_key).copied())
{
return Ok(node);
}
let connection = self.inner.pool.any().await?;
let version = connection.negotiated_for::<FindCoordinatorRequest>()?;
let request = FindCoordinatorRequest::default().with_key_type(kind.key_type());
let request = if version >= 4 {
request.with_coordinator_keys(vec![StrBytes::from_string(key.to_owned())])
} else {
request.with_key(StrBytes::from_string(key.to_owned()))
};
let response = connection.send(request).await?;
let (node_id, error_code, message) = match response.coordinators.first() {
Some(coordinator) => (
coordinator.node_id.0,
coordinator.error_code,
coordinator.error_message.as_ref().map(|m| m.to_string()),
),
None => (
response.node_id.0,
response.error_code,
response.error_message.as_ref().map(|m| m.to_string()),
),
};
if let Some(code) = ErrorCode::from_code(error_code) {
return Err(Error::from_code(code, message));
}
if node_id < 0 {
return Err(Error::from_code(
ErrorCode::CoordinatorNotAvailable,
Some(key.to_owned()),
));
}
if let Ok(mut map) = self.inner.coordinators.lock() {
map.insert(cache_key, node_id);
}
Ok(node_id)
}
pub async fn controller(&self) -> Result<i32> {
if let Some(id) = self.snapshot().controller_id() {
return Ok(id);
}
self.refresh()
.await?
.controller_id()
.ok_or_else(|| Error::from_code(ErrorCode::NotController, None))
}
pub fn invalidate_coordinator(&self, kind: CoordinatorKind, key: &str) {
if let Ok(mut map) = self.inner.coordinators.lock() {
map.remove(&(kind, key.to_owned()));
}
}
pub fn invalidate(&self) {
self.install(Arc::new(MetadataSnapshot::empty()));
}
pub async fn send_any<R: Rpc + Clone>(&self, request: R) -> Result<R::Response> {
self.dispatch(Target::Any, request).await
}
pub async fn send_to_controller<R: Rpc + Clone>(&self, request: R) -> Result<R::Response> {
self.dispatch(Target::Controller, request).await
}
pub async fn send_to_node<R: Rpc + Clone>(
&self,
node_id: i32,
request: R,
) -> Result<R::Response> {
self.dispatch(Target::Node(node_id), request).await
}
pub async fn send_to_coordinator<R: Rpc + Clone>(
&self,
kind: CoordinatorKind,
key: &str,
request: R,
) -> Result<R::Response> {
self.dispatch(Target::Coordinator(kind, key.to_owned()), request)
.await
}
pub async fn send_to_leader<R: Rpc + Clone>(
&self,
topic: &str,
partition: i32,
request: R,
) -> Result<R::Response> {
self.dispatch(Target::Leader(topic.to_owned(), partition), request)
.await
}
pub async fn send_routed<R: Rpc + Clone>(&self, request: R) -> Result<R::Response> {
match routing(R::API_KEY) {
Routing::Any => self.send_any(request).await,
Routing::Controller => self.send_to_controller(request).await,
Routing::Coordinator(kind) => Err(Error::InvalidRequest(format!(
"{} is routed to a {kind:?} coordinator; use send_to_coordinator",
R::API_KEY
))),
Routing::Specific(BrokerSelector::Caller) => Err(Error::InvalidRequest(format!(
"{} is routed to one broker; use send_to_node",
R::API_KEY
))),
Routing::Specific(BrokerSelector::PartitionLeader) => {
Err(Error::InvalidRequest(format!(
"{} is routed to a partition leader; use send_to_leader",
R::API_KEY
)))
}
}
}
async fn dispatch<R: Rpc + Clone>(&self, target: Target, request: R) -> Result<R::Response> {
let policy = self.inner.config.retry;
let started = std::time::Instant::now();
let mut attempt = 1;
loop {
let delay = policy.delay(attempt);
if !delay.is_zero() {
tokio::time::sleep(delay).await;
}
let outcome = self.attempt(&target, request.clone()).await;
let error = match outcome {
Ok(response) => return Ok(response),
Err(error) => error,
};
if error.needs_metadata_refresh() {
self.on_stale_metadata(&target).await;
}
let coordinator_moved =
error.needs_coordinator_refresh() && matches!(&target, Target::Coordinator(..));
if coordinator_moved && let Target::Coordinator(kind, key) = &target {
self.invalidate_coordinator(*kind, key);
}
let budget_left = if coordinator_moved {
started.elapsed() < policy.coordinator_timeout
} else {
policy.should_retry(attempt)
};
if !error.retriable() || !budget_left {
return Err(error);
}
tracing::debug!(api = %R::API_KEY, attempt, %error, "retrying");
attempt = attempt.saturating_add(1);
}
}
async fn attempt<R: Rpc + Clone>(&self, target: &Target, request: R) -> Result<R::Response> {
let connection = self.resolve(target).await?;
connection.send(request).await
}
async fn resolve(&self, target: &Target) -> Result<Connection> {
match target {
Target::Any => self.inner.pool.any().await,
Target::Node(node_id) => self.inner.pool.get(*node_id).await,
Target::Controller => {
let controller = self.controller().await?;
self.inner.pool.get(controller).await
}
Target::Coordinator(kind, key) => {
let node = self.coordinator(*kind, key).await?;
self.inner.pool.get(node).await
}
Target::Leader(topic, partition) => {
let leader = self.leader_for(topic, *partition).await?;
self.inner.pool.get(leader).await
}
}
}
async fn on_stale_metadata(&self, target: &Target) {
let refreshed = match target {
Target::Leader(topic, _) => self.refresh_topics(&[topic.as_str()]).await.map(|_| ()),
_ => self.refresh().await.map(|_| ()),
};
if let Err(error) = refreshed {
tracing::debug!(%error, "metadata refresh after a stale-view error failed");
}
}
fn install(&self, snapshot: Arc<MetadataSnapshot>) {
self.inner.pool.learn_addresses(
snapshot
.brokers()
.iter()
.map(|broker| (broker.node_id, broker.address())),
);
self.inner.snapshot.store(snapshot);
}
fn spawn_refresh_task(&self) {
let weak = Arc::downgrade(&self.inner);
let interval = self.inner.config.refresh_interval;
tokio::spawn(async move {
loop {
tokio::time::sleep(interval).await;
let Some(inner) = Weak::upgrade(&weak) else {
return;
};
let cluster = Cluster { inner };
if let Err(error) = cluster.refresh().await {
tracing::debug!(%error, "background metadata refresh failed");
}
}
});
}
}
#[derive(Debug, Clone)]
enum Target {
Any,
Controller,
Node(i32),
Coordinator(CoordinatorKind, String),
Leader(String, i32),
}
fn all_topics_request(connection: &Connection) -> MetadataRequest {
let version = connection.negotiated_version(ApiKey::Metadata).unwrap_or(1);
let topics = if version >= 1 { None } else { Some(Vec::new()) };
base_metadata_request().with_topics(topics)
}
fn topics_request(topics: &[&str]) -> MetadataRequest {
base_metadata_request().with_topics(Some(
topics
.iter()
.map(|name| {
MetadataRequestTopic::default()
.with_name(Some(TopicName(StrBytes::from_string((*name).to_owned()))))
})
.collect(),
))
}
fn base_metadata_request() -> MetadataRequest {
MetadataRequest::default().with_allow_auto_topic_creation(false)
}
fn decode_metadata(response: kafka_conn::protocol::messages::MetadataResponse) -> MetadataSnapshot {
let brokers = response
.brokers
.into_iter()
.map(|broker| BrokerInfo {
node_id: broker.node_id.0,
host: broker.host.to_string(),
port: broker.port,
rack: broker.rack.map(|r| r.to_string()),
})
.collect();
let topics = response
.topics
.into_iter()
.map(|topic| TopicInfo {
name: topic.name.map(|n| n.0.to_string()).unwrap_or_default(),
topic_id: TopicId::from_bytes(topic.topic_id.into_bytes()),
internal: topic.is_internal,
partitions: topic
.partitions
.into_iter()
.map(|partition| PartitionInfo {
partition: partition.partition_index,
leader: Some(partition.leader_id.0).filter(|id| *id >= 0),
leader_epoch: partition.leader_epoch,
replicas: partition.replica_nodes.iter().map(|id| id.0).collect(),
isr: partition.isr_nodes.iter().map(|id| id.0).collect(),
offline_replicas: partition.offline_replicas.iter().map(|id| id.0).collect(),
error: ErrorCode::from_code(partition.error_code),
})
.collect(),
error: ErrorCode::from_code(topic.error_code),
})
.collect();
MetadataSnapshot::new(
brokers,
topics,
Some(response.controller_id.0).filter(|id| *id >= 0),
response.cluster_id.map(|id| id.to_string()),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn metadata_requests_never_allow_auto_topic_creation() {
assert!(!base_metadata_request().allow_auto_topic_creation);
assert!(!topics_request(&["orders"]).allow_auto_topic_creation);
}
#[test]
fn the_crates_default_is_the_dangerous_one() {
assert!(MetadataRequest::default().allow_auto_topic_creation);
}
#[test]
fn a_targeted_request_names_its_topics() {
let request = topics_request(&["orders", "events"]);
let names: Vec<String> = request
.topics
.unwrap_or_default()
.into_iter()
.filter_map(|t| t.name.map(|n| n.0.to_string()))
.collect();
assert_eq!(names, vec!["orders".to_owned(), "events".to_owned()]);
}
#[test]
fn send_routed_refuses_the_classes_it_cannot_resolve() {
assert_eq!(
routing(ApiKey::OffsetFetch),
Routing::Coordinator(CoordinatorKind::Group)
);
assert_eq!(
routing(ApiKey::DescribeLogDirs),
Routing::Specific(BrokerSelector::Caller)
);
}
}