简体   繁体   中英

Interrupt CompletableFuture with default value

Suppose I have 3 services. First I call serviceA , it returns a CompletableFuture . After that with the result I call serviceB and serviceC paralelly ( thenCompose() ). After I have all result I would like to combine all 3 results and return it to some caller. In the caller I would like to wait overall X millseconds to the whole process so that:

  • If I interrupt the process while serviceA call is in progress: throw some exception (so it is mandatory)
  • If I interrupt the process while serviceB and serviceC calls are in progress: return some default value (they are optional). This is the reason I try to use the getNow(fallback) method of the CompletableFuture

Please check below my code snippets, if I use long delays in serviceB and serviceC calls, I always ends up with a TimeoutException . How can I do this?

public CompletableFuture<Result> getFuture() {
    CompletableFuture<A> resultA = serviceA.call();
    CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
    CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
    return CompletableFuture.allOf(resultB, resultC)
            .thenApply(ignoredVoid -> combine(
                    resultA.join(),
                    resultB.getNow(fallbackB),
                    resultC.getNow(fallbackC));
}

public Result extractFuture(CompletableFuture<Result> future) {
    Result result;
    try {
        result = future.get(timeOut, MILLISECONDS);
    } catch (ExecutionException ex) {
        ...
    } catch (InterruptedException | TimeoutException ex) {
        // I always ends up here...
    }
    return result;
}

The future returned by .allOf(resultB, resultC) is only completed when both, resultB and resultC are completed, therefore, the dependent function ignoredVoid -> combine(resultA.join(), resultB.getNow(fallbackB), resultC.getNow(fallbackC) will only get evaluated if resultB and resultC are completed and providing a fallback has no effect at all.

It is generally impossible to react on a get() call within these function. Which should be obvious considering that there can be an arbitrary number of get() calls on the future at different times with different timeouts, but the function passed to thenApply is only evaluated once.

The only way to handle a consumer specified timeout within getFuture() is to change it to return a function which receives the timeout:

interface FutureFunc<R> {
    R get(long time, TimeUnit u) throws ExecutionException;
}
public FutureFunc<Result> getFuture() {
    CompletableFuture<A> resultA = serviceA.call();
    CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
    CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
    CompletableFuture<Result> optimistic = CompletableFuture.allOf(resultB, resultC)
        .thenApply(ignoredVoid -> combine(resultA.join(), resultB.join(), resultC.join()));
    return (t,u) -> {
        try {
            return optimistic.get(t, u);
        } catch (InterruptedException | TimeoutException ex) {
            return combine(resultA.join(), resultB.getNow(fallbackB),
                                           resultC.getNow(fallbackC));
        }
    };
}

public Result extractFuture(FutureFunc<Result> future) {
    Result result;
    try {
        result = future.get(timeOut, MILLISECONDS);
    } catch (ExecutionException ex) {
        ...
    }
    return result;
}

Now, different calls with different timeouts can be made, with possibly different outcome as long as B or C have not completed yet. Not that there is some ambiguity regarding the combine method which may also take some time.

You could change the function to a

return (t,u) -> {
    try {
        if(resultB.isDone() && resultC.isDone()) return optimistic.get();
        return optimistic.get(t, u);
    } catch (InterruptedException | TimeoutException ex) {
        return combine(resultA.join(), resultB.getNow(fallbackB),
                                       resultC.getNow(fallbackC));
    }
};

to wait for the completion of a possibly already running combine . In either case, there is no guaranty that the result is delivered within the specified time, as even if the fallback values for B and C are used, there will be an execution of combine that may take an arbitrary amount of time.

If you want cancellation like behavior, ie that all result queries return the same result, even if it has been calculated using the fallback values due to the first query, you can use instead

public FutureFunc<Result> getFuture() {
    CompletableFuture<A> resultA = serviceA.call();
    CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
    CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
    CompletableFuture<Void> bAndC = CompletableFuture.allOf(resultB, resultC);
    CompletableFuture<Result> result = bAndC
        .thenApply(ignoredVoid -> combine(resultA.join(), resultB.join(),
                                                          resultC.join()));
    return (t,u) -> {
        try {
            bAndC.get(t, u);
        } catch (InterruptedException|TimeoutException ex) {
            resultB.complete(fallbackB);
            resultC.complete(fallbackC);
        }
        try {
            return result.get();
        } catch (InterruptedException ex) {
            throw new ExecutionException(ex);
        }
    };
}

With this, all queries on a single FutureFunc will consistently return the same result, even if it is based on fallback values due to the first timeout. This variant also consistently excludes the execution of combine from the timeout.

Of course, if different timeouts are not intended at all, you could refactor getFuture() to get the desired timeout in advance, eg as parameter. That would simplify the implementation significantly and it could return a future again:

public CompletableFuture<Result> getFuture(long timeOut, TimeUnit u) {
    CompletableFuture<A> resultA = serviceA.call();
    CompletableFuture<B> resultB = resultA.thenCompose(a -> serviceB.call(a));
    CompletableFuture<C> resultC = resultA.thenCompose(a -> serviceC.call(a));
    ScheduledExecutorService e = Executors.newSingleThreadScheduledExecutor();
    e.schedule(() -> resultB.complete(fallbackB), timeOut, u);
    e.schedule(() -> resultC.complete(fallbackC), timeOut, u);
    CompletableFuture<Void> bAndC = CompletableFuture.allOf(resultB, resultC);
    bAndC.thenRun(e::shutdown);
    return bAndC.thenApply(ignoredVoid ->
                           combine(resultA.join(), resultB.join(), resultC.join()));
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM