Artem Livshits created KAFKA-21349:
--------------------------------------
Summary: Transaction coordinator loads uncommitted records from
__transaction_state and can act on state that is later truncated
Key: KAFKA-21349
URL: https://issues.apache.org/jira/browse/KAFKA-21349
Project: Kafka
Issue Type: Bug
Components: core
Reporter: Artem Livshits
When a broker becomes the leader of a {{__transaction_state}} partition,
{{TransactionStateManager.loadTransactionMetadata}} reads the log up to the log
end offset ({{FetchIsolation.LOG_END}}). A newly elected leader can have
records above the high watermark: they were replicated to it but not yet
committed. These records can still be truncated if leadership moves again
before they are committed. This can happen even with clean elections: another
ISR member that doesn't have them can be elected.
Then the coordinator acts on state that is later rolled back. The most damaging
case is a PREPARE_COMMIT or PREPARE_ABORT above the high watermark. After the
load, the coordinator sends the markers for it right away. For example:
# The coordinator appends PREPARE_COMMIT for a transaction. The record reaches
follower B but not follower C, and the leader fails before the high watermark
advances.
# B becomes the leader and loads the partition, including the uncommitted
PREPARE_COMMIT. It starts sending COMMIT markers, and some data partitions
commit.
# B fails before C replicates the record. C, which is in the ISR, becomes the
leader, and the PREPARE_COMMIT is gone. C loads the transaction as ONGOING.
# The transaction times out, and C aborts it: PREPARE_ABORT, then ABORT markers
to the remaining partitions.
The transaction ends up committed on some partitions and aborted on others.
Smaller effects of acting on state that is later truncated: client responses or
fencing decisions based on state that disappears, and producer epochs being
reused after the rollback.
Since the coordinator only updates its in-memory state after an append with
acks=all completes, only the last transition of each transactional id can be
uncommitted. So the rollback is at most one step per transactional id.
Using {{FetchIsolation.HIGH_WATERMARK}} for the load is not enough. A newly
elected leader's high watermark can lag behind the true one, so the load would
miss committed records. For example, it could miss a committed PREPARE_COMMIT
and then abort the transaction on timeout.
Proposed fix:
The coordinator doesn't write to the partition while it is loading: requests
get COORDINATOR_LOAD_IN_PROGRESS. So the log end offset at the start of the
load doesn't change. Once the high watermark reaches it, every record the load
reads is committed. A new leader's high watermark only advances after the ISR
followers fetch from it (their logs are prefixes of the leader's log after
truncation). In the normal case, the wait is about one follower fetch round
trip.
* In {{loadTransactionsForTxnTopicPartition}}, if the high watermark is below
the log end offset, register a {{PartitionListener}}
({{ReplicaManager.maybeAddListener}}). Schedule the load once
{{onHighWatermarkUpdated}} reports an offset at or above the log end offset
recorded when the wait started.
* Check the high watermark again after registering.
{{Partition.maybeAddListener}} replays the current state before it adds the
listener, and high watermark updates from follower fetches can happen between
the replay and the insertion.
* If the leader epoch changes, the partition becomes a follower, or it fails or
is deleted before the high watermark catches up, abandon the load: remove the
epoch's {{loadingPartitions}} entry. Do this on the load scheduler, since the
listener is invoked with partition locks held.
* {{removeExpiredTransactionalIds()}} should skip partitions that are loading.
On a leader epoch bump, the previous epoch's cache entry stays until the new
load replaces it. Expiration can write tombstones for it, which would move the
log end offset while the load waits.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)