Skip to content

Commit ae56f30

Browse files
shawkinsmanusa
authored andcommitted
fix: use the Retry-After header value
closes: #6366 Signed-off-by: Steve Hawkins <[email protected]> Signed-off-by: Marc Nuri <[email protected]>
1 parent b3b22cb commit ae56f30

File tree

5 files changed

+60
-34
lines changed

5 files changed

+60
-34
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* Fix #6247: Support for proxy authentication from proxy URL user info
77
* Fix #6342: UnmatchedFieldTypeModule prevents certain jackson features from working
88
* Fix #6354: Prevent deadlock in okhttp AsyncBody.cancel
9+
* Fix #6366: Allow Retry-After header to be considered in retries
910

1011
### 6.13.3 (2024-08-13)
1112

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

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

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

+7-3
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

+18
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;
@@ -178,6 +180,22 @@ void testHttpRetryWithLessFailuresThanRetries() throws Exception {
178180
.hasSize(4);
179181
}
180182

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(),
196+
r -> r.webSocketUpgradeResponse, error, null, 1000)).isEqualTo(5000);
197+
}
198+
181199
@Test
182200
void testWebSocketWithLessFailuresThanRetries() throws Exception {
183201
client = client.newBuilder().tag(new RequestConfigBuilder()

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

+4-4
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)