1use std::{
35 collections::{BTreeMap, BTreeSet},
36 iter,
37};
38
39use futures_util::future::join_all;
40use krabka_client_core::{
41 AllTopicsOffsetFetch, AuthenticationError, ClientError, Connection, ConnectionOptions,
42 CoordinatorKeyType, MembersOnlyLeaveGroup, SaslAuthenticationError, TopicNameOffsetCommit,
43 build_find_coordinator, coordinator_endpoint,
44};
45use krabka_protocol::owned::{
46 alter_share_group_offsets_request::{
47 AlterShareGroupOffsetsRequest, AlterShareGroupOffsetsRequestPartition,
48 AlterShareGroupOffsetsRequestTopic,
49 },
50 alter_share_group_offsets_response::AlterShareGroupOffsetsResponse,
51 delete_share_group_offsets_request::{
52 DeleteShareGroupOffsetsRequest, DeleteShareGroupOffsetsRequestTopic,
53 },
54 delete_share_group_offsets_response::DeleteShareGroupOffsetsResponse,
55 leave_group_request::{LeaveGroupRequest, MemberIdentity},
56 leave_group_response::LeaveGroupResponse,
57 list_groups_request::{self, ListGroupsRequest},
58 list_groups_response::ListedGroup,
59 metadata_request::MetadataRequest,
60 metadata_response::MetadataResponseBroker,
61 offset_commit_request::{
62 OffsetCommitRequest, OffsetCommitRequestPartition, OffsetCommitRequestTopic,
63 },
64 offset_commit_response::{
65 OffsetCommitResponse, OffsetCommitResponsePartition, OffsetCommitResponseTopic,
66 },
67 offset_delete_request::{
68 OffsetDeleteRequest, OffsetDeleteRequestPartition, OffsetDeleteRequestTopic,
69 },
70 offset_delete_response::{
71 OffsetDeleteResponse, OffsetDeleteResponsePartition, OffsetDeleteResponseTopic,
72 },
73 offset_fetch_request::{OffsetFetchRequest, OffsetFetchRequestGroup},
74 offset_fetch_response::{
75 OffsetFetchResponse, OffsetFetchResponsePartition, OffsetFetchResponsePartitions,
76 OffsetFetchResponseTopic, OffsetFetchResponseTopics,
77 },
78};
79use tokio::time::{Instant, sleep, sleep_until, timeout_at};
80
81use crate::{
82 AdminClient, AdminError, KafkaError, format_host_port,
83 group_descriptions::DescribeGroupsOptions,
84 kafka_error_if, kafka_error_name,
85 retry::{CoordinatorRetry, RetryAction, RetryPolicy, connection_failure_action},
86 send_connection_at_least,
87};
88
89macro_rules! group_offset_attempt {
90 ($this:ident, $group:expr, $find_coordinator:expr, $request:expr, $handle:expr) => {{
91 if $find_coordinator && let Err(action) = $this.find_group_coordinator_attempt($group).await
92 {
93 return action;
94 }
95 match $this.conn.send($request).await {
96 Ok(response) => $handle($group, response),
97 Err(error) => connection_failure_action(error),
98 }
99 }};
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct ConsumerGroupOffsetOutcome {
104 pub topic: String,
105 pub partition: i32,
106 pub error: Option<KafkaError>,
107}
108
109struct GroupOffsetResponseTopic<P> {
110 name: String,
111 partitions: Vec<P>,
112}
113
114trait IntoGroupOffsetResponseTopic {
115 type Partition: GroupOffsetResponsePartition;
116
117 fn into_group_offset_topic(self) -> GroupOffsetResponseTopic<Self::Partition>;
118}
119
120trait GroupOffsetResponsePartition {
121 fn index(&self) -> i32;
122 fn error_code(&self) -> i16;
123}
124
125macro_rules! impl_group_offset_response {
126 ($topic:ty, $partition:ty) => {
127 impl IntoGroupOffsetResponseTopic for $topic {
128 type Partition = $partition;
129
130 fn into_group_offset_topic(self) -> GroupOffsetResponseTopic<Self::Partition> {
131 GroupOffsetResponseTopic {
132 name: self.name,
133 partitions: self.partitions,
134 }
135 }
136 }
137
138 impl GroupOffsetResponsePartition for $partition {
139 fn index(&self) -> i32 {
140 self.partition_index
141 }
142 fn error_code(&self) -> i16 {
143 self.error_code
144 }
145 }
146 };
147}
148
149impl_group_offset_response!(OffsetCommitResponseTopic, OffsetCommitResponsePartition);
150impl_group_offset_response!(OffsetDeleteResponseTopic, OffsetDeleteResponsePartition);
151
152fn group_offset_outcomes<T: IntoGroupOffsetResponseTopic>(
153 topics: impl IntoIterator<Item = T>,
154) -> Vec<ConsumerGroupOffsetOutcome> {
155 topics
156 .into_iter()
157 .flat_map(|topic| {
158 let GroupOffsetResponseTopic { name, partitions } = topic.into_group_offset_topic();
159 partitions
160 .into_iter()
161 .map(move |partition| ConsumerGroupOffsetOutcome {
162 topic: name.clone(),
163 partition: partition.index(),
164 error: kafka_error_if(partition.error_code(), None),
165 })
166 })
167 .collect()
168}
169
170pub type GroupOffsets = BTreeMap<(String, i32), i64>;
172
173#[derive(Clone, Copy)]
174enum GroupOffsetKind {
175 Consumer,
176 Share,
177}
178
179#[derive(Clone, Copy)]
180struct GroupOffsetUpdate<'a> {
181 kind: GroupOffsetKind,
182 group: &'a str,
183 offsets: &'a GroupOffsets,
184}
185
186impl<'a> GroupOffsetUpdate<'a> {
187 fn consumer(group: &'a str, offsets: &'a GroupOffsets) -> Self {
188 Self {
189 kind: GroupOffsetKind::Consumer,
190 group,
191 offsets,
192 }
193 }
194
195 fn share(group: &'a str, offsets: &'a GroupOffsets) -> Self {
196 Self {
197 kind: GroupOffsetKind::Share,
198 group,
199 offsets,
200 }
201 }
202}
203
204struct GroupOffsetAttempt<'a> {
205 update: GroupOffsetUpdate<'a>,
206 find_coordinator: bool,
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
213pub enum GroupState {
214 Unknown,
216 PreparingRebalance,
217 CompletingRebalance,
218 Stable,
219 Dead,
220 Empty,
221 Assigning,
222 Reconciling,
223 NotReady,
224}
225
226impl GroupState {
227 const ALL: [Self; 9] = [
228 Self::Unknown,
229 Self::PreparingRebalance,
230 Self::CompletingRebalance,
231 Self::Stable,
232 Self::Dead,
233 Self::Empty,
234 Self::Assigning,
235 Self::Reconciling,
236 Self::NotReady,
237 ];
238
239 #[must_use]
241 pub const fn as_str(self) -> &'static str {
242 match self {
243 Self::Unknown => "Unknown",
244 Self::PreparingRebalance => "PreparingRebalance",
245 Self::CompletingRebalance => "CompletingRebalance",
246 Self::Stable => "Stable",
247 Self::Dead => "Dead",
248 Self::Empty => "Empty",
249 Self::Assigning => "Assigning",
250 Self::Reconciling => "Reconciling",
251 Self::NotReady => "NotReady",
252 }
253 }
254}
255
256macro_rules! impl_group_name_parse {
257 ($type:ty, $($doc:literal),+ $(,)?) => {
258 impl $type {
259 $(#[doc = $doc])+
260 #[must_use]
261 pub fn parse(name: &str) -> Self {
262 parse_group_name(Self::ALL, name, Self::as_str, Self::Unknown)
263 }
264 }
265 };
266}
267impl_group_name_parse!(
268 GroupState,
269 "Reads a state name without regard to case. A name that the client does not know gives [`GroupState::Unknown`], as Kafka's `GroupState.parse` does."
270);
271
272macro_rules! impl_display_from_as_str {
273 ($type:ty) => {
274 impl std::fmt::Display for $type {
275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276 f.write_str(self.as_str())
277 }
278 }
279 };
280}
281impl_display_from_as_str!(GroupState);
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
287pub enum GroupType {
288 Unknown,
290 Consumer,
291 Classic,
292 Share,
293 Streams,
294}
295
296impl GroupType {
297 const ALL: [Self; 5] = [
298 Self::Unknown,
299 Self::Consumer,
300 Self::Classic,
301 Self::Share,
302 Self::Streams,
303 ];
304
305 #[must_use]
307 pub const fn as_str(self) -> &'static str {
308 match self {
309 Self::Unknown => "Unknown",
310 Self::Consumer => "Consumer",
311 Self::Classic => "Classic",
312 Self::Share => "Share",
313 Self::Streams => "Streams",
314 }
315 }
316}
317impl_group_name_parse!(
318 GroupType,
319 "Reads a type name without regard to case. A name that the client does not know gives [`GroupType::Unknown`], as Kafka's `GroupType.parse` does."
320);
321
322fn parse_group_name<T: Copy>(
323 values: impl IntoIterator<Item = T>,
324 name: &str,
325 as_str: fn(T) -> &'static str,
326 unknown: T,
327) -> T {
328 values
329 .into_iter()
330 .find(|value| as_str(*value).eq_ignore_ascii_case(name))
331 .unwrap_or(unknown)
332}
333
334impl_display_from_as_str!(GroupType);
335
336#[derive(Debug, Clone, Default, PartialEq, Eq, bon::Builder)]
339pub struct ListGroupsOptions {
340 #[builder(default)]
342 pub group_states: BTreeSet<GroupState>,
343 #[builder(default)]
345 pub types: BTreeSet<GroupType>,
346 #[builder(default)]
348 pub protocol_types: BTreeSet<String>,
349}
350
351impl ListGroupsOptions {
352 #[must_use]
355 pub fn for_consumer_groups() -> Self {
356 Self::builder()
357 .types(maplit::btreeset! {
358 GroupType::Classic,
359 GroupType::Consumer,
360 })
361 .protocol_types(maplit::btreeset! {
362 String::new(),
363 "consumer".to_owned(),
364 })
365 .build()
366 }
367
368 #[must_use]
370 pub fn for_share_groups() -> Self {
371 Self::builder()
372 .types(maplit::btreeset! {
373 GroupType::Share,
374 })
375 .build()
376 }
377
378 #[must_use]
381 pub fn for_streams_groups() -> Self {
382 Self::builder()
383 .types(maplit::btreeset! {
384 GroupType::Streams,
385 })
386 .build()
387 }
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct GroupListing {
394 pub group_id: String,
395 pub group_type: Option<GroupType>,
397 pub protocol_type: String,
400 pub group_state: Option<GroupState>,
402}
403
404impl GroupListing {
405 #[must_use]
415 pub fn is_simple_consumer_group(&self) -> bool {
416 self.group_type == Some(GroupType::Classic) && self.protocol_type.is_empty()
417 }
418}
419
420impl From<ListedGroup> for GroupListing {
421 fn from(group: ListedGroup) -> Self {
422 Self {
423 group_type: (!group.group_type.is_empty()).then(|| GroupType::parse(&group.group_type)),
424 group_state: (!group.group_state.is_empty())
425 .then(|| GroupState::parse(&group.group_state)),
426 group_id: group.group_id,
427 protocol_type: group.protocol_type,
428 }
429 }
430}
431
432#[derive(Debug, Clone, PartialEq, Eq)]
438pub struct ListGroupsError {
439 pub node_id: i32,
440 pub host: String,
441 pub port: i32,
442 pub error: KafkaError,
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
448pub struct ListGroupsResult {
449 pub valid: Vec<GroupListing>,
452 pub errors: Vec<ListGroupsError>,
454}
455
456impl ListGroupsResult {
457 pub fn all(self) -> Result<Vec<GroupListing>, ListGroupsError> {
463 match self.errors.into_iter().next() {
464 Some(error) => Err(error),
465 None => Ok(self.valid),
466 }
467 }
468}
469
470macro_rules! group_offsets_method_body {
471 (delete_consumer, $admin:ident, $group:ident, $input:ident) => {
472 $admin
473 .delete_consumer_group_offsets_with_retry($group, $input, $admin.retry)
474 .await
475 };
476 (delete_streams, $admin:ident, $group:ident, $input:ident) => {
477 $admin.delete_consumer_group_offsets($group, $input).await
478 };
479 (alter_consumer, $admin:ident, $group:ident, $input:ident) => {
480 $admin
481 .alter_group_offsets(GroupOffsetUpdate::consumer($group, $input))
482 .await
483 };
484 (alter_share, $admin:ident, $group:ident, $input:ident) => {
485 $admin
486 .alter_group_offsets(GroupOffsetUpdate::share($group, $input))
487 .await
488 };
489}
490
491macro_rules! group_offsets_method {
492 ($(#[$meta:meta])* $name:ident($input:ident: $input_type:ty) => $kind:ident) => {
493 $(#[$meta])*
494 pub async fn $name(
495 &mut self,
496 group: &str,
497 $input: &$input_type,
498 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
499 group_offsets_method_body!($kind, self, group, $input)
500 }
501 };
502}
503
504macro_rules! retry_group_call {
505 ($retry_policy:expr, |$find:ident| $attempt:expr) => {{
506 let mut retry = CoordinatorRetry::new($retry_policy);
507 loop {
508 let $find = retry.find_coordinator();
509 let action = retry.run($attempt).await;
510 if let Some(result) = retry.next(action).await {
511 return result;
512 }
513 }
514 }};
515}
516
517impl AdminClient {
518 group_offsets_method! {
519 alter_consumer_group_offsets(offsets: GroupOffsets) => alter_consumer
554 }
555
556 async fn alter_group_offsets(
557 &self,
558 update: GroupOffsetUpdate<'_>,
559 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
560 self.alter_group_offsets_with_retry(update, self.retry)
561 .await
562 }
563
564 async fn alter_group_offsets_with_retry(
565 &self,
566 update: GroupOffsetUpdate<'_>,
567 retry: RetryPolicy,
568 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
569 retry_group_call!(retry, |find_coordinator| {
570 self.alter_group_offsets_attempt(GroupOffsetAttempt {
571 update,
572 find_coordinator,
573 })
574 })
575 }
576
577 async fn alter_group_offsets_attempt(
579 &self,
580 attempt: GroupOffsetAttempt<'_>,
581 ) -> RetryAction<Vec<ConsumerGroupOffsetOutcome>> {
582 let GroupOffsetAttempt {
583 update,
584 find_coordinator,
585 } = attempt;
586 if find_coordinator
587 && let Err(action) = self.find_group_coordinator_attempt(update.group).await
588 {
589 return action;
590 }
591 match update.kind {
592 GroupOffsetKind::Consumer => self
593 .conn
594 .send(offset_commit_request(update.group, update.offsets))
595 .await
596 .map_or_else(connection_failure_action, offset_commit_retry_action),
597 GroupOffsetKind::Share => self
598 .conn
599 .send(alter_share_group_offsets_request(
600 update.group,
601 update.offsets,
602 ))
603 .await
604 .map_or_else(connection_failure_action, |response| {
605 alter_share_group_offsets_retry_action(update.offsets, response)
606 }),
607 }
608 }
609
610 pub async fn list_groups(
645 &self,
646 options: &ListGroupsOptions,
647 ) -> Result<ListGroupsResult, AdminError> {
648 self.list_groups_with_retry(options, self.retry).await
649 }
650
651 async fn list_groups_with_retry(
652 &self,
653 options: &ListGroupsOptions,
654 retry: RetryPolicy,
655 ) -> Result<ListGroupsResult, AdminError> {
656 let start = Instant::now();
657 let brokers = self.list_groups_brokers(start, retry).await?;
658 let request = ListGroupsRequest {
659 states_filter: options
660 .group_states
661 .iter()
662 .map(|state| state.as_str().to_owned())
663 .collect(),
664 types_filter: options
665 .types
666 .iter()
667 .map(|group_type| group_type.as_str().to_owned())
668 .collect(),
669 ..Default::default()
670 };
671 let min_version = list_groups_min_version(options);
672 let answers = join_all(brokers.iter().map(|broker| {
673 list_groups_on_broker(
674 broker,
675 self.options.clone(),
676 request.clone(),
677 min_version,
678 start,
679 retry,
680 )
681 }))
682 .await;
683
684 let mut listings = BTreeMap::new();
685 let mut errors = Vec::new();
686 for (broker, answer) in brokers.into_iter().zip(answers) {
687 match answer {
688 Ok(groups) => {
689 for group in groups {
690 if options.protocol_types.is_empty()
691 || options.protocol_types.contains(&group.protocol_type)
692 {
693 let listing = GroupListing::from(group);
694 listings.insert(listing.group_id.clone(), listing);
695 }
696 }
697 }
698 Err(error) => errors.push(ListGroupsError {
699 node_id: broker.node_id,
700 host: broker.host,
701 port: broker.port,
702 error,
703 }),
704 }
705 }
706 Ok(ListGroupsResult {
707 valid: listings.into_values().collect(),
708 errors,
709 })
710 }
711
712 async fn list_groups_brokers(
716 &self,
717 start: Instant,
718 retry: RetryPolicy,
719 ) -> Result<Vec<MetadataResponseBroker>, AdminError> {
720 let mut backoff = retry.initial_backoff;
721 loop {
722 let response = self
723 .conn
724 .send(MetadataRequest {
725 topics: Some(Vec::new()),
726 allow_auto_topic_creation: true,
727 ..Default::default()
728 })
729 .await?;
730 let mut brokers = response.brokers;
731 let mut seen = BTreeSet::new();
732 brokers.retain(|broker| seen.insert(broker.node_id));
733 if !brokers.is_empty() {
734 return Ok(brokers);
735 }
736 if start.elapsed() >= retry.timeout {
737 return Err(AdminError::Protocol(
738 "failed to find brokers to send ListGroups: metadata has no brokers".to_owned(),
739 ));
740 }
741 sleep(backoff).await;
742 backoff = backoff.saturating_mul(2).min(retry.max_backoff);
743 }
744 }
745
746 pub async fn list_consumer_group_offsets(
785 &mut self,
786 group: &str,
787 ) -> Result<GroupOffsets, AdminError> {
788 self.list_consumer_group_offsets_with_retry(group, self.retry)
789 .await
790 }
791
792 async fn list_consumer_group_offsets_with_retry(
793 &self,
794 group: &str,
795 retry: RetryPolicy,
796 ) -> Result<GroupOffsets, AdminError> {
797 retry_group_call!(retry, |find| self.offset_fetch_attempt(group, find))
798 }
799
800 async fn offset_fetch_attempt(
804 &self,
805 group: &str,
806 find_coordinator: bool,
807 ) -> RetryAction<GroupOffsets> {
808 let request = offset_fetch_request(group);
809 group_offset_attempt!(self, group, find_coordinator, request, |group, response| {
810 offset_fetch_retry_action(committed_offsets(group, response))
811 })
812 }
813
814 async fn find_group_coordinator_attempt<T>(&self, group: &str) -> Result<(), RetryAction<T>> {
821 match self.reconnect_group_coordinator(group).await {
822 Ok(()) => Ok(()),
823 Err(AdminError::Transport(ClientError::Server { error_code }))
824 if is_retriable_find_coordinator_error(error_code) =>
825 {
826 Err(RetryAction::FindCoordinator(Err(AdminError::Transport(
827 ClientError::Server { error_code },
828 ))))
829 }
830 Err(error) => Err(connection_failure_action(error)),
831 }
832 }
833
834 async fn reconnect_group_coordinator(&self, group: &str) -> Result<(), AdminError> {
835 let response = self
836 .conn
837 .send(build_find_coordinator(group, CoordinatorKeyType::Group))
838 .await?;
839 let coordinator = coordinator_endpoint(group, response)?;
840 self.reconnect(&format_host_port(&coordinator.host, coordinator.port))
841 .await
842 }
843
844 group_offsets_method! {
845 delete_consumer_group_offsets(partitions: [(String, i32)]) => delete_consumer
857 }
858
859 group_offsets_method! {
860 delete_streams_group_offsets(partitions: [(String, i32)]) => delete_streams
867 }
868
869 async fn delete_consumer_group_offsets_with_retry(
870 &self,
871 group: &str,
872 partitions: &[(String, i32)],
873 retry: RetryPolicy,
874 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
875 retry_group_call!(retry, |find| self
876 .offset_delete_attempt(group, partitions, find))
877 }
878
879 async fn offset_delete_attempt(
880 &self,
881 group: &str,
882 partitions: &[(String, i32)],
883 find_coordinator: bool,
884 ) -> RetryAction<Vec<ConsumerGroupOffsetOutcome>> {
885 let request = offset_delete_request(group, partitions);
886 group_offset_attempt!(self, group, find_coordinator, request, |group, response| {
887 offset_delete_retry_action(group, response)
888 })
889 }
890
891 pub async fn remove_members_from_consumer_group(
923 &mut self,
924 group: &str,
925 members: &[MemberToRemove],
926 options: &RemoveMembersOptions,
927 ) -> Result<RemovedMembers, AdminError> {
928 let reason = leave_group_reason(options.reason.as_deref());
929 let identities = if members.is_empty() {
930 self.all_members_of(group, &reason).await?
931 } else {
932 members
933 .iter()
934 .map(|member| MemberIdentity {
935 member_id: String::new(),
936 group_instance_id: Some(member.group_instance_id.clone()),
937 reason: Some(reason.clone()),
938 ..Default::default()
939 })
940 .collect()
941 };
942 self.remove_members_from_consumer_group_with_retry(group, &identities, self.retry)
943 .await
944 }
945
946 async fn all_members_of(
949 &self,
950 group: &str,
951 reason: &str,
952 ) -> Result<Vec<MemberIdentity>, AdminError> {
953 let description = self
954 .describe_consumer_groups(&[group], DescribeGroupsOptions::builder().build())
955 .await
956 .remove(group)
957 .unwrap_or_else(|| {
958 Err(KafkaError {
959 code: UNKNOWN_SERVER_ERROR,
960 name: kafka_error_name(UNKNOWN_SERVER_ERROR),
961 message: Some(format!("no description for group {group}")),
962 })
963 })
964 .map_err(|error| AdminError::Broker {
965 api: "describeConsumerGroups",
966 code: error.code,
967 name: error.name,
968 message: Some(format!(
969 "Encounter exception when trying to get members from group: {group}{}",
970 error
971 .message
972 .map(|message| format!(": {message}"))
973 .unwrap_or_default()
974 )),
975 })?;
976 Ok(description
977 .members
978 .into_iter()
979 .map(|member| {
980 let (member_id, group_instance_id) = match member.group_instance_id {
981 Some(instance_id) => (String::new(), Some(instance_id)),
982 None => (member.member_id, None),
983 };
984 MemberIdentity {
985 member_id,
986 group_instance_id,
987 reason: Some(reason.to_owned()),
988 ..Default::default()
989 }
990 })
991 .collect())
992 }
993
994 async fn remove_members_from_consumer_group_with_retry(
995 &self,
996 group: &str,
997 members: &[MemberIdentity],
998 retry: RetryPolicy,
999 ) -> Result<RemovedMembers, AdminError> {
1000 retry_group_call!(retry, |find| self.leave_group_attempt(group, members, find))
1001 }
1002
1003 async fn leave_group_attempt(
1004 &self,
1005 group: &str,
1006 members: &[MemberIdentity],
1007 find_coordinator: bool,
1008 ) -> RetryAction<RemovedMembers> {
1009 if find_coordinator && let Err(action) = self.find_group_coordinator_attempt(group).await {
1010 return action;
1011 }
1012 let request = MembersOnlyLeaveGroup::new(LeaveGroupRequest {
1013 group_id: group.into(),
1014 members: members.to_vec(),
1015 ..Default::default()
1016 });
1017 match self.conn.send(request).await {
1018 Ok(response) => leave_group_retry_action(response),
1019 Err(error) => connection_failure_action(error),
1020 }
1021 }
1022
1023 group_offsets_method! {
1024 alter_share_group_offsets(offsets: GroupOffsets) => alter_share
1041 }
1042
1043 pub async fn alter_streams_group_offsets(
1050 &mut self,
1051 group: &str,
1052 offsets: &GroupOffsets,
1053 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
1054 self.alter_consumer_group_offsets(group, offsets).await
1055 }
1056
1057 pub async fn list_streams_group_offsets(
1064 &mut self,
1065 group: &str,
1066 ) -> Result<GroupOffsets, AdminError> {
1067 self.list_consumer_group_offsets(group).await
1068 }
1069
1070 pub async fn delete_share_group_offsets(
1084 &mut self,
1085 group: &str,
1086 topics: &[String],
1087 ) -> Result<Vec<(String, Option<KafkaError>)>, AdminError> {
1088 self.delete_share_group_offsets_with_retry(group, topics, self.retry)
1089 .await
1090 }
1091
1092 async fn delete_share_group_offsets_with_retry(
1093 &self,
1094 group: &str,
1095 topics: &[String],
1096 retry: RetryPolicy,
1097 ) -> Result<Vec<(String, Option<KafkaError>)>, AdminError> {
1098 retry_group_call!(retry, |find| {
1099 self.delete_share_group_offsets_attempt(group, topics, find)
1100 })
1101 }
1102
1103 async fn delete_share_group_offsets_attempt(
1104 &self,
1105 group: &str,
1106 topics: &[String],
1107 find_coordinator: bool,
1108 ) -> RetryAction<Vec<(String, Option<KafkaError>)>> {
1109 if find_coordinator && let Err(action) = self.find_group_coordinator_attempt(group).await {
1110 return action;
1111 }
1112 let request = DeleteShareGroupOffsetsRequest {
1113 group_id: group.into(),
1114 topics: topics
1115 .iter()
1116 .map(|topic_name| delete_share_offset_topic(topic_name))
1117 .collect(),
1118 ..Default::default()
1119 };
1120 match self.conn.send(request).await {
1121 Ok(response) => delete_share_group_offsets_retry_action(response),
1122 Err(error) => connection_failure_action(error),
1123 }
1124 }
1125}
1126
1127fn delete_share_offset_topic(topic_name: &str) -> DeleteShareGroupOffsetsRequestTopic {
1128 DeleteShareGroupOffsetsRequestTopic {
1129 topic_name: topic_name.into(),
1130 ..Default::default()
1131 }
1132}
1133
1134const COORDINATOR_LOAD_IN_PROGRESS: i16 = 14;
1136const COORDINATOR_NOT_AVAILABLE: i16 = 15;
1138const NOT_COORDINATOR: i16 = 16;
1140
1141fn is_retriable_find_coordinator_error(code: i16) -> bool {
1144 matches!(
1145 code,
1146 COORDINATOR_LOAD_IN_PROGRESS | COORDINATOR_NOT_AVAILABLE
1147 )
1148}
1149
1150fn offset_fetch_retry_action<T>(result: Result<T, AdminError>) -> RetryAction<T> {
1155 match result {
1156 Err(AdminError::Broker {
1157 code: COORDINATOR_LOAD_IN_PROGRESS,
1158 ..
1159 }) => RetryAction::SameCoordinator(result),
1160 Err(AdminError::Broker {
1161 code: COORDINATOR_NOT_AVAILABLE | NOT_COORDINATOR,
1162 ..
1163 }) => RetryAction::FindCoordinator(result),
1164 result => RetryAction::Done(result),
1165 }
1166}
1167
1168const REBALANCE_IN_PROGRESS: i16 = 27;
1170
1171fn offset_commit_retry_action(
1178 response: OffsetCommitResponse,
1179) -> RetryAction<Vec<ConsumerGroupOffsetOutcome>> {
1180 let outcomes = group_offset_outcomes(response.topics);
1181 let codes = || {
1182 outcomes
1183 .iter()
1184 .filter_map(|outcome| outcome.error.as_ref().map(|error| error.code))
1185 };
1186 if codes().any(|code| matches!(code, COORDINATOR_NOT_AVAILABLE | NOT_COORDINATOR)) {
1187 RetryAction::FindCoordinator(Ok(outcomes))
1188 } else if codes()
1189 .any(|code| matches!(code, COORDINATOR_LOAD_IN_PROGRESS | REBALANCE_IN_PROGRESS))
1190 {
1191 RetryAction::SameCoordinator(Ok(outcomes))
1192 } else {
1193 completed_group_offsets(outcomes)
1194 }
1195}
1196
1197macro_rules! offset_request_topics {
1199 ($topics:expr, $topic_type:ident) => {
1200 $topics
1201 .into_iter()
1202 .map(|(name, partitions)| $topic_type {
1203 name,
1204 partitions,
1205 ..Default::default()
1206 })
1207 .collect()
1208 };
1209}
1210
1211fn offset_delete_request(group: &str, partitions: &[(String, i32)]) -> OffsetDeleteRequest {
1212 let mut topics = BTreeMap::<String, Vec<OffsetDeleteRequestPartition>>::new();
1213 for (topic, partition) in partitions {
1214 topics
1215 .entry(topic.clone())
1216 .or_default()
1217 .push(OffsetDeleteRequestPartition {
1218 partition_index: *partition,
1219 ..Default::default()
1220 });
1221 }
1222 OffsetDeleteRequest {
1223 group_id: group.into(),
1224 topics: offset_request_topics!(topics, OffsetDeleteRequestTopic),
1225 ..Default::default()
1226 }
1227}
1228
1229fn offset_delete_retry_action(
1235 group: &str,
1236 response: OffsetDeleteResponse,
1237) -> RetryAction<Vec<ConsumerGroupOffsetOutcome>> {
1238 if response.error_code != 0 {
1239 let result = Err(AdminError::Broker {
1240 api: "OffsetDelete",
1241 code: response.error_code,
1242 name: kafka_error_name(response.error_code),
1243 message: Some(format!("group={group}")),
1244 });
1245 return group_coordinator_error_action(response.error_code, result);
1246 }
1247 let outcomes = group_offset_outcomes(response.topics);
1248 completed_group_offsets(outcomes)
1249}
1250
1251fn leave_group_retry_action(response: LeaveGroupResponse) -> RetryAction<RemovedMembers> {
1262 if response.error_code != 0 {
1263 let result = Err(AdminError::Broker {
1264 api: "LeaveGroup",
1265 code: response.error_code,
1266 name: kafka_error_name(response.error_code),
1267 message: None,
1268 });
1269 return group_coordinator_error_action(response.error_code, result);
1270 }
1271 let members = response
1272 .members
1273 .into_iter()
1274 .map(|member| {
1275 (
1276 RemovedMember {
1277 member_id: member.member_id,
1278 group_instance_id: member.group_instance_id,
1279 },
1280 kafka_error_if(member.error_code, None),
1281 )
1282 })
1283 .collect();
1284 RetryAction::Done(Ok(RemovedMembers { members }))
1285}
1286
1287fn group_coordinator_error_action<T>(
1288 error_code: i16,
1289 result: Result<T, AdminError>,
1290) -> RetryAction<T> {
1291 match error_code {
1292 COORDINATOR_LOAD_IN_PROGRESS => RetryAction::SameCoordinator(result),
1293 COORDINATOR_NOT_AVAILABLE | NOT_COORDINATOR => RetryAction::FindCoordinator(result),
1294 _ => RetryAction::Done(result),
1295 }
1296}
1297
1298const UNKNOWN_SERVER_ERROR: i16 = -1;
1301const DEFAULT_LEAVE_GROUP_REASON: &str = "member was removed by an admin";
1303const MAX_REASON_CHARS: usize = 255;
1306
1307fn leave_group_reason(reason: Option<&str>) -> String {
1310 match reason {
1311 Some(reason) if !reason.is_empty() => reason.chars().take(MAX_REASON_CHARS).collect(),
1312 _ => DEFAULT_LEAVE_GROUP_REASON.to_owned(),
1313 }
1314}
1315
1316#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1319pub struct MemberToRemove {
1320 pub group_instance_id: String,
1321}
1322
1323#[derive(Debug, Clone, Default, PartialEq, Eq, bon::Builder)]
1326pub struct RemoveMembersOptions {
1327 #[builder(into)]
1330 pub reason: Option<String>,
1331}
1332
1333#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1336pub struct RemovedMember {
1337 pub member_id: String,
1338 pub group_instance_id: Option<String>,
1339}
1340
1341#[derive(Debug, Clone, Default, PartialEq, Eq)]
1344pub struct RemovedMembers {
1345 pub members: BTreeMap<RemovedMember, Option<KafkaError>>,
1347}
1348
1349impl RemovedMembers {
1350 pub fn member_result(&self, member: &MemberToRemove) -> Result<(), KafkaError> {
1358 let key = RemovedMember {
1359 member_id: String::new(),
1360 group_instance_id: Some(member.group_instance_id.clone()),
1361 };
1362 match self.members.get(&key) {
1363 Some(None) => Ok(()),
1364 Some(Some(error)) => Err(error.clone()),
1365 None => Err(KafkaError {
1366 code: UNKNOWN_SERVER_ERROR,
1367 name: kafka_error_name(UNKNOWN_SERVER_ERROR),
1368 message: Some(format!(
1369 "Member \"{}\" was not included in the removal response",
1370 member.group_instance_id
1371 )),
1372 }),
1373 }
1374 }
1375}
1376
1377fn alter_share_group_offsets_request(
1379 group: &str,
1380 offsets: &GroupOffsets,
1381) -> AlterShareGroupOffsetsRequest {
1382 let mut topics = BTreeMap::<String, Vec<AlterShareGroupOffsetsRequestPartition>>::new();
1383 for ((topic, partition), start_offset) in offsets {
1384 topics
1385 .entry(topic.clone())
1386 .or_default()
1387 .push(AlterShareGroupOffsetsRequestPartition {
1388 partition_index: *partition,
1389 start_offset: *start_offset,
1390 ..Default::default()
1391 });
1392 }
1393 AlterShareGroupOffsetsRequest {
1394 group_id: group.into(),
1395 topics: topics
1396 .into_iter()
1397 .map(
1398 |(topic_name, partitions)| AlterShareGroupOffsetsRequestTopic {
1399 topic_name,
1400 partitions,
1401 ..Default::default()
1402 },
1403 )
1404 .collect(),
1405 ..Default::default()
1406 }
1407}
1408
1409fn share_group_coordinator_error_action<T>(
1410 code: i16,
1411 error: AdminError,
1412 otherwise: impl FnOnce(AdminError) -> Result<T, AdminError>,
1413) -> RetryAction<T> {
1414 match code {
1415 COORDINATOR_LOAD_IN_PROGRESS | REBALANCE_IN_PROGRESS => {
1416 RetryAction::SameCoordinator(Err(error))
1417 }
1418 COORDINATOR_NOT_AVAILABLE | NOT_COORDINATOR => RetryAction::FindCoordinator(Err(error)),
1419 _ => RetryAction::Done(otherwise(error)),
1420 }
1421}
1422
1423fn completed_group_offsets<T>(outcomes: T) -> RetryAction<T> {
1424 RetryAction::Done(Ok(outcomes))
1425}
1426
1427fn map_group_topic_rows<T, Rows, P, O>(
1428 topics: impl IntoIterator<Item = T>,
1429 mut rows: impl FnMut(T) -> (String, Rows),
1430 mut outcome: impl FnMut(String, P) -> O,
1431) -> Vec<O>
1432where
1433 Rows: IntoIterator<Item = P>,
1434{
1435 let mut outcomes = Vec::new();
1436 for topic in topics {
1437 let (name, partitions) = rows(topic);
1438 for partition in partitions {
1439 outcomes.push(outcome(name.clone(), partition));
1440 }
1441 }
1442 outcomes
1443}
1444
1445fn alter_share_group_offsets_retry_action(
1453 offsets: &GroupOffsets,
1454 response: AlterShareGroupOffsetsResponse,
1455) -> RetryAction<Vec<ConsumerGroupOffsetOutcome>> {
1456 if response.error_code != 0 {
1457 let error = AdminError::Broker {
1458 api: "AlterShareGroupOffsets",
1459 code: response.error_code,
1460 name: kafka_error_name(response.error_code),
1461 message: response.error_message.clone(),
1462 };
1463 return share_group_coordinator_error_action(response.error_code, error, |_| {
1464 Ok(offsets
1465 .keys()
1466 .map(|(topic, partition)| ConsumerGroupOffsetOutcome {
1467 topic: topic.clone(),
1468 partition: *partition,
1469 error: kafka_error_if(response.error_code, response.error_message.clone()),
1470 })
1471 .collect())
1472 });
1473 }
1474 let outcomes = map_group_topic_rows(
1475 response.responses,
1476 |topic| (topic.topic_name, topic.partitions),
1477 |topic, partition| ConsumerGroupOffsetOutcome {
1478 topic,
1479 partition: partition.partition_index,
1480 error: kafka_error_if(partition.error_code, partition.error_message),
1481 },
1482 );
1483 completed_group_offsets(outcomes)
1484}
1485
1486fn delete_share_group_offsets_retry_action(
1493 response: DeleteShareGroupOffsetsResponse,
1494) -> RetryAction<Vec<(String, Option<KafkaError>)>> {
1495 if response.error_code != 0 {
1496 let error = AdminError::Broker {
1497 api: "DeleteShareGroupOffsets",
1498 code: response.error_code,
1499 name: kafka_error_name(response.error_code),
1500 message: response.error_message,
1501 };
1502 return share_group_coordinator_error_action(response.error_code, error, Err);
1503 }
1504 let outcomes = map_group_topic_rows(
1505 response.responses,
1506 |topic| {
1507 (
1508 topic.topic_name,
1509 iter::once((topic.error_code, topic.error_message)),
1510 )
1511 },
1512 |topic, (code, message)| (topic, kafka_error_if(code, message)),
1513 );
1514 completed_group_offsets(outcomes)
1515}
1516
1517fn list_groups_min_version(options: &ListGroupsOptions) -> i16 {
1525 let types_need_v5 = !options.types.is_empty()
1526 && (!options.types.contains(&GroupType::Classic)
1527 || options
1528 .types
1529 .iter()
1530 .any(|group_type| !matches!(group_type, GroupType::Classic | GroupType::Consumer)));
1531 if types_need_v5 {
1532 5
1533 } else if options.group_states.is_empty() {
1534 list_groups_request::MIN_VERSION
1535 } else {
1536 4
1537 }
1538}
1539
1540async fn list_groups_on_broker(
1554 broker: &MetadataResponseBroker,
1555 options: ConnectionOptions,
1556 request: ListGroupsRequest,
1557 min_version: i16,
1558 start: Instant,
1559 retry: RetryPolicy,
1560) -> Result<Vec<ListedGroup>, KafkaError> {
1561 let host_port = format_host_port(&broker.host, broker.port);
1562 let deadline = start + retry.timeout;
1563 let mut backoff = retry.initial_backoff;
1564 let mut connection = None;
1565 let mut attempts = 0_u32;
1566 let mut last = None;
1567 while Instant::now() < deadline {
1568 attempts += 1;
1569 let attempt = list_groups_attempt(
1570 &mut connection,
1571 &host_port,
1572 &options,
1573 request.clone(),
1574 min_version,
1575 );
1576 match timeout_at(deadline, attempt).await {
1577 Ok(RetryAction::Done(result)) => {
1578 return result.map_err(|error| list_groups_kafka_error(&error));
1579 }
1580 Ok(RetryAction::SameCoordinator(result) | RetryAction::FindCoordinator(result)) => {
1581 match result {
1582 Ok(groups) => return Ok(groups),
1583 Err(error) => last = Some(error),
1584 }
1585 }
1586 Err(_) => break,
1587 }
1588 if attempts > retry.max_retries {
1591 break;
1592 }
1593 tracing::debug!(
1594 node_id = broker.node_id,
1595 "ListGroups got a retriable error; retrying"
1596 );
1597 sleep_until(deadline.min(Instant::now() + backoff)).await;
1598 backoff = backoff.saturating_mul(2).min(retry.max_backoff);
1599 }
1600 Err(list_groups_timeout(attempts, last.as_ref()))
1601}
1602
1603fn list_groups_timeout(attempts: u32, last: Option<&AdminError>) -> KafkaError {
1608 const REQUEST_TIMED_OUT: i16 = 7;
1609 let cause = last
1610 .map(|error| format!("; last error: {error}"))
1611 .unwrap_or_default();
1612 KafkaError {
1613 code: REQUEST_TIMED_OUT,
1614 name: kafka_error_name(REQUEST_TIMED_OUT),
1615 message: Some(format!(
1616 "ListGroups timed out after {attempts} attempt(s){cause}"
1617 )),
1618 }
1619}
1620
1621async fn list_groups_attempt(
1624 connection: &mut Option<Connection>,
1625 host_port: &str,
1626 options: &ConnectionOptions,
1627 request: ListGroupsRequest,
1628 min_version: i16,
1629) -> RetryAction<Vec<ListedGroup>> {
1630 let current = match connection {
1631 Some(current) => current,
1632 None => match AdminClient::connect_one(host_port, options.clone()).await {
1633 Ok(new) => connection.insert(new),
1634 Err(error) if error.is_authentication_failure() => {
1635 return RetryAction::Done(Err(error));
1636 }
1637 Err(error) => return RetryAction::SameCoordinator(Err(error)),
1638 },
1639 };
1640 match send_connection_at_least(current, request, min_version).await {
1641 Ok(response) => match response.error_code {
1642 0 => RetryAction::Done(Ok(response.groups)),
1643 code => {
1644 let error = Err(AdminError::Broker {
1645 api: "ListGroups",
1646 code,
1647 name: kafka_error_name(code),
1648 message: None,
1649 });
1650 if matches!(
1651 code,
1652 COORDINATOR_LOAD_IN_PROGRESS | COORDINATOR_NOT_AVAILABLE
1653 ) {
1654 RetryAction::SameCoordinator(error)
1655 } else {
1656 RetryAction::Done(error)
1657 }
1658 }
1659 },
1660 Err(error) if AdminClient::is_retriable_transport_error(&error) => {
1661 *connection = None;
1662 RetryAction::SameCoordinator(Err(error.into()))
1663 }
1664 Err(error) => RetryAction::Done(Err(error.into())),
1665 }
1666}
1667
1668pub(crate) fn list_groups_kafka_error(error: &AdminError) -> KafkaError {
1676 const UNKNOWN_SERVER_ERROR: i16 = -1;
1677 const REQUEST_TIMED_OUT: i16 = 7;
1678 const NETWORK_EXCEPTION: i16 = 13;
1679 const UNSUPPORTED_SASL_MECHANISM: i16 = 33;
1680 const ILLEGAL_SASL_STATE: i16 = 34;
1681 const UNSUPPORTED_VERSION: i16 = 35;
1682 const INVALID_CONFIG: i16 = 40;
1683 const SASL_AUTHENTICATION_FAILED: i16 = 58;
1684 let (code, message) = match error {
1685 AdminError::Broker { code, message, .. } => (*code, message.clone()),
1686 AdminError::Transport(ClientError::IncompatibleVersion { .. }) => {
1687 (UNSUPPORTED_VERSION, Some(error.to_string()))
1688 }
1689 AdminError::Transport(ClientError::Timeout(_)) => {
1690 (REQUEST_TIMED_OUT, Some(error.to_string()))
1691 }
1692 AdminError::Transport(
1693 ClientError::Connect { .. } | ClientError::Disconnected | ClientError::Io(_),
1694 ) => (NETWORK_EXCEPTION, Some(error.to_string())),
1695 AdminError::Transport(ClientError::Authentication { source, .. }) => {
1696 let code = match source {
1697 AuthenticationError::Tls(_) => INVALID_CONFIG,
1698 AuthenticationError::Sasl(SaslAuthenticationError::UnsupportedMechanism(_)) => {
1699 UNSUPPORTED_SASL_MECHANISM
1700 }
1701 AuthenticationError::Sasl(SaslAuthenticationError::IllegalState(_)) => {
1702 ILLEGAL_SASL_STATE
1703 }
1704 AuthenticationError::Sasl(SaslAuthenticationError::Failed(_)) => {
1705 SASL_AUTHENTICATION_FAILED
1706 }
1707 };
1708 (code, Some(error.to_string()))
1709 }
1710 _ => (UNKNOWN_SERVER_ERROR, Some(error.to_string())),
1711 };
1712 KafkaError {
1713 code,
1714 name: kafka_error_name(code),
1715 message,
1716 }
1717}
1718
1719fn offset_fetch_group(group: &str) -> OffsetFetchRequestGroup {
1726 OffsetFetchRequestGroup {
1727 group_id: group.into(),
1728 member_id: None,
1729 member_epoch: -1,
1730 topics: None,
1731 ..Default::default()
1732 }
1733}
1734
1735fn offset_fetch_request(group: &str) -> AllTopicsOffsetFetch {
1736 AllTopicsOffsetFetch::new(OffsetFetchRequest {
1737 group_id: group.into(),
1738 topics: None,
1739 groups: vec![offset_fetch_group(group)],
1740 require_stable: false,
1741 ..Default::default()
1742 })
1743}
1744
1745fn committed_offsets(
1753 group: &str,
1754 response: OffsetFetchResponse,
1755) -> Result<GroupOffsets, AdminError> {
1756 let (error_code, rows): (i16, Vec<(String, i32, i64, i16)>) = if response.groups.is_empty() {
1757 (
1758 response.error_code,
1759 response
1760 .topics
1761 .into_iter()
1762 .flat_map(offset_fetch_topic_rows)
1763 .collect(),
1764 )
1765 } else {
1766 let mut error_code = 0;
1767 let mut rows = Vec::new();
1768 for entry in response
1769 .groups
1770 .into_iter()
1771 .filter(|entry| entry.group_id == group)
1772 {
1773 error_code = entry.error_code;
1774 for topic in entry.topics {
1775 rows.extend(offset_fetch_topic_rows(topic));
1776 }
1777 }
1778 (error_code, rows)
1779 };
1780 if error_code != 0 {
1781 return Err(AdminError::Broker {
1782 api: "OffsetFetch",
1783 code: error_code,
1784 name: kafka_error_name(error_code),
1785 message: Some(format!("group={group}")),
1786 });
1787 }
1788 Ok(rows
1789 .into_iter()
1790 .filter_map(|(topic, partition, offset, partition_error)| {
1791 if partition_error != 0 {
1792 tracing::warn!(
1793 topic,
1794 partition,
1795 error_code = partition_error,
1796 "skipping the committed offset of a partition with an error"
1797 );
1798 None
1799 } else if offset < 0 {
1800 None
1801 } else {
1802 Some(((topic, partition), offset))
1803 }
1804 })
1805 .collect())
1806}
1807
1808trait IntoOffsetFetchRow {
1809 fn into_offset_fetch_row(self, topic: &str) -> (String, i32, i64, i16);
1810}
1811
1812macro_rules! impl_offset_fetch_row {
1813 ($partition:ty) => {
1814 impl IntoOffsetFetchRow for $partition {
1815 fn into_offset_fetch_row(self, topic: &str) -> (String, i32, i64, i16) {
1816 (
1817 topic.to_owned(),
1818 self.partition_index,
1819 self.committed_offset,
1820 self.error_code,
1821 )
1822 }
1823 }
1824 };
1825}
1826
1827impl_offset_fetch_row!(OffsetFetchResponsePartition);
1828impl_offset_fetch_row!(OffsetFetchResponsePartitions);
1829
1830trait IntoOffsetFetchTopics {
1831 type Partition: IntoOffsetFetchRow;
1832
1833 fn into_offset_fetch_parts(self) -> (String, Vec<Self::Partition>);
1834}
1835
1836macro_rules! impl_offset_fetch_topics {
1837 ($topics:ty, $partition:ty) => {
1838 impl IntoOffsetFetchTopics for $topics {
1839 type Partition = $partition;
1840
1841 fn into_offset_fetch_parts(self) -> (String, Vec<Self::Partition>) {
1842 (self.name, self.partitions)
1843 }
1844 }
1845 };
1846}
1847
1848impl_offset_fetch_topics!(OffsetFetchResponseTopic, OffsetFetchResponsePartition);
1849impl_offset_fetch_topics!(OffsetFetchResponseTopics, OffsetFetchResponsePartitions);
1850
1851fn offset_fetch_topic_rows<T: IntoOffsetFetchTopics>(topic: T) -> Vec<(String, i32, i64, i16)> {
1852 let (name, partitions) = topic.into_offset_fetch_parts();
1853 partitions
1854 .into_iter()
1855 .map(|partition| partition.into_offset_fetch_row(&name))
1856 .collect()
1857}
1858
1859fn offset_commit_request(group: &str, offsets: &GroupOffsets) -> TopicNameOffsetCommit {
1860 let mut topics = BTreeMap::<String, Vec<OffsetCommitRequestPartition>>::new();
1861 for ((topic, partition), offset) in offsets {
1862 topics
1863 .entry(topic.clone())
1864 .or_default()
1865 .push(OffsetCommitRequestPartition {
1866 partition_index: *partition,
1867 committed_offset: *offset,
1868 committed_leader_epoch: -1,
1869 ..Default::default()
1872 });
1873 }
1874 TopicNameOffsetCommit::new(OffsetCommitRequest {
1875 group_id: group.into(),
1876 generation_id_or_member_epoch: -1,
1877 member_id: String::new(),
1878 topics: offset_request_topics!(topics, OffsetCommitRequestTopic),
1879 ..Default::default()
1880 })
1881}
1882
1883#[cfg(test)]
1884mod tests {
1885 use std::{
1886 fmt::Debug,
1887 net::SocketAddr,
1888 sync::{
1889 Arc, Mutex,
1890 atomic::{AtomicUsize, Ordering},
1891 },
1892 time::Duration,
1893 };
1894
1895 use assert2::assert;
1896 use bytes::{Buf, BytesMut};
1897 use krabka_client_core::{
1898 AuthenticationError, ClientError, MockBroker, MockReply, MockSaslAnswer,
1899 SaslAuthenticationError,
1900 mock_test_support::offset_commit_response,
1901 security::{ClientSecurity, SaslCredentials},
1902 };
1903 use krabka_protocol::{
1904 Decode, Encode, ProtocolRequest,
1905 owned::{
1906 alter_share_group_offsets_response::{
1907 AlterShareGroupOffsetsResponsePartition, AlterShareGroupOffsetsResponseTopic,
1908 },
1909 api_versions_request,
1910 api_versions_response::{ApiVersion, ApiVersionsResponse},
1911 delete_share_group_offsets_response::DeleteShareGroupOffsetsResponseTopic,
1912 find_coordinator_request,
1913 find_coordinator_response::FindCoordinatorResponse,
1914 leave_group_response::MemberResponse,
1915 list_groups_response::ListGroupsResponse,
1916 metadata_request,
1917 metadata_response::MetadataResponse,
1918 offset_commit_request,
1919 offset_commit_response::{
1920 OffsetCommitResponse, OffsetCommitResponsePartition, OffsetCommitResponseTopic,
1921 },
1922 offset_delete_response::{OffsetDeleteResponsePartition, OffsetDeleteResponseTopic},
1923 offset_fetch_request,
1924 offset_fetch_response::{
1925 OffsetFetchResponseGroup, OffsetFetchResponsePartition,
1926 OffsetFetchResponsePartitions, OffsetFetchResponseTopic, OffsetFetchResponseTopics,
1927 },
1928 sasl_handshake_request,
1929 },
1930 };
1931 use tokio::{net::TcpListener, runtime, task::JoinHandle, time::timeout};
1932
1933 use super::*;
1934
1935 fn test_group_offsets() -> GroupOffsets {
1936 maplit::btreemap! { ("orders".into(), 2) => 41 }
1937 }
1938
1939 #[test]
1940 fn offset_reset_builds_admin_commit() {
1941 let offsets = test_group_offsets();
1942 let request = offset_commit_request("worker", &offsets);
1943 let expected = TopicNameOffsetCommit::new(commit_request_for_test("worker", -1, -1));
1944 assert!(request == expected);
1945 }
1946
1947 fn commit_request_for_test(
1948 group_id: &str,
1949 generation_id_or_member_epoch: i32,
1950 committed_leader_epoch: i32,
1951 ) -> OffsetCommitRequest {
1952 OffsetCommitRequest {
1953 group_id: group_id.into(),
1954 generation_id_or_member_epoch,
1955 topics: vec![OffsetCommitRequestTopic {
1956 name: "orders".into(),
1957 partitions: vec![OffsetCommitRequestPartition {
1958 partition_index: 2,
1959 committed_offset: 41,
1960 committed_leader_epoch,
1961 ..Default::default()
1962 }],
1963 ..Default::default()
1964 }],
1965 ..Default::default()
1966 }
1967 }
1968
1969 fn encode(response: &impl Encode, version: i16, flexible: bool) -> Vec<u8> {
1970 let mut bytes = BytesMut::new();
1971 if flexible {
1972 bytes.extend_from_slice(&[0]);
1973 }
1974 response.encode(&mut bytes, version).unwrap();
1975 bytes.to_vec()
1976 }
1977
1978 fn encode_flexible(response: &impl Encode, version: i16) -> Vec<u8> {
1979 encode(response, version, true)
1980 }
1981
1982 fn broker_port(addr: SocketAddr) -> i32 {
1983 i32::from(addr.port())
1984 }
1985
1986 struct MockBrokerAddress {
1987 node_id: i32,
1988 host: String,
1989 port: i32,
1990 }
1991
1992 fn mock_broker_address(addr: SocketAddr, node_id: i32) -> MockBrokerAddress {
1993 MockBrokerAddress {
1994 node_id,
1995 host: addr.ip().to_string(),
1996 port: broker_port(addr),
1997 }
1998 }
1999
2000 macro_rules! broker_fields {
2001 ($type:ident, $broker:expr) => {
2002 $type {
2003 node_id: $broker.node_id,
2004 host: $broker.host,
2005 port: $broker.port,
2006 ..Default::default()
2007 }
2008 };
2009 }
2010
2011 fn mock_metadata_broker(addr: SocketAddr, node_id: i32) -> MetadataResponseBroker {
2012 let broker = mock_broker_address(addr, node_id);
2013 broker_fields!(MetadataResponseBroker, broker)
2014 }
2015
2016 async fn connect_admin(addr: SocketAddr, message: &'static str) -> AdminClient {
2017 AdminClient::connect(&[addr.to_string()])
2018 .await
2019 .expect(message)
2020 }
2021
2022 fn mock_coordinator_response(addr: SocketAddr, error_code: i16) -> FindCoordinatorResponse {
2023 let broker = mock_broker_address(addr, 2);
2024 let mut response = broker_fields!(FindCoordinatorResponse, broker);
2025 response.error_code = error_code;
2026 response
2027 }
2028
2029 fn incompatible_version(error: AdminError, case: &str) -> (i16, i16, i16, i16, i16) {
2030 match error {
2031 AdminError::Transport(ClientError::IncompatibleVersion {
2032 api_key,
2033 broker_min,
2034 broker_max,
2035 client_min,
2036 client_max,
2037 }) => (api_key, broker_min, broker_max, client_min, client_max),
2038 other => panic!("case {case}: unexpected error {other:?}"),
2039 }
2040 }
2041
2042 async fn with_incompatible_version<T>(
2043 case: &'static str,
2044 result: impl Future<Output = Result<T, AdminError>>,
2045 ) -> Result<T, (i16, i16, i16, i16, i16)> {
2046 result
2047 .await
2048 .map_err(|error| incompatible_version(error, case))
2049 }
2050
2051 #[derive(Clone, Copy)]
2054 struct GroupRanges {
2055 offset_commit: (i16, i16),
2056 offset_fetch: (i16, i16),
2057 }
2058
2059 impl GroupRanges {
2060 fn offset_commit(offset_commit: (i16, i16)) -> Self {
2061 Self {
2062 offset_commit,
2063 offset_fetch: (2, 10),
2064 }
2065 }
2066
2067 fn offset_fetch(offset_fetch: (i16, i16)) -> Self {
2068 Self {
2069 offset_commit: (2, 9),
2070 offset_fetch,
2071 }
2072 }
2073 }
2074
2075 fn encode_api_versions(api_keys: Vec<ApiVersion>) -> Vec<u8> {
2076 encode(
2077 &ApiVersionsResponse {
2078 api_keys,
2079 ..Default::default()
2080 },
2081 0,
2082 false,
2083 )
2084 }
2085
2086 macro_rules! api_version_entry {
2087 ($key:expr, $min:expr, $max:expr) => {
2088 ApiVersion {
2089 api_key: $key,
2090 min_version: $min,
2091 max_version: $max,
2092 ..Default::default()
2093 }
2094 };
2095 }
2096
2097 fn api_versions(ranges: GroupRanges) -> Vec<u8> {
2099 encode_api_versions(vec![
2100 api_version_entry!(api_versions_request::API_KEY, 0, 0),
2101 api_version_entry!(find_coordinator_request::API_KEY, 0, 0),
2102 api_version_entry!(
2103 offset_commit_request::API_KEY,
2104 ranges.offset_commit.0,
2105 ranges.offset_commit.1
2106 ),
2107 api_version_entry!(
2108 offset_fetch_request::API_KEY,
2109 ranges.offset_fetch.0,
2110 ranges.offset_fetch.1
2111 ),
2112 api_version_entry!(metadata_request::API_KEY, 13, 13),
2113 ])
2114 }
2115
2116 fn decode_request<R: ProtocolRequest + for<'de> Decode<'de>>(
2118 mut body: &[u8],
2119 version: i16,
2120 ) -> R {
2121 let client_id_len = body.get_i16();
2122 body.advance(usize::try_from(client_id_len).expect("client id length"));
2123 if version >= R::FLEXIBLE_MIN {
2124 body.advance(1);
2125 }
2126 R::decode(&mut body, version).expect("request decodes")
2127 }
2128
2129 async fn bootstrap_for(
2131 coordinator: &MockBroker,
2132 ranges: GroupRanges,
2133 group_rpcs: Arc<AtomicUsize>,
2134 ) -> MockBroker {
2135 let coordinator_addr = coordinator.addr;
2136 MockBroker::start(move |api_key, version, _, _| match api_key {
2137 api_versions_request::API_KEY => Some(api_versions(ranges)),
2138 find_coordinator_request::API_KEY => Some(encode(
2139 &mock_coordinator_response(coordinator_addr, 0),
2140 version,
2141 false,
2142 )),
2143 offset_commit_request::API_KEY => {
2144 group_rpcs.fetch_add(1, Ordering::SeqCst);
2145 Some(encode(&OffsetCommitResponse::default(), version, true))
2146 }
2147 offset_fetch_request::API_KEY => {
2148 group_rpcs.fetch_add(1, Ordering::SeqCst);
2149 Some(encode(&OffsetFetchResponse::default(), version, true))
2150 }
2151 metadata_request::API_KEY => Some(empty_metadata_reply(version)),
2152 _ => None,
2153 })
2154 .await
2155 .unwrap()
2156 }
2157
2158 struct GroupAdminConnection {
2159 bootstrap: MockBroker,
2160 admin: AdminClient,
2161 }
2162
2163 async fn connect_group_admin(
2164 coordinator: &MockBroker,
2165 ranges: GroupRanges,
2166 ) -> GroupAdminConnection {
2167 let bootstrap = bootstrap_for(coordinator, ranges, Arc::default()).await;
2168 let admin = connect_admin(bootstrap.addr, "admin connects").await;
2169 GroupAdminConnection { bootstrap, admin }
2170 }
2171
2172 fn stop_group_brokers(bootstrap: MockBroker, coordinator: MockBroker) {
2173 bootstrap.stop();
2174 coordinator.stop();
2175 }
2176
2177 fn record_group_request<Request>(
2178 requests: &Arc<Mutex<Vec<(i16, Request)>>>,
2179 version: i16,
2180 request: Request,
2181 ) {
2182 requests
2183 .lock()
2184 .expect("requests lock")
2185 .push((version, request));
2186 }
2187
2188 struct GroupVersionAssertion<Request, Output> {
2189 name: &'static str,
2190 requests: Vec<(i16, Request)>,
2191 result: Output,
2192 expected_requests: Vec<(i16, Request)>,
2193 expected_result: Output,
2194 }
2195
2196 fn assert_group_version_case<Request, Output>(case: GroupVersionAssertion<Request, Output>)
2197 where
2198 Request: Debug + PartialEq,
2199 Output: Debug + PartialEq,
2200 {
2201 assert!(
2202 (case.requests, case.result) == (case.expected_requests, case.expected_result),
2203 "case {}",
2204 case.name
2205 );
2206 }
2207
2208 struct ExpectedGroupVersionCase<Request, Output> {
2209 name: &'static str,
2210 ranges: GroupRanges,
2211 requests: Vec<(i16, Request)>,
2212 result: Output,
2213 }
2214
2215 fn expected_group_version_case<Request, Output>(
2216 name: &'static str,
2217 ranges: GroupRanges,
2218 requests: Vec<(i16, Request)>,
2219 result: Output,
2220 ) -> ExpectedGroupVersionCase<Request, Output> {
2221 ExpectedGroupVersionCase {
2222 name,
2223 ranges,
2224 requests,
2225 result,
2226 }
2227 }
2228
2229 fn incompatible_group_version<T>(
2230 api_key: i16,
2231 broker_min: i16,
2232 broker_max: i16,
2233 ) -> Result<T, (i16, i16, i16, i16, i16)> {
2234 Err((api_key, broker_min, broker_max, 2, 9))
2235 }
2236
2237 async fn run_group_version_case<Request, Output, Respond, Action, Fut>(
2238 case: ExpectedGroupVersionCase<Request, Output>,
2239 api_key: i16,
2240 respond: Respond,
2241 action: Action,
2242 ) where
2243 Request:
2244 ProtocolRequest + for<'de> Decode<'de> + Clone + PartialEq + Debug + Send + 'static,
2245 Output: PartialEq + Debug,
2246 Respond: Fn(&Request, i16) -> Vec<u8> + Copy + Send + Sync + 'static,
2247 Action: FnOnce(&'static str, AdminClient) -> Fut,
2248 Fut: Future<Output = Output>,
2249 {
2250 let ranges = case.ranges;
2251 let requests = Arc::new(Mutex::new(Vec::new()));
2252 let recorded = Arc::clone(&requests);
2253 let metadata_requests = Arc::new(AtomicUsize::new(0));
2254 let metadata_count = Arc::clone(&metadata_requests);
2255 let coordinator =
2256 MockBroker::start(move |request_api, version, _, body| match request_api {
2257 api_versions_request::API_KEY => Some(api_versions(ranges)),
2258 request_api if request_api == api_key => {
2259 let request = decode_request(body, version);
2260 let response = respond(&request, version);
2261 record_group_request(&recorded, version, request);
2262 Some(response)
2263 }
2264 metadata_request::API_KEY => {
2265 counted_reply(&metadata_count, Some(empty_metadata_reply(version)))
2266 }
2267 _ => None,
2268 })
2269 .await
2270 .unwrap();
2271 let GroupAdminConnection { bootstrap, admin } =
2272 connect_group_admin(&coordinator, ranges).await;
2273 let result = action(case.name, admin).await;
2274 stop_group_brokers(bootstrap, coordinator);
2275 let requests = requests.lock().expect("requests lock").clone();
2276 assert!(
2277 metadata_requests.load(Ordering::SeqCst) == 0,
2278 "case {}",
2279 case.name
2280 );
2281 assert_group_version_case(GroupVersionAssertion {
2282 name: case.name,
2283 requests,
2284 result,
2285 expected_requests: case.requests,
2286 expected_result: case.result,
2287 });
2288 }
2289
2290 type CommitResult = Result<Vec<ConsumerGroupOffsetOutcome>, (i16, i16, i16, i16, i16)>;
2293
2294 fn commit_group_version_cases()
2298 -> Vec<ExpectedGroupVersionCase<OffsetCommitRequest, CommitResult>> {
2299 let committed = Ok(successful_group_offset_outcome());
2300 let commit_case = |name, version_range, requests, result| {
2301 expected_group_version_case(
2302 name,
2303 GroupRanges::offset_commit(version_range),
2304 requests,
2305 result,
2306 )
2307 };
2308 let sent_request = |version| vec![(version, commit_request_for_test("workers", -1, -1))];
2309 vec![
2310 commit_case(
2311 "coordinator stops at v7",
2312 (2, 7),
2313 sent_request(7),
2314 committed.clone(),
2315 ),
2316 commit_case(
2317 "coordinator stops at v9",
2318 (2, 9),
2319 sent_request(9),
2320 committed.clone(),
2321 ),
2322 commit_case(
2323 "coordinator supports v10",
2324 (2, 10),
2325 sent_request(9),
2326 committed,
2327 ),
2328 commit_case(
2329 "coordinator supports only v10",
2330 (10, 10),
2331 Vec::new(),
2332 incompatible_group_version(offset_commit_request::API_KEY, 10, 10),
2333 ),
2334 ]
2335 }
2336
2337 #[yare::parameterized(
2338 coordinator_stops_at_v7 = { 0 },
2339 coordinator_stops_at_v9 = { 1 },
2340 coordinator_supports_v10 = { 2 },
2341 coordinator_only_supports_v10 = { 3 },
2342 )]
2343 fn alter_consumer_group_offsets_sends_offset_commit_by_topic_name(index: usize) {
2344 let case = commit_group_version_cases().remove(index);
2345 block_on_test(run_group_version_case(
2346 case,
2347 offset_commit_request::API_KEY,
2348 |request, version| {
2349 encode(
2350 &offset_commit_response(request, |_| 0),
2351 version,
2352 version >= offset_commit_request::FLEXIBLE_MIN,
2353 )
2354 },
2355 commit_group_offsets_by_topic,
2356 ));
2357 }
2358
2359 async fn commit_group_offsets_by_topic(
2360 name: &'static str,
2361 mut admin: AdminClient,
2362 ) -> CommitResult {
2363 let offsets = test_group_offsets();
2364 Box::pin(with_incompatible_version(
2365 name,
2366 admin.alter_consumer_group_offsets("workers", &offsets),
2367 ))
2368 .await
2369 }
2370
2371 macro_rules! offset_fetch_topics {
2372 ($topic:ident, $partition:ident, $rows:expr) => {
2373 ($rows)
2374 .iter()
2375 .map(
2376 |(name, partition_index, committed_offset, error_code)| $topic {
2377 name: (*name).into(),
2378 partitions: vec![$partition {
2379 partition_index: *partition_index,
2380 committed_offset: *committed_offset,
2381 error_code: *error_code,
2382 ..Default::default()
2383 }],
2384 ..Default::default()
2385 },
2386 )
2387 .collect()
2388 };
2389 }
2390
2391 fn offset_fetch_response(version: i16) -> OffsetFetchResponse {
2395 let rows = [
2396 ("orders", 2, 41, 0),
2397 ("orders", 3, -1, 0),
2398 ("payments", 0, 5, 3),
2399 ];
2400 if version < 8 {
2401 OffsetFetchResponse {
2402 topics: offset_fetch_topics!(
2403 OffsetFetchResponseTopic,
2404 OffsetFetchResponsePartition,
2405 &rows
2406 ),
2407 ..Default::default()
2408 }
2409 } else {
2410 OffsetFetchResponse {
2411 groups: vec![OffsetFetchResponseGroup {
2412 group_id: "workers".into(),
2413 topics: offset_fetch_topics!(
2414 OffsetFetchResponseTopics,
2415 OffsetFetchResponsePartitions,
2416 &rows
2417 ),
2418 ..Default::default()
2419 }],
2420 ..Default::default()
2421 }
2422 }
2423 }
2424
2425 type FetchResult = Result<GroupOffsets, (i16, i16, i16, i16, i16)>;
2428
2429 fn fetch_group_version_cases() -> Vec<ExpectedGroupVersionCase<OffsetFetchRequest, FetchResult>>
2434 {
2435 let fetched = Ok(test_group_offsets());
2436 let case = |name, ranges, requests, result| {
2437 expected_group_version_case(name, GroupRanges::offset_fetch(ranges), requests, result)
2438 };
2439 let legacy_request = |version| {
2440 vec![(
2441 version,
2442 OffsetFetchRequest {
2443 group_id: "workers".into(),
2444 topics: None,
2445 ..Default::default()
2446 },
2447 )]
2448 };
2449 let grouped_request = |version| {
2450 vec![(
2451 version,
2452 OffsetFetchRequest {
2453 groups: vec![offset_fetch_group("workers")],
2454 ..Default::default()
2455 },
2456 )]
2457 };
2458 vec![
2459 case(
2460 "coordinator stops at v7",
2461 (2, 7),
2462 legacy_request(7),
2463 fetched.clone(),
2464 ),
2465 case(
2466 "coordinator stops at v8",
2467 (2, 8),
2468 grouped_request(8),
2469 fetched.clone(),
2470 ),
2471 case(
2472 "coordinator stops at v9",
2473 (2, 9),
2474 grouped_request(9),
2475 fetched.clone(),
2476 ),
2477 case(
2478 "coordinator supports v10",
2479 (2, 10),
2480 grouped_request(9),
2481 fetched,
2482 ),
2483 case(
2484 "coordinator supports only v10",
2485 (10, 10),
2486 Vec::new(),
2487 incompatible_group_version(offset_fetch_request::API_KEY, 10, 10),
2488 ),
2489 case(
2490 "coordinator supports only v1",
2491 (1, 1),
2492 Vec::new(),
2493 incompatible_group_version(offset_fetch_request::API_KEY, 1, 1),
2494 ),
2495 ]
2496 }
2497
2498 async fn fetch_group_offsets_by_topic(
2499 name: &'static str,
2500 mut admin: AdminClient,
2501 ) -> FetchResult {
2502 let offsets = admin.list_consumer_group_offsets("workers");
2503 Box::pin(with_incompatible_version(name, offsets)).await
2504 }
2505
2506 #[yare::parameterized(
2507 coordinator_stops_at_v7 = { 0 },
2508 coordinator_stops_at_v8 = { 1 },
2509 coordinator_stops_at_v9 = { 2 },
2510 coordinator_supports_v10 = { 3 },
2511 coordinator_only_supports_v10 = { 4 },
2512 coordinator_only_supports_v1 = { 5 },
2513 )]
2514 fn list_consumer_group_offsets_sends_offset_fetch_by_topic_name(index: usize) {
2515 let case = fetch_group_version_cases().remove(index);
2516 block_on_test(run_group_version_case(
2517 case,
2518 offset_fetch_request::API_KEY,
2519 |_, version| {
2520 encode(
2521 &offset_fetch_response(version),
2522 version,
2523 version >= offset_fetch_request::FLEXIBLE_MIN,
2524 )
2525 },
2526 fetch_group_offsets_by_topic,
2527 ));
2528 }
2529
2530 type OffsetsResult = Result<GroupOffsets, (&'static str, i16, &'static str, Option<String>)>;
2532
2533 #[yare::parameterized(
2536 v2_to_v7_shape = { offset_fetch_response(7), Ok(test_group_offsets()) },
2537 v8_and_v9_shape = { offset_fetch_response(9), Ok(test_group_offsets()) },
2538 v2_to_v7_group_error = { OffsetFetchResponse { error_code: 15, ..Default::default() }, Err(("OffsetFetch", 15, "COORDINATOR_NOT_AVAILABLE", Some("group=workers".into()))) },
2539 v8_and_v9_group_error = { OffsetFetchResponse { groups: vec![OffsetFetchResponseGroup { group_id: "workers".into(), error_code: 16, ..Default::default() }], ..Default::default() }, Err(("OffsetFetch", 16, "NOT_COORDINATOR", Some("group=workers".into()))) },
2540 )]
2541 fn committed_offsets_reads_both_response_shapes(
2542 response: OffsetFetchResponse,
2543 expected: OffsetsResult,
2544 ) {
2545 let result: OffsetsResult =
2546 committed_offsets("workers", response).map_err(|error| match error {
2547 AdminError::Broker {
2548 api,
2549 code,
2550 name,
2551 message,
2552 } => (api, code, name, message),
2553 other => panic!("unexpected error {other:?}"),
2554 });
2555 assert2::assert!(result == expected);
2556 }
2557
2558 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2559 enum RetryExpected {
2560 Success,
2561 OffsetFetch(i16),
2562 CommitOutcome(i16),
2563 FindCoordinator(i16),
2564 Timeout,
2565 }
2566
2567 #[derive(Clone, Default)]
2571 struct RetryScript {
2572 find_coordinator: Vec<i16>,
2573 offset_fetch: Vec<i16>,
2574 offset_commit: Vec<i16>,
2575 find_coordinator_requests: usize,
2576 offset_fetch_requests: usize,
2577 offset_commit_requests: usize,
2578 unreachable_answers: usize,
2581 unreachable: Option<SocketAddr>,
2582 silent_offset_commit: bool,
2584 closed_offset_commits: usize,
2587 }
2588
2589 async fn refused_address() -> SocketAddr {
2591 let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind a port");
2592 listener.local_addr().expect("local address")
2593 }
2594
2595 impl RetryScript {
2596 fn next(codes: &[i16], requests: &mut usize) -> i16 {
2597 let code = codes[(*requests).min(codes.len() - 1)];
2598 *requests += 1;
2599 code
2600 }
2601 }
2602
2603 async fn scripted_group_broker(
2608 script: Arc<Mutex<RetryScript>>,
2609 coordinator: Arc<Mutex<Option<SocketAddr>>>,
2610 ) -> MockBroker {
2611 let ranges = GroupRanges {
2612 offset_commit: (2, 9),
2613 offset_fetch: (2, 9),
2614 };
2615 let closing = Arc::clone(&script);
2616 let handler = move |api_key: i16, version: i16, body: &[u8]| match api_key {
2617 api_versions_request::API_KEY => Some(api_versions(ranges)),
2618 offset_commit_request::API_KEY => {
2619 let request: OffsetCommitRequest = decode_request(body, version);
2620 let mut guard = script.lock().expect("script lock");
2621 let script = &mut *guard;
2622 let error_code =
2623 RetryScript::next(&script.offset_commit, &mut script.offset_commit_requests);
2624 let silent = script.silent_offset_commit;
2625 drop(guard);
2626 if silent {
2627 return None;
2628 }
2629 let response = offset_commit_response(&request, |_| error_code);
2630 Some(encode(
2631 &response,
2632 version,
2633 version >= offset_commit_request::FLEXIBLE_MIN,
2634 ))
2635 }
2636 find_coordinator_request::API_KEY => {
2637 let mut guard = script.lock().expect("script lock");
2638 let script = &mut *guard;
2639 let error_code = RetryScript::next(
2640 &script.find_coordinator,
2641 &mut script.find_coordinator_requests,
2642 );
2643 let addr = match script.unreachable {
2644 Some(unreachable)
2645 if script.find_coordinator_requests <= script.unreachable_answers =>
2646 {
2647 unreachable
2648 }
2649 _ => coordinator
2650 .lock()
2651 .expect("coordinator lock")
2652 .expect("coordinator address"),
2653 };
2654 drop(guard);
2655 Some(encode(
2656 &mock_coordinator_response(addr, error_code),
2657 version,
2658 false,
2659 ))
2660 }
2661 offset_fetch_request::API_KEY => {
2662 let mut guard = script.lock().expect("script lock");
2663 let script = &mut *guard;
2664 let error_code =
2665 RetryScript::next(&script.offset_fetch, &mut script.offset_fetch_requests);
2666 drop(guard);
2667 let response = if error_code == 0 {
2668 offset_fetch_response(version)
2669 } else {
2670 OffsetFetchResponse {
2671 groups: vec![OffsetFetchResponseGroup {
2672 group_id: "workers".into(),
2673 error_code,
2674 ..Default::default()
2675 }],
2676 ..Default::default()
2677 }
2678 };
2679 Some(encode(&response, version, true))
2680 }
2681 _ => None,
2682 };
2683 MockBroker::start_with_replies(move |api_key, version, _, body| {
2684 if api_key == offset_commit_request::API_KEY {
2685 let mut script = closing.lock().expect("script lock");
2686 let close = script.closed_offset_commits > 0;
2687 if close {
2688 script.closed_offset_commits -= 1;
2689 script.offset_commit_requests += 1;
2690 }
2691 drop(script);
2692 if close {
2693 return MockReply::Close;
2694 }
2695 }
2696 handler(api_key, version, body).map_or(MockReply::Silent, MockReply::Respond)
2697 })
2698 .await
2699 .unwrap()
2700 }
2701
2702 struct ScriptedGroupCluster {
2703 coordinator: MockBroker,
2704 bootstrap: MockBroker,
2705 admin: AdminClient,
2706 }
2707
2708 impl ScriptedGroupCluster {
2709 async fn start(script: Arc<Mutex<RetryScript>>) -> Self {
2710 let coordinator_addr = Arc::new(Mutex::new(None));
2711 let coordinator =
2712 scripted_group_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2713 *coordinator_addr.lock().expect("coordinator lock") = Some(coordinator.addr);
2714 let bootstrap =
2715 scripted_group_broker(Arc::clone(&script), Arc::clone(&coordinator_addr)).await;
2716 let admin = connect_admin(bootstrap.addr, "admin connects").await;
2717 Self {
2718 coordinator,
2719 bootstrap,
2720 admin,
2721 }
2722 }
2723 }
2724
2725 async fn run_scripted_call<T, F, Fut>(script: RetryScript, call: F) -> (T, RetryScript)
2726 where
2727 F: FnOnce(AdminClient) -> Fut,
2728 Fut: Future<Output = T>,
2729 {
2730 let shared_script = Arc::new(Mutex::new(script));
2731 let ScriptedGroupCluster {
2732 coordinator,
2733 bootstrap,
2734 admin,
2735 } = ScriptedGroupCluster::start(Arc::clone(&shared_script)).await;
2736 let result = call(admin).await;
2737 bootstrap.stop();
2738 coordinator.stop();
2739 let recorded = shared_script.lock().expect("script lock").clone();
2740 (result, recorded)
2741 }
2742
2743 const GROUP_RETRY_LONG: Duration = Duration::from_secs(5);
2744 const GROUP_RETRY_NOW: Duration = Duration::from_millis(500);
2745
2746 fn group_retry_backoff(timeout: Duration) -> Duration {
2747 if timeout == GROUP_RETRY_LONG {
2748 Duration::from_millis(1)
2749 } else {
2750 timeout
2751 }
2752 }
2753
2754 fn test_retry_policy(timeout: Duration, initial_backoff: Duration) -> RetryPolicy {
2755 RetryPolicy {
2756 timeout,
2757 initial_backoff,
2758 max_backoff: initial_backoff,
2759 jitter: 0.0,
2760 max_retries: u32::MAX,
2761 }
2762 }
2763
2764 fn list_groups_retry_policy(
2765 timeout: Duration,
2766 backoff: Duration,
2767 max_retries: u32,
2768 ) -> RetryPolicy {
2769 RetryPolicy {
2770 timeout,
2771 initial_backoff: backoff,
2772 max_backoff: backoff,
2773 jitter: 0.0,
2774 max_retries,
2775 }
2776 }
2777
2778 #[derive(Clone, Copy)]
2779 struct GroupRetryScenario {
2780 name: &'static str,
2781 find_coordinator: &'static [i16],
2782 offset_fetch: &'static [i16],
2783 offset_commit: &'static [i16],
2784 timeout: Duration,
2785 fetch_expected: RetryExpected,
2786 fetch_calls: (usize, usize),
2787 commit_expected: RetryExpected,
2788 commit_calls: (usize, usize),
2789 }
2790
2791 #[derive(Clone, Copy)]
2792 struct RetryCodes {
2793 find_coordinator: &'static [i16],
2794 offset_fetch: &'static [i16],
2795 offset_commit: &'static [i16],
2796 }
2797
2798 #[derive(Clone, Copy)]
2799 struct RetryExpectations {
2800 fetch: RetryExpected,
2801 commit: RetryExpected,
2802 }
2803
2804 fn successful_retry_case(
2805 name: &'static str,
2806 find_coordinator: &'static [i16],
2807 rpc_errors: &'static [i16],
2808 calls: (usize, usize),
2809 ) -> GroupRetryScenario {
2810 retry_scenario(
2811 name,
2812 RetryCodes {
2813 find_coordinator,
2814 offset_fetch: rpc_errors,
2815 offset_commit: rpc_errors,
2816 },
2817 GROUP_RETRY_LONG,
2818 RetryExpectations {
2819 fetch: RetryExpected::Success,
2820 commit: RetryExpected::CommitOutcome(0),
2821 },
2822 calls,
2823 )
2824 }
2825
2826 fn retry_scenario(
2827 name: &'static str,
2828 codes: RetryCodes,
2829 timeout: Duration,
2830 expected: RetryExpectations,
2831 calls: (usize, usize),
2832 ) -> GroupRetryScenario {
2833 GroupRetryScenario {
2834 name,
2835 find_coordinator: codes.find_coordinator,
2836 offset_fetch: codes.offset_fetch,
2837 offset_commit: codes.offset_commit,
2838 timeout,
2839 fetch_expected: expected.fetch,
2840 fetch_calls: calls,
2841 commit_expected: expected.commit,
2842 commit_calls: calls,
2843 }
2844 }
2845
2846 fn coordinator_recovery_case(name: &'static str, codes: &'static [i16]) -> GroupRetryScenario {
2847 successful_retry_case(name, codes, &[0], (2, 1))
2848 }
2849
2850 fn timed_out_retry_case(name: &'static str, codes: RetryCodes) -> GroupRetryScenario {
2851 retry_scenario(
2852 name,
2853 codes,
2854 GROUP_RETRY_NOW,
2855 RetryExpectations {
2856 fetch: RetryExpected::Timeout,
2857 commit: RetryExpected::Timeout,
2858 },
2859 (1, 1),
2860 )
2861 }
2862
2863 fn group_retry_scenarios() -> Vec<GroupRetryScenario> {
2864 use RetryExpected::{FindCoordinator as FetchFindError, Timeout as FetchTimeout};
2865
2866 vec![
2867 successful_retry_case("no error", &[0], &[0], (1, 1)),
2868 successful_retry_case("coordinator load in progress", &[0], &[14, 14, 0], (1, 3)),
2869 successful_retry_case("coordinator not available", &[0], &[15, 0], (2, 2)),
2870 successful_retry_case("not coordinator", &[0], &[16, 0], (2, 2)),
2871 timed_out_retry_case(
2872 "load in progress timeout",
2873 RetryCodes {
2874 find_coordinator: &[0],
2875 offset_fetch: &[14],
2876 offset_commit: &[27],
2877 },
2878 ),
2879 timed_out_retry_case(
2880 "not coordinator timeout",
2881 RetryCodes {
2882 find_coordinator: &[0],
2883 offset_fetch: &[16],
2884 offset_commit: &[16],
2885 },
2886 ),
2887 retry_scenario(
2888 "group error is final",
2889 RetryCodes {
2890 find_coordinator: &[0],
2891 offset_fetch: &[30],
2892 offset_commit: &[25],
2893 },
2894 GROUP_RETRY_LONG,
2895 RetryExpectations {
2896 fetch: RetryExpected::OffsetFetch(30),
2897 commit: RetryExpected::CommitOutcome(25),
2898 },
2899 (1, 1),
2900 ),
2901 retry_scenario(
2902 "another group error is final",
2903 RetryCodes {
2904 find_coordinator: &[0],
2905 offset_fetch: &[7],
2906 offset_commit: &[7],
2907 },
2908 GROUP_RETRY_LONG,
2909 RetryExpectations {
2910 fetch: RetryExpected::OffsetFetch(7),
2911 commit: RetryExpected::CommitOutcome(7),
2912 },
2913 (1, 1),
2914 ),
2915 coordinator_recovery_case("find coordinator not available then succeeds", &[15, 0]),
2916 coordinator_recovery_case("find coordinator load in progress then succeeds", &[14, 0]),
2917 retry_scenario(
2918 "find coordinator timeout",
2919 RetryCodes {
2920 find_coordinator: &[15],
2921 offset_fetch: &[0],
2922 offset_commit: &[0],
2923 },
2924 GROUP_RETRY_NOW,
2925 RetryExpectations {
2926 fetch: FetchTimeout,
2927 commit: FetchTimeout,
2928 },
2929 (1, 0),
2930 ),
2931 retry_scenario(
2932 "find coordinator authorization error",
2933 RetryCodes {
2934 find_coordinator: &[30],
2935 offset_fetch: &[0],
2936 offset_commit: &[0],
2937 },
2938 GROUP_RETRY_LONG,
2939 RetryExpectations {
2940 fetch: FetchFindError(30),
2941 commit: FetchFindError(30),
2942 },
2943 (1, 0),
2944 ),
2945 ]
2946 }
2947
2948 const TIMED_OUT: i16 = 7;
2949
2950 #[derive(Clone, Copy)]
2951 enum RetryApi {
2952 Fetch,
2953 Commit,
2954 }
2955
2956 #[derive(Debug, PartialEq)]
2957 enum RetryObservation {
2958 Fetch(Result<GroupOffsets, RetryExpected>),
2959 Commit(Result<Vec<ConsumerGroupOffsetOutcome>, i16>),
2960 }
2961
2962 async fn alter_consumer_offsets_with_retry(
2963 admin: AdminClient,
2964 timeout: Duration,
2965 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
2966 let offsets = test_group_offsets();
2967 admin
2968 .alter_group_offsets_with_retry(
2969 GroupOffsetUpdate {
2970 kind: GroupOffsetKind::Consumer,
2971 group: "workers",
2972 offsets: &offsets,
2973 },
2974 test_retry_policy(timeout, group_retry_backoff(timeout)),
2975 )
2976 .await
2977 }
2978
2979 async fn assert_retry_scenario(case: GroupRetryScenario, api: RetryApi) {
2980 let expected = match (api, case.fetch_expected, case.commit_expected) {
2981 (RetryApi::Fetch, RetryExpected::Success, _) => {
2982 RetryObservation::Fetch(Ok(test_group_offsets()))
2983 }
2984 (RetryApi::Fetch, RetryExpected::OffsetFetch(code), _) => {
2985 RetryObservation::Fetch(Err(RetryExpected::OffsetFetch(code)))
2986 }
2987 (RetryApi::Fetch, RetryExpected::FindCoordinator(code), _) => {
2988 RetryObservation::Fetch(Err(RetryExpected::FindCoordinator(code)))
2989 }
2990 (RetryApi::Fetch, RetryExpected::Timeout, _) => {
2991 RetryObservation::Fetch(Err(RetryExpected::Timeout))
2992 }
2993 (RetryApi::Commit, _, RetryExpected::CommitOutcome(code)) => {
2994 RetryObservation::Commit(Ok(vec![ConsumerGroupOffsetOutcome {
2995 topic: "orders".into(),
2996 partition: 2,
2997 error: kafka_error_if(code, None),
2998 }]))
2999 }
3000 (RetryApi::Commit, _, RetryExpected::FindCoordinator(code)) => {
3001 RetryObservation::Commit(Err(code))
3002 }
3003 (RetryApi::Commit, _, RetryExpected::Timeout) => {
3004 RetryObservation::Commit(Err(TIMED_OUT))
3005 }
3006 _ => unreachable!("retry expectation belongs to another API"),
3007 };
3008 let expected_calls = match api {
3009 RetryApi::Fetch => case.fetch_calls,
3010 RetryApi::Commit => case.commit_calls,
3011 };
3012 let mut script = RetryScript {
3013 find_coordinator: case.find_coordinator.to_vec(),
3014 ..RetryScript::default()
3015 };
3016 match api {
3017 RetryApi::Fetch => script.offset_fetch = case.offset_fetch.to_vec(),
3018 RetryApi::Commit => script.offset_commit = case.offset_commit.to_vec(),
3019 }
3020
3021 let ((result, within_deadline), script) = run_scripted_call(script, |admin| async move {
3022 let started = Instant::now();
3023 let result = match api {
3024 RetryApi::Fetch => {
3025 let result = admin
3026 .list_consumer_group_offsets_with_retry(
3027 "workers",
3028 test_retry_policy(case.timeout, group_retry_backoff(case.timeout)),
3029 )
3030 .await
3031 .map_err(|error| match error {
3032 AdminError::Broker {
3033 api: "OffsetFetch",
3034 code,
3035 ..
3036 } => RetryExpected::OffsetFetch(code),
3037 AdminError::Transport(ClientError::Server { error_code }) => {
3038 RetryExpected::FindCoordinator(error_code)
3039 }
3040 AdminError::Transport(ClientError::Timeout(_)) => {
3041 RetryExpected::Timeout
3042 }
3043 other => panic!("case {}: unexpected error {other:?}", case.name),
3044 });
3045 RetryObservation::Fetch(result)
3046 }
3047 RetryApi::Commit => {
3048 let result = alter_consumer_offsets_with_retry(admin, case.timeout)
3049 .await
3050 .map_err(|error| match error {
3051 AdminError::Transport(ClientError::Server { error_code }) => error_code,
3052 AdminError::Transport(ClientError::Timeout(_)) => TIMED_OUT,
3053 other => panic!("case {}: unexpected error {other:?}", case.name),
3054 });
3055 RetryObservation::Commit(result)
3056 }
3057 };
3058 let within_deadline = match api {
3059 RetryApi::Fetch => true,
3060 RetryApi::Commit => started.elapsed() < case.timeout + Duration::from_secs(2),
3061 };
3062 (result, within_deadline)
3063 })
3064 .await;
3065 let rpc_calls = match api {
3066 RetryApi::Fetch => script.offset_fetch_requests,
3067 RetryApi::Commit => script.offset_commit_requests,
3068 };
3069 assert!(
3070 (
3071 result,
3072 script.find_coordinator_requests,
3073 rpc_calls,
3074 within_deadline
3075 ) == (expected, expected_calls.0, expected_calls.1, true),
3076 "case {}",
3077 case.name
3078 );
3079 }
3080
3081 #[yare::parameterized(
3084 no_error = { 0 },
3085 coordinator_load_in_progress = { 1 },
3086 coordinator_not_available = { 2 },
3087 not_coordinator = { 3 },
3088 load_timeout = { 4 },
3089 coordinator_timeout = { 5 },
3090 group_error = { 6 },
3091 other_group_error = { 7 },
3092 find_coordinator_not_available = { 8 },
3093 find_coordinator_load = { 9 },
3094 find_coordinator_timeout = { 10 },
3095 find_coordinator_authorization = { 11 },
3096 )]
3097 fn group_offset_retry_scenarios_match_kafka(index: usize) {
3098 block_on_test(async move {
3099 let case = group_retry_scenarios()[index];
3100 assert_retry_scenario(case, RetryApi::Fetch).await;
3101 assert_retry_scenario(case, RetryApi::Commit).await;
3102 });
3103 }
3104
3105 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
3106 enum Coordinator {
3107 UnreachableOnce,
3108 Silent,
3109 ClosedOnce,
3110 }
3111
3112 #[yare::parameterized(
3115 closed_connection = { "closed connection", GROUP_RETRY_LONG, Coordinator::ClosedOnce, 1, 2, Ok(0) },
3116 unreachable_coordinator = { "unreachable coordinator", GROUP_RETRY_LONG, Coordinator::UnreachableOnce, 2, 1, Ok(0) },
3117 silent_coordinator = { "silent coordinator", Duration::from_millis(300), Coordinator::Silent, 1, 1, Err(TIMED_OUT) },
3118 )]
3119 #[test_macro(tokio::test(flavor = "multi_thread", worker_threads = 2))]
3120 async fn alter_consumer_group_offsets_recovers_from_connection_failures(
3121 name: &'static str,
3122 timeout: Duration,
3123 coordinator: Coordinator,
3124 find_calls: usize,
3125 commit_calls: usize,
3126 expected: Result<i16, i16>,
3127 ) {
3128 let script = RetryScript {
3129 find_coordinator: vec![0],
3130 offset_commit: vec![0],
3131 unreachable_answers: usize::from(coordinator == Coordinator::UnreachableOnce),
3132 unreachable: Some(refused_address().await),
3133 silent_offset_commit: coordinator == Coordinator::Silent,
3134 closed_offset_commits: usize::from(coordinator == Coordinator::ClosedOnce),
3135 ..RetryScript::default()
3136 };
3137 let ((result, within_deadline), script) = run_scripted_call(script, |admin| async move {
3138 let started = Instant::now();
3139 let result = alter_consumer_offsets_with_retry(admin, timeout)
3140 .await
3141 .map(|_| 0_i16)
3142 .map_err(|error| match error {
3143 AdminError::Transport(ClientError::Server { error_code }) => error_code,
3144 AdminError::Transport(ClientError::Timeout(_)) => TIMED_OUT,
3145 other => panic!("case {name}: unexpected error {other:?}"),
3146 });
3147 (result, started.elapsed() < timeout + Duration::from_secs(2))
3148 })
3149 .await;
3150 assert2::assert!(
3151 (
3152 result,
3153 script.find_coordinator_requests,
3154 script.offset_commit_requests,
3155 within_deadline
3156 ) == (expected, find_calls, commit_calls, true),
3157 "case {name}"
3158 );
3159 }
3160
3161 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3162 async fn group_offset_rpcs_use_the_group_coordinator() {
3163 let ranges = GroupRanges {
3164 offset_commit: (2, 10),
3165 offset_fetch: (2, 10),
3166 };
3167 let coordinator_group_rpcs = Arc::new(AtomicUsize::new(0));
3168 let seen = Arc::clone(&coordinator_group_rpcs);
3169 let coordinator = MockBroker::start(move |api_key, version, _, _| match api_key {
3170 api_versions_request::API_KEY => Some(api_versions(ranges)),
3171 offset_commit_request::API_KEY => {
3172 seen.fetch_add(1, Ordering::SeqCst);
3173 Some(crate::encode_test_struct_field!(
3174 encode,
3175 OffsetCommitResponse,
3176 topics,
3177 vec![successful_offset_commit_topic()],
3178 version,
3179 true,
3180 ))
3181 }
3182 offset_fetch_request::API_KEY => {
3183 counted_reply(&seen, Some(offset_fetch_reply(version)))
3184 }
3185 _ => None,
3186 })
3187 .await
3188 .unwrap();
3189
3190 let bootstrap_group_rpcs = Arc::new(AtomicUsize::new(0));
3191 let bootstrap =
3192 bootstrap_for(&coordinator, ranges, Arc::clone(&bootstrap_group_rpcs)).await;
3193 let mut admin = connect_admin(bootstrap.addr, "admin connects").await;
3194
3195 let committed = admin
3196 .alter_consumer_group_offsets("workers", &test_group_offsets())
3197 .await
3198 .expect("offset commit succeeds");
3199 let mut admin = connect_admin(bootstrap.addr, "second admin connects").await;
3200 let fetched = admin
3201 .list_consumer_group_offsets("workers")
3202 .await
3203 .expect("offset fetch succeeds");
3204
3205 assert!(bootstrap_group_rpcs.load(Ordering::SeqCst) == 0);
3206 assert!(coordinator_group_rpcs.load(Ordering::SeqCst) == 2);
3207 assert!(committed == successful_group_offset_outcome());
3208 assert!(fetched == test_group_offsets());
3209 bootstrap.stop();
3210 coordinator.stop();
3211 }
3212
3213 #[derive(Clone)]
3215 enum ListGroupsAnswer {
3216 Groups(Vec<ListedGroup>),
3219 Error(i16),
3221 }
3222
3223 #[derive(Clone)]
3226 struct ListGroupsBroker {
3227 max_version: i16,
3228 answers: Vec<ListGroupsAnswer>,
3229 }
3230
3231 fn listed(group_id: &str, protocol_type: &str, state: &str, group_type: &str) -> ListedGroup {
3232 ListedGroup {
3233 group_id: group_id.into(),
3234 protocol_type: protocol_type.into(),
3235 group_state: state.into(),
3236 group_type: group_type.into(),
3237 ..Default::default()
3238 }
3239 }
3240
3241 fn consumer_group(group_id: &str) -> ListedGroup {
3242 listed(group_id, "consumer", "Stable", "Classic")
3243 }
3244
3245 fn stable_listing(group_id: &str) -> GroupListing {
3246 GroupListing {
3247 group_id: group_id.into(),
3248 group_type: Some(GroupType::Classic),
3249 protocol_type: "consumer".into(),
3250 group_state: Some(GroupState::Stable),
3251 }
3252 }
3253
3254 fn stable_listings(group_ids: &[&str]) -> Vec<GroupListing> {
3255 group_ids
3256 .iter()
3257 .map(|group_id| stable_listing(group_id))
3258 .collect()
3259 }
3260
3261 fn groups(ids: &[&str]) -> ListGroupsBroker {
3262 ListGroupsBroker {
3263 max_version: 5,
3264 answers: vec![ListGroupsAnswer::Groups(
3265 ids.iter().map(|id| consumer_group(id)).collect(),
3266 )],
3267 }
3268 }
3269
3270 async fn list_groups_broker(
3274 script: ListGroupsBroker,
3275 cluster: Arc<Mutex<Vec<SocketAddr>>>,
3276 requests: Arc<Mutex<Vec<(i16, ListGroupsRequest)>>>,
3277 ) -> MockBroker {
3278 MockBroker::start_with_replies(move |api_key, version, _, body| {
3279 MockSaslAnswer::Accept
3280 .reply(api_key, version)
3281 .unwrap()
3282 .unwrap_or_else(|| {
3283 list_groups_reply(&script, &cluster, &requests, api_key, version, body)
3284 .map_or(MockReply::Silent, MockReply::Respond)
3285 })
3286 })
3287 .await
3288 .unwrap()
3289 }
3290
3291 fn list_groups_reply(
3293 script: &ListGroupsBroker,
3294 cluster: &Mutex<Vec<SocketAddr>>,
3295 requests: &Mutex<Vec<(i16, ListGroupsRequest)>>,
3296 api_key: i16,
3297 version: i16,
3298 body: &[u8],
3299 ) -> Option<Vec<u8>> {
3300 match api_key {
3301 api_versions_request::API_KEY => Some(encode_api_versions(vec![
3302 api_version_entry!(api_versions_request::API_KEY, 0, 0),
3303 api_version_entry!(metadata_request::API_KEY, 12, 12),
3304 api_version_entry!(list_groups_request::API_KEY, 0, script.max_version),
3305 ])),
3306 metadata_request::API_KEY => Some(crate::encode_test_struct_field!(
3307 encode,
3308 MetadataResponse,
3309 brokers,
3310 cluster
3311 .lock()
3312 .expect("cluster lock")
3313 .iter()
3314 .zip(1..)
3315 .map(|(addr, node_id)| mock_metadata_broker(*addr, node_id))
3316 .collect(),
3317 version,
3318 true,
3319 )),
3320 list_groups_request::API_KEY => {
3321 let request: ListGroupsRequest = decode_request(body, version);
3322 let mut requests = requests.lock().expect("requests lock");
3323 let answer = &script.answers[requests.len().min(script.answers.len() - 1)];
3324 let response = match answer {
3325 ListGroupsAnswer::Groups(groups) => ListGroupsResponse {
3326 groups: groups
3327 .iter()
3328 .filter(|group| {
3329 (request.states_filter.is_empty()
3330 || request.states_filter.contains(&group.group_state))
3331 && (request.types_filter.is_empty()
3332 || request.types_filter.contains(&group.group_type))
3333 })
3334 .cloned()
3335 .collect(),
3336 ..Default::default()
3337 },
3338 ListGroupsAnswer::Error(error_code) => ListGroupsResponse {
3339 error_code: *error_code,
3340 ..Default::default()
3341 },
3342 };
3343 requests.push((version, request));
3344 drop(requests);
3345 Some(encode(
3346 &response,
3347 version,
3348 version >= list_groups_request::FLEXIBLE_MIN,
3349 ))
3350 }
3351 _ => None,
3352 }
3353 }
3354
3355 type ExpectedError = (i32, i16, Option<String>);
3357
3358 struct ListGroupsCase {
3360 name: &'static str,
3361 brokers: [ListGroupsBroker; 3],
3362 options: ListGroupsOptions,
3363 timeout: Duration,
3364 valid: Vec<GroupListing>,
3365 errors: Vec<ExpectedError>,
3366 requests: [Vec<(i16, ListGroupsRequest)>; 3],
3368 }
3369
3370 const LONG: Duration = Duration::from_secs(10);
3372 const SHORT: Duration = Duration::from_millis(500);
3374 const BACKOFF: Duration = Duration::from_secs(1);
3376
3377 #[derive(Default)]
3378 struct ListGroupsCaseBuilder {
3379 name: &'static str,
3380 brokers: Option<[ListGroupsBroker; 3]>,
3381 options: ListGroupsOptions,
3382 timeout: Option<Duration>,
3383 valid: Vec<GroupListing>,
3384 errors: Vec<ExpectedError>,
3385 requests: Option<[Vec<(i16, ListGroupsRequest)>; 3]>,
3386 }
3387
3388 impl ListGroupsCase {
3389 fn builder() -> ListGroupsCaseBuilder {
3390 ListGroupsCaseBuilder::default()
3391 }
3392 }
3393
3394 impl ListGroupsCaseBuilder {
3395 fn name(mut self, name: &'static str) -> Self {
3396 self.name = name;
3397 self
3398 }
3399
3400 fn brokers(mut self, brokers: [ListGroupsBroker; 3]) -> Self {
3401 self.brokers = Some(brokers);
3402 self
3403 }
3404
3405 fn options(mut self, options: ListGroupsOptions) -> Self {
3406 self.options = options;
3407 self
3408 }
3409
3410 fn timeout(mut self, timeout: Duration) -> Self {
3411 self.timeout = Some(timeout);
3412 self
3413 }
3414
3415 fn valid(mut self, valid: Vec<GroupListing>) -> Self {
3416 self.valid = valid;
3417 self
3418 }
3419
3420 fn errors(mut self, errors: Vec<ExpectedError>) -> Self {
3421 self.errors = errors;
3422 self
3423 }
3424
3425 fn requests(mut self, requests: [Vec<(i16, ListGroupsRequest)>; 3]) -> Self {
3426 self.requests = Some(requests);
3427 self
3428 }
3429
3430 fn build(self) -> ListGroupsCase {
3431 ListGroupsCase {
3432 name: self.name,
3433 brokers: self.brokers.expect("brokers must be set"),
3434 options: self.options,
3435 timeout: self.timeout.unwrap_or(LONG),
3436 valid: self.valid,
3437 errors: self.errors,
3438 requests: self
3439 .requests
3440 .unwrap_or_else(|| [vec![plain(5)], vec![plain(5)], vec![plain(5)]]),
3441 }
3442 }
3443 }
3444
3445 fn plain(version: i16) -> (i16, ListGroupsRequest) {
3446 (version, ListGroupsRequest::default())
3447 }
3448
3449 fn stable(version: i16) -> (i16, ListGroupsRequest) {
3450 (
3451 version,
3452 ListGroupsRequest {
3453 states_filter: stable_state_filter(),
3454 ..Default::default()
3455 },
3456 )
3457 }
3458
3459 fn classic_and_consumer(version: i16) -> (i16, ListGroupsRequest) {
3462 (
3463 version,
3464 ListGroupsRequest {
3465 types_filter: if version >= 5 {
3466 vec!["Consumer".into(), "Classic".into()]
3467 } else {
3468 Vec::new()
3469 },
3470 ..Default::default()
3471 },
3472 )
3473 }
3474
3475 fn consumer_type(version: i16) -> (i16, ListGroupsRequest) {
3476 (
3477 version,
3478 ListGroupsRequest {
3479 types_filter: vec!["Consumer".into()],
3480 ..Default::default()
3481 },
3482 )
3483 }
3484
3485 fn unsupported(broker_max: i16, client_min: i16) -> String {
3488 AdminError::Transport(ClientError::IncompatibleVersion {
3489 api_key: list_groups_request::API_KEY,
3490 broker_min: 0,
3491 broker_max,
3492 client_min,
3493 client_max: 5,
3494 })
3495 .to_string()
3496 }
3497
3498 fn states(states: &[GroupState]) -> ListGroupsOptions {
3499 ListGroupsOptions::builder()
3500 .group_states(states.iter().copied().collect())
3501 .build()
3502 }
3503
3504 fn stable_state_filter() -> Vec<String> {
3505 vec![GroupState::Stable.to_string()]
3506 }
3507
3508 fn single_broker_error() -> Vec<ExpectedError> {
3509 vec![(2, 31, None)]
3510 }
3511
3512 fn expected_kafka_error(code: i16, message: Option<String>) -> KafkaError {
3513 kafka_error_if(code, message).expect("error code is nonzero")
3514 }
3515
3516 fn types(types: &[GroupType]) -> ListGroupsOptions {
3517 ListGroupsOptions::builder()
3518 .types(types.iter().copied().collect())
3519 .build()
3520 }
3521
3522 fn broker_error(code: i16) -> ListGroupsBroker {
3523 ListGroupsBroker {
3524 max_version: 5,
3525 answers: vec![ListGroupsAnswer::Error(code)],
3526 }
3527 }
3528
3529 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3532 async fn list_groups_stops_a_broker_after_the_retry_limit() {
3533 let cluster = Arc::new(Mutex::new(Vec::new()));
3534 let sent = Arc::new(Mutex::new(Vec::new()));
3535 let broker =
3536 list_groups_broker(broker_error(14), Arc::clone(&cluster), Arc::clone(&sent)).await;
3537 *cluster.lock().expect("cluster lock") = vec![broker.addr];
3538 let admin = connect_admin(broker.addr, "admin connects").await;
3539
3540 let options = ListGroupsOptions::builder().build();
3541 let result = run_list_groups_with_retry(
3542 &admin,
3543 &options,
3544 list_groups_retry_policy(Duration::from_secs(10), Duration::from_millis(1), 2),
3545 )
3546 .await;
3547
3548 broker.stop();
3549 let codes = result
3550 .errors
3551 .iter()
3552 .map(|error| error.error.code)
3553 .collect::<Vec<_>>();
3554 assert!((codes, sent.lock().expect("sent lock").len()) == (vec![7], 3));
3555 }
3556
3557 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3561 async fn list_groups_fails_when_the_metadata_never_names_a_broker() {
3562 let sent = Arc::new(Mutex::new(Vec::new()));
3563 let broker = list_groups_broker(
3564 groups(&[]),
3565 Arc::new(Mutex::new(Vec::new())),
3566 Arc::clone(&sent),
3567 )
3568 .await;
3569 let admin = connect_admin(broker.addr, "admin connects").await;
3570
3571 let result = admin
3572 .list_groups_with_retry(
3573 &ListGroupsOptions::builder().build(),
3574 list_groups_retry_policy(
3575 Duration::from_millis(50),
3576 Duration::from_millis(5),
3577 u32::MAX,
3578 ),
3579 )
3580 .await
3581 .map_err(|error| error.to_string());
3582
3583 broker.stop();
3584 assert!(
3585 (result, sent.lock().expect("sent lock").len())
3586 == (
3587 Err("protocol: failed to find brokers to send ListGroups: metadata has no brokers"
3588 .to_owned()),
3589 0
3590 )
3591 );
3592 }
3593
3594 fn below(max_version: i16, ids: &[&str]) -> ListGroupsBroker {
3595 ListGroupsBroker {
3596 max_version,
3597 ..groups(ids)
3598 }
3599 }
3600
3601 async fn run_list_groups_case(case: ListGroupsCase) {
3604 let cluster = Arc::new(Mutex::new(Vec::new()));
3605 let mut brokers = Vec::new();
3606 let mut requests = Vec::new();
3607 for script in case.brokers {
3608 let sent = Arc::new(Mutex::new(Vec::new()));
3609 brokers.push(list_groups_broker(script, Arc::clone(&cluster), Arc::clone(&sent)).await);
3610 requests.push(sent);
3611 }
3612 *cluster.lock().expect("cluster lock") = brokers.iter().map(|broker| broker.addr).collect();
3613 let admin = connect_admin(brokers[0].addr, "admin connects").await;
3614
3615 let result = run_list_groups_with_retry(
3616 &admin,
3617 &case.options,
3618 list_groups_retry_policy(case.timeout, BACKOFF, u32::MAX),
3619 )
3620 .await;
3621
3622 let expected = ListGroupsResult {
3623 valid: case.valid,
3624 errors: case
3625 .errors
3626 .into_iter()
3627 .map(|(node_id, code, message)| {
3628 let addr = brokers[usize::try_from(node_id - 1).expect("node id")].addr;
3629 ListGroupsError {
3630 node_id,
3631 host: addr.ip().to_string(),
3632 port: broker_port(addr),
3633 error: expected_kafka_error(code, message),
3634 }
3635 })
3636 .collect(),
3637 };
3638 for broker in brokers {
3639 broker.stop();
3640 }
3641 let requests = requests
3642 .iter()
3643 .map(|sent| sent.lock().expect("requests lock").clone())
3644 .collect::<Vec<_>>();
3645 let name = case.name;
3646 assert!(
3647 (result, requests) == (expected, case.requests.to_vec()),
3648 "case {name}"
3649 );
3650 }
3651
3652 async fn run_list_groups_with_retry(
3653 admin: &AdminClient,
3654 options: &ListGroupsOptions,
3655 policy: RetryPolicy,
3656 ) -> ListGroupsResult {
3657 admin
3658 .list_groups_with_retry(options, policy)
3659 .await
3660 .expect("list_groups succeeds")
3661 }
3662
3663 fn block_on_test(future: impl Future<Output = ()> + Send + 'static) {
3664 runtime::Builder::new_multi_thread()
3665 .worker_threads(2)
3666 .enable_all()
3667 .build()
3668 .expect("tokio runtime")
3669 .block_on(future);
3670 }
3671
3672 fn run_list_groups_case_sync(case: ListGroupsCase) {
3673 block_on_test(run_list_groups_case(case));
3674 }
3675
3676 #[yare::parameterized(
3682 one_group_each_broker = { 0 },
3683 empty_broker = { 1 },
3684 duplicate_group = { 2 },
3685 broker_timeout = { 3 },
3686 broker_retries = { 4 },
3687 authorization_error = { 5 },
3688 )]
3689 fn list_groups_asks_every_broker_and_merges(index: usize) {
3690 run_list_groups_case_sync(list_groups_merge_cases().remove(index));
3691 }
3692
3693 #[yare::parameterized(
3699 state_filter = { 0 },
3700 old_state_filter = { 1 },
3701 consumer_type_filter = { 2 },
3702 old_consumer_type_pair = { 3 },
3703 protocol_filter = { 4 },
3704 )]
3705 fn list_groups_sends_filters_as_kafka_does(index: usize) {
3706 run_list_groups_case_sync(list_groups_filter_cases().remove(index));
3707 }
3708
3709 #[derive(Clone, Copy, Debug)]
3712 enum SecuredBroker {
3713 Stalls,
3715 RejectsAuthentication,
3717 }
3718
3719 struct StartedSecuredBroker {
3722 addr: SocketAddr,
3723 connections: Arc<AtomicUsize>,
3724 mock: Option<MockBroker>,
3725 listener: Option<JoinHandle<()>>,
3726 }
3727
3728 impl SecuredBroker {
3729 async fn start(self) -> StartedSecuredBroker {
3730 let connections = Arc::new(AtomicUsize::new(0));
3731 let counter = Arc::clone(&connections);
3732 match self {
3733 Self::Stalls => {
3734 let listener = TcpListener::bind("127.0.0.1:0")
3735 .await
3736 .expect("listener binds");
3737 let addr = listener.local_addr().expect("listener address");
3738 let task = tokio::spawn(async move {
3739 let mut open = Vec::new();
3742 while let Ok((stream, _)) = listener.accept().await {
3743 counter.fetch_add(1, Ordering::SeqCst);
3744 open.push(stream);
3745 }
3746 drop(open);
3747 });
3748 StartedSecuredBroker {
3749 addr,
3750 connections,
3751 mock: None,
3752 listener: Some(task),
3753 }
3754 }
3755 Self::RejectsAuthentication => {
3756 let mock = MockBroker::start_with_replies(move |api_key, version, _, _| {
3757 if api_key == sasl_handshake_request::API_KEY {
3758 counter.fetch_add(1, Ordering::SeqCst);
3759 }
3760 MockSaslAnswer::AuthenticateError(SASL_AUTHENTICATION_FAILED)
3761 .reply(api_key, version)
3762 .unwrap()
3763 .unwrap_or(MockReply::Silent)
3764 })
3765 .await
3766 .unwrap();
3767 StartedSecuredBroker {
3768 addr: mock.addr,
3769 connections,
3770 mock: Some(mock),
3771 listener: None,
3772 }
3773 }
3774 }
3775 }
3776
3777 fn expected_error(self, addr: SocketAddr) -> (i16, Option<String>) {
3780 match self {
3781 Self::Stalls => (
3782 7,
3783 Some("ListGroups timed out after 1 attempt(s)".to_owned()),
3784 ),
3785 Self::RejectsAuthentication => (
3786 SASL_AUTHENTICATION_FAILED,
3787 Some(
3788 AdminError::Transport(ClientError::Authentication {
3789 addr,
3790 source: AuthenticationError::Sasl(SaslAuthenticationError::Failed(
3791 "SaslAuthenticate(PLAIN) error_code=58 \
3792 error_message=Some(\"rejected by mock broker\")"
3793 .to_owned(),
3794 )),
3795 })
3796 .to_string(),
3797 ),
3798 ),
3799 }
3800 }
3801 }
3802
3803 impl StartedSecuredBroker {
3804 fn stop(self) {
3805 if let Some(mock) = self.mock {
3806 mock.stop();
3807 }
3808 if let Some(listener) = self.listener {
3809 listener.abort();
3810 }
3811 }
3812 }
3813
3814 const SASL_AUTHENTICATION_FAILED: i16 = 58;
3815
3816 #[yare::parameterized(
3832 stalls = { SecuredBroker::Stalls },
3833 rejects_authentication = { SecuredBroker::RejectsAuthentication },
3834 )]
3835 #[test_macro(tokio::test(flavor = "multi_thread", worker_threads = 2))]
3836 async fn list_groups_isolates_a_secured_broker_that_stalls_or_rejects(broker_2: SecuredBroker) {
3837 const RETRY: RetryPolicy = RetryPolicy {
3838 timeout: Duration::from_secs(2),
3839 initial_backoff: Duration::from_millis(100),
3840 max_backoff: Duration::from_millis(100),
3841 jitter: 0.0,
3842 max_retries: u32::MAX,
3843 };
3844 let cluster = Arc::new(Mutex::new(Vec::new()));
3845 let requests = Arc::new(Mutex::new(Vec::new()));
3846 let broker_1 =
3847 list_groups_broker(groups(&["a"]), Arc::clone(&cluster), Arc::clone(&requests)).await;
3848 let broker_2_started = broker_2.start().await;
3849 let broker_3 =
3850 list_groups_broker(groups(&["c"]), Arc::clone(&cluster), Arc::clone(&requests)).await;
3851 let broker_2_addr = broker_2_started.addr;
3852 *cluster.lock().expect("cluster lock") = vec![broker_1.addr, broker_2_addr, broker_3.addr];
3853 let admin = AdminClient::connect_secured(
3854 &[broker_1.addr.to_string()],
3855 Some(ClientSecurity {
3856 protocol: krabka_security::ListenerProtocol::SaslPlaintext,
3857 tls: None,
3858 sasl: Some(SaslCredentials::Plain {
3859 username: "alice".into(),
3860 password: "secret".into(),
3861 }),
3862 sasl_host: None,
3863 }),
3864 )
3865 .await
3866 .expect("admin connects");
3867
3868 let outcome = timeout(
3869 RETRY.timeout * 5,
3870 admin.list_groups_with_retry(&ListGroupsOptions::builder().build(), RETRY),
3871 )
3872 .await
3873 .map(|result| result.expect("list_groups succeeds"));
3874 let observed = (outcome, broker_2_started.connections.load(Ordering::SeqCst));
3875
3876 let (code, message) = broker_2.expected_error(broker_2_addr);
3877 let expected = (
3878 Ok(ListGroupsResult {
3879 valid: stable_listings(&["a", "c"]),
3880 errors: vec![ListGroupsError {
3881 node_id: 2,
3882 host: broker_2_addr.ip().to_string(),
3883 port: broker_port(broker_2_addr),
3884 error: expected_kafka_error(code, message),
3885 }],
3886 }),
3887 1,
3888 );
3889 broker_1.stop();
3890 broker_2_started.stop();
3891 broker_3.stop();
3892 assert2::assert!(observed == expected, "case {broker_2:?}");
3893 }
3894
3895 fn list_groups_merge_cases() -> Vec<ListGroupsCase> {
3896 vec![
3897 ListGroupsCase::builder()
3898 .name("one group on each broker")
3899 .brokers([groups(&["a"]), groups(&["b"]), groups(&["c"])])
3900 .valid(stable_listings(&["a", "b", "c"]))
3901 .build(),
3902 ListGroupsCase::builder()
3903 .name("broker 2 has no groups")
3904 .brokers([groups(&["a"]), groups(&[]), groups(&["c"])])
3905 .valid(stable_listings(&["a", "c"]))
3906 .build(),
3907 ListGroupsCase::builder()
3908 .name("a group on two brokers is listed once")
3909 .brokers([groups(&["a"]), groups(&["a", "b"]), groups(&["c"])])
3910 .valid(stable_listings(&["a", "b", "c"]))
3911 .build(),
3912 ListGroupsCase::builder()
3913 .name("broker 2 answers coordinator not available until the timeout")
3914 .brokers([groups(&["a"]), broker_error(15), groups(&["c"])])
3915 .timeout(SHORT)
3916 .valid(stable_listings(&["a", "c"]))
3917 .errors(vec![(
3918 2,
3919 7,
3920 Some(format!(
3921 "ListGroups timed out after 1 attempt(s); last error: {}",
3922 AdminError::Broker {
3923 api: "ListGroups",
3924 code: 15,
3925 name: "COORDINATOR_NOT_AVAILABLE",
3926 message: None,
3927 }
3928 )),
3929 )])
3930 .build(),
3931 ListGroupsCase::builder()
3932 .name("broker 2 answers coordinator load in progress, then its groups")
3933 .brokers([
3934 groups(&["a"]),
3935 ListGroupsBroker {
3936 max_version: 5,
3937 answers: vec![
3938 ListGroupsAnswer::Error(14),
3939 ListGroupsAnswer::Error(15),
3940 ListGroupsAnswer::Groups(vec![consumer_group("b")]),
3941 ],
3942 },
3943 groups(&["c"]),
3944 ])
3945 .valid(vec![
3946 stable_listing("a"),
3947 stable_listing("b"),
3948 stable_listing("c"),
3949 ])
3950 .requests([
3951 vec![plain(5)],
3952 vec![plain(5), plain(5), plain(5)],
3953 vec![plain(5)],
3954 ])
3955 .build(),
3956 ListGroupsCase::builder()
3957 .name("broker 2 answers cluster authorization failed")
3958 .brokers([groups(&["a"]), broker_error(31), groups(&["c"])])
3959 .valid(stable_listings(&["a", "c"]))
3960 .errors(single_broker_error())
3961 .build(),
3962 ]
3963 }
3964
3965 fn list_groups_filter_cases() -> Vec<ListGroupsCase> {
3966 vec![
3967 ListGroupsCase::builder()
3968 .name("state filter Stable is sent to every broker")
3969 .brokers([
3970 ListGroupsBroker {
3971 max_version: 5,
3972 answers: vec![ListGroupsAnswer::Groups(vec![
3973 listed("a", "consumer", "Empty", "Classic"),
3974 consumer_group("b"),
3975 ])],
3976 },
3977 groups(&["c"]),
3978 groups(&[]),
3979 ])
3980 .options(states(&[GroupState::Stable]))
3981 .valid(stable_listings(&["b", "c"]))
3982 .requests([vec![stable(5)], vec![stable(5)], vec![stable(5)]])
3983 .build(),
3984 ListGroupsCase::builder()
3985 .name("state filter on a broker below v4 is unsupported on that broker")
3986 .brokers([groups(&["a"]), below(3, &["b"]), groups(&["c"])])
3987 .options(states(&[GroupState::Stable]))
3988 .valid(stable_listings(&["a", "c"]))
3989 .errors(vec![(2, 35, Some(unsupported(3, 4)))])
3990 .requests([vec![stable(5)], Vec::new(), vec![stable(5)]])
3991 .build(),
3992 ListGroupsCase::builder()
3993 .name("type filter Consumer on a broker below v5 is unsupported on that broker")
3994 .brokers([groups(&["a"]), below(4, &["b"]), groups(&["c"])])
3995 .options(types(&[GroupType::Consumer]))
3996 .errors(vec![(2, 35, Some(unsupported(4, 5)))])
3997 .requests([vec![consumer_type(5)], Vec::new(), vec![consumer_type(5)]])
3998 .build(),
3999 ListGroupsCase::builder()
4000 .name("type filter Classic and Consumer on a broker below v5 is omitted")
4001 .brokers([groups(&["a"]), below(4, &["b"]), groups(&["c"])])
4002 .options(types(&[GroupType::Classic, GroupType::Consumer]))
4003 .valid(vec![
4004 stable_listing("a"),
4005 GroupListing {
4006 group_type: None,
4007 ..stable_listing("b")
4008 },
4009 stable_listing("c"),
4010 ])
4011 .requests([
4012 vec![classic_and_consumer(5)],
4013 vec![classic_and_consumer(4)],
4014 vec![classic_and_consumer(5)],
4015 ])
4016 .build(),
4017 ListGroupsCase::builder()
4018 .name("protocol type filter is applied by the client")
4019 .brokers([
4020 ListGroupsBroker {
4021 max_version: 5,
4022 answers: vec![ListGroupsAnswer::Groups(vec![
4023 consumer_group("a"),
4024 listed("connect-a", "connect", "Stable", "Classic"),
4025 listed("simple", "", "Empty", "Classic"),
4026 ])],
4027 },
4028 groups(&[]),
4029 ListGroupsBroker {
4030 max_version: 5,
4031 answers: vec![ListGroupsAnswer::Groups(vec![listed(
4032 "share", "share", "Stable", "Share",
4033 )])],
4034 },
4035 ])
4036 .options(ListGroupsOptions::for_consumer_groups())
4037 .valid(vec![
4038 stable_listing("a"),
4039 GroupListing {
4040 group_id: "simple".into(),
4041 group_type: Some(GroupType::Classic),
4042 protocol_type: String::new(),
4043 group_state: Some(GroupState::Empty),
4044 },
4045 ])
4046 .requests([
4047 vec![classic_and_consumer(5)],
4048 vec![classic_and_consumer(5)],
4049 vec![classic_and_consumer(5)],
4050 ])
4051 .build(),
4052 ]
4053 }
4054
4055 #[yare::parameterized(
4057 classic_without_protocol = {
4058 Some(GroupType::Classic),
4059 "",
4060 true,
4061 },
4062 classic_with_protocol = {
4063 Some(GroupType::Classic),
4064 "consumer",
4065 false,
4066 },
4067 consumer = { Some(GroupType::Consumer), "", false },
4068 unknown = { Some(GroupType::Unknown), "", false },
4069 no_type = { None, "", false },
4070 no_type_with_protocol = { None, "consumer", false },
4071 )]
4072 fn is_simple_consumer_group_matches_kafka(
4073 group_type: Option<GroupType>,
4074 protocol_type: &str,
4075 expected: bool,
4076 ) {
4077 let listing = GroupListing {
4078 group_id: "g".into(),
4079 group_type,
4080 protocol_type: protocol_type.into(),
4081 group_state: None,
4082 };
4083 assert2::assert!(listing.is_simple_consumer_group() == expected);
4084 }
4085
4086 #[yare::parameterized(
4090 consumer = {
4091 listed("g", "consumer", "Stable", "Consumer"),
4092 GroupListing {
4093 group_id: "g".into(),
4094 group_type: Some(GroupType::Consumer),
4095 protocol_type: "consumer".into(),
4096 group_state: Some(GroupState::Stable),
4097 },
4098 },
4099 empty = {
4100 listed("g", "", "", ""),
4101 GroupListing {
4102 group_id: "g".into(),
4103 group_type: None,
4104 protocol_type: String::new(),
4105 group_state: None,
4106 },
4107 },
4108 streams = {
4109 listed("g", "consumer", "preparingrebalance", "STREAMS"),
4110 GroupListing {
4111 group_id: "g".into(),
4112 group_type: Some(GroupType::Streams),
4113 protocol_type: "consumer".into(),
4114 group_state: Some(GroupState::PreparingRebalance),
4115 },
4116 },
4117 unknown = {
4118 listed("g", "consumer", "Rebalancing", "Future"),
4119 GroupListing {
4120 group_id: "g".into(),
4121 group_type: Some(GroupType::Unknown),
4122 protocol_type: "consumer".into(),
4123 group_state: Some(GroupState::Unknown),
4124 },
4125 },
4126 )]
4127 fn group_listing_reads_listed_group(group: ListedGroup, expected: GroupListing) {
4128 assert2::assert!(GroupListing::from(group) == expected);
4129 }
4130
4131 fn api_versions_all() -> Vec<u8> {
4136 let api = |api_key, max_version| ApiVersion {
4137 api_key,
4138 min_version: 0,
4139 max_version,
4140 ..Default::default()
4141 };
4142 encode_api_versions(vec![
4143 api(api_versions_request::API_KEY, 0),
4144 api(find_coordinator_request::API_KEY, 0),
4145 api(metadata_request::API_KEY, 13),
4146 api(offset_commit_request::API_KEY, 9),
4147 api(offset_fetch_request::API_KEY, 9),
4148 api(OffsetDeleteRequest::API_KEY, 0),
4149 api(LeaveGroupRequest::API_KEY, 5),
4150 api(AlterShareGroupOffsetsRequest::API_KEY, 1),
4151 api(DeleteShareGroupOffsetsRequest::API_KEY, 0),
4152 ])
4153 }
4154
4155 fn empty_metadata_response(version: i16) -> Vec<u8> {
4156 encode(&MetadataResponse::default(), version, true)
4157 }
4158
4159 fn offset_fetch_response_bytes(version: i16) -> Vec<u8> {
4160 encode(&offset_fetch_response(version), version, true)
4161 }
4162
4163 fn empty_metadata_reply(version: i16) -> Vec<u8> {
4164 empty_metadata_response(version)
4165 }
4166
4167 fn offset_fetch_reply(version: i16) -> Vec<u8> {
4168 offset_fetch_response_bytes(version)
4169 }
4170
4171 fn counted_reply(counter: &AtomicUsize, response: Option<Vec<u8>>) -> Option<Vec<u8>> {
4172 counter.fetch_add(1, Ordering::SeqCst);
4173 response
4174 }
4175
4176 async fn connect_self_coordinating(
4186 target_api_key: i16,
4187 codes: Vec<i16>,
4188 respond: impl Fn(i16, i16) -> Vec<u8> + Send + Sync + 'static,
4189 ) -> (AdminClient, MockBroker, Arc<AtomicUsize>) {
4190 let coordinator_addr = Arc::new(Mutex::new(None::<SocketAddr>));
4191 let addr_in_mock = Arc::clone(&coordinator_addr);
4192 let script = Arc::new(Mutex::new((codes, 0_usize)));
4193 let calls = Arc::new(AtomicUsize::new(0));
4194 let calls_in_mock = Arc::clone(&calls);
4195 let broker = MockBroker::start(move |api_key, version, _, _| {
4196 if api_key == target_api_key {
4197 calls_in_mock.fetch_add(1, Ordering::SeqCst);
4198 let mut guard = script.lock().expect("script lock");
4199 let (codes, requests) = &mut *guard;
4200 let code = RetryScript::next(codes, requests);
4201 drop(guard);
4202 Some(respond(code, version))
4203 } else {
4204 match api_key {
4205 api_versions_request::API_KEY => Some(api_versions_all()),
4206 find_coordinator_request::API_KEY => {
4207 let addr = addr_in_mock
4208 .lock()
4209 .expect("coordinator lock")
4210 .expect("coordinator address set before use");
4211 Some(encode(&mock_coordinator_response(addr, 0), version, false))
4212 }
4213 metadata_request::API_KEY => Some(empty_metadata_reply(version)),
4214 _ => None,
4215 }
4216 }
4217 })
4218 .await
4219 .unwrap();
4220 *coordinator_addr.lock().expect("coordinator lock") = Some(broker.addr);
4221 let admin = connect_admin(broker.addr, "admin connects").await;
4222 (admin, broker, calls)
4223 }
4224
4225 fn successful_group_offset_outcome() -> Vec<ConsumerGroupOffsetOutcome> {
4226 vec![ConsumerGroupOffsetOutcome {
4227 topic: "orders".into(),
4228 partition: 2,
4229 error: None,
4230 }]
4231 }
4232
4233 macro_rules! successful_offset_topic {
4234 ($topic:ident, $partition:ident, $name_field:ident) => {
4235 $topic {
4236 $name_field: "orders".into(),
4237 partitions: vec![$partition {
4238 partition_index: 2,
4239 error_code: 0,
4240 ..Default::default()
4241 }],
4242 ..Default::default()
4243 }
4244 };
4245 }
4246
4247 fn successful_offset_delete_topic() -> OffsetDeleteResponseTopic {
4248 successful_offset_topic!(
4249 OffsetDeleteResponseTopic,
4250 OffsetDeleteResponsePartition,
4251 name
4252 )
4253 }
4254
4255 fn offset_delete_response(error_code: i16, version: i16) -> Vec<u8> {
4256 let topics =
4257 successful_response_rows(error_code, || vec![successful_offset_delete_topic()]);
4258 encode(
4259 &OffsetDeleteResponse {
4260 error_code,
4261 topics,
4262 ..Default::default()
4263 },
4264 version,
4265 false,
4266 )
4267 }
4268
4269 fn offset_commit_success_response(version: i16) -> Vec<u8> {
4270 encode(
4271 &OffsetCommitResponse {
4272 topics: vec![successful_offset_commit_topic()],
4273 ..Default::default()
4274 },
4275 version,
4276 true,
4277 )
4278 }
4279
4280 fn delete_share_offsets_response(error_code: i16, version: i16) -> Vec<u8> {
4281 let responses = successful_response_rows(error_code, || {
4282 vec![DeleteShareGroupOffsetsResponseTopic {
4283 topic_name: "orders".into(),
4284 error_code: 0,
4285 ..Default::default()
4286 }]
4287 });
4288 crate::encode_test_struct_fields!(
4289 encode,
4290 DeleteShareGroupOffsetsResponse,
4291 { error_code: error_code, responses: responses },
4292 version,
4293 true,
4294 )
4295 }
4296
4297 fn successful_offset_commit_topic() -> OffsetCommitResponseTopic {
4298 successful_offset_topic!(
4299 OffsetCommitResponseTopic,
4300 OffsetCommitResponsePartition,
4301 name
4302 )
4303 }
4304
4305 fn successful_share_offset_topic() -> AlterShareGroupOffsetsResponseTopic {
4306 successful_offset_topic!(
4307 AlterShareGroupOffsetsResponseTopic,
4308 AlterShareGroupOffsetsResponsePartition,
4309 topic_name
4310 )
4311 }
4312
4313 fn successful_response_rows<T>(error_code: i16, rows: impl FnOnce() -> Vec<T>) -> Vec<T> {
4314 if error_code == 0 { rows() } else { Vec::new() }
4315 }
4316
4317 fn assert_single_group_offset_success(
4318 result: Result<Vec<ConsumerGroupOffsetOutcome>, String>,
4319 calls: usize,
4320 ) {
4321 assert!((result, calls) == (Ok(successful_group_offset_outcome()), 1));
4322 }
4323
4324 struct CoordinatorRetryCase<T> {
4325 name: &'static str,
4326 codes: Vec<i16>,
4327 expected_calls: usize,
4328 expected: Result<T, i16>,
4329 }
4330
4331 #[bon::bon]
4332 impl<T> CoordinatorRetryCase<T> {
4333 #[builder]
4334 fn success(name: &'static str, codes: Vec<i16>, expected_calls: usize, outcome: T) -> Self {
4335 Self {
4336 name,
4337 codes,
4338 expected_calls,
4339 expected: Ok(outcome),
4340 }
4341 }
4342
4343 fn error(name: &'static str, code: i16) -> Self {
4344 Self {
4345 name,
4346 codes: vec![code],
4347 expected_calls: 1,
4348 expected: Err(code),
4349 }
4350 }
4351 }
4352
4353 impl<T: Clone> CoordinatorRetryCase<T> {
4354 fn standard(outcome: &T) -> Vec<Self> {
4355 vec![
4356 Self::success()
4357 .name("no error")
4358 .codes(vec![0])
4359 .expected_calls(1)
4360 .outcome(outcome.clone())
4361 .call(),
4362 Self::success()
4363 .name("coordinator load in progress retries on the same coordinator")
4364 .codes(vec![14, 0])
4365 .expected_calls(2)
4366 .outcome(outcome.clone())
4367 .call(),
4368 Self::success()
4369 .name("not coordinator finds the coordinator again")
4370 .codes(vec![16, 0])
4371 .expected_calls(2)
4372 .outcome(outcome.clone())
4373 .call(),
4374 ]
4375 }
4376
4377 fn with_rebalance_in_progress(outcome: &T) -> Vec<Self> {
4378 let mut cases = Self::standard(outcome);
4379 cases.push(
4380 Self::success()
4381 .name("rebalance in progress retries on the same coordinator")
4382 .codes(vec![27, 0])
4383 .expected_calls(2)
4384 .outcome(outcome.clone())
4385 .call(),
4386 );
4387 cases
4388 }
4389
4390 fn with_authorization_failure(mut cases: Vec<Self>) -> Vec<Self> {
4391 cases.push(Self::error("group authorization failed is final", 30));
4392 cases
4393 }
4394 }
4395
4396 #[bon::builder]
4397 async fn run_coordinator_retry_cases<T, F, Fut, Resp>(
4398 api_key: i16,
4399 respond: Resp,
4400 cases: Vec<CoordinatorRetryCase<T>>,
4401 action: F,
4402 ) where
4403 T: PartialEq + Debug,
4404 Resp: Fn(i16, i16) -> Vec<u8> + Copy + Send + Sync + 'static,
4405 F: Fn(AdminClient) -> Fut,
4406 Fut: Future<Output = Result<T, AdminError>>,
4407 {
4408 for case in cases {
4409 let (admin, broker, calls) =
4410 connect_self_coordinating(api_key, case.codes, respond).await;
4411 let result = action(admin).await.map_err(|error| match error {
4412 AdminError::Broker { code, .. } => code,
4413 other => panic!("case {}: unexpected error {other:?}", case.name),
4414 });
4415 broker.stop();
4416 assert!(
4417 (result, calls.load(Ordering::SeqCst)) == (case.expected, case.expected_calls),
4418 "case {}",
4419 case.name
4420 );
4421 }
4422 }
4423
4424 macro_rules! run_retry_cases {
4425 ($api_key:expr, $respond:expr, $cases:expr, $action:expr) => {
4426 run_coordinator_retry_cases()
4427 .api_key($api_key)
4428 .respond($respond)
4429 .cases($cases)
4430 .action($action)
4431 .call()
4432 .await
4433 };
4434 }
4435
4436 async fn delete_consumer_offsets(
4437 mut admin: AdminClient,
4438 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
4439 admin
4440 .delete_consumer_group_offsets("workers", &[("orders".into(), 2)])
4441 .await
4442 }
4443
4444 async fn alter_share_offsets(
4445 mut admin: AdminClient,
4446 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
4447 admin
4448 .alter_share_group_offsets("workers", &test_group_offsets())
4449 .await
4450 }
4451
4452 async fn delete_share_offsets(
4453 mut admin: AdminClient,
4454 ) -> Result<Vec<(String, Option<KafkaError>)>, AdminError> {
4455 admin
4456 .delete_share_group_offsets("workers", &["orders".to_owned()])
4457 .await
4458 }
4459
4460 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4465 async fn delete_consumer_group_offsets_retries_coordinator_errors_as_kafka_does() {
4466 let respond = offset_delete_response;
4467 let outcome = successful_group_offset_outcome();
4468 let mut cases = CoordinatorRetryCase::standard(&outcome);
4469 cases.push(CoordinatorRetryCase::error("non-empty group is final", 68));
4470 run_retry_cases!(
4471 OffsetDeleteRequest::API_KEY,
4472 respond,
4473 cases,
4474 delete_consumer_offsets
4475 );
4476 }
4477
4478 #[derive(Clone, Copy)]
4479 enum StreamsGroupOffsetCall {
4480 Delete,
4481 Alter,
4482 }
4483
4484 impl StreamsGroupOffsetCall {
4485 fn api_key(self) -> i16 {
4486 match self {
4487 Self::Delete => OffsetDeleteRequest::API_KEY,
4488 Self::Alter => offset_commit_request::API_KEY,
4489 }
4490 }
4491
4492 fn response(self, version: i16) -> Vec<u8> {
4493 match self {
4494 Self::Delete => offset_delete_response(0, version),
4495 Self::Alter => offset_commit_success_response(version),
4496 }
4497 }
4498
4499 async fn call(
4500 self,
4501 admin: &mut AdminClient,
4502 ) -> Result<Vec<ConsumerGroupOffsetOutcome>, AdminError> {
4503 match self {
4504 Self::Delete => {
4505 admin
4506 .delete_streams_group_offsets("workers", &[("orders".into(), 2)])
4507 .await
4508 }
4509 Self::Alter => {
4510 admin
4511 .alter_streams_group_offsets("workers", &test_group_offsets())
4512 .await
4513 }
4514 }
4515 }
4516 }
4517
4518 #[yare::parameterized(
4521 delete = { StreamsGroupOffsetCall::Delete },
4522 alter = { StreamsGroupOffsetCall::Alter },
4523 )]
4524 #[test_macro(tokio::test(flavor = "multi_thread", worker_threads = 2))]
4525 async fn streams_group_offset_calls_delegate_to_consumer_calls(call: StreamsGroupOffsetCall) {
4526 let respond = move |_code: i16, version: i16| call.response(version);
4527 let (mut admin, broker, calls) =
4528 connect_self_coordinating(call.api_key(), vec![0], respond).await;
4529 let result = call
4530 .call(&mut admin)
4531 .await
4532 .map_err(|error| error.to_string());
4533 broker.stop();
4534 assert_single_group_offset_success(result, calls.load(Ordering::SeqCst));
4535 }
4536
4537 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4543 async fn remove_members_from_consumer_group_retries_coordinator_errors_as_kafka_does() {
4544 let answered = |instance_id: &str, error_code| MemberResponse {
4545 member_id: String::new(),
4546 group_instance_id: Some(instance_id.into()),
4547 error_code,
4548 ..Default::default()
4549 };
4550 let respond = move |code: i16, version: i16| {
4551 let members = successful_response_rows(code, || {
4552 vec![answered("b", 25), answered("a", 0)]
4554 });
4555 let response = LeaveGroupResponse {
4556 error_code: code,
4557 members,
4558 ..Default::default()
4559 };
4560 encode_flexible(&response, version)
4561 };
4562 let removed = |instance_id: &str| RemovedMember {
4563 member_id: String::new(),
4564 group_instance_id: Some(instance_id.into()),
4565 };
4566 let outcome = RemovedMembers {
4567 members: maplit::btreemap! {
4568 removed("a") => None,
4569 removed("b") => Some(KafkaError {
4570 code: 25,
4571 name: "UNKNOWN_MEMBER_ID",
4572 message: None,
4573 }),
4574 },
4575 };
4576 let cases = CoordinatorRetryCase::with_authorization_failure(
4577 CoordinatorRetryCase::standard(&outcome),
4578 );
4579 run_retry_cases!(
4580 LeaveGroupRequest::API_KEY,
4581 respond,
4582 cases,
4583 |mut admin| async move {
4584 admin
4585 .remove_members_from_consumer_group(
4586 "workers",
4587 &[
4588 MemberToRemove {
4589 group_instance_id: "a".into(),
4590 },
4591 MemberToRemove {
4592 group_instance_id: "b".into(),
4593 },
4594 ],
4595 &RemoveMembersOptions::builder().build(),
4596 )
4597 .await
4598 }
4599 );
4600 }
4601
4602 #[yare::parameterized(
4606 none = { None, DEFAULT_LEAVE_GROUP_REASON.to_owned() },
4607 empty = { Some(String::new()), DEFAULT_LEAVE_GROUP_REASON.to_owned() },
4608 short = { Some("rolling restart".to_owned()), "rolling restart".to_owned() },
4609 long = { Some("r".repeat(300)), "r".repeat(255) },
4610 )]
4611 fn leave_group_reason_matches_kafka(reason: Option<String>, expected: String) {
4612 assert2::assert!(leave_group_reason(reason.as_deref()) == expected);
4613 }
4614
4615 #[test]
4619 fn removed_members_member_result_matches_kafka() {
4620 let member = |instance_id: &str| MemberToRemove {
4621 group_instance_id: instance_id.into(),
4622 };
4623 let fenced = KafkaError {
4624 code: 82,
4625 name: kafka_error_name(82),
4626 message: None,
4627 };
4628 let result = RemovedMembers {
4629 members: maplit::btreemap! {
4630 RemovedMember {
4631 member_id: String::new(),
4632 group_instance_id: Some("a".into()),
4633 } => None,
4634 RemovedMember {
4635 member_id: String::new(),
4636 group_instance_id: Some("b".into()),
4637 } => Some(fenced.clone()),
4638 },
4639 };
4640 assert!(
4641 [member("a"), member("b"), member("c")].map(|member| result.member_result(&member))
4642 == [
4643 Ok(()),
4644 Err(fenced),
4645 Err(KafkaError {
4646 code: -1,
4647 name: "UNKNOWN_SERVER_ERROR",
4648 message: Some(
4649 "Member \"c\" was not included in the removal response".into()
4650 ),
4651 }),
4652 ]
4653 );
4654 }
4655
4656 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4661 async fn alter_share_group_offsets_retries_coordinator_errors_as_kafka_does() {
4662 let respond = |code: i16, version: i16| {
4663 let responses =
4664 successful_response_rows(code, || vec![successful_share_offset_topic()]);
4665 let response = AlterShareGroupOffsetsResponse {
4666 error_code: code,
4667 responses,
4668 ..Default::default()
4669 };
4670 encode_flexible(&response, version)
4671 };
4672 let outcome = successful_group_offset_outcome();
4673 let mut cases = CoordinatorRetryCase::with_rebalance_in_progress(&outcome);
4674 cases.push(
4675 CoordinatorRetryCase::success()
4676 .name("a final group error is the error of each partition")
4677 .codes(vec![68])
4678 .expected_calls(1)
4679 .outcome(vec![ConsumerGroupOffsetOutcome {
4680 topic: "orders".into(),
4681 partition: 2,
4682 error: Some(KafkaError {
4683 code: 68,
4684 name: "NON_EMPTY_GROUP",
4685 message: None,
4686 }),
4687 }])
4688 .call(),
4689 );
4690 run_retry_cases!(
4691 AlterShareGroupOffsetsRequest::API_KEY,
4692 respond,
4693 cases,
4694 alter_share_offsets
4695 );
4696 }
4697
4698 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4701 async fn list_streams_group_offsets_delegates_to_offset_fetch() {
4702 let respond =
4703 |_code: i16, version: i16| encode(&offset_fetch_response(version), version, true);
4704 let (mut admin, broker, calls) =
4705 connect_self_coordinating(offset_fetch_request::API_KEY, vec![0], respond).await;
4706 let result = admin
4707 .list_streams_group_offsets("workers")
4708 .await
4709 .map_err(|error| error.to_string());
4710 broker.stop();
4711 assert!((result, calls.load(Ordering::SeqCst)) == (Ok(test_group_offsets()), 1));
4712 }
4713
4714 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
4719 async fn delete_share_group_offsets_retries_coordinator_errors_as_kafka_does() {
4720 let respond = delete_share_offsets_response;
4721 let outcome = vec![("orders".to_owned(), None)];
4722 let cases = CoordinatorRetryCase::with_authorization_failure(
4723 CoordinatorRetryCase::with_rebalance_in_progress(&outcome),
4724 );
4725 run_retry_cases!(
4726 DeleteShareGroupOffsetsRequest::API_KEY,
4727 respond,
4728 cases,
4729 delete_share_offsets
4730 );
4731 }
4732}