Repository navigation
fix: retry transactional requests that fail with RequestTimedOutError - #3192
Merged
dpkp merged 1 commit intoNov 11, 2026
Merged
Conversation
dpkp
reviewed
Nov 11, 2026
| if isinstance(response_or_exc, Errors.KafkaConnectionError): | ||
| # A request timeout closes the connection with RequestTimedOutError, failing every | ||
| # request in flight on it. Retry it like any other disconnect (as the Java client does). | ||
| if isinstance(response_or_exc, (Errors.KafkaConnectionError, Errors.RequestTimedOutError)): |
Owner
There was a problem hiding this comment.
We might also consider making RequestTimedOutError a subclass of KafkaConnectionError.
Owner
|
Thanks for the find! |
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.
Description
Related to #3181 and #3182. #3182 makes the producer request a new id after
reset_producer_id(); this makes that request survive a request timeout.Root Cause
TxnRequestHandler.on_completere-enqueues a request that failed withKafkaConnectionErrorand treats every other exception as fatal(L859-L871). A client-side request timeout closes the connection with
RequestTimedOutError(net/connection.py L190-L191), and
fail_in_flight_requests(L245) fails every request in flight on it with that error.
When an in-retry batch expires, the sender calls
reset_producer_id()(sender.py L276-L277), which since #3182returns the manager to
UNINITIALIZED, andinit_producer_id()sends a newInitProducerId. If the broker is still not answering, a request on the same connection can reachrequest_timeout_mswhile thatInitProducerIdis in flight.The connection is closed, the
InitProducerIdfails withRequestTimedOutError, andon_completecallsfatal_error():Fatal Error handling request InitProducerIdRequest: ... RequestTimedOutError. The producer stays inFATAL_ERRORwithout a producer id, nothing is sent afterwards, and only a new producer recovers. The broker never rejected the request.Observed end to end with
kafka-python 3.0.13in a service against a Kafka 3.8 cluster in Docker, with one in-sync follower frozen (docker pause) for166 s: the batch expired at120 s, the newInitProducerIdwas killed by a requesttimeout, and topic offsets stayed flat for 10 minutes after the cluster was healthy again.
The Java client handles this as a disconnect:
InFlightRequest.timedOut()is documented "A timed out request is considered disconnected as well" (NetworkClient.javaL1860-L1864), and
TxnRequestHandler.onCompleteretries onresponse.wasDisconnected()with the log line"Disconnected from {}. Will retry." that this code mirrors (TransactionManager.java
L1469-L1473).
Changes
RequestTimedOutErrorinTxnRequestHandler.on_completeexactly likeKafkaConnectionError(including the coordinator lookup when the handler needs one). No new retry path: a request timeout already closes the connection,and a closed connection is already retried.
on_completeis shared by allTxnRequestHandlers, so they get the same treatment.test_idempotent_producer_reinit_retries_after_request_timeoutintest/producer/test_transaction_manager_mock_broker.py. It uses theMockBrokerharness and the real client request timeout, with no injected exception: theidempotent producer acquires an id, then
reset_producer_id()andinit_producer_id()run as in the sender; the broker never answers theInitProducerId, so the client'srequest_timeout_ms(300 ms) fires. The test asserts the manageris not fatal and the handler is re-enqueued, then answers the retry and asserts the manager reaches
READYwith the new producer id._make_clienttakes an optionalrequest_timeout_ms(default unchanged, 5000).Test Evidence
master(3fa6acf) the new test fails atassert not tm.has_fatal_error()(5 of 5 runs). With this change it passes (20 of 20 runs, about 0.35 s).pytest test/producer test/test_mock_broker.py(334 passed)repro_request_timeout_reinit.py(attached) runs the same scenario without pytest and needs no cluster. Run it from the root of a checkout: onmasterit printsRESULT: BUG REPRODUCEDand exits 1; with this change it printsRESULT: FIXEDand exits 0. The outputs of both runs are attached.RequestTimedOutErrorobtained a new producer id and resumed in the end-to-end setup above. That build also retriesNodeNotReadyErrorandKafkaTimeoutError; the narrower change proposed here was validated withthe test and script, not in that service. I could not trigger the fatal state with a bare
KafkaProducerscript against a Docker cluster at shortened timeouts: it depends on a request timing out while the newInitProducerIdis inflight, which is why the test and script control that timing directly.
Diagram and Reproducibility Script
Not changed (open question, not needed for this fix)
Other exceptions believed to be able to reach
on_complete, such asNodeNotReadyErrorandKafkaTimeoutError, still end infatal_error(). I left them out because I have no reproduction for either with default settings, and I wanted this change to cover only the failure I could show.Cancelledis likely better left fatal, since it can arrive while the producer is closing. If you prefer parity with Java's broaderwasDisconnected()handling, I am happy to extend the change in a follow-up or made by the maintainer(s)Prepared with AI assistance (Claude), reviewed for correctness before submission