Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/src/main/scala/kafka/api/ApiVersion.scala
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ sealed trait ApiVersion extends Ordered[ApiVersion] {
def recordVersion: RecordVersion
def id: Int

def isAlterIsrSupported: Boolean = this >= KAFKA_2_7_IV1

override def compare(that: ApiVersion): Int =
ApiVersion.orderingByVersion.compare(this, that)

Expand Down
55 changes: 44 additions & 11 deletions core/src/main/scala/kafka/cluster/Partition.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,20 @@ import org.apache.kafka.common.{IsolationLevel, TopicPartition}
import scala.collection.{Map, Seq}
import scala.jdk.CollectionConverters._

trait IsrChangeListener {
Comment thread
hachikuji marked this conversation as resolved.
def markExpand(): Unit
def markShrink(): Unit
def markFailed(): Unit
}

trait PartitionStateStore {
def fetchTopicConfig(): Properties
def shrinkIsr(controllerEpoch: Int, leaderAndIsr: LeaderAndIsr): Option[Int]
def expandIsr(controllerEpoch: Int, leaderAndIsr: LeaderAndIsr): Option[Int]
}

class ZkPartitionStateStore(topicPartition: TopicPartition,
zkClient: KafkaZkClient,
replicaManager: ReplicaManager) extends PartitionStateStore {
zkClient: KafkaZkClient) extends PartitionStateStore {

override def fetchTopicConfig(): Properties = {
val adminZkClient = new AdminZkClient(zkClient)
Expand All @@ -62,15 +67,11 @@ class ZkPartitionStateStore(topicPartition: TopicPartition,

override def shrinkIsr(controllerEpoch: Int, leaderAndIsr: LeaderAndIsr): Option[Int] = {
val newVersionOpt = updateIsr(controllerEpoch, leaderAndIsr)
if (newVersionOpt.isDefined)
replicaManager.isrShrinkRate.mark()
newVersionOpt
}

override def expandIsr(controllerEpoch: Int, leaderAndIsr: LeaderAndIsr): Option[Int] = {
val newVersionOpt = updateIsr(controllerEpoch, leaderAndIsr)
if (newVersionOpt.isDefined)
replicaManager.isrExpandRate.mark()
newVersionOpt
}

Expand All @@ -79,10 +80,8 @@ class ZkPartitionStateStore(topicPartition: TopicPartition,
leaderAndIsr, controllerEpoch)

if (updateSucceeded) {
replicaManager.recordIsrChange(topicPartition)
Some(newVersion)
} else {
replicaManager.failedIsrUpdatesRate.mark()
None
}
}
Expand All @@ -107,10 +106,24 @@ object Partition extends KafkaMetricsGroup {
def apply(topicPartition: TopicPartition,
time: Time,
replicaManager: ReplicaManager): Partition = {

val isrChangeListener = new IsrChangeListener {
override def markExpand(): Unit = {
replicaManager.recordIsrChange(topicPartition)
Comment thread
hachikuji marked this conversation as resolved.
replicaManager.isrExpandRate.mark()
}

override def markShrink(): Unit = {
replicaManager.recordIsrChange(topicPartition)
replicaManager.isrShrinkRate.mark()
}

override def markFailed(): Unit = replicaManager.failedIsrUpdatesRate.mark()
}

val zkIsrBackingStore = new ZkPartitionStateStore(
topicPartition,
replicaManager.zkClient,
replicaManager)
replicaManager.zkClient)

val delayedOperations = new DelayedOperations(
topicPartition,
Expand All @@ -124,6 +137,7 @@ object Partition extends KafkaMetricsGroup {
localBrokerId = replicaManager.config.brokerId,
time = time,
stateStore = zkIsrBackingStore,
isrChangeListener = isrChangeListener,
delayedOperations = delayedOperations,
metadataCache = replicaManager.metadataCache,
logManager = replicaManager.logManager,
Expand Down Expand Up @@ -246,6 +260,7 @@ class Partition(val topicPartition: TopicPartition,
localBrokerId: Int,
time: Time,
stateStore: PartitionStateStore,
isrChangeListener: IsrChangeListener,
delayedOperations: DelayedOperations,
metadataCache: MetadataCache,
logManager: LogManager,
Expand All @@ -270,7 +285,7 @@ class Partition(val topicPartition: TopicPartition,
@volatile private[cluster] var isrState: IsrState = CommittedIsr(Set.empty)
@volatile var assignmentState: AssignmentState = SimpleAssignmentState(Seq.empty)

private val useAlterIsr: Boolean = interBrokerProtocolVersion >= KAFKA_2_7_IV2
private val useAlterIsr: Boolean = interBrokerProtocolVersion.isAlterIsrSupported

// Logs belonging to this partition. Majority of time it will be only one log, but if log directory
// is getting changed (as a result of ReplicaAlterLogDirs command), we may have two logs until copy
Expand Down Expand Up @@ -1325,6 +1340,9 @@ class Partition(val topicPartition: TopicPartition,
info(s"Expanding ISR from ${isrState.isr.mkString(",")} to ${newInSyncReplicaIds.mkString(",")}")
val newLeaderAndIsr = new LeaderAndIsr(localBrokerId, leaderEpoch, newInSyncReplicaIds.toList, zkVersion)
val zkVersionOpt = stateStore.expandIsr(controllerEpoch, newLeaderAndIsr)
if (zkVersionOpt.isDefined) {
isrChangeListener.markExpand()
}
maybeUpdateIsrAndVersionWithZk(newInSyncReplicaIds, zkVersionOpt)
}

Expand All @@ -1351,6 +1369,9 @@ class Partition(val topicPartition: TopicPartition,
private def shrinkIsrWithZk(newIsr: Set[Int]): Unit = {
val newLeaderAndIsr = new LeaderAndIsr(localBrokerId, leaderEpoch, newIsr.toList, zkVersion)
val zkVersionOpt = stateStore.shrinkIsr(controllerEpoch, newLeaderAndIsr)
if (zkVersionOpt.isDefined) {
isrChangeListener.markShrink()
}
maybeUpdateIsrAndVersionWithZk(newIsr, zkVersionOpt)
}

Expand All @@ -1363,6 +1384,7 @@ class Partition(val topicPartition: TopicPartition,

case None =>
info(s"Cached zkVersion $zkVersion not equal to that in zookeeper, skip updating ISR")
isrChangeListener.markFailed()
}
}

Expand All @@ -1378,6 +1400,7 @@ class Partition(val topicPartition: TopicPartition,
val alterIsrItem = AlterIsrItem(topicPartition, newLeaderAndIsr, handleAlterIsrResponse(proposedIsrState))

if (!alterIsrManager.enqueue(alterIsrItem)) {
isrChangeListener.markFailed()
throw new IllegalStateException(s"Failed to enqueue `AlterIsr` request with state " +
s"$newLeaderAndIsr for partition $topicPartition")
}
Expand Down Expand Up @@ -1406,10 +1429,13 @@ class Partition(val topicPartition: TopicPartition,
case Left(error: Errors) => error match {
case Errors.UNKNOWN_TOPIC_OR_PARTITION =>
debug(s"Controller failed to update ISR to $proposedIsrState since it doesn't know about this topic or partition. Giving up.")
isrChangeListener.markFailed()
case Errors.FENCED_LEADER_EPOCH =>
debug(s"Controller failed to update ISR to $proposedIsrState since we sent an old leader epoch. Giving up.")
isrChangeListener.markFailed()
case Errors.INVALID_UPDATE_VERSION =>
debug(s"Controller failed to update ISR to $proposedIsrState due to invalid zk version. Giving up.")
isrChangeListener.markFailed()
case _ =>
warn(s"Controller failed to update ISR to $proposedIsrState due to unexpected $error. Retrying.")
sendAlterIsrRequest(proposedIsrState)
Comment thread
mumrah marked this conversation as resolved.
Outdated
Expand All @@ -1418,12 +1444,19 @@ class Partition(val topicPartition: TopicPartition,
// Success from controller, still need to check a few things
if (leaderAndIsr.leaderEpoch != leaderEpoch) {
debug(s"Ignoring ISR from AlterIsr with ${leaderAndIsr} since we have a stale leader epoch $leaderEpoch.")
isrChangeListener.markFailed()
} else if (leaderAndIsr.zkVersion <= zkVersion) {
debug(s"Ignoring ISR from AlterIsr with ${leaderAndIsr} since we have a newer version $zkVersion.")
isrChangeListener.markFailed()
} else {
isrState = CommittedIsr(leaderAndIsr.isr.toSet)
zkVersion = leaderAndIsr.zkVersion
info(s"ISR updated from AlterIsr to ${isrState.isr.mkString(",")} and version updated to [$zkVersion]")
proposedIsrState match {
case PendingExpandIsr(_, _) => isrChangeListener.markExpand()
case PendingShrinkIsr(_, _) => isrChangeListener.markShrink()
case _ => // nothing to do, shouldn't get here
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/kafka/server/KafkaConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ package kafka.server
import java.util
import java.util.{Collections, Locale, Properties}

import kafka.api.{ApiVersion, ApiVersionValidator, KAFKA_0_10_0_IV1, KAFKA_2_1_IV0, KAFKA_2_7_IV0}
import kafka.api.{ApiVersion, ApiVersionValidator, KAFKA_0_10_0_IV1, KAFKA_2_1_IV0, KAFKA_2_7_IV0, KAFKA_2_7_IV2}
import kafka.cluster.EndPoint
import kafka.coordinator.group.OffsetConfig
import kafka.coordinator.transaction.{TransactionLog, TransactionStateManager}
Expand Down
10 changes: 6 additions & 4 deletions core/src/main/scala/kafka/server/ReplicaManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -293,9 +293,11 @@ class ReplicaManager(val config: KafkaConfig,
}

def recordIsrChange(topicPartition: TopicPartition): Unit = {
isrChangeSet synchronized {
isrChangeSet += topicPartition
lastIsrChangeMs.set(time.milliseconds())
if (!config.interBrokerProtocolVersion.isAlterIsrSupported) {
isrChangeSet synchronized {
isrChangeSet += topicPartition
lastIsrChangeMs.set(time.milliseconds())
}
}
}
/**
Expand Down Expand Up @@ -340,7 +342,7 @@ class ReplicaManager(val config: KafkaConfig,
// A follower can lag behind leader for up to config.replicaLagTimeMaxMs x 1.5 before it is removed from ISR
scheduler.schedule("isr-expiration", maybeShrinkIsr _, period = config.replicaLagTimeMaxMs / 2, unit = TimeUnit.MILLISECONDS)
// If using AlterIsr, we don't need the znode ISR propagation
if (config.interBrokerProtocolVersion < KAFKA_2_7_IV2) {
if (!config.interBrokerProtocolVersion.isAlterIsrSupported) {
scheduler.schedule("isr-change-propagation", maybePropagateIsrChanges _,
period = isrChangeNotificationConfig.checkIntervalMs, unit = TimeUnit.MILLISECONDS)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import kafka.api.ApiVersion
import kafka.log.{CleanerConfig, LogConfig, LogManager}
import kafka.server.{Defaults, MetadataCache}
import kafka.server.checkpoints.OffsetCheckpoints
import kafka.utils.TestUtils.MockAlterIsrManager
import kafka.utils.TestUtils.{MockAlterIsrManager, MockIsrChangeListener}
import kafka.utils.{MockTime, TestUtils}
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.utils.Utils
Expand All @@ -41,6 +41,7 @@ class AbstractPartitionTest {
var logDir2: File = _
var logManager: LogManager = _
var alterIsrManager: MockAlterIsrManager = _
var isrChangeListener: MockIsrChangeListener = _
var logConfig: LogConfig = _
val stateStore: PartitionStateStore = mock(classOf[PartitionStateStore])
val delayedOperations: DelayedOperations = mock(classOf[DelayedOperations])
Expand All @@ -63,12 +64,14 @@ class AbstractPartitionTest {
logManager.startup()

alterIsrManager = TestUtils.createAlterIsrManager()
isrChangeListener = TestUtils.createIsrChangeListener()
partition = new Partition(topicPartition,
replicaLagTimeMaxMs = Defaults.ReplicaLagTimeMaxMs,
interBrokerProtocolVersion = ApiVersion.latestVersion,
localBrokerId = brokerId,
time,
stateStore,
isrChangeListener,
delayedOperations,
metadataCache,
logManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ class PartitionLockTest extends Logging {
val brokerId = 0
val topicPartition = new TopicPartition("test-topic", 0)
val stateStore: PartitionStateStore = mock(classOf[PartitionStateStore])
val isrChangeListener: IsrChangeListener = mock(classOf[IsrChangeListener])
val delayedOperations: DelayedOperations = mock(classOf[DelayedOperations])
val metadataCache: MetadataCache = mock(classOf[MetadataCache])
val offsetCheckpoints: OffsetCheckpoints = mock(classOf[OffsetCheckpoints])
Expand All @@ -261,6 +262,7 @@ class PartitionLockTest extends Logging {
localBrokerId = brokerId,
mockTime,
stateStore,
isrChangeListener,
delayedOperations,
metadataCache,
logManager,
Expand Down
21 changes: 19 additions & 2 deletions core/src/test/scala/unit/kafka/cluster/PartitionTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ class PartitionTest extends AbstractPartitionTest {
localBrokerId = brokerId,
time,
stateStore,
isrChangeListener,
delayedOperations,
metadataCache,
logManager,
Expand Down Expand Up @@ -1164,11 +1165,20 @@ class PartitionTest extends AbstractPartitionTest {
leaderEndOffset = 6L)

assertEquals(alterIsrManager.isrUpdates.size, 1)
assertEquals(alterIsrManager.isrUpdates.dequeue().leaderAndIsr.isr, List(brokerId, remoteBrokerId))
val isrItem = alterIsrManager.isrUpdates.dequeue()
assertEquals(isrItem.leaderAndIsr.isr, List(brokerId, remoteBrokerId))
assertEquals(Set(brokerId), partition.isrState.isr)
assertEquals(Set(brokerId, remoteBrokerId), partition.isrState.maximalIsr)
assertEquals(10L, remoteReplica.logEndOffset)
assertEquals(0L, remoteReplica.logStartOffset)

// Complete the ISR expansion
isrItem.callback.apply(Right(new LeaderAndIsr(brokerId, leaderEpoch, List(brokerId, remoteBrokerId), 2)))
assertEquals(Set(brokerId, remoteBrokerId), partition.isrState.isr)

assertEquals(isrChangeListener.expands.get, 1)
assertEquals(isrChangeListener.shrinks.get, 0)
assertEquals(isrChangeListener.failures.get, 0)
}

@Test
Expand Down Expand Up @@ -1221,6 +1231,10 @@ class PartitionTest extends AbstractPartitionTest {
assertEquals(Set(brokerId), partition.inSyncReplicaIds)
assertEquals(Set(brokerId, remoteBrokerId), partition.isrState.maximalIsr)
assertEquals(alterIsrManager.isrUpdates.size, 0)

assertEquals(isrChangeListener.expands.get, 0)
assertEquals(isrChangeListener.shrinks.get, 0)
assertEquals(isrChangeListener.failures.get, 1)
}

@Test
Expand Down Expand Up @@ -1639,7 +1653,7 @@ class PartitionTest extends AbstractPartitionTest {
val topicPartition = new TopicPartition("test", 1)
val partition = new Partition(
topicPartition, 1000, ApiVersion.latestVersion, 0,
new SystemTime(), mock(classOf[PartitionStateStore]), mock(classOf[DelayedOperations]),
new SystemTime(), mock(classOf[PartitionStateStore]), mock(classOf[IsrChangeListener]), mock(classOf[DelayedOperations]),
mock(classOf[MetadataCache]), mock(classOf[LogManager]), mock(classOf[AlterIsrManager]))

val replicas = Seq(0, 1, 2, 3)
Expand Down Expand Up @@ -1682,6 +1696,7 @@ class PartitionTest extends AbstractPartitionTest {
localBrokerId = brokerId,
time,
stateStore,
isrChangeListener,
delayedOperations,
metadataCache,
spyLogManager,
Expand Down Expand Up @@ -1717,6 +1732,7 @@ class PartitionTest extends AbstractPartitionTest {
localBrokerId = brokerId,
time,
stateStore,
isrChangeListener,
delayedOperations,
metadataCache,
spyLogManager,
Expand Down Expand Up @@ -1753,6 +1769,7 @@ class PartitionTest extends AbstractPartitionTest {
localBrokerId = brokerId,
time,
stateStore,
isrChangeListener,
delayedOperations,
metadataCache,
spyLogManager,
Expand Down
25 changes: 24 additions & 1 deletion core/src/test/scala/unit/kafka/utils/TestUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import java.nio.charset.{Charset, StandardCharsets}
import java.nio.file.{Files, StandardOpenOption}
import java.security.cert.X509Certificate
import java.time.Duration
import java.util.concurrent.atomic.AtomicInteger
import java.util.{Arrays, Collections, Properties}
import java.util.concurrent.{Callable, ExecutionException, Executors, TimeUnit}

import javax.net.ssl.X509TrustManager
import kafka.api._
import kafka.cluster.{Broker, EndPoint}
import kafka.cluster.{Broker, EndPoint, IsrChangeListener}
import kafka.log._
import kafka.security.auth.{Acl, Resource, Authorizer => LegacyAuthorizer}
import kafka.server._
Expand Down Expand Up @@ -1082,6 +1083,28 @@ object TestUtils extends Logging {
new MockAlterIsrManager()
}

class MockIsrChangeListener extends IsrChangeListener {
val expands: AtomicInteger = new AtomicInteger(0)
val shrinks: AtomicInteger = new AtomicInteger(0)
val failures: AtomicInteger = new AtomicInteger(0)

override def markExpand(): Unit = expands.incrementAndGet()

override def markShrink(): Unit = shrinks.incrementAndGet()

override def markFailed(): Unit = failures.incrementAndGet()

def reset(): Unit = {
expands.set(0)
shrinks.set(0)
failures.set(0)
}
}

def createIsrChangeListener(): MockIsrChangeListener = {
new MockIsrChangeListener()
}

def produceMessages(servers: Seq[KafkaServer],
records: Seq[ProducerRecord[Array[Byte], Array[Byte]]],
acks: Int = -1): Unit = {
Expand Down
Loading