Optionally commit offsets synchronously on partition revoke#1502
Draft
sideeffffect wants to merge 1 commit into
Draft
Optionally commit offsets synchronously on partition revoke#1502sideeffffect wants to merge 1 commit into
sideeffffect wants to merge 1 commit into
Conversation
…voke Adds an opt-in `ConsumerSettings.withCommitOnRevoke` flag (default `false`, so existing behavior is unchanged). When enabled, the latest requested commit offset per partition is tracked, and on `ConsumerRebalanceListener.onPartitionsRevoked` those offsets are committed synchronously via `commitSync` — directly on the polling thread (not via the single-threaded blocking context, which would deadlock). This narrows the window for duplicate record processing across consumer-group rebalances and shutdowns under at-least-once delivery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cc7d2a9 to
a9dc791
Compare
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.
Summary
Adds an opt-in
ConsumerSettings.withCommitOnRevokeflag (defaultfalse) that, when enabled, synchronously commits the most recently requested offsets for the partitions being revoked — from insideConsumerRebalanceListener.onPartitionsRevoked, while the consumer still owns them.This narrows the window for duplicate record processing across consumer-group rebalances and shutdowns under at-least-once delivery.
Motivation
After a user calls
.commiton aCommittableOffset, the offset is committed asynchronously (KafkaConsumer.commitAsyncvia the consumer actor). An async commit is only delivered/acknowledged on a subsequentpoll/commitSync. If a rebalance or shutdown happens before that acknowledgement, the offset is never durably committed, the partition is reassigned, and those already-processed records are re-delivered to the new owner — duplicates.RebalanceRevokeMode.Gracefulwaits for in-flight stream processing to finish before releasing partitions, but it does not, on its own, force the pending commits to land — so it doesn't fully prevent these duplicates. This is the duplicate-on-revoke behaviour discussed in #1448 (and related to #931 / theRebalanceRevokeModework in #1399).Approach
ConsumerSettings.withCommitOnRevoke(Boolean)— defaultfalse, so behaviour and overhead are unchanged unless opted in.commitadditionally records the latest requested offset per partition inState.requestedCommitOffsets(keeping the highest offset perTopicPartition).onPartitionsRevokeddrains the revoked partitions' tracked offsets and commits them withcommitSyncbefore returning — i.e. while the consumer still owns them.The tricky part: where the commit runs
Kafka invokes
onPartitionsRevokedon the consumer's polling thread, from insidepoll. ThecommitSyncis therefore performed through a new internalWithConsumer.synchronouslyDuringRebalance— a direct, reentrant call on the underlyingKafkaConsumeron that same thread, which Kafka explicitly permits from within a rebalance callback. Routing it through the normal single-threaded blocking context instead would submit to the executor already occupied by the in-progresspolland deadlock. (State access is bounced onto the effect runtime, sinceAtomicCellis thread-safe; only the consumer call itself must stay on the polling thread.)Failures are logged and swallowed: this is an optimisation over the default async-commit behaviour, and the consumer remains at-least-once.
Limitations / open questions
.commit. Records that were consumed but whose commit is still batched (e.g.commitBatchWithin), or not yet processed, are still re-delivered. It narrows the duplicate window rather than eliminating it; for stronger guarantees it should be combined withRebalanceRevokeMode.Gracefuland/or idempotent processing.TopicPartitionand only the revoked subset is committed, so it should be correct there, but I haven't exercised that path.commitOnRevokeonConsumerSettingsthe right place, or would you prefer this folded intoRebalanceRevokeMode?KafkaConsumerSpecexercises the end-to-end revoke-commit path and guards the reentrant-commit deadlock. I'm happy to strengthen it into a deterministic duplicate-vs-no-duplicate assertion if you can point me at the preferred way to induce the race in the test harness.Note
I'm aware #1448 was closed as not planned. This PR deliberately scopes to a best-effort, opt-in mitigation of the duplicate-on-revoke case rather than attempting exactly-once or a rebalance-protocol rewrite. If you'd rather not carry it, no worries — flagging it for discussion.
🤖 Generated with Claude Code