Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
60 changes: 16 additions & 44 deletions core/src/main/scala/kafka/cluster/Partition.scala
Original file line number Diff line number Diff line change
Expand Up @@ -308,27 +308,27 @@ class Partition(val topicPartition: TopicPartition,
s"different from the requested log dir $logDir")
false
case None =>
createLogIfNotExists(isNew = false, isFutureReplica = true, highWatermarkCheckpoints)
createLogIfNotExists(isNew = false, isFutureReplica = true, highWatermarkCheckpoints, topicId)
true
}
}
}
}

def createLogIfNotExists(isNew: Boolean, isFutureReplica: Boolean, offsetCheckpoints: OffsetCheckpoints): Unit = {
def createLogIfNotExists(isNew: Boolean, isFutureReplica: Boolean, offsetCheckpoints: OffsetCheckpoints, topicId: Option[Uuid]): Unit = {
isFutureReplica match {
case true if futureLog.isEmpty =>
val log = createLog(isNew, isFutureReplica, offsetCheckpoints)
val log = createLog(isNew, isFutureReplica, offsetCheckpoints, topicId)
this.futureLog = Option(log)
case false if log.isEmpty =>
val log = createLog(isNew, isFutureReplica, offsetCheckpoints)
val log = createLog(isNew, isFutureReplica, offsetCheckpoints, topicId)
this.log = Option(log)
case _ => trace(s"${if (isFutureReplica) "Future Log" else "Log"} already exists.")
}
}

// Visible for testing
private[cluster] def createLog(isNew: Boolean, isFutureReplica: Boolean, offsetCheckpoints: OffsetCheckpoints): Log = {
private[cluster] def createLog(isNew: Boolean, isFutureReplica: Boolean, offsetCheckpoints: OffsetCheckpoints, topicId: Option[Uuid]): Log = {
def updateHighWatermark(log: Log) = {
val checkpointHighWatermark = offsetCheckpoints.fetch(log.parentDir, topicPartition).getOrElse {
info(s"No checkpointed highwatermark is found for partition $topicPartition")
Expand All @@ -341,7 +341,7 @@ class Partition(val topicPartition: TopicPartition,
logManager.initializingLog(topicPartition)
var maybeLog: Option[Log] = None
try {
val log = logManager.getOrCreateLog(topicPartition, isNew, isFutureReplica)
val log = logManager.getOrCreateLog(topicPartition, isNew, isFutureReplica, topicId)
maybeLog = Some(log)
updateHighWatermark(log)
log
Expand Down Expand Up @@ -425,41 +425,11 @@ class Partition(val topicPartition: TopicPartition,
}

/**
* Checks if the topic ID provided in the request is consistent with the topic ID in the log.
* If a valid topic ID is provided, and the log exists but has no ID set, set the log ID to be the request ID.
*
* @param requestTopicId the topic ID from the request
* @return true if the request topic id is consistent, false otherwise
* @return the topic ID for the log or None if the log or the topic ID does not exist.
*/
def checkOrSetTopicId(requestTopicId: Uuid): Boolean = {
// If the request had an invalid topic ID, then we assume that topic IDs are not supported.
// The topic ID was not inconsistent, so return true.
// If the log is empty, then we can not say that topic ID is inconsistent, so return true.
if (requestTopicId == null || requestTopicId == Uuid.ZERO_UUID)
true
else {
log match {
case None => true
case Some(log) => {
// Check if topic ID is in memory, if not, it must be new to the broker and does not have a metadata file.
// This is because if the broker previously wrote it to file, it would be recovered on restart after failure.
// Topic ID is consistent since we are just setting it here.
if (log.topicId == Uuid.ZERO_UUID) {
log.partitionMetadataFile.write(requestTopicId)
log.topicId = requestTopicId
true
} else if (log.topicId != requestTopicId) {
stateChangeLogger.error(s"Topic Id in memory: ${log.topicId} does not" +
s" match the topic Id for partition $topicPartition provided in the request: " +
s"$requestTopicId.")
false
} else {
// topic ID in log exists and matches request topic ID
true
}
}
}
}
def topicId: Option[Uuid] = {
val log = this.log.orElse(logManager.getLog(topicPartition))
log.flatMap(_.topicId)
}

// remoteReplicas will be called in the hot path, and must be inexpensive
Expand Down Expand Up @@ -540,7 +510,8 @@ class Partition(val topicPartition: TopicPartition,
* If the leader replica id does not change, return false to indicate the replica manager.
*/
def makeLeader(partitionState: LeaderAndIsrPartitionState,
highWatermarkCheckpoints: OffsetCheckpoints): Boolean = {
highWatermarkCheckpoints: OffsetCheckpoints,
topicId: Option[Uuid]): Boolean = {
val (leaderHWIncremented, isNewLeader) = inWriteLock(leaderIsrUpdateLock) {
// record the epoch of the controller that made the leadership decision. This is useful while updating the isr
// to maintain the decision maker controller's epoch in the zookeeper path
Expand All @@ -557,7 +528,7 @@ class Partition(val topicPartition: TopicPartition,
removingReplicas = removingReplicas
)
try {
createLogIfNotExists(partitionState.isNew, isFutureReplica = false, highWatermarkCheckpoints)
createLogIfNotExists(partitionState.isNew, isFutureReplica = false, highWatermarkCheckpoints, topicId)
} catch {
case e: ZooKeeperClientException =>
stateChangeLogger.error(s"A ZooKeeper client exception has occurred and makeLeader will be skipping the " +
Expand Down Expand Up @@ -624,7 +595,8 @@ class Partition(val topicPartition: TopicPartition,
* replica manager that state is already correct and the become-follower steps can be skipped
*/
def makeFollower(partitionState: LeaderAndIsrPartitionState,
highWatermarkCheckpoints: OffsetCheckpoints): Boolean = {
highWatermarkCheckpoints: OffsetCheckpoints,
topicId: Option[Uuid]): Boolean = {
inWriteLock(leaderIsrUpdateLock) {
val newLeaderBrokerId = partitionState.leader
val oldLeaderEpoch = leaderEpoch
Expand All @@ -639,7 +611,7 @@ class Partition(val topicPartition: TopicPartition,
removingReplicas = partitionState.removingReplicas.asScala.map(_.toInt)
)
try {
createLogIfNotExists(partitionState.isNew, isFutureReplica = false, highWatermarkCheckpoints)
createLogIfNotExists(partitionState.isNew, isFutureReplica = false, highWatermarkCheckpoints, topicId)
} catch {
case e: ZooKeeperClientException =>
stateChangeLogger.error(s"A ZooKeeper client exception has occurred. makeFollower will be skipping the " +
Expand Down
44 changes: 32 additions & 12 deletions core/src/main/scala/kafka/log/Log.scala
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,16 @@ case object SnapshotGenerated extends LogStartOffsetIncrementReason {
* @param producerIdExpirationCheckIntervalMs How often to check for producer ids which need to be expired
* @param hadCleanShutdown boolean flag to indicate if the Log had a clean/graceful shutdown last time. true means
* clean shutdown whereas false means a crash.
* @param topicId optional Uuid to specify the topic ID for the topic if it exists. Should only be specified when
* first creating the log through Partition.makeLeader or Partition.makeFollower. When reloading a log,
* this field will be populated by reading the topic ID value from partition.metadata if it exists.
* @param keepPartitionMetadataFile boolean flag to indicate whether the partition.metadata file should be kept in the
* log directory. A partition.metadata file is only created when the controller's
* inter-broker protocol version is at least 2.8. This file will persist the topic ID on
* the broker. If inter-broker protocol is downgraded below 2.8, a topic ID may be lost
* and a new ID generated upon re-upgrade. If the inter-broker protocol version is below
* 2.8, partition.metadata will be deleted to avoid ID conflicts upon re-upgrade.
* log directory. A partition.metadata file is only created when the raft controller is used
* or the ZK controller's inter-broker protocol version is at least 2.8.
* This file will persist the topic ID on the broker. If inter-broker protocol for a ZK controller
* is downgraded below 2.8, a topic ID may be lost and a new ID generated upon re-upgrade.
* If the inter-broker protocol version on a ZK cluster is below 2.8, partition.metadata
* will be deleted to avoid ID conflicts upon re-upgrade.
*/
@threadsafe
class Log(@volatile private var _dir: File,
Expand All @@ -262,7 +266,8 @@ class Log(@volatile private var _dir: File,
val producerStateManager: ProducerStateManager,
logDirFailureChannel: LogDirFailureChannel,
private val hadCleanShutdown: Boolean = true,
val keepPartitionMetadataFile: Boolean = true) extends Logging with KafkaMetricsGroup {
@volatile var topicId: Option[Uuid],
val keepPartitionMetadataFile: Boolean) extends Logging with KafkaMetricsGroup {

import kafka.log.Log._

Expand Down Expand Up @@ -315,8 +320,6 @@ class Log(@volatile private var _dir: File,

@volatile var partitionMetadataFile : PartitionMetadataFile = null

@volatile var topicId : Uuid = Uuid.ZERO_UUID

locally {
// create the log directory if it doesn't exist
Files.createDirectories(dir.toPath)
Expand Down Expand Up @@ -349,11 +352,21 @@ class Log(@volatile private var _dir: File,

// Delete partition metadata file if the version does not support topic IDs.
// Recover topic ID if present and topic IDs are supported
// If we were provided a topic ID when creating the log, partition metadata files are supported, and one does not yet exist
// write to the partition metadata file.
// Ensure we do not try to assign a provided topicId that is inconsistent with the ID on file.
if (partitionMetadataFile.exists()) {
if (!keepPartitionMetadataFile)
partitionMetadataFile.delete()
else
topicId = partitionMetadataFile.read().topicId
else {
val fileTopicId = partitionMetadataFile.read().topicId
if (topicId.isDefined && !topicId.contains(fileTopicId))
throw new InconsistentTopicIdException(s"Tried to assign topic ID $topicId to log for topic partition $topicPartition," +
s"but log already contained topic ID $fileTopicId")
topicId = Some(fileTopicId)
}
} else if (keepPartitionMetadataFile) {
topicId.foreach(partitionMetadataFile.write)
}
}

Expand Down Expand Up @@ -576,6 +589,12 @@ class Log(@volatile private var _dir: File,
partitionMetadataFile = new PartitionMetadataFile(partitionMetadata, logDirFailureChannel)
}

/** Only used for ZK clusters when we update and start using topic IDs on existing topics */
def assignTopicId(topicId: Uuid): Unit = {
partitionMetadataFile.write(topicId)
this.topicId = Some(topicId)
}

private def initializeLeaderEpochCache(): Unit = lock synchronized {
val leaderEpochFile = LeaderEpochCheckpointFile.newFile(dir)

Expand Down Expand Up @@ -2600,11 +2619,12 @@ object Log {
producerIdExpirationCheckIntervalMs: Int,
logDirFailureChannel: LogDirFailureChannel,
lastShutdownClean: Boolean = true,
keepPartitionMetadataFile: Boolean = true): Log = {
topicId: Option[Uuid],
keepPartitionMetadataFile: Boolean): Log = {
val topicPartition = Log.parseTopicPartitionName(dir)
val producerStateManager = new ProducerStateManager(topicPartition, dir, maxProducerIdExpirationMs)
new Log(dir, config, logStartOffset, recoveryPoint, scheduler, brokerTopicStats, time, maxProducerIdExpirationMs,
producerIdExpirationCheckIntervalMs, topicPartition, producerStateManager, logDirFailureChannel, lastShutdownClean, keepPartitionMetadataFile)
producerIdExpirationCheckIntervalMs, topicPartition, producerStateManager, logDirFailureChannel, lastShutdownClean, topicId, keepPartitionMetadataFile)
}

/**
Expand Down
26 changes: 22 additions & 4 deletions core/src/main/scala/kafka/log/LogManager.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import kafka.server.checkpoints.OffsetCheckpointFile
import kafka.server.metadata.ConfigRepository
import kafka.server._
import kafka.utils._
import org.apache.kafka.common.{KafkaException, TopicPartition}
import org.apache.kafka.common.{KafkaException, TopicPartition, Uuid}
import org.apache.kafka.common.utils.Time
import org.apache.kafka.common.errors.{KafkaStorageException, LogDirNotFoundException}
import org.apache.kafka.common.errors.{InconsistentTopicIdException, KafkaStorageException, LogDirNotFoundException}

import scala.jdk.CollectionConverters._
import scala.collection._
Expand Down Expand Up @@ -270,6 +270,7 @@ class LogManager(logDirs: Seq[File],
brokerTopicStats = brokerTopicStats,
logDirFailureChannel = logDirFailureChannel,
lastShutdownClean = hadCleanShutdown,
topicId = None,
keepPartitionMetadataFile = keepPartitionMetadataFile)

if (logDir.getName.endsWith(Log.DeleteDirSuffix)) {
Expand Down Expand Up @@ -775,11 +776,13 @@ class LogManager(logDirs: Seq[File],
* @param topicPartition The partition whose log needs to be returned or created
* @param isNew Whether the replica should have existed on the broker or not
* @param isFuture True if the future log of the specified partition should be returned or created
* @param topicId The topic ID of the partition's topic
* @throws KafkaStorageException if isNew=false, log is not found in the cache and there is offline log directory on the broker
* @throws InconsistentTopicIdException if the topic ID in the log does not match the topic ID provided
*/
def getOrCreateLog(topicPartition: TopicPartition, isNew: Boolean = false, isFuture: Boolean = false): Log = {
def getOrCreateLog(topicPartition: TopicPartition, isNew: Boolean = false, isFuture: Boolean = false, topicId: Option[Uuid]): Log = {
Comment thread
jolshan marked this conversation as resolved.
logCreationOrDeletionLock synchronized {
getLog(topicPartition, isFuture).getOrElse {
val log = getLog(topicPartition, isFuture).getOrElse {
// create the log if it has not already been created in another thread
if (!isNew && offlineLogDirs.nonEmpty)
throw new KafkaStorageException(s"Can not create log for $topicPartition because log directories ${offlineLogDirs.mkString(",")} are offline")
Expand Down Expand Up @@ -826,6 +829,7 @@ class LogManager(logDirs: Seq[File],
time = time,
brokerTopicStats = brokerTopicStats,
logDirFailureChannel = logDirFailureChannel,
topicId = topicId,
keepPartitionMetadataFile = keepPartitionMetadataFile)

if (isFuture)
Expand All @@ -839,6 +843,20 @@ class LogManager(logDirs: Seq[File],

log
}
// When running a ZK controller, we may get a log that does not have a topic ID. Assign it here.
if (log.topicId.isEmpty) {
topicId.foreach(log.assignTopicId)
}

// Ensure topic IDs are consistent
topicId.foreach { topicId =>
log.topicId.foreach { logTopicId =>
if (topicId != logTopicId)
throw new InconsistentTopicIdException(s"Tried to assign topic ID $topicId to log for topic partition $topicPartition," +
s"but log already contained topic ID $logTopicId")
}
}
log
}
}

Expand Down
1 change: 1 addition & 0 deletions core/src/main/scala/kafka/raft/KafkaMetadataLog.scala
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ object KafkaMetadataLog {
producerIdExpirationCheckIntervalMs = Int.MaxValue,
logDirFailureChannel = new LogDirFailureChannel(5),
lastShutdownClean = false,
topicId = None,
keepPartitionMetadataFile = false
)

Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/kafka/server/KafkaConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1892,8 +1892,9 @@ class KafkaConfig(val props: java.util.Map[_, _], doLog: Boolean, dynamicConfigO
}
}

// Topic IDs are used with all self-managed quorum clusters and ZK cluster with IBP greater than or equal to 2.8
def usesTopicId: Boolean =
Comment thread
jolshan marked this conversation as resolved.
interBrokerProtocolVersion >= KAFKA_2_8_IV0
usesSelfManagedQuorum || interBrokerProtocolVersion >= KAFKA_2_8_IV0

validateValues()

Expand Down
14 changes: 8 additions & 6 deletions core/src/main/scala/kafka/server/RaftReplicaChangeDelegate.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import kafka.log.Log
import kafka.server.checkpoints.OffsetCheckpoints
import kafka.server.metadata.{MetadataBrokers, MetadataPartition}
import kafka.utils.Implicits.MapExtensionMethods
import org.apache.kafka.common.TopicPartition
import org.apache.kafka.common.{TopicPartition, Uuid}
import org.apache.kafka.common.errors.KafkaStorageException

import scala.collection.{Map, Set, mutable}
Expand Down Expand Up @@ -72,7 +72,8 @@ class RaftReplicaChangeDelegate(helper: RaftReplicaChangeDelegateHelper) {
def makeLeaders(prevPartitionsAlreadyExisting: Set[MetadataPartition],
partitionStates: Map[Partition, MetadataPartition],
highWatermarkCheckpoints: OffsetCheckpoints,
metadataOffset: Option[Long]): Set[Partition] = {
metadataOffset: Option[Long],
topicIds: String => Option[Uuid]): Set[Partition] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could use a strong type here since topicIds are required for KIP-500, but maybe not worth it since we are delegating to Partition in the end.

val partitionsMadeLeaders = mutable.Set[Partition]()
val traceLoggingEnabled = helper.stateChangeLogger.isTraceEnabled
val deferredBatches = metadataOffset.isEmpty
Expand All @@ -94,7 +95,7 @@ class RaftReplicaChangeDelegate(helper: RaftReplicaChangeDelegateHelper) {
try {
val isrState = state.toLeaderAndIsrPartitionState(
!prevPartitionsAlreadyExisting(state))
if (partition.makeLeader(isrState, highWatermarkCheckpoints)) {
if (partition.makeLeader(isrState, highWatermarkCheckpoints, topicIds(partition.topic))) {
partitionsMadeLeaders += partition
if (traceLoggingEnabled) {
helper.stateChangeLogger.trace(s"$partitionLogMsgPrefix: completed the become-leader state change.")
Expand Down Expand Up @@ -125,7 +126,8 @@ class RaftReplicaChangeDelegate(helper: RaftReplicaChangeDelegateHelper) {
currentBrokers: MetadataBrokers,
partitionStates: Map[Partition, MetadataPartition],
highWatermarkCheckpoints: OffsetCheckpoints,
metadataOffset: Option[Long]): Set[Partition] = {
metadataOffset: Option[Long],
topicIds: String => Option[Uuid]): Set[Partition] = {
val traceLoggingEnabled = helper.stateChangeLogger.isTraceEnabled
val deferredBatches = metadataOffset.isEmpty
val topLevelLogPrefix = if (deferredBatches)
Expand Down Expand Up @@ -164,10 +166,10 @@ class RaftReplicaChangeDelegate(helper: RaftReplicaChangeDelegateHelper) {
s"since the new leader ${state.leaderId} is unavailable.")
// Create the local replica even if the leader is unavailable. This is required to ensure that we include
// the partition's high watermark in the checkpoint file (see KAFKA-1647)
partition.createLogIfNotExists(isNew, isFutureReplica = false, highWatermarkCheckpoints)
partition.createLogIfNotExists(isNew, isFutureReplica = false, highWatermarkCheckpoints, topicIds(partition.topic))
} else {
val isrState = state.toLeaderAndIsrPartitionState(isNew)
if (partition.makeFollower(isrState, highWatermarkCheckpoints)) {
if (partition.makeFollower(isrState, highWatermarkCheckpoints, topicIds(partition.topic))) {
partitionsMadeFollower += partition
if (traceLoggingEnabled) {
helper.stateChangeLogger.trace(s"$partitionLogMsgPrefix: completed the " +
Expand Down
Loading