Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,24 @@ public static MemoryRecords withRecords(byte magic, long initialOffset, Compress
return builder.build();
}

// for testing only, create a new memory-records that contains duplicated

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.

Seems like it would be easier to create a record set with two batches... Here's an example:

        ByteBuffer buffer = ByteBuffer.allocate(2048);
        MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, magic, compression, TimestampType.CREATE_TIME, 0L);
        builder.append(10L, "1".getBytes(), "a".getBytes());
        builder.close();

        builder = MemoryRecords.builder(buffer, magic, compression, TimestampType.CREATE_TIME, 1L);
        builder.append(11L, "2".getBytes(), "b".getBytes());
        builder.append(12L, "3".getBytes(), "c".getBytes());
        builder.close();
        buffer.flip();

In any case, perhaps we can move this to LogValidatorTest since we are not likely to need it elsewhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

builder.close(); would reset the position so that the next batch would overwrite the first one. Let me change a bit to make it work.

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.

It only resets the position of a dup. We use the pattern above in several existing test cases

// bytes (therefore duplicated batches) of the passed in memory records
public static MemoryRecords duplicateRecords(MemoryRecords inputRecords) {
ByteBuffer inputBuffer = inputRecords.buffer().duplicate ();
inputBuffer.limit (inputBuffer.position() + inputBuffer.capacity());
ByteBuffer outputBuffer = ByteBuffer.allocate(inputBuffer.capacity() * 2);
outputBuffer.put(inputBuffer);

// write again
inputBuffer = inputRecords.buffer().duplicate ();

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.

nit: unneeded space. A couple of these above also.

inputBuffer.limit (inputBuffer.position() + inputBuffer.capacity());
outputBuffer.put(inputBuffer);

outputBuffer.flip();
outputBuffer.position(0);
return MemoryRecords.readableRecords(outputBuffer.slice());
}

public static MemoryRecords withEndTransactionMarker(long producerId, short producerEpoch, EndTransactionMarker marker) {
return withEndTransactionMarker(0L, System.currentTimeMillis(), RecordBatch.NO_PARTITION_LEADER_EPOCH,
producerId, producerEpoch, marker);
Expand Down
154 changes: 84 additions & 70 deletions core/src/main/scala/kafka/log/LogValidator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ private[kafka] object LogValidator extends Logging {
}
}

private[kafka] def validateRecords(records: MemoryRecords): RecordBatch = {

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.

Maybe name should be validateCompressedRecords?

// Assume there's only one batch with compressed memory records; otherwise, return InvalidRecordException
val batchIterator = records.batches.iterator
val batch = batchIterator.next()
Comment thread
hachikuji marked this conversation as resolved.

if (batchIterator.hasNext) {
throw new InvalidRecordException("Compressed outer record should only have a single record batch")
}

batch
}

