kan
kan

Reputation: 28951

Combine two futures as main and backup

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;

Upvotes: 2

Views: 135

Answers (3)

michid
michid

Reputation: 10814

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

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

Edit: exceptionallyCompose is available since Java 12.

Upvotes: 1

Holger
Holger

Reputation: 298153

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());

Upvotes: 2

Joss Bird
Joss Bird

Reputation: 311

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.

Upvotes: 0

Related Questions