diff --git a/checkstyle/import-control.xml b/checkstyle/import-control.xml
index 8eebdb551ac28..62cd77a20783f 100644
--- a/checkstyle/import-control.xml
+++ b/checkstyle/import-control.xml
@@ -89,6 +89,7 @@
+
diff --git a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java
index 6fb4229f1112c..3b9d49c4b12c1 100644
--- a/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java
+++ b/clients/src/main/java/org/apache/kafka/clients/consumer/internals/Fetcher.java
@@ -61,6 +61,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
@@ -686,11 +687,13 @@ private PartitionRecords parseFetchedData(CompletedFetch completedFetch) {
}
List> parsed = new ArrayList<>();
- for (LogEntry logEntry : partition.records) {
+ Iterator deepIterator = partition.records.deepIterator();
+ while (deepIterator.hasNext()) {
+ LogEntry logEntry = deepIterator.next();
// Skip the messages earlier than current position.
if (logEntry.offset() >= position) {
parsed.add(parseRecord(tp, logEntry));
- bytes += logEntry.size();
+ bytes += logEntry.sizeInBytes();
}
}
diff --git a/clients/src/main/java/org/apache/kafka/clients/producer/internals/BufferPool.java b/clients/src/main/java/org/apache/kafka/clients/producer/internals/BufferPool.java
index b42b0ec010fce..077215c0f3589 100644
--- a/clients/src/main/java/org/apache/kafka/clients/producer/internals/BufferPool.java
+++ b/clients/src/main/java/org/apache/kafka/clients/producer/internals/BufferPool.java
@@ -198,7 +198,7 @@ private void freeUp(int size) {
* memory as free.
*
* @param buffer The buffer to return
- * @param size The size of the buffer to mark as deallocated, note that this maybe smaller than buffer.capacity
+ * @param size The size of the buffer to mark as deallocated, note that this may be smaller than buffer.capacity
* since the buffer may re-allocate itself during in-place compression
*/
public void deallocate(ByteBuffer buffer, int size) {
diff --git a/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordAccumulator.java b/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordAccumulator.java
index fa1e51352626b..06d39ecc68cab 100644
--- a/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordAccumulator.java
+++ b/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordAccumulator.java
@@ -27,8 +27,10 @@
import org.apache.kafka.common.metrics.stats.Rate;
import org.apache.kafka.common.record.CompressionType;
import org.apache.kafka.common.record.MemoryRecords;
+import org.apache.kafka.common.record.MemoryRecordsBuilder;
import org.apache.kafka.common.record.Record;
import org.apache.kafka.common.record.Records;
+import org.apache.kafka.common.record.TimestampType;
import org.apache.kafka.common.utils.CopyOnWriteMap;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;
@@ -49,7 +51,7 @@
import java.util.concurrent.atomic.AtomicInteger;
/**
- * This class acts as a queue that accumulates records into {@link org.apache.kafka.common.record.MemoryRecords}
+ * This class acts as a queue that accumulates records into {@link MemoryRecords}
* instances to be sent to the server.
*
* The accumulator uses a bounded amount of memory and append calls will block when that memory is exhausted, unless
@@ -77,7 +79,7 @@ public final class RecordAccumulator {
/**
* Create a new record accumulator
*
- * @param batchSize The size to use when allocating {@link org.apache.kafka.common.record.MemoryRecords} instances
+ * @param batchSize The size to use when allocating {@link MemoryRecords} instances
* @param totalSize The maximum memory the record accumulator can use.
* @param compression The compression codec for the records
* @param lingerMs An artificial delay time to add before declaring a records instance that isn't full ready for
@@ -190,13 +192,13 @@ public RecordAppendResult append(TopicPartition tp,
free.deallocate(buffer);
return appendResult;
}
- MemoryRecords records = MemoryRecords.emptyRecords(buffer, compression, this.batchSize);
- RecordBatch batch = new RecordBatch(tp, records, time.milliseconds());
+ MemoryRecordsBuilder recordsBuilder = MemoryRecords.builder(buffer, compression, TimestampType.CREATE_TIME, this.batchSize);
+ RecordBatch batch = new RecordBatch(tp, recordsBuilder, time.milliseconds());
FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, callback, time.milliseconds()));
dq.addLast(batch);
incomplete.add(batch);
- return new RecordAppendResult(future, dq.size() > 1 || batch.records.isFull(), true);
+ return new RecordAppendResult(future, dq.size() > 1 || batch.isFull(), true);
}
} finally {
appendsInProgress.decrementAndGet();
@@ -212,9 +214,9 @@ private RecordAppendResult tryAppend(long timestamp, byte[] key, byte[] value, C
if (last != null) {
FutureRecordMetadata future = last.tryAppend(timestamp, key, value, callback, time.milliseconds());
if (future == null)
- last.records.close();
+ last.close();
else
- return new RecordAppendResult(future, deque.size() > 1 || last.records.isFull(), false);
+ return new RecordAppendResult(future, deque.size() > 1 || last.isFull(), false);
}
return null;
}
@@ -240,7 +242,7 @@ public List abortExpiredBatches(int requestTimeout, long now) {
Iterator batchIterator = dq.iterator();
while (batchIterator.hasNext()) {
RecordBatch batch = batchIterator.next();
- boolean isFull = batch != lastBatch || batch.records.isFull();
+ boolean isFull = batch != lastBatch || batch.isFull();
// check if the batch is expired
if (batch.maybeExpire(requestTimeout, retryBackoffMs, now, this.lingerMs, isFull)) {
expiredBatches.add(batch);
@@ -319,7 +321,7 @@ public ReadyCheckResult ready(Cluster cluster, long nowMs) {
long waitedTimeMs = nowMs - batch.lastAttemptMs;
long timeToWaitMs = backingOff ? retryBackoffMs : lingerMs;
long timeLeftMs = Math.max(timeToWaitMs - waitedTimeMs, 0);
- boolean full = deque.size() > 1 || batch.records.isFull();
+ boolean full = deque.size() > 1 || batch.isFull();
boolean expired = waitedTimeMs >= timeToWaitMs;
boolean sendable = full || expired || exhausted || closed || flushInProgress();
if (sendable && !backingOff) {
@@ -389,15 +391,15 @@ public Map> drain(Cluster cluster,
boolean backoff = first.attempts > 0 && first.lastAttemptMs + retryBackoffMs > now;
// Only drain the batch if it is not during backoff period.
if (!backoff) {
- if (size + first.records.sizeInBytes() > maxSize && !ready.isEmpty()) {
+ if (size + first.sizeInBytes() > maxSize && !ready.isEmpty()) {
// there is a rare case that a single batch size is larger than the request size due
// to compression; in this case we will still eventually send this batch in a single
// request
break;
} else {
RecordBatch batch = deque.pollFirst();
- batch.records.close();
- size += batch.records.sizeInBytes();
+ batch.close();
+ size += batch.sizeInBytes();
ready.add(batch);
batch.drainedMs = now;
}
@@ -437,7 +439,7 @@ private Deque getOrCreateDeque(TopicPartition tp) {
*/
public void deallocate(RecordBatch batch) {
incomplete.remove(batch);
- free.deallocate(batch.records.buffer(), batch.records.initialCapacity());
+ free.deallocate(batch.buffer(), batch.initialCapacity());
}
/**
@@ -507,7 +509,7 @@ private void abortBatches() {
Deque dq = getDeque(batch.topicPartition);
// Close the batch before aborting
synchronized (dq) {
- batch.records.close();
+ batch.close();
dq.remove(batch);
}
batch.done(-1L, Record.NO_TIMESTAMP, new IllegalStateException("Producer is closed forcefully."));
diff --git a/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordBatch.java b/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordBatch.java
index 6706bfd5ea11a..e9ef441e0e28e 100644
--- a/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordBatch.java
+++ b/clients/src/main/java/org/apache/kafka/clients/producer/internals/RecordBatch.java
@@ -12,18 +12,20 @@
*/
package org.apache.kafka.clients.producer.internals;
-import java.util.ArrayList;
-import java.util.List;
-
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.record.MemoryRecords;
+import org.apache.kafka.common.record.MemoryRecordsBuilder;
import org.apache.kafka.common.record.Record;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* A batch of records that is or will be sent.
*
@@ -39,21 +41,21 @@ public final class RecordBatch {
public final long createdMs;
public long drainedMs;
public long lastAttemptMs;
- public final MemoryRecords records;
public final TopicPartition topicPartition;
public final ProduceRequestResult produceFuture;
public long lastAppendTime;
private final List thunks;
private long offsetCounter = 0L;
private boolean retry;
+ private final MemoryRecordsBuilder recordsBuilder;
- public RecordBatch(TopicPartition tp, MemoryRecords records, long now) {
+ public RecordBatch(TopicPartition tp, MemoryRecordsBuilder recordsBuilder, long now) {
this.createdMs = now;
this.lastAttemptMs = now;
- this.records = records;
+ this.recordsBuilder = recordsBuilder;
this.topicPartition = tp;
this.produceFuture = new ProduceRequestResult();
- this.thunks = new ArrayList();
+ this.thunks = new ArrayList<>();
this.lastAppendTime = createdMs;
this.retry = false;
}
@@ -64,10 +66,10 @@ public RecordBatch(TopicPartition tp, MemoryRecords records, long now) {
* @return The RecordSend corresponding to this record or null if there isn't sufficient room.
*/
public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Callback callback, long now) {
- if (!this.records.hasRoomFor(key, value)) {
+ if (!recordsBuilder.hasRoomFor(key, value)) {
return null;
} else {
- long checksum = this.records.append(offsetCounter++, timestamp, key, value);
+ long checksum = this.recordsBuilder.append(offsetCounter++, timestamp, key, value);
this.maxRecordSize = Math.max(this.maxRecordSize, Record.recordSize(key, value));
this.lastAppendTime = now;
FutureRecordMetadata future = new FutureRecordMetadata(this.produceFuture, this.recordCount,
@@ -94,9 +96,8 @@ public void done(long baseOffset, long timestamp, RuntimeException exception) {
baseOffset,
exception);
// execute callbacks
- for (int i = 0; i < this.thunks.size(); i++) {
+ for (Thunk thunk : thunks) {
try {
- Thunk thunk = this.thunks.get(i);
if (exception == null) {
// If the timestamp returned by server is NoTimestamp, that means CreateTime is used. Otherwise LogAppendTime is used.
RecordMetadata metadata = new RecordMetadata(this.topicPartition, baseOffset, thunk.future.relativeOffset(),
@@ -156,7 +157,7 @@ public boolean maybeExpire(int requestTimeoutMs, long retryBackoffMs, long now,
}
if (expire) {
- this.records.close();
+ close();
this.done(-1L, Record.NO_TIMESTAMP,
new TimeoutException("Expiring " + recordCount + " record(s) for " + topicPartition + " due to " + errorMessage));
}
@@ -177,4 +178,37 @@ public boolean inRetry() {
public void setRetry() {
this.retry = true;
}
+
+ public MemoryRecords records() {
+ return recordsBuilder.build();
+ }
+
+ public int sizeInBytes() {
+ return recordsBuilder.sizeInBytes();
+ }
+
+ public double compressionRate() {
+ return recordsBuilder.compressionRate();
+ }
+
+ public boolean isFull() {
+ return recordsBuilder.isFull();
+ }
+
+ public void close() {
+ recordsBuilder.close();
+ }
+
+ public ByteBuffer buffer() {
+ return recordsBuilder.buffer();
+ }
+
+ public int initialCapacity() {
+ return recordsBuilder.initialCapacity();
+ }
+
+ public boolean isWritable() {
+ return !recordsBuilder.isClosed();
+ }
+
}
diff --git a/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java b/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
index 7555b71ce08c6..1f54c0b8452a6 100644
--- a/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
+++ b/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
@@ -345,7 +345,7 @@ private void sendProduceRequest(long now, int destination, short acks, int timeo
final Map recordsByPartition = new HashMap<>(batches.size());
for (RecordBatch batch : batches) {
TopicPartition tp = batch.topicPartition;
- produceRecordsByPartition.put(tp, batch.records);
+ produceRecordsByPartition.put(tp, batch.records());
recordsByPartition.put(tp, batch);
}
@@ -505,17 +505,17 @@ public void updateProduceRequestMetrics(Map> batches)
// per-topic bytes send rate
String topicByteRateName = "topic." + topic + ".bytes";
Sensor topicByteRate = Utils.notNull(this.metrics.getSensor(topicByteRateName));
- topicByteRate.record(batch.records.sizeInBytes());
+ topicByteRate.record(batch.sizeInBytes());
// per-topic compression rate
String topicCompressionRateName = "topic." + topic + ".compression-rate";
Sensor topicCompressionRate = Utils.notNull(this.metrics.getSensor(topicCompressionRateName));
- topicCompressionRate.record(batch.records.compressionRate());
+ topicCompressionRate.record(batch.compressionRate());
// global metrics
- this.batchSizeSensor.record(batch.records.sizeInBytes(), now);
+ this.batchSizeSensor.record(batch.sizeInBytes(), now);
this.queueTimeSensor.record(batch.drainedMs - batch.createdMs, now);
- this.compressionRateSensor.record(batch.records.compressionRate());
+ this.compressionRateSensor.record(batch.compressionRate());
this.maxRecordSizeSensor.record(batch.maxRecordSize, now);
records += batch.recordCount;
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/AbstractRecords.java b/clients/src/main/java/org/apache/kafka/common/record/AbstractRecords.java
new file mode 100644
index 0000000000000..3794dc6c0a059
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/record/AbstractRecords.java
@@ -0,0 +1,92 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.apache.kafka.common.utils.AbstractIterator;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public abstract class AbstractRecords implements Records {
+
+ @Override
+ public boolean hasMatchingShallowMagic(byte magic) {
+ Iterator extends LogEntry> iterator = shallowIterator();
+ while (iterator.hasNext())
+ if (iterator.next().magic() != magic)
+ return false;
+ return true;
+ }
+
+ /**
+ * Convert this message set to use the specified message format.
+ */
+ @Override
+ public Records toMessageFormat(byte toMagic) {
+ List converted = new ArrayList<>();
+ Iterator deepIterator = deepIterator();
+ while (deepIterator.hasNext()) {
+ LogEntry entry = deepIterator.next();
+ converted.add(LogEntry.create(entry.offset(), entry.record().convert(toMagic)));
+ }
+
+ if (converted.isEmpty()) {
+ // This indicates that the message is too large, which indicates that the buffer is not large
+ // enough to hold a full log entry. We just return all the bytes in the file message set.
+ // Even though the message set does not have the right format version, we expect old clients
+ // to raise an error to the user after reading the message size and seeing that there
+ // are not enough available bytes in the response to read the full message.
+ return this;
+ } else {
+ // We use the first message to determine the compression type for the resulting message set.
+ // This could result in message sets which are either larger or smaller than the original size.
+ // For example, it could end up larger if most messages were previously compressed, but
+ // it just so happens that the first one is not. There is also some risk that this can
+ // cause some timestamp information to be lost (e.g. if the timestamp type was changed) since
+ // we are essentially merging multiple message sets. However, currently this method is only
+ // used for down-conversion, so we've ignored the problem.
+ CompressionType compressionType = shallowIterator().next().record().compressionType();
+ return MemoryRecords.withLogEntries(compressionType, converted);
+ }
+ }
+
+ public static int estimatedSize(CompressionType compressionType, Iterable entries) {
+ int size = 0;
+ for (LogEntry entry : entries)
+ size += entry.sizeInBytes();
+ // NOTE: 1024 is the minimum block size for snappy encoding
+ return compressionType == CompressionType.NONE ? size : Math.min(Math.max(size / 2, 1024), 1 << 16);
+ }
+
+ /**
+ * Get an iterator over the deep records.
+ * @return An iterator over the records
+ */
+ public Iterator records() {
+ return new AbstractIterator() {
+ private final Iterator extends LogEntry> deepEntries = deepIterator();
+ @Override
+ protected Record makeNext() {
+ if (deepEntries.hasNext())
+ return deepEntries.next().record();
+ return allDone();
+ }
+ };
+ }
+
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/ByteBufferInputStream.java b/clients/src/main/java/org/apache/kafka/common/record/ByteBufferInputStream.java
index 84668a586dba0..b25f949d2c286 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/ByteBufferInputStream.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/ByteBufferInputStream.java
@@ -16,34 +16,41 @@
*/
package org.apache.kafka.common.record;
+import java.io.DataInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* A byte buffer backed input inputStream
*/
-public class ByteBufferInputStream extends InputStream {
-
- private ByteBuffer buffer;
+public class ByteBufferInputStream extends DataInputStream {
public ByteBufferInputStream(ByteBuffer buffer) {
- this.buffer = buffer;
+ super(new UnderlyingInputStream(buffer));
}
- public int read() {
- if (!buffer.hasRemaining()) {
- return -1;
+ private static class UnderlyingInputStream extends InputStream {
+ private ByteBuffer buffer;
+
+ public UnderlyingInputStream(ByteBuffer buffer) {
+ this.buffer = buffer;
}
- return buffer.get() & 0xFF;
- }
- public int read(byte[] bytes, int off, int len) {
- if (!buffer.hasRemaining()) {
- return -1;
+ public int read() {
+ if (!buffer.hasRemaining()) {
+ return -1;
+ }
+ return buffer.get() & 0xFF;
}
- len = Math.min(len, buffer.remaining());
- buffer.get(bytes, off, len);
- return len;
+ public int read(byte[] bytes, int off, int len) {
+ if (!buffer.hasRemaining()) {
+ return -1;
+ }
+
+ len = Math.min(len, buffer.remaining());
+ buffer.get(bytes, off, len);
+ return len;
+ }
}
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/ByteBufferLogInputStream.java b/clients/src/main/java/org/apache/kafka/common/record/ByteBufferLogInputStream.java
new file mode 100644
index 0000000000000..ae0c91b20dd80
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/record/ByteBufferLogInputStream.java
@@ -0,0 +1,119 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.apache.kafka.common.errors.CorruptRecordException;
+import org.apache.kafka.common.utils.Utils;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import static org.apache.kafka.common.record.Records.LOG_OVERHEAD;
+import static org.apache.kafka.common.record.Records.OFFSET_OFFSET;
+
+/**
+ * A byte buffer backed log input stream. This class avoids the need to copy records by returning
+ * slices from the underlying byte buffer.
+ */
+class ByteBufferLogInputStream implements LogInputStream {
+ private final ByteBuffer buffer;
+ private final int maxMessageSize;
+
+ ByteBufferLogInputStream(ByteBuffer buffer, int maxMessageSize) {
+ this.buffer = buffer;
+ this.maxMessageSize = maxMessageSize;
+ }
+
+ public ByteBufferLogEntry nextEntry() throws IOException {
+ int remaining = buffer.remaining();
+ if (remaining < LOG_OVERHEAD)
+ return null;
+
+ int recordSize = buffer.getInt(buffer.position() + Records.SIZE_OFFSET);
+ if (recordSize < Record.RECORD_OVERHEAD_V0)
+ throw new CorruptRecordException(String.format("Record size is less than the minimum record overhead (%d)", Record.RECORD_OVERHEAD_V0));
+ if (recordSize > maxMessageSize)
+ throw new CorruptRecordException(String.format("Record size exceeds the largest allowable message size (%d).", maxMessageSize));
+
+ int entrySize = recordSize + LOG_OVERHEAD;
+ if (remaining < entrySize)
+ return null;
+
+ ByteBuffer entrySlice = buffer.slice();
+ entrySlice.limit(entrySize);
+ buffer.position(buffer.position() + entrySize);
+ return new ByteBufferLogEntry(entrySlice);
+ }
+
+ public static class ByteBufferLogEntry extends LogEntry {
+ private final ByteBuffer buffer;
+ private final Record record;
+
+ private ByteBufferLogEntry(ByteBuffer buffer) {
+ this.buffer = buffer;
+ buffer.position(LOG_OVERHEAD);
+ this.record = new Record(buffer.slice());
+ buffer.position(OFFSET_OFFSET);
+ }
+
+ @Override
+ public long offset() {
+ return buffer.getLong(OFFSET_OFFSET);
+ }
+
+ @Override
+ public Record record() {
+ return record;
+ }
+
+ public void setOffset(long offset) {
+ buffer.putLong(OFFSET_OFFSET, offset);
+ }
+
+ public void setCreateTime(long timestamp) {
+ if (record.magic() == Record.MAGIC_VALUE_V0)
+ throw new IllegalArgumentException("Cannot set timestamp for a record with magic = 0");
+
+ long currentTimestamp = record.timestamp();
+ // We don't need to recompute crc if the timestamp is not updated.
+ if (record.timestampType() == TimestampType.CREATE_TIME && currentTimestamp == timestamp)
+ return;
+
+ byte attributes = record.attributes();
+ buffer.put(LOG_OVERHEAD + Record.ATTRIBUTES_OFFSET, TimestampType.CREATE_TIME.updateAttributes(attributes));
+ buffer.putLong(LOG_OVERHEAD + Record.TIMESTAMP_OFFSET, timestamp);
+ long crc = record.computeChecksum();
+ Utils.writeUnsignedInt(buffer, LOG_OVERHEAD + Record.CRC_OFFSET, crc);
+ }
+
+ public void setLogAppendTime(long timestamp) {
+ if (record.magic() == Record.MAGIC_VALUE_V0)
+ throw new IllegalArgumentException("Cannot set timestamp for a record with magic = 0");
+
+ byte attributes = record.attributes();
+ buffer.put(LOG_OVERHEAD + Record.ATTRIBUTES_OFFSET, TimestampType.LOG_APPEND_TIME.updateAttributes(attributes));
+ buffer.putLong(LOG_OVERHEAD + Record.TIMESTAMP_OFFSET, timestamp);
+ long crc = record.computeChecksum();
+ Utils.writeUnsignedInt(buffer, LOG_OVERHEAD + Record.CRC_OFFSET, crc);
+ }
+
+ public ByteBuffer buffer() {
+ return buffer;
+ }
+ }
+
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/ByteBufferOutputStream.java b/clients/src/main/java/org/apache/kafka/common/record/ByteBufferOutputStream.java
index 1c9fbaa958423..3fb7f49f5d643 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/ByteBufferOutputStream.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/ByteBufferOutputStream.java
@@ -16,42 +16,54 @@
*/
package org.apache.kafka.common.record;
+import java.io.DataOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* A byte buffer backed output outputStream
*/
-public class ByteBufferOutputStream extends OutputStream {
+public class ByteBufferOutputStream extends DataOutputStream {
private static final float REALLOCATION_FACTOR = 1.1f;
- private ByteBuffer buffer;
-
public ByteBufferOutputStream(ByteBuffer buffer) {
- this.buffer = buffer;
+ super(new UnderlyingOutputStream(buffer));
}
- public void write(int b) {
- if (buffer.remaining() < 1)
- expandBuffer(buffer.capacity() + 1);
- buffer.put((byte) b);
+ public ByteBuffer buffer() {
+ return ((UnderlyingOutputStream) out).buffer;
}
- public void write(byte[] bytes, int off, int len) {
- if (buffer.remaining() < len)
- expandBuffer(buffer.capacity() + len);
- buffer.put(bytes, off, len);
- }
+ public static class UnderlyingOutputStream extends OutputStream {
+ private ByteBuffer buffer;
- public ByteBuffer buffer() {
- return buffer;
- }
+ public UnderlyingOutputStream(ByteBuffer buffer) {
+ this.buffer = buffer;
+ }
- private void expandBuffer(int size) {
- int expandSize = Math.max((int) (buffer.capacity() * REALLOCATION_FACTOR), size);
- ByteBuffer temp = ByteBuffer.allocate(expandSize);
- temp.put(buffer.array(), buffer.arrayOffset(), buffer.position());
- buffer = temp;
+ public void write(int b) {
+ if (buffer.remaining() < 1)
+ expandBuffer(buffer.capacity() + 1);
+ buffer.put((byte) b);
+ }
+
+ public void write(byte[] bytes, int off, int len) {
+ if (buffer.remaining() < len)
+ expandBuffer(buffer.capacity() + len);
+ buffer.put(bytes, off, len);
+ }
+
+ public ByteBuffer buffer() {
+ return buffer;
+ }
+
+ private void expandBuffer(int size) {
+ int expandSize = Math.max((int) (buffer.capacity() * REALLOCATION_FACTOR), size);
+ ByteBuffer temp = ByteBuffer.allocate(expandSize);
+ temp.put(buffer.array(), buffer.arrayOffset(), buffer.position());
+ buffer = temp;
+ }
}
+
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/CompressionType.java b/clients/src/main/java/org/apache/kafka/common/record/CompressionType.java
index 65a7e4323793d..e1d4754ed0411 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/CompressionType.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/CompressionType.java
@@ -26,7 +26,7 @@ public enum CompressionType {
public final String name;
public final float rate;
- private CompressionType(int id, String name, float rate) {
+ CompressionType(int id, String name, float rate) {
this.id = id;
this.name = name;
this.rate = rate;
diff --git a/clients/src/main/java/org/apache/kafka/common/record/Compressor.java b/clients/src/main/java/org/apache/kafka/common/record/Compressor.java
deleted file mode 100644
index a806975959edd..0000000000000
--- a/clients/src/main/java/org/apache/kafka/common/record/Compressor.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/**
- * 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 org.apache.kafka.common.record;
-
-import java.lang.reflect.Constructor;
-import org.apache.kafka.common.KafkaException;
-import org.apache.kafka.common.utils.Utils;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.zip.GZIPInputStream;
-import java.util.zip.GZIPOutputStream;
-
-public class Compressor {
-
- static private final float COMPRESSION_RATE_DAMPING_FACTOR = 0.9f;
- static private final float COMPRESSION_RATE_ESTIMATION_FACTOR = 1.05f;
- static private final int COMPRESSION_DEFAULT_BUFFER_SIZE = 1024;
-
- private static final float[] TYPE_TO_RATE;
-
- static {
- int maxTypeId = -1;
- for (CompressionType type : CompressionType.values())
- maxTypeId = Math.max(maxTypeId, type.id);
- TYPE_TO_RATE = new float[maxTypeId + 1];
- for (CompressionType type : CompressionType.values()) {
- TYPE_TO_RATE[type.id] = type.rate;
- }
- }
-
- // dynamically load the snappy and lz4 classes to avoid runtime dependency if we are not using compression
- // caching constructors to avoid invoking of Class.forName method for each batch
- private static MemoizingConstructorSupplier snappyOutputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
- @Override
- public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
- return Class.forName("org.xerial.snappy.SnappyOutputStream")
- .getConstructor(OutputStream.class, Integer.TYPE);
- }
- });
-
- private static MemoizingConstructorSupplier lz4OutputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
- @Override
- public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
- return Class.forName("org.apache.kafka.common.record.KafkaLZ4BlockOutputStream")
- .getConstructor(OutputStream.class);
- }
- });
-
- private static MemoizingConstructorSupplier snappyInputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
- @Override
- public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
- return Class.forName("org.xerial.snappy.SnappyInputStream")
- .getConstructor(InputStream.class);
- }
- });
-
- private static MemoizingConstructorSupplier lz4InputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
- @Override
- public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
- return Class.forName("org.apache.kafka.common.record.KafkaLZ4BlockInputStream")
- .getConstructor(InputStream.class, Boolean.TYPE);
- }
- });
-
- private final CompressionType type;
- private final DataOutputStream appendStream;
- private final ByteBufferOutputStream bufferStream;
- private final int initPos;
-
- public long writtenUncompressed;
- public long numRecords;
- public float compressionRate;
- public long maxTimestamp;
-
- public Compressor(ByteBuffer buffer, CompressionType type) {
- this.type = type;
- this.initPos = buffer.position();
-
- this.numRecords = 0;
- this.writtenUncompressed = 0;
- this.compressionRate = 1;
- this.maxTimestamp = Record.NO_TIMESTAMP;
-
- if (type != CompressionType.NONE) {
- // for compressed records, leave space for the header and the shallow message metadata
- // and move the starting position to the value payload offset
- buffer.position(initPos + Records.LOG_OVERHEAD + Record.RECORD_OVERHEAD);
- }
-
- // create the stream
- bufferStream = new ByteBufferOutputStream(buffer);
- appendStream = wrapForOutput(bufferStream, type, COMPRESSION_DEFAULT_BUFFER_SIZE);
- }
-
- public ByteBuffer buffer() {
- return bufferStream.buffer();
- }
-
- public double compressionRate() {
- return compressionRate;
- }
-
- public void close() {
- try {
- appendStream.close();
- } catch (IOException e) {
- throw new KafkaException(e);
- }
-
- if (type != CompressionType.NONE) {
- ByteBuffer buffer = bufferStream.buffer();
- int pos = buffer.position();
- // write the header, for the end offset write as number of records - 1
- buffer.position(initPos);
- buffer.putLong(numRecords - 1);
- buffer.putInt(pos - initPos - Records.LOG_OVERHEAD);
- // write the shallow message (the crc and value size are not correct yet)
- Record.write(buffer, maxTimestamp, null, null, type, 0, -1);
- // compute the fill the value size
- int valueSize = pos - initPos - Records.LOG_OVERHEAD - Record.RECORD_OVERHEAD;
- buffer.putInt(initPos + Records.LOG_OVERHEAD + Record.KEY_OFFSET_V1, valueSize);
- // compute and fill the crc at the beginning of the message
- long crc = Record.computeChecksum(buffer,
- initPos + Records.LOG_OVERHEAD + Record.MAGIC_OFFSET,
- pos - initPos - Records.LOG_OVERHEAD - Record.MAGIC_OFFSET);
- Utils.writeUnsignedInt(buffer, initPos + Records.LOG_OVERHEAD + Record.CRC_OFFSET, crc);
- // reset the position
- buffer.position(pos);
-
- // update the compression ratio
- this.compressionRate = (float) buffer.position() / this.writtenUncompressed;
- TYPE_TO_RATE[type.id] = TYPE_TO_RATE[type.id] * COMPRESSION_RATE_DAMPING_FACTOR +
- compressionRate * (1 - COMPRESSION_RATE_DAMPING_FACTOR);
- }
- }
-
- // Note that for all the write operations below, IO exceptions should
- // never be thrown since the underlying ByteBufferOutputStream does not throw IOException;
- // therefore upon encountering this issue we just close the append stream.
-
- public void putLong(final long value) {
- try {
- appendStream.writeLong(value);
- } catch (IOException e) {
- throw new KafkaException("I/O exception when writing to the append stream, closing", e);
- }
- }
-
- public void putInt(final int value) {
- try {
- appendStream.writeInt(value);
- } catch (IOException e) {
- throw new KafkaException("I/O exception when writing to the append stream, closing", e);
- }
- }
-
- public void put(final ByteBuffer buffer) {
- try {
- appendStream.write(buffer.array(), buffer.arrayOffset(), buffer.limit());
- } catch (IOException e) {
- throw new KafkaException("I/O exception when writing to the append stream, closing", e);
- }
- }
-
- public void putByte(final byte value) {
- try {
- appendStream.write(value);
- } catch (IOException e) {
- throw new KafkaException("I/O exception when writing to the append stream, closing", e);
- }
- }
-
- public void put(final byte[] bytes, final int offset, final int len) {
- try {
- appendStream.write(bytes, offset, len);
- } catch (IOException e) {
- throw new KafkaException("I/O exception when writing to the append stream, closing", e);
- }
- }
-
- /**
- * @return CRC of the record
- */
- public long putRecord(long timestamp, byte[] key, byte[] value, CompressionType type,
- int valueOffset, int valueSize) {
- // put a record as un-compressed into the underlying stream
- long crc = Record.computeChecksum(timestamp, key, value, type, valueOffset, valueSize);
- byte attributes = Record.computeAttributes(type);
- putRecord(crc, attributes, timestamp, key, value, valueOffset, valueSize);
- return crc;
- }
-
- /**
- * Put a record as uncompressed into the underlying stream
- * @return CRC of the record
- */
- public long putRecord(long timestamp, byte[] key, byte[] value) {
- return putRecord(timestamp, key, value, CompressionType.NONE, 0, -1);
- }
-
- private void putRecord(final long crc, final byte attributes, final long timestamp, final byte[] key, final byte[] value, final int valueOffset, final int valueSize) {
- maxTimestamp = Math.max(maxTimestamp, timestamp);
- Record.write(this, crc, attributes, timestamp, key, value, valueOffset, valueSize);
- }
-
- public void recordWritten(int size) {
- numRecords += 1;
- writtenUncompressed += size;
- }
-
- public long numRecordsWritten() {
- return numRecords;
- }
-
- public long estimatedBytesWritten() {
- if (type == CompressionType.NONE) {
- return bufferStream.buffer().position();
- } else {
- // estimate the written bytes to the underlying byte buffer based on uncompressed written bytes
- return (long) (writtenUncompressed * TYPE_TO_RATE[type.id] * COMPRESSION_RATE_ESTIMATION_FACTOR);
- }
- }
-
- // the following two functions also need to be public since they are used in MemoryRecords.iteration
-
- public static DataOutputStream wrapForOutput(ByteBufferOutputStream buffer, CompressionType type, int bufferSize) {
- try {
- switch (type) {
- case NONE:
- return new DataOutputStream(buffer);
- case GZIP:
- return new DataOutputStream(new GZIPOutputStream(buffer, bufferSize));
- case SNAPPY:
- try {
- OutputStream stream = (OutputStream) snappyOutputStreamSupplier.get().newInstance(buffer, bufferSize);
- return new DataOutputStream(stream);
- } catch (Exception e) {
- throw new KafkaException(e);
- }
- case LZ4:
- try {
- OutputStream stream = (OutputStream) lz4OutputStreamSupplier.get().newInstance(buffer);
- return new DataOutputStream(stream);
- } catch (Exception e) {
- throw new KafkaException(e);
- }
- default:
- throw new IllegalArgumentException("Unknown compression type: " + type);
- }
- } catch (IOException e) {
- throw new KafkaException(e);
- }
- }
-
- public static DataInputStream wrapForInput(ByteBufferInputStream buffer, CompressionType type, byte messageVersion) {
- try {
- switch (type) {
- case NONE:
- return new DataInputStream(buffer);
- case GZIP:
- return new DataInputStream(new GZIPInputStream(buffer));
- case SNAPPY:
- try {
- InputStream stream = (InputStream) snappyInputStreamSupplier.get().newInstance(buffer);
- return new DataInputStream(stream);
- } catch (Exception e) {
- throw new KafkaException(e);
- }
- case LZ4:
- try {
- InputStream stream = (InputStream) lz4InputStreamSupplier.get().newInstance(buffer,
- messageVersion == Record.MAGIC_VALUE_V0);
- return new DataInputStream(stream);
- } catch (Exception e) {
- throw new KafkaException(e);
- }
- default:
- throw new IllegalArgumentException("Unknown compression type: " + type);
- }
- } catch (IOException e) {
- throw new KafkaException(e);
- }
- }
-
- private interface ConstructorSupplier {
- Constructor get() throws ClassNotFoundException, NoSuchMethodException;
- }
-
- // this code is based on Guava's @see{com.google.common.base.Suppliers.MemoizingSupplier}
- private static class MemoizingConstructorSupplier {
- final ConstructorSupplier delegate;
- transient volatile boolean initialized;
- transient Constructor value;
-
- public MemoizingConstructorSupplier(ConstructorSupplier delegate) {
- this.delegate = delegate;
- }
-
- public Constructor get() throws NoSuchMethodException, ClassNotFoundException {
- if (!initialized) {
- synchronized (this) {
- if (!initialized) {
- Constructor constructor = delegate.get();
- value = constructor;
- initialized = true;
- return constructor;
- }
- }
- }
- return value;
- }
- }
-}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/FileLogInputStream.java b/clients/src/main/java/org/apache/kafka/common/record/FileLogInputStream.java
new file mode 100644
index 0000000000000..ae393b03c0d62
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/record/FileLogInputStream.java
@@ -0,0 +1,166 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.apache.kafka.common.KafkaException;
+import org.apache.kafka.common.errors.CorruptRecordException;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+
+/**
+ * A log input stream which is backed by a {@link FileChannel}.
+ */
+public class FileLogInputStream implements LogInputStream {
+ private int position;
+ private final int end;
+ private final FileChannel channel;
+ private final int maxRecordSize;
+ private final ByteBuffer logHeaderBuffer = ByteBuffer.allocate(Records.LOG_OVERHEAD);
+
+ /**
+ * Create a new log input stream over the FileChannel
+ * @param channel Underlying FileChannel
+ * @param maxRecordSize Maximum size of records
+ * @param start Position in the file channel to start from
+ * @param end Position in the file channel not to read past
+ */
+ public FileLogInputStream(FileChannel channel,
+ int maxRecordSize,
+ int start,
+ int end) {
+ this.channel = channel;
+ this.maxRecordSize = maxRecordSize;
+ this.position = start;
+ this.end = end;
+ }
+
+ @Override
+ public FileChannelLogEntry nextEntry() throws IOException {
+ if (position + Records.LOG_OVERHEAD >= end)
+ return null;
+
+ logHeaderBuffer.rewind();
+ channel.read(logHeaderBuffer, position);
+ if (logHeaderBuffer.hasRemaining())
+ return null;
+
+ logHeaderBuffer.rewind();
+ long offset = logHeaderBuffer.getLong();
+ int size = logHeaderBuffer.getInt();
+
+ if (size < Record.RECORD_OVERHEAD_V0)
+ throw new CorruptRecordException(String.format("Record size is smaller than minimum record overhead (%d).", Record.RECORD_OVERHEAD_V0));
+
+ if (size > maxRecordSize)
+ throw new CorruptRecordException(String.format("Record size exceeds the largest allowable message size (%d).", maxRecordSize));
+
+ if (position + Records.LOG_OVERHEAD + size > end)
+ return null;
+
+ FileChannelLogEntry logEntry = new FileChannelLogEntry(offset, channel, position, size);
+ position += logEntry.sizeInBytes();
+ return logEntry;
+ }
+
+ /**
+ * Log entry backed by an underlying FileChannel. This allows iteration over the shallow log
+ * entries without needing to read the record data into memory until it is needed. The downside
+ * is that entries will generally no longer be readable when the underlying channel is closed.
+ */
+ public static class FileChannelLogEntry extends LogEntry {
+ private final long offset;
+ private final FileChannel channel;
+ private final int position;
+ private final int recordSize;
+ private Record record = null;
+
+ private FileChannelLogEntry(long offset,
+ FileChannel channel,
+ int position,
+ int recordSize) {
+ this.offset = offset;
+ this.channel = channel;
+ this.position = position;
+ this.recordSize = recordSize;
+ }
+
+ @Override
+ public long offset() {
+ return offset;
+ }
+
+ public int position() {
+ return position;
+ }
+
+ @Override
+ public byte magic() {
+ if (record != null)
+ return record.magic();
+
+ try {
+ byte[] magic = new byte[1];
+ ByteBuffer buf = ByteBuffer.wrap(magic);
+ channel.read(buf, position + Records.LOG_OVERHEAD + Record.MAGIC_OFFSET);
+ if (buf.hasRemaining())
+ throw new KafkaException("Failed to read magic byte from FileChannel " + channel);
+ return magic[0];
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+ }
+
+ /**
+ * Force load the record and its data (key and value) into memory.
+ * @return The resulting record
+ * @throws IOException for any IO errors reading from the underlying file
+ */
+ private Record loadRecord() throws IOException {
+ if (record != null)
+ return record;
+
+ ByteBuffer recordBuffer = ByteBuffer.allocate(recordSize);
+ channel.read(recordBuffer, position + Records.LOG_OVERHEAD);
+ if (recordBuffer.hasRemaining())
+ throw new IOException("Failed to read full record from channel " + channel);
+
+ recordBuffer.rewind();
+ record = new Record(recordBuffer);
+ return record;
+ }
+
+ @Override
+ public Record record() {
+ if (record != null)
+ return record;
+
+ try {
+ return loadRecord();
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+ }
+
+ @Override
+ public int sizeInBytes() {
+ return Records.LOG_OVERHEAD + recordSize;
+ }
+
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/FileRecords.java b/clients/src/main/java/org/apache/kafka/common/record/FileRecords.java
index bdae08d87480f..faf61e9898674 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/FileRecords.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/FileRecords.java
@@ -18,22 +18,31 @@
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.network.TransportLayer;
+import org.apache.kafka.common.record.FileLogInputStream.FileChannelLogEntry;
+import org.apache.kafka.common.utils.Utils;
+import java.io.Closeable;
import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.GatheringByteChannel;
+import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicInteger;
/**
- * File-backed record set.
+ * A {@link Records} implementation backed by a file. An optional start and end position can be applied to this
+ * instance to enable slicing a range of the log records.
*/
-public class FileRecords implements Records {
- private final File file;
+public class FileRecords extends AbstractRecords implements Closeable {
+ private final boolean isSlice;
private final FileChannel channel;
- private final long start;
- private final long end;
- private final long size;
+ private final int start;
+ private final int end;
+ private volatile File file;
+ private final AtomicInteger size;
public FileRecords(File file,
FileChannel channel,
@@ -44,83 +53,435 @@ public FileRecords(File file,
this.channel = channel;
this.start = start;
this.end = end;
+ this.isSlice = isSlice;
+ this.size = new AtomicInteger();
- if (isSlice)
- this.size = end - start;
- else
- this.size = Math.min(channel.size(), end) - start;
+ // set the initial size of the buffer
+ resize();
+ }
+
+ public void resize() throws IOException {
+ if (isSlice) {
+ size.set(end - start);
+ } else {
+ int limit = Math.min((int) channel.size(), end);
+ size.set(limit - start);
+
+ // if this is not a slice, update the file pointer to the end of the file
+ // set the file position to the last byte in the file
+ channel.position(limit);
+ }
}
@Override
public int sizeInBytes() {
- return (int) size;
+ return size.get();
+ }
+
+ /**
+ * Get the underlying file.
+ * @return The file
+ */
+ public File file() {
+ return file;
+ }
+
+ /**
+ * Get the underlying file channel.
+ * @return The file channel
+ */
+ public FileChannel channel() {
+ return channel;
+ }
+
+ /**
+ * Read log entries into a given buffer.
+ * @param buffer The buffer to write the entries to
+ * @param position Position in the buffer to read from
+ * @return The same buffer
+ * @throws IOException
+ */
+ public ByteBuffer readInto(ByteBuffer buffer, int position) throws IOException {
+ channel.read(buffer, position + this.start);
+ buffer.flip();
+ return buffer;
+ }
+
+ /**
+ * Return a slice of records from this instance, which is a view into this set starting from the given position
+ * and with the given size limit.
+ *
+ * If the size is beyond the end of the file, the end will be based on the size of the file at the time of the read.
+ *
+ * If this message set is already sliced, the position will be taken relative to that slicing.
+ *
+ * @param position The start position to begin the read from
+ * @param size The number of bytes after the start position to include
+ * @return A sliced wrapper on this message set limited based on the given position and size
+ */
+ public FileRecords read(int position, int size) throws IOException {
+ if (position < 0)
+ throw new IllegalArgumentException("Invalid position: " + position);
+ if (size < 0)
+ throw new IllegalArgumentException("Invalid size: " + size);
+
+ final int end;
+ if (this.start + position + size < 0)
+ end = sizeInBytes();
+ else
+ end = Math.min(this.start + position + size, sizeInBytes());
+ return new FileRecords(file, channel, this.start + position, end, true);
+ }
+
+ /**
+ * Append log entries to the buffer
+ * @param records The records to append
+ * @return the number of bytes written to the underlying file
+ */
+ public int append(MemoryRecords records) throws IOException {
+ int written = records.writeFullyTo(channel);
+ size.getAndAdd(written);
+ return written;
+ }
+
+ /**
+ * Commit all written data to the physical disk
+ */
+ public void flush() throws IOException {
+ channel.force(true);
+ }
+
+ /**
+ * Close this record set
+ */
+ public void close() throws IOException {
+ flush();
+ trim();
+ channel.close();
+ }
+
+ /**
+ * Delete this message set from the filesystem
+ * @return True iff this message set was deleted.
+ */
+ public boolean delete() {
+ Utils.closeQuietly(channel, "FileChannel");
+ return file.delete();
+ }
+
+ /**
+ * Trim file when close or roll to next file
+ */
+ public void trim() throws IOException {
+ truncateTo(sizeInBytes());
+ }
+
+ /**
+ * Update the file reference (to be used with caution since this does not reopen the file channel)
+ * @param file The new file to use
+ */
+ public void setFile(File file) {
+ this.file = file;
+ }
+
+ /**
+ * Rename the file that backs this message set
+ * @throws IOException if rename fails.
+ */
+ public void renameTo(File f) throws IOException {
+ try {
+ Utils.atomicMoveWithFallback(file.toPath(), f.toPath());
+ } finally {
+ this.file = f;
+ }
+ }
+
+ /**
+ * Truncate this file message set to the given size in bytes. Note that this API does no checking that the
+ * given size falls on a valid message boundary.
+ * In some versions of the JDK truncating to the same size as the file message set will cause an
+ * update of the files mtime, so truncate is only performed if the targetSize is smaller than the
+ * size of the underlying FileChannel.
+ * It is expected that no other threads will do writes to the log when this function is called.
+ * @param targetSize The size to truncate to. Must be between 0 and sizeInBytes.
+ * @return The number of bytes truncated off
+ */
+ public int truncateTo(int targetSize) throws IOException {
+ int originalSize = sizeInBytes();
+ if (targetSize > originalSize || targetSize < 0)
+ throw new KafkaException("Attempt to truncate log segment to " + targetSize + " bytes failed, " +
+ " size of this log segment is " + originalSize + " bytes.");
+ if (targetSize < (int) channel.size()) {
+ channel.truncate(targetSize);
+ channel.position(targetSize);
+ size.set(targetSize);
+ }
+ return originalSize - targetSize;
}
@Override
public long writeTo(GatheringByteChannel destChannel, long offset, int length) throws IOException {
long newSize = Math.min(channel.size(), end) - start;
- if (newSize < size)
+ if (newSize < size.get())
throw new KafkaException(String.format("Size of FileRecords %s has been truncated during write: old size %d, new size %d", file.getAbsolutePath(), size, newSize));
- if (offset > size)
- throw new KafkaException(String.format("The requested offset %d is out of range. The size of this FileRecords is %d.", offset, size));
-
long position = start + offset;
- long count = Math.min(length, this.size - offset);
+ long count = Math.min(length, size.get());
+ final long bytesTransferred;
if (destChannel instanceof TransportLayer) {
TransportLayer tl = (TransportLayer) destChannel;
- return tl.transferFrom(this.channel, position, count);
+ bytesTransferred = tl.transferFrom(channel, position, count);
} else {
- return this.channel.transferTo(position, count, destChannel);
+ bytesTransferred = channel.transferTo(position, count, destChannel);
+ }
+ return bytesTransferred;
+ }
+
+ /**
+ * Search forward for the file position of the last offset that is greater than or equal to the target offset
+ * and return its physical position and the size of the message (including log overhead) at the returned offset. If
+ * no such offsets are found, return null.
+ *
+ * @param targetOffset The offset to search for.
+ * @param startingPosition The starting position in the file to begin searching from.
+ */
+ public LogEntryPosition searchForOffsetWithSize(long targetOffset, int startingPosition) {
+ Iterator iterator = shallowIteratorFrom(Integer.MAX_VALUE, startingPosition);
+ while (iterator.hasNext()) {
+ FileChannelLogEntry entry = iterator.next();
+ long offset = entry.offset();
+ if (offset >= targetOffset)
+ return new LogEntryPosition(offset, entry.position(), entry.sizeInBytes());
+ }
+ return null;
+ }
+
+ /**
+ * Search forward for the message whose timestamp is greater than or equals to the target timestamp.
+ *
+ * @param targetTimestamp The timestamp to search for.
+ * @param startingPosition The starting position to search.
+ * @return The timestamp and offset of the message found. None, if no message is found.
+ */
+ public TimestampAndOffset searchForTimestamp(long targetTimestamp, int startingPosition) {
+ Iterator shallowIterator = shallowIteratorFrom(startingPosition);
+ while (shallowIterator.hasNext()) {
+ LogEntry shallowEntry = shallowIterator.next();
+ Record shallowRecord = shallowEntry.record();
+ if (shallowRecord.timestamp() >= targetTimestamp) {
+ // We found a message
+ for (LogEntry deepLogEntry : shallowEntry) {
+ long timestamp = deepLogEntry.record().timestamp();
+ if (timestamp >= targetTimestamp)
+ return new TimestampAndOffset(timestamp, deepLogEntry.offset());
+ }
+ throw new IllegalStateException(String.format("The message set (max timestamp = %s, max offset = %s" +
+ " should contain target timestamp %s, but does not.", shallowRecord.timestamp(),
+ shallowEntry.offset(), targetTimestamp));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the largest timestamp of the messages after a given position in this file message set.
+ * @param startingPosition The starting position.
+ * @return The largest timestamp of the messages after the given position.
+ */
+ public TimestampAndOffset largestTimestampAfter(int startingPosition) {
+ long maxTimestamp = Record.NO_TIMESTAMP;
+ long offsetOfMaxTimestamp = -1L;
+
+ Iterator shallowIterator = shallowIteratorFrom(startingPosition);
+ while (shallowIterator.hasNext()) {
+ LogEntry shallowEntry = shallowIterator.next();
+ long timestamp = shallowEntry.record().timestamp();
+ if (timestamp > maxTimestamp) {
+ maxTimestamp = timestamp;
+ offsetOfMaxTimestamp = shallowEntry.offset();
+ }
}
+ return new TimestampAndOffset(maxTimestamp, offsetOfMaxTimestamp);
+ }
+
+ /**
+ * Get an iterator over the shallow entries in the file. Note that the entries are
+ * backed by the open file channel. When the channel is closed (i.e. when this instance
+ * is closed), the entries will generally no longer be readable.
+ * @return An iterator over the shallow entries
+ */
+ @Override
+ public Iterator shallowIterator() {
+ return shallowIteratorFrom(start);
+ }
+
+ /**
+ * Get an iterator over the shallow entries, enforcing a maximum record size
+ * @param maxRecordSize The maximum allowable size of individual records (including compressed record sets)
+ * @return An iterator over the shallow entries
+ */
+ public Iterator shallowIterator(int maxRecordSize) {
+ return shallowIteratorFrom(maxRecordSize, start);
+ }
+
+ private Iterator shallowIteratorFrom(int start) {
+ return shallowIteratorFrom(Integer.MAX_VALUE, start);
+ }
+
+ private Iterator shallowIteratorFrom(int maxRecordSize, int start) {
+ final int end;
+ if (isSlice)
+ end = this.end;
+ else
+ end = this.sizeInBytes();
+ FileLogInputStream inputStream = new FileLogInputStream(channel, maxRecordSize, start, end);
+ return RecordsIterator.shallowIterator(inputStream);
}
@Override
- public RecordsIterator iterator() {
- return new RecordsIterator(new FileLogInputStream(channel, start, end), false);
+ public Iterator deepIterator() {
+ final int end;
+ if (isSlice)
+ end = this.end;
+ else
+ end = this.sizeInBytes();
+ FileLogInputStream inputStream = new FileLogInputStream(channel, Integer.MAX_VALUE, start, end);
+ return new RecordsIterator(inputStream, false, false, Integer.MAX_VALUE);
+ }
+
+ public static FileRecords open(File file,
+ boolean mutable,
+ boolean fileAlreadyExists,
+ int initFileSize,
+ boolean preallocate) throws IOException {
+ FileChannel channel = openChannel(file, mutable, fileAlreadyExists, initFileSize, preallocate);
+ int end = (!fileAlreadyExists && preallocate) ? 0 : Integer.MAX_VALUE;
+ return new FileRecords(file, channel, 0, end, false);
+ }
+
+ public static FileRecords open(File file,
+ boolean fileAlreadyExists,
+ int initFileSize,
+ boolean preallocate) throws IOException {
+ return open(file, true, fileAlreadyExists, initFileSize, preallocate);
+ }
+
+ public static FileRecords open(File file, boolean mutable) throws IOException {
+ return open(file, mutable, false, 0, false);
+ }
+
+ public static FileRecords open(File file) throws IOException {
+ return open(file, true);
+ }
+
+ /**
+ * Open a channel for the given file
+ * For windows NTFS and some old LINUX file system, set preallocate to true and initFileSize
+ * with one value (for example 512 * 1025 *1024 ) can improve the kafka produce performance.
+ * @param file File path
+ * @param mutable mutable
+ * @param fileAlreadyExists File already exists or not
+ * @param initFileSize The size used for pre allocate file, for example 512 * 1025 *1024
+ * @param preallocate Pre allocate file or not, gotten from configuration.
+ */
+ private static FileChannel openChannel(File file,
+ boolean mutable,
+ boolean fileAlreadyExists,
+ int initFileSize,
+ boolean preallocate) throws IOException {
+ if (mutable) {
+ if (fileAlreadyExists) {
+ return new RandomAccessFile(file, "rw").getChannel();
+ } else {
+ if (preallocate) {
+ RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
+ randomAccessFile.setLength(initFileSize);
+ return randomAccessFile.getChannel();
+ } else {
+ return new RandomAccessFile(file, "rw").getChannel();
+ }
+ }
+ } else {
+ return new FileInputStream(file).getChannel();
+ }
}
- private static class FileLogInputStream implements LogInputStream {
- private long position;
- protected final long end;
- protected final FileChannel channel;
- private final ByteBuffer logHeaderBuffer = ByteBuffer.allocate(Records.LOG_OVERHEAD);
+ public static class LogEntryPosition {
+ public final long offset;
+ public final int position;
+ public final int size;
- public FileLogInputStream(FileChannel channel, long start, long end) {
- this.channel = channel;
- this.position = start;
- this.end = end;
+ public LogEntryPosition(long offset, int position, int size) {
+ this.offset = offset;
+ this.position = position;
+ this.size = size;
}
@Override
- public LogEntry nextEntry() throws IOException {
- if (position + Records.LOG_OVERHEAD >= end)
- return null;
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
- logHeaderBuffer.rewind();
- channel.read(logHeaderBuffer, position);
- if (logHeaderBuffer.hasRemaining())
- return null;
+ LogEntryPosition that = (LogEntryPosition) o;
- logHeaderBuffer.rewind();
- long offset = logHeaderBuffer.getLong();
- int size = logHeaderBuffer.getInt();
- if (size < 0)
- throw new IllegalStateException("Record with size " + size);
+ if (offset != that.offset) return false;
+ if (position != that.position) return false;
+ return size == that.size;
- if (position + Records.LOG_OVERHEAD + size > end)
- return null;
+ }
- ByteBuffer recordBuffer = ByteBuffer.allocate(size);
- channel.read(recordBuffer, position + Records.LOG_OVERHEAD);
- if (recordBuffer.hasRemaining())
- return null;
- recordBuffer.rewind();
+ @Override
+ public int hashCode() {
+ int result = (int) (offset ^ (offset >>> 32));
+ result = 31 * result + position;
+ result = 31 * result + size;
+ return result;
+ }
- Record record = new Record(recordBuffer);
- LogEntry logEntry = new LogEntry(offset, record);
- position += logEntry.size();
- return logEntry;
+ @Override
+ public String toString() {
+ return "LogEntryPosition(" +
+ "offset=" + offset +
+ ", position=" + position +
+ ", size=" + size +
+ ')';
}
}
+
+ public static class TimestampAndOffset {
+ public final long timestamp;
+ public final long offset;
+
+ public TimestampAndOffset(long timestamp, long offset) {
+ this.timestamp = timestamp;
+ this.offset = offset;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TimestampAndOffset that = (TimestampAndOffset) o;
+
+ if (timestamp != that.timestamp) return false;
+ return offset == that.offset;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) (timestamp ^ (timestamp >>> 32));
+ result = 31 * result + (int) (offset ^ (offset >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "TimestampAndOffset(" +
+ "timestamp=" + timestamp +
+ ", offset=" + offset +
+ ')';
+ }
+ }
+
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/InvalidRecordException.java b/clients/src/main/java/org/apache/kafka/common/record/InvalidRecordException.java
index a1009ca2e3839..ee6071314ff60 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/InvalidRecordException.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/InvalidRecordException.java
@@ -16,9 +16,9 @@
*/
package org.apache.kafka.common.record;
-import org.apache.kafka.common.KafkaException;
+import org.apache.kafka.common.errors.CorruptRecordException;
-public class InvalidRecordException extends KafkaException {
+public class InvalidRecordException extends CorruptRecordException {
private static final long serialVersionUID = 1;
diff --git a/clients/src/main/java/org/apache/kafka/common/record/LogEntry.java b/clients/src/main/java/org/apache/kafka/common/record/LogEntry.java
index 2e54b560ed98b..d2db3569f0ef3 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/LogEntry.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/LogEntry.java
@@ -16,33 +16,156 @@
*/
package org.apache.kafka.common.record;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.Iterator;
+
+import static org.apache.kafka.common.record.Records.LOG_OVERHEAD;
+
/**
* An offset and record pair
*/
-public final class LogEntry {
+public abstract class LogEntry implements Iterable {
- private final long offset;
- private final Record record;
+ /**
+ * Get the offset of this entry. Note that if this entry contains a compressed
+ * message set, then this offset will be the last offset of the nested entries
+ * @return the last offset contained in this entry
+ */
+ public abstract long offset();
- public LogEntry(long offset, Record record) {
- this.offset = offset;
- this.record = record;
+ /**
+ * Get the shallow record for this log entry.
+ * @return the shallow record
+ */
+ public abstract Record record();
+
+ /**
+ * Get the first offset of the records contained in this entry. Note that this
+ * generally requires deep iteration, which requires decompression, so this should
+ * be used with caution.
+ * @return The first offset contained in this entry
+ */
+ public long firstOffset() {
+ return iterator().next().offset();
}
- public long offset() {
- return this.offset;
+ /**
+ * Get the offset following this entry (i.e. the last offset contained in this entry plus one).
+ * @return the next consecutive offset following this entry
+ */
+ public long nextOffset() {
+ return offset() + 1;
}
- public Record record() {
- return this.record;
+ /**
+ * Get the message format version of this entry (i.e its magic value).
+ * @return the magic byte
+ */
+ public byte magic() {
+ return record().magic();
}
@Override
public String toString() {
- return "LogEntry(" + offset + ", " + record + ")";
+ return "LogEntry(" + offset() + ", " + record() + ")";
+ }
+
+ /**
+ * Get the size in bytes of this entry, including the size of the record and the log overhead.
+ * @return The size in bytes of this entry
+ */
+ public int sizeInBytes() {
+ return record().sizeInBytes() + LOG_OVERHEAD;
+ }
+
+ /**
+ * Check whether this entry contains a compressed message set.
+ * @return true if so, false otherwise
+ */
+ public boolean isCompressed() {
+ return record().compressionType() != CompressionType.NONE;
+ }
+
+ /**
+ * Write this entry into a buffer.
+ * @param buffer The buffer to write the entry to
+ */
+ public void writeTo(ByteBuffer buffer) {
+ writeHeader(buffer, offset(), record().sizeInBytes());
+ buffer.put(record().buffer().duplicate());
+ }
+
+ /**
+ * Get an iterator for the nested entries contained within this log entry. Note that
+ * if the entry is not compressed, then this method will return an iterator over the
+ * shallow entry only (i.e. this object).
+ * @return An iterator over the entries contained within this log entry
+ */
+ @Override
+ public Iterator iterator() {
+ if (isCompressed())
+ return new RecordsIterator.DeepRecordsIterator(this, false, Integer.MAX_VALUE);
+ return Collections.singletonList(this).iterator();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || !(o instanceof LogEntry)) return false;
+
+ LogEntry that = (LogEntry) o;
+
+ if (offset() != that.offset()) return false;
+ Record thisRecord = record();
+ Record thatRecord = that.record();
+ return thisRecord != null ? thisRecord.equals(thatRecord) : thatRecord == null;
+ }
+
+ @Override
+ public int hashCode() {
+ long offset = offset();
+ Record record = record();
+ int result = (int) (offset ^ (offset >>> 32));
+ result = 31 * result + (record != null ? record.hashCode() : 0);
+ return result;
+ }
+
+ public static void writeHeader(ByteBuffer buffer, long offset, int size) {
+ buffer.putLong(offset);
+ buffer.putInt(size);
+ }
+
+ public static void writeHeader(DataOutputStream out, long offset, int size) throws IOException {
+ out.writeLong(offset);
+ out.writeInt(size);
}
-
- public int size() {
- return record.size() + Records.LOG_OVERHEAD;
+
+ private static class SimpleLogEntry extends LogEntry {
+ private final long offset;
+ private final Record record;
+
+ public SimpleLogEntry(long offset, Record record) {
+ this.offset = offset;
+ this.record = record;
+ }
+
+ @Override
+ public long offset() {
+ return offset;
+ }
+
+ @Override
+ public Record record() {
+ return record;
+ }
+
+ }
+
+ public static LogEntry create(long offset, Record record) {
+ return new SimpleLogEntry(offset, record);
}
+
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/LogInputStream.java b/clients/src/main/java/org/apache/kafka/common/record/LogInputStream.java
index 4a4d569ba5558..a9af651d4d079 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/LogInputStream.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/LogInputStream.java
@@ -20,10 +20,14 @@
/**
* An abstraction between an underlying input stream and record iterators, a LogInputStream
- * returns only the shallow log entries, depending on {@link org.apache.kafka.common.record.RecordsIterator.DeepRecordsIterator}
- * for the deep iteration.
+ * returns only the shallow log entries, depending on {@link RecordsIterator.DeepRecordsIterator}
+ * for the deep iteration. The generic typing allows for implementations which present only
+ * a view of the log entries, which enables more efficient iteration when the record data is
+ * not actually needed. See for example {@link org.apache.kafka.common.record.FileLogInputStream.FileChannelLogEntry}
+ * in which the record is not brought into memory until needed.
+ * @param Type parameter of the log entry
*/
-interface LogInputStream {
+interface LogInputStream {
/**
* Get the next log entry from the underlying input stream.
@@ -31,5 +35,5 @@ interface LogInputStream {
* @return The next log entry or null if there is none
* @throws IOException for any IO errors
*/
- LogEntry nextEntry() throws IOException;
+ T nextEntry() throws IOException;
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/MemoryRecords.java b/clients/src/main/java/org/apache/kafka/common/record/MemoryRecords.java
index 65ccf9888a741..b945062da59b8 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/MemoryRecords.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/MemoryRecords.java
@@ -12,197 +12,185 @@
*/
package org.apache.kafka.common.record;
-import java.io.DataInputStream;
+import org.apache.kafka.common.record.ByteBufferLogInputStream.ByteBufferLogEntry;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.GatheringByteChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
+import java.util.List;
/**
- * A {@link Records} implementation backed by a ByteBuffer.
+ * A {@link Records} implementation backed by a ByteBuffer. This is used only for reading or
+ * modifying in-place an existing buffer of log entries. To create a new buffer see {@link MemoryRecordsBuilder},
+ * or one of the {@link #builder(ByteBuffer, byte, CompressionType, TimestampType) builder} variants.
*/
-public class MemoryRecords implements Records {
+public class MemoryRecords extends AbstractRecords {
public final static MemoryRecords EMPTY = MemoryRecords.readableRecords(ByteBuffer.allocate(0));
- private final static int WRITE_LIMIT_FOR_READABLE_ONLY = -1;
-
- // the compressor used for appends-only
- private final Compressor compressor;
-
- // the write limit for writable buffer, which may be smaller than the buffer capacity
- private final int writeLimit;
-
- // the capacity of the initial buffer, which is only used for de-allocation of writable records
- private final int initialCapacity;
-
// the underlying buffer used for read; while the records are still writable it is null
private ByteBuffer buffer;
-
- // indicate if the memory records is writable or not (i.e. used for appends or read-only)
- private boolean writable;
+ private int validBytes = -1;
// Construct a writable memory records
- private MemoryRecords(ByteBuffer buffer, CompressionType type, boolean writable, int writeLimit) {
- this.writable = writable;
- this.writeLimit = writeLimit;
- this.initialCapacity = buffer.capacity();
- if (this.writable) {
- this.buffer = null;
- this.compressor = new Compressor(buffer, type);
- } else {
- this.buffer = buffer;
- this.compressor = null;
- }
- }
-
- public static MemoryRecords emptyRecords(ByteBuffer buffer, CompressionType type, int writeLimit) {
- return new MemoryRecords(buffer, type, true, writeLimit);
- }
-
- public static MemoryRecords emptyRecords(ByteBuffer buffer, CompressionType type) {
- // use the buffer capacity as the default write limit
- return emptyRecords(buffer, type, buffer.capacity());
+ private MemoryRecords(ByteBuffer buffer) {
+ this.buffer = buffer;
}
- public static MemoryRecords readableRecords(ByteBuffer buffer) {
- return new MemoryRecords(buffer, CompressionType.NONE, false, WRITE_LIMIT_FOR_READABLE_ONLY);
+ @Override
+ public int sizeInBytes() {
+ return buffer.limit();
}
- /**
- * Append the given record and offset to the buffer
- */
- public void append(long offset, Record record) {
- if (!writable)
- throw new IllegalStateException("Memory records is not writable");
-
- int size = record.size();
- compressor.putLong(offset);
- compressor.putInt(size);
- compressor.put(record.buffer());
- compressor.recordWritten(size + Records.LOG_OVERHEAD);
- record.buffer().rewind();
+ @Override
+ public long writeTo(GatheringByteChannel channel, long position, int length) throws IOException {
+ ByteBuffer dup = buffer.duplicate();
+ int pos = (int) position;
+ dup.position(pos);
+ dup.limit(pos + length);
+ return channel.write(dup);
}
/**
- * Append a new record and offset to the buffer
- * @return crc of the record
+ * Write all records to the given channel (including partial records).
+ * @param channel The channel to write to
+ * @return The number of bytes written
+ * @throws IOException For any IO errors writing to the channel
*/
- public long append(long offset, long timestamp, byte[] key, byte[] value) {
- if (!writable)
- throw new IllegalStateException("Memory records is not writable");
-
- int size = Record.recordSize(key, value);
- compressor.putLong(offset);
- compressor.putInt(size);
- long crc = compressor.putRecord(timestamp, key, value);
- compressor.recordWritten(size + Records.LOG_OVERHEAD);
- return crc;
+ public int writeFullyTo(GatheringByteChannel channel) throws IOException {
+ buffer.mark();
+ int written = 0;
+ while (written < sizeInBytes())
+ written += channel.write(buffer);
+ buffer.reset();
+ return written;
}
/**
- * Check if we have room for a new record containing the given key/value pair
- *
- * Note that the return value is based on the estimate of the bytes written to the compressor, which may not be
- * accurate if compression is really used. When this happens, the following append may cause dynamic buffer
- * re-allocation in the underlying byte buffer stream.
- *
- * There is an exceptional case when appending a single message whose size is larger than the batch size, the
- * capacity will be the message size which is larger than the write limit, i.e. the batch size. In this case
- * the checking should be based on the capacity of the initialized buffer rather than the write limit in order
- * to accept this single record.
+ * The total number of bytes in this message set not including any partial, trailing messages. This
+ * may be smaller than what is returned by {@link #sizeInBytes()}.
+ * @return The number of valid bytes
*/
- public boolean hasRoomFor(byte[] key, byte[] value) {
- if (!this.writable)
- return false;
+ public int validBytes() {
+ if (validBytes >= 0)
+ return validBytes;
- return this.compressor.numRecordsWritten() == 0 ?
- this.initialCapacity >= Records.LOG_OVERHEAD + Record.recordSize(key, value) :
- this.writeLimit >= this.compressor.estimatedBytesWritten() + Records.LOG_OVERHEAD + Record.recordSize(key, value);
- }
+ int bytes = 0;
+ Iterator iterator = shallowIterator();
+ while (iterator.hasNext())
+ bytes += iterator.next().sizeInBytes();
- public boolean isFull() {
- return !this.writable || this.writeLimit <= this.compressor.estimatedBytesWritten();
+ this.validBytes = bytes;
+ return bytes;
}
/**
- * Close this batch for no more appends
+ * Filter the records into the provided ByteBuffer.
+ * @param filter The filter function
+ * @param buffer The byte buffer to write the filtered records to
+ * @return A FilterResult with a summary of the output (for metrics)
*/
- public void close() {
- if (writable) {
- // close the compressor to fill-in wrapper message metadata if necessary
- compressor.close();
-
- // flip the underlying buffer to be ready for reads
- buffer = compressor.buffer();
- buffer.flip();
-
- // reset the writable flag
- writable = false;
+ public FilterResult filterTo(LogEntryFilter filter, ByteBuffer buffer) {
+ long maxTimestamp = Record.NO_TIMESTAMP;
+ long shallowOffsetOfMaxTimestamp = -1L;
+ int messagesRead = 0;
+ int bytesRead = 0;
+ int messagesRetained = 0;
+ int bytesRetained = 0;
+
+ Iterator shallowIterator = shallowIterator();
+ while (shallowIterator.hasNext()) {
+ ByteBufferLogEntry shallowEntry = shallowIterator.next();
+ bytesRead += shallowEntry.sizeInBytes();
+
+ // We use the absolute offset to decide whether to retain the message or not (this is handled by the
+ // deep iterator). Because of KAFKA-4298, we have to allow for the possibility that a previous version
+ // corrupted the log by writing a compressed message set with a wrapper magic value not matching the magic
+ // of the inner messages. This will be fixed as we recopy the messages to the destination buffer.
+
+ Record shallowRecord = shallowEntry.record();
+ byte shallowMagic = shallowRecord.magic();
+ boolean writeOriginalEntry = true;
+ List retainedEntries = new ArrayList<>();
+
+ for (LogEntry deepEntry : shallowEntry) {
+ Record deepRecord = deepEntry.record();
+ messagesRead += 1;
+
+ if (filter.shouldRetain(deepEntry)) {
+ // Check for log corruption due to KAFKA-4298. If we find it, make sure that we overwrite
+ // the corrupted entry with correct data.
+ if (shallowMagic != deepRecord.magic())
+ writeOriginalEntry = false;
+
+ retainedEntries.add(deepEntry);
+ } else {
+ writeOriginalEntry = false;
+ }
+ }
+
+ if (writeOriginalEntry) {
+ // There are no messages compacted out and no message format conversion, write the original message set back
+ shallowEntry.writeTo(buffer);
+ messagesRetained += retainedEntries.size();
+ bytesRetained += shallowEntry.sizeInBytes();
+
+ if (shallowRecord.timestamp() > maxTimestamp) {
+ maxTimestamp = shallowRecord.timestamp();
+ shallowOffsetOfMaxTimestamp = shallowEntry.offset();
+ }
+ } else if (!retainedEntries.isEmpty()) {
+ ByteBuffer slice = buffer.slice();
+ MemoryRecordsBuilder builder = builderWithEntries(slice, shallowRecord.timestampType(), shallowRecord.compressionType(),
+ shallowRecord.timestamp(), retainedEntries);
+ MemoryRecords records = builder.build();
+ buffer.position(buffer.position() + slice.position());
+ messagesRetained += retainedEntries.size();
+ bytesRetained += records.sizeInBytes();
+
+ MemoryRecordsBuilder.RecordsInfo info = builder.info();
+ if (info.maxTimestamp > maxTimestamp) {
+ maxTimestamp = info.maxTimestamp;
+ shallowOffsetOfMaxTimestamp = info.shallowOffsetOfMaxTimestamp;
+ }
+ }
}
+
+ return new FilterResult(messagesRead, bytesRead, messagesRetained, bytesRetained, maxTimestamp, shallowOffsetOfMaxTimestamp);
}
/**
- * The size of this record set
+ * Get the byte buffer that backs this instance for reading.
*/
- @Override
- public int sizeInBytes() {
- if (writable) {
- return compressor.buffer().position();
- } else {
- return buffer.limit();
- }
+ public ByteBuffer buffer() {
+ return buffer.duplicate();
}
@Override
- public long writeTo(GatheringByteChannel channel, long offset, int length) throws IOException {
- ByteBuffer dup = buffer.duplicate();
- int position = (int) offset;
- dup.position(position);
- dup.limit(position + length);
- return channel.write(dup);
+ public Iterator shallowIterator() {
+ return RecordsIterator.shallowIterator(new ByteBufferLogInputStream(buffer.duplicate(), Integer.MAX_VALUE));
}
- /**
- * The compression rate of this record set
- */
- public double compressionRate() {
- if (compressor == null)
- return 1.0;
- else
- return compressor.compressionRate();
+ @Override
+ public Iterator deepIterator() {
+ return deepIterator(false);
}
- /**
- * Return the capacity of the initial buffer, for writable records
- * it may be different from the current buffer's capacity
- */
- public int initialCapacity() {
- return this.initialCapacity;
+ public Iterator deepIterator(boolean ensureMatchingMagic) {
+ return deepIterator(ensureMatchingMagic, Integer.MAX_VALUE);
}
- /**
- * Get the byte buffer that backs this records instance for reading
- */
- public ByteBuffer buffer() {
- if (writable)
- throw new IllegalStateException("The memory records must not be writable any more before getting its underlying buffer");
-
- return buffer.duplicate();
+ public Iterator deepIterator(boolean ensureMatchingMagic, int maxMessageSize) {
+ return new RecordsIterator(new ByteBufferLogInputStream(buffer.duplicate(), maxMessageSize), false,
+ ensureMatchingMagic, maxMessageSize);
}
- @Override
- public Iterator iterator() {
- ByteBuffer input = this.buffer.duplicate();
- if (writable)
- // flip on a duplicate buffer for reading
- input.flip();
- return new RecordsIterator(new ByteBufferLogInputStream(input), false);
- }
-
@Override
public String toString() {
- Iterator iter = iterator();
+ Iterator iter = deepIterator();
StringBuilder builder = new StringBuilder();
builder.append('[');
while (iter.hasNext()) {
@@ -214,16 +202,13 @@ public String toString() {
builder.append("record=");
builder.append(entry.record());
builder.append(")");
+ if (iter.hasNext())
+ builder.append(", ");
}
builder.append(']');
return builder.toString();
}
- /** Visible for testing */
- public boolean isWritable() {
- return writable;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -232,7 +217,6 @@ public boolean equals(Object o) {
MemoryRecords that = (MemoryRecords) o;
return buffer.equals(that.buffer);
-
}
@Override
@@ -240,28 +224,153 @@ public int hashCode() {
return buffer.hashCode();
}
- private static class ByteBufferLogInputStream implements LogInputStream {
- private final DataInputStream stream;
- private final ByteBuffer buffer;
+ public interface LogEntryFilter {
+ boolean shouldRetain(LogEntry entry);
+ }
- private ByteBufferLogInputStream(ByteBuffer buffer) {
- this.stream = new DataInputStream(new ByteBufferInputStream(buffer));
- this.buffer = buffer;
+ public static class FilterResult {
+ public final int messagesRead;
+ public final int bytesRead;
+ public final int messagesRetained;
+ public final int bytesRetained;
+ public final long maxTimestamp;
+ public final long shallowOffsetOfMaxTimestamp;
+
+ public FilterResult(int messagesRead,
+ int bytesRead,
+ int messagesRetained,
+ int bytesRetained,
+ long maxTimestamp,
+ long shallowOffsetOfMaxTimestamp) {
+ this.messagesRead = messagesRead;
+ this.bytesRead = bytesRead;
+ this.messagesRetained = messagesRetained;
+ this.bytesRetained = bytesRetained;
+ this.maxTimestamp = maxTimestamp;
+ this.shallowOffsetOfMaxTimestamp = shallowOffsetOfMaxTimestamp;
}
+ }
- public LogEntry nextEntry() throws IOException {
- long offset = stream.readLong();
- int size = stream.readInt();
- if (size < 0)
- throw new IllegalStateException("Record with size " + size);
-
- ByteBuffer slice = buffer.slice();
- int newPos = buffer.position() + size;
- if (newPos > buffer.limit())
- return null;
- buffer.position(newPos);
- slice.limit(size);
- return new LogEntry(offset, new Record(slice));
- }
+ public static MemoryRecordsBuilder builder(ByteBuffer buffer,
+ CompressionType compressionType,
+ TimestampType timestampType,
+ int writeLimit) {
+ return new MemoryRecordsBuilder(buffer, Record.CURRENT_MAGIC_VALUE, compressionType, timestampType, 0L, System.currentTimeMillis(), writeLimit);
+ }
+
+ public static MemoryRecordsBuilder builder(ByteBuffer buffer,
+ byte magic,
+ CompressionType compressionType,
+ TimestampType timestampType,
+ long baseOffset,
+ long logAppendTime) {
+ return new MemoryRecordsBuilder(buffer, magic, compressionType, timestampType, baseOffset, logAppendTime, buffer.capacity());
+ }
+
+ public static MemoryRecordsBuilder builder(ByteBuffer buffer,
+ CompressionType compressionType,
+ TimestampType timestampType) {
+ // use the buffer capacity as the default write limit
+ return builder(buffer, compressionType, timestampType, buffer.capacity());
+ }
+
+ public static MemoryRecordsBuilder builder(ByteBuffer buffer,
+ byte magic,
+ CompressionType compressionType,
+ TimestampType timestampType) {
+ return builder(buffer, magic, compressionType, timestampType, 0L);
+ }
+
+ public static MemoryRecordsBuilder builder(ByteBuffer buffer,
+ byte magic,
+ CompressionType compressionType,
+ TimestampType timestampType,
+ long baseOffset) {
+ return builder(buffer, magic, compressionType, timestampType, baseOffset, System.currentTimeMillis());
+ }
+
+ public static MemoryRecords readableRecords(ByteBuffer buffer) {
+ return new MemoryRecords(buffer);
+ }
+
+ public static MemoryRecords withLogEntries(CompressionType compressionType, List entries) {
+ return withLogEntries(TimestampType.CREATE_TIME, compressionType, System.currentTimeMillis(), entries);
+ }
+
+ public static MemoryRecords withLogEntries(LogEntry ... entries) {
+ return withLogEntries(CompressionType.NONE, Arrays.asList(entries));
+ }
+
+ public static MemoryRecords withRecords(CompressionType compressionType, long initialOffset, List records) {
+ return withRecords(initialOffset, TimestampType.CREATE_TIME, compressionType, System.currentTimeMillis(), records);
}
+
+ public static MemoryRecords withRecords(Record ... records) {
+ return withRecords(CompressionType.NONE, 0L, Arrays.asList(records));
+ }
+
+ public static MemoryRecords withRecords(long initialOffset, Record ... records) {
+ return withRecords(CompressionType.NONE, initialOffset, Arrays.asList(records));
+ }
+
+ public static MemoryRecords withRecords(CompressionType compressionType, Record ... records) {
+ return withRecords(compressionType, 0L, Arrays.asList(records));
+ }
+
+ public static MemoryRecords withRecords(TimestampType timestampType, CompressionType compressionType, Record ... records) {
+ return withRecords(0L, timestampType, compressionType, System.currentTimeMillis(), Arrays.asList(records));
+ }
+
+ public static MemoryRecords withRecords(long initialOffset,
+ TimestampType timestampType,
+ CompressionType compressionType,
+ long logAppendTime,
+ List records) {
+ return withLogEntries(timestampType, compressionType, logAppendTime, buildLogEntries(initialOffset, records));
+ }
+
+ private static MemoryRecords withLogEntries(TimestampType timestampType,
+ CompressionType compressionType,
+ long logAppendTime,
+ List entries) {
+ if (entries.isEmpty())
+ return MemoryRecords.EMPTY;
+ return builderWithEntries(timestampType, compressionType, logAppendTime, entries).build();
+ }
+
+ private static List buildLogEntries(long initialOffset, List records) {
+ List entries = new ArrayList<>();
+ for (Record record : records)
+ entries.add(LogEntry.create(initialOffset++, record));
+ return entries;
+ }
+
+ public static MemoryRecordsBuilder builderWithEntries(TimestampType timestampType,
+ CompressionType compressionType,
+ long logAppendTime,
+ List entries) {
+ ByteBuffer buffer = ByteBuffer.allocate(estimatedSize(compressionType, entries));
+ return builderWithEntries(buffer, timestampType, compressionType, logAppendTime, entries);
+ }
+
+ private static MemoryRecordsBuilder builderWithEntries(ByteBuffer buffer,
+ TimestampType timestampType,
+ CompressionType compressionType,
+ long logAppendTime,
+ List entries) {
+ if (entries.isEmpty())
+ throw new IllegalArgumentException();
+
+ LogEntry firstEntry = entries.iterator().next();
+ long firstOffset = firstEntry.offset();
+ byte magic = firstEntry.record().magic();
+
+ MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, magic, compressionType, timestampType,
+ firstOffset, logAppendTime);
+ for (LogEntry entry : entries)
+ builder.append(entry);
+
+ return builder;
+ }
+
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/MemoryRecordsBuilder.java b/clients/src/main/java/org/apache/kafka/common/record/MemoryRecordsBuilder.java
new file mode 100644
index 0000000000000..b90a9e673aab5
--- /dev/null
+++ b/clients/src/main/java/org/apache/kafka/common/record/MemoryRecordsBuilder.java
@@ -0,0 +1,461 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.apache.kafka.common.KafkaException;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.nio.ByteBuffer;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * This class is used to write new log data in memory, i.e. this is the write path for {@link MemoryRecords}.
+ * It transparently handles compression and exposes methods for appending new entries, possibly with message
+ * format conversion.
+ */
+public class MemoryRecordsBuilder {
+
+ static private final float COMPRESSION_RATE_DAMPING_FACTOR = 0.9f;
+ static private final float COMPRESSION_RATE_ESTIMATION_FACTOR = 1.05f;
+ static private final int COMPRESSION_DEFAULT_BUFFER_SIZE = 1024;
+
+ private static final float[] TYPE_TO_RATE;
+
+ static {
+ int maxTypeId = -1;
+ for (CompressionType type : CompressionType.values())
+ maxTypeId = Math.max(maxTypeId, type.id);
+ TYPE_TO_RATE = new float[maxTypeId + 1];
+ for (CompressionType type : CompressionType.values()) {
+ TYPE_TO_RATE[type.id] = type.rate;
+ }
+ }
+
+ // dynamically load the snappy and lz4 classes to avoid runtime dependency if we are not using compression
+ // caching constructors to avoid invoking of Class.forName method for each batch
+ private static MemoizingConstructorSupplier snappyOutputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
+ @Override
+ public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
+ return Class.forName("org.xerial.snappy.SnappyOutputStream")
+ .getConstructor(OutputStream.class, Integer.TYPE);
+ }
+ });
+
+ private static MemoizingConstructorSupplier lz4OutputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
+ @Override
+ public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
+ return Class.forName("org.apache.kafka.common.record.KafkaLZ4BlockOutputStream")
+ .getConstructor(OutputStream.class, Boolean.TYPE);
+ }
+ });
+
+ private static MemoizingConstructorSupplier snappyInputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
+ @Override
+ public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
+ return Class.forName("org.xerial.snappy.SnappyInputStream")
+ .getConstructor(InputStream.class);
+ }
+ });
+
+ private static MemoizingConstructorSupplier lz4InputStreamSupplier = new MemoizingConstructorSupplier(new ConstructorSupplier() {
+ @Override
+ public Constructor get() throws ClassNotFoundException, NoSuchMethodException {
+ return Class.forName("org.apache.kafka.common.record.KafkaLZ4BlockInputStream")
+ .getConstructor(InputStream.class, Boolean.TYPE);
+ }
+ });
+
+ private final TimestampType timestampType;
+ private final CompressionType compressionType;
+ private final DataOutputStream appendStream;
+ private final ByteBufferOutputStream bufferStream;
+ private final byte magic;
+ private final int initPos;
+ private final long baseOffset;
+ private final long logAppendTime;
+ private final int writeLimit;
+ private final int initialCapacity;
+
+ private MemoryRecords builtRecords;
+ private long writtenUncompressed;
+ private long numRecords;
+ private float compressionRate;
+ private long maxTimestamp;
+ private long offsetOfMaxTimestamp;
+ private long lastOffset = -1;
+
+ public MemoryRecordsBuilder(ByteBuffer buffer,
+ byte magic,
+ CompressionType compressionType,
+ TimestampType timestampType,
+ long baseOffset,
+ long logAppendTime,
+ int writeLimit) {
+ this.magic = magic;
+ this.timestampType = timestampType;
+ this.compressionType = compressionType;
+ this.baseOffset = baseOffset;
+ this.logAppendTime = logAppendTime;
+ this.initPos = buffer.position();
+ this.numRecords = 0;
+ this.writtenUncompressed = 0;
+ this.compressionRate = 1;
+ this.maxTimestamp = Record.NO_TIMESTAMP;
+ this.writeLimit = writeLimit;
+ this.initialCapacity = buffer.capacity();
+
+ if (compressionType != CompressionType.NONE) {
+ // for compressed records, leave space for the header and the shallow message metadata
+ // and move the starting position to the value payload offset
+ buffer.position(initPos + Records.LOG_OVERHEAD + Record.recordOverhead(magic));
+ }
+
+ // create the stream
+ bufferStream = new ByteBufferOutputStream(buffer);
+ appendStream = wrapForOutput(bufferStream, compressionType, magic, COMPRESSION_DEFAULT_BUFFER_SIZE);
+ }
+
+ public ByteBuffer buffer() {
+ return bufferStream.buffer();
+ }
+
+ public int initialCapacity() {
+ return initialCapacity;
+ }
+
+ public double compressionRate() {
+ return compressionRate;
+ }
+
+ /**
+ * Close this builder and return the resulting buffer.
+ * @return The built log buffer
+ */
+ public MemoryRecords build() {
+ close();
+ return builtRecords;
+ }
+
+ /**
+ * Get the max timestamp and its offset. If the log append time is used, then the offset will
+ * be either the first offset in the set if no compression is used or the last offset otherwise.
+ * @return The max timestamp and its offset
+ */
+ public RecordsInfo info() {
+ if (timestampType == TimestampType.LOG_APPEND_TIME)
+ return new RecordsInfo(logAppendTime, lastOffset);
+ else
+ return new RecordsInfo(maxTimestamp, compressionType == CompressionType.NONE ? offsetOfMaxTimestamp : lastOffset);
+ }
+
+ public void close() {
+ if (builtRecords != null)
+ return;
+
+ try {
+ appendStream.close();
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+
+ if (compressionType != CompressionType.NONE)
+ writerCompressedWrapperHeader();
+
+ ByteBuffer buffer = buffer().duplicate();
+ buffer.flip();
+ buffer.position(initPos);
+ builtRecords = MemoryRecords.readableRecords(buffer.slice());
+ }
+
+ private void writerCompressedWrapperHeader() {
+ ByteBuffer buffer = bufferStream.buffer();
+ int pos = buffer.position();
+ buffer.position(initPos);
+
+ int wrapperSize = pos - initPos - Records.LOG_OVERHEAD;
+ int writtenCompressed = wrapperSize - Record.recordOverhead(magic);
+ LogEntry.writeHeader(buffer, lastOffset, wrapperSize);
+
+ long timestamp = timestampType == TimestampType.LOG_APPEND_TIME ? logAppendTime : maxTimestamp;
+ Record.writeCompressedRecordHeader(buffer, magic, wrapperSize, timestamp, compressionType, timestampType);
+
+ buffer.position(pos);
+
+ // update the compression ratio
+ this.compressionRate = (float) writtenCompressed / this.writtenUncompressed;
+ TYPE_TO_RATE[compressionType.id] = TYPE_TO_RATE[compressionType.id] * COMPRESSION_RATE_DAMPING_FACTOR +
+ compressionRate * (1 - COMPRESSION_RATE_DAMPING_FACTOR);
+ }
+
+ /**
+ * Append a new record and offset to the buffer
+ * @param offset The absolute offset of the record in the log buffer
+ * @param timestamp The record timestamp
+ * @param key The record key
+ * @param value The record value
+ * @return crc of the record
+ */
+ public long append(long offset, long timestamp, byte[] key, byte[] value) {
+ try {
+ if (lastOffset > 0 && offset <= lastOffset)
+ throw new IllegalArgumentException(String.format("Illegal offset %s following previous offset %s (Offsets must increase monotonically).", offset, lastOffset));
+
+ int size = Record.recordSize(magic, key, value);
+ LogEntry.writeHeader(appendStream, toInnerOffset(offset), size);
+
+ if (timestampType == TimestampType.LOG_APPEND_TIME)
+ timestamp = logAppendTime;
+ long crc = Record.write(appendStream, magic, timestamp, key, value, CompressionType.NONE, timestampType);
+ recordWritten(offset, timestamp, size + Records.LOG_OVERHEAD);
+ return crc;
+ } catch (IOException e) {
+ throw new KafkaException("I/O exception when writing to the append stream, closing", e);
+ }
+ }
+
+ /**
+ * Add the record, converting to the desired magic value if necessary.
+ * @param offset The offset of the record
+ * @param record The record to add
+ */
+ public void convertAndAppend(long offset, Record record) {
+ if (magic == record.magic()) {
+ append(offset, record);
+ return;
+ }
+
+ if (lastOffset > 0 && offset <= lastOffset)
+ throw new IllegalArgumentException(String.format("Illegal offset %s following previous offset %s (Offsets must increase monotonically).", offset, lastOffset));
+
+ try {
+ int size = record.convertedSize(magic);
+ LogEntry.writeHeader(appendStream, toInnerOffset(offset), size);
+ long timestamp = timestampType == TimestampType.LOG_APPEND_TIME ? logAppendTime : record.timestamp();
+ record.convertTo(appendStream, magic, timestamp, timestampType);
+ recordWritten(offset, timestamp, size + Records.LOG_OVERHEAD);
+ } catch (IOException e) {
+ throw new KafkaException("I/O exception when writing to the append stream, closing", e);
+ }
+ }
+
+ /**
+ * Add a record without doing offset/magic validation (this should only be used in testing).
+ * @param offset The offset of the record
+ * @param record The record to add
+ */
+ public void appendUnchecked(long offset, Record record) {
+ try {
+ int size = record.sizeInBytes();
+ LogEntry.writeHeader(appendStream, toInnerOffset(offset), size);
+
+ ByteBuffer buffer = record.buffer().duplicate();
+ appendStream.write(buffer.array(), buffer.arrayOffset(), buffer.limit());
+
+ recordWritten(offset, record.timestamp(), size + Records.LOG_OVERHEAD);
+ } catch (IOException e) {
+ throw new KafkaException("I/O exception when writing to the append stream, closing", e);
+ }
+ }
+
+ /**
+ * Append the given log entry. The entry's record must have a magic which matches the magic use to
+ * construct this builder and the offset must be greater than the last appended entry.
+ * @param entry The entry to append
+ */
+ public void append(LogEntry entry) {
+ append(entry.offset(), entry.record());
+ }
+
+ /**
+ * Add a record with a given offset. The record must have a magic which matches the magic use to
+ * construct this builder and the offset must be greater than the last appended entry.
+ * @param offset The offset of the record
+ * @param record The record to add
+ */
+ public void append(long offset, Record record) {
+ if (record.magic() != magic)
+ throw new IllegalArgumentException("Inner log entries must have matching magic values as the wrapper");
+ if (lastOffset > 0 && offset <= lastOffset)
+ throw new IllegalArgumentException(String.format("Illegal offset %s following previous offset %s (Offsets must increase monotonically).", offset, lastOffset));
+ appendUnchecked(offset, record);
+ }
+
+ private long toInnerOffset(long offset) {
+ // use relative offsets for compressed messages with magic v1
+ if (magic > 0 && compressionType != CompressionType.NONE)
+ return offset - baseOffset;
+ return offset;
+ }
+
+ private void recordWritten(long offset, long timestamp, int size) {
+ numRecords += 1;
+ writtenUncompressed += size;
+ lastOffset = offset;
+
+ if (timestamp > maxTimestamp) {
+ maxTimestamp = timestamp;
+ offsetOfMaxTimestamp = offset;
+ }
+ }
+
+ /**
+ * Get an estimate of the number of bytes written (based on the estimation factor hard-coded in {@link CompressionType}.
+ * @return The estimated number of bytes written
+ */
+ private int estimatedBytesWritten() {
+ if (compressionType == CompressionType.NONE) {
+ return buffer().position();
+ } else {
+ // estimate the written bytes to the underlying byte buffer based on uncompressed written bytes
+ return (int) (writtenUncompressed * TYPE_TO_RATE[compressionType.id] * COMPRESSION_RATE_ESTIMATION_FACTOR);
+ }
+ }
+
+ /**
+ * Check if we have room for a new record containing the given key/value pair
+ *
+ * Note that the return value is based on the estimate of the bytes written to the compressor, which may not be
+ * accurate if compression is really used. When this happens, the following append may cause dynamic buffer
+ * re-allocation in the underlying byte buffer stream.
+ *
+ * There is an exceptional case when appending a single message whose size is larger than the batch size, the
+ * capacity will be the message size which is larger than the write limit, i.e. the batch size. In this case
+ * the checking should be based on the capacity of the initialized buffer rather than the write limit in order
+ * to accept this single record.
+ */
+ public boolean hasRoomFor(byte[] key, byte[] value) {
+ return !isFull() && (numRecords == 0 ?
+ this.initialCapacity >= Records.LOG_OVERHEAD + Record.recordSize(magic, key, value) :
+ this.writeLimit >= estimatedBytesWritten() + Records.LOG_OVERHEAD + Record.recordSize(magic, key, value));
+ }
+
+ public boolean isClosed() {
+ return builtRecords != null;
+ }
+
+ public boolean isFull() {
+ return isClosed() || this.writeLimit <= estimatedBytesWritten();
+ }
+
+ public int sizeInBytes() {
+ return builtRecords != null ? builtRecords.sizeInBytes() : estimatedBytesWritten();
+ }
+
+ private static DataOutputStream wrapForOutput(ByteBufferOutputStream buffer, CompressionType type, byte messageVersion, int bufferSize) {
+ try {
+ switch (type) {
+ case NONE:
+ return buffer;
+ case GZIP:
+ return new DataOutputStream(new GZIPOutputStream(buffer, bufferSize));
+ case SNAPPY:
+ try {
+ OutputStream stream = (OutputStream) snappyOutputStreamSupplier.get().newInstance(buffer, bufferSize);
+ return new DataOutputStream(stream);
+ } catch (Exception e) {
+ throw new KafkaException(e);
+ }
+ case LZ4:
+ try {
+ OutputStream stream = (OutputStream) lz4OutputStreamSupplier.get().newInstance(buffer,
+ messageVersion == Record.MAGIC_VALUE_V0);
+ return new DataOutputStream(stream);
+ } catch (Exception e) {
+ throw new KafkaException(e);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown compression type: " + type);
+ }
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+ }
+
+ public static DataInputStream wrapForInput(ByteBufferInputStream buffer, CompressionType type, byte messageVersion) {
+ try {
+ switch (type) {
+ case NONE:
+ return buffer;
+ case GZIP:
+ return new DataInputStream(new GZIPInputStream(buffer));
+ case SNAPPY:
+ try {
+ InputStream stream = (InputStream) snappyInputStreamSupplier.get().newInstance(buffer);
+ return new DataInputStream(stream);
+ } catch (Exception e) {
+ throw new KafkaException(e);
+ }
+ case LZ4:
+ try {
+ InputStream stream = (InputStream) lz4InputStreamSupplier.get().newInstance(buffer,
+ messageVersion == Record.MAGIC_VALUE_V0);
+ return new DataInputStream(stream);
+ } catch (Exception e) {
+ throw new KafkaException(e);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown compression type: " + type);
+ }
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+ }
+
+ private interface ConstructorSupplier {
+ Constructor get() throws ClassNotFoundException, NoSuchMethodException;
+ }
+
+ // this code is based on Guava's @see{com.google.common.base.Suppliers.MemoizingSupplier}
+ private static class MemoizingConstructorSupplier {
+ final ConstructorSupplier delegate;
+ transient volatile boolean initialized;
+ transient Constructor value;
+
+ public MemoizingConstructorSupplier(ConstructorSupplier delegate) {
+ this.delegate = delegate;
+ }
+
+ public Constructor get() throws NoSuchMethodException, ClassNotFoundException {
+ if (!initialized) {
+ synchronized (this) {
+ if (!initialized) {
+ value = delegate.get();
+ initialized = true;
+ }
+ }
+ }
+ return value;
+ }
+ }
+
+ public static class RecordsInfo {
+ public final long maxTimestamp;
+ public final long shallowOffsetOfMaxTimestamp;
+
+ public RecordsInfo(long maxTimestamp,
+ long shallowOffsetOfMaxTimestamp) {
+ this.maxTimestamp = maxTimestamp;
+ this.shallowOffsetOfMaxTimestamp = shallowOffsetOfMaxTimestamp;
+ }
+ }
+}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/Record.java b/clients/src/main/java/org/apache/kafka/common/record/Record.java
index 09cb80dff2daf..0c0fa3c49c4d6 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/Record.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/Record.java
@@ -16,11 +16,15 @@
*/
package org.apache.kafka.common.record;
-import java.nio.ByteBuffer;
-
+import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.utils.Crc32;
import org.apache.kafka.common.utils.Utils;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import static org.apache.kafka.common.utils.Utils.wrapNullable;
/**
* A record: a serialized key and value along with the associated CRC and other fields
@@ -53,7 +57,12 @@ public final class Record {
/**
* The amount of overhead bytes in a record
*/
- public static final int RECORD_OVERHEAD = HEADER_SIZE + TIMESTAMP_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH;
+ public static final int RECORD_OVERHEAD_V0 = HEADER_SIZE + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH;
+
+ /**
+ * The amount of overhead bytes in a record
+ */
+ public static final int RECORD_OVERHEAD_V1 = HEADER_SIZE + TIMESTAMP_LENGTH + KEY_SIZE_LENGTH + VALUE_SIZE_LENGTH;
/**
* The "magic" values
@@ -79,11 +88,6 @@ public final class Record {
public static final byte TIMESTAMP_TYPE_MASK = 0x08;
public static final int TIMESTAMP_TYPE_ATTRIBUTE_OFFSET = 3;
- /**
- * Compression code for uncompressed records
- */
- public static final int NO_COMPRESSION = 0;
-
/**
* Timestamp value for records without a timestamp
*/
@@ -94,155 +98,20 @@ public final class Record {
private final TimestampType wrapperRecordTimestampType;
public Record(ByteBuffer buffer) {
- this.buffer = buffer;
- this.wrapperRecordTimestamp = null;
- this.wrapperRecordTimestampType = null;
+ this(buffer, null, null);
}
- // Package private constructor for inner iteration.
- Record(ByteBuffer buffer, Long wrapperRecordTimestamp, TimestampType wrapperRecordTimestampType) {
+ public Record(ByteBuffer buffer, Long wrapperRecordTimestamp, TimestampType wrapperRecordTimestampType) {
this.buffer = buffer;
this.wrapperRecordTimestamp = wrapperRecordTimestamp;
this.wrapperRecordTimestampType = wrapperRecordTimestampType;
}
- /**
- * A constructor to create a LogRecord. If the record's compression type is not none, then
- * its value payload should be already compressed with the specified type; the constructor
- * would always write the value payload as is and will not do the compression itself.
- *
- * @param timestamp The timestamp of the record
- * @param key The key of the record (null, if none)
- * @param value The record value
- * @param type The compression type used on the contents of the record (if any)
- * @param valueOffset The offset into the payload array used to extract payload
- * @param valueSize The size of the payload to use
- */
- public Record(long timestamp, byte[] key, byte[] value, CompressionType type, int valueOffset, int valueSize) {
- this(ByteBuffer.allocate(recordSize(key == null ? 0 : key.length,
- value == null ? 0 : valueSize >= 0 ? valueSize : value.length - valueOffset)));
- write(this.buffer, timestamp, key, value, type, valueOffset, valueSize);
- this.buffer.rewind();
- }
-
- public Record(long timestamp, byte[] key, byte[] value, CompressionType type) {
- this(timestamp, key, value, type, 0, -1);
- }
-
- public Record(long timestamp, byte[] value, CompressionType type) {
- this(timestamp, null, value, type);
- }
-
- public Record(long timestamp, byte[] key, byte[] value) {
- this(timestamp, key, value, CompressionType.NONE);
- }
-
- public Record(long timestamp, byte[] value) {
- this(timestamp, null, value, CompressionType.NONE);
- }
-
- // Write a record to the buffer, if the record's compression type is none, then
- // its value payload should be already compressed with the specified type
- public static void write(ByteBuffer buffer, long timestamp, byte[] key, byte[] value, CompressionType type, int valueOffset, int valueSize) {
- // construct the compressor with compression type none since this function will not do any
- //compression according to the input type, it will just write the record's payload as is
- Compressor compressor = new Compressor(buffer, CompressionType.NONE);
- try {
- compressor.putRecord(timestamp, key, value, type, valueOffset, valueSize);
- } finally {
- compressor.close();
- }
- }
-
- public static void write(Compressor compressor, long crc, byte attributes, long timestamp, byte[] key, byte[] value, int valueOffset, int valueSize) {
- // write crc
- compressor.putInt((int) (crc & 0xffffffffL));
- // write magic value
- compressor.putByte(CURRENT_MAGIC_VALUE);
- // write attributes
- compressor.putByte(attributes);
- // write timestamp
- compressor.putLong(timestamp);
- // write the key
- if (key == null) {
- compressor.putInt(-1);
- } else {
- compressor.putInt(key.length);
- compressor.put(key, 0, key.length);
- }
- // write the value
- if (value == null) {
- compressor.putInt(-1);
- } else {
- int size = valueSize >= 0 ? valueSize : (value.length - valueOffset);
- compressor.putInt(size);
- compressor.put(value, valueOffset, size);
- }
- }
-
- public static int recordSize(byte[] key, byte[] value) {
- return recordSize(key == null ? 0 : key.length, value == null ? 0 : value.length);
- }
-
- public static int recordSize(int keySize, int valueSize) {
- return CRC_LENGTH + MAGIC_LENGTH + ATTRIBUTE_LENGTH + TIMESTAMP_LENGTH + KEY_SIZE_LENGTH + keySize + VALUE_SIZE_LENGTH + valueSize;
- }
-
- public ByteBuffer buffer() {
- return this.buffer;
- }
-
- public static byte computeAttributes(CompressionType type) {
- byte attributes = 0;
- if (type.id > 0)
- attributes = (byte) (attributes | (COMPRESSION_CODEC_MASK & type.id));
- return attributes;
- }
-
- /**
- * Compute the checksum of the record from the record contents
- */
- public static long computeChecksum(ByteBuffer buffer, int position, int size) {
- Crc32 crc = new Crc32();
- crc.update(buffer.array(), buffer.arrayOffset() + position, size);
- return crc.getValue();
- }
-
- /**
- * Compute the checksum of the record from the attributes, key and value payloads
- */
- public static long computeChecksum(long timestamp, byte[] key, byte[] value, CompressionType type, int valueOffset, int valueSize) {
- Crc32 crc = new Crc32();
- crc.update(CURRENT_MAGIC_VALUE);
- byte attributes = 0;
- if (type.id > 0)
- attributes = (byte) (attributes | (COMPRESSION_CODEC_MASK & type.id));
- crc.update(attributes);
- crc.updateLong(timestamp);
- // update for the key
- if (key == null) {
- crc.updateInt(-1);
- } else {
- crc.updateInt(key.length);
- crc.update(key, 0, key.length);
- }
- // update for the value
- if (value == null) {
- crc.updateInt(-1);
- } else {
- int size = valueSize >= 0 ? valueSize : (value.length - valueOffset);
- crc.updateInt(size);
- crc.update(value, valueOffset, size);
- }
- return crc.getValue();
- }
-
-
/**
* Compute the checksum of the record from the record contents
*/
public long computeChecksum() {
- return computeChecksum(buffer, MAGIC_OFFSET, buffer.limit() - MAGIC_OFFSET);
+ return Utils.computeChecksum(buffer, MAGIC_OFFSET, buffer.limit() - MAGIC_OFFSET);
}
/**
@@ -256,7 +125,15 @@ public long checksum() {
* Returns true if the crc stored with the record matches the crc computed off the record contents
*/
public boolean isValid() {
- return size() >= CRC_LENGTH && checksum() == computeChecksum();
+ return sizeInBytes() >= CRC_LENGTH && checksum() == computeChecksum();
+ }
+
+ public Long wrapperRecordTimestamp() {
+ return wrapperRecordTimestamp;
+ }
+
+ public TimestampType wrapperRecordTimestampType() {
+ return wrapperRecordTimestampType;
}
/**
@@ -264,9 +141,9 @@ public boolean isValid() {
*/
public void ensureValid() {
if (!isValid()) {
- if (size() < CRC_LENGTH)
+ if (sizeInBytes() < CRC_LENGTH)
throw new InvalidRecordException("Record is corrupt (crc could not be retrieved as the record is too "
- + "small, size = " + size() + ")");
+ + "small, size = " + sizeInBytes() + ")");
else
throw new InvalidRecordException("Record is corrupt (stored crc = " + checksum()
+ ", computed crc = " + computeChecksum() + ")");
@@ -274,14 +151,17 @@ public void ensureValid() {
}
/**
- * The complete serialized size of this record in bytes (including crc, header attributes, etc)
+ * The complete serialized size of this record in bytes (including crc, header attributes, etc), but
+ * excluding the log overhead (offset and record size).
+ * @return the size in bytes
*/
- public int size() {
+ public int sizeInBytes() {
return buffer.limit();
}
/**
* The length of the key in bytes
+ * @return the size in bytes of the key (0 if the key is null)
*/
public int keySize() {
if (magic() == MAGIC_VALUE_V0)
@@ -292,6 +172,7 @@ public int keySize() {
/**
* Does the record have a key?
+ * @return true if so, false otherwise
*/
public boolean hasKey() {
return keySize() >= 0;
@@ -309,13 +190,23 @@ private int valueSizeOffset() {
/**
* The length of the value in bytes
+ * @return the size in bytes of the value (0 if the value is null)
*/
public int valueSize() {
return buffer.getInt(valueSizeOffset());
}
/**
- * The magic version of this record
+ * Check whether the value field of this record is null.
+ * @return true if the value is null, false otherwise
+ */
+ public boolean hasNullValue() {
+ return valueSize() < 0;
+ }
+
+ /**
+ * The magic value (i.e. message format version) of this record
+ * @return the magic value
*/
public byte magic() {
return buffer.get(MAGIC_OFFSET);
@@ -323,6 +214,7 @@ public byte magic() {
/**
* The attributes stored with this record
+ * @return the attributes
*/
public byte attributes() {
return buffer.get(ATTRIBUTES_OFFSET);
@@ -333,6 +225,8 @@ public byte attributes() {
* 1. wrapperRecordTimestampType = null and wrapperRecordTimestamp is null - Uncompressed message, timestamp is in the message.
* 2. wrapperRecordTimestampType = LOG_APPEND_TIME and WrapperRecordTimestamp is not null - Compressed message using LOG_APPEND_TIME
* 3. wrapperRecordTimestampType = CREATE_TIME and wrapperRecordTimestamp is not null - Compressed message using CREATE_TIME
+ *
+ * @return the timestamp as determined above
*/
public long timestamp() {
if (magic() == MAGIC_VALUE_V0)
@@ -349,6 +243,8 @@ public long timestamp() {
/**
* The timestamp of the message.
+ * @return the timstamp type or {@link TimestampType#NO_TIMESTAMP_TYPE} if the magic is 0 or the message has
+ * been up-converted.
*/
public TimestampType timestampType() {
if (magic() == 0)
@@ -366,36 +262,30 @@ public CompressionType compressionType() {
/**
* A ByteBuffer containing the value of this record
+ * @return the value or null if the value for this record is null
*/
public ByteBuffer value() {
- return sliceDelimited(valueSizeOffset());
+ return Utils.sizeDelimited(buffer, valueSizeOffset());
}
/**
* A ByteBuffer containing the message key
+ * @return the buffer or null if the key for this record is null
*/
public ByteBuffer key() {
if (magic() == MAGIC_VALUE_V0)
- return sliceDelimited(KEY_SIZE_OFFSET_V0);
+ return Utils.sizeDelimited(buffer, KEY_SIZE_OFFSET_V0);
else
- return sliceDelimited(KEY_SIZE_OFFSET_V1);
+ return Utils.sizeDelimited(buffer, KEY_SIZE_OFFSET_V1);
}
/**
- * Read a size-delimited byte buffer starting at the given offset
+ * Get the underlying buffer backing this record instance.
+ *
+ * @return the buffer
*/
- private ByteBuffer sliceDelimited(int start) {
- int size = buffer.getInt(start);
- if (size < 0) {
- return null;
- } else {
- ByteBuffer b = buffer.duplicate();
- b.position(start + 4);
- b = b.slice();
- b.limit(size);
- b.rewind();
- return b;
- }
+ public ByteBuffer buffer() {
+ return this.buffer;
}
public String toString() {
@@ -434,4 +324,316 @@ public int hashCode() {
return buffer.hashCode();
}
+ /**
+ * Get the size of this record if converted to the given format.
+ *
+ * @param toMagic The target magic version to convert to
+ * @return The size in bytes after conversion
+ */
+ public int convertedSize(byte toMagic) {
+ return recordSize(toMagic, Math.max(0, keySize()), Math.max(0, valueSize()));
+ }
+
+ /**
+ * Convert this record to another message format.
+ *
+ * @param toMagic The target magic version to convert to
+ * @return A new record instance with a freshly allocated ByteBuffer.
+ */
+ public Record convert(byte toMagic) {
+ if (toMagic == magic())
+ return this;
+
+ ByteBuffer buffer = ByteBuffer.allocate(convertedSize(toMagic));
+ TimestampType timestampType = wrapperRecordTimestampType != null ?
+ wrapperRecordTimestampType : TimestampType.forAttributes(attributes());
+ convertTo(buffer, toMagic, timestamp(), timestampType);
+ buffer.rewind();
+ return new Record(buffer);
+ }
+
+ private void convertTo(ByteBuffer buffer, byte toMagic, long timestamp, TimestampType timestampType) {
+ if (compressionType() != CompressionType.NONE)
+ throw new IllegalArgumentException("Cannot use convertTo for deep conversion");
+
+ write(buffer, toMagic, timestamp, key(), value(), CompressionType.NONE, timestampType);
+ }
+
+ /**
+ * Convert this record to another message format and write the converted data to the provided outputs stream.
+ *
+ * @param out The output stream to write the converted data to
+ * @param toMagic The target magic version for conversion
+ * @param timestamp The timestamp to use in the converted record (for up-conversion)
+ * @param timestampType The timestamp type to use in the converted record (for up-conversion)
+ * @throws IOException for any IO errors writing the converted record.
+ */
+ public void convertTo(DataOutputStream out, byte toMagic, long timestamp, TimestampType timestampType) throws IOException {
+ if (compressionType() != CompressionType.NONE)
+ throw new IllegalArgumentException("Cannot use convertTo for deep conversion");
+
+ write(out, toMagic, timestamp, key(), value(), CompressionType.NONE, timestampType);
+ }
+
+ /**
+ * Create a new record instance. If the record's compression type is not none, then
+ * its value payload should be already compressed with the specified type; the constructor
+ * would always write the value payload as is and will not do the compression itself.
+ *
+ * @param magic The magic value to use
+ * @param timestamp The timestamp of the record
+ * @param key The key of the record (null, if none)
+ * @param value The record value
+ * @param compressionType The compression type used on the contents of the record (if any)
+ * @param timestampType The timestamp type to be used for this record
+ */
+ public static Record create(byte magic,
+ long timestamp,
+ byte[] key,
+ byte[] value,
+ CompressionType compressionType,
+ TimestampType timestampType) {
+ int keySize = key == null ? 0 : key.length;
+ int valueSize = value == null ? 0 : value.length;
+ ByteBuffer buffer = ByteBuffer.allocate(recordSize(magic, keySize, valueSize));
+ write(buffer, magic, timestamp, wrapNullable(key), wrapNullable(value), compressionType, timestampType);
+ buffer.rewind();
+ return new Record(buffer);
+ }
+
+ public static Record create(long timestamp, byte[] key, byte[] value) {
+ return create(CURRENT_MAGIC_VALUE, timestamp, key, value, CompressionType.NONE, TimestampType.CREATE_TIME);
+ }
+
+ public static Record create(byte magic, long timestamp, byte[] key, byte[] value) {
+ return create(magic, timestamp, key, value, CompressionType.NONE, TimestampType.CREATE_TIME);
+ }
+
+ public static Record create(byte magic, TimestampType timestampType, long timestamp, byte[] key, byte[] value) {
+ return create(magic, timestamp, key, value, CompressionType.NONE, timestampType);
+ }
+
+ public static Record create(byte magic, long timestamp, byte[] value) {
+ return create(magic, timestamp, null, value, CompressionType.NONE, TimestampType.CREATE_TIME);
+ }
+
+ public static Record create(byte magic, byte[] key, byte[] value) {
+ return create(magic, NO_TIMESTAMP, key, value);
+ }
+
+ public static Record create(byte[] key, byte[] value) {
+ return create(NO_TIMESTAMP, key, value);
+ }
+
+ public static Record create(byte[] value) {
+ return create(CURRENT_MAGIC_VALUE, NO_TIMESTAMP, null, value, CompressionType.NONE, TimestampType.CREATE_TIME);
+ }
+
+ /**
+ * Write the header for a compressed record set in-place (i.e. assuming the compressed record data has already
+ * been written at the value offset in a wrapped record). This lets you dynamically create a compressed message
+ * set, and then go back later and fill in its size and CRC, which saves the need for copying to another buffer.
+ *
+ * @param buffer The buffer containing the compressed record data positioned at the first offset of the
+ * @param magic The magic value of the record set
+ * @param recordSize The size of the record (including record overhead)
+ * @param timestamp The timestamp of the wrapper record
+ * @param compressionType The compression type used
+ * @param timestampType The timestamp type of the wrapper record
+ */
+ public static void writeCompressedRecordHeader(ByteBuffer buffer,
+ byte magic,
+ int recordSize,
+ long timestamp,
+ CompressionType compressionType,
+ TimestampType timestampType) {
+ int recordPosition = buffer.position();
+ int valueSize = recordSize - recordOverhead(magic);
+
+ // write the record header with a null value (the key is always null for the wrapper)
+ write(buffer, magic, timestamp, null, null, compressionType, timestampType);
+
+ // now fill in the value size
+ buffer.putInt(recordPosition + keyOffset(magic), valueSize);
+
+ // compute and fill the crc from the beginning of the message
+ long crc = Utils.computeChecksum(buffer, recordPosition + MAGIC_OFFSET, recordSize - MAGIC_OFFSET);
+ Utils.writeUnsignedInt(buffer, recordPosition + CRC_OFFSET, crc);
+ }
+
+ private static void write(ByteBuffer buffer,
+ byte magic,
+ long timestamp,
+ ByteBuffer key,
+ ByteBuffer value,
+ CompressionType compressionType,
+ TimestampType timestampType) {
+ try {
+ ByteBufferOutputStream out = new ByteBufferOutputStream(buffer);
+ write(out, magic, timestamp, key, value, compressionType, timestampType);
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+ }
+
+ /**
+ * Write the record data with the given compression type and return the computed crc.
+ *
+ * @param out The output stream to write to
+ * @param magic The magic value to be used
+ * @param timestamp The timestamp of the record
+ * @param key The record key
+ * @param value The record value
+ * @param compressionType The compression type
+ * @param timestampType The timestamp type
+ * @return the computed CRC for this record.
+ * @throws IOException for any IO errors writing to the output stream.
+ */
+ public static long write(DataOutputStream out,
+ byte magic,
+ long timestamp,
+ byte[] key,
+ byte[] value,
+ CompressionType compressionType,
+ TimestampType timestampType) throws IOException {
+ return write(out, magic, timestamp, wrapNullable(key), wrapNullable(value), compressionType, timestampType);
+ }
+
+ private static long write(DataOutputStream out,
+ byte magic,
+ long timestamp,
+ ByteBuffer key,
+ ByteBuffer value,
+ CompressionType compressionType,
+ TimestampType timestampType) throws IOException {
+ byte attributes = computeAttributes(magic, compressionType, timestampType);
+ long crc = computeChecksum(magic, attributes, timestamp, key, value);
+ write(out, magic, crc, attributes, timestamp, key, value);
+ return crc;
+ }
+
+
+ /**
+ * Write a record using raw fields (without validation). This should only be used in testing.
+ */
+ public static void write(DataOutputStream out,
+ byte magic,
+ long crc,
+ byte attributes,
+ long timestamp,
+ byte[] key,
+ byte[] value) throws IOException {
+ write(out, magic, crc, attributes, timestamp, wrapNullable(key), wrapNullable(value));
+ }
+
+ // Write a record to the buffer, if the record's compression type is none, then
+ // its value payload should be already compressed with the specified type
+ private static void write(DataOutputStream out,
+ byte magic,
+ long crc,
+ byte attributes,
+ long timestamp,
+ ByteBuffer key,
+ ByteBuffer value) throws IOException {
+ if (magic != MAGIC_VALUE_V0 && magic != MAGIC_VALUE_V1)
+ throw new IllegalArgumentException("Invalid magic value " + magic);
+ if (timestamp < 0 && timestamp != NO_TIMESTAMP)
+ throw new IllegalArgumentException("Invalid message timestamp " + timestamp);
+
+ // write crc
+ out.writeInt((int) (crc & 0xffffffffL));
+ // write magic value
+ out.writeByte(magic);
+ // write attributes
+ out.writeByte(attributes);
+
+ // maybe write timestamp
+ if (magic > 0)
+ out.writeLong(timestamp);
+
+ // write the key
+ if (key == null) {
+ out.writeInt(-1);
+ } else {
+ int size = key.remaining();
+ out.writeInt(size);
+ out.write(key.array(), key.arrayOffset(), size);
+ }
+ // write the value
+ if (value == null) {
+ out.writeInt(-1);
+ } else {
+ int size = value.remaining();
+ out.writeInt(size);
+ out.write(value.array(), value.arrayOffset(), size);
+ }
+ }
+
+ public static int recordSize(byte[] key, byte[] value) {
+ return recordSize(CURRENT_MAGIC_VALUE, key, value);
+ }
+
+ public static int recordSize(byte magic, byte[] key, byte[] value) {
+ return recordSize(magic, key == null ? 0 : key.length, value == null ? 0 : value.length);
+ }
+
+ private static int recordSize(byte magic, int keySize, int valueSize) {
+ return recordOverhead(magic) + keySize + valueSize;
+ }
+
+ // visible only for testing
+ public static byte computeAttributes(byte magic, CompressionType type, TimestampType timestampType) {
+ byte attributes = 0;
+ if (type.id > 0)
+ attributes = (byte) (attributes | (COMPRESSION_CODEC_MASK & type.id));
+ if (magic > 0)
+ return timestampType.updateAttributes(attributes);
+ return attributes;
+ }
+
+ // visible only for testing
+ public static long computeChecksum(byte magic, byte attributes, long timestamp, byte[] key, byte[] value) {
+ return computeChecksum(magic, attributes, timestamp, wrapNullable(key), wrapNullable(value));
+ }
+
+ /**
+ * Compute the checksum of the record from the attributes, key and value payloads
+ */
+ private static long computeChecksum(byte magic, byte attributes, long timestamp, ByteBuffer key, ByteBuffer value) {
+ Crc32 crc = new Crc32();
+ crc.update(magic);
+ crc.update(attributes);
+ if (magic > 0)
+ crc.updateLong(timestamp);
+ // update for the key
+ if (key == null) {
+ crc.updateInt(-1);
+ } else {
+ int size = key.remaining();
+ crc.updateInt(size);
+ crc.update(key.array(), key.arrayOffset(), size);
+ }
+ // update for the value
+ if (value == null) {
+ crc.updateInt(-1);
+ } else {
+ int size = value.remaining();
+ crc.updateInt(size);
+ crc.update(value.array(), value.arrayOffset(), size);
+ }
+ return crc.getValue();
+ }
+
+ public static int recordOverhead(byte magic) {
+ if (magic == 0)
+ return RECORD_OVERHEAD_V0;
+ return RECORD_OVERHEAD_V1;
+ }
+
+ private static int keyOffset(byte magic) {
+ if (magic == 0)
+ return KEY_OFFSET_V0;
+ return KEY_OFFSET_V1;
+ }
+
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/Records.java b/clients/src/main/java/org/apache/kafka/common/record/Records.java
index 3bc043f348125..823d2b7d1ff92 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/Records.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/Records.java
@@ -18,32 +18,74 @@
import java.io.IOException;
import java.nio.channels.GatheringByteChannel;
+import java.util.Iterator;
/**
- * A binary format which consists of a 4 byte size, an 8 byte offset, and the record bytes. See {@link MemoryRecords}
- * for the in-memory representation.
+ * Interface for accessing the records contained in a log. The log itself is represented as a sequence of log entries.
+ * Each log entry consists of an 8 byte offset, a 4 byte record size, and a "shallow" {@link Record record}.
+ * If the entry is not compressed, then each entry will have only the shallow record contained inside it. If it is
+ * compressed, the entry contains "deep" records, which are packed into the value field of the shallow record. To iterate
+ * over the shallow records, use {@link #shallowIterator()}; for the deep records, use {@link #deepIterator()}. Note
+ * that the deep iterator handles both compressed and non-compressed entries: if the entry is not compressed, the
+ * shallow record is returned; otherwise, the shallow record is decompressed and the deep entries are returned.
+ * See {@link MemoryRecords} for the in-memory representation and {@link FileRecords} for the on-disk representation.
*/
-public interface Records extends Iterable {
+public interface Records {
- int SIZE_LENGTH = 4;
+ int OFFSET_OFFSET = 0;
int OFFSET_LENGTH = 8;
- int LOG_OVERHEAD = SIZE_LENGTH + OFFSET_LENGTH;
+ int SIZE_OFFSET = OFFSET_OFFSET + OFFSET_LENGTH;
+ int SIZE_LENGTH = 4;
+ int LOG_OVERHEAD = SIZE_OFFSET + SIZE_LENGTH;
/**
- * The size of these records in bytes
- * @return The size in bytes
+ * The size of these records in bytes.
+ * @return The size in bytes of the records
*/
int sizeInBytes();
/**
- * Write the messages in this set to the given channel starting at the given offset byte.
+ * Write the contents of this buffer to a channel.
* @param channel The channel to write to
- * @param position The position within this record set to begin writing from
+ * @param position The position in the buffer to write from
* @param length The number of bytes to write
- * @return The number of bytes written to the channel (which may be fewer than requested)
- * @throws IOException For any IO errors copying the
+ * @return The number of bytes written
+ * @throws IOException For any IO errors
*/
long writeTo(GatheringByteChannel channel, long position, int length) throws IOException;
+ /**
+ * Get the shallow log entries in this log buffer. Note that the signature allows subclasses
+ * to return a more specific log entry type. This enables optimizations such as in-place offset
+ * assignment (see {@link ByteBufferLogInputStream.ByteBufferLogEntry}), and partial reading of
+ * record data (see {@link FileLogInputStream.FileChannelLogEntry#magic()}.
+ * @return An iterator over the shallow entries of the log
+ */
+ Iterator extends LogEntry> shallowIterator();
+
+ /**
+ * Get the deep log entries (i.e. descend into compressed message sets). For the deep records,
+ * there are fewer options for optimization since the data must be decompressed before it can be
+ * returned. Hence there is little advantage in allowing subclasses to return a more specific type
+ * as we do for {@link #shallowIterator()}.
+ * @return An iterator over the deep entries of the log
+ */
+ Iterator deepIterator();
+
+ /**
+ * Check whether all shallow entries in this buffer have a certain magic value.
+ * @param magic The magic value to check
+ * @return true if all shallow entries have a matching magic value, false otherwise
+ */
+ boolean hasMatchingShallowMagic(byte magic);
+
+
+ /**
+ * Convert all entries in this buffer to the format passed as a parameter. Note that this requires
+ * deep iteration since all of the deep records must also be converted to the desired format.
+ * @param toMagic The magic value to convert to
+ * @return A Records (which may or may not be the same instance)
+ */
+ Records toMessageFormat(byte toMagic);
}
diff --git a/clients/src/main/java/org/apache/kafka/common/record/RecordsIterator.java b/clients/src/main/java/org/apache/kafka/common/record/RecordsIterator.java
index 1bc8a65684afc..4a678d569ef11 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/RecordsIterator.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/RecordsIterator.java
@@ -17,6 +17,7 @@
package org.apache.kafka.common.record;
import org.apache.kafka.common.KafkaException;
+import org.apache.kafka.common.errors.CorruptRecordException;
import org.apache.kafka.common.utils.AbstractIterator;
import org.apache.kafka.common.utils.Utils;
@@ -25,51 +26,58 @@
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
+import java.util.Iterator;
+/**
+ * An iterator which handles both the shallow and deep iteration of record sets.
+ */
public class RecordsIterator extends AbstractIterator {
- private final LogInputStream logStream;
private final boolean shallow;
+ private final boolean ensureMatchingMagic;
+ private final int masRecordSize;
+ private final ShallowRecordsIterator> shallowIter;
private DeepRecordsIterator innerIter;
- public RecordsIterator(LogInputStream logStream, boolean shallow) {
- this.logStream = logStream;
+ public RecordsIterator(LogInputStream> logInputStream,
+ boolean shallow,
+ boolean ensureMatchingMagic,
+ int masRecordSize) {
+ this.shallowIter = new ShallowRecordsIterator<>(logInputStream);
this.shallow = shallow;
+ this.ensureMatchingMagic = ensureMatchingMagic;
+ this.masRecordSize = masRecordSize;
}
- /*
- * Read the next record from the buffer.
- *
- * Note that in the compressed message set, each message value size is set as the size of the un-compressed
- * version of the message value, so when we do de-compression allocating an array of the specified size for
- * reading compressed value data is sufficient.
+ /**
+ * Get a shallow iterator over the given input stream.
+ * @param logInputStream The log input stream to read the entries from
+ * @param The type of the log entry
+ * @return The shallow iterator.
*/
+ public static Iterator shallowIterator(LogInputStream logInputStream) {
+ return new ShallowRecordsIterator<>(logInputStream);
+ }
+
@Override
protected LogEntry makeNext() {
if (innerDone()) {
- try {
- LogEntry entry = logStream.nextEntry();
- // No more record to return.
- if (entry == null)
- return allDone();
-
- // decide whether to go shallow or deep iteration if it is compressed
- CompressionType compressionType = entry.record().compressionType();
- if (compressionType == CompressionType.NONE || shallow) {
- return entry;
- } else {
- // init the inner iterator with the value payload of the message,
- // which will de-compress the payload to a set of messages;
- // since we assume nested compression is not allowed, the deep iterator
- // would not try to further decompress underlying messages
- // There will be at least one element in the inner iterator, so we don't
- // need to call hasNext() here.
- innerIter = new DeepRecordsIterator(entry);
- return innerIter.next();
- }
- } catch (EOFException e) {
+ if (!shallowIter.hasNext())
return allDone();
- } catch (IOException e) {
- throw new KafkaException(e);
+
+ LogEntry entry = shallowIter.next();
+
+ // decide whether to go shallow or deep iteration if it is compressed
+ if (shallow || !entry.isCompressed()) {
+ return entry;
+ } else {
+ // init the inner iterator with the value payload of the message,
+ // which will de-compress the payload to a set of messages;
+ // since we assume nested compression is not allowed, the deep iterator
+ // would not try to further decompress underlying messages
+ // There will be at least one element in the inner iterator, so we don't
+ // need to call hasNext() here.
+ innerIter = new DeepRecordsIterator(entry, ensureMatchingMagic, masRecordSize);
+ return innerIter.next();
}
} else {
return innerIter.next();
@@ -80,38 +88,70 @@ private boolean innerDone() {
return innerIter == null || !innerIter.hasNext();
}
- private static class DataLogInputStream implements LogInputStream {
+ private static class DataLogInputStream implements LogInputStream {
private final DataInputStream stream;
+ protected final int maxMessageSize;
- private DataLogInputStream(DataInputStream stream) {
+ DataLogInputStream(DataInputStream stream, int maxMessageSize) {
this.stream = stream;
+ this.maxMessageSize = maxMessageSize;
}
public LogEntry nextEntry() throws IOException {
- long offset = stream.readLong();
- int size = stream.readInt();
- if (size < 0)
- throw new IllegalStateException("Record with size " + size);
-
- byte[] recordBuffer = new byte[size];
- stream.readFully(recordBuffer, 0, size);
- ByteBuffer buf = ByteBuffer.wrap(recordBuffer);
- return new LogEntry(offset, new Record(buf));
+ try {
+ long offset = stream.readLong();
+ int size = stream.readInt();
+ if (size < Record.RECORD_OVERHEAD_V0)
+ throw new CorruptRecordException(String.format("Record size is less than the minimum record overhead (%d)", Record.RECORD_OVERHEAD_V0));
+ if (size > maxMessageSize)
+ throw new CorruptRecordException(String.format("Record size exceeds the largest allowable message size (%d).", maxMessageSize));
+
+ byte[] recordBuffer = new byte[size];
+ stream.readFully(recordBuffer, 0, size);
+ ByteBuffer buf = ByteBuffer.wrap(recordBuffer);
+ return LogEntry.create(offset, new Record(buf));
+ } catch (EOFException e) {
+ return null;
+ }
}
}
- private static class DeepRecordsIterator extends AbstractIterator {
+ private static class ShallowRecordsIterator extends AbstractIterator {
+ private final LogInputStream logStream;
+
+ public ShallowRecordsIterator(LogInputStream logStream) {
+ this.logStream = logStream;
+ }
+
+ @Override
+ protected T makeNext() {
+ try {
+ T entry = logStream.nextEntry();
+ if (entry == null)
+ return allDone();
+ return entry;
+ } catch (IOException e) {
+ throw new KafkaException(e);
+ }
+ }
+ }
+
+ public static class DeepRecordsIterator extends AbstractIterator {
private final ArrayDeque logEntries;
private final long absoluteBaseOffset;
+ private final byte wrapperMagic;
+
+ public DeepRecordsIterator(LogEntry wrapperEntry, boolean ensureMatchingMagic, int maxMessageSize) {
+ Record wrapperRecord = wrapperEntry.record();
+ this.wrapperMagic = wrapperRecord.magic();
- private DeepRecordsIterator(LogEntry entry) {
- CompressionType compressionType = entry.record().compressionType();
- ByteBuffer buffer = entry.record().value();
- DataInputStream stream = Compressor.wrapForInput(new ByteBufferInputStream(buffer), compressionType, entry.record().magic());
- LogInputStream logStream = new DataLogInputStream(stream);
+ CompressionType compressionType = wrapperRecord.compressionType();
+ ByteBuffer buffer = wrapperRecord.value();
+ DataInputStream stream = MemoryRecordsBuilder.wrapForInput(new ByteBufferInputStream(buffer), compressionType, wrapperRecord.magic());
+ LogInputStream logStream = new DataLogInputStream(stream, maxMessageSize);
- long wrapperRecordOffset = entry.offset();
- long wrapperRecordTimestamp = entry.record().timestamp();
+ long wrapperRecordOffset = wrapperEntry.offset();
+ long wrapperRecordTimestamp = wrapperRecord.timestamp();
this.logEntries = new ArrayDeque<>();
// If relative offset is used, we need to decompress the entire message first to compute
@@ -119,22 +159,27 @@ private DeepRecordsIterator(LogEntry entry) {
// do the same for message format version 0
try {
while (true) {
- try {
- LogEntry logEntry = logStream.nextEntry();
- if (entry.record().magic() > Record.MAGIC_VALUE_V0) {
- Record recordWithTimestamp = new Record(
- logEntry.record().buffer(),
- wrapperRecordTimestamp,
- entry.record().timestampType()
- );
- logEntry = new LogEntry(logEntry.offset(), recordWithTimestamp);
- }
- logEntries.add(logEntry);
- } catch (EOFException e) {
+ LogEntry logEntry = logStream.nextEntry();
+ if (logEntry == null)
break;
+
+ Record record = logEntry.record();
+ byte magic = record.magic();
+
+ if (ensureMatchingMagic && magic != wrapperMagic)
+ throw new InvalidRecordException("Compressed message magic does not match wrapper magic");
+
+ if (magic > Record.MAGIC_VALUE_V0) {
+ Record recordWithTimestamp = new Record(
+ record.buffer(),
+ wrapperRecordTimestamp,
+ wrapperRecord.timestampType()
+ );
+ logEntry = LogEntry.create(logEntry.offset(), recordWithTimestamp);
}
+ logEntries.addLast(logEntry);
}
- if (entry.record().magic() > Record.MAGIC_VALUE_V0)
+ if (wrapperMagic > Record.MAGIC_VALUE_V0)
this.absoluteBaseOffset = wrapperRecordOffset - logEntries.getLast().offset();
else
this.absoluteBaseOffset = -1;
@@ -155,12 +200,10 @@ protected LogEntry makeNext() {
// Convert offset to absolute offset if needed.
if (absoluteBaseOffset >= 0) {
long absoluteOffset = absoluteBaseOffset + entry.offset();
- entry = new LogEntry(absoluteOffset, entry.record());
+ entry = LogEntry.create(absoluteOffset, entry.record());
}
- // decide whether to go shallow or deep iteration if it is compressed
- CompressionType compression = entry.record().compressionType();
- if (compression != CompressionType.NONE)
+ if (entry.isCompressed())
throw new InvalidRecordException("Inner messages must not be compressed");
return entry;
diff --git a/clients/src/main/java/org/apache/kafka/common/record/TimestampType.java b/clients/src/main/java/org/apache/kafka/common/record/TimestampType.java
index 62fd8144d371f..55c966ac731fb 100644
--- a/clients/src/main/java/org/apache/kafka/common/record/TimestampType.java
+++ b/clients/src/main/java/org/apache/kafka/common/record/TimestampType.java
@@ -27,6 +27,7 @@ public enum TimestampType {
public final int id;
public final String name;
+
TimestampType(int id, String name) {
this.id = id;
this.name = name;
diff --git a/clients/src/main/java/org/apache/kafka/common/utils/Utils.java b/clients/src/main/java/org/apache/kafka/common/utils/Utils.java
index c3c1045e46b2d..c5e67162dd84d 100755
--- a/clients/src/main/java/org/apache/kafka/common/utils/Utils.java
+++ b/clients/src/main/java/org/apache/kafka/common/utils/Utils.java
@@ -265,6 +265,24 @@ public static byte[] toArray(ByteBuffer buffer) {
return toArray(buffer, 0, buffer.limit());
}
+ /**
+ * Convert a ByteBuffer to a nullable array.
+ * @param buffer The buffer to convert
+ * @return The resulting array or null if the buffer is null
+ */
+ public static byte[] toNullableArray(ByteBuffer buffer) {
+ return buffer == null ? null : toArray(buffer);
+ }
+
+ /**
+ * Wrap an array as a nullable ByteBuffer.
+ * @param array The nullable array to wrap
+ * @return The wrapping ByteBuffer or null if array is null
+ */
+ public static ByteBuffer wrapNullable(byte[] array) {
+ return array == null ? null : ByteBuffer.wrap(array);
+ }
+
/**
* Read a byte array from the given offset and size in the buffer
*/
@@ -733,4 +751,37 @@ public static int toPositive(int number) {
public static int longHashcode(long value) {
return (int) (value ^ (value >>> 32));
}
+
+ /**
+ * Read a size-delimited byte buffer starting at the given offset.
+ * @param buffer Buffer containing the size and data
+ * @param start Offset in the buffer to read from
+ * @return A slice of the buffer containing only the delimited data (excluding the size)
+ */
+ public static ByteBuffer sizeDelimited(ByteBuffer buffer, int start) {
+ int size = buffer.getInt(start);
+ if (size < 0) {
+ return null;
+ } else {
+ ByteBuffer b = buffer.duplicate();
+ b.position(start + 4);
+ b = b.slice();
+ b.limit(size);
+ b.rewind();
+ return b;
+ }
+ }
+
+ /**
+ * Compute the checksum of a range of data
+ * @param buffer Buffer containing the data to checksum
+ * @param start Offset in the buffer to read from
+ * @param size The number of bytes to include
+ */
+ public static long computeChecksum(ByteBuffer buffer, int start, int size) {
+ Crc32 crc = new Crc32();
+ crc.update(buffer.array(), buffer.arrayOffset() + start, size);
+ return crc.getValue();
+ }
+
}
diff --git a/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaConsumerTest.java b/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaConsumerTest.java
index ad6c127d71af4..a4386f86e01ed 100644
--- a/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaConsumerTest.java
+++ b/clients/src/test/java/org/apache/kafka/clients/consumer/KafkaConsumerTest.java
@@ -39,6 +39,8 @@
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.record.CompressionType;
import org.apache.kafka.common.record.MemoryRecords;
+import org.apache.kafka.common.record.MemoryRecordsBuilder;
+import org.apache.kafka.common.record.TimestampType;
import org.apache.kafka.common.requests.AbstractRequest;
import org.apache.kafka.common.requests.FetchResponse;
import org.apache.kafka.common.requests.FetchResponse.PartitionData;
@@ -1323,11 +1325,10 @@ private FetchResponse fetchResponse(Map fetches) {
TopicPartition partition = fetchEntry.getKey();
long fetchOffset = fetchEntry.getValue().offset;
int fetchCount = fetchEntry.getValue().count;
- MemoryRecords records = MemoryRecords.emptyRecords(ByteBuffer.allocate(1024), CompressionType.NONE);
+ MemoryRecordsBuilder records = MemoryRecords.builder(ByteBuffer.allocate(1024), CompressionType.NONE, TimestampType.CREATE_TIME);
for (int i = 0; i < fetchCount; i++)
records.append(fetchOffset + i, 0L, ("key-" + i).getBytes(), ("value-" + i).getBytes());
- records.close();
- tpResponses.put(partition, new FetchResponse.PartitionData(Errors.NONE.code(), 0, records));
+ tpResponses.put(partition, new FetchResponse.PartitionData(Errors.NONE.code(), 0, records.build()));
}
return new FetchResponse(tpResponses, 0);
}
diff --git a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java
index 6d5896f7a334e..15075cb31dacf 100644
--- a/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java
+++ b/clients/src/test/java/org/apache/kafka/clients/consumer/internals/FetcherTest.java
@@ -37,10 +37,12 @@
import org.apache.kafka.common.metrics.KafkaMetric;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.protocol.Errors;
+import org.apache.kafka.common.record.ByteBufferOutputStream;
import org.apache.kafka.common.record.CompressionType;
-import org.apache.kafka.common.record.Compressor;
import org.apache.kafka.common.record.MemoryRecords;
+import org.apache.kafka.common.record.MemoryRecordsBuilder;
import org.apache.kafka.common.record.Record;
+import org.apache.kafka.common.record.TimestampType;
import org.apache.kafka.common.requests.AbstractRequest;
import org.apache.kafka.common.requests.FetchRequest;
import org.apache.kafka.common.requests.FetchResponse;
@@ -93,8 +95,8 @@ public class FetcherTest {
private static final double EPSILON = 0.0001;
private ConsumerNetworkClient consumerClient = new ConsumerNetworkClient(client, metadata, time, 100, 1000);
- private MemoryRecords records = MemoryRecords.emptyRecords(ByteBuffer.allocate(1024), CompressionType.NONE);
- private MemoryRecords nextRecords = MemoryRecords.emptyRecords(ByteBuffer.allocate(1024), CompressionType.NONE);
+ private MemoryRecords records;
+ private MemoryRecords nextRecords;
private Fetcher fetcher = createFetcher(subscriptions, metrics);
private Metrics fetcherMetrics = new Metrics(time);
private Fetcher fetcherNoAutoReset = createFetcher(subscriptionsNoAutoReset, fetcherMetrics);
@@ -104,14 +106,16 @@ public void setup() throws Exception {
metadata.update(cluster, time.milliseconds());
client.setNode(node);
- records.append(1L, 0L, "key".getBytes(), "value-1".getBytes());
- records.append(2L, 0L, "key".getBytes(), "value-2".getBytes());
- records.append(3L, 0L, "key".getBytes(), "value-3".getBytes());
- records.close();
+ MemoryRecordsBuilder builder = MemoryRecords.builder(ByteBuffer.allocate(1024), CompressionType.NONE, TimestampType.CREATE_TIME);
+ builder.append(1L, 0L, "key".getBytes(), "value-1".getBytes());
+ builder.append(2L, 0L, "key".getBytes(), "value-2".getBytes());
+ builder.append(3L, 0L, "key".getBytes(), "value-3".getBytes());
+ records = builder.build();
- nextRecords.append(4L, 0L, "key".getBytes(), "value-4".getBytes());
- nextRecords.append(5L, 0L, "key".getBytes(), "value-5".getBytes());
- nextRecords.close();
+ builder = MemoryRecords.builder(ByteBuffer.allocate(1024), CompressionType.NONE, TimestampType.CREATE_TIME);
+ builder.append(4L, 0L, "key".getBytes(), "value-4".getBytes());
+ builder.append(5L, 0L, "key".getBytes(), "value-5".getBytes());
+ nextRecords = builder.build();
}
@After
@@ -129,7 +133,7 @@ public void testFetchNormal() {
assertEquals(1, fetcher.sendFetches());
assertFalse(fetcher.hasCompletedFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.NONE.code(), 100L, 0));
consumerClient.poll(0);
assertTrue(fetcher.hasCompletedFetches());
@@ -154,7 +158,7 @@ public void testFetchError() {
assertEquals(1, fetcher.sendFetches());
assertFalse(fetcher.hasCompletedFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NOT_LEADER_FOR_PARTITION.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.NOT_LEADER_FOR_PARTITION.code(), 100L, 0));
consumerClient.poll(0);
assertTrue(fetcher.hasCompletedFetches());
@@ -192,7 +196,7 @@ public byte[] deserialize(String topic, byte[] data) {
subscriptions.assignFromUser(singleton(tp));
subscriptions.seek(tp, 1);
- client.prepareResponse(matchesOffset(tp, 1), fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(matchesOffset(tp, 1), fetchResponse(this.records, Errors.NONE.code(), 100L, 0));
assertEquals(1, fetcher.sendFetches());
consumerClient.poll(0);
@@ -206,29 +210,30 @@ public byte[] deserialize(String topic, byte[] data) {
}
@Test
- public void testParseInvalidRecord() {
+ public void testParseInvalidRecord() throws Exception {
ByteBuffer buffer = ByteBuffer.allocate(1024);
- Compressor compressor = new Compressor(buffer, CompressionType.NONE);
+ ByteBufferOutputStream out = new ByteBufferOutputStream(buffer);
+ byte magic = Record.CURRENT_MAGIC_VALUE;
byte[] key = "foo".getBytes();
byte[] value = "baz".getBytes();
long offset = 0;
long timestamp = 500L;
int size = Record.recordSize(key, value);
- long crc = Record.computeChecksum(timestamp, key, value, CompressionType.NONE, 0, -1);
+ byte attributes = Record.computeAttributes(magic, CompressionType.NONE, TimestampType.CREATE_TIME);
+ long crc = Record.computeChecksum(magic, attributes, timestamp, key, value);
// write one valid record
- compressor.putLong(offset);
- compressor.putInt(size);
- Record.write(compressor, crc, Record.computeAttributes(CompressionType.NONE), timestamp, key, value, 0, -1);
+ out.writeLong(offset);
+ out.writeInt(size);
+ Record.write(out, magic, crc, Record.computeAttributes(magic, CompressionType.NONE, TimestampType.CREATE_TIME), timestamp, key, value);
// and one invalid record (note the crc)
- compressor.putLong(offset);
- compressor.putInt(size);
- Record.write(compressor, crc + 1, Record.computeAttributes(CompressionType.NONE), timestamp, key, value, 0, -1);
+ out.writeLong(offset);
+ out.writeInt(size);
+ Record.write(out, magic, crc + 1, Record.computeAttributes(magic, CompressionType.NONE, TimestampType.CREATE_TIME), timestamp, key, value);
- compressor.close();
buffer.flip();
subscriptions.assignFromUser(singleton(tp));
@@ -236,7 +241,7 @@ public void testParseInvalidRecord() {
// normal fetch
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(buffer, Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(MemoryRecords.readableRecords(buffer), Errors.NONE.code(), 100L, 0));
consumerClient.poll(0);
try {
fetcher.fetchedRecords();
@@ -255,8 +260,8 @@ public void testFetchMaxPollRecords() {
subscriptions.assignFromUser(singleton(tp));
subscriptions.seek(tp, 1);
- client.prepareResponse(matchesOffset(tp, 1), fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 0));
- client.prepareResponse(matchesOffset(tp, 4), fetchResponse(this.nextRecords.buffer(), Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(matchesOffset(tp, 1), fetchResponse(this.records, Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(matchesOffset(tp, 4), fetchResponse(this.nextRecords, Errors.NONE.code(), 100L, 0));
assertEquals(1, fetcher.sendFetches());
consumerClient.poll(0);
@@ -287,11 +292,11 @@ public void testFetchNonContinuousRecords() {
// if we are fetching from a compacted topic, there may be gaps in the returned records
// this test verifies the fetcher updates the current fetched/consumed positions correctly for this case
- MemoryRecords records = MemoryRecords.emptyRecords(ByteBuffer.allocate(1024), CompressionType.NONE);
- records.append(15L, 0L, "key".getBytes(), "value-1".getBytes());
- records.append(20L, 0L, "key".getBytes(), "value-2".getBytes());
- records.append(30L, 0L, "key".getBytes(), "value-3".getBytes());
- records.close();
+ MemoryRecordsBuilder builder = MemoryRecords.builder(ByteBuffer.allocate(1024), CompressionType.NONE, TimestampType.CREATE_TIME);
+ builder.append(15L, 0L, "key".getBytes(), "value-1".getBytes());
+ builder.append(20L, 0L, "key".getBytes(), "value-2".getBytes());
+ builder.append(30L, 0L, "key".getBytes(), "value-3".getBytes());
+ MemoryRecords records = builder.build();
List> consumerRecords;
subscriptions.assignFromUser(singleton(tp));
@@ -299,7 +304,7 @@ public void testFetchNonContinuousRecords() {
// normal fetch
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(records.buffer(), Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(records, Errors.NONE.code(), 100L, 0));
consumerClient.poll(0);
consumerRecords = fetcher.fetchedRecords().get(tp);
assertEquals(3, consumerRecords.size());
@@ -317,7 +322,7 @@ public void testUnauthorizedTopic() {
// resize the limit of the buffer to pretend it is only fetch-size large
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.TOPIC_AUTHORIZATION_FAILED.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.TOPIC_AUTHORIZATION_FAILED.code(), 100L, 0));
consumerClient.poll(0);
try {
fetcher.fetchedRecords();
@@ -337,7 +342,7 @@ public void testFetchDuringRebalance() {
// Now the rebalance happens and fetch positions are cleared
subscriptions.assignFromSubscribed(singleton(tp));
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.NONE.code(), 100L, 0));
consumerClient.poll(0);
// The active fetch should be ignored since its position is no longer valid
@@ -352,7 +357,7 @@ public void testInFlightFetchOnPausedPartition() {
assertEquals(1, fetcher.sendFetches());
subscriptions.pause(tp);
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.NONE.code(), 100L, 0));
consumerClient.poll(0);
assertNull(fetcher.fetchedRecords().get(tp));
}
@@ -373,7 +378,7 @@ public void testFetchNotLeaderForPartition() {
subscriptions.seek(tp, 0);
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NOT_LEADER_FOR_PARTITION.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.NOT_LEADER_FOR_PARTITION.code(), 100L, 0));
consumerClient.poll(0);
assertEquals(0, fetcher.fetchedRecords().size());
assertEquals(0L, metadata.timeToNextUpdate(time.milliseconds()));
@@ -385,7 +390,7 @@ public void testFetchUnknownTopicOrPartition() {
subscriptions.seek(tp, 0);
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.UNKNOWN_TOPIC_OR_PARTITION.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.UNKNOWN_TOPIC_OR_PARTITION.code(), 100L, 0));
consumerClient.poll(0);
assertEquals(0, fetcher.fetchedRecords().size());
assertEquals(0L, metadata.timeToNextUpdate(time.milliseconds()));
@@ -397,7 +402,7 @@ public void testFetchOffsetOutOfRange() {
subscriptions.seek(tp, 0);
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
consumerClient.poll(0);
assertEquals(0, fetcher.fetchedRecords().size());
assertTrue(subscriptions.isOffsetResetNeeded(tp));
@@ -412,7 +417,7 @@ public void testStaleOutOfRangeError() {
subscriptions.seek(tp, 0);
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
subscriptions.seek(tp, 1);
consumerClient.poll(0);
assertEquals(0, fetcher.fetchedRecords().size());
@@ -426,7 +431,7 @@ public void testFetchedRecordsAfterSeek() {
subscriptionsNoAutoReset.seek(tp, 0);
assertTrue(fetcherNoAutoReset.sendFetches() > 0);
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
consumerClient.poll(0);
assertFalse(subscriptionsNoAutoReset.isOffsetResetNeeded(tp));
subscriptionsNoAutoReset.seek(tp, 2);
@@ -439,7 +444,7 @@ public void testFetchOffsetOutOfRangeException() {
subscriptionsNoAutoReset.seek(tp, 0);
fetcherNoAutoReset.sendFetches();
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
+ client.prepareResponse(fetchResponse(this.records, Errors.OFFSET_OUT_OF_RANGE.code(), 100L, 0));
consumerClient.poll(0);
assertFalse(subscriptionsNoAutoReset.isOffsetResetNeeded(tp));
@@ -459,7 +464,7 @@ public void testFetchDisconnected() {
subscriptions.seek(tp, 0);
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 0), true);
+ client.prepareResponse(fetchResponse(this.records, Errors.NONE.code(), 100L, 0), true);
consumerClient.poll(0);
assertEquals(0, fetcher.fetchedRecords().size());
@@ -611,14 +616,14 @@ public void testQuotaMetrics() throws Exception {
// We need to make sure the message offset grows. Otherwise they will be considered as already consumed
// and filtered out by consumer.
if (i > 1) {
- this.records = MemoryRecords.emptyRecords(ByteBuffer.allocate(1024), CompressionType.NONE);
+ MemoryRecordsBuilder builder = MemoryRecords.builder(ByteBuffer.allocate(1024), CompressionType.NONE, TimestampType.CREATE_TIME);
for (int v = 0; v < 3; v++) {
- this.records.append((long) i * 3 + v, Record.NO_TIMESTAMP, "key".getBytes(), String.format("value-%d", v).getBytes());
+ builder.append((long) i * 3 + v, Record.NO_TIMESTAMP, "key".getBytes(), String.format("value-%d", v).getBytes());
}
- this.records.close();
+ this.records = builder.build();
}
assertEquals(1, fetcher.sendFetches());
- client.prepareResponse(fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L, 100 * i));
+ client.prepareResponse(fetchResponse(this.records, Errors.NONE.code(), 100L, 100 * i));
consumerClient.poll(0);
records = fetcher.fetchedRecords().get(tp);
assertEquals(3, records.size());
@@ -722,8 +727,7 @@ private ListOffsetResponse listOffsetResponse(TopicPartition tp, Errors error, l
return new ListOffsetResponse(allPartitionData, 1);
}
- private FetchResponse fetchResponse(ByteBuffer buffer, short error, long hw, int throttleTime) {
- MemoryRecords records = MemoryRecords.readableRecords(buffer);
+ private FetchResponse fetchResponse(MemoryRecords records, short error, long hw, int throttleTime) {
return new FetchResponse(Collections.singletonMap(tp, new FetchResponse.PartitionData(error, hw, records)), throttleTime);
}
diff --git a/clients/src/test/java/org/apache/kafka/clients/producer/internals/RecordAccumulatorTest.java b/clients/src/test/java/org/apache/kafka/clients/producer/internals/RecordAccumulatorTest.java
index 28521e85e3a5f..4f25bdffab121 100644
--- a/clients/src/test/java/org/apache/kafka/clients/producer/internals/RecordAccumulatorTest.java
+++ b/clients/src/test/java/org/apache/kafka/clients/producer/internals/RecordAccumulatorTest.java
@@ -12,23 +12,6 @@
*/
package org.apache.kafka.clients.producer.internals;
-import static java.util.Arrays.asList;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.nio.ByteBuffer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-
import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.RecordMetadata;
import org.apache.kafka.common.Cluster;
@@ -45,6 +28,23 @@
import org.junit.After;
import org.junit.Test;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static java.util.Arrays.asList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
public class RecordAccumulatorTest {
private String topic = "test";
@@ -84,7 +84,7 @@ public void testFull() throws Exception {
accum.append(tp1, 0L, key, value, null, maxBlockTimeMs);
Deque partitionBatches = accum.batches().get(tp1);
assertEquals(1, partitionBatches.size());
- assertTrue(partitionBatches.peekFirst().records.isWritable());
+ assertTrue(partitionBatches.peekFirst().isWritable());
assertEquals("No partitions should be ready.", 0, accum.ready(cluster, now).readyNodes.size());
}
@@ -93,15 +93,15 @@ public void testFull() throws Exception {
Deque partitionBatches = accum.batches().get(tp1);
assertEquals(2, partitionBatches.size());
Iterator partitionBatchesIterator = partitionBatches.iterator();
- assertFalse(partitionBatchesIterator.next().records.isWritable());
- assertTrue(partitionBatchesIterator.next().records.isWritable());
+ assertFalse(partitionBatchesIterator.next().isWritable());
+ assertTrue(partitionBatchesIterator.next().isWritable());
assertEquals("Our partition's leader should be ready", Collections.singleton(node1), accum.ready(cluster, time.milliseconds()).readyNodes);
List batches = accum.drain(cluster, Collections.singleton(node1), Integer.MAX_VALUE, 0).get(node1.id());
assertEquals(1, batches.size());
RecordBatch batch = batches.get(0);
- Iterator iter = batch.records.iterator();
+ Iterator iter = batch.records().deepIterator();
for (int i = 0; i < appends; i++) {
LogEntry entry = iter.next();
assertEquals("Keys should match", ByteBuffer.wrap(key), entry.record().key());
@@ -130,7 +130,7 @@ public void testLinger() throws Exception {
assertEquals(1, batches.size());
RecordBatch batch = batches.get(0);
- Iterator iter = batch.records.iterator();
+ Iterator iter = batch.records().deepIterator();
LogEntry entry = iter.next();
assertEquals("Keys should match", ByteBuffer.wrap(key), entry.record().key());
assertEquals("Values should match", ByteBuffer.wrap(value), entry.record().value());
@@ -159,7 +159,7 @@ public void testStressfulSituation() throws Exception {
final int msgs = 10000;
final int numParts = 2;
final RecordAccumulator accum = new RecordAccumulator(1024, 10 * 1024, CompressionType.NONE, 0L, 100L, metrics, time);
- List threads = new ArrayList();
+ List threads = new ArrayList<>();
for (int i = 0; i < numThreads; i++) {
threads.add(new Thread() {
public void run() {
@@ -182,8 +182,11 @@ public void run() {
List batches = accum.drain(cluster, nodes, 5 * 1024, 0).get(node1.id());
if (batches != null) {
for (RecordBatch batch : batches) {
- for (LogEntry entry : batch.records)
+ Iterator deepEntries = batch.records().deepIterator();
+ while (deepEntries.hasNext()) {
+ deepEntries.next();
read++;
+ }
accum.deallocate(batch);
}
}
diff --git a/clients/src/test/java/org/apache/kafka/common/record/ByteBufferLogInputStreamTest.java b/clients/src/test/java/org/apache/kafka/common/record/ByteBufferLogInputStreamTest.java
new file mode 100644
index 0000000000000..62e8a05b691a9
--- /dev/null
+++ b/clients/src/test/java/org/apache/kafka/common/record/ByteBufferLogInputStreamTest.java
@@ -0,0 +1,110 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.Iterator;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ByteBufferLogInputStreamTest {
+
+ @Test
+ public void iteratorIgnoresIncompleteEntries() {
+ ByteBuffer buffer = ByteBuffer.allocate(2048);
+ MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, Record.MAGIC_VALUE_V1, CompressionType.NONE, TimestampType.CREATE_TIME, 0L);
+ builder.append(0L, 15L, "a".getBytes(), "1".getBytes());
+ builder.append(1L, 20L, "b".getBytes(), "2".getBytes());
+
+ ByteBuffer recordsBuffer = builder.build().buffer();
+ recordsBuffer.limit(recordsBuffer.limit() - 5);
+
+ Iterator iterator = MemoryRecords.readableRecords(recordsBuffer).shallowIterator();
+ assertTrue(iterator.hasNext());
+ ByteBufferLogInputStream.ByteBufferLogEntry first = iterator.next();
+ assertEquals(0L, first.offset());
+
+ assertFalse(iterator.hasNext());
+ }
+
+ @Test
+ public void testSetCreateTimeV1() {
+ ByteBuffer buffer = ByteBuffer.allocate(2048);
+ MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, Record.MAGIC_VALUE_V1, CompressionType.NONE, TimestampType.CREATE_TIME, 0L);
+ builder.append(0L, 15L, "a".getBytes(), "1".getBytes());
+ Iterator iterator = builder.build().shallowIterator();
+
+ assertTrue(iterator.hasNext());
+ ByteBufferLogInputStream.ByteBufferLogEntry entry = iterator.next();
+
+ long createTimeMs = 20L;
+ entry.setCreateTime(createTimeMs);
+
+ assertEquals(TimestampType.CREATE_TIME, entry.record().timestampType());
+ assertEquals(createTimeMs, entry.record().timestamp());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetCreateTimeNotAllowedV0() {
+ ByteBuffer buffer = ByteBuffer.allocate(2048);
+ MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, Record.MAGIC_VALUE_V0, CompressionType.NONE, TimestampType.CREATE_TIME, 0L);
+ builder.append(0L, 15L, "a".getBytes(), "1".getBytes());
+ Iterator iterator = builder.build().shallowIterator();
+
+ assertTrue(iterator.hasNext());
+ ByteBufferLogInputStream.ByteBufferLogEntry entry = iterator.next();
+
+ long createTimeMs = 20L;
+ entry.setCreateTime(createTimeMs);
+ }
+
+ @Test
+ public void testSetLogAppendTimeV1() {
+ ByteBuffer buffer = ByteBuffer.allocate(2048);
+ MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, Record.MAGIC_VALUE_V1, CompressionType.NONE, TimestampType.CREATE_TIME, 0L);
+ builder.append(0L, 15L, "a".getBytes(), "1".getBytes());
+ Iterator iterator = builder.build().shallowIterator();
+
+ assertTrue(iterator.hasNext());
+ ByteBufferLogInputStream.ByteBufferLogEntry entry = iterator.next();
+
+ long logAppendTime = 20L;
+ entry.setLogAppendTime(logAppendTime);
+
+ assertEquals(TimestampType.LOG_APPEND_TIME, entry.record().timestampType());
+ assertEquals(logAppendTime, entry.record().timestamp());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSetLogAppendTimeNotAllowedV0() {
+ ByteBuffer buffer = ByteBuffer.allocate(2048);
+ MemoryRecordsBuilder builder = MemoryRecords.builder(buffer, Record.MAGIC_VALUE_V0, CompressionType.NONE, TimestampType.CREATE_TIME, 0L);
+ builder.append(0L, 15L, "a".getBytes(), "1".getBytes());
+ Iterator iterator = builder.build().shallowIterator();
+
+ assertTrue(iterator.hasNext());
+ ByteBufferLogInputStream.ByteBufferLogEntry entry = iterator.next();
+
+ long logAppendTime = 20L;
+ entry.setLogAppendTime(logAppendTime);
+ }
+
+}
diff --git a/clients/src/test/java/org/apache/kafka/common/record/FileRecordsTest.java b/clients/src/test/java/org/apache/kafka/common/record/FileRecordsTest.java
new file mode 100644
index 0000000000000..7e2c2562e01f8
--- /dev/null
+++ b/clients/src/test/java/org/apache/kafka/common/record/FileRecordsTest.java
@@ -0,0 +1,410 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.apache.kafka.common.KafkaException;
+import org.apache.kafka.test.TestUtils;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.apache.kafka.test.TestUtils.tempFile;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class FileRecordsTest {
+
+ private Record[] records = new Record[] {
+ Record.create("abcd".getBytes()),
+ Record.create("efgh".getBytes()),
+ Record.create("ijkl".getBytes())
+ };
+ private FileRecords fileRecords;
+
+ @Before
+ public void setup() throws IOException {
+ this.fileRecords = createFileRecords(records);
+ }
+
+ /**
+ * Test that the cached size variable matches the actual file size as we append messages
+ */
+ @Test
+ public void testFileSize() throws IOException {
+ assertEquals(fileRecords.channel().size(), fileRecords.sizeInBytes());
+ for (int i = 0; i < 20; i++) {
+ fileRecords.append(MemoryRecords.withRecords(Record.create("abcd".getBytes())));
+ assertEquals(fileRecords.channel().size(), fileRecords.sizeInBytes());
+ }
+ }
+
+ /**
+ * Test that adding invalid bytes to the end of the log doesn't break iteration
+ */
+ @Test
+ public void testIterationOverPartialAndTruncation() throws IOException {
+ testPartialWrite(0, fileRecords);
+ testPartialWrite(2, fileRecords);
+ testPartialWrite(4, fileRecords);
+ testPartialWrite(5, fileRecords);
+ testPartialWrite(6, fileRecords);
+ }
+
+ private void testPartialWrite(int size, FileRecords fileRecords) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(size);
+ for (int i = 0; i < size; i++)
+ buffer.put((byte) 0);
+
+ buffer.rewind();
+
+ fileRecords.channel().write(buffer);
+
+ // appending those bytes should not change the contents
+ TestUtils.checkEquals(Arrays.asList(records).iterator(), fileRecords.records());
+ }
+
+ /**
+ * Iterating over the file does file reads but shouldn't change the position of the underlying FileChannel.
+ */
+ @Test
+ public void testIterationDoesntChangePosition() throws IOException {
+ long position = fileRecords.channel().position();
+ TestUtils.checkEquals(Arrays.asList(records).iterator(), fileRecords.records());
+ assertEquals(position, fileRecords.channel().position());
+ }
+
+ /**
+ * Test a simple append and read.
+ */
+ @Test
+ public void testRead() throws IOException {
+ FileRecords read = fileRecords.read(0, fileRecords.sizeInBytes());
+ TestUtils.checkEquals(fileRecords.shallowIterator(), read.shallowIterator());
+
+ List items = shallowEntries(read);
+ LogEntry second = items.get(1);
+
+ read = fileRecords.read(second.sizeInBytes(), fileRecords.sizeInBytes());
+ assertEquals("Try a read starting from the second message",
+ items.subList(1, 3), shallowEntries(read));
+
+ read = fileRecords.read(second.sizeInBytes(), second.sizeInBytes());
+ assertEquals("Try a read of a single message starting from the second message",
+ Collections.singletonList(second), shallowEntries(read));
+ }
+
+ /**
+ * Test the MessageSet.searchFor API.
+ */
+ @Test
+ public void testSearch() throws IOException {
+ // append a new message with a high offset
+ Record lastMessage = Record.create("test".getBytes());
+ fileRecords.append(MemoryRecords.withRecords(50L, lastMessage));
+
+ List entries = shallowEntries(fileRecords);
+ int position = 0;
+
+ int message1Size = entries.get(0).sizeInBytes();
+ assertEquals("Should be able to find the first message by its offset",
+ new FileRecords.LogEntryPosition(0L, position, message1Size),
+ fileRecords.searchForOffsetWithSize(0, 0));
+ position += message1Size;
+
+ int message2Size = entries.get(1).sizeInBytes();
+ assertEquals("Should be able to find second message when starting from 0",
+ new FileRecords.LogEntryPosition(1L, position, message2Size),
+ fileRecords.searchForOffsetWithSize(1, 0));
+ assertEquals("Should be able to find second message starting from its offset",
+ new FileRecords.LogEntryPosition(1L, position, message2Size),
+ fileRecords.searchForOffsetWithSize(1, position));
+ position += message2Size + entries.get(2).sizeInBytes();
+
+ int message4Size = entries.get(3).sizeInBytes();
+ assertEquals("Should be able to find fourth message from a non-existant offset",
+ new FileRecords.LogEntryPosition(50L, position, message4Size),
+ fileRecords.searchForOffsetWithSize(3, position));
+ assertEquals("Should be able to find fourth message by correct offset",
+ new FileRecords.LogEntryPosition(50L, position, message4Size),
+ fileRecords.searchForOffsetWithSize(50, position));
+ }
+
+ /**
+ * Test that the message set iterator obeys start and end slicing
+ */
+ @Test
+ public void testIteratorWithLimits() throws IOException {
+ LogEntry entry = shallowEntries(fileRecords).get(1);
+ int start = fileRecords.searchForOffsetWithSize(1, 0).position;
+ int size = entry.sizeInBytes();
+ FileRecords slice = fileRecords.read(start, size);
+ assertEquals(Collections.singletonList(entry), shallowEntries(slice));
+ FileRecords slice2 = fileRecords.read(start, size - 1);
+ assertEquals(Collections.emptyList(), shallowEntries(slice2));
+ }
+
+ /**
+ * Test the truncateTo method lops off messages and appropriately updates the size
+ */
+ @Test
+ public void testTruncate() throws IOException {
+ LogEntry entry = shallowEntries(fileRecords).get(0);
+ int end = fileRecords.searchForOffsetWithSize(1, 0).position;
+ fileRecords.truncateTo(end);
+ assertEquals(Collections.singletonList(entry), shallowEntries(fileRecords));
+ assertEquals(entry.sizeInBytes(), fileRecords.sizeInBytes());
+ }
+
+ /**
+ * Test that truncateTo only calls truncate on the FileChannel if the size of the
+ * FileChannel is bigger than the target size. This is important because some JVMs
+ * change the mtime of the file, even if truncate should do nothing.
+ */
+ @Test
+ public void testTruncateNotCalledIfSizeIsSameAsTargetSize() throws IOException {
+ FileChannel channelMock = EasyMock.createMock(FileChannel.class);
+
+ EasyMock.expect(channelMock.size()).andReturn(42L).atLeastOnce();
+ EasyMock.expect(channelMock.position(42L)).andReturn(null);
+ EasyMock.replay(channelMock);
+
+ FileRecords fileRecords = new FileRecords(tempFile(), channelMock, 0, Integer.MAX_VALUE, false);
+ fileRecords.truncateTo(42);
+
+ EasyMock.verify(channelMock);
+ }
+
+ /**
+ * Expect a KafkaException if targetSize is bigger than the size of
+ * the FileRecords.
+ */
+ @Test
+ public void testTruncateNotCalledIfSizeIsBiggerThanTargetSize() throws IOException {
+ FileChannel channelMock = EasyMock.createMock(FileChannel.class);
+
+ EasyMock.expect(channelMock.size()).andReturn(42L).atLeastOnce();
+ EasyMock.expect(channelMock.position(42L)).andReturn(null);
+ EasyMock.replay(channelMock);
+
+ FileRecords fileRecords = new FileRecords(tempFile(), channelMock, 0, Integer.MAX_VALUE, false);
+
+ try {
+ fileRecords.truncateTo(43);
+ fail("Should throw KafkaException");
+ } catch (KafkaException e) {
+ // expected
+ }
+
+ EasyMock.verify(channelMock);
+ }
+
+ /**
+ * see #testTruncateNotCalledIfSizeIsSameAsTargetSize
+ */
+ @Test
+ public void testTruncateIfSizeIsDifferentToTargetSize() throws IOException {
+ FileChannel channelMock = EasyMock.createMock(FileChannel.class);
+
+ EasyMock.expect(channelMock.size()).andReturn(42L).atLeastOnce();
+ EasyMock.expect(channelMock.position(42L)).andReturn(null).once();
+ EasyMock.expect(channelMock.truncate(23L)).andReturn(null).once();
+ EasyMock.expect(channelMock.position(23L)).andReturn(null).once();
+ EasyMock.replay(channelMock);
+
+ FileRecords fileRecords = new FileRecords(tempFile(), channelMock, 0, Integer.MAX_VALUE, false);
+ fileRecords.truncateTo(23);
+
+ EasyMock.verify(channelMock);
+ }
+
+ /**
+ * Test the new FileRecords with pre allocate as true
+ */
+ @Test
+ public void testPreallocateTrue() throws IOException {
+ File temp = tempFile();
+ FileRecords fileRecords = FileRecords.open(temp, false, 512 * 1024 * 1024, true);
+ long position = fileRecords.channel().position();
+ int size = fileRecords.sizeInBytes();
+ assertEquals(0, position);
+ assertEquals(0, size);
+ assertEquals(512 * 1024 * 1024, temp.length());
+ }
+
+ /**
+ * Test the new FileRecords with pre allocate as false
+ */
+ @Test
+ public void testPreallocateFalse() throws IOException {
+ File temp = tempFile();
+ FileRecords set = FileRecords.open(temp, false, 512 * 1024 * 1024, false);
+ long position = set.channel().position();
+ int size = set.sizeInBytes();
+ assertEquals(0, position);
+ assertEquals(0, size);
+ assertEquals(0, temp.length());
+ }
+
+ /**
+ * Test the new FileRecords with pre allocate as true and file has been clearly shut down, the file will be truncate to end of valid data.
+ */
+ @Test
+ public void testPreallocateClearShutdown() throws IOException {
+ File temp = tempFile();
+ FileRecords set = FileRecords.open(temp, false, 512 * 1024 * 1024, true);
+ set.append(MemoryRecords.withRecords(records));
+
+ int oldPosition = (int) set.channel().position();
+ int oldSize = set.sizeInBytes();
+ assertEquals(fileRecords.sizeInBytes(), oldPosition);
+ assertEquals(fileRecords.sizeInBytes(), oldSize);
+ set.close();
+
+ File tempReopen = new File(temp.getAbsolutePath());
+ FileRecords setReopen = FileRecords.open(tempReopen, true, 512 * 1024 * 1024, true);
+ int position = (int) setReopen.channel().position();
+ int size = setReopen.sizeInBytes();
+
+ assertEquals(oldPosition, position);
+ assertEquals(oldPosition, size);
+ assertEquals(oldPosition, tempReopen.length());
+ }
+
+ @Test
+ public void testFormatConversionWithPartialMessage() throws IOException {
+ LogEntry entry = shallowEntries(fileRecords).get(1);
+ int start = fileRecords.searchForOffsetWithSize(1, 0).position;
+ int size = entry.sizeInBytes();
+ FileRecords slice = fileRecords.read(start, size - 1);
+ Records messageV0 = slice.toMessageFormat(Record.MAGIC_VALUE_V0);
+ assertTrue("No message should be there", shallowEntries(messageV0).isEmpty());
+ assertEquals("There should be " + (size - 1) + " bytes", size - 1, messageV0.sizeInBytes());
+ }
+
+ @Test
+ public void testConvertNonCompressedToMagic1() throws IOException {
+ List entries = Arrays.asList(
+ LogEntry.create(0L, Record.create(Record.MAGIC_VALUE_V0, Record.NO_TIMESTAMP, "k1".getBytes(), "hello".getBytes())),
+ LogEntry.create(2L, Record.create(Record.MAGIC_VALUE_V0, Record.NO_TIMESTAMP, "k2".getBytes(), "goodbye".getBytes())));
+ MemoryRecords records = MemoryRecords.withLogEntries(CompressionType.NONE, entries);
+
+ // Up conversion. In reality we only do down conversion, but up conversion should work as well.
+ // up conversion for non-compressed messages
+ try (FileRecords fileRecords = FileRecords.open(tempFile())) {
+ fileRecords.append(records);
+ fileRecords.flush();
+ Records convertedRecords = fileRecords.toMessageFormat(Record.MAGIC_VALUE_V1);
+ verifyConvertedMessageSet(entries, convertedRecords, Record.MAGIC_VALUE_V1);
+ }
+ }
+
+ @Test
+ public void testConvertCompressedToMagic1() throws IOException {
+ List entries = Arrays.asList(
+ LogEntry.create(0L, Record.create(Record.MAGIC_VALUE_V0, Record.NO_TIMESTAMP, "k1".getBytes(), "hello".getBytes())),
+ LogEntry.create(2L, Record.create(Record.MAGIC_VALUE_V0, Record.NO_TIMESTAMP, "k2".getBytes(), "goodbye".getBytes())));
+ MemoryRecords records = MemoryRecords.withLogEntries(CompressionType.GZIP, entries);
+
+ // up conversion for compressed messages
+ try (FileRecords fileRecords = FileRecords.open(tempFile())) {
+ fileRecords.append(records);
+ fileRecords.flush();
+ Records convertedRecords = fileRecords.toMessageFormat(Record.MAGIC_VALUE_V1);
+ verifyConvertedMessageSet(entries, convertedRecords, Record.MAGIC_VALUE_V1);
+ }
+ }
+
+ @Test
+ public void testConvertNonCompressedToMagic0() throws IOException {
+ List entries = Arrays.asList(
+ LogEntry.create(0L, Record.create(Record.MAGIC_VALUE_V1, 1L, "k1".getBytes(), "hello".getBytes())),
+ LogEntry.create(2L, Record.create(Record.MAGIC_VALUE_V1, 2L, "k2".getBytes(), "goodbye".getBytes())));
+ MemoryRecords records = MemoryRecords.withLogEntries(CompressionType.NONE, entries);
+
+ // down conversion for non-compressed messages
+ try (FileRecords fileRecords = FileRecords.open(tempFile())) {
+ fileRecords.append(records);
+ fileRecords.flush();
+ Records convertedRecords = fileRecords.toMessageFormat(Record.MAGIC_VALUE_V0);
+ verifyConvertedMessageSet(entries, convertedRecords, Record.MAGIC_VALUE_V0);
+ }
+ }
+
+ @Test
+ public void testConvertCompressedToMagic0() throws IOException {
+ List entries = Arrays.asList(
+ LogEntry.create(0L, Record.create(Record.MAGIC_VALUE_V1, 1L, "k1".getBytes(), "hello".getBytes())),
+ LogEntry.create(2L, Record.create(Record.MAGIC_VALUE_V1, 2L, "k2".getBytes(), "goodbye".getBytes())));
+ MemoryRecords records = MemoryRecords.withLogEntries(CompressionType.GZIP, entries);
+
+ // down conversion for compressed messages
+ try (FileRecords fileRecords = FileRecords.open(tempFile())) {
+ fileRecords.append(records);
+ fileRecords.flush();
+ Records convertedRecords = fileRecords.toMessageFormat(Record.MAGIC_VALUE_V0);
+ verifyConvertedMessageSet(entries, convertedRecords, Record.MAGIC_VALUE_V0);
+ }
+ }
+
+ private void verifyConvertedMessageSet(List initialEntries, Records convertedRecords, byte magicByte) {
+ int i = 0;
+ for (LogEntry logEntry : deepEntries(convertedRecords)) {
+ assertEquals("magic byte should be " + magicByte, magicByte, logEntry.record().magic());
+ assertEquals("offset should not change", initialEntries.get(i).offset(), logEntry.offset());
+ assertEquals("key should not change", initialEntries.get(i).record().key(), logEntry.record().key());
+ assertEquals("payload should not change", initialEntries.get(i).record().value(), logEntry.record().value());
+ i += 1;
+ }
+ }
+
+ private static List shallowEntries(Records buffer) {
+ List entries = new ArrayList<>();
+ Iterator extends LogEntry> iterator = buffer.shallowIterator();
+ while (iterator.hasNext())
+ entries.add(iterator.next());
+ return entries;
+ }
+
+ private static List deepEntries(Records buffer) {
+ List entries = new ArrayList<>();
+ Iterator extends LogEntry> iterator = buffer.shallowIterator();
+ while (iterator.hasNext()) {
+ for (LogEntry deepEntry : iterator.next())
+ entries.add(deepEntry);
+ }
+ return entries;
+ }
+
+ private FileRecords createFileRecords(Record ... records) throws IOException {
+ FileRecords fileRecords = FileRecords.open(tempFile());
+ fileRecords.append(MemoryRecords.withRecords(records));
+ fileRecords.flush();
+ return fileRecords;
+ }
+
+}
diff --git a/clients/src/test/java/org/apache/kafka/common/record/MemoryRecordsBuilderTest.java b/clients/src/test/java/org/apache/kafka/common/record/MemoryRecordsBuilderTest.java
new file mode 100644
index 0000000000000..40fa212e215f2
--- /dev/null
+++ b/clients/src/test/java/org/apache/kafka/common/record/MemoryRecordsBuilderTest.java
@@ -0,0 +1,253 @@
+/**
+ * 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 org.apache.kafka.common.record;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+@RunWith(value = Parameterized.class)
+public class MemoryRecordsBuilderTest {
+
+ private final CompressionType compressionType;
+ private final int bufferOffset;
+
+ public MemoryRecordsBuilderTest(int bufferOffset, CompressionType compressionType) {
+ this.bufferOffset = bufferOffset;
+ this.compressionType = compressionType;
+ }
+
+ @Test
+ public void testCompressionRateV0() {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ buffer.position(bufferOffset);
+
+ Record[] records = new Record[] {
+ Record.create(Record.MAGIC_VALUE_V0, 0L, "a".getBytes(), "1".getBytes()),
+ Record.create(Record.MAGIC_VALUE_V0, 1L, "b".getBytes(), "2".getBytes()),
+ Record.create(Record.MAGIC_VALUE_V0, 2L, "c".getBytes(), "3".getBytes()),
+ };
+
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V0, compressionType,
+ TimestampType.CREATE_TIME, 0L, 0L, buffer.capacity());
+
+ int uncompressedSize = 0;
+ long offset = 0L;
+ for (Record record : records) {
+ uncompressedSize += record.sizeInBytes() + Records.LOG_OVERHEAD;
+ builder.append(offset++, record);
+ }
+
+ MemoryRecords built = builder.build();
+ if (compressionType == CompressionType.NONE) {
+ assertEquals(1.0, builder.compressionRate(), 0.00001);
+ } else {
+ int compressedSize = built.sizeInBytes() - Records.LOG_OVERHEAD - Record.RECORD_OVERHEAD_V0;
+ double computedCompressionRate = (double) compressedSize / uncompressedSize;
+ assertEquals(computedCompressionRate, builder.compressionRate(), 0.00001);
+ }
+ }
+
+ @Test
+ public void testCompressionRateV1() {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ buffer.position(bufferOffset);
+
+ Record[] records = new Record[] {
+ Record.create(Record.MAGIC_VALUE_V1, 0L, "a".getBytes(), "1".getBytes()),
+ Record.create(Record.MAGIC_VALUE_V1, 1L, "b".getBytes(), "2".getBytes()),
+ Record.create(Record.MAGIC_VALUE_V1, 2L, "c".getBytes(), "3".getBytes()),
+ };
+
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V1, compressionType,
+ TimestampType.CREATE_TIME, 0L, 0L, buffer.capacity());
+
+ int uncompressedSize = 0;
+ long offset = 0L;
+ for (Record record : records) {
+ uncompressedSize += record.sizeInBytes() + Records.LOG_OVERHEAD;
+ builder.append(offset++, record);
+ }
+
+ MemoryRecords built = builder.build();
+ if (compressionType == CompressionType.NONE) {
+ assertEquals(1.0, builder.compressionRate(), 0.00001);
+ } else {
+ int compressedSize = built.sizeInBytes() - Records.LOG_OVERHEAD - Record.RECORD_OVERHEAD_V1;
+ double computedCompressionRate = (double) compressedSize / uncompressedSize;
+ assertEquals(computedCompressionRate, builder.compressionRate(), 0.00001);
+ }
+ }
+
+ @Test
+ public void buildUsingLogAppendTime() {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ buffer.position(bufferOffset);
+
+ long logAppendTime = System.currentTimeMillis();
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V1, compressionType,
+ TimestampType.LOG_APPEND_TIME, 0L, logAppendTime, buffer.capacity());
+ builder.append(0L, 0L, "a".getBytes(), "1".getBytes());
+ builder.append(1L, 0L, "b".getBytes(), "2".getBytes());
+ builder.append(2L, 0L, "c".getBytes(), "3".getBytes());
+ MemoryRecords records = builder.build();
+
+ MemoryRecordsBuilder.RecordsInfo info = builder.info();
+ assertEquals(logAppendTime, info.maxTimestamp);
+
+ assertEquals(2L, info.shallowOffsetOfMaxTimestamp);
+
+ Iterator iterator = records.records();
+ while (iterator.hasNext()) {
+ Record record = iterator.next();
+ assertEquals(TimestampType.LOG_APPEND_TIME, record.timestampType());
+ assertEquals(logAppendTime, record.timestamp());
+ }
+ }
+
+ @Test
+ public void convertUsingLogAppendTime() {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ buffer.position(bufferOffset);
+
+ long logAppendTime = System.currentTimeMillis();
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V1, compressionType,
+ TimestampType.LOG_APPEND_TIME, 0L, logAppendTime, buffer.capacity());
+
+ builder.convertAndAppend(0L, Record.create(Record.MAGIC_VALUE_V0, 0L, "a".getBytes(), "1".getBytes()));
+ builder.convertAndAppend(1L, Record.create(Record.MAGIC_VALUE_V0, 0L, "b".getBytes(), "2".getBytes()));
+ builder.convertAndAppend(2L, Record.create(Record.MAGIC_VALUE_V0, 0L, "c".getBytes(), "3".getBytes()));
+ MemoryRecords records = builder.build();
+
+ MemoryRecordsBuilder.RecordsInfo info = builder.info();
+ assertEquals(logAppendTime, info.maxTimestamp);
+
+ assertEquals(2L, info.shallowOffsetOfMaxTimestamp);
+
+ Iterator iterator = records.records();
+ while (iterator.hasNext()) {
+ Record record = iterator.next();
+ assertEquals(TimestampType.LOG_APPEND_TIME, record.timestampType());
+ assertEquals(logAppendTime, record.timestamp());
+ }
+ }
+
+ @Test
+ public void buildUsingCreateTime() {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ buffer.position(bufferOffset);
+
+ long logAppendTime = System.currentTimeMillis();
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V1, compressionType,
+ TimestampType.CREATE_TIME, 0L, logAppendTime, buffer.capacity());
+ builder.append(0L, 0L, "a".getBytes(), "1".getBytes());
+ builder.append(1L, 2L, "b".getBytes(), "2".getBytes());
+ builder.append(2L, 1L, "c".getBytes(), "3".getBytes());
+ MemoryRecords records = builder.build();
+
+ MemoryRecordsBuilder.RecordsInfo info = builder.info();
+ assertEquals(2L, info.maxTimestamp);
+
+ if (compressionType == CompressionType.NONE)
+ assertEquals(1L, info.shallowOffsetOfMaxTimestamp);
+ else
+ assertEquals(2L, info.shallowOffsetOfMaxTimestamp);
+
+ Iterator iterator = records.records();
+ int i = 0;
+ long[] expectedTimestamps = new long[] {0L, 2L, 1L};
+ while (iterator.hasNext()) {
+ Record record = iterator.next();
+ assertEquals(TimestampType.CREATE_TIME, record.timestampType());
+ assertEquals(expectedTimestamps[i++], record.timestamp());
+ }
+ }
+
+ @Test
+ public void writePastLimit() {
+ ByteBuffer buffer = ByteBuffer.allocate(64);
+ buffer.position(bufferOffset);
+
+ long logAppendTime = System.currentTimeMillis();
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V1, compressionType,
+ TimestampType.CREATE_TIME, 0L, logAppendTime, buffer.capacity());
+ builder.append(0L, 0L, "a".getBytes(), "1".getBytes());
+ builder.append(1L, 1L, "b".getBytes(), "2".getBytes());
+
+ assertFalse(builder.hasRoomFor("c".getBytes(), "3".getBytes()));
+ builder.append(2L, 2L, "c".getBytes(), "3".getBytes());
+ MemoryRecords records = builder.build();
+
+ MemoryRecordsBuilder.RecordsInfo info = builder.info();
+ assertEquals(2L, info.maxTimestamp);
+ assertEquals(2L, info.shallowOffsetOfMaxTimestamp);
+
+ Iterator iterator = records.records();
+ long i = 0L;
+ while (iterator.hasNext()) {
+ Record record = iterator.next();
+ assertEquals(TimestampType.CREATE_TIME, record.timestampType());
+ assertEquals(i++, record.timestamp());
+ }
+ }
+
+ @Test
+ public void convertUsingCreateTime() {
+ ByteBuffer buffer = ByteBuffer.allocate(1024);
+ buffer.position(bufferOffset);
+
+ long logAppendTime = System.currentTimeMillis();
+ MemoryRecordsBuilder builder = new MemoryRecordsBuilder(buffer, Record.MAGIC_VALUE_V1, compressionType,
+ TimestampType.CREATE_TIME, 0L, logAppendTime, buffer.capacity());
+
+ builder.convertAndAppend(0L, Record.create(Record.MAGIC_VALUE_V0, 0L, "a".getBytes(), "1".getBytes()));
+ builder.convertAndAppend(0L, Record.create(Record.MAGIC_VALUE_V0, 0L, "b".getBytes(), "2".getBytes()));
+ builder.convertAndAppend(0L, Record.create(Record.MAGIC_VALUE_V0, 0L, "c".getBytes(), "3".getBytes()));
+ MemoryRecords records = builder.build();
+
+ MemoryRecordsBuilder.RecordsInfo info = builder.info();
+ assertEquals(Record.NO_TIMESTAMP, info.maxTimestamp);
+ assertEquals(0L, info.shallowOffsetOfMaxTimestamp);
+
+ Iterator iterator = records.records();
+ while (iterator.hasNext()) {
+ Record record = iterator.next();
+ assertEquals(TimestampType.CREATE_TIME, record.timestampType());
+ assertEquals(Record.NO_TIMESTAMP, record.timestamp());
+ }
+ }
+
+ @Parameterized.Parameters
+ public static Collection