I have the following situation where I'm trying to see if there is a solution for:
- Two Spring service calls must be made in parallel (one is an existing service call/logic and the second is the new addition).
- The results should then be merged and returned by the RESTful API.
A happy path should be straightforward, however, when it comes to errors emitted by the services the following rule should adhere to:
The API fails only when both service calls fail -- this should be thrown from the main thread and not the
@Async
pool since they are independent threads and don't have access to each other's exception (at least that's my reasoning).If only one of them fails, log the error through another service (asynchronously), and the API returns only the results from a service that was successful -- this can be done from the respective
@Async
threads.@Service public class Serv1 interface ServInf { @Async("customPool") public CompletableFuture<List<Obj>> getSomething(int id) { // The service ensures that the list is never null, but it can be empty return CompletableFuture.completedFuture(/* calling an external RESTful API */); } } @Service public class Serv2 interface ServInf { @Async("customPool") public CompletableFuture<List<Obj>> getSomething(int id) { // The service ensures that the list is never null, but it can be empty return CompletableFuture.completedFuture(/* calling another external RESTful API */); } } @RestController public class MyController { /** Typical service @Autowired's */ @GetMapping(/* ... */) public WrapperObj getById(String id) { CompletableFuture<List<String>> service1Result = service1.getSomething(id) .thenApply(result -> { if (result == null) { return null; } return result.stream().map(Obj::getName).collect(Collectors.toList()); }) .handle((result, exception) -> { if (exception != null) { // Call another asynchronous logging service which should be easy return null; } else { return result; } }); CompletableFuture<List<String>> service2Result = service2.getSomething(id) .thenApply(result -> { if (result == null) { return null; } return result.stream().map(Obj::getName).collect(Collectors.toList()); }) .handle((result, exception) -> { if (exception != null) { // Call another asynchronous logging service which should be easy return null; } else { return result; } }); // Blocking till we get the results from both services List<String> result1 = service1Result.get(); List<String> result2 = service2Result.get(); /** Where to get the exceptions thrown by the services if both fail if (result1 == null && result2 == null) { /** Signal that the API needs to fail as a whole */ throw new CustomException( /** where to get the messages? */); } /** merge and return the result */ } }
My question is, Since these services return a list of some object, even if I use CompletableFuture.handle()
and check for existence of an exception, I can't return the Exception itself in order to capture and let Spring Advice class handle it (chained to return a list).
One thing I thought of is to use AtomicReference
in order to capture the exceptions and set them within the handle()
and use them once the futures are done/complete, e.g.
AtomicReference<Throwable> ce1 = new AtomicReference<>();
AtomicReference<Throwable> ce2 = new AtomicReference<>();
.handle((result, exception) -> {
if (exception != null) {
ce1.set(exception);
return null; // This signals that there was a failure
} else {
return result;
}
});
List<String> result1 = service1Result.get();
List<String> result2 = service2Result.get();
/** Where to get the exceptions thrown by the services if both fail
if (result1 == null && result2 == null) {
/** Signal that the API needs to fail as a whole */
throw new CustomException(/** do logic to capture ce1.get().getMessage() + ce2.get().getMessage() */);
}
First, does this sound like a viable solution in this multi-threaded asynchronous calls?
Second, this looks messy, so I was wondering if there is a more elegant way of capturing these exceptions outside of Spring async pool, and deal with it in the main thread, e.g. combine the exception information and throw it to Spring Advice exception handler.