46 changed files with 2346 additions and 905 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,595 @@
@@ -0,0 +1,595 @@
|
||||
/** |
||||
* 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.clients.consumer.internals; |
||||
|
||||
import org.apache.kafka.clients.ClientRequest; |
||||
import org.apache.kafka.clients.ClientResponse; |
||||
import org.apache.kafka.clients.KafkaClient; |
||||
import org.apache.kafka.clients.Metadata; |
||||
import org.apache.kafka.clients.RequestCompletionHandler; |
||||
import org.apache.kafka.common.KafkaException; |
||||
import org.apache.kafka.common.MetricName; |
||||
import org.apache.kafka.common.Node; |
||||
import org.apache.kafka.common.TopicPartition; |
||||
import org.apache.kafka.common.metrics.Measurable; |
||||
import org.apache.kafka.common.metrics.MetricConfig; |
||||
import org.apache.kafka.common.metrics.Metrics; |
||||
import org.apache.kafka.common.metrics.Sensor; |
||||
import org.apache.kafka.common.metrics.stats.Avg; |
||||
import org.apache.kafka.common.metrics.stats.Count; |
||||
import org.apache.kafka.common.metrics.stats.Max; |
||||
import org.apache.kafka.common.metrics.stats.Rate; |
||||
import org.apache.kafka.common.protocol.ApiKeys; |
||||
import org.apache.kafka.common.protocol.Errors; |
||||
import org.apache.kafka.common.protocol.types.Struct; |
||||
import org.apache.kafka.common.requests.ConsumerMetadataRequest; |
||||
import org.apache.kafka.common.requests.ConsumerMetadataResponse; |
||||
import org.apache.kafka.common.requests.HeartbeatRequest; |
||||
import org.apache.kafka.common.requests.HeartbeatResponse; |
||||
import org.apache.kafka.common.requests.JoinGroupRequest; |
||||
import org.apache.kafka.common.requests.JoinGroupResponse; |
||||
import org.apache.kafka.common.requests.OffsetCommitRequest; |
||||
import org.apache.kafka.common.requests.OffsetCommitResponse; |
||||
import org.apache.kafka.common.requests.OffsetFetchRequest; |
||||
import org.apache.kafka.common.requests.OffsetFetchResponse; |
||||
import org.apache.kafka.common.requests.RequestHeader; |
||||
import org.apache.kafka.common.requests.RequestSend; |
||||
import org.apache.kafka.common.utils.Time; |
||||
import org.apache.kafka.common.utils.Utils; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Set; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
/** |
||||
* This class manage the coordination process with the consumer coordinator. |
||||
*/ |
||||
public final class Coordinator { |
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Coordinator.class); |
||||
|
||||
private final KafkaClient client; |
||||
|
||||
private final Time time; |
||||
private final String groupId; |
||||
private final Metadata metadata; |
||||
private final Heartbeat heartbeat; |
||||
private final long sessionTimeoutMs; |
||||
private final String assignmentStrategy; |
||||
private final SubscriptionState subscriptions; |
||||
private final CoordinatorMetrics sensors; |
||||
private final long retryBackoffMs; |
||||
private Node consumerCoordinator; |
||||
private String consumerId; |
||||
private int generation; |
||||
|
||||
/** |
||||
* Initialize the coordination manager. |
||||
*/ |
||||
public Coordinator(KafkaClient client, |
||||
String groupId, |
||||
long retryBackoffMs, |
||||
long sessionTimeoutMs, |
||||
String assignmentStrategy, |
||||
Metadata metadata, |
||||
SubscriptionState subscriptions, |
||||
Metrics metrics, |
||||
String metricGrpPrefix, |
||||
Map<String, String> metricTags, |
||||
Time time) { |
||||
|
||||
this.time = time; |
||||
this.client = client; |
||||
this.generation = -1; |
||||
this.consumerId = ""; |
||||
this.groupId = groupId; |
||||
this.metadata = metadata; |
||||
this.consumerCoordinator = null; |
||||
this.subscriptions = subscriptions; |
||||
this.retryBackoffMs = retryBackoffMs; |
||||
this.sessionTimeoutMs = sessionTimeoutMs; |
||||
this.assignmentStrategy = assignmentStrategy; |
||||
this.heartbeat = new Heartbeat(this.sessionTimeoutMs, time.milliseconds()); |
||||
this.sensors = new CoordinatorMetrics(metrics, metricGrpPrefix, metricTags); |
||||
} |
||||
|
||||
/** |
||||
* Assign partitions for the subscribed topics. |
||||
* |
||||
* @param subscribedTopics The subscribed topics list |
||||
* @param now The current time |
||||
* @return The assigned partition info |
||||
*/ |
||||
public List<TopicPartition> assignPartitions(List<String> subscribedTopics, long now) { |
||||
|
||||
// send a join group request to the coordinator
|
||||
log.debug("(Re-)joining group {} with subscribed topics {}", groupId, subscribedTopics); |
||||
|
||||
JoinGroupRequest request = new JoinGroupRequest(groupId, |
||||
(int) this.sessionTimeoutMs, |
||||
subscribedTopics, |
||||
this.consumerId, |
||||
this.assignmentStrategy); |
||||
ClientResponse resp = this.blockingCoordinatorRequest(ApiKeys.JOIN_GROUP, request.toStruct(), null, now); |
||||
|
||||
// process the response
|
||||
JoinGroupResponse response = new JoinGroupResponse(resp.responseBody()); |
||||
// TODO: needs to handle disconnects and errors
|
||||
Errors.forCode(response.errorCode()).maybeThrow(); |
||||
this.consumerId = response.consumerId(); |
||||
|
||||
// set the flag to refresh last committed offsets
|
||||
this.subscriptions.needRefreshCommits(); |
||||
|
||||
log.debug("Joined group: {}", response); |
||||
|
||||
// record re-assignment time
|
||||
this.sensors.partitionReassignments.record(time.milliseconds() - now); |
||||
|
||||
// return assigned partitions
|
||||
return response.assignedPartitions(); |
||||
} |
||||
|
||||
/** |
||||
* Commit offsets for the specified list of topics and partitions. |
||||
* |
||||
* A non-blocking commit will attempt to commit offsets asychronously. No error will be thrown if the commit fails. |
||||
* A blocking commit will wait for a response acknowledging the commit. In the event of an error it will retry until |
||||
* the commit succeeds. |
||||
* |
||||
* @param offsets The list of offsets per partition that should be committed. |
||||
* @param blocking Control whether the commit is blocking |
||||
* @param now The current time |
||||
*/ |
||||
public void commitOffsets(final Map<TopicPartition, Long> offsets, boolean blocking, long now) { |
||||
if (!offsets.isEmpty()) { |
||||
// create the offset commit request
|
||||
Map<TopicPartition, OffsetCommitRequest.PartitionData> offsetData; |
||||
offsetData = new HashMap<TopicPartition, OffsetCommitRequest.PartitionData>(offsets.size()); |
||||
for (Map.Entry<TopicPartition, Long> entry : offsets.entrySet()) |
||||
offsetData.put(entry.getKey(), new OffsetCommitRequest.PartitionData(entry.getValue(), now, "")); |
||||
OffsetCommitRequest req = new OffsetCommitRequest(this.groupId, this.generation, this.consumerId, offsetData); |
||||
|
||||
// send request and possibly wait for response if it is blocking
|
||||
RequestCompletionHandler handler = new CommitOffsetCompletionHandler(offsets); |
||||
|
||||
if (blocking) { |
||||
boolean done; |
||||
do { |
||||
ClientResponse response = blockingCoordinatorRequest(ApiKeys.OFFSET_COMMIT, req.toStruct(), handler, now); |
||||
|
||||
// check for errors
|
||||
done = true; |
||||
OffsetCommitResponse commitResponse = new OffsetCommitResponse(response.responseBody()); |
||||
for (short errorCode : commitResponse.responseData().values()) { |
||||
if (errorCode != Errors.NONE.code()) |
||||
done = false; |
||||
} |
||||
if (!done) { |
||||
log.debug("Error in offset commit, backing off for {} ms before retrying again.", |
||||
this.retryBackoffMs); |
||||
Utils.sleep(this.retryBackoffMs); |
||||
} |
||||
} while (!done); |
||||
} else { |
||||
this.client.send(initiateCoordinatorRequest(ApiKeys.OFFSET_COMMIT, req.toStruct(), handler, now)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Fetch the committed offsets of the given set of partitions. |
||||
* |
||||
* @param partitions The list of partitions which need to ask for committed offsets |
||||
* @param now The current time |
||||
* @return The fetched offset values |
||||
*/ |
||||
public Map<TopicPartition, Long> fetchOffsets(Set<TopicPartition> partitions, long now) { |
||||
log.debug("Fetching committed offsets for partitions: " + Utils.join(partitions, ", ")); |
||||
|
||||
while (true) { |
||||
// construct the request
|
||||
OffsetFetchRequest request = new OffsetFetchRequest(this.groupId, new ArrayList<TopicPartition>(partitions)); |
||||
|
||||
// send the request and block on waiting for response
|
||||
ClientResponse resp = this.blockingCoordinatorRequest(ApiKeys.OFFSET_FETCH, request.toStruct(), null, now); |
||||
|
||||
// parse the response to get the offsets
|
||||
boolean offsetsReady = true; |
||||
OffsetFetchResponse response = new OffsetFetchResponse(resp.responseBody()); |
||||
// TODO: needs to handle disconnects
|
||||
Map<TopicPartition, Long> offsets = new HashMap<TopicPartition, Long>(response.responseData().size()); |
||||
for (Map.Entry<TopicPartition, OffsetFetchResponse.PartitionData> entry : response.responseData().entrySet()) { |
||||
TopicPartition tp = entry.getKey(); |
||||
OffsetFetchResponse.PartitionData data = entry.getValue(); |
||||
if (data.hasError()) { |
||||
log.debug("Error fetching offset for topic-partition {}: {}", tp, Errors.forCode(data.errorCode) |
||||
.exception() |
||||
.getMessage()); |
||||
if (data.errorCode == Errors.OFFSET_LOAD_IN_PROGRESS.code()) { |
||||
// just retry
|
||||
offsetsReady = false; |
||||
Utils.sleep(this.retryBackoffMs); |
||||
} else if (data.errorCode == Errors.NOT_COORDINATOR_FOR_CONSUMER.code()) { |
||||
// re-discover the coordinator and retry
|
||||
coordinatorDead(); |
||||
offsetsReady = false; |
||||
Utils.sleep(this.retryBackoffMs); |
||||
} else if (data.errorCode == Errors.NO_OFFSETS_FETCHABLE.code() |
||||
|| data.errorCode == Errors.UNKNOWN_TOPIC_OR_PARTITION.code()) { |
||||
// just ignore this partition
|
||||
log.debug("No committed offset for partition " + tp); |
||||
} else { |
||||
throw new IllegalStateException("Unexpected error code " + data.errorCode + " while fetching offset"); |
||||
} |
||||
} else if (data.offset >= 0) { |
||||
// record the position with the offset (-1 seems to indicate no
|
||||
// such offset known)
|
||||
offsets.put(tp, data.offset); |
||||
} else { |
||||
log.debug("No committed offset for partition " + tp); |
||||
} |
||||
} |
||||
|
||||
if (offsetsReady) |
||||
return offsets; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Attempt to heartbeat the consumer coordinator if necessary, and check if the coordinator is still alive. |
||||
* |
||||
* @param now The current time |
||||
*/ |
||||
public void maybeHeartbeat(long now) { |
||||
if (heartbeat.shouldHeartbeat(now)) { |
||||
HeartbeatRequest req = new HeartbeatRequest(this.groupId, this.generation, this.consumerId); |
||||
this.client.send(initiateCoordinatorRequest(ApiKeys.HEARTBEAT, req.toStruct(), new HeartbeatCompletionHandler(), now)); |
||||
this.heartbeat.sentHeartbeat(now); |
||||
} |
||||
} |
||||
|
||||
public boolean coordinatorUnknown() { |
||||
return this.consumerCoordinator == null; |
||||
} |
||||
|
||||
/** |
||||
* Repeatedly attempt to send a request to the coordinator until a response is received (retry if we are |
||||
* disconnected). Note that this means any requests sent this way must be idempotent. |
||||
* |
||||
* @return The response |
||||
*/ |
||||
private ClientResponse blockingCoordinatorRequest(ApiKeys api, |
||||
Struct request, |
||||
RequestCompletionHandler handler, |
||||
long now) { |
||||
while (true) { |
||||
ClientRequest coordinatorRequest = initiateCoordinatorRequest(api, request, handler, now); |
||||
ClientResponse coordinatorResponse = sendAndReceive(coordinatorRequest, now); |
||||
if (coordinatorResponse.wasDisconnected()) { |
||||
handleCoordinatorDisconnect(coordinatorResponse); |
||||
Utils.sleep(this.retryBackoffMs); |
||||
} else { |
||||
return coordinatorResponse; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Ensure the consumer coordinator is known and we have a ready connection to it. |
||||
*/ |
||||
private void ensureCoordinatorReady() { |
||||
while (true) { |
||||
if (this.consumerCoordinator == null) |
||||
discoverCoordinator(); |
||||
|
||||
while (true) { |
||||
boolean ready = this.client.ready(this.consumerCoordinator, time.milliseconds()); |
||||
if (ready) { |
||||
return; |
||||
} else { |
||||
log.debug("No connection to coordinator, attempting to connect."); |
||||
this.client.poll(this.retryBackoffMs, time.milliseconds()); |
||||
|
||||
// if the coordinator connection has failed, we need to
|
||||
// break the inner loop to re-discover the coordinator
|
||||
if (this.client.connectionFailed(this.consumerCoordinator)) { |
||||
log.debug("Coordinator connection failed. Attempting to re-discover."); |
||||
coordinatorDead(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Mark the current coordinator as dead. |
||||
*/ |
||||
private void coordinatorDead() { |
||||
if (this.consumerCoordinator != null) { |
||||
log.info("Marking the coordinator {} dead.", this.consumerCoordinator.id()); |
||||
this.consumerCoordinator = null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Keep discovering the consumer coordinator until it is found. |
||||
*/ |
||||
private void discoverCoordinator() { |
||||
while (this.consumerCoordinator == null) { |
||||
log.debug("No coordinator known, attempting to discover one."); |
||||
Node coordinator = fetchConsumerCoordinator(); |
||||
|
||||
if (coordinator == null) { |
||||
log.debug("No coordinator found, backing off."); |
||||
Utils.sleep(this.retryBackoffMs); |
||||
} else { |
||||
log.debug("Found coordinator: " + coordinator); |
||||
this.consumerCoordinator = coordinator; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get the current consumer coordinator information via consumer metadata request. |
||||
* |
||||
* @return the consumer coordinator node |
||||
*/ |
||||
private Node fetchConsumerCoordinator() { |
||||
|
||||
// initiate the consumer metadata request
|
||||
ClientRequest request = initiateConsumerMetadataRequest(); |
||||
|
||||
// send the request and wait for its response
|
||||
ClientResponse response = sendAndReceive(request, request.createdTime()); |
||||
|
||||
// parse the response to get the coordinator info if it is not disconnected,
|
||||
// otherwise we need to request metadata update
|
||||
if (!response.wasDisconnected()) { |
||||
ConsumerMetadataResponse consumerMetadataResponse = new ConsumerMetadataResponse(response.responseBody()); |
||||
// use MAX_VALUE - node.id as the coordinator id to mimic separate connections
|
||||
// for the coordinator in the underlying network client layer
|
||||
// TODO: this needs to be better handled in KAFKA-1935
|
||||
if (consumerMetadataResponse.errorCode() == Errors.NONE.code()) |
||||
return new Node(Integer.MAX_VALUE - consumerMetadataResponse.node().id(), |
||||
consumerMetadataResponse.node().host(), |
||||
consumerMetadataResponse.node().port()); |
||||
} else { |
||||
this.metadata.requestUpdate(); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Handle the case when the request gets cancelled due to coordinator disconnection. |
||||
*/ |
||||
private void handleCoordinatorDisconnect(ClientResponse response) { |
||||
int correlation = response.request().request().header().correlationId(); |
||||
log.debug("Cancelled request {} with correlation id {} due to coordinator {} being disconnected", |
||||
response.request(), |
||||
correlation, |
||||
response.request().request().destination()); |
||||
|
||||
// mark the coordinator as dead
|
||||
coordinatorDead(); |
||||
} |
||||
|
||||
/** |
||||
* Initiate a consumer metadata request to the least loaded node. |
||||
* |
||||
* @return The created request |
||||
*/ |
||||
private ClientRequest initiateConsumerMetadataRequest() { |
||||
|
||||
// find a node to ask about the coordinator
|
||||
Node node = this.client.leastLoadedNode(time.milliseconds()); |
||||
while (node == null || !this.client.ready(node, time.milliseconds())) { |
||||
long now = time.milliseconds(); |
||||
this.client.poll(this.retryBackoffMs, now); |
||||
node = this.client.leastLoadedNode(now); |
||||
|
||||
// if there is no ready node, backoff before retry
|
||||
if (node == null) |
||||
Utils.sleep(this.retryBackoffMs); |
||||
} |
||||
|
||||
// create a consumer metadata request
|
||||
log.debug("Issuing consumer metadata request to broker {}", node.id()); |
||||
|
||||
ConsumerMetadataRequest request = new ConsumerMetadataRequest(this.groupId); |
||||
RequestSend send = new RequestSend(node.id(), |
||||
this.client.nextRequestHeader(ApiKeys.CONSUMER_METADATA), |
||||
request.toStruct()); |
||||
long now = time.milliseconds(); |
||||
return new ClientRequest(now, true, send, null); |
||||
} |
||||
|
||||
/** |
||||
* Initiate a request to the coordinator. |
||||
*/ |
||||
private ClientRequest initiateCoordinatorRequest(ApiKeys api, Struct request, RequestCompletionHandler handler, long now) { |
||||
|
||||
// first make sure the coordinator is known and ready
|
||||
ensureCoordinatorReady(); |
||||
|
||||
// create the request for the coordinator
|
||||
log.debug("Issuing request ({}: {}) to coordinator {}", api, request, this.consumerCoordinator.id()); |
||||
|
||||
RequestHeader header = this.client.nextRequestHeader(api); |
||||
RequestSend send = new RequestSend(this.consumerCoordinator.id(), header, request); |
||||
return new ClientRequest(now, true, send, handler); |
||||
} |
||||
|
||||
/** |
||||
* Attempt to send a request and receive its response. |
||||
* |
||||
* @return The response |
||||
*/ |
||||
private ClientResponse sendAndReceive(ClientRequest clientRequest, long now) { |
||||
|
||||
// send the request
|
||||
this.client.send(clientRequest); |
||||
|
||||
// drain all responses from the destination node
|
||||
List<ClientResponse> responses = this.client.completeAll(clientRequest.request().destination(), now); |
||||
if (responses.isEmpty()) { |
||||
throw new IllegalStateException("This should not happen."); |
||||
} else { |
||||
// other requests should be handled by the callback, and
|
||||
// we only care about the response of the last request
|
||||
return responses.get(responses.size() - 1); |
||||
} |
||||
} |
||||
|
||||
private class HeartbeatCompletionHandler implements RequestCompletionHandler { |
||||
@Override |
||||
public void onComplete(ClientResponse resp) { |
||||
if (resp.wasDisconnected()) { |
||||
handleCoordinatorDisconnect(resp); |
||||
} else { |
||||
HeartbeatResponse response = new HeartbeatResponse(resp.responseBody()); |
||||
if (response.errorCode() == Errors.NONE.code()) { |
||||
log.debug("Received successful heartbeat response."); |
||||
} else if (response.errorCode() == Errors.CONSUMER_COORDINATOR_NOT_AVAILABLE.code() |
||||
|| response.errorCode() == Errors.NOT_COORDINATOR_FOR_CONSUMER.code()) { |
||||
coordinatorDead(); |
||||
} else if (response.errorCode() == Errors.ILLEGAL_GENERATION.code()) { |
||||
subscriptions.needReassignment(); |
||||
} else { |
||||
throw new KafkaException("Unexpected error in heartbeat response: " |
||||
+ Errors.forCode(response.errorCode()).exception().getMessage()); |
||||
} |
||||
} |
||||
sensors.heartbeatLatency.record(resp.requestLatencyMs()); |
||||
} |
||||
} |
||||
|
||||
private class CommitOffsetCompletionHandler implements RequestCompletionHandler { |
||||
|
||||
private final Map<TopicPartition, Long> offsets; |
||||
|
||||
public CommitOffsetCompletionHandler(Map<TopicPartition, Long> offsets) { |
||||
this.offsets = offsets; |
||||
} |
||||
|
||||
@Override |
||||
public void onComplete(ClientResponse resp) { |
||||
if (resp.wasDisconnected()) { |
||||
handleCoordinatorDisconnect(resp); |
||||
} else { |
||||
OffsetCommitResponse response = new OffsetCommitResponse(resp.responseBody()); |
||||
for (Map.Entry<TopicPartition, Short> entry : response.responseData().entrySet()) { |
||||
TopicPartition tp = entry.getKey(); |
||||
short errorCode = entry.getValue(); |
||||
long offset = this.offsets.get(tp); |
||||
if (errorCode == Errors.NONE.code()) { |
||||
log.debug("Committed offset {} for partition {}", offset, tp); |
||||
subscriptions.committed(tp, offset); |
||||
} else if (errorCode == Errors.CONSUMER_COORDINATOR_NOT_AVAILABLE.code() |
||||
|| errorCode == Errors.NOT_COORDINATOR_FOR_CONSUMER.code()) { |
||||
coordinatorDead(); |
||||
} else { |
||||
log.error("Error committing partition {} at offset {}: {}", |
||||
tp, |
||||
offset, |
||||
Errors.forCode(errorCode).exception().getMessage()); |
||||
} |
||||
} |
||||
} |
||||
sensors.commitLatency.record(resp.requestLatencyMs()); |
||||
} |
||||
} |
||||
|
||||
private class CoordinatorMetrics { |
||||
public final Metrics metrics; |
||||
public final String metricGrpName; |
||||
|
||||
public final Sensor commitLatency; |
||||
public final Sensor heartbeatLatency; |
||||
public final Sensor partitionReassignments; |
||||
|
||||
public CoordinatorMetrics(Metrics metrics, String metricGrpPrefix, Map<String, String> tags) { |
||||
this.metrics = metrics; |
||||
this.metricGrpName = metricGrpPrefix + "-coordinator-metrics"; |
||||
|
||||
this.commitLatency = metrics.sensor("commit-latency"); |
||||
this.commitLatency.add(new MetricName("commit-latency-avg", |
||||
this.metricGrpName, |
||||
"The average time taken for a commit request", |
||||
tags), new Avg()); |
||||
this.commitLatency.add(new MetricName("commit-latency-max", |
||||
this.metricGrpName, |
||||
"The max time taken for a commit request", |
||||
tags), new Max()); |
||||
this.commitLatency.add(new MetricName("commit-rate", |
||||
this.metricGrpName, |
||||
"The number of commit calls per second", |
||||
tags), new Rate(new Count())); |
||||
|
||||
this.heartbeatLatency = metrics.sensor("heartbeat-latency"); |
||||
this.heartbeatLatency.add(new MetricName("heartbeat-response-time-max", |
||||
this.metricGrpName, |
||||
"The max time taken to receive a response to a hearbeat request", |
||||
tags), new Max()); |
||||
this.heartbeatLatency.add(new MetricName("heartbeat-rate", |
||||
this.metricGrpName, |
||||
"The average number of heartbeats per second", |
||||
tags), new Rate(new Count())); |
||||
|
||||
this.partitionReassignments = metrics.sensor("reassignment-latency"); |
||||
this.partitionReassignments.add(new MetricName("reassignment-time-avg", |
||||
this.metricGrpName, |
||||
"The average time taken for a partition reassignment", |
||||
tags), new Avg()); |
||||
this.partitionReassignments.add(new MetricName("reassignment-time-max", |
||||
this.metricGrpName, |
||||
"The max time taken for a partition reassignment", |
||||
tags), new Avg()); |
||||
this.partitionReassignments.add(new MetricName("reassignment-rate", |
||||
this.metricGrpName, |
||||
"The number of partition reassignments per second", |
||||
tags), new Rate(new Count())); |
||||
|
||||
Measurable numParts = |
||||
new Measurable() { |
||||
public double measure(MetricConfig config, long now) { |
||||
return subscriptions.assignedPartitions().size(); |
||||
} |
||||
}; |
||||
metrics.addMetric(new MetricName("assigned-partitions", |
||||
this.metricGrpName, |
||||
"The number of partitions currently assigned to this consumer", |
||||
tags), |
||||
numParts); |
||||
|
||||
Measurable lastHeartbeat = |
||||
new Measurable() { |
||||
public double measure(MetricConfig config, long now) { |
||||
return TimeUnit.SECONDS.convert(now - heartbeat.lastHeartbeatSend(), TimeUnit.MILLISECONDS); |
||||
} |
||||
}; |
||||
metrics.addMetric(new MetricName("last-heartbeat-seconds-ago", |
||||
this.metricGrpName, |
||||
"The number of seconds since the last controller heartbeat", |
||||
tags), |
||||
lastHeartbeat); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,459 @@
@@ -0,0 +1,459 @@
|
||||
/** |
||||
* 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.clients.consumer.internals; |
||||
|
||||
import org.apache.kafka.clients.ClientRequest; |
||||
import org.apache.kafka.clients.ClientResponse; |
||||
import org.apache.kafka.clients.KafkaClient; |
||||
import org.apache.kafka.clients.Metadata; |
||||
import org.apache.kafka.clients.RequestCompletionHandler; |
||||
import org.apache.kafka.clients.consumer.ConsumerRecord; |
||||
import org.apache.kafka.clients.consumer.NoOffsetForPartitionException; |
||||
import org.apache.kafka.common.Cluster; |
||||
import org.apache.kafka.common.MetricName; |
||||
import org.apache.kafka.common.Node; |
||||
import org.apache.kafka.common.PartitionInfo; |
||||
import org.apache.kafka.common.TopicPartition; |
||||
import org.apache.kafka.common.metrics.Metrics; |
||||
import org.apache.kafka.common.metrics.Sensor; |
||||
import org.apache.kafka.common.metrics.stats.Avg; |
||||
import org.apache.kafka.common.metrics.stats.Count; |
||||
import org.apache.kafka.common.metrics.stats.Max; |
||||
import org.apache.kafka.common.metrics.stats.Rate; |
||||
import org.apache.kafka.common.protocol.ApiKeys; |
||||
import org.apache.kafka.common.protocol.Errors; |
||||
import org.apache.kafka.common.record.LogEntry; |
||||
import org.apache.kafka.common.record.MemoryRecords; |
||||
import org.apache.kafka.common.requests.FetchRequest; |
||||
import org.apache.kafka.common.requests.FetchResponse; |
||||
import org.apache.kafka.common.requests.ListOffsetRequest; |
||||
import org.apache.kafka.common.requests.ListOffsetResponse; |
||||
import org.apache.kafka.common.requests.RequestSend; |
||||
import org.apache.kafka.common.serialization.Deserializer; |
||||
import org.apache.kafka.common.utils.Time; |
||||
import org.apache.kafka.common.utils.Utils; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
|
||||
import java.nio.ByteBuffer; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.LinkedList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
|
||||
/** |
||||
* This class manage the fetching process with the brokers. |
||||
*/ |
||||
public class Fetcher<K, V> { |
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(Fetcher.class); |
||||
private static final long EARLIEST_OFFSET_TIMESTAMP = -2L; |
||||
private static final long LATEST_OFFSET_TIMESTAMP = -1L; |
||||
|
||||
|
||||
private final KafkaClient client; |
||||
|
||||
private final Time time; |
||||
private final int minBytes; |
||||
private final int maxWaitMs; |
||||
private final int fetchSize; |
||||
private final boolean checkCrcs; |
||||
private final long retryBackoffMs; |
||||
private final Metadata metadata; |
||||
private final FetchManagerMetrics sensors; |
||||
private final SubscriptionState subscriptions; |
||||
private final List<PartitionRecords<K, V>> records; |
||||
private final AutoOffsetResetStrategy offsetResetStrategy; |
||||
private final Deserializer<K> keyDeserializer; |
||||
private final Deserializer<V> valueDeserializer; |
||||
|
||||
|
||||
public Fetcher(KafkaClient client, |
||||
long retryBackoffMs, |
||||
int minBytes, |
||||
int maxWaitMs, |
||||
int fetchSize, |
||||
boolean checkCrcs, |
||||
String offsetReset, |
||||
Deserializer<K> keyDeserializer, |
||||
Deserializer<V> valueDeserializer, |
||||
Metadata metadata, |
||||
SubscriptionState subscriptions, |
||||
Metrics metrics, |
||||
String metricGrpPrefix, |
||||
Map<String, String> metricTags, |
||||
Time time) { |
||||
|
||||
this.time = time; |
||||
this.client = client; |
||||
this.metadata = metadata; |
||||
this.subscriptions = subscriptions; |
||||
this.retryBackoffMs = retryBackoffMs; |
||||
this.minBytes = minBytes; |
||||
this.maxWaitMs = maxWaitMs; |
||||
this.fetchSize = fetchSize; |
||||
this.checkCrcs = checkCrcs; |
||||
this.offsetResetStrategy = AutoOffsetResetStrategy.valueOf(offsetReset); |
||||
|
||||
this.keyDeserializer = keyDeserializer; |
||||
this.valueDeserializer = valueDeserializer; |
||||
|
||||
this.records = new LinkedList<PartitionRecords<K, V>>(); |
||||
this.sensors = new FetchManagerMetrics(metrics, metricGrpPrefix, metricTags); |
||||
} |
||||
|
||||
/** |
||||
* Set-up a fetch request for any node that we have assigned partitions for which doesn't have one. |
||||
* |
||||
* @param cluster The current cluster metadata |
||||
* @param now The current time |
||||
*/ |
||||
public void initFetches(Cluster cluster, long now) { |
||||
for (ClientRequest request : createFetchRequests(cluster)) { |
||||
Node node = cluster.nodeById(request.request().destination()); |
||||
if (client.ready(node, now)) { |
||||
log.trace("Initiating fetch to node {}: {}", node.id(), request); |
||||
client.send(request); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the fetched records, empty the record buffer and update the consumed position. |
||||
* |
||||
* @return The fetched records per partition |
||||
*/ |
||||
public Map<TopicPartition, List<ConsumerRecord<K, V>>> fetchedRecords() { |
||||
if (this.subscriptions.partitionAssignmentNeeded()) { |
||||
return Collections.emptyMap(); |
||||
} else { |
||||
Map<TopicPartition, List<ConsumerRecord<K, V>>> drained = new HashMap<TopicPartition, List<ConsumerRecord<K, V>>>(); |
||||
for (PartitionRecords<K, V> part : this.records) { |
||||
Long consumed = subscriptions.consumed(part.partition); |
||||
if (this.subscriptions.assignedPartitions().contains(part.partition) |
||||
&& (consumed == null || part.fetchOffset == consumed)) { |
||||
List<ConsumerRecord<K, V>> records = drained.get(part.partition); |
||||
if (records == null) { |
||||
records = part.records; |
||||
drained.put(part.partition, records); |
||||
} else { |
||||
records.addAll(part.records); |
||||
} |
||||
subscriptions.consumed(part.partition, part.records.get(part.records.size() - 1).offset() + 1); |
||||
} else { |
||||
// these records aren't next in line based on the last consumed position, ignore them
|
||||
// they must be from an obsolete request
|
||||
log.debug("Ignoring fetched records for {} at offset {}", part.partition, part.fetchOffset); |
||||
} |
||||
} |
||||
this.records.clear(); |
||||
return drained; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Reset offsets for the given partition using the offset reset strategy. |
||||
* |
||||
* @param partition The given partition that needs reset offset |
||||
* @throws org.apache.kafka.clients.consumer.NoOffsetForPartitionException If no offset reset strategy is defined |
||||
*/ |
||||
public void resetOffset(TopicPartition partition) { |
||||
long timestamp; |
||||
if (this.offsetResetStrategy == AutoOffsetResetStrategy.EARLIEST) |
||||
timestamp = EARLIEST_OFFSET_TIMESTAMP; |
||||
else if (this.offsetResetStrategy == AutoOffsetResetStrategy.LATEST) |
||||
timestamp = LATEST_OFFSET_TIMESTAMP; |
||||
else |
||||
throw new NoOffsetForPartitionException("No offset is set and no reset policy is defined"); |
||||
|
||||
log.debug("Resetting offset for partition {} to {} offset.", partition, this.offsetResetStrategy.name() |
||||
.toLowerCase()); |
||||
this.subscriptions.seek(partition, offsetBefore(partition, timestamp)); |
||||
} |
||||
|
||||
/** |
||||
* Fetch a single offset before the given timestamp for the partition. |
||||
* |
||||
* @param topicPartition The partition that needs fetching offset. |
||||
* @param timestamp The timestamp for fetching offset. |
||||
* @return The offset of the message that is published before the given timestamp |
||||
*/ |
||||
public long offsetBefore(TopicPartition topicPartition, long timestamp) { |
||||
log.debug("Fetching offsets for partition {}.", topicPartition); |
||||
Map<TopicPartition, ListOffsetRequest.PartitionData> partitions = new HashMap<TopicPartition, ListOffsetRequest.PartitionData>(1); |
||||
partitions.put(topicPartition, new ListOffsetRequest.PartitionData(timestamp, 1)); |
||||
while (true) { |
||||
long now = time.milliseconds(); |
||||
PartitionInfo info = metadata.fetch().partition(topicPartition); |
||||
if (info == null) { |
||||
metadata.add(topicPartition.topic()); |
||||
log.debug("Partition {} is unknown for fetching offset, wait for metadata refresh", topicPartition); |
||||
awaitMetadataUpdate(); |
||||
} else if (info.leader() == null) { |
||||
log.debug("Leader for partition {} unavailable for fetching offset, wait for metadata refresh", topicPartition); |
||||
awaitMetadataUpdate(); |
||||
} else if (this.client.ready(info.leader(), now)) { |
||||
Node node = info.leader(); |
||||
ListOffsetRequest request = new ListOffsetRequest(-1, partitions); |
||||
RequestSend send = new RequestSend(node.id(), |
||||
this.client.nextRequestHeader(ApiKeys.LIST_OFFSETS), |
||||
request.toStruct()); |
||||
ClientRequest clientRequest = new ClientRequest(now, true, send, null); |
||||
this.client.send(clientRequest); |
||||
List<ClientResponse> responses = this.client.completeAll(node.id(), now); |
||||
if (responses.isEmpty()) |
||||
throw new IllegalStateException("This should not happen."); |
||||
ClientResponse response = responses.get(responses.size() - 1); |
||||
if (response.wasDisconnected()) { |
||||
awaitMetadataUpdate(); |
||||
} else { |
||||
ListOffsetResponse lor = new ListOffsetResponse(response.responseBody()); |
||||
short errorCode = lor.responseData().get(topicPartition).errorCode; |
||||
if (errorCode == Errors.NONE.code()) { |
||||
List<Long> offsets = lor.responseData().get(topicPartition).offsets; |
||||
if (offsets.size() != 1) |
||||
throw new IllegalStateException("This should not happen."); |
||||
long offset = offsets.get(0); |
||||
log.debug("Fetched offset {} for partition {}", offset, topicPartition); |
||||
return offset; |
||||
} else if (errorCode == Errors.NOT_LEADER_FOR_PARTITION.code() |
||||
|| errorCode == Errors.LEADER_NOT_AVAILABLE.code()) { |
||||
log.warn("Attempt to fetch offsets for partition {} failed due to obsolete leadership information, retrying.", |
||||
topicPartition); |
||||
awaitMetadataUpdate(); |
||||
} else { |
||||
Errors.forCode(errorCode).maybeThrow(); |
||||
} |
||||
} |
||||
} else { |
||||
log.debug("Leader for partition {} is not ready, retry fetching offsets", topicPartition); |
||||
client.poll(this.retryBackoffMs, now); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Create fetch requests for all nodes for which we have assigned partitions |
||||
* that have no existing requests in flight. |
||||
*/ |
||||
private List<ClientRequest> createFetchRequests(Cluster cluster) { |
||||
// create the fetch info
|
||||
Map<Integer, Map<TopicPartition, FetchRequest.PartitionData>> fetchable = new HashMap<Integer, Map<TopicPartition, FetchRequest.PartitionData>>(); |
||||
for (TopicPartition partition : subscriptions.assignedPartitions()) { |
||||
Node node = cluster.leaderFor(partition); |
||||
// if there is a leader and no in-flight requests, issue a new fetch
|
||||
if (node != null && this.client.inFlightRequestCount(node.id()) == 0) { |
||||
Map<TopicPartition, FetchRequest.PartitionData> fetch = fetchable.get(node); |
||||
if (fetch == null) { |
||||
fetch = new HashMap<TopicPartition, FetchRequest.PartitionData>(); |
||||
fetchable.put(node.id(), fetch); |
||||
} |
||||
long offset = this.subscriptions.fetched(partition); |
||||
fetch.put(partition, new FetchRequest.PartitionData(offset, this.fetchSize)); |
||||
} |
||||
} |
||||
|
||||
// create the requests
|
||||
List<ClientRequest> requests = new ArrayList<ClientRequest>(fetchable.size()); |
||||
for (Map.Entry<Integer, Map<TopicPartition, FetchRequest.PartitionData>> entry : fetchable.entrySet()) { |
||||
int nodeId = entry.getKey(); |
||||
final FetchRequest fetch = new FetchRequest(this.maxWaitMs, this.minBytes, entry.getValue()); |
||||
RequestSend send = new RequestSend(nodeId, this.client.nextRequestHeader(ApiKeys.FETCH), fetch.toStruct()); |
||||
RequestCompletionHandler handler = new RequestCompletionHandler() { |
||||
public void onComplete(ClientResponse response) { |
||||
handleFetchResponse(response, fetch); |
||||
} |
||||
}; |
||||
requests.add(new ClientRequest(time.milliseconds(), true, send, handler)); |
||||
} |
||||
return requests; |
||||
} |
||||
|
||||
/** |
||||
* The callback for fetch completion |
||||
*/ |
||||
private void handleFetchResponse(ClientResponse resp, FetchRequest request) { |
||||
if (resp.wasDisconnected()) { |
||||
int correlation = resp.request().request().header().correlationId(); |
||||
log.debug("Cancelled fetch request {} with correlation id {} due to node {} being disconnected", |
||||
resp.request(), correlation, resp.request().request().destination()); |
||||
} else { |
||||
int totalBytes = 0; |
||||
int totalCount = 0; |
||||
FetchResponse response = new FetchResponse(resp.responseBody()); |
||||
for (Map.Entry<TopicPartition, FetchResponse.PartitionData> entry : response.responseData().entrySet()) { |
||||
TopicPartition tp = entry.getKey(); |
||||
FetchResponse.PartitionData partition = entry.getValue(); |
||||
if (!subscriptions.assignedPartitions().contains(tp)) { |
||||
log.debug("Ignoring fetched data for partition {} which is no longer assigned.", tp); |
||||
} else if (partition.errorCode == Errors.NONE.code()) { |
||||
int bytes = 0; |
||||
ByteBuffer buffer = partition.recordSet; |
||||
MemoryRecords records = MemoryRecords.readableRecords(buffer); |
||||
long fetchOffset = request.fetchData().get(tp).offset; |
||||
List<ConsumerRecord<K, V>> parsed = new ArrayList<ConsumerRecord<K, V>>(); |
||||
for (LogEntry logEntry : records) { |
||||
parsed.add(parseRecord(tp, logEntry)); |
||||
bytes += logEntry.size(); |
||||
} |
||||
if (parsed.size() > 0) { |
||||
ConsumerRecord<K, V> record = parsed.get(parsed.size() - 1); |
||||
this.subscriptions.fetched(tp, record.offset() + 1); |
||||
this.records.add(new PartitionRecords<K, V>(fetchOffset, tp, parsed)); |
||||
this.sensors.recordsFetchLag.record(partition.highWatermark - record.offset()); |
||||
} |
||||
this.sensors.recordTopicFetchMetrics(tp.topic(), bytes, parsed.size()); |
||||
totalBytes += bytes; |
||||
totalCount += parsed.size(); |
||||
} else if (partition.errorCode == Errors.NOT_LEADER_FOR_PARTITION.code() |
||||
|| partition.errorCode == Errors.UNKNOWN_TOPIC_OR_PARTITION.code()) { |
||||
this.metadata.requestUpdate(); |
||||
} else if (partition.errorCode == Errors.OFFSET_OUT_OF_RANGE.code()) { |
||||
// TODO: this could be optimized by grouping all out-of-range partitions
|
||||
log.info("Fetch offset {} is out of range, resetting offset", subscriptions.fetched(tp)); |
||||
resetOffset(tp); |
||||
} else if (partition.errorCode == Errors.UNKNOWN.code()) { |
||||
log.warn("Unknown error fetching data for topic-partition {}", tp); |
||||
} else { |
||||
throw new IllegalStateException("Unexpected error code " + partition.errorCode + " while fetching data"); |
||||
} |
||||
} |
||||
this.sensors.bytesFetched.record(totalBytes); |
||||
this.sensors.recordsFetched.record(totalCount); |
||||
} |
||||
this.sensors.fetchLatency.record(resp.requestLatencyMs()); |
||||
} |
||||
|
||||
/** |
||||
* Parse the record entry, deserializing the key / value fields if necessary |
||||
*/ |
||||
private ConsumerRecord<K, V> parseRecord(TopicPartition partition, LogEntry logEntry) { |
||||
if (this.checkCrcs) |
||||
logEntry.record().ensureValid(); |
||||
|
||||
long offset = logEntry.offset(); |
||||
ByteBuffer keyBytes = logEntry.record().key(); |
||||
K key = keyBytes == null ? null : this.keyDeserializer.deserialize(partition.topic(), Utils.toArray(keyBytes)); |
||||
ByteBuffer valueBytes = logEntry.record().value(); |
||||
V value = valueBytes == null ? null : this.valueDeserializer.deserialize(partition.topic(), Utils.toArray(valueBytes)); |
||||
|
||||
return new ConsumerRecord<K, V>(partition.topic(), partition.partition(), offset, key, value); |
||||
} |
||||
|
||||
/* |
||||
* Request a metadata update and wait until it has occurred |
||||
*/ |
||||
private void awaitMetadataUpdate() { |
||||
int version = this.metadata.requestUpdate(); |
||||
do { |
||||
long now = time.milliseconds(); |
||||
this.client.poll(this.retryBackoffMs, now); |
||||
} while (this.metadata.version() == version); |
||||
} |
||||
|
||||
private static class PartitionRecords<K, V> { |
||||
public long fetchOffset; |
||||
public TopicPartition partition; |
||||
public List<ConsumerRecord<K, V>> records; |
||||
|
||||
public PartitionRecords(long fetchOffset, TopicPartition partition, List<ConsumerRecord<K, V>> records) { |
||||
this.fetchOffset = fetchOffset; |
||||
this.partition = partition; |
||||
this.records = records; |
||||
} |
||||
} |
||||
|
||||
private static enum AutoOffsetResetStrategy { |
||||
LATEST, EARLIEST, NONE |
||||
} |
||||
|
||||
private class FetchManagerMetrics { |
||||
public final Metrics metrics; |
||||
public final String metricGrpName; |
||||
|
||||
public final Sensor bytesFetched; |
||||
public final Sensor recordsFetched; |
||||
public final Sensor fetchLatency; |
||||
public final Sensor recordsFetchLag; |
||||
|
||||
|
||||
public FetchManagerMetrics(Metrics metrics, String metricGrpPrefix, Map<String, String> tags) { |
||||
this.metrics = metrics; |
||||
this.metricGrpName = metricGrpPrefix + "-fetch-manager-metrics"; |
||||
|
||||
this.bytesFetched = metrics.sensor("bytes-fetched"); |
||||
this.bytesFetched.add(new MetricName("fetch-size-avg", |
||||
this.metricGrpName, |
||||
"The average number of bytes fetched per request", |
||||
tags), new Avg()); |
||||
this.bytesFetched.add(new MetricName("fetch-size-max", |
||||
this.metricGrpName, |
||||
"The maximum number of bytes fetched per request", |
||||
tags), new Max()); |
||||
this.bytesFetched.add(new MetricName("bytes-consumed-rate", |
||||
this.metricGrpName, |
||||
"The average number of bytes consumed per second", |
||||
tags), new Rate()); |
||||
|
||||
this.recordsFetched = metrics.sensor("records-fetched"); |
||||
this.recordsFetched.add(new MetricName("records-per-request-avg", |
||||
this.metricGrpName, |
||||
"The average number of records in each request", |
||||
tags), new Avg()); |
||||
this.recordsFetched.add(new MetricName("records-consumed-rate", |
||||
this.metricGrpName, |
||||
"The average number of records consumed per second", |
||||
tags), new Rate()); |
||||
|
||||
this.fetchLatency = metrics.sensor("fetch-latency"); |
||||
this.fetchLatency.add(new MetricName("fetch-latency-avg", |
||||
this.metricGrpName, |
||||
"The average time taken for a fetch request.", |
||||
tags), new Avg()); |
||||
this.fetchLatency.add(new MetricName("fetch-latency-max", |
||||
this.metricGrpName, |
||||
"The max time taken for any fetch request.", |
||||
tags), new Max()); |
||||
this.fetchLatency.add(new MetricName("fetch-rate", |
||||
this.metricGrpName, |
||||
"The number of fetch requests per second.", |
||||
tags), new Rate(new Count())); |
||||
|
||||
this.recordsFetchLag = metrics.sensor("records-lag"); |
||||
this.recordsFetchLag.add(new MetricName("records-lag-max", |
||||
this.metricGrpName, |
||||
"The maximum lag in terms of number of records for any partition in this window", |
||||
tags), new Max()); |
||||
} |
||||
|
||||
public void recordTopicFetchMetrics(String topic, int bytes, int records) { |
||||
// record bytes fetched
|
||||
String name = "topic." + topic + ".bytes-fetched"; |
||||
Sensor bytesFetched = this.metrics.getSensor(name); |
||||
if (bytesFetched == null) |
||||
bytesFetched = this.metrics.sensor(name); |
||||
bytesFetched.record(bytes); |
||||
|
||||
// record records fetched
|
||||
name = "topic." + topic + ".records-fetched"; |
||||
Sensor recordsFetched = this.metrics.getSensor(name); |
||||
if (recordsFetched == null) |
||||
recordsFetched = this.metrics.sensor(name); |
||||
recordsFetched.record(records); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,284 @@
@@ -0,0 +1,284 @@
|
||||
/** |
||||
* 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.clients.consumer.internals; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertTrue; |
||||
|
||||
import org.apache.kafka.clients.Metadata; |
||||
import org.apache.kafka.clients.MockClient; |
||||
import org.apache.kafka.common.Cluster; |
||||
import org.apache.kafka.common.Node; |
||||
import org.apache.kafka.common.TopicPartition; |
||||
import org.apache.kafka.common.metrics.Metrics; |
||||
import org.apache.kafka.common.protocol.Errors; |
||||
import org.apache.kafka.common.protocol.types.Struct; |
||||
import org.apache.kafka.common.requests.ConsumerMetadataResponse; |
||||
import org.apache.kafka.common.requests.HeartbeatResponse; |
||||
import org.apache.kafka.common.requests.JoinGroupResponse; |
||||
import org.apache.kafka.common.requests.OffsetCommitResponse; |
||||
import org.apache.kafka.common.requests.OffsetFetchResponse; |
||||
import org.apache.kafka.common.utils.MockTime; |
||||
import org.apache.kafka.test.TestUtils; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
|
||||
public class CoordinatorTest { |
||||
|
||||
private String topicName = "test"; |
||||
private String groupId = "test-group"; |
||||
private TopicPartition tp = new TopicPartition(topicName, 0); |
||||
private long retryBackoffMs = 0L; |
||||
private long sessionTimeoutMs = 10L; |
||||
private String rebalanceStrategy = "not-matter"; |
||||
private MockTime time = new MockTime(); |
||||
private MockClient client = new MockClient(time); |
||||
private Metadata metadata = new Metadata(0, Long.MAX_VALUE); |
||||
private Cluster cluster = TestUtils.singletonCluster(topicName, 1); |
||||
private Node node = cluster.nodes().get(0); |
||||
private SubscriptionState subscriptions = new SubscriptionState(); |
||||
private Metrics metrics = new Metrics(time); |
||||
private Map<String, String> metricTags = new LinkedHashMap<String, String>(); |
||||
|
||||
private Coordinator coordinator = new Coordinator(client, |
||||
groupId, |
||||
retryBackoffMs, |
||||
sessionTimeoutMs, |
||||
rebalanceStrategy, |
||||
metadata, |
||||
subscriptions, |
||||
metrics, |
||||
"consumer" + groupId, |
||||
metricTags, |
||||
time); |
||||
|
||||
@Before |
||||
public void setup() { |
||||
metadata.update(cluster, time.milliseconds()); |
||||
client.setNode(node); |
||||
} |
||||
|
||||
@Test |
||||
public void testNormalHeartbeat() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// normal heartbeat
|
||||
time.sleep(sessionTimeoutMs); |
||||
coordinator.maybeHeartbeat(time.milliseconds()); // should send out the heartbeat
|
||||
assertEquals(1, client.inFlightRequestCount()); |
||||
client.respond(heartbeatResponse(Errors.NONE.code())); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
} |
||||
|
||||
@Test |
||||
public void testCoordinatorNotAvailable() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// consumer_coordinator_not_available will mark coordinator as unknown
|
||||
time.sleep(sessionTimeoutMs); |
||||
coordinator.maybeHeartbeat(time.milliseconds()); // should send out the heartbeat
|
||||
assertEquals(1, client.inFlightRequestCount()); |
||||
client.respond(heartbeatResponse(Errors.CONSUMER_COORDINATOR_NOT_AVAILABLE.code())); |
||||
time.sleep(sessionTimeoutMs); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
assertTrue(coordinator.coordinatorUnknown()); |
||||
} |
||||
|
||||
@Test |
||||
public void testNotCoordinator() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// not_coordinator will mark coordinator as unknown
|
||||
time.sleep(sessionTimeoutMs); |
||||
coordinator.maybeHeartbeat(time.milliseconds()); // should send out the heartbeat
|
||||
assertEquals(1, client.inFlightRequestCount()); |
||||
client.respond(heartbeatResponse(Errors.NOT_COORDINATOR_FOR_CONSUMER.code())); |
||||
time.sleep(sessionTimeoutMs); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
assertTrue(coordinator.coordinatorUnknown()); |
||||
} |
||||
|
||||
@Test |
||||
public void testIllegalGeneration() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// illegal_generation will cause re-partition
|
||||
subscriptions.subscribe(topicName); |
||||
subscriptions.changePartitionAssignment(Collections.singletonList(tp)); |
||||
|
||||
time.sleep(sessionTimeoutMs); |
||||
coordinator.maybeHeartbeat(time.milliseconds()); // should send out the heartbeat
|
||||
assertEquals(1, client.inFlightRequestCount()); |
||||
client.respond(heartbeatResponse(Errors.ILLEGAL_GENERATION.code())); |
||||
time.sleep(sessionTimeoutMs); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
assertTrue(subscriptions.partitionAssignmentNeeded()); |
||||
} |
||||
|
||||
@Test |
||||
public void testCoordinatorDisconnect() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// coordinator disconnect will mark coordinator as unknown
|
||||
time.sleep(sessionTimeoutMs); |
||||
coordinator.maybeHeartbeat(time.milliseconds()); // should send out the heartbeat
|
||||
assertEquals(1, client.inFlightRequestCount()); |
||||
client.respond(heartbeatResponse(Errors.NONE.code()), true); // return disconnected
|
||||
time.sleep(sessionTimeoutMs); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
assertTrue(coordinator.coordinatorUnknown()); |
||||
} |
||||
|
||||
@Test |
||||
public void testNormalJoinGroup() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// normal join group
|
||||
client.prepareResponse(joinGroupResponse(1, "consumer", Collections.singletonList(tp), Errors.NONE.code())); |
||||
assertEquals(Collections.singletonList(tp), |
||||
coordinator.assignPartitions(Collections.singletonList(topicName), time.milliseconds())); |
||||
assertEquals(0, client.inFlightRequestCount()); |
||||
} |
||||
|
||||
@Test |
||||
public void testReJoinGroup() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// diconnected from original coordinator will cause re-discover and join again
|
||||
client.prepareResponse(joinGroupResponse(1, "consumer", Collections.singletonList(tp), Errors.NONE.code()), true); |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
client.prepareResponse(joinGroupResponse(1, "consumer", Collections.singletonList(tp), Errors.NONE.code())); |
||||
assertEquals(Collections.singletonList(tp), |
||||
coordinator.assignPartitions(Collections.singletonList(topicName), time.milliseconds())); |
||||
assertEquals(0, client.inFlightRequestCount()); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void testCommitOffsetNormal() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// sync commit
|
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.NONE.code()))); |
||||
coordinator.commitOffsets(Collections.singletonMap(tp, 100L), true, time.milliseconds()); |
||||
|
||||
// async commit
|
||||
coordinator.commitOffsets(Collections.singletonMap(tp, 100L), false, time.milliseconds()); |
||||
client.respond(offsetCommitResponse(Collections.singletonMap(tp, Errors.NONE.code()))); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
} |
||||
|
||||
@Test |
||||
public void testCommitOffsetError() { |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// async commit with coordinator not available
|
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.CONSUMER_COORDINATOR_NOT_AVAILABLE.code()))); |
||||
coordinator.commitOffsets(Collections.singletonMap(tp, 100L), false, time.milliseconds()); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
assertTrue(coordinator.coordinatorUnknown()); |
||||
// resume
|
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// async commit with not coordinator
|
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.NOT_COORDINATOR_FOR_CONSUMER.code()))); |
||||
coordinator.commitOffsets(Collections.singletonMap(tp, 100L), false, time.milliseconds()); |
||||
assertEquals(1, client.poll(0, time.milliseconds()).size()); |
||||
assertTrue(coordinator.coordinatorUnknown()); |
||||
// resume
|
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// sync commit with not_coordinator
|
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.NOT_COORDINATOR_FOR_CONSUMER.code()))); |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.NONE.code()))); |
||||
coordinator.commitOffsets(Collections.singletonMap(tp, 100L), true, time.milliseconds()); |
||||
|
||||
// sync commit with coordinator disconnected
|
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.NONE.code())), true); |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
client.prepareResponse(offsetCommitResponse(Collections.singletonMap(tp, Errors.NONE.code()))); |
||||
coordinator.commitOffsets(Collections.singletonMap(tp, 100L), true, time.milliseconds()); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
public void testFetchOffset() { |
||||
|
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
|
||||
// normal fetch
|
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.NONE.code(), "", 100L)); |
||||
assertEquals(100L, (long) coordinator.fetchOffsets(Collections.singleton(tp), time.milliseconds()).get(tp)); |
||||
|
||||
// fetch with loading in progress
|
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.OFFSET_LOAD_IN_PROGRESS.code(), "", 100L)); |
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.NONE.code(), "", 100L)); |
||||
assertEquals(100L, (long) coordinator.fetchOffsets(Collections.singleton(tp), time.milliseconds()).get(tp)); |
||||
|
||||
// fetch with not coordinator
|
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.NOT_COORDINATOR_FOR_CONSUMER.code(), "", 100L)); |
||||
client.prepareResponse(consumerMetadataResponse(node, Errors.NONE.code())); |
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.NONE.code(), "", 100L)); |
||||
assertEquals(100L, (long) coordinator.fetchOffsets(Collections.singleton(tp), time.milliseconds()).get(tp)); |
||||
|
||||
// fetch with no fetchable offsets
|
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.NO_OFFSETS_FETCHABLE.code(), "", 100L)); |
||||
assertEquals(0, coordinator.fetchOffsets(Collections.singleton(tp), time.milliseconds()).size()); |
||||
|
||||
// fetch with offset topic unknown
|
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.UNKNOWN_TOPIC_OR_PARTITION.code(), "", 100L)); |
||||
assertEquals(0, coordinator.fetchOffsets(Collections.singleton(tp), time.milliseconds()).size()); |
||||
|
||||
// fetch with offset -1
|
||||
client.prepareResponse(offsetFetchResponse(tp, Errors.NONE.code(), "", -1L)); |
||||
assertEquals(0, coordinator.fetchOffsets(Collections.singleton(tp), time.milliseconds()).size()); |
||||
} |
||||
|
||||
private Struct consumerMetadataResponse(Node node, short error) { |
||||
ConsumerMetadataResponse response = new ConsumerMetadataResponse(error, node); |
||||
return response.toStruct(); |
||||
} |
||||
|
||||
private Struct heartbeatResponse(short error) { |
||||
HeartbeatResponse response = new HeartbeatResponse(error); |
||||
return response.toStruct(); |
||||
} |
||||
|
||||
private Struct joinGroupResponse(int generationId, String consumerId, List<TopicPartition> assignedPartitions, short error) { |
||||
JoinGroupResponse response = new JoinGroupResponse(error, generationId, consumerId, assignedPartitions); |
||||
return response.toStruct(); |
||||
} |
||||
|
||||
private Struct offsetCommitResponse(Map<TopicPartition, Short> responseData) { |
||||
OffsetCommitResponse response = new OffsetCommitResponse(responseData); |
||||
return response.toStruct(); |
||||
} |
||||
|
||||
private Struct offsetFetchResponse(TopicPartition tp, Short error, String metadata, long offset) { |
||||
OffsetFetchResponse.PartitionData data = new OffsetFetchResponse.PartitionData(offset, metadata, error); |
||||
OffsetFetchResponse response = new OffsetFetchResponse(Collections.singletonMap(tp, data)); |
||||
return response.toStruct(); |
||||
} |
||||
} |
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
/** |
||||
* 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.clients.consumer.internals; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
import org.apache.kafka.clients.Metadata; |
||||
import org.apache.kafka.clients.MockClient; |
||||
import org.apache.kafka.clients.consumer.ConsumerRecord; |
||||
import org.apache.kafka.common.Cluster; |
||||
import org.apache.kafka.common.Node; |
||||
import org.apache.kafka.common.TopicPartition; |
||||
import org.apache.kafka.common.metrics.Metrics; |
||||
import org.apache.kafka.common.protocol.Errors; |
||||
import org.apache.kafka.common.protocol.types.Struct; |
||||
import org.apache.kafka.common.record.CompressionType; |
||||
import org.apache.kafka.common.record.MemoryRecords; |
||||
import org.apache.kafka.common.requests.FetchResponse; |
||||
import org.apache.kafka.common.requests.ListOffsetResponse; |
||||
import org.apache.kafka.common.serialization.ByteArrayDeserializer; |
||||
import org.apache.kafka.common.utils.MockTime; |
||||
import org.apache.kafka.test.TestUtils; |
||||
|
||||
import java.nio.ByteBuffer; |
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class FetcherTest { |
||||
|
||||
private String topicName = "test"; |
||||
private String groupId = "test-group"; |
||||
private TopicPartition tp = new TopicPartition(topicName, 0); |
||||
private long retryBackoffMs = 0L; |
||||
private int minBytes = 1; |
||||
private int maxWaitMs = 0; |
||||
private int fetchSize = 1000; |
||||
private String offsetReset = "EARLIEST"; |
||||
private MockTime time = new MockTime(); |
||||
private MockClient client = new MockClient(time); |
||||
private Metadata metadata = new Metadata(0, Long.MAX_VALUE); |
||||
private Cluster cluster = TestUtils.singletonCluster(topicName, 1); |
||||
private Node node = cluster.nodes().get(0); |
||||
private SubscriptionState subscriptions = new SubscriptionState(); |
||||
private Metrics metrics = new Metrics(time); |
||||
private Map<String, String> metricTags = new LinkedHashMap<String, String>(); |
||||
|
||||
private MemoryRecords records = MemoryRecords.emptyRecords(ByteBuffer.allocate(1024), CompressionType.NONE); |
||||
|
||||
private Fetcher<byte[], byte[]> fetcher = new Fetcher<byte[], byte[]>(client, |
||||
retryBackoffMs, |
||||
minBytes, |
||||
maxWaitMs, |
||||
fetchSize, |
||||
true, // check crc
|
||||
offsetReset, |
||||
new ByteArrayDeserializer(), |
||||
new ByteArrayDeserializer(), |
||||
metadata, |
||||
subscriptions, |
||||
metrics, |
||||
"consumer" + groupId, |
||||
metricTags, |
||||
time); |
||||
|
||||
@Before |
||||
public void setup() throws Exception { |
||||
metadata.update(cluster, time.milliseconds()); |
||||
client.setNode(node); |
||||
|
||||
records.append(1L, "key".getBytes(), "value-1".getBytes()); |
||||
records.append(2L, "key".getBytes(), "value-2".getBytes()); |
||||
records.append(3L, "key".getBytes(), "value-3".getBytes()); |
||||
records.close(); |
||||
records.rewind(); |
||||
} |
||||
|
||||
@Test |
||||
public void testFetchNormal() { |
||||
List<ConsumerRecord<byte[], byte[]>> records; |
||||
subscriptions.subscribe(tp); |
||||
subscriptions.fetched(tp, 0); |
||||
subscriptions.consumed(tp, 0); |
||||
|
||||
// normal fetch
|
||||
fetcher.initFetches(cluster, time.milliseconds()); |
||||
client.respond(fetchResponse(this.records.buffer(), Errors.NONE.code(), 100L)); |
||||
client.poll(0, time.milliseconds()); |
||||
records = fetcher.fetchedRecords().get(tp); |
||||
assertEquals(3, records.size()); |
||||
assertEquals(4L, (long) subscriptions.fetched(tp)); // this is the next fetching position
|
||||
assertEquals(4L, (long) subscriptions.consumed(tp)); |
||||
long offset = 1; |
||||
for (ConsumerRecord<byte[], byte[]> record : records) { |
||||
assertEquals(offset, record.offset()); |
||||
offset += 1; |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void testFetchFailed() { |
||||
List<ConsumerRecord<byte[], byte[]>> records; |
||||
subscriptions.subscribe(tp); |
||||
subscriptions.fetched(tp, 0); |
||||
subscriptions.consumed(tp, 0); |
||||
|
||||
// fetch with not leader
|
||||
fetcher.initFetches(cluster, time.milliseconds()); |
||||
client.respond(fetchResponse(this.records.buffer(), Errors.NOT_LEADER_FOR_PARTITION.code(), 100L)); |
||||
client.poll(0, time.milliseconds()); |
||||
assertEquals(0, fetcher.fetchedRecords().size()); |
||||
assertEquals(0L, metadata.timeToNextUpdate(time.milliseconds())); |
||||
|
||||
// fetch with unknown topic partition
|
||||
fetcher.initFetches(cluster, time.milliseconds()); |
||||
client.respond(fetchResponse(this.records.buffer(), Errors.UNKNOWN_TOPIC_OR_PARTITION.code(), 100L)); |
||||
client.poll(0, time.milliseconds()); |
||||
assertEquals(0, fetcher.fetchedRecords().size()); |
||||
assertEquals(0L, metadata.timeToNextUpdate(time.milliseconds())); |
||||
|
||||
// fetch with out of range
|
||||
subscriptions.fetched(tp, 5); |
||||
fetcher.initFetches(cluster, time.milliseconds()); |
||||
client.respond(fetchResponse(this.records.buffer(), Errors.OFFSET_OUT_OF_RANGE.code(), 100L)); |
||||
client.prepareResponse(listOffsetResponse(Collections.singletonList(0L), Errors.NONE.code())); |
||||
client.poll(0, time.milliseconds()); |
||||
assertEquals(0, fetcher.fetchedRecords().size()); |
||||
assertEquals(0L, (long) subscriptions.fetched(tp)); |
||||
assertEquals(0L, (long) subscriptions.consumed(tp)); |
||||
} |
||||
|
||||
@Test |
||||
public void testFetchOutOfRange() { |
||||
List<ConsumerRecord<byte[], byte[]>> records; |
||||
subscriptions.subscribe(tp); |
||||
subscriptions.fetched(tp, 5); |
||||
subscriptions.consumed(tp, 5); |
||||
|
||||
// fetch with out of range
|
||||
fetcher.initFetches(cluster, time.milliseconds()); |
||||
client.respond(fetchResponse(this.records.buffer(), Errors.OFFSET_OUT_OF_RANGE.code(), 100L)); |
||||
client.prepareResponse(listOffsetResponse(Collections.singletonList(0L), Errors.NONE.code())); |
||||
client.poll(0, time.milliseconds()); |
||||
assertEquals(0, fetcher.fetchedRecords().size()); |
||||
assertEquals(0L, (long) subscriptions.fetched(tp)); |
||||
assertEquals(0L, (long) subscriptions.consumed(tp)); |
||||
} |
||||
|
||||
private Struct fetchResponse(ByteBuffer buffer, short error, long hw) { |
||||
FetchResponse response = new FetchResponse(Collections.singletonMap(tp, new FetchResponse.PartitionData(error, hw, buffer))); |
||||
return response.toStruct(); |
||||
} |
||||
|
||||
private Struct listOffsetResponse(List<Long> offsets, short error) { |
||||
ListOffsetResponse response = new ListOffsetResponse(Collections.singletonMap(tp, new ListOffsetResponse.PartitionData(error, offsets))); |
||||
return response.toStruct(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
/** |
||||
* 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.clients.consumer.internals; |
||||
|
||||
import org.apache.kafka.common.utils.MockTime; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertTrue; |
||||
|
||||
public class HeartbeatTest { |
||||
|
||||
private long timeout = 300L; |
||||
private MockTime time = new MockTime(); |
||||
private Heartbeat heartbeat = new Heartbeat(timeout, -1L); |
||||
|
||||
@Test |
||||
public void testShouldHeartbeat() { |
||||
heartbeat.sentHeartbeat(time.milliseconds()); |
||||
time.sleep((long) ((float) timeout / Heartbeat.HEARTBEATS_PER_SESSION_INTERVAL * 1.1)); |
||||
assertTrue(heartbeat.shouldHeartbeat(time.milliseconds())); |
||||
} |
||||
|
||||
@Test |
||||
public void testShouldNotHeartbeat() { |
||||
heartbeat.sentHeartbeat(time.milliseconds()); |
||||
time.sleep(timeout / (2 * Heartbeat.HEARTBEATS_PER_SESSION_INTERVAL)); |
||||
assertFalse(heartbeat.shouldHeartbeat(time.milliseconds())); |
||||
} |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* 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 kafka.common |
||||
|
||||
/** |
||||
* Number of insync replicas for the partition is lower than min.insync.replicas |
||||
* This exception is raised when the low ISR size is discovered *after* the message |
||||
* was already appended to the log. Producer retries will cause duplicates. |
||||
*/ |
||||
class NoOffsetsCommittedException(message: String) extends RuntimeException(message) { |
||||
def this() = this(null) |
||||
} |
@ -0,0 +1,301 @@
@@ -0,0 +1,301 @@
|
||||
/** |
||||
* 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 kafka.api |
||||
|
||||
import org.apache.kafka.clients.producer.ProducerConfig |
||||
import org.apache.kafka.clients.producer.ProducerRecord |
||||
import org.apache.kafka.clients.consumer.Consumer |
||||
import org.apache.kafka.clients.consumer.KafkaConsumer |
||||
import org.apache.kafka.clients.consumer.ConsumerRebalanceCallback |
||||
import org.apache.kafka.clients.consumer.ConsumerRecord |
||||
import org.apache.kafka.clients.consumer.ConsumerConfig |
||||
import org.apache.kafka.clients.consumer.CommitType |
||||
import org.apache.kafka.common.serialization.ByteArrayDeserializer |
||||
import org.apache.kafka.common.TopicPartition |
||||
import org.apache.kafka.clients.consumer.NoOffsetForPartitionException |
||||
|
||||
import kafka.utils.{ShutdownableThread, TestUtils, Logging} |
||||
import kafka.server.OffsetManager |
||||
|
||||
import java.util.ArrayList |
||||
import org.junit.Assert._ |
||||
|
||||
import scala.collection.JavaConversions._ |
||||
|
||||
|
||||
/** |
||||
* Integration tests for the new consumer that cover basic usage as well as server failures |
||||
*/ |
||||
class ConsumerTest extends IntegrationTestHarness with Logging { |
||||
|
||||
val producerCount = 1 |
||||
val consumerCount = 2 |
||||
val serverCount = 3 |
||||
|
||||
val topic = "topic" |
||||
val part = 0 |
||||
val tp = new TopicPartition(topic, part) |
||||
|
||||
// configure the servers and clients |
||||
this.serverConfig.setProperty("controlled.shutdown.enable", "false") // speed up shutdown |
||||
this.serverConfig.setProperty("offsets.topic.replication.factor", "3") // don't want to lose offset |
||||
this.serverConfig.setProperty("offsets.topic.num.partitions", "1") |
||||
this.producerConfig.setProperty(ProducerConfig.ACKS_CONFIG, "all") |
||||
this.consumerConfig.setProperty(ConsumerConfig.GROUP_ID_CONFIG, "my-test") |
||||
this.consumerConfig.setProperty(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, 4096.toString) |
||||
this.consumerConfig.setProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") |
||||
|
||||
override def setUp() { |
||||
super.setUp() |
||||
|
||||
// create the test topic with all the brokers as replicas |
||||
TestUtils.createTopic(this.zkClient, topic, 1, serverCount, this.servers) |
||||
} |
||||
|
||||
def testSimpleConsumption() { |
||||
val numRecords = 10000 |
||||
sendRecords(numRecords) |
||||
|
||||
assertEquals(0, this.consumers(0).subscriptions.size) |
||||
this.consumers(0).subscribe(tp) |
||||
assertEquals(1, this.consumers(0).subscriptions.size) |
||||
|
||||
this.consumers(0).seek(tp, 0) |
||||
consumeRecords(this.consumers(0), numRecords = numRecords, startingOffset = 0) |
||||
} |
||||
|
||||
def testAutoOffsetReset() { |
||||
sendRecords(1) |
||||
this.consumers(0).subscribe(tp) |
||||
consumeRecords(this.consumers(0), numRecords = 1, startingOffset = 0) |
||||
} |
||||
|
||||
def testSeek() { |
||||
val consumer = this.consumers(0) |
||||
val totalRecords = 50L |
||||
sendRecords(totalRecords.toInt) |
||||
consumer.subscribe(tp) |
||||
|
||||
consumer.seekToEnd(tp) |
||||
assertEquals(totalRecords, consumer.position(tp)) |
||||
assertFalse(consumer.poll(totalRecords).iterator().hasNext) |
||||
|
||||
consumer.seekToBeginning(tp) |
||||
assertEquals(0, consumer.position(tp), 0) |
||||
consumeRecords(consumer, numRecords = 1, startingOffset = 0) |
||||
|
||||
val mid = totalRecords / 2 |
||||
consumer.seek(tp, mid) |
||||
assertEquals(mid, consumer.position(tp)) |
||||
consumeRecords(consumer, numRecords = 1, startingOffset = mid.toInt) |
||||
} |
||||
|
||||
def testGroupConsumption() { |
||||
sendRecords(10) |
||||
this.consumers(0).subscribe(topic) |
||||
consumeRecords(this.consumers(0), numRecords = 1, startingOffset = 0) |
||||
} |
||||
|
||||
def testPositionAndCommit() { |
||||
sendRecords(5) |
||||
|
||||
// committed() on a partition with no committed offset throws an exception |
||||
intercept[NoOffsetForPartitionException] { |
||||
this.consumers(0).committed(new TopicPartition(topic, 15)) |
||||
} |
||||
|
||||
// position() on a partition that we aren't subscribed to throws an exception |
||||
intercept[IllegalArgumentException] { |
||||
this.consumers(0).position(new TopicPartition(topic, 15)) |
||||
} |
||||
|
||||
this.consumers(0).subscribe(tp) |
||||
|
||||
assertEquals("position() on a partition that we are subscribed to should reset the offset", 0L, this.consumers(0).position(tp)) |
||||
this.consumers(0).commit(CommitType.SYNC) |
||||
assertEquals(0L, this.consumers(0).committed(tp)) |
||||
|
||||
consumeRecords(this.consumers(0), 5, 0) |
||||
assertEquals("After consuming 5 records, position should be 5", 5L, this.consumers(0).position(tp)) |
||||
this.consumers(0).commit(CommitType.SYNC) |
||||
assertEquals("Committed offset should be returned", 5L, this.consumers(0).committed(tp)) |
||||
|
||||
sendRecords(1) |
||||
|
||||
// another consumer in the same group should get the same position |
||||
this.consumers(1).subscribe(tp) |
||||
consumeRecords(this.consumers(1), 1, 5) |
||||
} |
||||
|
||||
def testPartitionsFor() { |
||||
val numParts = 2 |
||||
TestUtils.createTopic(this.zkClient, "part-test", numParts, 1, this.servers) |
||||
val parts = this.consumers(0).partitionsFor("part-test") |
||||
assertNotNull(parts) |
||||
assertEquals(2, parts.length) |
||||
assertNull(this.consumers(0).partitionsFor("non-exist-topic")) |
||||
} |
||||
|
||||
def testConsumptionWithBrokerFailures() = consumeWithBrokerFailures(5) |
||||
|
||||
/* |
||||
* 1. Produce a bunch of messages |
||||
* 2. Then consume the messages while killing and restarting brokers at random |
||||
*/ |
||||
def consumeWithBrokerFailures(numIters: Int) { |
||||
val numRecords = 1000 |
||||
sendRecords(numRecords) |
||||
this.producers.map(_.close) |
||||
|
||||
var consumed = 0 |
||||
val consumer = this.consumers(0) |
||||
consumer.subscribe(topic) |
||||
|
||||
val scheduler = new BounceBrokerScheduler(numIters) |
||||
scheduler.start() |
||||
|
||||
while (scheduler.isRunning.get()) { |
||||
for (record <- consumer.poll(100)) { |
||||
assertEquals(consumed.toLong, record.offset()) |
||||
consumed += 1 |
||||
} |
||||
consumer.commit(CommitType.SYNC) |
||||
|
||||
if (consumed == numRecords) { |
||||
consumer.seekToBeginning() |
||||
consumed = 0 |
||||
} |
||||
} |
||||
scheduler.shutdown() |
||||
} |
||||
|
||||
def testSeekAndCommitWithBrokerFailures() = seekAndCommitWithBrokerFailures(5) |
||||
|
||||
def seekAndCommitWithBrokerFailures(numIters: Int) { |
||||
val numRecords = 1000 |
||||
sendRecords(numRecords) |
||||
this.producers.map(_.close) |
||||
|
||||
val consumer = this.consumers(0) |
||||
consumer.subscribe(tp) |
||||
consumer.seek(tp, 0) |
||||
|
||||
val scheduler = new BounceBrokerScheduler(numIters) |
||||
scheduler.start() |
||||
|
||||
while(scheduler.isRunning.get()) { |
||||
val coin = TestUtils.random.nextInt(3) |
||||
if (coin == 0) { |
||||
info("Seeking to end of log") |
||||
consumer.seekToEnd() |
||||
assertEquals(numRecords.toLong, consumer.position(tp)) |
||||
} else if (coin == 1) { |
||||
val pos = TestUtils.random.nextInt(numRecords).toLong |
||||
info("Seeking to " + pos) |
||||
consumer.seek(tp, pos) |
||||
assertEquals(pos, consumer.position(tp)) |
||||
} else if (coin == 2) { |
||||
info("Committing offset.") |
||||
consumer.commit(CommitType.SYNC) |
||||
assertEquals(consumer.position(tp), consumer.committed(tp)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
def testPartitionReassignmentCallback() { |
||||
val callback = new TestConsumerReassignmentCallback() |
||||
this.consumerConfig.setProperty(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "200"); // timeout quickly to avoid slow test |
||||
val consumer0 = new KafkaConsumer(this.consumerConfig, callback, new ByteArrayDeserializer(), new ByteArrayDeserializer()) |
||||
consumer0.subscribe(topic) |
||||
|
||||
// the initial subscription should cause a callback execution |
||||
while(callback.callsToAssigned == 0) |
||||
consumer0.poll(50) |
||||
|
||||
// get metadata for the topic |
||||
var parts = consumer0.partitionsFor(OffsetManager.OffsetsTopicName) |
||||
while(parts == null) |
||||
parts = consumer0.partitionsFor(OffsetManager.OffsetsTopicName) |
||||
assertEquals(1, parts.size) |
||||
assertNotNull(parts(0).leader()) |
||||
|
||||
// shutdown the coordinator |
||||
val coordinator = parts(0).leader().id() |
||||
this.servers(coordinator).shutdown() |
||||
|
||||
// this should cause another callback execution |
||||
while(callback.callsToAssigned < 2) |
||||
consumer0.poll(50) |
||||
assertEquals(2, callback.callsToAssigned) |
||||
assertEquals(2, callback.callsToRevoked) |
||||
|
||||
consumer0.close() |
||||
} |
||||
|
||||
private class TestConsumerReassignmentCallback extends ConsumerRebalanceCallback { |
||||
var callsToAssigned = 0 |
||||
var callsToRevoked = 0 |
||||
def onPartitionsAssigned(consumer: Consumer[_,_], partitions: java.util.Collection[TopicPartition]) { |
||||
info("onPartitionsAssigned called.") |
||||
callsToAssigned += 1 |
||||
} |
||||
def onPartitionsRevoked(consumer: Consumer[_,_], partitions: java.util.Collection[TopicPartition]) { |
||||
info("onPartitionsRevoked called.") |
||||
callsToRevoked += 1 |
||||
} |
||||
} |
||||
|
||||
private class BounceBrokerScheduler(val numIters: Int) extends ShutdownableThread("daemon-bounce-broker", false) |
||||
{ |
||||
var iter: Int = 0 |
||||
|
||||
override def doWork(): Unit = { |
||||
killRandomBroker() |
||||
restartDeadBrokers() |
||||
|
||||
iter += 1 |
||||
if (iter == numIters) |
||||
initiateShutdown() |
||||
else |
||||
Thread.sleep(500) |
||||
} |
||||
} |
||||
|
||||
private def sendRecords(numRecords: Int) { |
||||
val futures = (0 until numRecords).map { i => |
||||
this.producers(0).send(new ProducerRecord(topic, part, i.toString.getBytes, i.toString.getBytes)) |
||||
} |
||||
futures.map(_.get) |
||||
} |
||||
|
||||
private def consumeRecords(consumer: Consumer[Array[Byte], Array[Byte]], numRecords: Int, startingOffset: Int) { |
||||
val records = new ArrayList[ConsumerRecord[Array[Byte], Array[Byte]]]() |
||||
val maxIters = numRecords * 300 |
||||
var iters = 0 |
||||
while (records.size < numRecords) { |
||||
for (record <- consumer.poll(50)) |
||||
records.add(record) |
||||
if(iters > maxIters) |
||||
throw new IllegalStateException("Failed to consume the expected records after " + iters + " iterations."); |
||||
iters += 1 |
||||
} |
||||
for (i <- 0 until numRecords) { |
||||
val record = records.get(i) |
||||
val offset = startingOffset + i |
||||
assertEquals(topic, record.topic()) |
||||
assertEquals(part, record.partition()) |
||||
assertEquals(offset.toLong, record.offset()) |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue