1use std::{
13 collections::{BTreeMap, BTreeSet},
14 sync::{Mutex, PoisonError},
15};
16
17use bytes::{Buf, BufMut};
18use krabka_client_core::{ClientError, CoordinatorEndpoint};
19use krabka_protocol::{
20 Decode, Encode, ProtocolError, ProtocolRequest,
21 owned::{
22 common::consumer_group_describe_response::assignment::Assignment,
23 consumer_group_describe_request::{self, ConsumerGroupDescribeRequest},
24 consumer_group_describe_response::{
25 DescribedGroup as ConsumerDescribedGroup, Member as ConsumerMember,
26 },
27 consumer_protocol_assignment::{self, ConsumerProtocolAssignment},
28 describe_groups_request::DescribeGroupsRequest,
29 describe_groups_response::{DescribedGroup as ClassicDescribedGroup, DescribedGroupMember},
30 share_group_describe_request::ShareGroupDescribeRequest,
31 share_group_describe_response::DescribedGroup as ShareDescribedGroup,
32 streams_group_describe_request::{self, StreamsGroupDescribeRequest},
33 streams_group_describe_response::{
34 DescribedGroup as StreamsDescribedGroup, StreamsGroupDescribeResponse,
35 },
36 },
37};
38
39use crate::{
40 AdminClient, AdminError, ClusterNode, KafkaError,
41 cluster::{authorized_operations, node_with_host_port},
42 group_coordinators::{GroupOutcome, group_error_outcome, kafka_error},
43 groups::{GroupState, GroupType, list_groups_kafka_error},
44 kafka_error_name,
45 retry::is_connection_failure,
46 users::AclOperation,
47};
48
49const UNKNOWN_SERVER_ERROR: i16 = -1;
52const UNSUPPORTED_VERSION: i16 = 35;
54const GROUP_ID_NOT_FOUND: i16 = 69;
56const CONSUMER_PROTOCOL_TYPE: &str = "consumer";
59
60pub type ClassicGroupDescription = ClassicDescribedGroup;
64
65pub type ShareGroupDescription = ShareDescribedGroup;
69
70pub type StreamsGroupDescription = StreamsDescribedGroup;
74
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, bon::Builder)]
78pub struct DescribeGroupsOptions {
79 #[builder(default)]
82 pub include_authorized_operations: bool,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct MemberDescription {
89 pub member_id: String,
90 pub group_instance_id: Option<String>,
91 pub rack_id: Option<String>,
93 pub client_id: String,
94 pub host: String,
95 pub assignment: BTreeSet<(String, i32)>,
97 pub target_assignment: Option<BTreeSet<(String, i32)>>,
100 pub member_epoch: Option<i32>,
102 pub upgraded: Option<bool>,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct ConsumerGroupDescription {
112 pub group_id: String,
113 pub is_simple_consumer_group: bool,
115 pub members: Vec<MemberDescription>,
116 pub partition_assignor: String,
119 pub group_type: GroupType,
121 pub group_state: GroupState,
122 pub coordinator: ClusterNode,
124 pub authorized_operations: Option<BTreeSet<AclOperation>>,
126 pub group_epoch: Option<i32>,
128 pub target_assignment_epoch: Option<i32>,
130}
131
132pub type GroupDescriptions<T> = BTreeMap<String, Result<T, KafkaError>>;
135
136#[derive(Clone, Debug, PartialEq, Eq)]
142struct ReleasedStreamsGroupDescribe(StreamsGroupDescribeRequest);
143
144impl Encode for ReleasedStreamsGroupDescribe {
145 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
146 self.0.encode(buf, version)
147 }
148
149 fn encoded_len(&self, version: i16) -> usize {
150 self.0.encoded_len(version)
151 }
152}
153
154impl ProtocolRequest for ReleasedStreamsGroupDescribe {
155 const API_KEY: i16 = streams_group_describe_request::API_KEY;
156 const MIN_VERSION: i16 = streams_group_describe_request::MIN_VERSION;
157 const MAX_VERSION: i16 = 0;
159 const LATEST_STABLE_VERSION: i16 = Self::MAX_VERSION;
160 const FLEXIBLE_MIN: i16 = streams_group_describe_request::FLEXIBLE_MIN;
161 type Response = StreamsGroupDescribeResponse;
162}
163
164trait GroupDescribeRequest: ProtocolRequest {
165 fn for_groups(groups: Vec<String>, options: DescribeGroupsOptions) -> Self;
166}
167
168macro_rules! impl_group_describe_request {
169 ($request:ty, $groups_field:ident) => {
170 impl GroupDescribeRequest for $request {
171 fn for_groups(groups: Vec<String>, options: DescribeGroupsOptions) -> Self {
172 Self {
173 $groups_field: groups,
174 include_authorized_operations: options.include_authorized_operations,
175 ..Default::default()
176 }
177 }
178 }
179 };
180}
181
182impl_group_describe_request!(ConsumerGroupDescribeRequest, group_ids);
183impl_group_describe_request!(DescribeGroupsRequest, groups);
184impl_group_describe_request!(ShareGroupDescribeRequest, group_ids);
185
186impl GroupDescribeRequest for ReleasedStreamsGroupDescribe {
187 fn for_groups(groups: Vec<String>, options: DescribeGroupsOptions) -> Self {
188 Self(StreamsGroupDescribeRequest {
189 group_ids: groups,
190 include_authorized_operations: options.include_authorized_operations,
191 ..Default::default()
192 })
193 }
194}
195
196trait GroupDescriptionEntry: Sized {
197 fn group_id(&self) -> &str;
198 fn error_code(&self) -> i16;
199 fn error_message(self) -> Option<String>;
200 fn successful_outcome(self) -> GroupOutcome<Self>;
201
202 fn into_outcome(self) -> (String, GroupOutcome<Self>) {
203 let group_id = self.group_id().to_owned();
204 let error_code = self.error_code();
205 let outcome = if error_code == 0 {
206 self.successful_outcome()
207 } else {
208 group_error_outcome(error_code, self.error_message(), &[])
209 };
210 (group_id, outcome)
211 }
212}
213
214macro_rules! impl_group_description_entry {
215 ($entry:ty, $error_message:expr, $successful_outcome:expr) => {
216 impl GroupDescriptionEntry for $entry {
217 fn group_id(&self) -> &str {
218 &self.group_id
219 }
220
221 fn error_message(self) -> Option<String> {
222 ($error_message)(self)
223 }
224
225 fn successful_outcome(self) -> GroupOutcome<Self> {
226 ($successful_outcome)(self)
227 }
228
229 fn error_code(&self) -> i16 {
230 self.error_code
231 }
232 }
233 };
234}
235
236impl_group_description_entry!(
237 ClassicDescribedGroup,
238 |_: ClassicDescribedGroup| None,
239 |group: ClassicDescribedGroup| GroupOutcome::Done(Ok(group))
240);
241impl_group_description_entry!(
242 ShareDescribedGroup,
243 |group: ShareDescribedGroup| group.error_message,
244 |group: ShareDescribedGroup| GroupOutcome::Done(Ok(group))
245);
246impl_group_description_entry!(
247 StreamsDescribedGroup,
248 |group: StreamsDescribedGroup| group.error_message,
249 |group: StreamsDescribedGroup| {
250 if group.topology.is_none() {
251 GroupOutcome::Done(Err(KafkaError {
252 code: UNKNOWN_SERVER_ERROR,
253 name: kafka_error_name(UNKNOWN_SERVER_ERROR),
254 message: Some("Topology information is missing".to_owned()),
255 }))
256 } else {
257 GroupOutcome::Done(Ok(group))
258 }
259 }
260);
261
262trait GroupDescribeApi: GroupDescribeRequest {
263 type Description;
264
265 const METHOD_NAME: &'static str;
266
267 fn outcomes(response: Self::Response) -> BTreeMap<String, GroupOutcome<Self::Description>>;
268}
269
270macro_rules! impl_group_describe_api {
271 ($request:ty, $description:ty, $method_name:literal) => {
272 impl GroupDescribeApi for $request {
273 type Description = $description;
274
275 const METHOD_NAME: &'static str = $method_name;
276
277 fn outcomes(
278 response: Self::Response,
279 ) -> BTreeMap<String, GroupOutcome<Self::Description>> {
280 group_description_outcomes(response.groups)
281 }
282 }
283 };
284}
285
286impl_group_describe_api!(
287 DescribeGroupsRequest,
288 ClassicGroupDescription,
289 "describeClassicGroups"
290);
291impl_group_describe_api!(
292 ShareGroupDescribeRequest,
293 ShareGroupDescription,
294 "describeShareGroups"
295);
296impl_group_describe_api!(
297 ReleasedStreamsGroupDescribe,
298 StreamsGroupDescription,
299 "describeStreamsGroups"
300);
301
302fn group_description_outcomes<Group: GroupDescriptionEntry>(
303 groups: impl IntoIterator<Item = Group>,
304) -> BTreeMap<String, GroupOutcome<Group>> {
305 groups
306 .into_iter()
307 .map(GroupDescriptionEntry::into_outcome)
308 .collect()
309}
310
311fn insert_description_outcomes<Group, Description>(
312 outcomes: &mut BTreeMap<String, GroupOutcome<Description>>,
313 groups: impl IntoIterator<Item = Group>,
314 mut classify: impl FnMut(Group) -> (String, Option<GroupOutcome<Description>>),
315) {
316 for group in groups {
317 let (group_id, outcome) = classify(group);
318 if let Some(outcome) = outcome {
319 outcomes.insert(group_id, outcome);
320 }
321 }
322}
323
324fn consumer_description_outcome(
325 group: ConsumerDescribedGroup,
326 node: &ClusterNode,
327 classic_keys: &mut Vec<String>,
328 classic: &Mutex<BTreeMap<String, Option<String>>>,
329) -> (String, Option<GroupOutcome<ConsumerGroupDescription>>) {
330 let group_id = group.group_id.clone();
331 let outcome = match consumer_answer(group, node) {
332 ConsumerAnswer::Outcome(outcome) => Some(outcome),
333 ConsumerAnswer::Classic { not_found_message } => {
334 classic
335 .lock()
336 .unwrap_or_else(PoisonError::into_inner)
337 .insert(group_id.clone(), not_found_message);
338 classic_keys.push(group_id.clone());
339 None
340 }
341 };
342 (group_id, outcome)
343}
344
345fn classic_consumer_description_outcome(
346 group: ClassicDescribedGroup,
347 node: &ClusterNode,
348 classic: &Mutex<BTreeMap<String, Option<String>>>,
349) -> (String, Option<GroupOutcome<ConsumerGroupDescription>>) {
350 let group_id = group.group_id.clone();
351 let not_found_message = classic
352 .lock()
353 .unwrap_or_else(PoisonError::into_inner)
354 .get(&group_id)
355 .cloned()
356 .flatten();
357 (
358 group_id,
359 Some(classic_consumer_outcome(group, node, not_found_message)),
360 )
361}
362
363fn coordinator_node(coordinator: &CoordinatorEndpoint) -> ClusterNode {
365 node_with_host_port(
366 coordinator.node_id,
367 coordinator.host.clone(),
368 coordinator.port,
369 )
370}
371
372pub(crate) fn request_error_outcomes<V>(
377 keys: &[String],
378 error: &AdminError,
379) -> BTreeMap<String, GroupOutcome<V>> {
380 keys.iter()
381 .map(|key| {
382 let outcome = if is_connection_failure(error) {
383 GroupOutcome::Unmap(error.to_string())
384 } else {
385 GroupOutcome::Done(Err(list_groups_kafka_error(error)))
386 };
387 (key.clone(), outcome)
388 })
389 .collect()
390}
391
392fn consumer_assignment(assignment: Assignment) -> BTreeSet<(String, i32)> {
394 assignment_partitions(
395 assignment
396 .topic_partitions
397 .into_iter()
398 .map(|topic| (topic.topic_name, topic.partitions)),
399 )
400}
401
402fn assignment_partitions(
403 topics: impl IntoIterator<Item = (String, Vec<i32>)>,
404) -> BTreeSet<(String, i32)> {
405 topics
406 .into_iter()
407 .fold(BTreeSet::new(), |mut result, (name, partitions)| {
408 result.extend(
409 partitions
410 .into_iter()
411 .map(|partition| (name.clone(), partition)),
412 );
413 result
414 })
415}
416
417fn consumer_member(member: ConsumerMember) -> MemberDescription {
420 MemberDescription {
421 member_id: member.member_id,
422 group_instance_id: member.instance_id,
423 rack_id: member.rack_id,
424 client_id: member.client_id,
425 host: member.client_host,
426 assignment: consumer_assignment(member.assignment),
427 target_assignment: Some(consumer_assignment(member.target_assignment)),
428 member_epoch: Some(member.member_epoch),
429 upgraded: (member.member_type != -1).then_some(member.member_type == 1),
430 }
431}
432
433fn classic_assignment(mut bytes: &[u8]) -> Result<BTreeSet<(String, i32)>, String> {
438 if bytes.is_empty() {
439 return Ok(BTreeSet::new());
440 }
441 if bytes.len() < 2 {
442 return Err("Buffer underflow while parsing consumer protocol's header".to_owned());
443 }
444 let version = bytes.get_i16();
445 if version < consumer_protocol_assignment::MIN_VERSION {
446 return Err(format!("Unsupported assignment version: {version}"));
447 }
448 let version = version.min(consumer_protocol_assignment::MAX_VERSION);
449 let assignment = ConsumerProtocolAssignment::decode(&mut bytes, version)
450 .map_err(|error| format!("Error reading consumer assignment: {error}"))?;
451 Ok(assignment_partitions(
452 assignment
453 .assigned_partitions
454 .into_iter()
455 .map(|topic| (topic.topic, topic.partitions)),
456 ))
457}
458
459fn classic_member(member: DescribedGroupMember) -> Result<MemberDescription, String> {
462 Ok(MemberDescription {
463 assignment: classic_assignment(&member.member_assignment)?,
464 member_id: member.member_id,
465 group_instance_id: member.group_instance_id,
466 rack_id: None,
467 client_id: member.client_id,
468 host: member.client_host,
469 target_assignment: None,
470 member_epoch: None,
471 upgraded: None,
472 })
473}
474
475#[derive(Debug, Clone, PartialEq, Eq)]
477enum ConsumerAnswer {
478 Outcome(GroupOutcome<ConsumerGroupDescription>),
479 Classic {
483 not_found_message: Option<String>,
484 },
485}
486
487fn consumer_answer(group: ConsumerDescribedGroup, coordinator: &ClusterNode) -> ConsumerAnswer {
492 match group.error_code {
493 0 => ConsumerAnswer::Outcome(GroupOutcome::Done(Ok(ConsumerGroupDescription {
494 group_id: group.group_id,
495 is_simple_consumer_group: false,
496 members: group.members.into_iter().map(consumer_member).collect(),
497 partition_assignor: group.assignor_name,
498 group_type: GroupType::Consumer,
499 group_state: GroupState::parse(&group.group_state),
500 coordinator: coordinator.clone(),
501 authorized_operations: authorized_operations(group.authorized_operations),
502 group_epoch: Some(group.group_epoch),
503 target_assignment_epoch: Some(group.assignment_epoch),
504 }))),
505 UNSUPPORTED_VERSION => ConsumerAnswer::Classic {
506 not_found_message: None,
507 },
508 GROUP_ID_NOT_FOUND => ConsumerAnswer::Classic {
509 not_found_message: group.error_message,
510 },
511 code => ConsumerAnswer::Outcome(group_error_outcome(code, group.error_message, &[])),
512 }
513}
514
515fn classic_consumer_outcome(
521 group: ClassicDescribedGroup,
522 coordinator: &ClusterNode,
523 not_found_message: Option<String>,
524) -> GroupOutcome<ConsumerGroupDescription> {
525 match group.error_code {
526 0 => {}
527 GROUP_ID_NOT_FOUND => {
528 return GroupOutcome::Done(Err(kafka_error(
529 GROUP_ID_NOT_FOUND,
530 not_found_message.or(group.error_message),
531 )));
532 }
533 code => return group_error_outcome(code, group.error_message, &[]),
534 }
535 if group.protocol_type != CONSUMER_PROTOCOL_TYPE && !group.protocol_type.is_empty() {
536 return GroupOutcome::Done(Err(kafka_error(
537 UNKNOWN_SERVER_ERROR,
538 Some(format!(
539 "GroupId {} is not a consumer group ({}).",
540 group.group_id, group.protocol_type
541 )),
542 )));
543 }
544 let members = match group
545 .members
546 .into_iter()
547 .map(classic_member)
548 .collect::<Result<Vec<_>, _>>()
549 {
550 Ok(members) => members,
551 Err(message) => {
552 return GroupOutcome::Done(Err(kafka_error(UNKNOWN_SERVER_ERROR, Some(message))));
553 }
554 };
555 GroupOutcome::Done(Ok(ConsumerGroupDescription {
556 is_simple_consumer_group: group.protocol_type.is_empty(),
557 group_id: group.group_id,
558 members,
559 partition_assignor: group.protocol_data,
560 group_type: GroupType::Classic,
561 group_state: GroupState::parse(&group.group_state),
562 coordinator: coordinator.clone(),
563 authorized_operations: authorized_operations(group.authorized_operations),
564 group_epoch: None,
565 target_assignment_epoch: None,
566 }))
567}
568
569impl AdminClient {
570 pub async fn describe_consumer_groups(
597 &self,
598 groups: &[&str],
599 options: DescribeGroupsOptions,
600 ) -> GroupDescriptions<ConsumerGroupDescription> {
601 let classic = Mutex::new(BTreeMap::<String, Option<String>>::new());
602 self.call_group_coordinators("describeConsumerGroups", groups, |coordinator, keys| {
603 self.describe_consumer_groups_on(coordinator, keys, options, &classic)
604 })
605 .await
606 }
607
608 async fn describe_consumer_groups_on(
612 &self,
613 coordinator: CoordinatorEndpoint,
614 keys: Vec<String>,
615 options: DescribeGroupsOptions,
616 classic: &Mutex<BTreeMap<String, Option<String>>>,
617 ) -> Result<BTreeMap<String, GroupOutcome<ConsumerGroupDescription>>, AdminError> {
618 let connection = self.connect_coordinator(&coordinator).await?;
619 let node = coordinator_node(&coordinator);
620 let (classic_keys, consumer_keys): (Vec<_>, Vec<_>) = {
621 let classic = classic.lock().unwrap_or_else(PoisonError::into_inner);
622 keys.into_iter().partition(|key| classic.contains_key(key))
623 };
624 let mut outcomes = BTreeMap::new();
625 let mut classic_keys = classic_keys;
626 let consumer_describe_supported = connection
631 .advertised_api_range(consumer_group_describe_request::API_KEY)
632 .is_some();
633 if !consumer_describe_supported {
634 let mut classic = classic.lock().unwrap_or_else(PoisonError::into_inner);
635 for key in &consumer_keys {
636 classic.entry(key.clone()).or_insert(None);
637 }
638 drop(classic);
639 classic_keys.extend(consumer_keys.iter().cloned());
640 } else if !consumer_keys.is_empty() {
641 let request = ConsumerGroupDescribeRequest::for_groups(consumer_keys.clone(), options);
642 match connection.send(request).await {
643 Ok(response) => {
644 insert_description_outcomes(&mut outcomes, response.groups, |group| {
645 consumer_description_outcome(group, &node, &mut classic_keys, classic)
646 });
647 }
648 Err(ClientError::IncompatibleVersion { .. }) => {
651 let mut classic = classic.lock().unwrap_or_else(PoisonError::into_inner);
652 for key in &consumer_keys {
653 classic.entry(key.clone()).or_insert(None);
654 }
655 drop(classic);
656 classic_keys.extend(consumer_keys);
657 }
658 Err(error) => {
659 let error = AdminError::from(error);
660 if is_connection_failure(&error) {
661 return Err(error);
662 }
663 outcomes.extend(request_error_outcomes(&consumer_keys, &error));
664 }
665 }
666 }
667 if classic_keys.is_empty() {
668 return Ok(outcomes);
669 }
670 let request = DescribeGroupsRequest::for_groups(classic_keys.clone(), options);
671 match connection.send(request).await {
672 Ok(response) => {
673 insert_description_outcomes(&mut outcomes, response.groups, |group| {
674 classic_consumer_description_outcome(group, &node, classic)
675 });
676 }
677 Err(error) => {
678 outcomes.extend(request_error_outcomes(&classic_keys, &error.into()));
679 }
680 }
681 Ok(outcomes)
682 }
683
684 pub async fn describe_classic_groups(
697 &self,
698 groups: &[&str],
699 options: DescribeGroupsOptions,
700 ) -> GroupDescriptions<ClassicGroupDescription> {
701 self.describe_group_api::<DescribeGroupsRequest>(groups, options)
702 .await
703 }
704
705 pub async fn describe_share_groups(
718 &self,
719 groups: &[&str],
720 options: DescribeGroupsOptions,
721 ) -> GroupDescriptions<ShareGroupDescription> {
722 self.describe_group_api::<ShareGroupDescribeRequest>(groups, options)
723 .await
724 }
725
726 pub async fn describe_streams_groups(
741 &self,
742 groups: &[&str],
743 options: DescribeGroupsOptions,
744 ) -> GroupDescriptions<StreamsGroupDescription> {
745 self.describe_group_api::<ReleasedStreamsGroupDescribe>(groups, options)
746 .await
747 }
748
749 async fn describe_group_api<Request: GroupDescribeApi>(
750 &self,
751 groups: &[&str],
752 options: DescribeGroupsOptions,
753 ) -> GroupDescriptions<Request::Description> {
754 self.call_group_coordinators(
755 Request::METHOD_NAME,
756 groups,
757 |coordinator, keys| async move {
758 let connection = self.connect_coordinator(&coordinator).await?;
759 let request = Request::for_groups(keys.clone(), options);
760 Ok(match connection.send(request).await {
761 Ok(response) => Request::outcomes(response),
762 Err(error) => request_error_outcomes(&keys, &error.into()),
763 })
764 },
765 )
766 .await
767 }
768}
769
770#[cfg(test)]
773mod tests {
774 use std::{
775 net::SocketAddr,
776 sync::{Arc, Mutex},
777 };
778
779 use assert2::assert;
780 use bytes::BytesMut;
781 use krabka_client_core::MockReply;
782 use krabka_protocol::owned::{
783 common::consumer_group_describe_response::topic_partitions::TopicPartitions,
784 consumer_group_describe_response::ConsumerGroupDescribeResponse,
785 consumer_protocol_assignment::TopicPartition as AssignedTopic, describe_groups_request,
786 describe_groups_response::DescribeGroupsResponse, share_group_describe_request,
787 share_group_describe_response::ShareGroupDescribeResponse,
788 streams_group_describe_response::Topology,
789 };
790
791 use super::*;
792 use crate::{
793 group_coordinators::test_support::{GroupCluster, Lookup, Seen, group_cluster},
794 partition_leaders::test_support::{ApiVersionRange, decode_request, encode_response},
795 };
796
797 fn api_range(api_key: i16, max_version: i16) -> ApiVersionRange {
798 ApiVersionRange::builder()
799 .api_key(api_key)
800 .max_version(max_version)
801 .build()
802 }
803
804 fn mock_response(bytes: Vec<u8>) -> MockReply {
805 MockReply::Respond(bytes)
806 }
807
808 fn classic_response(groups: Vec<ClassicDescribedGroup>) -> DescribeGroupsResponse {
809 DescribeGroupsResponse {
810 groups,
811 ..DescribeGroupsResponse::default()
812 }
813 }
814
815 const FALLBACK_GROUPS: [&str; 5] = ["kip848", "classic", "other", "missing", "connect"];
816 const STREAMS_GROUPS: [&str; 2] = ["with", "without"];
817
818 struct AdminGroupCluster {
819 cluster: GroupCluster,
820 admin: AdminClient,
821 }
822
823 macro_rules! described_group {
824 ($entry:ident, $error_code:expr, $error_message:expr $(, $field:ident = $value:expr)* $(,)?) => {
825 $entry {
826 error_code: $error_code,
827 error_message: $error_message,
828 group_id: "workers".to_owned(),
829 $($field: $value,)*
830 ..Default::default()
831 }
832 };
833 }
834
835 macro_rules! encode_described_group {
836 ($response:ident, $entry:ident, $code:expr, $message:expr, $version:expr, $flexible:expr $(, $field:ident = $value:expr)* $(,)?) => {
837 crate::encode_test_struct_field!(
838 encode_response,
839 $response,
840 groups,
841 vec![described_group!($entry, $code, $message $(, $field = $value)*)],
842 $version,
843 $flexible,
844 )
845 };
846 }
847
848 #[bon::builder]
849 async fn admin_group_cluster(
850 apis: Vec<ApiVersionRange>,
851 answer: impl FnMut(usize, i16, i16, &[u8], SocketAddr) -> MockReply + Send + 'static,
852 ) -> AdminGroupCluster {
853 let cluster = group_cluster()
854 .count(1)
855 .apis(apis)
856 .lookup(|_, _| Lookup::At(0))
857 .answer(answer)
858 .call()
859 .await;
860 let admin = cluster.admin(krabka_units::secs(5)).await;
861 AdminGroupCluster { cluster, admin }
862 }
863
864 const OMITTED: i32 = i32::MIN;
865
866 fn assignment_bytes(version: i16, partitions: &[(&str, &[i32])]) -> bytes::Bytes {
867 let mut bytes = BytesMut::new();
868 bytes.put_i16(version);
869 ConsumerProtocolAssignment {
870 assigned_partitions: partitions
871 .iter()
872 .map(|(topic, partitions)| AssignedTopic {
873 topic: (*topic).to_owned(),
874 partitions: partitions.to_vec(),
875 ..Default::default()
876 })
877 .collect(),
878 ..Default::default()
879 }
880 .encode(&mut bytes, version.min(3))
881 .expect("assignment encodes");
882 bytes.freeze()
883 }
884
885 #[yare::parameterized(
886 empty = { bytes::Bytes::new(), Ok(BTreeSet::new()) },
887 v0 = { assignment_bytes(0, &[("orders", &[0, 2])]), Ok(maplit::btreeset! {
888 ("orders".to_owned(), 0),
889 ("orders".to_owned(), 2),
890 }) },
891 newer_version = { assignment_bytes(9, &[("orders", &[1])]), Ok(maplit::btreeset! {
892 ("orders".to_owned(), 1),
893 }) },
894 negative_version = { bytes::Bytes::from_static(&[0xff, 0xff]), Err("Unsupported assignment version: -1".to_owned()) },
895 truncated_header = { bytes::Bytes::from_static(&[0]), Err("Buffer underflow while parsing consumer protocol's header".to_owned()) },
896 )]
897 fn classic_assignment_reads_as_kafka_does(
898 bytes: bytes::Bytes,
899 expected: Result<BTreeSet<(String, i32)>, String>,
900 ) {
901 assert2::assert!(classic_assignment(&bytes) == expected);
902 }
903
904 fn node(addr: SocketAddr, id: i32) -> ClusterNode {
905 node_with_host_port(id, addr.ip().to_string(), i32::from(addr.port()))
906 }
907
908 fn nodes(addrs: [SocketAddr; 2]) -> [ClusterNode; 2] {
909 [node(addrs[0], 0), node(addrs[1], 1)]
910 }
911
912 fn consumer_entry(
913 group_id: &str,
914 error_code: i16,
915 message: Option<&str>,
916 ) -> ConsumerDescribedGroup {
917 ConsumerDescribedGroup {
918 error_code,
919 error_message: message.map(str::to_owned),
920 group_id: group_id.to_owned(),
921 group_state: "Stable".to_owned(),
922 group_epoch: 4,
923 assignment_epoch: 3,
924 assignor_name: "uniform".to_owned(),
925 members: vec![ConsumerMember {
926 member_id: "m1".to_owned(),
927 client_id: "c1".to_owned(),
928 client_host: "/10.0.0.1".to_owned(),
929 member_epoch: 4,
930 assignment: Assignment {
931 topic_partitions: vec![TopicPartitions {
932 topic_name: "orders".to_owned(),
933 partitions: vec![0],
934 ..Default::default()
935 }],
936 ..Default::default()
937 },
938 member_type: 1,
939 ..Default::default()
940 }],
941 authorized_operations: OMITTED,
942 ..Default::default()
943 }
944 }
945
946 fn classic_entry(
947 group_id: &str,
948 error_code: i16,
949 protocol_type: &str,
950 ) -> ClassicDescribedGroup {
951 ClassicDescribedGroup {
952 error_code,
953 group_id: group_id.to_owned(),
954 group_state: "Stable".to_owned(),
955 protocol_type: protocol_type.to_owned(),
956 protocol_data: "range".to_owned(),
957 members: vec![DescribedGroupMember {
958 member_id: "m2".to_owned(),
959 group_instance_id: Some("static-1".to_owned()),
960 client_id: "c2".to_owned(),
961 client_host: "/10.0.0.2".to_owned(),
962 member_assignment: assignment_bytes(1, &[("orders", &[1])]),
963 ..Default::default()
964 }],
965 authorized_operations: 1 << 3,
966 ..Default::default()
967 }
968 }
969
970 struct GroupDescriptionFields {
971 member: MemberDescription,
972 partition_assignor: String,
973 group_type: GroupType,
974 authorized_operations: Option<BTreeSet<AclOperation>>,
975 group_epoch: Option<i32>,
976 target_assignment_epoch: Option<i32>,
977 }
978
979 #[bon::builder]
980 fn group_description(
981 group_id: String,
982 coordinator: ClusterNode,
983 fields: GroupDescriptionFields,
984 ) -> ConsumerGroupDescription {
985 ConsumerGroupDescription {
986 group_id,
987 is_simple_consumer_group: false,
988 members: vec![fields.member],
989 partition_assignor: fields.partition_assignor,
990 group_type: fields.group_type,
991 group_state: GroupState::Stable,
992 coordinator,
993 authorized_operations: fields.authorized_operations,
994 group_epoch: fields.group_epoch,
995 target_assignment_epoch: fields.target_assignment_epoch,
996 }
997 }
998
999 #[derive(Clone, Copy)]
1000 enum GroupDescriptionKind {
1001 Consumer,
1002 Classic,
1003 }
1004
1005 #[bon::builder]
1006 fn member_description(
1007 member_id: String,
1008 group_instance_id: Option<String>,
1009 client_id: String,
1010 host: String,
1011 assignment: BTreeSet<(String, i32)>,
1012 target_assignment: Option<BTreeSet<(String, i32)>>,
1013 member_epoch: Option<i32>,
1014 upgraded: Option<bool>,
1015 ) -> MemberDescription {
1016 MemberDescription {
1017 member_id,
1018 group_instance_id,
1019 rack_id: None,
1020 client_id,
1021 host,
1022 assignment,
1023 target_assignment,
1024 member_epoch,
1025 upgraded,
1026 }
1027 }
1028
1029 fn group_description_fields(kind: GroupDescriptionKind) -> GroupDescriptionFields {
1030 match kind {
1031 GroupDescriptionKind::Consumer => GroupDescriptionFields {
1032 member: member_description()
1033 .member_id("m1".to_owned())
1034 .maybe_group_instance_id(None)
1035 .client_id("c1".to_owned())
1036 .host("/10.0.0.1".to_owned())
1037 .assignment(maplit::btreeset! {
1038 ("orders".to_owned(), 0),
1039 })
1040 .maybe_target_assignment(Some(BTreeSet::new()))
1041 .maybe_member_epoch(Some(4))
1042 .maybe_upgraded(Some(true))
1043 .call(),
1044 partition_assignor: "uniform".to_owned(),
1045 group_type: GroupType::Consumer,
1046 authorized_operations: None,
1047 group_epoch: Some(4),
1048 target_assignment_epoch: Some(3),
1049 },
1050 GroupDescriptionKind::Classic => GroupDescriptionFields {
1051 member: member_description()
1052 .member_id("m2".to_owned())
1053 .maybe_group_instance_id(Some("static-1".to_owned()))
1054 .client_id("c2".to_owned())
1055 .host("/10.0.0.2".to_owned())
1056 .assignment(maplit::btreeset! {
1057 ("orders".to_owned(), 1),
1058 })
1059 .maybe_target_assignment(None)
1060 .maybe_member_epoch(None)
1061 .maybe_upgraded(None)
1062 .call(),
1063 partition_assignor: "range".to_owned(),
1064 group_type: GroupType::Classic,
1065 authorized_operations: Some(maplit::btreeset! {
1066 AclOperation::Read,
1067 }),
1068 group_epoch: None,
1069 target_assignment_epoch: None,
1070 },
1071 }
1072 }
1073
1074 fn test_group_description(
1075 group_id: &str,
1076 coordinator: ClusterNode,
1077 kind: GroupDescriptionKind,
1078 ) -> ConsumerGroupDescription {
1079 let fields = group_description_fields(kind);
1080 group_description()
1081 .group_id(group_id.to_owned())
1082 .coordinator(coordinator)
1083 .fields(fields)
1084 .call()
1085 }
1086
1087 fn classic_group_description(coordinator: ClusterNode) -> ConsumerGroupDescription {
1088 test_group_description("classic", coordinator, GroupDescriptionKind::Classic)
1089 }
1090
1091 #[derive(Clone, Copy, Debug)]
1092 enum DescribeApiCall {
1093 Classic,
1094 Share,
1095 Streams,
1096 }
1097
1098 async fn describe_one_group(
1099 call: DescribeApiCall,
1100 admin: &AdminClient,
1101 options: DescribeGroupsOptions,
1102 ) -> Option<Result<(), KafkaError>> {
1103 match call {
1104 DescribeApiCall::Classic => admin
1105 .describe_classic_groups(&["workers"], options)
1106 .await
1107 .remove("workers")
1108 .map(|result| result.map(drop)),
1109 DescribeApiCall::Share => admin
1110 .describe_share_groups(&["workers"], options)
1111 .await
1112 .remove("workers")
1113 .map(|result| result.map(drop)),
1114 DescribeApiCall::Streams => admin
1115 .describe_streams_groups(&["workers"], options)
1116 .await
1117 .remove("workers")
1118 .map(|result| result.map(drop)),
1119 }
1120 }
1121
1122 fn default_describe_options() -> DescribeGroupsOptions {
1123 DescribeGroupsOptions::builder().build()
1124 }
1125
1126 async fn default_streams_descriptions(
1127 admin: &AdminClient,
1128 groups: &[&str],
1129 ) -> GroupDescriptions<StreamsGroupDescription> {
1130 admin
1131 .describe_streams_groups(groups, default_describe_options())
1132 .await
1133 }
1134
1135 fn requests_of<T>(
1138 seen: &Seen,
1139 api_key: i16,
1140 read: impl Fn(&[u8], i16) -> T,
1141 ) -> Vec<(usize, i16, T)> {
1142 let mut requests = seen
1143 .requests
1144 .iter()
1145 .filter(|request| request.api_key == api_key)
1146 .map(|request| {
1147 (
1148 request.broker,
1149 request.version,
1150 read(&request.body, request.version),
1151 )
1152 })
1153 .collect::<Vec<_>>();
1154 requests.sort_by_key(|(broker, _, _)| *broker);
1156 requests
1157 }
1158
1159 fn consumer_describe_ids(body: &[u8], version: i16) -> Vec<String> {
1160 decode_request::<ConsumerGroupDescribeRequest>(body, version, true).group_ids
1161 }
1162
1163 fn describe_groups_ids(body: &[u8], version: i16) -> Vec<String> {
1164 decode_request::<DescribeGroupsRequest>(body, version, version >= 5).groups
1165 }
1166
1167 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1174 async fn describe_consumer_groups_batches_and_falls_back_to_describe_groups() {
1175 let cluster: GroupCluster = group_cluster()
1178 .count(2)
1179 .apis(vec![
1180 api_range(consumer_group_describe_request::API_KEY, 1),
1181 api_range(describe_groups_request::API_KEY, 5),
1182 ])
1183 .lookup(|group, _| Lookup::At(usize::from(!matches!(group, "kip848" | "classic"))))
1184 .answer(|_, api_key, version, body, _| {
1185 let reply = match api_key {
1186 consumer_group_describe_request::API_KEY => {
1187 let ids = consumer_describe_ids(body, version);
1188 let groups = ids
1189 .iter()
1190 .map(|id| match id.as_str() {
1191 "kip848" | "other" => consumer_entry(id, 0, None),
1192 "classic" | "connect" => {
1193 consumer_entry(id, 69, Some("Group is not a consumer group."))
1194 }
1195 "missing" => {
1196 consumer_entry(id, 69, Some("Group missing does not exist."))
1197 }
1198 _ => consumer_entry(id, 35, None),
1199 })
1200 .collect();
1201 crate::encode_test_struct_field!(
1202 encode_response,
1203 ConsumerGroupDescribeResponse,
1204 groups,
1205 groups,
1206 version,
1207 true
1208 )
1209 }
1210 describe_groups_request::API_KEY => {
1211 let ids = describe_groups_ids(body, version);
1212 let groups = ids
1213 .iter()
1214 .map(|id| match id.as_str() {
1215 "classic" => classic_entry(id, 0, "consumer"),
1216 "connect" => classic_entry(id, 0, "connect"),
1217 _ => classic_entry(id, 69, ""),
1218 })
1219 .collect();
1220 encode_response(&classic_response(groups), version, version >= 5)
1221 }
1222 _ => return MockReply::Silent,
1223 };
1224 mock_response(reply)
1225 })
1226 .call()
1227 .await;
1228 let admin = cluster.admin(krabka_units::secs(5)).await;
1229
1230 let result = admin
1231 .describe_consumer_groups(&FALLBACK_GROUPS, default_describe_options())
1232 .await;
1233
1234 let [b0, b1] = nodes([cluster.addrs[0], cluster.addrs[1]]);
1235 let seen = cluster.stop();
1236 assert2::assert!(
1237 result
1238 == maplit::btreemap! {
1239 "kip848".to_owned() => Ok(test_group_description(
1240 "kip848",
1241 b0.clone(),
1242 GroupDescriptionKind::Consumer,
1243 )),
1244 "classic".to_owned() => Ok(classic_group_description(b0)),
1245 "other".to_owned() => Ok(test_group_description(
1246 "other",
1247 b1,
1248 GroupDescriptionKind::Consumer,
1249 )),
1250 "missing".to_owned() => Err(kafka_error(
1251 69,
1252 Some("Group missing does not exist.".to_owned())
1253 )),
1254 "connect".to_owned() => Err(kafka_error(
1255 -1,
1256 Some("GroupId connect is not a consumer group (connect).".to_owned())
1257 )),
1258 }
1259 );
1260 let ids = |ids: &[&str]| ids.iter().map(|id| (*id).to_owned()).collect::<Vec<_>>();
1261 assert!(
1262 (
1263 seen.find_coordinator.clone(),
1264 requests_of(
1265 &seen,
1266 consumer_group_describe_request::API_KEY,
1267 consumer_describe_ids
1268 ),
1269 requests_of(&seen, describe_groups_request::API_KEY, describe_groups_ids),
1270 ) == (
1271 vec![(
1272 6,
1273 ids(&["classic", "connect", "kip848", "missing", "other"])
1274 )],
1275 vec![
1276 (0, 1, ids(&["classic", "kip848"])),
1277 (1, 1, ids(&["connect", "missing", "other"])),
1278 ],
1279 vec![
1280 (0, 5, ids(&["classic"])),
1281 (1, 5, ids(&["connect", "missing"]))
1282 ],
1283 )
1284 );
1285 }
1286
1287 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1291 async fn describe_consumer_groups_uses_describe_groups_on_an_old_coordinator() {
1292 let AdminGroupCluster { cluster, admin } = admin_group_cluster()
1293 .apis(vec![api_range(describe_groups_request::API_KEY, 5)])
1294 .answer(|_, api_key, version, body, _| {
1295 if api_key != describe_groups_request::API_KEY {
1296 return MockReply::Silent;
1297 }
1298 let groups = describe_groups_ids(body, version)
1299 .iter()
1300 .map(|id| classic_entry(id, 0, "consumer"))
1301 .collect();
1302 mock_response(encode_response(&classic_response(groups), version, true))
1303 })
1304 .call()
1305 .await;
1306
1307 let result = admin
1308 .describe_consumer_groups(&["classic"], default_describe_options())
1309 .await;
1310
1311 let first_broker = cluster.addrs[0];
1312 let _seen = cluster.stop();
1313 let b0 = node(first_broker, 0);
1314 assert!(
1315 result
1316 == maplit::btreemap! {
1317 "classic".to_owned() => Ok(classic_group_description(b0)),
1318 }
1319 );
1320 }
1321
1322 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1326 async fn describe_consumer_groups_falls_back_when_no_version_is_common() {
1327 let AdminGroupCluster { cluster, admin } = admin_group_cluster()
1328 .apis(vec![
1329 ApiVersionRange::builder()
1330 .api_key(consumer_group_describe_request::API_KEY)
1331 .min_version(50)
1332 .max_version(50)
1333 .build(),
1334 api_range(describe_groups_request::API_KEY, 5),
1335 ])
1336 .answer(|_, api_key, version, body, _| {
1337 assert!(api_key == describe_groups_request::API_KEY);
1339 let groups = describe_groups_ids(body, version)
1340 .iter()
1341 .map(|id| classic_entry(id, 0, "consumer"))
1342 .collect();
1343 mock_response(encode_response(&classic_response(groups), version, true))
1344 })
1345 .call()
1346 .await;
1347
1348 let result = admin
1349 .describe_consumer_groups(&["classic"], default_describe_options())
1350 .await;
1351
1352 let first_broker = cluster.addrs[0];
1353 let _seen = cluster.stop();
1354 let b0 = node(first_broker, 0);
1355 assert!(
1356 result
1357 == maplit::btreemap! {
1358 "classic".to_owned() => Ok(classic_group_description(b0)),
1359 }
1360 );
1361 }
1362
1363 #[yare::parameterized(
1369 classic_no_error = { DescribeApiCall::Classic, "no error", vec![0], 1, 1, Ok(()) },
1370 classic_load = { DescribeApiCall::Classic, "load in progress", vec![14, 0], 1, 2, Ok(()) },
1371 classic_not_coordinator = { DescribeApiCall::Classic, "not coordinator", vec![16, 0], 2, 2, Ok(()) },
1372 classic_not_available = { DescribeApiCall::Classic, "not available", vec![15, 0], 2, 2, Ok(()) },
1373 classic_authorization = { DescribeApiCall::Classic, "group authorization failed", vec![30], 1, 1, Err(30) },
1374 classic_not_found = { DescribeApiCall::Classic, "group id not found", vec![69], 1, 1, Err(69) },
1375 share_no_error = { DescribeApiCall::Share, "no error", vec![0], 1, 1, Ok(()) },
1376 share_load = { DescribeApiCall::Share, "load in progress", vec![14, 0], 1, 2, Ok(()) },
1377 share_not_coordinator = { DescribeApiCall::Share, "not coordinator", vec![16, 0], 2, 2, Ok(()) },
1378 share_not_available = { DescribeApiCall::Share, "not available", vec![15, 0], 2, 2, Ok(()) },
1379 share_authorization = { DescribeApiCall::Share, "group authorization failed", vec![30], 1, 1, Err(30) },
1380 share_not_found = { DescribeApiCall::Share, "group id not found", vec![69], 1, 1, Err(69) },
1381 streams_no_error = { DescribeApiCall::Streams, "no error", vec![0], 1, 1, Ok(()) },
1382 streams_load = { DescribeApiCall::Streams, "load in progress", vec![14, 0], 1, 2, Ok(()) },
1383 streams_not_coordinator = { DescribeApiCall::Streams, "not coordinator", vec![16, 0], 2, 2, Ok(()) },
1384 streams_not_available = { DescribeApiCall::Streams, "not available", vec![15, 0], 2, 2, Ok(()) },
1385 streams_authorization = { DescribeApiCall::Streams, "group authorization failed", vec![30], 1, 1, Err(30) },
1386 streams_not_found = { DescribeApiCall::Streams, "group id not found", vec![69], 1, 1, Err(69) },
1387 )]
1388 #[test_macro(tokio::test(flavor = "multi_thread", worker_threads = 2))]
1389 async fn describe_calls_retry_coordinator_errors_as_kafka_does(
1390 call: DescribeApiCall,
1391 name: &'static str,
1392 codes: Vec<i16>,
1393 expected_lookups: usize,
1394 expected_requests: usize,
1395 expected: Result<(), i16>,
1396 ) {
1397 let message = Some("from the broker".to_owned());
1398 let api_key = match call {
1399 DescribeApiCall::Classic => describe_groups_request::API_KEY,
1400 DescribeApiCall::Share => share_group_describe_request::API_KEY,
1401 DescribeApiCall::Streams => streams_group_describe_request::API_KEY,
1402 };
1403 let script = Arc::new(Mutex::new(codes));
1404 let reply_message = message.clone();
1405 let cluster = group_cluster()
1406 .count(1)
1407 .apis(vec![api_range(api_key, 6)])
1408 .lookup(|_, _| Lookup::At(0))
1409 .answer(move |_, key, version, _, _| {
1410 if key != api_key {
1411 return MockReply::Silent;
1412 }
1413 let code = {
1414 let mut script = script.lock().expect("script lock");
1415 if script.len() > 1 {
1416 script.remove(0)
1417 } else {
1418 script[0]
1419 }
1420 };
1421 let body = match call {
1422 DescribeApiCall::Classic => encode_described_group!(
1423 DescribeGroupsResponse,
1424 ClassicDescribedGroup,
1425 code,
1426 reply_message.clone(),
1427 version,
1428 version >= 5,
1429 ),
1430 DescribeApiCall::Share => encode_described_group!(
1431 ShareGroupDescribeResponse,
1432 ShareDescribedGroup,
1433 code,
1434 reply_message.clone(),
1435 version,
1436 true,
1437 ),
1438 DescribeApiCall::Streams => encode_described_group!(
1439 StreamsGroupDescribeResponse,
1440 StreamsDescribedGroup,
1441 code,
1442 reply_message.clone(),
1443 version,
1444 true,
1445 topology = Some(Topology::default()),
1446 ),
1447 };
1448 mock_response(body)
1449 })
1450 .call()
1451 .await;
1452 let admin = cluster.admin(krabka_units::secs(5)).await;
1453 let options = DescribeGroupsOptions::builder()
1454 .include_authorized_operations(true)
1455 .build();
1456 let result = describe_one_group(call, &admin, options).await;
1457 let seen = cluster.stop();
1458 let expected_message = match call {
1459 DescribeApiCall::Classic => None,
1460 DescribeApiCall::Share | DescribeApiCall::Streams => message.clone(),
1461 };
1462 let flags = seen
1463 .requests
1464 .iter()
1465 .map(|request| {
1466 let (version, body) = (request.version, &request.body[..]);
1467 match call {
1468 DescribeApiCall::Classic => {
1469 decode_request::<DescribeGroupsRequest>(body, version, version >= 5)
1470 .include_authorized_operations
1471 }
1472 DescribeApiCall::Share => {
1473 decode_request::<ShareGroupDescribeRequest>(body, version, true)
1474 .include_authorized_operations
1475 }
1476 DescribeApiCall::Streams => {
1477 decode_request::<StreamsGroupDescribeRequest>(body, version, true)
1478 .include_authorized_operations
1479 }
1480 }
1481 })
1482 .collect::<Vec<_>>();
1483 assert2::assert!(
1484 (result, seen.find_coordinator.len(), flags)
1485 == (
1486 Some(expected.map_err(|code| kafka_error(code, expected_message))),
1487 expected_lookups,
1488 vec![true; expected_requests],
1489 ),
1490 "case {call:?} {name}"
1491 );
1492 }
1493
1494 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1497 async fn describe_streams_groups_sends_v0_and_needs_a_topology() {
1498 let AdminGroupCluster { cluster, admin } = admin_group_cluster()
1499 .apis(vec![api_range(streams_group_describe_request::API_KEY, 1)])
1500 .answer(|_, api_key, version, _, _| {
1501 if api_key != streams_group_describe_request::API_KEY {
1502 return MockReply::Silent;
1503 }
1504 mock_response(crate::encode_test_struct_field!(
1505 encode_response,
1506 StreamsGroupDescribeResponse,
1507 groups,
1508 vec![
1509 StreamsDescribedGroup {
1510 group_id: "with".to_owned(),
1511 topology: Some(Topology::default()),
1512 ..Default::default()
1513 },
1514 StreamsDescribedGroup {
1515 group_id: "without".to_owned(),
1516 ..Default::default()
1517 },
1518 ],
1519 version,
1520 true,
1521 ))
1522 })
1523 .call()
1524 .await;
1525
1526 let result = default_streams_descriptions(&admin, &STREAMS_GROUPS).await;
1527
1528 let seen = cluster.stop();
1529 let versions = seen
1530 .requests
1531 .iter()
1532 .map(|request| request.version)
1533 .collect::<Vec<_>>();
1534 assert!(
1535 (result, versions)
1536 == (
1537 maplit::btreemap! {
1538 "with".to_owned() => Ok(StreamsDescribedGroup {
1539 group_id: "with".to_owned(),
1540 topology: Some(Topology::default()),
1541 ..Default::default()
1542 }),
1543 "without".to_owned() => Err(kafka_error(
1544 -1,
1545 Some("Topology information is missing".to_owned())
1546 )),
1547 },
1548 vec![0],
1549 )
1550 );
1551 }
1552}