diff --git a/src/main/java/de/malkusch/km200/KM200.java b/src/main/java/de/malkusch/km200/KM200.java index b5ca207..f91574b 100644 --- a/src/main/java/de/malkusch/km200/KM200.java +++ b/src/main/java/de/malkusch/km200/KM200.java @@ -44,7 +44,20 @@ public final class KM200 { private final HttpClient http; private final Duration timeout; private final String uri; - private final FailsafeExecutor> 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 FailsafeExecutor buildRetry(Class... exceptions) { + return Failsafe.with( // + RetryPolicy. builder() // + .handle(exceptions) // + .withMaxRetries(RETRY_COUNT) // + .withDelay(RETRY_DELAY_MIN, RETRY_DELAY_MAX) // + .build()); + } /** * Configure the KM200 API. @@ -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.> builder() // - .handle(IOException.class, ServerError.class) // - .withMaxRetries(3) // - .withDelay(Duration.ofSeconds(1), Duration.ofSeconds(2)) // - .build()); - query("/system"); } @@ -161,6 +167,8 @@ public void update(String path, BigDecimal value) throws KM200Exception, IOExcep update(path, update); } + private final FailsafeExecutor> retryUpdate = buildRetry(ServerError.class); + private void update(String path, Object update) throws KM200Exception, IOException, InterruptedException { String json = null; try { @@ -173,7 +181,7 @@ 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( @@ -181,13 +189,16 @@ private void update(String path, Object update) throws KM200Exception, IOExcepti } } + private final FailsafeExecutor> 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); @@ -208,10 +219,11 @@ public String query(String path) throws KM200Exception, IOException, Interrupted return decrypted; } - private HttpResponse get(String path) throws IOException, InterruptedException { + private HttpResponse sendWithRetries(FailsafeExecutor> retry, HttpRequest request, + BodyHandler 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(); diff --git a/src/test/java/de/malkusch/km200/KM200Test.java b/src/test/java/de/malkusch/km200/KM200Test.java index 4735e51..ec41415 100644 --- a/src/test/java/de/malkusch/km200/KM200Test.java +++ b/src/test/java/de/malkusch/km200/KM200Test.java @@ -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()) @@ -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 { @@ -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())