sepuri sai krishna created KAFKA-20770:
------------------------------------------
Summary: LocalLeaderEndPoint returns leader epoch 0 instead of -1
when the epoch cannot be resolved, inconsistent with the ListOffsets-based path
Key: KAFKA-20770
URL: https://issues.apache.org/jira/browse/KAFKA-20770
Project: Kafka
Issue Type: Bug
Components: core
Affects Versions: 4.3.0
Reporter: sepuri sai krishna
LocalLeaderEndPoint (used by ReplicaAlterLogDirsThread) and
RemoteLeaderEndPoint (used by ReplicaFetcherThread) implement the same
LeaderEndPoint contract, but they disagree on what leader epoch to report when
the epoch for an offset cannot be resolved from the leader epoch cache.
The remote path goes through ListOffsets:
UnifiedLog#fetchEarliestPendingUploadOffset leaves the epoch unset when
LeaderEpochFileCache#epochForOffset returns empty, ReplicaManager only sets the
response leaderEpoch when present, and the ListOffsetsResponse schema default
is -1 (NO_LEADER_EPOCH). So RemoteLeaderEndPoint reports epoch -1, meaning
"unknown".
The local path instead fabricates epoch 0: fetchEarliestPendingUploadOffset in
core/src/main/scala/kafka/server/LocalLeaderEndPoint.scala ends with
epoch.orElse(0), and the sibling methods fetchEarliestOffset, fetchLatestOffset
and fetchEarliestLocalOffset use the same orElse(0) fallback. Note that
fetchEarliestPendingUploadOffset also returns fetchEarliestOffset's result
wholesale in its "no segments have been uploaded yet" branch, so the fabricated
epoch 0 can leak out of the pending-upload API through that branch as well.
This matters because the epoch returned by these methods is consumed by
TierStateMachine#buildRemoteLogAuxState, which treats it as meaningful: epoch 0
short-circuits the earlier-epoch lookup ("no need to fetch from earlier
epoch"), while any other value drives an OffsetsForLeaderEpoch probe and an
epoch-based remote log segment metadata lookup
(fetchRemoteLogSegmentMetadata(topicPartition, targetEpoch, offset)). A
fabricated 0 therefore claims the first leader epoch rather than admitting the
epoch is unknown, and the two fetcher paths behave differently for identical
log state.
In practice the fallback is hard to hit on a healthy broker, because message
format v2 always populates the epoch cache and
LeaderEpochFileCache#truncateFromStart clamps the first entry to the log start
offset, so epochForOffset resolves for any offset at or above logStartOffset.
It becomes reachable when the leader epoch cache is empty or truncated above
the queried offset (for example after checkpoint loss or corruption). Even then
the consequence is a misleading epoch handed to TierStateMachine rather than
data loss, hence Minor priority. The main goal of this fix is correctness and
consistency between the two LeaderEndPoint implementations.
Identified by code inspection on trunk while comparing the two implementations
introduced/extended by KIP-1023 (KAFKA-15433, shipped in 4.3.0), and confirmed
with unit tests. The existing tests in LocalLeaderEndPointTest did not catch
this because they computed the expected epoch using the same
epochForOffset(...).orElse(0) expression as the production code.
Proposed fix: replace the orElse(0) fallback with orElse(UNDEFINED_EPOCH) (-1,
org.apache.kafka.common.requests.OffsetsForLeaderEpochResponse.UNDEFINED_EPOCH,
already imported in the file) in all four methods, matching the
ListOffsets-based path. Behavior is unchanged whenever the epoch is resolvable.
Also replace the tautological epoch expectations in LocalLeaderEndPointTest
with concrete values and add tests covering a non-zero resolved epoch and the
unresolvable-epoch case.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)