Wrapper
Wrapper

Reputation: 932

Handling (un)checked exceptions when supplyAsync is called within another supplyAsync?

As title suggest, I have some weird requirement and that is how to handle exceptions in a above mentioned case.

So I basically need to do nested validation. Some of those validations are independent from others some are not. If (un)checked exception is thrown, it should be propagated to the top most caller. By propagated and top most caller, I mean to the controller layer, so ControllerAdvice can kick in.

Here is some code:

  1. We have a basic controller, which is our "top" level caller of asynchronous functions.

    @RestController
    @RequestMapping("/test")
    @AllArgsConstructor
    public class Controller {
    
            private final Helper helper;
    
    
            public String test(String code, String system) throws ExecutionException, InterruptedException {
    
                List<CompletableFuture<Boolean>> futureValidations =
                        List.of(helper.codeableConceptValidator(code, system));
    
                CompletableFuture<Void> allFutureValidations = CompletableFuture.allOf(
                        futureValidations.toArray(new CompletableFuture[futureValidations.size()]));
    
                CompletableFuture<List<Boolean>> validationResult = allFutureValidations.thenApply(v -> {
                    return futureValidations.stream()
                            .map(CompletableFuture::join) //futureValidation -> futureValidation.join()
                            .collect(Collectors.toList());
                });
    
                CompletableFuture<Long> countFuture = validationResult.thenApply(allValidations -> {
                    return allValidations.stream()
                            .filter(Boolean::valueOf)
                            .count();
                });
    
                if (countFuture.get() == futureValidations.size()){
    
                    System.out.println("ok");
                    return "ok";
                }
    
                return "not ok";
            }
        }
    

Part countFuture.get() is throwing unchecked exceptions and they can be processed with ControllerAdvice, so far so good.

Now we have helper.codeableConceptValidator(code, system) which also return CompletedFuture<Boolean> using supplyAsync, but when we do get() when need to handle somehow InterruptedException, ExecutionException and custom checked exception from another library. This last exception is very important since that library also uses RestControllerAdvice with ExceptionHandler to return custom POJO to the caller. I need that exception to be thrown from my controller.

@Component
@AllArgsConstructor
public class Helper {

    private final Executor executor;
    private final StringValidator stringValidator;
    private final TerminologyService terminologyService;

    public CompletableFuture<Boolean> codeableConceptValidator(String code, String system) {
        return CompletableFuture.supplyAsync(() -> {

            List<CompletableFuture<Boolean>> futureValidations =
                    List.of(stringValidator.codeValidator(code),
                            stringValidator.systemValidator(system)
                    );

            CompletableFuture<Void> allFutureValidations = CompletableFuture.allOf(
                    futureValidations.toArray(new CompletableFuture[futureValidations.size()]));

            CompletableFuture<List<Boolean>> validationResult = allFutureValidations.thenApply(v -> {
                return futureValidations.stream()
                        .map(CompletableFuture::join) //futureValidation -> futureValidation.join()
                        .collect(Collectors.toList());
            });

            CompletableFuture<Long> countFuture = validationResult.thenApply(allValidations -> {
                return allValidations.stream()
                        .filter(Boolean::valueOf)
                        .count();
            });


            try {
                if (countFuture.get() == futureValidations.size()){

                try{
                    terminologyService.validateCode(system, code);
                    //com.base.package.otherproject.exceptions.CustomException;
                }catch (ByCtsApiException e) {
                    e.printStackTrace();
                }

                return false;
            
            } catch (InterruptedException e) {
                e.printStackTrace();

            } catch (ExecutionException e) {
                e.printStackTrace();
            }


            return false;
        }, executor);
    }
}

And finally here is a simple StringValidator.

@Component
@AllArgsConstructor
public class StringValidator {

    private final Executor executor;

    public CompletableFuture<Boolean> codeValidator(String code){
        return CompletableFuture.supplyAsync(code::isEmpty, executor);
    }

    public CompletableFuture<Boolean> systemValidator(String system){
        return CompletableFuture.supplyAsync(system::isEmpty, executor);
    }


}

Upvotes: 0

Views: 297

Answers (1)

Holger
Holger

Reputation: 298233

Instead of get, you can invoke join which doesn’t require dealing with checked exceptions. But you should rethink the design of calling blocking methods within a function passed to supplyAsync. The whole point of this API is that you can chain dependent actions to construct a new CompletableFuture (or CompletionStage when you’re operating on the interface only), without the need to ever wait for a completion.

When you want to combine the result of two futures, you can use thenCombine, e.g. to get a future that is only true when both futures evaluated to true, you can use

CompletableFuture<Boolean> a = stringValidator.codeValidator(code);
CompletableFuture<Boolean> b = stringValidator.systemValidator(system);

CompletableFuture<Boolean> combined = a.thenCombine(b, Boolean::logicalAnd);

or short

CompletableFuture<Boolean> combined = stringValidator.codeValidator(code)
    .thenCombine(stringValidator.systemValidator(system), Boolean::logicalAnd);

When you want to process a dynamic number of futures, given as list, you can use

List<CompletableFuture<Boolean>> futureValidations =
   List.of(stringValidator.codeValidator(code), stringValidator.systemValidator(system));

CompletableFuture<Boolean> combined = futureValidations.stream()
    .reduce((a, b) -> a.thenCombine(b, Boolean::logicalAnd))
    .orElseGet(() -> CompletableFuture.completedFuture(true));

This works reasonably, as long as the list is not too large. For larger lists, say significantly over twenty futures, the code may suffer from the fact that typical sequential reduction operations do not produce a balanced tree of dependent futures.

In that case, you can resort to allOf:

CompletableFuture<Boolean> combined =
    CompletableFuture.allOf(futureValidations.toArray(new CompletableFuture<?>[0]))
        .thenApply(x -> futureValidations.stream().allMatch(CompletableFuture::join));

In this code, join can be invoked safely as the subsequent operation will only be executed when all futures are completed. In fact, we can even assume that join never throws an exception here, as the future returned by allOf will be completed exceptionally if any of the input futures is completed exceptionally and so will be the subsequent thenApply stage without ever evaluating the function. Therefore, we can use the short-circuiting allMatch; exceptions have been handled preemptively so when we enter the code, no exception occurred and the result is false as soon as we encounter the first false.

Upvotes: 1

Related Questions