Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

is using @Async and CompletableFuture in controller can increase performance of our api?

What I am trying to achieve is can I get a better performance by using @Async and CompletableFuture as result in my controller of my RESTApi by using the multi threading in this simple way?

here is what I do, here is my controller :

@PostMapping("/store")
@Async
public CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> saveNewCategoryBPSJ(@Valid @RequestBody InputRequest<CategoryBPSJRequestDto> request) {
    
    CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> future = new CompletableFuture<>();

    future.complete(ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJService.save(request))));
    return future;
}

VS

@PostMapping("/store")
public ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>> saveNewCategoryBPSJ(@Valid @RequestBody InputRequest<CategoryBPSJRequestDto> request) {
    
    return ResponseEntity.ok(new ResponseRequest<>("okay", categoryBPSJService.save(request));
}

as you can see in my first controller function, I add the CompletableFuture on my function response, but in my service, which is I do save in this line categoryBPSJService.save(request) is not async, just a simple function that looked like this :

public CategoryBpsjResponseDto save(InputRequest<CategoryBPSJRequestDto> request) {
    CategoryBPSJRequestDto categoryBPSJDto = request.getObject();

    Boolean result = categoryBPSJRepository.existsCategoryBPSJBycategoryBPSJName(categoryBPSJDto.getCategoryBPSJName());

    if(result){
        throw new ResourceAlreadyExistException("Category BPSJ "+ categoryBPSJDto.getCategoryBPSJName() + " already exists!");
    }

    CategoryBPSJ categoryBPSJ = new CategoryBPSJ();
    categoryBPSJ = map.DTOEntity(categoryBPSJDto);

    categoryBPSJ.setId(0L);
    categoryBPSJ.setIsDeleted(false);

    CategoryBPSJ newCategoryBPSJ = categoryBPSJRepository.save(categoryBPSJ);
    
    CategoryBpsjResponseDto categoryBpsjResponseDto = map.entityToDto(newCategoryBPSJ);

    return categoryBpsjResponseDto;

}

I just return simple Object with JPA connection, with this is way will my request performance increased? or am I missing something to increase it? or it make no difference with or without CompletableFuture and @Async on my controller?

*note : my project is based on java 13

like image 205
Ke Vin Avatar asked Dec 03 '20 05:12

Ke Vin


1 Answers

Using CompletableFuture won't magically improve the performances of your server.

If you're using Spring MVC, built on the Servlet API usually on top of Jetty or Tomcat, you'll have one thread per request. The pool those threads are taken from is usually pretty big, so that you can have a decent amount of concurrent requests. Here, blocking a request thread isn't that of an issue, as this thread is handling that single request only anyway, meaning other requests won't be blocked (unless there's no thread available in the pool anymore). This means, your IOs can be blocking, your code can be synchronous.

If you're using Spring WebFlux though, usually on top of Netty, requests are handled as messages/events: one thread can handle multiple requests, which allows reducing the size of the pool (threads are expensive). In this case, blocking on a thread is an issue, as it can/will lead to other requests waiting for the IO to finish. This means, your IOs must be non-blocking, your code must be asynchronous, so that the thread can be released and handle another request "in the meantime" instead of just waiting idle for the operation to finish. Just FYI, this reactive stack looks appealing, but it comes with many other drawbacks to be aware of, because of the asynchronous nature of the codebase.

JPA is blocking, because it relies on JDBC (which blocks on the IOs). This means, using JPA with Spring WebFlux doesn't make much sense and should be avoided, as it goes against the principle of "do not block a request thread". People have found workarounds (e.g. running the SQL queries from within another thread pool), but this doesn't really solve the underlying issue: the IOs will block, contention can/will occur. People are working on asynchronous SQL drivers for Java (e.g. Spring Data R2DBC and the underlying vendor-specific drivers), which can be used from within a WebFlux codebase for instance. Oracle started working on their own asynchronous driver too, ADBA, but they abandoned the project because of their focus on fibers via Project Loom (which might soon totally change the way concurrency is handled in Java).

You seem to be using Spring MVC, meaning relying on the thread-per-request model. Just dropping CompletableFuture in your code won't improve things. Say you delegate all your service layer logic onto another thread pool than the default request thread pool: your request threads will be available, yes, but contention will now happen on that other thread pool of yours, meaning you'll just be moving your problem around.

Some cases might still be interesting to postpone to another pool, e.g. computationally intensive operations (like passphrase hashing), or certain actions that would trigger a lot of (blocking) IOs, etc., but be aware that contention can still happen, meaning requests can still be blocked/waiting.

If you do observe performance issues with your codebase, profile it first. Use tools like YourKit (many others available) or even APMs like NewRelic (many others available as well). Understand where the bottlenecks are, fix the worsts, repeat. That being said, some usual suspects: too many IOs (especially with JPA, e.g. select n+1), too many serialisations/deserialisations (especially with JPA, e.g. eager fetching). Basically, JPA is the usual suspect: it's a powerful tool, but very easy to misconfigure, you need to think SQL to get it right IMHO. I highly recommend logging the generated SQL queries when developing, you might be surprised. Vlad Mihalcea's blog is a good resource for JPA related things. Interesting read as well: OrmHate by Martin Fowler.


Concerning your specific code snippet, say you're going vanilla Java without Spring's @Async support:

CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> future = new CompletableFuture<>();
future.complete(ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJService.save(request))));
return future;

This won't make categoryBPSJService.save(request) run asynchronously. It will be made more obvious if you split your code a bit:

CategoryBpsjResponseDto categoryBPSJ = categoryBPSJService.save(request)
CompletableFuture<ResponseEntity<ResponseRequest<CategoryBpsjResponseDto>>> future = new CompletableFuture<>();
future.complete(ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJ)));
return future;

See what happened here? categoryBPSJ will be called synchronously, and you'll then create an already-completed future holding the result. If you really wanted to use a CompletableFuture here, you'd have to use a supplier:

CompletableFuture<CategoryBpsjResponseDto> future = CompletableFuture.supplyAsync(
    () -> categoryBPSJService.save(request),
    someExecutor
);
return future.thenApply(categoryBPSJ -> ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJ)));

Spring's @Async is basically just syntax sugar for the above, use either/or. For technical AOP/proxying reasons, a method annotated with @Async does need to return a CompletableFuture indeed, in which case returning an already-completed future is fine: Spring will make it run in an executor anyway. The service layer is usually the one being "async" though, the controller just consumes and composes over the returned future:

CompletableFuture<CategoryBpsjResponseDto> = categoryBPSJService.save(request);
return future.thenApply(categoryBPSJ -> ResponseEntity.ok(new ResponseRequest<>("Okay", categoryBPSJ)));

Make sure it all behaves as you expect by debugging your code, IDEs show which thread is currently being blocked by a breakpoint.


Side note: that's a simplified summary of what I understood from blocking vs non-blocking, MVC vs WebFlux, sync vs async, etc. It's quite superficial, some of my points might not be specific enough to be 100% true.

like image 184
sp00m Avatar answered Oct 22 '22 10:10

sp00m