Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
465d46b
Basic implementation for loading snapshot
jsancio Feb 3, 2021
1ff2ffc
Add test for snapshot loading API
jsancio Feb 9, 2021
1ee5b27
Allow for configurable buffer supplier
jsancio Feb 10, 2021
abb1441
Add snapshot reader tests
jsancio Feb 10, 2021
77d9c88
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Feb 16, 2021
d8b4e13
Use functional style when constructing SnapshotReader
jsancio Feb 18, 2021
f15ac95
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Feb 18, 2021
96099a7
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Feb 18, 2021
b139893
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Mar 11, 2021
6bbed7f
Fix test for snapshot releading
jsancio Mar 11, 2021
6346275
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Mar 11, 2021
c72053b
Change oldest to earliest
jsancio Mar 11, 2021
259ea30
Add Serde based Records iterator
jsancio Mar 19, 2021
5b9805b
Add fix the buffer allocation pattern
jsancio Mar 24, 2021
7e52209
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Mar 24, 2021
1cbf5c2
Handle the case where the max batch size is smaller than the batch
jsancio Mar 24, 2021
a859ab9
Add more documentation to RawSnapshotReader
jsancio Mar 24, 2021
033e332
Generate snapshot in ReplicatedCounter and fix simulation
jsancio Mar 24, 2021
2dd4939
Remove TODO since Jira was created
jsancio Mar 24, 2021
8adbd4c
Use Colletions.emptyIterator instead of Optional.empty
jsancio Apr 9, 2021
aba403b
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Apr 9, 2021
029fea5
Add for reading batches greater than the suggest size
jsancio Apr 10, 2021
2776ae8
Merge remote-tracking branch 'upstream/trunk' into kafka-12154-snapsh…
jsancio Apr 27, 2021
d55165e
Fix imports
jsancio Apr 27, 2021
5a3aafd
Add snapshot at log start validation for the simulation
jsancio Apr 29, 2021
7b8a320
Improve snapshot verity performance
jsancio Apr 30, 2021
fbbf953
Use supplier for assert message
jsancio Apr 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions checkstyle/import-control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@
<allow pkg="org.apache.kafka.queue"/>
<allow pkg="org.apache.kafka.raft"/>
<allow pkg="org.apache.kafka.shell"/>
<allow pkg="org.apache.kafka.snapshot"/>
<allow pkg="org.jline"/>
<allow pkg="scala.compat"/>
</subpackage>
Expand Down
12 changes: 11 additions & 1 deletion core/src/main/scala/kafka/tools/TestRaftServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import org.apache.kafka.common.utils.{Time, Utils}
import org.apache.kafka.common.{TopicPartition, Uuid, protocol}
import org.apache.kafka.raft.BatchReader.Batch
import org.apache.kafka.raft.{BatchReader, RaftClient, RaftConfig, RecordSerde}
import org.apache.kafka.snapshot.SnapshotReader

import scala.jdk.CollectionConverters._

Expand Down Expand Up @@ -150,6 +151,7 @@ class TestRaftServer(
case class HandleClaim(epoch: Int) extends RaftEvent
case object HandleResign extends RaftEvent
case class HandleCommit(reader: BatchReader[Array[Byte]]) extends RaftEvent
case class HandleSnapshot(reader: SnapshotReader[Array[Byte]]) extends RaftEvent
case object Shutdown extends RaftEvent

private val eventQueue = new LinkedBlockingDeque[RaftEvent]()
Expand All @@ -175,6 +177,10 @@ class TestRaftServer(
eventQueue.offer(HandleCommit(reader))
}

override def handleSnapshot(reader: SnapshotReader[Array[Byte]]): Unit = {
eventQueue.offer(HandleSnapshot(reader))
}

override def initiateShutdown(): Boolean = {
val initiated = super.initiateShutdown()
eventQueue.offer(Shutdown)
Expand Down Expand Up @@ -226,7 +232,11 @@ class TestRaftServer(
reader.close()
}

case _ =>
case HandleSnapshot(reader) =>
// Ignore snapshots; only interested on records appended by this leader
Comment thread
jsancio marked this conversation as resolved.
Outdated
reader.close()

case Shutdown => // Ignore shutdown command
}
}

Expand Down
24 changes: 20 additions & 4 deletions raft/src/main/java/org/apache/kafka/raft/BatchReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.kafka.raft;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -60,19 +61,21 @@ public interface BatchReader<T> extends Iterator<BatchReader.Batch<T>>, AutoClos
@Override
void close();

class Batch<T> {
final class Batch<T> implements Iterable<T> {
Comment thread
jsancio marked this conversation as resolved.
Outdated
private final long baseOffset;
private final int epoch;
private final long lastOffset;
private final List<T> records;

public Batch(long baseOffset, int epoch, List<T> records) {
private Batch(long baseOffset, int epoch, long lastOffset, List<T> records) {
Comment thread
jsancio marked this conversation as resolved.
Outdated
this.baseOffset = baseOffset;
this.epoch = epoch;
this.lastOffset = lastOffset;
this.records = records;
}

public long lastOffset() {
return baseOffset + records.size() - 1;
return lastOffset;
}

public long baseOffset() {
Expand All @@ -87,11 +90,17 @@ public int epoch() {
return epoch;
}

@Override
public Iterator<T> iterator() {
return records.iterator();
}

@Override
public String toString() {
return "Batch(" +
"baseOffset=" + baseOffset +
", epoch=" + epoch +
", lastOffset=" + lastOffset +
", records=" + records +
')';
}
Expand All @@ -110,6 +119,13 @@ public boolean equals(Object o) {
public int hashCode() {
return Objects.hash(baseOffset, epoch, records);
}
}

public static <T> Batch<T> empty(long baseOffset, int epoch, long lastOffset) {
return new Batch<>(baseOffset, epoch, lastOffset, Collections.emptyList());
}

public static <T> Batch<T> of(long baseOffset, int epoch, List<T> records) {
return new Batch<>(baseOffset, epoch, baseOffset + records.size() - 1, records);
}
}
}
2 changes: 2 additions & 0 deletions raft/src/main/java/org/apache/kafka/raft/FollowerState.java
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ public String toString() {
", epoch=" + epoch +
", leaderId=" + leaderId +
", voters=" + voters +
", highWatermark=" + highWatermark +
", fetchingSnapshot=" + fetchingSnapshot +
')';
}

Expand Down
137 changes: 94 additions & 43 deletions raft/src/main/java/org/apache/kafka/raft/KafkaRaftClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
import org.apache.kafka.raft.internals.ThresholdPurgatory;
import org.apache.kafka.snapshot.RawSnapshotReader;
import org.apache.kafka.snapshot.RawSnapshotWriter;
import org.apache.kafka.snapshot.SnapshotReader;
import org.apache.kafka.snapshot.SnapshotWriter;
import org.slf4j.Logger;

Expand Down Expand Up @@ -137,7 +138,6 @@
* than the leader's log start offset. This API is similar to the Fetch API since the snapshot is stored
* as FileRecords, but we use {@link UnalignedRecords} in FetchSnapshotResponse because the records
* are not necessarily offset-aligned.
*
*/
public class KafkaRaftClient<T> implements RaftClient<T> {
private static final int RETRY_BACKOFF_BASE_MS = 100;
Expand Down Expand Up @@ -311,8 +311,18 @@ private void updateListenersProgress(long highWatermark) {
private void updateListenersProgress(List<ListenerContext> listenerContexts, long highWatermark) {
for (ListenerContext listenerContext : listenerContexts) {
listenerContext.nextExpectedOffset().ifPresent(nextExpectedOffset -> {
if (nextExpectedOffset < log.startOffset()) {
listenerContext.fireHandleSnapshot(log.startOffset());
if (nextExpectedOffset < log.startOffset() && nextExpectedOffset < highWatermark) {
SnapshotReader<T> snapshot = latestSnapshot().orElseThrow(() -> {
return new IllegalStateException(
String.format(
"Snapshot expected when next offset is %s, log start offset is %s and high-watermark is %s",
Comment thread
jsancio marked this conversation as resolved.
Outdated
nextExpectedOffset,
log.startOffset(),
highWatermark
)
);
});
listenerContext.fireHandleSnapshot(snapshot);
}
});

Expand All @@ -326,6 +336,16 @@ private void updateListenersProgress(List<ListenerContext> listenerContexts, lon
}
}

private Optional<SnapshotReader<T>> latestSnapshot() {
return log.latestSnapshotId().flatMap(snapshotId ->
log
.readSnapshot(snapshotId)
.map(reader ->
SnapshotReader.of(reader, serde, BufferSupplier.create(), MAX_BATCH_SIZE_BYTES)
)
);
}

private void maybeFireHandleCommit(long baseOffset, int epoch, List<T> records) {
for (ListenerContext listenerContext : listenerContexts) {
OptionalLong nextExpectedOffsetOpt = listenerContext.nextExpectedOffset();
Expand Down Expand Up @@ -355,24 +375,28 @@ private void fireHandleResign(int epoch) {
}

@Override
public void initialize() throws IOException {
quorum.initialize(new OffsetAndEpoch(log.endOffset().offset, log.lastFetchedEpoch()));
public void initialize() {
try {
quorum.initialize(new OffsetAndEpoch(log.endOffset().offset, log.lastFetchedEpoch()));

long currentTimeMs = time.milliseconds();
if (quorum.isLeader()) {
throw new IllegalStateException("Voter cannot initialize as a Leader");
} else if (quorum.isCandidate()) {
onBecomeCandidate(currentTimeMs);
} else if (quorum.isFollower()) {
onBecomeFollower(currentTimeMs);
}
long currentTimeMs = time.milliseconds();
if (quorum.isLeader()) {
throw new IllegalStateException("Voter cannot initialize as a Leader");
} else if (quorum.isCandidate()) {
onBecomeCandidate(currentTimeMs);
} else if (quorum.isFollower()) {
onBecomeFollower(currentTimeMs);
}

// When there is only a single voter, become candidate immediately
if (quorum.isVoter()
&& quorum.remoteVoters().isEmpty()
&& !quorum.isLeader()
&& !quorum.isCandidate()) {
transitionToCandidate(currentTimeMs);
// When there is only a single voter, become candidate immediately
if (quorum.isVoter()
&& quorum.remoteVoters().isEmpty()
&& !quorum.isCandidate()) {

transitionToCandidate(currentTimeMs);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

Expand Down Expand Up @@ -1131,6 +1155,7 @@ private boolean handleFetchResponse(
if (records.sizeInBytes() > 0) {
appendAsFollower(records);
}

OptionalLong highWatermark = partitionResponse.highWatermark() < 0 ?
OptionalLong.empty() : OptionalLong.of(partitionResponse.highWatermark());
updateFollowerHighWatermark(state, highWatermark);
Expand Down Expand Up @@ -1260,28 +1285,34 @@ private FetchSnapshotResponseData handleFetchSnapshotRequest(
}

try (RawSnapshotReader snapshot = snapshotOpt.get()) {
if (partitionSnapshot.position() < 0 || partitionSnapshot.position() >= snapshot.sizeInBytes()) {
long snapshotSize = snapshot.sizeInBytes();
if (partitionSnapshot.position() < 0 || partitionSnapshot.position() >= snapshotSize) {
return FetchSnapshotResponse.singleton(
log.topicPartition(),
responsePartitionSnapshot -> addQuorumLeader(responsePartitionSnapshot)
.setErrorCode(Errors.POSITION_OUT_OF_RANGE.code())
);
}

if (partitionSnapshot.position() > Integer.MAX_VALUE) {
throw new IllegalStateException(
String.format(
"Trying to fetch a snapshot with size (%s) and a position (%s) larger than %s",
snapshotSize,
partitionSnapshot.position(),
Integer.MAX_VALUE
)
);
}

int maxSnapshotSize;
try {
maxSnapshotSize = Math.toIntExact(snapshot.sizeInBytes());
maxSnapshotSize = Math.toIntExact(snapshotSize);
} catch (ArithmeticException e) {
maxSnapshotSize = Integer.MAX_VALUE;
}

if (partitionSnapshot.position() > Integer.MAX_VALUE) {
throw new IllegalStateException(String.format("Trying to fetch a snapshot with position: %d lager than Int.MaxValue", partitionSnapshot.position()));
}

UnalignedRecords records = snapshot.read(partitionSnapshot.position(), Math.min(data.maxBytes(), maxSnapshotSize));

long snapshotSize = snapshot.sizeInBytes();
UnalignedRecords records = snapshot.slice(partitionSnapshot.position(), Math.min(data.maxBytes(), maxSnapshotSize));
Comment thread
jsancio marked this conversation as resolved.

return FetchSnapshotResponse.singleton(
log.topicPartition(),
Expand Down Expand Up @@ -1386,10 +1417,15 @@ private boolean handleFetchSnapshotResponse(
);
}

if (!(partitionSnapshot.unalignedRecords() instanceof MemoryRecords)) {
final UnalignedMemoryRecords records;
if (partitionSnapshot.unalignedRecords() instanceof MemoryRecords) {
records = new UnalignedMemoryRecords(((MemoryRecords) partitionSnapshot.unalignedRecords()).buffer());
} else if (partitionSnapshot.unalignedRecords() instanceof UnalignedMemoryRecords) {
records = (UnalignedMemoryRecords) partitionSnapshot.unalignedRecords();
} else {
throw new IllegalStateException(String.format("Received unexpected fetch snapshot response: %s", partitionSnapshot));
}
snapshot.append(new UnalignedMemoryRecords(((MemoryRecords) partitionSnapshot.unalignedRecords()).buffer()));
snapshot.append(records);

if (snapshot.sizeInBytes() == partitionSnapshot.size()) {
// Finished fetching the snapshot.
Expand Down Expand Up @@ -2103,7 +2139,7 @@ private long pollUnattachedAsObserver(UnattachedState state, long currentTimeMs)
}

private long pollCurrentState(long currentTimeMs) throws IOException {
maybeUpdateOldestSnapshotId();
maybeDeleteBeforeSnapshot();

if (quorum.isLeader()) {
return pollLeader(currentTimeMs);
Expand Down Expand Up @@ -2167,8 +2203,14 @@ private boolean maybeCompleteShutdown(long currentTimeMs) {
return false;
}

private void maybeUpdateOldestSnapshotId() {
log.latestSnapshotId().ifPresent(log::deleteBeforeSnapshot);
private void maybeDeleteBeforeSnapshot() {
log.latestSnapshotId().ifPresent(snapshotId -> {
quorum.highWatermark().ifPresent(highWatermark -> {
if (highWatermark.offset >= snapshotId.offset) {
log.deleteBeforeSnapshot(snapshotId);
}
});
});
}

private void wakeup() {
Expand Down Expand Up @@ -2257,7 +2299,7 @@ public CompletableFuture<Void> shutdown(int timeoutMs) {
}

@Override
public SnapshotWriter<T> createSnapshot(OffsetAndEpoch snapshotId) throws IOException {
public SnapshotWriter<T> createSnapshot(OffsetAndEpoch snapshotId) {
return new SnapshotWriter<>(
log.createSnapshot(snapshotId),
MAX_BATCH_SIZE_BYTES,
Expand Down Expand Up @@ -2365,14 +2407,16 @@ public synchronized OptionalLong nextExpectedOffset() {
}

/**
* This API is used when the Listener needs to be notified of a new Snapshot. This happens
* when the context last acked end offset is less that then log start offset.
* This API is used when the Listener needs to be notified of a new snapshot. This happens
* when the context's next offset is less than the log start offset.
*/
public void fireHandleSnapshot(long logStartOffset) {
public void fireHandleSnapshot(SnapshotReader<T> reader) {
synchronized (this) {
nextOffset = logStartOffset;
nextOffset = reader.snapshotId().offset;
lastSent = null;
}

listener.handleSnapshot(reader);
}

/**
Expand All @@ -2382,10 +2426,16 @@ public void fireHandleSnapshot(long logStartOffset) {
* data in memory, we let the state machine read the records from disk.
*/
public void fireHandleCommit(long baseOffset, Records records) {
BufferSupplier bufferSupplier = BufferSupplier.create();
RecordsBatchReader<T> reader = new RecordsBatchReader<>(baseOffset, records,
serde, bufferSupplier, this);
fireHandleCommit(reader);
fireHandleCommit(
RecordsBatchReader.of(
baseOffset,
records,
serde,
BufferSupplier.create(),
MAX_BATCH_SIZE_BYTES,
this
)
);
}

/**
Expand All @@ -2396,7 +2446,7 @@ public void fireHandleCommit(long baseOffset, Records records) {
* followers.
*/
public void fireHandleCommit(long baseOffset, int epoch, List<T> records) {
BatchReader.Batch<T> batch = new BatchReader.Batch<>(baseOffset, epoch, records);
BatchReader.Batch<T> batch = BatchReader.Batch.of(baseOffset, epoch, records);
MemoryBatchReader<T> reader = new MemoryBatchReader<>(Collections.singletonList(batch), this);
fireHandleCommit(reader);
}
Expand Down Expand Up @@ -2425,6 +2475,7 @@ void fireHandleResign(int epoch) {

public synchronized void onClose(BatchReader<T> reader) {
OptionalLong lastOffset = reader.lastOffset();

if (lastOffset.isPresent()) {
nextOffset = lastOffset.getAsLong() + 1;
}
Expand Down
Loading