Bound the Kafka listener shutdown drain instead of awaiting forever (GH-3434) - #3436
Merged
Merged
Conversation
…H-3434) KafkaListener/KafkaTopicGroupListener drained the shared BackgroundReceiveLoop with Timeout.InfiniteTimeSpan in both StopAsync and Dispose. When the loop was parked in a blocking IConsumer.Consume(token) that hadn't observed cancellation, the drain — and therefore IHost.StopAsync() — never returned; the process only died on a force-kill (e.g. Kubernetes SIGKILL). HostOptions.ShutdownTimeout couldn't help because the blocking overload takes no CancellationToken. Drain with the configurable, bounded DurabilitySettings.DrainTimeout (default 30s) instead, matching the SQS (_drainTimeout) and RDBMS (_settings.DrainTimeout) listeners — Kafka was the lone Infinite outlier. BackgroundReceiveLoop.StopAsync already catches the TimeoutException and logs, so this yields clean bounded teardown; on a timed-out drain the existing _consumer.Close() then forces a wedged Consume to unwind. Plumbed through all four build sites, so per-tenant (broker-per-tenant) listeners get bounded shutdown too. Adds a BackgroundReceiveLoop regression test: an iteration that blocks ignoring its cancellation token (the wedged-Consume shape) still lets StopAsync(finite) return within budget instead of hanging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 #3434.
Problem
Since 6.6.0, an app using the Kafka transport never completes graceful shutdown —
IHost.StopAsync()blocks forever, so the process only terminates on a force-kill (e.g. KubernetesSIGKILLafter the grace period). Reproduced through 6.19.0.KafkaListener.StopAsync(andDispose, and theirKafkaTopicGroupListenertwins) drained the sharedBackgroundReceiveLoopwithTimeout.InfiniteTimeSpan. When the loop was parked inside a blockingIConsumer.Consume(token)that hadn't observed cancellation, the drain — and therefore host shutdown — never returned.HostOptions.ShutdownTimeoutcouldn't help because the blocking overload takes noCancellationToken.Fix
Drain with the configurable, bounded
DurabilitySettings.DrainTimeout(default 30s) instead ofTimeout.InfiniteTimeSpan, in bothStopAsyncandDispose. This matches the convention the SQS (_drainTimeout) and RDBMS (_settings.DrainTimeout) listeners already follow — Kafka was the loneInfiniteoutlier.BackgroundReceiveLoop.StopAsyncalready catches theTimeoutExceptionand logs, so this gives clean bounded teardown. On a timed-out drain, the existing_consumer.Close()that runs next forces a wedgedConsumeto unwind (the issue's alternative suggestion, for free). The timeout is plumbed through all four build sites, so per-tenant (broker-per-tenant) listeners get bounded shutdown as well. Configurable viaopts.Durability.DrainTimeout.Tests
Adds a
BackgroundReceiveLoopregression test: an iteration that blocks while ignoring its cancellation token (the wedged-Consumeshape) still letsStopAsync(finiteBudget)return within budget instead of hanging — an infinite await would never return.Full local
Wolverine.Kafka.Testsrun against the docker-compose broker: 230 passed, 2 skipped, 1 pre-existing[Flaky]failure (batch_processing_with_kafka.end_to_end, a batch-composition timing assertion) that fails identically on pristinemainand is unrelated to the shutdown path.🤖 Generated with Claude Code