11

CompletableFuture.exceptionally() method takes a lambda, but there is no flavor of the method that takes a custom Executor, or even an "...Async" flavor of it.

Which executor does exceptionally lambda run on? Would it be the same executor which ran the original CompletableFuture which threw the exception? Or (I would be surprised if this is the case) is it the commonPool ?

vishr
  • 985
  • 10
  • 28
  • 3
    They created the entire cartesian product of (args and returning [apply], args and not returning [accept], no args and not returning [run]) x (synchronous with no executor guarantee [no suffix], asynchronous in commonPool [async], asynchronous in specific executor [async with additional argument]) x (that future alone [then], that future and another [both], that future or another [either]) for treating successful completion value as its sole argument (plus the [compose] variety), but only 1 method for handling exclusively exception: `exceptionally()`... yeah, that's the exception. – Alicia Jan 17 '17 at 21:48

4 Answers4

8

Form JDK bug discussion CompletableFuture.exceptionally may execute on main thread :

CompletableFuture.exceptionally does not take an Executor argument since it is not designed to execute the exceptionally task asynchronously.

If the dependent task is not yet completed then the exceptionally task will complete on the same thread that dependent tasks completes on.

If the dependent task is completed then the exceptionally task will complete on the thread that executed the call to exceptionally.

This is the same behaviour that will occur for any non-asynchronous execution, such as thenAccept.

To guarantee that the exceptionally task is executed on a thread within the executor thread pool then it is necessary to use whenCompleteAsync or handleAsync and passing in the executor.

radistao
  • 14,889
  • 11
  • 66
  • 92
7

Note that as of JDK 12, there is CompletionStage.exceptionallyAsync (and exceptionallyAsync which takes an Executor).

Christian S.
  • 877
  • 2
  • 9
  • 20
0

I did some experiments. Looks like the executor for the exceptionally handler is not chosen deterministically.

  1. If I do not place any breakpoints, the exceptionally handler is occasionally executed in the same executor as the CompletableFuture that has the exceptionally handler. Furthermore, I could notice that it ran in the same ForkJoinTask. Once in two or three runs, it was executed on the main thread.
  2. If I placed breakpoint at runtime prior to attaching the exceptionally handler and waited there for a moment, the exceptionally lambda was consistently executed on the main (calling) thread!

Here is the experiment code:

    ForkJoinPool ex = new ForkJoinPool(2,
            ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false);

    // AsyncThreadLocal.requestId.set((int)(Math.random()*1000));

    CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
        System.out.println(Thread.currentThread().getName());
        throw new RuntimeException();
        // return 3;
        }, ex);

    CompletableFuture<Integer> f8 = f1.exceptionally(t -> {
        System.out.println(Thread.currentThread().getName());
        return 5;
    });

    Thread.sleep(10000);

Output of program: Sometimes it is

  ForkJoinPool-1-worker-1
  main

Other times it is

ForkJoinPool-1-worker-1
ForkJoinPool-1-worker-1
vishr
  • 985
  • 10
  • 28
0

Looks like it runs in the same executor as its CompletionStage

 public static void main(String[] args) throws Exception {
    //ExecutorService executorService = Executors.newFixedThreadPool(3);
    ExecutorService executorService = Executors.newWorkStealingPool();

    while (true) {
        int i = Instant.now().getNano();
        CompletableFuture<?> completableFuture = CompletableFuture.supplyAsync(
                ()-> {
                    System.err.printf("async thread at %d -> %s\n", i, Thread.currentThread().getName());
                    try {
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    throw new RuntimeException();
                }, executorService);

        completableFuture.exceptionally(
                (err)-> {
                    System.err.printf("error thread for %d -> %s\n", i, Thread.currentThread().getName());
                    return null;
                });

        TimeUnit.SECONDS.sleep(5);
    }
}

For fixed size:

async thread at 418000000 -> pool-1-thread-1

error thread for 418000000 -> pool-1-thread-1

async thread at 646000000 -> pool-1-thread-2

error thread for 646000000 -> pool-1-thread-2

async thread at 646000000 -> pool-1-thread-3

error thread for 646000000 -> pool-1-thread-3

async thread at 646000000 -> pool-1-thread-1

error thread for 646000000 -> pool-1-thread-1

async thread at 647000000 -> pool-1-thread-2

error thread for 647000000 -> pool-1-thread-2

For stealing pool(4 cores):

async thread at 96000000 -> ForkJoinPool-1-worker-1

error thread for 96000000 -> ForkJoinPool-1-worker-1

async thread at 196000000 -> ForkJoinPool-1-worker-1

error thread for 196000000 -> ForkJoinPool-1-worker-1

async thread at 197000000 -> ForkJoinPool-1-worker-1

error thread for 197000000 -> ForkJoinPool-1-worker-1

async thread at 197000000 -> ForkJoinPool-1-worker-1

error thread for 197000000 -> ForkJoinPool-1-worker-1

async thread at 197000000 -> ForkJoinPool-1-worker-1

error thread for 197000000 -> ForkJoinPool-1-worker-1

async thread at 197000000 -> ForkJoinPool-1-worker-1

with no executor:

async thread at 848000000 -> ForkJoinPool.commonPool-worker-1

error thread for 848000000 -> ForkJoinPool.commonPool-worker-1

async thread at 944000000 -> ForkJoinPool.commonPool-worker-1

error thread for 944000000 -> ForkJoinPool.commonPool-worker-1

async thread at 944000000 -> ForkJoinPool.commonPool-worker-1

error thread for 944000000 -> ForkJoinPool.commonPool-worker-1

async thread at 944000000 -> ForkJoinPool.commonPool-worker-1

error thread for 944000000 -> ForkJoinPool.commonPool-worker-1

async thread at 944000000 -> ForkJoinPool.commonPool-worker-1

error thread for 944000000 -> ForkJoinPool.commonPool-worker-1

Community
  • 1
  • 1
Ivan
  • 1,320
  • 16
  • 26
  • Does not seem to run on that executor consistently... please see my answer and code. When I placed the breakpoint on line where f8 is defined, the thread name changed. – vishr Jul 11 '16 at 20:56
  • Hi, I am not claiming that it will be ran in the same thread, but it the same executor. That's the idea about pools - threads can be interchanged and in the case of a stealing pool even taken away from a blocked one to some other task that needs it. But anyway seems like the debugging environment by itself may impact your results. – Ivan Jul 12 '16 at 06:07
  • I meant to say that it changed to a thread name outside of the assigned executor for the CompletableFuture. Please try my code snippet out on your machine, and confirm. – vishr Jul 12 '16 at 15:11