implement connection and read timeouts for synchronous requests

This commit is contained in:
Stefan Kalscheuer 2020-11-07 19:17:08 +01:00
parent d3d16e22a0
commit 9e84d9f40d
3 changed files with 114 additions and 4 deletions

View File

@ -475,10 +475,17 @@ public class UraClient implements Serializable {
*/ */
private InputStream request(String url) throws IOException { private InputStream request(String url) throws IOException {
try { try {
return HttpClient.newHttpClient().send( var clientBuilder = HttpClient.newBuilder();
HttpRequest.newBuilder(URI.create(url)).GET().build(), if (config.getConnectTimeout() != null) {
HttpResponse.BodyHandlers.ofInputStream() clientBuilder.connectTimeout(config.getConnectTimeout());
).body(); }
var reqBuilder = HttpRequest.newBuilder(URI.create(url)).GET();
if (config.getTimeout() != null) {
reqBuilder.timeout(config.getTimeout());
}
return clientBuilder.build().send(reqBuilder.build(), HttpResponse.BodyHandlers.ofInputStream()).body();
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new IOException("API request interrupted", e); throw new IOException("API request interrupted", e);

View File

@ -1,6 +1,7 @@
package de.stklcode.pubtrans.ura; package de.stklcode.pubtrans.ura;
import java.io.Serializable; import java.io.Serializable;
import java.time.Duration;
/** /**
* Configurstion Object for the {@link UraClient}. * Configurstion Object for the {@link UraClient}.
@ -17,6 +18,8 @@ public class UraClientConfiguration implements Serializable {
private final String baseURL; private final String baseURL;
private final String instantPath; private final String instantPath;
private final String streamPath; private final String streamPath;
private final Duration connectTimeout;
private final Duration timeout;
/** /**
* Get new configuration {@link Builder} for given base URL. * Get new configuration {@link Builder} for given base URL.
@ -38,6 +41,8 @@ public class UraClientConfiguration implements Serializable {
this.baseURL = builder.baseURL; this.baseURL = builder.baseURL;
this.instantPath = builder.instantPath; this.instantPath = builder.instantPath;
this.streamPath = builder.streamPath; this.streamPath = builder.streamPath;
this.connectTimeout = builder.connectTimeout;
this.timeout = builder.timeout;
} }
/** /**
@ -67,6 +72,24 @@ public class UraClientConfiguration implements Serializable {
return this.streamPath; return this.streamPath;
} }
/**
* Get the connection timeout, if any.
*
* @return Timeout duration or {@code null} if none specified.
*/
public Duration getConnectTimeout() {
return this.connectTimeout;
}
/**
* Get the response timeout, if any.
*
* @return Timeout duration or {@code null} if none specified.
*/
public Duration getTimeout() {
return this.timeout;
}
/** /**
* Builder for {@link UraClientConfiguration} objects. * Builder for {@link UraClientConfiguration} objects.
*/ */
@ -74,6 +97,8 @@ public class UraClientConfiguration implements Serializable {
private final String baseURL; private final String baseURL;
private String instantPath; private String instantPath;
private String streamPath; private String streamPath;
private Duration connectTimeout;
private Duration timeout;
/** /**
* Initialize the builder with mandatory base URL. * Initialize the builder with mandatory base URL.
@ -85,6 +110,8 @@ public class UraClientConfiguration implements Serializable {
this.baseURL = baseURL; this.baseURL = baseURL;
this.instantPath = DEFAULT_INSTANT_PATH; this.instantPath = DEFAULT_INSTANT_PATH;
this.streamPath = DEFAULT_STREAM_PATH; this.streamPath = DEFAULT_STREAM_PATH;
this.connectTimeout = null;
this.timeout = null;
} }
/** /**
@ -109,6 +136,28 @@ public class UraClientConfiguration implements Serializable {
return this; return this;
} }
/**
* Specify a custom connection timeout duration.
*
* @param connectTimeout Timeout duration.
* @return The builder.
*/
public Builder withConnectTimeout(Duration connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* Specify a custom timeout duration.
*
* @param timeout Timeout duration.
* @return The builder.
*/
public Builder withTimeout(Duration timeout) {
this.timeout = timeout;
return this;
}
/** /**
* Finally build the configuration object. * Finally build the configuration object.
* *

View File

@ -29,6 +29,9 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.IOException; import java.io.IOException;
import java.net.http.HttpConnectTimeoutException;
import java.net.http.HttpTimeoutException;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -389,6 +392,57 @@ public class UraClientTest {
assertThat(messages.get(0).getText(), is("Sehr geehrte Fahrgäste, wegen Strassenbauarbeiten kann diese Haltestelle nicht von den Bussen der Linien 17, 44 und N2 angefahren werden.")); assertThat(messages.get(0).getText(), is("Sehr geehrte Fahrgäste, wegen Strassenbauarbeiten kann diese Haltestelle nicht von den Bussen der Linien 17, 44 und N2 angefahren werden."));
} }
@Test
public void timeoutTest() throws IOException {
// Try to read trips from TEST-NET-1 IP that is not routed (hopefully) and will not connect within 100ms.
UraClientException exception = assertThrows(
UraClientException.class,
() -> new UraClient(
UraClientConfiguration.forBaseURL("http://192.0.2.1")
.withConnectTimeout(Duration.ofMillis(100))
.build()
).forDestinationNames("Piccadilly Circus").getTrips(),
"Connection to TEST-NET-1 address should fail"
);
assertTrue(exception.getCause() instanceof HttpConnectTimeoutException, "Exception cause is not HttpConnectionTimeoutException");
// Mock the HTTP call with delay of 200ms, but immediate connection.
WireMock.stubFor(
get(urlPathEqualTo("/interfaces/ura/instant_V1")).willReturn(
aResponse().withFixedDelay(200).withBodyFile("instant_V1_trips_destination.txt")
)
);
assertDoesNotThrow(
() -> new UraClient(
UraClientConfiguration.forBaseURL(httpMock.baseUrl())
.withConnectTimeout(Duration.ofMillis(100))
.build()
).forDestinationNames("Piccadilly Circus").getTrips(),
"Connection timeout should not affect response time."
);
// Now specify response timeout.
exception = assertThrows(
UraClientException.class,
() -> new UraClient(
UraClientConfiguration.forBaseURL(httpMock.baseUrl())
.withTimeout(Duration.ofMillis(100))
.build()
).forDestinationNames("Piccadilly Circus").getTrips(),
"Response timeout did not raise an exception"
);
assertTrue(exception.getCause() instanceof HttpTimeoutException, "Exception cause is not HttpTimeoutException");
assertDoesNotThrow(
() -> new UraClient(
UraClientConfiguration.forBaseURL(httpMock.baseUrl())
.withTimeout(Duration.ofMillis(300))
.build()
).forDestinationNames("Piccadilly Circus").getTrips(),
"Response timeout of 300ms with 100ms delay must not fail"
);
}
private static void mockHttpToFile(int version, String resourceFile) { private static void mockHttpToFile(int version, String resourceFile) {
WireMock.stubFor( WireMock.stubFor(