Skip to content

Commit 7c9a870

Browse files
committed
#14 - Add RetryHandler with the option to define it on the HttpContext
1 parent 303f930 commit 7c9a870

15 files changed

+188
-26
lines changed

client/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
--add-modules com.fasterxml.jackson.databind
9797
--add-opens io.avaje.http.client/io.avaje.http.client=ALL-UNNAMED
9898
--add-opens io.avaje.http.client/org.example.webserver=ALL-UNNAMED
99+
--add-opens io.avaje.http.client/org.example.github=ALL-UNNAMED
99100
--add-opens io.avaje.http.client/org.example.webserver=com.fasterxml.jackson.databind
100101
--add-opens io.avaje.http.client/org.example.github=com.fasterxml.jackson.databind
101102
</argLine>

client/src/main/java/io/avaje/http/client/DHttpClientContext.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ class DHttpClientContext implements HttpClientContext {
1919
private final BodyAdapter bodyAdapter;
2020
private final RequestListener requestListener;
2121
private final RequestIntercept requestIntercept;
22+
private final RetryHandler retryHandler;
2223
private final boolean withAuthToken;
2324
private final AuthTokenProvider authTokenProvider;
2425
private final AtomicReference<AuthToken> tokenRef = new AtomicReference<>();
2526

26-
DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RequestListener requestListener, AuthTokenProvider authTokenProvider, RequestIntercept intercept) {
27+
DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RetryHandler retryHandler, RequestListener requestListener, AuthTokenProvider authTokenProvider, RequestIntercept intercept) {
2728
this.httpClient = httpClient;
2829
this.baseUrl = baseUrl;
2930
this.requestTimeout = requestTimeout;
3031
this.bodyAdapter = bodyAdapter;
32+
this.retryHandler = retryHandler;
3133
this.requestListener = requestListener;
3234
this.authTokenProvider = authTokenProvider;
3335
this.withAuthToken = authTokenProvider != null;
@@ -58,12 +60,14 @@ public <T> T create(Class<T> clientInterface) {
5860
private <T> String clientImplementationClassName(Class<T> clientInterface) {
5961
String packageName = clientInterface.getPackageName();
6062
String simpleName = clientInterface.getSimpleName();
61-
return packageName + ".httpclient." + simpleName + "$httpclient";
63+
return packageName + ".httpclient." + simpleName + "$HttpClient";
6264
}
6365

6466
@Override
6567
public HttpClientRequest request() {
66-
return new DHttpClientRequest(this, requestTimeout);
68+
return retryHandler == null
69+
? new DHttpClientRequest(this, requestTimeout)
70+
: new DHttpClientRequestWithRetry(this, requestTimeout, retryHandler);
6771
}
6872

6973
@Override

client/src/main/java/io/avaje/http/client/DHttpClientContextBuilder.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,17 @@
1313
class DHttpClientContextBuilder implements HttpClientContext.Builder {
1414

1515
private HttpClient client;
16-
1716
private String baseUrl;
18-
1917
private Duration requestTimeout = Duration.ofSeconds(20);
20-
2118
private BodyAdapter bodyAdapter;
19+
private RetryHandler retryHandler;
20+
private AuthTokenProvider authTokenProvider;
2221

2322
private CookieHandler cookieHandler = new CookieManager();
24-
2523
private HttpClient.Redirect redirect = HttpClient.Redirect.NORMAL;
26-
2724
private HttpClient.Version version;
2825
private Executor executor;
2926

30-
private AuthTokenProvider authTokenProvider;
31-
3227
private final List<RequestIntercept> interceptors = new ArrayList<>();
3328
private final List<RequestListener> listeners = new ArrayList<>();
3429

@@ -59,6 +54,12 @@ public HttpClientContext.Builder withBodyAdapter(BodyAdapter adapter) {
5954
return this;
6055
}
6156

57+
@Override
58+
public HttpClientContext.Builder withRetryHandler(RetryHandler retryHandler) {
59+
this.retryHandler = retryHandler;
60+
return this;
61+
}
62+
6263
@Override
6364
public HttpClientContext.Builder withRequestListener(RequestListener requestListener) {
6465
this.listeners.add(requestListener);
@@ -108,7 +109,7 @@ public HttpClientContext build() {
108109
if (client == null) {
109110
client = defaultClient();
110111
}
111-
return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, buildListener(), authTokenProvider, buildIntercept());
112+
return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, retryHandler, buildListener(), authTokenProvider, buildIntercept());
112113
}
113114

114115
private RequestListener buildListener() {

client/src/main/java/io/avaje/http/client/DHttpClientRequest.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class DHttpClientRequest implements HttpClientRequest, HttpClientResponse {
4545
private boolean loggableResponseBody;
4646
private boolean skipAuthToken;
4747

48-
public DHttpClientRequest(DHttpClientContext context, Duration requestTimeout) {
48+
DHttpClientRequest(DHttpClientContext context, Duration requestTimeout) {
4949
this.context = context;
5050
this.requestTimeout = requestTimeout;
5151
this.url = context.url();
@@ -322,14 +322,21 @@ public <T> List<T> list(Class<T> cls) {
322322
public <T> HttpResponse<T> withResponseHandler(HttpResponse.BodyHandler<T> responseHandler) {
323323
context.beforeRequest(this);
324324
addHeaders();
325-
final long startNanos = System.nanoTime();
326-
final HttpResponse<T> response = context.send(httpRequest, responseHandler);
327-
requestTimeNanos = System.nanoTime() - startNanos;
325+
HttpResponse<T> response = performSend(responseHandler);
328326
httpResponse = response;
329327
context.afterResponse(this);
330328
return response;
331329
}
332330

331+
protected <T> HttpResponse<T> performSend(HttpResponse.BodyHandler<T> responseHandler) {
332+
final long startNanos = System.nanoTime();
333+
try {
334+
return context.send(httpRequest, responseHandler);
335+
} finally {
336+
requestTimeNanos = System.nanoTime() - startNanos;
337+
}
338+
}
339+
333340
@Override
334341
public HttpResponse<byte[]> asByteArray() {
335342
return withResponseHandler(HttpResponse.BodyHandlers.ofByteArray());
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.avaje.http.client;
2+
3+
import java.net.http.HttpResponse;
4+
import java.time.Duration;
5+
6+
/**
7+
* Extends DHttpClientRequest with retry attempts.
8+
*/
9+
class DHttpClientRequestWithRetry extends DHttpClientRequest {
10+
11+
private final RetryHandler retryHandler;
12+
private int retryCount;
13+
14+
DHttpClientRequestWithRetry(DHttpClientContext context, Duration requestTimeout, RetryHandler retryHandler) {
15+
super(context, requestTimeout);
16+
this.retryHandler = retryHandler;
17+
}
18+
19+
/**
20+
* Perform send with retry.
21+
*/
22+
@Override
23+
protected <T> HttpResponse<T> performSend(HttpResponse.BodyHandler<T> responseHandler) {
24+
HttpResponse<T> res;
25+
res = super.performSend(responseHandler);
26+
if (res.statusCode() < 300) {
27+
return res;
28+
}
29+
while (retryHandler.isRetry(retryCount++, res)) {
30+
res = super.performSend(responseHandler);
31+
}
32+
return res;
33+
}
34+
35+
}

client/src/main/java/io/avaje/http/client/HttpClientContext.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,11 @@ interface Builder {
159159
*/
160160
Builder withBodyAdapter(BodyAdapter adapter);
161161

162+
/**
163+
* Set a RetryHandler to use to retry requests.
164+
*/
165+
Builder withRetryHandler(RetryHandler retryHandler);
166+
162167
/**
163168
* Add a request listener. Multiple listeners may be added, when
164169
* do so they will process events in the order they were added.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.avaje.http.client;
2+
3+
import java.net.http.HttpResponse;
4+
5+
/**
6+
* Define how retry should occur on a request.
7+
*/
8+
public interface RetryHandler {
9+
10+
/**
11+
* Return true if the request should be retried.
12+
*
13+
* @param retryCount The number of retry attempts already executed
14+
* @param response The HTTP response
15+
* @return True if the request should be retried or false if not
16+
*/
17+
boolean isRetry(int retryCount, HttpResponse<?> response);
18+
19+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.avaje.http.client;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.net.http.HttpResponse;
7+
8+
/**
9+
* Simple retry with max attempts and linear backoff.
10+
*/
11+
public class SimpleRetryHandler implements RetryHandler {
12+
13+
private static final Logger log = LoggerFactory.getLogger(SimpleRetryHandler.class);
14+
15+
private final int maxRetries;
16+
private final long backoffMillis;
17+
18+
public SimpleRetryHandler(int maxRetries, long backoffMillis) {
19+
this.maxRetries = maxRetries;
20+
this.backoffMillis = backoffMillis;
21+
}
22+
23+
@Override
24+
public boolean isRetry(int retryCount, HttpResponse<?> response) {
25+
if (response.statusCode() < 500 || retryCount >= maxRetries) {
26+
return false;
27+
}
28+
log.debug("retry count:{} status:{} uri:{}", retryCount, response.statusCode(), response.uri());
29+
try {
30+
Thread.sleep(backoffMillis);
31+
} catch (InterruptedException e) {
32+
Thread.currentThread().interrupt();
33+
return false;
34+
}
35+
return true;
36+
}
37+
}

client/src/test/java/io/avaje/http/client/DHttpClientContextTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
class DHttpClientContextTest {
1212

13-
private final DHttpClientContext context = new DHttpClientContext(null, null, null, null, null, null, null);
13+
private final DHttpClientContext context = new DHttpClientContext(null, null, null, null, null, null, null, null);
1414

1515
@Test
1616
void create() {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.avaje.http.client;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.net.http.HttpResponse;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
public class RetryTest extends BaseWebTest {
11+
12+
final HttpClientContext clientContext = initClientWithRetry();
13+
14+
static HttpClientContext initClientWithRetry() {
15+
return HttpClientContext.newBuilder()
16+
.withBaseUrl("http://localhost:8887")
17+
.withBodyAdapter(new JacksonBodyAdapter(new ObjectMapper()))
18+
.withRequestListener(new RequestLogger())
19+
.withRetryHandler(new SimpleRetryHandler(4, 1))
20+
.build();
21+
}
22+
23+
@Test
24+
void retryTest() {
25+
26+
HttpResponse<String> res = clientContext.request()
27+
.path("hello/retry")
28+
.get()
29+
.asString();
30+
31+
assertThat(res.body()).isEqualTo("All good at 3rd attempt");
32+
}
33+
34+
}

0 commit comments

Comments
 (0)