Skip to content
25 changes: 20 additions & 5 deletions raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
public class KafkaRaftClient<T> implements RaftClient<T> {
private static final int RETRY_BACKOFF_BASE_MS = 100;
private static final int FETCH_MAX_WAIT_MS = 1000;
static final int MAX_BATCH_SIZE = 1024 * 1024;
static final int MAX_BATCH_SIZE = 8 * 1024 * 1024;

private final AtomicReference<GracefulShutdown> shutdown = new AtomicReference<>();
private final Logger logger;
Expand Down Expand Up @@ -2166,13 +2166,27 @@ public void poll() throws IOException {

@Override
public Long scheduleAppend(int epoch, List<T> records) {
return append(epoch, records, false);
}

@Override
public Long scheduleAtomicAppend(int epoch, List<T> records) {
return append(epoch, records, true);
}

private Long append(int epoch, List<T> records, boolean isAtomic) {
BatchAccumulator<T> accumulator = this.accumulator;
if (accumulator == null) {
return Long.MAX_VALUE;
}

boolean isFirstAppend = accumulator.isEmpty();
Long offset = accumulator.append(epoch, records);
final Long offset;
if (isAtomic) {
offset = accumulator.appendAtomic(epoch, records);
} else {
offset = accumulator.append(epoch, records);
}

// Wakeup the network channel if either this is the first append
// or the accumulator is ready to drain now. Checking for the first
Expand Down Expand Up @@ -2329,9 +2343,10 @@ public void fireHandleCommit(long baseOffset, Records records) {

/**
* This API is used for committed records originating from {@link #scheduleAppend(int, List)}
* on this instance. In this case, we are able to save the original record objects,
* which saves the need to read them back from disk. This is a nice optimization
* for the leader which is typically doing more work than all of the followers.
* or {@link #scheduleAtomicAppend(int, List)} on this instance. In this case, we are able to
* save the original record objects, which saves the need to read them back from disk. This is
* a nice optimization for the leader which is typically doing more work than all of the
* followers.
*/
public void fireHandleCommit(long baseOffset, int epoch, List<T> records) {
BatchReader.Batch<T> batch = new BatchReader.Batch<>(baseOffset, epoch, records);
Expand Down
48 changes: 38 additions & 10 deletions raft/src/main/java/org/apache/kafka/raft/RaftClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ interface Listener<T> {
* after consuming the reader.
*
* Note that there is not a one-to-one correspondence between writes through
* {@link #scheduleAppend(int, List)} and this callback. The Raft implementation
* is free to batch together the records from multiple append calls provided
* that batch boundaries are respected. This means that each batch specified
* through {@link #scheduleAppend(int, List)} is guaranteed to be a subset of
* a batch provided by the {@link BatchReader}.
* {@link #scheduleAppend(int, List)} or {@link #scheduleAtomicAppend(int, List)}
* and this callback. The Raft implementation is free to batch together the records
* from multiple append calls provided that batch boundaries are respected. Records
* specified through {@link #scheduleAtomicAppend(int, List)} are guaranteed to be a
* subset of a batch provided by the {@link BatchReader}. Records specified through
* {@link #scheduleAppend(int, List)} are guaranteed to be in the same order but
* they can map to any number of batches provided by the {@link BatchReader}.
*
* @param reader reader instance which must be iterated and closed
*/
Expand All @@ -48,7 +50,7 @@ interface Listener<T> {
* {@link #handleCommit(BatchReader)}.
*
* After becoming a leader, the client is eligible to write to the log
* using {@link #scheduleAppend(int, List)}.
* using {@link #scheduleAppend(int, List)} or {@link #scheduleAtomicAppend(int, List)}.
*
* @param epoch the claimed leader epoch
*/
Expand Down Expand Up @@ -77,6 +79,30 @@ default void handleResign() {}
*/
void register(Listener<T> listener);

/**
* Append a list of records to the log. The write will be scheduled for some time
* in the future. There is no guarantee that appended records will be written to
* the log and eventually committed. While the order of the records is preserve, they can
* be appended to the log using one or more batches. Each record may be committed independently.
* If a record is committed, then all records scheduled for append during this epoch
* and prior to this record are also committed.
*
* If the provided current leader epoch does not match the current epoch, which
* is possible when the state machine has yet to observe the epoch change, then
* this method will return {@link Long#MAX_VALUE} to indicate an offset which is
* not possible to become committed. The state machine is expected to discard all
* uncommitted entries after observing an epoch change.
*
* @param epoch the current leader epoch
* @param records the list of records to append
* @return the expected offset of the last record; {@link Long#MAX_VALUE} if the records could
* be committed; null if no memory could be allocated for the batch at this time
* @throws RecordBatchTooLargeException if the size of the records is greater than the maximum
* batch size; if this exception is throw none of the elements in records were
* committed
*/
Long scheduleAppend(int epoch, List<T> records);

/**
* Append a list of records to the log. The write will be scheduled for some time
* in the future. There is no guarantee that appended records will be written to
Expand All @@ -91,11 +117,13 @@ default void handleResign() {}
*
* @param epoch the current leader epoch
* @param records the list of records to append
* @return the offset within the current epoch that the log entries will be appended,
* or null if the leader was unable to accept the write (e.g. due to memory
* being reached).
* @return the expected offset of the last record; {@link Long#MAX_VALUE} if the records could
* be committed; null if no memory could be allocated for the batch at this time
* @throws RecordBatchTooLargeException if the size of the records is greater than the maximum
* batch size; if this exception is throw none of the elements in records were
* committed
*/
Long scheduleAppend(int epoch, List<T> records);
Long scheduleAtomicAppend(int epoch, List<T> records);

/**
* Attempt a graceful shutdown of the client. This allows the leader to proactively
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.kafka.raft.internals;

import org.apache.kafka.common.errors.RecordBatchTooLargeException;
import org.apache.kafka.common.memory.MemoryPool;
import org.apache.kafka.common.protocol.ObjectSerializationCache;
import org.apache.kafka.common.record.CompressionType;
Expand All @@ -26,8 +27,10 @@
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.OptionalInt;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
Expand Down Expand Up @@ -79,70 +82,111 @@ public BatchAccumulator(
}

/**
* Append a list of records into an atomic batch. We guarantee all records
* are included in the same underlying record batch so that either all of
* the records become committed or none of them do.
* Append a list of records into as many batches as necessary.
*
* @param epoch the expected leader epoch. If this does not match, then
* {@link Long#MAX_VALUE} will be returned as an offset which
* cannot become committed.
* @param records the list of records to include in a batch
* @return the expected offset of the last record (which will be
* {@link Long#MAX_VALUE} if the epoch does not match), or null if
* no memory could be allocated for the batch at this time
* The order of the elements in the records argument will match the order in the batches.
* This method will use as many batches as necessary to serialize all of the records. Since
* this method can split the records into multiple batches it is possible that some of the
* records will get committed while other will not when the leader fails.
*
* @param epoch the expected leader epoch. If this does not match, then {@link Long#MAX_VALUE}
* will be returned as an offset which cannot become committed
* @param records the list of records to include in the batches
* @return the expected offset of the last record; {@link Long#MAX_VALUE} if the epoch does not
* match; null if no memory could be allocated for the batch at this time
* @throws RecordBatchTooLargeException if the size of one record T is greater than the maximum
* batch size; if this exception is throw some of the elements in records may have
* been committed
*/
public Long append(int epoch, List<T> records) {
Comment thread
jsancio marked this conversation as resolved.
return append(epoch, records, false);
}

/**
* Append a list of records into an atomic batch. We guarantee all records are included in the
* same underlying record batch so that either all of the records become committed or none of
* them do.
*
* @param epoch the expected leader epoch. If this does not match, then {@link Long#MAX_VALUE}
* will be returned as an offset which cannot become committed
* @param records the list of records to include in a batch
* @return the expected offset of the last record; {@link Long#MAX_VALUE} if the epoch does not
* match; null if no memory could be allocated for the batch at this time
* @throws RecordBatchTooLargeException if the size of the records is greater than the maximum
* batch size; if this exception is throw none of the elements in records were
* committed
*/
public Long appendAtomic(int epoch, List<T> records) {
return append(epoch, records, true);
}

private Long append(int epoch, List<T> records, boolean isAtomic) {
if (epoch != this.epoch) {
// If the epoch does not match, then the state machine probably
// has not gotten the notification about the latest epoch change.
// In this case, ignore the append and return a large offset value
// which will never be committed.
return Long.MAX_VALUE;
}

ObjectSerializationCache serializationCache = new ObjectSerializationCache();
int batchSize = 0;
for (T record : records) {
batchSize += serde.recordSize(record, serializationCache);
}

if (batchSize > maxBatchSize) {
throw new IllegalArgumentException("The total size of " + records + " is " + batchSize +
", which exceeds the maximum allowed batch size of " + maxBatchSize);
}

appendLock.lock();
try {
maybeCompleteDrain();

BatchBuilder<T> batch = maybeAllocateBatch(batchSize);
if (batch == null) {
return null;
}

// Restart the linger timer if necessary
if (!lingerTimer.isRunning()) {
lingerTimer.reset(time.milliseconds() + lingerMs);
BatchBuilder<T> batch = null;
if (isAtomic) {
batch = maybeAllocateBatch(records, serializationCache);
}

for (T record : records) {
if (!isAtomic) {
batch = maybeAllocateBatch(Collections.singleton(record), serializationCache);
}

if (batch == null) {
return null;
}

batch.appendRecord(record, serializationCache);
nextOffset += 1;
}

maybeResetLinger();

return nextOffset - 1;
} finally {
appendLock.unlock();
}
}

private BatchBuilder<T> maybeAllocateBatch(int batchSize) {
private void maybeResetLinger() {
if (!lingerTimer.isRunning()) {
lingerTimer.reset(time.milliseconds() + lingerMs);
}
}

private BatchBuilder<T> maybeAllocateBatch(
Collection<T> records,
ObjectSerializationCache serializationCache
) {
if (currentBatch == null) {
startNewBatch();
} else if (!currentBatch.hasRoomFor(batchSize)) {
completeCurrentBatch();
startNewBatch();
}

if (currentBatch != null) {
OptionalInt bytesNeeded = currentBatch.bytesNeeded(records, serializationCache);
if (bytesNeeded.isPresent() && bytesNeeded.getAsInt() > maxBatchSize) {
throw new RecordBatchTooLargeException(
String.format(
"The total record(s) size of %s exceeds the maximum allowed batch size of %s",
bytesNeeded.getAsInt(),
maxBatchSize
)
);
} else if (bytesNeeded.isPresent()) {
completeCurrentBatch();
startNewBatch();
}
}

return currentBatch;
}

Expand Down Expand Up @@ -298,28 +342,30 @@ public static class CompletedBatch<T> {
public final List<T> records;
public final MemoryRecords data;
private final MemoryPool pool;
private final ByteBuffer buffer;
// Buffer that was allocated by the MemoryPool (pool). This may not be the buffer used in
// the MemoryRecords (data) object.
private final ByteBuffer initialBuffer;

private CompletedBatch(
long baseOffset,
List<T> records,
MemoryRecords data,
MemoryPool pool,
ByteBuffer buffer
ByteBuffer initialBuffer
) {
this.baseOffset = baseOffset;
this.records = records;
this.data = data;
this.pool = pool;
this.buffer = buffer;
this.initialBuffer = initialBuffer;
}

public int sizeInBytes() {
return data.sizeInBytes();
}

public void release() {
pool.release(buffer);
pool.release(initialBuffer);
}
}

Expand Down
Loading