Skip to content

Commit

Permalink
Retry updates
Browse files Browse the repository at this point in the history
  • Loading branch information
malkusch committed Nov 6, 2023
1 parent bb6430e commit 527801a
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 13 deletions.
38 changes: 25 additions & 13 deletions src/main/java/de/malkusch/km200/KM200.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,20 @@ public final class KM200 {
private final HttpClient http;
private final Duration timeout;
private final String uri;
private final FailsafeExecutor<HttpResponse<byte[]>> retry;

private static final int RETRY_COUNT = 3;
private static final Duration RETRY_DELAY_MIN = Duration.ofSeconds(1);
private static final Duration RETRY_DELAY_MAX = Duration.ofSeconds(2);

@SafeVarargs
private static <T> FailsafeExecutor<T> buildRetry(Class<? extends Throwable>... exceptions) {
return Failsafe.with( //
RetryPolicy.<T> builder() //
.handle(exceptions) //
.withMaxRetries(RETRY_COUNT) //
.withDelay(RETRY_DELAY_MIN, RETRY_DELAY_MAX) //
.build());
}

/**
* Configure the KM200 API.
Expand Down Expand Up @@ -119,13 +132,6 @@ public KM200(String uri, Duration timeout, String gatewayPassword, String privat
this.http = newBuilder().connectTimeout(timeout).cookieHandler(new CookieManager()).followRedirects(ALWAYS)
.build();

retry = Failsafe.with( //
RetryPolicy.<HttpResponse<byte[]>> builder() //
.handle(IOException.class, ServerError.class) //
.withMaxRetries(3) //
.withDelay(Duration.ofSeconds(1), Duration.ofSeconds(2)) //
.build());

query("/system");
}

Expand Down Expand Up @@ -161,6 +167,8 @@ public void update(String path, BigDecimal value) throws KM200Exception, IOExcep
update(path, update);
}

private final FailsafeExecutor<HttpResponse<String>> retryUpdate = buildRetry(ServerError.class);

private void update(String path, Object update) throws KM200Exception, IOException, InterruptedException {
String json = null;
try {
Expand All @@ -173,21 +181,24 @@ private void update(String path, Object update) throws KM200Exception, IOExcepti
throw new KM200Exception("Could not encrypt update " + json);
}
var request = request(path).POST(BodyPublishers.ofByteArray(encrypted)).build();
var response = send(request, BodyHandlers.ofString());
var response = sendWithRetries(retryUpdate, request, BodyHandlers.ofString());

if (!(response.statusCode() >= 200 && response.statusCode() < 300)) {
throw new KM200Exception(
String.format("Failed to update %s [%d]: %s", path, response.statusCode(), response.body()));
}
}

private final FailsafeExecutor<HttpResponse<byte[]>> retryQuery = buildRetry(IOException.class, ServerError.class);;

public String query(String path) throws KM200Exception, IOException, InterruptedException {
assertNotBlank(path, "Path must not be blank");
if (!path.startsWith("/")) {
throw new IllegalArgumentException("Path must start with a leading /");
}

var response = get(path);
var request = request(path).GET().build();
var response = sendWithRetries(retryQuery, request, BodyHandlers.ofByteArray());
var encrypted = response.body();
if (encrypted == null) {
throw new KM200Exception("No response when querying " + path);
Expand All @@ -208,10 +219,11 @@ public String query(String path) throws KM200Exception, IOException, Interrupted
return decrypted;
}

private HttpResponse<byte[]> get(String path) throws IOException, InterruptedException {
private <T> HttpResponse<T> sendWithRetries(FailsafeExecutor<HttpResponse<T>> retry, HttpRequest request,
BodyHandler<T> bodyHandler) throws IOException, InterruptedException {

try {
var request = request(path).GET().build();
return retry.get(() -> send(request, BodyHandlers.ofByteArray()));
return retry.get(() -> send(request, bodyHandler));

} catch (FailsafeException e) {
var cause = e.getCause();
Expand Down
37 changes: 37 additions & 0 deletions src/test/java/de/malkusch/km200/KM200Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ public void queryShouldRetryOnTimeout() throws Exception {
verify(2, getRequestedFor(urlEqualTo("/retry")));
}

@Test
public void updateShouldNotRetryOnTimeout() throws Exception {
stubFor(post("/retry-update-timeout").willReturn(ok(loadBody("gateway.DateTime")).withFixedDelay(100)));
var km200 = new KM200(URI, Duration.ofMillis(50), GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

assertThrows(HttpTimeoutException.class, () -> km200.update("/retry-update-timeout", 42));

verify(1, postRequestedFor(urlEqualTo("/retry-update-timeout")));
}

@Test
public void queryShouldRetryOnServerError() throws Exception {
stubFor(get("/retry500").inScenario("retry500").whenScenarioStateIs(STARTED).willReturn(serverError())
Expand All @@ -260,6 +270,23 @@ public void queryShouldRetryOnServerError() throws Exception {
verify(4, getRequestedFor(urlEqualTo("/retry500")));
}

@Test
public void updateShouldRetryOnServerError() throws Exception {
stubFor(post("/update-retry500").inScenario("updateRetry500").whenScenarioStateIs(STARTED)
.willReturn(serverError()).willSetStateTo("retry2"));
stubFor(post("/update-retry500").inScenario("updateRetry500").whenScenarioStateIs("retry2")
.willReturn(serverError()).willSetStateTo("retry3"));
stubFor(post("/update-retry500").inScenario("updateRetry500").whenScenarioStateIs("retry3")
.willReturn(serverError()).willSetStateTo("ok"));
stubFor(post("/update-retry500").inScenario("updateRetry500").whenScenarioStateIs("ok")
.willReturn(ok(loadBody("gateway.DateTime"))));
var km200 = new KM200(URI, TIMEOUT, GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

km200.update("/update-retry500", 42);

verify(4, postRequestedFor(urlEqualTo("/update-retry500")));
}

@ParameterizedTest
@EnumSource(Fault.class)
public void queryShouldRetryOnBadResponse(Fault fault) throws Exception {
Expand All @@ -279,6 +306,16 @@ public void queryShouldRetryOnBadResponse(Fault fault) throws Exception {
verify(4, getRequestedFor(urlEqualTo("/retry-bad-response")));
}

@ParameterizedTest
@EnumSource(Fault.class)
public void updateShouldNotRetryOnBadResponse(Fault fault) throws Exception {
stubFor(post("/update-bad-response-retry").willReturn(aResponse().withFault(fault)));
var km200 = new KM200(URI, TIMEOUT, GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

assertThrows(IOException.class, () -> km200.update("/update-bad-response-retry", 42));
verify(1, postRequestedFor(urlEqualTo("/update-bad-response-retry")));
}

@Test
public void queryShouldWaitWhenRetrying() throws Exception {
stubFor(get("/retry-wait").inScenario("retry-wait").whenScenarioStateIs(STARTED).willReturn(serverError())
Expand Down

0 comments on commit 527801a

Please sign in to comment.