private def validateBatch(batch: RecordBatch, isFromClient: Boolean, toMagic: Byte): Unit = {
if (isFromClient) {
if (batch.magic >= RecordBatch.MAGIC_VALUE_V2) {
Expand Down Expand Up @@ -249,83 +261,85 @@ private[kafka] object LogValidator extends Logging {
partitionLeaderEpoch: Int,
isFromClient: Boolean,
interBrokerProtocolVersion: ApiVersion): ValidationAndOffsetAssignResult = {
// No in place assignment situation 1 and 2
var inPlaceAssignment = sourceCodec == targetCodec && toMagic > RecordBatch.MAGIC_VALUE_V0

var maxTimestamp = RecordBatch.NO_TIMESTAMP
val expectedInnerOffset = new LongRef(0)
val validatedRecords = new mutable.ArrayBuffer[Record]

var uncompressedSizeInBytes = 0

for (batch <- records.batches.asScala) {
validateBatch(batch, isFromClient, toMagic)
uncompressedSizeInBytes += AbstractRecords.recordBatchHeaderSizeInBytes(toMagic, batch.compressionType())

// Do not compress control records unless they are written compressed
if (sourceCodec == NoCompressionCodec && batch.isControlBatch)
inPlaceAssignment = true

for (record <- batch.asScala) {
if (sourceCodec != NoCompressionCodec && record.isCompressed)
throw new InvalidRecordException("Compressed outer record should not have an inner record with a " +
s"compression attribute set: $record")
if (targetCodec == ZStdCompressionCodec && interBrokerProtocolVersion < KAFKA_2_1_IV0)
throw new UnsupportedCompressionTypeException("Produce requests to inter.broker.protocol.version < 2.1 broker " + "are not allowed to use ZStandard compression")
validateRecord(batch, record, now, timestampType, timestampDiffMaxMs, compactedTopic)

uncompressedSizeInBytes += record.sizeInBytes()
if (batch.magic > RecordBatch.MAGIC_VALUE_V0 && toMagic > RecordBatch.MAGIC_VALUE_V0) {
// Check if we need to overwrite offset
// No in place assignment situation 3
if (record.offset != expectedInnerOffset.getAndIncrement())
inPlaceAssignment = false
if (record.timestamp > maxTimestamp)
maxTimestamp = record.timestamp
}

// No in place assignment situation 4
if (!record.hasMagic(toMagic))
inPlaceAssignment = false

validatedRecords += record
}

// No in place assignment situation 1 and 2
var inPlaceAssignment = sourceCodec == targetCodec && toMagic > RecordBatch.MAGIC_VALUE_V0

var maxTimestamp = RecordBatch.NO_TIMESTAMP
val expectedInnerOffset = new LongRef(0)
val validatedRecords = new mutable.ArrayBuffer[Record]

var uncompressedSizeInBytes = 0

val batch = validateRecords(records)

validateBatch(batch, isFromClient, toMagic)
uncompressedSizeInBytes += AbstractRecords.recordBatchHeaderSizeInBytes(toMagic, batch.compressionType())

// Do not compress control records unless they are written compressed
if (sourceCodec == NoCompressionCodec && batch.isControlBatch)
inPlaceAssignment = true

for (record <- batch.asScala) {
if (sourceCodec != NoCompressionCodec && record.isCompressed)
throw new InvalidRecordException("Compressed outer record should not have an inner record with a " +
s"compression attribute set: $record")
if (targetCodec == ZStdCompressionCodec && interBrokerProtocolVersion < KAFKA_2_1_IV0)
throw new UnsupportedCompressionTypeException("Produce requests to inter.broker.protocol.version < 2.1 broker " + "are not allowed to use ZStandard compression")
validateRecord(batch, record, now, timestampType, timestampDiffMaxMs, compactedTopic)

uncompressedSizeInBytes += record.sizeInBytes()
if (batch.magic > RecordBatch.MAGIC_VALUE_V0 && toMagic > RecordBatch.MAGIC_VALUE_V0) {
// Check if we need to overwrite offset
// No in place assignment situation 3
if (record.offset != expectedInnerOffset.getAndIncrement())
inPlaceAssignment = false
if (record.timestamp > maxTimestamp)
maxTimestamp = record.timestamp
}

if (!inPlaceAssignment) {
val (producerId, producerEpoch, sequence, isTransactional) = {
// note that we only reassign offsets for requests coming straight from a producer. For records with magic V2,
// there should be exactly one RecordBatch per request, so the following is all we need to do. For Records
// with older magic versions, there will never be a producer id, etc.
val first = records.batches.asScala.head
(first.producerId, first.producerEpoch, first.baseSequence, first.isTransactional)
}
buildRecordsAndAssignOffsets(toMagic, offsetCounter, time, timestampType, CompressionType.forId(targetCodec.codec), now,
validatedRecords, producerId, producerEpoch, sequence, isTransactional, partitionLeaderEpoch, isFromClient,
uncompressedSizeInBytes)
} else {
// we can update the batch only and write the compressed payload as is
val batch = records.batches.iterator.next()
val lastOffset = offsetCounter.addAndGet(validatedRecords.size) - 1
// No in place assignment situation 4
if (!record.hasMagic(toMagic))
inPlaceAssignment = false

batch.setLastOffset(lastOffset)
validatedRecords += record
}

if (timestampType == TimestampType.LOG_APPEND_TIME)
maxTimestamp = now
if (!inPlaceAssignment) {
val (producerId, producerEpoch, sequence, isTransactional) = {
// note that we only reassign offsets for requests coming straight from a producer. For records with magic V2,
// there should be exactly one RecordBatch per request, so the following is all we need to do. For Records
// with older magic versions, there will never be a producer id, etc.
val first = records.batches.asScala.head
(first.producerId, first.producerEpoch, first.baseSequence, first.isTransactional)
}
buildRecordsAndAssignOffsets(toMagic, offsetCounter, time, timestampType, CompressionType.forId(targetCodec.codec), now,
validatedRecords, producerId, producerEpoch, sequence, isTransactional, partitionLeaderEpoch, isFromClient,
uncompressedSizeInBytes)
} else {
// we can update the batch only and write the compressed payload as is;
// again we assume only one record batch within the compressed set
val batch = records.batches.iterator.next()
val lastOffset = offsetCounter.addAndGet(validatedRecords.size) - 1

if (toMagic >= RecordBatch.MAGIC_VALUE_V1)
batch.setMaxTimestamp(timestampType, maxTimestamp)
batch.setLastOffset(lastOffset)

if (toMagic >= RecordBatch.MAGIC_VALUE_V2)
batch.setPartitionLeaderEpoch(partitionLeaderEpoch)
if (timestampType == TimestampType.LOG_APPEND_TIME)
maxTimestamp = now

val recordConversionStats = new RecordConversionStats(uncompressedSizeInBytes, 0, 0)
ValidationAndOffsetAssignResult(validatedRecords = records,
maxTimestamp = maxTimestamp,
shallowOffsetOfMaxTimestamp = lastOffset,
messageSizeMaybeChanged = false,
recordConversionStats = recordConversionStats)
}
if (toMagic >= RecordBatch.MAGIC_VALUE_V1)
batch.setMaxTimestamp(timestampType, maxTimestamp)

if (toMagic >= RecordBatch.MAGIC_VALUE_V2)
batch.setPartitionLeaderEpoch(partitionLeaderEpoch)

val recordConversionStats = new RecordConversionStats(uncompressedSizeInBytes, 0, 0)
ValidationAndOffsetAssignResult(validatedRecords = records,
maxTimestamp = maxTimestamp,
shallowOffsetOfMaxTimestamp = lastOffset,
messageSizeMaybeChanged = false,
recordConversionStats = recordConversionStats)
}
}

private def buildRecordsAndAssignOffsets(magic: Byte,
Expand Down
30 changes: 28 additions & 2 deletions core/src/test/scala/unit/kafka/log/LogValidatorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,46 @@ import java.util.concurrent.TimeUnit
import kafka.api.{ApiVersion, KAFKA_2_0_IV1}
import kafka.common.LongRef
import kafka.message._
import org.apache.kafka.common.errors.{InvalidTimestampException, UnsupportedCompressionTypeException, UnsupportedForMessageFormatException}
import org.apache.kafka.common.errors.{InvalidTimestampException, KafkaStorageException, UnsupportedCompressionTypeException, UnsupportedForMessageFormatException}
import org.apache.kafka.common.record._
import org.apache.kafka.common.utils.Time
import org.apache.kafka.test.TestUtils
import org.junit.Assert._
import org.junit.Test
import org.scalatest.Assertions.intercept
import org.scalatest.Assertions.{assertThrows, intercept}

import scala.collection.JavaConverters._

class LogValidatorTest {

val time = Time.SYSTEM

@Test
def testOnlyOneBatchV0(): Unit = {
checkOnlyOneBatchCompressed(RecordBatch.MAGIC_VALUE_V0)
}

@Test
def testOnlyOneBatchV1(): Unit = {
checkOnlyOneBatchCompressed(RecordBatch.MAGIC_VALUE_V1)
}

@Test
def testOnlyOneBatchV2(): Unit = {
checkOnlyOneBatchCompressed(RecordBatch.MAGIC_VALUE_V2)
}

private def checkOnlyOneBatchCompressed(magic: Byte) {
val records = createRecords(magic, 0L, CompressionType.GZIP)
val duplicates = MemoryRecords.duplicateRecords(records)

LogValidator.validateRecords(records)

assertThrows[InvalidRecordException] {
LogValidator.validateRecords(duplicates)
}
}

@Test
def testLogAppendTimeNonCompressedV1() {
checkLogAppendTimeNonCompressed(RecordBatch.MAGIC_VALUE_V1)
Expand Down