Skip to main content

partitionline/
admin.rs

1//! Kafka admin client: topics, configs, ACLs, groups, and cluster operations.
2//!
3//! [`Admin::connect`] / [`Admin::new`] negotiate ApiVersions. Methods that
4//! must land on the controller retry on `NOT_CONTROLLER`. Group and
5//! transaction methods retry on coordinator errors.
6
7use std::cmp::Ordering;
8use std::collections::{HashMap, VecDeque};
9use std::fmt;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use bytes::{Bytes, BytesMut};
14
15use crate::cluster::Cluster;
16use crate::error::{self, Error, Result};
17use crate::net::{BrokerConn, TlsConfig};
18use crate::protocol::acl::{
19    decode_create_acls_response, decode_delete_acls_filter_results, decode_describe_acls_response,
20    encode_create_acls_request, encode_delete_acls_request, encode_describe_acls_request,
21};
22use crate::protocol::admin::{
23    decode_allocate_producer_ids_response, decode_alter_client_quotas_response,
24    decode_alter_configs_resource_results, decode_alter_partition_reassignments_response,
25    decode_alter_replica_log_dirs_response, decode_alter_share_group_offsets_response,
26    decode_alter_user_scram_credentials_response, decode_assign_replicas_to_dirs_response,
27    decode_consumer_group_describe_response, decode_create_delegation_token_response,
28    decode_create_partitions_response, decode_create_topics_response,
29    decode_delete_groups_response, decode_delete_records_topics_response,
30    decode_delete_share_group_offsets_response, decode_delete_topics_response,
31    decode_describe_client_quotas_response, decode_describe_cluster_response,
32    decode_describe_configs_response, decode_describe_delegation_token_response,
33    decode_describe_groups_response, decode_describe_log_dirs_response,
34    decode_describe_producers_response, decode_describe_share_group_offsets_response,
35    decode_describe_topic_partitions_response, decode_describe_transactions_response,
36    decode_describe_user_scram_credentials_response, decode_expire_delegation_token_response,
37    decode_get_telemetry_subscriptions_response, decode_incremental_alter_configs_resource_results,
38    decode_list_config_resources_response, decode_list_groups_response,
39    decode_list_partition_reassignments_response, decode_list_transactions_response,
40    decode_push_telemetry_response, decode_renew_delegation_token_response,
41    decode_share_group_describe_response, decode_unregister_broker_response,
42    decode_update_features_response, encode_allocate_producer_ids_request,
43    encode_alter_client_quotas_request, encode_alter_configs_resources_request,
44    encode_alter_partition_reassignments_request, encode_alter_replica_log_dirs_request,
45    encode_alter_share_group_offsets_request, encode_alter_user_scram_credentials_request,
46    encode_assign_replicas_to_dirs_request, encode_consumer_group_describe_request,
47    encode_create_delegation_token_request, encode_create_partitions_request,
48    encode_create_topics_request, encode_delete_groups_request,
49    encode_delete_records_topics_request, encode_delete_share_group_offsets_request,
50    encode_delete_topics_states_request, encode_describe_client_quotas_request,
51    encode_describe_cluster_request, encode_describe_configs_request,
52    encode_describe_delegation_token_request, encode_describe_groups_request,
53    encode_describe_log_dirs_request, encode_describe_producers_topics_request,
54    encode_describe_share_group_offsets_request, encode_describe_topic_partitions_request,
55    encode_describe_transactions_request, encode_describe_user_scram_credentials_request,
56    encode_expire_delegation_token_request, encode_get_telemetry_subscriptions_request,
57    encode_incremental_alter_configs_resources_request, encode_list_config_resources_request,
58    encode_list_groups_request, encode_list_partition_reassignments_request,
59    encode_list_transactions_request, encode_push_telemetry_request,
60    encode_renew_delegation_token_request, encode_share_group_describe_request,
61    encode_unregister_broker_request, encode_update_features_request, AlterConfigsResource,
62    AlterableResource, CreatableTopic, CreatePartitionsTopic, CreateTopicsRequest,
63    DeleteRecordsPartition, DeleteRecordsTopic, DeleteTopicState, DescribeConfigsResource,
64    DescribeConfigsResult, DescribeProducersTopicRequest, FeatureUpdateKey, ListReassignmentTopic,
65    ReassignablePartition, ReassignableTopic, ReplicaAssignment, ScramCredentialDeletion,
66    ScramCredentialUpsertion, TopicConfig, TopicResult, RESOURCE_BROKER, RESOURCE_BROKER_LOGGER,
67    RESOURCE_CLIENT_METRICS, RESOURCE_GROUP, RESOURCE_TOPIC,
68};
69use crate::protocol::api::{
70    decode_api_versions_response, decode_metadata_response, encode_api_versions_request,
71    encode_metadata_request_topics, ApiVersion, MetadataRequestTopic, MetadataResponse,
72};
73use crate::protocol::api_keys::{
74    pick_version, ALLOCATE_PRODUCER_IDS, ALTER_CLIENT_QUOTAS, ALTER_CONFIGS,
75    ALTER_PARTITION_REASSIGNMENTS, ALTER_REPLICA_LOG_DIRS, ALTER_SHARE_GROUP_OFFSETS,
76    ALTER_USER_SCRAM_CREDENTIALS, API_VERSIONS, ASSIGN_REPLICAS_TO_DIRS, CONSUMER_GROUP_DESCRIBE,
77    CREATE_ACLS, CREATE_DELEGATION_TOKEN, CREATE_PARTITIONS, CREATE_TOPICS, DELETE_ACLS,
78    DELETE_GROUPS, DELETE_RECORDS, DELETE_SHARE_GROUP_OFFSETS, DELETE_TOPICS, DESCRIBE_ACLS,
79    DESCRIBE_CLIENT_QUOTAS, DESCRIBE_CLUSTER, DESCRIBE_CONFIGS, DESCRIBE_DELEGATION_TOKEN,
80    DESCRIBE_GROUPS, DESCRIBE_LOG_DIRS, DESCRIBE_PRODUCERS, DESCRIBE_SHARE_GROUP_OFFSETS,
81    DESCRIBE_TOPIC_PARTITIONS, DESCRIBE_TRANSACTIONS, DESCRIBE_USER_SCRAM_CREDENTIALS,
82    EXPIRE_DELEGATION_TOKEN, FIND_COORDINATOR, GET_TELEMETRY_SUBSCRIPTIONS,
83    INCREMENTAL_ALTER_CONFIGS, INIT_PRODUCER_ID, LEAVE_GROUP, LIST_CONFIG_RESOURCES, LIST_GROUPS,
84    LIST_OFFSETS, LIST_PARTITION_REASSIGNMENTS, LIST_TRANSACTIONS, METADATA, OFFSET_COMMIT,
85    OFFSET_DELETE, OFFSET_FETCH, PUSH_TELEMETRY, RENEW_DELEGATION_TOKEN, SHARE_GROUP_DESCRIBE,
86    UNREGISTER_BROKER, UPDATE_FEATURES, WRITE_TXN_MARKERS,
87};
88use crate::protocol::group::{
89    decode_find_coordinator_response, decode_find_coordinator_response_coordinators,
90    decode_leave_group_response_version, decode_offset_commit_response,
91    decode_offset_delete_response, decode_offset_fetch_groups_response,
92    decode_offset_fetch_response, encode_find_coordinator_request_keys,
93    encode_find_coordinator_request_typed, encode_leave_group_request_members,
94    encode_offset_commit_request, encode_offset_delete_request, encode_offset_fetch_groups_request,
95    encode_offset_fetch_request, LeaveGroupMember, OffsetDeleteTopic, OffsetFetchGroup,
96    COORDINATOR_GROUP, COORDINATOR_TRANSACTION, DEFAULT_RETENTION_TIME,
97};
98use crate::protocol::idem::{decode_init_producer_id_response, encode_init_producer_id_request};
99use crate::protocol::offsets::{
100    decode_list_offsets_topics_response, encode_list_offsets_topics_request,
101    ListOffsetsPartitionRequest, ListOffsetsResponsePartition, ListOffsetsTopicRequest,
102};
103use crate::protocol::sasl;
104use crate::protocol::txn::{
105    decode_write_txn_markers_response, encode_write_txn_markers_request, TransactionResult,
106    WritableTxnMarker, WritableTxnMarkerTopic,
107};
108
109pub use crate::protocol::acl::{
110    AccessControlEntry, AccessControlEntryFilter, AclBinding, AclBindingFilter, AclCreationResult,
111    AclOperation, AclPatternType, AclPermission, AclResourceType, DeleteAclsMatchingAcl,
112    DeletedAclsFilterResult, ResourcePattern, ResourcePatternFilter,
113};
114pub use crate::protocol::admin::{
115    ActiveProducer, AlterConfig, AlterConfigOp, AlterConfigOpType, AlterConfigsResourceResult,
116    AlterReplicaLogDirsDirectory, AlterReplicaLogDirsRequest, AlterReplicaLogDirsResponse,
117    AlterReplicaLogDirsResponsePartition, AlterReplicaLogDirsResponseTopic,
118    AlterReplicaLogDirsTopic, AlterShareGroupOffsetsPartition, AlterShareGroupOffsetsTopic,
119    AlteredShareGroupOffsets, AlteredShareGroupOffsetsPartition, AlteredShareGroupOffsetsTopic,
120    AssignReplicasToDirsDirectory, AssignReplicasToDirsPartition, AssignReplicasToDirsRequest,
121    AssignReplicasToDirsResponse, AssignReplicasToDirsResponseDirectory,
122    AssignReplicasToDirsResponsePartition, AssignReplicasToDirsResponseTopic,
123    AssignReplicasToDirsTopic, ClientQuotaAlteration, ClientQuotaAlterationResult,
124    ClientQuotaEntity, ClientQuotaEntry, ClientQuotaFilter, ClientQuotaFilterComponent,
125    ClientQuotaOp, ClientQuotaValue, ClusterDescription, ClusterResource, Config, ConfigEntry,
126    ConfigSource, ConfigSynonym, ConfigType, ConsumerGroupAssignment, ConsumerGroupMember,
127    ConsumerGroupTopicPartitions, CreatableRenewer, CreateDelegationTokenRequest,
128    CreateDelegationTokenResponse, DeletableGroupResult, DeleteShareGroupOffsetsTopic,
129    DeletedShareGroupOffsets, DeletedShareGroupOffsetsTopic, DescribableLogDirTopic,
130    DescribeClusterBroker, DescribeDelegationTokenOwner, DescribeDelegationTokenRequest,
131    DescribeDelegationTokenResponse, DescribeLogDirsPartition, DescribeLogDirsRequest,
132    DescribeLogDirsResponse, DescribeLogDirsResult, DescribeLogDirsTopic,
133    DescribeProducersPartition, DescribeProducersTopic, DescribeShareGroupOffsetsGroup,
134    DescribeShareGroupOffsetsTopic, DescribeTopicPartitionsResponse,
135    DescribeUserScramCredentialsResult, DescribedConsumerGroup, DescribedDelegationToken,
136    DescribedDelegationTokenRenewer, DescribedGroup, DescribedGroupMember, DescribedShareGroup,
137    DescribedShareGroupOffsets, DescribedShareGroupOffsetsPartition,
138    DescribedShareGroupOffsetsTopic, DescribedTopicPartition, DescribedTopicPartitions,
139    EndpointType, ExpireDelegationTokenRequest, ExpireDelegationTokenResponse,
140    GetTelemetrySubscriptionsResponse, GroupState, GroupType, ListedConfigResource, ListedGroup,
141    Node, PushTelemetryRequest, PushTelemetryResponse, RenewDelegationTokenRequest,
142    RenewDelegationTokenResponse, ScramCredentialInfo, ScramMechanism, ShareGroupAssignment,
143    ShareGroupMember, ShareGroupTopicPartitions, TopicPartitionCursor, TopicPartitionInfo,
144    TransactionListing, TransactionState, TransactionTopic, UnregisterBrokerResponse, UpgradeType,
145    ALTER_CONFIG_APPEND, ALTER_CONFIG_DELETE, ALTER_CONFIG_SET, ALTER_CONFIG_SUBTRACT,
146    AUTHORIZED_OPERATIONS_OMITTED, CONFIG_SOURCE_DEFAULT, CONFIG_SOURCE_DYNAMIC_BROKER,
147    CONFIG_SOURCE_DYNAMIC_BROKER_LOGGER, CONFIG_SOURCE_DYNAMIC_CLIENT_METRICS,
148    CONFIG_SOURCE_DYNAMIC_DEFAULT_BROKER, CONFIG_SOURCE_DYNAMIC_GROUP, CONFIG_SOURCE_DYNAMIC_TOPIC,
149    CONFIG_SOURCE_STATIC_BROKER, CONFIG_SOURCE_UNKNOWN, CONFIG_TYPE_BOOLEAN, CONFIG_TYPE_CLASS,
150    CONFIG_TYPE_DOUBLE, CONFIG_TYPE_INT, CONFIG_TYPE_LIST, CONFIG_TYPE_LONG, CONFIG_TYPE_PASSWORD,
151    CONFIG_TYPE_SHORT, CONFIG_TYPE_STRING, CONFIG_TYPE_UNKNOWN, ENDPOINT_TYPE_BROKERS,
152    ENDPOINT_TYPE_CONTROLLERS, INVALID_OFFSET_LAG, QUOTA_MATCH_ANY, QUOTA_MATCH_DEFAULT,
153    QUOTA_MATCH_EXACT, RESOURCE_BROKER as CONFIG_RESOURCE_BROKER,
154    RESOURCE_BROKER_LOGGER as CONFIG_RESOURCE_BROKER_LOGGER,
155    RESOURCE_CLIENT_METRICS as CONFIG_RESOURCE_CLIENT_METRICS,
156    RESOURCE_GROUP as CONFIG_RESOURCE_GROUP, RESOURCE_TOPIC as CONFIG_RESOURCE_TOPIC,
157    SCRAM_SHA_256, SCRAM_SHA_512, SCRAM_UNKNOWN, UNKNOWN_VOLUME_BYTES, UPGRADE_TYPE_SAFE_DOWNGRADE,
158    UPGRADE_TYPE_UNSAFE_DOWNGRADE, UPGRADE_TYPE_UPGRADE,
159};
160pub use crate::protocol::group::OffsetDeleteResult;
161
162/// Bootstrap, identity, SASL, and TLS for [`Admin`].
163#[derive(Debug, Clone)]
164pub struct AdminConfig {
165    /// Bootstrap brokers, `host:port`.
166    pub bootstrap: Vec<String>,
167    /// Kafka `client.id`.
168    pub client_id: String,
169    /// Per-request timeout.
170    pub request_timeout: Duration,
171    /// TCP connect timeout.
172    pub connect_timeout: Duration,
173    /// Kafka `reconnect.backoff.ms`. Wait after a failed TCP/TLS/SASL
174    /// connect to a broker before the next attempt. Default 50ms (Java).
175    /// Zero retries immediately. Grows as `base * 2^n` up to
176    /// [`Self::reconnect_backoff_max`].
177    pub reconnect_backoff: Duration,
178    /// Kafka `reconnect.backoff.max.ms`. Cap on [`Self::reconnect_backoff`]
179    /// exponential growth. Default 1s (Java).
180    pub reconnect_backoff_max: Duration,
181    /// Kafka `connections.max.idle.ms`. Close a broker TCP connection that
182    /// has been unused for this long and reconnect on the next RPC. Default
183    /// 9 minutes (Java). Zero never closes for idle.
184    pub connections_max_idle: Duration,
185    /// Kafka `retry.backoff.ms`. Wait after a retriable admin error
186    /// (`NOT_CONTROLLER`, coordinator moves, IO) before the next attempt.
187    /// Default 100ms (Java / librdkafka). Zero retries immediately.
188    /// Grows as `base * 2^n` up to [`Self::retry_backoff_max`]. Distinct
189    /// from [`Self::reconnect_backoff`] (TCP/handshake failures).
190    pub retry_backoff: Duration,
191    /// Kafka `retry.backoff.max.ms`. Cap on [`Self::retry_backoff`]
192    /// exponential growth. Default 1s.
193    pub retry_backoff_max: Duration,
194    /// SASL PLAIN `(username, password)`.
195    pub sasl_plain: Option<(String, String)>,
196    /// SASL SCRAM-SHA-256 `(username, password)`.
197    pub sasl_scram: Option<(String, String)>,
198    /// SASL SCRAM-SHA-512 `(username, password)`.
199    pub sasl_scram_sha512: Option<(String, String)>,
200    /// Unsecured OAUTHBEARER token.
201    pub sasl_oauthbearer: Option<String>,
202    /// OIDC client-credentials token endpoint.
203    pub sasl_oauthbearer_oidc: Option<crate::OidcConfig>,
204    /// rustls. No OpenSSL.
205    pub tls: Option<TlsConfig>,
206}
207
208impl Default for AdminConfig {
209    fn default() -> Self {
210        Self {
211            bootstrap: vec!["127.0.0.1:9092".into()],
212            client_id: "partitionline".into(),
213            request_timeout: Duration::from_secs(30),
214            connect_timeout: Duration::from_secs(10),
215            reconnect_backoff: crate::config::DEFAULT_RECONNECT_BACKOFF,
216            reconnect_backoff_max: crate::config::DEFAULT_RECONNECT_BACKOFF_MAX,
217            connections_max_idle: crate::config::DEFAULT_CONNECTIONS_MAX_IDLE,
218            retry_backoff: crate::config::DEFAULT_RETRY_BACKOFF,
219            retry_backoff_max: crate::config::DEFAULT_RETRY_BACKOFF_MAX,
220            sasl_plain: None,
221            sasl_scram: None,
222            sasl_scram_sha512: None,
223            sasl_oauthbearer: None,
224            sasl_oauthbearer_oidc: None,
225            tls: None,
226        }
227    }
228}
229
230impl AdminConfig {
231    /// Bootstrap brokers, for example `["127.0.0.1:9092"]`.
232    pub fn bootstrap<S: Into<String>>(servers: impl IntoIterator<Item = S>) -> Self {
233        Self {
234            bootstrap: servers.into_iter().map(Into::into).collect(),
235            ..Self::default()
236        }
237    }
238
239    /// Kafka `client.id`.
240    #[must_use]
241    pub fn client_id(mut self, id: impl Into<String>) -> Self {
242        self.client_id = id.into();
243        self
244    }
245
246    /// SASL. Replaces any previously set mechanism.
247    #[must_use]
248    pub fn sasl(mut self, sasl: crate::Sasl) -> Self {
249        sasl.apply_to(
250            &mut self.sasl_plain,
251            &mut self.sasl_scram,
252            &mut self.sasl_scram_sha512,
253            &mut self.sasl_oauthbearer,
254            &mut self.sasl_oauthbearer_oidc,
255        );
256        self
257    }
258
259    /// rustls. No OpenSSL.
260    #[must_use]
261    pub fn tls(mut self, tls: TlsConfig) -> Self {
262        crate::config::apply_tls(&mut self.tls, tls);
263        self
264    }
265
266    /// Per-request timeout.
267    #[must_use]
268    pub fn request_timeout(mut self, timeout: Duration) -> Self {
269        self.request_timeout = timeout;
270        self
271    }
272
273    /// TCP connect timeout.
274    #[must_use]
275    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
276        self.connect_timeout = timeout;
277        self
278    }
279
280    /// Kafka `reconnect.backoff.ms`. Wait after a failed broker connect.
281    ///
282    /// Default 50ms (Java). Zero retries immediately. Combined with
283    /// [`Self::reconnect_backoff_max`] this is exponential (`base * 2^n`),
284    /// no jitter.
285    #[must_use]
286    pub fn reconnect_backoff(mut self, backoff: Duration) -> Self {
287        self.reconnect_backoff = backoff;
288        self
289    }
290
291    /// Kafka `reconnect.backoff.max.ms`. Cap on exponential reconnect waits.
292    ///
293    /// Default 1s (Java). Raised to [`Self::reconnect_backoff`] when set lower.
294    #[must_use]
295    pub fn reconnect_backoff_max(mut self, backoff: Duration) -> Self {
296        self.reconnect_backoff_max = backoff;
297        self
298    }
299
300    /// Kafka `connections.max.idle.ms`. Close unused broker TCP connections.
301    ///
302    /// Default 9 minutes (Java). Zero never closes for idle. The next admin
303    /// RPC reconnects.
304    #[must_use]
305    pub fn connections_max_idle(mut self, idle: Duration) -> Self {
306        self.connections_max_idle = idle;
307        self
308    }
309
310    /// Kafka `retry.backoff.ms`. Wait after a retriable admin error.
311    ///
312    /// Default 100ms. Zero retries immediately. Combined with
313    /// [`Self::retry_backoff_max`] this is exponential (`base * 2^n`),
314    /// no jitter. Failed TCP/handshake still uses [`Self::reconnect_backoff`].
315    #[must_use]
316    pub fn retry_backoff(mut self, backoff: Duration) -> Self {
317        self.retry_backoff = backoff;
318        self
319    }
320
321    /// Kafka `retry.backoff.max.ms`. Cap on exponential admin retry waits.
322    ///
323    /// Default 1s. Raised to [`Self::retry_backoff`] when set lower.
324    #[must_use]
325    pub fn retry_backoff_max(mut self, backoff: Duration) -> Self {
326        self.retry_backoff_max = backoff;
327        self
328    }
329}
330
331/// Topic to create (`CreateTopics`). Java `NewTopic`.
332///
333/// [`std::fmt::Display`] is Java `NewTopic.toString`. Negative partition count or
334/// replication factor prints `default`. Empty assignments and configs
335/// print `null`.
336#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct NewTopic {
338    /// Topic name.
339    pub name: String,
340    /// Partition count, or `-1` for broker `num.partitions` (KIP-464) or
341    /// when `assignments` is set (Java `NewTopic(String, Map)`).
342    pub num_partitions: i32,
343    /// Replication factor, or `-1` for broker `default.replication.factor`
344    /// (KIP-464) or when `assignments` is set (Java `NewTopic(String, Map)`).
345    pub replication_factor: i16,
346    /// Manual replica assignments `(partition_index, broker_ids)`.
347    ///
348    /// Empty is Java `NewTopic(String, int, short)` (broker assigns).
349    /// Non-empty is Java `NewTopic(String, Map<Integer, List<Integer>>)`.
350    pub assignments: Vec<(i32, Vec<i32>)>,
351    /// Optional topic configs `(name, value)`.
352    pub configs: Vec<(String, Option<String>)>,
353}
354
355impl NewTopic {
356    /// `name` with partition count and replication factor.
357    ///
358    /// Assignments are empty; the broker places replicas. `-1` on either
359    /// count is Java `Optional.empty()` (KIP-464 broker default).
360    pub fn new(name: impl Into<String>, num_partitions: i32, replication_factor: i16) -> Self {
361        Self {
362            name: name.into(),
363            num_partitions,
364            replication_factor,
365            assignments: Vec::new(),
366            configs: Vec::new(),
367        }
368    }
369
370    /// Java `NewTopic(String, Optional.empty(), Optional.empty())` (KIP-464).
371    ///
372    /// Sends NumPartitions `-1` and ReplicationFactor `-1` with an empty
373    /// Assignments array so the broker uses `num.partitions` /
374    /// `default.replication.factor`.
375    #[must_use]
376    pub fn broker_defaults(name: impl Into<String>) -> Self {
377        Self::new(
378            name,
379            CreateTopicsRequest::NO_NUM_PARTITIONS,
380            CreateTopicsRequest::NO_REPLICATION_FACTOR,
381        )
382    }
383
384    /// Java `NewTopic(String, Map<Integer, List<Integer>>)`.
385    ///
386    /// `num_partitions` and `replication_factor` are `-1`. Each pair is
387    /// partition index → replica broker ids.
388    #[must_use]
389    pub fn with_assignments<A, B>(name: impl Into<String>, assignments: A) -> Self
390    where
391        A: IntoIterator<Item = (i32, B)>,
392        B: IntoIterator<Item = i32>,
393    {
394        Self {
395            name: name.into(),
396            num_partitions: CreateTopicsRequest::NO_NUM_PARTITIONS,
397            replication_factor: CreateTopicsRequest::NO_REPLICATION_FACTOR,
398            assignments: assignments
399                .into_iter()
400                .map(|(partition, brokers)| (partition, brokers.into_iter().collect()))
401                .collect(),
402            configs: Vec::new(),
403        }
404    }
405
406    /// Topic config `(name, value)`.
407    #[must_use]
408    pub fn config(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
409        self.configs.push((name.into(), Some(value.into())));
410        self
411    }
412
413    /// Java `NewTopic.configs(Map)`.
414    ///
415    /// Replaces any configs set by [`Self::config`]. Each pair is
416    /// `(name, value)`.
417    #[must_use]
418    pub fn configs<I, K, V>(mut self, configs: I) -> Self
419    where
420        I: IntoIterator<Item = (K, V)>,
421        K: Into<String>,
422        V: Into<String>,
423    {
424        self.configs = configs
425            .into_iter()
426            .map(|(k, v)| (k.into(), Some(v.into())))
427            .collect();
428        self
429    }
430
431    /// Java `NewTopic.name`.
432    #[must_use]
433    pub fn name(&self) -> &str {
434        self.name.as_str()
435    }
436
437    /// Java `NewTopic.numPartitions` (`-1` is broker default / assignments).
438    #[must_use]
439    pub fn num_partitions(&self) -> i32 {
440        self.num_partitions
441    }
442
443    /// Java `NewTopic.replicationFactor` (`-1` is broker default / assignments).
444    #[must_use]
445    pub fn replication_factor(&self) -> i16 {
446        self.replication_factor
447    }
448
449    /// Java `NewTopic.replicasAssignments` (`None` is Java `null`).
450    #[must_use]
451    pub fn replicas_assignments(&self) -> Option<&[(i32, Vec<i32>)]> {
452        if self.assignments.is_empty() {
453            None
454        } else {
455            Some(self.assignments.as_slice())
456        }
457    }
458}
459
460impl fmt::Display for NewTopic {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        write!(f, "(name={}, numPartitions=", self.name)?;
463        if self.num_partitions < 0 {
464            f.write_str("default")?;
465        } else {
466            write!(f, "{}", self.num_partitions)?;
467        }
468        f.write_str(", replicationFactor=")?;
469        if self.replication_factor < 0 {
470            f.write_str("default")?;
471        } else {
472            write!(f, "{}", self.replication_factor)?;
473        }
474        f.write_str(", replicasAssignments=")?;
475        if self.assignments.is_empty() {
476            f.write_str("null")?;
477        } else {
478            f.write_str("{")?;
479            for (i, (partition, brokers)) in self.assignments.iter().enumerate() {
480                if i > 0 {
481                    f.write_str(", ")?;
482                }
483                write!(f, "{partition}=")?;
484                write_java_int_list(f, brokers)?;
485            }
486            f.write_str("}")?;
487        }
488        f.write_str(", configs=")?;
489        if self.configs.is_empty() {
490            f.write_str("null")?;
491        } else {
492            f.write_str("{")?;
493            for (i, (name, value)) in self.configs.iter().enumerate() {
494                if i > 0 {
495                    f.write_str(", ")?;
496                }
497                write!(f, "{name}=")?;
498                match value {
499                    Some(v) => f.write_str(v)?,
500                    None => f.write_str("null")?,
501                }
502            }
503            f.write_str("}")?;
504        }
505        f.write_str(")")
506    }
507}
508
509/// Java `RecordsToDelete` for [`Admin::delete_records`].
510///
511/// Converts to the DeleteRecords Offset INT64: records strictly before
512/// this offset are deleted; records at or after it are kept.
513/// [`crate::protocol::admin::DeleteRecordsRequest::HIGH_WATERMARK`] is Java
514/// `DeleteRecordsRequest.HIGH_WATERMARK` (truncate to the high watermark).
515/// [`std::fmt::Display`] is Java `RecordsToDelete.toString`.
516#[derive(Clone, Copy, Debug, PartialEq, Eq)]
517pub struct RecordsToDelete {
518    offset: i64,
519}
520
521impl RecordsToDelete {
522    /// Java `RecordsToDelete.beforeOffset(long)`.
523    #[must_use]
524    pub const fn before_offset(offset: i64) -> Self {
525        Self { offset }
526    }
527
528    /// DeleteRecords Offset INT64 (Java `RecordsToDelete.beforeOffset()`).
529    #[must_use]
530    pub const fn offset(self) -> i64 {
531        self.offset
532    }
533}
534
535impl fmt::Display for RecordsToDelete {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        write!(f, "(beforeOffset = {})", self.offset)
538    }
539}
540
541impl From<RecordsToDelete> for i64 {
542    fn from(records: RecordsToDelete) -> Self {
543        records.offset
544    }
545}
546
547/// Java `DeletedRecords` for [`Admin::delete_records`].
548///
549/// `low_watermark` is Java `lowWatermark()`. `error_code` is the
550/// per-partition DeleteRecords ErrorCode (Java surfaces non-zero via
551/// the future). [`Self::INVALID_LOW_WATERMARK`] is Java
552/// `DeleteRecordsResponse.INVALID_LOW_WATERMARK`.
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554pub struct DeletedRecords {
555    /// New log start offset after the delete.
556    pub low_watermark: i64,
557    /// Per-partition ErrorCode (`0` is success).
558    pub error_code: i16,
559}
560
561impl DeletedRecords {
562    /// Java `DeleteRecordsResponse.INVALID_LOW_WATERMARK`.
563    pub const INVALID_LOW_WATERMARK: i64 = -1;
564
565    /// Java `DeletedRecords(long)` (`error_code` 0).
566    #[must_use]
567    pub const fn new(low_watermark: i64) -> Self {
568        Self {
569            low_watermark,
570            error_code: 0,
571        }
572    }
573
574    /// Low watermark plus per-partition ErrorCode.
575    #[must_use]
576    pub const fn with_error_code(low_watermark: i64, error_code: i16) -> Self {
577        Self {
578            low_watermark,
579            error_code,
580        }
581    }
582
583    /// Java `DeletedRecords.lowWatermark`.
584    #[must_use]
585    pub const fn low_watermark(self) -> i64 {
586        self.low_watermark
587    }
588
589    /// Per-partition DeleteRecords ErrorCode.
590    #[must_use]
591    pub const fn error_code(self) -> i16 {
592        self.error_code
593    }
594}
595
596impl From<(i64, i16)> for DeletedRecords {
597    fn from((low_watermark, error_code): (i64, i16)) -> Self {
598        Self {
599            low_watermark,
600            error_code,
601        }
602    }
603}
604
605impl From<DeletedRecords> for (i64, i16) {
606    fn from(deleted: DeletedRecords) -> Self {
607        (deleted.low_watermark, deleted.error_code)
608    }
609}
610
611/// Kafka `org.apache.kafka.common.Uuid` (16 bytes; `toString` is base64url).
612///
613/// Wire TopicId / ClientInstanceId stay `[u8; 16]`. [`Self::from_string`] /
614/// [`std::fmt::Display`] match Java `fromString` / `toString` (`URL_SAFE` without
615/// padding). [`Self::random_uuid`] is Java `randomUuid`. Kafka 4.1 Raft
616/// voter APIs are not spoken.
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
618pub struct Uuid([u8; 16]);
619
620impl Uuid {
621    /// Java `Uuid.ZERO_UUID` (`0, 0`).
622    pub const ZERO: Self = Self([0; 16]);
623
624    /// Java `Uuid.ZERO_UUID` ([`Self::ZERO`]).
625    pub const ZERO_UUID: Self = Self::ZERO;
626
627    /// Java `Uuid.ONE_UUID` (`0, 1`). Also [`Self::METADATA_TOPIC_ID`].
628    pub const ONE: Self = Self([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
629
630    /// Java `Uuid.ONE_UUID` ([`Self::ONE`]).
631    pub const ONE_UUID: Self = Self::ONE;
632
633    /// Java `Uuid.METADATA_TOPIC_ID` (KRaft metadata topic).
634    pub const METADATA_TOPIC_ID: Self = Self::ONE;
635
636    /// Java `Uuid.RESERVED` (`ZERO_UUID`, `ONE_UUID` / `METADATA_TOPIC_ID`).
637    ///
638    /// [`Self::random_uuid`] never returns these.
639    pub const RESERVED: [Self; 2] = [Self::ZERO, Self::ONE];
640
641    /// Wrap a 16-byte Kafka UUID (big-endian most then least).
642    #[must_use]
643    pub const fn from_bytes(bytes: [u8; 16]) -> Self {
644        Self(bytes)
645    }
646
647    /// 16-byte Kafka UUID.
648    #[must_use]
649    pub const fn to_bytes(self) -> [u8; 16] {
650        self.0
651    }
652
653    /// 16-byte Kafka UUID.
654    #[must_use]
655    pub const fn as_bytes(&self) -> &[u8; 16] {
656        &self.0
657    }
658
659    /// Java `Uuid(long, long)` (`mostSignificantBits`, `leastSignificantBits`).
660    #[must_use]
661    pub fn from_parts(most: i64, least: i64) -> Self {
662        let mut bytes = [0u8; 16];
663        if let Some(hi) = bytes.first_chunk_mut::<8>() {
664            *hi = most.to_be_bytes();
665        }
666        if let Some(lo) = bytes.last_chunk_mut::<8>() {
667            *lo = least.to_be_bytes();
668        }
669        Self(bytes)
670    }
671
672    /// Java `getMostSignificantBits`.
673    #[must_use]
674    pub fn most_significant_bits(self) -> i64 {
675        match self.0.first_chunk::<8>() {
676            Some(hi) => i64::from_be_bytes(*hi),
677            None => 0,
678        }
679    }
680
681    /// Java `getLeastSignificantBits`.
682    #[must_use]
683    pub fn least_significant_bits(self) -> i64 {
684        match self.0.last_chunk::<8>() {
685            Some(lo) => i64::from_be_bytes(*lo),
686            None => 0,
687        }
688    }
689
690    /// Java `Uuid.fromString` (base64url; optional padding).
691    pub fn from_string(s: &str) -> Result<Self> {
692        if s.len() > 24 {
693            let prefix = s.get(..24).unwrap_or(s);
694            return Err(Error::protocol(format!(
695                "Input string with prefix `{prefix}` is too long to be decoded as a base64 UUID"
696            )));
697        }
698        let decoded =
699            match base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, s) {
700                Ok(b) => b,
701                Err(_) => base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE, s)
702                    .map_err(|_| {
703                        Error::protocol(format!("Uuid string `{s}` is not a base64url UUID"))
704                    })?,
705            };
706        let n = decoded.len();
707        let bytes = <[u8; 16]>::try_from(decoded).map_err(|_| {
708            Error::protocol(format!(
709                "Input string `{s}` decoded as {n} bytes, which is not equal to the expected 16 bytes of a base64-encoded UUID"
710            ))
711        })?;
712        Ok(Self(bytes))
713    }
714
715    /// Java `Uuid.randomUuid` (RFC 4122 variant 2 version 4).
716    ///
717    /// Never returns [`Self::ZERO`], [`Self::ONE`] /
718    /// [`Self::METADATA_TOPIC_ID`], or a UUID whose [`std::fmt::Display`]
719    /// (base64url) starts with `-`.
720    #[must_use]
721    pub fn random_uuid() -> Self {
722        loop {
723            let uuid = Self::new_type4_uuid();
724            if Self::RESERVED.contains(&uuid) || uuid.to_string().starts_with('-') {
725                continue;
726            }
727            return uuid;
728        }
729    }
730
731    fn new_type4_uuid() -> Self {
732        let mut bytes = [0u8; 16];
733        loop {
734            if getrandom::getrandom(&mut bytes).is_ok() {
735                break;
736            }
737        }
738        if let Some(b6) = bytes.get_mut(6) {
739            *b6 = (*b6 & 0x0f) | 0x40;
740        }
741        if let Some(b8) = bytes.get_mut(8) {
742            *b8 = (*b8 & 0x3f) | 0x80;
743        }
744        Self(bytes)
745    }
746}
747
748impl fmt::Display for Uuid {
749    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
750        f.write_str(&base64::Engine::encode(
751            &base64::engine::general_purpose::URL_SAFE_NO_PAD,
752            self.0.as_slice(),
753        ))
754    }
755}
756
757impl From<[u8; 16]> for Uuid {
758    fn from(bytes: [u8; 16]) -> Self {
759        Self(bytes)
760    }
761}
762
763impl From<Uuid> for [u8; 16] {
764    fn from(id: Uuid) -> Self {
765        id.0
766    }
767}
768
769impl std::str::FromStr for Uuid {
770    type Err = Error;
771
772    fn from_str(s: &str) -> Result<Self> {
773        Self::from_string(s)
774    }
775}
776
777impl PartialOrd for Uuid {
778    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
779        Some(self.cmp(other))
780    }
781}
782
783impl Ord for Uuid {
784    fn cmp(&self, other: &Self) -> Ordering {
785        // Java `Uuid.compareTo` compares the two longs as signed values.
786        self.most_significant_bits()
787            .cmp(&other.most_significant_bits())
788            .then(
789                self.least_significant_bits()
790                    .cmp(&other.least_significant_bits()),
791            )
792    }
793}
794
795/// Java `org.apache.kafka.common.TopicCollection` (`ofTopicNames` /
796/// `ofTopicIds`).
797///
798/// [`Admin::describe_topics_for`] / [`Admin::delete_topics_for`] dispatch
799/// names to DescribeTopicPartitions (Metadata fallback) and ids to
800/// Metadata v12+ / DeleteTopics v6. Existing
801/// [`Admin::describe_topics_by_id`] / [`Admin::delete_topics_by_id`] keep
802/// `&[[u8; 16]]` so `describe_topics_by_id(&[])` still infers.
803#[derive(Debug, Clone, PartialEq, Eq)]
804pub enum TopicCollection {
805    /// Java `TopicCollection.TopicNameCollection`.
806    Names(Vec<String>),
807    /// Java `TopicCollection.TopicIdCollection`.
808    Ids(Vec<Uuid>),
809}
810
811impl TopicCollection {
812    /// Java `TopicCollection.ofTopicNames`.
813    #[must_use]
814    pub fn of_topic_names(names: impl IntoIterator<Item = impl Into<String>>) -> Self {
815        Self::Names(names.into_iter().map(Into::into).collect())
816    }
817
818    /// Java `TopicCollection.ofTopicIds`.
819    ///
820    /// `ids` items are [`Uuid`] or `[u8; 16]` ([`From`]). Empty `&[]`
821    /// does not infer; use [`Self::Ids`] with `Vec::new` or
822    /// `Vec::<Uuid>::new()`.
823    #[must_use]
824    pub fn of_topic_ids<I, Id>(ids: I) -> Self
825    where
826        I: IntoIterator<Item = Id>,
827        Id: Into<Uuid>,
828    {
829        Self::Ids(ids.into_iter().map(Into::into).collect())
830    }
831
832    /// Java `TopicNameCollection.topicNames` (`None` when this is ids).
833    #[must_use]
834    pub fn topic_names(&self) -> Option<&[String]> {
835        match self {
836            Self::Names(names) => Some(names.as_slice()),
837            Self::Ids(_) => None,
838        }
839    }
840
841    /// Java `TopicIdCollection.topicIds` (`None` when this is names).
842    #[must_use]
843    pub fn topic_ids(&self) -> Option<&[Uuid]> {
844        match self {
845            Self::Ids(ids) => Some(ids.as_slice()),
846            Self::Names(_) => None,
847        }
848    }
849}
850
851/// One topic from [`Admin::list_topics`] (Java `TopicListing`).
852#[derive(Debug, Clone, PartialEq, Eq)]
853pub struct TopicListing {
854    /// Topic name.
855    pub name: String,
856    /// Topic id (Metadata v10+), or zeros.
857    pub topic_id: [u8; 16],
858    /// Internal topic (for example `__consumer_offsets`).
859    pub is_internal: bool,
860}
861
862impl TopicListing {
863    /// Topic `name` with optional id and internal flag.
864    #[must_use]
865    pub fn new(name: impl Into<String>, topic_id: impl Into<[u8; 16]>, is_internal: bool) -> Self {
866        Self {
867            name: name.into(),
868            topic_id: topic_id.into(),
869            is_internal,
870        }
871    }
872
873    /// Java `TopicListing.name`.
874    #[must_use]
875    pub fn name(&self) -> &str {
876        self.name.as_str()
877    }
878
879    /// Java `TopicListing.topicId`.
880    #[must_use]
881    pub fn topic_id(&self) -> Uuid {
882        Uuid::from_bytes(self.topic_id)
883    }
884
885    /// Java `TopicListing.isInternal`.
886    #[must_use]
887    pub fn is_internal(&self) -> bool {
888        self.is_internal
889    }
890}
891
892impl fmt::Display for TopicListing {
893    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
894        write!(
895            f,
896            "(name={}, topicId={}, internal={})",
897            self.name,
898            Uuid::from_bytes(self.topic_id),
899            self.is_internal
900        )
901    }
902}
903
904/// One group from [`Admin::describe_consumer_groups`] (Java
905/// `ConsumerGroupDescription`).
906///
907/// Kafka 4.0 `DescribeConsumerGroupsHandler` sends ConsumerGroupDescribe
908/// (api 69) first. Per-group [`crate::error::UNSUPPORTED_VERSION`] (35) or
909/// [`crate::error::GROUP_ID_NOT_FOUND`] (69), a broker that does not
910/// advertise api 69, or an RPC-level [`Error::Unsupported`], fall back to
911/// DescribeGroups (api 15).
912#[derive(Debug, Clone, PartialEq, Eq)]
913pub enum ConsumerGroupDescription {
914    /// New consumer protocol (KIP-848) via ConsumerGroupDescribe.
915    Consumer(DescribedConsumerGroup),
916    /// Classic protocol via DescribeGroups.
917    Classic(DescribedGroup),
918}
919
920impl ConsumerGroupDescription {
921    /// Kafka `group.id`.
922    #[must_use]
923    pub fn group_id(&self) -> &str {
924        match self {
925            Self::Consumer(g) => g.group_id.as_str(),
926            Self::Classic(g) => g.group_id.as_str(),
927        }
928    }
929
930    /// Kafka error code (`0` is success).
931    #[must_use]
932    pub fn error_code(&self) -> i16 {
933        match self {
934            Self::Consumer(g) => g.error_code,
935            Self::Classic(g) => g.error_code,
936        }
937    }
938
939    /// Group state name from the broker.
940    #[must_use]
941    pub fn group_state(&self) -> &str {
942        match self {
943            Self::Consumer(g) => g.group_state.as_str(),
944            Self::Classic(g) => g.group_state.as_str(),
945        }
946    }
947
948    /// Bitfield of authorized operations, or [`AUTHORIZED_OPERATIONS_OMITTED`].
949    #[must_use]
950    pub fn authorized_operations(&self) -> i32 {
951        match self {
952            Self::Consumer(g) => g.authorized_operations,
953            Self::Classic(g) => g.authorized_operations,
954        }
955    }
956
957    /// `true` when this came from ConsumerGroupDescribe (api 69).
958    #[must_use]
959    pub fn is_consumer_protocol(&self) -> bool {
960        matches!(self, Self::Consumer(_))
961    }
962
963    /// Java `ConsumerGroupDescription.isSimpleConsumerGroup`.
964    ///
965    /// [`DescribeConsumerGroupsHandler`](https://github.com/apache/kafka/blob/4.0.0/clients/src/main/java/org/apache/kafka/clients/admin/internals/DescribeConsumerGroupsHandler.java)
966    /// is `false` for api 69 and empty classic `ProtocolType` for api 15.
967    #[must_use]
968    pub fn is_simple_consumer_group(&self) -> bool {
969        match self {
970            Self::Consumer(_) => false,
971            Self::Classic(g) => g.is_simple_consumer_group(),
972        }
973    }
974
975    /// Java `ConsumerGroupDescription.partitionAssignor`.
976    #[must_use]
977    pub fn partition_assignor(&self) -> &str {
978        match self {
979            Self::Consumer(g) => g.assignor_name(),
980            Self::Classic(g) => g.protocol_data(),
981        }
982    }
983
984    /// Java `ConsumerGroupDescription.type`.
985    #[must_use]
986    pub fn group_type(&self) -> GroupType {
987        match self {
988            Self::Consumer(_) => GroupType::Consumer,
989            Self::Classic(_) => GroupType::Classic,
990        }
991    }
992
993    /// Java `ConsumerGroupDescription.groupEpoch` (empty for CLASSIC groups).
994    #[must_use]
995    pub fn group_epoch(&self) -> Option<i32> {
996        match self {
997            Self::Consumer(g) => Some(g.group_epoch()),
998            Self::Classic(_) => None,
999        }
1000    }
1001
1002    /// Java `ConsumerGroupDescription.targetAssignmentEpoch` (empty for CLASSIC groups).
1003    #[must_use]
1004    pub fn target_assignment_epoch(&self) -> Option<i32> {
1005        match self {
1006            Self::Consumer(g) => Some(g.assignment_epoch()),
1007            Self::Classic(_) => None,
1008        }
1009    }
1010}
1011
1012/// One topic from [`Admin::describe_topics`] (Java `TopicDescription`).
1013#[derive(Debug, Clone, PartialEq, Eq)]
1014pub struct TopicDescription {
1015    /// Topic name (empty when a TopicId describe returns no name).
1016    pub name: String,
1017    /// Topic id (Metadata v10+), or zeros.
1018    pub topic_id: [u8; 16],
1019    /// Internal topic.
1020    pub is_internal: bool,
1021    /// Per-topic Metadata error (`0` is success).
1022    pub error_code: i16,
1023    /// Partitions (empty when [`Self::error_code`] is not `0`).
1024    pub partitions: Vec<crate::PartitionInfo>,
1025    /// Topic authorized operations (Metadata v8+), or
1026    /// [`AUTHORIZED_OPERATIONS_OMITTED`] when not requested.
1027    pub authorized_operations: i32,
1028}
1029
1030impl TopicDescription {
1031    /// Topic `name` with id, internal flag, error, and partitions.
1032    #[must_use]
1033    pub fn new(
1034        name: impl Into<String>,
1035        topic_id: impl Into<[u8; 16]>,
1036        is_internal: bool,
1037        error_code: i16,
1038        partitions: Vec<crate::PartitionInfo>,
1039    ) -> Self {
1040        Self {
1041            name: name.into(),
1042            topic_id: topic_id.into(),
1043            is_internal,
1044            error_code,
1045            partitions,
1046            authorized_operations: AUTHORIZED_OPERATIONS_OMITTED,
1047        }
1048    }
1049
1050    /// Java `TopicDescription.name`.
1051    #[must_use]
1052    pub fn name(&self) -> &str {
1053        self.name.as_str()
1054    }
1055
1056    /// Java `TopicDescription.topicId`.
1057    #[must_use]
1058    pub fn topic_id(&self) -> Uuid {
1059        Uuid::from_bytes(self.topic_id)
1060    }
1061
1062    /// Java `TopicDescription.isInternal`.
1063    #[must_use]
1064    pub fn is_internal(&self) -> bool {
1065        self.is_internal
1066    }
1067
1068    /// Per-topic Metadata error (`0` is success). Java `TopicDescription`
1069    /// throws instead of storing this.
1070    #[must_use]
1071    pub fn error_code(&self) -> i16 {
1072        self.error_code
1073    }
1074
1075    /// Java `TopicDescription.partitions`.
1076    #[must_use]
1077    pub fn partitions(&self) -> &[crate::PartitionInfo] {
1078        &self.partitions
1079    }
1080
1081    /// Java `TopicDescription.authorizedOperations` as the Metadata
1082    /// bitfield, or [`AUTHORIZED_OPERATIONS_OMITTED`].
1083    #[must_use]
1084    pub fn authorized_operations(&self) -> i32 {
1085        self.authorized_operations
1086    }
1087}
1088
1089impl TopicResult {
1090    /// Java `CreateTopicsResult.topicId` (KIP-516). Zero when the broker
1091    /// omitted TopicId (CreateTopics below v7 / DeleteTopics below v6, or
1092    /// an error).
1093    #[must_use]
1094    pub fn topic_id(&self) -> Uuid {
1095        Uuid::from_bytes(self.topic_id)
1096    }
1097}
1098
1099impl GetTelemetrySubscriptionsResponse {
1100    /// Java `clientInstanceId` (KIP-714). Wire field stays `[u8; 16]`.
1101    #[must_use]
1102    pub fn client_instance_id(&self) -> Uuid {
1103        Uuid::from_bytes(self.client_instance_id)
1104    }
1105}
1106
1107impl PushTelemetryRequest {
1108    /// Java `clientInstanceId` (KIP-714). Wire field stays `[u8; 16]`.
1109    #[must_use]
1110    pub fn client_instance_id(&self) -> Uuid {
1111        Uuid::from_bytes(self.client_instance_id)
1112    }
1113}
1114
1115impl AssignReplicasToDirsTopic {
1116    /// Topic id (KIP-858). Wire field stays `[u8; 16]`.
1117    #[must_use]
1118    pub fn topic_id(&self) -> Uuid {
1119        Uuid::from_bytes(self.topic_id)
1120    }
1121}
1122
1123impl AssignReplicasToDirsDirectory {
1124    /// Directory id (KIP-858). Wire field stays `[u8; 16]`.
1125    #[must_use]
1126    pub fn id(&self) -> Uuid {
1127        Uuid::from_bytes(self.id)
1128    }
1129}
1130
1131impl AssignReplicasToDirsResponseTopic {
1132    /// Topic id (KIP-858). Wire field stays `[u8; 16]`.
1133    #[must_use]
1134    pub fn topic_id(&self) -> Uuid {
1135        Uuid::from_bytes(self.topic_id)
1136    }
1137}
1138
1139impl AssignReplicasToDirsResponseDirectory {
1140    /// Directory id (KIP-858). Wire field stays `[u8; 16]`.
1141    #[must_use]
1142    pub fn id(&self) -> Uuid {
1143        Uuid::from_bytes(self.id)
1144    }
1145}
1146
1147/// One replica: topic, partition, and broker id (Java `TopicPartitionReplica`).
1148#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1149pub struct TopicPartitionReplica {
1150    /// Topic name.
1151    pub topic: String,
1152    /// Partition index.
1153    pub partition: i32,
1154    /// Broker that hosts this replica.
1155    pub broker_id: i32,
1156}
1157
1158impl TopicPartitionReplica {
1159    /// Topic `topic`, partition `partition`, on `broker_id`.
1160    #[must_use]
1161    pub fn new(topic: impl Into<String>, partition: i32, broker_id: i32) -> Self {
1162        Self {
1163            topic: topic.into(),
1164            partition,
1165            broker_id,
1166        }
1167    }
1168
1169    /// Java `TopicPartitionReplica.topic`.
1170    #[must_use]
1171    pub fn topic(&self) -> &str {
1172        self.topic.as_str()
1173    }
1174
1175    /// Java `TopicPartitionReplica.partition`.
1176    #[must_use]
1177    pub fn partition(&self) -> i32 {
1178        self.partition
1179    }
1180
1181    /// Java `TopicPartitionReplica.brokerId`.
1182    #[must_use]
1183    pub fn broker_id(&self) -> i32 {
1184        self.broker_id
1185    }
1186}
1187
1188impl fmt::Display for TopicPartitionReplica {
1189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1190        write!(f, "{}-{}-{}", self.topic, self.partition, self.broker_id)
1191    }
1192}
1193
1194/// Log directory for one replica (Java `ReplicaLogDirInfo`).
1195///
1196/// Missing current or future dirs are [`None`]. Unknown lag is `-1`.
1197#[derive(Debug, Clone, PartialEq, Eq)]
1198pub struct ReplicaLogDirInfo {
1199    /// Current log directory, when the replica is online there.
1200    pub current_log_dir: Option<String>,
1201    /// Offset lag in [`Self::current_log_dir`] (`-1` when unknown).
1202    pub current_offset_lag: i64,
1203    /// Future log directory (AlterReplicaLogDirs in progress).
1204    pub future_log_dir: Option<String>,
1205    /// Offset lag in [`Self::future_log_dir`] (`-1` when unknown).
1206    pub future_offset_lag: i64,
1207}
1208
1209impl ReplicaLogDirInfo {
1210    /// Current and future log dirs with their offset lags.
1211    #[must_use]
1212    pub fn new(
1213        current_log_dir: Option<String>,
1214        current_offset_lag: i64,
1215        future_log_dir: Option<String>,
1216        future_offset_lag: i64,
1217    ) -> Self {
1218        Self {
1219            current_log_dir,
1220            current_offset_lag,
1221            future_log_dir,
1222            future_offset_lag,
1223        }
1224    }
1225
1226    /// No dirs known (Java `ReplicaLogDirInfo()`). Lags are `-1`.
1227    #[must_use]
1228    pub fn unknown() -> Self {
1229        Self::new(None, -1, None, -1)
1230    }
1231
1232    /// Java `ReplicaLogDirInfo.currentLogDir`.
1233    #[must_use]
1234    pub fn current_log_dir(&self) -> Option<&str> {
1235        self.current_log_dir.as_deref()
1236    }
1237
1238    /// Java `ReplicaLogDirInfo.currentOffsetLag`.
1239    #[must_use]
1240    pub fn current_offset_lag(&self) -> i64 {
1241        self.current_offset_lag
1242    }
1243
1244    /// Java `ReplicaLogDirInfo.futureLogDir`.
1245    #[must_use]
1246    pub fn future_log_dir(&self) -> Option<&str> {
1247        self.future_log_dir.as_deref()
1248    }
1249
1250    /// Java `ReplicaLogDirInfo.futureOffsetLag`.
1251    #[must_use]
1252    pub fn future_offset_lag(&self) -> i64 {
1253        self.future_offset_lag
1254    }
1255}
1256
1257impl fmt::Display for ReplicaLogDirInfo {
1258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1259        match self.future_log_dir.as_deref() {
1260            Some(future) => {
1261                f.write_str("(currentReplicaLogDir=")?;
1262                write_java_nullable_str(f, self.current_log_dir.as_deref())?;
1263                f.write_str(", futureReplicaLogDir=")?;
1264                f.write_str(future)?;
1265                write!(f, ", futureReplicaOffsetLag={})", self.future_offset_lag)
1266            }
1267            None => {
1268                f.write_str("ReplicaLogDirInfo(currentReplicaLogDir=")?;
1269                write_java_nullable_str(f, self.current_log_dir.as_deref())?;
1270                f.write_str(")")
1271            }
1272        }
1273    }
1274}
1275
1276fn write_java_nullable_str(f: &mut fmt::Formatter<'_>, s: Option<&str>) -> fmt::Result {
1277    match s {
1278        Some(s) => f.write_str(s),
1279        None => f.write_str("null"),
1280    }
1281}
1282
1283/// Java `List.toString` id list: `[1, 2]` (comma-space).
1284fn write_java_int_list(f: &mut fmt::Formatter<'_>, ids: &[i32]) -> fmt::Result {
1285    f.write_str("[")?;
1286    for (i, id) in ids.iter().enumerate() {
1287        if i > 0 {
1288            f.write_str(", ")?;
1289        }
1290        write!(f, "{id}")?;
1291    }
1292    f.write_str("]")
1293}
1294
1295/// Java `FeatureMetadata.mapToString`: `{(name -> range), ...}`.
1296fn write_java_feature_map<T, N>(f: &mut fmt::Formatter<'_>, items: &[T], name: N) -> fmt::Result
1297where
1298    T: fmt::Display,
1299    N: Fn(&T) -> &str,
1300{
1301    f.write_str("{")?;
1302    for (i, item) in items.iter().enumerate() {
1303        if i > 0 {
1304            f.write_str(", ")?;
1305        }
1306        f.write_str("(")?;
1307        f.write_str(name(item))?;
1308        f.write_str(" -> ")?;
1309        write!(f, "{item}")?;
1310        f.write_str(")")?;
1311    }
1312    f.write_str("}")
1313}
1314
1315/// Increase a topic's partition count (`CreatePartitions`). Java `NewPartitions`.
1316///
1317/// [`std::fmt::Display`] is Java `NewPartitions.toString` (no topic name; `None`
1318/// assignments print `null`).
1319#[derive(Debug, Clone, PartialEq, Eq)]
1320pub struct NewPartitions {
1321    /// Topic name.
1322    pub name: String,
1323    /// Total partition count after the increase (not a delta).
1324    pub total_count: i32,
1325    /// Replica assignments for the new partitions (Java
1326    /// `NewPartitions.increaseTo(int, List<List<Integer>>)`).
1327    ///
1328    /// `None` is a null Assignments array: the broker assigns replicas
1329    /// (Java `increaseTo(int)`).
1330    pub assignments: Option<Vec<Vec<i32>>>,
1331}
1332
1333impl NewPartitions {
1334    /// Set `name` to `total_count` partitions (Java `NewPartitions.increaseTo`).
1335    ///
1336    /// Assignments are null; the broker picks replicas.
1337    #[must_use]
1338    pub fn increase_to(name: impl Into<String>, total_count: i32) -> Self {
1339        Self {
1340            name: name.into(),
1341            total_count,
1342            assignments: None,
1343        }
1344    }
1345
1346    /// Java `NewPartitions.increaseTo(int, List<List<Integer>>)`.
1347    ///
1348    /// Each inner list is the replica broker ids for one new partition.
1349    #[must_use]
1350    pub fn with_assignments(
1351        mut self,
1352        assignments: impl IntoIterator<Item = impl IntoIterator<Item = i32>>,
1353    ) -> Self {
1354        self.assignments = Some(
1355            assignments
1356                .into_iter()
1357                .map(|brokers| brokers.into_iter().collect())
1358                .collect(),
1359        );
1360        self
1361    }
1362
1363    /// Topic name.
1364    #[must_use]
1365    pub fn name(&self) -> &str {
1366        self.name.as_str()
1367    }
1368
1369    /// Java `NewPartitions.totalCount`.
1370    #[must_use]
1371    pub fn total_count(&self) -> i32 {
1372        self.total_count
1373    }
1374
1375    /// Java `NewPartitions.assignments` (`None` is Java `null`).
1376    #[must_use]
1377    pub fn assignments(&self) -> Option<&[Vec<i32>]> {
1378        self.assignments.as_deref()
1379    }
1380}
1381
1382impl fmt::Display for NewPartitions {
1383    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1384        write!(f, "(totalCount={}, newAssignments=", self.total_count)?;
1385        match &self.assignments {
1386            None => f.write_str("null")?,
1387            Some(asns) => {
1388                f.write_str("[")?;
1389                for (i, brokers) in asns.iter().enumerate() {
1390                    if i > 0 {
1391                        f.write_str(", ")?;
1392                    }
1393                    write_java_int_list(f, brokers)?;
1394                }
1395                f.write_str("]")?;
1396            }
1397        }
1398        f.write_str(")")
1399    }
1400}
1401
1402/// Kafka config resource type (`ConfigResource.Type`).
1403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1404#[repr(i8)]
1405pub enum ConfigResourceType {
1406    /// Topic.
1407    Topic = RESOURCE_TOPIC,
1408    /// Broker (`broker.id` as the name).
1409    Broker = RESOURCE_BROKER,
1410    /// Broker logger (KIP-1142).
1411    BrokerLogger = RESOURCE_BROKER_LOGGER,
1412    /// Client metrics (KIP-714 / KIP-1142).
1413    ClientMetrics = RESOURCE_CLIENT_METRICS,
1414    /// Consumer group.
1415    Group = RESOURCE_GROUP,
1416}
1417
1418impl From<ConfigResourceType> for i8 {
1419    fn from(ty: ConfigResourceType) -> Self {
1420        ty as i8
1421    }
1422}
1423
1424impl ConfigResourceType {
1425    /// Java `ConfigResource.Type.id`.
1426    #[must_use]
1427    pub const fn id(self) -> i8 {
1428        self as i8
1429    }
1430
1431    /// Wire id to [`Self`]. Unknown ids are [`None`].
1432    #[must_use]
1433    pub const fn from_id(id: i8) -> Option<Self> {
1434        match id {
1435            RESOURCE_TOPIC => Some(Self::Topic),
1436            RESOURCE_BROKER => Some(Self::Broker),
1437            RESOURCE_BROKER_LOGGER => Some(Self::BrokerLogger),
1438            RESOURCE_CLIENT_METRICS => Some(Self::ClientMetrics),
1439            RESOURCE_GROUP => Some(Self::Group),
1440            _ => None,
1441        }
1442    }
1443}
1444
1445impl fmt::Display for ConfigResourceType {
1446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1447        f.write_str(match *self {
1448            Self::Topic => "TOPIC",
1449            Self::Broker => "BROKER",
1450            Self::BrokerLogger => "BROKER_LOGGER",
1451            Self::ClientMetrics => "CLIENT_METRICS",
1452            Self::Group => "GROUP",
1453        })
1454    }
1455}
1456
1457/// Resource for DescribeConfigs / IncrementalAlterConfigs / AlterConfigs.
1458#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1459pub struct ConfigResource {
1460    /// Kafka resource type (`CONFIG_RESOURCE_TOPIC`, [`ConfigResourceType::Topic`], …).
1461    pub resource_type: i8,
1462    /// Resource name (topic name, or broker id as a string).
1463    pub name: String,
1464    /// Config keys to fetch; `None` means all.
1465    pub keys: Option<Vec<String>>,
1466}
1467
1468impl ConfigResource {
1469    /// Resource of this type. Fetches every config key unless [`Self::keys`] is set.
1470    #[must_use]
1471    pub fn of(ty: ConfigResourceType, name: impl Into<String>) -> Self {
1472        Self {
1473            resource_type: i8::from(ty),
1474            name: name.into(),
1475            keys: None,
1476        }
1477    }
1478
1479    /// Topic resource. Fetches every config key unless [`Self::keys`] is set.
1480    #[must_use]
1481    pub fn topic(name: impl Into<String>) -> Self {
1482        Self::of(ConfigResourceType::Topic, name)
1483    }
1484
1485    /// Broker resource (`broker.id` as the name).
1486    #[must_use]
1487    pub fn broker(id: i32) -> Self {
1488        Self::of(ConfigResourceType::Broker, id.to_string())
1489    }
1490
1491    /// Consumer-group resource.
1492    #[must_use]
1493    pub fn group(name: impl Into<String>) -> Self {
1494        Self::of(ConfigResourceType::Group, name)
1495    }
1496
1497    /// Restrict DescribeConfigs to these keys.
1498    #[must_use]
1499    pub fn keys(mut self, keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
1500        self.keys = Some(keys.into_iter().map(Into::into).collect());
1501        self
1502    }
1503
1504    /// Java `ConfigResource.name`.
1505    #[must_use]
1506    pub fn name(&self) -> &str {
1507        self.name.as_str()
1508    }
1509
1510    /// Java `ConfigResource.type`.
1511    #[must_use]
1512    pub fn resource_type(&self) -> Option<ConfigResourceType> {
1513        ConfigResourceType::from_id(self.resource_type)
1514    }
1515
1516    /// Java `ConfigResource.isDefault` (empty name).
1517    #[must_use]
1518    pub fn is_default(&self) -> bool {
1519        self.name.is_empty()
1520    }
1521}
1522
1523impl fmt::Display for ConfigResource {
1524    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1525        f.write_str("ConfigResource(type=")?;
1526        match self.resource_type() {
1527            Some(ty) => write!(f, "{ty}")?,
1528            None => f.write_str("UNKNOWN")?,
1529        }
1530        write!(f, ", name='{}')", self.name)
1531    }
1532}
1533
1534/// Java `ConfigResource.Type.forId` then `ConfigResource(Type, name)`.
1535///
1536/// Unknown type ids become `UNKNOWN` (0).
1537fn config_resource_from_response(resource_type: i8, name: impl Into<String>) -> ConfigResource {
1538    ConfigResource {
1539        resource_type: ConfigResourceType::from_id(resource_type)
1540            .map(ConfigResourceType::id)
1541            .unwrap_or(0),
1542        name: name.into(),
1543        keys: None,
1544    }
1545}
1546
1547fn alter_config_api_errors(
1548    results: &[AlterConfigsResourceResult],
1549) -> HashMap<ConfigResource, crate::error::ApiError> {
1550    let mut map = HashMap::new();
1551    for result in results {
1552        map.extend([(
1553            config_resource_from_response(result.resource_type, result.name.clone()),
1554            crate::error::ApiError::from_code(result.error_code, result.error_message.clone()),
1555        )]);
1556    }
1557    map
1558}
1559
1560impl crate::protocol::admin::AlterConfigsResponse {
1561    /// Java `AlterConfigsResponse.errors`.
1562    ///
1563    /// Each key is a [`ConfigResource`] (Java `Type.forId`; unknown ids are
1564    /// `UNKNOWN`). Values are [`crate::error::ApiError`].
1565    #[must_use]
1566    pub fn errors(
1567        results: &[AlterConfigsResourceResult],
1568    ) -> HashMap<ConfigResource, crate::error::ApiError> {
1569        alter_config_api_errors(results)
1570    }
1571}
1572
1573impl crate::protocol::admin::AlterConfigsRequest {
1574    /// Java `AlterConfigsRequest.configs`.
1575    ///
1576    /// Each key is a [`ConfigResource`] (Java `Type.forId`; unknown ids are
1577    /// `UNKNOWN`). Each value is [`Config`] of [`ConfigEntry::new`] for that
1578    /// resource's name/value pairs. Official Java wraps an inner
1579    /// `AlterConfigsRequest.Config` whose `ConfigEntry` constructor rejects a
1580    /// null Value; crate `None` is Java `null`.
1581    #[must_use]
1582    pub fn configs(resources: &[AlterConfigsResource]) -> HashMap<ConfigResource, Config> {
1583        let mut map = HashMap::new();
1584        for resource in resources {
1585            map.extend([(
1586                config_resource_from_response(resource.resource_type, resource.name.clone()),
1587                Config::new(
1588                    resource
1589                        .configs
1590                        .iter()
1591                        .map(|c| ConfigEntry::new(c.name.clone(), c.value.clone())),
1592                ),
1593            )]);
1594        }
1595        map
1596    }
1597}
1598
1599impl crate::protocol::admin::IncrementalAlterConfigsResponse {
1600    /// Java `IncrementalAlterConfigsResponse.fromResponseData`.
1601    ///
1602    /// Same map as [`crate::protocol::admin::AlterConfigsResponse::errors`].
1603    #[must_use]
1604    pub fn from_response_data(
1605        results: &[AlterConfigsResourceResult],
1606    ) -> HashMap<ConfigResource, crate::error::ApiError> {
1607        alter_config_api_errors(results)
1608    }
1609}
1610
1611impl crate::protocol::admin::DescribeConfigsResponse {
1612    /// Java `DescribeConfigsResponse.resultMap`.
1613    ///
1614    /// Each key is a [`ConfigResource`] (Java `Type.forId`; unknown ids are
1615    /// `UNKNOWN`).
1616    #[must_use]
1617    pub fn result_map(
1618        results: &[DescribeConfigsResult],
1619    ) -> HashMap<ConfigResource, DescribeConfigsResult> {
1620        let mut map = HashMap::new();
1621        for result in results {
1622            map.extend([(
1623                config_resource_from_response(result.resource_type, result.name.clone()),
1624                result.clone(),
1625            )]);
1626        }
1627        map
1628    }
1629}
1630
1631impl crate::protocol::admin::ListConfigResourcesResponse {
1632    /// Java `ListConfigResourcesResponse.configResources`.
1633    ///
1634    /// Each entry is [`ConfigResource`] via `Type.forId` then
1635    /// `ConfigResource(Type, name)`. Unknown type ids become `UNKNOWN`
1636    /// (`0`). `keys` stays `None`. Official Java
1637    /// `ListConfigResourcesResponse.configResources` (4.1.0; 4.0.0 404
1638    /// as the current name). This crate speaks 0–1. This is not
1639    /// [`Self::error`] / [`Self::error_counts`] /
1640    /// [`ListedConfigResource::to_config_resource`] (keeps the wire id).
1641    #[must_use]
1642    pub fn config_resources(&self) -> Vec<ConfigResource> {
1643        self.config_resources
1644            .iter()
1645            .map(|r| config_resource_from_response(r.resource_type, r.resource_name.clone()))
1646            .collect()
1647    }
1648}
1649
1650impl ListedConfigResource {
1651    /// Java `ConfigResource.name` (`listConfigResources` item).
1652    #[must_use]
1653    pub fn name(&self) -> &str {
1654        self.resource_name.as_str()
1655    }
1656
1657    /// Java `ConfigResource.type`. Unknown wire ids are [`None`].
1658    #[must_use]
1659    pub fn resource_type(&self) -> Option<ConfigResourceType> {
1660        ConfigResourceType::from_id(self.resource_type)
1661    }
1662
1663    /// This listing as a [`ConfigResource`] with no key filter (Java
1664    /// `listConfigResources` returns `ConfigResource`).
1665    #[must_use]
1666    pub fn to_config_resource(&self) -> ConfigResource {
1667        ConfigResource {
1668            resource_type: self.resource_type,
1669            name: self.resource_name.clone(),
1670            keys: None,
1671        }
1672    }
1673
1674    /// Java `ConfigResource.isDefault` (empty name).
1675    #[must_use]
1676    pub fn is_default(&self) -> bool {
1677        self.resource_name.is_empty()
1678    }
1679}
1680
1681impl fmt::Display for ListedConfigResource {
1682    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1683        fmt::Display::fmt(&self.to_config_resource(), f)
1684    }
1685}
1686
1687impl From<ListedConfigResource> for ConfigResource {
1688    fn from(listed: ListedConfigResource) -> Self {
1689        Self {
1690            resource_type: listed.resource_type,
1691            name: listed.resource_name,
1692            keys: None,
1693        }
1694    }
1695}
1696
1697/// One resource plus ops for [`Admin::incremental_alter_configs_for`]
1698/// (Java `incrementalAlterConfigs(Map)`).
1699#[derive(Debug, Clone, PartialEq, Eq)]
1700pub struct ConfigResourceUpdate {
1701    /// Resource to alter.
1702    pub resource: ConfigResource,
1703    /// Incremental ops (`AlterConfig::set` / `delete` / `append` / `subtract`).
1704    pub configs: Vec<AlterConfig>,
1705}
1706
1707impl ConfigResourceUpdate {
1708    /// `resource` with these ops.
1709    #[must_use]
1710    pub fn new(resource: ConfigResource, configs: impl IntoIterator<Item = AlterConfig>) -> Self {
1711        Self {
1712            resource,
1713            configs: configs.into_iter().collect(),
1714        }
1715    }
1716}
1717
1718/// One resource plus replacement entries for [`Admin::alter_configs_for`]
1719/// (Java `alterConfigs(Map)`).
1720#[derive(Debug, Clone, PartialEq, Eq)]
1721pub struct ConfigReplacement {
1722    /// Resource to replace configs on.
1723    pub resource: ConfigResource,
1724    /// Replacement name/value pairs (value `None` clears the key).
1725    pub configs: Vec<(String, Option<String>)>,
1726}
1727
1728impl ConfigReplacement {
1729    /// `resource` with these entries.
1730    #[must_use]
1731    pub fn new(
1732        resource: ConfigResource,
1733        configs: impl IntoIterator<Item = (String, Option<String>)>,
1734    ) -> Self {
1735        Self {
1736            resource,
1737            configs: configs.into_iter().collect(),
1738        }
1739    }
1740
1741    /// Java `alterConfigs(Map)` value: this resource plus [`Config::entries`].
1742    #[must_use]
1743    pub fn from_config(resource: ConfigResource, config: &Config) -> Self {
1744        Self {
1745            resource,
1746            configs: config
1747                .entries()
1748                .iter()
1749                .map(|e| (e.name.clone(), e.value.clone()))
1750                .collect(),
1751        }
1752    }
1753}
1754
1755/// Java `NewPartitionReassignment` for
1756/// [`Admin::alter_partition_reassignments_for`].
1757///
1758/// [`Self::new`] rejects an empty replica list (Java throws
1759/// `IllegalArgumentException`).
1760#[derive(Debug, Clone, PartialEq, Eq)]
1761pub struct NewPartitionReassignment {
1762    /// Target replica broker ids (Java `targetReplicas`).
1763    pub target_replicas: Vec<i32>,
1764}
1765
1766impl NewPartitionReassignment {
1767    /// Java `NewPartitionReassignment(List)`. Empty replicas is an error.
1768    pub fn new(target_replicas: impl IntoIterator<Item = i32>) -> Result<Self> {
1769        let target_replicas: Vec<i32> = target_replicas.into_iter().collect();
1770        if target_replicas.is_empty() {
1771            return Err(Error::protocol(
1772                "Cannot create a new partition reassignment without any replicas",
1773            ));
1774        }
1775        Ok(Self { target_replicas })
1776    }
1777
1778    /// Java `NewPartitionReassignment.targetReplicas`.
1779    #[must_use]
1780    pub fn target_replicas(&self) -> &[i32] {
1781        &self.target_replicas
1782    }
1783}
1784
1785/// One partition in `Admin::alter_partition_reassignments`.
1786///
1787/// `replicas = None` cancels a pending reassignment (KIP-455).
1788#[derive(Debug, Clone, PartialEq, Eq)]
1789pub struct PartitionReassignment {
1790    /// Topic name.
1791    pub topic: String,
1792    /// Partition index.
1793    pub partition: i32,
1794    /// Target replica broker ids, or `None` to cancel a pending move.
1795    pub replicas: Option<Vec<i32>>,
1796}
1797
1798impl PartitionReassignment {
1799    /// Move `partition` onto `replicas`.
1800    #[must_use]
1801    pub fn assign(
1802        partition: impl Into<crate::TopicPartition>,
1803        replicas: impl IntoIterator<Item = i32>,
1804    ) -> Self {
1805        let tp = partition.into();
1806        Self {
1807            topic: tp.topic,
1808            partition: tp.partition,
1809            replicas: Some(replicas.into_iter().collect()),
1810        }
1811    }
1812
1813    /// Cancel a pending reassignment for this partition.
1814    #[must_use]
1815    pub fn cancel(partition: impl Into<crate::TopicPartition>) -> Self {
1816        let tp = partition.into();
1817        Self {
1818            topic: tp.topic,
1819            partition: tp.partition,
1820            replicas: None,
1821        }
1822    }
1823
1824    /// Java `alterPartitionReassignments(Map)` value: `Some` assigns,
1825    /// `None` cancels (Java `Optional.empty()`).
1826    #[must_use]
1827    pub fn from_new(
1828        partition: impl Into<crate::TopicPartition>,
1829        assignment: Option<NewPartitionReassignment>,
1830    ) -> Self {
1831        match assignment {
1832            Some(n) => Self::assign(partition, n.target_replicas),
1833            None => Self::cancel(partition),
1834        }
1835    }
1836
1837    /// Topic name.
1838    #[must_use]
1839    pub fn topic(&self) -> &str {
1840        self.topic.as_str()
1841    }
1842
1843    /// Partition index.
1844    #[must_use]
1845    pub fn partition(&self) -> i32 {
1846        self.partition
1847    }
1848
1849    /// Target replicas, or `None` to cancel.
1850    #[must_use]
1851    pub fn replicas(&self) -> Option<&[i32]> {
1852        self.replicas.as_deref()
1853    }
1854}
1855
1856/// Flattened per-partition result of AlterPartitionReassignments.
1857#[derive(Debug, Clone, PartialEq, Eq)]
1858pub struct ReassignmentResult {
1859    /// Topic name.
1860    pub topic: String,
1861    /// Partition index.
1862    pub partition: i32,
1863    /// Per-partition error code (`0` is success).
1864    pub error_code: i16,
1865    /// Broker error message, when present.
1866    pub error_message: Option<String>,
1867}
1868
1869impl ReassignmentResult {
1870    /// Topic name.
1871    #[must_use]
1872    pub fn topic(&self) -> &str {
1873        self.topic.as_str()
1874    }
1875
1876    /// Partition index.
1877    #[must_use]
1878    pub fn partition(&self) -> i32 {
1879        self.partition
1880    }
1881
1882    /// Per-partition error code (`0` is success).
1883    #[must_use]
1884    pub fn error_code(&self) -> i16 {
1885        self.error_code
1886    }
1887
1888    /// Broker error message, when present.
1889    #[must_use]
1890    pub fn error_message(&self) -> Option<&str> {
1891        self.error_message.as_deref()
1892    }
1893}
1894
1895/// Flattened ongoing reassignment from ListPartitionReassignments
1896/// (Java `PartitionReassignment`).
1897///
1898/// [`std::fmt::Display`] is Java `PartitionReassignment.toString` (no topic or
1899/// partition; those are the `listPartitionReassignments` map keys).
1900#[derive(Debug, Clone, PartialEq, Eq)]
1901pub struct OngoingReassignment {
1902    /// Topic name.
1903    pub topic: String,
1904    /// Partition index.
1905    pub partition: i32,
1906    /// Replica set after the move completes.
1907    pub replicas: Vec<i32>,
1908    /// Brokers being added.
1909    pub adding_replicas: Vec<i32>,
1910    /// Brokers being removed.
1911    pub removing_replicas: Vec<i32>,
1912}
1913
1914impl OngoingReassignment {
1915    /// Topic name.
1916    #[must_use]
1917    pub fn topic(&self) -> &str {
1918        self.topic.as_str()
1919    }
1920
1921    /// Partition index.
1922    #[must_use]
1923    pub fn partition(&self) -> i32 {
1924        self.partition
1925    }
1926
1927    /// Java `PartitionReassignment.replicas`.
1928    #[must_use]
1929    pub fn replicas(&self) -> &[i32] {
1930        &self.replicas
1931    }
1932
1933    /// Java `PartitionReassignment.addingReplicas`.
1934    #[must_use]
1935    pub fn adding_replicas(&self) -> &[i32] {
1936        &self.adding_replicas
1937    }
1938
1939    /// Java `PartitionReassignment.removingReplicas`.
1940    #[must_use]
1941    pub fn removing_replicas(&self) -> &[i32] {
1942        &self.removing_replicas
1943    }
1944}
1945
1946impl fmt::Display for OngoingReassignment {
1947    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1948        f.write_str("PartitionReassignment(replicas=")?;
1949        write_java_int_list(f, &self.replicas)?;
1950        f.write_str(", addingReplicas=")?;
1951        write_java_int_list(f, &self.adding_replicas)?;
1952        f.write_str(", removingReplicas=")?;
1953        write_java_int_list(f, &self.removing_replicas)?;
1954        f.write_str(")")
1955    }
1956}
1957
1958/// One finalized-feature update for `Admin::update_features`.
1959///
1960/// [`std::fmt::Display`] is Java `FeatureUpdate.toString` (no feature name; that is
1961/// the `updateFeatures` map key). Unknown upgrade-type codes print
1962/// `UNKNOWN`. Java `FeatureUpdate` constructor rejects maxVersionLevel 0
1963/// with [`UPGRADE_TYPE_UPGRADE`] and a negative maxVersionLevel; this crate
1964/// checks those rules when encoding UpdateFeatures.
1965/// [`Self::is_delete_request`] is Java
1966/// `UpdateFeaturesRequest.FeatureUpdateItem.isDeleteRequest`.
1967#[derive(Debug, Clone, PartialEq, Eq)]
1968pub struct FeatureUpdate {
1969    /// Feature name (for example `metadata.version`).
1970    pub name: String,
1971    /// Target finalized version.
1972    pub max_version_level: i16,
1973    /// When true, allow a downgrade (v0 `AllowDowngrade`; Java maps this
1974    /// to safe downgrade).
1975    pub allow_downgrade: bool,
1976    /// Upgrade type (v1+). [`UPGRADE_TYPE_UPGRADE`] / safe / unsafe.
1977    pub upgrade_type: i8,
1978}
1979
1980impl FeatureUpdate {
1981    /// Feature `name` at `max_version_level` (upgrade only).
1982    #[must_use]
1983    pub fn new(name: impl Into<String>, max_version_level: i16) -> Self {
1984        Self {
1985            name: name.into(),
1986            max_version_level,
1987            allow_downgrade: false,
1988            upgrade_type: UPGRADE_TYPE_UPGRADE,
1989        }
1990    }
1991
1992    /// Allow a downgrade of this feature (Java `FeatureUpdate(short, true)`
1993    /// → safe downgrade). v0 sends `AllowDowngrade`. v1+ sends
1994    /// [`UPGRADE_TYPE_SAFE_DOWNGRADE`].
1995    #[must_use]
1996    pub fn allow_downgrade(mut self, allow: bool) -> Self {
1997        self.allow_downgrade = allow;
1998        self.upgrade_type = if allow {
1999            UPGRADE_TYPE_SAFE_DOWNGRADE
2000        } else {
2001            UPGRADE_TYPE_UPGRADE
2002        };
2003        self
2004    }
2005
2006    /// Set v1+ [`UpgradeType`] (`1` upgrade, `2` safe, `3` unsafe).
2007    /// v0 sends `AllowDowngrade` when the type is not upgrade-only.
2008    #[must_use]
2009    pub fn upgrade_type(mut self, upgrade_type: impl Into<i8>) -> Self {
2010        let upgrade_type = upgrade_type.into();
2011        self.upgrade_type = upgrade_type;
2012        self.allow_downgrade = upgrade_type != UPGRADE_TYPE_UPGRADE;
2013        self
2014    }
2015
2016    /// Feature name (Java map key for `updateFeatures`).
2017    #[must_use]
2018    pub fn name(&self) -> &str {
2019        self.name.as_str()
2020    }
2021
2022    /// Java `FeatureUpdate.maxVersionLevel`.
2023    #[must_use]
2024    pub fn max_version_level(&self) -> i16 {
2025        self.max_version_level
2026    }
2027
2028    /// Java `UpdateFeaturesRequest.FeatureUpdateItem.isDeleteRequest`.
2029    ///
2030    /// True when `maxVersionLevel` is below 1 and the upgrade type is not
2031    /// [`UPGRADE_TYPE_UPGRADE`]. Encode still rejects level 0 with
2032    /// `UpgradeType.UPGRADE` and a negative level.
2033    #[must_use]
2034    pub fn is_delete_request(&self) -> bool {
2035        self.max_version_level < 1 && self.upgrade_type != UPGRADE_TYPE_UPGRADE
2036    }
2037}
2038
2039impl fmt::Display for FeatureUpdate {
2040    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2041        f.write_str("FeatureUpdate{maxVersionLevel:")?;
2042        write!(f, "{}", self.max_version_level)?;
2043        f.write_str(", upgradeType:")?;
2044        f.write_str(match self.upgrade_type {
2045            UPGRADE_TYPE_UPGRADE => "UPGRADE",
2046            UPGRADE_TYPE_SAFE_DOWNGRADE => "SAFE_DOWNGRADE",
2047            UPGRADE_TYPE_UNSAFE_DOWNGRADE => "UNSAFE_DOWNGRADE",
2048            _ => "UNKNOWN",
2049        })?;
2050        f.write_str("}")
2051    }
2052}
2053
2054/// Per-feature result of UpdateFeatures.
2055#[derive(Debug, Clone, PartialEq, Eq)]
2056pub struct FeatureUpdateResult {
2057    /// Feature name.
2058    pub name: String,
2059    /// Per-feature error code (`0` is success).
2060    pub error_code: i16,
2061    /// Broker error message, when present.
2062    pub error_message: Option<String>,
2063}
2064
2065impl FeatureUpdateResult {
2066    /// Feature name.
2067    #[must_use]
2068    pub fn name(&self) -> &str {
2069        self.name.as_str()
2070    }
2071
2072    /// Per-feature error code (`0` is success).
2073    #[must_use]
2074    pub fn error_code(&self) -> i16 {
2075        self.error_code
2076    }
2077
2078    /// Broker error message, when present.
2079    #[must_use]
2080    pub fn error_message(&self) -> Option<&str> {
2081        self.error_message.as_deref()
2082    }
2083}
2084
2085/// Supported version range from [`Admin::describe_features`] (Java `SupportedVersionRange`).
2086///
2087/// [`std::fmt::Display`] is Java `SupportedVersionRange.toString` (no feature name;
2088/// that is the `supportedFeatures` map key). Java constructor requires a
2089/// non-negative min through max; [`Self::new`] checks that.
2090#[derive(Debug, Clone, PartialEq, Eq)]
2091pub struct SupportedVersionRange {
2092    /// Feature name (for example `metadata.version`).
2093    pub name: String,
2094    /// Lowest version the broker supports.
2095    pub min_version: i16,
2096    /// Highest version the broker supports.
2097    pub max_version: i16,
2098}
2099
2100impl SupportedVersionRange {
2101    /// Feature `name` supported from `min_version` through `max_version`.
2102    ///
2103    /// Java `SupportedVersionRange(short, short)` rejects a negative min
2104    /// or max, or max below min.
2105    pub fn new(name: impl Into<String>, min_version: i16, max_version: i16) -> Result<Self> {
2106        if min_version < 0 || max_version < 0 || max_version < min_version {
2107            return Err(Error::protocol(format!(
2108                "Expected 0 <= minVersion <= maxVersion but received minVersion:{min_version}, maxVersion:{max_version}."
2109            )));
2110        }
2111        Ok(Self {
2112            name: name.into(),
2113            min_version,
2114            max_version,
2115        })
2116    }
2117
2118    /// Feature name.
2119    #[must_use]
2120    pub fn name(&self) -> &str {
2121        self.name.as_str()
2122    }
2123
2124    /// Java `SupportedVersionRange.min`.
2125    #[must_use]
2126    pub fn min_version(&self) -> i16 {
2127        self.min_version
2128    }
2129
2130    /// Java `SupportedVersionRange.max`.
2131    #[must_use]
2132    pub fn max_version(&self) -> i16 {
2133        self.max_version
2134    }
2135}
2136
2137impl fmt::Display for SupportedVersionRange {
2138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2139        f.write_str("SupportedVersionRange[min_version:")?;
2140        write!(f, "{}", self.min_version())?;
2141        f.write_str(", max_version:")?;
2142        write!(f, "{}", self.max_version())?;
2143        f.write_str("]")
2144    }
2145}
2146
2147/// Finalized version range from [`Admin::describe_features`] (Java `FinalizedVersionRange`).
2148///
2149/// [`std::fmt::Display`] is Java `FinalizedVersionRange.toString` (no feature name;
2150/// that is the `finalizedFeatures` map key). Java constructor requires
2151/// non-negative min/max and max at least min; [`Self::new`] checks that.
2152#[derive(Debug, Clone, PartialEq, Eq)]
2153pub struct FinalizedVersionRange {
2154    /// Feature name (for example `metadata.version`).
2155    pub name: String,
2156    /// Lowest finalized version.
2157    pub min_version_level: i16,
2158    /// Highest finalized version.
2159    pub max_version_level: i16,
2160}
2161
2162impl FinalizedVersionRange {
2163    /// Feature `name` finalized from `min_version_level` through `max_version_level`.
2164    ///
2165    /// Java `FinalizedVersionRange(short, short)` rejects a negative min
2166    /// or max, or max below min.
2167    pub fn new(
2168        name: impl Into<String>,
2169        min_version_level: i16,
2170        max_version_level: i16,
2171    ) -> Result<Self> {
2172        if min_version_level < 0 || max_version_level < 0 || max_version_level < min_version_level {
2173            return Err(Error::protocol(format!(
2174                "Expected minVersionLevel >= 0, maxVersionLevel >= 0 and maxVersionLevel >= minVersionLevel, but received minVersionLevel: {min_version_level}, maxVersionLevel: {max_version_level}"
2175            )));
2176        }
2177        Ok(Self {
2178            name: name.into(),
2179            min_version_level,
2180            max_version_level,
2181        })
2182    }
2183
2184    /// Feature name.
2185    #[must_use]
2186    pub fn name(&self) -> &str {
2187        self.name.as_str()
2188    }
2189
2190    /// Java `FinalizedVersionRange.minVersionLevel`.
2191    #[must_use]
2192    pub fn min_version_level(&self) -> i16 {
2193        self.min_version_level
2194    }
2195
2196    /// Java `FinalizedVersionRange.maxVersionLevel`.
2197    #[must_use]
2198    pub fn max_version_level(&self) -> i16 {
2199        self.max_version_level
2200    }
2201}
2202
2203impl fmt::Display for FinalizedVersionRange {
2204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2205        f.write_str("FinalizedVersionRange[min_version_level:")?;
2206        write!(f, "{}", self.min_version_level())?;
2207        f.write_str(", max_version_level:")?;
2208        write!(f, "{}", self.max_version_level())?;
2209        f.write_str("]")
2210    }
2211}
2212
2213/// Cluster feature metadata from [`Admin::describe_features`] (Java `FeatureMetadata`).
2214///
2215/// There is no DescribeFeatures api key. Java and this client re-issue
2216/// ApiVersions v3–v4 and read KIP-482 tagged fields.
2217///
2218/// [`std::fmt::Display`] is Java `FeatureMetadata.toString` (crate vec order;
2219/// empty epoch prints a space; omits `zk_migration_ready`).
2220#[derive(Debug, Clone, PartialEq, Eq)]
2221pub struct FeatureMetadata {
2222    /// Features the broker supports (`supportedFeatures`).
2223    pub supported_features: Vec<SupportedVersionRange>,
2224    /// Finalized features (`finalizedFeatures`).
2225    pub finalized_features: Vec<FinalizedVersionRange>,
2226    /// Monotonic finalized-features epoch. `None` when the broker sends `-1`.
2227    pub finalized_features_epoch: Option<i64>,
2228    /// ApiVersions tagged field 3 (`zkMigrationReady`, KIP-866).
2229    pub zk_migration_ready: bool,
2230}
2231
2232impl FeatureMetadata {
2233    /// Java `FeatureMetadata.supportedFeatures`.
2234    #[must_use]
2235    pub fn supported_features(&self) -> &[SupportedVersionRange] {
2236        &self.supported_features
2237    }
2238
2239    /// Java `FeatureMetadata.finalizedFeatures`.
2240    #[must_use]
2241    pub fn finalized_features(&self) -> &[FinalizedVersionRange] {
2242        &self.finalized_features
2243    }
2244
2245    /// Java `FeatureMetadata.finalizedFeaturesEpoch`.
2246    #[must_use]
2247    pub fn finalized_features_epoch(&self) -> Option<i64> {
2248        self.finalized_features_epoch
2249    }
2250
2251    /// ApiVersions tagged field 3 (`zkMigrationReady`).
2252    #[must_use]
2253    pub fn zk_migration_ready(&self) -> bool {
2254        self.zk_migration_ready
2255    }
2256}
2257
2258impl fmt::Display for FeatureMetadata {
2259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2260        f.write_str("FeatureMetadata{finalizedFeatures:")?;
2261        write_java_feature_map(f, self.finalized_features(), FinalizedVersionRange::name)?;
2262        f.write_str(", finalizedFeaturesEpoch:")?;
2263        match self.finalized_features_epoch() {
2264            Some(epoch) => write!(f, "{epoch}")?,
2265            None => f.write_str(" ")?,
2266        }
2267        f.write_str(", supportedFeatures:")?;
2268        write_java_feature_map(f, self.supported_features(), SupportedVersionRange::name)?;
2269        f.write_str("}")
2270    }
2271}
2272
2273/// One SCRAM credential to remove for `Admin::alter_user_scram_credentials`.
2274#[derive(Debug, Clone, PartialEq, Eq)]
2275pub struct UserScramCredentialDeletion {
2276    /// User name.
2277    pub name: String,
2278    /// `SCRAM_SHA_256` / [`crate::ScramMechanism::Sha256`], or SHA-512.
2279    pub mechanism: i8,
2280}
2281
2282impl UserScramCredentialDeletion {
2283    /// Delete this user's credential for `mechanism`.
2284    #[must_use]
2285    pub fn new(name: impl Into<String>, mechanism: impl Into<i8>) -> Self {
2286        Self {
2287            name: name.into(),
2288            mechanism: mechanism.into(),
2289        }
2290    }
2291
2292    /// Java `UserScramCredentialAlteration.user`.
2293    #[must_use]
2294    pub fn user(&self) -> &str {
2295        self.name.as_str()
2296    }
2297
2298    /// Java `UserScramCredentialDeletion.mechanism`.
2299    #[must_use]
2300    pub fn mechanism(&self) -> ScramMechanism {
2301        ScramMechanism::from_id(self.mechanism)
2302    }
2303}
2304
2305/// One SCRAM credential to insert or replace.
2306///
2307/// Callers supply dummy `salt` / `salted_password` bytes. This crate does
2308/// not hash a password or keep a credential store. `Debug` redacts those
2309/// fields.
2310#[derive(Clone, PartialEq, Eq)]
2311pub struct UserScramCredentialUpsertion {
2312    /// User name.
2313    pub name: String,
2314    /// `SCRAM_SHA_256` / [`crate::ScramMechanism::Sha256`], or SHA-512.
2315    pub mechanism: i8,
2316    /// PBKDF2 iteration count.
2317    pub iterations: i32,
2318    /// Salt bytes. Redacted in `Debug`.
2319    pub salt: Vec<u8>,
2320    /// Salted password bytes. Redacted in `Debug`.
2321    pub salted_password: Vec<u8>,
2322}
2323
2324impl UserScramCredentialUpsertion {
2325    /// Insert or replace this user's SCRAM credential.
2326    #[must_use]
2327    pub fn new(
2328        name: impl Into<String>,
2329        mechanism: impl Into<i8>,
2330        iterations: i32,
2331        salt: impl Into<Vec<u8>>,
2332        salted_password: impl Into<Vec<u8>>,
2333    ) -> Self {
2334        Self {
2335            name: name.into(),
2336            mechanism: mechanism.into(),
2337            iterations,
2338            salt: salt.into(),
2339            salted_password: salted_password.into(),
2340        }
2341    }
2342
2343    /// Java `UserScramCredentialAlteration.user`.
2344    #[must_use]
2345    pub fn user(&self) -> &str {
2346        self.name.as_str()
2347    }
2348
2349    /// Java `UserScramCredentialUpsertion.credentialInfo`.
2350    #[must_use]
2351    pub fn credential_info(&self) -> ScramCredentialInfo {
2352        ScramCredentialInfo::new(self.mechanism, self.iterations)
2353    }
2354
2355    /// Java `UserScramCredentialUpsertion.salt`.
2356    #[must_use]
2357    pub fn salt(&self) -> &[u8] {
2358        &self.salt
2359    }
2360}
2361
2362impl std::fmt::Debug for UserScramCredentialUpsertion {
2363    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2364        f.debug_struct("UserScramCredentialUpsertion")
2365            .field("name", &self.name)
2366            .field("mechanism", &self.mechanism)
2367            .field("iterations", &self.iterations)
2368            .field("salt", &"<redacted>")
2369            .field("salted_password", &"<redacted>")
2370            .finish()
2371    }
2372}
2373
2374/// Java `UserScramCredentialAlteration` for
2375/// [`Admin::alter_user_scram_credentials_with`].
2376///
2377/// Java `alterUserScramCredentials(List)` is a mixed list of
2378/// [`UserScramCredentialDeletion`] and [`UserScramCredentialUpsertion`].
2379/// The wire request still splits Deletions then Upsertions.
2380#[derive(Clone, PartialEq, Eq, Debug)]
2381pub enum UserScramCredentialAlteration {
2382    /// Java `UserScramCredentialDeletion`.
2383    Deletion(UserScramCredentialDeletion),
2384    /// Java `UserScramCredentialUpsertion`.
2385    Upsertion(UserScramCredentialUpsertion),
2386}
2387
2388impl UserScramCredentialAlteration {
2389    /// Java `UserScramCredentialAlteration.user()`.
2390    #[must_use]
2391    pub fn user(&self) -> &str {
2392        match self {
2393            Self::Deletion(d) => d.name.as_str(),
2394            Self::Upsertion(u) => u.name.as_str(),
2395        }
2396    }
2397}
2398
2399impl From<UserScramCredentialDeletion> for UserScramCredentialAlteration {
2400    fn from(deletion: UserScramCredentialDeletion) -> Self {
2401        Self::Deletion(deletion)
2402    }
2403}
2404
2405impl From<UserScramCredentialUpsertion> for UserScramCredentialAlteration {
2406    fn from(upsertion: UserScramCredentialUpsertion) -> Self {
2407        Self::Upsertion(upsertion)
2408    }
2409}
2410
2411/// Per-user result of AlterUserScramCredentials.
2412#[derive(Debug, Clone, PartialEq, Eq)]
2413pub struct UserScramCredentialResult {
2414    /// User name.
2415    pub user: String,
2416    /// Per-user error code (`0` is success).
2417    pub error_code: i16,
2418    /// Broker error message, when present.
2419    pub error_message: Option<String>,
2420}
2421
2422impl UserScramCredentialResult {
2423    /// User name.
2424    #[must_use]
2425    pub fn user(&self) -> &str {
2426        self.user.as_str()
2427    }
2428
2429    /// Per-user error code (`0` is success).
2430    #[must_use]
2431    pub fn error_code(&self) -> i16 {
2432        self.error_code
2433    }
2434
2435    /// Broker error message, when present.
2436    #[must_use]
2437    pub fn error_message(&self) -> Option<&str> {
2438        self.error_message.as_deref()
2439    }
2440}
2441
2442/// PID block from `Admin::allocate_producer_ids` (AllocateProducerIds api 67).
2443///
2444/// Fixture broker id/epoch only. This is not a live cluster PID allocator.
2445#[derive(Debug, Clone, PartialEq, Eq)]
2446pub struct ProducerIdBlock {
2447    /// First producer id in the allocated block.
2448    pub producer_id_start: i64,
2449    /// Number of ids in the block.
2450    pub producer_id_len: i32,
2451}
2452
2453impl ProducerIdBlock {
2454    /// First producer id in the allocated block.
2455    #[must_use]
2456    pub fn producer_id_start(&self) -> i64 {
2457        self.producer_id_start
2458    }
2459
2460    /// Number of ids in the block.
2461    #[must_use]
2462    pub fn producer_id_len(&self) -> i32 {
2463        self.producer_id_len
2464    }
2465}
2466
2467/// One transactional.id fenced by [`Admin::fence_producers`].
2468///
2469/// Java `FenceProducersResult`: [`Self::producer_id`] is `producerId`,
2470/// [`Self::epoch`] is `epoch`.
2471#[derive(Debug, Clone, PartialEq, Eq)]
2472pub struct FencedProducer {
2473    /// Kafka `transactional.id`.
2474    pub transactional_id: String,
2475    /// Producer id after InitProducerId.
2476    pub producer_id: i64,
2477    /// Producer epoch after InitProducerId.
2478    pub epoch: i16,
2479}
2480
2481impl FencedProducer {
2482    /// Kafka `transactional.id`.
2483    #[must_use]
2484    pub fn transactional_id(&self) -> &str {
2485        self.transactional_id.as_str()
2486    }
2487
2488    /// Java `FenceProducersResult.producerId`.
2489    #[must_use]
2490    pub fn producer_id(&self) -> i64 {
2491        self.producer_id
2492    }
2493
2494    /// Java `FenceProducersResult.epochId`.
2495    #[must_use]
2496    pub fn epoch(&self) -> i16 {
2497        self.epoch
2498    }
2499}
2500
2501/// Spec for [`Admin::abort_transaction`] (Java `AbortTransactionSpec`).
2502///
2503/// Sends WriteTxnMarkers (api 27) with
2504/// [`crate::TransactionResult::Abort`] (`transactionResult` false)
2505/// to the Metadata partition leader.
2506///
2507/// [`std::fmt::Display`] is Java `AbortTransactionSpec.toString`.
2508#[derive(Debug, Clone, PartialEq, Eq)]
2509pub struct AbortTransactionSpec {
2510    /// Topic name.
2511    pub topic: String,
2512    /// Partition index.
2513    pub partition: i32,
2514    /// Producer id that owns the open transaction.
2515    pub producer_id: i64,
2516    /// Producer epoch.
2517    pub producer_epoch: i16,
2518    /// Transaction coordinator epoch.
2519    pub coordinator_epoch: i32,
2520}
2521
2522impl AbortTransactionSpec {
2523    /// Abort the open transaction of `producer_id` on `partition`.
2524    #[must_use]
2525    pub fn new(
2526        partition: impl Into<crate::TopicPartition>,
2527        producer_id: i64,
2528        producer_epoch: i16,
2529        coordinator_epoch: i32,
2530    ) -> Self {
2531        let tp = partition.into();
2532        Self {
2533            topic: tp.topic,
2534            partition: tp.partition,
2535            producer_id,
2536            producer_epoch,
2537            coordinator_epoch,
2538        }
2539    }
2540
2541    /// Java `AbortTransactionSpec.topicPartition`.
2542    #[must_use]
2543    pub fn topic_partition(&self) -> crate::TopicPartition {
2544        crate::TopicPartition::new(self.topic.clone(), self.partition)
2545    }
2546
2547    /// Java `AbortTransactionSpec.producerId`.
2548    #[must_use]
2549    pub fn producer_id(&self) -> i64 {
2550        self.producer_id
2551    }
2552
2553    /// Java `AbortTransactionSpec.producerEpoch`.
2554    #[must_use]
2555    pub fn producer_epoch(&self) -> i16 {
2556        self.producer_epoch
2557    }
2558
2559    /// Java `AbortTransactionSpec.coordinatorEpoch`.
2560    #[must_use]
2561    pub fn coordinator_epoch(&self) -> i32 {
2562        self.coordinator_epoch
2563    }
2564}
2565
2566impl fmt::Display for AbortTransactionSpec {
2567    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2568        f.write_str("AbortTransactionSpec(topicPartition=")?;
2569        write!(f, "{}-{}", self.topic, self.partition)?;
2570        f.write_str(", producerId=")?;
2571        write!(f, "{}", self.producer_id())?;
2572        f.write_str(", producerEpoch=")?;
2573        write!(f, "{}", self.producer_epoch())?;
2574        f.write_str(", coordinatorEpoch=")?;
2575        write!(f, "{}", self.coordinator_epoch())?;
2576        f.write_str(")")
2577    }
2578}
2579
2580/// Java `KafkaAdminClient.DEFAULT_LEAVE_GROUP_REASON` (KIP-800).
2581pub const DEFAULT_LEAVE_GROUP_REASON: &str = "member was removed by an admin";
2582
2583/// LeaveGroup v5 Reason: empty/null → default; otherwise KIP-800 truncate.
2584fn admin_leave_reason(reason: Option<&str>) -> String {
2585    match reason {
2586        None | Some("") => DEFAULT_LEAVE_GROUP_REASON.to_string(),
2587        Some(r) => crate::group::truncate_group_reason(r),
2588    }
2589}
2590
2591/// One static member for [`Admin::remove_members_from_consumer_group`].
2592///
2593/// Java `MemberToRemove`. Identified by Kafka `group.instance.id` (KIP-345).
2594#[derive(Debug, Clone, PartialEq, Eq)]
2595pub struct MemberToRemove {
2596    /// Kafka `group.instance.id`.
2597    pub group_instance_id: String,
2598}
2599
2600impl MemberToRemove {
2601    /// Remove the static member with this `group.instance.id`.
2602    #[must_use]
2603    pub fn new(group_instance_id: impl Into<String>) -> Self {
2604        Self {
2605            group_instance_id: group_instance_id.into(),
2606        }
2607    }
2608
2609    /// Java `MemberToRemove.groupInstanceId`.
2610    #[must_use]
2611    pub fn group_instance_id(&self) -> &str {
2612        self.group_instance_id.as_str()
2613    }
2614}
2615
2616impl From<&str> for MemberToRemove {
2617    fn from(id: &str) -> Self {
2618        Self::new(id)
2619    }
2620}
2621
2622impl From<String> for MemberToRemove {
2623    fn from(id: String) -> Self {
2624        Self::new(id)
2625    }
2626}
2627
2628/// One group's topic-partition filter (Java `ListConsumerGroupOffsetsSpec`).
2629///
2630/// `partitions: None` is every committed partition (OffsetFetch null
2631/// Topics). Empty `partitions` is a no-op for that group.
2632///
2633/// [`std::fmt::Display`] is Java `ListConsumerGroupOffsetsSpec.toString`
2634/// (`topicPartitions=null` when [`Self::all`]).
2635#[derive(Debug, Clone, PartialEq, Eq, Default)]
2636pub struct ListConsumerGroupOffsetsSpec {
2637    /// Topic-partitions to fetch. `None` is all committed partitions.
2638    pub partitions: Option<Vec<crate::TopicPartition>>,
2639}
2640
2641impl ListConsumerGroupOffsetsSpec {
2642    /// Every committed partition (Java null `topicPartitions`).
2643    #[must_use]
2644    pub fn all() -> Self {
2645        Self { partitions: None }
2646    }
2647
2648    /// Named topic-partitions (Java `ListConsumerGroupOffsetsSpec.topicPartitions`).
2649    #[must_use]
2650    pub fn topic_partitions(
2651        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
2652    ) -> Self {
2653        Self {
2654            partitions: Some(partitions.into_iter().map(Into::into).collect()),
2655        }
2656    }
2657
2658    /// Java `ListConsumerGroupOffsetsSpec.topicPartitions` (`None` is all).
2659    #[must_use]
2660    pub fn partitions(&self) -> Option<&[crate::TopicPartition]> {
2661        self.partitions.as_deref()
2662    }
2663}
2664
2665impl fmt::Display for ListConsumerGroupOffsetsSpec {
2666    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2667        f.write_str("ListConsumerGroupOffsetsSpec(topicPartitions=")?;
2668        match &self.partitions {
2669            None => f.write_str("null")?,
2670            Some(tps) => {
2671                f.write_str("[")?;
2672                for (i, tp) in tps.iter().enumerate() {
2673                    if i > 0 {
2674                        f.write_str(", ")?;
2675                    }
2676                    write!(f, "{tp}")?;
2677                }
2678                f.write_str("]")?;
2679            }
2680        }
2681        f.write_str(")")
2682    }
2683}
2684
2685/// Per-member result of [`Admin::remove_members_from_consumer_group`].
2686#[derive(Debug, Clone, PartialEq, Eq)]
2687pub struct RemovedMember {
2688    /// Kafka member id from the LeaveGroup response (empty when unknown).
2689    pub member_id: String,
2690    /// Kafka `group.instance.id`, when present.
2691    pub group_instance_id: Option<String>,
2692    /// Per-member error code (`0` is success).
2693    pub error_code: i16,
2694}
2695
2696impl RemovedMember {
2697    /// Kafka member id from the LeaveGroup response.
2698    #[must_use]
2699    pub fn member_id(&self) -> &str {
2700        self.member_id.as_str()
2701    }
2702
2703    /// Kafka `group.instance.id`, when present.
2704    #[must_use]
2705    pub fn group_instance_id(&self) -> Option<&str> {
2706        self.group_instance_id.as_deref()
2707    }
2708
2709    /// Per-member error code (`0` is success).
2710    #[must_use]
2711    pub fn error_code(&self) -> i16 {
2712        self.error_code
2713    }
2714}
2715
2716/// Kafka admin client: topics, configs, ACLs, groups, and cluster operations.
2717pub struct Admin {
2718    cfg: AdminConfig,
2719    conn: BrokerConn,
2720    versions: HashMap<i16, ApiVersion>,
2721    create_version: i16,
2722    delete_version: i16,
2723    describe_version: i16,
2724    partitions_version: i16,
2725    alter_version: Option<i16>,
2726    legacy_alter_version: i16,
2727    delete_records_version: i16,
2728    describe_producers_version: Option<i16>,
2729    describe_cluster_version: Option<i16>,
2730    create_acls_version: i16,
2731    describe_acls_version: i16,
2732    delete_acls_version: i16,
2733    metadata_version: i16,
2734    find_coord_version: i16,
2735    offset_delete_version: Option<i16>,
2736    reassign_version: Option<i16>,
2737    list_reassign_version: Option<i16>,
2738    update_features_version: Option<i16>,
2739    alter_user_scram_version: Option<i16>,
2740    describe_user_scram_version: Option<i16>,
2741    unregister_broker_version: Option<i16>,
2742    describe_client_quotas_version: Option<i16>,
2743    alter_client_quotas_version: Option<i16>,
2744    allocate_producer_ids_version: Option<i16>,
2745    describe_transactions_version: Option<i16>,
2746    list_transactions_version: Option<i16>,
2747    consumer_group_describe_version: Option<i16>,
2748    describe_groups_version: i16,
2749    list_groups_version: i16,
2750    delete_groups_version: i16,
2751    share_group_describe_version: Option<i16>,
2752    describe_share_group_offsets_version: Option<i16>,
2753    alter_share_group_offsets_version: Option<i16>,
2754    delete_share_group_offsets_version: Option<i16>,
2755    describe_topic_partitions_version: Option<i16>,
2756    list_config_resources_version: Option<i16>,
2757    get_telemetry_subscriptions_version: Option<i16>,
2758    /// Cached KIP-714 client instance UUID (`None` until first fetch).
2759    cached_client_instance_id: Option<[u8; 16]>,
2760    push_telemetry_version: Option<i16>,
2761    assign_replicas_to_dirs_version: Option<i16>,
2762    alter_replica_log_dirs_version: Option<i16>,
2763    describe_log_dirs_version: Option<i16>,
2764    create_delegation_token_version: Option<i16>,
2765    renew_delegation_token_version: Option<i16>,
2766    expire_delegation_token_version: Option<i16>,
2767    describe_delegation_token_version: Option<i16>,
2768    cluster: Cluster,
2769    conns: HashMap<i32, BrokerConn>,
2770    reconnect_fails: HashMap<i32, u32>,
2771    group_coord: Option<(String, i32)>,
2772    group_coords: HashMap<String, i32>,
2773    txn_coord: Option<(String, i32)>,
2774    txn_coords: HashMap<String, i32>,
2775    stats: Arc<crate::metrics::AdminTracker>,
2776}
2777
2778pub(crate) async fn fetch_client_instance_id(
2779    conn: &mut BrokerConn,
2780    version: i16,
2781    timeout: Duration,
2782    known: [u8; 16],
2783) -> Result<[u8; 16]> {
2784    let body = conn
2785        .roundtrip(
2786            GET_TELEMETRY_SUBSCRIPTIONS,
2787            version,
2788            |buf| encode_get_telemetry_subscriptions_request(buf, &known),
2789            timeout,
2790        )
2791        .await?;
2792    let resp = decode_get_telemetry_subscriptions_response(&mut body.clone())?;
2793    if resp.error_code != 0 {
2794        return Err(Error::broker(resp.error_code, "GetTelemetrySubscriptions"));
2795    }
2796    Ok(resp.client_instance_id)
2797}
2798
2799fn offset_fetch_topics_for_spec(
2800    spec: &ListConsumerGroupOffsetsSpec,
2801) -> Option<Vec<crate::protocol::group::OffsetFetchTopic>> {
2802    spec.partitions.as_ref().map(|ps| {
2803        let wanted: Vec<(String, i32)> = ps
2804            .iter()
2805            .map(|tp| (tp.topic.clone(), tp.partition))
2806            .collect();
2807        crate::group::group_offset_fetch_topics(&wanted)
2808    })
2809}
2810
2811fn listed_group_offsets(
2812    spec: &ListConsumerGroupOffsetsSpec,
2813    fetched: &[crate::protocol::group::FetchedOffsetTopic],
2814) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
2815    let map = crate::group::committed_offset_map(fetched)?;
2816    match &spec.partitions {
2817        None => Ok(map
2818            .into_iter()
2819            .map(|((topic, partition), md)| (crate::TopicPartition::new(topic, partition), md))
2820            .collect()),
2821        Some(ps) => Ok(ps
2822            .iter()
2823            .map(|tp| {
2824                let md = map
2825                    .get(&(tp.topic.clone(), tp.partition))
2826                    .cloned()
2827                    .unwrap_or_else(|| crate::OffsetAndMetadata::new(-1));
2828                (tp.clone(), md)
2829            })
2830            .collect()),
2831    }
2832}
2833
2834fn delete_records_topics(
2835    records: &[(crate::TopicPartition, i64)],
2836    idxs: &[usize],
2837) -> Vec<DeleteRecordsTopic> {
2838    let mut by_topic: HashMap<String, Vec<DeleteRecordsPartition>> = HashMap::new();
2839    let mut order: Vec<String> = Vec::new();
2840    for &i in idxs {
2841        let Some((tp, offset)) = records.get(i) else {
2842            continue;
2843        };
2844        match by_topic.entry(tp.topic.clone()) {
2845            std::collections::hash_map::Entry::Vacant(slot) => {
2846                order.push(tp.topic.clone());
2847                let _ = slot.insert(vec![DeleteRecordsPartition {
2848                    partition: tp.partition,
2849                    offset: *offset,
2850                }]);
2851            }
2852            std::collections::hash_map::Entry::Occupied(mut slot) => {
2853                slot.get_mut().push(DeleteRecordsPartition {
2854                    partition: tp.partition,
2855                    offset: *offset,
2856                });
2857            }
2858        }
2859    }
2860    order
2861        .into_iter()
2862        .filter_map(|topic| {
2863            by_topic
2864                .remove(&topic)
2865                .map(|partitions| DeleteRecordsTopic { topic, partitions })
2866        })
2867        .collect()
2868}
2869
2870fn describe_producers_topics(
2871    partitions: &[crate::TopicPartition],
2872    idxs: &[usize],
2873) -> Vec<DescribeProducersTopicRequest> {
2874    let mut by_topic: HashMap<String, Vec<i32>> = HashMap::new();
2875    let mut order: Vec<String> = Vec::new();
2876    for &i in idxs {
2877        let Some(tp) = partitions.get(i) else {
2878            continue;
2879        };
2880        match by_topic.entry(tp.topic.clone()) {
2881            std::collections::hash_map::Entry::Vacant(slot) => {
2882                order.push(tp.topic.clone());
2883                let _ = slot.insert(vec![tp.partition]);
2884            }
2885            std::collections::hash_map::Entry::Occupied(mut slot) => {
2886                slot.get_mut().push(tp.partition);
2887            }
2888        }
2889    }
2890    order
2891        .into_iter()
2892        .filter_map(|name| {
2893            by_topic
2894                .remove(&name)
2895                .map(|partition_indexes| DescribeProducersTopicRequest {
2896                    name,
2897                    partition_indexes,
2898                })
2899        })
2900        .collect()
2901}
2902
2903type DescribeProducersNodeOutcome = (Vec<(usize, DescribeProducersPartition)>, Vec<usize>);
2904
2905fn creatable_from_new(topics: &[NewTopic]) -> Vec<CreatableTopic> {
2906    topics
2907        .iter()
2908        .map(|t| CreatableTopic {
2909            name: t.name.clone(),
2910            num_partitions: t.num_partitions,
2911            replication_factor: t.replication_factor,
2912            assignments: t
2913                .assignments
2914                .iter()
2915                .map(|(partition_index, broker_ids)| ReplicaAssignment {
2916                    partition_index: *partition_index,
2917                    broker_ids: broker_ids.clone(),
2918                })
2919                .collect(),
2920            configs: t
2921                .configs
2922                .iter()
2923                .map(|(n, v)| TopicConfig {
2924                    name: n.clone(),
2925                    value: v.clone(),
2926                })
2927                .collect(),
2928        })
2929        .collect()
2930}
2931
2932fn delete_state_matches(topic: &DeleteTopicState, result: &TopicResult) -> bool {
2933    match &topic.name {
2934        Some(name) => name == &result.name,
2935        None => topic.topic_id != [0; 16] && topic.topic_id == result.topic_id,
2936    }
2937}
2938
2939fn partitions_from_new(topics: &[NewPartitions]) -> Vec<CreatePartitionsTopic> {
2940    topics
2941        .iter()
2942        .map(|t| CreatePartitionsTopic {
2943            name: t.name.clone(),
2944            count: t.total_count,
2945            assignments: t.assignments.clone(),
2946        })
2947        .collect()
2948}
2949
2950impl Admin {
2951    /// Connect with default config to one bootstrap server.
2952    pub async fn connect(bootstrap: impl Into<String>) -> Result<Self> {
2953        Self::new(AdminConfig::bootstrap([bootstrap.into()])).await
2954    }
2955
2956    /// Connect using `cfg`. Negotiates ApiVersions and optional SASL/TLS.
2957    ///
2958    /// DescribeTopicPartitions, ConsumerGroupDescribe, ShareGroupDescribe,
2959    /// the share-offset RPCs, AllocateProducerIds, ListConfigResources,
2960    /// GetTelemetrySubscriptions, PushTelemetry, AssignReplicasToDirs,
2961    /// UnregisterBroker, DescribeProducers, DescribeCluster,
2962    /// UpdateFeatures, DescribeClientQuotas, AlterClientQuotas,
2963    /// AlterUserScramCredentials, DescribeUserScramCredentials,
2964    /// AlterReplicaLogDirs, DescribeLogDirs, the delegation-token APIs,
2965    /// DescribeTransactions, ListTransactions, AlterPartitionReassignments,
2966    /// ListPartitionReassignments, OffsetDelete, and IncrementalAlterConfigs
2967    /// are optional at connect. Missing APIs fail on the method with
2968    /// [`Error::Unsupported`].
2969    pub async fn new(cfg: AdminConfig) -> Result<Self> {
2970        let mut cfg = cfg;
2971        cfg.bootstrap = crate::net::parse_and_validate_addresses(&cfg.bootstrap)?;
2972        let stats = Arc::new(crate::metrics::AdminTracker::default());
2973        let mut conn = BrokerConn::connect_tls_any(
2974            &cfg.bootstrap,
2975            &cfg.client_id,
2976            cfg.connect_timeout,
2977            cfg.tls.as_ref(),
2978        )
2979        .await?;
2980        conn.set_stats(Arc::clone(&stats));
2981        let resp =
2982            crate::protocol::api::negotiate_api_versions(&mut conn, cfg.request_timeout).await?;
2983        sasl::apply_api_keys(&mut conn, &resp.api_keys);
2984        let mut versions = HashMap::new();
2985        for api in resp.api_keys {
2986            let _prev = versions.insert(api.api_key, api);
2987        }
2988        sasl::authenticate(
2989            &mut conn,
2990            cfg.sasl_plain.as_ref(),
2991            cfg.sasl_scram.as_ref(),
2992            cfg.sasl_scram_sha512.as_ref(),
2993            cfg.sasl_oauthbearer.as_deref(),
2994            cfg.sasl_oauthbearer_oidc.as_ref(),
2995            cfg.request_timeout,
2996        )
2997        .await?;
2998        let create_version = versions
2999            .get(&CREATE_TOPICS)
3000            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 7))
3001            .ok_or_else(|| {
3002                Error::Unsupported("broker does not support CreateTopics v0-7".into())
3003            })?;
3004        let delete_version = versions
3005            .get(&DELETE_TOPICS)
3006            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 6))
3007            .ok_or_else(|| {
3008                Error::Unsupported("broker does not support DeleteTopics v0-6".into())
3009            })?;
3010        let describe_version = versions
3011            .get(&DESCRIBE_CONFIGS)
3012            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 4))
3013            .ok_or_else(|| {
3014                Error::Unsupported("broker does not support DescribeConfigs v0-4".into())
3015            })?;
3016        let partitions_version = versions
3017            .get(&CREATE_PARTITIONS)
3018            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 3))
3019            .ok_or_else(|| {
3020                Error::Unsupported("broker does not support CreatePartitions v0-3".into())
3021            })?;
3022        let alter_version = versions
3023            .get(&INCREMENTAL_ALTER_CONFIGS)
3024            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3025        let legacy_alter_version = versions
3026            .get(&ALTER_CONFIGS)
3027            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 2))
3028            .ok_or_else(|| {
3029                Error::Unsupported("broker does not support AlterConfigs v0-2".into())
3030            })?;
3031        let delete_records_version = versions
3032            .get(&DELETE_RECORDS)
3033            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 2))
3034            .ok_or_else(|| {
3035                Error::Unsupported("broker does not support DeleteRecords v0-2".into())
3036            })?;
3037        let describe_producers_version = versions
3038            .get(&DESCRIBE_PRODUCERS)
3039            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3040        let describe_cluster_version = versions
3041            .get(&DESCRIBE_CLUSTER)
3042            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 2));
3043        let create_acls_version = versions
3044            .get(&CREATE_ACLS)
3045            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 3))
3046            .ok_or_else(|| Error::Unsupported("broker does not support CreateAcls v0-3".into()))?;
3047        let describe_acls_version = versions
3048            .get(&DESCRIBE_ACLS)
3049            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 3))
3050            .ok_or_else(|| {
3051                Error::Unsupported("broker does not support DescribeAcls v0-3".into())
3052            })?;
3053        let delete_acls_version = versions
3054            .get(&DELETE_ACLS)
3055            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 3))
3056            .ok_or_else(|| Error::Unsupported("broker does not support DeleteAcls v0-3".into()))?;
3057        let metadata_version = versions
3058            .get(&METADATA)
3059            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 13))
3060            .ok_or_else(|| Error::Unsupported("broker does not support Metadata".into()))?;
3061        let find_coord_version = versions
3062            .get(&FIND_COORDINATOR)
3063            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 6))
3064            .ok_or_else(|| {
3065                Error::Unsupported("broker does not support FindCoordinator v1-6".into())
3066            })?;
3067        let offset_delete_version = versions
3068            .get(&OFFSET_DELETE)
3069            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3070        let reassign_version = versions
3071            .get(&ALTER_PARTITION_REASSIGNMENTS)
3072            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3073        let list_reassign_version = versions
3074            .get(&LIST_PARTITION_REASSIGNMENTS)
3075            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3076        let update_features_version = versions
3077            .get(&UPDATE_FEATURES)
3078            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 2));
3079        let alter_user_scram_version = versions
3080            .get(&ALTER_USER_SCRAM_CREDENTIALS)
3081            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3082        let describe_user_scram_version = versions
3083            .get(&DESCRIBE_USER_SCRAM_CREDENTIALS)
3084            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3085        let unregister_broker_version = versions
3086            .get(&UNREGISTER_BROKER)
3087            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3088        let describe_client_quotas_version = versions
3089            .get(&DESCRIBE_CLIENT_QUOTAS)
3090            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3091        let alter_client_quotas_version = versions
3092            .get(&ALTER_CLIENT_QUOTAS)
3093            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3094        let allocate_producer_ids_version = versions
3095            .get(&ALLOCATE_PRODUCER_IDS)
3096            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3097        let describe_transactions_version = versions
3098            .get(&DESCRIBE_TRANSACTIONS)
3099            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3100        let list_transactions_version = versions
3101            .get(&LIST_TRANSACTIONS)
3102            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3103        let consumer_group_describe_version = versions
3104            .get(&CONSUMER_GROUP_DESCRIBE)
3105            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3106        let describe_groups_version = versions
3107            .get(&DESCRIBE_GROUPS)
3108            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 6))
3109            .ok_or_else(|| {
3110                Error::Unsupported("broker does not support DescribeGroups v0-6".into())
3111            })?;
3112        let list_groups_version = versions
3113            .get(&LIST_GROUPS)
3114            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 5))
3115            .ok_or_else(|| Error::Unsupported("broker does not support ListGroups v0-5".into()))?;
3116        let delete_groups_version = versions
3117            .get(&DELETE_GROUPS)
3118            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 2))
3119            .ok_or_else(|| {
3120                Error::Unsupported("broker does not support DeleteGroups v0-2".into())
3121            })?;
3122        let share_group_describe_version = versions
3123            .get(&SHARE_GROUP_DESCRIBE)
3124            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3125        let describe_share_group_offsets_version = versions
3126            .get(&DESCRIBE_SHARE_GROUP_OFFSETS)
3127            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3128        let alter_share_group_offsets_version = versions
3129            .get(&ALTER_SHARE_GROUP_OFFSETS)
3130            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3131        let delete_share_group_offsets_version = versions
3132            .get(&DELETE_SHARE_GROUP_OFFSETS)
3133            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3134        let describe_topic_partitions_version = versions
3135            .get(&DESCRIBE_TOPIC_PARTITIONS)
3136            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3137        let list_config_resources_version = versions
3138            .get(&LIST_CONFIG_RESOURCES)
3139            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1));
3140        let get_telemetry_subscriptions_version = versions
3141            .get(&GET_TELEMETRY_SUBSCRIPTIONS)
3142            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3143        let push_telemetry_version = versions
3144            .get(&PUSH_TELEMETRY)
3145            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3146        let assign_replicas_to_dirs_version = versions
3147            .get(&ASSIGN_REPLICAS_TO_DIRS)
3148            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 0));
3149        let alter_replica_log_dirs_version = versions
3150            .get(&ALTER_REPLICA_LOG_DIRS)
3151            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 2));
3152        let describe_log_dirs_version = versions
3153            .get(&DESCRIBE_LOG_DIRS)
3154            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 4));
3155        let create_delegation_token_version = versions
3156            .get(&CREATE_DELEGATION_TOKEN)
3157            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 3));
3158        let renew_delegation_token_version = versions
3159            .get(&RENEW_DELEGATION_TOKEN)
3160            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 2));
3161        let expire_delegation_token_version = versions
3162            .get(&EXPIRE_DELEGATION_TOKEN)
3163            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 2));
3164        let describe_delegation_token_version = versions
3165            .get(&DESCRIBE_DELEGATION_TOKEN)
3166            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 3));
3167        Ok(Self {
3168            cfg,
3169            conn,
3170            versions,
3171            create_version,
3172            delete_version,
3173            describe_version,
3174            partitions_version,
3175            alter_version,
3176            legacy_alter_version,
3177            delete_records_version,
3178            describe_producers_version,
3179            describe_cluster_version,
3180            create_acls_version,
3181            describe_acls_version,
3182            delete_acls_version,
3183            metadata_version,
3184            find_coord_version,
3185            offset_delete_version,
3186            reassign_version,
3187            list_reassign_version,
3188            update_features_version,
3189            alter_user_scram_version,
3190            describe_user_scram_version,
3191            unregister_broker_version,
3192            describe_client_quotas_version,
3193            alter_client_quotas_version,
3194            allocate_producer_ids_version,
3195            describe_transactions_version,
3196            list_transactions_version,
3197            consumer_group_describe_version,
3198            describe_groups_version,
3199            list_groups_version,
3200            delete_groups_version,
3201            share_group_describe_version,
3202            describe_share_group_offsets_version,
3203            alter_share_group_offsets_version,
3204            delete_share_group_offsets_version,
3205            describe_topic_partitions_version,
3206            list_config_resources_version,
3207            get_telemetry_subscriptions_version,
3208            cached_client_instance_id: None,
3209            push_telemetry_version,
3210            assign_replicas_to_dirs_version,
3211            alter_replica_log_dirs_version,
3212            describe_log_dirs_version,
3213            create_delegation_token_version,
3214            renew_delegation_token_version,
3215            expire_delegation_token_version,
3216            describe_delegation_token_version,
3217            cluster: Cluster::default(),
3218            conns: HashMap::new(),
3219            reconnect_fails: HashMap::new(),
3220            group_coord: None,
3221            group_coords: HashMap::new(),
3222            txn_coord: None,
3223            txn_coords: HashMap::new(),
3224            stats,
3225        })
3226    }
3227
3228    /// Negotiated ApiVersions for this connection.
3229    #[must_use]
3230    pub fn versions(&self) -> &HashMap<i16, ApiVersion> {
3231        &self.versions
3232    }
3233
3234    /// Admin RPC counters and round-trip latency since connect.
3235    ///
3236    /// Java `Admin.metrics()` is Kafka's live metric map. This snapshot
3237    /// counts every Admin [`BrokerConn::roundtrip`]. `errors` is I/O,
3238    /// timeout, and protocol failure — not a decoded broker `error_code`
3239    /// on a valid body. `connections` is the bootstrap socket plus
3240    /// per-node sockets.
3241    #[must_use]
3242    pub fn metrics(&self) -> crate::AdminMetrics {
3243        self.stats.snapshot(1 + self.conns.len() as u64)
3244    }
3245
3246    /// Drop the admin connection.
3247    pub async fn close(self) -> Result<()> {
3248        Ok(())
3249    }
3250
3251    /// [`Self::close`] with a timeout (Java `close(Duration)`).
3252    ///
3253    /// Admin has no LeaveGroup; the duration is unused, same as
3254    /// [`crate::Consumer::close_timeout`].
3255    pub async fn close_timeout(self, _timeout: Duration) -> Result<()> {
3256        self.close().await
3257    }
3258
3259    /// Java `clientInstanceId` (KIP-714 GetTelemetrySubscriptions).
3260    ///
3261    /// Returns [`Uuid`] (Java `Uuid`). The first call sends a zero UUID;
3262    /// the broker assigns one. Later calls return the cached id without
3263    /// another round-trip. Waits up to [`AdminConfig::request_timeout`].
3264    /// For a one-shot timeout, use [`Self::client_instance_id_timeout`].
3265    pub async fn client_instance_id(&mut self) -> Result<Uuid> {
3266        let timeout = self.cfg.request_timeout;
3267        self.client_instance_id_timeout(timeout).await
3268    }
3269
3270    /// [`Self::client_instance_id`] with a one-shot timeout (Java
3271    /// `clientInstanceId(Duration)`).
3272    ///
3273    /// `timeout` is the GetTelemetrySubscriptions RPC deadline. Cached after
3274    /// the first successful call; later calls ignore `timeout`.
3275    pub async fn client_instance_id_timeout(&mut self, timeout: Duration) -> Result<Uuid> {
3276        if let Some(id) = self.cached_client_instance_id {
3277            return Ok(Uuid::from_bytes(id));
3278        }
3279        self.ensure_bootstrap().await?;
3280        let version = self.get_telemetry_subscriptions_version.ok_or_else(|| {
3281            Error::Unsupported("broker does not support GetTelemetrySubscriptions".into())
3282        })?;
3283        let id = fetch_client_instance_id(&mut self.conn, version, timeout, [0; 16]).await?;
3284        self.cached_client_instance_id = Some(id);
3285        Ok(Uuid::from_bytes(id))
3286    }
3287
3288    /// Create topics (`CreateTopics`).
3289    ///
3290    /// Negotiates v0–v7 (v5+ flexible; v5 returns NumPartitions /
3291    /// ReplicationFactor / Configs, KIP-525; v7 TopicId, KIP-516).
3292    /// Kafka 4.0 `validVersions` is `2-7`. v8+ is not spoken.
3293    /// `timeout_ms` is CreateTopics TimeoutMs. The RPC deadline is
3294    /// [`AdminConfig::request_timeout`]. For a one-shot timeout that
3295    /// drives both the RPC deadline and TimeoutMs, use
3296    /// [`Self::create_topics_timeout`].
3297    /// Java `CreateTopicsOptions.retryOnQuotaViolation` defaults to
3298    /// `true` (KIP-599); use [`Self::create_topics_with_quota_retry`]
3299    /// to disable.
3300    pub async fn create_topics(
3301        &mut self,
3302        topics: &[NewTopic],
3303        timeout_ms: i32,
3304        validate_only: bool,
3305    ) -> Result<Vec<TopicResult>> {
3306        let timeout = self.cfg.request_timeout;
3307        self.create_topics_with(topics, timeout_ms, validate_only, timeout, true)
3308            .await
3309    }
3310
3311    /// [`Self::create_topics`] with a one-shot timeout (Java
3312    /// `CreateTopicsOptions.timeoutMs`).
3313    ///
3314    /// `timeout` is the RPC deadline and CreateTopics TimeoutMs.
3315    pub async fn create_topics_timeout(
3316        &mut self,
3317        topics: &[NewTopic],
3318        timeout: Duration,
3319        validate_only: bool,
3320    ) -> Result<Vec<TopicResult>> {
3321        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3322        self.create_topics_with(topics, timeout_ms, validate_only, timeout, true)
3323            .await
3324    }
3325
3326    /// [`Self::create_topics`] with Java `CreateTopicsOptions.retryOnQuotaViolation`.
3327    ///
3328    /// [`Self::create_topics`] defaults this to `true` (KIP-599). When
3329    /// true, topics that return `THROTTLING_QUOTA_EXCEEDED` (89) are
3330    /// retried alone until the RPC deadline.
3331    pub async fn create_topics_with_quota_retry(
3332        &mut self,
3333        topics: &[NewTopic],
3334        timeout_ms: i32,
3335        validate_only: bool,
3336        retry_on_quota_violation: bool,
3337    ) -> Result<Vec<TopicResult>> {
3338        let timeout = self.cfg.request_timeout;
3339        self.create_topics_with(
3340            topics,
3341            timeout_ms,
3342            validate_only,
3343            timeout,
3344            retry_on_quota_violation,
3345        )
3346        .await
3347    }
3348
3349    /// [`Self::create_topics_with_quota_retry`] with a one-shot timeout
3350    /// (Java `CreateTopicsOptions.timeoutMs` + `retryOnQuotaViolation`).
3351    ///
3352    /// `timeout` is the RPC deadline and CreateTopics TimeoutMs.
3353    pub async fn create_topics_timeout_with_quota_retry(
3354        &mut self,
3355        topics: &[NewTopic],
3356        timeout: Duration,
3357        validate_only: bool,
3358        retry_on_quota_violation: bool,
3359    ) -> Result<Vec<TopicResult>> {
3360        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3361        self.create_topics_with(
3362            topics,
3363            timeout_ms,
3364            validate_only,
3365            timeout,
3366            retry_on_quota_violation,
3367        )
3368        .await
3369    }
3370
3371    async fn create_topics_with(
3372        &mut self,
3373        topics: &[NewTopic],
3374        timeout_ms: i32,
3375        validate_only: bool,
3376        timeout: Duration,
3377        retry_on_quota_violation: bool,
3378    ) -> Result<Vec<TopicResult>> {
3379        let mut pending: Vec<NewTopic> = topics.to_vec();
3380        let mut finished: HashMap<String, TopicResult> = HashMap::new();
3381        let version = self.create_version;
3382        let deadline = Instant::now() + timeout;
3383        let mut attempt = 0u32;
3384        loop {
3385            let req = CreateTopicsRequest {
3386                topics: creatable_from_new(&pending),
3387                timeout_ms,
3388                validate_only,
3389            };
3390            if self.cluster.controller().is_err() {
3391                self.refresh_metadata(None).await?;
3392            }
3393            let node = self.cluster.controller()?;
3394            self.connect_node(node).await?;
3395            let body = {
3396                let conn = self
3397                    .conns
3398                    .get_mut(&node)
3399                    .ok_or_else(|| Error::protocol("missing create_topics conn"))?;
3400                conn.roundtrip(
3401                    CREATE_TOPICS,
3402                    version,
3403                    |buf| encode_create_topics_request(buf, version, &req),
3404                    timeout,
3405                )
3406                .await
3407            };
3408            let body = match body {
3409                Ok(b) => b,
3410                Err(e) if e.is_retriable() => {
3411                    let _ = self.conns.remove(&node);
3412                    self.cluster.invalidate_controller();
3413                    self.wait_retry(&mut attempt, deadline).await?;
3414                    continue;
3415                }
3416                Err(e) => return Err(e),
3417            };
3418            let (results, ..) = decode_create_topics_response(&mut body.clone(), version)?;
3419            if results
3420                .iter()
3421                .any(|r| r.error_code == error::NOT_CONTROLLER)
3422            {
3423                // NOT_CONTROLLER (41): Metadata, then the new controller.
3424                self.cluster.invalidate_controller();
3425                let _ = self.conns.remove(&node);
3426                self.wait_retry(&mut attempt, deadline).await?;
3427                self.refresh_metadata(None).await?;
3428                continue;
3429            }
3430            let mut next_pending = Vec::new();
3431            for r in results {
3432                if retry_on_quota_violation && r.error_code == error::THROTTLING_QUOTA_EXCEEDED {
3433                    if let Some(t) = pending.iter().find(|t| t.name == r.name) {
3434                        next_pending.push(t.clone());
3435                    }
3436                } else {
3437                    let name = r.name.clone();
3438                    let _prev = finished.insert(name, r);
3439                }
3440            }
3441            if next_pending.is_empty() {
3442                let mut out = Vec::with_capacity(topics.len());
3443                for t in topics {
3444                    let r = finished.remove(&t.name).ok_or_else(|| {
3445                        Error::protocol(format!("missing create_topics result for {}", t.name))
3446                    })?;
3447                    out.push(r);
3448                }
3449                return Ok(out);
3450            }
3451            pending = next_pending;
3452            self.wait_retry(&mut attempt, deadline).await?;
3453        }
3454    }
3455
3456    /// Delete topics (`DeleteTopics`).
3457    ///
3458    /// Negotiates v0–v6 (v4+ flexible; v5 ErrorMessage, KIP-599; v6
3459    /// Topics of Name + TopicId, KIP-516). Name-based deletes send a
3460    /// zero UUID at v6 (Java `deleteTopics(Collection<String>)`).
3461    /// For TopicId deletes (Java `TopicCollection.ofTopicIds`), use
3462    /// [`Self::delete_topics_by_id`].
3463    /// Kafka 4.0 `validVersions` is `1-6`. v7+ is not spoken.
3464    ///
3465    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
3466    /// Metadata and retries on the new controller.
3467    /// `timeout_ms` is DeleteTopics TimeoutMs. The RPC deadline is
3468    /// [`AdminConfig::request_timeout`]. For a one-shot timeout that
3469    /// drives both the RPC deadline and TimeoutMs, use
3470    /// [`Self::delete_topics_timeout`].
3471    /// Java `DeleteTopicsOptions.retryOnQuotaViolation` defaults to
3472    /// `true` (KIP-599); use [`Self::delete_topics_with_quota_retry`]
3473    /// to disable.
3474    pub async fn delete_topics(
3475        &mut self,
3476        names: &[impl AsRef<str>],
3477        timeout_ms: i32,
3478    ) -> Result<Vec<TopicResult>> {
3479        let names: Vec<String> = names.iter().map(|n| n.as_ref().to_string()).collect();
3480        let timeout = self.cfg.request_timeout;
3481        self.delete_topics_with(names, timeout_ms, timeout, true)
3482            .await
3483    }
3484
3485    /// [`Self::delete_topics`] with a one-shot timeout (Java
3486    /// `DeleteTopicsOptions.timeoutMs`).
3487    ///
3488    /// `timeout` is the RPC deadline and DeleteTopics TimeoutMs.
3489    pub async fn delete_topics_timeout(
3490        &mut self,
3491        names: &[impl AsRef<str>],
3492        timeout: Duration,
3493    ) -> Result<Vec<TopicResult>> {
3494        let names: Vec<String> = names.iter().map(|n| n.as_ref().to_string()).collect();
3495        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3496        self.delete_topics_with(names, timeout_ms, timeout, true)
3497            .await
3498    }
3499
3500    /// [`Self::delete_topics`] with Java `DeleteTopicsOptions.retryOnQuotaViolation`.
3501    ///
3502    /// [`Self::delete_topics`] defaults this to `true` (KIP-599). When
3503    /// true, topics that return `THROTTLING_QUOTA_EXCEEDED` (89) are
3504    /// retried alone until the RPC deadline.
3505    pub async fn delete_topics_with_quota_retry(
3506        &mut self,
3507        names: &[impl AsRef<str>],
3508        timeout_ms: i32,
3509        retry_on_quota_violation: bool,
3510    ) -> Result<Vec<TopicResult>> {
3511        let names: Vec<String> = names.iter().map(|n| n.as_ref().to_string()).collect();
3512        let timeout = self.cfg.request_timeout;
3513        self.delete_topics_with(names, timeout_ms, timeout, retry_on_quota_violation)
3514            .await
3515    }
3516
3517    /// [`Self::delete_topics_with_quota_retry`] with a one-shot timeout
3518    /// (Java `DeleteTopicsOptions.timeoutMs` + `retryOnQuotaViolation`).
3519    ///
3520    /// `timeout` is the RPC deadline and DeleteTopics TimeoutMs.
3521    pub async fn delete_topics_timeout_with_quota_retry(
3522        &mut self,
3523        names: &[impl AsRef<str>],
3524        timeout: Duration,
3525        retry_on_quota_violation: bool,
3526    ) -> Result<Vec<TopicResult>> {
3527        let names: Vec<String> = names.iter().map(|n| n.as_ref().to_string()).collect();
3528        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3529        self.delete_topics_with(names, timeout_ms, timeout, retry_on_quota_violation)
3530            .await
3531    }
3532
3533    /// Delete topics by TopicId (Java `deleteTopics(TopicCollection.ofTopicIds)`).
3534    ///
3535    /// DeleteTopics v6 sends Topics of null Name + TopicId. Brokers that
3536    /// only speak v0–v5 return [`Error::Unsupported`]. Empty `ids` is a
3537    /// no-op. Unknown ids return `UNKNOWN_TOPIC_ID` (100) per topic.
3538    /// `timeout_ms` is DeleteTopics TimeoutMs. The RPC deadline is
3539    /// [`AdminConfig::request_timeout`]. See
3540    /// [`Self::delete_topics_by_id_timeout`].
3541    /// Java `DeleteTopicsOptions.retryOnQuotaViolation` defaults to
3542    /// `true` (KIP-599); use [`Self::delete_topics_by_id_with_quota_retry`]
3543    /// to disable.
3544    pub async fn delete_topics_by_id(
3545        &mut self,
3546        ids: &[[u8; 16]],
3547        timeout_ms: i32,
3548    ) -> Result<Vec<TopicResult>> {
3549        let timeout = self.cfg.request_timeout;
3550        self.delete_topics_by_id_with(ids, timeout_ms, timeout, true)
3551            .await
3552    }
3553
3554    /// [`Self::delete_topics_by_id`] with a one-shot timeout (Java
3555    /// `DeleteTopicsOptions.timeoutMs`).
3556    pub async fn delete_topics_by_id_timeout(
3557        &mut self,
3558        ids: &[[u8; 16]],
3559        timeout: Duration,
3560    ) -> Result<Vec<TopicResult>> {
3561        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3562        self.delete_topics_by_id_with(ids, timeout_ms, timeout, true)
3563            .await
3564    }
3565
3566    /// [`Self::delete_topics_by_id`] with Java
3567    /// `DeleteTopicsOptions.retryOnQuotaViolation`.
3568    ///
3569    /// [`Self::delete_topics_by_id`] defaults this to `true` (KIP-599).
3570    /// When true, topics that return `THROTTLING_QUOTA_EXCEEDED` (89)
3571    /// are retried alone until the RPC deadline.
3572    pub async fn delete_topics_by_id_with_quota_retry(
3573        &mut self,
3574        ids: &[[u8; 16]],
3575        timeout_ms: i32,
3576        retry_on_quota_violation: bool,
3577    ) -> Result<Vec<TopicResult>> {
3578        let timeout = self.cfg.request_timeout;
3579        self.delete_topics_by_id_with(ids, timeout_ms, timeout, retry_on_quota_violation)
3580            .await
3581    }
3582
3583    /// [`Self::delete_topics_by_id_with_quota_retry`] with a one-shot
3584    /// timeout (Java `DeleteTopicsOptions.timeoutMs` +
3585    /// `retryOnQuotaViolation`).
3586    ///
3587    /// `timeout` is the RPC deadline and DeleteTopics TimeoutMs.
3588    pub async fn delete_topics_by_id_timeout_with_quota_retry(
3589        &mut self,
3590        ids: &[[u8; 16]],
3591        timeout: Duration,
3592        retry_on_quota_violation: bool,
3593    ) -> Result<Vec<TopicResult>> {
3594        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3595        self.delete_topics_by_id_with(ids, timeout_ms, timeout, retry_on_quota_violation)
3596            .await
3597    }
3598
3599    /// Java `deleteTopics(TopicCollection)`.
3600    ///
3601    /// [`TopicCollection::Names`] is [`Self::delete_topics`].
3602    /// [`TopicCollection::Ids`] is [`Self::delete_topics_by_id`].
3603    /// Empty collections are a no-op. `timeout_ms` is DeleteTopics
3604    /// TimeoutMs. The RPC deadline is [`AdminConfig::request_timeout`].
3605    /// Java `DeleteTopicsOptions.retryOnQuotaViolation` defaults to
3606    /// `true` (KIP-599); use [`Self::delete_topics_for_with_quota_retry`]
3607    /// to disable.
3608    pub async fn delete_topics_for(
3609        &mut self,
3610        topics: &TopicCollection,
3611        timeout_ms: i32,
3612    ) -> Result<Vec<TopicResult>> {
3613        let timeout = self.cfg.request_timeout;
3614        self.delete_topics_for_inner(topics, timeout_ms, timeout, true)
3615            .await
3616    }
3617
3618    /// [`Self::delete_topics_for`] with a one-shot timeout (Java
3619    /// `DeleteTopicsOptions.timeoutMs`).
3620    ///
3621    /// `timeout` is the RPC deadline and DeleteTopics TimeoutMs.
3622    pub async fn delete_topics_for_timeout(
3623        &mut self,
3624        topics: &TopicCollection,
3625        timeout: Duration,
3626    ) -> Result<Vec<TopicResult>> {
3627        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3628        self.delete_topics_for_inner(topics, timeout_ms, timeout, true)
3629            .await
3630    }
3631
3632    /// [`Self::delete_topics_for`] with Java
3633    /// `DeleteTopicsOptions.retryOnQuotaViolation`.
3634    pub async fn delete_topics_for_with_quota_retry(
3635        &mut self,
3636        topics: &TopicCollection,
3637        timeout_ms: i32,
3638        retry_on_quota_violation: bool,
3639    ) -> Result<Vec<TopicResult>> {
3640        let timeout = self.cfg.request_timeout;
3641        self.delete_topics_for_inner(topics, timeout_ms, timeout, retry_on_quota_violation)
3642            .await
3643    }
3644
3645    /// [`Self::delete_topics_for_with_quota_retry`] with a one-shot
3646    /// timeout (Java `DeleteTopicsOptions.timeoutMs` +
3647    /// `retryOnQuotaViolation`).
3648    pub async fn delete_topics_for_timeout_with_quota_retry(
3649        &mut self,
3650        topics: &TopicCollection,
3651        timeout: Duration,
3652        retry_on_quota_violation: bool,
3653    ) -> Result<Vec<TopicResult>> {
3654        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
3655        self.delete_topics_for_inner(topics, timeout_ms, timeout, retry_on_quota_violation)
3656            .await
3657    }
3658
3659    async fn delete_topics_for_inner(
3660        &mut self,
3661        topics: &TopicCollection,
3662        timeout_ms: i32,
3663        timeout: Duration,
3664        retry_on_quota_violation: bool,
3665    ) -> Result<Vec<TopicResult>> {
3666        match topics {
3667            TopicCollection::Names(names) => {
3668                self.delete_topics_with(
3669                    names.clone(),
3670                    timeout_ms,
3671                    timeout,
3672                    retry_on_quota_violation,
3673                )
3674                .await
3675            }
3676            TopicCollection::Ids(ids) => {
3677                let ids: Vec<[u8; 16]> = ids.iter().copied().map(Uuid::to_bytes).collect();
3678                self.delete_topics_by_id_with(&ids, timeout_ms, timeout, retry_on_quota_violation)
3679                    .await
3680            }
3681        }
3682    }
3683
3684    async fn delete_topics_by_id_with(
3685        &mut self,
3686        ids: &[[u8; 16]],
3687        timeout_ms: i32,
3688        timeout: Duration,
3689        retry_on_quota_violation: bool,
3690    ) -> Result<Vec<TopicResult>> {
3691        if ids.is_empty() {
3692            return Ok(Vec::new());
3693        }
3694        if self.delete_version < 6 {
3695            return Err(Error::Unsupported(
3696                "broker does not support DeleteTopics v6 topic IDs".into(),
3697            ));
3698        }
3699        let topics: Vec<DeleteTopicState> =
3700            ids.iter().copied().map(DeleteTopicState::by_id).collect();
3701        self.delete_topics_states_with(topics, timeout_ms, timeout, retry_on_quota_violation)
3702            .await
3703    }
3704
3705    async fn delete_topics_with(
3706        &mut self,
3707        names: Vec<String>,
3708        timeout_ms: i32,
3709        timeout: Duration,
3710        retry_on_quota_violation: bool,
3711    ) -> Result<Vec<TopicResult>> {
3712        let topics: Vec<DeleteTopicState> =
3713            names.into_iter().map(DeleteTopicState::by_name).collect();
3714        self.delete_topics_states_with(topics, timeout_ms, timeout, retry_on_quota_violation)
3715            .await
3716    }
3717
3718    async fn delete_topics_states_with(
3719        &mut self,
3720        topics: Vec<DeleteTopicState>,
3721        timeout_ms: i32,
3722        timeout: Duration,
3723        retry_on_quota_violation: bool,
3724    ) -> Result<Vec<TopicResult>> {
3725        let original = topics;
3726        let mut pending = original.clone();
3727        let mut finished_names: HashMap<String, TopicResult> = HashMap::new();
3728        let mut finished_ids: HashMap<[u8; 16], TopicResult> = HashMap::new();
3729        let version = self.delete_version;
3730        let deadline = Instant::now() + timeout;
3731        let mut attempt = 0u32;
3732        loop {
3733            if self.cluster.controller().is_err() {
3734                self.refresh_metadata(None).await?;
3735            }
3736            let node = self.cluster.controller()?;
3737            self.connect_node(node).await?;
3738            let body = {
3739                let conn = self
3740                    .conns
3741                    .get_mut(&node)
3742                    .ok_or_else(|| Error::protocol("missing delete_topics conn"))?;
3743                conn.roundtrip(
3744                    DELETE_TOPICS,
3745                    version,
3746                    |buf| encode_delete_topics_states_request(buf, version, &pending, timeout_ms),
3747                    timeout,
3748                )
3749                .await
3750            };
3751            let body = match body {
3752                Ok(b) => b,
3753                Err(e) if e.is_retriable() => {
3754                    let _ = self.conns.remove(&node);
3755                    self.cluster.invalidate_controller();
3756                    self.wait_retry(&mut attempt, deadline).await?;
3757                    continue;
3758                }
3759                Err(e) => return Err(e),
3760            };
3761            let (results, ..) = decode_delete_topics_response(&mut body.clone(), version)?;
3762            if results
3763                .iter()
3764                .any(|r| r.error_code == error::NOT_CONTROLLER)
3765            {
3766                // NOT_CONTROLLER (41): Metadata, then the new controller.
3767                self.cluster.invalidate_controller();
3768                let _ = self.conns.remove(&node);
3769                self.wait_retry(&mut attempt, deadline).await?;
3770                self.refresh_metadata(None).await?;
3771                continue;
3772            }
3773            let mut next_pending = Vec::new();
3774            for r in results {
3775                let matched = pending.iter().find(|t| delete_state_matches(t, &r));
3776                if retry_on_quota_violation && r.error_code == error::THROTTLING_QUOTA_EXCEEDED {
3777                    if let Some(t) = matched {
3778                        next_pending.push(t.clone());
3779                    }
3780                } else if let Some(t) = matched {
3781                    if let Some(n) = &t.name {
3782                        let _prev = finished_names.insert(n.clone(), r);
3783                    } else {
3784                        let _prev = finished_ids.insert(t.topic_id, r);
3785                    }
3786                }
3787            }
3788            if next_pending.is_empty() {
3789                let mut out = Vec::with_capacity(original.len());
3790                for t in &original {
3791                    let r = if let Some(n) = &t.name {
3792                        finished_names.remove(n)
3793                    } else {
3794                        finished_ids.remove(&t.topic_id)
3795                    }
3796                    .ok_or_else(|| {
3797                        Error::protocol(format!(
3798                            "missing delete_topics result for {}",
3799                            t.name.as_deref().unwrap_or("topic id")
3800                        ))
3801                    })?;
3802                    out.push(r);
3803                }
3804                return Ok(out);
3805            }
3806            pending = next_pending;
3807            self.wait_retry(&mut attempt, deadline).await?;
3808        }
3809    }
3810
3811    /// Cluster topics (Java `Admin.listTopics`).
3812    ///
3813    /// Sends Metadata with a null topic array (all topics) on the
3814    /// bootstrap connection. Includes internal topics;
3815    /// [`TopicListing::is_internal`] is Metadata `IsInternal`.
3816    /// Metadata has no TimeoutMs; the RPC deadline is
3817    /// [`AdminConfig::request_timeout`]. For Java
3818    /// `ListTopicsOptions.listInternal`, use [`Self::list_topics_with`].
3819    /// For a one-shot RPC deadline, use [`Self::list_topics_timeout`].
3820    pub async fn list_topics(&mut self) -> Result<Vec<TopicListing>> {
3821        let timeout = self.cfg.request_timeout;
3822        self.list_topics_with_timeout(true, timeout).await
3823    }
3824
3825    /// [`Self::list_topics`] filtered by Java `ListTopicsOptions.listInternal`.
3826    ///
3827    /// Metadata still returns every topic; this crate drops rows with
3828    /// `IsInternal` when `list_internal` is false. [`Self::list_topics`]
3829    /// keeps internals (Java's default `listInternal` is false).
3830    pub async fn list_topics_with(&mut self, list_internal: bool) -> Result<Vec<TopicListing>> {
3831        let timeout = self.cfg.request_timeout;
3832        self.list_topics_with_timeout(list_internal, timeout).await
3833    }
3834
3835    /// [`Self::list_topics`] with a one-shot RPC deadline (Java
3836    /// `ListTopicsOptions.timeoutMs`).
3837    ///
3838    /// Metadata has no TimeoutMs. Includes internal topics, matching
3839    /// [`Self::list_topics`].
3840    pub async fn list_topics_timeout(&mut self, timeout: Duration) -> Result<Vec<TopicListing>> {
3841        self.list_topics_with_timeout(true, timeout).await
3842    }
3843
3844    /// [`Self::list_topics`] with Java `ListTopicsOptions.listInternal` and
3845    /// `timeoutMs`.
3846    ///
3847    /// Metadata has no TimeoutMs; `timeout` is the RPC deadline.
3848    pub async fn list_topics_with_timeout(
3849        &mut self,
3850        list_internal: bool,
3851        timeout: Duration,
3852    ) -> Result<Vec<TopicListing>> {
3853        let md = self
3854            .fetch_metadata_request_with(None, false, timeout)
3855            .await?;
3856        Ok(topic_listings_from(&md, list_internal))
3857    }
3858
3859    /// Topic partition layouts (Java `Admin.describeTopics`).
3860    ///
3861    /// Sends DescribeTopicPartitions (api 75, KIP-966) when the broker
3862    /// advertises it (Java `TopicCollection.ofTopicNames`). Otherwise
3863    /// Metadata for the named topics (Java fallback before KIP-966).
3864    /// `ResponsePartitionLimit` is 2000; a `NextCursor` is followed until
3865    /// the broker is done. Empty input is a no-op (no RPC). Per-topic
3866    /// errors live on [`TopicDescription::error_code`];
3867    /// [`TopicDescription::partitions`] is filled only when that code is `0`.
3868    /// DescribeTopicPartitions has no IncludeTopicAuthorizedOperations
3869    /// request flag; [`TopicDescription::authorized_operations`] is
3870    /// [`AUTHORIZED_OPERATIONS_OMITTED`] unless
3871    /// [`Self::describe_topics_with`] is used. For TopicId describes
3872    /// (Java `TopicCollection.ofTopicIds`), use [`Self::describe_topics_by_id`].
3873    /// DescribeTopicPartitions and Metadata have no TimeoutMs; the RPC
3874    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
3875    /// deadline, use [`Self::describe_topics_timeout`].
3876    pub async fn describe_topics(
3877        &mut self,
3878        topics: impl IntoIterator<Item = impl AsRef<str>>,
3879    ) -> Result<Vec<TopicDescription>> {
3880        self.describe_topics_with(topics, false).await
3881    }
3882
3883    /// [`Self::describe_topics`] with a one-shot RPC deadline (Java
3884    /// `DescribeTopicsOptions.timeoutMs`).
3885    pub async fn describe_topics_timeout(
3886        &mut self,
3887        topics: impl IntoIterator<Item = impl AsRef<str>>,
3888        timeout: Duration,
3889    ) -> Result<Vec<TopicDescription>> {
3890        self.describe_topics_with_timeout(topics, false, timeout)
3891            .await
3892    }
3893
3894    /// [`Self::describe_topics`] with Java
3895    /// `DescribeTopicsOptions.includeAuthorizedOperations`.
3896    ///
3897    /// DescribeTopicPartitions always returns TopicAuthorizedOperations.
3898    /// When `include_authorized_operations` is false, this crate stores
3899    /// [`AUTHORIZED_OPERATIONS_OMITTED`] (Java Metadata path omits the
3900    /// request flag; DTP has no equivalent field). The Metadata fallback
3901    /// sends IncludeTopicAuthorizedOperations when the flag is true.
3902    pub async fn describe_topics_with(
3903        &mut self,
3904        topics: impl IntoIterator<Item = impl AsRef<str>>,
3905        include_authorized_operations: bool,
3906    ) -> Result<Vec<TopicDescription>> {
3907        let timeout = self.cfg.request_timeout;
3908        self.describe_topics_with_timeout(topics, include_authorized_operations, timeout)
3909            .await
3910    }
3911
3912    /// [`Self::describe_topics_with`] with Java
3913    /// `DescribeTopicsOptions.timeoutMs`.
3914    ///
3915    /// DescribeTopicPartitions and Metadata have no TimeoutMs; `timeout`
3916    /// is the RPC deadline (and the DTP NextCursor loop budget).
3917    /// `ResponsePartitionLimit` is 2000 (Java
3918    /// `DescribeTopicsOptions.partitionSizeLimitPerResponse` default).
3919    /// For a custom limit, use
3920    /// [`Self::describe_topics_with_partition_limit_timeout`].
3921    pub async fn describe_topics_with_timeout(
3922        &mut self,
3923        topics: impl IntoIterator<Item = impl AsRef<str>>,
3924        include_authorized_operations: bool,
3925        timeout: Duration,
3926    ) -> Result<Vec<TopicDescription>> {
3927        self.describe_topics_with_partition_limit_timeout(
3928            topics,
3929            include_authorized_operations,
3930            DESCRIBE_TOPIC_PARTITIONS_LIMIT,
3931            timeout,
3932        )
3933        .await
3934    }
3935
3936    /// [`Self::describe_topics_with`] with Java
3937    /// `DescribeTopicsOptions.partitionSizeLimitPerResponse`.
3938    ///
3939    /// `partition_size_limit` is DescribeTopicPartitions
3940    /// `ResponsePartitionLimit` (Java default 2000). The broker may cap
3941    /// it at `max.request.partition.size.limit`. Ignored on the Metadata
3942    /// fallback when api 75 is not advertised. Topic-id describes
3943    /// ([`Self::describe_topics_by_id`]) stay on Metadata (KAFKA-19628).
3944    pub async fn describe_topics_with_partition_limit(
3945        &mut self,
3946        topics: impl IntoIterator<Item = impl AsRef<str>>,
3947        include_authorized_operations: bool,
3948        partition_size_limit: i32,
3949    ) -> Result<Vec<TopicDescription>> {
3950        let timeout = self.cfg.request_timeout;
3951        self.describe_topics_with_partition_limit_timeout(
3952            topics,
3953            include_authorized_operations,
3954            partition_size_limit,
3955            timeout,
3956        )
3957        .await
3958    }
3959
3960    /// [`Self::describe_topics_with_partition_limit`] with Java
3961    /// `DescribeTopicsOptions.timeoutMs`.
3962    ///
3963    /// DescribeTopicPartitions and Metadata have no TimeoutMs; `timeout`
3964    /// is the RPC deadline (and the DTP NextCursor loop budget).
3965    pub async fn describe_topics_with_partition_limit_timeout(
3966        &mut self,
3967        topics: impl IntoIterator<Item = impl AsRef<str>>,
3968        include_authorized_operations: bool,
3969        partition_size_limit: i32,
3970        timeout: Duration,
3971    ) -> Result<Vec<TopicDescription>> {
3972        let names: Vec<String> = topics.into_iter().map(|s| s.as_ref().to_string()).collect();
3973        if names.is_empty() {
3974            return Ok(Vec::new());
3975        }
3976        if self.describe_topic_partitions_version.is_some() {
3977            self.describe_topics_dtp(
3978                &names,
3979                include_authorized_operations,
3980                partition_size_limit,
3981                timeout,
3982            )
3983            .await
3984        } else {
3985            self.describe_topics_metadata(&names, include_authorized_operations, timeout)
3986                .await
3987        }
3988    }
3989
3990    /// Describe topics by TopicId (Java `describeTopics(TopicCollection.ofTopicIds)`).
3991    ///
3992    /// Metadata v12+ sends Topics of null Name + TopicId (Java
3993    /// `MetadataRequest.Builder`; v10 and v11 must not send TopicId).
3994    /// `AllowAutoTopicCreation` is false. Brokers that only speak v1–v11
3995    /// return [`Error::Unsupported`]. Empty `ids` is a no-op. Unknown
3996    /// ids return `UNKNOWN_TOPIC_ID` (100) per topic with an empty name.
3997    /// See [`Self::describe_topics_by_id_with`]. Metadata has no TimeoutMs;
3998    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
3999    /// one-shot deadline, use [`Self::describe_topics_by_id_timeout`].
4000    pub async fn describe_topics_by_id(
4001        &mut self,
4002        ids: &[[u8; 16]],
4003    ) -> Result<Vec<TopicDescription>> {
4004        self.describe_topics_by_id_with(ids, false).await
4005    }
4006
4007    /// [`Self::describe_topics_by_id`] with Java
4008    /// `DescribeTopicsOptions.includeAuthorizedOperations`.
4009    ///
4010    /// Metadata v12+ sends IncludeTopicAuthorizedOperations (the flag
4011    /// exists from v8; TopicId on the request requires v12). Metadata has no TimeoutMs;
4012    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
4013    /// one-shot deadline, use [`Self::describe_topics_by_id_with_timeout`].
4014    pub async fn describe_topics_by_id_with(
4015        &mut self,
4016        ids: &[[u8; 16]],
4017        include_authorized_operations: bool,
4018    ) -> Result<Vec<TopicDescription>> {
4019        let timeout = self.cfg.request_timeout;
4020        self.describe_topics_by_id_with_timeout(ids, include_authorized_operations, timeout)
4021            .await
4022    }
4023
4024    /// [`Self::describe_topics_by_id`] with a one-shot RPC deadline (Java
4025    /// `DescribeTopicsOptions.timeoutMs`).
4026    pub async fn describe_topics_by_id_timeout(
4027        &mut self,
4028        ids: &[[u8; 16]],
4029        timeout: Duration,
4030    ) -> Result<Vec<TopicDescription>> {
4031        self.describe_topics_by_id_with_timeout(ids, false, timeout)
4032            .await
4033    }
4034
4035    /// [`Self::describe_topics_by_id_with`] with Java
4036    /// `DescribeTopicsOptions.timeoutMs`.
4037    ///
4038    /// Metadata has no TimeoutMs; `timeout` is the RPC deadline.
4039    pub async fn describe_topics_by_id_with_timeout(
4040        &mut self,
4041        ids: &[[u8; 16]],
4042        include_authorized_operations: bool,
4043        timeout: Duration,
4044    ) -> Result<Vec<TopicDescription>> {
4045        if ids.is_empty() {
4046            return Ok(Vec::new());
4047        }
4048        if self.metadata_version < 12 {
4049            return Err(Error::Unsupported(
4050                "broker does not support Metadata v12 topic IDs".into(),
4051            ));
4052        }
4053        let topics = MetadataRequestTopic::convert_from_ids(ids.iter().copied());
4054        let md = self
4055            .fetch_metadata_request_with(Some(&topics), include_authorized_operations, timeout)
4056            .await?;
4057        Ok(topic_descriptions_including_unnamed(&md))
4058    }
4059
4060    /// Java `describeTopics(TopicCollection)`.
4061    ///
4062    /// [`TopicCollection::Names`] is [`Self::describe_topics`]
4063    /// (DescribeTopicPartitions, Metadata fallback).
4064    /// [`TopicCollection::Ids`] is [`Self::describe_topics_by_id`]
4065    /// (Metadata v12+). Empty collections are a no-op.
4066    /// DescribeTopicPartitions and Metadata have no TimeoutMs; the RPC
4067    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
4068    /// deadline, use [`Self::describe_topics_for_timeout`].
4069    pub async fn describe_topics_for(
4070        &mut self,
4071        topics: &TopicCollection,
4072    ) -> Result<Vec<TopicDescription>> {
4073        self.describe_topics_for_with(topics, false).await
4074    }
4075
4076    /// [`Self::describe_topics_for`] with a one-shot RPC deadline (Java
4077    /// `DescribeTopicsOptions.timeoutMs`).
4078    pub async fn describe_topics_for_timeout(
4079        &mut self,
4080        topics: &TopicCollection,
4081        timeout: Duration,
4082    ) -> Result<Vec<TopicDescription>> {
4083        self.describe_topics_for_with_timeout(topics, false, timeout)
4084            .await
4085    }
4086
4087    /// [`Self::describe_topics_for`] with Java
4088    /// `DescribeTopicsOptions.includeAuthorizedOperations`.
4089    pub async fn describe_topics_for_with(
4090        &mut self,
4091        topics: &TopicCollection,
4092        include_authorized_operations: bool,
4093    ) -> Result<Vec<TopicDescription>> {
4094        let timeout = self.cfg.request_timeout;
4095        self.describe_topics_for_with_timeout(topics, include_authorized_operations, timeout)
4096            .await
4097    }
4098
4099    /// [`Self::describe_topics_for_with`] with Java
4100    /// `DescribeTopicsOptions.timeoutMs`.
4101    ///
4102    /// DescribeTopicPartitions and Metadata have no TimeoutMs; `timeout`
4103    /// is the RPC deadline (and the DTP NextCursor loop budget).
4104    pub async fn describe_topics_for_with_timeout(
4105        &mut self,
4106        topics: &TopicCollection,
4107        include_authorized_operations: bool,
4108        timeout: Duration,
4109    ) -> Result<Vec<TopicDescription>> {
4110        match topics {
4111            TopicCollection::Names(names) => {
4112                self.describe_topics_with_timeout(names, include_authorized_operations, timeout)
4113                    .await
4114            }
4115            TopicCollection::Ids(ids) => {
4116                let ids: Vec<[u8; 16]> = ids.iter().copied().map(Uuid::to_bytes).collect();
4117                self.describe_topics_by_id_with_timeout(
4118                    &ids,
4119                    include_authorized_operations,
4120                    timeout,
4121                )
4122                .await
4123            }
4124        }
4125    }
4126
4127    /// Describe broker or topic configs (`DescribeConfigs`).
4128    ///
4129    /// Negotiates v0–v4 (v1 IncludeSynonyms / ConfigSource / Synonyms;
4130    /// v3 IncludeDocumentation / ConfigType, KIP-226; v4 flexible).
4131    /// Kafka 4.0 `validVersions` is `1-4`. v5+ is not spoken.
4132    /// Documentation is omitted (`false`); see
4133    /// [`Self::describe_configs_with_documentation`]. DescribeConfigs has
4134    /// no TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
4135    /// For a one-shot deadline, use [`Self::describe_configs_timeout`].
4136    pub async fn describe_configs(
4137        &mut self,
4138        resources: &[ConfigResource],
4139        include_synonyms: bool,
4140    ) -> Result<Vec<DescribeConfigsResult>> {
4141        self.describe_configs_with_documentation(resources, include_synonyms, false)
4142            .await
4143    }
4144
4145    /// [`Self::describe_configs`] with a one-shot RPC deadline (Java
4146    /// `DescribeConfigsOptions.timeoutMs`).
4147    ///
4148    /// DescribeConfigs has no TimeoutMs; `timeout` is the RPC deadline.
4149    pub async fn describe_configs_timeout(
4150        &mut self,
4151        resources: &[ConfigResource],
4152        include_synonyms: bool,
4153        timeout: Duration,
4154    ) -> Result<Vec<DescribeConfigsResult>> {
4155        self.describe_configs_with_documentation_timeout(
4156            resources,
4157            include_synonyms,
4158            false,
4159            timeout,
4160        )
4161        .await
4162    }
4163
4164    /// DescribeConfigs with documentation (Java `describeConfigs` plus
4165    /// `DescribeConfigsOptions.includeDocumentation`).
4166    ///
4167    /// v3+ sends IncludeDocumentation. v0–v2 omit the field even when
4168    /// `include_documentation` is set. DescribeConfigs has no TimeoutMs;
4169    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
4170    /// one-shot deadline, use
4171    /// [`Self::describe_configs_with_documentation_timeout`].
4172    pub async fn describe_configs_with_documentation(
4173        &mut self,
4174        resources: &[ConfigResource],
4175        include_synonyms: bool,
4176        include_documentation: bool,
4177    ) -> Result<Vec<DescribeConfigsResult>> {
4178        let timeout = self.cfg.request_timeout;
4179        self.describe_configs_with_documentation_timeout(
4180            resources,
4181            include_synonyms,
4182            include_documentation,
4183            timeout,
4184        )
4185        .await
4186    }
4187
4188    /// [`Self::describe_configs_with_documentation`] with Java
4189    /// `DescribeConfigsOptions.timeoutMs`.
4190    ///
4191    /// DescribeConfigs has no TimeoutMs; `timeout` is the RPC deadline.
4192    pub async fn describe_configs_with_documentation_timeout(
4193        &mut self,
4194        resources: &[ConfigResource],
4195        include_synonyms: bool,
4196        include_documentation: bool,
4197        timeout: Duration,
4198    ) -> Result<Vec<DescribeConfigsResult>> {
4199        let req: Vec<DescribeConfigsResource> = resources
4200            .iter()
4201            .map(|r| DescribeConfigsResource {
4202                resource_type: r.resource_type,
4203                name: r.name.clone(),
4204                keys: r.keys.clone(),
4205            })
4206            .collect();
4207        let version = self.describe_version;
4208        let body = self
4209            .roundtrip_bootstrap(
4210                DESCRIBE_CONFIGS,
4211                version,
4212                |buf| {
4213                    encode_describe_configs_request(
4214                        buf,
4215                        version,
4216                        &req,
4217                        include_synonyms,
4218                        include_documentation,
4219                    )
4220                },
4221                timeout,
4222            )
4223            .await?;
4224        let (results, ..) = decode_describe_configs_response(&mut body.clone(), version)?;
4225        Ok(results)
4226    }
4227
4228    /// Increase partition count (`CreatePartitions`).
4229    ///
4230    /// Negotiates v0–v3 (v0–v1 classic; v2+ flexible; v3 KIP-599
4231    /// THROTTLING_QUOTA_EXCEEDED). Kafka 4.0 `validVersions` is `0-3`.
4232    /// v4+ is not spoken. [`NewPartitions::total_count`] is the new
4233    /// total, not a delta. Lands on the Metadata controller.
4234    /// `NOT_CONTROLLER` (41) refreshes Metadata and retries.
4235    /// `timeout_ms` is CreatePartitions TimeoutMs. The RPC deadline is
4236    /// [`AdminConfig::request_timeout`]. For a one-shot timeout that
4237    /// drives both the RPC deadline and TimeoutMs, use
4238    /// [`Self::create_partitions_timeout`].
4239    /// Java `CreatePartitionsOptions.retryOnQuotaViolation` defaults to
4240    /// `true` (KIP-599); use [`Self::create_partitions_with_quota_retry`]
4241    /// to disable.
4242    pub async fn create_partitions(
4243        &mut self,
4244        topics: &[NewPartitions],
4245        timeout_ms: i32,
4246        validate_only: bool,
4247    ) -> Result<Vec<TopicResult>> {
4248        let timeout = self.cfg.request_timeout;
4249        self.create_partitions_with(topics, timeout_ms, validate_only, timeout, true)
4250            .await
4251    }
4252
4253    /// [`Self::create_partitions`] with a one-shot timeout (Java
4254    /// `CreatePartitionsOptions.timeoutMs`).
4255    ///
4256    /// `timeout` is the RPC deadline and CreatePartitions TimeoutMs.
4257    pub async fn create_partitions_timeout(
4258        &mut self,
4259        topics: &[NewPartitions],
4260        timeout: Duration,
4261        validate_only: bool,
4262    ) -> Result<Vec<TopicResult>> {
4263        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4264        self.create_partitions_with(topics, timeout_ms, validate_only, timeout, true)
4265            .await
4266    }
4267
4268    /// [`Self::create_partitions`] with Java `CreatePartitionsOptions.retryOnQuotaViolation`.
4269    ///
4270    /// [`Self::create_partitions`] defaults this to `true` (KIP-599). When
4271    /// true, topics that return `THROTTLING_QUOTA_EXCEEDED` (89) are
4272    /// retried alone until the RPC deadline.
4273    pub async fn create_partitions_with_quota_retry(
4274        &mut self,
4275        topics: &[NewPartitions],
4276        timeout_ms: i32,
4277        validate_only: bool,
4278        retry_on_quota_violation: bool,
4279    ) -> Result<Vec<TopicResult>> {
4280        let timeout = self.cfg.request_timeout;
4281        self.create_partitions_with(
4282            topics,
4283            timeout_ms,
4284            validate_only,
4285            timeout,
4286            retry_on_quota_violation,
4287        )
4288        .await
4289    }
4290
4291    /// [`Self::create_partitions_with_quota_retry`] with a one-shot timeout
4292    /// (Java `CreatePartitionsOptions.timeoutMs` + `retryOnQuotaViolation`).
4293    ///
4294    /// `timeout` is the RPC deadline and CreatePartitions TimeoutMs.
4295    pub async fn create_partitions_timeout_with_quota_retry(
4296        &mut self,
4297        topics: &[NewPartitions],
4298        timeout: Duration,
4299        validate_only: bool,
4300        retry_on_quota_violation: bool,
4301    ) -> Result<Vec<TopicResult>> {
4302        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4303        self.create_partitions_with(
4304            topics,
4305            timeout_ms,
4306            validate_only,
4307            timeout,
4308            retry_on_quota_violation,
4309        )
4310        .await
4311    }
4312
4313    async fn create_partitions_with(
4314        &mut self,
4315        topics: &[NewPartitions],
4316        timeout_ms: i32,
4317        validate_only: bool,
4318        timeout: Duration,
4319        retry_on_quota_violation: bool,
4320    ) -> Result<Vec<TopicResult>> {
4321        let mut pending: Vec<NewPartitions> = topics.to_vec();
4322        let mut finished: HashMap<String, TopicResult> = HashMap::new();
4323        let version = self.partitions_version;
4324        let deadline = Instant::now() + timeout;
4325        let mut attempt = 0u32;
4326        loop {
4327            let encoded = partitions_from_new(&pending);
4328            if self.cluster.controller().is_err() {
4329                self.refresh_metadata(None).await?;
4330            }
4331            let node = self.cluster.controller()?;
4332            self.connect_node(node).await?;
4333            let body = {
4334                let conn = self
4335                    .conns
4336                    .get_mut(&node)
4337                    .ok_or_else(|| Error::protocol("missing create_partitions conn"))?;
4338                conn.roundtrip(
4339                    CREATE_PARTITIONS,
4340                    version,
4341                    |buf| {
4342                        encode_create_partitions_request(
4343                            buf,
4344                            version,
4345                            &encoded,
4346                            timeout_ms,
4347                            validate_only,
4348                        )
4349                    },
4350                    timeout,
4351                )
4352                .await
4353            };
4354            let body = match body {
4355                Ok(b) => b,
4356                Err(e) if e.is_retriable() => {
4357                    let _ = self.conns.remove(&node);
4358                    self.cluster.invalidate_controller();
4359                    self.wait_retry(&mut attempt, deadline).await?;
4360                    continue;
4361                }
4362                Err(e) => return Err(e),
4363            };
4364            let (results, ..) = decode_create_partitions_response(&mut body.clone(), version)?;
4365            if results
4366                .iter()
4367                .any(|r| r.error_code == error::NOT_CONTROLLER)
4368            {
4369                // NOT_CONTROLLER (41): Metadata, then the new controller.
4370                self.cluster.invalidate_controller();
4371                let _ = self.conns.remove(&node);
4372                self.wait_retry(&mut attempt, deadline).await?;
4373                self.refresh_metadata(None).await?;
4374                continue;
4375            }
4376            let mut next_pending = Vec::new();
4377            for r in results {
4378                if retry_on_quota_violation && r.error_code == error::THROTTLING_QUOTA_EXCEEDED {
4379                    if let Some(t) = pending.iter().find(|t| t.name == r.name) {
4380                        next_pending.push(t.clone());
4381                    }
4382                } else {
4383                    let name = r.name.clone();
4384                    let _prev = finished.insert(name, r);
4385                }
4386            }
4387            if next_pending.is_empty() {
4388                let mut out = Vec::with_capacity(topics.len());
4389                for t in topics {
4390                    let r = finished.remove(&t.name).ok_or_else(|| {
4391                        Error::protocol(format!("missing create_partitions result for {}", t.name))
4392                    })?;
4393                    out.push(r);
4394                }
4395                return Ok(out);
4396            }
4397            pending = next_pending;
4398            self.wait_retry(&mut attempt, deadline).await?;
4399        }
4400    }
4401
4402    /// Alter configs incrementally (`IncrementalAlterConfigs`).
4403    ///
4404    /// Negotiates v0–v1 (v0 classic; v1 flexible). Kafka 4.0
4405    /// `validVersions` is `0-1`. v2+ is not spoken.
4406    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
4407    /// Metadata and retries on the new controller.
4408    /// Returns the first resource's error code. For several resources in
4409    /// one RPC (Java `incrementalAlterConfigs(Map)`), use
4410    /// [`Self::incremental_alter_configs_for`]. IncrementalAlterConfigs
4411    /// has no TimeoutMs; the RPC deadline is
4412    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
4413    /// [`Self::incremental_alter_configs_timeout`]. Optional at
4414    /// [`Self::new`] (Kafka 2.3+ / KIP-339); a broker that omits api 44
4415    /// returns [`Error::Unsupported`].
4416    pub async fn incremental_alter_configs(
4417        &mut self,
4418        resource: &ConfigResource,
4419        configs: &[AlterConfig],
4420        validate_only: bool,
4421    ) -> Result<i16> {
4422        let results = self
4423            .incremental_alter_configs_for(
4424                &[ConfigResourceUpdate::new(
4425                    resource.clone(),
4426                    configs.iter().cloned(),
4427                )],
4428                validate_only,
4429            )
4430            .await?;
4431        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
4432    }
4433
4434    /// [`Self::incremental_alter_configs`] with a one-shot RPC deadline
4435    /// (Java `AlterConfigsOptions.timeoutMs`).
4436    ///
4437    /// IncrementalAlterConfigs has no TimeoutMs; `timeout` is the RPC
4438    /// deadline and the `NOT_CONTROLLER` retry budget.
4439    pub async fn incremental_alter_configs_timeout(
4440        &mut self,
4441        resource: &ConfigResource,
4442        configs: &[AlterConfig],
4443        timeout: Duration,
4444        validate_only: bool,
4445    ) -> Result<i16> {
4446        let results = self
4447            .incremental_alter_configs_for_timeout(
4448                &[ConfigResourceUpdate::new(
4449                    resource.clone(),
4450                    configs.iter().cloned(),
4451                )],
4452                timeout,
4453                validate_only,
4454            )
4455            .await?;
4456        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
4457    }
4458
4459    /// [`Self::incremental_alter_configs`] for several resources (Java
4460    /// `incrementalAlterConfigs(Map)`; IncrementalAlterConfigs Resources of N).
4461    ///
4462    /// Empty `updates` is a no-op.
4463    pub async fn incremental_alter_configs_for(
4464        &mut self,
4465        updates: &[ConfigResourceUpdate],
4466        validate_only: bool,
4467    ) -> Result<Vec<AlterConfigsResourceResult>> {
4468        let timeout = self.cfg.request_timeout;
4469        self.incremental_alter_configs_for_timeout(updates, timeout, validate_only)
4470            .await
4471    }
4472
4473    /// [`Self::incremental_alter_configs_for`] with Java
4474    /// `AlterConfigsOptions.timeoutMs`.
4475    ///
4476    /// Empty `updates` is a no-op. IncrementalAlterConfigs has no
4477    /// TimeoutMs; `timeout` is the RPC deadline and the `NOT_CONTROLLER`
4478    /// retry budget.
4479    pub async fn incremental_alter_configs_for_timeout(
4480        &mut self,
4481        updates: &[ConfigResourceUpdate],
4482        timeout: Duration,
4483        validate_only: bool,
4484    ) -> Result<Vec<AlterConfigsResourceResult>> {
4485        if updates.is_empty() {
4486            return Ok(Vec::new());
4487        }
4488        let version = self.alter_version.ok_or_else(|| {
4489            Error::Unsupported("broker does not support IncrementalAlterConfigs v0-1".into())
4490        })?;
4491        let resources: Vec<AlterableResource> = updates
4492            .iter()
4493            .map(|u| AlterableResource {
4494                resource_type: u.resource.resource_type,
4495                name: u.resource.name.clone(),
4496                configs: u.configs.clone(),
4497            })
4498            .collect();
4499        let deadline = Instant::now() + timeout;
4500        let mut attempt = 0u32;
4501        loop {
4502            if self.cluster.controller().is_err() {
4503                self.refresh_metadata(None).await?;
4504            }
4505            let node = self.cluster.controller()?;
4506            self.connect_node(node).await?;
4507            let body = {
4508                let conn = self
4509                    .conns
4510                    .get_mut(&node)
4511                    .ok_or_else(|| Error::protocol("missing incremental_alter_configs conn"))?;
4512                conn.roundtrip(
4513                    INCREMENTAL_ALTER_CONFIGS,
4514                    version,
4515                    |buf| {
4516                        encode_incremental_alter_configs_resources_request(
4517                            buf,
4518                            version,
4519                            &resources,
4520                            validate_only,
4521                        )
4522                    },
4523                    timeout,
4524                )
4525                .await
4526            };
4527            let body = match body {
4528                Ok(b) => b,
4529                Err(e) if e.is_retriable() => {
4530                    let _ = self.conns.remove(&node);
4531                    self.cluster.invalidate_controller();
4532                    self.wait_retry(&mut attempt, deadline).await?;
4533                    continue;
4534                }
4535                Err(e) => return Err(e),
4536            };
4537            let (results, ..) =
4538                decode_incremental_alter_configs_resource_results(&mut body.clone(), version)?;
4539            if results
4540                .iter()
4541                .any(|r| r.error_code == error::NOT_CONTROLLER)
4542            {
4543                self.cluster.invalidate_controller();
4544                let _ = self.conns.remove(&node);
4545                self.wait_retry(&mut attempt, deadline).await?;
4546                self.refresh_metadata(None).await?;
4547                continue;
4548            }
4549            return Ok(results);
4550        }
4551    }
4552
4553    /// Create ACL bindings (`CreateAcls`).
4554    ///
4555    /// Negotiates v0–v3 (v0–v1 classic; v2+ flexible). v1 adds
4556    /// ResourcePatternType (LITERAL unless [`AclBinding::pattern_type`]
4557    /// is set). v3 is the same layout (user resource type). Kafka 4.0
4558    /// `validVersions` is `1-3`. v4+ is not spoken. Java `ResourcePattern`
4559    /// / `AccessControlEntry` constructors reject ANY resource type,
4560    /// ANY/MATCH pattern type, and ANY operation / permission.
4561    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
4562    /// Metadata and retries on the new controller. CreateAcls has no
4563    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
4564    /// For a one-shot deadline, use [`Self::create_acls_timeout`].
4565    /// Returns one [`AclCreationResult`] per request binding (Java
4566    /// `CreateAclsResult`; request bindings are not copied into the
4567    /// result).
4568    pub async fn create_acls(&mut self, acls: &[AclBinding]) -> Result<Vec<AclCreationResult>> {
4569        let timeout = self.cfg.request_timeout;
4570        self.create_acls_timeout(acls, timeout).await
4571    }
4572
4573    /// [`Self::create_acls`] with a one-shot RPC deadline (Java
4574    /// `CreateAclsOptions.timeoutMs`).
4575    ///
4576    /// CreateAcls has no TimeoutMs; `timeout` is the RPC deadline and
4577    /// the `NOT_CONTROLLER` retry budget.
4578    pub async fn create_acls_timeout(
4579        &mut self,
4580        acls: &[AclBinding],
4581        timeout: Duration,
4582    ) -> Result<Vec<AclCreationResult>> {
4583        let version = self.create_acls_version;
4584        let deadline = Instant::now() + timeout;
4585        let mut attempt = 0u32;
4586        let acls = acls.to_vec();
4587        loop {
4588            if self.cluster.controller().is_err() {
4589                self.refresh_metadata(None).await?;
4590            }
4591            let node = self.cluster.controller()?;
4592            self.connect_node(node).await?;
4593            let body = {
4594                let conn = self
4595                    .conns
4596                    .get_mut(&node)
4597                    .ok_or_else(|| Error::protocol("missing create_acls conn"))?;
4598                conn.roundtrip(
4599                    CREATE_ACLS,
4600                    version,
4601                    |buf| encode_create_acls_request(buf, version, &acls),
4602                    timeout,
4603                )
4604                .await
4605            };
4606            let body = match body {
4607                Ok(b) => b,
4608                Err(e) if e.is_retriable() => {
4609                    let _ = self.conns.remove(&node);
4610                    self.cluster.invalidate_controller();
4611                    self.wait_retry(&mut attempt, deadline).await?;
4612                    continue;
4613                }
4614                Err(e) => return Err(e),
4615            };
4616            let (results, ..) = decode_create_acls_response(&mut body.clone(), version)?;
4617            if results
4618                .iter()
4619                .any(|r| r.error_code == error::NOT_CONTROLLER)
4620            {
4621                // NOT_CONTROLLER (41): Metadata, then the new controller.
4622                self.cluster.invalidate_controller();
4623                let _ = self.conns.remove(&node);
4624                self.wait_retry(&mut attempt, deadline).await?;
4625                self.refresh_metadata(None).await?;
4626                continue;
4627            }
4628            return Ok(results);
4629        }
4630    }
4631
4632    /// Alter partition replicas (AlterPartitionReassignments api 45).
4633    ///
4634    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
4635    /// Metadata and retries on the new controller.
4636    /// `timeout_ms` is AlterPartitionReassignments TimeoutMs. The RPC
4637    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
4638    /// timeout that drives both the RPC deadline and TimeoutMs, use
4639    /// [`Self::alter_partition_reassignments_timeout`]. Optional at
4640    /// [`Self::new`] (Kafka 2.4+ / KIP-455); a broker that omits api 45
4641    /// returns [`Error::Unsupported`].
4642    pub async fn alter_partition_reassignments(
4643        &mut self,
4644        assignments: &[PartitionReassignment],
4645        timeout_ms: i32,
4646    ) -> Result<Vec<ReassignmentResult>> {
4647        let timeout = self.cfg.request_timeout;
4648        self.alter_partition_reassignments_with(assignments, timeout_ms, timeout)
4649            .await
4650    }
4651
4652    /// [`Self::alter_partition_reassignments`] with a one-shot timeout
4653    /// (Java `AlterPartitionReassignmentsOptions.timeoutMs`).
4654    ///
4655    /// `timeout` is the RPC deadline and AlterPartitionReassignments
4656    /// TimeoutMs.
4657    pub async fn alter_partition_reassignments_timeout(
4658        &mut self,
4659        assignments: &[PartitionReassignment],
4660        timeout: Duration,
4661    ) -> Result<Vec<ReassignmentResult>> {
4662        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4663        self.alter_partition_reassignments_with(assignments, timeout_ms, timeout)
4664            .await
4665    }
4666
4667    /// Java `alterPartitionReassignments(Map)` ([`NewPartitionReassignment`];
4668    /// `None` cancels).
4669    ///
4670    /// `timeout_ms` is AlterPartitionReassignments TimeoutMs. The RPC
4671    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
4672    /// timeout that drives both, use
4673    /// [`Self::alter_partition_reassignments_for_timeout`].
4674    pub async fn alter_partition_reassignments_for<I, Tp>(
4675        &mut self,
4676        assignments: I,
4677        timeout_ms: i32,
4678    ) -> Result<Vec<ReassignmentResult>>
4679    where
4680        I: IntoIterator<Item = (Tp, Option<NewPartitionReassignment>)>,
4681        Tp: Into<crate::TopicPartition>,
4682    {
4683        let collected: Vec<PartitionReassignment> = assignments
4684            .into_iter()
4685            .map(|(tp, assignment)| PartitionReassignment::from_new(tp, assignment))
4686            .collect();
4687        self.alter_partition_reassignments(&collected, timeout_ms)
4688            .await
4689    }
4690
4691    /// [`Self::alter_partition_reassignments_for`] with a one-shot timeout
4692    /// (Java `AlterPartitionReassignmentsOptions.timeoutMs`).
4693    ///
4694    /// `timeout` is the RPC deadline and AlterPartitionReassignments
4695    /// TimeoutMs.
4696    pub async fn alter_partition_reassignments_for_timeout<I, Tp>(
4697        &mut self,
4698        assignments: I,
4699        timeout: Duration,
4700    ) -> Result<Vec<ReassignmentResult>>
4701    where
4702        I: IntoIterator<Item = (Tp, Option<NewPartitionReassignment>)>,
4703        Tp: Into<crate::TopicPartition>,
4704    {
4705        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4706        let collected: Vec<PartitionReassignment> = assignments
4707            .into_iter()
4708            .map(|(tp, assignment)| PartitionReassignment::from_new(tp, assignment))
4709            .collect();
4710        self.alter_partition_reassignments_with(&collected, timeout_ms, timeout)
4711            .await
4712    }
4713
4714    async fn alter_partition_reassignments_with(
4715        &mut self,
4716        assignments: &[PartitionReassignment],
4717        timeout_ms: i32,
4718        timeout: Duration,
4719    ) -> Result<Vec<ReassignmentResult>> {
4720        let topics = group_reassignments(assignments);
4721        let version = self.reassign_version.ok_or_else(|| {
4722            Error::Unsupported("broker does not support AlterPartitionReassignments".into())
4723        })?;
4724        let deadline = Instant::now() + timeout;
4725        let mut attempt = 0u32;
4726        loop {
4727            if self.cluster.controller().is_err() {
4728                self.refresh_metadata(None).await?;
4729            }
4730            let node = self.cluster.controller()?;
4731            self.connect_node(node).await?;
4732            let body = {
4733                let conn = self
4734                    .conns
4735                    .get_mut(&node)
4736                    .ok_or_else(|| Error::protocol("missing alter_partition_reassignments conn"))?;
4737                conn.roundtrip(
4738                    ALTER_PARTITION_REASSIGNMENTS,
4739                    version,
4740                    |buf| encode_alter_partition_reassignments_request(buf, timeout_ms, &topics),
4741                    timeout,
4742                )
4743                .await
4744            };
4745            let body = match body {
4746                Ok(b) => b,
4747                Err(e) if e.is_retriable() => {
4748                    let _ = self.conns.remove(&node);
4749                    self.cluster.invalidate_controller();
4750                    self.wait_retry(&mut attempt, deadline).await?;
4751                    continue;
4752                }
4753                Err(e) => return Err(e),
4754            };
4755            let resp = decode_alter_partition_reassignments_response(&mut body.clone())?;
4756            if resp.error_code == error::NOT_CONTROLLER
4757                || resp.results.iter().any(|t| {
4758                    t.partitions
4759                        .iter()
4760                        .any(|p| p.error_code == error::NOT_CONTROLLER)
4761                })
4762            {
4763                // NOT_CONTROLLER (41): Metadata, then the new controller.
4764                self.cluster.invalidate_controller();
4765                let _ = self.conns.remove(&node);
4766                self.wait_retry(&mut attempt, deadline).await?;
4767                self.refresh_metadata(None).await?;
4768                continue;
4769            }
4770            if resp.error_code != 0 {
4771                return Err(Error::broker(
4772                    resp.error_code,
4773                    "AlterPartitionReassignments",
4774                ));
4775            }
4776            return Ok(flatten_reassignment_results(&resp.results));
4777        }
4778    }
4779
4780    /// List ongoing replica moves (ListPartitionReassignments api 46).
4781    ///
4782    /// `partitions = None` lists every ongoing reassignment. Lands on the
4783    /// Metadata controller. `NOT_CONTROLLER` (41) refreshes Metadata and
4784    /// retries on the new controller.
4785    /// `timeout_ms` is ListPartitionReassignments TimeoutMs. The RPC
4786    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
4787    /// timeout that drives both the RPC deadline and TimeoutMs, use
4788    /// [`Self::list_partition_reassignments_timeout`]. Optional at
4789    /// [`Self::new`] (Kafka 2.4+ / KIP-455); a broker that omits api 46
4790    /// returns [`Error::Unsupported`].
4791    pub async fn list_partition_reassignments(
4792        &mut self,
4793        partitions: Option<&[crate::TopicPartition]>,
4794        timeout_ms: i32,
4795    ) -> Result<Vec<OngoingReassignment>> {
4796        let timeout = self.cfg.request_timeout;
4797        self.list_partition_reassignments_with(partitions, timeout_ms, timeout)
4798            .await
4799    }
4800
4801    /// [`Self::list_partition_reassignments`] with a one-shot timeout
4802    /// (Java `ListPartitionReassignmentsOptions.timeoutMs`).
4803    ///
4804    /// `timeout` is the RPC deadline and ListPartitionReassignments
4805    /// TimeoutMs.
4806    pub async fn list_partition_reassignments_timeout(
4807        &mut self,
4808        partitions: Option<&[crate::TopicPartition]>,
4809        timeout: Duration,
4810    ) -> Result<Vec<OngoingReassignment>> {
4811        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4812        self.list_partition_reassignments_with(partitions, timeout_ms, timeout)
4813            .await
4814    }
4815
4816    /// List every ongoing replica move (Java
4817    /// `Admin.listPartitionReassignments()`).
4818    ///
4819    /// Same wire as [`Self::list_partition_reassignments`] with `topics`
4820    /// null. TimeoutMs is [`AdminConfig::request_timeout`].
4821    pub async fn list_partition_reassignments_all(&mut self) -> Result<Vec<OngoingReassignment>> {
4822        let timeout = self.cfg.request_timeout;
4823        self.list_partition_reassignments_all_timeout(timeout).await
4824    }
4825
4826    /// [`Self::list_partition_reassignments_all`] with a one-shot timeout
4827    /// (Java `ListPartitionReassignmentsOptions.timeoutMs`).
4828    ///
4829    /// `timeout` is the RPC deadline and ListPartitionReassignments
4830    /// TimeoutMs.
4831    pub async fn list_partition_reassignments_all_timeout(
4832        &mut self,
4833        timeout: Duration,
4834    ) -> Result<Vec<OngoingReassignment>> {
4835        self.list_partition_reassignments_timeout(None, timeout)
4836            .await
4837    }
4838
4839    /// List ongoing replica moves for `partitions` (Java
4840    /// `Admin.listPartitionReassignments(Set)`).
4841    ///
4842    /// TimeoutMs is [`AdminConfig::request_timeout`]. For a one-shot
4843    /// timeout, use [`Self::list_partition_reassignments_timeout`].
4844    pub async fn list_partition_reassignments_for(
4845        &mut self,
4846        partitions: &[crate::TopicPartition],
4847    ) -> Result<Vec<OngoingReassignment>> {
4848        let timeout = self.cfg.request_timeout;
4849        self.list_partition_reassignments_timeout(Some(partitions), timeout)
4850            .await
4851    }
4852
4853    async fn list_partition_reassignments_with(
4854        &mut self,
4855        partitions: Option<&[crate::TopicPartition]>,
4856        timeout_ms: i32,
4857        timeout: Duration,
4858    ) -> Result<Vec<OngoingReassignment>> {
4859        let topics = partitions.map(group_list_reassignments);
4860        let version = self.list_reassign_version.ok_or_else(|| {
4861            Error::Unsupported("broker does not support ListPartitionReassignments".into())
4862        })?;
4863        let deadline = Instant::now() + timeout;
4864        let mut attempt = 0u32;
4865        loop {
4866            if self.cluster.controller().is_err() {
4867                self.refresh_metadata(None).await?;
4868            }
4869            let node = self.cluster.controller()?;
4870            self.connect_node(node).await?;
4871            let body = {
4872                let conn = self
4873                    .conns
4874                    .get_mut(&node)
4875                    .ok_or_else(|| Error::protocol("missing list_partition_reassignments conn"))?;
4876                conn.roundtrip(
4877                    LIST_PARTITION_REASSIGNMENTS,
4878                    version,
4879                    |buf| {
4880                        encode_list_partition_reassignments_request(
4881                            buf,
4882                            timeout_ms,
4883                            topics.as_deref(),
4884                        )
4885                    },
4886                    timeout,
4887                )
4888                .await
4889            };
4890            let body = match body {
4891                Ok(b) => b,
4892                Err(e) if e.is_retriable() => {
4893                    let _ = self.conns.remove(&node);
4894                    self.cluster.invalidate_controller();
4895                    self.wait_retry(&mut attempt, deadline).await?;
4896                    continue;
4897                }
4898                Err(e) => return Err(e),
4899            };
4900            let resp = decode_list_partition_reassignments_response(&mut body.clone())?;
4901            if resp.error_code == error::NOT_CONTROLLER {
4902                // NOT_CONTROLLER (41): Metadata, then the new controller.
4903                self.cluster.invalidate_controller();
4904                let _ = self.conns.remove(&node);
4905                self.wait_retry(&mut attempt, deadline).await?;
4906                self.refresh_metadata(None).await?;
4907                continue;
4908            }
4909            if resp.error_code != 0 {
4910                return Err(Error::broker(resp.error_code, "ListPartitionReassignments"));
4911            }
4912            return Ok(flatten_list_reassignments(&resp.topics));
4913        }
4914    }
4915
4916    /// Update finalized feature versions (UpdateFeatures api 57).
4917    ///
4918    /// Negotiates v0–v2 (flexible from v0). v0 sends `AllowDowngrade`.
4919    /// v1+ sends `UpgradeType` and `ValidateOnly` false. v2 omits
4920    /// per-feature Results; this method synthesizes success rows from
4921    /// the request when the top-level error is `0`. Kafka 4.0
4922    /// `validVersions` is `0-2`. v3+ is not spoken. Lands on the
4923    /// Metadata controller. `NOT_CONTROLLER` (41) refreshes Metadata
4924    /// and retries. `timeout_ms` is UpdateFeatures TimeoutMs. The RPC
4925    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
4926    /// timeout that drives both the RPC deadline and TimeoutMs, use
4927    /// [`Self::update_features_timeout`]. See
4928    /// [`Self::update_features_with`] for Java
4929    /// `UpdateFeaturesOptions.validateOnly`. Optional at [`Self::new`]
4930    /// (Kafka 2.7+ / KIP-584); a broker that omits api 57 returns
4931    /// [`Error::Unsupported`]. An empty list is Java `IllegalArgumentException`
4932    /// (`Feature updates can not be null or empty`). A blank feature name
4933    /// is `Provided feature can not be empty`.
4934    pub async fn update_features(
4935        &mut self,
4936        updates: &[FeatureUpdate],
4937        timeout_ms: i32,
4938    ) -> Result<Vec<FeatureUpdateResult>> {
4939        let timeout = self.cfg.request_timeout;
4940        self.update_features_inner(updates, timeout_ms, false, timeout)
4941            .await
4942    }
4943
4944    /// [`Self::update_features`] with a one-shot timeout (Java
4945    /// `UpdateFeaturesOptions.timeoutMs`).
4946    ///
4947    /// `timeout` is the RPC deadline and UpdateFeatures TimeoutMs.
4948    pub async fn update_features_timeout(
4949        &mut self,
4950        updates: &[FeatureUpdate],
4951        timeout: Duration,
4952    ) -> Result<Vec<FeatureUpdateResult>> {
4953        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4954        self.update_features_inner(updates, timeout_ms, false, timeout)
4955            .await
4956    }
4957
4958    /// UpdateFeatures with validate-only (Java `updateFeatures` plus
4959    /// `UpdateFeaturesOptions.validateOnly`).
4960    ///
4961    /// v1+ sends `ValidateOnly`. v0 omits the field even when set.
4962    /// `timeout_ms` is UpdateFeatures TimeoutMs. The RPC deadline is
4963    /// [`AdminConfig::request_timeout`]. For a one-shot timeout that
4964    /// drives both, use [`Self::update_features_with_timeout`].
4965    pub async fn update_features_with(
4966        &mut self,
4967        updates: &[FeatureUpdate],
4968        timeout_ms: i32,
4969        validate_only: bool,
4970    ) -> Result<Vec<FeatureUpdateResult>> {
4971        let timeout = self.cfg.request_timeout;
4972        self.update_features_inner(updates, timeout_ms, validate_only, timeout)
4973            .await
4974    }
4975
4976    /// [`Self::update_features_with`] with a one-shot timeout (Java
4977    /// `UpdateFeaturesOptions.timeoutMs` plus `validateOnly`).
4978    ///
4979    /// `timeout` is the RPC deadline and UpdateFeatures TimeoutMs.
4980    pub async fn update_features_with_timeout(
4981        &mut self,
4982        updates: &[FeatureUpdate],
4983        timeout: Duration,
4984        validate_only: bool,
4985    ) -> Result<Vec<FeatureUpdateResult>> {
4986        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
4987        self.update_features_inner(updates, timeout_ms, validate_only, timeout)
4988            .await
4989    }
4990
4991    /// Java `KafkaAdminClient.updateFeatures` argument checks.
4992    fn reject_java_feature_updates(updates: &[FeatureUpdate]) -> Result<()> {
4993        if updates.is_empty() {
4994            return Err(Error::protocol("Feature updates can not be null or empty."));
4995        }
4996        for u in updates {
4997            if u.name.trim().is_empty() {
4998                return Err(Error::protocol("Provided feature can not be empty."));
4999            }
5000        }
5001        Ok(())
5002    }
5003
5004    async fn update_features_inner(
5005        &mut self,
5006        updates: &[FeatureUpdate],
5007        timeout_ms: i32,
5008        validate_only: bool,
5009        timeout: Duration,
5010    ) -> Result<Vec<FeatureUpdateResult>> {
5011        Self::reject_java_feature_updates(updates)?;
5012        let keys: Vec<FeatureUpdateKey> = updates
5013            .iter()
5014            .map(|u| FeatureUpdateKey {
5015                name: u.name.clone(),
5016                max_version_level: u.max_version_level,
5017                allow_downgrade: u.allow_downgrade,
5018                upgrade_type: u.upgrade_type,
5019            })
5020            .collect();
5021        let version = self.update_features_version.ok_or_else(|| {
5022            Error::Unsupported("broker does not support UpdateFeatures v0-2".into())
5023        })?;
5024        let deadline = Instant::now() + timeout;
5025        let mut attempt = 0u32;
5026        loop {
5027            if self.cluster.controller().is_err() {
5028                self.refresh_metadata(None).await?;
5029            }
5030            let node = self.cluster.controller()?;
5031            self.connect_node(node).await?;
5032            let body = {
5033                let conn = self
5034                    .conns
5035                    .get_mut(&node)
5036                    .ok_or_else(|| Error::protocol("missing update_features conn"))?;
5037                conn.roundtrip(
5038                    UPDATE_FEATURES,
5039                    version,
5040                    |buf| {
5041                        encode_update_features_request(
5042                            buf,
5043                            version,
5044                            timeout_ms,
5045                            &keys,
5046                            validate_only,
5047                        )
5048                    },
5049                    timeout,
5050                )
5051                .await
5052            };
5053            let body = match body {
5054                Ok(b) => b,
5055                Err(e) if e.is_retriable() => {
5056                    let _ = self.conns.remove(&node);
5057                    self.cluster.invalidate_controller();
5058                    self.wait_retry(&mut attempt, deadline).await?;
5059                    continue;
5060                }
5061                Err(e) => return Err(e),
5062            };
5063            let resp = decode_update_features_response(&mut body.clone(), version)?;
5064            if resp.error_code == error::NOT_CONTROLLER
5065                || resp
5066                    .results
5067                    .iter()
5068                    .any(|r| r.error_code == error::NOT_CONTROLLER)
5069            {
5070                // NOT_CONTROLLER (41): Metadata, then the new controller.
5071                self.cluster.invalidate_controller();
5072                let _ = self.conns.remove(&node);
5073                self.wait_retry(&mut attempt, deadline).await?;
5074                self.refresh_metadata(None).await?;
5075                continue;
5076            }
5077            if resp.error_code != 0 {
5078                return Err(Error::broker(resp.error_code, "UpdateFeatures"));
5079            }
5080            if version >= 2 {
5081                return Ok(keys
5082                    .iter()
5083                    .map(|k| FeatureUpdateResult {
5084                        name: k.name.clone(),
5085                        error_code: 0,
5086                        error_message: None,
5087                    })
5088                    .collect());
5089            }
5090            return Ok(resp
5091                .results
5092                .into_iter()
5093                .map(|r| FeatureUpdateResult {
5094                    name: r.name,
5095                    error_code: r.error_code,
5096                    error_message: r.error_message,
5097                })
5098                .collect());
5099        }
5100    }
5101
5102    /// Supported and finalized features (Java `describeFeatures`).
5103    ///
5104    /// There is no DescribeFeatures api key. This re-issues ApiVersions v3–v4
5105    /// on the bootstrap connection and reads KIP-482 tagged fields
5106    /// (`supportedFeatures`, `finalizedFeaturesEpoch`, `finalizedFeatures`,
5107    /// `zkMigrationReady`). v4 includes SupportedFeatures with MinVersion 0
5108    /// (KAFKA-17011). Java `SupportedVersionRange` / `FinalizedVersionRange`
5109    /// constructors reject a negative min or max, or max below min.
5110    /// ApiVersions has no TimeoutMs; the RPC deadline is
5111    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
5112    /// [`Self::describe_features_timeout`].
5113    pub async fn describe_features(&mut self) -> Result<FeatureMetadata> {
5114        let timeout = self.cfg.request_timeout;
5115        self.describe_features_timeout(timeout).await
5116    }
5117
5118    /// [`Self::describe_features`] with a one-shot RPC deadline (Java
5119    /// `DescribeFeaturesOptions.timeoutMs`).
5120    ///
5121    /// There is no DescribeFeatures api key; this re-issues ApiVersions
5122    /// v3–v4. ApiVersions has no TimeoutMs; `timeout` is the RPC deadline.
5123    pub async fn describe_features_timeout(
5124        &mut self,
5125        timeout: Duration,
5126    ) -> Result<FeatureMetadata> {
5127        let version = self
5128            .versions
5129            .get(&API_VERSIONS)
5130            .and_then(|v| pick_version(v.min_version, v.max_version, 3, 4))
5131            .unwrap_or(3);
5132        let body = self
5133            .roundtrip_bootstrap(
5134                API_VERSIONS,
5135                version,
5136                |buf| encode_api_versions_request(buf, version, "partitionline", "0.1.0"),
5137                timeout,
5138            )
5139            .await?;
5140        let resp = decode_api_versions_response(&mut body.clone(), version)?;
5141        if resp.error_code != 0 {
5142            return Err(Error::broker(resp.error_code, "ApiVersions"));
5143        }
5144        Ok(FeatureMetadata {
5145            supported_features: resp
5146                .supported_features
5147                .into_iter()
5148                .map(|f| SupportedVersionRange::new(f.name, f.min_version, f.max_version))
5149                .collect::<Result<Vec<_>>>()?,
5150            finalized_features: resp
5151                .finalized_features
5152                .into_iter()
5153                .map(|f| {
5154                    FinalizedVersionRange::new(f.name, f.min_version_level, f.max_version_level)
5155                })
5156                .collect::<Result<Vec<_>>>()?,
5157            finalized_features_epoch: resp.finalized_features_epoch,
5158            zk_migration_ready: resp.zk_migration_ready,
5159        })
5160    }
5161
5162    /// Upsert or delete user SCRAM credentials (AlterUserScramCredentials
5163    /// api 51).
5164    ///
5165    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
5166    /// Metadata and retries on the new controller. AlterUserScramCredentials
5167    /// has no TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
5168    /// For a one-shot deadline, use
5169    /// [`Self::alter_user_scram_credentials_timeout`]. Optional at
5170    /// [`Self::new`] (Kafka 2.7+ / KIP-554); a broker that omits api 51
5171    /// returns [`Error::Unsupported`]. Java
5172    /// `alterUserScramCredentials(List)` is
5173    /// [`Self::alter_user_scram_credentials_with`].
5174    pub async fn alter_user_scram_credentials(
5175        &mut self,
5176        deletions: &[UserScramCredentialDeletion],
5177        upsertions: &[UserScramCredentialUpsertion],
5178    ) -> Result<Vec<UserScramCredentialResult>> {
5179        let timeout = self.cfg.request_timeout;
5180        self.alter_user_scram_credentials_timeout(deletions, upsertions, timeout)
5181            .await
5182    }
5183
5184    /// Java `alterUserScramCredentials(List)` of [`UserScramCredentialAlteration`].
5185    ///
5186    /// The wire request still splits Deletions then Upsertions. AlterUserScramCredentials
5187    /// has no TimeoutMs; the RPC deadline is
5188    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
5189    /// [`Self::alter_user_scram_credentials_with_timeout`].
5190    pub async fn alter_user_scram_credentials_with<A>(
5191        &mut self,
5192        alterations: impl IntoIterator<Item = A>,
5193    ) -> Result<Vec<UserScramCredentialResult>>
5194    where
5195        A: Into<UserScramCredentialAlteration>,
5196    {
5197        let timeout = self.cfg.request_timeout;
5198        self.alter_user_scram_credentials_with_timeout(alterations, timeout)
5199            .await
5200    }
5201
5202    /// [`Self::alter_user_scram_credentials_with`] with a one-shot RPC
5203    /// deadline (Java `AlterUserScramCredentialsOptions.timeoutMs`).
5204    ///
5205    /// AlterUserScramCredentials has no TimeoutMs; `timeout` is the RPC
5206    /// deadline and the `NOT_CONTROLLER` retry budget.
5207    pub async fn alter_user_scram_credentials_with_timeout<A>(
5208        &mut self,
5209        alterations: impl IntoIterator<Item = A>,
5210        timeout: Duration,
5211    ) -> Result<Vec<UserScramCredentialResult>>
5212    where
5213        A: Into<UserScramCredentialAlteration>,
5214    {
5215        let mut deletions = Vec::new();
5216        let mut upsertions = Vec::new();
5217        for item in alterations {
5218            match item.into() {
5219                UserScramCredentialAlteration::Deletion(d) => deletions.push(d),
5220                UserScramCredentialAlteration::Upsertion(u) => upsertions.push(u),
5221            }
5222        }
5223        self.alter_user_scram_credentials_timeout(&deletions, &upsertions, timeout)
5224            .await
5225    }
5226
5227    /// [`Self::alter_user_scram_credentials`] with a one-shot RPC deadline
5228    /// (Java `AlterUserScramCredentialsOptions.timeoutMs`).
5229    ///
5230    /// AlterUserScramCredentials has no TimeoutMs; `timeout` is the RPC
5231    /// deadline and the `NOT_CONTROLLER` retry budget.
5232    pub async fn alter_user_scram_credentials_timeout(
5233        &mut self,
5234        deletions: &[UserScramCredentialDeletion],
5235        upsertions: &[UserScramCredentialUpsertion],
5236        timeout: Duration,
5237    ) -> Result<Vec<UserScramCredentialResult>> {
5238        let deletions: Vec<ScramCredentialDeletion> = deletions
5239            .iter()
5240            .map(|d| ScramCredentialDeletion {
5241                name: d.name.clone(),
5242                mechanism: d.mechanism,
5243            })
5244            .collect();
5245        let upsertions: Vec<ScramCredentialUpsertion> = upsertions
5246            .iter()
5247            .map(|u| ScramCredentialUpsertion {
5248                name: u.name.clone(),
5249                mechanism: u.mechanism,
5250                iterations: u.iterations,
5251                salt: u.salt.clone(),
5252                salted_password: u.salted_password.clone(),
5253            })
5254            .collect();
5255        let version = self.alter_user_scram_version.ok_or_else(|| {
5256            Error::Unsupported("broker does not support AlterUserScramCredentials".into())
5257        })?;
5258        let deadline = Instant::now() + timeout;
5259        let mut attempt = 0u32;
5260        loop {
5261            if self.cluster.controller().is_err() {
5262                self.refresh_metadata(None).await?;
5263            }
5264            let node = self.cluster.controller()?;
5265            self.connect_node(node).await?;
5266            let body = {
5267                let conn = self
5268                    .conns
5269                    .get_mut(&node)
5270                    .ok_or_else(|| Error::protocol("missing alter_user_scram_credentials conn"))?;
5271                conn.roundtrip(
5272                    ALTER_USER_SCRAM_CREDENTIALS,
5273                    version,
5274                    |buf| encode_alter_user_scram_credentials_request(buf, &deletions, &upsertions),
5275                    timeout,
5276                )
5277                .await
5278            };
5279            let body = match body {
5280                Ok(b) => b,
5281                Err(e) if e.is_retriable() => {
5282                    let _ = self.conns.remove(&node);
5283                    self.cluster.invalidate_controller();
5284                    self.wait_retry(&mut attempt, deadline).await?;
5285                    continue;
5286                }
5287                Err(e) => return Err(e),
5288            };
5289            let (results, ..) = decode_alter_user_scram_credentials_response(&mut body.clone())?;
5290            if results
5291                .iter()
5292                .any(|r| r.error_code == error::NOT_CONTROLLER)
5293            {
5294                // NOT_CONTROLLER (41): Metadata, then the new controller.
5295                self.cluster.invalidate_controller();
5296                let _ = self.conns.remove(&node);
5297                self.wait_retry(&mut attempt, deadline).await?;
5298                self.refresh_metadata(None).await?;
5299                continue;
5300            }
5301            return Ok(results
5302                .into_iter()
5303                .map(|r| UserScramCredentialResult {
5304                    user: r.user,
5305                    error_code: r.error_code,
5306                    error_message: r.error_message,
5307                })
5308                .collect());
5309        }
5310    }
5311
5312    /// Describe user SCRAM credentials (DescribeUserScramCredentials
5313    /// api 50).
5314    ///
5315    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
5316    /// Metadata and retries on the new controller. Top-level `error_code`
5317    /// (bytes 4–5), not a first-result field. Empty `users` sends a null
5318    /// Users array (Java `describeUserScramCredentials()` / empty list)
5319    /// and describes every user. DescribeUserScramCredentials has no
5320    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
5321    /// For a one-shot deadline, use
5322    /// [`Self::describe_user_scram_credentials_timeout`]. Optional at
5323    /// [`Self::new`] (Kafka 2.7+ / KIP-554); a broker that omits api 50
5324    /// returns [`Error::Unsupported`].
5325    pub async fn describe_user_scram_credentials(
5326        &mut self,
5327        users: &[&str],
5328    ) -> Result<Vec<DescribeUserScramCredentialsResult>> {
5329        let timeout = self.cfg.request_timeout;
5330        self.describe_user_scram_credentials_timeout(users, timeout)
5331            .await
5332    }
5333
5334    /// [`Self::describe_user_scram_credentials`] with a one-shot RPC
5335    /// deadline (Java `DescribeUserScramCredentialsOptions.timeoutMs`).
5336    ///
5337    /// DescribeUserScramCredentials has no TimeoutMs; `timeout` is the
5338    /// RPC deadline and the `NOT_CONTROLLER` retry budget.
5339    pub async fn describe_user_scram_credentials_timeout(
5340        &mut self,
5341        users: &[&str],
5342        timeout: Duration,
5343    ) -> Result<Vec<DescribeUserScramCredentialsResult>> {
5344        let users: Vec<String> = users.iter().map(|s| (*s).to_string()).collect();
5345        let users_wire: Option<&[String]> = if users.is_empty() {
5346            None
5347        } else {
5348            Some(users.as_slice())
5349        };
5350        let version = self.describe_user_scram_version.ok_or_else(|| {
5351            Error::Unsupported("broker does not support DescribeUserScramCredentials".into())
5352        })?;
5353        let deadline = Instant::now() + timeout;
5354        let mut attempt = 0u32;
5355        loop {
5356            if self.cluster.controller().is_err() {
5357                self.refresh_metadata(None).await?;
5358            }
5359            let node = self.cluster.controller()?;
5360            self.connect_node(node).await?;
5361            let body = {
5362                let conn = self.conns.get_mut(&node).ok_or_else(|| {
5363                    Error::protocol("missing describe_user_scram_credentials conn")
5364                })?;
5365                conn.roundtrip(
5366                    DESCRIBE_USER_SCRAM_CREDENTIALS,
5367                    version,
5368                    |buf| encode_describe_user_scram_credentials_request(buf, users_wire),
5369                    timeout,
5370                )
5371                .await
5372            };
5373            let body = match body {
5374                Ok(b) => b,
5375                Err(e) if e.is_retriable() => {
5376                    let _ = self.conns.remove(&node);
5377                    self.cluster.invalidate_controller();
5378                    self.wait_retry(&mut attempt, deadline).await?;
5379                    continue;
5380                }
5381                Err(e) => return Err(e),
5382            };
5383            let resp = decode_describe_user_scram_credentials_response(&mut body.clone())?;
5384            if resp.error_code == error::NOT_CONTROLLER {
5385                // NOT_CONTROLLER (41): Metadata, then the new controller.
5386                self.cluster.invalidate_controller();
5387                let _ = self.conns.remove(&node);
5388                self.wait_retry(&mut attempt, deadline).await?;
5389                self.refresh_metadata(None).await?;
5390                continue;
5391            }
5392            if resp.error_code != 0 {
5393                return Err(Error::broker(
5394                    resp.error_code,
5395                    "DescribeUserScramCredentials",
5396                ));
5397            }
5398            return Ok(resp.results);
5399        }
5400    }
5401
5402    /// Describe every user's SCRAM credentials (Java
5403    /// `Admin.describeUserScramCredentials()`).
5404    ///
5405    /// Same wire as [`Self::describe_user_scram_credentials`] with an
5406    /// empty user list (Users null).
5407    pub async fn describe_user_scram_credentials_all(
5408        &mut self,
5409    ) -> Result<Vec<DescribeUserScramCredentialsResult>> {
5410        let timeout = self.cfg.request_timeout;
5411        self.describe_user_scram_credentials_all_timeout(timeout)
5412            .await
5413    }
5414
5415    /// [`Self::describe_user_scram_credentials_all`] with a one-shot RPC
5416    /// deadline (Java `DescribeUserScramCredentialsOptions.timeoutMs`).
5417    pub async fn describe_user_scram_credentials_all_timeout(
5418        &mut self,
5419        timeout: Duration,
5420    ) -> Result<Vec<DescribeUserScramCredentialsResult>> {
5421        self.describe_user_scram_credentials_timeout(&[], timeout)
5422            .await
5423    }
5424
5425    /// Unregister a broker (UnregisterBroker api 64, KIP-500).
5426    ///
5427    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
5428    /// Metadata and retries on the new controller. Top-level `error_code`
5429    /// (bytes 4–5), after throttle. Fixture broker id only; this is not
5430    /// a live KRaft unregistration. Optional at [`Self::new`] (Java
5431    /// `@InterfaceStability.Unstable`); a broker that omits api 64
5432    /// returns [`Error::Unsupported`]. UnregisterBroker has no TimeoutMs;
5433    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
5434    /// one-shot deadline, use [`Self::unregister_broker_timeout`].
5435    pub async fn unregister_broker(&mut self, broker_id: i32) -> Result<()> {
5436        let timeout = self.cfg.request_timeout;
5437        self.unregister_broker_timeout(broker_id, timeout).await
5438    }
5439
5440    /// [`Self::unregister_broker`] with a one-shot RPC deadline (Java
5441    /// `UnregisterBrokerOptions.timeoutMs`).
5442    ///
5443    /// UnregisterBroker has no TimeoutMs; `timeout` is the RPC deadline
5444    /// and the `NOT_CONTROLLER` retry budget.
5445    pub async fn unregister_broker_timeout(
5446        &mut self,
5447        broker_id: i32,
5448        timeout: Duration,
5449    ) -> Result<()> {
5450        let version = self
5451            .unregister_broker_version
5452            .ok_or_else(|| Error::Unsupported("broker does not support UnregisterBroker".into()))?;
5453        let deadline = Instant::now() + timeout;
5454        let mut attempt = 0u32;
5455        loop {
5456            if self.cluster.controller().is_err() {
5457                self.refresh_metadata(None).await?;
5458            }
5459            let node = self.cluster.controller()?;
5460            self.connect_node(node).await?;
5461            let body = {
5462                let conn = self
5463                    .conns
5464                    .get_mut(&node)
5465                    .ok_or_else(|| Error::protocol("missing unregister_broker conn"))?;
5466                conn.roundtrip(
5467                    UNREGISTER_BROKER,
5468                    version,
5469                    |buf| encode_unregister_broker_request(buf, broker_id),
5470                    timeout,
5471                )
5472                .await
5473            };
5474            let body = match body {
5475                Ok(b) => b,
5476                Err(e) if e.is_retriable() => {
5477                    let _ = self.conns.remove(&node);
5478                    self.cluster.invalidate_controller();
5479                    self.wait_retry(&mut attempt, deadline).await?;
5480                    continue;
5481                }
5482                Err(e) => return Err(e),
5483            };
5484            let resp = decode_unregister_broker_response(&mut body.clone())?;
5485            if resp.error_code == error::NOT_CONTROLLER {
5486                // NOT_CONTROLLER (41): Metadata, then the new controller.
5487                self.cluster.invalidate_controller();
5488                let _ = self.conns.remove(&node);
5489                self.wait_retry(&mut attempt, deadline).await?;
5490                self.refresh_metadata(None).await?;
5491                continue;
5492            }
5493            if resp.error_code != 0 {
5494                return Err(Error::broker(resp.error_code, "UnregisterBroker"));
5495            }
5496            return Ok(());
5497        }
5498    }
5499
5500    /// Describe client quotas (DescribeClientQuotas api 48, KIP-219).
5501    ///
5502    /// Lands on the connected broker (bootstrap is fine). Negotiates
5503    /// DescribeClientQuotas v0–v1 (Kafka 4.0 `validVersions` `0-1`;
5504    /// v0 classic, v1 flexible). Official Apache
5505    /// JSON listeners are `broker` only. This is not a controller hop:
5506    /// there is no Metadata `controller_id` lookup and no
5507    /// `NOT_CONTROLLER` (41) retry. Top-level `error_code` is the INT16
5508    /// at bytes 4–5, after throttle. DescribeClientQuotas has no TimeoutMs;
5509    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
5510    /// one-shot deadline, use [`Self::describe_client_quotas_timeout`].
5511    /// Optional at [`Self::new`] (Kafka 2.6+ / KIP-219); a broker that
5512    /// omits api 48 returns [`Error::Unsupported`]. See
5513    /// [`Self::describe_client_quotas_all`] for Java
5514    /// `ClientQuotaFilter.all()`, or [`Self::describe_client_quotas_with`]
5515    /// for [`ClientQuotaFilter::contains`] / [`ClientQuotaFilter::contains_only`].
5516    pub async fn describe_client_quotas(
5517        &mut self,
5518        components: &[ClientQuotaFilterComponent],
5519        strict: bool,
5520    ) -> Result<Vec<ClientQuotaEntry>> {
5521        let timeout = self.cfg.request_timeout;
5522        self.describe_client_quotas_timeout(components, strict, timeout)
5523            .await
5524    }
5525
5526    /// [`Self::describe_client_quotas`] with a one-shot RPC deadline (Java
5527    /// `DescribeClientQuotasOptions.timeoutMs`).
5528    ///
5529    /// DescribeClientQuotas has no TimeoutMs; `timeout` is the RPC deadline.
5530    pub async fn describe_client_quotas_timeout(
5531        &mut self,
5532        components: &[ClientQuotaFilterComponent],
5533        strict: bool,
5534        timeout: Duration,
5535    ) -> Result<Vec<ClientQuotaEntry>> {
5536        let components = components.to_vec();
5537        let version = self.describe_client_quotas_version.ok_or_else(|| {
5538            Error::Unsupported("broker does not support DescribeClientQuotas v0-1".into())
5539        })?;
5540        let body = self
5541            .roundtrip_bootstrap(
5542                DESCRIBE_CLIENT_QUOTAS,
5543                version,
5544                |buf| encode_describe_client_quotas_request(buf, version, &components, strict),
5545                timeout,
5546            )
5547            .await?;
5548        let resp = decode_describe_client_quotas_response(&mut body.clone(), version)?;
5549        if resp.error_code != 0 {
5550            return Err(Error::broker(resp.error_code, "DescribeClientQuotas"));
5551        }
5552        Ok(resp.entries.unwrap_or_default())
5553    }
5554
5555    /// [`Self::describe_client_quotas`] with a Java `ClientQuotaFilter`.
5556    ///
5557    /// DescribeClientQuotas has no TimeoutMs; the RPC deadline is
5558    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
5559    /// [`Self::describe_client_quotas_with_timeout`].
5560    pub async fn describe_client_quotas_with(
5561        &mut self,
5562        filter: &ClientQuotaFilter,
5563    ) -> Result<Vec<ClientQuotaEntry>> {
5564        self.describe_client_quotas(filter.components(), filter.strict())
5565            .await
5566    }
5567
5568    /// [`Self::describe_client_quotas_with`] with a one-shot RPC deadline
5569    /// (Java `DescribeClientQuotasOptions.timeoutMs`).
5570    ///
5571    /// DescribeClientQuotas has no TimeoutMs; `timeout` is the RPC deadline.
5572    pub async fn describe_client_quotas_with_timeout(
5573        &mut self,
5574        filter: &ClientQuotaFilter,
5575        timeout: Duration,
5576    ) -> Result<Vec<ClientQuotaEntry>> {
5577        self.describe_client_quotas_timeout(filter.components(), filter.strict(), timeout)
5578            .await
5579    }
5580
5581    /// Describe every client quota (Java `ClientQuotaFilter.all()`).
5582    ///
5583    /// Same wire as [`Self::describe_client_quotas_with`] with
5584    /// [`ClientQuotaFilter::all`].
5585    pub async fn describe_client_quotas_all(&mut self) -> Result<Vec<ClientQuotaEntry>> {
5586        self.describe_client_quotas_with(&ClientQuotaFilter::all())
5587            .await
5588    }
5589
5590    /// [`Self::describe_client_quotas_all`] with a one-shot RPC deadline
5591    /// (Java `DescribeClientQuotasOptions.timeoutMs`).
5592    pub async fn describe_client_quotas_all_timeout(
5593        &mut self,
5594        timeout: Duration,
5595    ) -> Result<Vec<ClientQuotaEntry>> {
5596        self.describe_client_quotas_with_timeout(&ClientQuotaFilter::all(), timeout)
5597            .await
5598    }
5599
5600    /// Upsert or delete client quotas (AlterClientQuotas api 49).
5601    ///
5602    /// Lands on the Metadata controller. Negotiates AlterClientQuotas
5603    /// v0–v1 (Kafka 4.0 `validVersions` `0-1`; v0 classic, v1 flexible).
5604    /// `NOT_CONTROLLER` (41) refreshes Metadata and retries on the new
5605    /// controller. AlterClientQuotas has no TimeoutMs; the RPC deadline
5606    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
5607    /// [`Self::alter_client_quotas_timeout`]. Optional at [`Self::new`]
5608    /// (Kafka 2.6+ / KIP-219); a broker that omits api 49 returns
5609    /// [`Error::Unsupported`].
5610    pub async fn alter_client_quotas(
5611        &mut self,
5612        entries: &[ClientQuotaAlteration],
5613        validate_only: bool,
5614    ) -> Result<Vec<ClientQuotaAlterationResult>> {
5615        let timeout = self.cfg.request_timeout;
5616        self.alter_client_quotas_timeout(entries, timeout, validate_only)
5617            .await
5618    }
5619
5620    /// [`Self::alter_client_quotas`] with a one-shot RPC deadline (Java
5621    /// `AlterClientQuotasOptions.timeoutMs`).
5622    ///
5623    /// AlterClientQuotas has no TimeoutMs; `timeout` is the RPC deadline
5624    /// and the `NOT_CONTROLLER` retry budget.
5625    pub async fn alter_client_quotas_timeout(
5626        &mut self,
5627        entries: &[ClientQuotaAlteration],
5628        timeout: Duration,
5629        validate_only: bool,
5630    ) -> Result<Vec<ClientQuotaAlterationResult>> {
5631        let entries = entries.to_vec();
5632        let version = self.alter_client_quotas_version.ok_or_else(|| {
5633            Error::Unsupported("broker does not support AlterClientQuotas v0-1".into())
5634        })?;
5635        let deadline = Instant::now() + timeout;
5636        let mut attempt = 0u32;
5637        loop {
5638            if self.cluster.controller().is_err() {
5639                self.refresh_metadata(None).await?;
5640            }
5641            let node = self.cluster.controller()?;
5642            self.connect_node(node).await?;
5643            let body = {
5644                let conn = self
5645                    .conns
5646                    .get_mut(&node)
5647                    .ok_or_else(|| Error::protocol("missing alter_client_quotas conn"))?;
5648                conn.roundtrip(
5649                    ALTER_CLIENT_QUOTAS,
5650                    version,
5651                    |buf| encode_alter_client_quotas_request(buf, version, &entries, validate_only),
5652                    timeout,
5653                )
5654                .await
5655            };
5656            let body = match body {
5657                Ok(b) => b,
5658                Err(e) if e.is_retriable() => {
5659                    let _ = self.conns.remove(&node);
5660                    self.cluster.invalidate_controller();
5661                    self.wait_retry(&mut attempt, deadline).await?;
5662                    continue;
5663                }
5664                Err(e) => return Err(e),
5665            };
5666            let (results, ..) = decode_alter_client_quotas_response(&mut body.clone(), version)?;
5667            if results
5668                .iter()
5669                .any(|r| r.error_code == error::NOT_CONTROLLER)
5670            {
5671                // NOT_CONTROLLER (41): Metadata, then the new controller.
5672                self.cluster.invalidate_controller();
5673                let _ = self.conns.remove(&node);
5674                self.wait_retry(&mut attempt, deadline).await?;
5675                self.refresh_metadata(None).await?;
5676                continue;
5677            }
5678            return Ok(results);
5679        }
5680    }
5681
5682    /// Allocate a producer-id block (AllocateProducerIds api 67).
5683    ///
5684    /// Lands on the Metadata controller. `NOT_CONTROLLER` (41) refreshes
5685    /// Metadata and retries on the new controller. `broker_id` /
5686    /// `broker_epoch` are the requesting broker's fixture identity.
5687    /// AllocateProducerIds has no TimeoutMs; the RPC deadline is
5688    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
5689    /// [`Self::allocate_producer_ids_timeout`].
5690    pub async fn allocate_producer_ids(
5691        &mut self,
5692        broker_id: i32,
5693        broker_epoch: i64,
5694    ) -> Result<ProducerIdBlock> {
5695        let timeout = self.cfg.request_timeout;
5696        self.allocate_producer_ids_timeout(broker_id, broker_epoch, timeout)
5697            .await
5698    }
5699
5700    /// [`Self::allocate_producer_ids`] with a one-shot RPC deadline.
5701    ///
5702    /// AllocateProducerIds has no TimeoutMs; `timeout` is the RPC
5703    /// deadline and the `NOT_CONTROLLER` retry budget. Java `Admin`
5704    /// has no `allocateProducerIds`; this overload is crate-first.
5705    pub async fn allocate_producer_ids_timeout(
5706        &mut self,
5707        broker_id: i32,
5708        broker_epoch: i64,
5709        timeout: Duration,
5710    ) -> Result<ProducerIdBlock> {
5711        let version = self.allocate_producer_ids_version.ok_or_else(|| {
5712            Error::Unsupported("broker does not support AllocateProducerIds".into())
5713        })?;
5714        let deadline = Instant::now() + timeout;
5715        let mut attempt = 0u32;
5716        loop {
5717            if self.cluster.controller().is_err() {
5718                self.refresh_metadata(None).await?;
5719            }
5720            let node = self.cluster.controller()?;
5721            self.connect_node(node).await?;
5722            let body = {
5723                let conn = self
5724                    .conns
5725                    .get_mut(&node)
5726                    .ok_or_else(|| Error::protocol("missing allocate_producer_ids conn"))?;
5727                conn.roundtrip(
5728                    ALLOCATE_PRODUCER_IDS,
5729                    version,
5730                    |buf| encode_allocate_producer_ids_request(buf, broker_id, broker_epoch),
5731                    timeout,
5732                )
5733                .await
5734            };
5735            let body = match body {
5736                Ok(b) => b,
5737                Err(e) if e.is_retriable() => {
5738                    let _ = self.conns.remove(&node);
5739                    self.cluster.invalidate_controller();
5740                    self.wait_retry(&mut attempt, deadline).await?;
5741                    continue;
5742                }
5743                Err(e) => return Err(e),
5744            };
5745            let resp = decode_allocate_producer_ids_response(&mut body.clone())?;
5746            if resp.error_code == error::NOT_CONTROLLER {
5747                // NOT_CONTROLLER (41): Metadata, then the new controller.
5748                self.cluster.invalidate_controller();
5749                let _ = self.conns.remove(&node);
5750                self.wait_retry(&mut attempt, deadline).await?;
5751                self.refresh_metadata(None).await?;
5752                continue;
5753            }
5754            if resp.error_code != 0 {
5755                return Err(Error::broker(resp.error_code, "AllocateProducerIds"));
5756            }
5757            return Ok(ProducerIdBlock {
5758                producer_id_start: resp.producer_id_start,
5759                producer_id_len: resp.producer_id_len,
5760            });
5761        }
5762    }
5763
5764    /// Fence transactional producers (Java `Admin.fenceProducers`).
5765    ///
5766    /// InitProducerId v0–v5 on each `transactional.id`'s transaction
5767    /// coordinator (`FindCoordinator` `key_type=1`). Empty
5768    /// `transactional_ids` returns an empty list. Coordinator load /
5769    /// move errors refresh and retry. [`AdminConfig::request_timeout`]
5770    /// is the RPC deadline and is sent as `transaction.timeout.ms`. For
5771    /// a one-shot timeout, use [`Self::fence_producers_timeout`]. Java
5772    /// `forceTerminateTransaction` is [`Self::force_terminate_transaction`].
5773    pub async fn fence_producers(
5774        &mut self,
5775        transactional_ids: impl IntoIterator<Item = impl Into<String>>,
5776    ) -> Result<Vec<FencedProducer>> {
5777        let timeout = self.cfg.request_timeout;
5778        self.fence_producers_timeout(transactional_ids, timeout)
5779            .await
5780    }
5781
5782    /// [`Self::fence_producers`] with a one-shot timeout (Java
5783    /// `fenceProducers` plus `FenceProducersOptions.timeoutMs`).
5784    ///
5785    /// `timeout` is the RPC deadline and InitProducerId
5786    /// `transaction.timeout.ms`.
5787    pub async fn fence_producers_timeout(
5788        &mut self,
5789        transactional_ids: impl IntoIterator<Item = impl Into<String>>,
5790        timeout: Duration,
5791    ) -> Result<Vec<FencedProducer>> {
5792        let mut out = Vec::new();
5793        for id in transactional_ids {
5794            let transactional_id = id.into();
5795            let (producer_id, epoch) = self.fence_one(&transactional_id, timeout).await?;
5796            out.push(FencedProducer {
5797                transactional_id,
5798                producer_id,
5799                epoch,
5800            });
5801        }
5802        Ok(out)
5803    }
5804
5805    /// Force-terminate a transactional id (Java
5806    /// `Admin.forceTerminateTransaction`).
5807    ///
5808    /// Same wire as [`Self::fence_producers`] for one id: InitProducerId
5809    /// on the transaction coordinator (`FindCoordinator` `key_type=1`).
5810    /// Java's `forceTerminateTransaction` calls `fenceProducers` with a
5811    /// singleton set. Waits up to [`AdminConfig::request_timeout`]. For
5812    /// a one-shot timeout, use [`Self::force_terminate_transaction_timeout`].
5813    pub async fn force_terminate_transaction(
5814        &mut self,
5815        transactional_id: impl Into<String>,
5816    ) -> Result<FencedProducer> {
5817        let timeout = self.cfg.request_timeout;
5818        self.force_terminate_transaction_timeout(transactional_id, timeout)
5819            .await
5820    }
5821
5822    /// [`Self::force_terminate_transaction`] with a one-shot timeout
5823    /// (Java `forceTerminateTransaction` plus
5824    /// `FenceProducersOptions.timeoutMs`).
5825    pub async fn force_terminate_transaction_timeout(
5826        &mut self,
5827        transactional_id: impl Into<String>,
5828        timeout: Duration,
5829    ) -> Result<FencedProducer> {
5830        let transactional_id = transactional_id.into();
5831        let (producer_id, epoch) = self.fence_one(&transactional_id, timeout).await?;
5832        Ok(FencedProducer {
5833            transactional_id,
5834            producer_id,
5835            epoch,
5836        })
5837    }
5838
5839    async fn fence_one(&mut self, transactional_id: &str, timeout: Duration) -> Result<(i64, i16)> {
5840        let version = self
5841            .versions
5842            .get(&INIT_PRODUCER_ID)
5843            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 5))
5844            .ok_or_else(|| Error::Unsupported("broker does not support InitProducerId".into()))?;
5845        let txn_timeout_ms = crate::consumer::duration_millis_i32(timeout);
5846        let deadline = Instant::now() + timeout;
5847        let mut attempt = 0u32;
5848        loop {
5849            let stale = self
5850                .txn_coord
5851                .as_ref()
5852                .is_none_or(|(k, _)| k != transactional_id);
5853            if stale {
5854                let node = self.discover_txn_coord(transactional_id).await?;
5855                self.txn_coord = Some((transactional_id.to_string(), node));
5856            }
5857            let node = self
5858                .txn_coord
5859                .as_ref()
5860                .map(|(_, n)| *n)
5861                .ok_or_else(|| Error::protocol("missing transaction coordinator"))?;
5862            self.connect_node(node).await?;
5863            let body = {
5864                let conn = self
5865                    .conns
5866                    .get_mut(&node)
5867                    .ok_or_else(|| Error::protocol("missing fence_producers conn"))?;
5868                conn.roundtrip(
5869                    INIT_PRODUCER_ID,
5870                    version,
5871                    |buf| {
5872                        encode_init_producer_id_request(
5873                            buf,
5874                            version,
5875                            Some(transactional_id),
5876                            txn_timeout_ms,
5877                            crate::RecordBatch::NO_PRODUCER_ID,
5878                            crate::RecordBatch::NO_PRODUCER_EPOCH,
5879                        )
5880                    },
5881                    timeout,
5882                )
5883                .await
5884            };
5885            let body = match body {
5886                Ok(b) => b,
5887                Err(e) if e.is_retriable() => {
5888                    let _ = self.conns.remove(&node);
5889                    self.txn_coord = None;
5890                    self.wait_retry(&mut attempt, deadline).await?;
5891                    continue;
5892                }
5893                Err(e) => return Err(e),
5894            };
5895            let (err, producer_id, epoch, ..) =
5896                decode_init_producer_id_response(&mut body.clone(), version)?;
5897            if err == 0 {
5898                return Ok((producer_id, epoch));
5899            }
5900            if error::coordinator_retriable(err) {
5901                self.txn_coord = None;
5902                let _ = self.conns.remove(&node);
5903                self.wait_retry(&mut attempt, deadline).await?;
5904                continue;
5905            }
5906            return Err(Error::broker(err, "InitProducerId"));
5907        }
5908    }
5909
5910    /// Force-abort an open transaction on a partition (Java `abortTransaction`).
5911    ///
5912    /// Sends WriteTxnMarkers (api 27) v0 (classic) or v1 (flexible;
5913    /// Kafka 4.0 baseline) with `transactionResult=false` to the
5914    /// Metadata partition leader. `NOT_LEADER_OR_FOLLOWER` and
5915    /// fenced/unknown leader epochs refresh Metadata and retry. This is
5916    /// not a controller hop and not a transaction-coordinator hop.
5917    /// v2 `TransactionVersion` (KIP-1228) is not spoken. WriteTxnMarkers
5918    /// has no TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
5919    /// For a one-shot deadline, use [`Self::abort_transaction_timeout`].
5920    pub async fn abort_transaction(&mut self, spec: AbortTransactionSpec) -> Result<()> {
5921        let timeout = self.cfg.request_timeout;
5922        self.abort_transaction_timeout(spec, timeout).await
5923    }
5924
5925    /// [`Self::abort_transaction`] with a one-shot RPC deadline (Java
5926    /// `AbortTransactionOptions.timeoutMs`).
5927    ///
5928    /// WriteTxnMarkers has no TimeoutMs; `timeout` is the RPC deadline
5929    /// and the `NOT_LEADER_OR_FOLLOWER` retry budget.
5930    pub async fn abort_transaction_timeout(
5931        &mut self,
5932        spec: AbortTransactionSpec,
5933        timeout: Duration,
5934    ) -> Result<()> {
5935        let version = self
5936            .versions
5937            .get(&WRITE_TXN_MARKERS)
5938            .and_then(|v| pick_version(v.min_version, v.max_version, 0, 1))
5939            .ok_or_else(|| Error::Unsupported("broker does not support WriteTxnMarkers".into()))?;
5940        let deadline = Instant::now() + timeout;
5941        let mut attempt = 0u32;
5942        let marker = WritableTxnMarker {
5943            producer_id: spec.producer_id,
5944            producer_epoch: spec.producer_epoch,
5945            transaction_result: TransactionResult::Abort.id(),
5946            topics: vec![WritableTxnMarkerTopic {
5947                name: spec.topic.clone(),
5948                partitions: vec![spec.partition],
5949            }],
5950            coordinator_epoch: spec.coordinator_epoch,
5951        };
5952        loop {
5953            if self.cluster.leader(&spec.topic, spec.partition).is_err() {
5954                let topics = [spec.topic.clone()];
5955                self.refresh_metadata(Some(&topics)).await?;
5956            }
5957            let (node, _) = self.cluster.leader(&spec.topic, spec.partition)?;
5958            self.connect_node(node).await?;
5959            let body = {
5960                let conn = self
5961                    .conns
5962                    .get_mut(&node)
5963                    .ok_or_else(|| Error::protocol("missing abort_transaction conn"))?;
5964                conn.roundtrip(
5965                    WRITE_TXN_MARKERS,
5966                    version,
5967                    |buf| {
5968                        encode_write_txn_markers_request(
5969                            buf,
5970                            version,
5971                            std::slice::from_ref(&marker),
5972                        )
5973                    },
5974                    timeout,
5975                )
5976                .await
5977            };
5978            let body = match body {
5979                Ok(b) => b,
5980                Err(e) if e.is_retriable() => {
5981                    let _ = self.conns.remove(&node);
5982                    self.wait_retry(&mut attempt, deadline).await?;
5983                    continue;
5984                }
5985                Err(e) => return Err(e),
5986            };
5987            let resp = decode_write_txn_markers_response(&mut body.clone(), version)?;
5988            let error_code = resp
5989                .iter()
5990                .flat_map(|m| m.topics.iter())
5991                .flat_map(|t| t.partitions.iter())
5992                .map(|p| p.error_code)
5993                .find(|&c| c != 0)
5994                .unwrap_or(0);
5995            if error_code == 0 {
5996                return Ok(());
5997            }
5998            let e = Error::broker(error_code, "WriteTxnMarkers");
5999            if matches!(
6000                error_code,
6001                error::FENCED_LEADER_EPOCH | error::UNKNOWN_LEADER_EPOCH
6002            ) || e.is_retriable()
6003            {
6004                self.cluster.invalidate_topic(&spec.topic);
6005                let _ = self.conns.remove(&node);
6006                self.wait_retry(&mut attempt, deadline).await?;
6007                let topics = [spec.topic.clone()];
6008                self.refresh_metadata(Some(&topics)).await?;
6009                continue;
6010            }
6011            return Err(e);
6012        }
6013    }
6014
6015    /// Describe transactional.id state (DescribeTransactions api 65).
6016    ///
6017    /// Lands on the transaction coordinator (`FindCoordinator`
6018    /// `key_type=1`). `COORDINATOR_LOAD_IN_PROGRESS` /
6019    /// `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh the
6020    /// coordinator and retry. This is not `NOT_CONTROLLER` (41).
6021    /// FindCoordinator v4+ CoordinatorKeys array of N (KIP-699): one
6022    /// FindCoordinator per retry for uncached transactional ids.
6023    /// DescribeTransactions is one RPC per coordinator. Brokers that
6024    /// only speak FindCoordinator v1–v3 get one FindCoordinator per
6025    /// uncached id. Empty input is a no-op. Optional at [`Self::new`]
6026    /// (Kafka 2.5+ / KIP-573); a broker that omits api 65 returns
6027    /// [`Error::Unsupported`]. DescribeTransactions has no TimeoutMs;
6028    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
6029    /// one-shot deadline, use [`Self::describe_transactions_timeout`].
6030    pub async fn describe_transactions(
6031        &mut self,
6032        transactional_ids: &[&str],
6033    ) -> Result<Vec<TransactionState>> {
6034        let timeout = self.cfg.request_timeout;
6035        self.describe_transactions_timeout(transactional_ids, timeout)
6036            .await
6037    }
6038
6039    /// [`Self::describe_transactions`] with a one-shot RPC deadline (Java
6040    /// `DescribeTransactionsOptions.timeoutMs`).
6041    ///
6042    /// DescribeTransactions has no TimeoutMs; `timeout` is the RPC
6043    /// deadline and the coordinator retry budget.
6044    pub async fn describe_transactions_timeout(
6045        &mut self,
6046        transactional_ids: &[&str],
6047        timeout: Duration,
6048    ) -> Result<Vec<TransactionState>> {
6049        let ids: Vec<String> = transactional_ids.iter().map(|s| (*s).to_string()).collect();
6050        if ids.is_empty() {
6051            return Ok(Vec::new());
6052        }
6053        let version = self.describe_transactions_version.ok_or_else(|| {
6054            Error::Unsupported("broker does not support DescribeTransactions".into())
6055        })?;
6056        let deadline = Instant::now() + timeout;
6057        let mut attempt = 0u32;
6058        let mut out: Vec<Option<TransactionState>> = vec![None; ids.len()];
6059        let mut pending: Vec<usize> = (0..ids.len()).collect();
6060        loop {
6061            let by_node = self.txn_coord_nodes(&ids, &pending).await?;
6062            let mut nodes: Vec<i32> = by_node.keys().copied().collect();
6063            nodes.sort_unstable();
6064            let mut still = Vec::new();
6065            for node in nodes {
6066                let idxs = by_node.get(&node).cloned().unwrap_or_default();
6067                match self
6068                    .describe_transactions_on_node(node, &ids, &idxs, version, timeout)
6069                    .await
6070                {
6071                    Ok(done) => {
6072                        for (i, t) in done {
6073                            if error::coordinator_retriable(t.error_code) {
6074                                self.invalidate_txn_coord_idxs(&ids, &[i], node);
6075                                still.push(i);
6076                            } else if let Some(slot) = out.get_mut(i) {
6077                                *slot = Some(t);
6078                            }
6079                        }
6080                    }
6081                    Err(e) if e.is_retriable() => {
6082                        self.invalidate_txn_coord_idxs(&ids, &idxs, node);
6083                        still.extend(idxs);
6084                    }
6085                    Err(e) => return Err(e),
6086                }
6087            }
6088            pending = still;
6089            if pending.is_empty() {
6090                break;
6091            }
6092            self.wait_retry(&mut attempt, deadline).await?;
6093        }
6094        out.into_iter()
6095            .zip(ids)
6096            .map(|(t, id)| {
6097                t.ok_or_else(|| Error::protocol(format!("DescribeTransactions missing {id}")))
6098            })
6099            .collect()
6100    }
6101
6102    /// List transactional.id state (ListTransactions api 66).
6103    ///
6104    /// Lands on the transaction coordinator (`FindCoordinator`
6105    /// `key_type=1`). `COORDINATOR_LOAD_IN_PROGRESS` /
6106    /// `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh the
6107    /// coordinator and retry. This is not `NOT_CONTROLLER` (41). Top-level
6108    /// `error_code` (bytes 4–5), not a first-result field.
6109    /// Duration is unfiltered (`-1`). See
6110    /// [`Self::list_transactions_with_duration`] for Java
6111    /// `ListTransactionsOptions.filterOnDuration`. Optional at
6112    /// [`Self::new`] (Kafka 2.5+ / KIP-573); a broker that omits api 66
6113    /// returns [`Error::Unsupported`]. ListTransactions has no TimeoutMs;
6114    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
6115    /// one-shot deadline, use [`Self::list_transactions_timeout`].
6116    pub async fn list_transactions(
6117        &mut self,
6118        state_filters: &[&str],
6119        producer_id_filters: &[i64],
6120    ) -> Result<Vec<TransactionListing>> {
6121        let timeout = self.cfg.request_timeout;
6122        self.list_transactions_timeout(state_filters, producer_id_filters, timeout)
6123            .await
6124    }
6125
6126    /// [`Self::list_transactions`] with a one-shot RPC deadline (Java
6127    /// `ListTransactionsOptions.timeoutMs`).
6128    ///
6129    /// ListTransactions has no TimeoutMs; `timeout` is the RPC deadline
6130    /// and the coordinator retry budget. DurationFilter stays `-1`
6131    /// (unfiltered). See [`Self::list_transactions_with_duration_timeout`]
6132    /// for `filterOnDuration` plus deadline.
6133    pub async fn list_transactions_timeout(
6134        &mut self,
6135        state_filters: &[&str],
6136        producer_id_filters: &[i64],
6137        timeout: Duration,
6138    ) -> Result<Vec<TransactionListing>> {
6139        self.list_transactions_with_duration_timeout(
6140            state_filters,
6141            producer_id_filters,
6142            -1,
6143            timeout,
6144        )
6145        .await
6146    }
6147
6148    /// List every transaction (Java `Admin.listTransactions()`).
6149    ///
6150    /// Same wire as [`Self::list_transactions`] with empty state and
6151    /// producer-id filters and DurationFilter `-1` (Java
6152    /// `ListTransactionsOptions` default).
6153    pub async fn list_transactions_all(&mut self) -> Result<Vec<TransactionListing>> {
6154        self.list_transactions(&[], &[]).await
6155    }
6156
6157    /// [`Self::list_transactions_all`] with a one-shot RPC deadline (Java
6158    /// `ListTransactionsOptions.timeoutMs`).
6159    pub async fn list_transactions_all_timeout(
6160        &mut self,
6161        timeout: Duration,
6162    ) -> Result<Vec<TransactionListing>> {
6163        self.list_transactions_timeout(&[], &[], timeout).await
6164    }
6165
6166    /// ListTransactions with a duration filter (Java `listTransactions`
6167    /// plus `ListTransactionsOptions.filterOnDuration`).
6168    ///
6169    /// Negative `duration_ms` means no duration filter (Java default `-1`).
6170    /// v1 sends `DurationFilter` INT64 (KIP-994). Java
6171    /// `ListTransactionsRequest.Builder.build` rejects a non-negative
6172    /// DurationFilter on v0 ([`Error::Unsupported`]). Kafka 4.0
6173    /// `validVersions` is `0-1`. This crate speaks 0–1. v2
6174    /// TransactionalIdPattern is not spoken. ListTransactions has no
6175    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
6176    /// For a one-shot deadline, use
6177    /// [`Self::list_transactions_with_duration_timeout`].
6178    pub async fn list_transactions_with_duration(
6179        &mut self,
6180        state_filters: &[&str],
6181        producer_id_filters: &[i64],
6182        duration_ms: i64,
6183    ) -> Result<Vec<TransactionListing>> {
6184        let timeout = self.cfg.request_timeout;
6185        self.list_transactions_with_duration_timeout(
6186            state_filters,
6187            producer_id_filters,
6188            duration_ms,
6189            timeout,
6190        )
6191        .await
6192    }
6193
6194    /// [`Self::list_transactions_with_duration`] with a one-shot RPC
6195    /// deadline (Java `ListTransactionsOptions.filterOnDuration` and
6196    /// `timeoutMs`).
6197    ///
6198    /// ListTransactions has no TimeoutMs; `timeout` is the RPC deadline
6199    /// and the coordinator retry budget. `duration_ms` is DurationFilter
6200    /// (v1), not TimeoutMs.
6201    pub async fn list_transactions_with_duration_timeout(
6202        &mut self,
6203        state_filters: &[&str],
6204        producer_id_filters: &[i64],
6205        duration_ms: i64,
6206        timeout: Duration,
6207    ) -> Result<Vec<TransactionListing>> {
6208        let states: Vec<String> = state_filters.iter().map(|s| (*s).to_string()).collect();
6209        let pids = producer_id_filters.to_vec();
6210        // ListTransactions has no transactional.id; FindCoordinator still
6211        // needs a key. Empty string is the no-id lookup used here.
6212        const COORD_KEY: &str = "";
6213        let version = self
6214            .list_transactions_version
6215            .ok_or_else(|| Error::Unsupported("broker does not support ListTransactions".into()))?;
6216        let deadline = Instant::now() + timeout;
6217        let mut attempt = 0u32;
6218        loop {
6219            let stale = self.txn_coord.as_ref().is_none_or(|(k, _)| k != COORD_KEY);
6220            if stale {
6221                let node = self.discover_txn_coord(COORD_KEY).await?;
6222                self.txn_coord = Some((COORD_KEY.to_string(), node));
6223            }
6224            let node = self
6225                .txn_coord
6226                .as_ref()
6227                .map(|(_, n)| *n)
6228                .ok_or_else(|| Error::protocol("missing transaction coordinator"))?;
6229            self.connect_node(node).await?;
6230            let body = {
6231                let conn = self
6232                    .conns
6233                    .get_mut(&node)
6234                    .ok_or_else(|| Error::protocol("missing list_transactions conn"))?;
6235                conn.roundtrip(
6236                    LIST_TRANSACTIONS,
6237                    version,
6238                    |buf| {
6239                        encode_list_transactions_request(buf, version, &states, &pids, duration_ms)
6240                    },
6241                    timeout,
6242                )
6243                .await
6244            };
6245            let body = match body {
6246                Ok(b) => b,
6247                Err(e) if e.is_retriable() => {
6248                    let _ = self.conns.remove(&node);
6249                    self.txn_coord = None;
6250                    self.wait_retry(&mut attempt, deadline).await?;
6251                    continue;
6252                }
6253                Err(e) => return Err(e),
6254            };
6255            let resp = decode_list_transactions_response(&mut body.clone(), version)?;
6256            if error::coordinator_retriable(resp.error_code) {
6257                // 14/15/16: FindCoordinator, then the new txn coordinator.
6258                self.txn_coord = None;
6259                let _ = self.conns.remove(&node);
6260                self.wait_retry(&mut attempt, deadline).await?;
6261                continue;
6262            }
6263            if resp.error_code != 0 {
6264                return Err(Error::broker(resp.error_code, "ListTransactions"));
6265            }
6266            return Ok(resp.transaction_states);
6267        }
6268    }
6269
6270    /// Describe ACL bindings (`DescribeAcls`) matching `resource_type`.
6271    ///
6272    /// Negotiates v0–v3 (v0–v1 classic; v2+ flexible). v1+ sends
6273    /// PatternTypeFilter ANY. Operation and Permission are ANY (Java
6274    /// `AccessControlEntryFilter.ANY`). Kafka 4.0 `validVersions` is
6275    /// `1-3`. v4+ is not spoken.
6276    ///
6277    /// `resource_type` is [`crate::AclResourceType`] or a protocol `i8`
6278    /// (`ACL_RESOURCE_TOPIC`, …). For principal / host / name / operation
6279    /// filters, use [`Self::describe_acls_with`]. DescribeAcls has no
6280    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
6281    /// For a one-shot deadline, use [`Self::describe_acls_timeout`].
6282    pub async fn describe_acls(&mut self, resource_type: impl Into<i8>) -> Result<Vec<AclBinding>> {
6283        self.describe_acls_with(&AclBindingFilter::resource_type(resource_type))
6284            .await
6285    }
6286
6287    /// [`Self::describe_acls`] with a one-shot RPC deadline (Java
6288    /// `DescribeAclsOptions.timeoutMs`).
6289    pub async fn describe_acls_timeout(
6290        &mut self,
6291        resource_type: impl Into<i8>,
6292        timeout: Duration,
6293    ) -> Result<Vec<AclBinding>> {
6294        self.describe_acls_with_timeout(&AclBindingFilter::resource_type(resource_type), timeout)
6295            .await
6296    }
6297
6298    /// [`Self::describe_acls`] with a Java `AclBindingFilter`.
6299    ///
6300    /// DescribeAcls has no TimeoutMs; the RPC deadline is
6301    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
6302    /// [`Self::describe_acls_with_timeout`].
6303    pub async fn describe_acls_with(
6304        &mut self,
6305        filter: &AclBindingFilter,
6306    ) -> Result<Vec<AclBinding>> {
6307        let timeout = self.cfg.request_timeout;
6308        self.describe_acls_with_timeout(filter, timeout).await
6309    }
6310
6311    /// [`Self::describe_acls_with`] with Java `DescribeAclsOptions.timeoutMs`.
6312    ///
6313    /// DescribeAcls has no TimeoutMs; `timeout` is the RPC deadline.
6314    pub async fn describe_acls_with_timeout(
6315        &mut self,
6316        filter: &AclBindingFilter,
6317        timeout: Duration,
6318    ) -> Result<Vec<AclBinding>> {
6319        let version = self.describe_acls_version;
6320        let body = self
6321            .roundtrip_bootstrap(
6322                DESCRIBE_ACLS,
6323                version,
6324                |buf| encode_describe_acls_request(buf, version, filter),
6325                timeout,
6326            )
6327            .await?;
6328        let (acls, ..) = decode_describe_acls_response(&mut body.clone(), version)?;
6329        Ok(acls)
6330    }
6331
6332    /// Describe every ACL (Java `describeAcls(AclBindingFilter.ANY)`).
6333    ///
6334    /// Same wire as [`Self::describe_acls_with`] with
6335    /// [`AclBindingFilter::any`]. DescribeAcls has no TimeoutMs; the RPC
6336    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
6337    /// deadline, use [`Self::describe_acls_any_timeout`].
6338    pub async fn describe_acls_any(&mut self) -> Result<Vec<AclBinding>> {
6339        self.describe_acls_with(&AclBindingFilter::any()).await
6340    }
6341
6342    /// [`Self::describe_acls_any`] with a one-shot RPC deadline (Java
6343    /// `DescribeAclsOptions.timeoutMs`).
6344    ///
6345    /// DescribeAcls has no TimeoutMs; `timeout` is the RPC deadline.
6346    pub async fn describe_acls_any_timeout(
6347        &mut self,
6348        timeout: Duration,
6349    ) -> Result<Vec<AclBinding>> {
6350        self.describe_acls_with_timeout(&AclBindingFilter::any(), timeout)
6351            .await
6352    }
6353
6354    /// Replace configs (`AlterConfigs`, legacy api 33).
6355    ///
6356    /// Negotiates v0–v2 (v0–v1 classic; v2 flexible). v1 response adds
6357    /// ThrottleTimeMs (KIP-219). Kafka 4.0 `validVersions` is `0-2`.
6358    /// v3+ is not spoken.
6359    ///
6360    /// Prefer [`Self::incremental_alter_configs`] on modern brokers.
6361    /// Returns the first resource's error code. For several resources in
6362    /// one RPC (Java `alterConfigs(Map)`), use [`Self::alter_configs_for`].
6363    /// AlterConfigs has no TimeoutMs; the RPC deadline is
6364    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
6365    /// [`Self::alter_configs_timeout`].
6366    pub async fn alter_configs(
6367        &mut self,
6368        resource: &ConfigResource,
6369        configs: &[(String, Option<String>)],
6370        validate_only: bool,
6371    ) -> Result<i16> {
6372        let results = self
6373            .alter_configs_for(
6374                &[ConfigReplacement::new(
6375                    resource.clone(),
6376                    configs.iter().cloned(),
6377                )],
6378                validate_only,
6379            )
6380            .await?;
6381        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
6382    }
6383
6384    /// [`Self::alter_configs`] with a one-shot RPC deadline (Java
6385    /// `AlterConfigsOptions.timeoutMs`).
6386    ///
6387    /// AlterConfigs has no TimeoutMs; `timeout` is the RPC deadline.
6388    pub async fn alter_configs_timeout(
6389        &mut self,
6390        resource: &ConfigResource,
6391        configs: &[(String, Option<String>)],
6392        timeout: Duration,
6393        validate_only: bool,
6394    ) -> Result<i16> {
6395        let results = self
6396            .alter_configs_for_timeout(
6397                &[ConfigReplacement::new(
6398                    resource.clone(),
6399                    configs.iter().cloned(),
6400                )],
6401                timeout,
6402                validate_only,
6403            )
6404            .await?;
6405        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
6406    }
6407
6408    /// [`Self::alter_configs`] with a Java `Config` (`alterConfigs(Map)` one
6409    /// resource).
6410    ///
6411    /// AlterConfigs has no TimeoutMs; the RPC deadline is
6412    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
6413    /// [`Self::alter_configs_with_timeout`].
6414    pub async fn alter_configs_with(
6415        &mut self,
6416        resource: &ConfigResource,
6417        config: &Config,
6418        validate_only: bool,
6419    ) -> Result<i16> {
6420        let results = self
6421            .alter_configs_for(
6422                &[ConfigReplacement::from_config(resource.clone(), config)],
6423                validate_only,
6424            )
6425            .await?;
6426        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
6427    }
6428
6429    /// [`Self::alter_configs_with`] with a one-shot RPC deadline (Java
6430    /// `AlterConfigsOptions.timeoutMs`).
6431    ///
6432    /// AlterConfigs has no TimeoutMs; `timeout` is the RPC deadline.
6433    pub async fn alter_configs_with_timeout(
6434        &mut self,
6435        resource: &ConfigResource,
6436        config: &Config,
6437        timeout: Duration,
6438        validate_only: bool,
6439    ) -> Result<i16> {
6440        let results = self
6441            .alter_configs_for_timeout(
6442                &[ConfigReplacement::from_config(resource.clone(), config)],
6443                timeout,
6444                validate_only,
6445            )
6446            .await?;
6447        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
6448    }
6449
6450    /// [`Self::alter_configs`] for several resources (Java `alterConfigs(Map)`;
6451    /// AlterConfigs Resources of N).
6452    ///
6453    /// Empty `updates` is a no-op.
6454    pub async fn alter_configs_for(
6455        &mut self,
6456        updates: &[ConfigReplacement],
6457        validate_only: bool,
6458    ) -> Result<Vec<AlterConfigsResourceResult>> {
6459        let timeout = self.cfg.request_timeout;
6460        self.alter_configs_for_timeout(updates, timeout, validate_only)
6461            .await
6462    }
6463
6464    /// [`Self::alter_configs_for`] with Java `AlterConfigsOptions.timeoutMs`.
6465    ///
6466    /// Empty `updates` is a no-op. AlterConfigs has no TimeoutMs;
6467    /// `timeout` is the RPC deadline.
6468    pub async fn alter_configs_for_timeout(
6469        &mut self,
6470        updates: &[ConfigReplacement],
6471        timeout: Duration,
6472        validate_only: bool,
6473    ) -> Result<Vec<AlterConfigsResourceResult>> {
6474        if updates.is_empty() {
6475            return Ok(Vec::new());
6476        }
6477        let resources: Vec<AlterConfigsResource> = updates
6478            .iter()
6479            .map(|u| AlterConfigsResource {
6480                resource_type: u.resource.resource_type,
6481                name: u.resource.name.clone(),
6482                configs: u
6483                    .configs
6484                    .iter()
6485                    .map(|(n, v)| TopicConfig {
6486                        name: n.clone(),
6487                        value: v.clone(),
6488                    })
6489                    .collect(),
6490            })
6491            .collect();
6492        let version = self.legacy_alter_version;
6493        let body = self
6494            .roundtrip_bootstrap(
6495                ALTER_CONFIGS,
6496                version,
6497                |buf| {
6498                    encode_alter_configs_resources_request(buf, version, &resources, validate_only)
6499                },
6500                timeout,
6501            )
6502            .await?;
6503        let (results, ..) = decode_alter_configs_resource_results(&mut body.clone(), version)?;
6504        Ok(results)
6505    }
6506
6507    async fn fetch_metadata(&mut self, topics: Option<&[String]>) -> Result<MetadataResponse> {
6508        self.fetch_metadata_with(topics, false).await
6509    }
6510
6511    async fn fetch_metadata_with(
6512        &mut self,
6513        topics: Option<&[String]>,
6514        include_topic_authorized_operations: bool,
6515    ) -> Result<MetadataResponse> {
6516        let owned =
6517            topics.map(|names| MetadataRequestTopic::convert_from_names(names.iter().cloned()));
6518        let timeout = self.cfg.request_timeout;
6519        self.fetch_metadata_request_with(
6520            owned.as_deref(),
6521            include_topic_authorized_operations,
6522            timeout,
6523        )
6524        .await
6525    }
6526
6527    async fn fetch_metadata_request_with(
6528        &mut self,
6529        topics: Option<&[MetadataRequestTopic]>,
6530        include_topic_authorized_operations: bool,
6531        timeout: Duration,
6532    ) -> Result<MetadataResponse> {
6533        let version = self.metadata_version;
6534        let body = self
6535            .roundtrip_bootstrap(
6536                METADATA,
6537                version,
6538                |buf| {
6539                    encode_metadata_request_topics(
6540                        buf,
6541                        version,
6542                        topics,
6543                        false,
6544                        include_topic_authorized_operations,
6545                    )
6546                },
6547                timeout,
6548            )
6549            .await?;
6550        let md = decode_metadata_response(&mut body.clone(), version)?;
6551        md.check()?;
6552        self.cluster.apply(&md, version);
6553        Ok(md)
6554    }
6555
6556    async fn refresh_metadata(&mut self, topics: Option<&[String]>) -> Result<()> {
6557        self.fetch_metadata(topics).await.map(|_| ())
6558    }
6559
6560    /// Wait [`AdminConfig::retry_backoff`] (exponential) or [`Error::Timeout`].
6561    async fn wait_retry(&self, attempt: &mut u32, deadline: Instant) -> Result<()> {
6562        if Instant::now() >= deadline {
6563            return Err(Error::Timeout);
6564        }
6565        crate::config::sleep_retry_backoff(
6566            self.cfg.retry_backoff,
6567            self.cfg.retry_backoff_max,
6568            *attempt,
6569            deadline,
6570        )
6571        .await;
6572        *attempt = attempt.saturating_add(1);
6573        if Instant::now() >= deadline {
6574            return Err(Error::Timeout);
6575        }
6576        Ok(())
6577    }
6578
6579    async fn ensure_bootstrap(&mut self) -> Result<()> {
6580        if !self.conn.idle_expired(self.cfg.connections_max_idle) {
6581            return Ok(());
6582        }
6583        let addr = self.conn.addr().to_string();
6584        self.conn = self.open_node_conn(&addr).await?;
6585        Ok(())
6586    }
6587
6588    /// Reconnect the bootstrap socket if it has been idle, then round-trip.
6589    async fn roundtrip_bootstrap(
6590        &mut self,
6591        api_key: i16,
6592        api_version: i16,
6593        encode_body: impl FnOnce(&mut BytesMut) -> Result<()>,
6594        timeout: Duration,
6595    ) -> Result<Bytes> {
6596        self.ensure_bootstrap().await?;
6597        self.conn
6598            .roundtrip(api_key, api_version, encode_body, timeout)
6599            .await
6600    }
6601
6602    async fn connect_node(&mut self, node: i32) -> Result<()> {
6603        if self
6604            .conns
6605            .get(&node)
6606            .is_some_and(|c| c.idle_expired(self.cfg.connections_max_idle))
6607        {
6608            let _ = self.conns.remove(&node);
6609        }
6610        if self.conns.contains_key(&node) {
6611            return Ok(());
6612        }
6613        let addr = self
6614            .cluster
6615            .brokers
6616            .get(&node)
6617            .cloned()
6618            .ok_or_else(|| Error::protocol(format!("unknown broker {node}")))?;
6619        let deadline = Instant::now() + self.cfg.request_timeout;
6620        loop {
6621            let fails = self.reconnect_fails.get(&node).copied().unwrap_or(0);
6622            crate::config::sleep_reconnect_backoff(
6623                self.cfg.reconnect_backoff,
6624                self.cfg.reconnect_backoff_max,
6625                fails,
6626            )
6627            .await;
6628            if Instant::now() >= deadline {
6629                return Err(Error::Timeout);
6630            }
6631            match self.open_node_conn(&addr).await {
6632                Ok(conn) => {
6633                    let _ = self.reconnect_fails.remove(&node);
6634                    let _prev = self.conns.insert(node, conn);
6635                    return Ok(());
6636                }
6637                Err(e) if e.is_retriable() => {
6638                    let _fails =
6639                        crate::config::bump_reconnect_fails(&mut self.reconnect_fails, node);
6640                    if Instant::now() >= deadline {
6641                        return Err(Error::Timeout);
6642                    }
6643                }
6644                Err(e) => {
6645                    let _fails =
6646                        crate::config::bump_reconnect_fails(&mut self.reconnect_fails, node);
6647                    return Err(e);
6648                }
6649            }
6650        }
6651    }
6652
6653    async fn open_node_conn(&self, addr: &str) -> Result<BrokerConn> {
6654        let mut conn = BrokerConn::connect_tls(
6655            addr,
6656            &self.cfg.client_id,
6657            self.cfg.connect_timeout,
6658            self.cfg.tls.as_ref(),
6659        )
6660        .await?;
6661        conn.set_stats(Arc::clone(&self.stats));
6662        let versions_resp =
6663            crate::protocol::api::negotiate_api_versions(&mut conn, self.cfg.request_timeout)
6664                .await?;
6665        sasl::apply_api_keys(&mut conn, &versions_resp.api_keys);
6666        sasl::authenticate(
6667            &mut conn,
6668            self.cfg.sasl_plain.as_ref(),
6669            self.cfg.sasl_scram.as_ref(),
6670            self.cfg.sasl_scram_sha512.as_ref(),
6671            self.cfg.sasl_oauthbearer.as_deref(),
6672            self.cfg.sasl_oauthbearer_oidc.as_ref(),
6673            self.cfg.request_timeout,
6674        )
6675        .await?;
6676        Ok(conn)
6677    }
6678
6679    /// Delete records before `offset` (`DeleteRecords`).
6680    ///
6681    /// Negotiates v0–v2 (v0–v1 classic; v2 flexible). v1 response adds
6682    /// ThrottleTimeMs (KIP-219). Kafka 4.0 `validVersions` is `0-2`.
6683    /// v3+ is not spoken.
6684    ///
6685    /// `offset` is [`RecordsToDelete`] or INT64 (Java
6686    /// `RecordsToDelete.beforeOffset(long)`).
6687    /// [`crate::protocol::admin::DeleteRecordsRequest::HIGH_WATERMARK`]
6688    /// truncates to the high watermark. Lands on the Metadata
6689    /// partition leader. `NOT_LEADER_OR_FOLLOWER` (6) and other
6690    /// retriable codes refresh Metadata and retry on the new leader.
6691    /// Returns [`DeletedRecords`] (`lowWatermark` plus per-partition
6692    /// ErrorCode). `timeout_ms` is
6693    /// DeleteRecords TimeoutMs. The RPC deadline is
6694    /// [`AdminConfig::request_timeout`]. For several partitions, use
6695    /// [`Self::delete_records_for`]. For a one-shot timeout that drives
6696    /// both the RPC deadline and TimeoutMs, use
6697    /// [`Self::delete_records_timeout`].
6698    pub async fn delete_records(
6699        &mut self,
6700        partition: impl Into<crate::TopicPartition>,
6701        offset: impl Into<i64>,
6702        timeout_ms: i32,
6703    ) -> Result<DeletedRecords> {
6704        let timeout = self.cfg.request_timeout;
6705        self.delete_records_one(partition.into(), offset.into(), timeout_ms, timeout)
6706            .await
6707            .map(DeletedRecords::from)
6708    }
6709
6710    /// [`Self::delete_records`] with a one-shot timeout (Java
6711    /// `DeleteRecordsOptions.timeoutMs`).
6712    ///
6713    /// `timeout` is the RPC deadline and DeleteRecords TimeoutMs.
6714    pub async fn delete_records_timeout(
6715        &mut self,
6716        partition: impl Into<crate::TopicPartition>,
6717        offset: impl Into<i64>,
6718        timeout: Duration,
6719    ) -> Result<DeletedRecords> {
6720        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
6721        self.delete_records_one(partition.into(), offset.into(), timeout_ms, timeout)
6722            .await
6723            .map(DeletedRecords::from)
6724    }
6725
6726    /// Delete records on several partitions (Java `deleteRecords(Map)`).
6727    ///
6728    /// Each item is a [`crate::TopicPartition`] and an offset
6729    /// ([`RecordsToDelete`] or INT64). One DeleteRecords RPC per
6730    /// Metadata partition leader. Empty input is a no-op. Returns
6731    /// [`DeletedRecords`] per partition. TimeoutMs and
6732    /// the RPC deadline are [`AdminConfig::request_timeout`]. For a
6733    /// one-shot timeout, use [`Self::delete_records_for_timeout`].
6734    pub async fn delete_records_for<Tp, Off>(
6735        &mut self,
6736        records: impl IntoIterator<Item = (Tp, Off)>,
6737    ) -> Result<Vec<(crate::TopicPartition, DeletedRecords)>>
6738    where
6739        Tp: Into<crate::TopicPartition>,
6740        Off: Into<i64>,
6741    {
6742        let timeout = self.cfg.request_timeout;
6743        self.delete_records_for_timeout(records, timeout).await
6744    }
6745
6746    /// [`Self::delete_records_for`] with a one-shot timeout (Java
6747    /// `deleteRecords` plus `DeleteRecordsOptions.timeoutMs`).
6748    ///
6749    /// `timeout` is the RPC deadline and DeleteRecords TimeoutMs.
6750    pub async fn delete_records_for_timeout<Tp, Off>(
6751        &mut self,
6752        records: impl IntoIterator<Item = (Tp, Off)>,
6753        timeout: Duration,
6754    ) -> Result<Vec<(crate::TopicPartition, DeletedRecords)>>
6755    where
6756        Tp: Into<crate::TopicPartition>,
6757        Off: Into<i64>,
6758    {
6759        let timeout_ms = crate::consumer::duration_millis_i32(timeout);
6760        let raw = self
6761            .delete_records_for_with(records, timeout_ms, timeout)
6762            .await?;
6763        Ok(raw
6764            .into_iter()
6765            .map(|(tp, low, err)| (tp, DeletedRecords::with_error_code(low, err)))
6766            .collect())
6767    }
6768
6769    async fn delete_records_one(
6770        &mut self,
6771        tp: crate::TopicPartition,
6772        offset: i64,
6773        timeout_ms: i32,
6774        timeout: Duration,
6775    ) -> Result<(i64, i16)> {
6776        let mut out = self
6777            .delete_records_for_with([(tp, offset)], timeout_ms, timeout)
6778            .await?;
6779        match out.pop() {
6780            Some((_, low, err)) => Ok((low, err)),
6781            None => Err(Error::protocol("missing DeleteRecords result")),
6782        }
6783    }
6784
6785    async fn delete_records_for_with<Tp, Off>(
6786        &mut self,
6787        records: impl IntoIterator<Item = (Tp, Off)>,
6788        timeout_ms: i32,
6789        timeout: Duration,
6790    ) -> Result<Vec<(crate::TopicPartition, i64, i16)>>
6791    where
6792        Tp: Into<crate::TopicPartition>,
6793        Off: Into<i64>,
6794    {
6795        let records: Vec<(crate::TopicPartition, i64)> = records
6796            .into_iter()
6797            .map(|(tp, off)| (tp.into(), off.into()))
6798            .collect();
6799        if records.is_empty() {
6800            return Ok(Vec::new());
6801        }
6802        let version = self.delete_records_version;
6803        let deadline = Instant::now() + timeout;
6804        let mut attempt = 0u32;
6805        let mut out: Vec<Option<(i64, i16)>> = vec![None; records.len()];
6806        let mut pending: Vec<usize> = (0..records.len()).collect();
6807        loop {
6808            if pending.is_empty() {
6809                break;
6810            }
6811            let mut need: Vec<String> = Vec::new();
6812            for &i in &pending {
6813                let Some((tp, _)) = records.get(i) else {
6814                    continue;
6815                };
6816                if self.cluster.leader(&tp.topic, tp.partition).is_err()
6817                    && !need.iter().any(|t| t == &tp.topic)
6818                {
6819                    need.push(tp.topic.clone());
6820                }
6821            }
6822            if !need.is_empty() {
6823                self.refresh_metadata(Some(&need)).await?;
6824            }
6825            let mut by_node: HashMap<i32, Vec<usize>> = HashMap::new();
6826            let mut nodes: Vec<i32> = Vec::new();
6827            for &i in &pending {
6828                let (tp, _) = records
6829                    .get(i)
6830                    .ok_or_else(|| Error::protocol("missing DeleteRecords query"))?;
6831                let (node, _) = self.cluster.leader(&tp.topic, tp.partition)?;
6832                match by_node.entry(node) {
6833                    std::collections::hash_map::Entry::Vacant(slot) => {
6834                        nodes.push(node);
6835                        let _ = slot.insert(vec![i]);
6836                    }
6837                    std::collections::hash_map::Entry::Occupied(mut slot) => {
6838                        slot.get_mut().push(i);
6839                    }
6840                }
6841            }
6842            let mut still = Vec::new();
6843            for node in nodes {
6844                let idxs = by_node.remove(&node).unwrap_or_default();
6845                match self
6846                    .delete_records_on_node(node, version, timeout_ms, &records, &idxs, timeout)
6847                    .await
6848                {
6849                    Ok((done, retry)) => {
6850                        for (i, low, err) in done {
6851                            if let Some(slot) = out.get_mut(i) {
6852                                *slot = Some((low, err));
6853                            }
6854                        }
6855                        still.extend(retry);
6856                    }
6857                    Err(e) if e.is_retriable() => {
6858                        let _ = self.conns.remove(&node);
6859                        still.extend(idxs);
6860                    }
6861                    Err(e) => return Err(e),
6862                }
6863            }
6864            pending = still;
6865            if pending.is_empty() {
6866                break;
6867            }
6868            self.wait_retry(&mut attempt, deadline).await?;
6869            for &i in &pending {
6870                if let Some((tp, _)) = records.get(i) {
6871                    self.cluster.invalidate_topic(&tp.topic);
6872                }
6873            }
6874            let topics: Vec<String> = {
6875                let mut t = Vec::new();
6876                for &i in &pending {
6877                    if let Some((tp, _)) = records.get(i) {
6878                        if !t.iter().any(|n| n == &tp.topic) {
6879                            t.push(tp.topic.clone());
6880                        }
6881                    }
6882                }
6883                t
6884            };
6885            if !topics.is_empty() {
6886                self.refresh_metadata(Some(&topics)).await?;
6887            }
6888        }
6889        out.into_iter()
6890            .zip(records)
6891            .map(|(got, (tp, _))| {
6892                got.map(|(low, err)| (tp, low, err))
6893                    .ok_or_else(|| Error::protocol("DeleteRecords missing result"))
6894            })
6895            .collect()
6896    }
6897
6898    async fn delete_records_on_node(
6899        &mut self,
6900        node: i32,
6901        version: i16,
6902        timeout_ms: i32,
6903        records: &[(crate::TopicPartition, i64)],
6904        idxs: &[usize],
6905        timeout: Duration,
6906    ) -> Result<(Vec<(usize, i64, i16)>, Vec<usize>)> {
6907        let topics = delete_records_topics(records, idxs);
6908        self.connect_node(node).await?;
6909        let body = {
6910            let conn = self
6911                .conns
6912                .get_mut(&node)
6913                .ok_or_else(|| Error::protocol("missing delete_records conn"))?;
6914            conn.roundtrip(
6915                DELETE_RECORDS,
6916                version,
6917                |buf| encode_delete_records_topics_request(buf, version, &topics, timeout_ms),
6918                timeout,
6919            )
6920            .await
6921        }?;
6922        let (resp, ..) = decode_delete_records_topics_response(&mut body.clone(), version)?;
6923        let mut by_key: HashMap<(String, i32), VecDeque<(i64, i16)>> = HashMap::new();
6924        for t in resp {
6925            for p in t.partitions {
6926                by_key
6927                    .entry((t.topic.clone(), p.partition))
6928                    .or_default()
6929                    .push_back((p.low_watermark, p.error_code));
6930            }
6931        }
6932        let mut done = Vec::new();
6933        let mut retry = Vec::new();
6934        for &i in idxs {
6935            let (tp, _) = records
6936                .get(i)
6937                .ok_or_else(|| Error::protocol("missing DeleteRecords query"))?;
6938            let (low, err) = by_key
6939                .get_mut(&(tp.topic.clone(), tp.partition))
6940                .and_then(VecDeque::pop_front)
6941                .ok_or_else(|| {
6942                    Error::protocol(format!(
6943                        "DeleteRecords missing {}-{}",
6944                        tp.topic, tp.partition
6945                    ))
6946                })?;
6947            if err == 0 {
6948                done.push((i, low, err));
6949                continue;
6950            }
6951            let e = Error::broker(err, format!("{}-{}", tp.topic, tp.partition));
6952            if e.is_retriable() {
6953                self.cluster.invalidate_topic(&tp.topic);
6954                let _ = self.conns.remove(&node);
6955                retry.push(i);
6956            } else {
6957                done.push((i, low, err));
6958            }
6959        }
6960        Ok((done, retry))
6961    }
6962
6963    /// ListOffsets for these partitions (Java `Admin.listOffsets`).
6964    ///
6965    /// Isolation is read-uncommitted. See
6966    /// [`Self::list_offsets_with_isolation`] for Java
6967    /// `ListOffsetsOptions.isolationLevel`. Waits up to
6968    /// [`AdminConfig::request_timeout`]. For a one-shot timeout, use
6969    /// [`Self::list_offsets_timeout`]. Each timestamp is
6970    /// [`crate::OffsetSpec`] or INT64 (see
6971    /// [`Self::list_offsets_with_isolation`]).
6972    pub async fn list_offsets<Tp, Ts>(
6973        &mut self,
6974        queries: impl IntoIterator<Item = (Tp, Ts)>,
6975    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndTimestamp)>>
6976    where
6977        Tp: Into<crate::TopicPartition>,
6978        Ts: Into<i64>,
6979    {
6980        let timeout = self.cfg.request_timeout;
6981        self.list_offsets_timeout(queries, timeout).await
6982    }
6983
6984    /// [`Self::list_offsets`] with a one-shot timeout (Java `listOffsets`
6985    /// plus `ListOffsetsOptions.timeoutMs`).
6986    ///
6987    /// `timeout` is the RPC deadline and ListOffsets v10 `TimeoutMs`.
6988    pub async fn list_offsets_timeout<Tp, Ts>(
6989        &mut self,
6990        queries: impl IntoIterator<Item = (Tp, Ts)>,
6991        timeout: Duration,
6992    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndTimestamp)>>
6993    where
6994        Tp: Into<crate::TopicPartition>,
6995        Ts: Into<i64>,
6996    {
6997        self.list_offsets_with_isolation_timeout(
6998            queries,
6999            crate::IsolationLevel::ReadUncommitted,
7000            timeout,
7001        )
7002        .await
7003    }
7004
7005    /// ListOffsets with isolation (Java `listOffsets` +
7006    /// `ListOffsetsOptions.isolationLevel`).
7007    ///
7008    /// Each item is a [`crate::TopicPartition`] and a timestamp
7009    /// ([`crate::OffsetSpec`] or INT64): [`crate::OffsetSpec::earliest`]
7010    /// / [`crate::EARLIEST_TIMESTAMP`] (`-2`), [`crate::OffsetSpec::latest`]
7011    /// / [`crate::LATEST_TIMESTAMP`] (`-1`), [`crate::OffsetSpec::max_timestamp`]
7012    /// / [`crate::MAX_TIMESTAMP`] (`-3`),
7013    /// [`crate::OffsetSpec::earliest_local`] /
7014    /// [`crate::EARLIEST_LOCAL_TIMESTAMP`] (`-4`),
7015    /// [`crate::OffsetSpec::latest_tiered`] /
7016    /// [`crate::LATEST_TIERED_TIMESTAMP`] (`-5`), or milliseconds since
7017    /// the Unix epoch. One ListOffsets
7018    /// RPC per Metadata partition leader (duplicate partitions keep
7019    /// separate timestamps). `NOT_LEADER_OR_FOLLOWER` refreshes
7020    /// Metadata and retries.
7021    /// [`crate::OffsetAndTimestamp::leader_epoch`] is ListOffsets v4+.
7022    /// v1–v5 are classic; v6–v10 are flexible. v10 `TimeoutMs` is
7023    /// [`AdminConfig::request_timeout`]. For a one-shot timeout, use
7024    /// [`Self::list_offsets_with_isolation_timeout`]. Empty input is a no-op.
7025    pub async fn list_offsets_with_isolation<Tp, Ts>(
7026        &mut self,
7027        queries: impl IntoIterator<Item = (Tp, Ts)>,
7028        isolation: crate::IsolationLevel,
7029    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndTimestamp)>>
7030    where
7031        Tp: Into<crate::TopicPartition>,
7032        Ts: Into<i64>,
7033    {
7034        let timeout = self.cfg.request_timeout;
7035        self.list_offsets_with_isolation_timeout(queries, isolation, timeout)
7036            .await
7037    }
7038
7039    /// [`Self::list_offsets_with_isolation`] with a one-shot timeout (Java
7040    /// `listOffsets` plus `ListOffsetsOptions.isolationLevel` and
7041    /// `timeoutMs`).
7042    ///
7043    /// `timeout` is the RPC deadline and ListOffsets v10 `TimeoutMs`.
7044    pub async fn list_offsets_with_isolation_timeout<Tp, Ts>(
7045        &mut self,
7046        queries: impl IntoIterator<Item = (Tp, Ts)>,
7047        isolation: crate::IsolationLevel,
7048        timeout: Duration,
7049    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndTimestamp)>>
7050    where
7051        Tp: Into<crate::TopicPartition>,
7052        Ts: Into<i64>,
7053    {
7054        let queries: Vec<(crate::TopicPartition, i64)> = queries
7055            .into_iter()
7056            .map(|(tp, ts)| (tp.into(), ts.into()))
7057            .collect();
7058        if queries.is_empty() {
7059            return Ok(Vec::new());
7060        }
7061        let version = self
7062            .versions
7063            .get(&LIST_OFFSETS)
7064            .and_then(|v| pick_version(v.min_version, v.max_version, 1, 10))
7065            .ok_or_else(|| Error::Unsupported("broker does not support ListOffsets".into()))?;
7066        let deadline = Instant::now() + timeout;
7067        let mut attempt = 0u32;
7068        let isolation = isolation.as_i8();
7069        let mut out: Vec<Option<crate::OffsetAndTimestamp>> = vec![None; queries.len()];
7070        let mut pending: Vec<usize> = (0..queries.len()).collect();
7071        loop {
7072            if pending.is_empty() {
7073                break;
7074            }
7075            let mut need: Vec<String> = Vec::new();
7076            for &i in &pending {
7077                let Some((tp, _)) = queries.get(i) else {
7078                    continue;
7079                };
7080                if self.cluster.leader(&tp.topic, tp.partition).is_err()
7081                    && !need.iter().any(|t| t == &tp.topic)
7082                {
7083                    need.push(tp.topic.clone());
7084                }
7085            }
7086            if !need.is_empty() {
7087                self.refresh_metadata(Some(&need)).await?;
7088            }
7089            let mut by_node: HashMap<i32, Vec<usize>> = HashMap::new();
7090            let mut nodes: Vec<i32> = Vec::new();
7091            for &i in &pending {
7092                let (tp, _) = queries
7093                    .get(i)
7094                    .ok_or_else(|| Error::protocol("missing ListOffsets query"))?;
7095                let (node, _) = self.cluster.leader(&tp.topic, tp.partition)?;
7096                match by_node.entry(node) {
7097                    std::collections::hash_map::Entry::Vacant(slot) => {
7098                        nodes.push(node);
7099                        let _ = slot.insert(vec![i]);
7100                    }
7101                    std::collections::hash_map::Entry::Occupied(mut slot) => {
7102                        slot.get_mut().push(i);
7103                    }
7104                }
7105            }
7106            let mut still = Vec::new();
7107            for node in nodes {
7108                let idxs = by_node.remove(&node).unwrap_or_default();
7109                match self
7110                    .list_offsets_on_node(node, version, isolation, &queries, &idxs, timeout)
7111                    .await
7112                {
7113                    Ok((done, retry)) => {
7114                        for (i, ot) in done {
7115                            if let Some(slot) = out.get_mut(i) {
7116                                *slot = Some(ot);
7117                            }
7118                        }
7119                        still.extend(retry);
7120                    }
7121                    Err(e) if e.is_retriable() => {
7122                        let _ = self.conns.remove(&node);
7123                        still.extend(idxs);
7124                    }
7125                    Err(e) => return Err(e),
7126                }
7127            }
7128            pending = still;
7129            if pending.is_empty() {
7130                break;
7131            }
7132            self.wait_retry(&mut attempt, deadline).await?;
7133            for &i in &pending {
7134                if let Some((tp, _)) = queries.get(i) {
7135                    self.cluster.invalidate_topic(&tp.topic);
7136                }
7137            }
7138            let topics: Vec<String> = {
7139                let mut t = Vec::new();
7140                for &i in &pending {
7141                    if let Some((tp, _)) = queries.get(i) {
7142                        if !t.iter().any(|n| n == &tp.topic) {
7143                            t.push(tp.topic.clone());
7144                        }
7145                    }
7146                }
7147                t
7148            };
7149            if !topics.is_empty() {
7150                self.refresh_metadata(Some(&topics)).await?;
7151            }
7152        }
7153        out.into_iter()
7154            .zip(queries)
7155            .map(|(ot, (tp, _))| {
7156                ot.map(|ot| (tp, ot))
7157                    .ok_or_else(|| Error::protocol("ListOffsets missing result"))
7158            })
7159            .collect()
7160    }
7161
7162    async fn list_offsets_on_node(
7163        &mut self,
7164        node: i32,
7165        version: i16,
7166        isolation: i8,
7167        queries: &[(crate::TopicPartition, i64)],
7168        idxs: &[usize],
7169        timeout: Duration,
7170    ) -> Result<(Vec<(usize, crate::OffsetAndTimestamp)>, Vec<usize>)> {
7171        let topics = list_offset_topic_requests(queries, idxs, &self.cluster);
7172        self.connect_node(node).await?;
7173        let body = {
7174            let conn = self
7175                .conns
7176                .get_mut(&node)
7177                .ok_or_else(|| Error::protocol("missing list_offsets conn"))?;
7178            conn.roundtrip(
7179                LIST_OFFSETS,
7180                version,
7181                |buf| {
7182                    encode_list_offsets_topics_request(
7183                        buf,
7184                        version,
7185                        isolation,
7186                        &topics,
7187                        crate::consumer::duration_millis_i32(timeout),
7188                    )
7189                },
7190                timeout,
7191            )
7192            .await
7193        }?;
7194        let (resp, ..) = decode_list_offsets_topics_response(&mut body.clone(), version)?;
7195        let mut by_key: HashMap<(String, i32), VecDeque<ListOffsetsResponsePartition>> =
7196            HashMap::new();
7197        for t in resp {
7198            for p in t.partitions {
7199                by_key
7200                    .entry((t.name.clone(), p.partition_index))
7201                    .or_default()
7202                    .push_back(p);
7203            }
7204        }
7205        let mut done = Vec::new();
7206        let mut retry = Vec::new();
7207        for &i in idxs {
7208            let (tp, _) = queries
7209                .get(i)
7210                .ok_or_else(|| Error::protocol("missing ListOffsets query"))?;
7211            let part = by_key
7212                .get_mut(&(tp.topic.clone(), tp.partition))
7213                .and_then(VecDeque::pop_front)
7214                .ok_or_else(|| {
7215                    Error::protocol(format!("ListOffsets missing {}-{}", tp.topic, tp.partition))
7216                })?;
7217            if part.error_code == 0 {
7218                done.push((
7219                    i,
7220                    crate::OffsetAndTimestamp::new(part.offset, part.timestamp)
7221                        .with_leader_epoch(part.leader_epoch),
7222                ));
7223                continue;
7224            }
7225            let e = Error::broker(part.error_code, format!("{}-{}", tp.topic, tp.partition));
7226            if matches!(
7227                part.error_code,
7228                error::FENCED_LEADER_EPOCH | error::UNKNOWN_LEADER_EPOCH
7229            ) || e.is_retriable()
7230            {
7231                self.cluster.invalidate_topic(&tp.topic);
7232                let _ = self.conns.remove(&node);
7233                retry.push(i);
7234            } else {
7235                return Err(e);
7236            }
7237        }
7238        Ok((done, retry))
7239    }
7240
7241    /// Describe active producers on a partition (DescribeProducers api 61,
7242    /// KIP-360).
7243    ///
7244    /// Lands on the Metadata partition leader (same class as
7245    /// DeleteRecords / ListOffsets / OffsetForLeaderEpoch). Official
7246    /// Apache JSON listeners are `broker` only. This is not a
7247    /// controller hop and not a transaction-coordinator hop: there is
7248    /// no Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41)
7249    /// retry, and no FindCoordinator / `NOT_COORDINATOR` (16) retry.
7250    /// `NOT_LEADER_OR_FOLLOWER` (6) and other `is_retriable` broker
7251    /// codes refresh Metadata and retry on the new leader. ErrorCode
7252    /// is per-partition (bytes 12–13 on leftover-empty fixture topic
7253    /// `"t"` partition `0`), not top-level after throttle.
7254    /// For several partitions in one RPC per leader (Java
7255    /// `describeProducers(Collection)`), use [`Self::describe_producers_for`].
7256    /// Optional at [`Self::new`] (Kafka 2.8+ / KIP-664); a broker that
7257    /// omits api 61 returns [`Error::Unsupported`]. DescribeProducers
7258    /// has no TimeoutMs; the RPC deadline is
7259    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
7260    /// [`Self::describe_producers_timeout`].
7261    pub async fn describe_producers(
7262        &mut self,
7263        partition: impl Into<crate::TopicPartition>,
7264    ) -> Result<DescribeProducersPartition> {
7265        let timeout = self.cfg.request_timeout;
7266        self.describe_producers_timeout(partition, timeout).await
7267    }
7268
7269    /// [`Self::describe_producers`] with a one-shot RPC deadline (Java
7270    /// `DescribeProducersOptions.timeoutMs`).
7271    ///
7272    /// DescribeProducers has no TimeoutMs; `timeout` is the RPC deadline
7273    /// and the `NOT_LEADER_OR_FOLLOWER` retry budget.
7274    pub async fn describe_producers_timeout(
7275        &mut self,
7276        partition: impl Into<crate::TopicPartition>,
7277        timeout: Duration,
7278    ) -> Result<DescribeProducersPartition> {
7279        let topics = self
7280            .describe_producers_for_timeout([partition.into()], timeout)
7281            .await?;
7282        topics
7283            .into_iter()
7284            .next()
7285            .and_then(|t| t.partitions.into_iter().next())
7286            .ok_or_else(|| Error::protocol("empty DescribeProducers response"))
7287    }
7288
7289    /// [`Self::describe_producers`] for several partitions (Java
7290    /// `describeProducers(Collection)`; DescribeProducers Topics of N).
7291    ///
7292    /// Groups by Metadata leader and sends one RPC per leader. Empty
7293    /// `partitions` is a no-op. To pin every partition to one broker
7294    /// (Java `DescribeProducersOptions.brokerId`), use
7295    /// [`Self::describe_producers_for_on_broker`]. DescribeProducers has
7296    /// no TimeoutMs; the RPC deadline is
7297    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
7298    /// [`Self::describe_producers_for_timeout`].
7299    pub async fn describe_producers_for(
7300        &mut self,
7301        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7302    ) -> Result<Vec<DescribeProducersTopic>> {
7303        let timeout = self.cfg.request_timeout;
7304        self.describe_producers_for_with(partitions, timeout, None)
7305            .await
7306    }
7307
7308    /// [`Self::describe_producers_for`] with a one-shot RPC deadline (Java
7309    /// `describeProducers` plus `DescribeProducersOptions.timeoutMs`).
7310    ///
7311    /// DescribeProducers has no TimeoutMs; `timeout` is the RPC deadline
7312    /// and the `NOT_LEADER_OR_FOLLOWER` retry budget.
7313    pub async fn describe_producers_for_timeout(
7314        &mut self,
7315        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7316        timeout: Duration,
7317    ) -> Result<Vec<DescribeProducersTopic>> {
7318        self.describe_producers_for_with(partitions, timeout, None)
7319            .await
7320    }
7321
7322    /// [`Self::describe_producers_for`] pinned to one broker (Java
7323    /// `DescribeProducersOptions.brokerId`).
7324    ///
7325    /// Sends one DescribeProducers RPC to `broker_id` for every
7326    /// partition. `NOT_LEADER_OR_FOLLOWER` (6) is returned on that
7327    /// partition and is not retried on the Metadata leader.
7328    pub async fn describe_producers_for_on_broker(
7329        &mut self,
7330        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7331        broker_id: i32,
7332    ) -> Result<Vec<DescribeProducersTopic>> {
7333        let timeout = self.cfg.request_timeout;
7334        self.describe_producers_for_with(partitions, timeout, Some(broker_id))
7335            .await
7336    }
7337
7338    /// [`Self::describe_producers_for_on_broker`] with a one-shot RPC
7339    /// deadline (Java `DescribeProducersOptions.brokerId` + `timeoutMs`).
7340    ///
7341    /// DescribeProducers has no TimeoutMs; `timeout` is the RPC deadline.
7342    /// `NOT_LEADER_OR_FOLLOWER` is not retried onto another broker.
7343    pub async fn describe_producers_for_on_broker_timeout(
7344        &mut self,
7345        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7346        broker_id: i32,
7347        timeout: Duration,
7348    ) -> Result<Vec<DescribeProducersTopic>> {
7349        self.describe_producers_for_with(partitions, timeout, Some(broker_id))
7350            .await
7351    }
7352
7353    async fn describe_producers_for_with(
7354        &mut self,
7355        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7356        timeout: Duration,
7357        broker_id: Option<i32>,
7358    ) -> Result<Vec<DescribeProducersTopic>> {
7359        let partitions: Vec<crate::TopicPartition> =
7360            partitions.into_iter().map(Into::into).collect();
7361        if partitions.is_empty() {
7362            return Ok(Vec::new());
7363        }
7364        let version = self.describe_producers_version.ok_or_else(|| {
7365            Error::Unsupported("broker does not support DescribeProducers".into())
7366        })?;
7367        let deadline = Instant::now() + timeout;
7368        let mut attempt = 0u32;
7369        let mut out: Vec<Option<DescribeProducersPartition>> = vec![None; partitions.len()];
7370        let mut pending: Vec<usize> = (0..partitions.len()).collect();
7371        let pin_broker = broker_id.is_some();
7372        loop {
7373            if pending.is_empty() {
7374                break;
7375            }
7376            if !pin_broker {
7377                let mut need: Vec<String> = Vec::new();
7378                for &i in &pending {
7379                    let Some(tp) = partitions.get(i) else {
7380                        continue;
7381                    };
7382                    if self.cluster.leader(&tp.topic, tp.partition).is_err()
7383                        && !need.iter().any(|t| t == &tp.topic)
7384                    {
7385                        need.push(tp.topic.clone());
7386                    }
7387                }
7388                if !need.is_empty() {
7389                    self.refresh_metadata(Some(&need)).await?;
7390                }
7391            }
7392            let mut by_node: HashMap<i32, Vec<usize>> = HashMap::new();
7393            let mut nodes: Vec<i32> = Vec::new();
7394            if let Some(id) = broker_id {
7395                let _prev = by_node.insert(id, pending.clone());
7396                nodes.push(id);
7397            } else {
7398                for &i in &pending {
7399                    let tp = partitions
7400                        .get(i)
7401                        .ok_or_else(|| Error::protocol("missing DescribeProducers query"))?;
7402                    let (node, _) = self.cluster.leader(&tp.topic, tp.partition)?;
7403                    match by_node.entry(node) {
7404                        std::collections::hash_map::Entry::Vacant(slot) => {
7405                            nodes.push(node);
7406                            let _ = slot.insert(vec![i]);
7407                        }
7408                        std::collections::hash_map::Entry::Occupied(mut slot) => {
7409                            slot.get_mut().push(i);
7410                        }
7411                    }
7412                }
7413            }
7414            let mut still = Vec::new();
7415            for node in nodes {
7416                let idxs = by_node.remove(&node).unwrap_or_default();
7417                match self
7418                    .describe_producers_on_node(
7419                        node,
7420                        version,
7421                        &partitions,
7422                        &idxs,
7423                        timeout,
7424                        pin_broker,
7425                    )
7426                    .await
7427                {
7428                    Ok((done, retry)) => {
7429                        for (i, part) in done {
7430                            if let Some(slot) = out.get_mut(i) {
7431                                *slot = Some(part);
7432                            }
7433                        }
7434                        still.extend(retry);
7435                    }
7436                    Err(e) if e.is_retriable() => {
7437                        let _ = self.conns.remove(&node);
7438                        still.extend(idxs);
7439                    }
7440                    Err(e) => return Err(e),
7441                }
7442            }
7443            pending = still;
7444            if pending.is_empty() {
7445                break;
7446            }
7447            self.wait_retry(&mut attempt, deadline).await?;
7448            if pin_broker {
7449                continue;
7450            }
7451            for &i in &pending {
7452                if let Some(tp) = partitions.get(i) {
7453                    self.cluster.invalidate_topic(&tp.topic);
7454                }
7455            }
7456            let topics: Vec<String> = {
7457                let mut t = Vec::new();
7458                for &i in &pending {
7459                    if let Some(tp) = partitions.get(i) {
7460                        if !t.iter().any(|n| n == &tp.topic) {
7461                            t.push(tp.topic.clone());
7462                        }
7463                    }
7464                }
7465                t
7466            };
7467            if !topics.is_empty() {
7468                self.refresh_metadata(Some(&topics)).await?;
7469            }
7470        }
7471        let mut grouped: Vec<DescribeProducersTopic> = Vec::new();
7472        for (tp, part) in partitions.into_iter().zip(out) {
7473            let part = part.ok_or_else(|| Error::protocol("DescribeProducers missing result"))?;
7474            match grouped.last_mut() {
7475                Some(topic) if topic.name == tp.topic => topic.partitions.push(part),
7476                _ => grouped.push(DescribeProducersTopic::new(tp.topic, vec![part])),
7477            }
7478        }
7479        Ok(grouped)
7480    }
7481
7482    async fn describe_producers_on_node(
7483        &mut self,
7484        node: i32,
7485        version: i16,
7486        partitions: &[crate::TopicPartition],
7487        idxs: &[usize],
7488        timeout: Duration,
7489        pin_broker: bool,
7490    ) -> Result<DescribeProducersNodeOutcome> {
7491        let topics = describe_producers_topics(partitions, idxs);
7492        self.connect_node(node).await?;
7493        let body = {
7494            let conn = self
7495                .conns
7496                .get_mut(&node)
7497                .ok_or_else(|| Error::protocol("missing describe_producers conn"))?;
7498            conn.roundtrip(
7499                DESCRIBE_PRODUCERS,
7500                version,
7501                |buf| encode_describe_producers_topics_request(buf, &topics),
7502                timeout,
7503            )
7504            .await
7505        }?;
7506        let resp = decode_describe_producers_response(&mut body.clone())?;
7507        let mut by_tp: HashMap<(String, i32), DescribeProducersPartition> = HashMap::new();
7508        for topic in resp.topics {
7509            for part in topic.partitions {
7510                let _ = by_tp.insert((topic.name.clone(), part.partition_index), part);
7511            }
7512        }
7513        let mut done = Vec::new();
7514        let mut retry = Vec::new();
7515        for &i in idxs {
7516            let tp = partitions
7517                .get(i)
7518                .ok_or_else(|| Error::protocol("missing DescribeProducers query"))?;
7519            let part = by_tp
7520                .remove(&(tp.topic.clone(), tp.partition))
7521                .ok_or_else(|| {
7522                    Error::protocol(format!(
7523                        "DescribeProducers missing {}-{}",
7524                        tp.topic, tp.partition
7525                    ))
7526                })?;
7527            if part.error_code == 0 {
7528                done.push((i, part));
7529                continue;
7530            }
7531            let e = Error::broker(part.error_code, format!("{}-{}", tp.topic, tp.partition));
7532            if !pin_broker && e.is_retriable() {
7533                retry.push(i);
7534            } else {
7535                done.push((i, part));
7536            }
7537        }
7538        Ok((done, retry))
7539    }
7540
7541    /// Brokers, controller, and cluster id (`DescribeCluster`).
7542    ///
7543    /// Negotiates v0–v2 (flexible from v0). v1 EndpointType is brokers
7544    /// (KIP-919). v2 omits fenced brokers (`IncludeFencedBrokers` false).
7545    /// Kafka 4.0 `validVersions` is `0-2`. v3+ is not spoken. See
7546    /// [`Self::describe_cluster_with`] for Java `DescribeClusterOptions`.
7547    /// Optional at [`Self::new`] (Kafka 2.8+ / KIP-700); a broker that
7548    /// omits api 60 returns [`Error::Unsupported`]. DescribeCluster has
7549    /// no TimeoutMs; the RPC deadline is
7550    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
7551    /// [`Self::describe_cluster_timeout`].
7552    pub async fn describe_cluster(&mut self) -> Result<ClusterDescription> {
7553        self.describe_cluster_with(false, ENDPOINT_TYPE_BROKERS, false)
7554            .await
7555    }
7556
7557    /// [`Self::describe_cluster`] with a one-shot RPC deadline (Java
7558    /// `DescribeClusterOptions.timeoutMs`).
7559    ///
7560    /// DescribeCluster has no TimeoutMs; `timeout` is the RPC deadline.
7561    pub async fn describe_cluster_timeout(
7562        &mut self,
7563        timeout: Duration,
7564    ) -> Result<ClusterDescription> {
7565        self.describe_cluster_with_timeout(false, ENDPOINT_TYPE_BROKERS, false, timeout)
7566            .await
7567    }
7568
7569    /// DescribeCluster with authorized operations, endpoint type, and
7570    /// fenced brokers (Java `describeCluster` plus
7571    /// `DescribeClusterOptions`).
7572    ///
7573    /// `endpoint_type` is [`EndpointType`] or a protocol `i8` (`1` brokers,
7574    /// `2` controllers). v1+ sends EndpointType. v2 sends
7575    /// IncludeFencedBrokers. v0 omits both even when set. DescribeCluster
7576    /// has no TimeoutMs; the RPC deadline is
7577    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
7578    /// [`Self::describe_cluster_with_timeout`].
7579    pub async fn describe_cluster_with(
7580        &mut self,
7581        include_authorized_operations: bool,
7582        endpoint_type: impl Into<i8>,
7583        include_fenced_brokers: bool,
7584    ) -> Result<ClusterDescription> {
7585        let timeout = self.cfg.request_timeout;
7586        self.describe_cluster_with_timeout(
7587            include_authorized_operations,
7588            endpoint_type,
7589            include_fenced_brokers,
7590            timeout,
7591        )
7592        .await
7593    }
7594
7595    /// [`Self::describe_cluster_with`] with Java
7596    /// `DescribeClusterOptions.timeoutMs`.
7597    ///
7598    /// DescribeCluster has no TimeoutMs; `timeout` is the RPC deadline.
7599    pub async fn describe_cluster_with_timeout(
7600        &mut self,
7601        include_authorized_operations: bool,
7602        endpoint_type: impl Into<i8>,
7603        include_fenced_brokers: bool,
7604        timeout: Duration,
7605    ) -> Result<ClusterDescription> {
7606        let endpoint_type = endpoint_type.into();
7607        let version = self.describe_cluster_version.ok_or_else(|| {
7608            Error::Unsupported("broker does not support DescribeCluster v0-2".into())
7609        })?;
7610        let body = self
7611            .roundtrip_bootstrap(
7612                DESCRIBE_CLUSTER,
7613                version,
7614                |buf| {
7615                    encode_describe_cluster_request(
7616                        buf,
7617                        version,
7618                        include_authorized_operations,
7619                        endpoint_type,
7620                        include_fenced_brokers,
7621                    )
7622                },
7623                timeout,
7624            )
7625            .await?;
7626        decode_describe_cluster_response(&mut body.clone(), version)
7627    }
7628
7629    /// Delete ACL bindings (`DeleteAcls`) matching `resource_type`.
7630    ///
7631    /// Negotiates v0–v3 (v0–v1 classic; v2+ flexible). v1+ sends
7632    /// PatternTypeFilter ANY. Operation and Permission are ANY (Java
7633    /// `AccessControlEntryFilter.ANY`). Kafka 4.0 `validVersions` is
7634    /// `1-3`. v4+ is not spoken.
7635    ///
7636    /// `resource_type` is [`crate::AclResourceType`] or a protocol `i8`.
7637    /// Returns the first filter's error code. For principal / host / name
7638    /// filters or Filters of N, use [`Self::delete_acls_with`]. DeleteAcls
7639    /// has no TimeoutMs; the RPC deadline is
7640    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
7641    /// [`Self::delete_acls_timeout`].
7642    pub async fn delete_acls(&mut self, resource_type: impl Into<i8>) -> Result<i16> {
7643        let results = self
7644            .delete_acls_with(&[AclBindingFilter::resource_type(resource_type)])
7645            .await?;
7646        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
7647    }
7648
7649    /// [`Self::delete_acls`] with a one-shot RPC deadline (Java
7650    /// `DeleteAclsOptions.timeoutMs`).
7651    pub async fn delete_acls_timeout(
7652        &mut self,
7653        resource_type: impl Into<i8>,
7654        timeout: Duration,
7655    ) -> Result<i16> {
7656        let results = self
7657            .delete_acls_with_timeout(&[AclBindingFilter::resource_type(resource_type)], timeout)
7658            .await?;
7659        Ok(results.first().map(|r| r.error_code).unwrap_or(0))
7660    }
7661
7662    /// [`Self::delete_acls`] with Java `deleteAcls(Collection)` Filters of N.
7663    ///
7664    /// Empty `filters` is a no-op. DeleteAcls has no TimeoutMs; the RPC
7665    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
7666    /// deadline, use [`Self::delete_acls_with_timeout`].
7667    pub async fn delete_acls_with(
7668        &mut self,
7669        filters: &[AclBindingFilter],
7670    ) -> Result<Vec<DeletedAclsFilterResult>> {
7671        let timeout = self.cfg.request_timeout;
7672        self.delete_acls_with_timeout(filters, timeout).await
7673    }
7674
7675    /// [`Self::delete_acls_with`] with Java `DeleteAclsOptions.timeoutMs`.
7676    ///
7677    /// Empty `filters` is a no-op. DeleteAcls has no TimeoutMs; `timeout`
7678    /// is the RPC deadline.
7679    pub async fn delete_acls_with_timeout(
7680        &mut self,
7681        filters: &[AclBindingFilter],
7682        timeout: Duration,
7683    ) -> Result<Vec<DeletedAclsFilterResult>> {
7684        if filters.is_empty() {
7685            return Ok(Vec::new());
7686        }
7687        let version = self.delete_acls_version;
7688        let body = self
7689            .roundtrip_bootstrap(
7690                DELETE_ACLS,
7691                version,
7692                |buf| encode_delete_acls_request(buf, version, filters),
7693                timeout,
7694            )
7695            .await?;
7696        let (results, ..) = decode_delete_acls_filter_results(&mut body.clone(), version)?;
7697        Ok(results)
7698    }
7699
7700    /// Delete committed offsets for `group_id` (OffsetDelete api 47).
7701    ///
7702    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
7703    /// `COORDINATOR_LOAD_IN_PROGRESS` / `COORDINATOR_NOT_AVAILABLE` /
7704    /// `NOT_COORDINATOR` refresh the coordinator and retry.
7705    ///
7706    /// Each item is a [`crate::TopicPartition`] (or anything that converts
7707    /// to one). Java `deleteConsumerGroupOffsets` is
7708    /// [`Self::delete_consumer_group_offsets`]. Optional at [`Self::new`]
7709    /// (Kafka 2.4+ / KIP-496); a broker that omits api 47 returns
7710    /// [`Error::Unsupported`]. OffsetDelete has no TimeoutMs; the RPC
7711    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
7712    /// deadline, use [`Self::delete_offsets_timeout`].
7713    pub async fn delete_offsets(
7714        &mut self,
7715        group_id: &str,
7716        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7717    ) -> Result<Vec<OffsetDeleteResult>> {
7718        let timeout = self.cfg.request_timeout;
7719        self.delete_offsets_timeout(group_id, partitions, timeout)
7720            .await
7721    }
7722
7723    /// [`Self::delete_offsets`] with a one-shot RPC deadline (Java
7724    /// `DeleteConsumerGroupOffsetsOptions.timeoutMs`).
7725    ///
7726    /// OffsetDelete has no TimeoutMs; `timeout` is the RPC deadline and
7727    /// the coordinator retry budget.
7728    pub async fn delete_offsets_timeout(
7729        &mut self,
7730        group_id: &str,
7731        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7732        timeout: Duration,
7733    ) -> Result<Vec<OffsetDeleteResult>> {
7734        let partitions: Vec<(String, i32)> = partitions
7735            .into_iter()
7736            .map(|p| {
7737                let tp = p.into();
7738                (tp.topic, tp.partition)
7739            })
7740            .collect();
7741        let topics = offset_delete_topics(&partitions);
7742        let version = self
7743            .offset_delete_version
7744            .ok_or_else(|| Error::Unsupported("broker does not support OffsetDelete".into()))?;
7745        let deadline = Instant::now() + timeout;
7746        let mut attempt = 0u32;
7747        let group_id = group_id.to_string();
7748        loop {
7749            let stale = self
7750                .group_coord
7751                .as_ref()
7752                .is_none_or(|(g, _)| g != &group_id);
7753            if stale {
7754                let node = self.discover_group_coord(&group_id).await?;
7755                self.group_coord = Some((group_id.clone(), node));
7756            }
7757            let node = self
7758                .group_coord
7759                .as_ref()
7760                .map(|(_, n)| *n)
7761                .ok_or_else(|| Error::protocol("missing group coordinator"))?;
7762            self.connect_node(node).await?;
7763            let body = {
7764                let conn = self
7765                    .conns
7766                    .get_mut(&node)
7767                    .ok_or_else(|| Error::protocol("missing delete_offsets conn"))?;
7768                conn.roundtrip(
7769                    OFFSET_DELETE,
7770                    version,
7771                    |buf| encode_offset_delete_request(buf, &group_id, &topics),
7772                    timeout,
7773                )
7774                .await
7775            };
7776            let body = match body {
7777                Ok(b) => b,
7778                Err(e) if e.is_retriable() => {
7779                    let _ = self.conns.remove(&node);
7780                    self.group_coord = None;
7781                    self.wait_retry(&mut attempt, deadline).await?;
7782                    continue;
7783                }
7784                Err(e) => return Err(e),
7785            };
7786            let (top, results, ..) = decode_offset_delete_response(&mut body.clone())?;
7787            if error::coordinator_retriable(top) {
7788                // 14/15/16: FindCoordinator, then the new group coordinator.
7789                self.group_coord = None;
7790                let _ = self.conns.remove(&node);
7791                self.wait_retry(&mut attempt, deadline).await?;
7792                continue;
7793            }
7794            if top != 0 {
7795                return Err(Error::broker(top, "OffsetDelete"));
7796            }
7797            return Ok(results);
7798        }
7799    }
7800
7801    /// Delete committed offsets (Java `Admin.deleteConsumerGroupOffsets`).
7802    ///
7803    /// Same wire as [`Self::delete_offsets`]: OffsetDelete api 47 on the
7804    /// group coordinator. OffsetDelete has no TimeoutMs; the RPC deadline
7805    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
7806    /// [`Self::delete_consumer_group_offsets_timeout`].
7807    pub async fn delete_consumer_group_offsets(
7808        &mut self,
7809        group_id: &str,
7810        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7811    ) -> Result<Vec<OffsetDeleteResult>> {
7812        let timeout = self.cfg.request_timeout;
7813        self.delete_consumer_group_offsets_timeout(group_id, partitions, timeout)
7814            .await
7815    }
7816
7817    /// [`Self::delete_consumer_group_offsets`] with a one-shot RPC deadline
7818    /// (Java `DeleteConsumerGroupOffsetsOptions.timeoutMs`).
7819    ///
7820    /// OffsetDelete has no TimeoutMs; `timeout` is the RPC deadline and
7821    /// the coordinator retry budget.
7822    pub async fn delete_consumer_group_offsets_timeout(
7823        &mut self,
7824        group_id: &str,
7825        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7826        timeout: Duration,
7827    ) -> Result<Vec<OffsetDeleteResult>> {
7828        self.delete_offsets_timeout(group_id, partitions, timeout)
7829            .await
7830    }
7831
7832    /// List committed offsets for `group_id` (Java `listConsumerGroupOffsets`).
7833    ///
7834    /// OffsetFetch v1–v9 on the group coordinator. Partitions with no committed
7835    /// offset return [`crate::OffsetAndMetadata`] offset `-1`. Empty
7836    /// `partitions` returns an empty list. For every committed partition,
7837    /// use [`Self::list_all_consumer_group_offsets`]. `COORDINATOR_LOAD_IN_PROGRESS` /
7838    /// `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` refresh the coordinator
7839    /// and retry. Waits up to [`AdminConfig::request_timeout`]. For a one-shot
7840    /// timeout, use [`Self::list_consumer_group_offsets_timeout`].
7841    pub async fn list_consumer_group_offsets(
7842        &mut self,
7843        group_id: &str,
7844        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7845    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
7846        let timeout = self.cfg.request_timeout;
7847        self.list_consumer_group_offsets_timeout(group_id, partitions, timeout)
7848            .await
7849    }
7850
7851    /// [`Self::list_consumer_group_offsets`] with a one-shot timeout (Java
7852    /// `ListConsumerGroupOffsetsOptions.timeoutMs`).
7853    pub async fn list_consumer_group_offsets_timeout(
7854        &mut self,
7855        group_id: &str,
7856        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7857        timeout: Duration,
7858    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
7859        self.list_consumer_group_offsets_with(group_id, partitions, false, timeout)
7860            .await
7861    }
7862
7863    /// [`Self::list_consumer_group_offsets`] plus `requireStable` and timeout
7864    /// (Java `ListConsumerGroupOffsetsOptions.requireStable` and `timeoutMs`).
7865    ///
7866    /// `require_stable` is OffsetFetch v7+ RequireStable. `timeout` is the
7867    /// RPC deadline.
7868    pub async fn list_consumer_group_offsets_with(
7869        &mut self,
7870        group_id: &str,
7871        partitions: impl IntoIterator<Item = impl Into<crate::TopicPartition>>,
7872        require_stable: bool,
7873        timeout: Duration,
7874    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
7875        let partitions: Vec<crate::TopicPartition> =
7876            partitions.into_iter().map(Into::into).collect();
7877        if partitions.is_empty() {
7878            return Ok(Vec::new());
7879        }
7880        let wanted: Vec<(String, i32)> = partitions
7881            .iter()
7882            .map(|tp| (tp.topic.clone(), tp.partition))
7883            .collect();
7884        let topics = crate::group::group_offset_fetch_topics(&wanted);
7885        let fetched = self
7886            .fetch_consumer_group_offsets(group_id, Some(topics), require_stable, timeout)
7887            .await?;
7888        let map = crate::group::committed_offset_map(&fetched)?;
7889        Ok(partitions
7890            .iter()
7891            .map(|tp| {
7892                let md = map
7893                    .get(&(tp.topic.clone(), tp.partition))
7894                    .cloned()
7895                    .unwrap_or_else(|| crate::OffsetAndMetadata::new(-1));
7896                (tp.clone(), md)
7897            })
7898            .collect())
7899    }
7900
7901    /// List every committed offset for `group_id` (Java
7902    /// `listConsumerGroupOffsets(groupId)` with no topic-partition filter).
7903    ///
7904    /// OffsetFetch null Topics (v2+). Waits up to
7905    /// [`AdminConfig::request_timeout`]. For a one-shot timeout, use
7906    /// [`Self::list_all_consumer_group_offsets_timeout`]. For
7907    /// `requireStable` and a one-shot timeout, use
7908    /// [`Self::list_all_consumer_group_offsets_with`].
7909    pub async fn list_all_consumer_group_offsets(
7910        &mut self,
7911        group_id: &str,
7912    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
7913        let timeout = self.cfg.request_timeout;
7914        self.list_all_consumer_group_offsets_timeout(group_id, timeout)
7915            .await
7916    }
7917
7918    /// [`Self::list_all_consumer_group_offsets`] with a one-shot timeout
7919    /// (Java `ListConsumerGroupOffsetsOptions.timeoutMs`).
7920    ///
7921    /// OffsetFetch has no TimeoutMs; `timeout` is the RPC deadline.
7922    /// RequireStable stays false. For `requireStable`, use
7923    /// [`Self::list_all_consumer_group_offsets_with`].
7924    pub async fn list_all_consumer_group_offsets_timeout(
7925        &mut self,
7926        group_id: &str,
7927        timeout: Duration,
7928    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
7929        self.list_all_consumer_group_offsets_with(group_id, false, timeout)
7930            .await
7931    }
7932
7933    /// [`Self::list_all_consumer_group_offsets`] plus `requireStable` and
7934    /// timeout (Java `ListConsumerGroupOffsetsOptions`).
7935    pub async fn list_all_consumer_group_offsets_with(
7936        &mut self,
7937        group_id: &str,
7938        require_stable: bool,
7939        timeout: Duration,
7940    ) -> Result<Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>> {
7941        let fetched = self
7942            .fetch_consumer_group_offsets(group_id, None, require_stable, timeout)
7943            .await?;
7944        let map = crate::group::committed_offset_map(&fetched)?;
7945        Ok(map
7946            .into_iter()
7947            .map(|((topic, partition), md)| (crate::TopicPartition::new(topic, partition), md))
7948            .collect())
7949    }
7950
7951    /// List committed offsets for several groups (Java
7952    /// `listConsumerGroupOffsets(Map<String, ListConsumerGroupOffsetsSpec>)`).
7953    ///
7954    /// OffsetFetch v8+ Groups array of N (KIP-709): groups that share a
7955    /// coordinator go in one RPC. FindCoordinator v4+ CoordinatorKeys
7956    /// array of N (KIP-699): one FindCoordinator per retry, not one per
7957    /// group. Brokers that only speak OffsetFetch v1–v7 get one
7958    /// OffsetFetch per group. Brokers that only speak FindCoordinator
7959    /// v1–v3 get one FindCoordinator per group. Empty input is a no-op.
7960    /// Empty [`ListConsumerGroupOffsetsSpec::topic_partitions`] for a
7961    /// group is a no-op for that group. Waits up to
7962    /// [`AdminConfig::request_timeout`]. For a one-shot timeout, use
7963    /// [`Self::list_consumer_group_offsets_for_groups_timeout`]. For
7964    /// `requireStable` and a one-shot timeout, use
7965    /// [`Self::list_consumer_group_offsets_for_groups_with`].
7966    pub async fn list_consumer_group_offsets_for_groups(
7967        &mut self,
7968        groups: impl IntoIterator<Item = (impl Into<String>, ListConsumerGroupOffsetsSpec)>,
7969    ) -> Result<
7970        Vec<(
7971            String,
7972            Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>,
7973        )>,
7974    > {
7975        let timeout = self.cfg.request_timeout;
7976        self.list_consumer_group_offsets_for_groups_timeout(groups, timeout)
7977            .await
7978    }
7979
7980    /// [`Self::list_consumer_group_offsets_for_groups`] with a one-shot
7981    /// timeout (Java `ListConsumerGroupOffsetsOptions.timeoutMs`).
7982    ///
7983    /// OffsetFetch has no TimeoutMs; `timeout` is the RPC deadline.
7984    /// RequireStable stays false. For `requireStable`, use
7985    /// [`Self::list_consumer_group_offsets_for_groups_with`].
7986    pub async fn list_consumer_group_offsets_for_groups_timeout(
7987        &mut self,
7988        groups: impl IntoIterator<Item = (impl Into<String>, ListConsumerGroupOffsetsSpec)>,
7989        timeout: Duration,
7990    ) -> Result<
7991        Vec<(
7992            String,
7993            Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>,
7994        )>,
7995    > {
7996        self.list_consumer_group_offsets_for_groups_with(groups, false, timeout)
7997            .await
7998    }
7999
8000    /// [`Self::list_consumer_group_offsets_for_groups`] plus `requireStable`
8001    /// and timeout (Java `ListConsumerGroupOffsetsOptions`).
8002    ///
8003    /// `require_stable` is OffsetFetch v7+ RequireStable. `timeout` is the
8004    /// RPC deadline.
8005    pub async fn list_consumer_group_offsets_for_groups_with(
8006        &mut self,
8007        groups: impl IntoIterator<Item = (impl Into<String>, ListConsumerGroupOffsetsSpec)>,
8008        require_stable: bool,
8009        timeout: Duration,
8010    ) -> Result<
8011        Vec<(
8012            String,
8013            Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>,
8014        )>,
8015    > {
8016        let jobs: Vec<(String, ListConsumerGroupOffsetsSpec)> = groups
8017            .into_iter()
8018            .map(|(g, spec)| (g.into(), spec))
8019            .collect();
8020        if jobs.is_empty() {
8021            return Ok(Vec::new());
8022        }
8023        let mut out: Vec<(
8024            String,
8025            Vec<(crate::TopicPartition, crate::OffsetAndMetadata)>,
8026        )> = jobs.iter().map(|(g, _)| (g.clone(), Vec::new())).collect();
8027        let mut remaining: Vec<usize> = Vec::new();
8028        for (i, (_, spec)) in jobs.iter().enumerate() {
8029            if spec.partitions.as_ref().is_some_and(Vec::is_empty) {
8030                continue;
8031            }
8032            remaining.push(i);
8033        }
8034        if remaining.is_empty() {
8035            return Ok(out);
8036        }
8037        if let Some(version) = self
8038            .versions
8039            .get(&OFFSET_FETCH)
8040            .and_then(|v| pick_version(v.min_version, v.max_version, 8, 9))
8041        {
8042            let deadline = Instant::now() + timeout;
8043            let mut attempt = 0u32;
8044            loop {
8045                let group_ids: Vec<String> = remaining
8046                    .iter()
8047                    .filter_map(|&i| jobs.get(i).map(|j| j.0.clone()))
8048                    .collect();
8049                let coords = self.discover_group_coords(&group_ids).await?;
8050                let mut by_node: HashMap<i32, Vec<usize>> = HashMap::new();
8051                for &i in &remaining {
8052                    let group_id = jobs
8053                        .get(i)
8054                        .ok_or_else(|| Error::protocol("missing group spec"))?
8055                        .0
8056                        .clone();
8057                    let node = *coords.get(&group_id).ok_or_else(|| {
8058                        Error::protocol(format!("missing coordinator for {group_id}"))
8059                    })?;
8060                    by_node.entry(node).or_default().push(i);
8061                }
8062                let mut nodes: Vec<i32> = by_node.keys().copied().collect();
8063                nodes.sort_unstable();
8064                let mut next_remaining = Vec::new();
8065                let mut retry = false;
8066                for node in nodes {
8067                    let idxs = by_node.get(&node).cloned().unwrap_or_default();
8068                    let mut groups = Vec::new();
8069                    for &i in &idxs {
8070                        let job = jobs
8071                            .get(i)
8072                            .ok_or_else(|| Error::protocol("missing group spec"))?;
8073                        groups.push(OffsetFetchGroup::new(
8074                            job.0.clone(),
8075                            offset_fetch_topics_for_spec(&job.1),
8076                        ));
8077                    }
8078                    self.connect_node(node).await?;
8079                    let body = {
8080                        let conn = self.conns.get_mut(&node).ok_or_else(|| {
8081                            Error::protocol("missing list_consumer_group_offsets conn")
8082                        })?;
8083                        conn.roundtrip(
8084                            OFFSET_FETCH,
8085                            version,
8086                            |buf| {
8087                                encode_offset_fetch_groups_request(
8088                                    buf,
8089                                    version,
8090                                    &groups,
8091                                    require_stable,
8092                                )
8093                            },
8094                            timeout,
8095                        )
8096                        .await
8097                    };
8098                    let body = match body {
8099                        Ok(b) => b,
8100                        Err(e) if e.is_retriable() => {
8101                            let _ = self.conns.remove(&node);
8102                            self.group_coord = None;
8103                            next_remaining.extend(idxs);
8104                            retry = true;
8105                            continue;
8106                        }
8107                        Err(e) => return Err(e),
8108                    };
8109                    let results =
8110                        match decode_offset_fetch_groups_response(&mut body.clone(), version) {
8111                            Ok((r, ..)) => r,
8112                            Err(e) if e.broker_code().is_some_and(error::coordinator_retriable) => {
8113                                self.group_coord = None;
8114                                let _ = self.conns.remove(&node);
8115                                next_remaining.extend(idxs);
8116                                retry = true;
8117                                continue;
8118                            }
8119                            Err(e) => return Err(e),
8120                        };
8121                    if results
8122                        .iter()
8123                        .any(|r| error::coordinator_retriable(r.error_code))
8124                    {
8125                        self.group_coord = None;
8126                        let _ = self.conns.remove(&node);
8127                        next_remaining.extend(idxs);
8128                        retry = true;
8129                        continue;
8130                    }
8131                    let mut by_id: HashMap<String, crate::protocol::group::OffsetFetchGroupResult> =
8132                        HashMap::new();
8133                    for g in results {
8134                        if g.error_code != 0 {
8135                            return Err(Error::broker(g.error_code, g.group_id));
8136                        }
8137                        let _ = by_id.insert(g.group_id.clone(), g);
8138                    }
8139                    for i in idxs {
8140                        let job = jobs
8141                            .get(i)
8142                            .ok_or_else(|| Error::protocol("missing group spec"))?;
8143                        let Some(got) = by_id.remove(&job.0) else {
8144                            return Err(Error::protocol(format!(
8145                                "OffsetFetch response missing group {}",
8146                                job.0
8147                            )));
8148                        };
8149                        let listed = listed_group_offsets(&job.1, &got.topics)?;
8150                        let slot = out
8151                            .get_mut(i)
8152                            .ok_or_else(|| Error::protocol("missing group result slot"))?;
8153                        slot.1 = listed;
8154                    }
8155                }
8156                if !retry {
8157                    break;
8158                }
8159                remaining = next_remaining;
8160                self.wait_retry(&mut attempt, deadline).await?;
8161            }
8162            return Ok(out);
8163        }
8164        for i in remaining {
8165            let (group_id, topics, spec) = {
8166                let job = jobs
8167                    .get(i)
8168                    .ok_or_else(|| Error::protocol("missing group spec"))?;
8169                (
8170                    job.0.clone(),
8171                    offset_fetch_topics_for_spec(&job.1),
8172                    job.1.clone(),
8173                )
8174            };
8175            let fetched = self
8176                .fetch_consumer_group_offsets(&group_id, topics, require_stable, timeout)
8177                .await?;
8178            let listed = listed_group_offsets(&spec, &fetched)?;
8179            let slot = out
8180                .get_mut(i)
8181                .ok_or_else(|| Error::protocol("missing group result slot"))?;
8182            slot.1 = listed;
8183        }
8184        Ok(out)
8185    }
8186
8187    async fn fetch_consumer_group_offsets(
8188        &mut self,
8189        group_id: &str,
8190        topics: Option<Vec<crate::protocol::group::OffsetFetchTopic>>,
8191        require_stable: bool,
8192        timeout: Duration,
8193    ) -> Result<Vec<crate::protocol::group::FetchedOffsetTopic>> {
8194        let client_min = if topics.is_none() { 2 } else { 1 };
8195        let version = self
8196            .versions
8197            .get(&OFFSET_FETCH)
8198            .and_then(|v| pick_version(v.min_version, v.max_version, client_min, 9))
8199            .ok_or_else(|| {
8200                Error::Unsupported(if topics.is_none() {
8201                    "broker does not support OffsetFetch v2-9 (null Topics)".into()
8202                } else {
8203                    "broker does not support OffsetFetch v1-9".into()
8204                })
8205            })?;
8206        let deadline = Instant::now() + timeout;
8207        let mut attempt = 0u32;
8208        let group_id = group_id.to_string();
8209        loop {
8210            let stale = self
8211                .group_coord
8212                .as_ref()
8213                .is_none_or(|(g, _)| g != &group_id);
8214            if stale {
8215                let node = self.discover_group_coord(&group_id).await?;
8216                self.group_coord = Some((group_id.clone(), node));
8217            }
8218            let node = self
8219                .group_coord
8220                .as_ref()
8221                .map(|(_, n)| *n)
8222                .ok_or_else(|| Error::protocol("missing group coordinator"))?;
8223            self.connect_node(node).await?;
8224            let body = {
8225                let conn = self
8226                    .conns
8227                    .get_mut(&node)
8228                    .ok_or_else(|| Error::protocol("missing list_consumer_group_offsets conn"))?;
8229                conn.roundtrip(
8230                    OFFSET_FETCH,
8231                    version,
8232                    |buf| {
8233                        encode_offset_fetch_request(
8234                            buf,
8235                            version,
8236                            &group_id,
8237                            None,
8238                            -1,
8239                            require_stable,
8240                            topics.as_deref(),
8241                        )
8242                    },
8243                    timeout,
8244                )
8245                .await
8246            };
8247            let body = match body {
8248                Ok(b) => b,
8249                Err(e) if e.is_retriable() => {
8250                    let _ = self.conns.remove(&node);
8251                    self.group_coord = None;
8252                    self.wait_retry(&mut attempt, deadline).await?;
8253                    continue;
8254                }
8255                Err(e) => return Err(e),
8256            };
8257            match decode_offset_fetch_response(&mut body.clone(), version) {
8258                Ok(t) => return Ok(t),
8259                Err(e) if e.broker_code().is_some_and(error::coordinator_retriable) => {
8260                    self.group_coord = None;
8261                    let _ = self.conns.remove(&node);
8262                    self.wait_retry(&mut attempt, deadline).await?;
8263                }
8264                Err(e) => return Err(e),
8265            }
8266        }
8267    }
8268
8269    /// Write committed offsets for `group_id` (Java `alterConsumerGroupOffsets`).
8270    ///
8271    /// OffsetCommit v2–v9 on the group coordinator with generation `-1` and an
8272    /// empty member id (admin, not a group member). Empty `offsets` is a
8273    /// no-op. Coordinator load / move errors refresh and retry.
8274    /// OffsetCommit has no TimeoutMs; the RPC deadline is
8275    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8276    /// [`Self::alter_consumer_group_offsets_timeout`].
8277    pub async fn alter_consumer_group_offsets(
8278        &mut self,
8279        group_id: &str,
8280        offsets: impl IntoIterator<Item = (impl Into<crate::TopicPartition>, crate::OffsetAndMetadata)>,
8281    ) -> Result<()> {
8282        let timeout = self.cfg.request_timeout;
8283        self.alter_consumer_group_offsets_timeout(group_id, offsets, timeout)
8284            .await
8285    }
8286
8287    /// [`Self::alter_consumer_group_offsets`] with a one-shot RPC deadline
8288    /// (Java `AlterConsumerGroupOffsetsOptions.timeoutMs`).
8289    ///
8290    /// OffsetCommit has no TimeoutMs; `timeout` is the RPC deadline and
8291    /// the coordinator retry budget. v2–v4 RetentionTimeMs stays `-1`.
8292    pub async fn alter_consumer_group_offsets_timeout(
8293        &mut self,
8294        group_id: &str,
8295        offsets: impl IntoIterator<Item = (impl Into<crate::TopicPartition>, crate::OffsetAndMetadata)>,
8296        timeout: Duration,
8297    ) -> Result<()> {
8298        let offsets: Vec<(crate::TopicPartition, crate::OffsetAndMetadata)> = offsets
8299            .into_iter()
8300            .map(|(tp, md)| (tp.into(), md))
8301            .collect();
8302        if offsets.is_empty() {
8303            return Ok(());
8304        }
8305        let topics = crate::group::group_offset_topics(&offsets);
8306        let version = self
8307            .versions
8308            .get(&OFFSET_COMMIT)
8309            .and_then(|v| pick_version(v.min_version, v.max_version, 2, 9))
8310            .ok_or_else(|| {
8311                Error::Unsupported("broker does not support OffsetCommit v2-9".into())
8312            })?;
8313        let deadline = Instant::now() + timeout;
8314        let mut attempt = 0u32;
8315        let group_id = group_id.to_string();
8316        loop {
8317            let stale = self
8318                .group_coord
8319                .as_ref()
8320                .is_none_or(|(g, _)| g != &group_id);
8321            if stale {
8322                let node = self.discover_group_coord(&group_id).await?;
8323                self.group_coord = Some((group_id.clone(), node));
8324            }
8325            let node = self
8326                .group_coord
8327                .as_ref()
8328                .map(|(_, n)| *n)
8329                .ok_or_else(|| Error::protocol("missing group coordinator"))?;
8330            self.connect_node(node).await?;
8331            let body = {
8332                let conn = self
8333                    .conns
8334                    .get_mut(&node)
8335                    .ok_or_else(|| Error::protocol("missing alter_consumer_group_offsets conn"))?;
8336                conn.roundtrip(
8337                    OFFSET_COMMIT,
8338                    version,
8339                    |buf| {
8340                        encode_offset_commit_request(
8341                            buf,
8342                            version,
8343                            &group_id,
8344                            -1,
8345                            "",
8346                            None,
8347                            DEFAULT_RETENTION_TIME,
8348                            &topics,
8349                        )
8350                    },
8351                    timeout,
8352                )
8353                .await
8354            };
8355            let body = match body {
8356                Ok(b) => b,
8357                Err(e) if e.is_retriable() => {
8358                    let _ = self.conns.remove(&node);
8359                    self.group_coord = None;
8360                    self.wait_retry(&mut attempt, deadline).await?;
8361                    continue;
8362                }
8363                Err(e) => return Err(e),
8364            };
8365            let err = decode_offset_commit_response(&mut body.clone(), version)?;
8366            if error::coordinator_retriable(err) {
8367                self.group_coord = None;
8368                let _ = self.conns.remove(&node);
8369                self.wait_retry(&mut attempt, deadline).await?;
8370                continue;
8371            }
8372            if err != 0 {
8373                return Err(Error::broker(err, "OffsetCommit"));
8374            }
8375            return Ok(());
8376        }
8377    }
8378
8379    /// Describe KIP-848 consumer groups (ConsumerGroupDescribe api 69).
8380    ///
8381    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
8382    /// Negotiates ConsumerGroupDescribe v0–v1 (Kafka 4.0 `validVersions`
8383    /// `0-1`; flexible from v0). Request layout is the same on both
8384    /// versions. v1 adds MemberType INT8 (KIP-1099; `-1` unknown, `0`
8385    /// classic, `1` consumer). v0 omits MemberType; decode fills `-1`.
8386    /// Official Apache JSON listeners are `broker` only; the official
8387    /// response lists `NOT_COORDINATOR` among supported errors. This is
8388    /// not a controller hop and not a partition-leader hop: there is no
8389    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41) retry,
8390    /// and no `NOT_LEADER_OR_FOLLOWER` (6) hop. `COORDINATOR_LOAD_IN_PROGRESS`
8391    /// / `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh the
8392    /// coordinator and retry. ErrorCode is per-group (bytes 5–6 on
8393    /// leftover-empty fixture group `"g"`), not top-level after throttle.
8394    /// FindCoordinator v4+ CoordinatorKeys array of N (KIP-699): one
8395    /// FindCoordinator per retry for uncached groups. ConsumerGroupDescribe
8396    /// is one RPC per coordinator. Brokers that only speak FindCoordinator
8397    /// v1–v3 get one FindCoordinator per uncached group. Empty input is
8398    /// a no-op. ConsumerGroupDescribe has no TimeoutMs; the RPC deadline
8399    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8400    /// [`Self::consumer_group_describe_timeout`].
8401    pub async fn consumer_group_describe(
8402        &mut self,
8403        group_ids: &[&str],
8404        include_authorized_operations: bool,
8405    ) -> Result<Vec<DescribedConsumerGroup>> {
8406        let timeout = self.cfg.request_timeout;
8407        self.consumer_group_describe_timeout(group_ids, include_authorized_operations, timeout)
8408            .await
8409    }
8410
8411    /// [`Self::consumer_group_describe`] with a one-shot RPC deadline.
8412    ///
8413    /// ConsumerGroupDescribe has no TimeoutMs; `timeout` is the RPC
8414    /// deadline and the coordinator retry budget.
8415    /// Java `Admin.describeConsumerGroups` is
8416    /// [`Self::describe_consumer_groups_timeout`] (tries this RPC, then
8417    /// DescribeGroups). This overload is the crate-first KIP-848 describe.
8418    pub async fn consumer_group_describe_timeout(
8419        &mut self,
8420        group_ids: &[&str],
8421        include_authorized_operations: bool,
8422        timeout: Duration,
8423    ) -> Result<Vec<DescribedConsumerGroup>> {
8424        let ids: Vec<String> = group_ids.iter().map(|s| (*s).to_string()).collect();
8425        if ids.is_empty() {
8426            return Ok(Vec::new());
8427        }
8428        let version = self.consumer_group_describe_version.ok_or_else(|| {
8429            Error::Unsupported("broker does not support ConsumerGroupDescribe v0-1".into())
8430        })?;
8431        let deadline = Instant::now() + timeout;
8432        let mut attempt = 0u32;
8433        let mut out: Vec<Option<DescribedConsumerGroup>> = vec![None; ids.len()];
8434        let mut pending: Vec<usize> = (0..ids.len()).collect();
8435        loop {
8436            let by_node = self.group_coord_nodes(&ids, &pending).await?;
8437            let mut nodes: Vec<i32> = by_node.keys().copied().collect();
8438            nodes.sort_unstable();
8439            let mut still = Vec::new();
8440            for node in nodes {
8441                let idxs = by_node.get(&node).cloned().unwrap_or_default();
8442                match self
8443                    .consumer_group_describe_on_node(
8444                        node,
8445                        version,
8446                        &ids,
8447                        &idxs,
8448                        include_authorized_operations,
8449                        timeout,
8450                    )
8451                    .await
8452                {
8453                    Ok(done) => {
8454                        for (i, g) in done {
8455                            if error::coordinator_retriable(g.error_code) {
8456                                self.invalidate_group_coord_idxs(&ids, &[i], node);
8457                                still.push(i);
8458                            } else if let Some(slot) = out.get_mut(i) {
8459                                *slot = Some(g);
8460                            }
8461                        }
8462                    }
8463                    Err(e) if e.is_retriable() => {
8464                        self.invalidate_group_coord_idxs(&ids, &idxs, node);
8465                        still.extend(idxs);
8466                    }
8467                    Err(e) => return Err(e),
8468                }
8469            }
8470            pending = still;
8471            if pending.is_empty() {
8472                break;
8473            }
8474            self.wait_retry(&mut attempt, deadline).await?;
8475        }
8476        out.into_iter()
8477            .zip(ids)
8478            .map(|(g, id)| {
8479                g.ok_or_else(|| Error::protocol(format!("ConsumerGroupDescribe missing {id}")))
8480            })
8481            .collect()
8482    }
8483
8484    /// Describe classic consumer groups (DescribeGroups api 15).
8485    ///
8486    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
8487    /// Official Apache JSON listeners are `broker` only. Official
8488    /// listed per-group errors include `NOT_COORDINATOR` (16). This is
8489    /// not a controller hop and not a partition-leader hop: there is no
8490    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41) retry,
8491    /// and no `NOT_LEADER_OR_FOLLOWER` (6) hop. `COORDINATOR_LOAD_IN_PROGRESS`
8492    /// / `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh the
8493    /// coordinator and retry. ErrorCode is per-group (bytes 5–6 on
8494    /// leftover-empty fixture group `"g"` at v6), not top-level after throttle.
8495    /// Negotiates v0–v6 (v3 IncludeAuthorizedOperations; v4 GroupInstanceId;
8496    /// v5 flexible; v6 ErrorMessage / GROUP_ID_NOT_FOUND).
8497    /// Java `describeClassicGroups` is [`Self::describe_classic_groups`].
8498    /// Java `describeConsumerGroups` is [`Self::describe_consumer_groups`].
8499    /// FindCoordinator v4+ CoordinatorKeys array of N (KIP-699): one
8500    /// FindCoordinator per retry for uncached groups. DescribeGroups is
8501    /// one RPC per coordinator. Brokers that only speak FindCoordinator
8502    /// v1–v3 get one FindCoordinator per uncached group. Empty input is
8503    /// a no-op. DescribeGroups has no TimeoutMs; the RPC deadline is
8504    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8505    /// [`Self::describe_groups_timeout`].
8506    pub async fn describe_groups(
8507        &mut self,
8508        group_ids: &[&str],
8509        include_authorized_operations: bool,
8510    ) -> Result<Vec<DescribedGroup>> {
8511        let timeout = self.cfg.request_timeout;
8512        self.describe_groups_timeout(group_ids, include_authorized_operations, timeout)
8513            .await
8514    }
8515
8516    /// [`Self::describe_groups`] with a one-shot RPC deadline (Java
8517    /// `DescribeClassicGroupsOptions` / `DescribeConsumerGroupsOptions.timeoutMs`).
8518    ///
8519    /// DescribeGroups has no TimeoutMs; `timeout` is the RPC deadline
8520    /// and the coordinator retry budget.
8521    pub async fn describe_groups_timeout(
8522        &mut self,
8523        group_ids: &[&str],
8524        include_authorized_operations: bool,
8525        timeout: Duration,
8526    ) -> Result<Vec<DescribedGroup>> {
8527        let ids: Vec<String> = group_ids.iter().map(|s| (*s).to_string()).collect();
8528        if ids.is_empty() {
8529            return Ok(Vec::new());
8530        }
8531        let version = self.describe_groups_version;
8532        let deadline = Instant::now() + timeout;
8533        let mut attempt = 0u32;
8534        let mut out: Vec<Option<DescribedGroup>> = vec![None; ids.len()];
8535        let mut pending: Vec<usize> = (0..ids.len()).collect();
8536        loop {
8537            let by_node = self.group_coord_nodes(&ids, &pending).await?;
8538            let mut nodes: Vec<i32> = by_node.keys().copied().collect();
8539            nodes.sort_unstable();
8540            let mut still = Vec::new();
8541            for node in nodes {
8542                let idxs = by_node.get(&node).cloned().unwrap_or_default();
8543                match self
8544                    .describe_groups_on_node(
8545                        node,
8546                        version,
8547                        &ids,
8548                        &idxs,
8549                        include_authorized_operations,
8550                        timeout,
8551                    )
8552                    .await
8553                {
8554                    Ok(done) => {
8555                        for (i, g) in done {
8556                            if error::coordinator_retriable(g.error_code) {
8557                                self.invalidate_group_coord_idxs(&ids, &[i], node);
8558                                still.push(i);
8559                            } else if let Some(slot) = out.get_mut(i) {
8560                                *slot = Some(g);
8561                            }
8562                        }
8563                    }
8564                    Err(e) if e.is_retriable() => {
8565                        self.invalidate_group_coord_idxs(&ids, &idxs, node);
8566                        still.extend(idxs);
8567                    }
8568                    Err(e) => return Err(e),
8569                }
8570            }
8571            pending = still;
8572            if pending.is_empty() {
8573                break;
8574            }
8575            self.wait_retry(&mut attempt, deadline).await?;
8576        }
8577        out.into_iter()
8578            .zip(ids)
8579            .map(|(g, id)| g.ok_or_else(|| Error::protocol(format!("DescribeGroups missing {id}"))))
8580            .collect()
8581    }
8582
8583    /// Describe classic groups (Java `Admin.describeClassicGroups`).
8584    ///
8585    /// Same wire as [`Self::describe_groups`]: DescribeGroups api 15 on
8586    /// the group coordinator. Java's `DescribeClassicGroupsHandler`
8587    /// builds a DescribeGroups request. Empty input is a no-op.
8588    /// DescribeGroups has no TimeoutMs; the RPC deadline is
8589    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8590    /// [`Self::describe_classic_groups_timeout`].
8591    pub async fn describe_classic_groups(
8592        &mut self,
8593        group_ids: &[&str],
8594        include_authorized_operations: bool,
8595    ) -> Result<Vec<DescribedGroup>> {
8596        let timeout = self.cfg.request_timeout;
8597        self.describe_classic_groups_timeout(group_ids, include_authorized_operations, timeout)
8598            .await
8599    }
8600
8601    /// [`Self::describe_classic_groups`] with a one-shot RPC deadline (Java
8602    /// `DescribeClassicGroupsOptions.timeoutMs`).
8603    ///
8604    /// DescribeGroups has no TimeoutMs; `timeout` is the RPC deadline
8605    /// and the coordinator retry budget.
8606    pub async fn describe_classic_groups_timeout(
8607        &mut self,
8608        group_ids: &[&str],
8609        include_authorized_operations: bool,
8610        timeout: Duration,
8611    ) -> Result<Vec<DescribedGroup>> {
8612        self.describe_groups_timeout(group_ids, include_authorized_operations, timeout)
8613            .await
8614    }
8615
8616    /// Describe consumer groups (Java `Admin.describeConsumerGroups`).
8617    ///
8618    /// Kafka 4.0 `DescribeConsumerGroupsHandler` sends ConsumerGroupDescribe
8619    /// (api 69) on the group coordinator first. Per-group
8620    /// [`crate::error::UNSUPPORTED_VERSION`] (35) or
8621    /// [`crate::error::GROUP_ID_NOT_FOUND`] (69) retries that group with
8622    /// DescribeGroups (api 15). A broker that does not advertise api 69, or
8623    /// an RPC-level [`Error::Unsupported`] for that API, uses DescribeGroups
8624    /// for every group. Coordinator errors still refresh FindCoordinator
8625    /// and retry. Empty input is a no-op. Neither RPC has TimeoutMs; the
8626    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
8627    /// deadline, use [`Self::describe_consumer_groups_timeout`].
8628    pub async fn describe_consumer_groups(
8629        &mut self,
8630        group_ids: &[&str],
8631        include_authorized_operations: bool,
8632    ) -> Result<Vec<ConsumerGroupDescription>> {
8633        let timeout = self.cfg.request_timeout;
8634        self.describe_consumer_groups_timeout(group_ids, include_authorized_operations, timeout)
8635            .await
8636    }
8637
8638    /// [`Self::describe_consumer_groups`] with a one-shot RPC deadline (Java
8639    /// `DescribeConsumerGroupsOptions.timeoutMs`).
8640    ///
8641    /// Neither ConsumerGroupDescribe nor DescribeGroups has TimeoutMs;
8642    /// `timeout` is the RPC deadline and the coordinator retry budget
8643    /// across both hops.
8644    pub async fn describe_consumer_groups_timeout(
8645        &mut self,
8646        group_ids: &[&str],
8647        include_authorized_operations: bool,
8648        timeout: Duration,
8649    ) -> Result<Vec<ConsumerGroupDescription>> {
8650        if group_ids.is_empty() {
8651            return Ok(Vec::new());
8652        }
8653        let deadline = Instant::now() + timeout;
8654        let mut out: Vec<Option<ConsumerGroupDescription>> = vec![None; group_ids.len()];
8655        let mut classic_pending: Vec<usize> = Vec::new();
8656        if self.consumer_group_describe_version.is_some() {
8657            match self
8658                .consumer_group_describe_timeout(group_ids, include_authorized_operations, timeout)
8659                .await
8660            {
8661                Ok(groups) => {
8662                    for (i, g) in groups.into_iter().enumerate() {
8663                        if error::consumer_group_describe_classic_fallback(g.error_code) {
8664                            classic_pending.push(i);
8665                        } else if let Some(slot) = out.get_mut(i) {
8666                            *slot = Some(ConsumerGroupDescription::Consumer(g));
8667                        }
8668                    }
8669                }
8670                Err(Error::Unsupported(_)) => {
8671                    classic_pending.extend(0..group_ids.len());
8672                }
8673                Err(e) => return Err(e),
8674            }
8675        } else {
8676            classic_pending.extend(0..group_ids.len());
8677        }
8678        if !classic_pending.is_empty() {
8679            let classic_ids: Vec<&str> = classic_pending
8680                .iter()
8681                .filter_map(|&i| group_ids.get(i).copied())
8682                .collect();
8683            let remaining = deadline.saturating_duration_since(Instant::now());
8684            let classic = self
8685                .describe_groups_timeout(&classic_ids, include_authorized_operations, remaining)
8686                .await?;
8687            for (i, g) in classic_pending.iter().copied().zip(classic) {
8688                if let Some(slot) = out.get_mut(i) {
8689                    *slot = Some(ConsumerGroupDescription::Classic(g));
8690                }
8691            }
8692        }
8693        out.into_iter()
8694            .enumerate()
8695            .map(|(i, g)| {
8696                g.ok_or_else(|| {
8697                    let id = group_ids.get(i).copied().unwrap_or("");
8698                    Error::protocol(format!("describeConsumerGroups missing {id}"))
8699                })
8700            })
8701            .collect()
8702    }
8703
8704    /// List consumer groups (ListGroups api 16).
8705    ///
8706    /// Lands on the connected broker (bootstrap is fine). Official Apache
8707    /// JSON listeners are `broker` only. Official listed errors are
8708    /// `COORDINATOR_LOAD_IN_PROGRESS` (14), `COORDINATOR_NOT_AVAILABLE`
8709    /// (15), `AUTHORIZATION_FAILED` (29). `NOT_COORDINATOR` (16) is not
8710    /// listed. This is not a group-coordinator hop, not a controller hop,
8711    /// and not a partition-leader hop: there is no FindCoordinator,
8712    /// no Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41)
8713    /// retry, and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level
8714    /// `error_code` is the INT16 at bytes 4–5 on v1+ (after throttle) — not a
8715    /// first-group field. Negotiates v0–v5 (v3 flexible; v4 StatesFilter /
8716    /// GroupState; v5 TypesFilter / GroupType). Java `listConsumerGroups` is
8717    /// [`Self::list_consumer_groups`]. ListGroups has no TimeoutMs; the
8718    /// RPC deadline is [`AdminConfig::request_timeout`]. For a one-shot
8719    /// deadline, use [`Self::list_groups_timeout`].
8720    pub async fn list_groups(
8721        &mut self,
8722        states_filter: &[&str],
8723        types_filter: &[&str],
8724    ) -> Result<Vec<ListedGroup>> {
8725        let timeout = self.cfg.request_timeout;
8726        self.list_groups_timeout(states_filter, types_filter, timeout)
8727            .await
8728    }
8729
8730    /// [`Self::list_groups`] with a one-shot RPC deadline (Java
8731    /// `ListGroupsOptions.timeoutMs`).
8732    ///
8733    /// ListGroups has no TimeoutMs; `timeout` is the RPC deadline.
8734    pub async fn list_groups_timeout(
8735        &mut self,
8736        states_filter: &[&str],
8737        types_filter: &[&str],
8738        timeout: Duration,
8739    ) -> Result<Vec<ListedGroup>> {
8740        let states: Vec<String> = states_filter.iter().map(|s| (*s).to_string()).collect();
8741        let types: Vec<String> = types_filter.iter().map(|s| (*s).to_string()).collect();
8742        self.list_groups_owned(states, types, timeout).await
8743    }
8744
8745    async fn list_groups_owned(
8746        &mut self,
8747        states: Vec<String>,
8748        types: Vec<String>,
8749        timeout: Duration,
8750    ) -> Result<Vec<ListedGroup>> {
8751        let version = self.list_groups_version;
8752        let body = self
8753            .roundtrip_bootstrap(
8754                LIST_GROUPS,
8755                version,
8756                |buf| encode_list_groups_request(buf, version, &states, &types),
8757                timeout,
8758            )
8759            .await?;
8760        let (resp, ..) = decode_list_groups_response(&mut body.clone(), version)?;
8761        if resp.error_code != 0 {
8762            return Err(Error::broker(resp.error_code, "ListGroups"));
8763        }
8764        Ok(resp.groups)
8765    }
8766
8767    /// [`Self::list_groups`] with Java `GroupState` / `GroupType`
8768    /// (`ListGroupsOptions.inGroupStates` / `withTypes`).
8769    ///
8770    /// TypesFilter strings are Java `GroupType.toString` (`Classic`, not
8771    /// `classic`). ListGroups has no TimeoutMs; the RPC deadline is
8772    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8773    /// [`Self::list_groups_with_timeout`].
8774    pub async fn list_groups_with(
8775        &mut self,
8776        states: impl IntoIterator<Item = GroupState>,
8777        types: impl IntoIterator<Item = GroupType>,
8778    ) -> Result<Vec<ListedGroup>> {
8779        let timeout = self.cfg.request_timeout;
8780        self.list_groups_with_timeout(states, types, timeout).await
8781    }
8782
8783    /// [`Self::list_groups_with`] with a one-shot RPC deadline (Java
8784    /// `ListGroupsOptions.timeoutMs`).
8785    ///
8786    /// ListGroups has no TimeoutMs; `timeout` is the RPC deadline.
8787    pub async fn list_groups_with_timeout(
8788        &mut self,
8789        states: impl IntoIterator<Item = GroupState>,
8790        types: impl IntoIterator<Item = GroupType>,
8791        timeout: Duration,
8792    ) -> Result<Vec<ListedGroup>> {
8793        let states: Vec<String> = states.into_iter().map(String::from).collect();
8794        let types: Vec<String> = types.into_iter().map(String::from).collect();
8795        self.list_groups_owned(states, types, timeout).await
8796    }
8797
8798    /// List every group (Java `Admin.listGroups()`).
8799    ///
8800    /// Same wire as [`Self::list_groups`] with empty StatesFilter and
8801    /// TypesFilter (Java `ListGroupsOptions` default).
8802    pub async fn list_groups_all(&mut self) -> Result<Vec<ListedGroup>> {
8803        self.list_groups(&[], &[]).await
8804    }
8805
8806    /// [`Self::list_groups_all`] with a one-shot RPC deadline (Java
8807    /// `ListGroupsOptions.timeoutMs`).
8808    pub async fn list_groups_all_timeout(&mut self, timeout: Duration) -> Result<Vec<ListedGroup>> {
8809        self.list_groups_timeout(&[], &[], timeout).await
8810    }
8811
8812    /// List consumer groups (Java `Admin.listConsumerGroups`).
8813    ///
8814    /// Same wire as [`Self::list_groups`]: ListGroups api 16 on the
8815    /// connected broker. Java's `ListConsumerGroupsHandler` builds a
8816    /// ListGroups request. ListGroups has no TimeoutMs; the RPC
8817    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
8818    /// deadline, use [`Self::list_consumer_groups_timeout`].
8819    pub async fn list_consumer_groups(
8820        &mut self,
8821        states_filter: &[&str],
8822        types_filter: &[&str],
8823    ) -> Result<Vec<ListedGroup>> {
8824        self.list_groups(states_filter, types_filter).await
8825    }
8826
8827    /// [`Self::list_consumer_groups`] with a one-shot RPC deadline (Java
8828    /// `ListConsumerGroupsOptions.timeoutMs`).
8829    ///
8830    /// ListGroups has no TimeoutMs; `timeout` is the RPC deadline.
8831    pub async fn list_consumer_groups_timeout(
8832        &mut self,
8833        states_filter: &[&str],
8834        types_filter: &[&str],
8835        timeout: Duration,
8836    ) -> Result<Vec<ListedGroup>> {
8837        self.list_groups_timeout(states_filter, types_filter, timeout)
8838            .await
8839    }
8840
8841    /// [`Self::list_consumer_groups`] with Java `GroupState` / `GroupType`
8842    /// (`ListConsumerGroupsOptions.inGroupStates` / `withTypes`).
8843    ///
8844    /// Same wire as [`Self::list_groups_with`].
8845    pub async fn list_consumer_groups_with(
8846        &mut self,
8847        states: impl IntoIterator<Item = GroupState>,
8848        types: impl IntoIterator<Item = GroupType>,
8849    ) -> Result<Vec<ListedGroup>> {
8850        self.list_groups_with(states, types).await
8851    }
8852
8853    /// [`Self::list_consumer_groups_with`] with a one-shot RPC deadline
8854    /// (Java `ListConsumerGroupsOptions.timeoutMs`).
8855    pub async fn list_consumer_groups_with_timeout(
8856        &mut self,
8857        states: impl IntoIterator<Item = GroupState>,
8858        types: impl IntoIterator<Item = GroupType>,
8859        timeout: Duration,
8860    ) -> Result<Vec<ListedGroup>> {
8861        self.list_groups_with_timeout(states, types, timeout).await
8862    }
8863
8864    /// List every consumer group (Java `Admin.listConsumerGroups()`).
8865    ///
8866    /// Same wire as [`Self::list_consumer_groups`] with empty StatesFilter
8867    /// and TypesFilter (Java `ListConsumerGroupsOptions` default).
8868    pub async fn list_consumer_groups_all(&mut self) -> Result<Vec<ListedGroup>> {
8869        self.list_groups_all().await
8870    }
8871
8872    /// [`Self::list_consumer_groups_all`] with a one-shot RPC deadline
8873    /// (Java `ListConsumerGroupsOptions.timeoutMs`).
8874    pub async fn list_consumer_groups_all_timeout(
8875        &mut self,
8876        timeout: Duration,
8877    ) -> Result<Vec<ListedGroup>> {
8878        self.list_groups_all_timeout(timeout).await
8879    }
8880
8881    /// Delete consumer groups (DeleteGroups api 42).
8882    ///
8883    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
8884    /// Negotiates DeleteGroups v0–v2 (Kafka 4.0 `validVersions` `0-2`).
8885    /// v0–v1 are classic; v2 is the first flexible version. Response
8886    /// ThrottleTimeMs is present on every spoken version. Official Apache
8887    /// JSON listeners are `broker` only. Official listed per-group errors
8888    /// include `NOT_COORDINATOR` (16). This is not a controller hop and
8889    /// not a partition-leader hop: there is no Metadata `controller_id`
8890    /// lookup, no `NOT_CONTROLLER` (41) retry, and no
8891    /// `NOT_LEADER_OR_FOLLOWER` (6) hop. `COORDINATOR_LOAD_IN_PROGRESS` /
8892    /// `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh the
8893    /// coordinator and retry. ErrorCode is per-group after GroupId
8894    /// (bytes 7–8 on leftover-empty fixture group `"g"` on v2; classic
8895    /// v0–v1 place that ErrorCode later). Java `deleteShareGroups` is
8896    /// [`Self::delete_share_groups`]. Java `deleteConsumerGroups` is
8897    /// [`Self::delete_consumer_groups`]. FindCoordinator v4+
8898    /// CoordinatorKeys array of N (KIP-699): one FindCoordinator per
8899    /// retry for uncached groups. DeleteGroups is one RPC per
8900    /// coordinator. Brokers that only speak FindCoordinator v1–v3 get
8901    /// one FindCoordinator per uncached group. Empty input is a no-op.
8902    /// DeleteGroups has no TimeoutMs; the RPC deadline is
8903    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8904    /// [`Self::delete_groups_timeout`].
8905    pub async fn delete_groups(&mut self, group_ids: &[&str]) -> Result<Vec<DeletableGroupResult>> {
8906        let timeout = self.cfg.request_timeout;
8907        self.delete_groups_timeout(group_ids, timeout).await
8908    }
8909
8910    /// [`Self::delete_groups`] with a one-shot RPC deadline (Java
8911    /// `DeleteConsumerGroupsOptions` / `DeleteShareGroupsOptions.timeoutMs`).
8912    ///
8913    /// DeleteGroups has no TimeoutMs; `timeout` is the RPC deadline and
8914    /// the coordinator retry budget.
8915    pub async fn delete_groups_timeout(
8916        &mut self,
8917        group_ids: &[&str],
8918        timeout: Duration,
8919    ) -> Result<Vec<DeletableGroupResult>> {
8920        self.delete_group_ids(
8921            group_ids.iter().map(|s| (*s).to_string()).collect(),
8922            timeout,
8923        )
8924        .await
8925    }
8926
8927    async fn delete_group_ids(
8928        &mut self,
8929        ids: Vec<String>,
8930        timeout: Duration,
8931    ) -> Result<Vec<DeletableGroupResult>> {
8932        if ids.is_empty() {
8933            return Ok(Vec::new());
8934        }
8935        let version = self.delete_groups_version;
8936        let deadline = Instant::now() + timeout;
8937        let mut attempt = 0u32;
8938        let mut out: Vec<Option<DeletableGroupResult>> = vec![None; ids.len()];
8939        let mut pending: Vec<usize> = (0..ids.len()).collect();
8940        loop {
8941            let by_node = self.group_coord_nodes(&ids, &pending).await?;
8942            let mut nodes: Vec<i32> = by_node.keys().copied().collect();
8943            nodes.sort_unstable();
8944            let mut still = Vec::new();
8945            for node in nodes {
8946                let idxs = by_node.get(&node).cloned().unwrap_or_default();
8947                match self
8948                    .delete_groups_on_node(node, version, &ids, &idxs, timeout)
8949                    .await
8950                {
8951                    Ok(done) => {
8952                        for (i, g) in done {
8953                            if error::coordinator_retriable(g.error_code) {
8954                                self.invalidate_group_coord_idxs(&ids, &[i], node);
8955                                still.push(i);
8956                            } else if let Some(slot) = out.get_mut(i) {
8957                                *slot = Some(g);
8958                            }
8959                        }
8960                    }
8961                    Err(e) if e.is_retriable() => {
8962                        self.invalidate_group_coord_idxs(&ids, &idxs, node);
8963                        still.extend(idxs);
8964                    }
8965                    Err(e) => return Err(e),
8966                }
8967            }
8968            pending = still;
8969            if pending.is_empty() {
8970                break;
8971            }
8972            self.wait_retry(&mut attempt, deadline).await?;
8973        }
8974        out.into_iter()
8975            .zip(ids)
8976            .map(|(g, id)| g.ok_or_else(|| Error::protocol(format!("DeleteGroups missing {id}"))))
8977            .collect()
8978    }
8979
8980    /// Delete share groups (Java `Admin.deleteShareGroups`).
8981    ///
8982    /// Same wire as [`Self::delete_groups`]: DeleteGroups api 42 on the
8983    /// group coordinator (`FindCoordinator` `key_type=0`). Java's
8984    /// `DeleteShareGroupsHandler` extends `DeleteGroupsHandler`. Empty
8985    /// input is a no-op. DeleteGroups has no TimeoutMs; the RPC deadline
8986    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
8987    /// [`Self::delete_share_groups_timeout`].
8988    pub async fn delete_share_groups(
8989        &mut self,
8990        group_ids: impl IntoIterator<Item = impl AsRef<str>>,
8991    ) -> Result<Vec<DeletableGroupResult>> {
8992        let timeout = self.cfg.request_timeout;
8993        self.delete_share_groups_timeout(group_ids, timeout).await
8994    }
8995
8996    /// [`Self::delete_share_groups`] with a one-shot RPC deadline (Java
8997    /// `DeleteShareGroupsOptions.timeoutMs`).
8998    ///
8999    /// DeleteGroups has no TimeoutMs; `timeout` is the RPC deadline and
9000    /// the coordinator retry budget.
9001    pub async fn delete_share_groups_timeout(
9002        &mut self,
9003        group_ids: impl IntoIterator<Item = impl AsRef<str>>,
9004        timeout: Duration,
9005    ) -> Result<Vec<DeletableGroupResult>> {
9006        let ids: Vec<String> = group_ids
9007            .into_iter()
9008            .map(|s| s.as_ref().to_string())
9009            .collect();
9010        self.delete_group_ids(ids, timeout).await
9011    }
9012
9013    /// Delete consumer groups (Java `Admin.deleteConsumerGroups`).
9014    ///
9015    /// Same wire as [`Self::delete_groups`]: DeleteGroups api 42 on the
9016    /// group coordinator (`FindCoordinator` `key_type=0`). Java's
9017    /// `DeleteConsumerGroupsHandler` sends DeleteGroups. Empty
9018    /// input is a no-op. DeleteGroups has no TimeoutMs; the RPC deadline
9019    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
9020    /// [`Self::delete_consumer_groups_timeout`].
9021    pub async fn delete_consumer_groups(
9022        &mut self,
9023        group_ids: impl IntoIterator<Item = impl AsRef<str>>,
9024    ) -> Result<Vec<DeletableGroupResult>> {
9025        let timeout = self.cfg.request_timeout;
9026        self.delete_consumer_groups_timeout(group_ids, timeout)
9027            .await
9028    }
9029
9030    /// [`Self::delete_consumer_groups`] with a one-shot RPC deadline (Java
9031    /// `DeleteConsumerGroupsOptions.timeoutMs`).
9032    ///
9033    /// DeleteGroups has no TimeoutMs; `timeout` is the RPC deadline and
9034    /// the coordinator retry budget.
9035    pub async fn delete_consumer_groups_timeout(
9036        &mut self,
9037        group_ids: impl IntoIterator<Item = impl AsRef<str>>,
9038        timeout: Duration,
9039    ) -> Result<Vec<DeletableGroupResult>> {
9040        let ids: Vec<String> = group_ids
9041            .into_iter()
9042            .map(|s| s.as_ref().to_string())
9043            .collect();
9044        self.delete_group_ids(ids, timeout).await
9045    }
9046
9047    /// Remove static members from a consumer group (Java
9048    /// `removeMembersFromConsumerGroup`).
9049    ///
9050    /// LeaveGroup v3 (classic), v4 (flexible), or v5 (KIP-800 Reason)
9051    /// on the group coordinator (`FindCoordinator` `key_type=0`) with a
9052    /// members array. Each [`MemberToRemove`] is a `group.instance.id`
9053    /// (KIP-345); member id on the wire is empty. v5 sends
9054    /// [`DEFAULT_LEAVE_GROUP_REASON`]. Empty `members` returns an empty
9055    /// list. Coordinator load / move errors refresh and retry. To remove
9056    /// every member, use [`Self::remove_all_members_from_consumer_group`].
9057    /// For a custom LeaveGroup v5 Reason, use
9058    /// [`Self::remove_members_from_consumer_group_with_reason`].
9059    /// LeaveGroup has no TimeoutMs; the RPC deadline is
9060    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
9061    /// [`Self::remove_members_from_consumer_group_timeout`].
9062    pub async fn remove_members_from_consumer_group(
9063        &mut self,
9064        group_id: &str,
9065        members: impl IntoIterator<Item = impl Into<MemberToRemove>>,
9066    ) -> Result<Vec<RemovedMember>> {
9067        let timeout = self.cfg.request_timeout;
9068        self.remove_members_from_consumer_group_timeout(group_id, members, timeout)
9069            .await
9070    }
9071
9072    /// [`Self::remove_members_from_consumer_group`] with a one-shot RPC
9073    /// deadline (Java `RemoveMembersFromConsumerGroupOptions.timeoutMs`).
9074    ///
9075    /// LeaveGroup has no TimeoutMs; `timeout` is the RPC deadline and the
9076    /// coordinator retry budget.
9077    pub async fn remove_members_from_consumer_group_timeout(
9078        &mut self,
9079        group_id: &str,
9080        members: impl IntoIterator<Item = impl Into<MemberToRemove>>,
9081        timeout: Duration,
9082    ) -> Result<Vec<RemovedMember>> {
9083        self.remove_members_from_consumer_group_timeout_with_reason(group_id, members, timeout, "")
9084            .await
9085    }
9086
9087    /// [`Self::remove_members_from_consumer_group`] with Java
9088    /// `RemoveMembersFromConsumerGroupOptions.reason`.
9089    ///
9090    /// LeaveGroup v5 sends `reason` (KIP-800). Empty reason uses
9091    /// [`DEFAULT_LEAVE_GROUP_REASON`]. The string is truncated to 255
9092    /// characters. Kafka 4.0 `KafkaAdminClient` does not wire this
9093    /// Options field; later Java and this crate do. For a one-shot
9094    /// deadline, use
9095    /// [`Self::remove_members_from_consumer_group_timeout_with_reason`].
9096    pub async fn remove_members_from_consumer_group_with_reason(
9097        &mut self,
9098        group_id: &str,
9099        members: impl IntoIterator<Item = impl Into<MemberToRemove>>,
9100        reason: impl Into<String>,
9101    ) -> Result<Vec<RemovedMember>> {
9102        let timeout = self.cfg.request_timeout;
9103        self.remove_members_from_consumer_group_timeout_with_reason(
9104            group_id, members, timeout, reason,
9105        )
9106        .await
9107    }
9108
9109    /// [`Self::remove_members_from_consumer_group_with_reason`] with a
9110    /// one-shot RPC deadline (Java
9111    /// `RemoveMembersFromConsumerGroupOptions.timeoutMs` plus `reason`).
9112    ///
9113    /// LeaveGroup has no TimeoutMs; `timeout` is the RPC deadline and the
9114    /// coordinator retry budget. Duration is before `reason`.
9115    pub async fn remove_members_from_consumer_group_timeout_with_reason(
9116        &mut self,
9117        group_id: &str,
9118        members: impl IntoIterator<Item = impl Into<MemberToRemove>>,
9119        timeout: Duration,
9120        reason: impl Into<String>,
9121    ) -> Result<Vec<RemovedMember>> {
9122        let reason = reason.into();
9123        let members: Vec<LeaveGroupMember> = members
9124            .into_iter()
9125            .map(|m| {
9126                let m = m.into();
9127                LeaveGroupMember {
9128                    member_id: String::new(),
9129                    group_instance_id: Some(m.group_instance_id),
9130                    reason: Some(reason.clone()),
9131                }
9132            })
9133            .collect();
9134        if members.is_empty() {
9135            return Ok(Vec::new());
9136        }
9137        self.leave_group_members(group_id, members, timeout).await
9138    }
9139
9140    /// Remove every member of a consumer group (Java
9141    /// `RemoveMembersFromConsumerGroupOptions.removeAll`).
9142    ///
9143    /// DescribeGroups, then LeaveGroup v3–v5 with those members (member
9144    /// id plus `group.instance.id` when present). v5 sends
9145    /// [`DEFAULT_LEAVE_GROUP_REASON`]. A group with no members is a
9146    /// no-op (no LeaveGroup). This is not
9147    /// [`Self::remove_members_from_consumer_group`] with an empty list.
9148    /// For a custom LeaveGroup v5 Reason, use
9149    /// [`Self::remove_all_members_from_consumer_group_with_reason`].
9150    /// DescribeGroups and LeaveGroup have no TimeoutMs; the RPC deadline
9151    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
9152    /// [`Self::remove_all_members_from_consumer_group_timeout`].
9153    pub async fn remove_all_members_from_consumer_group(
9154        &mut self,
9155        group_id: &str,
9156    ) -> Result<Vec<RemovedMember>> {
9157        let timeout = self.cfg.request_timeout;
9158        self.remove_all_members_from_consumer_group_timeout(group_id, timeout)
9159            .await
9160    }
9161
9162    /// [`Self::remove_all_members_from_consumer_group`] with a one-shot RPC
9163    /// deadline (Java `RemoveMembersFromConsumerGroupOptions.timeoutMs`
9164    /// plus `removeAll`).
9165    ///
9166    /// DescribeGroups and LeaveGroup have no TimeoutMs; `timeout` is the
9167    /// RPC deadline and the coordinator retry budget for both hops.
9168    pub async fn remove_all_members_from_consumer_group_timeout(
9169        &mut self,
9170        group_id: &str,
9171        timeout: Duration,
9172    ) -> Result<Vec<RemovedMember>> {
9173        self.remove_all_members_from_consumer_group_timeout_with_reason(group_id, timeout, "")
9174            .await
9175    }
9176
9177    /// [`Self::remove_all_members_from_consumer_group`] with Java
9178    /// `RemoveMembersFromConsumerGroupOptions.reason` plus `removeAll`.
9179    ///
9180    /// LeaveGroup v5 sends `reason` (KIP-800). Empty reason uses
9181    /// [`DEFAULT_LEAVE_GROUP_REASON`]. The string is truncated to 255
9182    /// characters. Kafka 4.0 `KafkaAdminClient` does not wire this
9183    /// Options field; later Java and this crate do. For a one-shot
9184    /// deadline, use
9185    /// [`Self::remove_all_members_from_consumer_group_timeout_with_reason`].
9186    pub async fn remove_all_members_from_consumer_group_with_reason(
9187        &mut self,
9188        group_id: &str,
9189        reason: impl Into<String>,
9190    ) -> Result<Vec<RemovedMember>> {
9191        let timeout = self.cfg.request_timeout;
9192        self.remove_all_members_from_consumer_group_timeout_with_reason(group_id, timeout, reason)
9193            .await
9194    }
9195
9196    /// [`Self::remove_all_members_from_consumer_group_with_reason`] with a
9197    /// one-shot RPC deadline (Java
9198    /// `RemoveMembersFromConsumerGroupOptions.timeoutMs` plus `removeAll`
9199    /// and `reason`).
9200    ///
9201    /// DescribeGroups and LeaveGroup have no TimeoutMs; `timeout` is the
9202    /// RPC deadline and the coordinator retry budget for both hops.
9203    /// Duration is before `reason`.
9204    pub async fn remove_all_members_from_consumer_group_timeout_with_reason(
9205        &mut self,
9206        group_id: &str,
9207        timeout: Duration,
9208        reason: impl Into<String>,
9209    ) -> Result<Vec<RemovedMember>> {
9210        let described = self
9211            .describe_groups_timeout(&[group_id], false, timeout)
9212            .await?;
9213        let Some(g) = described.first() else {
9214            return Ok(Vec::new());
9215        };
9216        if g.error_code != 0 {
9217            return Err(Error::broker(g.error_code, "DescribeGroups"));
9218        }
9219        let reason = reason.into();
9220        let members: Vec<LeaveGroupMember> = g
9221            .members
9222            .iter()
9223            .map(|m| LeaveGroupMember {
9224                member_id: m.member_id.clone(),
9225                group_instance_id: m.group_instance_id.clone(),
9226                reason: Some(reason.clone()),
9227            })
9228            .collect();
9229        if members.is_empty() {
9230            return Ok(Vec::new());
9231        }
9232        self.leave_group_members(group_id, members, timeout).await
9233    }
9234
9235    async fn leave_group_members(
9236        &mut self,
9237        group_id: &str,
9238        members: Vec<LeaveGroupMember>,
9239        timeout: Duration,
9240    ) -> Result<Vec<RemovedMember>> {
9241        let version = self
9242            .versions
9243            .get(&LEAVE_GROUP)
9244            .and_then(|v| pick_version(v.min_version, v.max_version, 3, 5))
9245            .ok_or_else(|| Error::Unsupported("broker does not support LeaveGroup v3+".into()))?;
9246        let members: Vec<LeaveGroupMember> = members
9247            .into_iter()
9248            .map(|mut m| {
9249                m.reason = Some(admin_leave_reason(m.reason.as_deref()));
9250                m
9251            })
9252            .collect();
9253        let deadline = Instant::now() + timeout;
9254        let mut attempt = 0u32;
9255        let group_id = group_id.to_string();
9256        loop {
9257            let stale = self
9258                .group_coord
9259                .as_ref()
9260                .is_none_or(|(g, _)| g != &group_id);
9261            if stale {
9262                let node = self.discover_group_coord(&group_id).await?;
9263                self.group_coord = Some((group_id.clone(), node));
9264            }
9265            let node = self
9266                .group_coord
9267                .as_ref()
9268                .map(|(_, n)| *n)
9269                .ok_or_else(|| Error::protocol("missing group coordinator"))?;
9270            self.connect_node(node).await?;
9271            let body = {
9272                let conn = self.conns.get_mut(&node).ok_or_else(|| {
9273                    Error::protocol("missing remove_members_from_consumer_group conn")
9274                })?;
9275                conn.roundtrip(
9276                    LEAVE_GROUP,
9277                    version,
9278                    |buf| encode_leave_group_request_members(buf, version, &group_id, &members),
9279                    timeout,
9280                )
9281                .await
9282            };
9283            let body = match body {
9284                Ok(b) => b,
9285                Err(e) if e.is_retriable() => {
9286                    let _ = self.conns.remove(&node);
9287                    self.group_coord = None;
9288                    self.wait_retry(&mut attempt, deadline).await?;
9289                    continue;
9290                }
9291                Err(e) => return Err(e),
9292            };
9293            let (err, results, ..) =
9294                decode_leave_group_response_version(&mut body.clone(), version)?;
9295            if error::coordinator_retriable(err) {
9296                self.group_coord = None;
9297                let _ = self.conns.remove(&node);
9298                self.wait_retry(&mut attempt, deadline).await?;
9299                continue;
9300            }
9301            if err != 0 {
9302                return Err(Error::broker(err, "LeaveGroup"));
9303            }
9304            return Ok(results
9305                .into_iter()
9306                .map(|m| RemovedMember {
9307                    member_id: m.member_id,
9308                    group_instance_id: m.group_instance_id,
9309                    error_code: m.error_code,
9310                })
9311                .collect());
9312        }
9313    }
9314
9315    /// Describe KIP-932 share groups (ShareGroupDescribe api 77).
9316    ///
9317    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
9318    /// Negotiates ShareGroupDescribe v0–v1. Kafka 4.0 `validVersions` is
9319    /// `"0"` (`latestVersionUnstable`). Kafka 4.1 `validVersions` is
9320    /// `"1"` (v0 removed). Request and response fields are identical.
9321    /// This crate speaks 0–1. Official Apache JSON listeners are
9322    /// `broker` only. Official listed errors include `NOT_COORDINATOR`
9323    /// (16). Official Java `DescribeShareGroupsHandler` uses
9324    /// `CoordinatorType.GROUP`. This is not a controller hop and not a
9325    /// partition-leader hop: there is no Metadata `controller_id`
9326    /// lookup, no `NOT_CONTROLLER` (41) retry, and no
9327    /// `NOT_LEADER_OR_FOLLOWER` (6) hop. SHARE (`key_type=2`) is the
9328    /// FindCoordinator v6 share-state key
9329    /// (`groupId:topicId:partition`) and is not used here.
9330    /// `COORDINATOR_LOAD_IN_PROGRESS` / `COORDINATOR_NOT_AVAILABLE` /
9331    /// `NOT_COORDINATOR` (16) refresh the coordinator and retry.
9332    /// ErrorCode is per-group (bytes 5–6 on leftover-empty fixture
9333    /// group `"g"`), not top-level after throttle.
9334    /// Java `describeShareGroups` is [`Self::describe_share_groups`].
9335    /// FindCoordinator v4+ CoordinatorKeys array of N (KIP-699): one
9336    /// FindCoordinator per retry for uncached groups. ShareGroupDescribe
9337    /// is one RPC per coordinator. Brokers that only speak FindCoordinator
9338    /// v1–v3 get one FindCoordinator per uncached group. Empty input is
9339    /// a no-op. ShareGroupDescribe has no TimeoutMs; the RPC deadline
9340    /// is [`AdminConfig::request_timeout`]. For a one-shot deadline, use
9341    /// [`Self::share_group_describe_timeout`].
9342    pub async fn share_group_describe(
9343        &mut self,
9344        group_ids: &[&str],
9345        include_authorized_operations: bool,
9346    ) -> Result<Vec<DescribedShareGroup>> {
9347        let timeout = self.cfg.request_timeout;
9348        self.share_group_describe_timeout(group_ids, include_authorized_operations, timeout)
9349            .await
9350    }
9351
9352    /// [`Self::share_group_describe`] with a one-shot RPC deadline (Java
9353    /// `DescribeShareGroupsOptions.timeoutMs`).
9354    ///
9355    /// ShareGroupDescribe has no TimeoutMs; `timeout` is the RPC deadline
9356    /// and the coordinator retry budget.
9357    pub async fn share_group_describe_timeout(
9358        &mut self,
9359        group_ids: &[&str],
9360        include_authorized_operations: bool,
9361        timeout: Duration,
9362    ) -> Result<Vec<DescribedShareGroup>> {
9363        let ids: Vec<String> = group_ids.iter().map(|s| (*s).to_string()).collect();
9364        if ids.is_empty() {
9365            return Ok(Vec::new());
9366        }
9367        let version = self.share_group_describe_version.ok_or_else(|| {
9368            Error::Unsupported("broker does not support ShareGroupDescribe v0-1".into())
9369        })?;
9370        let deadline = Instant::now() + timeout;
9371        let mut attempt = 0u32;
9372        let mut out: Vec<Option<DescribedShareGroup>> = vec![None; ids.len()];
9373        let mut pending: Vec<usize> = (0..ids.len()).collect();
9374        loop {
9375            let by_node = self.group_coord_nodes(&ids, &pending).await?;
9376            let mut nodes: Vec<i32> = by_node.keys().copied().collect();
9377            nodes.sort_unstable();
9378            let mut still = Vec::new();
9379            for node in nodes {
9380                let idxs = by_node.get(&node).cloned().unwrap_or_default();
9381                match self
9382                    .share_group_describe_on_node(
9383                        node,
9384                        version,
9385                        &ids,
9386                        &idxs,
9387                        include_authorized_operations,
9388                        timeout,
9389                    )
9390                    .await
9391                {
9392                    Ok(done) => {
9393                        for (i, g) in done {
9394                            if error::coordinator_retriable(g.error_code) {
9395                                self.invalidate_group_coord_idxs(&ids, &[i], node);
9396                                still.push(i);
9397                            } else if let Some(slot) = out.get_mut(i) {
9398                                *slot = Some(g);
9399                            }
9400                        }
9401                    }
9402                    Err(e) if e.is_retriable() => {
9403                        self.invalidate_group_coord_idxs(&ids, &idxs, node);
9404                        still.extend(idxs);
9405                    }
9406                    Err(e) => return Err(e),
9407                }
9408            }
9409            pending = still;
9410            if pending.is_empty() {
9411                break;
9412            }
9413            self.wait_retry(&mut attempt, deadline).await?;
9414        }
9415        out.into_iter()
9416            .zip(ids)
9417            .map(|(g, id)| {
9418                g.ok_or_else(|| Error::protocol(format!("ShareGroupDescribe missing {id}")))
9419            })
9420            .collect()
9421    }
9422
9423    /// Describe share groups (Java `Admin.describeShareGroups`).
9424    ///
9425    /// Same wire as [`Self::share_group_describe`]: ShareGroupDescribe
9426    /// api 77 v0–v1 on the group coordinator. Java's
9427    /// `DescribeShareGroupsHandler` uses `CoordinatorType.GROUP`. Empty
9428    /// input is a no-op. ShareGroupDescribe has no TimeoutMs; the RPC
9429    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
9430    /// deadline, use [`Self::describe_share_groups_timeout`].
9431    pub async fn describe_share_groups(
9432        &mut self,
9433        group_ids: &[&str],
9434        include_authorized_operations: bool,
9435    ) -> Result<Vec<DescribedShareGroup>> {
9436        let timeout = self.cfg.request_timeout;
9437        self.describe_share_groups_timeout(group_ids, include_authorized_operations, timeout)
9438            .await
9439    }
9440
9441    /// [`Self::describe_share_groups`] with a one-shot RPC deadline (Java
9442    /// `DescribeShareGroupsOptions.timeoutMs`).
9443    ///
9444    /// ShareGroupDescribe has no TimeoutMs; `timeout` is the RPC deadline
9445    /// and the coordinator retry budget.
9446    pub async fn describe_share_groups_timeout(
9447        &mut self,
9448        group_ids: &[&str],
9449        include_authorized_operations: bool,
9450        timeout: Duration,
9451    ) -> Result<Vec<DescribedShareGroup>> {
9452        self.share_group_describe_timeout(group_ids, include_authorized_operations, timeout)
9453            .await
9454    }
9455
9456    /// Describe KIP-932 share-group offsets (DescribeShareGroupOffsets
9457    /// api 90).
9458    ///
9459    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
9460    /// Official Apache JSON listeners are `broker` only. Official listed
9461    /// errors include `NOT_COORDINATOR` (16). Official Java
9462    /// `ListShareGroupOffsetsHandler` (the AdminClient handler for this
9463    /// RPC) uses `CoordinatorType.GROUP`. This is not a controller hop
9464    /// and not a partition-leader hop: there is no Metadata
9465    /// `controller_id` lookup, no `NOT_CONTROLLER` (41) retry, and no
9466    /// `NOT_LEADER_OR_FOLLOWER` (6) hop. SHARE (`key_type=2`) is the
9467    /// FindCoordinator v6 share-state key
9468    /// (`groupId:topicId:partition`) and is not used here.
9469    /// `COORDINATOR_LOAD_IN_PROGRESS` / `COORDINATOR_NOT_AVAILABLE` /
9470    /// `NOT_COORDINATOR` (16) refresh the coordinator and retry.
9471    /// Group-level ErrorCode is per-group after GroupId and Topics
9472    /// (bytes 8–9 on leftover-empty fixture group `"g"`), not
9473    /// top-level after throttle and not first-partition.
9474    /// Java `listShareGroupOffsets` is [`Self::list_share_group_offsets`].
9475    /// FindCoordinator v4+ CoordinatorKeys array of N (KIP-699): one
9476    /// FindCoordinator per retry for uncached groups. DescribeShareGroupOffsets
9477    /// is one RPC per coordinator. Brokers that only speak FindCoordinator
9478    /// v1–v3 get one FindCoordinator per uncached group. Empty input is
9479    /// a no-op. DescribeShareGroupOffsets has no TimeoutMs; the RPC
9480    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
9481    /// deadline, use [`Self::describe_share_group_offsets_timeout`].
9482    pub async fn describe_share_group_offsets(
9483        &mut self,
9484        groups: &[DescribeShareGroupOffsetsGroup],
9485    ) -> Result<Vec<DescribedShareGroupOffsets>> {
9486        let timeout = self.cfg.request_timeout;
9487        self.describe_share_group_offsets_timeout(groups, timeout)
9488            .await
9489    }
9490
9491    /// [`Self::describe_share_group_offsets`] with a one-shot RPC deadline
9492    /// (Java `ListShareGroupOffsetsOptions.timeoutMs`).
9493    ///
9494    /// DescribeShareGroupOffsets has no TimeoutMs; `timeout` is the RPC
9495    /// deadline and the coordinator retry budget.
9496    pub async fn describe_share_group_offsets_timeout(
9497        &mut self,
9498        groups: &[DescribeShareGroupOffsetsGroup],
9499        timeout: Duration,
9500    ) -> Result<Vec<DescribedShareGroupOffsets>> {
9501        if groups.is_empty() {
9502            return Ok(Vec::new());
9503        }
9504        let ids: Vec<String> = groups.iter().map(|g| g.group_id.clone()).collect();
9505        let version = self.describe_share_group_offsets_version.ok_or_else(|| {
9506            Error::Unsupported("broker does not support DescribeShareGroupOffsets".into())
9507        })?;
9508        let deadline = Instant::now() + timeout;
9509        let mut attempt = 0u32;
9510        let mut out: Vec<Option<DescribedShareGroupOffsets>> = vec![None; groups.len()];
9511        let mut pending: Vec<usize> = (0..groups.len()).collect();
9512        loop {
9513            let by_node = self.group_coord_nodes(&ids, &pending).await?;
9514            let mut nodes: Vec<i32> = by_node.keys().copied().collect();
9515            nodes.sort_unstable();
9516            let mut still = Vec::new();
9517            for node in nodes {
9518                let idxs = by_node.get(&node).cloned().unwrap_or_default();
9519                match self
9520                    .describe_share_group_offsets_on_node(node, version, groups, &idxs, timeout)
9521                    .await
9522                {
9523                    Ok(done) => {
9524                        for (i, g) in done {
9525                            if error::coordinator_retriable(g.error_code) {
9526                                self.invalidate_group_coord_idxs(&ids, &[i], node);
9527                                still.push(i);
9528                            } else if let Some(slot) = out.get_mut(i) {
9529                                *slot = Some(g);
9530                            }
9531                        }
9532                    }
9533                    Err(e) if e.is_retriable() => {
9534                        self.invalidate_group_coord_idxs(&ids, &idxs, node);
9535                        still.extend(idxs);
9536                    }
9537                    Err(e) => return Err(e),
9538                }
9539            }
9540            pending = still;
9541            if pending.is_empty() {
9542                break;
9543            }
9544            self.wait_retry(&mut attempt, deadline).await?;
9545        }
9546        out.into_iter()
9547            .zip(ids)
9548            .map(|(g, id)| {
9549                g.ok_or_else(|| Error::protocol(format!("DescribeShareGroupOffsets missing {id}")))
9550            })
9551            .collect()
9552    }
9553
9554    /// List share-group offsets (Java `Admin.listShareGroupOffsets`).
9555    ///
9556    /// Same wire as [`Self::describe_share_group_offsets`]:
9557    /// DescribeShareGroupOffsets api 90 on the group coordinator.
9558    /// Java's `ListShareGroupOffsetsHandler` sends that RPC. Empty
9559    /// input is a no-op. DescribeShareGroupOffsets has no TimeoutMs;
9560    /// the RPC deadline is [`AdminConfig::request_timeout`]. For a
9561    /// one-shot deadline, use [`Self::list_share_group_offsets_timeout`].
9562    pub async fn list_share_group_offsets(
9563        &mut self,
9564        groups: &[DescribeShareGroupOffsetsGroup],
9565    ) -> Result<Vec<DescribedShareGroupOffsets>> {
9566        let timeout = self.cfg.request_timeout;
9567        self.list_share_group_offsets_timeout(groups, timeout).await
9568    }
9569
9570    /// [`Self::list_share_group_offsets`] with a one-shot RPC deadline (Java
9571    /// `ListShareGroupOffsetsOptions.timeoutMs`).
9572    ///
9573    /// DescribeShareGroupOffsets has no TimeoutMs; `timeout` is the RPC
9574    /// deadline and the coordinator retry budget.
9575    pub async fn list_share_group_offsets_timeout(
9576        &mut self,
9577        groups: &[DescribeShareGroupOffsetsGroup],
9578        timeout: Duration,
9579    ) -> Result<Vec<DescribedShareGroupOffsets>> {
9580        self.describe_share_group_offsets_timeout(groups, timeout)
9581            .await
9582    }
9583
9584    /// Alter KIP-932 share-group offsets (AlterShareGroupOffsets
9585    /// api 91).
9586    ///
9587    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
9588    /// Official Apache JSON listeners are `broker` only. Official listed
9589    /// errors include `NOT_COORDINATOR` (16). Official Java
9590    /// `AlterShareGroupOffsetsHandler` uses `CoordinatorType.GROUP`.
9591    /// This is not a controller hop and not a partition-leader hop:
9592    /// there is no Metadata `controller_id` lookup, no
9593    /// `NOT_CONTROLLER` (41) retry, and no `NOT_LEADER_OR_FOLLOWER`
9594    /// (6) hop. SHARE (`key_type=2`) is the FindCoordinator v6
9595    /// share-state key (`groupId:topicId:partition`) and is not used
9596    /// here. `COORDINATOR_LOAD_IN_PROGRESS` /
9597    /// `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh
9598    /// the coordinator and retry. ErrorCode is top-level after
9599    /// throttle (bytes 4–5 on leftover-empty fixture group `"g"`),
9600    /// not first-group and not first-partition (bytes 31–32 when
9601    /// leftover-empty topic `"t"` partition `0` is present).
9602    /// AlterShareGroupOffsets has no TimeoutMs; the RPC deadline is
9603    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
9604    /// [`Self::alter_share_group_offsets_timeout`].
9605    pub async fn alter_share_group_offsets(
9606        &mut self,
9607        group_id: &str,
9608        topics: &[AlterShareGroupOffsetsTopic],
9609    ) -> Result<AlteredShareGroupOffsets> {
9610        let timeout = self.cfg.request_timeout;
9611        self.alter_share_group_offsets_timeout(group_id, topics, timeout)
9612            .await
9613    }
9614
9615    /// [`Self::alter_share_group_offsets`] with a one-shot RPC deadline (Java
9616    /// `AlterShareGroupOffsetsOptions.timeoutMs`).
9617    ///
9618    /// AlterShareGroupOffsets has no TimeoutMs; `timeout` is the RPC
9619    /// deadline and the coordinator retry budget.
9620    pub async fn alter_share_group_offsets_timeout(
9621        &mut self,
9622        group_id: &str,
9623        topics: &[AlterShareGroupOffsetsTopic],
9624        timeout: Duration,
9625    ) -> Result<AlteredShareGroupOffsets> {
9626        let coord_key = group_id.to_string();
9627        let version = self.alter_share_group_offsets_version.ok_or_else(|| {
9628            Error::Unsupported("broker does not support AlterShareGroupOffsets".into())
9629        })?;
9630        let deadline = Instant::now() + timeout;
9631        let mut attempt = 0u32;
9632        loop {
9633            let stale = self
9634                .group_coord
9635                .as_ref()
9636                .is_none_or(|(g, _)| g != &coord_key);
9637            if stale {
9638                let node = self.discover_group_coord(&coord_key).await?;
9639                self.group_coord = Some((coord_key.clone(), node));
9640            }
9641            let node = self
9642                .group_coord
9643                .as_ref()
9644                .map(|(_, n)| *n)
9645                .ok_or_else(|| Error::protocol("missing group coordinator"))?;
9646            self.connect_node(node).await?;
9647            let body = {
9648                let conn = self
9649                    .conns
9650                    .get_mut(&node)
9651                    .ok_or_else(|| Error::protocol("missing alter_share_group_offsets conn"))?;
9652                conn.roundtrip(
9653                    ALTER_SHARE_GROUP_OFFSETS,
9654                    version,
9655                    |buf| encode_alter_share_group_offsets_request(buf, group_id, topics),
9656                    timeout,
9657                )
9658                .await
9659            };
9660            let body = match body {
9661                Ok(b) => b,
9662                Err(e) if e.is_retriable() => {
9663                    let _ = self.conns.remove(&node);
9664                    self.group_coord = None;
9665                    self.wait_retry(&mut attempt, deadline).await?;
9666                    continue;
9667                }
9668                Err(e) => return Err(e),
9669            };
9670            let result = decode_alter_share_group_offsets_response(&mut body.clone())?;
9671            if error::coordinator_retriable(result.error_code) {
9672                // 14/15/16: FindCoordinator, then the new group coordinator.
9673                self.group_coord = None;
9674                let _ = self.conns.remove(&node);
9675                self.wait_retry(&mut attempt, deadline).await?;
9676                continue;
9677            }
9678            return Ok(result);
9679        }
9680    }
9681
9682    /// Delete KIP-932 share-group offsets (DeleteShareGroupOffsets
9683    /// api 92).
9684    ///
9685    /// Lands on the group coordinator (`FindCoordinator` `key_type=0`).
9686    /// Official Apache JSON listeners are `broker` only. Official listed
9687    /// errors include `NOT_COORDINATOR` (16). Official Java
9688    /// `DeleteShareGroupOffsetsHandler` uses `CoordinatorType.GROUP`.
9689    /// This is not a controller hop and not a partition-leader hop:
9690    /// there is no Metadata `controller_id` lookup, no
9691    /// `NOT_CONTROLLER` (41) retry, and no `NOT_LEADER_OR_FOLLOWER`
9692    /// (6) hop. SHARE (`key_type=2`) is the FindCoordinator v6
9693    /// share-state key (`groupId:topicId:partition`) and is not used
9694    /// here. `COORDINATOR_LOAD_IN_PROGRESS` /
9695    /// `COORDINATOR_NOT_AVAILABLE` / `NOT_COORDINATOR` (16) refresh
9696    /// the coordinator and retry. ErrorCode is top-level after
9697    /// throttle (bytes 4–5 on leftover-empty fixture group `"g"`),
9698    /// not first-group and not first-topic (bytes 26–27 when
9699    /// leftover-empty topic `"t"` is present). Official request topics
9700    /// are names only — no partitions. DeleteShareGroupOffsets has no
9701    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
9702    /// For a one-shot deadline, use
9703    /// [`Self::delete_share_group_offsets_timeout`].
9704    pub async fn delete_share_group_offsets(
9705        &mut self,
9706        group_id: &str,
9707        topics: &[DeleteShareGroupOffsetsTopic],
9708    ) -> Result<DeletedShareGroupOffsets> {
9709        let timeout = self.cfg.request_timeout;
9710        self.delete_share_group_offsets_timeout(group_id, topics, timeout)
9711            .await
9712    }
9713
9714    /// [`Self::delete_share_group_offsets`] with a one-shot RPC deadline
9715    /// (Java `DeleteShareGroupOffsetsOptions.timeoutMs`).
9716    ///
9717    /// DeleteShareGroupOffsets has no TimeoutMs; `timeout` is the RPC
9718    /// deadline and the coordinator retry budget.
9719    pub async fn delete_share_group_offsets_timeout(
9720        &mut self,
9721        group_id: &str,
9722        topics: &[DeleteShareGroupOffsetsTopic],
9723        timeout: Duration,
9724    ) -> Result<DeletedShareGroupOffsets> {
9725        let coord_key = group_id.to_string();
9726        let version = self.delete_share_group_offsets_version.ok_or_else(|| {
9727            Error::Unsupported("broker does not support DeleteShareGroupOffsets".into())
9728        })?;
9729        let deadline = Instant::now() + timeout;
9730        let mut attempt = 0u32;
9731        loop {
9732            let stale = self
9733                .group_coord
9734                .as_ref()
9735                .is_none_or(|(g, _)| g != &coord_key);
9736            if stale {
9737                let node = self.discover_group_coord(&coord_key).await?;
9738                self.group_coord = Some((coord_key.clone(), node));
9739            }
9740            let node = self
9741                .group_coord
9742                .as_ref()
9743                .map(|(_, n)| *n)
9744                .ok_or_else(|| Error::protocol("missing group coordinator"))?;
9745            self.connect_node(node).await?;
9746            let body = {
9747                let conn = self
9748                    .conns
9749                    .get_mut(&node)
9750                    .ok_or_else(|| Error::protocol("missing delete_share_group_offsets conn"))?;
9751                conn.roundtrip(
9752                    DELETE_SHARE_GROUP_OFFSETS,
9753                    version,
9754                    |buf| encode_delete_share_group_offsets_request(buf, group_id, topics),
9755                    timeout,
9756                )
9757                .await
9758            };
9759            let body = match body {
9760                Ok(b) => b,
9761                Err(e) if e.is_retriable() => {
9762                    let _ = self.conns.remove(&node);
9763                    self.group_coord = None;
9764                    self.wait_retry(&mut attempt, deadline).await?;
9765                    continue;
9766                }
9767                Err(e) => return Err(e),
9768            };
9769            let result = decode_delete_share_group_offsets_response(&mut body.clone())?;
9770            if error::coordinator_retriable(result.error_code) {
9771                // 14/15/16: FindCoordinator, then the new group coordinator.
9772                self.group_coord = None;
9773                let _ = self.conns.remove(&node);
9774                self.wait_retry(&mut attempt, deadline).await?;
9775                continue;
9776            }
9777            return Ok(result);
9778        }
9779    }
9780
9781    /// Describe topic partitions (DescribeTopicPartitions api 75,
9782    /// KIP-966).
9783    ///
9784    /// Lands on the connected broker (bootstrap is fine). Official
9785    /// Apache JSON listeners are `broker` only. Official JSON lists no
9786    /// `errorCodes`. Official Java
9787    /// `DescribeTopicPartitionsRequestHandler` answers from the broker
9788    /// `MetadataCache`. `NOT_COORDINATOR` (16) is not listed. This is
9789    /// not a group-coordinator hop, not a controller hop, and not a
9790    /// partition-leader hop: there is no FindCoordinator, no Metadata
9791    /// `controller_id` lookup, no `NOT_CONTROLLER` (41) retry, and no
9792    /// `NOT_LEADER_OR_FOLLOWER` (6) hop. ErrorCode is first-topic after
9793    /// throttle and the compact topics length (bytes 5–6 on leftover-
9794    /// empty fixture topic `"t"`), not top-level after throttle.
9795    /// First-partition ErrorCode is at bytes 27–28 when leftover-empty
9796    /// partition `0` is present and is not the first ErrorCode.
9797    /// DescribeTopicPartitions has no TimeoutMs; the RPC deadline is
9798    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
9799    /// [`Self::describe_topic_partitions_timeout`]. Java `describeTopics`
9800    /// is [`Self::describe_topics_timeout`].
9801    pub async fn describe_topic_partitions(
9802        &mut self,
9803        topics: &[&str],
9804        response_partition_limit: i32,
9805        cursor: Option<&TopicPartitionCursor>,
9806    ) -> Result<DescribeTopicPartitionsResponse> {
9807        let timeout = self.cfg.request_timeout;
9808        self.describe_topic_partitions_timeout(topics, response_partition_limit, cursor, timeout)
9809            .await
9810    }
9811
9812    /// [`Self::describe_topic_partitions`] with a one-shot RPC deadline.
9813    ///
9814    /// DescribeTopicPartitions has no TimeoutMs; `timeout` is the RPC
9815    /// deadline. Java `Admin.describeTopics` uses
9816    /// [`Self::describe_topics_timeout`]. This overload is the crate-first
9817    /// raw DescribeTopicPartitions (api 75) path.
9818    pub async fn describe_topic_partitions_timeout(
9819        &mut self,
9820        topics: &[&str],
9821        response_partition_limit: i32,
9822        cursor: Option<&TopicPartitionCursor>,
9823        timeout: Duration,
9824    ) -> Result<DescribeTopicPartitionsResponse> {
9825        let names: Vec<String> = topics.iter().map(|s| (*s).to_string()).collect();
9826        self.describe_topic_partitions_once(&names, response_partition_limit, cursor, timeout)
9827            .await
9828    }
9829
9830    async fn describe_topic_partitions_once(
9831        &mut self,
9832        names: &[String],
9833        response_partition_limit: i32,
9834        cursor: Option<&TopicPartitionCursor>,
9835        timeout: Duration,
9836    ) -> Result<DescribeTopicPartitionsResponse> {
9837        let version = self.describe_topic_partitions_version.ok_or_else(|| {
9838            Error::Unsupported("broker does not support DescribeTopicPartitions".into())
9839        })?;
9840        let body = self
9841            .roundtrip_bootstrap(
9842                DESCRIBE_TOPIC_PARTITIONS,
9843                version,
9844                |buf| {
9845                    encode_describe_topic_partitions_request(
9846                        buf,
9847                        names,
9848                        response_partition_limit,
9849                        cursor,
9850                    )
9851                },
9852                timeout,
9853            )
9854            .await?;
9855        decode_describe_topic_partitions_response(&mut body.clone())
9856    }
9857
9858    async fn describe_topics_dtp(
9859        &mut self,
9860        names: &[String],
9861        include_authorized_operations: bool,
9862        response_partition_limit: i32,
9863        timeout: Duration,
9864    ) -> Result<Vec<TopicDescription>> {
9865        let deadline = Instant::now() + timeout;
9866        let mut cursor: Option<TopicPartitionCursor> = None;
9867        let mut out: Vec<TopicDescription> = Vec::new();
9868        loop {
9869            if Instant::now() >= deadline {
9870                return Err(Error::Timeout);
9871            }
9872            let resp = self
9873                .describe_topic_partitions_once(
9874                    names,
9875                    response_partition_limit,
9876                    cursor.as_ref(),
9877                    timeout,
9878                )
9879                .await?;
9880            for t in &resp.topics {
9881                let desc = topic_description_from_dtp(t, include_authorized_operations);
9882                if let Some(existing) = out.iter_mut().find(|d| d.name == desc.name) {
9883                    existing.partitions.extend(desc.partitions);
9884                } else {
9885                    out.push(desc);
9886                }
9887            }
9888            match resp.next_cursor {
9889                Some(next) if cursor.as_ref() != Some(&next) => cursor = Some(next),
9890                _ => break,
9891            }
9892        }
9893        Ok(out)
9894    }
9895
9896    async fn describe_topics_metadata(
9897        &mut self,
9898        names: &[String],
9899        include_authorized_operations: bool,
9900        timeout: Duration,
9901    ) -> Result<Vec<TopicDescription>> {
9902        let owned = MetadataRequestTopic::convert_from_names(names.iter().cloned());
9903        let md = self
9904            .fetch_metadata_request_with(Some(&owned), include_authorized_operations, timeout)
9905            .await?;
9906        Ok(topic_descriptions_for_names(&md, names))
9907    }
9908
9909    /// List configuration resources (ListConfigResources api 74,
9910    /// KIP-1142; formerly ListClientMetricsResources).
9911    ///
9912    /// Java `listClientMetricsResources` is
9913    /// [`Self::list_client_metrics_resources`] (CLIENT_METRICS only).
9914    ///
9915    /// Lands on the connected broker (bootstrap is fine). Negotiates
9916    /// ListConfigResources v0–v1 (flexible from v0). Kafka 4.0 api 74
9917    /// is ListClientMetricsResources v0 only (empty request; response
9918    /// names, ResourceType decode-fills `CLIENT_METRICS`). v1 adds
9919    /// ResourceTypes / ResourceType (KIP-1142). Official
9920    /// Apache JSON listeners are `broker` only. Official JSON lists no
9921    /// `errorCodes`. Official Java `KafkaApis.handleListConfigResources`
9922    /// answers from the connected broker. Official Java
9923    /// `ListConfigResourcesRequest.getErrorResponse` writes the
9924    /// exception onto the top-level ErrorCode.
9925    /// `CLUSTER_AUTHORIZATION_FAILED` (31) and `UNSUPPORTED_VERSION`
9926    /// (35) are handler-observed. `NOT_COORDINATOR` (16) is not listed.
9927    /// This is not a group-coordinator hop, not a controller hop, and
9928    /// not a partition-leader hop: there is no FindCoordinator, no
9929    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41) retry,
9930    /// and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level `error_code`
9931    /// is the INT16 at bytes 4–5, after throttle — not a first-resource
9932    /// field. Resources have no ErrorCode.
9933    ///
9934    /// `resource_types` is [`ConfigResourceType`] or a protocol `i8`
9935    /// (`CONFIG_RESOURCE_TOPIC`, …). Empty is Java `listConfigResources()`
9936    /// ([`Self::list_config_resources_all`]). ListConfigResources has no
9937    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
9938    /// For a one-shot deadline, use [`Self::list_config_resources_timeout`].
9939    pub async fn list_config_resources(
9940        &mut self,
9941        resource_types: impl IntoIterator<Item = impl Into<i8>>,
9942    ) -> Result<Vec<ListedConfigResource>> {
9943        let timeout = self.cfg.request_timeout;
9944        self.list_config_resources_timeout(resource_types, timeout)
9945            .await
9946    }
9947
9948    /// [`Self::list_config_resources`] with a one-shot RPC deadline (Java
9949    /// `ListConfigResourcesOptions.timeoutMs`).
9950    ///
9951    /// ListConfigResources has no TimeoutMs; `timeout` is the RPC deadline.
9952    pub async fn list_config_resources_timeout(
9953        &mut self,
9954        resource_types: impl IntoIterator<Item = impl Into<i8>>,
9955        timeout: Duration,
9956    ) -> Result<Vec<ListedConfigResource>> {
9957        let types: Vec<i8> = resource_types.into_iter().map(Into::into).collect();
9958        let version = self.list_config_resources_version.ok_or_else(|| {
9959            Error::Unsupported("broker does not support ListConfigResources v0-1".into())
9960        })?;
9961        let body = self
9962            .roundtrip_bootstrap(
9963                LIST_CONFIG_RESOURCES,
9964                version,
9965                |buf| encode_list_config_resources_request(buf, version, &types),
9966                timeout,
9967            )
9968            .await?;
9969        let resp = decode_list_config_resources_response(&mut body.clone(), version)?;
9970        if resp.error_code != 0 {
9971            return Err(Error::broker(resp.error_code, "ListConfigResources"));
9972        }
9973        Ok(resp.config_resources)
9974    }
9975
9976    /// List every configuration resource (Java `listConfigResources()`).
9977    ///
9978    /// Same wire as [`Self::list_config_resources`] with an empty
9979    /// ResourceTypes array (Java `Set.of()`). Kafka 4.1 Admin; Kafka 4.0
9980    /// has [`Self::list_client_metrics_resources`] only.
9981    pub async fn list_config_resources_all(&mut self) -> Result<Vec<ListedConfigResource>> {
9982        self.list_config_resources(std::iter::empty::<i8>()).await
9983    }
9984
9985    /// [`Self::list_config_resources_all`] with a one-shot RPC deadline
9986    /// (Java `ListConfigResourcesOptions.timeoutMs`).
9987    ///
9988    /// ListConfigResources has no TimeoutMs; `timeout` is the RPC deadline.
9989    pub async fn list_config_resources_all_timeout(
9990        &mut self,
9991        timeout: Duration,
9992    ) -> Result<Vec<ListedConfigResource>> {
9993        self.list_config_resources_timeout(std::iter::empty::<i8>(), timeout)
9994            .await
9995    }
9996
9997    /// List client-metrics resources (Java
9998    /// `Admin.listClientMetricsResources`).
9999    ///
10000    /// Same wire as [`Self::list_config_resources`] with
10001    /// [`ConfigResourceType::ClientMetrics`]. Java 4.0 implements the
10002    /// deprecated `listClientMetricsResources` as ListConfigResources
10003    /// for that type. ListConfigResources has no TimeoutMs; the RPC
10004    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
10005    /// deadline, use [`Self::list_client_metrics_resources_timeout`].
10006    pub async fn list_client_metrics_resources(&mut self) -> Result<Vec<ListedConfigResource>> {
10007        self.list_config_resources([ConfigResourceType::ClientMetrics])
10008            .await
10009    }
10010
10011    /// [`Self::list_client_metrics_resources`] with a one-shot RPC deadline
10012    /// (Java `ListClientMetricsResourcesOptions.timeoutMs`).
10013    ///
10014    /// ListConfigResources has no TimeoutMs; `timeout` is the RPC deadline.
10015    pub async fn list_client_metrics_resources_timeout(
10016        &mut self,
10017        timeout: Duration,
10018    ) -> Result<Vec<ListedConfigResource>> {
10019        self.list_config_resources_timeout([ConfigResourceType::ClientMetrics], timeout)
10020            .await
10021    }
10022
10023    /// Get client telemetry subscriptions (GetTelemetrySubscriptions
10024    /// api 71, KIP-714).
10025    ///
10026    /// Lands on the connected broker (bootstrap is fine). Official
10027    /// Apache JSON listeners are `broker` only. Official JSON lists no
10028    /// `errorCodes`. Official Java
10029    /// `KafkaApis.handleGetTelemetrySubscriptionsRequest` answers from
10030    /// the connected broker (`clientMetricsManager`). Official Java
10031    /// `GetTelemetrySubscriptionsRequest.getErrorResponse` writes the
10032    /// exception onto the top-level ErrorCode. `INVALID_REQUEST` (42),
10033    /// `UNSUPPORTED_VERSION` (35), and `THROTTLING_QUOTA_EXCEEDED` (89)
10034    /// are handler-observed. `NOT_COORDINATOR` (16) is not listed.
10035    /// This is not a group-coordinator hop, not a controller hop, and
10036    /// not a partition-leader hop: there is no FindCoordinator, no
10037    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41) retry,
10038    /// and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level `error_code`
10039    /// is the INT16 at bytes 4–5, after throttle — not a first-
10040    /// subscription field and not a first-metric field.
10041    /// `client_instance_id` is Java `Uuid` ([`Uuid`]) or `[u8; 16]`.
10042    pub async fn get_telemetry_subscriptions(
10043        &mut self,
10044        client_instance_id: impl Into<[u8; 16]>,
10045    ) -> Result<GetTelemetrySubscriptionsResponse> {
10046        let version = self.get_telemetry_subscriptions_version.ok_or_else(|| {
10047            Error::Unsupported("broker does not support GetTelemetrySubscriptions".into())
10048        })?;
10049        let timeout = self.cfg.request_timeout;
10050        let client_instance_id = client_instance_id.into();
10051        let body = self
10052            .roundtrip_bootstrap(
10053                GET_TELEMETRY_SUBSCRIPTIONS,
10054                version,
10055                |buf| encode_get_telemetry_subscriptions_request(buf, &client_instance_id),
10056                timeout,
10057            )
10058            .await?;
10059        let resp = decode_get_telemetry_subscriptions_response(&mut body.clone())?;
10060        if resp.error_code != 0 {
10061            return Err(Error::broker(resp.error_code, "GetTelemetrySubscriptions"));
10062        }
10063        Ok(resp)
10064    }
10065
10066    /// Push client telemetry metrics (PushTelemetry api 72, KIP-714).
10067    ///
10068    /// Lands on the connected broker (bootstrap is fine). Official
10069    /// Apache JSON listeners are `broker` only. Official JSON lists no
10070    /// `errorCodes`. Official Java
10071    /// `KafkaApis.handlePushTelemetryRequest` answers from the
10072    /// connected broker (`clientMetricsManager`). Official Java
10073    /// `PushTelemetryRequest.getErrorResponse` writes the exception
10074    /// onto the top-level ErrorCode. `INVALID_REQUEST` (42),
10075    /// `UNKNOWN_SUBSCRIPTION_ID` (117), `THROTTLING_QUOTA_EXCEEDED`
10076    /// (89), `UNSUPPORTED_COMPRESSION_TYPE` (76),
10077    /// `TELEMETRY_TOO_LARGE` (118), and `INVALID_RECORD` (87) are
10078    /// handler-observed. `NOT_COORDINATOR` (16) is not listed.
10079    /// This is not a group-coordinator hop, not a controller hop, and
10080    /// not a partition-leader hop: there is no FindCoordinator, no
10081    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41) retry,
10082    /// and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level `error_code`
10083    /// is the INT16 at bytes 4–5, after throttle — not a first-metric
10084    /// field and not a first-payload field.
10085    /// `client_instance_id` is Java `Uuid` ([`Uuid`]) or `[u8; 16]`.
10086    pub async fn push_telemetry(
10087        &mut self,
10088        client_instance_id: impl Into<[u8; 16]>,
10089        subscription_id: i32,
10090        terminating: bool,
10091        compression_type: i8,
10092        metrics: &[u8],
10093    ) -> Result<PushTelemetryResponse> {
10094        let version = self
10095            .push_telemetry_version
10096            .ok_or_else(|| Error::Unsupported("broker does not support PushTelemetry".into()))?;
10097        let timeout = self.cfg.request_timeout;
10098        let req = PushTelemetryRequest::new(
10099            client_instance_id,
10100            subscription_id,
10101            terminating,
10102            compression_type,
10103            metrics.to_vec(),
10104        );
10105        let body = self
10106            .roundtrip_bootstrap(
10107                PUSH_TELEMETRY,
10108                version,
10109                |buf| encode_push_telemetry_request(buf, &req),
10110                timeout,
10111            )
10112            .await?;
10113        let resp = decode_push_telemetry_response(&mut body.clone())?;
10114        if resp.error_code != 0 {
10115            return Err(Error::broker(resp.error_code, "PushTelemetry"));
10116        }
10117        Ok(resp)
10118    }
10119
10120    /// Assign replicas to log directories (AssignReplicasToDirs api 73,
10121    /// KIP-858).
10122    ///
10123    /// Lands on the Metadata controller. Official Apache JSON listeners
10124    /// are `controller` only. Official JSON lists no `errorCodes`.
10125    /// Official Java `AssignReplicasToDirsRequest.getErrorResponse`
10126    /// writes `Errors.forException(e).code()` onto the top-level
10127    /// ErrorCode. Official Java `QuorumController.assignReplicasToDirs`
10128    /// is an `appendWriteEvent`; `ControllerWriteEvent.run` throws
10129    /// `NotControllerException` when the node is not the active
10130    /// controller, which `getErrorResponse` writes as
10131    /// `NOT_CONTROLLER` (41). `NOT_CONTROLLER` (41) refreshes Metadata
10132    /// and retries on the new controller. Official
10133    /// `ReplicationControlManager.handleAssignReplicasToDirs` does not
10134    /// write `NOT_COORDINATOR` (16); per-partition
10135    /// `NOT_LEADER_OR_FOLLOWER` (6) is a handler code when the broker
10136    /// is not a replica, not a partition-leader hop. This is not a
10137    /// FindCoordinator hop and has no `key_type`. Top-level
10138    /// `error_code` is the INT16 at bytes 4–5, after throttle — not a
10139    /// first-directory field and not a first-partition field. Fixture
10140    /// broker id/epoch and directory UUIDs only; this is not a replica
10141    /// directory store. AssignReplicasToDirs has no TimeoutMs; the RPC
10142    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
10143    /// deadline, use [`Self::assign_replicas_to_dirs_timeout`].
10144    pub async fn assign_replicas_to_dirs(
10145        &mut self,
10146        broker_id: i32,
10147        broker_epoch: i64,
10148        directories: Vec<AssignReplicasToDirsDirectory>,
10149    ) -> Result<AssignReplicasToDirsResponse> {
10150        let timeout = self.cfg.request_timeout;
10151        self.assign_replicas_to_dirs_timeout(broker_id, broker_epoch, directories, timeout)
10152            .await
10153    }
10154
10155    /// [`Self::assign_replicas_to_dirs`] with a one-shot RPC deadline (Java
10156    /// `AssignReplicasToDirsOptions.timeoutMs`).
10157    ///
10158    /// AssignReplicasToDirs has no TimeoutMs; `timeout` is the RPC deadline
10159    /// and the `NOT_CONTROLLER` retry budget.
10160    pub async fn assign_replicas_to_dirs_timeout(
10161        &mut self,
10162        broker_id: i32,
10163        broker_epoch: i64,
10164        directories: Vec<AssignReplicasToDirsDirectory>,
10165        timeout: Duration,
10166    ) -> Result<AssignReplicasToDirsResponse> {
10167        let version = self.assign_replicas_to_dirs_version.ok_or_else(|| {
10168            Error::Unsupported("broker does not support AssignReplicasToDirs".into())
10169        })?;
10170        let deadline = Instant::now() + timeout;
10171        let mut attempt = 0u32;
10172        let req = AssignReplicasToDirsRequest::new(broker_id, broker_epoch, directories);
10173        loop {
10174            if self.cluster.controller().is_err() {
10175                self.refresh_metadata(None).await?;
10176            }
10177            let node = self.cluster.controller()?;
10178            self.connect_node(node).await?;
10179            let body = {
10180                let conn = self
10181                    .conns
10182                    .get_mut(&node)
10183                    .ok_or_else(|| Error::protocol("missing assign_replicas_to_dirs conn"))?;
10184                conn.roundtrip(
10185                    ASSIGN_REPLICAS_TO_DIRS,
10186                    version,
10187                    |buf| encode_assign_replicas_to_dirs_request(buf, &req),
10188                    timeout,
10189                )
10190                .await
10191            };
10192            let body = match body {
10193                Ok(b) => b,
10194                Err(e) if e.is_retriable() => {
10195                    let _ = self.conns.remove(&node);
10196                    self.cluster.invalidate_controller();
10197                    self.wait_retry(&mut attempt, deadline).await?;
10198                    continue;
10199                }
10200                Err(e) => return Err(e),
10201            };
10202            let resp = decode_assign_replicas_to_dirs_response(&mut body.clone())?;
10203            if resp.error_code == error::NOT_CONTROLLER {
10204                // NOT_CONTROLLER (41): Metadata, then the new controller.
10205                self.cluster.invalidate_controller();
10206                let _ = self.conns.remove(&node);
10207                self.wait_retry(&mut attempt, deadline).await?;
10208                self.refresh_metadata(None).await?;
10209                continue;
10210            }
10211            if resp.error_code != 0 {
10212                return Err(Error::broker(resp.error_code, "AssignReplicasToDirs"));
10213            }
10214            return Ok(resp);
10215        }
10216    }
10217
10218    /// Alter replica log directories (AlterReplicaLogDirs api 34,
10219    /// KIP-113; v1–v2, classic at v1, flexible from v2).
10220    ///
10221    /// Lands on the connected broker (bootstrap is fine). Official
10222    /// Apache JSON listeners are `broker` only. Official JSON lists no
10223    /// `errorCodes`. Official Java
10224    /// `KafkaApis.handleAlterReplicaLogDirsRequest` answers from the
10225    /// connected broker (`replicaManager.alterReplicaLogDirs`).
10226    /// Official Java `AlterReplicaLogDirsRequest.getErrorResponse`
10227    /// writes the exception onto each partition ErrorCode (no
10228    /// top-level field). `CLUSTER_AUTHORIZATION_FAILED` (31),
10229    /// `INVALID_TOPIC_EXCEPTION` (17), `KAFKA_STORAGE_ERROR` (56),
10230    /// `INVALID_REPLICA_ASSIGNMENT` (39), `LOG_DIR_NOT_FOUND` (57),
10231    /// and `REPLICA_NOT_AVAILABLE` (9) (handler converts
10232    /// `NotLeaderOrFollowerException` to 9) are handler-observed.
10233    /// `NOT_COORDINATOR` (16) is not listed. `NOT_CONTROLLER` (41) is
10234    /// not listed. This is not a group-coordinator hop, not a
10235    /// controller hop, and not a partition-leader hop: there is no
10236    /// FindCoordinator, no Metadata `controller_id` lookup, no
10237    /// `NOT_CONTROLLER` (41) retry, and no `NOT_LEADER_OR_FOLLOWER`
10238    /// (6) hop. First-partition `error_code` is the INT16 at bytes
10239    /// 12–13 on leftover-empty **v2** fixture topic `"t"` partition `0`
10240    /// — not a top-level field after throttle. Classic **v1** places
10241    /// that ErrorCode later (bytes 19–20 on the same fixture). Fixture
10242    /// directory path and topic/partition indexes only; this is not a
10243    /// log-dir store. AlterReplicaLogDirs has no TimeoutMs; the RPC
10244    /// deadline is [`AdminConfig::request_timeout`]. For a one-shot
10245    /// deadline, use [`Self::alter_replica_log_dirs_timeout`]. Optional at
10246    /// [`Self::new`] (Kafka 1.1+ / KIP-113); a broker that omits api 34
10247    /// returns [`Error::Unsupported`]. Java `alterReplicaLogDirs(Map)` is
10248    /// [`Self::alter_replica_log_dirs_for`].
10249    pub async fn alter_replica_log_dirs(
10250        &mut self,
10251        dirs: Vec<AlterReplicaLogDirsDirectory>,
10252    ) -> Result<AlterReplicaLogDirsResponse> {
10253        let timeout = self.cfg.request_timeout;
10254        self.alter_replica_log_dirs_timeout(dirs, timeout).await
10255    }
10256
10257    /// [`Self::alter_replica_log_dirs`] with a one-shot RPC deadline (Java
10258    /// `AlterReplicaLogDirsOptions.timeoutMs`).
10259    ///
10260    /// AlterReplicaLogDirs has no TimeoutMs; `timeout` is the RPC deadline.
10261    pub async fn alter_replica_log_dirs_timeout(
10262        &mut self,
10263        dirs: Vec<AlterReplicaLogDirsDirectory>,
10264        timeout: Duration,
10265    ) -> Result<AlterReplicaLogDirsResponse> {
10266        let version = self.alter_replica_log_dirs_version.ok_or_else(|| {
10267            Error::Unsupported("broker does not support AlterReplicaLogDirs".into())
10268        })?;
10269        let req = AlterReplicaLogDirsRequest::new(dirs);
10270        let body = self
10271            .roundtrip_bootstrap(
10272                ALTER_REPLICA_LOG_DIRS,
10273                version,
10274                |buf| encode_alter_replica_log_dirs_request(buf, version, &req),
10275                timeout,
10276            )
10277            .await?;
10278        decode_alter_replica_log_dirs_response(&mut body.clone(), version)
10279    }
10280
10281    /// Java `Admin.alterReplicaLogDirs(Map)`.
10282    ///
10283    /// Groups by [`TopicPartitionReplica::broker_id`] and sends
10284    /// AlterReplicaLogDirs (api 34) to each replica's broker. Empty
10285    /// input is a no-op. Per-replica error codes follow input order
10286    /// (`0` is success). This is not [`Self::alter_replica_log_dirs`]
10287    /// (bootstrap only) and not a coordinator or controller hop.
10288    /// AlterReplicaLogDirs has no TimeoutMs; the RPC deadline is
10289    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
10290    /// [`Self::alter_replica_log_dirs_for_timeout`].
10291    pub async fn alter_replica_log_dirs_for<I, Dir>(
10292        &mut self,
10293        replica_assignment: I,
10294    ) -> Result<Vec<(TopicPartitionReplica, i16)>>
10295    where
10296        I: IntoIterator<Item = (TopicPartitionReplica, Dir)>,
10297        Dir: Into<String>,
10298    {
10299        let timeout = self.cfg.request_timeout;
10300        self.alter_replica_log_dirs_for_timeout(replica_assignment, timeout)
10301            .await
10302    }
10303
10304    /// [`Self::alter_replica_log_dirs_for`] with a one-shot RPC deadline
10305    /// (Java `alterReplicaLogDirs` plus `AlterReplicaLogDirsOptions.timeoutMs`).
10306    ///
10307    /// AlterReplicaLogDirs has no TimeoutMs; `timeout` is the RPC deadline
10308    /// for each replica-broker hop.
10309    pub async fn alter_replica_log_dirs_for_timeout<I, Dir>(
10310        &mut self,
10311        replica_assignment: I,
10312        timeout: Duration,
10313    ) -> Result<Vec<(TopicPartitionReplica, i16)>>
10314    where
10315        I: IntoIterator<Item = (TopicPartitionReplica, Dir)>,
10316        Dir: Into<String>,
10317    {
10318        let assignment: Vec<(TopicPartitionReplica, String)> = replica_assignment
10319            .into_iter()
10320            .map(|(replica, dir)| (replica, dir.into()))
10321            .collect();
10322        if assignment.is_empty() {
10323            return Ok(Vec::new());
10324        }
10325        let mut errors: HashMap<TopicPartitionReplica, i16> = HashMap::new();
10326        for (broker_id, dirs) in group_alter_replica_log_dirs(&assignment) {
10327            self.ensure_broker(broker_id).await?;
10328            let resp = self
10329                .alter_replica_log_dirs_on(broker_id, dirs, timeout)
10330                .await?;
10331            for (replica, _) in assignment.iter().filter(|(r, _)| r.broker_id == broker_id) {
10332                let code = alter_replica_partition_error(
10333                    &resp.results,
10334                    replica.topic(),
10335                    replica.partition,
10336                );
10337                let _ = errors.insert(replica.clone(), code);
10338            }
10339        }
10340        let mut out = Vec::with_capacity(assignment.len());
10341        for (replica, _) in assignment {
10342            let code = errors.get(&replica).copied().unwrap_or(0);
10343            out.push((replica, code));
10344        }
10345        Ok(out)
10346    }
10347
10348    /// Describe log directories (DescribeLogDirs api 35, KIP-113 /
10349    /// KIP-784 / KIP-827; v1–v4, classic at v1, flexible from v2,
10350    /// top-level ErrorCode v3+, TotalBytes / UsableBytes v4).
10351    ///
10352    /// Lands on the connected broker (bootstrap is fine). Official
10353    /// Apache JSON listeners are `broker` only. Official JSON lists no
10354    /// `errorCodes`. Official Java
10355    /// `KafkaApis.handleDescribeLogDirsRequest` answers from the
10356    /// connected broker (`replicaManager.describeLogDirs`). Auth
10357    /// failure writes `CLUSTER_AUTHORIZATION_FAILED` (31) onto the
10358    /// top-level ErrorCode (v3+; v1–v2 omit that field and decode
10359    /// fills `0`). Official `ReplicaManager.describeLogDirs`
10360    /// writes `KAFKA_STORAGE_ERROR` (56) onto a first-directory
10361    /// ErrorCode when that dir is offline. `NOT_COORDINATOR` (16) is
10362    /// not listed. `NOT_CONTROLLER` (41) is not listed. This is not a
10363    /// group-coordinator hop, not a controller hop, and not a
10364    /// partition-leader hop: there is no FindCoordinator, no Metadata
10365    /// `controller_id` lookup, no `NOT_CONTROLLER` (41) retry, and no
10366    /// `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level `error_code` is the
10367    /// INT16 at bytes 4–5 on leftover-empty **v3+**, after throttle —
10368    /// not a first-directory field and not a first-partition field.
10369    /// Fixture directory path and topic/partition indexes only; this
10370    /// is not a log-dir store. v5 is a named STATUS hole and is not
10371    /// spoken. Java `describeLogDirs(Collection<Integer>)` is
10372    /// [`Self::describe_broker_log_dirs`]. DescribeLogDirs has no
10373    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
10374    /// For a one-shot deadline, use [`Self::describe_log_dirs_timeout`].
10375    /// Optional at [`Self::new`] (Kafka 1.0+ / KIP-113); a broker that
10376    /// omits api 35 returns [`Error::Unsupported`].
10377    pub async fn describe_log_dirs(
10378        &mut self,
10379        topics: Option<Vec<DescribableLogDirTopic>>,
10380    ) -> Result<DescribeLogDirsResponse> {
10381        let timeout = self.cfg.request_timeout;
10382        self.describe_log_dirs_timeout(topics, timeout).await
10383    }
10384
10385    /// [`Self::describe_log_dirs`] with a one-shot RPC deadline (Java
10386    /// `DescribeLogDirsOptions.timeoutMs`).
10387    ///
10388    /// DescribeLogDirs has no TimeoutMs; `timeout` is the RPC deadline.
10389    pub async fn describe_log_dirs_timeout(
10390        &mut self,
10391        topics: Option<Vec<DescribableLogDirTopic>>,
10392        timeout: Duration,
10393    ) -> Result<DescribeLogDirsResponse> {
10394        let version = self
10395            .describe_log_dirs_version
10396            .ok_or_else(|| Error::Unsupported("broker does not support DescribeLogDirs".into()))?;
10397        let req = DescribeLogDirsRequest::new(topics);
10398        let body = self
10399            .roundtrip_bootstrap(
10400                DESCRIBE_LOG_DIRS,
10401                version,
10402                |buf| encode_describe_log_dirs_request(buf, version, &req),
10403                timeout,
10404            )
10405            .await?;
10406        decode_describe_log_dirs_response(&mut body.clone(), version)
10407    }
10408
10409    /// Replica log directories (Java `Admin.describeReplicaLogDirs`).
10410    ///
10411    /// Groups replicas by [`TopicPartitionReplica::broker_id`] and
10412    /// sends DescribeLogDirs (api 35) to each replica's broker. Empty
10413    /// input is a no-op. This is not [`Self::describe_log_dirs`]
10414    /// (bootstrap only) and not a controller or partition-leader hop.
10415    /// Replicas missing from the response get [`ReplicaLogDirInfo::unknown`].
10416    /// DescribeLogDirs has no TimeoutMs; the RPC deadline is
10417    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
10418    /// [`Self::describe_replica_log_dirs_timeout`].
10419    pub async fn describe_replica_log_dirs(
10420        &mut self,
10421        replicas: impl IntoIterator<Item = TopicPartitionReplica>,
10422    ) -> Result<Vec<(TopicPartitionReplica, ReplicaLogDirInfo)>> {
10423        let timeout = self.cfg.request_timeout;
10424        self.describe_replica_log_dirs_timeout(replicas, timeout)
10425            .await
10426    }
10427
10428    /// [`Self::describe_replica_log_dirs`] with a one-shot RPC deadline
10429    /// (Java `describeReplicaLogDirs` plus `DescribeLogDirsOptions.timeoutMs`).
10430    ///
10431    /// DescribeLogDirs has no TimeoutMs; `timeout` is the RPC deadline
10432    /// for each replica-broker hop.
10433    pub async fn describe_replica_log_dirs_timeout(
10434        &mut self,
10435        replicas: impl IntoIterator<Item = TopicPartitionReplica>,
10436        timeout: Duration,
10437    ) -> Result<Vec<(TopicPartitionReplica, ReplicaLogDirInfo)>> {
10438        let replicas: Vec<TopicPartitionReplica> = replicas.into_iter().collect();
10439        if replicas.is_empty() {
10440            return Ok(Vec::new());
10441        }
10442        let mut infos: HashMap<(String, i32, i32), ReplicaLogDirInfo> = HashMap::new();
10443        for broker_id in replica_broker_ids(&replicas) {
10444            self.ensure_broker(broker_id).await?;
10445            let topics = describable_topics_for_broker(&replicas, broker_id);
10446            let resp = self
10447                .describe_log_dirs_on(broker_id, Some(topics), timeout)
10448                .await?;
10449            if resp.error_code != 0 {
10450                return Err(Error::broker(resp.error_code, "DescribeLogDirs"));
10451            }
10452            for r in replicas.iter().filter(|r| r.broker_id == broker_id) {
10453                let info = replica_log_dir_info_from(r, &resp.results);
10454                let _ = infos.insert((r.topic.clone(), r.partition, r.broker_id), info);
10455            }
10456        }
10457        let mut out = Vec::with_capacity(replicas.len());
10458        for r in replicas {
10459            let info = infos
10460                .get(&(r.topic.clone(), r.partition, r.broker_id))
10461                .cloned()
10462                .unwrap_or_else(ReplicaLogDirInfo::unknown);
10463            out.push((r, info));
10464        }
10465        Ok(out)
10466    }
10467
10468    /// Log directories on these brokers (Java
10469    /// `Admin.describeLogDirs(Collection<Integer>)`).
10470    ///
10471    /// Sends DescribeLogDirs with a null topic array (all dirs) to
10472    /// each broker. Duplicate ids are sent once, in first-seen order.
10473    /// Empty input is a no-op. Unknown broker ids refresh Metadata
10474    /// then fail. This is not [`Self::describe_log_dirs`] (bootstrap,
10475    /// optional topic filter) and not
10476    /// [`Self::describe_replica_log_dirs`]. DescribeLogDirs has no
10477    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
10478    /// For a one-shot deadline, use
10479    /// [`Self::describe_broker_log_dirs_timeout`].
10480    pub async fn describe_broker_log_dirs(
10481        &mut self,
10482        brokers: impl IntoIterator<Item = i32>,
10483    ) -> Result<Vec<(i32, DescribeLogDirsResponse)>> {
10484        let timeout = self.cfg.request_timeout;
10485        self.describe_broker_log_dirs_timeout(brokers, timeout)
10486            .await
10487    }
10488
10489    /// [`Self::describe_broker_log_dirs`] with a one-shot RPC deadline
10490    /// (Java `describeLogDirs` plus `DescribeLogDirsOptions.timeoutMs`).
10491    ///
10492    /// DescribeLogDirs has no TimeoutMs; `timeout` is the RPC deadline
10493    /// for each broker hop.
10494    pub async fn describe_broker_log_dirs_timeout(
10495        &mut self,
10496        brokers: impl IntoIterator<Item = i32>,
10497        timeout: Duration,
10498    ) -> Result<Vec<(i32, DescribeLogDirsResponse)>> {
10499        let mut ids = Vec::new();
10500        for id in brokers {
10501            if !ids.contains(&id) {
10502                ids.push(id);
10503            }
10504        }
10505        if ids.is_empty() {
10506            return Ok(Vec::new());
10507        }
10508        let mut out = Vec::with_capacity(ids.len());
10509        for broker_id in ids {
10510            self.ensure_broker(broker_id).await?;
10511            let resp = self.describe_log_dirs_on(broker_id, None, timeout).await?;
10512            out.push((broker_id, resp));
10513        }
10514        Ok(out)
10515    }
10516
10517    async fn describe_log_dirs_on(
10518        &mut self,
10519        node: i32,
10520        topics: Option<Vec<DescribableLogDirTopic>>,
10521        timeout: Duration,
10522    ) -> Result<DescribeLogDirsResponse> {
10523        let version = self
10524            .describe_log_dirs_version
10525            .ok_or_else(|| Error::Unsupported("broker does not support DescribeLogDirs".into()))?;
10526        let deadline = Instant::now() + timeout;
10527        let mut attempt = 0u32;
10528        let req = DescribeLogDirsRequest::new(topics);
10529        loop {
10530            self.connect_node(node).await?;
10531            let body = {
10532                let conn = self
10533                    .conns
10534                    .get_mut(&node)
10535                    .ok_or_else(|| Error::protocol("missing describe_log_dirs conn"))?;
10536                conn.roundtrip(
10537                    DESCRIBE_LOG_DIRS,
10538                    version,
10539                    |buf| encode_describe_log_dirs_request(buf, version, &req),
10540                    timeout,
10541                )
10542                .await
10543            };
10544            let body = match body {
10545                Ok(b) => b,
10546                Err(e) if e.is_retriable() => {
10547                    let _ = self.conns.remove(&node);
10548                    self.wait_retry(&mut attempt, deadline).await?;
10549                    continue;
10550                }
10551                Err(e) => return Err(e),
10552            };
10553            return decode_describe_log_dirs_response(&mut body.clone(), version);
10554        }
10555    }
10556
10557    async fn alter_replica_log_dirs_on(
10558        &mut self,
10559        node: i32,
10560        dirs: Vec<AlterReplicaLogDirsDirectory>,
10561        timeout: Duration,
10562    ) -> Result<AlterReplicaLogDirsResponse> {
10563        let version = self.alter_replica_log_dirs_version.ok_or_else(|| {
10564            Error::Unsupported("broker does not support AlterReplicaLogDirs".into())
10565        })?;
10566        let deadline = Instant::now() + timeout;
10567        let mut attempt = 0u32;
10568        let req = AlterReplicaLogDirsRequest::new(dirs);
10569        loop {
10570            self.connect_node(node).await?;
10571            let body = {
10572                let conn = self
10573                    .conns
10574                    .get_mut(&node)
10575                    .ok_or_else(|| Error::protocol("missing alter_replica_log_dirs conn"))?;
10576                conn.roundtrip(
10577                    ALTER_REPLICA_LOG_DIRS,
10578                    version,
10579                    |buf| encode_alter_replica_log_dirs_request(buf, version, &req),
10580                    timeout,
10581                )
10582                .await
10583            };
10584            let body = match body {
10585                Ok(b) => b,
10586                Err(e) if e.is_retriable() => {
10587                    let _ = self.conns.remove(&node);
10588                    self.wait_retry(&mut attempt, deadline).await?;
10589                    continue;
10590                }
10591                Err(e) => return Err(e),
10592            };
10593            return decode_alter_replica_log_dirs_response(&mut body.clone(), version);
10594        }
10595    }
10596
10597    async fn ensure_broker(&mut self, node: i32) -> Result<()> {
10598        if !self.cluster.brokers.contains_key(&node) {
10599            self.refresh_metadata(None).await?;
10600        }
10601        if self.cluster.brokers.contains_key(&node) {
10602            Ok(())
10603        } else {
10604            Err(Error::protocol(format!("unknown broker {node}")))
10605        }
10606    }
10607
10608    /// Create a delegation token (CreateDelegationToken api 38, KIP-48 /
10609    /// KIP-373; v1–v3, classic at v1, flexible from v2, owner /
10610    /// requester principals v3).
10611    ///
10612    /// Lands on the connected broker (bootstrap is fine). Official
10613    /// Apache JSON listeners are `broker` and `controller`. Official
10614    /// JSON lists no `errorCodes`. Official Java
10615    /// `KafkaApis.handleCreateTokenRequest` validates the connection
10616    /// then `forwardToController` (broker-side envelope, not a client
10617    /// hop). Official Java `KafkaAdminClient.createDelegationToken`
10618    /// uses `LeastLoadedNodeProvider`. Official handler writes
10619    /// `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED` (64) onto the top-level
10620    /// ErrorCode when the channel is not allowed. `NOT_COORDINATOR`
10621    /// (16) is not listed. `NOT_CONTROLLER` (41) is not listed. This
10622    /// is not a group-coordinator hop, not a controller hop, and not
10623    /// a partition-leader hop: there is no FindCoordinator, no
10624    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41)
10625    /// retry, and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level
10626    /// `error_code` is the INT16 at bytes 0–1, first field — not after
10627    /// throttle, not a first-renewer field, and not a first-token
10628    /// field. Fixture principal / lifetime only; this is not a token
10629    /// store. Kafka 4.0 `validVersions` is `1-3`. This crate speaks
10630    /// 1–3. v0 and v4+ are not spoken. v1–v2 omit owner on the wire
10631    /// (decode fills `None`) and omit requester (decode fills empty).
10632    /// CreateDelegationToken has no TimeoutMs; the RPC deadline is
10633    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
10634    /// [`Self::create_delegation_token_timeout`]. Optional at [`Self::new`]
10635    /// (Kafka 1.1+ / KIP-48); a broker that omits api 38 returns
10636    /// [`Error::Unsupported`].
10637    pub async fn create_delegation_token(
10638        &mut self,
10639        req: CreateDelegationTokenRequest,
10640    ) -> Result<CreateDelegationTokenResponse> {
10641        let timeout = self.cfg.request_timeout;
10642        self.create_delegation_token_timeout(req, timeout).await
10643    }
10644
10645    /// [`Self::create_delegation_token`] with a one-shot RPC deadline (Java
10646    /// `CreateDelegationTokenOptions.timeoutMs`).
10647    ///
10648    /// CreateDelegationToken has no TimeoutMs; `timeout` is the RPC deadline.
10649    pub async fn create_delegation_token_timeout(
10650        &mut self,
10651        req: CreateDelegationTokenRequest,
10652        timeout: Duration,
10653    ) -> Result<CreateDelegationTokenResponse> {
10654        let version = self.create_delegation_token_version.ok_or_else(|| {
10655            Error::Unsupported("broker does not support CreateDelegationToken".into())
10656        })?;
10657        let body = self
10658            .roundtrip_bootstrap(
10659                CREATE_DELEGATION_TOKEN,
10660                version,
10661                |buf| encode_create_delegation_token_request(buf, version, &req),
10662                timeout,
10663            )
10664            .await?;
10665        decode_create_delegation_token_response(&mut body.clone(), version)
10666    }
10667
10668    /// Create a delegation token with default options (Java
10669    /// `Admin.createDelegationToken()`).
10670    ///
10671    /// Same wire as [`Self::create_delegation_token`] with
10672    /// [`CreateDelegationTokenRequest::default`] (request principal,
10673    /// empty renewers, `max_lifetime_ms = -1`).
10674    pub async fn create_delegation_token_default(
10675        &mut self,
10676    ) -> Result<CreateDelegationTokenResponse> {
10677        let timeout = self.cfg.request_timeout;
10678        self.create_delegation_token_default_timeout(timeout).await
10679    }
10680
10681    /// [`Self::create_delegation_token_default`] with a one-shot RPC
10682    /// deadline (Java `CreateDelegationTokenOptions.timeoutMs`).
10683    pub async fn create_delegation_token_default_timeout(
10684        &mut self,
10685        timeout: Duration,
10686    ) -> Result<CreateDelegationTokenResponse> {
10687        self.create_delegation_token_timeout(CreateDelegationTokenRequest::default(), timeout)
10688            .await
10689    }
10690
10691    /// Renew a delegation token (RenewDelegationToken api 39, KIP-48 /
10692    /// KIP-373; v1–v2, classic at v1, flexible from v2).
10693    ///
10694    /// Lands on the connected broker (bootstrap is fine). Official
10695    /// Apache JSON listeners are `broker` and `controller`. Official
10696    /// JSON lists no `errorCodes`. Official Java
10697    /// `KafkaApis.handleRenewTokenRequest` validates the connection
10698    /// then `forwardToController` (broker-side envelope, not a client
10699    /// hop). Official Java `KafkaAdminClient.renewDelegationToken`
10700    /// uses `LeastLoadedNodeProvider`. Official handler writes
10701    /// `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED` (64) onto the top-level
10702    /// ErrorCode when the channel is not allowed. `NOT_COORDINATOR`
10703    /// (16) is not listed. `NOT_CONTROLLER` (41) is not listed. This
10704    /// is not a group-coordinator hop, not a controller hop, and not
10705    /// a partition-leader hop: there is no FindCoordinator, no
10706    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41)
10707    /// retry, and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level
10708    /// `error_code` is the INT16 at bytes 0–1, first field — not after
10709    /// throttle and not a first-token field. Fixture hmac / period
10710    /// only; this is not a token store. Kafka 4.0 `validVersions` is
10711    /// `1-2`. This crate speaks 1–2. v0 and v3+ are not spoken. Same
10712    /// fields on v1 and v2. Do not copy CreateDelegationToken just
10713    /// because it is the previous slice. RenewDelegationToken has no
10714    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
10715    /// For a one-shot deadline, use [`Self::renew_delegation_token_timeout`].
10716    /// Optional at [`Self::new`] (Kafka 1.1+ / KIP-48); a broker that
10717    /// omits api 39 returns [`Error::Unsupported`].
10718    pub async fn renew_delegation_token(
10719        &mut self,
10720        req: RenewDelegationTokenRequest,
10721    ) -> Result<RenewDelegationTokenResponse> {
10722        let timeout = self.cfg.request_timeout;
10723        self.renew_delegation_token_timeout(req, timeout).await
10724    }
10725
10726    /// [`Self::renew_delegation_token`] with a one-shot RPC deadline (Java
10727    /// `RenewDelegationTokenOptions.timeoutMs`).
10728    ///
10729    /// RenewDelegationToken has no TimeoutMs; `timeout` is the RPC deadline.
10730    pub async fn renew_delegation_token_timeout(
10731        &mut self,
10732        req: RenewDelegationTokenRequest,
10733        timeout: Duration,
10734    ) -> Result<RenewDelegationTokenResponse> {
10735        let version = self.renew_delegation_token_version.ok_or_else(|| {
10736            Error::Unsupported("broker does not support RenewDelegationToken".into())
10737        })?;
10738        let body = self
10739            .roundtrip_bootstrap(
10740                RENEW_DELEGATION_TOKEN,
10741                version,
10742                |buf| encode_renew_delegation_token_request(buf, version, &req),
10743                timeout,
10744            )
10745            .await?;
10746        decode_renew_delegation_token_response(&mut body.clone(), version)
10747    }
10748
10749    /// Renew a delegation token by HMAC (Java
10750    /// `Admin.renewDelegationToken(byte[])`).
10751    ///
10752    /// `renew_period_ms` is `-1` (Java `RenewDelegationTokenOptions`
10753    /// default: broker default period).
10754    pub async fn renew_delegation_token_hmac(
10755        &mut self,
10756        hmac: impl AsRef<[u8]>,
10757    ) -> Result<RenewDelegationTokenResponse> {
10758        let timeout = self.cfg.request_timeout;
10759        self.renew_delegation_token_hmac_timeout(hmac, timeout)
10760            .await
10761    }
10762
10763    /// [`Self::renew_delegation_token_hmac`] with a one-shot RPC deadline
10764    /// (Java `RenewDelegationTokenOptions.timeoutMs`).
10765    pub async fn renew_delegation_token_hmac_timeout(
10766        &mut self,
10767        hmac: impl AsRef<[u8]>,
10768        timeout: Duration,
10769    ) -> Result<RenewDelegationTokenResponse> {
10770        self.renew_delegation_token_timeout(
10771            RenewDelegationTokenRequest::new(hmac.as_ref().to_vec(), -1),
10772            timeout,
10773        )
10774        .await
10775    }
10776
10777    /// Expire a delegation token (ExpireDelegationToken api 40, KIP-48 /
10778    /// KIP-373; v1–v2, classic at v1, flexible from v2).
10779    ///
10780    /// Lands on the connected broker (bootstrap is fine). Official
10781    /// Apache JSON listeners are `broker` and `controller`. Official
10782    /// JSON lists no `errorCodes`. Official Java
10783    /// `KafkaApis.handleExpireTokenRequest` validates the connection
10784    /// then `forwardToController` (broker-side envelope, not a client
10785    /// hop). Official Java `KafkaAdminClient.expireDelegationToken`
10786    /// uses `LeastLoadedNodeProvider`. Official handler writes
10787    /// `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED` (64) onto the top-level
10788    /// ErrorCode when the channel is not allowed. `NOT_COORDINATOR`
10789    /// (16) is not listed. `NOT_CONTROLLER` (41) is not listed. This
10790    /// is not a group-coordinator hop, not a controller hop, and not
10791    /// a partition-leader hop: there is no FindCoordinator, no
10792    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41)
10793    /// retry, and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level
10794    /// `error_code` is the INT16 at bytes 0–1, first field — not after
10795    /// throttle and not a first-token field. Fixture hmac / period
10796    /// only; this is not a token store. Kafka 4.0 `validVersions` is
10797    /// `1-2`. This crate speaks 1–2. v0 and v3+ are not spoken. Same
10798    /// fields on v1 and v2. Do not copy RenewDelegationToken just
10799    /// because it is the previous slice. ExpireDelegationToken has no
10800    /// TimeoutMs; the RPC deadline is [`AdminConfig::request_timeout`].
10801    /// For a one-shot deadline, use [`Self::expire_delegation_token_timeout`].
10802    /// Optional at [`Self::new`] (Kafka 1.1+ / KIP-48); a broker that
10803    /// omits api 40 returns [`Error::Unsupported`].
10804    pub async fn expire_delegation_token(
10805        &mut self,
10806        req: ExpireDelegationTokenRequest,
10807    ) -> Result<ExpireDelegationTokenResponse> {
10808        let timeout = self.cfg.request_timeout;
10809        self.expire_delegation_token_timeout(req, timeout).await
10810    }
10811
10812    /// [`Self::expire_delegation_token`] with a one-shot RPC deadline (Java
10813    /// `ExpireDelegationTokenOptions.timeoutMs`).
10814    ///
10815    /// ExpireDelegationToken has no TimeoutMs; `timeout` is the RPC deadline.
10816    pub async fn expire_delegation_token_timeout(
10817        &mut self,
10818        req: ExpireDelegationTokenRequest,
10819        timeout: Duration,
10820    ) -> Result<ExpireDelegationTokenResponse> {
10821        let version = self.expire_delegation_token_version.ok_or_else(|| {
10822            Error::Unsupported("broker does not support ExpireDelegationToken".into())
10823        })?;
10824        let body = self
10825            .roundtrip_bootstrap(
10826                EXPIRE_DELEGATION_TOKEN,
10827                version,
10828                |buf| encode_expire_delegation_token_request(buf, version, &req),
10829                timeout,
10830            )
10831            .await?;
10832        decode_expire_delegation_token_response(&mut body.clone(), version)
10833    }
10834
10835    /// Expire a delegation token by HMAC (Java
10836    /// `Admin.expireDelegationToken(byte[])`).
10837    ///
10838    /// `expiry_time_period_ms` is `-1` (Java default: expire immediately).
10839    pub async fn expire_delegation_token_hmac(
10840        &mut self,
10841        hmac: impl AsRef<[u8]>,
10842    ) -> Result<ExpireDelegationTokenResponse> {
10843        let timeout = self.cfg.request_timeout;
10844        self.expire_delegation_token_hmac_timeout(hmac, timeout)
10845            .await
10846    }
10847
10848    /// [`Self::expire_delegation_token_hmac`] with a one-shot RPC deadline
10849    /// (Java `ExpireDelegationTokenOptions.timeoutMs`).
10850    pub async fn expire_delegation_token_hmac_timeout(
10851        &mut self,
10852        hmac: impl AsRef<[u8]>,
10853        timeout: Duration,
10854    ) -> Result<ExpireDelegationTokenResponse> {
10855        self.expire_delegation_token_timeout(
10856            ExpireDelegationTokenRequest::new(hmac.as_ref().to_vec(), -1),
10857            timeout,
10858        )
10859        .await
10860    }
10861
10862    /// Describe delegation tokens (DescribeDelegationToken api 41,
10863    /// KIP-48 / KIP-373; v1–v3, classic at v1, flexible from v2,
10864    /// TokenRequester v3).
10865    ///
10866    /// Lands on the connected broker (bootstrap is fine). Official
10867    /// Apache JSON listeners are `broker` and `controller`. Official
10868    /// JSON lists no `errorCodes`. Official Java
10869    /// `KafkaApis.handleDescribeTokensRequest` answers locally
10870    /// (`allowTokenRequests` → `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED`
10871    /// (64); it does not `forwardToController`). Official Java
10872    /// `KafkaAdminClient.describeDelegationToken` uses
10873    /// `LeastLoadedNodeProvider`. Official handler writes
10874    /// `DELEGATION_TOKEN_REQUEST_NOT_ALLOWED` (64) onto the top-level
10875    /// ErrorCode when the channel is not allowed. `NOT_COORDINATOR`
10876    /// (16) is not listed. `NOT_CONTROLLER` (41) is not listed.
10877    /// apiKey 41 is not error code 41 and is not a hop. This is not a
10878    /// group-coordinator hop, not a controller hop, and not a
10879    /// partition-leader hop: there is no FindCoordinator, no
10880    /// Metadata `controller_id` lookup, no `NOT_CONTROLLER` (41)
10881    /// retry, and no `NOT_LEADER_OR_FOLLOWER` (6) hop. Top-level
10882    /// `error_code` is the INT16 at bytes 0–1, first field — not after
10883    /// throttle and not a first-token field. Fixture owners only;
10884    /// this is not a token store. Kafka 4.0 `validVersions` is `1-3`.
10885    /// This crate speaks 1–3. v0 and v4+ are not spoken. Request
10886    /// Owners is the same on v1–v3. v1–v2 omit TokenRequester on each
10887    /// token (decode fills empty). Do not copy ExpireDelegationToken
10888    /// just because it is the previous slice. DescribeDelegationToken
10889    /// has no TimeoutMs; the RPC deadline is
10890    /// [`AdminConfig::request_timeout`]. For a one-shot deadline, use
10891    /// [`Self::describe_delegation_token_timeout`]. Optional at
10892    /// [`Self::new`] (Kafka 1.1+ / KIP-48); a broker that omits api 41
10893    /// returns [`Error::Unsupported`].
10894    pub async fn describe_delegation_token(
10895        &mut self,
10896        req: DescribeDelegationTokenRequest,
10897    ) -> Result<DescribeDelegationTokenResponse> {
10898        let timeout = self.cfg.request_timeout;
10899        self.describe_delegation_token_timeout(req, timeout).await
10900    }
10901
10902    /// [`Self::describe_delegation_token`] with a one-shot RPC deadline
10903    /// (Java `DescribeDelegationTokenOptions.timeoutMs`).
10904    ///
10905    /// DescribeDelegationToken has no TimeoutMs; `timeout` is the RPC
10906    /// deadline.
10907    pub async fn describe_delegation_token_timeout(
10908        &mut self,
10909        req: DescribeDelegationTokenRequest,
10910        timeout: Duration,
10911    ) -> Result<DescribeDelegationTokenResponse> {
10912        let version = self.describe_delegation_token_version.ok_or_else(|| {
10913            Error::Unsupported("broker does not support DescribeDelegationToken".into())
10914        })?;
10915        let body = self
10916            .roundtrip_bootstrap(
10917                DESCRIBE_DELEGATION_TOKEN,
10918                version,
10919                |buf| encode_describe_delegation_token_request(buf, version, &req),
10920                timeout,
10921            )
10922            .await?;
10923        decode_describe_delegation_token_response(&mut body.clone(), version)
10924    }
10925
10926    /// Describe visible delegation tokens (Java
10927    /// `Admin.describeDelegationToken()`).
10928    ///
10929    /// Same wire as [`Self::describe_delegation_token`] with owners
10930    /// `None` (Java `DescribeDelegationTokenOptions` default).
10931    pub async fn describe_delegation_tokens(&mut self) -> Result<DescribeDelegationTokenResponse> {
10932        let timeout = self.cfg.request_timeout;
10933        self.describe_delegation_tokens_timeout(timeout).await
10934    }
10935
10936    /// [`Self::describe_delegation_tokens`] with a one-shot RPC deadline
10937    /// (Java `DescribeDelegationTokenOptions.timeoutMs`).
10938    pub async fn describe_delegation_tokens_timeout(
10939        &mut self,
10940        timeout: Duration,
10941    ) -> Result<DescribeDelegationTokenResponse> {
10942        self.describe_delegation_token_timeout(DescribeDelegationTokenRequest::default(), timeout)
10943            .await
10944    }
10945
10946    async fn discover_group_coord(&mut self, group_id: &str) -> Result<i32> {
10947        if self.cluster.brokers.is_empty() {
10948            self.refresh_metadata(None).await?;
10949        }
10950        let version = self.find_coord_version;
10951        let timeout = self.cfg.request_timeout;
10952        let deadline = Instant::now() + timeout;
10953        let mut attempt = 0u32;
10954        loop {
10955            let body = self
10956                .roundtrip_bootstrap(
10957                    FIND_COORDINATOR,
10958                    version,
10959                    |buf| {
10960                        encode_find_coordinator_request_typed(
10961                            buf,
10962                            version,
10963                            group_id,
10964                            COORDINATOR_GROUP,
10965                        )
10966                    },
10967                    timeout,
10968                )
10969                .await;
10970            let body = match body {
10971                Ok(b) => b,
10972                Err(e) if e.is_retriable() => {
10973                    self.wait_retry(&mut attempt, deadline).await?;
10974                    continue;
10975                }
10976                Err(e) => return Err(e),
10977            };
10978            let (err, node, _host, _port) =
10979                decode_find_coordinator_response(&mut body.clone(), version)?;
10980            if err == 0 {
10981                if !self.cluster.brokers.contains_key(&node) {
10982                    self.refresh_metadata(None).await?;
10983                }
10984                return Ok(node);
10985            }
10986            if error::coordinator_retriable(err) {
10987                self.wait_retry(&mut attempt, deadline).await?;
10988                continue;
10989            }
10990            return Err(Error::broker(err, "FindCoordinator"));
10991        }
10992    }
10993
10994    async fn discover_group_coords(
10995        &mut self,
10996        group_ids: &[String],
10997    ) -> Result<HashMap<String, i32>> {
10998        self.discover_coords(group_ids, COORDINATOR_GROUP).await
10999    }
11000
11001    async fn discover_txn_coords(
11002        &mut self,
11003        transactional_ids: &[String],
11004    ) -> Result<HashMap<String, i32>> {
11005        self.discover_coords(transactional_ids, COORDINATOR_TRANSACTION)
11006            .await
11007    }
11008
11009    async fn discover_coords(
11010        &mut self,
11011        keys: &[String],
11012        key_type: i8,
11013    ) -> Result<HashMap<String, i32>> {
11014        let mut uniq: Vec<String> = Vec::new();
11015        for k in keys {
11016            if !uniq.iter().any(|u| u == k) {
11017                uniq.push(k.clone());
11018            }
11019        }
11020        if uniq.is_empty() {
11021            return Ok(HashMap::new());
11022        }
11023        let version = self.find_coord_version;
11024        if version < 4 {
11025            let mut out = HashMap::new();
11026            for k in &uniq {
11027                let node = if key_type == COORDINATOR_TRANSACTION {
11028                    self.discover_txn_coord(k).await?
11029                } else {
11030                    self.discover_group_coord(k).await?
11031                };
11032                let _prev = out.insert(k.clone(), node);
11033            }
11034            return Ok(out);
11035        }
11036        if self.cluster.brokers.is_empty() {
11037            self.refresh_metadata(None).await?;
11038        }
11039        let timeout = self.cfg.request_timeout;
11040        let deadline = Instant::now() + timeout;
11041        let mut attempt = 0u32;
11042        loop {
11043            let key_refs: Vec<&str> = uniq.iter().map(String::as_str).collect();
11044            let body = self
11045                .roundtrip_bootstrap(
11046                    FIND_COORDINATOR,
11047                    version,
11048                    |buf| encode_find_coordinator_request_keys(buf, version, &key_refs, key_type),
11049                    timeout,
11050                )
11051                .await;
11052            let body = match body {
11053                Ok(b) => b,
11054                Err(e) if e.is_retriable() => {
11055                    self.wait_retry(&mut attempt, deadline).await?;
11056                    continue;
11057                }
11058                Err(e) => return Err(e),
11059            };
11060            let (coords, ..) =
11061                decode_find_coordinator_response_coordinators(&mut body.clone(), version)?;
11062            let mut by_key: HashMap<String, (i16, i32)> = HashMap::new();
11063            for c in coords {
11064                let _prev = by_key.insert(c.key, (c.error_code, c.node_id));
11065            }
11066            let mut retry = false;
11067            let mut out = HashMap::new();
11068            for k in &uniq {
11069                let (err, node) = by_key
11070                    .get(k)
11071                    .copied()
11072                    .ok_or_else(|| Error::protocol(format!("FindCoordinator missing {k}")))?;
11073                if err == 0 {
11074                    if !self.cluster.brokers.contains_key(&node) {
11075                        self.refresh_metadata(None).await?;
11076                    }
11077                    let _prev = out.insert(k.clone(), node);
11078                    continue;
11079                }
11080                if error::coordinator_retriable(err) {
11081                    retry = true;
11082                    continue;
11083                }
11084                return Err(Error::broker(err, k.clone()));
11085            }
11086            if retry {
11087                self.wait_retry(&mut attempt, deadline).await?;
11088                continue;
11089            }
11090            return Ok(out);
11091        }
11092    }
11093
11094    async fn group_coord_nodes(
11095        &mut self,
11096        ids: &[String],
11097        pending: &[usize],
11098    ) -> Result<HashMap<i32, Vec<usize>>> {
11099        let mut need: Vec<String> = Vec::new();
11100        for &i in pending {
11101            let Some(id) = ids.get(i) else {
11102                continue;
11103            };
11104            if !self.group_coords.contains_key(id) && !need.iter().any(|k| k == id) {
11105                need.push(id.clone());
11106            }
11107        }
11108        if !need.is_empty() {
11109            let found = self.discover_group_coords(&need).await?;
11110            self.group_coords.extend(found);
11111        }
11112        let mut by_node: HashMap<i32, Vec<usize>> = HashMap::new();
11113        for &i in pending {
11114            let id = ids
11115                .get(i)
11116                .ok_or_else(|| Error::protocol("missing group id"))?;
11117            let node = *self
11118                .group_coords
11119                .get(id)
11120                .ok_or_else(|| Error::protocol(format!("missing coordinator for {id}")))?;
11121            by_node.entry(node).or_default().push(i);
11122        }
11123        Ok(by_node)
11124    }
11125
11126    fn invalidate_group_coord_idxs(&mut self, ids: &[String], idxs: &[usize], node: i32) {
11127        let _ = self.conns.remove(&node);
11128        for &i in idxs {
11129            if let Some(id) = ids.get(i) {
11130                let _ = self.group_coords.remove(id);
11131            }
11132        }
11133    }
11134
11135    async fn txn_coord_nodes(
11136        &mut self,
11137        ids: &[String],
11138        pending: &[usize],
11139    ) -> Result<HashMap<i32, Vec<usize>>> {
11140        let mut need: Vec<String> = Vec::new();
11141        for &i in pending {
11142            let Some(id) = ids.get(i) else {
11143                continue;
11144            };
11145            if !self.txn_coords.contains_key(id) && !need.iter().any(|k| k == id) {
11146                need.push(id.clone());
11147            }
11148        }
11149        if !need.is_empty() {
11150            let found = self.discover_txn_coords(&need).await?;
11151            self.txn_coords.extend(found);
11152        }
11153        let mut by_node: HashMap<i32, Vec<usize>> = HashMap::new();
11154        for &i in pending {
11155            let id = ids
11156                .get(i)
11157                .ok_or_else(|| Error::protocol("missing transactional id"))?;
11158            let node = *self
11159                .txn_coords
11160                .get(id)
11161                .ok_or_else(|| Error::protocol(format!("missing coordinator for {id}")))?;
11162            by_node.entry(node).or_default().push(i);
11163        }
11164        Ok(by_node)
11165    }
11166
11167    fn invalidate_txn_coord_idxs(&mut self, ids: &[String], idxs: &[usize], node: i32) {
11168        let _ = self.conns.remove(&node);
11169        for &i in idxs {
11170            if let Some(id) = ids.get(i) {
11171                let _ = self.txn_coords.remove(id);
11172            }
11173        }
11174    }
11175
11176    async fn describe_groups_on_node(
11177        &mut self,
11178        node: i32,
11179        version: i16,
11180        ids: &[String],
11181        idxs: &[usize],
11182        include_authorized_operations: bool,
11183        timeout: Duration,
11184    ) -> Result<Vec<(usize, DescribedGroup)>> {
11185        let subset: Vec<String> = idxs.iter().filter_map(|&i| ids.get(i).cloned()).collect();
11186        self.connect_node(node).await?;
11187        let body = {
11188            let conn = self
11189                .conns
11190                .get_mut(&node)
11191                .ok_or_else(|| Error::protocol("missing describe_groups conn"))?;
11192            conn.roundtrip(
11193                DESCRIBE_GROUPS,
11194                version,
11195                |buf| {
11196                    encode_describe_groups_request(
11197                        buf,
11198                        version,
11199                        &subset,
11200                        include_authorized_operations,
11201                    )
11202                },
11203                timeout,
11204            )
11205            .await
11206        }?;
11207        let (results, ..) = decode_describe_groups_response(&mut body.clone(), version)?;
11208        let mut by_id: HashMap<String, VecDeque<DescribedGroup>> = HashMap::new();
11209        for g in results {
11210            by_id.entry(g.group_id.clone()).or_default().push_back(g);
11211        }
11212        let mut out = Vec::new();
11213        for &i in idxs {
11214            let id = ids
11215                .get(i)
11216                .ok_or_else(|| Error::protocol("missing group id"))?;
11217            let g = by_id
11218                .get_mut(id)
11219                .and_then(VecDeque::pop_front)
11220                .ok_or_else(|| Error::protocol(format!("DescribeGroups missing {id}")))?;
11221            out.push((i, g));
11222        }
11223        Ok(out)
11224    }
11225
11226    async fn delete_groups_on_node(
11227        &mut self,
11228        node: i32,
11229        version: i16,
11230        ids: &[String],
11231        idxs: &[usize],
11232        timeout: Duration,
11233    ) -> Result<Vec<(usize, DeletableGroupResult)>> {
11234        let subset: Vec<String> = idxs.iter().filter_map(|&i| ids.get(i).cloned()).collect();
11235        self.connect_node(node).await?;
11236        let body = {
11237            let conn = self
11238                .conns
11239                .get_mut(&node)
11240                .ok_or_else(|| Error::protocol("missing delete_groups conn"))?;
11241            conn.roundtrip(
11242                DELETE_GROUPS,
11243                version,
11244                |buf| encode_delete_groups_request(buf, version, &subset),
11245                timeout,
11246            )
11247            .await
11248        }?;
11249        let (results, ..) = decode_delete_groups_response(&mut body.clone(), version)?;
11250        let mut by_id: HashMap<String, VecDeque<DeletableGroupResult>> = HashMap::new();
11251        for g in results {
11252            by_id.entry(g.group_id.clone()).or_default().push_back(g);
11253        }
11254        let mut out = Vec::new();
11255        for &i in idxs {
11256            let id = ids
11257                .get(i)
11258                .ok_or_else(|| Error::protocol("missing group id"))?;
11259            let g = by_id
11260                .get_mut(id)
11261                .and_then(VecDeque::pop_front)
11262                .ok_or_else(|| Error::protocol(format!("DeleteGroups missing {id}")))?;
11263            out.push((i, g));
11264        }
11265        Ok(out)
11266    }
11267
11268    async fn consumer_group_describe_on_node(
11269        &mut self,
11270        node: i32,
11271        version: i16,
11272        ids: &[String],
11273        idxs: &[usize],
11274        include_authorized_operations: bool,
11275        timeout: Duration,
11276    ) -> Result<Vec<(usize, DescribedConsumerGroup)>> {
11277        let subset: Vec<String> = idxs.iter().filter_map(|&i| ids.get(i).cloned()).collect();
11278        self.connect_node(node).await?;
11279        let body = {
11280            let conn = self
11281                .conns
11282                .get_mut(&node)
11283                .ok_or_else(|| Error::protocol("missing consumer_group_describe conn"))?;
11284            conn.roundtrip(
11285                CONSUMER_GROUP_DESCRIBE,
11286                version,
11287                |buf| {
11288                    encode_consumer_group_describe_request(
11289                        buf,
11290                        version,
11291                        &subset,
11292                        include_authorized_operations,
11293                    )
11294                },
11295                timeout,
11296            )
11297            .await
11298        }?;
11299        let (results, ..) = decode_consumer_group_describe_response(&mut body.clone(), version)?;
11300        let mut by_id: HashMap<String, VecDeque<DescribedConsumerGroup>> = HashMap::new();
11301        for g in results {
11302            by_id.entry(g.group_id.clone()).or_default().push_back(g);
11303        }
11304        let mut out = Vec::new();
11305        for &i in idxs {
11306            let id = ids
11307                .get(i)
11308                .ok_or_else(|| Error::protocol("missing group id"))?;
11309            let g = by_id
11310                .get_mut(id)
11311                .and_then(VecDeque::pop_front)
11312                .ok_or_else(|| Error::protocol(format!("ConsumerGroupDescribe missing {id}")))?;
11313            out.push((i, g));
11314        }
11315        Ok(out)
11316    }
11317
11318    async fn share_group_describe_on_node(
11319        &mut self,
11320        node: i32,
11321        version: i16,
11322        ids: &[String],
11323        idxs: &[usize],
11324        include_authorized_operations: bool,
11325        timeout: Duration,
11326    ) -> Result<Vec<(usize, DescribedShareGroup)>> {
11327        let subset: Vec<String> = idxs.iter().filter_map(|&i| ids.get(i).cloned()).collect();
11328        self.connect_node(node).await?;
11329        let body = {
11330            let conn = self
11331                .conns
11332                .get_mut(&node)
11333                .ok_or_else(|| Error::protocol("missing share_group_describe conn"))?;
11334            conn.roundtrip(
11335                SHARE_GROUP_DESCRIBE,
11336                version,
11337                |buf| {
11338                    encode_share_group_describe_request(
11339                        buf,
11340                        version,
11341                        &subset,
11342                        include_authorized_operations,
11343                    )
11344                },
11345                timeout,
11346            )
11347            .await
11348        }?;
11349        let (results, ..) = decode_share_group_describe_response(&mut body.clone(), version)?;
11350        let mut by_id: HashMap<String, VecDeque<DescribedShareGroup>> = HashMap::new();
11351        for g in results {
11352            by_id.entry(g.group_id.clone()).or_default().push_back(g);
11353        }
11354        let mut out = Vec::new();
11355        for &i in idxs {
11356            let id = ids
11357                .get(i)
11358                .ok_or_else(|| Error::protocol("missing group id"))?;
11359            let g = by_id
11360                .get_mut(id)
11361                .and_then(VecDeque::pop_front)
11362                .ok_or_else(|| Error::protocol(format!("ShareGroupDescribe missing {id}")))?;
11363            out.push((i, g));
11364        }
11365        Ok(out)
11366    }
11367
11368    async fn describe_share_group_offsets_on_node(
11369        &mut self,
11370        node: i32,
11371        version: i16,
11372        groups: &[DescribeShareGroupOffsetsGroup],
11373        idxs: &[usize],
11374        timeout: Duration,
11375    ) -> Result<Vec<(usize, DescribedShareGroupOffsets)>> {
11376        let subset: Vec<DescribeShareGroupOffsetsGroup> = idxs
11377            .iter()
11378            .filter_map(|&i| groups.get(i).cloned())
11379            .collect();
11380        self.connect_node(node).await?;
11381        let body = {
11382            let conn = self
11383                .conns
11384                .get_mut(&node)
11385                .ok_or_else(|| Error::protocol("missing describe_share_group_offsets conn"))?;
11386            conn.roundtrip(
11387                DESCRIBE_SHARE_GROUP_OFFSETS,
11388                version,
11389                |buf| encode_describe_share_group_offsets_request(buf, &subset),
11390                timeout,
11391            )
11392            .await
11393        }?;
11394        let (results, ..) = decode_describe_share_group_offsets_response(&mut body.clone())?;
11395        let mut by_id: HashMap<String, VecDeque<DescribedShareGroupOffsets>> = HashMap::new();
11396        for g in results {
11397            by_id.entry(g.group_id.clone()).or_default().push_back(g);
11398        }
11399        let mut out = Vec::new();
11400        for &i in idxs {
11401            let id = groups
11402                .get(i)
11403                .map(|g| g.group_id.as_str())
11404                .ok_or_else(|| Error::protocol("missing group id"))?;
11405            let g = by_id
11406                .get_mut(id)
11407                .and_then(VecDeque::pop_front)
11408                .ok_or_else(|| {
11409                    Error::protocol(format!("DescribeShareGroupOffsets missing {id}"))
11410                })?;
11411            out.push((i, g));
11412        }
11413        Ok(out)
11414    }
11415
11416    async fn describe_transactions_on_node(
11417        &mut self,
11418        node: i32,
11419        ids: &[String],
11420        idxs: &[usize],
11421        version: i16,
11422        timeout: Duration,
11423    ) -> Result<Vec<(usize, TransactionState)>> {
11424        let subset: Vec<String> = idxs.iter().filter_map(|&i| ids.get(i).cloned()).collect();
11425        self.connect_node(node).await?;
11426        let body = {
11427            let conn = self
11428                .conns
11429                .get_mut(&node)
11430                .ok_or_else(|| Error::protocol("missing describe_transactions conn"))?;
11431            conn.roundtrip(
11432                DESCRIBE_TRANSACTIONS,
11433                version,
11434                |buf| encode_describe_transactions_request(buf, &subset),
11435                timeout,
11436            )
11437            .await
11438        }?;
11439        let (results, ..) = decode_describe_transactions_response(&mut body.clone())?;
11440        let mut by_id: HashMap<String, VecDeque<TransactionState>> = HashMap::new();
11441        for t in results {
11442            by_id
11443                .entry(t.transactional_id.clone())
11444                .or_default()
11445                .push_back(t);
11446        }
11447        let mut out = Vec::new();
11448        for &i in idxs {
11449            let id = ids
11450                .get(i)
11451                .ok_or_else(|| Error::protocol("missing transactional id"))?;
11452            let t = by_id
11453                .get_mut(id)
11454                .and_then(VecDeque::pop_front)
11455                .ok_or_else(|| Error::protocol(format!("DescribeTransactions missing {id}")))?;
11456            out.push((i, t));
11457        }
11458        Ok(out)
11459    }
11460
11461    async fn discover_txn_coord(&mut self, transactional_id: &str) -> Result<i32> {
11462        if self.cluster.brokers.is_empty() {
11463            self.refresh_metadata(None).await?;
11464        }
11465        let version = self.find_coord_version;
11466        let timeout = self.cfg.request_timeout;
11467        let deadline = Instant::now() + timeout;
11468        let mut attempt = 0u32;
11469        loop {
11470            let body = self
11471                .roundtrip_bootstrap(
11472                    FIND_COORDINATOR,
11473                    version,
11474                    |buf| {
11475                        encode_find_coordinator_request_typed(
11476                            buf,
11477                            version,
11478                            transactional_id,
11479                            COORDINATOR_TRANSACTION,
11480                        )
11481                    },
11482                    timeout,
11483                )
11484                .await;
11485            let body = match body {
11486                Ok(b) => b,
11487                Err(e) if e.is_retriable() => {
11488                    self.wait_retry(&mut attempt, deadline).await?;
11489                    continue;
11490                }
11491                Err(e) => return Err(e),
11492            };
11493            let (err, node, _host, _port) =
11494                decode_find_coordinator_response(&mut body.clone(), version)?;
11495            if err == 0 {
11496                if !self.cluster.brokers.contains_key(&node) {
11497                    self.refresh_metadata(None).await?;
11498                }
11499                return Ok(node);
11500            }
11501            if error::coordinator_retriable(err) {
11502                self.wait_retry(&mut attempt, deadline).await?;
11503                continue;
11504            }
11505            return Err(Error::broker(err, "FindCoordinator"));
11506        }
11507    }
11508}
11509
11510fn group_reassignments(assignments: &[PartitionReassignment]) -> Vec<ReassignableTopic> {
11511    let mut by_topic: HashMap<String, Vec<ReassignablePartition>> = HashMap::new();
11512    let mut order: Vec<String> = Vec::new();
11513    for a in assignments {
11514        match by_topic.entry(a.topic.clone()) {
11515            std::collections::hash_map::Entry::Vacant(slot) => {
11516                order.push(a.topic.clone());
11517                let _ = slot.insert(vec![ReassignablePartition::new(
11518                    a.partition,
11519                    a.replicas.clone(),
11520                )]);
11521            }
11522            std::collections::hash_map::Entry::Occupied(mut slot) => {
11523                slot.get_mut()
11524                    .push(ReassignablePartition::new(a.partition, a.replicas.clone()));
11525            }
11526        }
11527    }
11528    order
11529        .into_iter()
11530        .map(|name| {
11531            let partitions = by_topic.remove(&name).unwrap_or_default();
11532            ReassignableTopic::new(name, partitions)
11533        })
11534        .collect()
11535}
11536
11537fn group_list_reassignments(partitions: &[crate::TopicPartition]) -> Vec<ListReassignmentTopic> {
11538    let mut by_topic: HashMap<String, Vec<i32>> = HashMap::new();
11539    let mut order: Vec<String> = Vec::new();
11540    for tp in partitions {
11541        match by_topic.entry(tp.topic.clone()) {
11542            std::collections::hash_map::Entry::Vacant(slot) => {
11543                order.push(tp.topic.clone());
11544                let _ = slot.insert(vec![tp.partition]);
11545            }
11546            std::collections::hash_map::Entry::Occupied(mut slot) => {
11547                slot.get_mut().push(tp.partition);
11548            }
11549        }
11550    }
11551    order
11552        .into_iter()
11553        .map(|name| {
11554            let partitions = by_topic.remove(&name).unwrap_or_default();
11555            ListReassignmentTopic::new(name, partitions)
11556        })
11557        .collect()
11558}
11559
11560fn flatten_list_reassignments(
11561    topics: &[crate::protocol::admin::OngoingTopicReassignment],
11562) -> Vec<OngoingReassignment> {
11563    let mut out = Vec::new();
11564    for t in topics {
11565        for p in &t.partitions {
11566            out.push(OngoingReassignment {
11567                topic: t.name.clone(),
11568                partition: p.partition_index,
11569                replicas: p.replicas.clone(),
11570                adding_replicas: p.adding_replicas.clone(),
11571                removing_replicas: p.removing_replicas.clone(),
11572            });
11573        }
11574    }
11575    out
11576}
11577
11578fn flatten_reassignment_results(
11579    results: &[crate::protocol::admin::ReassignmentTopicResult],
11580) -> Vec<ReassignmentResult> {
11581    let mut out = Vec::new();
11582    for t in results {
11583        for p in &t.partitions {
11584            out.push(ReassignmentResult {
11585                topic: t.name.clone(),
11586                partition: p.partition_index,
11587                error_code: p.error_code,
11588                error_message: p.error_message.clone(),
11589            });
11590        }
11591    }
11592    out
11593}
11594
11595fn offset_delete_topics(partitions: &[(String, i32)]) -> Vec<OffsetDeleteTopic> {
11596    let mut by_topic: HashMap<String, Vec<i32>> = HashMap::new();
11597    let mut order: Vec<String> = Vec::new();
11598    for (topic, part) in partitions {
11599        match by_topic.entry(topic.clone()) {
11600            std::collections::hash_map::Entry::Vacant(slot) => {
11601                order.push(topic.clone());
11602                let _ = slot.insert(vec![*part]);
11603            }
11604            std::collections::hash_map::Entry::Occupied(mut slot) => {
11605                slot.get_mut().push(*part);
11606            }
11607        }
11608    }
11609    order
11610        .into_iter()
11611        .map(|topic| OffsetDeleteTopic {
11612            partitions: by_topic.remove(&topic).unwrap_or_default(),
11613            topic,
11614        })
11615        .collect()
11616}
11617
11618/// Java DescribeTopicPartitions default ResponsePartitionLimit.
11619const DESCRIBE_TOPIC_PARTITIONS_LIMIT: i32 = 2000;
11620
11621fn topic_listings_from(md: &MetadataResponse, list_internal: bool) -> Vec<TopicListing> {
11622    md.topics
11623        .iter()
11624        .filter(|t| t.error_code == 0)
11625        .filter(|t| list_internal || !t.is_internal)
11626        .filter_map(|t| {
11627            t.name.as_ref().map(|name| TopicListing {
11628                name: name.clone(),
11629                topic_id: t.topic_id,
11630                is_internal: t.is_internal,
11631            })
11632        })
11633        .collect()
11634}
11635
11636fn topic_descriptions_including_unnamed(md: &MetadataResponse) -> Vec<TopicDescription> {
11637    md.topics.iter().map(topic_description_from).collect()
11638}
11639
11640fn topic_descriptions_for_names(md: &MetadataResponse, names: &[String]) -> Vec<TopicDescription> {
11641    names
11642        .iter()
11643        .map(|name| {
11644            md.topics
11645                .iter()
11646                .find(|t| t.name.as_deref() == Some(name.as_str()))
11647                .map(topic_description_from)
11648                .unwrap_or_else(|| {
11649                    TopicDescription::new(
11650                        name.clone(),
11651                        [0; 16],
11652                        false,
11653                        error::UNKNOWN_TOPIC_OR_PARTITION,
11654                        Vec::new(),
11655                    )
11656                })
11657        })
11658        .collect()
11659}
11660
11661fn topic_description_from(t: &crate::protocol::api::TopicMetadata) -> TopicDescription {
11662    let name = t.name.clone().unwrap_or_default();
11663    let partitions = if t.error_code == 0 {
11664        t.partitions
11665            .iter()
11666            .map(|p| crate::PartitionInfo::from_partition_metadata(name.as_str(), p))
11667            .collect()
11668    } else {
11669        Vec::new()
11670    };
11671    TopicDescription {
11672        name,
11673        topic_id: t.topic_id,
11674        is_internal: t.is_internal,
11675        error_code: t.error_code,
11676        partitions,
11677        authorized_operations: t.topic_authorized_operations,
11678    }
11679}
11680
11681fn topic_description_from_dtp(
11682    t: &DescribedTopicPartitions,
11683    include_authorized_operations: bool,
11684) -> TopicDescription {
11685    let name = t.name.clone().unwrap_or_default();
11686    let partitions = if t.error_code == 0 {
11687        t.partitions
11688            .iter()
11689            .map(|p| crate::PartitionInfo {
11690                topic: name.clone(),
11691                partition: p.partition_index,
11692                leader: p.leader_id,
11693                leader_epoch: p.leader_epoch,
11694                replicas: p.replica_nodes.clone(),
11695                isr: p.isr_nodes.clone(),
11696                offline_replicas: p.offline_replicas.clone(),
11697            })
11698            .collect()
11699    } else {
11700        Vec::new()
11701    };
11702    TopicDescription {
11703        name,
11704        topic_id: t.topic_id,
11705        is_internal: t.is_internal,
11706        error_code: t.error_code,
11707        partitions,
11708        authorized_operations: if include_authorized_operations {
11709            t.topic_authorized_operations
11710        } else {
11711            AUTHORIZED_OPERATIONS_OMITTED
11712        },
11713    }
11714}
11715
11716fn list_offset_topic_requests(
11717    queries: &[(crate::TopicPartition, i64)],
11718    idxs: &[usize],
11719    cluster: &Cluster,
11720) -> Vec<ListOffsetsTopicRequest> {
11721    let mut order: Vec<String> = Vec::new();
11722    let mut by_topic: HashMap<String, Vec<ListOffsetsPartitionRequest>> = HashMap::new();
11723    for &i in idxs {
11724        let Some((tp, ts)) = queries.get(i) else {
11725            continue;
11726        };
11727        let part = ListOffsetsPartitionRequest {
11728            partition: tp.partition,
11729            current_leader_epoch: cluster.leader_epoch(&tp.topic, tp.partition),
11730            timestamp: *ts,
11731        };
11732        match by_topic.entry(tp.topic.clone()) {
11733            std::collections::hash_map::Entry::Vacant(slot) => {
11734                order.push(tp.topic.clone());
11735                let _ = slot.insert(vec![part]);
11736            }
11737            std::collections::hash_map::Entry::Occupied(mut slot) => {
11738                slot.get_mut().push(part);
11739            }
11740        }
11741    }
11742    order
11743        .into_iter()
11744        .map(|name| ListOffsetsTopicRequest {
11745            partitions: by_topic.remove(&name).unwrap_or_default(),
11746            name,
11747        })
11748        .collect()
11749}
11750
11751fn replica_broker_ids(replicas: &[TopicPartitionReplica]) -> Vec<i32> {
11752    let mut ids = Vec::new();
11753    for r in replicas {
11754        if !ids.contains(&r.broker_id) {
11755            ids.push(r.broker_id);
11756        }
11757    }
11758    ids
11759}
11760
11761fn group_alter_replica_log_dirs(
11762    assignment: &[(TopicPartitionReplica, String)],
11763) -> Vec<(i32, Vec<AlterReplicaLogDirsDirectory>)> {
11764    let replicas: Vec<TopicPartitionReplica> = assignment.iter().map(|(r, _)| r.clone()).collect();
11765    replica_broker_ids(&replicas)
11766        .into_iter()
11767        .map(|broker_id| {
11768            (
11769                broker_id,
11770                alter_replica_dirs_for_broker(assignment, broker_id),
11771            )
11772        })
11773        .collect()
11774}
11775
11776fn alter_replica_dirs_for_broker(
11777    assignment: &[(TopicPartitionReplica, String)],
11778    broker_id: i32,
11779) -> Vec<AlterReplicaLogDirsDirectory> {
11780    let mut path_order: Vec<String> = Vec::new();
11781    let mut topics_by_path: HashMap<String, Vec<String>> = HashMap::new();
11782    let mut partitions_by_path_topic: HashMap<(String, String), Vec<i32>> = HashMap::new();
11783    for (replica, path) in assignment {
11784        if replica.broker_id != broker_id {
11785            continue;
11786        }
11787        if !path_order.contains(path) {
11788            path_order.push(path.clone());
11789        }
11790        match topics_by_path.entry(path.clone()) {
11791            std::collections::hash_map::Entry::Vacant(slot) => {
11792                let _ = slot.insert(vec![replica.topic.clone()]);
11793            }
11794            std::collections::hash_map::Entry::Occupied(mut slot) => {
11795                if !slot.get().contains(&replica.topic) {
11796                    slot.get_mut().push(replica.topic.clone());
11797                }
11798            }
11799        }
11800        match partitions_by_path_topic.entry((path.clone(), replica.topic.clone())) {
11801            std::collections::hash_map::Entry::Vacant(slot) => {
11802                let _ = slot.insert(vec![replica.partition]);
11803            }
11804            std::collections::hash_map::Entry::Occupied(mut slot) => {
11805                if !slot.get().contains(&replica.partition) {
11806                    slot.get_mut().push(replica.partition);
11807                }
11808            }
11809        }
11810    }
11811    path_order
11812        .into_iter()
11813        .map(|path| {
11814            let topics = topics_by_path.remove(&path).unwrap_or_default();
11815            let topics = topics
11816                .into_iter()
11817                .map(|name| {
11818                    let partitions = partitions_by_path_topic
11819                        .remove(&(path.clone(), name.clone()))
11820                        .unwrap_or_default();
11821                    AlterReplicaLogDirsTopic { name, partitions }
11822                })
11823                .collect();
11824            AlterReplicaLogDirsDirectory { path, topics }
11825        })
11826        .collect()
11827}
11828
11829fn alter_replica_partition_error(
11830    results: &[AlterReplicaLogDirsResponseTopic],
11831    topic: &str,
11832    partition: i32,
11833) -> i16 {
11834    for result in results {
11835        if result.topic_name != topic {
11836            continue;
11837        }
11838        for part in &result.partitions {
11839            if part.partition_index == partition {
11840                return part.error_code;
11841            }
11842        }
11843    }
11844    0
11845}
11846
11847fn describable_topics_for_broker(
11848    replicas: &[TopicPartitionReplica],
11849    broker_id: i32,
11850) -> Vec<DescribableLogDirTopic> {
11851    let mut order: Vec<String> = Vec::new();
11852    let mut by_topic: HashMap<String, Vec<i32>> = HashMap::new();
11853    for r in replicas {
11854        if r.broker_id != broker_id {
11855            continue;
11856        }
11857        match by_topic.entry(r.topic.clone()) {
11858            std::collections::hash_map::Entry::Vacant(slot) => {
11859                order.push(r.topic.clone());
11860                let _ = slot.insert(vec![r.partition]);
11861            }
11862            std::collections::hash_map::Entry::Occupied(mut slot) => {
11863                if !slot.get().contains(&r.partition) {
11864                    slot.get_mut().push(r.partition);
11865                }
11866            }
11867        }
11868    }
11869    order
11870        .into_iter()
11871        .map(|name| DescribableLogDirTopic {
11872            partitions: by_topic.remove(&name).unwrap_or_default(),
11873            name,
11874        })
11875        .collect()
11876}
11877
11878fn replica_log_dir_info_from(
11879    replica: &TopicPartitionReplica,
11880    results: &[DescribeLogDirsResult],
11881) -> ReplicaLogDirInfo {
11882    let mut info = ReplicaLogDirInfo::unknown();
11883    for dir in results {
11884        if dir.error_code != 0 {
11885            continue;
11886        }
11887        for topic in &dir.topics {
11888            if topic.name != replica.topic {
11889                continue;
11890            }
11891            for part in &topic.partitions {
11892                if part.partition_index != replica.partition {
11893                    continue;
11894                }
11895                if part.is_future_key {
11896                    info.future_log_dir = Some(dir.log_dir.clone());
11897                    info.future_offset_lag = part.offset_lag;
11898                } else {
11899                    info.current_log_dir = Some(dir.log_dir.clone());
11900                    info.current_offset_lag = part.offset_lag;
11901                }
11902            }
11903        }
11904    }
11905    info
11906}
11907
11908#[cfg(test)]
11909mod tests {
11910    use super::*;
11911    use crate::protocol::admin::{
11912        decode_list_config_resources_response, encode_list_config_resources_response,
11913        CreatedTopicConfig, ListConfigResourcesResponse,
11914    };
11915    use bytes::BytesMut;
11916
11917    #[test]
11918    fn records_to_delete_before_offset_converts_to_i64() {
11919        assert_eq!(i64::from(RecordsToDelete::before_offset(42)), 42);
11920        assert_eq!(RecordsToDelete::before_offset(7).offset(), 7);
11921        assert_eq!(
11922            RecordsToDelete::before_offset(42).to_string(),
11923            "(beforeOffset = 42)"
11924        );
11925        assert_eq!(
11926            RecordsToDelete::before_offset(
11927                crate::protocol::admin::DeleteRecordsRequest::HIGH_WATERMARK
11928            )
11929            .offset(),
11930            crate::protocol::admin::DeleteRecordsRequest::HIGH_WATERMARK
11931        );
11932    }
11933
11934    #[test]
11935    fn list_config_resources_response_config_resources_matches_java() {
11936        // Java 4.1.0 ListConfigResourcesResponse.configResources maps
11937        // each ConfigResource (Type.forId, resourceName). Unknown ids
11938        // are UNKNOWN (0). Official Java
11939        // ListConfigResourcesResponse.configResources (4.1.0; 4.0.0 404
11940        // as the current name). ListedConfigResource::to_config_resource
11941        // keeps the wire id. This crate speaks 0-1. This is not error()
11942        // / errorCounts / getErrorResponse / supportedResourceTypes /
11943        // Builder.build.
11944        assert!(ListConfigResourcesResponse::new(0, Vec::new())
11945            .config_resources()
11946            .is_empty());
11947        let full = ListConfigResourcesResponse::new(
11948            crate::error::INVALID_REQUEST,
11949            vec![
11950                ListedConfigResource::new("t", CONFIG_RESOURCE_TOPIC),
11951                ListedConfigResource::new("1", CONFIG_RESOURCE_BROKER),
11952                ListedConfigResource::new("r", CONFIG_RESOURCE_CLIENT_METRICS),
11953                ListedConfigResource::new("g", CONFIG_RESOURCE_GROUP),
11954                ListedConfigResource::new("x", 99),
11955            ],
11956        );
11957        assert_eq!(
11958            full.config_resources(),
11959            vec![
11960                ConfigResource::topic("t"),
11961                ConfigResource::broker(1),
11962                ConfigResource::of(ConfigResourceType::ClientMetrics, "r"),
11963                ConfigResource::group("g"),
11964                ConfigResource {
11965                    resource_type: 0,
11966                    name: "x".into(),
11967                    keys: None,
11968                },
11969            ]
11970        );
11971        assert!(full.config_resources().iter().all(|r| r.keys.is_none()));
11972        assert_eq!(
11973            ListedConfigResource::new("x", 99)
11974                .to_config_resource()
11975                .resource_type,
11976            99,
11977            "to_config_resource keeps the wire id; configResources uses Type.forId"
11978        );
11979
11980        let metrics = ListConfigResourcesResponse::new(
11981            0,
11982            vec![ListedConfigResource::new(
11983                "r",
11984                CONFIG_RESOURCE_CLIENT_METRICS,
11985            )],
11986        );
11987        for version in 0..=1_i16 {
11988            let mut buf = BytesMut::new();
11989            encode_list_config_resources_response(&mut buf, version, &metrics).unwrap();
11990            let mut cur = buf.as_ref();
11991            let decoded = decode_list_config_resources_response(&mut cur, version).unwrap();
11992            assert_eq!(
11993                decoded.config_resources(),
11994                vec![ConfigResource::of(ConfigResourceType::ClientMetrics, "r")],
11995                "ListConfigResources v{version} configResources must convert the decoded listing"
11996            );
11997            assert!(
11998                cur.is_empty(),
11999                "ListConfigResources v{version} configResources leftover-empty; leftover {} bytes",
12000                cur.len()
12001            );
12002        }
12003
12004        let unknown = ListConfigResourcesResponse::new(0, vec![ListedConfigResource::new("x", 99)]);
12005        let mut v1 = BytesMut::new();
12006        encode_list_config_resources_response(&mut v1, 1, &unknown).unwrap();
12007        let mut cur = v1.as_ref();
12008        let decoded = decode_list_config_resources_response(&mut cur, 1).unwrap();
12009        assert_eq!(
12010            decoded.config_resources.first().map(|r| r.resource_type),
12011            Some(99)
12012        );
12013        assert_eq!(
12014            decoded.config_resources(),
12015            vec![ConfigResource {
12016                resource_type: 0,
12017                name: "x".into(),
12018                keys: None,
12019            }]
12020        );
12021        assert!(
12022            cur.is_empty(),
12023            "ListConfigResources v1 configResources leftover-empty; leftover {} bytes",
12024            cur.len()
12025        );
12026    }
12027
12028    #[test]
12029    fn update_features_client_checks_match_java() {
12030        let empty = Admin::reject_java_feature_updates(&[])
12031            .unwrap_err()
12032            .to_string();
12033        assert!(
12034            empty.contains("Feature updates can not be null or empty."),
12035            "{empty}"
12036        );
12037        let blank = Admin::reject_java_feature_updates(&[FeatureUpdate::new("  ", 1)])
12038            .unwrap_err()
12039            .to_string();
12040        assert!(
12041            blank.contains("Provided feature can not be empty."),
12042            "{blank}"
12043        );
12044        let unnamed = Admin::reject_java_feature_updates(&[FeatureUpdate::new("", 1)])
12045            .unwrap_err()
12046            .to_string();
12047        assert!(
12048            unnamed.contains("Provided feature can not be empty."),
12049            "{unnamed}"
12050        );
12051        Admin::reject_java_feature_updates(&[FeatureUpdate::new("metadata.version", 17)]).unwrap();
12052    }
12053
12054    #[test]
12055    fn deleted_records_matches_java() {
12056        assert_eq!(DeletedRecords::INVALID_LOW_WATERMARK, -1);
12057        let ok = DeletedRecords::new(42);
12058        assert_eq!(ok.low_watermark(), 42);
12059        assert_eq!(ok.error_code(), 0);
12060        let with_err = DeletedRecords::with_error_code(7, 6);
12061        assert_eq!(with_err.low_watermark(), 7);
12062        assert_eq!(with_err.error_code(), 6);
12063        let pair: (i64, i16) = with_err.into();
12064        assert_eq!(pair, (7, 6));
12065        assert_eq!(DeletedRecords::from((9, 0)).low_watermark(), 9);
12066        assert_eq!(
12067            DeletedRecords::with_error_code(DeletedRecords::INVALID_LOW_WATERMARK, 6)
12068                .low_watermark(),
12069            DeletedRecords::INVALID_LOW_WATERMARK
12070        );
12071    }
12072
12073    #[test]
12074    fn topic_partition_replica_and_log_dir_getters() {
12075        let replica = TopicPartitionReplica::new("t", 2, 5);
12076        assert_eq!(replica.topic(), "t");
12077        assert_eq!(replica.partition(), 2);
12078        assert_eq!(replica.broker_id(), 5);
12079        assert_eq!(replica.to_string(), "t-2-5");
12080        let dirs = ReplicaLogDirInfo::new(Some("/data".into()), 3, Some("/next".into()), 4);
12081        assert_eq!(dirs.current_log_dir(), Some("/data"));
12082        assert_eq!(dirs.current_offset_lag(), 3);
12083        assert_eq!(dirs.future_log_dir(), Some("/next"));
12084        assert_eq!(dirs.future_offset_lag(), 4);
12085        assert_eq!(
12086            dirs.to_string(),
12087            "(currentReplicaLogDir=/data, futureReplicaLogDir=/next, futureReplicaOffsetLag=4)"
12088        );
12089        let unknown = ReplicaLogDirInfo::unknown();
12090        assert!(unknown.current_log_dir().is_none());
12091        assert_eq!(unknown.current_offset_lag(), -1);
12092        assert_eq!(
12093            unknown.to_string(),
12094            "ReplicaLogDirInfo(currentReplicaLogDir=null)"
12095        );
12096        assert_eq!(
12097            ReplicaLogDirInfo::new(None, -1, Some("/next".into()), 7).to_string(),
12098            "(currentReplicaLogDir=null, futureReplicaLogDir=/next, futureReplicaOffsetLag=7)"
12099        );
12100        let replica_info = DescribeLogDirsPartition::new(0, 10, 3, false);
12101        assert_eq!(replica_info.size(), 10);
12102        assert_eq!(replica_info.offset_lag(), 3);
12103        assert!(!replica_info.is_future());
12104        let log_dir = DescribeLogDirsResult::new(
12105            0,
12106            "/data",
12107            vec![DescribeLogDirsTopic::new("t", vec![replica_info])],
12108            4096,
12109            1024,
12110        );
12111        assert_eq!(log_dir.log_dir(), "/data");
12112        assert_eq!(log_dir.total_bytes(), Some(4096));
12113        assert_eq!(log_dir.usable_bytes(), Some(1024));
12114        assert_eq!(
12115            DescribeLogDirsResult::new(
12116                0,
12117                "/d",
12118                Vec::new(),
12119                UNKNOWN_VOLUME_BYTES,
12120                UNKNOWN_VOLUME_BYTES
12121            )
12122            .total_bytes(),
12123            None
12124        );
12125        let grouped = group_alter_replica_log_dirs(&[
12126            (TopicPartitionReplica::new("t", 0, 2), "/d1".into()),
12127            (TopicPartitionReplica::new("u", 1, 2), "/d1".into()),
12128            (TopicPartitionReplica::new("t", 0, 1), "/d2".into()),
12129        ]);
12130        assert_eq!(grouped.len(), 2);
12131        assert_eq!(grouped[0].0, 2);
12132        assert_eq!(grouped[0].1[0].path(), "/d1");
12133        assert_eq!(grouped[0].1[0].topics()[0].name(), "t");
12134        assert_eq!(grouped[0].1[0].topics()[0].partitions(), &[0]);
12135        assert_eq!(grouped[0].1[0].topics()[1].name(), "u");
12136        assert_eq!(grouped[1].0, 1);
12137        assert_eq!(grouped[1].1[0].path(), "/d2");
12138        let dir = AssignReplicasToDirsDirectory::new(
12139            Uuid::from_bytes([0x11; 16]),
12140            vec![AssignReplicasToDirsTopic::new(
12141                Uuid::from_bytes([0x22; 16]),
12142                vec![AssignReplicasToDirsPartition::new(0)],
12143            )],
12144        );
12145        assert_eq!(dir.id(), Uuid::from_bytes([0x11; 16]));
12146        assert_eq!(dir.topics()[0].topic_id(), Uuid::from_bytes([0x22; 16]));
12147        assert_eq!(dir.topics()[0].partitions()[0].partition_index(), 0);
12148        let assigned = AssignReplicasToDirsResponse::new(
12149            0,
12150            vec![AssignReplicasToDirsResponseDirectory::new(
12151                [0x11; 16],
12152                vec![AssignReplicasToDirsResponseTopic::new(
12153                    [0x22; 16],
12154                    vec![AssignReplicasToDirsResponsePartition::new(0, 0)],
12155                )],
12156            )],
12157        );
12158        assert_eq!(assigned.error_code(), 0);
12159        assert_eq!(assigned.throttle_time_ms(), 0);
12160        assert_eq!(assigned.directories()[0].id(), Uuid::from_bytes([0x11; 16]));
12161        assert_eq!(
12162            assigned.directories()[0].topics()[0].topic_id(),
12163            Uuid::from_bytes([0x22; 16])
12164        );
12165        assert_eq!(
12166            assigned.directories()[0].topics()[0].partitions()[0].error_code(),
12167            0
12168        );
12169        let altered =
12170            AlterReplicaLogDirsResponse::new(vec![AlterReplicaLogDirsResponseTopic::new(
12171                "t",
12172                vec![AlterReplicaLogDirsResponsePartition::new(0, 0)],
12173            )]);
12174        assert_eq!(altered.results()[0].topic_name(), "t");
12175        assert_eq!(altered.throttle_time_ms(), 0);
12176        assert_eq!(altered.results()[0].partitions()[0].partition_index(), 0);
12177        assert_eq!(altered.results()[0].partitions()[0].error_code(), 0);
12178        assert_eq!(alter_replica_partition_error(altered.results(), "t", 0), 0);
12179        assert_eq!(
12180            alter_replica_partition_error(altered.results(), "missing", 0),
12181            0
12182        );
12183    }
12184
12185    #[test]
12186    fn admin_java_spec_getters_match_fields() {
12187        let topic = NewTopic::new("orders", 3, 1);
12188        assert_eq!(topic.name(), "orders");
12189        assert_eq!(topic.num_partitions(), 3);
12190        assert_eq!(topic.replication_factor(), 1);
12191        assert!(topic.replicas_assignments().is_none());
12192        assert_eq!(
12193            topic.to_string(),
12194            "(name=orders, numPartitions=3, replicationFactor=1, replicasAssignments=null, configs=null)"
12195        );
12196        assert_eq!(
12197            NewTopic::broker_defaults("t").to_string(),
12198            "(name=t, numPartitions=default, replicationFactor=default, replicasAssignments=null, configs=null)"
12199        );
12200        let assigned = NewTopic::with_assignments("t", [(0, vec![1, 2])]);
12201        assert_eq!(
12202            assigned.num_partitions(),
12203            CreateTopicsRequest::NO_NUM_PARTITIONS
12204        );
12205        assert_eq!(
12206            assigned.replication_factor(),
12207            CreateTopicsRequest::NO_REPLICATION_FACTOR
12208        );
12209        assert_eq!(
12210            assigned.replicas_assignments(),
12211            Some(&[(0, vec![1, 2])][..])
12212        );
12213        assert_eq!(
12214            assigned.to_string(),
12215            "(name=t, numPartitions=default, replicationFactor=default, replicasAssignments={0=[1, 2]}, configs=null)"
12216        );
12217        assert_eq!(
12218            NewTopic::new("orders", 3, 1)
12219                .config("cleanup.policy", "compact")
12220                .to_string(),
12221            "(name=orders, numPartitions=3, replicationFactor=1, replicasAssignments=null, configs={cleanup.policy=compact})"
12222        );
12223        let parts = NewPartitions::increase_to("t", 5);
12224        assert_eq!(parts.name(), "t");
12225        assert_eq!(parts.total_count(), 5);
12226        assert!(parts.assignments().is_none());
12227        assert_eq!(parts.to_string(), "(totalCount=5, newAssignments=null)");
12228        let with = parts.with_assignments([vec![1, 2]]);
12229        assert_eq!(with.assignments().map(<[Vec<i32>]>::len), Some(1));
12230        assert_eq!(with.to_string(), "(totalCount=5, newAssignments=[[1, 2]])");
12231        assert_eq!(
12232            NewPartitions::increase_to("t", 5)
12233                .with_assignments(Vec::<Vec<i32>>::new())
12234                .to_string(),
12235            "(totalCount=5, newAssignments=[])"
12236        );
12237        let abort = AbortTransactionSpec::new(("events", 2), 9, 1, 3);
12238        assert_eq!(
12239            abort.topic_partition(),
12240            crate::TopicPartition::new("events", 2)
12241        );
12242        assert_eq!(abort.producer_id(), 9);
12243        assert_eq!(abort.producer_epoch(), 1);
12244        assert_eq!(abort.coordinator_epoch(), 3);
12245        assert_eq!(
12246            abort.to_string(),
12247            "AbortTransactionSpec(topicPartition=events-2, producerId=9, producerEpoch=1, coordinatorEpoch=3)"
12248        );
12249        let member = MemberToRemove::new("i-1");
12250        assert_eq!(member.group_instance_id(), "i-1");
12251        let removed = RemovedMember {
12252            member_id: "m".into(),
12253            group_instance_id: Some("i-1".into()),
12254            error_code: 0,
12255        };
12256        assert_eq!(removed.member_id(), "m");
12257        assert_eq!(removed.group_instance_id(), Some("i-1"));
12258        assert_eq!(removed.error_code(), 0);
12259        let spec = ListConsumerGroupOffsetsSpec::topic_partitions([("t", 0)]);
12260        assert_eq!(spec.partitions().map(<[_]>::len), Some(1));
12261        assert_eq!(
12262            spec.to_string(),
12263            "ListConsumerGroupOffsetsSpec(topicPartitions=[t-0])"
12264        );
12265        assert_eq!(
12266            ListConsumerGroupOffsetsSpec::topic_partitions([("t", 0), ("t", 1)]).to_string(),
12267            "ListConsumerGroupOffsetsSpec(topicPartitions=[t-0, t-1])"
12268        );
12269        assert_eq!(
12270            ListConsumerGroupOffsetsSpec {
12271                partitions: Some(Vec::new()),
12272            }
12273            .to_string(),
12274            "ListConsumerGroupOffsetsSpec(topicPartitions=[])"
12275        );
12276        assert!(ListConsumerGroupOffsetsSpec::all().partitions().is_none());
12277        assert_eq!(
12278            ListConsumerGroupOffsetsSpec::all().to_string(),
12279            "ListConsumerGroupOffsetsSpec(topicPartitions=null)"
12280        );
12281        let update = FeatureUpdate::new("metadata.version", 20);
12282        assert_eq!(update.name(), "metadata.version");
12283        assert_eq!(update.max_version_level(), 20);
12284        assert!(!update.is_delete_request());
12285        assert_eq!(
12286            update.to_string(),
12287            "FeatureUpdate{maxVersionLevel:20, upgradeType:UPGRADE}"
12288        );
12289        assert_eq!(
12290            FeatureUpdate::new("metadata.version", 20)
12291                .upgrade_type(UpgradeType::SafeDowngrade)
12292                .to_string(),
12293            "FeatureUpdate{maxVersionLevel:20, upgradeType:SAFE_DOWNGRADE}"
12294        );
12295        assert!(!FeatureUpdate::new("metadata.version", 20)
12296            .upgrade_type(UpgradeType::SafeDowngrade)
12297            .is_delete_request());
12298        assert_eq!(
12299            FeatureUpdate::new("metadata.version", 0)
12300                .upgrade_type(UpgradeType::UnsafeDowngrade)
12301                .to_string(),
12302            "FeatureUpdate{maxVersionLevel:0, upgradeType:UNSAFE_DOWNGRADE}"
12303        );
12304        assert!(FeatureUpdate::new("metadata.version", 0)
12305            .upgrade_type(UpgradeType::UnsafeDowngrade)
12306            .is_delete_request());
12307        assert!(FeatureUpdate::new("metadata.version", 0)
12308            .upgrade_type(UpgradeType::SafeDowngrade)
12309            .is_delete_request());
12310        assert!(!FeatureUpdate::new("metadata.version", 0).is_delete_request());
12311        assert_eq!(UpgradeType::Upgrade.to_string(), "UPGRADE");
12312        assert_eq!(UpgradeType::SafeDowngrade.to_string(), "SAFE_DOWNGRADE");
12313        assert_eq!(UpgradeType::UnsafeDowngrade.to_string(), "UNSAFE_DOWNGRADE");
12314        assert_eq!(UpgradeType::Upgrade.code(), UPGRADE_TYPE_UPGRADE);
12315        assert_eq!(
12316            UpgradeType::SafeDowngrade.code(),
12317            UPGRADE_TYPE_SAFE_DOWNGRADE
12318        );
12319        assert_eq!(
12320            UpgradeType::UnsafeDowngrade.code(),
12321            UPGRADE_TYPE_UNSAFE_DOWNGRADE
12322        );
12323        assert_eq!(UpgradeType::from_code(1), Some(UpgradeType::Upgrade));
12324        assert_eq!(UpgradeType::from_code(2), Some(UpgradeType::SafeDowngrade));
12325        assert_eq!(
12326            UpgradeType::from_code(3),
12327            Some(UpgradeType::UnsafeDowngrade)
12328        );
12329        assert!(UpgradeType::from_code(0).is_none());
12330        assert!(UpgradeType::from_code(99).is_none());
12331        assert_eq!(
12332            FeatureUpdate::new("metadata.version", 1)
12333                .upgrade_type(0_i8)
12334                .to_string(),
12335            "FeatureUpdate{maxVersionLevel:1, upgradeType:UNKNOWN}"
12336        );
12337        let range = SupportedVersionRange::new("metadata.version", 1, 20).unwrap();
12338        assert_eq!(range.min_version(), 1);
12339        assert_eq!(range.max_version(), 20);
12340        assert_eq!(
12341            range.to_string(),
12342            "SupportedVersionRange[min_version:1, max_version:20]"
12343        );
12344        let fin = FinalizedVersionRange::new("metadata.version", 1, 17).unwrap();
12345        assert_eq!(fin.min_version_level(), 1);
12346        assert_eq!(fin.max_version_level(), 17);
12347        assert_eq!(
12348            fin.to_string(),
12349            "FinalizedVersionRange[min_version_level:1, max_version_level:17]"
12350        );
12351        let kraft = SupportedVersionRange::new("kraft.version", 0, 1).unwrap();
12352        assert_eq!(
12353            kraft.to_string(),
12354            "SupportedVersionRange[min_version:0, max_version:1]"
12355        );
12356        let err = SupportedVersionRange::new("f", 0, -1).unwrap_err();
12357        assert!(
12358            matches!(err, Error::Protocol(_)),
12359            "negative maxVersion is Java IllegalArgumentException, got {err}"
12360        );
12361        assert!(
12362            err.to_string().contains(
12363                "Expected 0 <= minVersion <= maxVersion but received minVersion:0, maxVersion:-1."
12364            ),
12365            "got {err}"
12366        );
12367        let err = SupportedVersionRange::new("f", 2, 1).unwrap_err();
12368        assert!(
12369            err.to_string()
12370                .contains("but received minVersion:2, maxVersion:1."),
12371            "got {err}"
12372        );
12373        let err = FinalizedVersionRange::new("f", -1, 1).unwrap_err();
12374        assert!(
12375            matches!(err, Error::Protocol(_)),
12376            "negative minVersionLevel is Java IllegalArgumentException, got {err}"
12377        );
12378        assert!(
12379            err.to_string().contains(
12380                "Expected minVersionLevel >= 0, maxVersionLevel >= 0 and maxVersionLevel >= minVersionLevel, but received minVersionLevel: -1, maxVersionLevel: 1"
12381            ),
12382            "got {err}"
12383        );
12384        let err = FinalizedVersionRange::new("f", 2, 1).unwrap_err();
12385        assert!(
12386            err.to_string()
12387                .contains("minVersionLevel: 2, maxVersionLevel: 1"),
12388            "got {err}"
12389        );
12390        let zero = FinalizedVersionRange::new("f", 0, 0).unwrap();
12391        assert_eq!(zero.min_version_level(), 0);
12392        assert_eq!(zero.max_version_level(), 0);
12393        let md = FeatureMetadata {
12394            supported_features: vec![range],
12395            finalized_features: vec![fin],
12396            finalized_features_epoch: Some(8),
12397            zk_migration_ready: true,
12398        };
12399        assert_eq!(md.supported_features().len(), 1);
12400        assert_eq!(md.finalized_features().len(), 1);
12401        assert_eq!(md.finalized_features_epoch(), Some(8));
12402        assert!(md.zk_migration_ready());
12403        assert_eq!(
12404            md.to_string(),
12405            "FeatureMetadata{finalizedFeatures:{(metadata.version -> FinalizedVersionRange[min_version_level:1, max_version_level:17])}, finalizedFeaturesEpoch:8, supportedFeatures:{(metadata.version -> SupportedVersionRange[min_version:1, max_version:20])}}"
12406        );
12407        let empty_epoch = FeatureMetadata {
12408            finalized_features_epoch: None,
12409            supported_features: Vec::new(),
12410            finalized_features: Vec::new(),
12411            zk_migration_ready: false,
12412        };
12413        assert_eq!(
12414            empty_epoch.to_string(),
12415            "FeatureMetadata{finalizedFeatures:{}, finalizedFeaturesEpoch: , supportedFeatures:{}}"
12416        );
12417        let cluster = ClusterDescription::new(
12418            0,
12419            None,
12420            Some("mock".into()),
12421            1,
12422            1,
12423            AUTHORIZED_OPERATIONS_OMITTED,
12424            vec![DescribeClusterBroker::new(
12425                1,
12426                "127.0.0.1",
12427                9092,
12428                Some("r".into()),
12429                false,
12430            )],
12431        );
12432        assert_eq!(cluster.error_code(), 0);
12433        assert_eq!(cluster.cluster_id(), Some("mock"));
12434        assert_eq!(
12435            cluster.cluster_resource().to_string(),
12436            "ClusterResource(clusterId=mock)"
12437        );
12438        assert_eq!(cluster.cluster_resource().cluster_id(), Some("mock"));
12439        assert_eq!(
12440            ClusterResource::new(None::<String>).to_string(),
12441            "ClusterResource(clusterId=null)"
12442        );
12443        assert_eq!(cluster.controller_id(), 1);
12444        assert_eq!(cluster.brokers()[0].id(), 1);
12445        assert_eq!(cluster.brokers()[0].host(), "127.0.0.1");
12446        assert_eq!(cluster.brokers()[0].port(), 9092);
12447        assert_eq!(cluster.brokers()[0].rack(), Some("r"));
12448        assert!(cluster.brokers()[0].has_rack());
12449        assert!(!cluster.brokers()[0].is_fenced());
12450        assert_eq!(cluster.brokers()[0].id_string(), "1");
12451        assert!(!cluster.brokers()[0].is_empty());
12452        assert!(cluster.error_message().is_none());
12453        assert_eq!(
12454            cluster.brokers()[0].to_string(),
12455            "127.0.0.1:9092 (id: 1 rack: r isFenced: false)"
12456        );
12457        let empty = DescribeClusterBroker::no_node();
12458        assert_eq!(empty.id(), -1);
12459        assert!(empty.is_empty());
12460        assert_eq!(empty.id_string(), "-1");
12461        assert_eq!(empty.to_string(), ":-1 (id: -1 rack: null isFenced: false)");
12462        let from_broker: Node =
12463            crate::protocol::api::Broker::new(1, "127.0.0.1", 9092, Some("r".into())).into();
12464        assert_eq!(from_broker.id(), 1);
12465        assert_eq!(from_broker.host(), "127.0.0.1");
12466        assert!(!from_broker.is_fenced());
12467        let from_endpoint: Node =
12468            crate::protocol::api::NodeEndpoint::new(2, "h2", 9093, None).into();
12469        assert_eq!(from_endpoint.id_string(), "2");
12470        assert!(!from_endpoint.has_rack());
12471        assert_eq!(
12472            from_endpoint.to_string(),
12473            "h2:9093 (id: 2 rack: null isFenced: false)"
12474        );
12475    }
12476
12477    #[test]
12478    fn config_get_and_replacement_match_java() {
12479        let entry = ConfigEntry::new("retention.ms", Some("1000".into()));
12480        let config = Config::new([entry.clone()]);
12481        assert_eq!(config.entries(), std::slice::from_ref(&entry));
12482        assert_eq!(config.get("retention.ms"), Some(&entry));
12483        assert_eq!(config.get("missing"), None);
12484        let described = DescribeConfigsResult {
12485            error_code: 0,
12486            error_message: None,
12487            resource_type: CONFIG_RESOURCE_TOPIC,
12488            name: "t".into(),
12489            entries: vec![entry.clone()],
12490        };
12491        assert_eq!(described.config().get("retention.ms"), Some(&entry));
12492        assert_eq!(described.name(), "t");
12493        assert_eq!(described.error_code(), 0);
12494        assert_eq!(described.entries().len(), 1);
12495        assert_eq!(entry.name(), "retention.ms");
12496        assert_eq!(entry.value(), Some("1000"));
12497        let replacement = ConfigReplacement::from_config(ConfigResource::topic("t"), &config);
12498        assert_eq!(replacement.resource.name, "t");
12499        assert_eq!(
12500            replacement.configs,
12501            vec![("retention.ms".into(), Some("1000".into()))]
12502        );
12503        let by_resource = crate::protocol::admin::DescribeConfigsResponse::result_map(
12504            std::slice::from_ref(&described),
12505        );
12506        assert_eq!(
12507            by_resource.get(&ConfigResource::topic("t")),
12508            Some(&described)
12509        );
12510        let unknown_type = DescribeConfigsResult {
12511            error_code: 0,
12512            error_message: None,
12513            resource_type: 99,
12514            name: "x".into(),
12515            entries: Vec::new(),
12516        };
12517        let unknown_map = crate::protocol::admin::DescribeConfigsResponse::result_map(
12518            std::slice::from_ref(&unknown_type),
12519        );
12520        assert_eq!(
12521            unknown_map.get(&ConfigResource {
12522                resource_type: 0,
12523                name: "x".into(),
12524                keys: None,
12525            }),
12526            Some(&unknown_type)
12527        );
12528    }
12529
12530    #[test]
12531    fn alter_configs_errors_match_java() {
12532        let ok = AlterConfigsResourceResult::error(CONFIG_RESOURCE_TOPIC, "t", 0);
12533        let bad = AlterConfigsResourceResult::error(
12534            CONFIG_RESOURCE_BROKER,
12535            "1",
12536            crate::error::INVALID_REQUEST,
12537        );
12538        let results = [ok, bad];
12539        let errors = crate::protocol::admin::AlterConfigsResponse::errors(&results);
12540        assert_eq!(errors.len(), 2);
12541        assert_eq!(
12542            errors.get(&ConfigResource::topic("t")),
12543            Some(&crate::error::ApiError::NONE)
12544        );
12545        let broker = errors.get(&ConfigResource::broker(1)).unwrap();
12546        assert_eq!(broker.error(), crate::error::INVALID_REQUEST);
12547        assert!(broker.message().is_none());
12548        assert_eq!(
12549            crate::protocol::admin::IncrementalAlterConfigsResponse::from_response_data(&results),
12550            errors
12551        );
12552    }
12553
12554    #[test]
12555    fn alter_configs_request_configs_match_java() {
12556        let resources = [
12557            AlterConfigsResource {
12558                resource_type: CONFIG_RESOURCE_TOPIC,
12559                name: "t".into(),
12560                configs: vec![TopicConfig {
12561                    name: "retention.ms".into(),
12562                    value: Some("1000".into()),
12563                }],
12564            },
12565            AlterConfigsResource {
12566                resource_type: 99,
12567                name: "x".into(),
12568                configs: vec![TopicConfig {
12569                    name: "unset".into(),
12570                    value: None,
12571                }],
12572            },
12573        ];
12574        let configs = crate::protocol::admin::AlterConfigsRequest::configs(&resources);
12575        assert_eq!(configs.len(), 2);
12576        assert_eq!(
12577            configs
12578                .get(&ConfigResource::topic("t"))
12579                .and_then(|c| c.get("retention.ms"))
12580                .and_then(ConfigEntry::value),
12581            Some("1000")
12582        );
12583        let unknown_key = ConfigResource {
12584            resource_type: 0,
12585            name: "x".into(),
12586            keys: None,
12587        };
12588        assert_eq!(
12589            configs
12590                .get(&unknown_key)
12591                .and_then(|c| c.get("unset"))
12592                .and_then(ConfigEntry::value),
12593            None
12594        );
12595        assert_eq!(
12596            configs.get(&unknown_key).map(|c| c.entries().len()),
12597            Some(1)
12598        );
12599    }
12600
12601    #[test]
12602    fn user_scram_credential_alteration_user_matches_java() {
12603        let d = UserScramCredentialDeletion::new("alice", SCRAM_SHA_256);
12604        assert_eq!(d.user(), "alice");
12605        assert_eq!(d.mechanism(), ScramMechanism::Sha256);
12606        assert_eq!(UserScramCredentialAlteration::from(d).user(), "alice");
12607        let u = UserScramCredentialUpsertion::new(
12608            "bob",
12609            SCRAM_SHA_256,
12610            4096,
12611            b"s".to_vec(),
12612            b"p".to_vec(),
12613        );
12614        assert_eq!(u.user(), "bob");
12615        assert_eq!(u.salt(), b"s");
12616        assert_eq!(u.credential_info().mechanism(), ScramMechanism::Sha256);
12617        assert_eq!(u.credential_info().iterations(), 4096);
12618        assert_eq!(UserScramCredentialAlteration::from(u).user(), "bob");
12619        let result = UserScramCredentialResult {
12620            user: "alice".into(),
12621            error_code: 0,
12622            error_message: None,
12623        };
12624        assert_eq!(result.user(), "alice");
12625        assert_eq!(result.error_code(), 0);
12626        assert!(result.error_message().is_none());
12627    }
12628
12629    #[test]
12630    fn config_resource_type_matches_protocol_consts() {
12631        assert_eq!(i8::from(ConfigResourceType::Topic), CONFIG_RESOURCE_TOPIC);
12632        assert_eq!(i8::from(ConfigResourceType::Broker), CONFIG_RESOURCE_BROKER);
12633        assert_eq!(
12634            i8::from(ConfigResourceType::BrokerLogger),
12635            CONFIG_RESOURCE_BROKER_LOGGER
12636        );
12637        assert_eq!(
12638            i8::from(ConfigResourceType::ClientMetrics),
12639            CONFIG_RESOURCE_CLIENT_METRICS
12640        );
12641        assert_eq!(i8::from(ConfigResourceType::Group), CONFIG_RESOURCE_GROUP);
12642        assert_eq!(
12643            ConfigResource::topic("t").resource_type,
12644            i8::from(ConfigResourceType::Topic)
12645        );
12646        assert_eq!(ConfigResource::topic("t").name(), "t");
12647        assert_eq!(
12648            ConfigResource::topic("t").resource_type(),
12649            Some(ConfigResourceType::Topic)
12650        );
12651        assert!(!ConfigResource::topic("t").is_default());
12652        assert_eq!(
12653            ConfigResource::topic("t").to_string(),
12654            "ConfigResource(type=TOPIC, name='t')"
12655        );
12656        assert!(ConfigResource::of(ConfigResourceType::Broker, "").is_default());
12657        assert_eq!(
12658            ConfigResource::of(ConfigResourceType::Broker, "").to_string(),
12659            "ConfigResource(type=BROKER, name='')"
12660        );
12661        assert_eq!(ConfigResourceType::Topic.to_string(), "TOPIC");
12662        assert_eq!(ConfigResourceType::Topic.id(), CONFIG_RESOURCE_TOPIC);
12663        assert_eq!(ConfigResourceType::Broker.id(), CONFIG_RESOURCE_BROKER);
12664        assert_eq!(
12665            ConfigResourceType::BrokerLogger.id(),
12666            CONFIG_RESOURCE_BROKER_LOGGER
12667        );
12668        assert_eq!(
12669            ConfigResourceType::ClientMetrics.id(),
12670            CONFIG_RESOURCE_CLIENT_METRICS
12671        );
12672        assert_eq!(ConfigResourceType::Group.id(), CONFIG_RESOURCE_GROUP);
12673        assert_eq!(
12674            ConfigResourceType::from_id(CONFIG_RESOURCE_TOPIC),
12675            Some(ConfigResourceType::Topic)
12676        );
12677        assert_eq!(ConfigResourceType::from_id(99), None);
12678    }
12679
12680    #[test]
12681    fn config_type_and_source_match_wire_ids() {
12682        assert_eq!(i8::from(ConfigType::Unknown), CONFIG_TYPE_UNKNOWN);
12683        assert_eq!(i8::from(ConfigType::String), CONFIG_TYPE_STRING);
12684        assert_eq!(i8::from(ConfigType::Password), CONFIG_TYPE_PASSWORD);
12685        assert_eq!(ConfigType::from_id(CONFIG_TYPE_STRING), ConfigType::String);
12686        assert_eq!(ConfigType::from_id(99), ConfigType::Unknown);
12687        assert_eq!(ConfigType::Unknown.to_string(), "UNKNOWN");
12688        assert_eq!(ConfigType::Int.to_string(), "INT");
12689        assert_eq!(ConfigSource::Default.to_string(), "DEFAULT_CONFIG");
12690        assert_eq!(
12691            ConfigSource::DynamicTopic.to_string(),
12692            "DYNAMIC_TOPIC_CONFIG"
12693        );
12694        assert_eq!(i8::from(ConfigSource::Unknown), CONFIG_SOURCE_UNKNOWN);
12695        assert_eq!(
12696            i8::from(ConfigSource::DynamicTopic),
12697            CONFIG_SOURCE_DYNAMIC_TOPIC
12698        );
12699        assert_eq!(i8::from(ConfigSource::Default), CONFIG_SOURCE_DEFAULT);
12700        assert_eq!(
12701            i8::from(ConfigSource::DynamicGroup),
12702            CONFIG_SOURCE_DYNAMIC_GROUP
12703        );
12704        assert_eq!(
12705            ConfigSource::from_id(CONFIG_SOURCE_DEFAULT),
12706            ConfigSource::Default
12707        );
12708        assert_eq!(ConfigSource::from_id(-1), ConfigSource::Unknown);
12709        assert_eq!(ConfigSource::from_id(99), ConfigSource::Unknown);
12710        let def = ConfigEntry {
12711            name: "k".into(),
12712            value: Some("v".into()),
12713            read_only: false,
12714            source: CONFIG_SOURCE_DEFAULT,
12715            is_sensitive: false,
12716            synonyms: Vec::new(),
12717            config_type: CONFIG_TYPE_INT,
12718            documentation: None,
12719        };
12720        assert!(def.is_default());
12721        assert_eq!(def.source(), ConfigSource::Default);
12722        assert_eq!(def.config_type(), ConfigType::Int);
12723        assert_eq!(def.name(), "k");
12724        assert_eq!(def.value(), Some("v"));
12725        assert!(!def.is_sensitive());
12726        assert!(!def.is_read_only());
12727        assert!(def.synonyms().is_empty());
12728        assert!(def.documentation().is_none());
12729        let syn = ConfigSynonym {
12730            name: "k".into(),
12731            value: Some("v".into()),
12732            source: CONFIG_SOURCE_DEFAULT,
12733        };
12734        assert_eq!(syn.name(), "k");
12735        assert_eq!(syn.value(), Some("v"));
12736        assert_eq!(syn.source(), ConfigSource::Default);
12737        assert_eq!(
12738            syn.to_string(),
12739            "ConfigSynonym(name=k, value=v, source=DEFAULT_CONFIG)"
12740        );
12741        assert_eq!(
12742            def.to_string(),
12743            "ConfigEntry(name=k, value=v, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[], type=INT, documentation=null)"
12744        );
12745        let with_syn = ConfigEntry {
12746            synonyms: vec![syn.clone()],
12747            ..def.clone()
12748        };
12749        assert_eq!(
12750            with_syn.to_string(),
12751            "ConfigEntry(name=k, value=v, source=DEFAULT_CONFIG, isSensitive=false, isReadOnly=false, synonyms=[ConfigSynonym(name=k, value=v, source=DEFAULT_CONFIG)], type=INT, documentation=null)"
12752        );
12753    }
12754
12755    #[test]
12756    fn alter_config_op_type_matches_java() {
12757        assert_eq!(i8::from(AlterConfigOpType::Set), ALTER_CONFIG_SET);
12758        assert_eq!(AlterConfigOpType::Set.id(), ALTER_CONFIG_SET);
12759        assert_eq!(AlterConfigOpType::Delete.id(), ALTER_CONFIG_DELETE);
12760        assert_eq!(AlterConfigOpType::Append.id(), ALTER_CONFIG_APPEND);
12761        assert_eq!(AlterConfigOpType::Subtract.id(), ALTER_CONFIG_SUBTRACT);
12762        assert_eq!(i8::from(AlterConfigOpType::Delete), ALTER_CONFIG_DELETE);
12763        assert_eq!(i8::from(AlterConfigOpType::Append), ALTER_CONFIG_APPEND);
12764        assert_eq!(i8::from(AlterConfigOpType::Subtract), ALTER_CONFIG_SUBTRACT);
12765        assert_eq!(
12766            AlterConfigOpType::from_id(ALTER_CONFIG_SET),
12767            Some(AlterConfigOpType::Set)
12768        );
12769        assert_eq!(AlterConfigOpType::from_id(99), None);
12770        assert_eq!(AlterConfigOpType::Set.to_string(), "SET");
12771        assert_eq!(AlterConfigOpType::Delete.to_string(), "DELETE");
12772        assert_eq!(AlterConfigOpType::Append.to_string(), "APPEND");
12773        assert_eq!(AlterConfigOpType::Subtract.to_string(), "SUBTRACT");
12774        let entry = ConfigEntry::new("retention.ms", Some("1000".into()));
12775        let op = AlterConfig::from_entry(&entry, AlterConfigOpType::Set);
12776        assert_eq!(op.op_type(), Some(AlterConfigOpType::Set));
12777        assert_eq!(op.config_entry().name, "retention.ms");
12778        assert_eq!(op.config_entry().value.as_deref(), Some("1000"));
12779        assert_eq!(
12780            op.to_string(),
12781            "AlterConfigOp{opType=SET, configEntry=ConfigEntry(name=retention.ms, value=1000, source=UNKNOWN, isSensitive=false, isReadOnly=false, synonyms=[], type=UNKNOWN, documentation=null)}"
12782        );
12783        let unknown = AlterConfig {
12784            name: "k".into(),
12785            op: 99,
12786            value: None,
12787        };
12788        assert!(unknown.op_type().is_none());
12789        assert_eq!(
12790            unknown.to_string(),
12791            "AlterConfigOp{opType=null, configEntry=ConfigEntry(name=k, value=null, source=UNKNOWN, isSensitive=false, isReadOnly=false, synonyms=[], type=UNKNOWN, documentation=null)}"
12792        );
12793        assert_eq!(AlterConfigOp::set("k", "v").op, ALTER_CONFIG_SET);
12794    }
12795
12796    #[test]
12797    fn group_type_and_state_match_java() {
12798        assert_eq!(GroupType::Classic.as_str(), "Classic");
12799        assert_eq!(GroupType::parse("classic"), GroupType::Classic);
12800        assert_eq!(GroupType::parse("CONSUMER"), GroupType::Consumer);
12801        assert_eq!(GroupType::parse("Unknown"), GroupType::Unknown);
12802        assert_eq!(GroupType::parse("Streams"), GroupType::Unknown);
12803        assert_eq!(GroupType::parse("nope"), GroupType::Unknown);
12804        assert_eq!(GroupState::Stable.as_str(), "Stable");
12805        assert_eq!(GroupState::parse("stable"), GroupState::Stable);
12806        assert_eq!(
12807            GroupState::parse("PreparingRebalance"),
12808            GroupState::PreparingRebalance
12809        );
12810        // Java keys `toString().toUpperCase()`, not the enum name: underscore
12811        // form is UNKNOWN, same as `GroupState.parse("PREPARING_REBALANCE")`.
12812        assert_eq!(
12813            GroupState::parse("PREPARING_REBALANCE"),
12814            GroupState::Unknown
12815        );
12816        assert_eq!(GroupState::parse("Unknown"), GroupState::Unknown);
12817        assert_eq!(GroupState::parse("nope"), GroupState::Unknown);
12818        assert_eq!(
12819            GroupState::group_states_for_type(GroupType::Share),
12820            &[GroupState::Stable, GroupState::Dead, GroupState::Empty]
12821        );
12822        assert!(GroupState::group_states_for_type(GroupType::Unknown).is_empty());
12823        let listed = ListedGroup {
12824            group_id: "g".into(),
12825            protocol_type: "consumer".into(),
12826            group_state: "Stable".into(),
12827            group_type: "classic".into(),
12828        };
12829        assert_eq!(listed.group_state(), GroupState::Stable);
12830        assert_eq!(listed.group_type(), GroupType::Classic);
12831        assert_eq!(listed.group_id(), "g");
12832        assert_eq!(listed.protocol(), "consumer");
12833        assert!(!listed.is_simple_consumer_group());
12834        assert_eq!(
12835            listed.to_string(),
12836            "(groupId='g', type=Classic, protocol='consumer', groupState=Stable)"
12837        );
12838        assert_eq!(
12839            ListedGroup::new("g").to_string(),
12840            "(groupId='g', type=none, protocol='', groupState=none)"
12841        );
12842        let simple = ListedGroup {
12843            group_id: "s".into(),
12844            protocol_type: String::new(),
12845            group_state: "Empty".into(),
12846            group_type: "classic".into(),
12847        };
12848        assert!(simple.is_simple_consumer_group());
12849        assert_eq!(simple.protocol(), "");
12850        let consumer = ConsumerGroupDescription::Consumer({
12851            let mut g = DescribedConsumerGroup::new("g-cons", 0);
12852            g.group_state = "Stable".into();
12853            g.group_epoch = 4;
12854            g.assignment_epoch = 5;
12855            g.assignor_name = "uniform".into();
12856            g
12857        });
12858        assert_eq!(consumer.group_id(), "g-cons");
12859        assert_eq!(consumer.group_state(), "Stable");
12860        assert_eq!(consumer.partition_assignor(), "uniform");
12861        assert_eq!(consumer.group_type(), GroupType::Consumer);
12862        assert_eq!(consumer.group_epoch(), Some(4));
12863        assert_eq!(consumer.target_assignment_epoch(), Some(5));
12864        assert!(!consumer.is_simple_consumer_group());
12865        assert!(consumer.is_consumer_protocol());
12866        let classic = ConsumerGroupDescription::Classic({
12867            let mut g = DescribedGroup::new("g-classic", 0);
12868            g.group_state = "Stable".into();
12869            g.protocol_type = "consumer".into();
12870            g.protocol_data = "range".into();
12871            g
12872        });
12873        assert_eq!(classic.partition_assignor(), "range");
12874        assert_eq!(classic.group_type(), GroupType::Classic);
12875        assert!(classic.group_epoch().is_none());
12876        assert!(classic.target_assignment_epoch().is_none());
12877        assert!(!classic.is_simple_consumer_group());
12878        assert!(!classic.is_consumer_protocol());
12879        let simple_desc = ConsumerGroupDescription::Classic(DescribedGroup::new("s", 0));
12880        assert!(simple_desc.is_simple_consumer_group());
12881        assert_eq!(simple_desc.partition_assignor(), "");
12882    }
12883
12884    #[test]
12885    fn remaining_admin_result_getters_match_java() {
12886        let pid = ProducerIdBlock {
12887            producer_id_start: 1000,
12888            producer_id_len: 1000,
12889        };
12890        assert_eq!(pid.producer_id_start(), 1000);
12891        assert_eq!(pid.producer_id_len(), 1000);
12892        let fenced = FencedProducer {
12893            transactional_id: "tid".into(),
12894            producer_id: 9,
12895            epoch: 1,
12896        };
12897        assert_eq!(fenced.transactional_id(), "tid");
12898        assert_eq!(fenced.producer_id(), 9);
12899        assert_eq!(fenced.epoch(), 1);
12900        let deleted = DeletedAclsFilterResult {
12901            error_code: 0,
12902            error_message: None,
12903            matching: vec![crate::protocol::acl::DeleteAclsResponse::matching_acl(
12904                &AclBinding::allow_topic("t", "User:alice"),
12905                &crate::error::ApiError::NONE,
12906            )],
12907        };
12908        assert_eq!(deleted.error_code(), 0);
12909        assert!(deleted.error_message().is_none());
12910        assert_eq!(deleted.matching().len(), 1);
12911        let altered = AlterConfigsResourceResult {
12912            error_code: 0,
12913            error_message: None,
12914            resource_type: CONFIG_RESOURCE_TOPIC,
12915            name: "t".into(),
12916        };
12917        assert_eq!(altered.error_code(), 0);
12918        assert_eq!(altered.name(), "t");
12919        assert!(altered.error_message().is_none());
12920        let created = TopicResult {
12921            name: "orders".into(),
12922            error_code: 0,
12923            error_message: None,
12924            topic_id: Uuid::ONE.to_bytes(),
12925            num_partitions: 3,
12926            replication_factor: 1,
12927            configs: vec![CreatedTopicConfig {
12928                name: "cleanup.policy".into(),
12929                value: Some("compact".into()),
12930                read_only: false,
12931                config_source: CONFIG_SOURCE_DYNAMIC_TOPIC,
12932                is_sensitive: false,
12933            }],
12934        };
12935        assert_eq!(created.name(), "orders");
12936        assert_eq!(created.error_code(), 0);
12937        assert!(created.error_message().is_none());
12938        assert_eq!(created.topic_id(), Uuid::ONE);
12939        assert_eq!(created.num_partitions(), 3);
12940        assert_eq!(created.replication_factor(), 1);
12941        assert_eq!(
12942            created
12943                .config()
12944                .get("cleanup.policy")
12945                .and_then(ConfigEntry::value),
12946            Some("compact")
12947        );
12948        let listed = ListedConfigResource::new("r", CONFIG_RESOURCE_CLIENT_METRICS);
12949        assert_eq!(listed.name(), "r");
12950        assert_eq!(
12951            listed.resource_type(),
12952            Some(ConfigResourceType::ClientMetrics)
12953        );
12954        assert!(!listed.is_default());
12955        assert_eq!(
12956            listed.to_string(),
12957            "ConfigResource(type=CLIENT_METRICS, name='r')"
12958        );
12959        assert!(ListedConfigResource::new("", CONFIG_RESOURCE_BROKER).is_default());
12960        assert_eq!(
12961            ListedConfigResource::new("t", CONFIG_RESOURCE_TOPIC).to_string(),
12962            "ConfigResource(type=TOPIC, name='t')"
12963        );
12964        let resource = listed.to_config_resource();
12965        assert_eq!(resource.name(), "r");
12966        assert_eq!(
12967            resource.resource_type(),
12968            Some(ConfigResourceType::ClientMetrics)
12969        );
12970        let owned = ConfigResource::from(listed);
12971        assert_eq!(owned.name(), "r");
12972        let deleted = DeletableGroupResult::new("g", 0);
12973        assert_eq!(deleted.group_id(), "g");
12974        assert_eq!(deleted.error_code(), 0);
12975        let telemetry = GetTelemetrySubscriptionsResponse::new(
12976            0,
12977            Uuid::from_bytes([0x11; 16]),
12978            1,
12979            vec![1],
12980            1000,
12981            100,
12982            true,
12983            vec!["m".into()],
12984        );
12985        assert_eq!(telemetry.error_code(), 0);
12986        assert_eq!(telemetry.throttle_time_ms(), 0);
12987        assert_eq!(telemetry.client_instance_id(), Uuid::from_bytes([0x11; 16]));
12988        assert_eq!(telemetry.subscription_id(), 1);
12989        assert_eq!(telemetry.accepted_compression_types(), &[1]);
12990        assert_eq!(telemetry.push_interval_ms(), 1000);
12991        assert_eq!(telemetry.telemetry_max_bytes(), 100);
12992        assert!(telemetry.delta_temporality());
12993        assert_eq!(telemetry.requested_metrics(), &["m".to_string()]);
12994        assert_eq!(PushTelemetryResponse::new(0).error_code(), 0);
12995        assert_eq!(PushTelemetryResponse::new(0).throttle_time_ms(), 0);
12996        let push =
12997            PushTelemetryRequest::new(Uuid::from_bytes([0x11; 16]), 1, false, 0, b"m".to_vec());
12998        assert_eq!(push.client_instance_id(), Uuid::from_bytes([0x11; 16]));
12999        assert_eq!(push.subscription_id(), 1);
13000        assert!(!push.terminating());
13001        assert_eq!(push.compression_type(), 0);
13002        assert_eq!(push.metrics(), b"m");
13003        let cluster = ClusterDescription::new(
13004            0,
13005            None,
13006            Some("c".into()),
13007            2,
13008            1,
13009            AUTHORIZED_OPERATIONS_OMITTED,
13010            vec![
13011                DescribeClusterBroker::new(1, "h1", 9092, None, false),
13012                DescribeClusterBroker::new(2, "h2", 9092, Some("r".into()), false),
13013            ],
13014        );
13015        assert_eq!(cluster.nodes().len(), 2);
13016        assert_eq!(cluster.controller().map(DescribeClusterBroker::id), Some(2));
13017        assert_eq!(
13018            cluster.authorized_operations(),
13019            AUTHORIZED_OPERATIONS_OMITTED
13020        );
13021        assert!(cluster.error_message().is_none());
13022        let controller = cluster.controller().unwrap();
13023        assert_eq!(controller.id_string(), "2");
13024        assert!(!controller.is_empty());
13025        assert_eq!(
13026            controller.to_string(),
13027            "h2:9092 (id: 2 rack: r isFenced: false)"
13028        );
13029        let no_controller = ClusterDescription {
13030            controller_id: -1,
13031            ..cluster.clone()
13032        };
13033        assert!(no_controller.controller().is_none());
13034        let unreg = UnregisterBrokerResponse::new(0, None);
13035        assert_eq!(unreg.error_code(), 0);
13036        assert!(unreg.error_message().is_none());
13037    }
13038
13039    #[test]
13040    fn delegation_token_getters_match_java() {
13041        let renewer = CreatableRenewer::new("User", "r");
13042        assert_eq!(renewer.principal_type(), "User");
13043        assert_eq!(renewer.principal_name(), "r");
13044        assert_eq!(renewer.to_string(), "User:r");
13045        assert_eq!(CreatableRenewer::USER_TYPE, "User");
13046        assert_eq!(CreatableRenewer::anonymous().to_string(), "User:ANONYMOUS");
13047        assert_eq!(
13048            DescribeDelegationTokenOwner::anonymous().principal_name(),
13049            "ANONYMOUS"
13050        );
13051        assert_eq!(
13052            DescribedDelegationTokenRenewer::anonymous().to_string(),
13053            "User:ANONYMOUS"
13054        );
13055        let req = CreateDelegationTokenRequest::new(
13056            Some("User".into()),
13057            Some("alice".into()),
13058            vec![renewer],
13059            -1,
13060        );
13061        assert_eq!(req.owner_principal_type(), Some("User"));
13062        assert_eq!(req.owner_principal_name(), Some("alice"));
13063        assert_eq!(req.renewers().len(), 1);
13064        assert_eq!(req.max_lifetime_ms(), -1);
13065        let created = CreateDelegationTokenResponse::new(
13066            0,
13067            "User",
13068            "alice",
13069            "User",
13070            "bob",
13071            1,
13072            2,
13073            3,
13074            "tid",
13075            vec![0xaa],
13076        );
13077        assert_eq!(created.error_code(), 0);
13078        assert_eq!(created.principal_type(), "User");
13079        assert_eq!(created.principal_name(), "alice");
13080        assert_eq!(created.owner_as_string(), "User:alice");
13081        assert_eq!(created.token_requester_as_string(), "User:bob");
13082        assert_eq!(created.issue_timestamp(), 1);
13083        assert_eq!(created.expiry_timestamp(), 2);
13084        assert_eq!(created.max_timestamp(), 3);
13085        assert_eq!(created.token_id(), "tid");
13086        assert_eq!(created.hmac(), &[0xaa]);
13087        assert_eq!(created.hmac_as_base64_string(), "qg==");
13088        let created_debug = format!("{created:?}");
13089        assert!(
13090            created_debug.contains("[*******]"),
13091            "Java DelegationToken.toString redacts hmac: {created_debug}"
13092        );
13093        assert!(
13094            !created_debug.contains("aa") && !created_debug.contains("170"),
13095            "Debug must not leak hmac bytes: {created_debug}"
13096        );
13097        let owner = DescribeDelegationTokenOwner::new("User", "alice");
13098        assert_eq!(owner.to_string(), "User:alice");
13099        let described_req = DescribeDelegationTokenRequest::new(Some(vec![owner]));
13100        assert_eq!(
13101            described_req
13102                .owners()
13103                .map(<[DescribeDelegationTokenOwner]>::len),
13104            Some(1)
13105        );
13106        let token = DescribedDelegationToken::new(
13107            "User",
13108            "alice",
13109            "User",
13110            "bob",
13111            1,
13112            2,
13113            3,
13114            "tid",
13115            vec![0xaa],
13116            vec![DescribedDelegationTokenRenewer::new("User", "r")],
13117        );
13118        assert_eq!(token.owner_as_string(), "User:alice");
13119        assert_eq!(token.hmac_as_base64_string(), "qg==");
13120        assert_eq!(token.renewers()[0].to_string(), "User:r");
13121        assert_eq!(token.renewers_as_string(), vec!["User:r".to_string()]);
13122        assert!(token.owner_or_renewer("User", "alice"));
13123        assert!(token.owner_or_renewer("User", "bob"));
13124        assert!(token.owner_or_renewer("User", "r"));
13125        assert!(!token.owner_or_renewer("User", "other"));
13126        assert_eq!(
13127            token.to_string(),
13128            "DelegationToken{tokenInformation=TokenInformation{owner=User:alice, tokenRequester=User:bob, renewers=[User:r], issueTimestamp=1, maxTimestamp=3, expiryTimestamp=2, tokenId='tid'}, hmac=[*******]}"
13129        );
13130        let empty_renewers = DescribedDelegationToken::new(
13131            "User",
13132            "alice",
13133            "User",
13134            "bob",
13135            1,
13136            2,
13137            3,
13138            "tid",
13139            vec![0xaa],
13140            vec![],
13141        );
13142        assert_eq!(
13143            empty_renewers.to_string(),
13144            "DelegationToken{tokenInformation=TokenInformation{owner=User:alice, tokenRequester=User:bob, renewers=[], issueTimestamp=1, maxTimestamp=3, expiryTimestamp=2, tokenId='tid'}, hmac=[*******]}"
13145        );
13146        let two_renewers = DescribedDelegationToken::new(
13147            "User",
13148            "alice",
13149            "User",
13150            "bob",
13151            1,
13152            2,
13153            3,
13154            "tid",
13155            vec![0xaa],
13156            vec![
13157                DescribedDelegationTokenRenewer::new("User", "r"),
13158                DescribedDelegationTokenRenewer::new("User", "s"),
13159            ],
13160        );
13161        assert_eq!(
13162            two_renewers.to_string(),
13163            "DelegationToken{tokenInformation=TokenInformation{owner=User:alice, tokenRequester=User:bob, renewers=[User:r, User:s], issueTimestamp=1, maxTimestamp=3, expiryTimestamp=2, tokenId='tid'}, hmac=[*******]}"
13164        );
13165        let token_debug = format!("{token:?}");
13166        assert!(token_debug.contains("[*******]"));
13167        assert!(!token_debug.contains("170"));
13168        let listed = DescribeDelegationTokenResponse::new(0, vec![token]);
13169        assert_eq!(listed.error_code(), 0);
13170        assert_eq!(listed.tokens().len(), 1);
13171        let renewed = RenewDelegationTokenResponse::new(0, 9);
13172        assert_eq!(renewed.error_code(), 0);
13173        assert_eq!(renewed.expiry_timestamp(), 9);
13174        let expired = ExpireDelegationTokenResponse::new(0, 8);
13175        assert_eq!(expired.error_code(), 0);
13176        assert_eq!(expired.expiry_timestamp(), 8);
13177        let renew_req = RenewDelegationTokenRequest::new(vec![0xaa], -1);
13178        assert_eq!(renew_req.hmac(), &[0xaa]);
13179        assert_eq!(renew_req.renew_period_ms(), -1);
13180        let expire_req = ExpireDelegationTokenRequest::new(vec![0xaa], -1);
13181        assert_eq!(expire_req.hmac(), &[0xaa]);
13182        assert_eq!(expire_req.expiry_time_period_ms(), -1);
13183    }
13184
13185    #[test]
13186    fn new_partition_reassignment_matches_java() {
13187        let neu = NewPartitionReassignment::new([2, 1]).unwrap();
13188        assert_eq!(neu.target_replicas(), &[2, 1]);
13189        let err = NewPartitionReassignment::new(Vec::<i32>::new()).unwrap_err();
13190        assert!(
13191            err.to_string().contains("without any replicas"),
13192            "Java NewPartitionReassignment rejects an empty replica list: {err}"
13193        );
13194        let assigned = PartitionReassignment::from_new(("t", 0), Some(neu));
13195        assert_eq!(assigned.topic(), "t");
13196        assert_eq!(assigned.partition(), 0);
13197        assert_eq!(assigned.replicas(), Some(&[2, 1][..]));
13198        let cancelled = PartitionReassignment::from_new(("t", 0), None);
13199        assert!(cancelled.replicas().is_none());
13200        let ongoing = OngoingReassignment {
13201            topic: "t".into(),
13202            partition: 0,
13203            replicas: vec![2, 1],
13204            adding_replicas: vec![2],
13205            removing_replicas: vec![3],
13206        };
13207        assert_eq!(ongoing.topic(), "t");
13208        assert_eq!(ongoing.partition(), 0);
13209        assert_eq!(ongoing.replicas(), &[2, 1]);
13210        assert_eq!(ongoing.adding_replicas(), &[2]);
13211        assert_eq!(ongoing.removing_replicas(), &[3]);
13212        assert_eq!(
13213            ongoing.to_string(),
13214            "PartitionReassignment(replicas=[2, 1], addingReplicas=[2], removingReplicas=[3])"
13215        );
13216        let result = ReassignmentResult {
13217            topic: "t".into(),
13218            partition: 0,
13219            error_code: 0,
13220            error_message: None,
13221        };
13222        assert_eq!(result.topic(), "t");
13223        assert_eq!(result.partition(), 0);
13224        assert_eq!(result.error_code(), 0);
13225        assert!(result.error_message().is_none());
13226    }
13227
13228    #[test]
13229    fn uuid_matches_java() {
13230        assert_eq!(Uuid::ZERO.to_string(), "AAAAAAAAAAAAAAAAAAAAAA");
13231        assert_eq!(Uuid::ONE.to_string(), "AAAAAAAAAAAAAAAAAAAAAQ");
13232        assert_eq!(Uuid::ZERO_UUID, Uuid::ZERO);
13233        assert_eq!(Uuid::ONE_UUID, Uuid::ONE);
13234        assert_eq!(Uuid::METADATA_TOPIC_ID, Uuid::ONE);
13235        assert_eq!(Uuid::ZERO.most_significant_bits(), 0);
13236        assert_eq!(Uuid::ZERO.least_significant_bits(), 0);
13237        assert_eq!(Uuid::ONE.most_significant_bits(), 0);
13238        assert_eq!(Uuid::ONE.least_significant_bits(), 1);
13239        assert_eq!(
13240            Uuid::from_string("AAAAAAAAAAAAAAAAAAAAAA").unwrap(),
13241            Uuid::ZERO
13242        );
13243        assert_eq!(
13244            Uuid::from_string("AAAAAAAAAAAAAAAAAAAAAQ").unwrap(),
13245            Uuid::ONE
13246        );
13247        assert_eq!(
13248            Uuid::from_string("AAAAAAAAAAAAAAAAAAAAAQ==").unwrap(),
13249            Uuid::ONE,
13250            "Java fromString accepts URL-safe padding"
13251        );
13252        let invalid = Uuid::from_string("!!!!").unwrap_err();
13253        assert!(
13254            invalid
13255                .to_string()
13256                .contains("Uuid string `!!!!` is not a base64url UUID"),
13257            "{invalid}"
13258        );
13259        let too_long = Uuid::from_string("AAAAAAAAAAAAAAAAAAAAAAAAA").unwrap_err();
13260        assert!(
13261            too_long.to_string().contains(
13262                "Input string with prefix `AAAAAAAAAAAAAAAAAAAAAAAA` is too long to be decoded as a base64 UUID"
13263            ),
13264            "{too_long}"
13265        );
13266        let wrong_len = Uuid::from_string("AAAA").unwrap_err();
13267        assert!(
13268            wrong_len.to_string().contains(
13269                "Input string `AAAA` decoded as 3 bytes, which is not equal to the expected 16 bytes of a base64-encoded UUID"
13270            ),
13271            "{wrong_len}"
13272        );
13273        let parsed: Uuid = "AAAAAAAAAAAAAAAAAAAAAA".parse().unwrap();
13274        assert_eq!(parsed, Uuid::ZERO);
13275        assert_eq!(<[u8; 16]>::from(Uuid::ONE), Uuid::ONE.to_bytes());
13276        let neg = Uuid::from_parts(i64::MIN, 0);
13277        assert!(neg < Uuid::ZERO, "Java compareTo uses signed longs");
13278        let listing = TopicListing::new("t", Uuid::ONE, false);
13279        assert_eq!(listing.topic_id(), Uuid::ONE);
13280        assert_eq!(listing.name(), "t");
13281        assert!(!listing.is_internal());
13282        assert_eq!(
13283            listing.to_string(),
13284            "(name=t, topicId=AAAAAAAAAAAAAAAAAAAAAQ, internal=false)"
13285        );
13286        assert!(Uuid::RESERVED.contains(&Uuid::ZERO));
13287        assert!(Uuid::RESERVED.contains(&Uuid::ONE));
13288        assert!(Uuid::RESERVED.contains(&Uuid::METADATA_TOPIC_ID));
13289        let samples: Vec<Uuid> = (0..32).map(|_| Uuid::random_uuid()).collect();
13290        for (i, u) in samples.iter().enumerate() {
13291            assert_ne!(*u, Uuid::ZERO);
13292            assert_ne!(*u, Uuid::ONE);
13293            assert!(
13294                !u.to_string().starts_with('-'),
13295                "Java randomUuid skips dash-prefixed base64url"
13296            );
13297            assert_eq!(
13298                u.as_bytes().get(6).map(|b| b >> 4),
13299                Some(4),
13300                "Java randomUuid is RFC 4122 version 4"
13301            );
13302            assert_eq!(
13303                u.as_bytes().get(8).map(|b| b >> 6),
13304                Some(2),
13305                "Java randomUuid is RFC 4122 variant 2"
13306            );
13307            for (j, other) in samples.iter().enumerate() {
13308                if i != j {
13309                    assert_ne!(*u, *other);
13310                }
13311            }
13312        }
13313    }
13314
13315    #[test]
13316    fn topic_collection_matches_java() {
13317        let names = TopicCollection::of_topic_names(["orders", "events"]);
13318        assert_eq!(names.topic_names().map(<[String]>::len), Some(2));
13319        assert!(names.topic_ids().is_none());
13320        let ids = TopicCollection::of_topic_ids([Uuid::ONE, Uuid::ZERO]);
13321        assert_eq!(ids.topic_ids(), Some(&[Uuid::ONE, Uuid::ZERO][..]));
13322        assert!(ids.topic_names().is_none());
13323        let from_bytes = TopicCollection::of_topic_ids([[1u8; 16]]);
13324        assert_eq!(
13325            from_bytes.topic_ids().and_then(|ids| ids.first().copied()),
13326            Some(Uuid::from_bytes([1u8; 16]))
13327        );
13328        let empty = TopicCollection::of_topic_ids(Vec::<Uuid>::new());
13329        assert_eq!(empty, TopicCollection::Ids(Vec::new()));
13330        assert!(empty.topic_ids().unwrap().is_empty());
13331    }
13332
13333    #[test]
13334    fn scram_mechanism_matches_protocol_consts() {
13335        assert_eq!(i8::from(ScramMechanism::Unknown), SCRAM_UNKNOWN);
13336        assert_eq!(i8::from(ScramMechanism::Sha256), SCRAM_SHA_256);
13337        assert_eq!(i8::from(ScramMechanism::Sha512), SCRAM_SHA_512);
13338        assert_eq!(ScramMechanism::Unknown.id(), SCRAM_UNKNOWN);
13339        assert_eq!(ScramMechanism::Sha256.id(), SCRAM_SHA_256);
13340        assert_eq!(ScramMechanism::Sha512.id(), SCRAM_SHA_512);
13341        assert_eq!(
13342            ScramMechanism::from_id(SCRAM_SHA_256),
13343            ScramMechanism::Sha256
13344        );
13345        assert_eq!(ScramMechanism::from_id(0), ScramMechanism::Unknown);
13346        assert_eq!(ScramMechanism::from_id(99), ScramMechanism::Unknown);
13347        assert_eq!(
13348            ScramMechanism::from_mechanism_name("SCRAM-SHA-256"),
13349            ScramMechanism::Sha256
13350        );
13351        assert_eq!(
13352            ScramMechanism::from_mechanism_name("SCRAM_SHA_256"),
13353            ScramMechanism::Unknown
13354        );
13355        assert_eq!(ScramMechanism::Sha256.mechanism_name(), "SCRAM-SHA-256");
13356        assert_eq!(ScramMechanism::Unknown.to_string(), "UNKNOWN");
13357        assert_eq!(ScramMechanism::Sha256.to_string(), "SCRAM_SHA_256");
13358        assert_eq!(ScramMechanism::Sha512.to_string(), "SCRAM_SHA_512");
13359        let info = ScramCredentialInfo::new(ScramMechanism::Sha512, 8192);
13360        assert_eq!(info.mechanism(), ScramMechanism::Sha512);
13361        assert_eq!(info.iterations(), 8192);
13362        assert_eq!(
13363            info.to_string(),
13364            "ScramCredentialInfo{mechanism=SCRAM_SHA_512, iterations=8192}"
13365        );
13366        let described = DescribeUserScramCredentialsResult {
13367            user: "alice".into(),
13368            error_code: 0,
13369            error_message: None,
13370            credential_infos: vec![ScramCredentialInfo::new(ScramMechanism::Sha256, 4096)],
13371        };
13372        assert_eq!(
13373            described.to_string(),
13374            "UserScramCredentialsDescription{name='alice', credentialInfos=[ScramCredentialInfo{mechanism=SCRAM_SHA_256, iterations=4096}]}"
13375        );
13376    }
13377
13378    #[test]
13379    fn topic_listings_skip_errors_and_unnamed() {
13380        use crate::protocol::api::{PartitionMetadata, TopicMetadata};
13381
13382        let md = MetadataResponse {
13383            throttle_time_ms: 0,
13384            brokers: Vec::new(),
13385            cluster_id: None,
13386            controller_id: 1,
13387            topics: vec![
13388                TopicMetadata {
13389                    error_code: 0,
13390                    name: Some("ok".into()),
13391                    topic_id: [1; 16],
13392                    is_internal: false,
13393                    partitions: vec![PartitionMetadata {
13394                        error_code: 0,
13395                        partition_index: 0,
13396                        leader_id: 1,
13397                        leader_epoch: 3,
13398                        replica_nodes: vec![1],
13399                        isr_nodes: vec![1],
13400                        offline_replicas: Vec::new(),
13401                    }],
13402                    topic_authorized_operations: i32::MIN,
13403                },
13404                TopicMetadata {
13405                    error_code: error::UNKNOWN_TOPIC_OR_PARTITION,
13406                    name: Some("gone".into()),
13407                    topic_id: [0; 16],
13408                    is_internal: false,
13409                    partitions: vec![PartitionMetadata {
13410                        error_code: error::UNKNOWN_TOPIC_OR_PARTITION,
13411                        partition_index: 0,
13412                        leader_id: -1,
13413                        leader_epoch: -1,
13414                        replica_nodes: Vec::new(),
13415                        isr_nodes: Vec::new(),
13416                        offline_replicas: Vec::new(),
13417                    }],
13418                    topic_authorized_operations: i32::MIN,
13419                },
13420                TopicMetadata {
13421                    error_code: 0,
13422                    name: None,
13423                    topic_id: [2; 16],
13424                    is_internal: true,
13425                    partitions: Vec::new(),
13426                    topic_authorized_operations: i32::MIN,
13427                },
13428                TopicMetadata {
13429                    error_code: 0,
13430                    name: Some("__consumer_offsets".into()),
13431                    topic_id: [3; 16],
13432                    is_internal: true,
13433                    partitions: Vec::new(),
13434                    topic_authorized_operations: i32::MIN,
13435                },
13436            ],
13437            cluster_authorized_operations: MetadataResponse::AUTHORIZED_OPERATIONS_OMITTED,
13438            error_code: 0,
13439        };
13440        let listed = topic_listings_from(&md, true);
13441        assert_eq!(listed.len(), 2);
13442        assert_eq!(listed[0].name, "ok");
13443        assert_eq!(listed[0].name(), "ok");
13444        assert_eq!(listed[0].topic_id, [1; 16]);
13445        assert_eq!(listed[0].topic_id(), Uuid::from_bytes([1; 16]));
13446        assert!(!listed[0].is_internal());
13447        assert_eq!(listed[1].name, "__consumer_offsets");
13448        assert!(listed[1].is_internal);
13449        assert!(listed[1].is_internal());
13450        let listed = topic_listings_from(&md, false);
13451        assert_eq!(listed.len(), 1);
13452        assert_eq!(listed[0].name, "ok");
13453        let described = topic_descriptions_including_unnamed(&md);
13454        assert_eq!(described.len(), 4);
13455        assert_eq!(described[0].name, "ok");
13456        assert_eq!(described[0].partitions.len(), 1);
13457        assert_eq!(described[0].partitions().len(), 1);
13458        assert_eq!(described[0].error_code(), 0);
13459        assert_eq!(
13460            described[0].authorized_operations(),
13461            AUTHORIZED_OPERATIONS_OMITTED
13462        );
13463        assert_eq!(described[0].partitions[0].leader_epoch, 3);
13464        assert_eq!(described[1].name, "gone");
13465        assert_eq!(described[1].error_code, error::UNKNOWN_TOPIC_OR_PARTITION);
13466        assert_eq!(described[1].error_code(), error::UNKNOWN_TOPIC_OR_PARTITION);
13467        assert!(described[1].partitions.is_empty());
13468        assert!(described[1].partitions().is_empty());
13469        assert!(described[2].name.is_empty());
13470        assert_eq!(described[2].topic_id, [2; 16]);
13471        assert_eq!(described[2].topic_id(), Uuid::from_bytes([2; 16]));
13472        assert_eq!(described[3].name, "__consumer_offsets");
13473        assert_eq!(described[3].name(), "__consumer_offsets");
13474        assert!(described[3].is_internal);
13475        assert!(described[3].is_internal());
13476        let named =
13477            topic_descriptions_for_names(&md, &["ok".into(), "gone".into(), "missing".into()]);
13478        assert_eq!(named.len(), 3);
13479        assert_eq!(named[0].name, "ok");
13480        assert_eq!(named[0].error_code, 0);
13481        assert_eq!(named[1].name, "gone");
13482        assert_eq!(named[1].error_code, error::UNKNOWN_TOPIC_OR_PARTITION);
13483        assert_eq!(named[2].name, "missing");
13484        assert_eq!(named[2].error_code, error::UNKNOWN_TOPIC_OR_PARTITION);
13485    }
13486
13487    #[test]
13488    fn replica_log_dirs_group_and_map_current_future() {
13489        let replicas = [
13490            TopicPartitionReplica::new("t", 0, 2),
13491            TopicPartitionReplica::new("t", 1, 2),
13492            TopicPartitionReplica::new("u", 0, 1),
13493            TopicPartitionReplica::new("t", 0, 2),
13494        ];
13495        assert_eq!(replica_broker_ids(&replicas), vec![2, 1]);
13496        let on_two = describable_topics_for_broker(&replicas, 2);
13497        assert_eq!(on_two.len(), 1);
13498        assert_eq!(on_two[0].name, "t");
13499        assert_eq!(on_two[0].partitions, vec![0, 1]);
13500        let on_one = describable_topics_for_broker(&replicas, 1);
13501        assert_eq!(on_one.len(), 1);
13502        assert_eq!(on_one[0].name, "u");
13503        assert_eq!(on_one[0].partitions, vec![0]);
13504
13505        let dirs = vec![
13506            DescribeLogDirsResult::new(
13507                0,
13508                "/current",
13509                vec![DescribeLogDirsTopic::new(
13510                    "t",
13511                    vec![DescribeLogDirsPartition::new(0, 10, 3, false)],
13512                )],
13513                -1,
13514                -1,
13515            ),
13516            DescribeLogDirsResult::new(
13517                0,
13518                "/future",
13519                vec![DescribeLogDirsTopic::new(
13520                    "t",
13521                    vec![DescribeLogDirsPartition::new(0, 4, 7, true)],
13522                )],
13523                -1,
13524                -1,
13525            ),
13526            DescribeLogDirsResult::new(56, "/offline", Vec::new(), -1, -1),
13527        ];
13528        let replica = TopicPartitionReplica::new("t", 0, 2);
13529        let info = replica_log_dir_info_from(&replica, &dirs);
13530        assert_eq!(info.current_log_dir.as_deref(), Some("/current"));
13531        assert_eq!(info.current_offset_lag, 3);
13532        assert_eq!(info.future_log_dir.as_deref(), Some("/future"));
13533        assert_eq!(info.future_offset_lag, 7);
13534        let missing = replica_log_dir_info_from(&TopicPartitionReplica::new("t", 9, 2), &dirs);
13535        assert_eq!(missing, ReplicaLogDirInfo::unknown());
13536    }
13537
13538    #[test]
13539    fn list_offset_topic_requests_keeps_duplicate_partitions() {
13540        let cluster = Cluster::default();
13541        let queries = [
13542            (
13543                crate::TopicPartition::new("t", 0),
13544                crate::EARLIEST_TIMESTAMP,
13545            ),
13546            (crate::TopicPartition::new("t", 0), crate::LATEST_TIMESTAMP),
13547            (crate::TopicPartition::new("t", 1), crate::LATEST_TIMESTAMP),
13548        ];
13549        let topics = list_offset_topic_requests(&queries, &[0, 1, 2], &cluster);
13550        assert_eq!(topics.len(), 1);
13551        assert_eq!(topics[0].name, "t");
13552        assert_eq!(topics[0].partitions.len(), 3);
13553        assert_eq!(topics[0].partitions[0].timestamp, crate::EARLIEST_TIMESTAMP);
13554        assert_eq!(topics[0].partitions[1].timestamp, crate::LATEST_TIMESTAMP);
13555        assert_eq!(topics[0].partitions[2].partition, 1);
13556    }
13557}