junrao commented on code in PR #23318:
URL: https://github.com/apache/kafka/pull/23318#discussion_r4067037745


##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -502,24 +433,20 @@ abstract class AbstractFetcherThread(name: String,
 
   /**
    * Returns initial partition fetch state based on current state and the 
provided `initialFetchState`.
-   * From IBP 2.7 onwards, we can rely on truncation based on diverging data 
returned in fetch responses.
-   * For older versions, we can skip the truncation step iff the leader epoch 
matches the existing epoch.
+   * We rely on truncation based on diverging data returned in fetch responses.
    */
   private def partitionFetchState(tp: TopicPartition, initialFetchState: 
InitialFetchState, currentState: PartitionFetchState): PartitionFetchState = {
     if (currentState != null && currentState.currentLeaderEpoch == 
initialFetchState.currentLeaderEpoch) {
       currentState
     } else if (initialFetchState.initOffset < 0) {
       fetchOffsetAndTruncate(tp, initialFetchState.topicId, 
initialFetchState.currentLeaderEpoch)
-    } else if (leader.isTruncationOnFetchSupported) {
+    } else {

Review Comment:
   It seems that we can simplify partitionFetchState() to always set the state 
to FETCHING. Currently, this method sets the state to TRUNCATING only when 
lastFetchedEpoch.isPresent is false. In that case, when exercising 
maybeTruncate(), it degenerates to truncating to the HWM. However, In 
ReplicaManager.initialFetchOffset(), if log.latestEpoch.isPresent is false, the 
initial fetch offset is set to HWM. So, truncating to HMW to a no-op.



##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -502,24 +433,20 @@ abstract class AbstractFetcherThread(name: String,
 
   /**
    * Returns initial partition fetch state based on current state and the 
provided `initialFetchState`.
-   * From IBP 2.7 onwards, we can rely on truncation based on diverging data 
returned in fetch responses.
-   * For older versions, we can skip the truncation step iff the leader epoch 
matches the existing epoch.
+   * We rely on truncation based on diverging data returned in fetch responses.
    */
   private def partitionFetchState(tp: TopicPartition, initialFetchState: 
InitialFetchState, currentState: PartitionFetchState): PartitionFetchState = {
     if (currentState != null && currentState.currentLeaderEpoch == 
initialFetchState.currentLeaderEpoch) {
       currentState
     } else if (initialFetchState.initOffset < 0) {

Review Comment:
   This is an existing issue. It seems that initialFetchState.initOffset is 
always >=0. We can just remove this check.



##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -502,24 +433,20 @@ abstract class AbstractFetcherThread(name: String,
 
   /**
    * Returns initial partition fetch state based on current state and the 
provided `initialFetchState`.
-   * From IBP 2.7 onwards, we can rely on truncation based on diverging data 
returned in fetch responses.
-   * For older versions, we can skip the truncation step iff the leader epoch 
matches the existing epoch.
+   * We rely on truncation based on diverging data returned in fetch responses.

Review Comment:
   diverging data => diverging epoch



##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -604,10 +525,7 @@ abstract class AbstractFetcherThread(name: String,
   private def getOffsetTruncationState(tp: TopicPartition, leaderEpochOffset: 
EpochEndOffset): OffsetTruncationState =
     LockUtils.inLock(partitionMapLock , () => {
     if (leaderEpochOffset.endOffset == UNDEFINED_EPOCH_OFFSET) {
-      // truncate to initial offset which is the high watermark for follower 
replica. For
-      // future replica, it is either high watermark of the future replica or 
current
-      // replica's truncation offset (when the current replica truncates, it 
forces future
-      // replica's partition state to 'truncating' and sets initial offset to 
its truncation offset)
+      // truncate to initial offset which is the high watermark of the replica

Review Comment:
   It seems that leaderEpochOffset.endOffset == UNDEFINED_EPOCH_OFFSET can 
never be true in production?
   
   Also, it seems that the branch leaderEpochOffset.leaderEpoch == 
UNDEFINED_EPOCH) can never be true and the comment "inter-broker protocol 
version < IBP_2_0_IV0" is no longer valid.



##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -273,9 +223,7 @@ abstract class AbstractFetcherThread(name: String,
               fetchOffsets.put(tp, offsetTruncationState)
 
           case Errors.FENCED_LEADER_EPOCH =>
-            val currentLeaderEpoch = latestEpochsForPartitions.get(tp)
-              .map(epochEndOffset => 
Int.box(epochEndOffset.currentLeaderEpoch)).toJava
-            if (onPartitionFenced(tp, currentLeaderEpoch))
+            if (onPartitionFenced(tp, Optional.empty()))

Review Comment:
   If requestEpoch is empty, onPartitionFenced() always returns true. So we can 
skip this test.
   
   Also, maybeTruncateToEpochEndOffsets() is only called when there is 
diverging epoch in the response. In that case, the response error is None. So, 
the Errors.FENCED_LEADER_EPOCH path seems unnecessary.



##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -144,38 +143,22 @@ abstract class AbstractFetcherThread(name: String,
   }
 
   /**
-   * Builds offset for leader epoch requests for partitions that are in the 
truncating phase based
-   * on latest epochs of the future replicas (the one that is fetching)
+   * Truncate partitions that are in the truncating phase to their local high 
watermark. A partition
+   * only enters the truncating phase when the replica has no leader epoch 
information (e.g. it only
+   * contains messages with the old format); replicas with leader epochs rely 
on the diverging epoch
+   * returned in fetch responses for truncation.
    */
-  private def fetchTruncatingPartitions(): (Map[TopicPartition, EpochData], 
Set[TopicPartition]) =
-    LockUtils.inLock(partitionMapLock, () => {
-    val partitionsWithEpochs = mutable.Map.empty[TopicPartition, EpochData]
-    val partitionsWithoutEpochs = mutable.Set.empty[TopicPartition]
-
-    partitionStates.partitionStateMap.forEach { (tp, state) =>
-      if (state.isTruncating) {
-        latestEpoch(tp).toScala match {
-          case Some(epoch) =>
-            partitionsWithEpochs += tp -> new EpochData()
-              .setPartition(tp.partition)
-              .setCurrentLeaderEpoch(state.currentLeaderEpoch)
-              .setLeaderEpoch(epoch)
-          case _ =>
-            partitionsWithoutEpochs += tp
-        }
-      }
-    }
-
-    (partitionsWithEpochs, partitionsWithoutEpochs)
-  })
-
   private def maybeTruncate(): Unit = {
-    val (partitionsWithEpochs, partitionsWithoutEpochs) = 
fetchTruncatingPartitions()
-    if (partitionsWithEpochs.nonEmpty) {
-      truncateToEpochEndOffsets(partitionsWithEpochs)
-    }
-    if (partitionsWithoutEpochs.nonEmpty) {
-      truncateToHighWatermark(partitionsWithoutEpochs)
+    val truncatingPartitions = LockUtils.inLock(partitionMapLock, () => {
+      val partitions = mutable.Set.empty[TopicPartition]
+      partitionStates.partitionStateMap.forEach { (tp, state) =>
+        if (state.isTruncating)
+          partitions += tp
+      }
+      partitions
+    })
+    if (truncatingPartitions.nonEmpty) {
+      truncateToHighWatermark(truncatingPartitions)

Review Comment:
   This is an existing issue, but truncateToHighWatermark() is probably better 
named as truncateToFetchOffset().



##########
core/src/main/scala/kafka/server/AbstractFetcherThread.scala:
##########
@@ -630,11 +548,12 @@ abstract class AbstractFetcherThread(name: String,
         if (followerEpoch != leaderEpochOffset.leaderEpoch) {
           // the follower does not know about the epoch that leader replied 
with
           // we truncate to the end offset of the largest epoch that is 
smaller than the
-          // epoch the leader replied with, and send another offset for leader 
epoch request
+          // epoch the leader replied with; the next fetch request carries 
that epoch as
+          // the last fetched epoch and the leader may reply with another 
diverging epoch
           val intermediateOffsetToTruncateTo = min(followerEndOffset, 
replicaEndOffset)
           info(s"Based on replica's leader epoch, leader replied with epoch 
${leaderEpochOffset.leaderEpoch} " +
             s"unknown to the replica for $tp. " +
-            s"Will truncate to $intermediateOffsetToTruncateTo and send 
another leader epoch request to the leader.")
+            s"Will truncate to $intermediateOffsetToTruncateTo and continue 
fetching with the last known epoch.")

Review Comment:
   In the else branch in line 563, we probably want to adjust the following 
comment. If the follower is not tracking the epoch, the leader won't reply with 
diverging epoch and won't reach there.
   
   >         // This can happen if the follower was not tracking leader epochs 
at that point (before the
   >         // upgrade, or if this broker is new).



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to