CompletableFuture<R> (@NotNull CompletableFuture<? extends @NotNull ProgressIndicator> progressFuture, @NotNull Semaphore modalityEntered, @NotNull Function<ProgressIndicator, R> onThreadCallable) { CompletableFuture<R> taskFuture = launchTask(onThreadCallable, progressFuture); CompletableFuture<R> resultFuture; if (isModal) { // Running task with blocking EDT event pumping has the following contract in test mode: // if a random EDT event processed by blockingPI fails with an exception, the event pumping // is stopped and the submitted task fails with respective exception. // The task submitted to e.g. POOLED thread might not be able to finish at all because it requires invoke&waits, // but EDT is broken due an exception. Hence, initial task should be completed exceptionally CompletableFuture<Void> blockingRunFuture = progressFuture.thenAccept(progressIndicator -> { if (progressIndicator instanceof BlockingProgressIndicator) { //noinspection deprecation ((BlockingProgressIndicator)progressIndicator).startBlocking(modalityEntered::up, taskFuture); } else { Logger.getInstance(ProgressRunner.class).warn("Can't go modal without BlockingProgressIndicator"); modalityEntered.up(); } }); blockingRunFuture.exceptionally(throwable -> { taskFuture.completeExceptionally(throwable); return null; }); // `startBlocking` might throw unrelated exceptions execute the submitted task so potential failure should be handled. // Relates to testMode-ish execution: unhandled exceptions on event pumping are `LOG.error`-ed, hence throw exceptions in tests. resultFuture = taskFuture.thenCombine(blockingRunFuture, (r, __) -> r); } else { resultFuture = taskFuture; } if (isSync) { // At first here was a blocking resultFuture.get() call, // which lead to deadlocks when `BlockingProgressIndicator.startBlocking` already exited, // and nobody was dispatching EDT events because the current thread (EDT) was blocked on this future, // so instead of blocking, we assert that the future should be done at this point, // otherwise, `startBlocking` should continue pumping the events to give cancelled tasks a chance to complete. if (!resultFuture.isDone()) { throw new IllegalStateException("Result future must be done at this point"); } } return resultFuture; }