Skip to content

KAFKA-20785: Return PRODUCER_FENCED when EndTxn commit races with coordinator-side abort - #23204

Merged
jolshan merged 4 commits into
apache:trunkfrom
suzhiking:KAFKA-20785-endtxn-timeout-race
Aug 31, 2026
Merged

KAFKA-20785: Return PRODUCER_FENCED when EndTxn commit races with coordinator-side abort#23204
jolshan merged 4 commits into
apache:trunkfrom
suzhiking:KAFKA-20785-endtxn-timeout-race

Conversation

@suzhiking

@suzhiking suzhiking commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Under transactions V2, when the transaction coordinator aborts an open
transaction on its own (e.g. after transaction.timeout.ms elapses),
the abort bumps the producer epoch. A commit that was already in flight
when the abort completed arrives with the pre-abort epoch, which matches
the V2 EndTxn retry condition (stored epoch == request epoch + 1). It
therefore passes the epoch check and fails the state check instead: the
coordinator logs "state is COMPLETE_ABORT, but received transaction
marker result to send: COMMIT" and returns INVALID_TXN_STATE, which
producer clients treat as unconditionally fatal. Kafka Streams cannot
recover from it and the affected StreamThread dies, even though the
transaction was fully rolled back server-side and the commit never took
effect.

Under transactions V1 the same race fails the strict epoch check and
returns PRODUCER_FENCED, which applications such as Kafka Streams
handle gracefully by rebalancing; the fatal outcome is V2-only.

This change returns the recoverable PRODUCER_FENCED instead of
INVALID_TXN_STATE when a commit arrives in COMPLETE_ABORT at a retry
epoch (both the epoch-bump and the producer-id-overflow variants). Only
a coordinator-initiated abort can produce this combination: a client
retrying its own EndTxn always carries the operation it originally sent,
so a COMMIT at the pre-abort epoch means the producer never requested
the abort. PRODUCER_FENCED matches what V1 returns for this race and
follows the existing convention that transactional requests return
PRODUCER_FENCED on epoch mismatches while produce requests return
INVALID_PRODUCER_EPOCH; the producer client folds
INVALID_PRODUCER_EPOCH into ProducerFencedException on EndTxn
responses anyway, so the two codes behave identically for clients —
Kafka Streams recovers by rebalancing, restoring the V1 behavior for
this race. A commit in COMPLETE_ABORT at the current epoch still
returns INVALID_TXN_STATE, since that indicates a client-side bug
rather than this race. Only transactions-V2 clients reach this code
path, so no protocol or version-gating changes are needed.

