Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: use the Retry-After header value #6367

Merged
merged 1 commit into from
Sep 25, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Fix #5264: Remove deprecated `Config.errorMessages` field
* Fix #6008: removing the optional dependency on bouncy castle
* Fix #6230: introduced Quantity.multiply(int) to allow for Quantity multiplication by an integer
* Fix #6366: Allow Retry-After header to be considered in retries
* Fix #6247: Support for proxy authentication from proxy URL user info
* Fix #6281: use GitHub binary repo for Kube API Tests
* Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.ZonedDateTime;
Expand Down Expand Up @@ -152,7 +151,6 @@ private CompletableFuture<HttpResponse<AsyncBody>> consumeBytesOnce(StandardHttp
private <V> CompletableFuture<V> retryWithExponentialBackoff(
StandardHttpRequest request, Supplier<CompletableFuture<V>> action, java.util.function.Consumer<V> onCancel,
Function<V, HttpResponse<?>> responseExtractor) {
final URI uri = request.uri();
final RequestConfig requestConfig = getTag(RequestConfig.class);
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = ExponentialBackoffIntervalCalculator
.from(requestConfig);
Expand All @@ -164,34 +162,39 @@ private <V> CompletableFuture<V> retryWithExponentialBackoff(
}
return AsyncUtils.retryWithExponentialBackoff(action, onCancel, timeout, retryIntervalCalculator,
(response, throwable, retryInterval) -> {
if (response != null) {
HttpResponse<?> httpResponse = responseExtractor.apply(response);
if (httpResponse != null) {
final int code = httpResponse.code();
if (code == 429 || code >= 500) {
retryInterval = Math.max(retryAfterMillis(httpResponse), retryInterval);
LOG.debug(
"HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis",
uri, code, retryInterval);
return true;
}
}
} else {
final Throwable actualCause = unwrapCompletionException(throwable);
builder.interceptors.forEach((s, interceptor) -> interceptor.afterConnectionFailure(request, actualCause));
if (actualCause instanceof IOException) {
// TODO: may not be specific enough - incorrect ssl settings for example will get caught here
LOG.debug(
String.format("HTTP operation on url: %s should be retried after %d millis because of IOException",
uri, retryInterval),
actualCause);
return true;
}
}
return false;
return shouldRetry(request, responseExtractor, response, throwable, retryInterval);
});
}

<V> long shouldRetry(StandardHttpRequest request, Function<V, HttpResponse<?>> responseExtractor, V response,
Throwable throwable, long retryInterval) {
if (response != null) {
HttpResponse<?> httpResponse = responseExtractor.apply(response);
if (httpResponse != null) {
final int code = httpResponse.code();
if (code == 429 || code >= 500) {
retryInterval = Math.max(retryAfterMillis(httpResponse), retryInterval);
LOG.debug(
"HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis",
request.uri(), code, retryInterval);
return retryInterval;
}
}
} else {
final Throwable actualCause = unwrapCompletionException(throwable);
builder.interceptors.forEach((s, interceptor) -> interceptor.afterConnectionFailure(request, actualCause));
if (actualCause instanceof IOException) {
// TODO: may not be specific enough - incorrect ssl settings for example will get caught here
LOG.debug(
String.format("HTTP operation on url: %s should be retried after %d millis because of IOException",
request.uri(), retryInterval),
actualCause);
return retryInterval;
}
}
return -1;
}

static Throwable unwrapCompletionException(Throwable throwable) {
final Throwable actualCause;
if (throwable instanceof CompletionException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static <T> CompletableFuture<T> withTimeout(CompletableFuture<T> future,
/**
* Returns a new {@link CompletableFuture} that will complete once the action provided by the action supplier completes.
* The action will be retried with an exponential backoff using the {@link ExponentialBackoffIntervalCalculator} as
* long as the {@link ShouldRetry} predicate returns true.
* long as the {@link ShouldRetry} predicate returns a non-negative value.
* Each action retrieval retry will time out after the provided timeout {@link Duration}.
*
* @param action the action supplier.
Expand All @@ -75,7 +75,8 @@ private static <T> void retryWithExponentialBackoff(CompletableFuture<T> result,
withTimeout(action.get(), timeout).whenComplete((r, t) -> {
if (retryIntervalCalculator.shouldRetry() && !result.isDone()) {
final long retryInterval = retryIntervalCalculator.nextReconnectInterval();
if (shouldRetry.shouldRetry(r, t, retryInterval)) {
long retryValue = shouldRetry.shouldRetry(r, t, retryInterval);
if (retryValue >= 0) {
if (r != null) {
onCancel.accept(r);
}
Expand All @@ -95,6 +96,9 @@ private static <T> void retryWithExponentialBackoff(CompletableFuture<T> result,

@FunctionalInterface
public interface ShouldRetry<T> {
boolean shouldRetry(T result, Throwable exception, long retryInterval);
/**
* @return the retry interval in ms, or a negative value indicating retries should be aborted
*/
long shouldRetry(T result, Throwable exception, long retryInterval);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
Expand Down Expand Up @@ -178,6 +180,22 @@ void testHttpRetryWithLessFailuresThanRetries() throws Exception {
.hasSize(4);
}

@Test
void testShouldRetryUsesRetryAfterHeader() throws Exception {
client = client.newBuilder().tag(new RequestConfigBuilder()
.withRequestRetryBackoffLimit(3)
.withRequestRetryBackoffInterval(50).build())
.build();

Map<String, List<String>> headers = new HashMap<>();
headers.put(StandardHttpHeaders.RETRY_AFTER, Arrays.asList("5"));
// the exception type doesn't matter
final WebSocketResponse error = new WebSocketResponse(new WebSocketUpgradeResponse(null, 429, headers), new IOException());

assertThat(client.shouldRetry((StandardHttpRequest) client.newHttpRequestBuilder().uri("http://localhost").build(),
r -> r.webSocketUpgradeResponse, error, null, 1000)).isEqualTo(5000);
}

@Test
void testWebSocketWithLessFailuresThanRetries() throws Exception {
client = client.newBuilder().tag(new RequestConfigBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ void retryWithExponentialBackoff_timeout() {
final Supplier<CompletableFuture<Void>> action = CompletableFuture::new;
final CompletableFuture<Void> onCancel = new CompletableFuture<>();
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 1);
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> true;
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> retryInterval;
// When
final CompletableFuture<Void> result = retryWithExponentialBackoff(action, onCancel::complete, Duration.ofMillis(1),
retryIntervalCalculator, shouldRetry);
Expand All @@ -98,7 +98,7 @@ void retryWithExponentialBackoff_withCancelledFuture_onCancel() {
final Supplier<CompletableFuture<Void>> actionSupplier = () -> action;
final CompletableFuture<Void> onCancel = new CompletableFuture<>();
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 0);
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> false;
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> -1;
// When
final CompletableFuture<Void> result = retryWithExponentialBackoff(actionSupplier, onCancel::complete,
Duration.ofMillis(100), retryIntervalCalculator, shouldRetry);
Expand All @@ -119,7 +119,7 @@ void retryWithExponentialBackoff_withCompletedResult_onCancel() throws Exception
final Supplier<CompletableFuture<Boolean>> actionSupplier = () -> action;
final CompletableFuture<Boolean> onCancel = new CompletableFuture<>();
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 1);
final AsyncUtils.ShouldRetry<Boolean> shouldRetry = (v, t, retryInterval) -> true;
final AsyncUtils.ShouldRetry<Boolean> shouldRetry = (v, t, retryInterval) -> retryInterval;
// When
CompletableFuture<Boolean> result = retryWithExponentialBackoff(actionSupplier, onCancel::complete,
Duration.ofMillis(100), retryIntervalCalculator, shouldRetry);
Expand All @@ -140,7 +140,7 @@ void retryWithExponentialBackoff_complete() {
final Supplier<CompletableFuture<Void>> actionSupplier = () -> action;
final CompletableFuture<Void> onCancel = new CompletableFuture<>();
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 0);
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> false;
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> -1;
// When
final CompletableFuture<Void> result = retryWithExponentialBackoff(actionSupplier, onCancel::complete,
Duration.ofMillis(100), retryIntervalCalculator, shouldRetry);
Expand Down
Loading