Hi, I recently traced a race in an application (https://github.com/bazelbuild/bazel/issues/21773) down to a particular behavior of ExecutorService#close that, to me, doesn't seem to be obvious from its documentation: If a task that has been submitted to the executor is canceled while it is already executing, ExecutorService#close will not wait for the associated Runnable to return.
Consider the following example: var taskRunning = new AtomicBoolean(true); try (var executorService = Executors.newVirtualThreadPerTaskExecutor()) { var taskStarted = new CountDownLatch(1); var task = executorService.submit( () -> { // Uninterruptibly wait for a second. taskStarted.countDown(); long end = System.currentTimeMillis() + 1000; long remaining; while ((remaining = end - System.currentTimeMillis()) > 0) { try { Thread.sleep(remaining); } catch (InterruptedException e) { } } taskRunning.set(false); }); // Cancel the task after it has started execution. try { taskStarted.await(); } catch (InterruptedException e) { throw new IllegalStateException(e); } task.cancel(false); } System.err.println("Task still running: " + taskRunning.get()); This will print "Task still running: true" and exit immediately instead of waiting for a second. It would have been helpful to me if the phrase "completed execution" in the docs for #awaitTermination and #close had mentioned that canceled tasks are always considered to have completed execution, even if their Runnable hasn't returned yet. Would a change that more clearly documents this behavior be welcome? Is there a clear definition of "completed execution" in some other parts of the j.u.c docs that specifies this behavior? Fabian