Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The four group kinds

Kafka 4.x groups come in four kinds, described by different RPCs with different response shapes. Do not flatten them into one struct.

This is the single most likely thing to get wrong in a Kafka 4.x client, because every earlier version of Kafka had exactly one kind and every tutorial still assumes it.

KindKIPDescribed bySupported here
classicDescribeGroupsyes
consumerKIP-848ConsumerGroupDescribeyes
shareKIP-932ShareGroupDescribeyes
streamsKIP-1071StreamsGroupDescribeno schema in the codec

ListGroups reports the type per group, so the flow is: list, then dispatch to the right describe RPC per group type.

The distinction is preserved, not flattened

GroupDescription is an enum whose variants carry the fields their protocol actually has — not a struct with everything optional:

struct ClassicGroupMember; struct ConsumerGroupMember; struct ShareGroupMember;
#[derive(Debug)] enum GroupState { Empty }
enum GroupDescription {
    Classic {
        group_id: String,
        state: GroupState,
        protocol_type: String,      // "consumer", "connect", …
        protocol: String,           // the assignor the group agreed on
        members: Vec<ClassicGroupMember>,
    },
    Consumer {
        group_id: String,
        state: GroupState,
        group_epoch: i32,           // KIP-848 has epochs; classic has generations
        assignment_epoch: i32,
        assignor: String,           // server-side, not client-negotiated
        members: Vec<ConsumerGroupMember>,
    },
    Share { /* … */ },
    Unrecognized { group_id: String, group_type: String },
}

Flattening these into one struct forces every field to be Option, and then every consumer has to know which combinations are possible for which kind — which is the same knowledge, moved somewhere it cannot be checked.

The differences are real, not cosmetic. A classic group negotiates its assignor between members and has a generation; a KIP-848 consumer group has the broker choose the assignor and has a group epoch and a separate assignment epoch. There is no honest single field for "the version number of this group's membership".

Unrecognized is what makes this work on real clusters

Streams groups exist on the wire, list on any 4.1+ broker running Kafka Streams, and have no schema in kafka-protocol 0.17. See The upstream schema gap.

So the enum needs a variant meaning "this group exists, I know its id and its type, and I cannot tell you more":

enum GroupDescription {
Unrecognized { group_id: String, group_type: String },
}

A UI that hard-fails on an undescribable group is a UI that hard-fails on most real clusters. Rendering the group id with "streams group — not describable by this client" is both honest and useful; returning an error for the whole group list because one entry was a streams group is neither.

The acceptance test asserts a fourth, undescribable group type surfaces as Unrecognized rather than Err.

GroupState::Other(String)

The same reasoning one level down. The named states are Empty, PreparingRebalance, CompletingRebalance, Stable and Dead, and Other(String) carries anything else the broker says.

Group states are transmitted as strings, and Kafka adds them — KIP-848 introduced Assigning and Reconciling for consumer groups. Collapsing an unknown state into Dead or into an error would be worse than reporting the string the broker actually sent.

Offset reset differs by protocol

A trap worth naming because it fails on exactly one of the two group types, which is easy to miss if your fixture only covers one:

Group kindNon-member offset reset uses
classicgeneration_id = -1
KIP-848 consumermember_epoch = -1

Get it wrong and the broker returns ILLEGAL_GENERATION — against one kind only, while the other keeps working.

kafka-admin also refuses an offset reset when the group is not EMPTY, with a clear error, rather than letting the broker accept a commit that a live member immediately overwrites. That is a silent no-op from the operator's point of view, and a silent no-op in an admin tool is worse than a refusal.

Fixtures come from the container

Worth recording, because it is a trap in the test rather than the code.

librdkafka has no KIP-932 share-group support, so rdkafka cannot generate the share-group fixture the acceptance test requires — and it drags cmake and a C toolchain into CI besides.

The apache/kafka image already ships kafka-console-consumer.sh and kafka-console-share-consumer.sh. Driving those through testkit's exec helper reaches every group kind with zero build dependencies. rdkafka earns its place in the interop suite, where being a genuinely different client implementation is the entire point.