Skip to content

Commit

Permalink
Workaround hanging KM200
Browse files Browse the repository at this point in the history
It appears that the JDK's http client has a bug which causes it to hang
infinetely. This commit adds a workaround by wrapping the call into
an executor with a hard timeout.

See also: https://bugs.openjdk.org/browse/JDK-8258397
          https://bugs.openjdk.org/browse/JDK-8208693
          https://bugs.openjdk.org/browse/JDK-8254223
  • Loading branch information
malkusch committed Nov 7, 2023
1 parent 697ff98 commit 39efec6
Show file tree
Hide file tree
Showing 2 changed files with 64 additions and 4 deletions.
43 changes: 42 additions & 1 deletion src/main/java/de/malkusch/km200/http/JdkHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
import static java.net.http.HttpClient.newBuilder;
import static java.net.http.HttpClient.Redirect.ALWAYS;
import static java.net.http.HttpRequest.BodyPublishers.ofByteArray;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import java.io.IOException;
import java.net.CookieManager;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import de.malkusch.km200.KM200Exception;
import de.malkusch.km200.http.Http.Request.Get;
Expand All @@ -22,11 +27,13 @@ public final class JdkHttp extends Http {
private final String uri;
private final String userAgent;
private final Duration timeout;
private final Duration hardTimeout;

public JdkHttp(String uri, String userAgent, Duration timeout) {
this.uri = uri;
this.userAgent = userAgent;
this.timeout = timeout;
this.hardTimeout = timeout.plus(timeout.dividedBy(2)); // 1.5 * timeout

this.client = newBuilder() //
.connectTimeout(timeout) //
Expand All @@ -38,7 +45,8 @@ public JdkHttp(String uri, String userAgent, Duration timeout) {
@Override
public Response send(Request request) throws IOException, InterruptedException, KM200Exception {
var httpRequest = httpRequest(request);
var response = client.send(httpRequest, BodyHandlers.ofByteArray());
var response = send(httpRequest);

var status = response.statusCode();

if (status >= 200 && status <= 299) {
Expand All @@ -56,6 +64,39 @@ public Response send(Request request) throws IOException, InterruptedException,
}
}

private HttpResponse<byte[]> send(HttpRequest request) throws IOException, InterruptedException {
try {
/*
* It appears that the JDK's http client has a bug which causes it
* to block infinitely. This is the async workaround with the hard
* timeout of CompletableFuture.get().
*
* https://bugs.openjdk.org/browse/JDK-8258397
* https://bugs.openjdk.org/browse/JDK-8208693
* https://bugs.openjdk.org/browse/JDK-8254223
*/
return client.sendAsync(request, BodyHandlers.ofByteArray()) //
.get(hardTimeout.toMillis(), MILLISECONDS);

} catch (TimeoutException e) {
throw new HttpTimeoutException(request.uri() + " timed out");

} catch (ExecutionException e) {
if (e.getCause() instanceof IOException cause) {
throw cause;

} else if (e.getCause() instanceof InterruptedException cause) {
throw cause;

} else if (e.getCause() instanceof KM200Exception cause) {
throw cause;

} else {
throw new KM200Exception("Unexpected error for " + request.uri(), e);
}
}
}

private HttpRequest httpRequest(Request request) {
var builder = HttpRequest.newBuilder(URI.create(uri + request.path())) //
.setHeader("User-Agent", userAgent) //
Expand Down
25 changes: 22 additions & 3 deletions src/test/java/de/malkusch/km200/KM200Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,18 +316,37 @@ public void updateShouldTimeoutResponseBody() throws Exception {
assertThrows(HttpTimeoutException.class, () -> km200.update("/update-timeout-body", 42));
}

@Test
public void queryShouldHardTimeout() throws Exception {
stubFor(get("/hard-timeout").willReturn(ok(loadBody("gateway.DateTime")).withChunkedDribbleDelay(10, 200)));
var km200 = new KM200(URI, Duration.ofMillis(50), GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

assertThrows(HttpTimeoutException.class, () -> km200.query("/hard-timeout"));
}

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

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

@Test
public void queryShouldRetryOnTimeout() throws Exception {
stubFor(get("/retry").inScenario("retry").whenScenarioStateIs(STARTED)
.willReturn(notFound().withFixedDelay(100)).willSetStateTo("retry"));
stubFor(get("/retry").inScenario("retry").whenScenarioStateIs("retry")
.willReturn(ok(loadBody("gateway.DateTime")).withFixedDelay(100)).willSetStateTo("retry2"));
stubFor(get("/retry").inScenario("retry").whenScenarioStateIs("retry2").willReturn(serverError())
.willReturn(ok(loadBody("gateway.DateTime")).withChunkedDribbleDelay(10, 200)).willSetStateTo("ok"));
stubFor(get("/retry").inScenario("retry").whenScenarioStateIs("ok")
.willReturn(ok(loadBody("gateway.DateTime"))));
var km200 = new KM200(URI, Duration.ofMillis(50), GATEWAY_PASSWORD, PRIVATE_PASSWORD, SALT);

var dateTime = km200.queryString("/retry");

assertEquals("2021-09-21T10:49:25", dateTime);
verify(2, getRequestedFor(urlEqualTo("/retry")));
verify(3, getRequestedFor(urlEqualTo("/retry")));
}

@Test
Expand Down

0 comments on commit 39efec6

Please sign in to comment.