Combine two futures as main and backup

Viewed 52

I have two processes which should start in parallel and if the first process fails, I should use second process result. I came up with the following code. Is there any sensible way to simplify it?

final CompletableFuture<Data> mainFut = mainProcess(...);
final CompletableFuture<Data> backupFut = backupProcess(...);
final CompletableFuture<Data> resultFut = new CompletableFuture<>();
mainFut.handle((r, t) -> {
    if (t == null) {
        resultFut.complete(r); // main process is fine, use its result
    } else {
        LOGGER.error("Main process failed, using backup", t);
        backupFut.handle((rb, tb) -> {
            if (tb == null) {
                resultFut.complete(rb); //backup is fine, use its result
            } else {
                resultFut.completeExceptionally(tb); //backup failed too
            }
            return null;
        });
    }
    return null;
});
return resultFut;
3 Answers

You are not using the result of handle, which would be perfect for your use case, e.g.

final CompletableFuture<Data> mainFut = mainProcess(...);
final CompletableFuture<Data> backupFut = backupProcess(...);

return mainFut.handle((r,t) -> t == null? r: backupFut.join());

If you want to avoid the potentially blocking join() call, you can use

final CompletableFuture<Data> mainFut = mainProcess(...);
final CompletableFuture<Data> backupFut = backupProcess(...);

return mainFut.handle((r1, t1) -> t1 == null? mainFut: backupFut)
    .thenCompose(Function.identity());

The backup operation’s result will supersede the main operation’s exception, but when both fail, you can include the original exception in the exceptional result like

return mainFut.handle((r1, t1) -> t1 == null? mainFut:
        backupFut.whenComplete((r2, t2) -> { if(t2 != null) t2.addSuppressed(t1); }))
    .thenCompose(Function.identity());

Using CompletableFuture#exceptionallyCompose, this becomes basically a one liner:

var resultFut = mainFut.exceptionallyCompose(throwable -> backupFut);

Edit: exceptionallyCompose is available since Java 12.

Not sure if this code would match your coding style but would something like this work to reduce indentation levels and try and avoid nesting hell?

mainFut.handle((r, t) -> {
    if (t == null) {
        resultFut.complete(r); // main process is fine, use its result
        return null;
    }

    LOGGER.error("Main process failed, using backup", t);
    backupFut.handle((rb, tb) -> {
        if (tb == null) {
            resultFut.complete(rb); //backup is fine, use its result
            return null;
        }
        
        resultFut.completeExceptionally(tb); //backup failed too
        return null;
    });
    return null;
});

Also just a thought I'm not 100% familiar with lambda functions do you need to return values in those lambda functions? You could remove the last two return statements if not.

Related