Conversation
…rship (JITSU-214) A batch consumer that dropped out of its consumer group kept running every period, polled nothing, and looked like an idle topic while events piled up (262k messages waiting on one topic when found). Nothing detected the lost membership, and the ways out were themselves broken: the paused heartbeat ran at exactly half of max.poll.interval.ms with no margin, and restartConsumer created the replacement while the old consumer with the same static group.instance.id was still closing, so the broker fenced one of them. - ConsumeAll treats a settled consumer with no assignment and topic lag as having lost membership, and restarts it. Batch topics are sharded per instance, so such a consumer is its group's only member. - Retry mode keeps its own rule: one missed assignment is a rebalance, several runs in a row is a zombie. - restartConsumer creates the new consumer first under a fresh group.instance.id suffix, swaps it in, and closes the old one after a quarantine that outlasts any in-flight poll. Closes are idempotent; suspend only nils the pointer it looked at. - Fatal and non-retriable ReadMessage errors restart the consumer instead of leaving a dead one in place. - The paused heartbeat and the in-batch pause timer run at a third of max.poll.interval.ms. - A missing committed offset logs at info; a real Committed() failure stays an error. New membership_lost and query_committed_failed metric labels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
…mer (PR review) Keying it off any consumer's creation time meant a fatal error on the first consumer, or on one started after a suspend, was skipped and left an unusable handle in place until some later run happened to retry. The cooldown exists to suppress a duplicate restart of a consumer another restart just installed, so it now keys off that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
… object (PR review) Retirement can land between the pre-publish check and the swap, with close() running in between: it takes the old consumer out and closes it, and nothing would ever close the one just published. Re-check after the swap and close the replacement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q
There was a problem hiding this comment.
Reviewed the Kafka batch/retry consumer membership-recovery changes, including restart serialization, paused-consumer handling, offset commit recovery, and retirement races. I found no additional actionable issues. Existing resolved review threads were checked and not re-raised.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes
JITSU-214: batch consumers that lost their Kafka consumer-group membership kept running every period, polled nothing, and looked like idle topics while events piled up (262k waiting on one topic when found). The only trace wasFailed to query commited offsets.once the broker garbage-collected the empty group a week later.How a consumer became a zombie
ConsumeAllsaw a high watermark, polled for 5 s, got nothing, and returned silently.ReadMessageerror inside a batch only ended the batch. librdkafka marks the instance inoperable after a fatal error, so every later poll just timed out.restartConsumerclosed the old consumer asynchronously and created the new one after a fixed delay, with the same staticgroup.instance.id. When the close was still in flight, the broker fenced one of them (FENCED_INSTANCE_ID, fatal) — 6 occurrences in a week of logs.max.poll.interval.ms; one poll waiting its full timeout plus scheduling delay overshot and librdkafka left the group — 96 occurrences in a week.Changes (
bulkerapp/app/abstract_batch_consumer.go,batch_consumer.go,retry_consumer.go)ConsumeAll, a consumer that was not created in this run, is older than a grace period (heartbeat interval + session timeout, ~145 s with defaults), has no partition assignment, and sees lag on the topic is treated as having lost membership:membership_lostmetric + error log, then a synchronous restart and the run continues on the new consumer. Valid because batch topics are sharded per bulker instance, so a batch consumer is its group's only member for its partition. Retry mode is deliberately excluded: those consumers share a group across the fleet, so a missing assignment is a routine rebalance and restarting would amplify it. A zombied retry consumer still recovers through the read-error path below.restartConsumercreates the new consumer first, under a freshgroup.instance.idsuffix (-rN), swaps it in, and closes the old one after a quarantine that outlasts any in-flight poll. Two static members with different instance ids cannot fence each other; the broker hands the partition over when the old member's session times out. Every close goes through an idempotentcloseConsumer(confluent-kafka-go panics on a doubleClose), the suspend path only nils the pointer it looked at, restarts are serialized, and a consumer younger than a session timeout is not restarted again. Init failures retry as before.ReadMessageerror in the batch or retry read loop triggers an async restart (onReadError), and errors that mean "left the group" (MAX_POLL_EXCEEDED,FENCED_INSTANCE_ID,UNKNOWN_MEMBER_ID, any fatal) are logged as membership loss with the metric, in the read loops and in the paused heartbeat.max.poll.interval.ms(paused heartbeat and the in-batch pause timer during slow loads).Committed()failure stays error with aquery_committed_failedmetric.pauseKafkaConsumerandresume, which could dereference nil during a restart.Metrics are on the existing
ConsumerErrorscounter as newerrorTypelabels:membership_lost,query_committed_failed.Testing
go build ./...,go vet, and unit tests for the pure helpers (hasLag,membershipLossReason). No integration test: reproducing group deletion or fencing needs a broker with a tiny offsets retention and timing control. Verification is in production: after deploy,membership_lostshould fire once per zombie and the topics listed in the issue should drain; the remediation restart ofbulker3-7/11/13is no longer needed.🤖 Generated with Claude Code
https://claude.ai/code/session_01ARxaC6w4JJ8Xw4b8Pr6b5Q