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,29 @@ 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. This means that each record could
* be committed independently.
Comment thread
jsancio marked this conversation as resolved.
Outdated
*
* 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 +116,14 @@ 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 Down Expand Up @@ -79,24 +80,69 @@ 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
* recors will get committed while other will not when the leader fails.
Comment thread
jsancio marked this conversation as resolved.
Outdated
*
* @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.
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();

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

for (T record : records) {
BatchBuilder<T> batch = maybeAllocateBatch(
Comment thread
jsancio marked this conversation as resolved.
Outdated
serde.recordSize(record, serializationCache)
);
if (batch == null) {
return null;
}

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

maybeResetLinger();

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

/**
* 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) {
if (epoch != this.epoch) {
return Long.MAX_VALUE;
}

Expand All @@ -106,11 +152,6 @@ public Long append(int epoch, List<T> 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();
Expand All @@ -120,24 +161,35 @@ public Long append(int epoch, List<T> records) {
return null;
}

// Restart the linger timer if necessary
if (!lingerTimer.isRunning()) {
lingerTimer.reset(time.milliseconds() + lingerMs);
}

for (T record : records) {
batch.appendRecord(record, serializationCache);
nextOffset += 1;
}

maybeResetLinger();

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

private void maybeResetLinger() {
if (!lingerTimer.isRunning()) {
lingerTimer.reset(time.milliseconds() + lingerMs);
}
}

private BatchBuilder<T> maybeAllocateBatch(int batchSize) {
if (currentBatch == null) {
if (batchSize > maxBatchSize) {
throw new RecordBatchTooLargeException(
String.format(
"The total record(s) size of %s exceeds the maximum allowed batch size of %s",
batchSize,
maxBatchSize
)
);
} else if (currentBatch == null) {
startNewBatch();
} else if (!currentBatch.hasRoomFor(batchSize)) {
completeCurrentBatch();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,16 @@ public boolean hasRoomFor(int sizeInBytes) {
sizeInBytes,
DefaultRecord.EMPTY_HEADERS
);
int bytesNeeded = ByteUtils.sizeOfVarint(recordSizeInBytes) + recordSizeInBytes;
int approxUnusedSizeInBytes = maxBytes - approximateSizeInBytes();

int unusedSizeInBytes = maxBytes - approximateSizeInBytes();
if (unusedSizeInBytes >= recordSizeInBytes) {
if (approxUnusedSizeInBytes >= bytesNeeded) {
return true;
} else if (unflushedBytes > 0) {
recordOutput.flush();
unflushedBytes = 0;
unusedSizeInBytes = maxBytes - flushedSizeInBytes();
return unusedSizeInBytes >= recordSizeInBytes;
int unusedSizeInBytes = maxBytes - flushedSizeInBytes();
return unusedSizeInBytes >= bytesNeeded;
} else {
return false;
}
Expand Down
Loading