diff --git a/README.md b/README.md index c1d81af..620e699 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,20 @@ SeatsioClient client = new SeatsioClient(Region.EU, ).maxR Passing in 0 disables exponential backoff completely. In that case, the client will never retry a failed request. +## Timeouts + +By default, the client waits up to 10 seconds to establish a connection, and up to 30 seconds for the server to respond. + +To change these timeouts, create the `SeatsioClient` as follows: + +```java +SeatsioClient client = new SeatsioClient(Region.EU, ) + .connectTimeout(Duration.ofSeconds(5)) + .socketTimeout(Duration.ofSeconds(60)); +``` + +The timeouts apply per `SeatsioClient` instance. A request that times out is not retried: a `SeatsioException` is thrown. + ## Multi-threading It's perfectly fine to re-use an instance of `SeatsioClient` in different threads. All methods are thread-safe. diff --git a/src/main/java/seatsio/SeatsioClient.java b/src/main/java/seatsio/SeatsioClient.java index 17b747f..b11b7a9 100644 --- a/src/main/java/seatsio/SeatsioClient.java +++ b/src/main/java/seatsio/SeatsioClient.java @@ -12,6 +12,8 @@ import seatsio.util.UnirestWrapper; import seatsio.workspaces.Workspaces; +import java.time.Duration; + public class SeatsioClient { private final String baseUrl; @@ -56,6 +58,16 @@ public SeatsioClient maxRetries(int maxRetries) { return this; } + public SeatsioClient connectTimeout(Duration connectTimeout) { + unirest.connectTimeout(connectTimeout); + return this; + } + + public SeatsioClient socketTimeout(Duration socketTimeout) { + unirest.socketTimeout(socketTimeout); + return this; + } + public String getBaseUrl() { return baseUrl; } diff --git a/src/main/java/seatsio/util/UnirestWrapper.java b/src/main/java/seatsio/util/UnirestWrapper.java index 5cbff59..701736b 100644 --- a/src/main/java/seatsio/util/UnirestWrapper.java +++ b/src/main/java/seatsio/util/UnirestWrapper.java @@ -3,6 +3,7 @@ import kong.unirest.*; import seatsio.SeatsioException; +import java.time.Duration; import java.util.concurrent.atomic.AtomicReference; import static java.nio.charset.StandardCharsets.UTF_8; @@ -10,10 +11,13 @@ public class UnirestWrapper { private static UnirestInstance unirest = new UnirestInstance(new Config() - .socketTimeout(10000) + .connectTimeout(10000) + .socketTimeout(30000) .concurrency(200, 200)); private int maxRetries = 5; + private int connectTimeoutMillis = 10000; + private int socketTimeoutMillis = 30000; private final String secretKey; private final String workspaceKey; @@ -36,6 +40,8 @@ private byte[] execute(HttpRequest request, int retryCount) { AtomicReference response = new AtomicReference<>(); AtomicReference responseBody = new AtomicReference<>(); request.header("X-Client-Lib", "Java"); + request.connectTimeout(connectTimeoutMillis); + request.socketTimeout(socketTimeoutMillis); authenticate(request, secretKey, workspaceKey).thenConsume(r -> { response.set((RawResponse) r); responseBody.set(response.get().getContentAsBytes()); @@ -93,6 +99,16 @@ public UnirestWrapper maxRetries(int maxRetries) { return this; } + public UnirestWrapper connectTimeout(Duration connectTimeout) { + this.connectTimeoutMillis = Math.toIntExact(connectTimeout.toMillis()); + return this; + } + + public UnirestWrapper socketTimeout(Duration socketTimeout) { + this.socketTimeoutMillis = Math.toIntExact(socketTimeout.toMillis()); + return this; + } + public static void reinitializeHttpConnectionPool() { unirest.shutDown(false); } diff --git a/src/test/java/seatsio/TimeoutTest.java b/src/test/java/seatsio/TimeoutTest.java new file mode 100644 index 0000000..35e3861 --- /dev/null +++ b/src/test/java/seatsio/TimeoutTest.java @@ -0,0 +1,52 @@ +package seatsio; + +import com.sun.net.httpserver.HttpServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class TimeoutTest { + + private HttpServer server; + private final AtomicInteger requestCount = new AtomicInteger(); + + @BeforeEach + public void startSlowServer() throws Exception { + server = HttpServer.create(new InetSocketAddress("localhost", 0), 0); + server.createContext("/", exchange -> { + requestCount.incrementAndGet(); + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + exchange.sendResponseHeaders(200, -1); + exchange.close(); + }); + server.start(); + } + + @AfterEach + public void stopServer() { + server.stop(0); + } + + @Test + public void socketTimeoutIsConfigurableAndTimedOutRequestsAreNotRetried() { + SeatsioClient client = new SeatsioClient("secretKey", null, "http://localhost:" + server.getAddress().getPort()) + .socketTimeout(Duration.ofMillis(300)); + + Instant start = Instant.now(); + assertThatThrownBy(() -> client.charts.retrieve("someChart")).isInstanceOf(SeatsioException.class); + + assertThat(Duration.between(start, Instant.now())).isLessThan(Duration.ofSeconds(2)); + assertThat(requestCount.get()).isEqualTo(1); + } +}