Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4f4cb59
initial commit - wip
philipnee Feb 16, 2022
d902d73
wip - tests
philipnee Feb 18, 2022
e1eed37
Clean up commits
philipnee Feb 22, 2022
411d330
RetryUtil
philipnee Feb 22, 2022
e77aaa7
clean up
philipnee Feb 22, 2022
03e67c7
clean up tests
philipnee Feb 22, 2022
8d2e324
Revert KAFKA-12339 (#10152)
philipnee Feb 23, 2022
074521d
Address PR comments
philipnee Feb 25, 2022
1da527f
PR comment
philipnee Feb 25, 2022
dd636a3
Update PR
philipnee Feb 25, 2022
93202cc
PR Revision
philipnee Feb 25, 2022
5ab4d6a
Update connect/runtime/src/main/java/org/apache/kafka/connect/util/Re…
philipnee Feb 28, 2022
5e5c6a3
Apply suggestions from code review
philipnee Mar 2, 2022
a5390fa
Retry until timeout instead of exhausting number of retries
philipnee Mar 2, 2022
6821683
Apply suggestions from code review
philipnee Mar 3, 2022
e46f3f8
Added message supplier to supply custom message
philipnee Mar 3, 2022
b53f372
less retries to speed up the process
philipnee Mar 3, 2022
01c0a87
timeout test if unable to complete within a reasonable timeframe
philipnee Mar 3, 2022
0b6da79
Clean up the documentation
philipnee Mar 3, 2022
ec89772
Apply suggestions from code review
philipnee Mar 3, 2022
669feee
retryEndOffsets should be public
philipnee Mar 3, 2022
5b8ff9f
update retryEndOffsets
philipnee Mar 3, 2022
73deef2
correcting test
philipnee Mar 3, 2022
298fdc9
validating retry and timeout
philipnee Mar 4, 2022
6170bea
Apply suggestions from code review
philipnee Mar 4, 2022
1634208
make warning message debug instead
philipnee Mar 4, 2022
ef0583d
Handling small edge case when no retry might happen for small timeout
philipnee Mar 4, 2022
8145729
Refactor retry logic
philipnee Mar 4, 2022
4584555
handing timeoutDuration and backoffMs edge cases
philipnee Mar 5, 2022
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 @@ -82,7 +82,6 @@ public Collection<String> topics() {

public static void handleMetadataErrors(MetadataResponse response) {
for (TopicMetadata tm : response.topicMetadata()) {
if (shouldRefreshMetadata(tm.error())) throw tm.error().exception();
for (PartitionMetadata pm : tm.partitionMetadata()) {
if (shouldRefreshMetadata(pm.error)) {
throw pm.error.exception();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,16 +489,12 @@ private static FindCoordinatorResponse prepareOldFindCoordinatorResponse(Errors
}

private static MetadataResponse prepareMetadataResponse(Cluster cluster, Errors error) {
return prepareMetadataResponse(cluster, error, error);
}

private static MetadataResponse prepareMetadataResponse(Cluster cluster, Errors topicError, Errors partitionError) {
List<MetadataResponseTopic> metadata = new ArrayList<>();
for (String topic : cluster.topics()) {
List<MetadataResponsePartition> pms = new ArrayList<>();
for (PartitionInfo pInfo : cluster.availablePartitionsForTopic(topic)) {
MetadataResponsePartition pm = new MetadataResponsePartition()
.setErrorCode(partitionError.code())
.setErrorCode(error.code())
.setPartitionIndex(pInfo.partition())
.setLeaderId(pInfo.leader().id())
.setLeaderEpoch(234)
Expand All @@ -508,7 +504,7 @@ private static MetadataResponse prepareMetadataResponse(Cluster cluster, Errors
pms.add(pm);
}
MetadataResponseTopic tm = new MetadataResponseTopic()
.setErrorCode(topicError.code())
.setErrorCode(error.code())
.setName(topic)
.setIsInternal(false)
.setPartitions(pms);
Expand Down Expand Up @@ -4339,40 +4335,6 @@ public void testListOffsets() throws Exception {
}
}

@Test
public void testListOffsetsRetriableErrorOnMetadata() throws Exception {
Node node = new Node(0, "localhost", 8120);
List<Node> nodes = Collections.singletonList(node);
final Cluster cluster = new Cluster(
"mockClusterId",
nodes,
Collections.singleton(new PartitionInfo("foo", 0, node, new Node[]{node}, new Node[]{node})),
Collections.emptySet(),
Collections.emptySet(),
node);
final TopicPartition tp0 = new TopicPartition("foo", 0);

try (AdminClientUnitTestEnv env = new AdminClientUnitTestEnv(cluster)) {
env.kafkaClient().setNodeApiVersions(NodeApiVersions.create());
env.kafkaClient().prepareResponse(prepareMetadataResponse(cluster, Errors.UNKNOWN_TOPIC_OR_PARTITION, Errors.NONE));
// metadata refresh because of UNKNOWN_TOPIC_OR_PARTITION
env.kafkaClient().prepareResponse(prepareMetadataResponse(cluster, Errors.NONE));
// listoffsets response from broker 0
ListOffsetsResponseData responseData = new ListOffsetsResponseData()
.setThrottleTimeMs(0)
.setTopics(Collections.singletonList(ListOffsetsResponse.singletonListOffsetsTopicResponse(tp0, Errors.NONE, -1L, 123L, 321)));
env.kafkaClient().prepareResponseFrom(new ListOffsetsResponse(responseData), node);

ListOffsetsResult result = env.adminClient().listOffsets(Collections.singletonMap(tp0, OffsetSpec.latest()));

Map<TopicPartition, ListOffsetsResultInfo> offsets = result.all().get(3, TimeUnit.SECONDS);
assertEquals(1, offsets.size());
assertEquals(123L, offsets.get(tp0).offset());
assertEquals(321, offsets.get(tp0).leaderEpoch().get().intValue());
assertEquals(-1L, offsets.get(tp0).timestamp());
}
}

@Test
public void testListOffsetsRetriableErrors() throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ public class KafkaBasedLog<K, V> {
private static final Logger log = LoggerFactory.getLogger(KafkaBasedLog.class);
private static final long CREATE_TOPIC_TIMEOUT_NS = TimeUnit.SECONDS.toNanos(30);
private static final long MAX_SLEEP_MS = TimeUnit.SECONDS.toMillis(1);
// 15min of admin retry duration to ensure successful metadata propagation. 10 seconds of backoff
// in between retries
private static final Duration ADMIN_CLIENT_RETRY_DURATION = Duration.ofMinutes(15);
private static final long ADMIN_CLIENT_RETRY_BACKOFF_MS = TimeUnit.SECONDS.toMillis(10);

private Time time;
private final String topic;
Expand Down Expand Up @@ -194,7 +198,7 @@ public void start() {
// when a 'group.id' is specified (if offsets happen to have been committed unexpectedly).
consumer.seekToBeginning(partitions);

readToLogEnd();
readToLogEnd(true);

thread = new WorkThread();
thread.start();
Expand Down Expand Up @@ -319,9 +323,16 @@ private void poll(long timeoutMs) {
}
}

private void readToLogEnd() {
/**
* This method finds the end offsets of the Kafka log's topic partitions, optionally retrying
* if the {@code listOffsets()} method of the admin client throws a {@link RetriableException}.
*
* @param shouldRetry Boolean flag to enable retry for the admin client {@code listOffsets()} call.
* @see TopicAdmin#retryEndOffsets
*/
private void readToLogEnd(boolean shouldRetry) {
Set<TopicPartition> assignment = consumer.assignment();
Map<TopicPartition, Long> endOffsets = readEndOffsets(assignment);
Map<TopicPartition, Long> endOffsets = readEndOffsets(assignment, shouldRetry);
log.trace("Reading to end of log offsets {}", endOffsets);

while (!endOffsets.isEmpty()) {
Expand All @@ -345,7 +356,7 @@ private void readToLogEnd() {
}

// Visible for testing
Map<TopicPartition, Long> readEndOffsets(Set<TopicPartition> assignment) {
Map<TopicPartition, Long> readEndOffsets(Set<TopicPartition> assignment, boolean shouldRetry) {
log.trace("Reading to end of offset log");

// Note that we'd prefer to not use the consumer to find the end offsets for the assigned topic partitions.
Expand All @@ -360,6 +371,12 @@ Map<TopicPartition, Long> readEndOffsets(Set<TopicPartition> assignment) {
// Use the admin client to immediately find the end offsets for the assigned topic partitions.
// Unlike using the consumer
try {
if (shouldRetry) {
return admin.retryEndOffsets(assignment,
ADMIN_CLIENT_RETRY_DURATION,
ADMIN_CLIENT_RETRY_BACKOFF_MS);
}

return admin.endOffsets(assignment);
} catch (UnsupportedVersionException e) {
// This may happen with really old brokers that don't support the auto topic creation
Expand Down Expand Up @@ -395,7 +412,7 @@ public void run() {

if (numCallbacks > 0) {
try {
readToLogEnd();
readToLogEnd(false);
log.trace("Finished read to end log for topic {}", topic);
} catch (TimeoutException e) {
log.warn("Timeout while reading log to end for topic '{}'. Retrying automatically. " +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.connect.util;

import org.apache.kafka.common.errors.RetriableException;
import org.apache.kafka.common.errors.WakeupException;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.connect.errors.ConnectException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.function.Supplier;

public class RetryUtil {
private static final Logger log = LoggerFactory.getLogger(RetryUtil.class);

/**
* The method executes the callable at least once, optionally retrying the callable if
* {@link org.apache.kafka.connect.errors.RetriableException} is being thrown. If timeout is exhausted,
* then the last exception is wrapped with a {@link org.apache.kafka.connect.errors.ConnectException} and thrown.
*
* <p>{@code description} supplies the message that indicates the purpose of the callable since the message will
* be used for logging. For example, "list offsets". If the supplier is null or the supplied string is
* null, {@code callable} will be used as the default string.
*
* <p>The task will be executed at least once. No retries will be performed
* if {@code timeoutDuration} is 0 or negative, or if {@code timeoutDuration} is less than {@code retryBackoffMs}.
*
* <p>A {@code retryBackoffMs} that is negative or zero will result in no delays between retries.
*
* @param callable the function to execute
* @param description supplier that provides custom message for logging purpose
* @param timeoutDuration timeout duration; must not be null
* @param retryBackoffMs the number of milliseconds to delay upon receiving a
* {@link org.apache.kafka.connect.errors.RetriableException} before retrying again
* @throws ConnectException If the task exhausted all the retries
*/
public static <T> T retryUntilTimeout(Callable<T> callable, Supplier<String> description, Duration timeoutDuration, long retryBackoffMs) throws Exception {
// if null supplier or string is provided, the message will be default to "callabe"
final String descriptionStr = Optional.ofNullable(description)
.map(Supplier::get)
.orElse("callable");

// handling null duration
final long timeoutMs = Optional.ofNullable(timeoutDuration)
.map(Duration::toMillis)
.orElse(0L);

if (retryBackoffMs < 0) {
log.debug("Assuming no retry backoff since retryBackoffMs={} is negative", retryBackoffMs);
retryBackoffMs = 0;
}
if (timeoutMs <= 0 || retryBackoffMs >= timeoutMs) {
log.debug("Executing {} only once, since timeoutMs={} is not larger than retryBackoffMs={}",
descriptionStr, timeoutMs, retryBackoffMs);
return callable.call();
}

final long end = System.currentTimeMillis() + timeoutMs;
int attempt = 0;
Throwable lastError = null;
do {
attempt++;
try {
return callable.call();
} catch (RetriableException | org.apache.kafka.connect.errors.RetriableException e) {
log.warn("Attempt {} to {} resulted in RetriableException; retrying automatically. " +
"Reason: {}", attempt, descriptionStr, e.getMessage(), e);
lastError = e;
} catch (WakeupException e) {
lastError = e;
}

long millisRemaining = Math.max(0, end - System.currentTimeMillis());
if (millisRemaining < retryBackoffMs) {
// exit when the time remaining is less than retryBackoffMs
break;
}
Utils.sleep(retryBackoffMs);
Comment thread
rhauch marked this conversation as resolved.
} while (System.currentTimeMillis() < end);

throw new ConnectException("Fail to " + descriptionStr + " after " + attempt + " attempts. Reason: " + lastError.getMessage(), lastError);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ public Map<String, Config> describeTopicConfigs(String... topicNames) {
* @throws TimeoutException if the offset metadata could not be fetched before the amount of time allocated
* by {@code request.timeout.ms} expires, and this call can be retried
* @throws LeaderNotAvailableException if the leader was not available and this call can be retried
* @throws RetriableException if a retriable error occurs, or the thread is interrupted while attempting
* @throws RetriableException if a retriable error occurs, or the thread is interrupted while attempting
* to perform this operation
* @throws ConnectException if a non retriable error occurs
*/
Expand Down Expand Up @@ -703,6 +703,33 @@ public Map<TopicPartition, Long> endOffsets(Set<TopicPartition> partitions) {
return result;
}

/**
* Fetch the most recent offset for each of the supplied {@link TopicPartition} objects, and performs retry when
* {@link org.apache.kafka.connect.errors.RetriableException} is thrown.
*
* @param partitions the topic partitions
* @param timeoutDuration timeout duration; may not be null
* @param retryBackoffMs the number of milliseconds to delay upon receiving a
* {@link org.apache.kafka.connect.errors.RetriableException} before retrying again;
* must be 0 or more
* @return the map of offset for each topic partition, or an empty map if the supplied partitions
* are null or empty
* @throws ConnectException if {@code timeoutDuration} is exhausted
* @see TopicAdmin#endOffsets(Set)
*/
public Map<TopicPartition, Long> retryEndOffsets(Set<TopicPartition> partitions, Duration timeoutDuration, long retryBackoffMs) {

try {
return RetryUtil.retryUntilTimeout(
() -> endOffsets(partitions),
() -> "list offsets for topic partitions",
timeoutDuration,
retryBackoffMs);
} catch (Exception e) {
throw new ConnectException("Failed to list offsets for topic partitions.", e);
}
}

@Override
public void close() {
admin.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,13 +490,15 @@ public void testReadEndOffsetsUsingAdmin() throws Exception {
Map<TopicPartition, Long> endOffsets = new HashMap<>();
endOffsets.put(TP0, 0L);
endOffsets.put(TP1, 0L);
admin.retryEndOffsets(EasyMock.eq(tps), EasyMock.anyObject(), EasyMock.anyLong());
PowerMock.expectLastCall().andReturn(endOffsets).times(1);
admin.endOffsets(EasyMock.eq(tps));
PowerMock.expectLastCall().andReturn(endOffsets).times(2);
PowerMock.expectLastCall().andReturn(endOffsets).times(1);

PowerMock.replayAll();

store.start();
assertEquals(endOffsets, store.readEndOffsets(tps));
assertEquals(endOffsets, store.readEndOffsets(tps, false));
}

@Test
Expand All @@ -507,7 +509,7 @@ public void testReadEndOffsetsUsingAdminThatFailsWithUnsupported() throws Except

Set<TopicPartition> tps = new HashSet<>(Arrays.asList(TP0, TP1));
// Getting end offsets using the admin client should fail with unsupported version
admin.endOffsets(EasyMock.eq(tps));
admin.retryEndOffsets(EasyMock.eq(tps), EasyMock.anyObject(), EasyMock.anyLong());
PowerMock.expectLastCall().andThrow(new UnsupportedVersionException("too old"));

// Falls back to the consumer
Expand All @@ -519,7 +521,7 @@ public void testReadEndOffsetsUsingAdminThatFailsWithUnsupported() throws Except
PowerMock.replayAll();

store.start();
assertEquals(endOffsets, store.readEndOffsets(tps));
assertEquals(endOffsets, store.readEndOffsets(tps, false));
}

@Test
Expand All @@ -533,7 +535,7 @@ public void testReadEndOffsetsUsingAdminThatFailsWithRetriable() throws Exceptio
endOffsets.put(TP0, 0L);
endOffsets.put(TP1, 0L);
// Getting end offsets upon startup should work fine
admin.endOffsets(EasyMock.eq(tps));
admin.retryEndOffsets(EasyMock.eq(tps), EasyMock.anyObject(), EasyMock.anyLong());
PowerMock.expectLastCall().andReturn(endOffsets).times(1);
// Getting end offsets using the admin client should fail with leader not available
admin.endOffsets(EasyMock.eq(tps));
Expand All @@ -542,7 +544,7 @@ public void testReadEndOffsetsUsingAdminThatFailsWithRetriable() throws Exceptio
PowerMock.replayAll();

store.start();
assertThrows(LeaderNotAvailableException.class, () -> store.readEndOffsets(tps));
assertThrows(LeaderNotAvailableException.class, () -> store.readEndOffsets(tps, false));
}

@SuppressWarnings("unchecked")
Expand Down
Loading