So firstly the problem that triggers this, I'm seeing deadlocks in CompletableFutures when you try to use asyncCompose in combination with a trampoline executor.
For those unaware, a trampoline executor is one that runs submitted tasks on the current thread, however it attempts to solve stack overflow errors caused when tasks submit big chains of nested tasks to the executor, by using a thread local to queue these tasks up to be executed sequentially when nested submission happens, rather than immediately.
Now the problem appears to be that in certain circumstances CompletableFuture will block on another CompletableFuture, for example, it blocks until the result of a CompletableFuture returned by the callback passed to thenCompose is redeemed. This is of course dangerous, since if this returned future is dependent on the same executor that the current thread is blocking in, it might deadlock due to thread exhaustion. CompletableFuture does try to handle this, but its handling seems to assume that the only executor this could happen on is a ForkJoinPool, and it addresses it by temporarily increasing the allowed parallelism of that pool. However, that approach is tied to ForkJoinPools, and does not work for any other type of executor, in particular trampoline executors, which don't support any parallelism, but require all operations to be non blocking. So I think there are some bad assumptions about the executors being used here. If CompletableFuture can only work with ForkJoinPool, then it should only accept ForkJoinPool in its API. Since it accepts Executor, it must work with all executors.
But, this brings us to AsyncCompose being blocking. It seems that AsyncCompose will always block on the returned CompletableFuture. Am I correct, or have I misread the code? Why is this? Shouldn't AsyncCompose rather attach a callback to the returned CompletableFuture, and redeem its associated CompletableFuture when that callback is executed? Doesn't blocking here defeat the whole purpose of asynchronous programming?