-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-9838; Add log concurrency test and fix minor race condition #8476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,183 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package kafka.log | ||
|
|
||
| import java.util.Properties | ||
| import java.util.concurrent.{Callable, Executors} | ||
|
|
||
| import kafka.server.{BrokerTopicStats, FetchHighWatermark, LogDirFailureChannel} | ||
| import kafka.utils.{KafkaScheduler, TestUtils} | ||
| import org.apache.kafka.common.record.SimpleRecord | ||
| import org.apache.kafka.common.utils.{Time, Utils} | ||
| import org.junit.Assert._ | ||
| import org.junit.{After, Before, Test} | ||
|
|
||
| import scala.collection.mutable.ListBuffer | ||
| import scala.util.Random | ||
|
|
||
| class LogConcurrencyTest { | ||
| private val brokerTopicStats = new BrokerTopicStats | ||
| private val random = new Random() | ||
| private val scheduler = new KafkaScheduler(1) | ||
| private val tmpDir = TestUtils.tempDir() | ||
| private val logDir = TestUtils.randomPartitionLogDir(tmpDir) | ||
|
|
||
| @Before | ||
| def setup(): Unit = { | ||
| scheduler.startup() | ||
| } | ||
|
|
||
| @After | ||
| def shutdown(): Unit = { | ||
| scheduler.shutdown() | ||
| Utils.delete(tmpDir) | ||
| } | ||
|
|
||
| @Test | ||
| def testUncommittedDataNotConsumed(): Unit = { | ||
| testUncommittedDataNotConsumed(createLog()) | ||
| } | ||
|
|
||
| @Test | ||
| def testUncommittedDataNotConsumedFrequentSegmentRolls(): Unit = { | ||
| val logProps = new Properties() | ||
| logProps.put(LogConfig.SegmentBytesProp, 237: Integer) | ||
| val logConfig = LogConfig(logProps) | ||
| testUncommittedDataNotConsumed(createLog(logConfig)) | ||
| } | ||
|
|
||
| def testUncommittedDataNotConsumed(log: Log): Unit = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just curious how long would a single run take with 5000 records and a max batchsize of 10? Being a bit paranoid of it taking too long.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It takes about 10 second locally, which seems reasonable for a concurrency test. |
||
| val executor = Executors.newFixedThreadPool(2) | ||
| try { | ||
| val maxOffset = 5000 | ||
| val consumer = new ConsumerTask(log, maxOffset) | ||
| val appendTask = new LogAppendTask(log, maxOffset) | ||
|
|
||
| val consumerFuture = executor.submit(consumer) | ||
| val fetcherTaskFuture = executor.submit(appendTask) | ||
|
|
||
| fetcherTaskFuture.get() | ||
| consumerFuture.get() | ||
|
|
||
| validateConsumedData(log, consumer.consumedBatches) | ||
| } finally executor.shutdownNow() | ||
| } | ||
|
|
||
| /** | ||
| * Simple consumption task which reads the log in ascending order and collects | ||
| * consumed batches for validation | ||
| */ | ||
| private class ConsumerTask(log: Log, lastOffset: Int) extends Callable[Unit] { | ||
| val consumedBatches = ListBuffer.empty[FetchedBatch] | ||
|
|
||
| override def call(): Unit = { | ||
| var fetchOffset = 0L | ||
| while (log.highWatermark < lastOffset) { | ||
| val readInfo = log.read( | ||
| startOffset = fetchOffset, | ||
| maxLength = 1, | ||
| isolation = FetchHighWatermark, | ||
| minOneMessage = true | ||
| ) | ||
| readInfo.records.batches().forEach { batch => | ||
| consumedBatches += FetchedBatch(batch.baseOffset, batch.partitionLeaderEpoch) | ||
| fetchOffset = batch.lastOffset + 1 | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * This class simulates basic leader/follower behavior. | ||
| */ | ||
| private class LogAppendTask(log: Log, lastOffset: Long) extends Callable[Unit] { | ||
| override def call(): Unit = { | ||
| var leaderEpoch = 1 | ||
| var isLeader = true | ||
|
|
||
| while (log.highWatermark < lastOffset) { | ||
| random.nextInt(2) match { | ||
| case 0 => | ||
| val logEndOffsetMetadata = log.logEndOffsetMetadata | ||
| val logEndOffset = logEndOffsetMetadata.messageOffset | ||
| val batchSize = random.nextInt(9) + 1 | ||
| val records = (0 to batchSize).map(i => new SimpleRecord(s"$i".getBytes)) | ||
|
|
||
| if (isLeader) { | ||
| log.appendAsLeader(TestUtils.records(records), leaderEpoch) | ||
| log.maybeIncrementHighWatermark(logEndOffsetMetadata) | ||
| } else { | ||
| log.appendAsFollower(TestUtils.records(records, | ||
| baseOffset = logEndOffset, | ||
| partitionLeaderEpoch = leaderEpoch)) | ||
| log.updateHighWatermark(logEndOffset) | ||
| } | ||
|
|
||
| case 1 => | ||
| isLeader = !isLeader | ||
| leaderEpoch += 1 | ||
|
|
||
| if (!isLeader) { | ||
| log.truncateTo(log.highWatermark) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private def createLog(config: LogConfig = LogConfig(new Properties())): Log = { | ||
| Log(dir = logDir, | ||
| config = config, | ||
| logStartOffset = 0L, | ||
| recoveryPoint = 0L, | ||
| scheduler = scheduler, | ||
| brokerTopicStats = brokerTopicStats, | ||
| time = Time.SYSTEM, | ||
| maxProducerIdExpirationMs = 60 * 60 * 1000, | ||
| producerIdExpirationCheckIntervalMs = LogManager.ProducerIdExpirationCheckIntervalMs, | ||
| logDirFailureChannel = new LogDirFailureChannel(10)) | ||
| } | ||
|
|
||
| private def validateConsumedData(log: Log, consumedBatches: Iterable[FetchedBatch]): Unit = { | ||
| val iter = consumedBatches.iterator | ||
| log.logSegments.foreach { segment => | ||
| segment.log.batches.forEach { batch => | ||
| if (iter.hasNext) { | ||
| val consumedBatch = iter.next() | ||
| try { | ||
| assertEquals("Consumed batch with unexpected leader epoch", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we check last / next offset of the batch as well?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could, but I thought it would be overkill since the generation logic generates batches which are unique by base offset and leader epoch.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool |
||
| batch.partitionLeaderEpoch, consumedBatch.epoch) | ||
| assertEquals("Consumed batch with unexpected base offset", | ||
| batch.baseOffset, consumedBatch.baseOffset) | ||
| } catch { | ||
| case t: Throwable => | ||
| throw new AssertionError(s"Consumed batch $consumedBatch " + | ||
| s"does not match next expected batch in log $batch", t) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private case class FetchedBatch(baseOffset: Long, epoch: Int) { | ||
| override def toString: String = { | ||
| s"FetchedBatch(baseOffset=$baseOffset, epoch=$epoch)" | ||
| } | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why in this case we use
maxOffsetMetadataand in the else if we usestartOffsetMetadataasemptyFetchDataInfo#fetchOffsetMetadata? Is there a specific rationale for that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the start offset matches max offset, then we do not need to lookup the metadata as we already have it. Really this is the bug fix. Without this, then we will proceed to read from the log segment, which results in an error like the following:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I understand this is the bug fix, I'm only curious to see why we used
not
I realized it is just the same, so curious if you intentionally use
maxOffsetMetadatafor any other reasons, but I think it is just to avoid unnecessaryconvertToOffsetMetadataOrThrow:)Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It lets us skip the call to
convertToOffsetMetadataOrThrowin order to findstartOffsetMetadata. I tried changing the check below to>=and hit a similar error when trying to translate the offset inLogSegment. We might also be able to fix this by adding some additional checks inLogSegmentto avoid the offset translation when the max position matches the start position of the requested offset.