Skip to content

Commit 125e838

Browse files
shawkinsmanusa
authored andcommitted
adding handling for 429 and Retry-After
1 parent c984cff commit 125e838

File tree

4 files changed

+61
-20
lines changed

4 files changed

+61
-20
lines changed

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.List;
2020
import java.util.Map;
21+
import java.util.stream.Collectors;
2122

2223
public interface HttpHeaders {
2324

@@ -36,4 +37,20 @@ public interface HttpHeaders {
3637
*/
3738
Map<String, List<String>> headers();
3839

40+
/**
41+
* Return the header as a single string value
42+
*
43+
* @return will be null if the header is unset
44+
*/
45+
default String header(String key) {
46+
List<String> headers = headers(key);
47+
if (headers.size() == 1) {
48+
return headers.get(0);
49+
}
50+
if (headers.isEmpty()) {
51+
return null;
52+
}
53+
return headers.stream().collect(Collectors.joining(","));
54+
}
55+
3956
}

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

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,17 @@
3131
import java.net.URI;
3232
import java.nio.ByteBuffer;
3333
import java.time.Duration;
34+
import java.time.ZonedDateTime;
35+
import java.time.format.DateTimeFormatter;
36+
import java.time.format.DateTimeParseException;
3437
import java.util.List;
3538
import java.util.Optional;
3639
import java.util.concurrent.CompletableFuture;
3740
import java.util.concurrent.CompletionException;
3841
import java.util.concurrent.TimeUnit;
3942
import java.util.function.BiConsumer;
43+
import java.util.function.Function;
4044
import java.util.function.Supplier;
41-
import java.util.function.ToIntFunction;
4245

4346
public abstract class StandardHttpClient<C extends HttpClient, F extends HttpClient.Factory, T extends StandardHttpClientBuilder<C, F, ?>>
4447
implements HttpClient, RequestTags {
@@ -85,7 +88,7 @@ public CompletableFuture<HttpResponse<AsyncBody>> consumeBytes(HttpRequest reque
8588
standardHttpRequest,
8689
() -> consumeBytesOnce(standardHttpRequest, consumer),
8790
r -> r.body().cancel(),
88-
HttpResponse::code);
91+
r -> r);
8992
}
9093

