Quantcast
Channel: All – akquinet – Blog
Viewing all articles
Browse latest Browse all 134

StreamResults with OpenApi and SpringBoot

$
0
0

In a customer project, documentation forms (data record for a dialysis treatment) from dialysis centers are read in and subjected to a quality analysis. The quarterly billing to the centers is based on the number of processed documentation forms per quarter and center.

An extra interface was implemented for billing. In the first draft, the data was queried internally, summarized and returned as a finished CSV file. Controlling wanted a CSV file so that it could be used internally via Excel for further evaluations.

For a second implementation, it was to be investigated whether an implementation on a streaming basis was feasible with the existing architecture and whether this would bring advantages for the user.

Used frameworks and architecture

The application runs with Spring Boot version 3 and an Elasticsearch installation as the backend. The account information is queried by the application via a REST interface. This interface was formulated in OpenApi 3.0.1. The server stubs are generated from the interface description using maven and the OpenApi generator for Spring (https://openapi-generator.tech/docs/generators/spring/).

Procedure

Initial situation

The original definition of the interface in openapi.yml was as follows:

/v2/dokumentationsboegen:
  get:
    tags:
      - accounting-controller
    summary: liefert alle verarbeiteten Dokumentationsbögen
    description: liefert alle verfügbaren Quartale (für die übergebenen BSNRs und Quartale)
    operationId: dokumentationsboegen
    parameters:
      - name: quarter
        in: query
        description: Welche Quartale
        required: false
        schema:
          type: array
          items:
            type: string
      - name: bsnr
        in: query
        description: Welche BSNRs
        required: false
        schema:
          type: array
          items:
            type: string
    responses:
      '200':
        description: Dokubögen im System
        content:
          text/csv:
            schema:
              type: string
      '422':
        description: keine gültigen Quartale oder BSNRs
        content:
          application/json:
            schema:
              type: string

The creation of the stubs was defined in pom.xml:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.1.0</version>
    <executions>
        <execution>
            <id>generate interface</id>
            <phase>process-resources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>
                    ${project.build.directory}/classes/openapi/openapi.yml</inputSpec>
                <generatorName>spring</generatorName>
                <configOptions>
                    <modelPackage>de.akquinet.tas.qmd.rest.model</modelPackage>
                    <apiPackage>de.akquinet.tas.qmd.rest.api</apiPackage>
                    <interfaceOnly>true</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

The implementation in the controller retrieved the necessary data from ElasticSearch, generated the result as a string using OpenCSV and wrote it into the response.

List accountingBeans = getAccountingBeansFromElasticSearch(...);
String content = convertToCSVString(accountingBeans);
return ResponseEntity.ok()
          .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=accounting.csv")
          .body(content);

Final result

This should now be changed to an interface based on streaming. SpringBoot provides the StreamingResponseBody interface for this purpose. However, adapting the OpenApi description to this turned out to be somewhat difficult. After a few failures, controlling the generation of the stubs via type and import mapping proved to be the right way.

Only the format of the response was added to the interface description. The specification “streaming” contains no(!) semantics, but is merely a “marker”.

responses:
  '200':
    description: Dokubögen im System
    content:
      text/csv:
        schema:
          type: string
          format: streaming

and in the generation of the stubs, this format was handled specifically:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.1.0</version>
    <executions>
 
        <execution>
            <id>generate interface</id>
            <phase>process-resources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>
                    ${project.build.directory}/classes/openapi/openapi.yml</inputSpec>
                <generatorName>spring</generatorName>
                <typeMappings>
                    <typeMapping>
                        string+streaming=StreamingResponseBody
                    </typeMapping>
                </typeMappings>
                <importMappings>
                    <importMapping>
                        StreamingResponseBody=org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
                    </importMapping>
                </importMappings>
                <configOptions>
                    <modelPackage>de.akquinet.tas.qmd.rest.model</modelPackage>
                    <apiPackage>de.akquinet.tas.qmd.rest.api</apiPackage>
                    <interfaceOnly>true</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

Then, of course, the implementation had to be adapted:

List accountingBeans = getAccountingBeansFromElasticSearch(...);
StreamingResponseBody stream = out -> writeBeansToOutStream(accountingCsvBeans, out);
 
return ResponseEntity.ok()
    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=accounting.csv")
    .body(stream);

Benchmarks

Now, of course, it was interesting to examine both variants in terms of their runtime. To do this, the application was started as a Docker container, test data was generated and imported for 5 operating sites with 5000 sheets each and then the account data was retrieved. First with the old interfaces and then with the new ones. As controlling is only carried out once a quarter and by a small number of people, the load test is carried out with a maximum of 4 users simultaneously.

The benchmark was implemented with Gatling https://gatling.io/). It comprises 4 scenarios with 1, 2 and 4 users who pulled the accounting data in parallel. The CSV data was retrieved for five operating sites and two quarters. The CSV file has a size of 7.1 MB. The benchmark ran completely on a laptop with 32GB main memory and an Intel i9 CPU with 2.60GHz.

In the first variant without streaming, Gatling delivered the following table. Note: If you are not yet familiar with Gatling, you can find a good description of the table in the “reports” section at https://gatling.io/2022/01/my-first-gatling-test-report/.

ScenarioTotalOKKO%KOCnt/sMin50 pct75 pct95 pct99 pctMaxMeanStdDev
1 User1100%0.007256702567025670256702567025670256700
2 Users2200%0.01528453285802864428694287042870728580127
3 Users3300%0.02232581330203317233293333173332332975305
4 Users4400%0.02939690402444043340487404974050040170319

The variant with streaming resulted in the following table:

ScenarioTotalOKKO%KOCnt/sMin50 pct75 pct95 pct99 pctMaxMeanStdDev
1 User1100%0.007250942509425094250942509425094250940
2 Users2200%0.014287892879128791287922879228792287912
3 Users3300%0.02132847331243314433159331623316333045141
4 Users4400%0.028420154456044610446614467144674439521119

If you now compare the mean values without streaming and the mean values with streaming, there are hardly any differences.

ScenarioMean without StreamingMean with Streaming
1 Users2567025094
2 Users2858028791
3 Users3297533045
4 Users4017043952

Conclusions

It is therefore possible to specify a StreamingResponse for the REST interface using openapi’s own means. In our case, this did not result in any performance advantages. In the interest of faster feedback to the user, it was then decided to retain streaming.

For an architecturally complete solution based entirely on streaming, the call to getAccountingBeansFromElasticSearch() would now have to be converted to streaming. However, this is not the focus of this article.


Viewing all articles
Browse latest Browse all 134

Trending Articles