A fresh (unpinned) key arriving while no worker is routable — e.g. the
whole pool draining during a deploy overlap — was dropped from
assignment, failing the batch and restarting the process for a
transient condition. Unroutable groups are now stashed like pinned
deferrals and flushed once a worker returns, bounded by the existing
deferred-flush timeout. Keys with stashed groups but no pin keep
deferring so newer messages cannot race ahead of them.
Also expands failure-scenario coverage driven by the gap analysis from
#67447: consumer crash before commit, forced reap at drain timeout
(the e2e harness now runs the production reaper loop), lost-ACK
replay, poison 4xx batches, partial acceptance, crash mid-drain,
full-pool loss at max_in_flight>1, full fleet redeploy via membership
reconciliation, a second consumer joining the group, static-membership
fencing, headerless and non-UTF-8 messages, and post-churn asserts
that pins/in-flight/stash drain to zero.
Problem
PR #67447 fixed a drain-locality bug the test suite structurally could not see, which prompted a broader audit of failure scenarios the ingestion-consumer tests never exercise. The audit found one live routing bug plus a set of untested failure paths, some with zero e2e coverage (the reaper loop never ran in any e2e test).
The bug: a fresh (unpinned) key arriving while no worker is routable, for example when the whole pool drains during a deploy overlap, was silently dropped from assignment. The batch then fails its accepted-count check and the process exits. Pinned keys already got the defer-and-wait treatment, so a transient full-pool drain turned into a restart storm for fresh keys only.
Changes
How did you test this code?
TDD. The regression tests for the fix were written first and confirmed red: test_unroutable_fresh_key_defers_until_a_worker_returns (unit) and full_pool_drain_holds_fresh_keys_until_a_worker_returns (e2e) both failed on the unfixed dispatcher, then went green with the fix. test_fresh_key_queues_behind_its_own_deferred_groups catches the second-order ordering hazard the fix introduces (a deferring key with no pin must keep deferring).
Each new scenario test catches a regression nothing covered before, notably redelivery-without-loss after a consumer crash, reaper removal racing a live request, at-least-once behavior on ambiguous ACKs, fail-safe behavior (no commit) on poison and under-acknowledged batches, and bookkeeping leaks after churn.
Full crate suite passes with Kafka running: 151 tests across 6 suites (108 lib unit, 6 dispatcher integration, 27 e2e, 10 transport). cargo fmt clean, cargo clippy clean (the workspace patch notice is pre-existing), cargo shear clean. No manual or production testing.
Automatic notifications
Docs update
None needed. The crate README added here is the documentation.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Written by Claude (Claude Code), directed by me. I asked for an analysis of failure scenarios the tests do not cover, picked the relevant ones, and had the tests written first to confirm the red state before any fix. Two candidate code changes were rejected after closer analysis: a saturating guard in Stash::completed (underflow is unreachable since entries are removed at zero, a no-op regression test documents the invariant) and a bytes-preserving wire format for non-UTF-8 payloads (capture guarantees UTF-8 JSON, so that class goes to the follow-up DLQ design instead). The two-consumer test asserts no loss only; cross-pod ordering during rebalance is a documented follow-up (revoke-aware handoff). No repo skills were invoked.