1use std::{
17 any::Any,
18 collections::BTreeMap,
19 io,
20 net::SocketAddr,
21 sync::{Arc, Mutex, PoisonError},
22};
23
24#[cfg(test)]
25use krabka_client_core::{ClientDnsTimeout, MockBroker};
26use krabka_client_core::{
27 ClientError, Connection, ConnectionOptions, MetadataRecoveryRebootstrapTrigger,
28 MetadataRecoveryStrategy, ProtocolRequest as _, connection_target_host, security,
29 telemetry::{
30 ClientMetrics, ClientTelemetry, ClientTelemetryConfig, NetworkMetrics, TelemetryClientType,
31 TelemetryTiming,
32 },
33 transport,
34};
35use krabka_protocol::owned::{
36 describe_cluster_response::DescribeClusterResponse, metadata_request, metadata_response,
37};
38use krabka_units::{Time, convert::TimeExt as _};
39use thiserror::Error;
40use tokio::{
41 sync::RwLock,
42 time::{Instant, timeout},
43};
44
45pub mod brokers;
46pub mod cluster;
47mod config;
48pub mod config_resources;
49pub mod configs;
50pub mod consumer_group_listings;
51pub mod delegation_tokens;
52pub mod elections;
53pub mod features;
54mod group_coordinators;
55pub mod group_deletions;
56pub mod group_descriptions;
57pub mod groups;
58pub mod log_dirs;
59pub mod offsets;
60mod partition_leaders;
61pub mod quorum;
62pub mod quotas;
63mod retry;
64pub mod share_group_offsets;
65mod telemetry;
66pub mod topic_descriptions;
67pub mod topics;
68pub mod transactions;
69
70#[cfg(test)]
71#[macro_export]
72macro_rules! encode_test_struct_field {
73 ($encoder:ident, $response:ident, $field:ident, $value:expr, $version:expr, $flexible:expr $(,)?) => {
74 $encoder(
75 &$response {
76 $field: $value,
77 ..Default::default()
78 },
79 $version,
80 $flexible,
81 )
82 };
83}
84
85#[cfg(test)]
86#[macro_export]
87macro_rules! encode_test_struct_fields {
88 ($encoder:ident, $response:ident, { $($field:ident: $value:expr),+ $(,)? }, $version:expr, $flexible:expr $(,)?) => {
89 $encoder(
90 &$response {
91 $($field: $value,)+
92 ..Default::default()
93 },
94 $version,
95 $flexible,
96 )
97 };
98}
99pub mod users;
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct MetadataVersionUpdate {
104 pub level: i16,
105}
106
107pub use cluster::{ClusterDescription, ClusterNode, DescribeClusterOptions};
108pub use config::{
109 AdminClientConfig, DEFAULT_ADMIN_CONNECTIONS_MAX_IDLE, DEFAULT_ADMIN_REQUEST_TIMEOUT,
110 DEFAULT_API_TIMEOUT, DEFAULT_ENABLE_METRICS_PUSH, DEFAULT_RETRY_BACKOFF,
111 DEFAULT_RETRY_BACKOFF_MAX, DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT,
112 DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX,
113};
114pub use config_resources::{ConfigResource, ConfigResourceType};
115pub use configs::{
116 AlterConfigOp, AlterConfigOpType, AlterConfigsResults, Config, ConfigEntry, ConfigSource,
117 ConfigSynonym, ConfigType, DescribeConfigsOptions, DescribeConfigsResults,
118 IncrementalAlterConfigsOptions,
119};
120pub use consumer_group_listings::{
121 ConsumerGroupListing, ListConsumerGroupsOptions, ListConsumerGroupsResult,
122};
123pub use delegation_tokens::{
124 CreateDelegationTokenOptions, DelegationToken, DescribeDelegationTokenOptions,
125 ExpireDelegationTokenOptions, RenewDelegationTokenOptions,
126};
127pub use elections::{ElectionResults, ElectionType};
128pub use features::{
129 DescribeFeaturesOptions, FeatureMetadata, FeatureRange, FeatureUpdate, UpdateFeaturesOptions,
130 UpdateFeaturesResults, UpgradeType,
131};
132pub use group_deletions::GroupDeletions;
133pub use group_descriptions::{
134 ClassicGroupDescription, ConsumerGroupDescription, DescribeGroupsOptions, GroupDescriptions,
135 MemberDescription, ShareGroupDescription, StreamsGroupDescription,
136};
137pub use groups::{
138 ConsumerGroupOffsetOutcome, MemberToRemove, RemoveMembersOptions, RemovedMember, RemovedMembers,
139};
140pub use log_dirs::{
141 BrokerResult, LogDirInfo, LogDirPartitionInfo, LogDirTopicInfo, ReplicaLogDir,
142 ReplicaLogDirInfo, TopicPartitionReplica,
143};
144pub use offsets::{IsolationLevel, ListedOffset, OffsetSpec};
145pub use quorum::{MetadataQuorum, QuorumNode, QuorumReplica, RaftVoterEndpoint};
146pub use quotas::{
147 ClientQuotaAlteration, ClientQuotaEntity, ClientQuotaFilter, ClientQuotaFilterComponent,
148 ClientQuotaMatch, ClientQuotas, ENTITY_CLIENT_ID, ENTITY_IP, ENTITY_USER, QuotaOp,
149 UserQuotaConfig, diff_user_quotas,
150};
151pub use share_group_offsets::{
152 ListShareGroupOffsetsSpec, ShareGroupOffsets, SharePartitionOffsetInfo,
153};
154pub use topic_descriptions::{
155 DescribeTopicsOptions, ListTopicsOptions, TopicDescription, TopicDescriptions, TopicListing,
156 TopicPartitionInfo,
157};
158pub use topics::{
159 AlterPartitionReassignmentsOptions, CreatePartitionsOp, CreatePartitionsOutcome,
160 CreateTopicOutcome, CreateTopicSpec, DeleteRecordsOp, DeleteRecordsOutcome, DeleteTopicOutcome,
161 PartitionAssignment, PartitionAssignmentOutcome, TopicMetadata, TopicMetadataEntry,
162 TopicMutationOptions, TopicReplicationStatus,
163};
164pub use transactions::{
165 AbortTransactionSpec, ListTransactionsFilter, ProducerStateInfo, TransactionDescription,
166 TransactionListing,
167};
168pub use users::{
169 AclEntry, AclEntryFilter, AclOperation, CreateAclOutcome, DEFAULT_SCRAM_ITERATIONS,
170 DeleteAclFilterOutcome, MAX_SCRAM_ITERATIONS, MIN_SCRAM_ITERATIONS, PatternType,
171 PermissionType, ResourceType, ScramDeletion, ScramIterations, ScramUpsertion, ScramUserOutcome,
172 UserScramCredential, UserScramCredentials,
173};
174
175#[async_trait::async_trait]
185pub trait AdminClientLike: Send {
186 async fn update_metadata_version(
192 &mut self,
193 _level: i16,
194 _upgrade_type: UpgradeType,
195 _timeout: Time,
196 ) -> Result<MetadataVersionUpdate, AdminError> {
197 Err(AdminError::Protocol(
198 "UpdateFeatures is not implemented by this admin client".into(),
199 ))
200 }
201 async fn describe_metadata_quorum(&mut self) -> Result<MetadataQuorum, AdminError> {
206 Err(AdminError::Protocol(
207 "DescribeQuorum is not implemented by this admin client".into(),
208 ))
209 }
210 async fn remove_raft_voter(
225 &mut self,
226 _cluster_id: Option<&str>,
227 _node_id: i32,
228 _directory_id: uuid::Uuid,
229 ) -> Result<(), AdminError> {
230 Err(AdminError::Protocol(
231 "RemoveRaftVoter is not implemented by this admin client".into(),
232 ))
233 }
234 async fn metadata(&mut self, topics: &[&str]) -> Result<TopicMetadata, AdminError>;
240 async fn reconcile_topic_replication_factor(
252 &mut self,
253 topic: &str,
254 replication_factor: i32,
255 timeout: Time,
256 ) -> Result<TopicReplicationStatus, AdminError>;
257 async fn describe_partition_assignments(
265 &mut self,
266 _topics: &[&str],
267 ) -> Result<Vec<PartitionAssignment>, AdminError> {
268 Err(AdminError::Protocol(
269 "Describe partition assignments is not implemented by this admin client".into(),
270 ))
271 }
272 async fn list_partition_reassignments(
279 &mut self,
280 _partitions: &BTreeMap<String, Vec<i32>>,
281 _timeout: Time,
282 ) -> Result<Vec<PartitionAssignment>, AdminError> {
283 Err(AdminError::Protocol(
284 "ListPartitionReassignments is not implemented by this admin client".into(),
285 ))
286 }
287 async fn alter_partition_assignments(
294 &mut self,
295 _assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
296 _timeout: Time,
297 ) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
298 Err(AdminError::Protocol(
299 "AlterPartitionReassignments is not implemented by this admin client".into(),
300 ))
301 }
302 async fn unregister_broker(&mut self, _broker_id: i32) -> Result<(), AdminError> {
313 Err(AdminError::Protocol(
314 "UnregisterBroker is not implemented by this admin client".into(),
315 ))
316 }
317 async fn create_topics(
338 &mut self,
339 specs: &[CreateTopicSpec],
340 options: TopicMutationOptions,
341 ) -> Result<Vec<CreateTopicOutcome>, AdminError>;
342 async fn delete_topics(
350 &mut self,
351 names: &[&str],
352 options: TopicMutationOptions,
353 ) -> Result<Vec<DeleteTopicOutcome>, AdminError>;
354 async fn create_partitions(
362 &mut self,
363 ops: &[CreatePartitionsOp],
364 options: TopicMutationOptions,
365 ) -> Result<Vec<CreatePartitionsOutcome>, AdminError>;
366 async fn delete_records(
378 &mut self,
379 ops: &[DeleteRecordsOp],
380 timeout: Time,
381 ) -> Result<Vec<DeleteRecordsOutcome>, AdminError>;
382 async fn describe_configs(
407 &mut self,
408 resources: &[ConfigResource],
409 options: DescribeConfigsOptions,
410 ) -> Result<DescribeConfigsResults, AdminError>;
411 async fn incremental_alter_configs(
435 &mut self,
436 configs: &BTreeMap<ConfigResource, Vec<AlterConfigOp>>,
437 options: IncrementalAlterConfigsOptions,
438 ) -> Result<AlterConfigsResults, AdminError>;
439 async fn alter_user_scram_credentials_sha512(
451 &mut self,
452 upsertions: &[ScramUpsertion],
453 deletions: &[ScramDeletion],
454 ) -> Result<Vec<ScramUserOutcome>, AdminError>;
455 async fn alter_user_scram_credentials_sha256(
468 &mut self,
469 upsertions: &[ScramUpsertion],
470 deletions: &[ScramDeletion],
471 ) -> Result<Vec<ScramUserOutcome>, AdminError>;
472 async fn describe_acls(&mut self, filter: &AclEntryFilter)
481 -> Result<Vec<AclEntry>, AdminError>;
482 async fn create_acls(
491 &mut self,
492 creations: &[AclEntry],
493 ) -> Result<Vec<CreateAclOutcome>, AdminError>;
494 async fn delete_acls(
502 &mut self,
503 filters: &[AclEntryFilter],
504 ) -> Result<Vec<DeleteAclFilterOutcome>, AdminError>;
505 async fn describe_user_quotas(&mut self, username: &str)
514 -> Result<UserQuotaConfig, AdminError>;
515 async fn alter_user_quotas(
522 &mut self,
523 username: &str,
524 ops: &[QuotaOp],
525 validate_only: bool,
526 ) -> Result<Option<KafkaError>, AdminError>;
527
528 async fn create_delegation_token(
552 &mut self,
553 options: &CreateDelegationTokenOptions,
554 ) -> Result<DelegationToken, AdminError>;
555 async fn renew_delegation_token(
566 &mut self,
567 hmac: &[u8],
568 options: RenewDelegationTokenOptions,
569 ) -> Result<i64, AdminError>;
570 async fn expire_delegation_token(
582 &mut self,
583 hmac: &[u8],
584 options: ExpireDelegationTokenOptions,
585 ) -> Result<i64, AdminError>;
586 async fn describe_delegation_token(
596 &mut self,
597 options: &DescribeDelegationTokenOptions,
598 ) -> Result<Vec<DelegationToken>, AdminError>;
599}
600
601#[async_trait::async_trait]
602impl AdminClientLike for AdminClient {
603 async fn update_metadata_version(
604 &mut self,
605 level: i16,
606 upgrade_type: UpgradeType,
607 timeout: Time,
608 ) -> Result<MetadataVersionUpdate, AdminError> {
609 Self::update_metadata_version_impl(self, level, upgrade_type, timeout).await
610 }
611
612 async fn describe_metadata_quorum(&mut self) -> Result<MetadataQuorum, AdminError> {
613 Self::describe_metadata_quorum_impl(self).await
614 }
615
616 async fn remove_raft_voter(
617 &mut self,
618 cluster_id: Option<&str>,
619 node_id: i32,
620 directory_id: uuid::Uuid,
621 ) -> Result<(), AdminError> {
622 Self::remove_raft_voter_impl(self, cluster_id, node_id, directory_id).await
623 }
624
625 async fn metadata(&mut self, topics: &[&str]) -> Result<TopicMetadata, AdminError> {
626 Self::metadata_impl(self, topics).await
627 }
628 async fn reconcile_topic_replication_factor(
629 &mut self,
630 topic: &str,
631 replication_factor: i32,
632 timeout: Time,
633 ) -> Result<TopicReplicationStatus, AdminError> {
634 Self::reconcile_topic_replication_factor_impl(self, topic, replication_factor, timeout)
635 .await
636 }
637 async fn describe_partition_assignments(
638 &mut self,
639 topics: &[&str],
640 ) -> Result<Vec<PartitionAssignment>, AdminError> {
641 Self::describe_partition_assignments_impl(self, topics).await
642 }
643 async fn list_partition_reassignments(
644 &mut self,
645 partitions: &BTreeMap<String, Vec<i32>>,
646 timeout: Time,
647 ) -> Result<Vec<PartitionAssignment>, AdminError> {
648 Self::list_partition_reassignments_impl(self, partitions, timeout).await
649 }
650 async fn alter_partition_assignments(
651 &mut self,
652 assignments: &BTreeMap<(String, i32), Option<Vec<i32>>>,
653 timeout: Time,
654 ) -> Result<Vec<PartitionAssignmentOutcome>, AdminError> {
655 Self::alter_partition_assignments_impl(self, assignments, timeout).await
656 }
657 async fn unregister_broker(&mut self, broker_id: i32) -> Result<(), AdminError> {
658 Self::unregister_broker_impl(self, broker_id).await
659 }
660 async fn create_topics(
661 &mut self,
662 specs: &[CreateTopicSpec],
663 options: TopicMutationOptions,
664 ) -> Result<Vec<CreateTopicOutcome>, AdminError> {
665 Self::create_topics_impl(self, specs, options).await
666 }
667 async fn delete_topics(
668 &mut self,
669 names: &[&str],
670 options: TopicMutationOptions,
671 ) -> Result<Vec<DeleteTopicOutcome>, AdminError> {
672 Self::delete_topics_impl(self, names, options).await
673 }
674 async fn create_partitions(
675 &mut self,
676 ops: &[CreatePartitionsOp],
677 options: TopicMutationOptions,
678 ) -> Result<Vec<CreatePartitionsOutcome>, AdminError> {
679 Self::create_partitions_impl(self, ops, options).await
680 }
681 async fn delete_records(
682 &mut self,
683 ops: &[DeleteRecordsOp],
684 timeout: Time,
685 ) -> Result<Vec<DeleteRecordsOutcome>, AdminError> {
686 Self::delete_records_impl(self, ops, timeout).await
687 }
688 async fn describe_configs(
689 &mut self,
690 resources: &[ConfigResource],
691 options: DescribeConfigsOptions,
692 ) -> Result<DescribeConfigsResults, AdminError> {
693 Self::describe_configs_impl(self, resources, options).await
694 }
695 async fn incremental_alter_configs(
696 &mut self,
697 configs: &BTreeMap<ConfigResource, Vec<AlterConfigOp>>,
698 options: IncrementalAlterConfigsOptions,
699 ) -> Result<AlterConfigsResults, AdminError> {
700 Self::incremental_alter_configs_impl(self, configs, options).await
701 }
702 async fn alter_user_scram_credentials_sha512(
703 &mut self,
704 upsertions: &[ScramUpsertion],
705 deletions: &[ScramDeletion],
706 ) -> Result<Vec<ScramUserOutcome>, AdminError> {
707 Self::alter_user_scram_credentials_sha512_impl(self, upsertions, deletions).await
708 }
709 async fn alter_user_scram_credentials_sha256(
710 &mut self,
711 upsertions: &[ScramUpsertion],
712 deletions: &[ScramDeletion],
713 ) -> Result<Vec<ScramUserOutcome>, AdminError> {
714 Self::alter_user_scram_credentials_sha256_impl(self, upsertions, deletions).await
715 }
716 async fn describe_acls(
717 &mut self,
718 filter: &AclEntryFilter,
719 ) -> Result<Vec<AclEntry>, AdminError> {
720 Self::describe_acls_impl(self, filter).await
721 }
722 async fn create_acls(
723 &mut self,
724 creations: &[AclEntry],
725 ) -> Result<Vec<CreateAclOutcome>, AdminError> {
726 Self::create_acls_impl(self, creations).await
727 }
728 async fn delete_acls(
729 &mut self,
730 filters: &[AclEntryFilter],
731 ) -> Result<Vec<DeleteAclFilterOutcome>, AdminError> {
732 Self::delete_acls_impl(self, filters).await
733 }
734 async fn describe_user_quotas(
735 &mut self,
736 username: &str,
737 ) -> Result<UserQuotaConfig, AdminError> {
738 Self::describe_user_quotas_impl(self, username).await
739 }
740 async fn alter_user_quotas(
741 &mut self,
742 username: &str,
743 ops: &[QuotaOp],
744 validate_only: bool,
745 ) -> Result<Option<KafkaError>, AdminError> {
746 Self::alter_user_quotas_impl(self, username, ops, validate_only).await
747 }
748
749 async fn create_delegation_token(
750 &mut self,
751 options: &CreateDelegationTokenOptions,
752 ) -> Result<DelegationToken, AdminError> {
753 Self::create_delegation_token_impl(self, options).await
754 }
755 async fn renew_delegation_token(
756 &mut self,
757 hmac: &[u8],
758 options: RenewDelegationTokenOptions,
759 ) -> Result<i64, AdminError> {
760 Self::renew_delegation_token_impl(self, hmac, options).await
761 }
762 async fn expire_delegation_token(
763 &mut self,
764 hmac: &[u8],
765 options: ExpireDelegationTokenOptions,
766 ) -> Result<i64, AdminError> {
767 Self::expire_delegation_token_impl(self, hmac, options).await
768 }
769 async fn describe_delegation_token(
770 &mut self,
771 options: &DescribeDelegationTokenOptions,
772 ) -> Result<Vec<DelegationToken>, AdminError> {
773 Self::describe_delegation_token_impl(self, options).await
774 }
775}
776
777#[derive(Debug, Error)]
778pub enum AdminError {
779 #[error("no bootstrap address connected: tried {tried}{cause}",
791 cause = .source.as_ref().map(|e| format!("; last error: {e}")).unwrap_or_default())]
792 Connect {
793 tried: usize,
794 #[source]
795 source: Option<Box<Self>>,
796 },
797 #[error("invalid admin client configuration: {0}")]
800 InvalidConfig(String),
801 #[error("invalid argument: {0}")]
804 InvalidArgument(String),
805 #[error("broker returned error: api={api} code={code} ({name}){detail}",
806 detail = .message.as_deref().map(|m| format!(" {m:?}")).unwrap_or_default())]
807 Broker {
808 api: &'static str,
809 code: i16,
810 name: &'static str,
811 message: Option<String>,
812 },
813 #[error("client-core: {0}")]
814 Transport(#[from] ClientError),
815 #[error("protocol: {0}")]
816 Protocol(String),
817}
818
819impl AdminError {
820 #[must_use]
824 pub const fn is_authentication_failure(&self) -> bool {
825 matches!(self, Self::Transport(error) if error.is_authentication_failure())
826 }
827}
828
829#[derive(Debug, Clone, PartialEq, Eq)]
831pub struct KafkaError {
832 pub code: i16,
833 pub name: &'static str,
834 pub message: Option<String>,
835}
836
837pub(crate) fn kafka_error_if(code: i16, message: Option<String>) -> Option<KafkaError> {
838 if code == 0 {
839 None
840 } else {
841 Some(KafkaError {
842 code,
843 name: kafka_error_name(code),
844 message,
845 })
846 }
847}
848
849async fn lookup_first<F, I>(
850 host_port: &str,
851 dns_timeout: krabka_client_core::ClientDnsTimeout,
852 lookup: F,
853) -> Result<SocketAddr, AdminError>
854where
855 F: Future<Output = io::Result<I>>,
856 I: IntoIterator<Item = SocketAddr>,
857{
858 let addrs = timeout(dns_timeout.time().to_std(), lookup)
859 .await
860 .map_err(|_| {
861 AdminError::Protocol(format!(
862 "DNS lookup {host_port} timed out after {} ms",
863 dns_timeout.milliseconds(),
864 ))
865 })?
866 .map_err(|error| AdminError::Protocol(format!("DNS lookup {host_port}: {error}")))?;
867 addrs
868 .into_iter()
869 .next()
870 .ok_or_else(|| AdminError::Protocol(format!("no addresses for {host_port}")))
871}
872
873fn format_host_port(host: &str, port: i32) -> String {
874 if host.contains(':') && !host.starts_with('[') {
875 format!("[{host}]:{port}")
876 } else {
877 format!("{host}:{port}")
878 }
879}
880
881pub struct AdminClient {
893 pub(crate) conn: Arc<RecoveringConnection>,
894 bootstrap_addrs: Vec<String>,
895 options: ConnectionOptions,
899 pub(crate) retry: retry::RetryPolicy,
903 telemetry: Option<ClientTelemetry>,
906}
907
908#[derive(Clone, Copy, Debug, Eq, PartialEq)]
909enum BootstrapTarget {
910 Brokers,
911 Controllers,
912}
913
914struct ConnectedTarget {
915 connection: Connection,
916 discovered_addrs: Vec<String>,
917}
918
919pub(crate) struct RecoveringConnection {
921 inner: RwLock<Arc<Connection>>,
922 bootstrap_addrs: Vec<String>,
923 options: ConnectionOptions,
924 strategy: MetadataRecoveryStrategy,
925 trigger: MetadataRecoveryRebootstrapTrigger,
926 target: BootstrapTarget,
927 known_addrs: Mutex<Vec<String>>,
928 first_metadata_attempt: Mutex<Option<Instant>>,
929}
930
931impl RecoveringConnection {
932 fn new(
933 connection: Connection,
934 bootstrap_addrs: Vec<String>,
935 options: ConnectionOptions,
936 strategy: MetadataRecoveryStrategy,
937 trigger: MetadataRecoveryRebootstrapTrigger,
938 target: BootstrapTarget,
939 known_addrs: Vec<String>,
940 ) -> Self {
941 Self {
942 inner: RwLock::new(Arc::new(connection)),
943 bootstrap_addrs,
944 options,
945 strategy,
946 trigger,
947 target,
948 known_addrs: Mutex::new(known_addrs),
949 first_metadata_attempt: Mutex::new(None),
950 }
951 }
952
953 pub(crate) async fn send<R>(&self, request: R) -> Result<R::Response, AdminError>
954 where
955 R: krabka_protocol::ProtocolRequest + Clone,
956 R::Response: 'static,
957 {
958 self.send_with_min_version(request, None).await
959 }
960
961 pub(crate) async fn send_at_least<R>(
962 &self,
963 request: R,
964 min_version: i16,
965 ) -> Result<R::Response, AdminError>
966 where
967 R: krabka_protocol::ProtocolRequest + Clone,
968 R::Response: 'static,
969 {
970 self.send_with_min_version(request, Some(min_version)).await
971 }
972
973 async fn send_with_min_version<R>(
974 &self,
975 request: R,
976 min_version: Option<i16>,
977 ) -> Result<R::Response, AdminError>
978 where
979 R: krabka_protocol::ProtocolRequest + Clone,
980 R::Response: 'static,
981 {
982 self.require_advertised_controller_api::<R>().await?;
983 self.begin_metadata_attempt::<R>();
984 let first = self.send_current(request.clone(), min_version).await;
985 match first {
986 Ok(response) if Self::response_requires_rebootstrap(&response) => {
987 if self.strategy == MetadataRecoveryStrategy::None {
988 return Err(AdminError::Transport(ClientError::Server {
989 error_code: 129,
990 }));
991 }
992 self.send_after_rebootstrap(request, min_version).await
993 }
994 Ok(response) if self.metadata_attempt_timed_out(&response) => {
995 self.send_after_rebootstrap(request, min_version).await
996 }
997 Ok(response) => {
998 self.observe_cluster_endpoints(&response);
999 self.complete_metadata_attempt(&response);
1000 Ok(response)
1001 }
1002 Err(error) if AdminClient::is_retriable_transport_error(&error) => {
1003 match self
1004 .send_after_current_metadata(request.clone(), min_version)
1005 .await
1006 {
1007 Ok(response) => Ok(response),
1008 Err(retry_error) if retry_error.is_authentication_failure() => Err(retry_error),
1009 Err(_) if self.strategy == MetadataRecoveryStrategy::Rebootstrap => {
1010 self.send_after_rebootstrap(request, min_version).await
1011 }
1012 Err(_) => Err(AdminError::from(error)),
1013 }
1014 }
1015 Err(error) => Err(AdminError::from(error)),
1016 }
1017 }
1018
1019 async fn replace(&self, connection: Connection) {
1020 *self.inner.write().await = Arc::new(connection);
1021 }
1022
1023 async fn send_current<R>(
1024 &self,
1025 request: R,
1026 min_version: Option<i16>,
1027 ) -> Result<R::Response, ClientError>
1028 where
1029 R: krabka_protocol::ProtocolRequest,
1030 {
1031 let connection = self.inner.read().await;
1032 if let Some(min_version) = min_version {
1033 return send_connection_at_least(&connection, request, min_version).await;
1034 }
1035 connection.send(request).await
1036 }
1037
1038 async fn send_after_rebootstrap<R>(
1039 &self,
1040 request: R,
1041 min_version: Option<i16>,
1042 ) -> Result<R::Response, AdminError>
1043 where
1044 R: krabka_protocol::ProtocolRequest,
1045 R::Response: 'static,
1046 {
1047 self.rebootstrap().await?;
1048 self.require_advertised_controller_api::<R>().await?;
1049 self.begin_metadata_attempt::<R>();
1050 let response = self.send_current(request, min_version).await?;
1051 if Self::response_requires_rebootstrap(&response) {
1052 return Err(AdminError::Transport(ClientError::Server {
1053 error_code: 129,
1054 }));
1055 }
1056 self.observe_cluster_endpoints(&response);
1057 self.complete_metadata_attempt(&response);
1058 Ok(response)
1059 }
1060
1061 async fn send_after_current_metadata<R>(
1062 &self,
1063 request: R,
1064 min_version: Option<i16>,
1065 ) -> Result<R::Response, AdminError>
1066 where
1067 R: krabka_protocol::ProtocolRequest + Clone,
1068 R::Response: 'static,
1069 {
1070 self.reconnect_current_metadata().await?;
1071 self.require_advertised_controller_api::<R>().await?;
1072 let response = self.send_current(request.clone(), min_version).await?;
1073 if Self::response_requires_rebootstrap(&response) {
1074 if self.strategy == MetadataRecoveryStrategy::Rebootstrap {
1075 return self.send_after_rebootstrap(request, min_version).await;
1076 }
1077 return Err(AdminError::Transport(ClientError::Server {
1078 error_code: 129,
1079 }));
1080 }
1081 if self.metadata_attempt_timed_out(&response) {
1082 return self.send_after_rebootstrap(request, min_version).await;
1083 }
1084 self.observe_cluster_endpoints(&response);
1085 self.complete_metadata_attempt(&response);
1086 Ok(response)
1087 }
1088
1089 async fn require_advertised_controller_api<R>(&self) -> Result<(), AdminError>
1090 where
1091 R: krabka_protocol::ProtocolRequest,
1092 {
1093 const UNSUPPORTED_ENDPOINT_TYPE: i16 = 115;
1094 if self.target == BootstrapTarget::Controllers
1095 && self
1096 .inner
1097 .read()
1098 .await
1099 .advertised_api_range(R::API_KEY)
1100 .is_none()
1101 {
1102 return Err(AdminError::Broker {
1103 api: "ControllerEndpoint",
1104 code: UNSUPPORTED_ENDPOINT_TYPE,
1105 name: kafka_error_name(UNSUPPORTED_ENDPOINT_TYPE),
1106 message: Some(format!(
1107 "api_key {} is not supported by the controller listener",
1108 R::API_KEY
1109 )),
1110 });
1111 }
1112 Ok(())
1113 }
1114
1115 pub(crate) async fn rebootstrap(&self) -> Result<(), AdminError> {
1116 let mut last_error = None;
1117 for host_port in &self.bootstrap_addrs {
1118 match AdminClient::connect_target_one_with_discovery(
1119 host_port,
1120 self.options.clone(),
1121 self.target,
1122 )
1123 .await
1124 {
1125 Ok(connected) => {
1126 self.replace(connected.connection).await;
1127 self.replace_known_addrs(if connected.discovered_addrs.is_empty() {
1128 self.bootstrap_addrs.clone()
1129 } else {
1130 connected.discovered_addrs
1131 });
1132 self.clear_metadata_attempt();
1133 return Ok(());
1134 }
1135 Err(error) if error.is_authentication_failure() => return Err(error),
1136 Err(error) => last_error = Some(Box::new(error)),
1137 }
1138 }
1139 Err(AdminError::Connect {
1140 tried: self.bootstrap_addrs.len(),
1141 source: last_error,
1142 })
1143 }
1144
1145 async fn reconnect_current_metadata(&self) -> Result<(), AdminError> {
1146 let known_addrs = self
1147 .known_addrs
1148 .lock()
1149 .unwrap_or_else(PoisonError::into_inner)
1150 .clone();
1151 let mut last_error = None;
1152 for host_port in &known_addrs {
1153 match AdminClient::connect_target_one_with_discovery(
1154 host_port,
1155 self.options.clone(),
1156 self.target,
1157 )
1158 .await
1159 {
1160 Ok(connected) => {
1161 self.replace(connected.connection).await;
1162 if !connected.discovered_addrs.is_empty() {
1163 self.replace_known_addrs(connected.discovered_addrs);
1164 }
1165 return Ok(());
1166 }
1167 Err(error) if error.is_authentication_failure() => return Err(error),
1168 Err(error) => last_error = Some(Box::new(error)),
1169 }
1170 }
1171 Err(AdminError::Connect {
1172 tried: known_addrs.len(),
1173 source: last_error,
1174 })
1175 }
1176
1177 fn replace_known_addrs(&self, addrs: Vec<String>) {
1178 *self
1179 .known_addrs
1180 .lock()
1181 .unwrap_or_else(PoisonError::into_inner) = addrs;
1182 }
1183
1184 fn observe_cluster_endpoints<T: Any>(&self, response: &T) {
1185 if let Some(metadata) =
1186 (response as &dyn Any).downcast_ref::<metadata_response::MetadataResponse>()
1187 {
1188 let endpoints = metadata
1189 .brokers
1190 .iter()
1191 .filter(|broker| !broker.host.is_empty() && broker.port > 0)
1192 .map(|broker| format_host_port(&broker.host, broker.port))
1193 .collect::<Vec<_>>();
1194 if !endpoints.is_empty() {
1195 self.replace_known_addrs(endpoints);
1196 }
1197 return;
1198 }
1199 if self.target == BootstrapTarget::Controllers
1200 && let Some(cluster) = (response as &dyn Any).downcast_ref::<DescribeClusterResponse>()
1201 {
1202 let endpoints = cluster
1203 .brokers
1204 .iter()
1205 .filter(|controller| !controller.host.is_empty() && controller.port > 0)
1206 .map(|controller| format_host_port(&controller.host, controller.port))
1207 .collect::<Vec<_>>();
1208 if !endpoints.is_empty() {
1209 self.replace_known_addrs(endpoints);
1210 }
1211 }
1212 }
1213
1214 pub(crate) fn uses_controller_bootstrap(&self) -> bool {
1215 self.target == BootstrapTarget::Controllers
1216 }
1217
1218 pub(crate) async fn advertised_api_range(&self, api_key: i16) -> Option<(i16, i16)> {
1221 self.inner.read().await.advertised_api_range(api_key)
1222 }
1223
1224 fn begin_metadata_attempt<R: krabka_protocol::ProtocolRequest>(&self) {
1225 if R::API_KEY != metadata_request::MetadataRequest::API_KEY
1226 || self.strategy == MetadataRecoveryStrategy::None
1227 {
1228 return;
1229 }
1230 self.first_metadata_attempt
1231 .lock()
1232 .unwrap_or_else(PoisonError::into_inner)
1233 .get_or_insert_with(Instant::now);
1234 }
1235
1236 fn response_requires_rebootstrap<T: Any>(response: &T) -> bool {
1237 (response as &dyn Any)
1238 .downcast_ref::<metadata_response::MetadataResponse>()
1239 .is_some_and(|metadata| metadata.error_code == 129)
1240 }
1241
1242 fn metadata_attempt_timed_out<T: Any>(&self, response: &T) -> bool {
1243 if self.strategy == MetadataRecoveryStrategy::None {
1244 return false;
1245 }
1246 let Some(metadata) =
1247 (response as &dyn Any).downcast_ref::<metadata_response::MetadataResponse>()
1248 else {
1249 return false;
1250 };
1251 metadata.brokers.is_empty()
1252 && self
1253 .first_metadata_attempt
1254 .lock()
1255 .unwrap_or_else(PoisonError::into_inner)
1256 .is_some_and(|started| started.elapsed() >= self.trigger.time().to_std())
1257 }
1258
1259 fn complete_metadata_attempt<T: Any>(&self, response: &T) {
1260 let has_brokers = (response as &dyn Any)
1261 .downcast_ref::<metadata_response::MetadataResponse>()
1262 .is_some_and(|metadata| !metadata.brokers.is_empty());
1263 if has_brokers {
1264 self.clear_metadata_attempt();
1265 }
1266 }
1267
1268 fn clear_metadata_attempt(&self) {
1269 *self
1270 .first_metadata_attempt
1271 .lock()
1272 .unwrap_or_else(PoisonError::into_inner) = None;
1273 }
1274}
1275
1276async fn send_connection_at_least<R>(
1277 connection: &Connection,
1278 request: R,
1279 min_version: i16,
1280) -> Result<R::Response, ClientError>
1281where
1282 R: krabka_protocol::ProtocolRequest,
1283{
1284 let (broker_min, broker_max) = connection
1285 .advertised_api_range(R::API_KEY)
1286 .unwrap_or((0, 0));
1287 let client_min = R::MIN_VERSION.max(min_version);
1288 let chosen = R::LATEST_STABLE_VERSION.min(broker_max);
1289 if chosen < client_min || chosen < broker_min {
1290 return Err(ClientError::IncompatibleVersion {
1291 api_key: R::API_KEY,
1292 broker_min,
1293 broker_max,
1294 client_min,
1295 client_max: R::LATEST_STABLE_VERSION,
1296 });
1297 }
1298 connection.send(request).await
1299}
1300
1301impl AdminClient {
1302 fn opts(security: Option<security::ClientSecurity>) -> Result<ConnectionOptions, AdminError> {
1305 Ok(AdminClientConfig {
1306 security,
1307 ..AdminClientConfig::default()
1308 }
1309 .resolve()?
1310 .connection)
1311 }
1312
1313 pub async fn connect_with_config(
1322 bootstrap_addrs: &[String],
1323 config: AdminClientConfig,
1324 ) -> Result<Self, AdminError> {
1325 let resolved = config.resolve()?;
1326 Self::connect_with_metadata_recovery_target(
1327 bootstrap_addrs,
1328 resolved.connection,
1329 resolved.retry,
1330 (
1331 resolved.metadata_recovery_strategy,
1332 resolved.metadata_recovery_rebootstrap_trigger,
1333 ),
1334 BootstrapTarget::Brokers,
1335 resolved.enable_metrics_push,
1336 )
1337 .await
1338 }
1339
1340 pub async fn connect_controller_with_config(
1348 bootstrap_controllers: &[String],
1349 config: AdminClientConfig,
1350 ) -> Result<Self, AdminError> {
1351 let resolved = config.resolve()?;
1352 Self::connect_with_metadata_recovery_target(
1353 bootstrap_controllers,
1354 resolved.connection,
1355 resolved.retry,
1356 (
1357 resolved.metadata_recovery_strategy,
1358 resolved.metadata_recovery_rebootstrap_trigger,
1359 ),
1360 BootstrapTarget::Controllers,
1361 resolved.enable_metrics_push,
1362 )
1363 .await
1364 }
1365
1366 pub async fn connect_secured(
1373 bootstrap_addrs: &[String],
1374 security: Option<security::ClientSecurity>,
1375 ) -> Result<Self, AdminError> {
1376 Self::connect_with_options(bootstrap_addrs, Self::opts(security)?).await
1377 }
1378
1379 pub async fn connect_secured_with_dns_timeout(
1386 bootstrap_addrs: &[String],
1387 security: Option<security::ClientSecurity>,
1388 dns_timeout: krabka_client_core::ClientDnsTimeout,
1389 ) -> Result<Self, AdminError> {
1390 let mut options = Self::opts(security)?;
1391 options.dns_timeout = dns_timeout;
1392 Self::connect_with_options(bootstrap_addrs, options).await
1393 }
1394
1395 pub async fn connect_with_dns_timeout(
1402 bootstrap_addrs: &[String],
1403 dns_timeout: krabka_client_core::ClientDnsTimeout,
1404 ) -> Result<Self, AdminError> {
1405 Self::connect_secured_with_dns_timeout(bootstrap_addrs, None, dns_timeout).await
1406 }
1407
1408 pub async fn connect_with_options(
1414 bootstrap_addrs: &[String],
1415 options: ConnectionOptions,
1416 ) -> Result<Self, AdminError> {
1417 let retry = config::retry_for_connection_options(&options);
1418 Self::connect_with_metadata_recovery_target(
1419 bootstrap_addrs,
1420 options,
1421 retry,
1422 (
1423 MetadataRecoveryStrategy::default(),
1424 krabka_client_core::DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER,
1425 ),
1426 BootstrapTarget::Brokers,
1427 DEFAULT_ENABLE_METRICS_PUSH,
1428 )
1429 .await
1430 }
1431
1432 pub async fn connect_controller(bootstrap_controllers: &[String]) -> Result<Self, AdminError> {
1439 Self::connect_controller_secured(bootstrap_controllers, None).await
1440 }
1441
1442 pub async fn connect_controller_secured(
1448 bootstrap_controllers: &[String],
1449 security: Option<security::ClientSecurity>,
1450 ) -> Result<Self, AdminError> {
1451 Self::connect_controller_with_options(bootstrap_controllers, Self::opts(security)?).await
1452 }
1453
1454 pub async fn connect_controller_with_options(
1460 bootstrap_controllers: &[String],
1461 options: ConnectionOptions,
1462 ) -> Result<Self, AdminError> {
1463 let retry = config::retry_for_connection_options(&options);
1464 Self::connect_with_metadata_recovery_target(
1465 bootstrap_controllers,
1466 options,
1467 retry,
1468 (
1469 MetadataRecoveryStrategy::default(),
1470 krabka_client_core::DEFAULT_METADATA_RECOVERY_REBOOTSTRAP_TRIGGER,
1471 ),
1472 BootstrapTarget::Controllers,
1473 DEFAULT_ENABLE_METRICS_PUSH,
1474 )
1475 .await
1476 }
1477
1478 pub async fn connect_with_metadata_recovery(
1485 bootstrap_addrs: &[String],
1486 options: ConnectionOptions,
1487 strategy: MetadataRecoveryStrategy,
1488 rebootstrap_trigger: Time,
1489 ) -> Result<Self, AdminError> {
1490 let retry = config::retry_for_connection_options(&options);
1491 Self::connect_with_metadata_recovery_target(
1492 bootstrap_addrs,
1493 options,
1494 retry,
1495 (strategy, rebootstrap_trigger),
1496 BootstrapTarget::Brokers,
1497 DEFAULT_ENABLE_METRICS_PUSH,
1498 )
1499 .await
1500 }
1501
1502 async fn connect_with_metadata_recovery_target(
1503 bootstrap_addrs: &[String],
1504 mut options: ConnectionOptions,
1505 retry: retry::RetryPolicy,
1506 (strategy, rebootstrap_trigger): (MetadataRecoveryStrategy, Time),
1507 target: BootstrapTarget,
1508 enable_metrics_push: bool,
1509 ) -> Result<Self, AdminError> {
1510 let trigger = MetadataRecoveryRebootstrapTrigger::new(rebootstrap_trigger)
1511 .map_err(AdminError::Protocol)?;
1512 let metrics = enable_metrics_push.then(ClientMetrics::new);
1516 options.network_metrics = metrics.as_ref().map(|metrics| {
1517 NetworkMetrics::register(metrics, TelemetryClientType::Admin.metric_group())
1518 });
1519 let mut last_error = None;
1520 for host_port in bootstrap_addrs {
1521 match Self::connect_target_one_with_discovery(host_port, options.clone(), target).await
1522 {
1523 Ok(connected) => {
1524 let known_addrs = if connected.discovered_addrs.is_empty() {
1525 bootstrap_addrs.to_vec()
1526 } else {
1527 connected.discovered_addrs
1528 };
1529 let conn = Arc::new(RecoveringConnection::new(
1530 connected.connection,
1531 bootstrap_addrs.to_vec(),
1532 options.clone(),
1533 strategy,
1534 trigger,
1535 target,
1536 known_addrs,
1537 ));
1538 let telemetry = metrics.map(|metrics| {
1539 ClientTelemetry::start(
1540 telemetry::AdminTelemetryConnection(Arc::clone(&conn)),
1541 &ClientTelemetryConfig::admin(),
1542 metrics,
1543 TelemetryTiming {
1544 request_timeout: options.request_timeout.to_std(),
1545 reconnect_backoff: options.reconnect_backoff.to_std(),
1546 },
1547 )
1548 });
1549 return Ok(Self {
1550 conn,
1551 bootstrap_addrs: bootstrap_addrs.to_vec(),
1552 options,
1553 retry,
1554 telemetry,
1555 });
1556 }
1557 Err(e) if e.is_authentication_failure() => return Err(e),
1558 Err(e) => {
1559 tracing::debug!(
1560 target: "krabka_client_admin",
1561 addr = %host_port,
1562 error = %e,
1563 "bootstrap connect failed",
1564 );
1565 last_error = Some(e);
1566 }
1567 }
1568 }
1569 match (target, last_error) {
1570 (BootstrapTarget::Controllers, Some(error)) => Err(error),
1571 (_, last_error) => Err(AdminError::Connect {
1572 tried: bootstrap_addrs.len(),
1573 source: last_error.map(Box::new),
1574 }),
1575 }
1576 }
1577
1578 pub async fn connect(bootstrap_addrs: &[String]) -> Result<Self, AdminError> {
1589 Self::connect_secured(bootstrap_addrs, None).await
1590 }
1591
1592 async fn connect_one(
1593 host_port: &str,
1594 mut opts: ConnectionOptions,
1595 ) -> Result<Connection, AdminError> {
1596 if let Some(security) = opts.security.as_mut() {
1597 **security = security.for_target_host(connection_target_host(host_port));
1598 }
1599 let addr = lookup_first(host_port, opts.dns_timeout, transport::resolve(host_port)).await?;
1600 Connection::connect_with_options(addr, opts)
1601 .await
1602 .map_err(AdminError::from)
1603 }
1604
1605 #[cfg(test)]
1606 async fn connect_target_one(
1607 host_port: &str,
1608 options: ConnectionOptions,
1609 target: BootstrapTarget,
1610 ) -> Result<Connection, AdminError> {
1611 Self::connect_target_one_with_discovery(host_port, options, target)
1612 .await
1613 .map(|connected| connected.connection)
1614 }
1615
1616 async fn connect_target_one_with_discovery(
1617 host_port: &str,
1618 options: ConnectionOptions,
1619 target: BootstrapTarget,
1620 ) -> Result<ConnectedTarget, AdminError> {
1621 match target {
1622 BootstrapTarget::Brokers => Ok(ConnectedTarget {
1623 connection: Self::connect_one(host_port, options).await?,
1624 discovered_addrs: Vec::new(),
1625 }),
1626 BootstrapTarget::Controllers => {
1627 Self::connect_controller_one_with_discovery(host_port, options).await
1628 }
1629 }
1630 }
1631
1632 async fn connect_controller_one_with_discovery(
1633 host_port: &str,
1634 options: ConnectionOptions,
1635 ) -> Result<ConnectedTarget, AdminError> {
1636 use krabka_protocol::owned::describe_cluster_request::DescribeClusterRequest;
1637
1638 const ENDPOINT_TYPE_CONTROLLERS: i8 = 2;
1639 let bootstrap = Self::connect_one(host_port, options.clone()).await?;
1640 let response = send_connection_at_least(
1641 &bootstrap,
1642 DescribeClusterRequest {
1643 endpoint_type: ENDPOINT_TYPE_CONTROLLERS,
1644 ..Default::default()
1645 },
1646 1,
1647 )
1648 .await?;
1649 if response.error_code != 0 {
1650 return Err(AdminError::Broker {
1651 api: "DescribeCluster",
1652 code: response.error_code,
1653 name: kafka_error_name(response.error_code),
1654 message: response.error_message,
1655 });
1656 }
1657 if response.endpoint_type != ENDPOINT_TYPE_CONTROLLERS {
1658 return Err(AdminError::Protocol(format!(
1659 "DescribeCluster returned endpoint_type={}, expected CONTROLLERS",
1660 response.endpoint_type
1661 )));
1662 }
1663 let discovered_addrs = response
1664 .brokers
1665 .iter()
1666 .filter(|controller| !controller.host.is_empty() && controller.port > 0)
1667 .map(|controller| format_host_port(&controller.host, controller.port))
1668 .collect::<Vec<_>>();
1669 let controller = response
1670 .brokers
1671 .into_iter()
1672 .find(|controller| controller.broker_id == response.controller_id)
1673 .ok_or_else(|| {
1674 AdminError::Protocol(format!(
1675 "DescribeCluster omitted controller_id={} from controller endpoints",
1676 response.controller_id
1677 ))
1678 })?;
1679 if controller.host.is_empty() || controller.port <= 0 {
1680 return Err(AdminError::Protocol(format!(
1681 "DescribeCluster returned invalid controller endpoint {}:{}",
1682 controller.host, controller.port
1683 )));
1684 }
1685 let endpoint = format_host_port(&controller.host, controller.port);
1686 Ok(ConnectedTarget {
1687 connection: Self::connect_one(&endpoint, options).await?,
1688 discovered_addrs,
1689 })
1690 }
1691
1692 pub(crate) async fn reconnect(&self, host_port: &str) -> Result<(), AdminError> {
1695 let opts = self.options.clone();
1696 self.conn
1697 .replace(Self::connect_one(host_port, opts).await?)
1698 .await;
1699 Ok(())
1700 }
1701
1702 #[cfg(test)]
1703 pub(crate) async fn reconnect_bootstrap(&self) -> Result<(), AdminError> {
1704 let opts = self.options.clone();
1705 let mut last_error = None;
1706 for host_port in &self.bootstrap_addrs {
1707 match Self::connect_target_one(host_port, opts.clone(), self.conn.target).await {
1708 Ok(conn) => {
1709 self.conn.replace(conn).await;
1710 return Ok(());
1711 }
1712 Err(error) if error.is_authentication_failure() => return Err(error),
1713 Err(error) => {
1714 tracing::debug!(
1715 target: "krabka_client_admin",
1716 addr = %host_port,
1717 error = %error,
1718 "bootstrap reconnect failed",
1719 );
1720 last_error = Some(Box::new(error));
1721 }
1722 }
1723 }
1724 Err(AdminError::Connect {
1725 tried: self.bootstrap_addrs.len(),
1726 source: last_error,
1727 })
1728 }
1729
1730 pub(crate) fn is_retriable_transport_error(error: &ClientError) -> bool {
1731 matches!(
1732 error,
1733 ClientError::Timeout(_) | ClientError::Disconnected | ClientError::Io(_)
1734 )
1735 }
1736}
1737
1738pub(crate) const NOT_CONTROLLER: i16 = 41;
1742
1743pub(crate) fn kafka_error_name(code: i16) -> &'static str {
1750 match code {
1751 -1 => "UNKNOWN_SERVER_ERROR",
1752 0 => "NONE",
1753 1 => "OFFSET_OUT_OF_RANGE",
1754 2 => "CORRUPT_MESSAGE",
1755 3 => "UNKNOWN_TOPIC_OR_PARTITION",
1756 4 => "INVALID_FETCH_SIZE",
1757 5 => "LEADER_NOT_AVAILABLE",
1758 6 => "NOT_LEADER_OR_FOLLOWER",
1759 7 => "REQUEST_TIMED_OUT",
1760 8 => "BROKER_NOT_AVAILABLE",
1761 9 => "REPLICA_NOT_AVAILABLE",
1762 10 => "MESSAGE_TOO_LARGE",
1763 11 => "STALE_CONTROLLER_EPOCH",
1764 12 => "OFFSET_METADATA_TOO_LARGE",
1765 13 => "NETWORK_EXCEPTION",
1766 14 => "COORDINATOR_LOAD_IN_PROGRESS",
1767 15 => "COORDINATOR_NOT_AVAILABLE",
1768 16 => "NOT_COORDINATOR",
1769 17 => "INVALID_TOPIC_EXCEPTION",
1770 18 => "RECORD_LIST_TOO_LARGE",
1771 19 => "NOT_ENOUGH_REPLICAS",
1772 20 => "NOT_ENOUGH_REPLICAS_AFTER_APPEND",
1773 21 => "INVALID_REQUIRED_ACKS",
1774 22 => "ILLEGAL_GENERATION",
1775 23 => "INCONSISTENT_GROUP_PROTOCOL",
1776 24 => "INVALID_GROUP_ID",
1777 25 => "UNKNOWN_MEMBER_ID",
1778 26 => "INVALID_SESSION_TIMEOUT",
1779 27 => "REBALANCE_IN_PROGRESS",
1780 28 => "INVALID_COMMIT_OFFSET_SIZE",
1781 29 => "TOPIC_AUTHORIZATION_FAILED",
1782 30 => "GROUP_AUTHORIZATION_FAILED",
1783 31 => "CLUSTER_AUTHORIZATION_FAILED",
1784 32 => "INVALID_TIMESTAMP",
1785 33 => "UNSUPPORTED_SASL_MECHANISM",
1786 34 => "ILLEGAL_SASL_STATE",
1787 35 => "UNSUPPORTED_VERSION",
1788 36 => "TOPIC_ALREADY_EXISTS",
1789 37 => "INVALID_PARTITIONS",
1790 38 => "INVALID_REPLICATION_FACTOR",
1791 39 => "INVALID_REPLICA_ASSIGNMENT",
1792 40 => "INVALID_CONFIG",
1793 41 => "NOT_CONTROLLER",
1794 42 => "INVALID_REQUEST",
1795 43 => "UNSUPPORTED_FOR_MESSAGE_FORMAT",
1796 44 => "POLICY_VIOLATION",
1797 45 => "OUT_OF_ORDER_SEQUENCE_NUMBER",
1798 46 => "DUPLICATE_SEQUENCE_NUMBER",
1799 47 => "INVALID_PRODUCER_EPOCH",
1800 48 => "INVALID_TXN_STATE",
1801 49 => "INVALID_PRODUCER_ID_MAPPING",
1802 50 => "INVALID_TRANSACTION_TIMEOUT",
1803 51 => "CONCURRENT_TRANSACTIONS",
1804 52 => "TRANSACTION_COORDINATOR_FENCED",
1805 53 => "TRANSACTIONAL_ID_AUTHORIZATION_FAILED",
1806 54 => "SECURITY_DISABLED",
1807 55 => "OPERATION_NOT_ATTEMPTED",
1808 56 => "KAFKA_STORAGE_ERROR",
1809 57 => "LOG_DIR_NOT_FOUND",
1810 58 => "SASL_AUTHENTICATION_FAILED",
1811 59 => "UNKNOWN_PRODUCER_ID",
1812 60 => "REASSIGNMENT_IN_PROGRESS",
1813 61 => "DELEGATION_TOKEN_AUTH_DISABLED",
1814 62 => "DELEGATION_TOKEN_NOT_FOUND",
1815 63 => "DELEGATION_TOKEN_OWNER_MISMATCH",
1816 64 => "DELEGATION_TOKEN_REQUEST_NOT_ALLOWED",
1817 65 => "DELEGATION_TOKEN_AUTHORIZATION_FAILED",
1818 66 => "DELEGATION_TOKEN_EXPIRED",
1819 67 => "INVALID_PRINCIPAL_TYPE",
1820 68 => "NON_EMPTY_GROUP",
1821 69 => "GROUP_ID_NOT_FOUND",
1822 70 => "FETCH_SESSION_ID_NOT_FOUND",
1823 71 => "INVALID_FETCH_SESSION_EPOCH",
1824 72 => "LISTENER_NOT_FOUND",
1825 73 => "TOPIC_DELETION_DISABLED",
1826 74 => "FENCED_LEADER_EPOCH",
1827 75 => "UNKNOWN_LEADER_EPOCH",
1828 76 => "UNSUPPORTED_COMPRESSION_TYPE",
1829 77 => "STALE_BROKER_EPOCH",
1830 78 => "OFFSET_NOT_AVAILABLE",
1831 79 => "MEMBER_ID_REQUIRED",
1832 80 => "PREFERRED_LEADER_NOT_AVAILABLE",
1833 81 => "GROUP_MAX_SIZE_REACHED",
1834 82 => "FENCED_INSTANCE_ID",
1835 83 => "ELIGIBLE_LEADERS_NOT_AVAILABLE",
1836 84 => "ELECTION_NOT_NEEDED",
1837 85 => "NO_REASSIGNMENT_IN_PROGRESS",
1838 86 => "GROUP_SUBSCRIBED_TO_TOPIC",
1839 87 => "INVALID_RECORD",
1840 88 => "UNSTABLE_OFFSET_COMMIT",
1841 89 => "THROTTLING_QUOTA_EXCEEDED",
1842 90 => "PRODUCER_FENCED",
1843 91 => "RESOURCE_NOT_FOUND",
1844 92 => "DUPLICATE_RESOURCE",
1845 93 => "UNACCEPTABLE_CREDENTIAL",
1846 94 => "INCONSISTENT_VOTER_SET",
1847 95 => "INVALID_UPDATE_VERSION",
1848 96 => "FEATURE_UPDATE_FAILED",
1849 97 => "PRINCIPAL_DESERIALIZATION_FAILURE",
1850 98 => "SNAPSHOT_NOT_FOUND",
1851 99 => "POSITION_OUT_OF_RANGE",
1852 100 => "UNKNOWN_TOPIC_ID",
1853 101 => "DUPLICATE_BROKER_REGISTRATION",
1854 102 => "BROKER_ID_NOT_REGISTERED",
1855 103 => "INCONSISTENT_TOPIC_ID",
1856 104 => "INCONSISTENT_CLUSTER_ID",
1857 105 => "TRANSACTIONAL_ID_NOT_FOUND",
1858 106 => "FETCH_SESSION_TOPIC_ID_ERROR",
1859 107 => "INELIGIBLE_REPLICA",
1860 108 => "NEW_LEADER_ELECTED",
1861 109 => "OFFSET_MOVED_TO_TIERED_STORAGE",
1862 110 => "FENCED_MEMBER_EPOCH",
1863 111 => "UNRELEASED_INSTANCE_ID",
1864 112 => "UNSUPPORTED_ASSIGNOR",
1865 113 => "STALE_MEMBER_EPOCH",
1866 114 => "MISMATCHED_ENDPOINT_TYPE",
1867 115 => "UNSUPPORTED_ENDPOINT_TYPE",
1868 116 => "UNKNOWN_CONTROLLER_ID",
1869 117 => "UNKNOWN_SUBSCRIPTION_ID",
1870 118 => "TELEMETRY_TOO_LARGE",
1871 119 => "INVALID_REGISTRATION",
1872 120 => "TRANSACTION_ABORTABLE",
1873 121 => "INVALID_RECORD_STATE",
1874 122 => "SHARE_SESSION_NOT_FOUND",
1875 123 => "INVALID_SHARE_SESSION_EPOCH",
1876 124 => "FENCED_STATE_EPOCH",
1877 125 => "INVALID_VOTER_KEY",
1878 126 => "DUPLICATE_VOTER",
1879 127 => "VOTER_NOT_FOUND",
1880 128 => "INVALID_REGULAR_EXPRESSION",
1881 129 => "REBOOTSTRAP_REQUIRED",
1882 130 => "STREAMS_INVALID_TOPOLOGY",
1883 131 => "STREAMS_INVALID_TOPOLOGY_EPOCH",
1884 132 => "STREAMS_TOPOLOGY_FENCED",
1885 133 => "SHARE_SESSION_LIMIT_REACHED",
1886 134 => "GROUP_DELETION_FAILED",
1887 135 => "STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED",
1888 136 => "CONTROLLER_ID_NOT_REGISTERED",
1889 _ => "UNKNOWN",
1890 }
1891}
1892
1893#[cfg(test)]
1894mod tests {
1895 use std::{
1896 future::pending,
1897 hint,
1898 sync::{
1899 Arc, Mutex,
1900 atomic::{AtomicU16, AtomicUsize, Ordering},
1901 },
1902 time::Duration,
1903 vec,
1904 };
1905
1906 use bytes::{BufMut, BytesMut};
1907 use krabka_protocol::{
1908 Decode, Encode,
1909 owned::{
1910 api_versions_request,
1911 api_versions_response::{ApiVersion, ApiVersionsResponse},
1912 describe_cluster_request::{self, DescribeClusterRequest},
1913 describe_cluster_response::{DescribeClusterBroker, DescribeClusterResponse},
1914 metadata_request,
1915 metadata_response::{
1916 FLEXIBLE_MIN as METADATA_FLEXIBLE_MIN, MetadataResponse, MetadataResponseBroker,
1917 },
1918 sasl_authenticate_request,
1919 sasl_authenticate_response::SaslAuthenticateResponse,
1920 sasl_handshake_request,
1921 sasl_handshake_response::SaslHandshakeResponse,
1922 },
1923 };
1924 use krabka_security::ListenerProtocol;
1925 use krabka_units::secs;
1926 use security::{ClientSecurity, SaslCredentials};
1927 use tokio::{
1928 io::{AsyncReadExt, AsyncWriteExt},
1929 net::{TcpListener, TcpStream},
1930 task::yield_now,
1931 time::sleep,
1932 };
1933 use tokio_util::sync::CancellationToken;
1934
1935 use super::*;
1936
1937 struct ObservedAdminBroker {
1938 addr: SocketAddr,
1939 shutdown: CancellationToken,
1940 connections: Arc<AtomicUsize>,
1941 sasl_handshakes: Arc<AtomicUsize>,
1942 client_ids: Arc<Mutex<Vec<String>>>,
1943 }
1944
1945 impl ObservedAdminBroker {
1946 async fn start(api_versions_delay: Duration) -> Self {
1947 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1948 let addr = listener.local_addr().unwrap();
1949 let shutdown = CancellationToken::new();
1950 let connections = Arc::new(AtomicUsize::new(0));
1951 let sasl_handshakes = Arc::new(AtomicUsize::new(0));
1952 let client_ids = Arc::new(Mutex::new(Vec::new()));
1953 let task_shutdown = shutdown.clone();
1954 let task_connections = Arc::clone(&connections);
1955 let task_sasl_handshakes = Arc::clone(&sasl_handshakes);
1956 let task_client_ids = Arc::clone(&client_ids);
1957
1958 tokio::spawn(async move {
1959 loop {
1960 let Ok((mut stream, _)) = listener.accept().await else {
1961 break;
1962 };
1963 if task_shutdown.is_cancelled() {
1964 break;
1965 }
1966 task_connections.fetch_add(1, Ordering::SeqCst);
1967 let conn_sasl_handshakes = Arc::clone(&task_sasl_handshakes);
1968 let conn_client_ids = Arc::clone(&task_client_ids);
1969 tokio::spawn(async move {
1970 while let Ok(frame_len) = stream.read_u32().await {
1971 let mut request = vec![0_u8; frame_len as usize];
1972 if stream.read_exact(&mut request).await.is_err() || request.len() < 10
1973 {
1974 break;
1975 }
1976 let api_key = i16::from_be_bytes([request[0], request[1]]);
1977 let correlation_id =
1978 i32::from_be_bytes(request[4..8].try_into().unwrap());
1979 let client_id_len =
1980 usize::from(u16::from_be_bytes([request[8], request[9]]));
1981 if request.len() < 10 + client_id_len {
1982 break;
1983 }
1984 conn_client_ids.lock().unwrap().push(
1985 String::from_utf8_lossy(&request[10..10 + client_id_len]).into(),
1986 );
1987
1988 let (body, flexible_header) = match api_key {
1989 sasl_handshake_request::API_KEY => {
1990 conn_sasl_handshakes.fetch_add(1, Ordering::SeqCst);
1991 let mut body = BytesMut::new();
1992 SaslHandshakeResponse {
1993 error_code: 0,
1994 ..Default::default()
1995 }
1996 .encode(&mut body, 1)
1997 .unwrap();
1998 (body, false)
1999 }
2000 sasl_authenticate_request::API_KEY => {
2001 let mut body = BytesMut::new();
2002 SaslAuthenticateResponse {
2003 error_code: 0,
2004 ..Default::default()
2005 }
2006 .encode(&mut body, 2)
2007 .unwrap();
2008 (body, true)
2009 }
2010 api_versions_request::API_KEY => {
2011 sleep(api_versions_delay).await;
2012 let mut body = BytesMut::new();
2013 ApiVersionsResponse {
2014 error_code: 0,
2015 api_keys: vec![
2016 ApiVersion {
2017 api_key: api_versions_request::API_KEY,
2018 min_version: 0,
2019 max_version: 3,
2020 ..Default::default()
2021 },
2022 ApiVersion {
2023 api_key: metadata_request::API_KEY,
2024 min_version: 0,
2025 max_version: 12,
2026 ..Default::default()
2027 },
2028 ApiVersion {
2029 api_key: sasl_handshake_request::API_KEY,
2030 min_version: 0,
2031 max_version: 1,
2032 ..Default::default()
2033 },
2034 ApiVersion {
2035 api_key: sasl_authenticate_request::API_KEY,
2036 min_version: 0,
2037 max_version: 2,
2038 ..Default::default()
2039 },
2040 ],
2041 ..Default::default()
2042 }
2043 .encode(&mut body, 0)
2044 .unwrap();
2045 (body, false)
2046 }
2047 _ => continue,
2048 };
2049 let mut response = BytesMut::new();
2050 response.put_i32(correlation_id);
2051 if flexible_header {
2052 response.put_u8(0);
2053 }
2054 response.extend_from_slice(&body);
2055 if stream
2056 .write_u32(u32::try_from(response.len()).unwrap())
2057 .await
2058 .is_err()
2059 || stream.write_all(&response).await.is_err()
2060 {
2061 break;
2062 }
2063 }
2064 });
2065 }
2066 });
2067
2068 Self {
2069 addr,
2070 shutdown,
2071 connections,
2072 sasl_handshakes,
2073 client_ids,
2074 }
2075 }
2076
2077 fn observed_custom_security_and_id(&self) -> bool {
2078 self.sasl_handshakes.load(Ordering::SeqCst) > 0
2079 && self
2080 .client_ids
2081 .lock()
2082 .unwrap()
2083 .iter()
2084 .any(|client_id| client_id == "custom-admin")
2085 }
2086
2087 fn stop(self) {
2088 self.shutdown.cancel();
2089 }
2090 }
2091
2092 struct ObservedController {
2093 addr: SocketAddr,
2094 connections: Arc<AtomicUsize>,
2095 endpoint_types: Arc<Mutex<Vec<i8>>>,
2096 }
2097
2098 impl ObservedController {
2099 async fn start(describe_cluster_max_version: i16) -> Self {
2100 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2101 let addr = listener.local_addr().unwrap();
2102 let connections = Arc::new(AtomicUsize::new(0));
2103 let endpoint_types = Arc::new(Mutex::new(Vec::new()));
2104 let task_connections = Arc::clone(&connections);
2105 let task_endpoint_types = Arc::clone(&endpoint_types);
2106
2107 tokio::spawn(async move {
2108 while let Ok((mut stream, _)) = listener.accept().await {
2109 task_connections.fetch_add(1, Ordering::SeqCst);
2110 let endpoint_types = Arc::clone(&task_endpoint_types);
2111 tokio::spawn(async move {
2112 while let Ok(frame_len) = stream.read_u32().await {
2113 let mut request = vec![0_u8; frame_len as usize];
2114 if stream.read_exact(&mut request).await.is_err() || request.len() < 10
2115 {
2116 break;
2117 }
2118 let api_key = i16::from_be_bytes([request[0], request[1]]);
2119 let api_version = i16::from_be_bytes([request[2], request[3]]);
2120 let correlation_id =
2121 i32::from_be_bytes(request[4..8].try_into().unwrap());
2122 let client_id_len =
2123 usize::from(u16::from_be_bytes([request[8], request[9]]));
2124 let header_len = 10 + client_id_len;
2125 if request.len() < header_len {
2126 break;
2127 }
2128
2129 let (body, flexible_header) = match api_key {
2130 api_versions_request::API_KEY => {
2131 let mut body = BytesMut::new();
2132 ApiVersionsResponse {
2133 api_keys: vec![
2134 ApiVersion {
2135 api_key: api_versions_request::API_KEY,
2136 min_version: 0,
2137 max_version: 4,
2138 ..Default::default()
2139 },
2140 ApiVersion {
2141 api_key: describe_cluster_request::API_KEY,
2142 min_version: 0,
2143 max_version: describe_cluster_max_version,
2144 ..Default::default()
2145 },
2146 ],
2147 ..Default::default()
2148 }
2149 .encode(&mut body, 0)
2150 .unwrap();
2151 (body, false)
2152 }
2153 describe_cluster_request::API_KEY => {
2154 let mut encoded = &request[header_len + 1..];
2155 let decoded =
2156 DescribeClusterRequest::decode(&mut encoded, api_version)
2157 .unwrap();
2158 endpoint_types.lock().unwrap().push(decoded.endpoint_type);
2159 let mut body = BytesMut::new();
2160 DescribeClusterResponse {
2161 endpoint_type: 2,
2162 cluster_id: "controller-test".into(),
2163 controller_id: 1,
2164 brokers: vec![DescribeClusterBroker {
2165 broker_id: 1,
2166 host: addr.ip().to_string(),
2167 port: i32::from(addr.port()),
2168 ..Default::default()
2169 }],
2170 ..Default::default()
2171 }
2172 .encode(&mut body, api_version)
2173 .unwrap();
2174 (body, true)
2175 }
2176 _ => continue,
2177 };
2178 let mut response = BytesMut::new();
2179 response.put_i32(correlation_id);
2180 if flexible_header {
2181 response.put_u8(0);
2182 }
2183 response.extend_from_slice(&body);
2184 if stream
2185 .write_u32(u32::try_from(response.len()).unwrap())
2186 .await
2187 .is_err()
2188 || stream.write_all(&response).await.is_err()
2189 {
2190 break;
2191 }
2192 }
2193 });
2194 }
2195 });
2196
2197 Self {
2198 addr,
2199 connections,
2200 endpoint_types,
2201 }
2202 }
2203 }
2204
2205 fn custom_admin_options() -> ConnectionOptions {
2206 ConnectionOptions {
2207 dns_timeout: ClientDnsTimeout::default(),
2208 client_id: "custom-admin".into(),
2209 socket_connection_setup_timeout: krabka_units::millis(100),
2210 request_timeout: krabka_units::millis(150),
2211 dispatch_queue_capacity: krabka_client_core::ConnectionDispatchQueueCapacity::new(7)
2212 .unwrap(),
2213 frame_max: krabka_client_core::ClientFrameMax::try_from(krabka_units::kibibytes(32))
2214 .unwrap(),
2215 security: Some(Box::new(ClientSecurity {
2216 protocol: ListenerProtocol::SaslPlaintext,
2217 tls: None,
2218 sasl: Some(SaslCredentials::Plain {
2219 username: "u".into(),
2220 password: "p".into(),
2221 }),
2222 sasl_host: Some("broker.example".into()),
2223 })),
2224 ..ConnectionOptions::default()
2225 }
2226 }
2227
2228 async fn metadata_times_out_with_custom_request_timeout(admin: &mut AdminClient) {
2229 let result = timeout(Duration::from_secs(2), admin.metadata(&[]))
2230 .await
2231 .expect("custom request timeout fires");
2232 assert2::assert!(result.is_err());
2233 }
2234
2235 fn assert_custom_connect_timeout_is_stored(admin: &AdminClient) {
2236 assert2::assert!(
2237 admin.options.socket_connection_setup_timeout == krabka_units::millis(100)
2238 );
2239 assert2::assert!(admin.options.dispatch_queue_capacity.get() == 7);
2240 assert2::assert!(admin.options.frame_max.size() == krabka_units::kibibytes(32));
2241 }
2242
2243 fn metadata_mock_response(version: i16, broker_id: i32, port: u16) -> Vec<u8> {
2244 let response = MetadataResponse {
2245 brokers: vec![MetadataResponseBroker {
2246 node_id: broker_id,
2247 host: "127.0.0.1".into(),
2248 port: i32::from(port),
2249 ..Default::default()
2250 }],
2251 ..Default::default()
2252 };
2253 let mut bytes = BytesMut::new();
2254 if version >= METADATA_FLEXIBLE_MIN {
2255 bytes.put_u8(0);
2256 }
2257 response.encode(&mut bytes, version).unwrap();
2258 bytes.to_vec()
2259 }
2260
2261 fn admin_mock_api_versions() -> Vec<u8> {
2262 let response = ApiVersionsResponse {
2263 api_keys: vec![
2264 ApiVersion {
2265 api_key: api_versions_request::API_KEY,
2266 min_version: 0,
2267 max_version: 3,
2268 ..Default::default()
2269 },
2270 ApiVersion {
2271 api_key: metadata_request::API_KEY,
2272 min_version: 0,
2273 max_version: 13,
2274 ..Default::default()
2275 },
2276 ],
2277 ..Default::default()
2278 };
2279 let mut bytes = BytesMut::new();
2280 response.encode(&mut bytes, 0).unwrap();
2281 bytes.to_vec()
2282 }
2283
2284 fn admin_mock_api_versions_with_describe_cluster(max_version: i16) -> Vec<u8> {
2285 let response = ApiVersionsResponse {
2286 api_keys: vec![
2287 ApiVersion {
2288 api_key: api_versions_request::API_KEY,
2289 min_version: 0,
2290 max_version: 3,
2291 ..Default::default()
2292 },
2293 ApiVersion {
2294 api_key: describe_cluster_request::API_KEY,
2295 min_version: 0,
2296 max_version,
2297 ..Default::default()
2298 },
2299 ],
2300 ..Default::default()
2301 };
2302 let mut bytes = BytesMut::new();
2303 response.encode(&mut bytes, 0).unwrap();
2304 bytes.to_vec()
2305 }
2306
2307 async fn wait_for_admin_mock_closed(addr: SocketAddr) {
2308 timeout(Duration::from_secs(10), async {
2309 while TcpStream::connect(addr).await.is_ok() {
2310 yield_now().await;
2311 }
2312 })
2313 .await
2314 .expect("mock broker listener closes");
2315 }
2316
2317 #[test]
2318 fn error_names_match_the_codes_that_apache_kafka_defines() {
2319 let cases: [(i16, &str); 138] = [
2325 (-1, "UNKNOWN_SERVER_ERROR"),
2326 (0, "NONE"),
2327 (1, "OFFSET_OUT_OF_RANGE"),
2328 (2, "CORRUPT_MESSAGE"),
2329 (3, "UNKNOWN_TOPIC_OR_PARTITION"),
2330 (4, "INVALID_FETCH_SIZE"),
2331 (5, "LEADER_NOT_AVAILABLE"),
2332 (6, "NOT_LEADER_OR_FOLLOWER"),
2333 (7, "REQUEST_TIMED_OUT"),
2334 (8, "BROKER_NOT_AVAILABLE"),
2335 (9, "REPLICA_NOT_AVAILABLE"),
2336 (10, "MESSAGE_TOO_LARGE"),
2337 (11, "STALE_CONTROLLER_EPOCH"),
2338 (12, "OFFSET_METADATA_TOO_LARGE"),
2339 (13, "NETWORK_EXCEPTION"),
2340 (14, "COORDINATOR_LOAD_IN_PROGRESS"),
2341 (15, "COORDINATOR_NOT_AVAILABLE"),
2342 (16, "NOT_COORDINATOR"),
2343 (17, "INVALID_TOPIC_EXCEPTION"),
2344 (18, "RECORD_LIST_TOO_LARGE"),
2345 (19, "NOT_ENOUGH_REPLICAS"),
2346 (20, "NOT_ENOUGH_REPLICAS_AFTER_APPEND"),
2347 (21, "INVALID_REQUIRED_ACKS"),
2348 (22, "ILLEGAL_GENERATION"),
2349 (23, "INCONSISTENT_GROUP_PROTOCOL"),
2350 (24, "INVALID_GROUP_ID"),
2351 (25, "UNKNOWN_MEMBER_ID"),
2352 (26, "INVALID_SESSION_TIMEOUT"),
2353 (27, "REBALANCE_IN_PROGRESS"),
2354 (28, "INVALID_COMMIT_OFFSET_SIZE"),
2355 (29, "TOPIC_AUTHORIZATION_FAILED"),
2356 (30, "GROUP_AUTHORIZATION_FAILED"),
2357 (31, "CLUSTER_AUTHORIZATION_FAILED"),
2358 (32, "INVALID_TIMESTAMP"),
2359 (33, "UNSUPPORTED_SASL_MECHANISM"),
2360 (34, "ILLEGAL_SASL_STATE"),
2361 (35, "UNSUPPORTED_VERSION"),
2362 (36, "TOPIC_ALREADY_EXISTS"),
2363 (37, "INVALID_PARTITIONS"),
2364 (38, "INVALID_REPLICATION_FACTOR"),
2365 (39, "INVALID_REPLICA_ASSIGNMENT"),
2366 (40, "INVALID_CONFIG"),
2367 (41, "NOT_CONTROLLER"),
2368 (42, "INVALID_REQUEST"),
2369 (43, "UNSUPPORTED_FOR_MESSAGE_FORMAT"),
2370 (44, "POLICY_VIOLATION"),
2371 (45, "OUT_OF_ORDER_SEQUENCE_NUMBER"),
2372 (46, "DUPLICATE_SEQUENCE_NUMBER"),
2373 (47, "INVALID_PRODUCER_EPOCH"),
2374 (48, "INVALID_TXN_STATE"),
2375 (49, "INVALID_PRODUCER_ID_MAPPING"),
2376 (50, "INVALID_TRANSACTION_TIMEOUT"),
2377 (51, "CONCURRENT_TRANSACTIONS"),
2378 (52, "TRANSACTION_COORDINATOR_FENCED"),
2379 (53, "TRANSACTIONAL_ID_AUTHORIZATION_FAILED"),
2380 (54, "SECURITY_DISABLED"),
2381 (55, "OPERATION_NOT_ATTEMPTED"),
2382 (56, "KAFKA_STORAGE_ERROR"),
2383 (57, "LOG_DIR_NOT_FOUND"),
2384 (58, "SASL_AUTHENTICATION_FAILED"),
2385 (59, "UNKNOWN_PRODUCER_ID"),
2386 (60, "REASSIGNMENT_IN_PROGRESS"),
2387 (61, "DELEGATION_TOKEN_AUTH_DISABLED"),
2388 (62, "DELEGATION_TOKEN_NOT_FOUND"),
2389 (63, "DELEGATION_TOKEN_OWNER_MISMATCH"),
2390 (64, "DELEGATION_TOKEN_REQUEST_NOT_ALLOWED"),
2391 (65, "DELEGATION_TOKEN_AUTHORIZATION_FAILED"),
2392 (66, "DELEGATION_TOKEN_EXPIRED"),
2393 (67, "INVALID_PRINCIPAL_TYPE"),
2394 (68, "NON_EMPTY_GROUP"),
2395 (69, "GROUP_ID_NOT_FOUND"),
2396 (70, "FETCH_SESSION_ID_NOT_FOUND"),
2397 (71, "INVALID_FETCH_SESSION_EPOCH"),
2398 (72, "LISTENER_NOT_FOUND"),
2399 (73, "TOPIC_DELETION_DISABLED"),
2400 (74, "FENCED_LEADER_EPOCH"),
2401 (75, "UNKNOWN_LEADER_EPOCH"),
2402 (76, "UNSUPPORTED_COMPRESSION_TYPE"),
2403 (77, "STALE_BROKER_EPOCH"),
2404 (78, "OFFSET_NOT_AVAILABLE"),
2405 (79, "MEMBER_ID_REQUIRED"),
2406 (80, "PREFERRED_LEADER_NOT_AVAILABLE"),
2407 (81, "GROUP_MAX_SIZE_REACHED"),
2408 (82, "FENCED_INSTANCE_ID"),
2409 (83, "ELIGIBLE_LEADERS_NOT_AVAILABLE"),
2410 (84, "ELECTION_NOT_NEEDED"),
2411 (85, "NO_REASSIGNMENT_IN_PROGRESS"),
2412 (86, "GROUP_SUBSCRIBED_TO_TOPIC"),
2413 (87, "INVALID_RECORD"),
2414 (88, "UNSTABLE_OFFSET_COMMIT"),
2415 (89, "THROTTLING_QUOTA_EXCEEDED"),
2416 (90, "PRODUCER_FENCED"),
2417 (91, "RESOURCE_NOT_FOUND"),
2418 (92, "DUPLICATE_RESOURCE"),
2419 (93, "UNACCEPTABLE_CREDENTIAL"),
2420 (94, "INCONSISTENT_VOTER_SET"),
2421 (95, "INVALID_UPDATE_VERSION"),
2422 (96, "FEATURE_UPDATE_FAILED"),
2423 (97, "PRINCIPAL_DESERIALIZATION_FAILURE"),
2424 (98, "SNAPSHOT_NOT_FOUND"),
2425 (99, "POSITION_OUT_OF_RANGE"),
2426 (100, "UNKNOWN_TOPIC_ID"),
2427 (101, "DUPLICATE_BROKER_REGISTRATION"),
2428 (102, "BROKER_ID_NOT_REGISTERED"),
2429 (103, "INCONSISTENT_TOPIC_ID"),
2430 (104, "INCONSISTENT_CLUSTER_ID"),
2431 (105, "TRANSACTIONAL_ID_NOT_FOUND"),
2432 (106, "FETCH_SESSION_TOPIC_ID_ERROR"),
2433 (107, "INELIGIBLE_REPLICA"),
2434 (108, "NEW_LEADER_ELECTED"),
2435 (109, "OFFSET_MOVED_TO_TIERED_STORAGE"),
2436 (110, "FENCED_MEMBER_EPOCH"),
2437 (111, "UNRELEASED_INSTANCE_ID"),
2438 (112, "UNSUPPORTED_ASSIGNOR"),
2439 (113, "STALE_MEMBER_EPOCH"),
2440 (114, "MISMATCHED_ENDPOINT_TYPE"),
2441 (115, "UNSUPPORTED_ENDPOINT_TYPE"),
2442 (116, "UNKNOWN_CONTROLLER_ID"),
2443 (117, "UNKNOWN_SUBSCRIPTION_ID"),
2444 (118, "TELEMETRY_TOO_LARGE"),
2445 (119, "INVALID_REGISTRATION"),
2446 (120, "TRANSACTION_ABORTABLE"),
2447 (121, "INVALID_RECORD_STATE"),
2448 (122, "SHARE_SESSION_NOT_FOUND"),
2449 (123, "INVALID_SHARE_SESSION_EPOCH"),
2450 (124, "FENCED_STATE_EPOCH"),
2451 (125, "INVALID_VOTER_KEY"),
2452 (126, "DUPLICATE_VOTER"),
2453 (127, "VOTER_NOT_FOUND"),
2454 (128, "INVALID_REGULAR_EXPRESSION"),
2455 (129, "REBOOTSTRAP_REQUIRED"),
2456 (130, "STREAMS_INVALID_TOPOLOGY"),
2457 (131, "STREAMS_INVALID_TOPOLOGY_EPOCH"),
2458 (132, "STREAMS_TOPOLOGY_FENCED"),
2459 (133, "SHARE_SESSION_LIMIT_REACHED"),
2460 (134, "GROUP_DELETION_FAILED"),
2461 (135, "STREAMS_TOPOLOGY_DESCRIPTION_UPDATE_FAILED"),
2462 (136, "CONTROLLER_ID_NOT_REGISTERED"),
2463 ];
2464
2465 let names = cases
2466 .iter()
2467 .map(|(code, _)| (*code, kafka_error_name(*code)))
2468 .collect::<Vec<_>>();
2469 assert2::assert!(names == cases.to_vec());
2470 }
2473
2474 #[yare::parameterized(
2475 minimum = { i16::MIN },
2476 negative = { -2 },
2477 beyond_table = { 137 },
2478 maximum = { i16::MAX },
2479 )]
2480 fn kafka_error_name_unknown_codes_are_unknown(code: i16) {
2481 assert2::assert!(kafka_error_name(code) == "UNKNOWN", "code {code}");
2482 }
2483
2484 #[yare::parameterized(
2485 success = { 0, "NONE" },
2486 topic_exists = { 36, "TOPIC_ALREADY_EXISTS" },
2487 not_controller = { 41, "NOT_CONTROLLER" },
2488 )]
2489 fn kafka_error_name_known_codes(code: i16, want: &str) {
2490 assert2::assert!(kafka_error_name(code) == want);
2491 }
2492
2493 #[yare::parameterized(
2494 cluster_authorization = { 31, "CLUSTER_AUTHORIZATION_FAILED" },
2495 unsupported_sasl = { 33, "UNSUPPORTED_SASL_MECHANISM" },
2496 unsupported_version = { 35, "UNSUPPORTED_VERSION" },
2497 delegation_token_expired = { 66, "DELEGATION_TOKEN_EXPIRED" },
2498 eligible_leaders_unavailable = { 83, "ELIGIBLE_LEADERS_NOT_AVAILABLE" },
2499 election_unnecessary = { 84, "ELECTION_NOT_NEEDED" },
2500 resource_not_found = { 91, "RESOURCE_NOT_FOUND" },
2501 duplicate_resource = { 92, "DUPLICATE_RESOURCE" },
2502 unacceptable_credential = { 93, "UNACCEPTABLE_CREDENTIAL" },
2503 )]
2504 fn users_kafka_error_name_includes_scram_describe_codes(code: i16, want: &str) {
2505 assert2::assert!(kafka_error_name(code) == want);
2506 }
2507
2508 #[test]
2509 fn users_kafka_error_name_includes_ineligible_replica() {
2510 assert2::assert!(kafka_error_name(107) == "INELIGIBLE_REPLICA");
2511 }
2512
2513 #[test]
2514 fn kafka_error_name_unknown_returns_unknown() {
2515 assert2::assert!(kafka_error_name(9999) == "UNKNOWN");
2516 }
2517
2518 #[test]
2519 fn kafka_error_if_zero_code_is_none() {
2520 assert2::assert!(kafka_error_if(0, None).is_none());
2521 }
2522
2523 #[test]
2524 fn kafka_error_if_nonzero_carries_name() {
2525 let e = kafka_error_if(36, Some("dup".into())).unwrap();
2526 assert2::assert!(
2527 e == KafkaError {
2528 code: 36,
2529 name: "TOPIC_ALREADY_EXISTS",
2530 message: Some("dup".to_owned()),
2531 }
2532 );
2533 }
2534
2535 #[tokio::test]
2536 async fn connect_secured_threads_security_and_fails_to_closed_port() {
2537 use krabka_security::ListenerProtocol;
2538 use security::{ClientSecurity, SaslCredentials};
2539
2540 let security = ClientSecurity {
2541 protocol: ListenerProtocol::SaslPlaintext,
2542 tls: None,
2543 sasl: Some(SaslCredentials::Plain {
2544 username: "u".into(),
2545 password: "p".into(),
2546 }),
2547 sasl_host: None,
2548 };
2549 let res = AdminClient::connect_secured(&["127.0.0.1:1".to_owned()], Some(security)).await;
2552 assert2::assert!(res.is_err());
2553 }
2554
2555 #[tokio::test(start_paused = true)]
2556 async fn dns_lookup_stops_at_connection_option_deadline() {
2557 let timeout = ClientDnsTimeout::new(Time::from_millis(37)).expect("positive timeout");
2558 let started = Instant::now();
2559 let pending = pending::<io::Result<vec::IntoIter<SocketAddr>>>();
2560
2561 let result = lookup_first("broker.invalid:9092", timeout, pending).await;
2562
2563 assert2::assert!(result.is_err());
2564 assert2::assert!(started.elapsed() == Duration::from_millis(37));
2565 }
2566
2567 #[tokio::test]
2568 async fn custom_options_are_observable_on_initial_dial() {
2569 let live = ObservedAdminBroker::start(Duration::ZERO).await;
2570 let mut admin =
2571 AdminClient::connect_with_options(&[live.addr.to_string()], custom_admin_options())
2572 .await
2573 .unwrap();
2574
2575 assert2::assert!(live.observed_custom_security_and_id());
2576 assert_custom_connect_timeout_is_stored(&admin);
2577 metadata_times_out_with_custom_request_timeout(&mut admin).await;
2578
2579 let slow = ObservedAdminBroker::start(Duration::from_millis(300)).await;
2580 let result = timeout(
2581 Duration::from_secs(1),
2582 AdminClient::connect_with_options(&[slow.addr.to_string()], custom_admin_options()),
2583 )
2584 .await
2585 .expect("ApiVersions obeys the stored connect timeout");
2586 assert2::assert!(result.is_err());
2587
2588 live.stop();
2589 slow.stop();
2590 }
2591
2592 #[tokio::test]
2596 async fn connect_with_config_carries_the_settings_and_refuses_an_invalid_one() {
2597 let live = ObservedAdminBroker::start(Duration::ZERO).await;
2598 let config = || AdminClientConfig {
2599 client_id: Some("tool-a".to_owned()),
2600 request_timeout: secs(45),
2601 default_api_timeout: Some(secs(90)),
2602 retries: 4,
2603 retry_backoff: Time::from_millis(30),
2604 retry_backoff_max: Time::from_millis(300),
2605 ..AdminClientConfig::default()
2606 };
2607
2608 let admin = AdminClient::connect_with_config(&[live.addr.to_string()], config())
2609 .await
2610 .expect("admin connects");
2611
2612 assert2::assert!(admin.options.client_id == "tool-a");
2613 assert2::assert!(admin.options.request_timeout == secs(45));
2614 assert2::assert!(
2615 admin.retry
2616 == retry::RetryPolicy {
2617 timeout: Duration::from_secs(90),
2618 initial_backoff: Duration::from_millis(30),
2619 max_backoff: Duration::from_millis(300),
2620 jitter: admin.retry.jitter,
2621 max_retries: 4,
2622 }
2623 );
2624 live.stop();
2625
2626 let controller = ObservedController::start(2).await;
2627 let on_controller =
2628 AdminClient::connect_controller_with_config(&[controller.addr.to_string()], config())
2629 .await
2630 .expect("controller admin connects");
2631
2632 assert2::assert!(on_controller.conn.uses_controller_bootstrap());
2633 assert2::assert!(on_controller.options.client_id == "tool-a");
2634 assert2::assert!(on_controller.retry.max_retries == 4);
2635
2636 let refused = AdminClient::connect_with_config(
2637 &["127.0.0.1:1".to_owned()],
2638 AdminClientConfig {
2639 request_timeout: secs(90),
2640 default_api_timeout: Some(secs(60)),
2641 ..AdminClientConfig::default()
2642 },
2643 )
2644 .await;
2645
2646 assert2::assert!(matches!(refused, Err(AdminError::InvalidConfig(_))));
2647 }
2648
2649 #[tokio::test]
2650 async fn connect_with_dns_timeout_preserves_admin_defaults() {
2651 let live = ObservedAdminBroker::start(Duration::ZERO).await;
2652 let timeout = ClientDnsTimeout::new(Time::from_millis(37)).expect("positive timeout");
2653 let admin = AdminClient::connect_with_dns_timeout(&[live.addr.to_string()], timeout)
2654 .await
2655 .expect("admin connects");
2656
2657 assert2::assert!(admin.options.dns_timeout == timeout);
2658 assert2::assert!(admin.options.client_id.starts_with("adminclient-"));
2659 assert2::assert!(admin.options.socket_connection_setup_timeout == secs(10));
2660 assert2::assert!(admin.options.request_timeout == secs(30));
2661 live.stop();
2662 }
2663
2664 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2665 async fn admin_metadata_uses_a_learned_broker_after_the_only_seed_retires() {
2666 let healthy_port = Arc::new(AtomicU16::new(0));
2667 let healthy_calls = Arc::new(AtomicUsize::new(0));
2668 let h_port = Arc::clone(&healthy_port);
2669 let h_calls = Arc::clone(&healthy_calls);
2670 let healthy = MockBroker::start(move |api_key, version, _correlation_id, _body| {
2671 if api_key == api_versions_request::API_KEY {
2672 return Some(admin_mock_api_versions());
2673 }
2674 if api_key == metadata_request::API_KEY {
2675 h_calls.fetch_add(1, Ordering::SeqCst);
2676 return Some(metadata_mock_response(
2677 version,
2678 7,
2679 h_port.load(Ordering::SeqCst),
2680 ));
2681 }
2682 None
2683 })
2684 .await
2685 .unwrap();
2686 healthy_port.store(healthy.addr.port(), Ordering::SeqCst);
2687
2688 let advertised_port = Arc::clone(&healthy_port);
2689 let seed = MockBroker::start(move |api_key, version, _correlation_id, _body| {
2690 if api_key == api_versions_request::API_KEY {
2691 return Some(admin_mock_api_versions());
2692 }
2693 (api_key == metadata_request::API_KEY)
2694 .then(|| metadata_mock_response(version, 7, advertised_port.load(Ordering::SeqCst)))
2695 })
2696 .await
2697 .unwrap();
2698 let admin = AdminClient::connect(&[seed.addr.to_string()])
2699 .await
2700 .expect("admin connects to its seed");
2701 let first = admin
2702 .conn
2703 .send(metadata_request::MetadataRequest::default())
2704 .await
2705 .expect("seed provides last-known broker metadata");
2706 assert2::assert!(first.brokers[0].node_id == 7);
2707
2708 let seed_addr = seed.addr;
2709 seed.stop();
2710 wait_for_admin_mock_closed(seed_addr).await;
2711
2712 let refreshed = admin
2713 .conn
2714 .send(metadata_request::MetadataRequest::default())
2715 .await
2716 .expect("admin refreshes through the learned broker");
2717 assert2::assert!(refreshed.brokers[0].node_id == 7);
2718 assert2::assert!(
2719 healthy_calls.load(Ordering::SeqCst) >= 1,
2720 "recovery must issue metadata through the learned broker"
2721 );
2722 }
2723
2724 #[tokio::test]
2725 async fn recovering_connection_enforces_required_api_version() {
2726 let broker = MockBroker::start(|api_key, _version, _correlation_id, _body| {
2727 (api_key == api_versions_request::API_KEY)
2728 .then(|| admin_mock_api_versions_with_describe_cluster(1))
2729 })
2730 .await
2731 .unwrap();
2732 let admin = AdminClient::connect(&[broker.addr.to_string()])
2733 .await
2734 .expect("admin connects");
2735
2736 let result = admin
2737 .conn
2738 .send_at_least(DescribeClusterRequest::default(), 2)
2739 .await;
2740
2741 assert2::assert!(matches!(
2742 result,
2743 Err(AdminError::Transport(ClientError::IncompatibleVersion {
2744 api_key: describe_cluster_request::API_KEY,
2745 broker_max: 1,
2746 client_min: 2,
2747 ..
2748 }))
2749 ));
2750 broker.stop();
2751 }
2752
2753 #[tokio::test]
2754 async fn controller_bootstrap_discovers_and_connects_active_controller() {
2755 let controller = ObservedController::start(2).await;
2756
2757 let admin = AdminClient::connect_controller(&[controller.addr.to_string()])
2758 .await
2759 .unwrap_or_else(|error| {
2760 panic!(
2761 "controller bootstrap: {error}; connections={}; endpoint_types={:?}",
2762 controller.connections.load(Ordering::SeqCst),
2763 controller.endpoint_types.lock().unwrap(),
2764 )
2765 });
2766
2767 assert2::assert!(admin.conn.uses_controller_bootstrap());
2768 assert2::assert!(controller.connections.load(Ordering::SeqCst) == 2);
2769 assert2::assert!(*controller.endpoint_types.lock().unwrap() == vec![2]);
2770 }
2771
2772 #[tokio::test]
2773 async fn controller_bootstrap_requires_endpoint_type_version() {
2774 let controller = ObservedController::start(0).await;
2775
2776 let result = AdminClient::connect_controller(&[controller.addr.to_string()]).await;
2777
2778 assert2::assert!(matches!(
2779 result,
2780 Err(AdminError::Transport(ClientError::IncompatibleVersion {
2781 api_key: describe_cluster_request::API_KEY,
2782 broker_max: 0,
2783 client_min: 1,
2784 ..
2785 }))
2786 ));
2787 assert2::assert!(controller.endpoint_types.lock().unwrap().is_empty());
2788 }
2789
2790 #[tokio::test]
2791 async fn secured_dns_timeout_preserves_security_and_admin_defaults() {
2792 let live = ObservedAdminBroker::start(Duration::ZERO).await;
2793 let timeout = ClientDnsTimeout::new(Time::from_millis(37)).expect("positive timeout");
2794 let security = ClientSecurity {
2795 protocol: ListenerProtocol::SaslPlaintext,
2796 tls: None,
2797 sasl: Some(SaslCredentials::Plain {
2798 username: "u".into(),
2799 password: "p".into(),
2800 }),
2801 sasl_host: Some("broker.example".into()),
2802 };
2803 let admin = AdminClient::connect_secured_with_dns_timeout(
2804 &[live.addr.to_string()],
2805 Some(security),
2806 timeout,
2807 )
2808 .await
2809 .expect("secured admin connects");
2810
2811 assert2::assert!(admin.options.dns_timeout == timeout);
2812 assert2::assert!(admin.options.security.is_some());
2813 assert2::assert!(admin.options.client_id.starts_with("adminclient-"));
2814 assert2::assert!(admin.options.socket_connection_setup_timeout == secs(10));
2815 assert2::assert!(admin.options.request_timeout == secs(30));
2816 live.stop();
2817 }
2818
2819 #[tokio::test]
2820 async fn custom_options_are_observable_on_controller_reconnect() {
2821 let bootstrap = ObservedAdminBroker::start(Duration::ZERO).await;
2822 let controller = ObservedAdminBroker::start(Duration::ZERO).await;
2823 let slow = ObservedAdminBroker::start(Duration::from_millis(300)).await;
2824 let mut admin = AdminClient::connect_with_options(
2825 &[bootstrap.addr.to_string()],
2826 custom_admin_options(),
2827 )
2828 .await
2829 .unwrap();
2830
2831 admin.reconnect(&controller.addr.to_string()).await.unwrap();
2832 assert2::assert!(controller.observed_custom_security_and_id());
2833 assert_custom_connect_timeout_is_stored(&admin);
2834 metadata_times_out_with_custom_request_timeout(&mut admin).await;
2835 let result = timeout(
2836 Duration::from_secs(1),
2837 admin.reconnect(&slow.addr.to_string()),
2838 )
2839 .await
2840 .expect("reconnected ApiVersions obeys the stored request timeout");
2841 assert2::assert!(result.is_err());
2842
2843 bootstrap.stop();
2844 controller.stop();
2845 slow.stop();
2846 }
2847
2848 #[tokio::test]
2849 async fn custom_options_are_observable_on_bootstrap_reconnect() {
2850 let slow = ObservedAdminBroker::start(Duration::from_millis(300)).await;
2851 let live = ObservedAdminBroker::start(Duration::ZERO).await;
2852 let bootstrap = [slow.addr.to_string(), live.addr.to_string()];
2853 let mut admin = timeout(
2854 Duration::from_secs(1),
2855 AdminClient::connect_with_options(&bootstrap, custom_admin_options()),
2856 )
2857 .await
2858 .expect("custom initial timeout advances to next bootstrap")
2859 .unwrap();
2860
2861 timeout(Duration::from_secs(1), admin.reconnect_bootstrap())
2862 .await
2863 .expect("custom reconnect timeout advances to next bootstrap")
2864 .unwrap();
2865 assert2::assert!(live.connections.load(Ordering::SeqCst) == 2);
2866 assert2::assert!(live.observed_custom_security_and_id());
2867 assert_custom_connect_timeout_is_stored(&admin);
2868 metadata_times_out_with_custom_request_timeout(&mut admin).await;
2869
2870 slow.stop();
2871 live.stop();
2872 }
2873
2874 #[test]
2875 fn existing_connectors_keep_admin_defaults() {
2876 let options = AdminClient::opts(None).unwrap();
2877
2878 assert2::assert!(options.client_id.starts_with("adminclient-"));
2879 assert2::assert!(options.socket_connection_setup_timeout == secs(10));
2880 assert2::assert!(options.connections_max_idle == krabka_units::minutes(5));
2881 assert2::assert!(options.request_timeout == secs(30));
2882 assert2::assert!(options.security.is_none());
2883 }
2884
2885 #[test]
2886 fn evacuation_methods_are_available_on_trait_objects() {
2887 fn compile_calls(client: &mut dyn AdminClientLike) {
2888 drop(client.describe_partition_assignments(&["orders"]));
2889 drop(client.list_partition_reassignments(&BTreeMap::new(), secs(30)));
2890 drop(client.alter_partition_assignments(&BTreeMap::new(), secs(30)));
2891 drop(client.unregister_broker(7));
2892 }
2893
2894 hint::black_box(compile_calls as fn(&mut dyn AdminClientLike));
2895 }
2896}