diff --git a/feasibility-dsf-process/pom.xml b/feasibility-dsf-process/pom.xml
index 2aca210..7c19e6f 100755
--- a/feasibility-dsf-process/pom.xml
+++ b/feasibility-dsf-process/pom.xml
@@ -85,19 +85,25 @@
org.testcontainers
testcontainers
- 1.18.3
+ ${testcontainers.version}
test
org.testcontainers
nginx
- 1.18.3
+ ${testcontainers.version}
+ test
+
+
+ org.testcontainers
+ toxiproxy
+ ${testcontainers.version}
test
org.testcontainers
junit-jupiter
- 1.18.3
+ ${testcontainers.version}
test
diff --git a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java
index 22d7c21..1252194 100644
--- a/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java
+++ b/feasibility-dsf-process/src/main/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientSpringConfig.java
@@ -99,7 +99,11 @@ private FlareWebserviceClient createFlareClient(HttpClient httpClient) {
public HttpClient flareHttpClient(@Qualifier("base-client") SSLContext sslContext,
EvaluationSettingsProvider evaluationSettingsProvider) {
if (EvaluationStrategy.STRUCTURED_QUERY == evaluationSettingsProvider.evaluationStrategy()) {
- HttpClientBuilder builder = new TlsClientFactory(null, sslContext).getNativeHttpClientBuilder();
+ TlsClientFactory clientFactory = new TlsClientFactory(null, sslContext);
+ clientFactory.setConnectTimeout(connectTimeout);
+ clientFactory.setConnectionRequestTimeout(connectTimeout);
+ clientFactory.setSocketTimeout(connectTimeout);
+ HttpClientBuilder builder = clientFactory.getNativeHttpClientBuilder();
BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
diff --git a/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTimeoutsIT.java b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTimeoutsIT.java
new file mode 100644
index 0000000..f9f8d75
--- /dev/null
+++ b/feasibility-dsf-process/src/test/java/de/medizininformatik_initiative/feasibility_dsf_process/client/flare/FlareWebserviceClientImplTimeoutsIT.java
@@ -0,0 +1,116 @@
+package de.medizininformatik_initiative.feasibility_dsf_process.client.flare;
+
+import com.google.common.base.Stopwatch;
+import eu.rekawek.toxiproxy.Proxy;
+import eu.rekawek.toxiproxy.ToxiproxyClient;
+import eu.rekawek.toxiproxy.model.ToxicDirection;
+import eu.rekawek.toxiproxy.model.toxic.Latency;
+import org.assertj.core.api.Condition;
+import org.assertj.core.description.Description;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.ToxiproxyContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+import java.time.Duration;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+@Tag("client")
+@Tag("flare")
+@SpringBootTest(classes = FlareWebserviceClientSpringConfig.class)
+@Testcontainers
+public class FlareWebserviceClientImplTimeoutsIT extends FlareWebserviceClientImplBaseIT {
+
+ private static final int PROXY_PORT = 8666;
+ private static final int RANDOM_CLIENT_TIMEOUT = new Random().nextInt(5000, 20000);
+
+ private Stopwatch executionTimer = Stopwatch.createUnstarted();
+
+ @Autowired protected FlareWebserviceClient flareClient;
+
+ @Container
+ public static ToxiproxyContainer toxiproxy = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.7.0")
+ .withNetwork(DEFAULT_CONTAINER_NETWORK)
+ .dependsOn(flare);
+ private static ToxiproxyClient toxiproxyClient;
+ private static Proxy proxy;
+ private static Latency latency;
+
+ @DynamicPropertySource
+ static void dynamicProperties(DynamicPropertyRegistry registry) {
+ var flareHost = toxiproxy.getHost();
+ var flarePort = toxiproxy.getMappedPort(PROXY_PORT);
+
+ registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.timeout.connect",
+ () -> RANDOM_CLIENT_TIMEOUT);
+ registry.add("de.medizininformatik_initiative.feasibility_dsf_process.evaluation.strategy",
+ () -> "structured-query");
+ registry.add("de.medizininformatik_initiative.feasibility_dsf_process.client.flare.base_url",
+ () -> String.format("http://%s:%s/", flareHost, flarePort));
+ }
+
+ @BeforeAll
+ static void setup() throws IOException {
+ toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort());
+ proxy = toxiproxyClient.createProxy("flare", "0.0.0.0:" + PROXY_PORT,
+ format("%s:%s", flare.getNetworkAliases().get(0), flare.getExposedPorts().get(0)));
+ latency = proxy.toxics().latency("latency", ToxicDirection.UPSTREAM, 0);
+ }
+
+ @BeforeEach
+ void startClock() {
+ executionTimer.reset().start();
+ }
+
+ @Test
+ @DisplayName("flare client fails getting no response after given socket timeout")
+ public void requestFeasibilityWithLongerProxyTimeoutFails() throws IOException {
+ var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json")
+ .openStream().readAllBytes();
+ var proxyTimeout = RANDOM_CLIENT_TIMEOUT + 10000;
+ latency.setLatency(proxyTimeout);
+
+ assertThatThrownBy(() -> flareClient.requestFeasibility(rawStructuredQuery))
+ .describedAs(new Description() {
+
+ @Override
+ public String value() {
+ executionTimer.stop();
+ return format("execution time is %s ms", executionTimer.elapsed(TimeUnit.MILLISECONDS));
+ }
+ })
+ .isInstanceOf(IOException.class)
+ .hasMessageStartingWith("Error sending POST request to flare webservice")
+ .hasCauseInstanceOf(SocketTimeoutException.class)
+ .is(new Condition<>(
+ _e -> Duration.ofMillis(RANDOM_CLIENT_TIMEOUT).minus(executionTimer.elapsed()).isNegative(),
+ "executed longer than client timeout of %dms", RANDOM_CLIENT_TIMEOUT))
+ .is(new Condition<>(_e -> executionTimer.elapsed().minusMillis(proxyTimeout).isNegative(),
+ "executed shorter than proxy timeout of %dms", proxyTimeout));
+ }
+
+ @Test
+ @DisplayName("flare client succeeds getting a response before given socket timeout")
+ public void requestFeasibilityWithShorterProxyTimeoutSucceeds() throws IOException {
+ var rawStructuredQuery = this.getClass().getResource("valid-structured-query.json")
+ .openStream().readAllBytes();
+ latency.setLatency(RANDOM_CLIENT_TIMEOUT - 2000);
+
+ assertThatNoException().isThrownBy(() -> flareClient.requestFeasibility(rawStructuredQuery));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 0ecccee..adff70a 100755
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,7 @@
17
1.3.1
5.1.0
+ 1.19.3
MII Feasibility Processes