This is the EndTxn-path analogue of KAFKA-19690 (#20534), which made the
same correction on the Produce path for producers that were mid-produce
when the timeout abort landed, returning INVALID_PRODUCER_EPOCH per
the produce-path convention.

Reviewers: Justine Olshan jolshan@confluent.io

…ith coordinator-side abort

Under transactions V2, when the transaction coordinator aborts an open
transaction on its own (e.g. after transaction.timeout.ms elapses), the
abort bumps the producer epoch. A commit that was already in flight when
the abort completed arrives with the pre-abort epoch, which matches the
V2 EndTxn retry condition (stored epoch == request epoch + 1), so it
passes the epoch check and fails the state check instead: the
coordinator returns INVALID_TXN_STATE, which producer clients treat as
unconditionally fatal. Kafka Streams cannot recover from it and the
affected StreamThread dies, even though the transaction was fully rolled
back server-side and the commit never took effect. Under transactions V1
the same race fails the strict epoch check and returns PRODUCER_FENCED,
which applications such as Kafka Streams handle by rebalancing; the
fatal outcome is V2-only.

Return the recoverable INVALID_PRODUCER_EPOCH instead of
INVALID_TXN_STATE for a commit that arrives in COMPLETE_ABORT at a retry
epoch (epoch-bump or producer-id-overflow variant). Only a
coordinator-initiated abort can produce this combination, since a client
retrying its own EndTxn always carries the operation it originally sent.
Producer clients map INVALID_PRODUCER_EPOCH on the EndTxn response to
ProducerFencedException, which Kafka Streams recovers from by
rebalancing, restoring the V1 behavior for this race. A commit in
COMPLETE_ABORT at the current epoch still returns INVALID_TXN_STATE, as
that indicates a client-side bug rather than this race.

This is the EndTxn-path analogue of KAFKA-19690, which made the same
correction on the Produce path.
@github-actions github-actions Bot added triage PRs from the community core Kafka Broker small Small PRs labels Aug 20, 2026
@suzhiking
suzhiking marked this pull request as ready for review August 20, 2026 06:04
@github-actions

Copy link
Copy Markdown

A label of 'needs-attention' was automatically added to this PR in order to raise the
attention of the committers. Once this issue has been triaged, the triage label
should be removed to prevent this automation from happening again.

@jolshan
jolshan self-requested a review August 28, 2026 17:52
@jolshan jolshan added transactions Transactions and EOS and removed triage PRs from the community needs-attention labels Aug 28, 2026
transaction on its own (e.g. when it exceeds transaction.timeout.ms), bumping the epoch without the
producer's knowledge. A commit that was already in flight when such an abort completed arrives with the
pre-abort epoch and is indistinguishable from a retry. The commit is guaranteed not to have taken effect,
so the recoverable INVALID_PRODUCER_EPOCH is returned, matching the transaction V1 behavior for this race,

@jolshan jolshan Aug 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to confirm the reasoning of IPE vs PF here -- it's because in TV1 we return IPE?
Is the error returned from a different part of the code (ie, in the log code right before the write happens)? I didn't see IPE used in this file for TV1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. That's my bad wording, V1 does not return IPE, "matching the transaction V1 behavior" is supposed to mean V1 returns PF, which gets handled the same as IPE by client.

I picked IPE here to (a) tell the same story as the produce path for the same event, and (b) keep the semantic distinction: PF implies a newer producer took the transactional.id, which isn't what happened in a timeout abort. On the EndTxn response path the client already folds IPE into ProducerFencedException (EndTxnHandler), so the two codes behave identically for all TV2 clients. I can change it to PF if you'd prefer that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think there was an informal standard that only produce requests returned IPE, and all transactional requests return PF. I don't think it matters a ton in the grand scheme of things, but maybe its easier to keep things consistent.

My understanding for PF, is just that the producer was fenced, which in the server-side abort scenario, it is 😄 , but I see how the comment for the error could be confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, that makes sense. I've changed returned error from IPE to PF

…ommit/abort race

The previous comments said the error choice matches transaction V1
behavior, which misread as V1 returning INVALID_PRODUCER_EPOCH here. V1
actually fails this race at its strict epoch check with PRODUCER_FENCED;
what the fix restores is the recoverable outcome, not the error code.
State that explicitly in the state-table note, along with why
INVALID_PRODUCER_EPOCH is used instead of PRODUCER_FENCED: no newer
producer exists (the epoch is merely stale), it mirrors KAFKA-19690's
produce-path fix in UnifiedLog, and producer clients treat the two
errors identically on EndTxn responses. The branch comment now defers
to the state-table note instead of repeating it.
…NCED

Transactional requests conventionally return PRODUCER_FENCED on epoch
mismatches, and PRODUCER_FENCED is also what transactions V1's strict
epoch check returns for this same race; INVALID_PRODUCER_EPOCH is the
produce-path convention (KAFKA-19690). Client behavior is unchanged:
the producer client already folds INVALID_PRODUCER_EPOCH into
ProducerFencedException on EndTxn responses, so the two codes are
handled identically.
@suzhiking suzhiking changed the title KAFKA-20785: Return INVALID_PRODUCER_EPOCH when EndTxn commit races with coordinator-side abort KAFKA-20785: Return PRODUCER_FENCED when EndTxn commit races with coordinator-side abort Aug 28, 2026

@jolshan jolshan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@jolshan
jolshan merged commit c328f3d into apache:trunk Aug 31, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Kafka Broker small Small PRs transactions Transactions and EOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants