You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kafka.apache.org by da...@apache.org on 2023/11/07 15:24:48 UTC
(kafka) branch trunk updated: KAFKA-15583: Enforce HWM advance only if partition is not under-min-ISR (#14594)
This is an automated email from the ASF dual-hosted git repository.
davidarthur pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/trunk by this push:
new edc7e10a745 KAFKA-15583: Enforce HWM advance only if partition is not under-min-ISR (#14594)
edc7e10a745 is described below
commit edc7e10a745c350ad1efa9e4866370dc8ea0e034
Author: Calvin Liu <83...@users.noreply.github.com>
AuthorDate: Tue Nov 7 07:24:40 2023 -0800
KAFKA-15583: Enforce HWM advance only if partition is not under-min-ISR (#14594)
Only advance the HWM for a partition if the ISR set is equal to or above the min ISR config. This patch also sets an upper bound on the min ISR config so it cannot exceed the number of replicas.
Reviewers: David Arthur <mu...@gmail.com>
---
core/src/main/scala/kafka/cluster/Partition.scala | 27 ++++++++--
.../kafka/api/ProducerFailureHandlingTest.scala | 14 -----
.../scala/unit/kafka/cluster/PartitionTest.scala | 60 ++++++++++++++++++++++
.../kafka/server/ReplicaManagerQuotasTest.scala | 3 +-
.../unit/kafka/server/ReplicaManagerTest.scala | 1 +
5 files changed, 86 insertions(+), 19 deletions(-)
diff --git a/core/src/main/scala/kafka/cluster/Partition.scala b/core/src/main/scala/kafka/cluster/Partition.scala
index 104fca8ea90..251f198dc57 100755
--- a/core/src/main/scala/kafka/cluster/Partition.scala
+++ b/core/src/main/scala/kafka/cluster/Partition.scala
@@ -359,10 +359,23 @@ class Partition(val topicPartition: TopicPartition,
// a false positive under min isr check, it has to check the leaderReplicaIdOpt again. Though it can still be affected
// by ABA problems when leader->follower->leader, but it should be good enough for a metric.
def isUnderMinIsr: Boolean = {
- leaderLogIfLocal.exists { partitionState.isr.size < _.config.minInSyncReplicas } && isLeader
+ leaderLogIfLocal.exists { partitionState.isr.size < effectiveMinIsr(_) } && isLeader
}
- def isAtMinIsr: Boolean = leaderLogIfLocal.exists { partitionState.isr.size == _.config.minInSyncReplicas }
+/**
+ * When setting the min ISR, there is no restriction on it. Even if the value does not make sense to be larger than
+ * the replication factor. In case there are such setting, the effective min ISR of min(replication factor, min ISR)
+ * is returned here.
+ */
+ private def effectiveMinIsr(leaderLog: UnifiedLog): Int = {
+ leaderLog.config.minInSyncReplicas.min(remoteReplicasMap.size + 1)
+ }
+
+ /**
+ * Returns whether the partition ISR size is equals to the effective min ISR. For more info about this min ISR, refer to
+ * the effectiveMinIsr().
+ */
+ def isAtMinIsr: Boolean = leaderLogIfLocal.exists { partitionState.isr.size == effectiveMinIsr(_) }
def isReassigning: Boolean = assignmentState.isInstanceOf[OngoingReassignmentState]
@@ -1066,7 +1079,7 @@ class Partition(val topicPartition: TopicPartition,
s"awaiting ${awaitingReplicas.map(logEndOffsetString)}")
}
- val minIsr = leaderLog.config.minInSyncReplicas
+ val minIsr = effectiveMinIsr(leaderLog)
if (leaderLog.highWatermark >= requiredOffset) {
/*
* The topic may be configured not to accept messages if there are not enough replicas in ISR
@@ -1097,6 +1110,8 @@ class Partition(val topicPartition: TopicPartition,
* advancing the HW, the follower's log end offset may keep falling behind the HW (determined by the leader's log end
* offset) and therefore will never be added to ISR.
*
+ * The HW can only advance if the ISR size is equal or large than the min ISR(min.insync.replicas).
+ *
* With the addition of AlterPartition, we also consider newly added replicas as part of the ISR when advancing
* the HW. These replicas have not yet been committed to the ISR by the controller, so we could revert to the previously
* committed ISR. However, adding additional replicas to the ISR makes it more restrictive and therefore safe. We call
@@ -1107,6 +1122,10 @@ class Partition(val topicPartition: TopicPartition,
* @return true if the HW was incremented, and false otherwise.
*/
private def maybeIncrementLeaderHW(leaderLog: UnifiedLog, currentTimeMs: Long = time.milliseconds): Boolean = {
+ if (isUnderMinIsr) {
+ trace(s"Not increasing HWM because partition is under min ISR(ISR=${partitionState.isr}")
+ return false
+ }
// maybeIncrementLeaderHW is in the hot path, the following code is written to
// avoid unnecessary collection generation
val leaderLogEndOffset = leaderLog.logEndOffsetMetadata
@@ -1306,7 +1325,7 @@ class Partition(val topicPartition: TopicPartition,
val (info, leaderHWIncremented) = inReadLock(leaderIsrUpdateLock) {
leaderLogIfLocal match {
case Some(leaderLog) =>
- val minIsr = leaderLog.config.minInSyncReplicas
+ val minIsr = effectiveMinIsr(leaderLog)
val inSyncSize = partitionState.isr.size
// Avoid writing to leader if there are not enough insync replicas to make it safe
diff --git a/core/src/test/scala/integration/kafka/api/ProducerFailureHandlingTest.scala b/core/src/test/scala/integration/kafka/api/ProducerFailureHandlingTest.scala
index 93d6a460acd..0b1628a762e 100644
--- a/core/src/test/scala/integration/kafka/api/ProducerFailureHandlingTest.scala
+++ b/core/src/test/scala/integration/kafka/api/ProducerFailureHandlingTest.scala
@@ -231,20 +231,6 @@ class ProducerFailureHandlingTest extends KafkaServerTestHarness {
assertTrue(thrown.getCause.isInstanceOf[InvalidTopicException], "Unexpected exception while sending to an invalid topic " + thrown.getCause)
}
- @ParameterizedTest(name = TestInfoUtils.TestWithParameterizedQuorumName)
- @ValueSource(strings = Array("zk", "kraft"))
- def testNotEnoughReplicas(quorum: String): Unit = {
- val topicName = "minisrtest"
- val topicProps = new Properties()
- topicProps.put("min.insync.replicas",(numServers+1).toString)
-
- createTopic(topicName, replicationFactor = numServers, topicConfig = topicProps)
-
- val record = new ProducerRecord(topicName, null, "key".getBytes, "value".getBytes)
- val e = assertThrows(classOf[ExecutionException], () => producer3.send(record).get)
- assertEquals(classOf[NotEnoughReplicasException], e.getCause.getClass)
- }
-
@ParameterizedTest(name = TestInfoUtils.TestWithParameterizedQuorumName)
@ValueSource(strings = Array("zk", "kraft"))
def testNotEnoughReplicasAfterBrokerShutdown(quorum: String): Unit = {
diff --git a/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala b/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala
index a4aba277e3a..afa9b8b6e0b 100644
--- a/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala
+++ b/core/src/test/scala/unit/kafka/cluster/PartitionTest.scala
@@ -2171,6 +2171,66 @@ class PartitionTest extends AbstractPartitionTest {
assertEquals(log.logEndOffset, partition.localLogOrException.highWatermark)
}
+ @Test
+ def testHighWatermarkAdvanceShouldNotAdvanceWhenUnderMinISR(): Unit = {
+ configRepository.setTopicConfig(topicPartition.topic, TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG, "3")
+ val log = logManager.getOrCreateLog(topicPartition, topicId = None)
+ seedLogData(log, numRecords = 10, leaderEpoch = 4)
+
+ val controllerEpoch = 0
+ val leaderEpoch = 5
+ val remoteBrokerId1 = brokerId + 1
+ val remoteBrokerId2 = brokerId + 2
+ val replicas = Seq(brokerId, remoteBrokerId1, remoteBrokerId2)
+ val isr = Seq(brokerId, remoteBrokerId1)
+
+ val metadataCache = mock(classOf[KRaftMetadataCache])
+ addBrokerEpochToMockMetadataCache(metadataCache, replicas.toList)
+
+ val partition = new Partition(
+ topicPartition,
+ replicaLagTimeMaxMs = Defaults.ReplicaLagTimeMaxMs,
+ interBrokerProtocolVersion = MetadataVersion.IBP_3_7_IV1,
+ localBrokerId = brokerId,
+ () => defaultBrokerEpoch(brokerId),
+ time,
+ alterPartitionListener,
+ delayedOperations,
+ metadataCache,
+ logManager,
+ alterPartitionManager
+ )
+ partition.createLogIfNotExists(isNew = false, isFutureReplica = false, offsetCheckpoints, None)
+
+ assertTrue(partition.makeLeader(
+ new LeaderAndIsrPartitionState()
+ .setControllerEpoch(controllerEpoch)
+ .setLeader(brokerId)
+ .setLeaderEpoch(leaderEpoch)
+ .setIsr(isr.toList.map(Int.box).asJava)
+ .setPartitionEpoch(1)
+ .setReplicas(replicas.map(Int.box).asJava)
+ .setIsNew(true),
+ offsetCheckpoints, None), "Expected become leader transition to succeed")
+
+ assertTrue(partition.isUnderMinIsr)
+ assertEquals(0L, partition.localLogOrException.highWatermark)
+
+ fetchFollower(partition, replicaId = remoteBrokerId1, fetchOffset = log.logEndOffset)
+ assertEquals(0L, partition.localLogOrException.highWatermark)
+
+ // Though the maximum ISR has been larger than min ISR, the HWM can't advance.
+ fetchFollower(partition, replicaId = remoteBrokerId2, fetchOffset = log.logEndOffset)
+ assertEquals(0L, partition.localLogOrException.highWatermark)
+ assertEquals(3, partition.partitionState.maximalIsr.size)
+ assertEquals(1, alterPartitionManager.isrUpdates.size)
+
+ // Update the ISR to size 3
+ alterPartitionManager.completeIsrUpdate(2)
+ assertFalse(partition.isUnderMinIsr)
+ assertEquals(log.logEndOffset, partition.localLogOrException.highWatermark)
+ }
+
@Test
def testAlterIsrLeaderAndIsrRace(): Unit = {
val log = logManager.getOrCreateLog(topicPartition, topicId = None)
diff --git a/core/src/test/scala/unit/kafka/server/ReplicaManagerQuotasTest.scala b/core/src/test/scala/unit/kafka/server/ReplicaManagerQuotasTest.scala
index 320d766d3a5..1006a3e0471 100644
--- a/core/src/test/scala/unit/kafka/server/ReplicaManagerQuotasTest.scala
+++ b/core/src/test/scala/unit/kafka/server/ReplicaManagerQuotasTest.scala
@@ -30,7 +30,7 @@ import org.apache.kafka.common.requests.FetchRequest.PartitionData
import org.apache.kafka.common.{TopicIdPartition, TopicPartition, Uuid}
import org.apache.kafka.metadata.LeaderRecoveryState
import org.apache.kafka.server.util.{KafkaScheduler, MockTime}
-import org.apache.kafka.storage.internals.log.{FetchDataInfo, FetchIsolation, FetchParams, LogDirFailureChannel, LogOffsetMetadata, LogOffsetSnapshot}
+import org.apache.kafka.storage.internals.log.{FetchDataInfo, FetchIsolation, FetchParams, LogConfig, LogDirFailureChannel, LogOffsetMetadata, LogOffsetSnapshot}
import org.junit.jupiter.api.Assertions._
import org.junit.jupiter.api.{AfterEach, Test}
import org.mockito.ArgumentMatchers.{any, anyBoolean, anyInt, anyLong}
@@ -261,6 +261,7 @@ class ReplicaManagerQuotasTest {
when(log.lastStableOffset).thenReturn(5)
when(log.logEndOffsetMetadata).thenReturn(new LogOffsetMetadata(20L))
when(log.topicId).thenReturn(Some(topicId))
+ when(log.config).thenReturn(new LogConfig(Collections.emptyMap()))
//if we ask for len 1 return a message
when(log.read(anyLong,
diff --git a/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala b/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala
index 47b58d563ae..ec61da1e9c7 100644
--- a/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala
+++ b/core/src/test/scala/unit/kafka/server/ReplicaManagerTest.scala
@@ -3882,6 +3882,7 @@ class ReplicaManagerTest {
when(mockLog.logEndOffset).thenReturn(endOffset)
when(mockLog.localLogStartOffset()).thenReturn(endOffset - 10)
when(mockLog.remoteLogEnabled()).thenReturn(true)
+ when(mockLog.config).thenReturn(new LogConfig(Collections.emptyMap()))
mockLog
}