Skip to content

fix(binding-mcp-kafka): stop reset_offsets hanging after a stale stage END - #2540

Merged
jfallows merged 4 commits into
developfrom
claude/zilla-issue-2532-qz171g
Sep 4, 2026
Merged

fix(binding-mcp-kafka): stop reset_offsets hanging after a stale stage END#2540
jfallows merged 4 commits into
developfrom
claude/zilla-issue-2532-qz171g

Conversation

@jfallows

@jfallows jfallows commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes kafka__reset_offsets hanging forever against a consumer group whose state is Dead (e.g. one that has never had any members).

Root cause: KafkaApiResetOffsetsClient runs a 3-stage flow (STAGE_FIND_COORDINATORSTAGE_DESCRIBE_GROUPSSTAGE_OFFSET_COMMIT), reusing the same stage field and reassigning kafkaInitialId/kafkaReplyId on each transition. advanceToOffsetCommit() synchronously ends the DescribeGroups stream and reassigns stage = STAGE_OFFSET_COMMIT before returning, but the engine delivers that ended stream's own reply-side END on a later turn. By the time it arrives, onKafkaEnd()'s guard (stage == STAGE_OFFSET_COMMIT) is already true, so the stale END from the previous stage is misattributed to the newly-opened OffsetCommit stream, and the flow gets stuck without ever sending the offset-commit request or surfacing an error.

This was root-caused by reproducing the hang against a real Kafka broker via docker compose, with zilla dump/instrumented debug logging pinpointing the exact call sequence — not by adding a timeout as a band-aid; the connection was never actually hanging at the transport layer, only misinterpreting an internal event ordering. A sibling class (KafkaApiDescribeConsumerGroupLagClient.onKafkaEnd()) already guards correctly against this by checking end.streamId() == kafkaReplyId before honoring stage; the same guard is applied here.

Fix: onKafkaEnd() now only advances state / signals MCP completion when the END's streamId matches the current kafkaReplyId, so a stale END from a previous stage transition can no longer be misattributed to the current stage.

Test coverage added:

  • runtime/binding-kafka: new ClientOffsetCommitIT scenarios covering a bare offset-commit and the find-coordinator → describe-groups chained api-request reuse pattern, backed by new paired client.rpt/server.rpt k3po scripts at both the application and network layers.
  • runtime/binding-mcp-kafka / specs/binding-mcp-kafka.spec: new end-to-end reset_offsets scenario against a Dead consumer group, exercised both peer-to-peer (KafkaIT, self-consistency check) and against a live engine (McpKafkaClientIT#shouldResetOffsetsForDeadGroup) — this is the regression test that reproduces the original hang and confirms the fix.

All existing and new tests pass: binding-mcp-kafka (51/51 ITs, 80/80 unit tests), binding-mcp-kafka.spec (80/80 ITs), binding-kafka + binding-kafka.spec (412/412 tests). Checkstyle and license checks are clean.

Fixes #2532

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kk7te5cPXiBGfwnVH3cMiq


Generated by Claude Code

… reuse

Closes a gap toward diagnosing #2532 (mcp-kafka reset_offsets
hangs against a Dead consumer group): binding-kafka had no coverage for
KafkaClientOffsetCommitFactory's bare admin commit (generationId=-1,
memberId="", instanceId="") or for chaining multiple apiRequest-kind
streams (FindCoordinator, DescribeGroups) on one reused connection, which
is exactly how binding-mcp-kafka's reset_offsets flow drives binding-kafka.

Add ClientOffsetCommitIT scenarios exercising these paths directly against
a live engine:
- shouldCommitBareOffset: bare admin commit in isolation
- shouldFindCoordinatorOnly / shouldChainFindCoordinatorThenDescribeGroups(NoWait):
  chained apiRequest-kind streams sharing one affinity/connection, both
  waiting for and not waiting for the prior stream's END before reopening
- shouldResetOffsetsForDeadGroup: the full FindCoordinator -> DescribeGroups
  (Dead) -> bare OffsetCommit sequence end to end

All pass, which rules out KafkaClientApiFactory/KafkaClientOffsetCommitFactory's
state machines as the cause of the reported hang in the plaintext, no-SASL
case. Root cause investigation continues upstream in binding-mcp-kafka.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk7te5cPXiBGfwnVH3cMiq
…e END

Fixes #2532. Reproduced against a real Kafka broker (docker
compose) with zilla dump/logs and an instrumented build: reset_offsets
against a Dead (or Empty) consumer group correctly runs FindCoordinator
then DescribeGroups, but the actual bare OffsetCommit result never reaches
the MCP client - the broker commits successfully, yet the caller hangs
until the MCP session's own inactivity timeout closes the connection.

KafkaApiResetOffsetsClient.advanceToOffsetCommit() ends the just-finished
DescribeGroups stream and opens the OffsetCommit stream synchronously,
setting stage = STAGE_OFFSET_COMMIT before returning. The engine delivers
the DescribeGroups stream's own reply-side END acknowledgment on a later
turn, by which point onKafkaEnd() sees stage == STAGE_OFFSET_COMMIT and
misattributes that stale END as the OffsetCommit stream's own completion,
calling peer.doMcpEnd() and closing the MCP response before the real
result is ever produced (the subsequent OffsetCommit request and its
successful broker response are then orphaned).

KafkaApiDescribeConsumerGroupLagClient.onKafkaEnd() already guards against
exactly this hazard (comparing end.streamId() against the current
kafkaReplyId) for its own two-stage OffsetFetch -> ListOffsets sequence;
apply the same guard here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk7te5cPXiBGfwnVH3cMiq
…_offsets against a dead group

Adds k3po scripts and IT coverage for the mcp-kafka -> kafka offset-commit
handoff (find-coordinator -> describe-groups -> offset-commit) against a
consumer group that has never had members, exercising the full 3-stage
reset_offsets flow through a live engine (McpKafkaClientIT) as well as a
peer-to-peer self-consistency check (KafkaIT), closing the gap that let the
stale-END misattribution bug in KafkaApiResetOffsetsClient.onKafkaEnd ship
undetected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kk7te5cPXiBGfwnVH3cMiq
@jfallows
jfallows merged commit acc46d8 into develop Sep 4, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mcp-kafka: reset_offsets hangs forever when target consumer group state is Dead

2 participants