Skip to content

Commit 3e9a85e

Browse files
committed
Add Performance Tests
Closes: #204
1 parent 6de804a commit 3e9a85e

21 files changed

+951
-64
lines changed

README.md

20 Bytes

Download Data

If a server is set up for the files e.g. NGINX, the files can be fetched by a Request on the URL set in NGINX_FILELOCATIONTORCH_OUTPUT_FILE_SERVER_URL in enviroment variables.

curl -s "http://localhost:8080/da4a1c56-f5d9-468c-b57a-b8186ea4fea8/f33634bd-d51b-463c-a956-93409d96935f.ndjson"

pom.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,30 @@
335335
</configuration>
336336
</plugin>
337337

338+
<plugin>
339+
<groupId>org.codehaus.mojo</groupId>
340+
<artifactId>exec-maven-plugin</artifactId>
341+
<version>3.5.0</version>
342+
<executions>
343+
<execution>
344+
<phase>pre-integration-test</phase>
345+
<goals>
346+
<goal>exec</goal>
347+
</goals>
348+
<configuration>
349+
<executable>jq</executable>
350+
<arguments>
351+
<argument>-f</argument>
352+
<argument>src/test/jq/transaction-bundle-converter.jq</argument>
353+
<argument>target/kds-testdata-2024.0.1/resources/Bundle-mii-exa-test-data-bundle.json</argument>
354+
</arguments>
355+
<outputFile>target/Bundle-mii-exa-test-data-bundle.json</outputFile>
356+
</configuration>
357+
</execution>
358+
</executions>
359+
</plugin>
360+
361+
338362
<!-- Failsafe Plugin for System Tests -->
339363
<plugin>
340364
<groupId>org.apache.maven.plugins</groupId>

src/main/java/de/medizininformatikinitiative/torch/model/fhir/Query.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ public record Query(String type, QueryParams params) {
99
requireNonNull(params);
1010
}
1111

12-
1312
public static Query of(String type, QueryParams params) {
1413
return new Query(type, params);
1514
}
@@ -20,6 +19,6 @@ public static Query ofType(String type) {
2019

2120
@Override
2221
public String toString() {
23-
return params.toString().isEmpty() ? type : type + "?" + params;
22+
return params.params().isEmpty() ? type : type + "?" + params;
2423
}
2524
}

