Skip to content

Commit

Permalink
Make retry feature explicit
Browse files Browse the repository at this point in the history
Previously the retry feature was an implementation detail. It might
confuse users when configuring a timeout and the actual request will
take multitude of that in case of retries.

This commit adds a new constructor to configure an explicit retry
count. The old constructor's behaviour remains unchanged by using a
default of 3 retries. Use 0 (KM200.RETRY_DISABLED) to disable retries
completely.
  • Loading branch information
malkusch committed Nov 6, 2023
1 parent 9631942 commit 2559bcc
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 43 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>de.malkusch.km200</groupId>
<artifactId>km200</artifactId>
<version>2.0.11-SNAPSHOT</version>
<version>2.1.0-SNAPSHOT</version>
<name>KM200</name>
<description>KM200 API</description>
<parent>
Expand Down
109 changes: 67 additions & 42 deletions src/main/java/de/malkusch/km200/KM200.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@
/**
* This is an API for Bosch/Buderus/Junkers heaters with a KM200 gateway.
*
* Example:
*
* <pre>
* {@code
* var uri = "http://192.168.0.44";
* var gatewayPassword = "1234-5678-90ab-cdef";
* var privatePassword = "secretExample";
* var timeout = Duration.ofSeconds(5);
* var salt = "1234567890aabbccddeeff11223344556677889900aabbccddeeffa0a1a2b2d3";
*
* var km200 = new KM200(uri, timeout, gatewayPassword, privatePassword, salt);
*
* // Read the heater's time
* var time = km200.queryString("/gateway/DateTime");
* System.out.println(time);
*
* // Update the heater's time
* km200.update("/gateway/DateTime", LocalDateTime.now());
*
* // Explore the heater's endpoints
* km200.endpoints().forEach(System.out::println);
* }
* </pre>
*
* Although code wise this class is thread safe, it is highly recommended to not
* use it concurrently. Chances are that your KM200 gateway itself is not thread
* safe.
Expand All @@ -45,18 +69,21 @@ public final class KM200 {
private final Duration timeout;
private final String uri;

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);
private final FailsafeExecutor<HttpResponse<byte[]>> retryQuery;
private final FailsafeExecutor<HttpResponse<String>> retryUpdate;

@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());
public static final int RETRY_DEFAULT = 3;
public static final int RETRY_DISABLED = 0;

/**
* Configure the KM200 API with a default retry of {@link #RETRY_DEFAULT}.
*
* @see #KM200(String, int, Duration, String, String, String)
**/
public KM200(String uri, Duration timeout, String gatewayPassword, String privatePassword, String salt)
throws KM200Exception, IOException, InterruptedException {

this(uri, RETRY_DEFAULT, timeout, gatewayPassword, privatePassword, salt);
}

