1use std::collections::BTreeMap;
19use std::time::Duration;
20
21use barnabas_core::{Disposition, ErrorCode};
22use kafka_protocol::messages::{
23 create_partitions_request::CreatePartitionsTopic,
24 create_topics_request::CreatableTopic,
25 delete_records_request::{DeleteRecordsPartition, DeleteRecordsTopic},
26 delete_topics_request::DeleteTopicState,
27 describe_configs_request::DescribeConfigsResource,
28 ApiKey, CreatePartitionsRequest, CreatePartitionsResponse, CreateTopicsRequest,
29 CreateTopicsResponse, DeleteRecordsRequest, DeleteRecordsResponse, DeleteTopicsRequest,
30 DeleteTopicsResponse, DescribeConfigsRequest, DescribeConfigsResponse, TopicName,
31};
32use kafka_protocol::protocol::StrBytes;
33
34use crate::cluster::Cluster;
35use crate::{check, Error, Result, Transport};
36
37const MAX_RETRIES: usize = 20;
39const BACKOFF: Duration = Duration::from_millis(50);
40
41const NOT_CONTROLLER: i16 = 41;
44
45#[derive(Debug, Clone)]
47pub struct NewTopic {
48 pub name: String,
49 pub partitions: i32,
50 pub replication_factor: i16,
51 pub config: BTreeMap<String, String>,
54}
55
56impl NewTopic {
57 #[must_use]
59 pub fn new(name: impl Into<String>, partitions: i32, replication_factor: i16) -> Self {
60 Self {
61 name: name.into(),
62 partitions,
63 replication_factor,
64 config: BTreeMap::new(),
65 }
66 }
67
68 #[must_use]
69 pub fn with_config(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
70 self.config.insert(key.into(), value.into());
71 self
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct BrokerInfo {
78 pub node_id: i32,
79 pub host: String,
80 pub port: i32,
81 pub is_controller: bool,
83}
84
85pub struct Admin<T: Transport> {
87 cluster: Cluster<T>,
88 timeout_ms: i32,
89}
90
91impl<T: Transport> Admin<T> {
92 pub async fn connect(transport: T, bootstrap: &[String], client_id: &str) -> Result<Self> {
97 Ok(Self {
98 cluster: Cluster::connect(transport, bootstrap, client_id).await?,
99 timeout_ms: 30_000,
100 })
101 }
102
103 pub fn set_operation_timeout(&mut self, timeout: Duration) {
106 self.timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
107 }
108
109 pub fn cluster(&mut self) -> &mut Cluster<T> {
111 &mut self.cluster
112 }
113
114 async fn controller_call<Req, Resp, F>(
120 &mut self,
121 op: &'static str,
122 api_key: ApiKey,
123 version: i16,
124 req: &Req,
125 error_of: F,
126 ) -> Result<Resp>
127 where
128 Req: kafka_protocol::protocol::Encodable,
129 Resp: kafka_protocol::protocol::Decodable,
130 F: Fn(&Resp) -> i16,
131 {
132 for attempt in 0..MAX_RETRIES {
133 let addr = match self.cluster.controller_addr().await {
134 Ok(addr) => addr,
135 Err(Error::Missing("a controller")) if attempt + 1 < MAX_RETRIES => {
136 T::sleep(BACKOFF).await;
138 continue;
139 }
140 Err(e) => return Err(e),
141 };
142 let resp: Resp = self.cluster.call_at(&addr, api_key, version, req).await?;
143
144 let code = ErrorCode(error_of(&resp));
145 if code.is_ok() {
146 return Ok(resp);
147 }
148 if code.0 == NOT_CONTROLLER {
149 self.cluster.invalidate_controller();
150 T::sleep(BACKOFF).await;
151 continue;
152 }
153 if code.disposition() == Disposition::Retry && attempt + 1 < MAX_RETRIES {
154 T::sleep(BACKOFF).await;
155 continue;
156 }
157 return Err(Error::Broker {
158 op,
159 code: code.0,
160 disposition: code.disposition(),
161 });
162 }
163 Err(Error::Broker {
164 op,
165 code: NOT_CONTROLLER,
166 disposition: Disposition::Retry,
167 })
168 }
169
170 pub async fn create_topics(&mut self, topics: &[NewTopic]) -> Result<()> {
180 if topics.is_empty() {
181 return Ok(());
182 }
183 let mut req = CreateTopicsRequest::default();
184 req.timeout_ms = self.timeout_ms;
185 req.topics = topics
186 .iter()
187 .map(|topic| {
188 let mut entry = CreatableTopic::default();
189 entry.name = TopicName(StrBytes::from_string(topic.name.clone()));
190 entry.num_partitions = topic.partitions;
191 entry.replication_factor = topic.replication_factor;
192 entry.configs = topic
193 .config
194 .iter()
195 .map(|(key, value)| {
196 let mut config =
197 kafka_protocol::messages::create_topics_request::CreatableTopicConfig::default();
198 config.name = StrBytes::from_string(key.clone());
199 config.value = Some(StrBytes::from_string(value.clone()));
200 config
201 })
202 .collect();
203 entry
204 })
205 .collect();
206
207 let _: CreateTopicsResponse = self
208 .controller_call(
209 "CreateTopics",
210 ApiKey::CreateTopics,
211 5,
212 &req,
213 |r: &CreateTopicsResponse| {
214 r.topics
215 .iter()
216 .map(|t| t.error_code)
217 .find(|c| *c != 0)
218 .unwrap_or(0)
219 },
220 )
221 .await?;
222
223 for topic in topics {
230 for attempt in 0..MAX_RETRIES {
231 let _ = self.cluster.refresh_metadata(&topic.name).await;
232 if self.cluster.metadata().partition_count(&topic.name) >= topic.partitions {
233 break;
234 }
235 if attempt + 1 == MAX_RETRIES {
236 return Err(Error::NoLeader {
237 topic: topic.name.clone(),
238 partition: -1,
239 });
240 }
241 T::sleep(BACKOFF).await;
242 }
243 }
244 Ok(())
245 }
246
247 pub async fn delete_topics(&mut self, names: &[String]) -> Result<()> {
253 if names.is_empty() {
254 return Ok(());
255 }
256 let mut req = DeleteTopicsRequest::default();
257 req.timeout_ms = self.timeout_ms;
258 req.topics = names
259 .iter()
260 .map(|name| {
261 let mut entry = DeleteTopicState::default();
262 entry.name = Some(TopicName(StrBytes::from_string(name.clone())));
263 entry
264 })
265 .collect();
266
267 let _: DeleteTopicsResponse = self
268 .controller_call(
269 "DeleteTopics",
270 ApiKey::DeleteTopics,
271 6,
272 &req,
273 |r: &DeleteTopicsResponse| {
274 r.responses
275 .iter()
276 .map(|t| t.error_code)
277 .find(|c| *c != 0)
278 .unwrap_or(0)
279 },
280 )
281 .await?;
282 Ok(())
283 }
284
285 pub async fn create_partitions(&mut self, topic: &str, count: i32) -> Result<()> {
298 let mut entry = CreatePartitionsTopic::default();
299 entry.name = TopicName(StrBytes::from_string(topic.to_owned()));
300 entry.count = count;
301 entry.assignments = None;
304
305 let mut req = CreatePartitionsRequest::default();
306 req.timeout_ms = self.timeout_ms;
307 req.validate_only = false;
308 req.topics = vec![entry];
309
310 let _: CreatePartitionsResponse = self
311 .controller_call(
312 "CreatePartitions",
313 ApiKey::CreatePartitions,
314 3,
315 &req,
316 |r: &CreatePartitionsResponse| {
317 r.results
318 .iter()
319 .map(|t| t.error_code)
320 .find(|c| *c != 0)
321 .unwrap_or(0)
322 },
323 )
324 .await?;
325
326 for attempt in 0..MAX_RETRIES {
328 let _ = self.cluster.refresh_metadata(topic).await;
329 if self.cluster.metadata().partition_count(topic) >= count {
330 return Ok(());
331 }
332 if attempt + 1 == MAX_RETRIES {
333 return Err(Error::NoLeader {
334 topic: topic.to_owned(),
335 partition: -1,
336 });
337 }
338 T::sleep(BACKOFF).await;
339 }
340 Ok(())
341 }
342
343 pub async fn describe_cluster(&mut self) -> Result<Vec<BrokerInfo>> {
348 self.cluster.refresh_cluster().await?;
349 let metadata = self.cluster.metadata();
350 let controller = metadata.controller().map(|b| b.node_id);
351 Ok(metadata
352 .brokers()
353 .map(|broker| BrokerInfo {
354 node_id: broker.node_id,
355 host: broker.host.clone(),
356 port: broker.port,
357 is_controller: controller == Some(broker.node_id),
358 })
359 .collect())
360 }
361
362 pub async fn describe_topic_config(
368 &mut self,
369 topic: &str,
370 ) -> Result<BTreeMap<String, Option<String>>> {
371 let mut resource = DescribeConfigsResource::default();
372 resource.resource_type = 2;
376 resource.resource_name = StrBytes::from_string(topic.to_owned());
377 resource.configuration_keys = None;
378
379 let mut req = DescribeConfigsRequest::default();
380 req.resources = vec![resource];
381 req.include_synonyms = false;
382 req.include_documentation = false;
383
384 for attempt in 0..MAX_RETRIES {
392 let resp: DescribeConfigsResponse = self
393 .cluster
394 .call_any(ApiKey::DescribeConfigs, 4, &req)
395 .await?;
396
397 let first = resp.results.first().ok_or(Error::Missing("a resource"))?;
398 let code = ErrorCode(first.error_code);
399 if !code.is_ok()
400 && code.disposition() == Disposition::RefreshMetadata
401 && attempt + 1 < MAX_RETRIES
402 {
403 T::sleep(BACKOFF).await;
404 continue;
405 }
406
407 let mut out = BTreeMap::new();
408 for resource in &resp.results {
409 check("DescribeConfigs", resource.error_code)?;
410 for config in &resource.configs {
411 out.insert(
412 config.name.to_string(),
413 config.value.as_ref().map(ToString::to_string),
414 );
415 }
416 }
417 return Ok(out);
418 }
419 unreachable!("the loop returns on its last attempt")
420 }
421
422 pub async fn delete_records(
434 &mut self,
435 before: &[(barnabas_core::group::TopicPartition, i64)],
436 ) -> Result<BTreeMap<barnabas_core::group::TopicPartition, i64>> {
437 let mut out = BTreeMap::new();
438 if before.is_empty() {
439 return Ok(out);
440 }
441
442 let mut by_leader: BTreeMap<String, Vec<(barnabas_core::group::TopicPartition, i64)>> =
443 BTreeMap::new();
444 for (tp, offset) in before {
445 let addr = self.cluster.leader_addr(&tp.topic, tp.partition).await?;
446 by_leader
447 .entry(addr)
448 .or_default()
449 .push((tp.clone(), *offset));
450 }
451
452 for (addr, group) in by_leader {
453 let mut topics: BTreeMap<String, Vec<DeleteRecordsPartition>> = BTreeMap::new();
454 for (tp, offset) in &group {
455 let mut entry = DeleteRecordsPartition::default();
456 entry.partition_index = tp.partition;
457 entry.offset = *offset;
458 topics.entry(tp.topic.clone()).or_default().push(entry);
459 }
460
461 let mut req = DeleteRecordsRequest::default();
462 req.timeout_ms = self.timeout_ms;
463 req.topics = topics
464 .into_iter()
465 .map(|(name, partitions)| {
466 let mut topic = DeleteRecordsTopic::default();
467 topic.name = TopicName(StrBytes::from_string(name));
468 topic.partitions = partitions;
469 topic
470 })
471 .collect();
472
473 let resp: DeleteRecordsResponse = self
474 .cluster
475 .call_at(&addr, ApiKey::DeleteRecords, 2, &req)
476 .await?;
477
478 for topic in &resp.topics {
479 for partition in &topic.partitions {
480 check("DeleteRecords", partition.error_code)?;
481 out.insert(
482 barnabas_core::group::TopicPartition::new(
483 topic.name.0.to_string(),
484 partition.partition_index,
485 ),
486 partition.low_watermark,
487 );
488 }
489 }
490 }
491 Ok(out)
492 }
493}