blob: a24bc1fb653d33303f34d647967dea78705d2594 [file]
// 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();
}
}
}