9194
private CompletableFuture<HttpResponse<AsyncBody>> consumeBytesOnce(StandardHttpRequest standardHttpRequest,
@@ -146,7 +149,7 @@ private CompletableFuture<HttpResponse<AsyncBody>> consumeBytesOnce(StandardHttp
146149
*/
147150
private <V> CompletableFuture<V> retryWithExponentialBackoff(
148151
StandardHttpRequest request, Supplier<CompletableFuture<V>> action, java.util.function.Consumer<V> onCancel,
149-
ToIntFunction<V> codeExtractor) {
152+
Function<V, HttpResponse<?>> responseExtractor) {
150153
final URI uri = request.uri();
151154
final RequestConfig requestConfig = getTag(RequestConfig.class);
152155
final ExponentialBackoffIntervalCalculator retryIntervalCalculator = ExponentialBackoffIntervalCalculator
@@ -160,18 +163,23 @@ private <V> CompletableFuture<V> retryWithExponentialBackoff(
160163
return AsyncUtils.retryWithExponentialBackoff(action, onCancel, timeout, retryIntervalCalculator,
161164
(response, throwable, retryInterval) -> {
162165
if (response != null) {
163-
final int code = codeExtractor.applyAsInt(response);
164-
if (code >= 500) {
165-
LOG.debug(
166-
"HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis",
167-
uri, code, retryInterval);
168-
return true;
166+
HttpResponse<?> httpResponse = responseExtractor.apply(response);
167+
if (httpResponse != null) {
168+
final int code = httpResponse.code();
169+
if (code == 429 || code >= 500) {
170+
retryInterval = Math.max(retryAfterMillis(httpResponse), retryInterval);
171+
LOG.debug(
172+
"HTTP operation on url: {} should be retried as the response code was {}, retrying after {} millis",
173+
uri, code, retryInterval);
174+
return true;
175+
}
169176
}
170177
} else {
171178
if (throwable instanceof CompletionException) {
172179
throwable = throwable.getCause();
173180
}
174181
if (throwable instanceof IOException) {
182+
// TODO: may not be specific enough - incorrect ssl settings for example will get caught here
175183
LOG.debug(
176184
String.format("HTTP operation on url: %s should be retried after %d millis because of IOException",
177185
uri, retryInterval),
@@ -183,6 +191,25 @@ private <V> CompletableFuture<V> retryWithExponentialBackoff(
183191
});
184192
}
185193

194+
private long retryAfterMillis(HttpResponse<?> httpResponse) {
195+
String retryAfter = httpResponse.header("Retry-After");
196+
if (retryAfter != null) {
197+
try {
198+
return Integer.parseInt(retryAfter) * 1000L;
199+
} catch (NumberFormatException e) {
200+
// not a simple number
201+
}
202+
// Kubernetes does not seem to currently use this, but just in case
203+
try {
204+
ZonedDateTime after = ZonedDateTime.parse(retryAfter, DateTimeFormatter.RFC_1123_DATE_TIME);
205+
return after.toEpochSecond() * 1000 - System.currentTimeMillis();
206+
} catch (DateTimeParseException e1) {
207+
// not a recognized http date
208+
}
209+
}
210+
return 0; // we'll just use the default
211+
}
212+
186213
@Override
187214
public io.fabric8.kubernetes.client.http.WebSocket.Builder newWebSocketBuilder() {
188215
return new StandardWebSocketBuilder(this);
@@ -201,7 +228,7 @@ final CompletableFuture<WebSocket> buildWebSocket(StandardWebSocketBuilder stand
201228
standardWebSocketBuilder.asHttpRequest(),
202229
() -> buildWebSocketOnce(standardWebSocketBuilder, listener),
203230
r -> Optional.ofNullable(r.webSocket).ifPresent(w -> w.sendClose(1000, null)),
204-
r -> Optional.of(r.webSocketUpgradeResponse).map(HttpResponse::code).orElse(null));
231+
r -> r.webSocketUpgradeResponse);
205232

206233
CompletableFuture<WebSocket> result = new CompletableFuture<>();
207234

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ void testHttpRetryWithMoreFailuresThanRetries() throws Exception {
121121
.build();
122122

123123
IntStream.range(0, 3).forEach(i -> client.expect(".*", new IOException("Unreachable!")));
124-
client.expect(".*", new TestHttpResponse<AsyncBody>().withCode(500));
124+
client.expect(".*", new TestHttpResponse<AsyncBody>().withCode(403));
125125

126126
CompletableFuture<HttpResponse<AsyncBody>> consumeFuture = client.consumeBytes(
127127
client.newHttpRequestBuilder().uri("http://localhost").build(),
@@ -132,7 +132,7 @@ void testHttpRetryWithMoreFailuresThanRetries() throws Exception {
132132
long start = System.currentTimeMillis();
133133

134134
// should ultimately error with the final 500
135-
assertEquals(500, consumeFuture.get().code());
135+
assertEquals(403, consumeFuture.get().code());
136136
long stop = System.currentTimeMillis();
137137

138138
// should take longer than the delay
@@ -172,7 +172,7 @@ void testWebSocketWithLessFailuresThanRetries() throws Exception {
172172
.withRequestRetryBackoffLimit(3)
173173
.withRequestRetryBackoffInterval(50).build())
174174
.build();
175-
final WebSocketResponse error = new WebSocketResponse(new WebSocketUpgradeResponse(null, 500, null), new IOException());
175+
final WebSocketResponse error = new WebSocketResponse(new WebSocketUpgradeResponse(null, 500), new IOException());
176176
IntStream.range(0, 2).forEach(i -> client.wsExpect(".*", error));
177177
client.wsExpect(".*", new WebSocketResponse(new WebSocketUpgradeResponse(null), mock(WebSocket.class)));
178178

kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodTest.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,11 @@ void testEvict() {
274274
.andReturn(200, new PodBuilder().build())
275275
.once();
276276
server.expect()
277-
.withPath("/api/v1/namespaces/ns1/pods/pod3/eviction")
277+
.withPath("/api/v1/namespaces/ns1/pods/pod-429/eviction")
278278
.andReturn(PodOperationsImpl.HTTP_TOO_MANY_REQUESTS, new PodBuilder().build())
279279
.once();
280280
server.expect()
281-
.withPath("/api/v1/namespaces/ns1/pods/pod3/eviction")
281+
.withPath("/api/v1/namespaces/ns1/pods/pod-429/eviction")
282282
.andReturn(200, new PodBuilder().build())
283283
.once();
284284
server.expect()
@@ -296,11 +296,8 @@ void testEvict() {
296296
deleted = client.pods().inNamespace("ns1").withName("pod2").evict();
297297
assertTrue(deleted);
298298

299-
// too many requests
300-
deleted = client.pods().inNamespace("ns1").withName("pod3").evict();
301-
assertFalse(deleted);
302-
303-
deleted = client.pods().inNamespace("ns1").withName("pod3").evict();
299+
// too many requests - automatically retries
300+
deleted = client.pods().inNamespace("ns1").withName("pod-429").evict();
304301
assertTrue(deleted);
305302

306303
// unhandled error

0 commit comments

Comments
 (0)