| // 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.base.Preconditions; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.util.concurrent.AbstractFuture; |
| import com.google.common.util.concurrent.internal.InternalFutures; |
| import com.google.devtools.build.lib.concurrent.safeexecutor.RejectionHandlingRunnable; |
| import com.google.devtools.build.lib.concurrent.safeexecutor.SafeExecutor; |
| import com.google.errorprone.annotations.ForOverride; |
| import com.google.errorprone.annotations.Keep; |
| import java.lang.invoke.MethodHandles; |
| import java.lang.invoke.VarHandle; |
| import java.util.concurrent.CancellationException; |
| import java.util.concurrent.ConcurrentLinkedQueue; |
| import java.util.concurrent.RejectedExecutionException; |
| import javax.annotation.Nullable; |
| |
| /** |
| * A base class for futures that track in-flight tasks and complete when the tasks quiesce or an |
| * error occurs. |
| */ |
| public abstract class AbstractQuiescingFuture<T> extends AbstractFuture<T> |
| implements RejectionHandlingRunnable { |
| /** |
| * Handle for {@link #taskCount}. |
| * |
| * <p>This uses less memory than {@link java.util.concurrent.atomic.AtomicInteger}. |
| */ |
| private static final VarHandle TASK_COUNT_HANDLE; |
| |
| private static final VarHandle SECONDARY_EXCEPTIONS_HANDLE; |
| |
| private final SafeExecutor getValueExecutor; |
| |
| /** |
| * Count of in-flight tasks. |
| * |
| * <p>This is initialized to 1 to support the "pre-increment" pattern, which prevents premature |
| * completion during initialization. |
| * |
| * <p>Use {@link #TASK_COUNT_HANDLE} for atomic operations. |
| */ |
| @Keep // used via TASK_COUNT_HANDLE |
| private volatile int taskCount; |
| |
| /** |
| * The first exception observed by this future. |
| * |
| * <p>{@code null} if no exceptions occurred or if the future was cancelled. |
| */ |
| @Nullable private volatile Throwable primaryException; |
| |
| /** Any additional exceptions observed after the first. */ |
| @Keep // used via SECONDARY_EXCEPTIONS_HANDLE |
| @Nullable |
| private volatile ConcurrentLinkedQueue<Throwable> secondaryExceptions; |
| |
| /** |
| * Constructor. |
| * |
| * @param getValueExecutor runner for running {@link #getValue} or {@link #doneWithError}. |
| * @param taskCount initial task count. |
| */ |
| protected AbstractQuiescingFuture(SafeExecutor getValueExecutor, int taskCount) { |
| this.getValueExecutor = Preconditions.checkNotNull(getValueExecutor, "getValueExecutor"); |
| this.taskCount = taskCount; |
| } |
| |
| /** |
| * A unit of subtask work executed via {@link #executeSubtask}. |
| * |
| * <p>This interface is defined as a distinct functional interface rather than reusing {@link |
| * Runnable} to prevent ambiguous subtyping bugs with {@link RejectionHandlingRunnable}. Because |
| * {@link RejectionHandlingRunnable} extends {@link Runnable}, accepting a plain {@link Runnable} |
| * would allow callers to pass an existing {@link RejectionHandlingRunnable} instance (such as a |
| * child {@link QuiescingFutureTask}), under the mistaken assumption that its {@link |
| * RejectionHandlingRunnable#handleRejection} method would be invoked upon rejection. By using an |
| * independent {@link Subtask} interface, the compiler prevents accidental passing of {@link |
| * RejectionHandlingRunnable}. |
| */ |
| @FunctionalInterface |
| public interface Subtask { |
| /** Executes the subtask. */ |
| void runSubtask(); |
| } |
| |
| /** |
| * Submits a {@link Subtask} to the given {@link SafeExecutor}, incrementing {@link #taskCount} |
| * and ensuring proper decrements and exception recording upon execution, exception, or rejection. |
| * |
| * @param subtask the subtask work to execute |
| * @param executor the runner for executing the subtask |
| */ |
| public final void executeSubtask(Subtask subtask, SafeExecutor executor) { |
| Preconditions.checkNotNull(subtask, "subtask"); |
| Preconditions.checkNotNull(executor, "executor"); |
| increment(); |
| try { |
| executor.execute(new SubtaskRejectionHandlingRunnable(subtask)); |
| } catch (Throwable t) { |
| notifyException(t); |
| } |
| } |
| |
| /** |
| * The resulting value of this future. |
| * |
| * <p>Called after the final decrement. Implementations must guarantee that the value is ready at |
| * that time. Not called if there were any errors. |
| */ |
| @ForOverride |
| protected abstract T getValue(); |
| |
| /** |
| * Called if there was an error, after all the associated tasks complete. |
| * |
| * <p>Allows clients to perform cleanup work if there is an error. Receives the primary cause (or |
| * null if cancelled without an explicit exception) and any secondary causes recorded during |
| * execution. |
| */ |
| @ForOverride |
| protected void doneWithError( |
| @Nullable Throwable primaryCause, ImmutableList<Throwable> secondaryCauses) {} |
| |
| /** |
| * Sets the future as failing with {@code t} and decrements the task count. |
| * |
| * <p>If the client calls this, it should not call {@link #decrement} for the same task. |
| */ |
| protected final void notifyException(Throwable t) { |
| Preconditions.checkNotNull(t, "t"); |
| recordException(t); |
| decrement(); |
| } |
| |
| /** |
| * Records an exception on the future. |
| * |
| * <p>If this is the first exception recorded, sets the future's failure cause. Subsequent |
| * exceptions are collected into {@link #secondaryExceptions} to preserve multi-failure |
| * diagnostics for {@link #doneWithError(Throwable, ImmutableList)}. |
| */ |
| protected final void recordException(Throwable t) { |
| Preconditions.checkNotNull(t, "t"); |
| if (t instanceof CancellationException) { |
| // Propagates CancellationException by cancelling this future. |
| cancel(/* mayInterruptIfRunning= */ false); |
| } else if (setException(t)) { |
| primaryException = t; |
| } else { |
| getOrCreateSecondaryExceptions().add(t); |
| } |
| } |
| |
| /** Increments the task count. */ |
| protected final void increment() { |
| TASK_COUNT_HANDLE.getAndAdd(this, 1); |
| } |
| |
| /** Decrements the task count. */ |
| protected final void decrement() { |
| int countBeforeDecrement = (int) TASK_COUNT_HANDLE.getAndAdd(this, -1); |
| if (countBeforeDecrement == 1) { |
| try { |
| getValueExecutor.execute(this); |
| } catch (RejectedExecutionException e) { |
| handleRejection(e); |
| } |
| } |
| } |
| |
| /** |
| * Decrements the task count without submitting to {@link #getValueExecutor}. |
| * |
| * <p>For use by {@link QuiescingFutureTask#handleRejection} when a setup task is rejected before |
| * running to clear the pre-increment. |
| */ |
| final void decrementWithoutScheduling() { |
| TASK_COUNT_HANDLE.getAndAdd(this, -1); |
| } |
| |
| /** |
| * Completes this future upon quiescence (or runs error cleanup if cancelled or failed). |
| * |
| * <p>Package-private to allow dispatch from {@link QuiescingFuture#run}, {@link |
| * QuiescingFutureTask#run}, and {@link QuiescingFutureTask#handleRejection}. |
| */ |
| final void handleQuiescence() { |
| Throwable primaryCause = getPrimaryCause(); |
| if (isCancelled() || primaryCause != null) { |
| runDoneWithError(primaryCause); |
| return; |
| } |
| |
| boolean setSuccess = false; |
| try { |
| T value = getValue(); |
| setSuccess = set(value); |
| } catch (Throwable t) { |
| recordException(t); |
| } finally { |
| if (!setSuccess) { |
| // This may occur when cancellation races with set, above, or if getValue() throws an |
| // exception. |
| runDoneWithError(getPrimaryCause()); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") // required for VarHandle usage |
| private ConcurrentLinkedQueue<Throwable> getOrCreateSecondaryExceptions() { |
| var queue = (ConcurrentLinkedQueue<Throwable>) SECONDARY_EXCEPTIONS_HANDLE.getAcquire(this); |
| if (queue == null) { |
| queue = new ConcurrentLinkedQueue<>(); |
| if (!SECONDARY_EXCEPTIONS_HANDLE.compareAndSet(this, null, queue)) { |
| queue = (ConcurrentLinkedQueue<Throwable>) SECONDARY_EXCEPTIONS_HANDLE.getAcquire(this); |
| } |
| } |
| return queue; |
| } |
| |
| private final class SubtaskRejectionHandlingRunnable implements RejectionHandlingRunnable { |
| private final Subtask subtask; |
| |
| SubtaskRejectionHandlingRunnable(Subtask subtask) { |
| this.subtask = subtask; |
| } |
| |
| @Override |
| public void run() { |
| try { |
| subtask.runSubtask(); |
| } catch (Throwable t) { |
| recordException(t); |
| } finally { |
| decrement(); |
| } |
| } |
| |
| @Override |
| public void handleRejection(Throwable t) { |
| notifyException(t); |
| } |
| } |
| |
| @Nullable |
| private Throwable getPrimaryCause() { |
| if (primaryException != null) { |
| return primaryException; |
| } |
| return InternalFutures.tryInternalFastPathGetFailure(this); |
| } |
| |
| @SuppressWarnings("unchecked") // required by VarHandle usage |
| private void runDoneWithError(@Nullable Throwable primaryCause) { |
| var queue = (ConcurrentLinkedQueue<Throwable>) SECONDARY_EXCEPTIONS_HANDLE.getAcquire(this); |
| ImmutableList<Throwable> secondaryCauses = |
| queue == null ? ImmutableList.of() : ImmutableList.copyOf(queue); |
| |
| doneWithError(primaryCause, secondaryCauses); |
| } |
| |
| static { |
| MethodHandles.Lookup lookup = MethodHandles.lookup(); |
| try { |
| TASK_COUNT_HANDLE = |
| lookup.findVarHandle(AbstractQuiescingFuture.class, "taskCount", int.class); |
| SECONDARY_EXCEPTIONS_HANDLE = |
| lookup.findVarHandle( |
| AbstractQuiescingFuture.class, "secondaryExceptions", ConcurrentLinkedQueue.class); |
| } catch (ReflectiveOperationException e) { |
| throw new ExceptionInInitializerError(e); |
| } |
| } |
| } |