You are viewing a plain text version of this content. The canonical link for it is here.
Posted to jira@kafka.apache.org by "Chia-Ping Tsai (Jira)" <ji...@apache.org> on 2025/12/16 19:42:00 UTC
[jira] [Commented] (KAFKA-19999) Transaction coordinator livelock caused by invalid producer epoch
[ https://issues.apache.org/jira/browse/KAFKA-19999?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18045610#comment-18045610 ]
Chia-Ping Tsai commented on KAFKA-19999:
----------------------------------------
ping [~junrao] [~jolshan]
> Transaction coordinator livelock caused by invalid producer epoch
> -----------------------------------------------------------------
>
> Key: KAFKA-19999
> URL: https://issues.apache.org/jira/browse/KAFKA-19999
> Project: Kafka
> Issue Type: Bug
> Reporter: Chia-Ping Tsai
> Assignee: Chia-Ping Tsai
> Priority: Blocker
> Fix For: 4.2.0, 4.0.2, 4.1.2
>
>
> *case 1: during recovery*
> When a Transaction Coordinator fails over and reloads transactions in PREPARE_COMMIT or PREPARE_ABORT state from the transaction log, it currently reuses the logged producer epoch to send the transaction markers.
> In Transaction V2, brokers enforce strict epoch monotonicity for control batches (markers), requiring marker_epoch > current_epoch. Consequently, the broker rejects the recovery marker with InvalidProducerEpochException.
> {code:java}
> private void checkProducerEpoch(short producerEpoch, long offset, short transactionVersion) {
> short current = updatedEntry.producerEpoch();
> boolean invalidEpoch = (transactionVersion >= 2) ? (producerEpoch <= current) : (producerEpoch < current);
> if (invalidEpoch) {
> String comparison = (transactionVersion >= 2) ? "<=" : "<";
> String message = "Epoch of producer " + producerId + " at offset " + offset + " in " + topicPartition +
> " is " + producerEpoch + ", which is " + comparison + " the last seen epoch " + current +
> " (TV" + transactionVersion + ")";
> if (origin == AppendOrigin.REPLICATION) {
> log.warn(message);
> } else {
> // Starting from 2.7, we replaced ProducerFenced error with InvalidProducerEpoch in the
> // producer send response callback to differentiate from the former fatal exception,
> // letting client abort the ongoing transaction and retry.
> throw new InvalidProducerEpochException(message);
> }
> }
> }
> {code}
> The coordinator handles this error by removing the pending transaction from memory without writing a COMPLETE state to the transaction log. This leaves the transaction permanently hanging in the PREPARE state. Clients attempting to continue or commit offsets subsequently fail with CONCURRENT_TRANSACTIONS in an infinite retry loop.
> {code:scala}
> case Errors.INVALID_PRODUCER_EPOCH |
> Errors.TRANSACTION_COORDINATOR_FENCED => // producer or coordinator epoch has changed, this txn can now be ignored
> info(s"Sending $transactionalId's transaction marker for partition $topicPartition has permanently failed with error ${error.exceptionName} " +
> s"with the current coordinator epoch ${epochAndMetadata.coordinatorEpoch}; cancel sending any more transaction markers $txnMarker to the brokers")
> txnMarkerChannelManager.removeMarkersForTxn(pendingCompleteTxn)
> abortSending = true
> {code}
> *case 2: due to disconnection*
> If a WriteTxnMarkersRequest is persisted by a partition leader but the response is lost (e.g., due to disconnection), the Transaction Coordinator (TC) retries the request using the same producer epoch.
> If the target broker restarts and restores its state before receiving the retry, it rejects the request with InvalidProducerEpochException.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)