Search code examples
javaspringspring-bootvirtual-threads

Parallel service calls with Spring Boot 3.2 and virtual threads


I am trying to call two external services in parallel using virtual threads on spring boot 3.2 with RestClient, but I'm not sure if it is correct to simply call them sequentially or use futures. My application.properties already contains: spring.threads.virtual.enabled=true

Is it correct to simply do:

String response1 = performGetRequest(uri1);  //RestClient get request
String response2 = performGetRequest(uri2);

return response1 + response2;

Or do I need to do:

Future<String> response1 = performGetRequest(uri1);
Future<String> response2 = performGetRequest(uri2);

return response1.get() + response2.get();

Also, is it necessary to wrap the block of code in a try-with-resources block, or is there no need because spring is already using virtual thread per task as it is enabled in application.properties? As so:

try (ExecutorService service = Executors.newVirtualThreadPerTaskExecutor();)

Solution

  • If you can use virtual threads with your applications, prefer using the latter approach i.e. invoke both the REST API calls using newVirtualThreadPerTaskExecutor. A similar use case for fanout scenarios is even suggested in the documentation of virtual threads under the section #Represent Every Concurrent Task as a Virtual Thread; Never Pool Virtual Threads.

    First Approach

    Spring by itself would make use of the virtual threads to take care of the requests initiated to the server. This would mostly mean that every request would be assigned to a new virtual thread handled by Tomcat and these threads depending on the I/O performed within each request can switch to accepting further requests considering higher throughput and concurrency.

    But once a thread is assigned the request (consider tracing a request-id), it will carry out all subsequent synchronous steps until it responds within the same thread. If logs could help you relate, this would result in an output like the following:

    GreetingsController       : 34ace5cc-2b6b-4226-a088-b5888fc30f99 : VirtualThread[#568,tomcat-handler-51]/runnable@ForkJoinPool-1-worker-10
    WebPageService            : 34ace5cc-2b6b-4226-a088-b5888fc30f99 : tomcat-handler-51 : /uri1
    WebPageService            : 34ace5cc-2b6b-4226-a088-b5888fc30f99 : tomcat-handler-51 : /uri2
    

    Second Approach

    With the approach of using newVirtualThreadPerTaskExecutor, you are assigning all the service calls to a new virtual thread as a task concurrently. In this case, the application further spawns new virtual threads from the common ForkJoinPool for each of your service calls submitted. Here are the logs for reference for this case:

    GreetingsController       : ad39b86e-073c-479e-a5d8-ec35bbb34dac : VirtualThread[#575,tomcat-handler-54]/runnable@ForkJoinPool-1-worker-12
    WebPageService            : ad39b86e-073c-479e-a5d8-ec35bbb34dac : VirtualThread[#576]/runnable@ForkJoinPool-1-worker-12: /uri1
    WebPageService            : ad39b86e-073c-479e-a5d8-ec35bbb34dac : VirtualThread[#580]/runnable@ForkJoinPool-1-worker-12 : /uri2
    
    

    Side Note - The thread name in the above logs is added with the help of a debugger.


    Additionally, for your question about the usage of try-with-resources, quoting the referenced link summarising the usefulness of the newly introduced ExecutorService.close:

    The close method, that is implicitly called at the end of the try block will automatically wait for all tasks submitted to the ExecutorService—that is, all virtual threads spawned by the ExecutorService—to terminate.

    This could certainly help release the resources for the other threads to make use of and reduce any possible leaks in the application.