1use std::{
11 collections::{BTreeMap, BTreeSet},
12 mem,
13};
14
15use krabka_client_core::ClientError;
16use krabka_protocol::{
17 ProtocolRequest as _,
18 owned::{
19 describe_topic_partitions_request::{
20 Cursor as RequestCursor, DescribeTopicPartitionsRequest, TopicRequest,
21 },
22 describe_topic_partitions_response::{
23 DescribeTopicPartitionsResponse, DescribeTopicPartitionsResponsePartition,
24 DescribeTopicPartitionsResponseTopic,
25 },
26 metadata_request::{MetadataRequest, MetadataRequestTopic},
27 metadata_response::{MetadataResponse, MetadataResponsePartition, MetadataResponseTopic},
28 },
29 primitives::uuid::Uuid as ProtoUuid,
30};
31use uuid::Uuid;
32
33use crate::{
34 AdminClient, AdminError, ClusterNode, DescribeClusterOptions, KafkaError,
35 cluster::authorized_operations, groups::list_groups_kafka_error, kafka_error_name,
36 retry::ControllerRetry, users::AclOperation,
37};
38
39const UNKNOWN_TOPIC_OR_PARTITION: i16 = 3;
41const INVALID_TOPIC_EXCEPTION: i16 = 17;
43const UNKNOWN_TOPIC_ID: i16 = 100;
45
46#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, bon::Builder)]
48pub struct ListTopicsOptions {
49 #[builder(default)]
51 pub list_internal: bool,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct TopicListing {
57 pub name: String,
59 pub topic_id: Option<Uuid>,
61 pub is_internal: bool,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, bon::Builder)]
68pub struct DescribeTopicsOptions {
69 #[builder(default)]
71 pub include_authorized_operations: bool,
72 #[builder(default = 2000)]
75 pub partition_size_limit_per_response: i32,
76}
77
78impl Default for DescribeTopicsOptions {
79 fn default() -> Self {
80 Self {
81 include_authorized_operations: false,
82 partition_size_limit_per_response: 2000,
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct TopicPartitionInfo {
94 pub partition: i32,
96 pub leader: Option<ClusterNode>,
99 pub replicas: Vec<ClusterNode>,
101 pub isr: Vec<ClusterNode>,
103 pub elr: Vec<ClusterNode>,
106 pub last_known_elr: Vec<ClusterNode>,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct TopicDescription {
115 pub name: String,
117 pub topic_id: Option<Uuid>,
119 pub is_internal: bool,
121 pub partitions: Vec<TopicPartitionInfo>,
123 pub authorized_operations: Option<BTreeSet<AclOperation>>,
126}
127
128pub type TopicDescriptions<K> = BTreeMap<K, Result<TopicDescription, KafkaError>>;
130
131fn topic_id(id: ProtoUuid) -> Option<Uuid> {
132 (id != ProtoUuid::ZERO).then(|| Uuid::from_bytes(id.0))
133}
134
135fn topic_error(code: i16, message: String) -> KafkaError {
136 KafkaError {
137 code,
138 name: kafka_error_name(code),
139 message: Some(message),
140 }
141}
142
143fn node(nodes: &BTreeMap<i32, ClusterNode>, id: i32) -> ClusterNode {
145 nodes.get(&id).cloned().unwrap_or_else(|| ClusterNode {
146 id,
147 host: String::new(),
148 port: -1,
149 rack: None,
150 is_fenced: false,
151 })
152}
153
154fn node_list(nodes: &BTreeMap<i32, ClusterNode>, ids: &[i32]) -> Vec<ClusterNode> {
155 ids.iter().map(|id| node(nodes, *id)).collect()
156}
157
158fn all_topics_request() -> MetadataRequest {
160 MetadataRequest {
161 topics: None,
162 allow_auto_topic_creation: true,
163 ..Default::default()
164 }
165}
166
167fn topic_listings(
170 response: MetadataResponse,
171 options: ListTopicsOptions,
172) -> BTreeMap<String, TopicListing> {
173 response
174 .topics
175 .into_iter()
176 .filter(|topic| !topic.is_internal || options.list_internal)
177 .filter_map(|topic| {
178 let name = topic.name?;
179 Some((
180 name.clone(),
181 TopicListing {
182 name,
183 topic_id: topic_id(topic.topic_id),
184 is_internal: topic.is_internal,
185 },
186 ))
187 })
188 .collect()
189}
190
191fn describe_partitions_request(
196 pending: &BTreeSet<String>,
197 partial: Option<&TopicDescription>,
198 options: DescribeTopicsOptions,
199) -> DescribeTopicPartitionsRequest {
200 DescribeTopicPartitionsRequest {
201 topics: pending
202 .iter()
203 .map(|name| TopicRequest {
204 name: name.clone(),
205 ..Default::default()
206 })
207 .collect(),
208 response_partition_limit: options.partition_size_limit_per_response,
209 cursor: partial.map(|partial| RequestCursor {
210 topic_name: partial.name.clone(),
211 partition_index: i32::try_from(partial.partitions.len()).unwrap_or(i32::MAX),
212 ..Default::default()
213 }),
214 ..Default::default()
215 }
216}
217
218fn partition_from_describe(
219 partition: &DescribeTopicPartitionsResponsePartition,
220 nodes: &BTreeMap<i32, ClusterNode>,
221) -> TopicPartitionInfo {
222 TopicPartitionInfo {
223 partition: partition.partition_index,
224 leader: nodes.get(&partition.leader_id).cloned(),
225 replicas: node_list(nodes, &partition.replica_nodes),
226 isr: node_list(nodes, &partition.isr_nodes),
227 elr: node_list(
228 nodes,
229 partition
230 .eligible_leader_replicas
231 .as_deref()
232 .unwrap_or_default(),
233 ),
234 last_known_elr: node_list(
235 nodes,
236 partition.last_known_elr.as_deref().unwrap_or_default(),
237 ),
238 }
239}
240
241fn description_from_describe(
244 topic: &DescribeTopicPartitionsResponseTopic,
245 name: String,
246 nodes: &BTreeMap<i32, ClusterNode>,
247 include_authorized_operations: bool,
248) -> TopicDescription {
249 TopicDescription {
250 name,
251 topic_id: topic_id(topic.topic_id),
252 is_internal: topic.is_internal,
253 partitions: topic
254 .partitions
255 .iter()
256 .map(|partition| partition_from_describe(partition, nodes))
257 .collect(),
258 authorized_operations: if include_authorized_operations {
259 authorized_operations(topic.topic_authorized_operations)
260 } else {
261 None
262 },
263 }
264}
265
266#[derive(Debug, Default)]
268struct DescribePages {
269 pending: BTreeSet<String>,
271 partial: Option<TopicDescription>,
273 out: TopicDescriptions<String>,
275}
276
277impl DescribePages {
278 fn read(
282 &mut self,
283 response: DescribeTopicPartitionsResponse,
284 nodes: &BTreeMap<i32, ClusterNode>,
285 include_authorized_operations: bool,
286 ) {
287 let mut cursor = response.next_cursor.map(|cursor| cursor.topic_name);
288 let mut next = None;
289 for topic in response.topics {
290 let Some(name) = topic.name.clone() else {
291 tracing::warn!("the DescribeTopicPartitions response names a topic without a name");
292 continue;
293 };
294 let asked = self.pending.contains(&name)
295 || self.partial.as_ref().is_some_and(|p| p.name == name);
296 if !asked {
297 tracing::warn!(
298 topic = %name,
299 "the DescribeTopicPartitions response names a topic that is not in the request"
300 );
301 continue;
302 }
303 if topic.error_code != 0 {
304 self.out.entry(name.clone()).or_insert_with(|| {
305 Err(KafkaError {
306 code: topic.error_code,
307 name: kafka_error_name(topic.error_code),
308 message: None,
309 })
310 });
311 self.pending.remove(&name);
312 if cursor.as_deref() == Some(name.as_str()) {
313 cursor = None;
314 }
315 continue;
316 }
317 let current = description_from_describe(
318 &topic,
319 name.clone(),
320 nodes,
321 include_authorized_operations,
322 );
323 if let Some(partial) = self.partial.as_mut()
324 && partial.name == name
325 {
326 partial.partitions.extend(current.partitions);
327 continue;
328 }
329 if cursor.as_deref() == Some(name.as_str()) {
330 next = Some(current);
331 continue;
332 }
333 self.pending.remove(&name);
334 self.out.entry(name).or_insert(Ok(current));
335 }
336 if let Some(partial) = self
337 .partial
338 .take_if(|partial| cursor.as_deref() != Some(partial.name.as_str()))
339 {
340 self.pending.remove(&partial.name);
341 self.out.entry(partial.name.clone()).or_insert(Ok(partial));
342 }
343 if next.is_some() {
344 self.partial = next;
345 }
346 }
347
348 fn unfinished(&self) -> bool {
350 !self.pending.is_empty() || self.partial.is_some()
351 }
352
353 fn fail(mut self, error: &KafkaError) -> TopicDescriptions<String> {
355 let unfinished = mem::take(&mut self.pending)
356 .into_iter()
357 .chain(self.partial.take().map(|partial| partial.name));
358 for name in unfinished {
359 self.out.entry(name).or_insert_with(|| Err(error.clone()));
360 }
361 self.out
362 }
363}
364
365fn metadata_by_name_request(
368 names: &BTreeSet<String>,
369 options: DescribeTopicsOptions,
370) -> MetadataRequest {
371 MetadataRequest {
372 topics: Some(
373 names
374 .iter()
375 .map(|name| MetadataRequestTopic {
376 topic_id: ProtoUuid::ZERO,
377 name: Some(name.clone()),
378 ..Default::default()
379 })
380 .collect(),
381 ),
382 allow_auto_topic_creation: false,
383 include_topic_authorized_operations: options.include_authorized_operations,
384 ..Default::default()
385 }
386}
387
388fn metadata_by_id_request(ids: &BTreeSet<Uuid>, options: DescribeTopicsOptions) -> MetadataRequest {
391 MetadataRequest {
392 topics: Some(
393 ids.iter()
394 .map(|id| MetadataRequestTopic {
395 topic_id: ProtoUuid(*id.as_bytes()),
396 name: None,
397 ..Default::default()
398 })
399 .collect(),
400 ),
401 allow_auto_topic_creation: false,
402 include_topic_authorized_operations: options.include_authorized_operations,
403 ..Default::default()
404 }
405}
406
407fn metadata_nodes(response: &MetadataResponse) -> BTreeMap<i32, ClusterNode> {
408 response
409 .brokers
410 .iter()
411 .map(|broker| {
412 (
413 broker.node_id,
414 ClusterNode {
415 id: broker.node_id,
416 host: broker.host.clone(),
417 port: broker.port,
418 rack: broker.rack.clone(),
419 is_fenced: false,
420 },
421 )
422 })
423 .collect()
424}
425
426fn partition_from_metadata(
427 partition: &MetadataResponsePartition,
428 nodes: &BTreeMap<i32, ClusterNode>,
429) -> TopicPartitionInfo {
430 TopicPartitionInfo {
431 partition: partition.partition_index,
432 leader: (partition.leader_id >= 0)
433 .then(|| nodes.get(&partition.leader_id).cloned())
434 .flatten(),
435 replicas: node_list(nodes, &partition.replica_nodes),
436 isr: node_list(nodes, &partition.isr_nodes),
437 elr: Vec::new(),
438 last_known_elr: Vec::new(),
439 }
440}
441
442fn description_from_metadata(
446 topic: &MetadataResponseTopic,
447 name: String,
448 nodes: &BTreeMap<i32, ClusterNode>,
449) -> TopicDescription {
450 let mut partitions = topic
451 .partitions
452 .iter()
453 .map(|partition| partition_from_metadata(partition, nodes))
454 .collect::<Vec<_>>();
455 partitions.sort_by_key(|partition| partition.partition);
456 TopicDescription {
457 name,
458 topic_id: topic_id(topic.topic_id),
459 is_internal: topic.is_internal,
460 partitions,
461 authorized_operations: authorized_operations(topic.topic_authorized_operations),
462 }
463}
464
465fn descriptions_by_name(
470 names: &BTreeSet<String>,
471 response: MetadataResponse,
472) -> TopicDescriptions<String> {
473 let nodes = metadata_nodes(&response);
474 let mut topics = response
475 .topics
476 .into_iter()
477 .filter_map(|topic| Some((topic.name.clone()?, topic)))
478 .collect::<BTreeMap<_, _>>();
479 names
480 .iter()
481 .map(|name| {
482 let result = match topics.remove(name) {
483 Some(topic) if topic.error_code != 0 => Err(KafkaError {
484 code: topic.error_code,
485 name: kafka_error_name(topic.error_code),
486 message: None,
487 }),
488 Some(topic) => Ok(description_from_metadata(&topic, name.clone(), &nodes)),
489 None => Err(topic_error(
490 UNKNOWN_TOPIC_OR_PARTITION,
491 format!("Topic {name} not found."),
492 )),
493 };
494 (name.clone(), result)
495 })
496 .collect()
497}
498
499fn descriptions_by_id(ids: &BTreeSet<Uuid>, response: MetadataResponse) -> TopicDescriptions<Uuid> {
504 let nodes = metadata_nodes(&response);
505 let mut topics = response
506 .topics
507 .into_iter()
508 .filter(|topic| topic.error_code == 0)
509 .filter_map(|topic| Some((topic_id(topic.topic_id)?, topic)))
510 .collect::<BTreeMap<_, _>>();
511 ids.iter()
512 .map(|id| {
513 let result = topics
514 .remove(id)
515 .and_then(|mut topic| {
516 let name = topic.name.take()?;
517 Some(description_from_metadata(&topic, name, &nodes))
518 })
519 .ok_or_else(|| topic_error(UNKNOWN_TOPIC_ID, format!("TopicId {id} not found.")));
520 (*id, result)
521 })
522 .collect()
523}
524
525const fn is_unsupported_version(error: &AdminError) -> bool {
528 matches!(
529 error,
530 AdminError::Transport(ClientError::IncompatibleVersion { .. })
531 )
532}
533
534impl AdminClient {
535 pub async fn list_topics(
543 &self,
544 options: ListTopicsOptions,
545 ) -> Result<BTreeMap<String, TopicListing>, AdminError> {
546 let retry = ControllerRetry::new("Metadata", self.retry);
547 let response = retry
548 .bounded(self.conn.send_at_least(all_topics_request(), 1))
549 .await?;
550 Ok(topic_listings(response, options))
551 }
552
553 pub async fn describe_topics(
575 &self,
576 names: &[&str],
577 options: DescribeTopicsOptions,
578 ) -> TopicDescriptions<String> {
579 let mut pages = DescribePages::default();
580 for name in names {
581 if name.is_empty() {
582 pages.out.insert(
583 String::new(),
584 Err(topic_error(
585 INVALID_TOPIC_EXCEPTION,
586 "The given topic name '' cannot be represented in a request.".to_owned(),
587 )),
588 );
589 } else {
590 pages.pending.insert((*name).to_owned());
591 }
592 }
593 if pages.pending.is_empty() {
594 return pages.out;
595 }
596 let nodes = match self
597 .describe_cluster(DescribeClusterOptions::builder().build())
598 .await
599 {
600 Ok(cluster) => cluster
601 .nodes
602 .into_iter()
603 .map(|node| (node.id, node))
604 .collect::<BTreeMap<_, _>>(),
605 Err(error) => return pages.fail(&list_groups_kafka_error(&error)),
606 };
607 if self
608 .conn
609 .advertised_api_range(DescribeTopicPartitionsRequest::API_KEY)
610 .await
611 .is_none()
612 {
613 return self.describe_topics_with_metadata(pages, options).await;
614 }
615 let retry = ControllerRetry::new("DescribeTopicPartitions", self.retry);
616 while pages.unfinished() {
617 let request =
618 describe_partitions_request(&pages.pending, pages.partial.as_ref(), options);
619 match retry.bounded(self.conn.send(request)).await {
620 Ok(response) => pages.read(response, &nodes, options.include_authorized_operations),
621 Err(error) if is_unsupported_version(&error) => {
622 return self.describe_topics_with_metadata(pages, options).await;
623 }
624 Err(error) => return pages.fail(&list_groups_kafka_error(&error)),
625 }
626 }
627 pages.out
628 }
629
630 async fn describe_topics_with_metadata(
635 &self,
636 mut pages: DescribePages,
637 options: DescribeTopicsOptions,
638 ) -> TopicDescriptions<String> {
639 if let Some(partial) = pages.partial.take() {
640 pages.pending.insert(partial.name);
641 }
642 let retry = ControllerRetry::new("Metadata", self.retry);
643 let request = metadata_by_name_request(&pages.pending, options);
644 let response = match retry.bounded(self.conn.send_at_least(request, 4)).await {
645 Err(error) if is_unsupported_version(&error) => {
646 retry
647 .bounded(self.conn.send_at_least(all_topics_request(), 1))
648 .await
649 }
650 other => other,
651 };
652 match response {
653 Ok(response) => {
654 let pending = mem::take(&mut pages.pending);
655 for (name, result) in descriptions_by_name(&pending, response) {
656 pages.out.entry(name).or_insert(result);
657 }
658 pages.out
659 }
660 Err(error) => pages.fail(&list_groups_kafka_error(&error)),
661 }
662 }
663
664 pub async fn describe_topics_by_id(
674 &self,
675 ids: &[Uuid],
676 options: DescribeTopicsOptions,
677 ) -> TopicDescriptions<Uuid> {
678 let mut out = TopicDescriptions::new();
679 let mut pending = BTreeSet::new();
680 for id in ids {
681 if id.is_nil() {
682 out.insert(
683 *id,
684 Err(topic_error(
685 INVALID_TOPIC_EXCEPTION,
686 format!("The given topic id '{id}' cannot be represented in a request."),
687 )),
688 );
689 } else {
690 pending.insert(*id);
691 }
692 }
693 if pending.is_empty() {
694 return out;
695 }
696 let retry = ControllerRetry::new("Metadata", self.retry);
697 let request = metadata_by_id_request(&pending, options);
698 match retry.bounded(self.conn.send_at_least(request, 12)).await {
699 Ok(response) => out.extend(descriptions_by_id(&pending, response)),
700 Err(error) => {
701 let error = list_groups_kafka_error(&error);
702 out.extend(pending.into_iter().map(|id| (id, Err(error.clone()))));
703 }
704 }
705 out
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use std::{
712 net::SocketAddr,
713 sync::{Arc, Mutex},
714 };
715
716 use krabka_client_core::MockReply;
717 use krabka_protocol::owned::{
718 describe_cluster_request,
719 describe_cluster_response::{DescribeClusterBroker, DescribeClusterResponse},
720 describe_topic_partitions_request,
721 describe_topic_partitions_response::Cursor as ResponseCursor,
722 metadata_request,
723 metadata_response::MetadataResponseBroker,
724 };
725
726 use super::*;
727 use crate::partition_leaders::test_support::{
728 ApiVersionRange, decode_request, encode_response, fast_admin, scripted_broker,
729 };
730
731 const ALPHA: Uuid = Uuid::from_u128(0xa);
732 const BETA: Uuid = Uuid::from_u128(0xb);
733
734 fn id_of(name: &str) -> Uuid {
735 if name == "alpha" { ALPHA } else { BETA }
736 }
737
738 fn broker_one(own: SocketAddr) -> ClusterNode {
740 ClusterNode {
741 id: 1,
742 host: own.ip().to_string(),
743 port: i32::from(own.port()),
744 rack: None,
745 is_fenced: false,
746 }
747 }
748
749 fn unknown_two() -> ClusterNode {
751 ClusterNode {
752 id: 2,
753 host: String::new(),
754 port: -1,
755 rack: None,
756 is_fenced: false,
757 }
758 }
759
760 fn page_partition(index: i32) -> DescribeTopicPartitionsResponsePartition {
763 DescribeTopicPartitionsResponsePartition {
764 partition_index: index,
765 leader_id: 1,
766 replica_nodes: vec![1, 2],
767 isr_nodes: vec![1],
768 eligible_leader_replicas: Some(vec![2]),
769 last_known_elr: Some(Vec::new()),
770 ..Default::default()
771 }
772 }
773
774 fn page_topic(
777 name: &str,
778 error_code: i16,
779 partitions: &[i32],
780 ) -> DescribeTopicPartitionsResponseTopic {
781 DescribeTopicPartitionsResponseTopic {
782 error_code,
783 name: Some(name.to_owned()),
784 topic_id: ProtoUuid(*id_of(name).as_bytes()),
785 partitions: partitions.iter().copied().map(page_partition).collect(),
786 topic_authorized_operations: 1 << 3,
787 ..Default::default()
788 }
789 }
790
791 fn page(
792 topics: Vec<DescribeTopicPartitionsResponseTopic>,
793 cursor: Option<(&str, i32)>,
794 ) -> DescribeTopicPartitionsResponse {
795 DescribeTopicPartitionsResponse {
796 topics,
797 next_cursor: cursor.map(|(topic_name, partition_index)| ResponseCursor {
798 topic_name: topic_name.to_owned(),
799 partition_index,
800 ..Default::default()
801 }),
802 ..Default::default()
803 }
804 }
805
806 fn paged(name: &str, partitions: &[i32], own: SocketAddr) -> TopicDescription {
808 TopicDescription {
809 name: name.to_owned(),
810 topic_id: Some(id_of(name)),
811 is_internal: false,
812 partitions: partitions
813 .iter()
814 .map(|partition| TopicPartitionInfo {
815 partition: *partition,
816 leader: Some(broker_one(own)),
817 replicas: vec![broker_one(own), unknown_two()],
818 isr: vec![broker_one(own)],
819 elr: vec![unknown_two()],
820 last_known_elr: Vec::new(),
821 })
822 .collect(),
823 authorized_operations: None,
824 }
825 }
826
827 fn dtp_request(topics: &[&str], cursor: Option<(&str, i32)>) -> DescribeTopicPartitionsRequest {
828 DescribeTopicPartitionsRequest {
829 topics: topics
830 .iter()
831 .map(|name| TopicRequest {
832 name: (*name).to_owned(),
833 ..Default::default()
834 })
835 .collect(),
836 response_partition_limit: 2000,
837 cursor: cursor.map(|(topic_name, partition_index)| RequestCursor {
838 topic_name: topic_name.to_owned(),
839 partition_index,
840 ..Default::default()
841 }),
842 ..Default::default()
843 }
844 }
845
846 fn metadata_topic(
849 name: &str,
850 error_code: i16,
851 indexes: &[i32],
852 leader: i32,
853 ) -> MetadataResponseTopic {
854 MetadataResponseTopic {
855 error_code,
856 name: Some(name.to_owned()),
857 topic_id: ProtoUuid(*id_of(name).as_bytes()),
858 is_internal: name.starts_with("__"),
859 partitions: indexes
860 .iter()
861 .map(|index| MetadataResponsePartition {
862 partition_index: *index,
863 leader_id: leader,
864 replica_nodes: vec![1, 2],
865 isr_nodes: vec![1],
866 ..Default::default()
867 })
868 .collect(),
869 ..Default::default()
870 }
871 }
872
873 fn metadata(own: SocketAddr, topics: Vec<MetadataResponseTopic>) -> MetadataResponse {
874 MetadataResponse {
875 brokers: vec![MetadataResponseBroker {
876 node_id: 1,
877 host: own.ip().to_string(),
878 port: i32::from(own.port()),
879 ..Default::default()
880 }],
881 controller_id: 1,
882 topics,
883 ..Default::default()
884 }
885 }
886
887 fn from_metadata(
889 name: &str,
890 partitions: &[i32],
891 own: SocketAddr,
892 leader: bool,
893 ) -> TopicDescription {
894 TopicDescription {
895 name: name.to_owned(),
896 topic_id: Some(id_of(name)),
897 is_internal: false,
898 partitions: partitions
899 .iter()
900 .map(|partition| TopicPartitionInfo {
901 partition: *partition,
902 leader: leader.then(|| broker_one(own)),
903 replicas: vec![broker_one(own), unknown_two()],
904 isr: vec![broker_one(own)],
905 elr: Vec::new(),
906 last_known_elr: Vec::new(),
907 })
908 .collect(),
909 authorized_operations: None,
910 }
911 }
912
913 fn topic_err(code: i16, message: Option<String>) -> Result<TopicDescription, KafkaError> {
914 Err(KafkaError {
915 code,
916 name: kafka_error_name(code),
917 message,
918 })
919 }
920
921 #[derive(Clone)]
923 enum Answer {
924 Page(DescribeTopicPartitionsResponse),
925 Close,
926 }
927
928 #[derive(Debug, Clone, PartialEq)]
930 enum Received {
931 Pages(DescribeTopicPartitionsRequest),
932 Metadata(i16, MetadataRequest),
933 }
934
935 struct DescribeCase {
937 name: &'static str,
938 names: Vec<&'static str>,
939 cluster_error: i16,
940 dtp: bool,
941 answers: Vec<Answer>,
942 metadata_topics: Vec<MetadataResponseTopic>,
943 expected: fn(SocketAddr) -> (TopicDescriptions<String>, Vec<Received>),
944 }
945
946 async fn describe_case(
951 case: &DescribeCase,
952 ) -> (TopicDescriptions<String>, Vec<Received>, SocketAddr) {
953 let received = Arc::new(Mutex::new(Vec::new()));
954 let handler_received = Arc::clone(&received);
955 let mut apis = vec![
956 ApiVersionRange::builder()
957 .api_key(describe_cluster_request::API_KEY)
958 .max_version(2)
959 .build(),
960 ];
961 if case.dtp {
962 apis.push(
963 ApiVersionRange::builder()
964 .api_key(describe_topic_partitions_request::API_KEY)
965 .build(),
966 );
967 }
968 let cluster_error = case.cluster_error;
969 let metadata_topics = case.metadata_topics.clone();
970 let pages = Arc::new(Mutex::new(case.answers.clone().into_iter()));
971 let broker = scripted_broker(apis, move |api_key, version, body, own| {
972 let mut received = handler_received.lock().expect("received lock");
973 let reply = match api_key {
974 describe_cluster_request::API_KEY => encode_response(
975 &DescribeClusterResponse {
976 error_code: cluster_error,
977 controller_id: 1,
978 brokers: vec![DescribeClusterBroker {
979 broker_id: 1,
980 host: own.ip().to_string(),
981 port: i32::from(own.port()),
982 ..Default::default()
983 }],
984 ..Default::default()
985 },
986 version,
987 true,
988 ),
989 describe_topic_partitions_request::API_KEY => {
990 received.push(Received::Pages(decode_request(body, version, true)));
991 let answer = pages.lock().expect("pages lock").next();
992 match answer {
993 Some(Answer::Page(page)) => encode_response(&page, version, true),
994 Some(Answer::Close) => return MockReply::Close,
995 None => return MockReply::Silent,
996 }
997 }
998 metadata_request::API_KEY => {
999 let flexible = version >= metadata_request::FLEXIBLE_MIN;
1000 received.push(Received::Metadata(
1001 version,
1002 decode_request(body, version, flexible),
1003 ));
1004 encode_response(&metadata(own, metadata_topics.clone()), version, flexible)
1005 }
1006 _ => return MockReply::Silent,
1007 };
1008 drop(received);
1009 MockReply::Respond(reply)
1010 })
1011 .await;
1012 let admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
1013 let result = admin
1014 .describe_topics(&case.names, DescribeTopicsOptions::builder().build())
1015 .await;
1016 let own = broker.addr;
1017 broker.stop();
1018 let received = received.lock().expect("received lock").clone();
1019 (result, received, own)
1020 }
1021
1022 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1028 async fn describe_topics_pages_and_falls_back_to_metadata() {
1029 let cases = [
1030 DescribeCase {
1031 name: "one page",
1032 names: vec!["beta", "alpha"],
1033 cluster_error: 0,
1034 dtp: true,
1035 answers: vec![Answer::Page(page(
1036 vec![page_topic("alpha", 0, &[0]), page_topic("beta", 0, &[0, 1])],
1037 None,
1038 ))],
1039 metadata_topics: Vec::new(),
1040 expected: |own| {
1041 (
1042 maplit::btreemap! {
1043 "alpha".to_owned() => Ok(paged("alpha", &[0], own)),
1044 "beta".to_owned() => Ok(paged("beta", &[0, 1], own)),
1045 },
1046 vec![Received::Pages(dtp_request(&["alpha", "beta"], None))],
1047 )
1048 },
1049 },
1050 DescribeCase {
1051 name: "the cursor splits a topic across pages",
1052 names: vec!["alpha", "beta"],
1053 cluster_error: 0,
1054 dtp: true,
1055 answers: vec![
1056 Answer::Page(page(
1057 vec![page_topic("alpha", 0, &[0]), page_topic("beta", 0, &[0])],
1058 Some(("beta", 1)),
1059 )),
1060 Answer::Page(page(vec![page_topic("beta", 0, &[1, 2])], None)),
1061 ],
1062 metadata_topics: Vec::new(),
1063 expected: |own| {
1064 (
1065 maplit::btreemap! {
1066 "alpha".to_owned() => Ok(paged("alpha", &[0], own)),
1067 "beta".to_owned() => Ok(paged("beta", &[0, 1, 2], own)),
1068 },
1069 vec![
1070 Received::Pages(dtp_request(&["alpha", "beta"], None)),
1071 Received::Pages(dtp_request(&["beta"], Some(("beta", 1)))),
1072 ],
1073 )
1074 },
1075 },
1076 DescribeCase {
1077 name: "a topic error fails that topic only",
1078 names: vec!["alpha", "beta"],
1079 cluster_error: 0,
1080 dtp: true,
1081 answers: vec![Answer::Page(page(
1082 vec![page_topic("alpha", 29, &[]), page_topic("beta", 0, &[0])],
1083 None,
1084 ))],
1085 metadata_topics: Vec::new(),
1086 expected: |own| {
1087 (
1088 maplit::btreemap! {
1089 "alpha".to_owned() => topic_err(29, None),
1090 "beta".to_owned() => Ok(paged("beta", &[0], own)),
1091 },
1092 vec![Received::Pages(dtp_request(&["alpha", "beta"], None))],
1093 )
1094 },
1095 },
1096 DescribeCase {
1097 name: "a lost connection is retried",
1098 names: vec!["alpha"],
1099 cluster_error: 0,
1100 dtp: true,
1101 answers: vec![
1102 Answer::Close,
1103 Answer::Page(page(vec![page_topic("alpha", 0, &[0])], None)),
1104 ],
1105 metadata_topics: Vec::new(),
1106 expected: |own| {
1107 (
1108 maplit::btreemap! {
1109 "alpha".to_owned() => Ok(paged("alpha", &[0], own)),
1110 },
1111 vec![
1112 Received::Pages(dtp_request(&["alpha"], None)),
1113 Received::Pages(dtp_request(&["alpha"], None)),
1114 ],
1115 )
1116 },
1117 },
1118 DescribeCase {
1119 name: "a peer without DescribeTopicPartitions is asked with Metadata",
1120 names: vec!["alpha", "beta", ""],
1121 cluster_error: 0,
1122 dtp: false,
1123 answers: Vec::new(),
1124 metadata_topics: vec![metadata_topic("alpha", 0, &[1, 0], -1)],
1125 expected: |own| {
1126 (
1127 maplit::btreemap! {
1128 String::new() => topic_err(
1129 17,
1130 Some(
1131 "The given topic name '' cannot be represented in a \
1132 request."
1133 .to_owned(),
1134 ),
1135 ),
1136 "alpha".to_owned() => Ok(from_metadata("alpha", &[0, 1], own, false)),
1137 "beta".to_owned() => topic_err(3, Some("Topic beta not found.".to_owned())),
1138 },
1139 vec![Received::Metadata(
1140 12,
1141 metadata_by_name_request(
1142 &maplit::btreeset! {
1143 "alpha".to_owned(),
1144 "beta".to_owned(),
1145 },
1146 DescribeTopicsOptions::builder().build(),
1147 ),
1148 )],
1149 )
1150 },
1151 },
1152 DescribeCase {
1153 name: "a failed DescribeCluster fails every topic",
1154 names: vec!["alpha"],
1155 cluster_error: 31,
1156 dtp: true,
1157 answers: Vec::new(),
1158 metadata_topics: Vec::new(),
1159 expected: |_| {
1160 (
1161 maplit::btreemap! {
1162 "alpha".to_owned() => topic_err(31, None),
1163 },
1164 Vec::new(),
1165 )
1166 },
1167 },
1168 ];
1169 for case in &cases {
1170 let (result, received, own) = describe_case(case).await;
1171 assert2::assert!(
1172 (result, received) == (case.expected)(own),
1173 "case {}",
1174 case.name
1175 );
1176 }
1177 }
1178
1179 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1182 async fn list_topics_keeps_internal_topics_on_request() {
1183 let listing = |name: &str, is_internal| TopicListing {
1184 name: name.to_owned(),
1185 topic_id: Some(id_of(name)),
1186 is_internal,
1187 };
1188 for (name, list_internal, close_first, expected) in [
1189 (
1190 "without internal topics",
1191 false,
1192 false,
1193 (
1194 maplit::btreemap! {
1195 "alpha".to_owned() => listing("alpha", false),
1196 },
1197 1,
1198 ),
1199 ),
1200 (
1201 "with internal topics",
1202 true,
1203 false,
1204 (
1205 maplit::btreemap! {
1206 "__consumer_offsets".to_owned() => listing("__consumer_offsets", true),
1207 "alpha".to_owned() => listing("alpha", false),
1208 },
1209 1,
1210 ),
1211 ),
1212 (
1213 "a lost connection is retried",
1214 false,
1215 true,
1216 (
1217 maplit::btreemap! {
1218 "alpha".to_owned() => listing("alpha", false),
1219 },
1220 2,
1221 ),
1222 ),
1223 ] {
1224 let requests = Arc::new(Mutex::new(Vec::new()));
1225 let handler_requests = Arc::clone(&requests);
1226 let broker = scripted_broker(Vec::new(), move |api_key, version, body, own| {
1227 if api_key != metadata_request::API_KEY {
1228 return MockReply::Silent;
1229 }
1230 let mut requests = handler_requests.lock().expect("requests lock");
1231 let request: MetadataRequest = decode_request(body, version, true);
1232 requests.push(request);
1233 let first = requests.len() == 1;
1234 drop(requests);
1235 if close_first && first {
1236 return MockReply::Close;
1237 }
1238 MockReply::Respond(encode_response(
1239 &metadata(
1240 own,
1241 vec![
1242 metadata_topic("alpha", 0, &[0], 1),
1243 metadata_topic("__consumer_offsets", 0, &[0], 1),
1244 ],
1245 ),
1246 version,
1247 true,
1248 ))
1249 })
1250 .await;
1251 let admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
1252 let result = admin
1253 .list_topics(
1254 ListTopicsOptions::builder()
1255 .list_internal(list_internal)
1256 .build(),
1257 )
1258 .await
1259 .expect("list topics");
1260 broker.stop();
1261 let requests = requests.lock().expect("requests lock").clone();
1262 assert2::assert!(
1263 (result, requests) == (expected.0, vec![all_topics_request(); expected.1]),
1264 "case {name}"
1265 );
1266 }
1267 }
1268
1269 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1274 async fn describe_topics_by_id_uses_metadata_v12() {
1275 let nil = topic_err(
1276 17,
1277 Some(format!(
1278 "The given topic id '{}' cannot be represented in a request.",
1279 Uuid::nil()
1280 )),
1281 );
1282 let unknown_beta = topic_err(100, Some(format!("TopicId {BETA} not found.")));
1283 let unsupported = topic_err(35, None);
1284 let options = DescribeTopicsOptions::builder()
1285 .include_authorized_operations(true)
1286 .build();
1287 let asked = maplit::btreeset! {
1288 ALPHA,
1289 BETA,
1290 };
1291 for (name, max_metadata, topics, expected_requests) in [
1292 (
1293 "found and missing",
1294 12,
1295 vec![metadata_topic("alpha", 0, &[0], 1)],
1296 vec![(12, metadata_by_id_request(&asked, options))],
1297 ),
1298 (
1299 "a topic with an error is unknown",
1300 13,
1301 vec![
1302 metadata_topic("alpha", 0, &[0], 1),
1303 metadata_topic("beta", 29, &[], -1),
1304 ],
1305 vec![(13, metadata_by_id_request(&asked, options))],
1306 ),
1307 ("a peer below v12", 11, Vec::new(), Vec::new()),
1308 ] {
1309 let requests = Arc::new(Mutex::new(Vec::new()));
1310 let handler_requests = Arc::clone(&requests);
1311 let broker = scripted_broker(
1312 vec![
1313 ApiVersionRange::builder()
1314 .api_key(metadata_request::API_KEY)
1315 .min_version(1)
1316 .max_version(max_metadata)
1317 .build(),
1318 ],
1319 move |api_key, version, body, own| {
1320 if api_key != metadata_request::API_KEY {
1321 return MockReply::Silent;
1322 }
1323 let request: MetadataRequest = decode_request(body, version, true);
1324 handler_requests
1325 .lock()
1326 .expect("requests lock")
1327 .push((version, request));
1328 MockReply::Respond(encode_response(
1329 &metadata(own, topics.clone()),
1330 version,
1331 true,
1332 ))
1333 },
1334 )
1335 .await;
1336 let admin = fast_admin(broker.addr, krabka_units::secs(5)).await;
1337 let result = admin
1338 .describe_topics_by_id(&[BETA, Uuid::nil(), ALPHA], options)
1339 .await
1340 .into_iter()
1341 .map(|(id, result)| {
1342 let result = result.map_err(|error| KafkaError {
1345 message: error.message.filter(|_| error.code != 35),
1346 ..error
1347 });
1348 (id, result)
1349 })
1350 .collect::<BTreeMap<_, _>>();
1351 let own = broker.addr;
1352 broker.stop();
1353 let expected = if max_metadata < 12 {
1354 maplit::btreemap! {
1355 Uuid::nil() => nil.clone(),
1356 ALPHA => unsupported.clone(),
1357 BETA => unsupported.clone(),
1358 }
1359 } else {
1360 maplit::btreemap! {
1361 Uuid::nil() => nil.clone(),
1362 ALPHA => Ok(from_metadata("alpha", &[0], own, true)),
1363 BETA => unknown_beta.clone(),
1364 }
1365 };
1366 assert2::assert!(
1367 (result, requests.lock().expect("requests lock").clone())
1368 == (expected, expected_requests),
1369 "case {name}"
1370 );
1371 }
1372 }
1373
1374 #[test]
1377 fn describe_page_reads_authorized_operations_on_request() {
1378 for (include, expected) in [
1379 (false, None),
1380 (
1381 true,
1382 Some(maplit::btreeset! {
1383 AclOperation::Read,
1384 }),
1385 ),
1386 ] {
1387 let description = description_from_describe(
1388 &page_topic("alpha", 0, &[]),
1389 "alpha".to_owned(),
1390 &BTreeMap::new(),
1391 include,
1392 );
1393 assert2::assert!(
1394 description.authorized_operations == expected,
1395 "include {include}"
1396 );
1397 }
1398 }
1399}