Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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 @@ -194,7 +194,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 +319,11 @@ private void poll(long timeoutMs) {
}
}

private void readToLogEnd() {
private void readToLogEnd(boolean shouldRetry) {
Set<TopicPartition> assignment = consumer.assignment();
Map<TopicPartition, Long> endOffsets = readEndOffsets(assignment);
// readEndOffsets makes listOffsets call to adminClient, if shouldRetry is set to True, the adminClinet
// will retry on RetriableExceptions
Comment thread
rhauch marked this conversation as resolved.
Outdated
Map<TopicPartition, Long> endOffsets = readEndOffsets(assignment, shouldRetry);
log.trace("Reading to end of log offsets {}", endOffsets);

while (!endOffsets.isEmpty()) {
Expand All @@ -345,7 +347,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 +362,10 @@ 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);
}

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 +401,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,52 @@
/*
* 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.util.concurrent.Callable;

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

public static <T> T retry(Callable<T> callable, long maxRetries, long retryBackoffMs) throws Exception {
Throwable lastError = null;
int retries = 0;
while (retries++ < maxRetries) {
try {
return callable.call();
Comment thread
rhauch marked this conversation as resolved.
Outdated
} catch (RetriableException | org.apache.kafka.connect.errors.RetriableException e) {
log.warn("RetriableException caught, retrying automatically up to {} more times. " +
"Reason: {}", maxRetries - retries, e.getMessage());
lastError = e;
} catch (WakeupException e) {
lastError = e;
} catch (Exception e) {
log.warn("Non-retriable exception caught. Re-throwing. Reason: {}, {}", e.getClass(), e.getMessage());
throw e;
Comment thread
rhauch marked this conversation as resolved.
Outdated
}
Utils.sleep(retryBackoffMs);
Comment thread
rhauch marked this conversation as resolved.
}

throw new ConnectException("Fail to retry the operation after " + maxRetries + " attempts. Reason: " + lastError, lastError);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ public String toString() {
public static final int NO_PARTITIONS = -1;
public static final short NO_REPLICATION_FACTOR = -1;

private static final String DEFAULT_ADMIN_CLIENT_RETRIES = "10";
private static final String DEFAULT_ADMIN_CLIENT_BACKOFF_MS = "100";

Comment thread
rhauch marked this conversation as resolved.
Outdated
Comment thread
rhauch marked this conversation as resolved.
Outdated
private static final String CLEANUP_POLICY_CONFIG = TopicConfig.CLEANUP_POLICY_CONFIG;
private static final String CLEANUP_POLICY_COMPACT = TopicConfig.CLEANUP_POLICY_COMPACT;
private static final String MIN_INSYNC_REPLICAS_CONFIG = TopicConfig.MIN_IN_SYNC_REPLICAS_CONFIG;
Expand Down Expand Up @@ -655,7 +658,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 +706,20 @@ public Map<TopicPartition, Long> endOffsets(Set<TopicPartition> partitions) {
return result;
}

protected Map<TopicPartition, Long> retryEndOffsets(Set<TopicPartition> partitions) {
int maxRetries = Integer.parseInt(adminConfig.getOrDefault(AdminClientConfig.RETRIES_CONFIG, DEFAULT_ADMIN_CLIENT_RETRIES).toString());
Comment thread
rhauch marked this conversation as resolved.
Outdated
long retryBackoffMs = Long.parseLong(adminConfig.getOrDefault(AdminClientConfig.RETRY_BACKOFF_MS_CONFIG, DEFAULT_ADMIN_CLIENT_BACKOFF_MS).toString());

try {
return RetryUtil.retry(
() -> endOffsets(partitions),
maxRetries,
Comment thread
rhauch marked this conversation as resolved.
Outdated
retryBackoffMs);
} catch (Exception e) {
throw new ConnectException("Failed to read offsets for topic partitions.", e);
}
Comment thread
philipnee marked this conversation as resolved.
}

@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));
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));
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));
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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 static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

import org.apache.kafka.common.errors.TimeoutException;
import org.apache.kafka.connect.errors.ConnectException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.powermock.modules.junit4.PowerMockRunner;

import java.util.concurrent.Callable;

@RunWith(PowerMockRunner.class)
public class RetryUtilTest {

@Mock
Comment thread
philipnee marked this conversation as resolved.
Outdated
private Callable<String> mockCallable;

@SuppressWarnings("unchecked")
@Before
public void setUp() throws Exception {
mockCallable = Mockito.mock(Callable.class);
}

@Test
public void TestSuccess() throws Exception {
Comment thread
rhauch marked this conversation as resolved.
Outdated
Mockito.when(mockCallable.call()).thenReturn("success");
//EasyMock.expect(mockCallable.call()).andReturn("success").times(1);
Comment thread
rhauch marked this conversation as resolved.
Outdated
try {
assertEquals("success", RetryUtil.retry(mockCallable, 10, 100));
} catch (Exception e) {
fail("Not expecting an exception: ", e.getCause());
}
Comment thread
rhauch marked this conversation as resolved.
Outdated
Mockito.verify(mockCallable, Mockito.times(1)).call();
}

@Test
public void TestFailingRetries() throws Exception {
Mockito.when(mockCallable.call()).thenThrow(new TimeoutException());
try {
RetryUtil.retry(mockCallable, 10, 100);
fail("Expect exception being thrown here");
} catch (ConnectException e) {
// expecting a connect exception
} catch (Exception e) {
fail("Only expecting ConnectException");
}
Comment thread
rhauch marked this conversation as resolved.
Outdated
Mockito.verify(mockCallable, Mockito.times(10)).call();
}

@Test
public void RetriesEventuallySucceed() throws Exception {
Mockito.when(mockCallable.call())
.thenThrow(new TimeoutException())
.thenThrow(new TimeoutException())
.thenThrow(new TimeoutException())
.thenReturn("success");
try {
assertEquals("success", RetryUtil.retry(mockCallable, 10, 100));
} catch (Exception e) {
fail("Not expecting an exception: ", e.getCause());
}
Comment thread
rhauch marked this conversation as resolved.
Outdated
Mockito.verify(mockCallable, Mockito.times(4)).call();
}

@Test
public void FailWithNonRetriableException() throws Exception {
Mockito.when(mockCallable.call())
.thenThrow(new TimeoutException("timeout"))
.thenThrow(new TimeoutException("timeout"))
.thenThrow(new TimeoutException("timeout"))
.thenThrow(new TimeoutException("timeout"))
.thenThrow(new TimeoutException("timeout"))
.thenThrow(new NullPointerException("Non retriable"));
try {
for (int i = 0; i < 10; i++) {
RetryUtil.retry(mockCallable, 10, 100);
}
fail("Not expecting an exception: ");
} catch (ConnectException e) {
fail("Should fail with NPE");
} catch (NullPointerException e) {
// good
}
Comment thread
rhauch marked this conversation as resolved.
Outdated
Mockito.verify(mockCallable, Mockito.times(6)).call();
}
}
Loading