| // Copyright 2026 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| package com.google.devtools.build.lib.concurrent; |
| |
| import com.google.common.util.concurrent.Uninterruptibles; |
| import com.google.errorprone.annotations.concurrent.GuardedBy; |
| import java.util.Objects; |
| import java.util.concurrent.CountDownLatch; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A one-shot task whose cancellation can wait until its execution has quiesced. |
| * |
| * <p>An optional completion action runs exactly once when the task's lifecycle ends, no matter how |
| * it ends: on the task thread after the body has run (even if it threw), or on the thread of the |
| * canceler that prevented the task from starting. Once the completion action begins, cancellation |
| * does not interrupt it. Quiescence includes the completion action: a normal return from {@link |
| * #runIfNotCancelled}, {@link #awaitCompletion}, {@link #awaitCompletionUninterruptibly}, or {@link |
| * #cancelAndAwait} guarantees that both the body and the completion action have finished. {@link |
| * #requestCancellation} only initiates cancellation and does not provide this guarantee. |
| * |
| * <p>This class serves as a replacement for {@link java.util.concurrent.Future} when canceled |
| * non-pure tasks may still interfere with other tasks until they have completed. Since {@link |
| * java.util.concurrent.Future#cancel} is specified to immediately mark the future as completed, it |
| * is unfortunately not possible for any conforming implementation to provide awaitable |
| * cancellation. |
| * |
| * @param <E> the checked exception thrown by the task body, if any |
| */ |
| public final class CancellableTask<E extends Exception> { |
| |
| private enum Phase { |
| NOT_STARTED, |
| RUNNING, |
| COMPLETING_AFTER_RUNNING, |
| COMPLETING_WITHOUT_RUNNING, |
| COMPLETED_AFTER_RUNNING, |
| COMPLETED_WITHOUT_RUNNING |
| } |
| |
| /** The work performed by a {@link CancellableTask}. */ |
| @FunctionalInterface |
| public interface Task<E extends Exception> { |
| void run() throws E; |
| } |
| |
| private final Task<E> task; |
| private final Runnable onCompletion; |
| private final CountDownLatch done = new CountDownLatch(1); |
| |
| @GuardedBy("this") |
| private Phase phase = Phase.NOT_STARTED; |
| |
| @GuardedBy("this") |
| @Nullable |
| private Thread executingThread; |
| |
| public CancellableTask(Task<E> task) { |
| this(task, () -> {}); |
| } |
| |
| /** |
| * Creates a task with a completion action. |
| * |
| * <p>The completion action must not throw or reenter this task. It may begin with its thread's |
| * interrupt bit set if the task body was interrupted, but cancellation does not interrupt it once |
| * it has begun. |
| */ |
| public CancellableTask(Task<E> task, Runnable onCompletion) { |
| this.task = Objects.requireNonNull(task); |
| this.onCompletion = Objects.requireNonNull(onCompletion); |
| } |
| |
| /** |
| * Runs the task synchronously unless cancellation prevented it from starting. |
| * |
| * <p>A normal return guarantees that the completion action has finished, including when |
| * cancellation prevented the task body from running. |
| * |
| * @return whether the task ran |
| */ |
| public boolean runIfNotCancelled() throws E { |
| boolean wasCancelled; |
| synchronized (this) { |
| wasCancelled = |
| switch (phase) { |
| case NOT_STARTED -> { |
| phase = Phase.RUNNING; |
| executingThread = Thread.currentThread(); |
| yield false; |
| } |
| case COMPLETING_WITHOUT_RUNNING, COMPLETED_WITHOUT_RUNNING -> { |
| if (executingThread == Thread.currentThread()) { |
| throw new IllegalStateException( |
| "completion action must not attempt to execute its task, this would deadlock"); |
| } |
| yield true; |
| } |
| default -> throw new IllegalStateException("task executed more than once"); |
| }; |
| } |
| if (wasCancelled) { |
| Uninterruptibles.awaitUninterruptibly(done); |
| return false; |
| } |
| try { |
| task.run(); |
| return true; |
| } finally { |
| synchronized (this) { |
| phase = Phase.COMPLETING_AFTER_RUNNING; |
| } |
| runCompletion(); |
| } |
| } |
| |
| /** |
| * Requests cancellation without waiting for the task to quiesce. |
| * |
| * <p>If the task has not started, this prevents it from starting and runs the completion action |
| * on its behalf before returning. Otherwise, it interrupts the thread executing the task body, |
| * which may still be executing when this method returns. Call {@link #awaitCompletion} afterward |
| * if quiescence is required without requesting cancellation a second time. |
| * |
| * @throws IllegalStateException if called from the task body or completion action |
| */ |
| public void requestCancellation() { |
| boolean preventedStart; |
| synchronized (this) { |
| preventedStart = |
| switch (phase) { |
| case NOT_STARTED -> { |
| phase = Phase.COMPLETING_WITHOUT_RUNNING; |
| executingThread = Thread.currentThread(); |
| yield true; |
| } |
| case RUNNING -> { |
| if (executingThread == Thread.currentThread()) { |
| throw new IllegalStateException("task cannot cancel itself"); |
| } |
| // Interrupt while holding the state lock so that the task cannot enter its completion |
| // action between publishing the executing thread and receiving the interrupt. |
| Objects.requireNonNull(executingThread).interrupt(); |
| yield false; |
| } |
| case COMPLETING_AFTER_RUNNING, COMPLETING_WITHOUT_RUNNING -> { |
| if (executingThread == Thread.currentThread()) { |
| throw new IllegalStateException("task cannot cancel itself"); |
| } |
| yield false; |
| } |
| case COMPLETED_AFTER_RUNNING, COMPLETED_WITHOUT_RUNNING -> false; |
| }; |
| } |
| if (preventedStart) { |
| // The task will never run, so run the completion action on its behalf. Signal completion only |
| // afterward so that concurrent awaiters only return once it has finished. |
| runCompletion(); |
| } |
| } |
| |
| /** |
| * Waits until the task body and completion action have finished without requesting cancellation. |
| * |
| * <p>If neither execution nor cancellation has started, this waits until one of them does. |
| * |
| * @throws IllegalStateException if called from the task body or completion action, neither of |
| * which can await the task |
| */ |
| public void awaitCompletion() throws InterruptedException { |
| checkCanAwait(); |
| done.await(); |
| } |
| |
| /** |
| * An uninterruptible variant of {@link #awaitCompletion} that restores the interrupt bit before |
| * returning. |
| * |
| * @throws IllegalStateException if called from the task body or completion action, neither of |
| * which can await the task |
| */ |
| public void awaitCompletionUninterruptibly() { |
| checkCanAwait(); |
| Uninterruptibles.awaitUninterruptibly(done); |
| } |
| |
| /** |
| * Cancels the task and waits until it no longer executes. |
| * |
| * <p>If the task has not started, this prevents it from starting and runs the completion action |
| * on its behalf. Otherwise, it interrupts the thread executing the task body and waits for the |
| * body and completion action to finish. Once the completion action begins, cancellation waits for |
| * it without interrupting its thread. Unlike {@link java.util.concurrent.Future#cancel}, a normal |
| * return therefore guarantees that the task has quiesced. |
| * |
| * @throws IllegalStateException if called from the task body or completion action, neither of |
| * which can await the task |
| */ |
| public void cancelAndAwait() throws InterruptedException { |
| requestCancellation(); |
| awaitCompletion(); |
| } |
| |
| private void checkCanAwait() { |
| synchronized (this) { |
| if (executingThread == Thread.currentThread()) { |
| throw new IllegalStateException("task cannot await itself"); |
| } |
| } |
| } |
| |
| /** Runs the completion action and publishes terminal state even if the action throws. */ |
| private void runCompletion() { |
| try { |
| onCompletion.run(); |
| } finally { |
| synchronized (this) { |
| executingThread = null; |
| phase = |
| phase == Phase.COMPLETING_WITHOUT_RUNNING |
| ? Phase.COMPLETED_WITHOUT_RUNNING |
| : Phase.COMPLETED_AFTER_RUNNING; |
| } |
| done.countDown(); |
| } |
| } |
| } |