use std::collections::BTreeMap;
use std::time::Duration;
use barnabas_core::{Disposition, ErrorCode};
use kafka_protocol::messages::{
create_partitions_request::CreatePartitionsTopic,
create_topics_request::CreatableTopic,
delete_records_request::{DeleteRecordsPartition, DeleteRecordsTopic},
delete_topics_request::DeleteTopicState,
describe_configs_request::DescribeConfigsResource,
ApiKey, CreatePartitionsRequest, CreatePartitionsResponse, CreateTopicsRequest,
CreateTopicsResponse, DeleteRecordsRequest, DeleteRecordsResponse, DeleteTopicsRequest,
DeleteTopicsResponse, DescribeConfigsRequest, DescribeConfigsResponse, TopicName,
};
use kafka_protocol::protocol::StrBytes;
use crate::cluster::Cluster;
use crate::{check, Error, Result, Transport};
const MAX_RETRIES: usize = 20;
const BACKOFF: Duration = Duration::from_millis(50);
const NOT_CONTROLLER: i16 = 41;
#[derive(Debug, Clone)]
pub struct NewTopic {
pub name: String,
pub partitions: i32,
pub replication_factor: i16,
pub config: BTreeMap<String, String>,
}
impl NewTopic {
#[must_use]
pub fn new(name: impl Into<String>, partitions: i32, replication_factor: i16) -> Self {
Self {
name: name.into(),
partitions,
replication_factor,
config: BTreeMap::new(),
}
}
#[must_use]
pub fn with_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BrokerInfo {
pub node_id: i32,
pub host: String,
pub port: i32,
pub is_controller: bool,
}
pub struct Admin<T: Transport> {
cluster: Cluster<T>,
timeout_ms: i32,
}
impl<T: Transport> Admin<T> {
pub async fn connect(transport: T, bootstrap: &[String], client_id: &str) -> Result<Self> {
Ok(Self {
cluster: Cluster::connect(transport, bootstrap, client_id).await?,
timeout_ms: 30_000,
})
}
pub fn set_operation_timeout(&mut self, timeout: Duration) {
self.timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
}
pub fn cluster(&mut self) -> &mut Cluster<T> {
&mut self.cluster
}
async fn controller_call<Req, Resp, F>(
&mut self,
op: &'static str,
api_key: ApiKey,
version: i16,
req: &Req,
error_of: F,
) -> Result<Resp>
where
Req: kafka_protocol::protocol::Encodable,
Resp: kafka_protocol::protocol::Decodable,
F: Fn(&Resp) -> i16,
{
for attempt in 0..MAX_RETRIES {
let addr = match self.cluster.controller_addr().await {
Ok(addr) => addr,
Err(Error::Missing("a controller")) if attempt + 1 < MAX_RETRIES => {
T::sleep(BACKOFF).await;
continue;
}
Err(e) => return Err(e),
};
let resp: Resp = self.cluster.call_at(&addr, api_key, version, req).await?;
let code = ErrorCode(error_of(&resp));
if code.is_ok() {
return Ok(resp);
}
if code.0 == NOT_CONTROLLER {
self.cluster.invalidate_controller();
T::sleep(BACKOFF).await;
continue;
}
if code.disposition() == Disposition::Retry && attempt + 1 < MAX_RETRIES {
T::sleep(BACKOFF).await;
continue;
}
return Err(Error::Broker {
op,
code: code.0,
disposition: code.disposition(),
});
}
Err(Error::Broker {
op,
code: NOT_CONTROLLER,
disposition: Disposition::Retry,
})
}
pub async fn create_topics(&mut self, topics: &[NewTopic]) -> Result<()> {
if topics.is_empty() {
return Ok(());
}
let mut req = CreateTopicsRequest::default();
req.timeout_ms = self.timeout_ms;
req.topics = topics
.iter()
.map(|topic| {
let mut entry = CreatableTopic::default();
entry.name = TopicName(StrBytes::from_string(topic.name.clone()));
entry.num_partitions = topic.partitions;
entry.replication_factor = topic.replication_factor;
entry.configs = topic
.config
.iter()
.map(|(key, value)| {
let mut config =
kafka_protocol::messages::create_topics_request::CreatableTopicConfig::default();
config.name = StrBytes::from_string(key.clone());
config.value = Some(StrBytes::from_string(value.clone()));
config
})
.collect();
entry
})
.collect();
let _: CreateTopicsResponse = self
.controller_call(
"CreateTopics",
ApiKey::CreateTopics,
5,
&req,
|r: &CreateTopicsResponse| {
r.topics
.iter()
.map(|t| t.error_code)
.find(|c| *c != 0)
.unwrap_or(0)
},
)
.await?;
for topic in topics {
for attempt in 0..MAX_RETRIES {
let _ = self.cluster.refresh_metadata(&topic.name).await;
if self.cluster.metadata().partition_count(&topic.name) >= topic.partitions {
break;
}
if attempt + 1 == MAX_RETRIES {
return Err(Error::NoLeader {
topic: topic.name.clone(),
partition: -1,
});
}
T::sleep(BACKOFF).await;
}
}
Ok(())
}
pub async fn delete_topics(&mut self, names: &[String]) -> Result<()> {
if names.is_empty() {
return Ok(());
}
let mut req = DeleteTopicsRequest::default();
req.timeout_ms = self.timeout_ms;
req.topics = names
.iter()
.map(|name| {
let mut entry = DeleteTopicState::default();
entry.name = Some(TopicName(StrBytes::from_string(name.clone())));
entry
})
.collect();
let _: DeleteTopicsResponse = self
.controller_call(
"DeleteTopics",
ApiKey::DeleteTopics,
6,
&req,
|r: &DeleteTopicsResponse| {
r.responses
.iter()
.map(|t| t.error_code)
.find(|c| *c != 0)
.unwrap_or(0)
},
)
.await?;
Ok(())
}
pub async fn create_partitions(&mut self, topic: &str, count: i32) -> Result<()> {
let mut entry = CreatePartitionsTopic::default();
entry.name = TopicName(StrBytes::from_string(topic.to_owned()));
entry.count = count;
entry.assignments = None;
let mut req = CreatePartitionsRequest::default();
req.timeout_ms = self.timeout_ms;
req.validate_only = false;
req.topics = vec![entry];
let _: CreatePartitionsResponse = self
.controller_call(
"CreatePartitions",
ApiKey::CreatePartitions,
3,
&req,
|r: &CreatePartitionsResponse| {
r.results
.iter()
.map(|t| t.error_code)
.find(|c| *c != 0)
.unwrap_or(0)
},
)
.await?;
for attempt in 0..MAX_RETRIES {
let _ = self.cluster.refresh_metadata(topic).await;
if self.cluster.metadata().partition_count(topic) >= count {
return Ok(());
}
if attempt + 1 == MAX_RETRIES {
return Err(Error::NoLeader {
topic: topic.to_owned(),
partition: -1,
});
}
T::sleep(BACKOFF).await;
}
Ok(())
}
pub async fn describe_cluster(&mut self) -> Result<Vec<BrokerInfo>> {
self.cluster.refresh_cluster().await?;
let metadata = self.cluster.metadata();
let controller = metadata.controller().map(|b| b.node_id);
Ok(metadata
.brokers()
.map(|broker| BrokerInfo {
node_id: broker.node_id,
host: broker.host.clone(),
port: broker.port,
is_controller: controller == Some(broker.node_id),
})
.collect())
}
pub async fn describe_topic_config(
&mut self,
topic: &str,
) -> Result<BTreeMap<String, Option<String>>> {
let mut resource = DescribeConfigsResource::default();
resource.resource_type = 2;
resource.resource_name = StrBytes::from_string(topic.to_owned());
resource.configuration_keys = None;
let mut req = DescribeConfigsRequest::default();
req.resources = vec![resource];
req.include_synonyms = false;
req.include_documentation = false;
for attempt in 0..MAX_RETRIES {
let resp: DescribeConfigsResponse = self
.cluster
.call_any(ApiKey::DescribeConfigs, 4, &req)
.await?;
let first = resp.results.first().ok_or(Error::Missing("a resource"))?;
let code = ErrorCode(first.error_code);
if !code.is_ok()
&& code.disposition() == Disposition::RefreshMetadata
&& attempt + 1 < MAX_RETRIES
{
T::sleep(BACKOFF).await;
continue;
}
let mut out = BTreeMap::new();
for resource in &resp.results {
check("DescribeConfigs", resource.error_code)?;
for config in &resource.configs {
out.insert(
config.name.to_string(),
config.value.as_ref().map(ToString::to_string),
);
}
}
return Ok(out);
}
unreachable!("the loop returns on its last attempt")
}
pub async fn delete_records(
&mut self,
before: &[(barnabas_core::group::TopicPartition, i64)],
) -> Result<BTreeMap<barnabas_core::group::TopicPartition, i64>> {
let mut out = BTreeMap::new();
if before.is_empty() {
return Ok(out);
}
let mut by_leader: BTreeMap<String, Vec<(barnabas_core::group::TopicPartition, i64)>> =
BTreeMap::new();
for (tp, offset) in before {
let addr = self.cluster.leader_addr(&tp.topic, tp.partition).await?;
by_leader
.entry(addr)
.or_default()
.push((tp.clone(), *offset));
}
for (addr, group) in by_leader {
let mut topics: BTreeMap<String, Vec<DeleteRecordsPartition>> = BTreeMap::new();
for (tp, offset) in &group {
let mut entry = DeleteRecordsPartition::default();
entry.partition_index = tp.partition;
entry.offset = *offset;
topics.entry(tp.topic.clone()).or_default().push(entry);
}
let mut req = DeleteRecordsRequest::default();
req.timeout_ms = self.timeout_ms;
req.topics = topics
.into_iter()
.map(|(name, partitions)| {
let mut topic = DeleteRecordsTopic::default();
topic.name = TopicName(StrBytes::from_string(name));
topic.partitions = partitions;
topic
})
.collect();
let resp: DeleteRecordsResponse = self
.cluster
.call_at(&addr, ApiKey::DeleteRecords, 2, &req)
.await?;
for topic in &resp.topics {
for partition in &topic.partitions {
check("DeleteRecords", partition.error_code)?;
out.insert(
barnabas_core::group::TopicPartition::new(
topic.name.0.to_string(),
partition.partition_index,
),
partition.low_watermark,
);
}
}
}
Ok(out)
}
}