Consumer Groups
Rafka P3.A ships single-member consumer groups — the full seven-API consumer-group protocol with committed offsets stored in broker RAM. Multi-member rebalance (P3.C) and durable offset storage on the WAL (P3.B) are the next phases.
Consumer group identity — cgp_<ULID>
Every consumer group is a first-class entity in its organisation. When a consumer first joins a group, the broker mints a stable ConsumerGroupId:
cgp_01ktb7wm5jvkbfgpzvfnk3kqqb
The prefix cgp_ (consumer-group-pod) prevents cross-entity confusion with topics (top_), organisations, and other entity classes. The ULID suffix is lexicographically sortable and globally unique.
The id is minted once and is stable. If member1 joins order-processor, commits offsets, leaves, and member2 later joins the same group (same group.id string, same org), the group keeps the same cgp_id. Offsets survive the leave.
Group entity tier
Consumer groups are org-tier entities. They have no environment or cluster qualifier — they belong to an organisation, not to a specific deployment. This matches how Kafka treats group.id: as a durable, cross-session identity.
The RRL (Rafka Resource Locator) for a group is:
<org>/consumer-groups/<slug>
where <slug> is derived from the group.id string by Slug::sanitize(group_id).
Note: RRLs contain slashes, not colons. They are never placed in a URL path segment.
Offset semantics — P3.A (in-memory)
Committed offsets in P3.A are stored entirely in broker RAM. They are:
- Committed on
OffsetCommit: stored immediately in an in-memoryHashMapkeyed by(org_id, group_id, topic, partition). - Resumed on rejoin: when a new consumer instance joins the same group and calls
OffsetFetchon join, the broker returns the committed offset. This is the resume guarantee. - Lost on broker restart: because offsets are in memory, a broker restart vaporises all committed offsets. Consumers that rejoin after a restart will receive
-1(no committed offset) and fall back toauto.offset.reset.
P3.B (next phase) writes committed offsets to the SingleWal, making them durable across restarts.
Uncommitted partition
When a group has never committed an offset for a (topic, partition) — or the broker was restarted — OffsetFetch returns committed_offset=-1 with error_code=0. librdkafka interprets -1 as "no committed offset" and falls back to the auto.offset.reset policy (earliest = offset 0, latest = high-watermark).
Rebalance — single-member one-shot (P3.A)
For single-member groups, the rebalance is immediate:
JoinGrouparrives. Broker inserts the member → statePreparingRebalance.- Because
waiters.len() == members.len()(single member),complete_join_phasefires synchronously. generation_idincrements; member becomes leader; state →CompletingRebalance.- Broker responds to
JoinGroupwith theJoinResult(leader gets full member list). SyncGrouparrives (leader assigns its own partitions). State →Stable.
There is no timer or polling loop in this path — the transition is event-driven via a Tokio oneshot channel. The gateway-broker rail has no time cap (1 MiB size cap only), so the JoinGroup response can be held until rebalance completes without a timeout.
Multi-member rebalance (P3.C) introduces a configurable RAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS debounce window. In P3.A this defaults to 0 (immediate completion).
Group state machine
┌──────────────────────────────────────────┐
join (single member) │ │
───────────────┼───▶ PreparingRebalance │
▲ │ │ │
│ │ complete_join_phase │
Empty ◀─────────────┼──── CompletingRebalance │
(last leave) │ │ sync (leader) │
│ ─────▼───── │
cgp_id, offsets │ │ Stable │◀── heartbeat (ok) │
persist here ────────┼──▶ └─────┬─────┘ │
│ │ │
│ leave (was Stable, members > 0) │
│ │ │
│ PreparingRebalance (again) │
└──────────────────────────────────────────┘
States:
- Empty — group exists in the broker map; offsets are preserved; no members.
- PreparingRebalance — rebalance in progress; members are waiting for
JoinResult. - CompletingRebalance —
JoinResultdelivered; waiting forSyncGroup. - Stable — all members have received assignments; heartbeats are accepted.
- Dead — reserved; not currently triggered in P3.A (no expiry path yet).
What survives a leave
When the last member calls LeaveGroup, the group's state transitions to Empty but the group entry and its offsets are NOT deleted from the broker's in-memory maps. A subsequent JoinGroup for the same (org_id, group.id) reuses the existing entry — same cgp_id, same committed offsets — and increments generation_id from where the previous session left off.
This is the "offset persistence across member leave" invariant that makes the P3.A rejoin guarantee possible.