Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.network.ChannelBuilders;
import org.apache.kafka.common.protocol.SecurityProtocol;
import org.apache.kafka.common.network.ChannelBuilder;
Expand Down Expand Up @@ -79,4 +80,10 @@ public static ChannelBuilder createChannelBuilder(Map<String, ?> configs) {
return ChannelBuilders.create(securityProtocol, SSLFactory.Mode.CLIENT, configs);
}

public static long maybeGetRemainingTime(long start, long now, long timeout, String message) {
long elapsed = now - start;
if (elapsed > timeout)
throw new TimeoutException(message);
else return timeout - elapsed;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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;

import org.apache.kafka.common.TopicPartition;

import java.util.Properties;


public class CommittedTimeoutKafkaConsumer {
public static void main(String[] args) {
KafkaConsumer<String, String> consumer = makeConsumer();
System.out.println("COMMITTED: " + consumer.committed(new TopicPartition("t", 0)));
}

public static KafkaConsumer<String, String> makeConsumer() {
Properties props = new Properties();
props.put("bootstrap.servers", "okaraman-ld1.linkedin.biz:9092");
props.put("group.id", System.currentTimeMillis() + "");
props.put("partition.assignment.strategy", "roundrobin");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("session.timeout.ms", 30 * 1000);
return new KafkaConsumer<>(props);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,7 @@ public OffsetAndMetadata committed(TopicPartition partition) {
committed = this.subscriptions.committed(partition);
}
} else {
Map<TopicPartition, OffsetAndMetadata> offsets = coordinator.fetchCommittedOffsets(Collections.singleton(partition));
Map<TopicPartition, OffsetAndMetadata> offsets = coordinator.fetchCommittedOffsets(Collections.singleton(partition), requestTimeoutMs);
committed = offsets.get(partition);
}

Expand Down Expand Up @@ -1067,7 +1067,7 @@ public List<PartitionInfo> partitionsFor(String topic) {
List<PartitionInfo> parts = cluster.partitionsForTopic(topic);
if (parts == null) {
metadata.add(topic);
client.awaitMetadataUpdate();
client.awaitMetadataUpdate(requestTimeoutMs);
parts = metadata.fetch().partitionsForTopic(topic);
}
return parts;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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;

import java.util.Properties;


public class ListTopicsTimeoutKafkaConsumer {
public static void main(String[] args) {
KafkaConsumer<String, String> consumer = makeConsumer();
System.out.println("LIST TOPICS: " + consumer.listTopics());
}

public static KafkaConsumer<String, String> makeConsumer() {
Properties props = new Properties();
props.put("bootstrap.servers", "okaraman-ld1.linkedin.biz:9092");
props.put("group.id", System.currentTimeMillis() + "");
props.put("partition.assignment.strategy", "roundrobin");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("session.timeout.ms", 30 * 1000);
props.put("enable.auto.commit", false);
props.put("request.timeout.ms", 20 * 1000);
return new KafkaConsumer<>(props);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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;

import java.util.Properties;


public class PartitionsForTimeoutKafkaConsumer {
public static void main(String[] args) {
KafkaConsumer<String, String> consumer = makeConsumer();
System.out.println("PARTITIONS FOR: " + consumer.partitionsFor("t"));
}

public static KafkaConsumer<String, String> makeConsumer() {
Properties props = new Properties();
props.put("bootstrap.servers", "okaraman-ld1.linkedin.biz:9092");
props.put("group.id", System.currentTimeMillis() + "");
props.put("partition.assignment.strategy", "roundrobin");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("session.timeout.ms", 30 * 1000);
props.put("enable.auto.commit", false);
props.put("request.timeout.ms", 20 * 1000);
return new KafkaConsumer<>(props);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.apache.kafka.clients.RequestCompletionHandler;
import org.apache.kafka.clients.consumer.ConsumerWakeupException;
import org.apache.kafka.common.Node;
import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.protocol.ApiKeys;
import org.apache.kafka.common.requests.AbstractRequest;
import org.apache.kafka.common.requests.RequestHeader;
Expand Down Expand Up @@ -126,6 +127,24 @@ public void awaitMetadataUpdate() {
} while (this.metadata.version() == version);
}

/**
* Block until the metadata has been refreshed or the timeout has expired.
* @param timeout The maximum duration (in ms) to wait for the request
* @throws TimeoutException if the timeout has expired
*/
public void awaitMetadataUpdate(long timeout) {
long now = time.milliseconds();
long deadline = now + timeout;
int version = this.metadata.requestUpdate();
while (this.metadata.version() == version && now < deadline) {

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.

One question: what is our plan for cases where the metadata update fails? A few options that I can think of:

  1. Attempt it again until the timeout expires
  2. Throw an exception immediately
  3. Wait until the timeout expires and then throw an exception

If I understand correctly, we are doing 3 at the moment (please correct me if I am wrong). I think 1 or 2 would be better although perhaps more work. We can talk about implementation issues after we decide what we want to do though. cc @hachikuji

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.

The tough part about the consumer is that the handling is context-dependent. If the call to awaitMetadataUpdate happens in KafkaConsumer.poll(), we might want to ignore the timeout and just return an empty record set to the user. If it's a blocking call, we want to throw the exception. I wonder if it would be a good idea to let this variant return a boolean indicating whether the update happened so that the caller can more easily choose what to do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the quick reply @ijuma. Yeah the point you raise is valid.

It looks like KafkaConsumer.listTopics (more specifically, its call to Fetcher.getAllTopics) in this RB is actually doing 1, but KafkaConsumer.partitionsFor (more specifically, its call to ConsumerNetworkClient.awaitMetadataUpdate) is doing 3. Maybe we should make them both act the same.

Also just a heads up, this PR is very much WIP. It only has listTopics and partitionsFor with timeouts so far. I put this PR up partially so that it shows the flaw in using request.timeout.ms as the timeout. From the jira ticket:

  • if request.timeout.ms < session.timeout.ms, then a user may drop JoinGroupResponses which can arrive worst case after session.timeout.ms.
  • if request.timeout.ms > session.timeout.ms, then a user calling one of these blocking calls will lose their session and trigger a rebalance.

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.

Yes, good points @hachikuji and @onurkaraman. By the way, I totally understand that this is a WIP. I just thought it would be worth discussing this sooner rather than later. :)

It does seem that a variant that lets the caller decide what to do would be good to have. Not sure if a boolean is good enough to represent the 3 possible states (or if the callers need to know about the 3 possible states).

poll(deadline - now, now);
now = time.milliseconds();
}

if (this.metadata.version() == version)
throw new TimeoutException("Failed to get metadata after " + timeout + " ms.");
}

/**
* Wakeup an active poll. This will cause the polling thread to throw an exception either
* on the current poll if one is active, or the next poll.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.apache.kafka.clients.consumer.internals;

import org.apache.kafka.clients.ClientResponse;
import org.apache.kafka.clients.ClientUtils;
import org.apache.kafka.clients.consumer.ConsumerRebalanceListener;
import org.apache.kafka.clients.consumer.OffsetAndMetadata;
import org.apache.kafka.clients.consumer.OffsetCommitCallback;
Expand All @@ -21,6 +22,7 @@
import org.apache.kafka.common.Node;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.DisconnectException;
import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.errors.UnknownConsumerIdException;
import org.apache.kafka.common.metrics.Measurable;
import org.apache.kafka.common.metrics.MetricConfig;
Expand Down Expand Up @@ -125,7 +127,7 @@ public Coordinator(ConsumerNetworkClient client,
*/
public void refreshCommittedOffsetsIfNeeded() {
if (subscriptions.refreshCommitsNeeded()) {
Map<TopicPartition, OffsetAndMetadata> offsets = fetchCommittedOffsets(subscriptions.assignedPartitions());
Map<TopicPartition, OffsetAndMetadata> offsets = fetchCommittedOffsets(subscriptions.assignedPartitions(), requestTimeoutMs);
for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : offsets.entrySet()) {
TopicPartition tp = entry.getKey();
// verify assignment is still active
Expand All @@ -141,22 +143,27 @@ public void refreshCommittedOffsetsIfNeeded() {
* @param partitions The partitions to fetch offsets for
* @return A map from partition to the committed offset
*/
public Map<TopicPartition, OffsetAndMetadata> fetchCommittedOffsets(Set<TopicPartition> partitions) {
while (true) {
ensureCoordinatorKnown();
public Map<TopicPartition, OffsetAndMetadata> fetchCommittedOffsets(Set<TopicPartition> partitions, long timeout) {
long start = time.milliseconds();
long deadline = start + timeout;
while (time.milliseconds() < deadline) {
long remainingTime = ClientUtils.maybeGetRemainingTime(start, time.milliseconds(), timeout, "");
ensureCoordinatorKnown(remainingTime);

// contact coordinator to fetch committed offsets
RequestFuture<Map<TopicPartition, OffsetAndMetadata>> future = sendOffsetFetchRequest(partitions);
client.poll(future);
remainingTime = ClientUtils.maybeGetRemainingTime(start, time.milliseconds(), timeout, "");
client.poll(future, remainingTime);

if (future.succeeded())
return future.value();

if (!future.isRetriable())
else if (future.failed() && !future.isRetriable())
throw future.exception();

Utils.sleep(retryBackoffMs);
}
throw new TimeoutException("foo");
}

/**
Expand Down Expand Up @@ -231,6 +238,15 @@ public void ensureCoordinatorKnown() {
}
}

public void ensureCoordinatorKnown(long timeout) {
long now = time.milliseconds();
long deadline = now + timeout;
while (coordinatorUnknown() && now < deadline) {
RequestFuture<Void> future = sendConsumerMetadataRequest();
client.poll(future, requestTimeoutMs);
}
throw new TimeoutException("foo");
}

@Override
public void close() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.kafka.common.errors.DisconnectException;
import org.apache.kafka.common.errors.InvalidMetadataException;
import org.apache.kafka.common.errors.OffsetOutOfRangeException;
import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.metrics.Sensor;
import org.apache.kafka.common.metrics.stats.Avg;
Expand Down Expand Up @@ -173,15 +174,15 @@ public void updateFetchPositions(Set<TopicPartition> partitions) {
* @return The map of topics and its partitions
*/
public Map<String, List<PartitionInfo>> getAllTopics(long timeout) {
final HashMap<String, List<PartitionInfo>> topicsPartitionInfos = new HashMap<>();
long startTime = time.milliseconds();

while (time.milliseconds() - startTime < timeout) {
RequestFuture<ClientResponse> requestFuture = sendMetadataRequest();
if (requestFuture != null) {
client.poll(requestFuture);
client.poll(requestFuture, timeout);

if (requestFuture.succeeded()) {
HashMap<String, List<PartitionInfo>> topicsPartitionInfos = new HashMap<>();
MetadataResponse response =
new MetadataResponse(requestFuture.value().responseBody());

Expand All @@ -190,16 +191,15 @@ public Map<String, List<PartitionInfo>> getAllTopics(long timeout) {
topic, response.cluster().availablePartitionsForTopic(topic));

return topicsPartitionInfos;
}

if (!requestFuture.isRetriable())
} else if (requestFuture.failed() && !requestFuture.isRetriable()) {
throw requestFuture.exception();
}
}

Utils.sleep(retryBackoffMs);
}

return topicsPartitionInfos;
throw new TimeoutException("Failed to get metadata for all topics after " + timeout + " ms.");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.apache.kafka.common.PartitionInfo;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.errors.OffsetOutOfRangeException;
import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.common.metrics.KafkaMetric;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.protocol.Errors;
Expand Down Expand Up @@ -330,6 +331,11 @@ public void testGetAllTopics() throws InterruptedException {
assertEquals(cluster.topics().size(), allTopics.size());
}

@Test(expected = TimeoutException.class)
public void testGetAllTopicsTimeout() {
fetcher.getAllTopics(0);
}

/*
* Send multiple requests. Verify that the client side quota metrics have the right values
*/
Expand Down