Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.errors;

public class SnapshotNotFoundException extends ApiException {

private static final long serialVersionUID = 1;

public SnapshotNotFoundException(String s) {
super(s);
}

public SnapshotNotFoundException(String message, Throwable cause) {
super(message, cause);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
import org.apache.kafka.common.message.ExpireDelegationTokenResponseData;
import org.apache.kafka.common.message.FetchRequestData;
import org.apache.kafka.common.message.FetchResponseData;
import org.apache.kafka.common.message.FetchSnapshotRequestData;
import org.apache.kafka.common.message.FetchSnapshotResponseData;
import org.apache.kafka.common.message.FindCoordinatorRequestData;
import org.apache.kafka.common.message.FindCoordinatorResponseData;
import org.apache.kafka.common.message.HeartbeatRequestData;
Expand Down Expand Up @@ -254,7 +256,9 @@ public Struct parseResponse(short version, ByteBuffer buffer) {
ALTER_ISR(56, "AlterIsr", AlterIsrRequestData.SCHEMAS, AlterIsrResponseData.SCHEMAS),
UPDATE_FEATURES(57, "UpdateFeatures",
UpdateFeaturesRequestData.SCHEMAS, UpdateFeaturesResponseData.SCHEMAS, true),
ENVELOPE(58, "Envelope", true, false, EnvelopeRequestData.SCHEMAS, EnvelopeResponseData.SCHEMAS);
ENVELOPE(58, "Envelope", true, false, EnvelopeRequestData.SCHEMAS, EnvelopeResponseData.SCHEMAS),
FETCH_SNAPSHOT(59, "FetchSnapshot", false, false,
FetchSnapshotRequestData.SCHEMAS, FetchSnapshotResponseData.SCHEMAS);

private static final ApiKeys[] ID_TO_TYPE;
private static final int MIN_API_KEY = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
import org.apache.kafka.common.errors.RetriableException;
import org.apache.kafka.common.errors.SaslAuthenticationException;
import org.apache.kafka.common.errors.SecurityDisabledException;
import org.apache.kafka.common.errors.SnapshotNotFoundException;
import org.apache.kafka.common.errors.StaleBrokerEpochException;
import org.apache.kafka.common.errors.ThrottlingQuotaExceededException;
import org.apache.kafka.common.errors.TimeoutException;
Expand Down Expand Up @@ -341,7 +342,8 @@ public enum Errors {
INVALID_UPDATE_VERSION(95, "The given update version was invalid.", InvalidUpdateVersionException::new),
FEATURE_UPDATE_FAILED(96, "Unable to update finalized features due to an unexpected server error.", FeatureUpdateFailedException::new),
PRINCIPAL_DESERIALIZATION_FAILURE(97, "Request principal deserialization failed during forwarding. " +
"This indicates an internal error on the broker cluster security setup.", PrincipalDeserializationException::new);
"This indicates an internal error on the broker cluster security setup.", PrincipalDeserializationException::new),
SNAPSHOT_NOT_FOUND(98, "Requested snapshot was not found", SnapshotNotFoundException::new);

private static final Logger log = LoggerFactory.getLogger(Errors.class);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.requests;

import java.util.Collections;
import java.util.Optional;
import java.util.function.UnaryOperator;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.message.FetchSnapshotRequestData;
import org.apache.kafka.common.message.FetchSnapshotResponseData;
import org.apache.kafka.common.protocol.ApiKeys;
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.protocol.Message;

final public class FetchSnapshotRequest extends AbstractRequest {
public final FetchSnapshotRequestData data;

public FetchSnapshotRequest(FetchSnapshotRequestData data) {
super(ApiKeys.FETCH_SNAPSHOT, (short) (FetchSnapshotRequestData.SCHEMAS.length - 1));
this.data = data;
}

@Override
public FetchSnapshotResponse getErrorResponse(int throttleTimeMs, Throwable e) {
// TODO: we need to handle throttleTimeMs
return new FetchSnapshotResponse(new FetchSnapshotResponseData().setErrorCode(Errors.forException(e).code()));
}

@Override
protected Message data() {
return data;
}

public static FetchSnapshotRequestData singleton(
TopicPartition topicPartition,
UnaryOperator<FetchSnapshotRequestData.PartitionSnapshot> operator
) {
FetchSnapshotRequestData.PartitionSnapshot partitionSnapshot = operator.apply(
new FetchSnapshotRequestData.PartitionSnapshot().setPartition(topicPartition.partition())
);

return new FetchSnapshotRequestData()
.setTopics(
Collections.singletonList(
new FetchSnapshotRequestData.TopicSnapshot()
.setName(topicPartition.topic())
.setPartitions(Collections.singletonList(partitionSnapshot))
)
);
}

// TODO: write documentation. This function assumes that topic partitions are unique in `data`
public static Optional<FetchSnapshotRequestData.PartitionSnapshot> forTopicPartition(
FetchSnapshotRequestData data,
TopicPartition topicPartition
) {
return data
.topics()
.stream()
.filter(topic -> topic.name().equals(topicPartition.topic()))
.flatMap(topic -> topic.partitions().stream())
.filter(partition -> partition.partition() == topicPartition.partition())
.findAny();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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.requests;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.UnaryOperator;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.message.FetchSnapshotResponseData;
import org.apache.kafka.common.protocol.ApiKeys;
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.protocol.Message;

final public class FetchSnapshotResponse extends AbstractResponse {
public final FetchSnapshotResponseData data;

public FetchSnapshotResponse(FetchSnapshotResponseData data) {
super(ApiKeys.FETCH_SNAPSHOT);

this.data = data;
}

@Override
public Map<Errors, Integer> errorCounts() {
Map<Errors, Integer> errors = new HashMap<>();

Errors topLevelError = Errors.forCode(data.errorCode());
if (topLevelError != Errors.NONE) {
errors.put(topLevelError, 1);
}

for (FetchSnapshotResponseData.TopicSnapshot topicResponse : data.topics()) {
for (FetchSnapshotResponseData.PartitionSnapshot partitionResponse : topicResponse.partitions()) {
errors.compute(Errors.forCode(partitionResponse.errorCode()),
(error, count) -> count == null ? 1 : count + 1);
}
}

return errors;
}

@Override
public int throttleTimeMs() {
return data.throttleTimeMs();
}

@Override
protected Message data() {
return data;
}

public static FetchSnapshotResponseData withTopError(Errors error) {
Comment thread
jsancio marked this conversation as resolved.
Outdated
return new FetchSnapshotResponseData().setErrorCode(error.code());
}

public static FetchSnapshotResponseData singleton(
TopicPartition topicPartition,
UnaryOperator<FetchSnapshotResponseData.PartitionSnapshot> operator
) {
FetchSnapshotResponseData.PartitionSnapshot partitionSnapshot = operator.apply(
new FetchSnapshotResponseData.PartitionSnapshot().setIndex(topicPartition.partition())
);

return new FetchSnapshotResponseData()
.setTopics(
Collections.singletonList(
new FetchSnapshotResponseData.TopicSnapshot()
.setName(topicPartition.topic())
.setPartitions(Collections.singletonList(partitionSnapshot))
)
);
}

// TODO: write documentation. This function assumes that topic partitions are unique in `data`
public static Optional<FetchSnapshotResponseData.PartitionSnapshot> forTopicPartition(
FetchSnapshotResponseData data,
TopicPartition topicPartition
) {
return data
.topics()
.stream()
.filter(topic -> topic.name().equals(topicPartition.topic()))
.flatMap(topic -> topic.partitions().stream())
.filter(parition -> parition.index() == topicPartition.partition())
.findAny();
}
}
7 changes: 7 additions & 0 deletions clients/src/main/resources/common/message/FetchResponse.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@
{ "name": "LeaderEpoch", "type": "int32", "versions": "12+", "default": "-1",
"about": "The latest known leader epoch"}
]},
{ "name": "SnapshotId", "type": "SnapshotId",
"versions": "12+", "taggedVersions": "12+", "tag": 2,
"about": "In the case of fetching an offset less than the LogStartOffset, this is the end offset and epoch that should be used in the FetchSnapshot request.",
"fields": [
{ "name": "EndOffset", "type": "int64", "versions": "0+", "default": "-1" },
{ "name": "Epoch", "type": "int32", "versions": "0+", "default": "-1" }
]},
{ "name": "AbortedTransactions", "type": "[]AbortedTransaction", "versions": "4+", "nullableVersions": "4+", "ignorable": true,
"about": "The aborted transactions.", "fields": [
{ "name": "ProducerId", "type": "int64", "versions": "4+", "entityType": "producerId",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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.

{
"apiKey": 59,
"type": "request",
"name": "FetchSnapshotRequest",
"validVersions": "0",
"flexibleVersions": "0+",
"fields": [
{ "name": "ClusterId", "type": "string", "versions": "0+", "nullableVersions": "0+", "default": "null", "taggedVersions": "0+", "tag": 0,
"about": "The clusterId if known, this is used to validate metadata fetches prior to broker registration" },
{ "name": "ReplicaId", "type": "int32", "versions": "0+", "default": "-1",
"about": "The broker ID of the follower" },
{ "name": "MaxBytes", "type": "int32", "versions": "0+", "default": "0x7fffffff",
"about": "The maximum bytes to fetch from all of the snapshots" },
{ "name": "Topics", "type": "[]TopicSnapshot", "versions": "0+",
"about": "The topics to fetch", "fields": [
{ "name": "Name", "type": "string", "versions": "0+", "entityType": "topicName",
"about": "The name of the topic to fetch" },
{ "name": "Partitions", "type": "[]PartitionSnapshot", "versions": "0+",
"about": "The partitions to fetch", "fields": [
{ "name": "Partition", "type": "int32", "versions": "0+",
"about": "The partition index" },
{ "name": "CurrentLeaderEpoch", "type": "int32", "versions": "0+",
"about": "The current leader epoch of the partition, -1 for unknown leader epoch" },
{ "name": "SnapshotId", "type": "SnapshotId", "versions": "0+",
"about": "The snapshot endOffset and epoch to fetch",
"fields": [
{ "name": "EndOffset", "type": "int64", "versions": "0+" },
{ "name": "Epoch", "type": "int32", "versions": "0+" }
]},
{ "name": "Position", "type": "int64", "versions": "0+",
"about": "The byte position within the snapshot to start fetching from" }
]}
]}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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.

{
"apiKey": 59,
"type": "response",
"name": "FetchSnapshotResponse",
"validVersions": "0",
"flexibleVersions": "0+",
"fields": [
{ "name": "ThrottleTimeMs", "type": "int32", "versions": "0+", "ignorable": true,
"about": "The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota." },
{ "name": "ErrorCode", "type": "int16", "versions": "0+", "ignorable": false,
"about": "The top level response error code." },
{ "name": "Topics", "type": "[]TopicSnapshot", "versions": "0+",
"about": "The topics to fetch.", "fields": [
{ "name": "Name", "type": "string", "versions": "0+", "entityType": "topicName",
"about": "The name of the topic to fetch." },
{ "name": "Partitions", "type": "[]PartitionSnapshot", "versions": "0+",
"about": "The partitions to fetch.", "fields": [
{ "name": "Index", "type": "int32", "versions": "0+",
"about": "The partition index." },
{ "name": "ErrorCode", "type": "int16", "versions": "0+",
"about": "The error code, or 0 if there was no fetch error." },
{ "name": "SnapshotId", "type": "SnapshotId", "versions": "0+",
"about": "The snapshot endOffset and epoch fetched",
"fields": [
{ "name": "EndOffset", "type": "int64", "versions": "0+" },
{ "name": "Epoch", "type": "int32", "versions": "0+" }
]},
{ "name": "CurrentLeader", "type": "LeaderIdAndEpoch",
"versions": "0+", "taggedVersions": "0+", "tag": 0, "fields": [
{ "name": "LeaderId", "type": "int32", "versions": "0+",
"about": "The ID of the current leader or -1 if the leader is unknown."},
{ "name": "LeaderEpoch", "type": "int32", "versions": "0+",
"about": "The latest known leader epoch"}
]},
{ "name": "Size", "type": "int64", "versions": "0+",
"about": "The total size of the snapshot." },
{ "name": "Position", "type": "int64", "versions": "0+",
"about": "The starting byte position within the snapshot included in the Bytes field." },
{ "name": "Bytes", "type": "bytes", "versions": "0+", "zeroCopy": true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you remind me if we are planning to change the type to "records"? I don't think we will get the benefit of sendfile unless we do so.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah. I created this issue: https://issues.apache.org/jira/browse/KAFKA-10694

I think we have two options:

  1. Use "records" as you have suggested.
  2. Similar to "records" introduce a new type called for example Memory that has two implementations one that encapsulates ByteBuffer and another that encapsulates a file and a start position. This is similar to "records", MemoryRecords and FileRecords without the requirement that the start needs to be a valid RecordBatch.

I want to play around with this soon and restart the conversation on that issue and PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@jsancio Can we add an index file to the snapshot file so that the FetchSnaphotResponseData can also satisfy the requirement that the start is a valid RecordBatch? I can help with some work of this issue: https://issues.apache.org/jira/browse/KAFKA-10694.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@dengziming, Thanks for the comment. The FetchSnapshotRequest differs from FetchRequest in that Position is a byte offset. Because of this we don't need an Offset to Byte index.

If we implement option 1 in my comment above, we can still use Records by making sure that:

  1. The leaders send at least one entire RecordBatch in the FetchSnapshotResponse.
  2. The followers only write complete RecordBatches by not writing bytes sent by the leader that are not complete RecordBatch.

This guarantees that the Position sent in FetchSnapshotRequest is RecordBatch aligned and it always makes progress because the leader sends at least one complete RecordBatch.

Having said that, I think we should try and implement option 2. Unfortunately, the Jira https://issues.apache.org/jira/browse/KAFKA-10694 doesn't document option 2.

"about": "Snapshot data." }
]}
]}
]
}
1 change: 1 addition & 0 deletions core/src/main/scala/kafka/server/KafkaApis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ class KafkaApis(val requestChannel: RequestChannel,
case ApiKeys.BEGIN_QUORUM_EPOCH => closeConnection(request, util.Collections.emptyMap())
case ApiKeys.END_QUORUM_EPOCH => closeConnection(request, util.Collections.emptyMap())
case ApiKeys.DESCRIBE_QUORUM => closeConnection(request, util.Collections.emptyMap())
case ApiKeys.FETCH_SNAPSHOT => closeConnection(request, util.Collections.emptyMap())
}
} catch {
case e: FatalExitError => throw e
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/scala/kafka/tools/TestRaftRequestHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ class TestRaftRequestHandler(
case ApiKeys.VOTE
| ApiKeys.BEGIN_QUORUM_EPOCH
| ApiKeys.END_QUORUM_EPOCH
| ApiKeys.FETCH =>
| ApiKeys.FETCH
| ApiKeys.FETCH_SNAPSHOT =>
val requestBody = request.body[AbstractRequest]
networkChannel.postInboundRequest(requestBody, response =>
sendResponse(request, Some(response)))
Expand Down
Loading