src/main/java/de/medizininformatikinitiative/torch/rest/FhirController.java

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import ca.uhn.fhir.context.FhirContext;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import de.medizininformatikinitiative.torch.config.TorchProperties;
65
import de.medizininformatikinitiative.torch.exceptions.ValidationException;
76
import de.medizininformatikinitiative.torch.model.crtdl.Crtdl;
87
import de.medizininformatikinitiative.torch.service.CrtdlProcessingService;
@@ -14,7 +13,6 @@
1413
import org.slf4j.Logger;
1514
import org.slf4j.LoggerFactory;
1615
import org.springframework.beans.factory.annotation.Autowired;
17-
import org.springframework.beans.factory.annotation.Value;
1816
import org.springframework.context.annotation.Bean;
1917
import org.springframework.http.HttpStatus;
2018
import org.springframework.http.MediaType;
@@ -46,10 +44,10 @@
4644
@RestController
4745
public class FhirController {
4846

47+
private static final Logger logger = LoggerFactory.getLogger(FhirController.class);
4948

50-
private final String torchBaseUrl;
5149
private static final MediaType MEDIA_TYPE_FHIR_JSON = MediaType.valueOf("application/fhir+json");
52-
private static final Logger logger = LoggerFactory.getLogger(FhirController.class);
50+
5351
private final ObjectMapper objectMapper;
5452
private final FhirContext fhirContext;
5553
private final ResultFileManager resultFileManager;
@@ -83,15 +81,11 @@ private record DecodedContent(byte[] crtdl, List<String> patientIds) {
8381
}
8482

8583
@Autowired
86-
public FhirController(
87-
TorchProperties torchProperties, ResultFileManager resultFileManager,
88-
FhirContext fhirContext,
89-
CrtdlProcessingService crtdlProcessingService, ObjectMapper objectMapper, CrtdlValidatorService validatorService) {
90-
this.torchBaseUrl = torchProperties.base().url();
84+
public FhirController(ObjectMapper objectMapper, FhirContext fhirContext, ResultFileManager resultFileManager,
85+
CrtdlProcessingService crtdlProcessingService, CrtdlValidatorService validatorService) {
9186
this.objectMapper = objectMapper;
9287
this.fhirContext = fhirContext;
9388
this.resultFileManager = resultFileManager;
94-
9589
this.crtdlProcessingService = crtdlProcessingService;
9690
this.validatorService = validatorService;
9791
}
@@ -189,7 +183,7 @@ public Mono<ServerResponse> handleExtractData(ServerRequest request) {
189183
.subscribeOn(Schedulers.boundedElastic()).subscribe(); // final fire-and-forget
190184

191185
return accepted()
192-
.header("Content-Location", torchBaseUrl + "/fhir/__status/" + jobId)
186+
.header("Content-Location", request.uriBuilder().replacePath("/fhir/__status/" + jobId).build().toString())
193187
.build();
194188
})
195189
.onErrorResume(IllegalArgumentException.class, e -> {

src/main/java/de/medizininformatikinitiative/torch/service/DataStore.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ public class DataStore {
4949
private static final RetryBackoffSpec ASYNC_POLL_RETRY_SPEC = Retry.fixedDelay(1000, ASYNC_POLL_DELAY)
5050
.filter(AsyncRetryException.class::isInstance);
5151
private static final RetryBackoffSpec RETRY_SPEC = Retry.backoff(5, Duration.ofSeconds(1))
52-
.filter(e -> e instanceof WebClientResponseException e1 &&
53-
shouldRetry((e1).getStatusCode()));
52+
.filter(e -> e instanceof WebClientResponseException e1 && shouldRetry(e1.getStatusCode()));
5453
public static final String APPLICATION_FHIR_JSON = "application/fhir+json";
5554
public static final String CONTENT_TYPE = "Content-Type";
5655

@@ -73,7 +72,7 @@ public DataStore(@Qualifier("fhirClient") WebClient client, FhirContext fhirCont
7372
}
7473

7574
private static boolean shouldRetry(HttpStatusCode code) {
76-
return code.is5xxServerError() || code.value() == 404;
75+
return code.is5xxServerError() || code.value() == 404 || code.value() == 429;
7776
}
7877

7978
private static Exception handleAcceptedResponse(ClientResponse response) {
@@ -155,7 +154,7 @@ private String serializeBatchBundle(Map<String, Set<String>> idsByType) {
155154
*/
156155
public <T extends Resource> Flux<T> search(Query query, Class<T> resourceType) {
157156
var start = System.nanoTime();
158-
logger.debug("Execute query: {}", query);
157+
logger.trace("Execute query: {}", query);
159158

160159
return client.post()
161160
.uri("/" + query.type() + "/_search")
@@ -177,7 +176,7 @@ public <T extends Resource> Flux<T> search(Query query, Class<T> resourceType) {
177176
return Mono.empty();
178177
}
179178
})
180-
.doOnComplete(() -> logger.debug("Finished query `{}` in {} seconds.", query,
179+
.doOnComplete(() -> logger.trace("Finished query `{}` in {} seconds.", query,
181180
"%.1f".formatted(TimeUtils.durationSecondsSince(start))))
182181
.doOnError(e -> logger.error("Error while executing resource query `{}`: {}", query, e.getMessage()));
183182
}

src/main/java/de/medizininformatikinitiative/torch/service/DirectResourceLoader.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ private Flux<Query> groupQueries(AnnotatedAttributeGroup group) {
8080
}
8181

8282
Flux<DomainResource> executeQueryWithBatch(PatientBatch batch, Query query) {
83+
logger.debug("Execute query {} over {} patients", query, batch.ids().size());
8384
return dataStore.search(Query.of(query.type(), batch.compartmentSearchParam(query.type()).appendParams(query.params())), DomainResource.class);
8485
}
8586

src/main/java/de/medizininformatikinitiative/torch/util/ResultFileManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public ResultFileManager(String resultsDir, String duration, FhirContext fhirCon
5252
this.fileServerName = fileServerName;
5353

5454

55-
logger.debug(" Duration of persistence{}", duration1);
55+
logger.debug("Duration of persistence {}", duration1);
5656
// Ensure the directory exists
5757
if (!Files.exists(resultsDirPath)) {
5858
try {

src/test/java/de/medizininformatikinitiative/torch/BlackBoxIntegrationTestEnv.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.slf4j.Logger;
44
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
5+
import org.springframework.web.reactive.function.client.ExchangeStrategies;
56
import org.springframework.web.reactive.function.client.WebClient;
67
import org.testcontainers.containers.ComposeContainer;
78
import org.testcontainers.containers.output.Slf4jLogConsumer;
@@ -24,7 +25,6 @@ public BlackBoxIntegrationTestEnv(Logger logger) {
2425
.withLogConsumer("torch", new Slf4jLogConsumer(logger).withPrefix("torch"))
2526
.withExposedService("nginx", 8080, Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(5)))
2627
.withLogConsumer("nginx", new Slf4jLogConsumer(logger).withPrefix("nginx"));
27-
2828
}
2929

3030
public void start() {
@@ -39,7 +39,13 @@ public TorchClient torchClient() {
3939
var host = environment.getServiceHost("torch", 8080);
4040
var port = environment.getServicePort("torch", 8080);
4141

42+
// Allow 1 MB payload size
43+
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
44+
.codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(1024 * 1024))
45+
.build();
46+
4247
return new TorchClient(WebClient.builder()
48+
.exchangeStrategies(exchangeStrategies)
4349
.baseUrl("http://%s:%s/fhir".formatted(host, port))
4450
.defaultHeader("Accept", "application/fhir+json")
4551
.build());
@@ -49,7 +55,14 @@ public FhirClient blazeClient() {
4955
var host = environment.getServiceHost("blaze", 8080);
5056
var port = environment.getServicePort("blaze", 8080);
5157

58+
// Restrict the concurrent connections to 4
59+
ConnectionProvider provider = ConnectionProvider.builder("blaze")
60+
.maxConnections(4)
61+
.build();
62+
HttpClient httpClient = HttpClient.create(provider);
63+
5264
return new FhirClient(WebClient.builder()
65+
.clientConnector(new ReactorClientHttpConnector(httpClient))
5366
.baseUrl("http://%s:%s/fhir".formatted(host, port))
5467
.defaultHeader("Accept", "application/fhir+json")
5568
.build());
@@ -59,7 +72,13 @@ public FileServerClient fileServerClient() {
5972
var host = environment.getServiceHost("nginx", 8080);
6073
var port = environment.getServicePort("nginx", 8080);
6174

75+
// Allow 100 MB payload size
76+
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
77+
.codecs(codecs -> codecs.defaultCodecs().maxInMemorySize(100 * 1024 * 1024))
78+
.build();
79+
6280
return new FileServerClient(WebClient.builder()
81+
.exchangeStrategies(exchangeStrategies)
6382
.baseUrl("http://%s:%s".formatted(host, port))
6483
.build());
6584
}

src/test/java/de/medizininformatikinitiative/torch/CdsExecutionIT.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import de.medizininformatikinitiative.torch.assertions.BundleAssert;
44
import org.hl7.fhir.r4.model.ResourceType;
5-
import org.junit.jupiter.api.*;
5+
import org.junit.jupiter.api.AfterAll;
6+
import org.junit.jupiter.api.BeforeAll;
7+
import org.junit.jupiter.api.Test;
68
import org.slf4j.Logger;
79
import org.slf4j.LoggerFactory;
810
import org.testcontainers.junit.jupiter.Testcontainers;
@@ -38,7 +40,8 @@ static void setUp() throws IOException {
3840
var blazeClient = environment.blazeClient();
3941

4042
logger.info("Uploading test data...");
41-
blazeClient.transact(Files.readString(Path.of("target/kds-testdata-2024.0.1/resources/Bundle-mii-exa-test-data-bundle.json")));
43+
var bundle = Files.readString(Path.of("target/kds-testdata-2024.0.1/resources/Bundle-mii-exa-test-data-bundle.json"));
44+
blazeClient.transact(bundle).block();
4245
}
4346

4447
@AfterAll
@@ -48,9 +51,12 @@ static void tearDown() {
4851

4952
@Test
5053
public void testExamples() throws IOException {
51-
var statusUrl = torchClient.executeExtractData(TestUtils.loadCrtdl("CRTDL_test_it-kds-crtdl.json"));
54+
var statusUrl = torchClient.executeExtractData(TestUtils.loadCrtdl("CRTDL_test_it-kds-crtdl.json")).block();
55+
assertThat(statusUrl).isNotNull();
56+
57+
var statusResponse = torchClient.pollStatus(statusUrl).block();
58+
assertThat(statusResponse).isNotNull();
5259

53-
var statusResponse = torchClient.pollStatus(statusUrl.replace("8080", String.valueOf(environment.getTorchPort())));
5460
var coreBundles = statusResponse.coreBundleUrl().stream().flatMap(fileServerClient::fetchBundles).toList();
5561
var patientBundles = statusResponse.patientBundleUrls().stream().flatMap(fileServerClient::fetchBundles).toList();
5662

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package de.medizininformatikinitiative.torch;
2+
3+
import de.medizininformatikinitiative.torch.assertions.BundleAssert;
4+
import org.junit.jupiter.api.AfterAll;
5+
import org.junit.jupiter.api.BeforeAll;
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.Disabled;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.testcontainers.junit.jupiter.Testcontainers;
11+
12+
import java.io.IOException;
13+
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
16+
/**
17+
* In order for the tests to work locally, a torch image must be built:
18+
* => mvn clean package -DskipTests && docker build -t torch:latest . && mvn -P blackbox-integration-tests -B verify
19+
*/
20+
@Testcontainers
21+
public class CdsPerformanceExecutionIT {
22+
23+
private static final Logger logger = LoggerFactory.getLogger(CdsPerformanceExecutionIT.class);
24+
25+
private static PerformanceIntegrationTestEnv environment;
26+
private static TorchClient torchClient;
27+
private static FileServerClient fileServerClient;
28+
29+
@BeforeAll
30+
static void setUp() {
31+
environment = new PerformanceIntegrationTestEnv(logger);
32+
environment.start();
33+
34+
torchClient = environment.torchClient();
35+
fileServerClient = environment.fileServerClient();
36+
}
37+
38+
@AfterAll
39+
static void tearDown() {
40+
environment.stop();
41+
}
42+
43+
@Test
44+
public void testWithoutReferences() throws IOException {
45+
var statusUrl = torchClient.executeExtractData(TestUtils.loadCrtdl("CRTDL_test_it-kds-perf-wo-ref.json")).block();
46+
assertThat(statusUrl).isNotNull();
47+
48+
var statusResponse = torchClient.pollStatus(statusUrl).block();
49+
assertThat(statusResponse).isNotNull();
50+
51+
var coreBundles = statusResponse.coreBundleUrl().stream().flatMap(fileServerClient::fetchBundles).toList();
52+
var patientBundles = statusResponse.patientBundleUrls().stream().flatMap(fileServerClient::fetchBundles).toList();
53+
54+
assertThat(coreBundles, BundleAssert.class).singleElement().containsNEntries(0);
55+
assertThat(patientBundles).hasSize(25000);
56+
}
57+
58+
@Test
59+
public void testWithReferences() throws IOException {
60+
var statusUrl = torchClient.executeExtractData(TestUtils.loadCrtdl("CRTDL_test_it-kds-perf-w-ref.json")).block();
61+
assertThat(statusUrl).isNotNull();
62+
63+
var statusResponse = torchClient.pollStatus(statusUrl).block();
64+
assertThat(statusResponse).isNotNull();
65+
66+
var coreBundles = statusResponse.coreBundleUrl().stream().flatMap(fileServerClient::fetchBundles).toList();
67+
var patientBundles = statusResponse.patientBundleUrls().stream().flatMap(fileServerClient::fetchBundles).toList();
68+
69+
assertThat(coreBundles, BundleAssert.class).singleElement().containsNEntries(25000);
70+
assertThat(patientBundles).hasSize(25000);
71+
}
72+
}

0 commit comments

Comments
 (0)