/**
Expand All @@ -65,57 +92,38 @@ RetryPolicy.<T> builder() //
* This will also issue a silent query to /system to verify that you
* configuration is correct.
*
* Example:
*
* <pre>
* {@code
* var uri = "http://192.168.0.44";
* var gatewayPassword = "1234-5678-90ab-cdef";
* var privatePassword = "secretExample";
* var timeout = Duration.ofSeconds(5);
* var salt = "1234567890aabbccddeeff11223344556677889900aabbccddeeffa0a1a2b2d3";
*
* var km200 = new KM200(uri, timeout, gatewayPassword, privatePassword, salt);
*
* // Read the heater's time
* var time = km200.queryString("/gateway/DateTime");
* System.out.println(time);
*
* // Update the heater's time
* km200.update("/gateway/DateTime", LocalDateTime.now());
*
* // Explore the heater's endpoints
* km200.endpoints().forEach(System.out::println);
* }
* </pre>
*
* @param uri
* The base URI of your KM200 e.g. http://192.168.0.44
* @param retries
* The amount of retries. Set to {@link #RETRY_DISABLED} to
* disable retrying.
* @param timeout
* An IO timeout for requests to your heater
* An IO timeout for individual requests to your heater. Retries
* might block the API longer than this timeout.
* @param gatewayPassword
* The constant gateway password which you need to read out from
* your heater's display e.g. 1234-5678-90ab-cdef
* @param privatePassword
* The private password which you did assign in the app. If you
* forgot your private password you can start the "reset internet
* password" flow in the menu of your heater and then reassign a
* new passwort in the app.
* new password in the app.
* @param salt
* The salt in the hexadecimal representation e.g. "12a0b2…"
*
* @throws KM200Exception
* @throws IOException
* @throws InterruptedException
*/
public KM200(String uri, Duration timeout, String gatewayPassword, String privatePassword, String salt)
public KM200(String uri, int retries, Duration timeout, String gatewayPassword, String privatePassword, String salt)
throws KM200Exception, IOException, InterruptedException {

assertNotBlank(uri, "uri must not be blank");
requireNonNull(timeout);
assertNotBlank(gatewayPassword, "gatewayPassword must not be blank");
assertNotBlank(privatePassword, "privatePassword must not be blank");
assertNotBlank(salt, "salt must not be blank");
assertNotNegative(retries, "retries must not be negative");

var device = new KM200Device();
device.setCharSet("UTF-8");
Expand All @@ -127,6 +135,8 @@ public KM200(String uri, Duration timeout, String gatewayPassword, String privat
this.device = device;

this.comm = new KM200Comm();
this.retryQuery = buildRetry(retries, IOException.class, ServerError.class);
this.retryUpdate = buildRetry(retries, ServerError.class);
this.timeout = timeout;
this.uri = uri.replaceAll("/*$", "");
this.http = newBuilder().connectTimeout(timeout).cookieHandler(new CookieManager()).followRedirects(ALWAYS)
Expand Down Expand Up @@ -167,8 +177,6 @@ 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 @@ -189,8 +197,6 @@ private void update(String path, Object update) throws KM200Exception, IOExcepti
}
}

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("/")) {
Expand Down Expand Up @@ -294,9 +300,28 @@ private HttpRequest.Builder request(String path) {
.timeout(timeout);
}

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(int retries, Class<? extends Throwable>... exceptions) {
return Failsafe.with( //
RetryPolicy.<T> builder() //
.handle(exceptions) //
.withMaxRetries(retries) //
.withDelay(RETRY_DELAY_MIN, RETRY_DELAY_MAX) //
.build());
}

private static void assertNotBlank(String var, String message) {
if (requireNonNull(var).isBlank()) {
throw new IllegalArgumentException(message);
}
}

private static void assertNotNegative(int var, String message) {
if (var < 0) {
throw new IllegalArgumentException(message);
}
}
}
21 changes: 21 additions & 0 deletions src/test/java/de/malkusch/km200/KM200Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static de.malkusch.km200.KM200.RETRY_DISABLED;
import static de.malkusch.km200.KM200.USER_AGENT;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
Expand Down Expand Up @@ -395,6 +396,26 @@ public void queryShouldWaitWhenRetrying() throws Exception {
verify(4, getRequestedFor(urlEqualTo("/retry-wait")));
}

@Test
public void queryShouldNotRetryWhenDisabled() throws Exception {
stubFor(get("/retry-disabled").willReturn(serverError()));
var km200 = new KM200(URI, RETRY_DISABLED, TIMEOUT, GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

assertThrows(KM200Exception.class, () -> km200.queryString("/retry-disabled"));

verify(1, getRequestedFor(urlEqualTo("/retry-disabled")));
}

@Test
public void updateShouldNotRetryWhenDisabled() throws Exception {
stubFor(post("/update-retry-disabled").willReturn(serverError()));
var km200 = new KM200(URI, RETRY_DISABLED, TIMEOUT, GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

assertThrows(KM200Exception.class, () -> km200.update("/update-retry-disabled", 42));

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

private static String loadBody(String path) throws IOException {
return resourceToString(path, UTF_8, KM200Test.class.getClassLoader());
}
Expand Down

0 comments on commit 2559bcc

Please sign in to comment.