From 547c249ca80220b8a546278255e31e852691e57a Mon Sep 17 00:00:00 2001 From: Markus Malkusch Date: Fri, 3 Nov 2023 13:54:04 +0100 Subject: [PATCH] Workaround JDK http timeout bug --- pom.xml | 7 +- .../malkusch/km200/HttpTimeoutWorkaround.java | 77 +++++++++++++++++++ src/main/java/de/malkusch/km200/KM200.java | 16 ++-- 3 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 src/main/java/de/malkusch/km200/HttpTimeoutWorkaround.java diff --git a/pom.xml b/pom.xml index 2ebf664..f3314ec 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,6 @@ - + 4.0.0 de.malkusch.km200 km200 @@ -13,7 +15,8 @@ https://github.com/malkusch/${project.artifactId} scm:git:git://github.com/malkusch/${project.artifactId}.git - scm:git:git@github.com:malkusch/${project.artifactId}.git + + scm:git:git@github.com:malkusch/${project.artifactId}.git malkusch/km200 diff --git a/src/main/java/de/malkusch/km200/HttpTimeoutWorkaround.java b/src/main/java/de/malkusch/km200/HttpTimeoutWorkaround.java new file mode 100644 index 0000000..e503b1f --- /dev/null +++ b/src/main/java/de/malkusch/km200/HttpTimeoutWorkaround.java @@ -0,0 +1,77 @@ +package de.malkusch.km200; + +import static java.net.http.HttpClient.newBuilder; +import static java.net.http.HttpClient.Redirect.ALWAYS; +import static java.util.Optional.ofNullable; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodyHandler; +import java.net.http.HttpTimeoutException; +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +final class HttpTimeoutWorkaround implements AutoCloseable { + + private final Duration timeout; + private final HttpClient client; + private final ScheduledExecutorService executor; + + HttpTimeoutWorkaround(Duration timeout) { + this.client = newBuilder() // + .connectTimeout(timeout) // + .cookieHandler(new CookieManager()) // + .followRedirects(ALWAYS) // + .build(); + + this.timeout = timeout.plusSeconds(1); + + executor = Executors.newSingleThreadScheduledExecutor(r -> { + var thread = new Thread(r, "KM200"); + thread.setUncaughtExceptionHandler((t, e) -> { + e.printStackTrace(); + }); + thread.setDaemon(true); + return thread; + }); + } + + public HttpResponse send(HttpRequest request, BodyHandler bodyHandler) + throws IOException, InterruptedException { + + try { + return executor // + .submit(() -> client.send(request, bodyHandler)) // + .get(timeout.toMillis(), MILLISECONDS); + + } catch (TimeoutException e) { + throw new HttpTimeoutException("KM200 timed out (HttpClient timed out)"); + + } catch (ExecutionException e) { + if (e.getCause() instanceof HttpTimeoutException timeoutException) { + throw timeoutException; + } + throw new IOException("KM200 failed", ofNullable(e.getCause()).orElse(e)); + } + } + + @Override + public void close() throws Exception { + executor.shutdown(); + if (executor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + return; + } + executor.shutdownNow(); + if (!executor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new IOException("Closing KM200 failed"); + } + } +} diff --git a/src/main/java/de/malkusch/km200/KM200.java b/src/main/java/de/malkusch/km200/KM200.java index 314cd76..7c45445 100644 --- a/src/main/java/de/malkusch/km200/KM200.java +++ b/src/main/java/de/malkusch/km200/KM200.java @@ -1,14 +1,10 @@ package de.malkusch.km200; -import static java.net.http.HttpClient.newBuilder; -import static java.net.http.HttpClient.Redirect.ALWAYS; import static java.util.Objects.requireNonNull; import java.io.IOException; import java.math.BigDecimal; -import java.net.CookieManager; import java.net.URI; -import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpRequest.BodyPublishers; import java.net.http.HttpResponse; @@ -35,12 +31,12 @@ * use it concurrently. Chances are that your KM200 gateway itself is not thread * safe. */ -public final class KM200 { +public final class KM200 implements AutoCloseable { private final KM200Device device; private final KM200Comm comm; private final ObjectMapper mapper = new ObjectMapper(); - private final HttpClient http; + private final HttpTimeoutWorkaround http; private final Duration timeout; private final String uri; private final FailsafeExecutor> retry; @@ -115,8 +111,7 @@ public KM200(String uri, Duration timeout, String gatewayPassword, String privat this.comm = new KM200Comm(); this.timeout = timeout; this.uri = uri.replaceAll("/*$", ""); - this.http = newBuilder().connectTimeout(timeout).cookieHandler(new CookieManager()).followRedirects(ALWAYS) - .build(); + this.http = new HttpTimeoutWorkaround(timeout); retry = Failsafe.with( // RetryPolicy.> builder() // @@ -283,4 +278,9 @@ private static void assertNotBlank(String var, String message) { throw new IllegalArgumentException(message); } } + + @Override + public void close() throws Exception { + http.close(); + } }