Skip to content

Commit f2a12bc

Browse files
committed
fix: use the Retry-After header value
closes: fabric8io#6366 Signed-off-by: Steve Hawkins <[email protected]>
1 parent 17939f6 commit f2a12bc

File tree

5 files changed

+58
-34
lines changed

5 files changed

+58
-34
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* Fix #5264: Remove deprecated `Config.errorMessages` field
1111
* Fix #6008: removing the optional dependency on bouncy castle
1212
* Fix #6230: introduced Quantity.multiply(int) to allow for Quantity multiplication by an integer
13+
* Fix #6366: Allow Retry-After header to be considered in retries
1314
* Fix #6247: Support for proxy authentication from proxy URL user info
1415
* Fix #6281: use GitHub binary repo for Kube API Tests
1516
* Fix #6282: Allow annotated types with Pattern, Min, and Max with Lists and Maps and CRD generation

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpClient.java

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import java.io.Closeable;
2929
import java.io.IOException;
30-
import java.net.URI;
3130
import java.nio.ByteBuffer;
3231
import java.time.Duration;
3332
import java.time.ZonedDateTime;
@@ -152,7 +151,6 @@ private CompletableFuture<HttpResponse<AsyncBody>> consumeBytesOnce(StandardHttp
152151
private <V> CompletableFuture<V> retryWithExponentialBackoff(
153152
StandardHttpRequest request, Supplier<CompletableFuture<V>> action, java.util.function.Consumer<V> onCancel,
154153
Function<V, HttpResponse<?>> responseExtractor) {
155-
final URI uri = request.uri();
156154
final RequestConfig requestConfig = getTag(RequestConfig.class);
157155
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = ExponentialBackoffIntervalCalculator
158156
.from(requestConfig);
@@ -164,34 +162,38 @@ private <V> CompletableFuture<V> retryWithExponentialBackoff(
164162
}
165163
return AsyncUtils.retryWithExponentialBackoff(action, onCancel, timeout, retryIntervalCalculator,
166164
(response, throwable, retryInterval) -> {
167-
if (response != null) {
168-
HttpResponse<?> httpResponse = responseExtractor.apply(response);
169-
if (httpResponse != null) {
170-
final int code = httpResponse.code();
171-
if (code == 429 || code >= 500) {
172-
retryInterval = Math.max(retryAfterMillis(httpResponse), retryInterval);
173-
LOG.debug(
174-
"HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis",
175-
uri, code, retryInterval);
176-
return true;
177-
}
178-
}
179-
} else {
180-
final Throwable actualCause = unwrapCompletionException(throwable);
181-
builder.interceptors.forEach((s, interceptor) -> interceptor.afterConnectionFailure(request, actualCause));
182-
if (actualCause instanceof IOException) {
183-
// TODO: may not be specific enough - incorrect ssl settings for example will get caught here
184-
LOG.debug(
185-
String.format("HTTP operation on url: %s should be retried after %d millis because of IOException",
186-
uri, retryInterval),
187-
actualCause);
188-
return true;
189-
}
190-
}
191-
return false;
165+
return shouldRetry(request, responseExtractor, response, throwable, retryInterval);
192166
});
193167
}
194168

169+
<V> long shouldRetry(StandardHttpRequest request, Function<V, HttpResponse<?>> responseExtractor, V response, Throwable throwable, long retryInterval) {
170+
if (response != null) {
171+
HttpResponse<?> httpResponse = responseExtractor.apply(response);
172+
if (httpResponse != null) {
173+
final int code = httpResponse.code();
174+
if (code == 429 || code >= 500) {
175+
retryInterval = Math.max(retryAfterMillis(httpResponse), retryInterval);
176+
LOG.debug(
177+
"HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis",
178+
request.uri(), code, retryInterval);
179+
return retryInterval;
180+
}
181+
}
182+
} else {
183+
final Throwable actualCause = unwrapCompletionException(throwable);
184+
builder.interceptors.forEach((s, interceptor) -> interceptor.afterConnectionFailure(request, actualCause));
185+
if (actualCause instanceof IOException) {
186+
// TODO: may not be specific enough - incorrect ssl settings for example will get caught here
187+
LOG.debug(
188+
String.format("HTTP operation on url: %s should be retried after %d millis because of IOException",
189+
request.uri(), retryInterval),
190+
actualCause);
191+
return retryInterval;
192+
}
193+
}
194+
return -1;
195+
}
196+
195197
static Throwable unwrapCompletionException(Throwable throwable) {
196198
final Throwable actualCause;
197199
if (throwable instanceof CompletionException) {

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/AsyncUtils.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static <T> CompletableFuture<T> withTimeout(CompletableFuture<T> future,
4949
/**
5050
* Returns a new {@link CompletableFuture} that will complete once the action provided by the action supplier completes.
5151
* The action will be retried with an exponential backoff using the {@link ExponentialBackoffIntervalCalculator} as
52-
* long as the {@link ShouldRetry} predicate returns true.
52+
* long as the {@link ShouldRetry} predicate returns a non-negative value.
5353
* Each action retrieval retry will time out after the provided timeout {@link Duration}.
5454
*
5555
* @param action the action supplier.
@@ -75,7 +75,8 @@ private static <T> void retryWithExponentialBackoff(CompletableFuture<T> result,
7575
withTimeout(action.get(), timeout).whenComplete((r, t) -> {
7676
if (retryIntervalCalculator.shouldRetry() && !result.isDone()) {
7777
final long retryInterval = retryIntervalCalculator.nextReconnectInterval();
78-
if (shouldRetry.shouldRetry(r, t, retryInterval)) {
78+
long retryValue = shouldRetry.shouldRetry(r, t, retryInterval);
79+
if (retryValue >= 0) {
7980
if (r != null) {
8081
onCancel.accept(r);
8182
}
@@ -95,6 +96,9 @@ private static <T> void retryWithExponentialBackoff(CompletableFuture<T> result,
9596

9697
@FunctionalInterface
9798
public interface ShouldRetry<T> {
98-
boolean shouldRetry(T result, Throwable exception, long retryInterval);
99+
/**
100+
* @return the retry interval in ms, or a negative value indicating retries should be aborted
101+
*/
102+
long shouldRetry(T result, Throwable exception, long retryInterval);
99103
}
100104
}

kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/StandardHttpClientTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
import java.time.format.DateTimeFormatter;
3333
import java.util.Arrays;
3434
import java.util.Collections;
35+
import java.util.HashMap;
3536
import java.util.List;
37+
import java.util.Map;
3638
import java.util.concurrent.CompletableFuture;
3739
import java.util.concurrent.CompletionException;
3840
import java.util.concurrent.ExecutionException;
@@ -177,6 +179,21 @@ void testHttpRetryWithLessFailuresThanRetries() throws Exception {
177179
assertThat(client.getRecordedConsumeBytesDirects())
178180
.hasSize(4);
179181
}
182+
183+
@Test
184+
void testShouldRetryUsesRetryAfterHeader() throws Exception {
185+
client = client.newBuilder().tag(new RequestConfigBuilder()
186+
.withRequestRetryBackoffLimit(3)
187+
.withRequestRetryBackoffInterval(50).build())
188+
.build();
189+
190+
Map<String, List<String>> headers = new HashMap<>();
191+
headers.put(StandardHttpHeaders.RETRY_AFTER, Arrays.asList("5"));
192+
// the exception type doesn't matter
193+
final WebSocketResponse error = new WebSocketResponse(new WebSocketUpgradeResponse(null, 429, headers), new IOException());
194+
195+
assertThat(client.shouldRetry((StandardHttpRequest)client.newHttpRequestBuilder().uri("http://localhost").build(), r -> r.webSocketUpgradeResponse, error, null, 1000)).isEqualTo(5000);
196+
}
180197

181198
@Test
182199
void testWebSocketWithLessFailuresThanRetries() throws Exception {

kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/AsyncUtilsTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ void retryWithExponentialBackoff_timeout() {
7979
final Supplier<CompletableFuture<Void>> action = CompletableFuture::new;
8080
final CompletableFuture<Void> onCancel = new CompletableFuture<>();
8181
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 1);
82-
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> true;
82+
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> retryInterval;
8383
// When
8484
final CompletableFuture<Void> result = retryWithExponentialBackoff(action, onCancel::complete, Duration.ofMillis(1),
8585
retryIntervalCalculator, shouldRetry);
@@ -98,7 +98,7 @@ void retryWithExponentialBackoff_withCancelledFuture_onCancel() {
9898
final Supplier<CompletableFuture<Void>> actionSupplier = () -> action;
9999
final CompletableFuture<Void> onCancel = new CompletableFuture<>();
100100
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 0);
101-
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> false;
101+
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> -1;
102102
// When
103103
final CompletableFuture<Void> result = retryWithExponentialBackoff(actionSupplier, onCancel::complete,
104104
Duration.ofMillis(100), retryIntervalCalculator, shouldRetry);
@@ -119,7 +119,7 @@ void retryWithExponentialBackoff_withCompletedResult_onCancel() throws Exception
119119
final Supplier<CompletableFuture<Boolean>> actionSupplier = () -> action;
120120
final CompletableFuture<Boolean> onCancel = new CompletableFuture<>();
121121
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 1);
122-
final AsyncUtils.ShouldRetry<Boolean> shouldRetry = (v, t, retryInterval) -> true;
122+
final AsyncUtils.ShouldRetry<Boolean> shouldRetry = (v, t, retryInterval) -> retryInterval;
123123
// When
124124
CompletableFuture<Boolean> result = retryWithExponentialBackoff(actionSupplier, onCancel::complete,
125125
Duration.ofMillis(100), retryIntervalCalculator, shouldRetry);
@@ -140,7 +140,7 @@ void retryWithExponentialBackoff_complete() {
140140
final Supplier<CompletableFuture<Void>> actionSupplier = () -> action;
141141
final CompletableFuture<Void> onCancel = new CompletableFuture<>();
142142
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = new ExponentialBackoffIntervalCalculator(1, 0);
143-
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> false;
143+
final AsyncUtils.ShouldRetry<Void> shouldRetry = (v, t, retryInterval) -> -1;
144144
// When
145145
final CompletableFuture<Void> result = retryWithExponentialBackoff(actionSupplier, onCancel::complete,
146146
Duration.ofMillis(100), retryIntervalCalculator, shouldRetry);

0 commit comments

Comments
 (0)