| // Copyright 2023 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.skyframe; | |
| import static com.google.common.util.concurrent.MoreExecutors.directExecutor; | |
| import com.google.common.util.concurrent.ListenableFuture; | |
| import com.google.common.util.concurrent.ListeningExecutorService; | |
| import com.google.common.util.concurrent.MoreExecutors; | |
| import com.google.devtools.build.skyframe.SkyFunction.Environment; | |
| import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState; | |
| import java.util.concurrent.ArrayBlockingQueue; | |
| import java.util.concurrent.BlockingQueue; | |
| import java.util.concurrent.CancellationException; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.Semaphore; | |
| import javax.annotation.Nullable; | |
| import javax.annotation.concurrent.GuardedBy; | |
| /** | |
| * A {@link SkyKeyComputeState} that manages a non-Skyframe virtual worker thread that persists | |
| * across different invocations of a SkyFunction. | |
| * | |
| * <p>The worker thread uses a {@link SkyFunction.Environment} object acquired from the host thread. | |
| * When a new Skyframe dependency is needed, the worker thread itself does not need to restart; | |
| * instead, it can signal the host thread to restart to get a fresh Environment object. | |
| * | |
| * <p>Similar to other implementations of {@link SkyKeyComputeState}, this avoids redoing expensive | |
| * work when a new Skyframe dependency is needed; but because it holds on to an entire worker | |
| * thread, this class is more suited to cases where the intermediate result of expensive work cannot | |
| * be easily serialized (in particular, if there's an ongoing Starlark evaluation, as is the case in | |
| * repo fetching). | |
| */ | |
| public class WorkerSkyKeyComputeState<T> implements SkyKeyComputeState { | |
| /** | |
| * A semaphore with 0 or 1 permit. The worker can release a permit either when it's finished | |
| * (successfully or otherwise), or to indicate that the host thread should return {@code null}, | |
| * causing a Skyframe restart. In the latter case, the worker will immediately block on {@code | |
| * delegateEnvQueue}, waiting for the host thread to send a fresh {@link SkyFunction.Environment} | |
| * over. | |
| */ | |
| // A Semaphore is useful here because, crucially, releasing a permit never blocks and thus cannot | |
| // be interrupted. | |
| private final Semaphore signalSemaphore = new Semaphore(0); | |
| /** | |
| * The channel for the host Skyframe thread to send fresh {@link SkyFunction.Environment} objects | |
| * back to the worker thread. | |
| */ | |
| // We use an ArrayBlockingQueue of size 1 instead of a SynchronousQueue, so that if the worker | |
| // gets interrupted before the host thread restarts, the host thread doesn't hang forever. | |
| private final BlockingQueue<SkyFunction.Environment> delegateEnvQueue = | |
| new ArrayBlockingQueue<>(1); | |
| /** | |
| * This future holds on to the worker thread in order to cancel it when necessary; it also serves | |
| * to tell whether a worker thread is already running. | |
| */ | |
| @GuardedBy("this") | |
| @Nullable | |
| private ListenableFuture<T> workerFuture = null; | |
| /** The executor service that manages the worker thread. */ | |
| // We hold on to this alongside `workerFuture` because it offers a convenient mechanism to make | |
| // sure the worker thread has shut down (with its blocking `close()` method). | |
| @GuardedBy("this") | |
| @Nullable | |
| private ListeningExecutorService workerExecutorService = null; | |
| /** | |
| * Represents work that will will be performed on the worker thread, yielding a result of type | |
| * {@code T}. The worker thread should exclusively use the provided {@code workerEnv} for Skyframe | |
| * access. | |
| */ | |
| @FunctionalInterface | |
| public interface WorkerCallable<T> { | |
| T call(Environment workerEnv) throws Exception; | |
| } | |
| /** | |
| * Starts a worker performing the given {@link WorkerCallable}, or if such a worker already exists | |
| * and is waiting for a Skyframe restart, sends over a fresh Environment and asks it to continue | |
| * its work. This method blocks until the worker thread finishes (either successfully or | |
| * otherwise), <em>or</em> until the worker needs a Skyframe restart, in which case the worker | |
| * will suspend itself and wait for the next invocation of this method by a restarted host | |
| * SkyFunction with a fresh Environment. | |
| * | |
| * @param env The Skyframe Environment of the host SkyFunction. | |
| * @param workerThreadName The name of the worker thread to be started by this method, if one | |
| * doesn't already exist. | |
| * @param workerCallable The work to be performed on the worker thread. Note that code in this | |
| * callable should exclusively use the Environment passed to {@link | |
| * WorkerCallable#call(Environment)}, <em>not</em> the original host Environment. | |
| * @return If the worker finishes successfully, this method returns whatever {@code | |
| * workerCallable} returns. If the worker needs a Skyframe restart, returns null. | |
| * @throws InterruptedException if the caller (host) thread is interrupted. | |
| * @throws CancellationException if the worker thread is interrupted (most likely by {@link | |
| * #close()}) | |
| * @throws ExecutionException if the worker callable throws an exception. | |
| */ | |
| @Nullable | |
| public T startOrContinueWork( | |
| Environment env, String workerThreadName, WorkerCallable<T> workerCallable) | |
| throws InterruptedException, CancellationException, ExecutionException { | |
| ListenableFuture<T> workerFuture = getOrStartWorker(workerThreadName, workerCallable); | |
| try { | |
| delegateEnvQueue.put(env); | |
| signalSemaphore.acquire(); | |
| if (!workerFuture.isDone()) { | |
| // This means that the worker is still running, and expecting a fresh Environment. Return | |
| // null to trigger a Skyframe restart, but *don't* shut down the worker executor. | |
| return null; | |
| } | |
| return workerFuture.get(); | |
| } finally { | |
| if (workerFuture.isDone()) { | |
| // Unless we know the worker is waiting on a fresh Environment, we should *always* shut | |
| // down the worker executor by the time we finish executing (successfully or otherwise). | |
| // This ensures that 1) no background work happens without our knowledge, and 2) if the | |
| // SkyFunction is re-entered for any reason (for example b/330892334 and | |
| // https://github.com/bazelbuild/bazel/issues/21238), we know we'll need to create a new | |
| // worker from scratch. | |
| close(); | |
| } | |
| } | |
| } | |
| /** | |
| * Returns the worker future, or if a worker is not already running, starts a worker thread | |
| * running the given callable. This makes sure to release a permit on the {@code signalSemaphore} | |
| * when the worker finishes, successfully or otherwise. This may only be called from the host | |
| * Skyframe thread. | |
| */ | |
| private synchronized ListenableFuture<T> getOrStartWorker( | |
| String workerThreadName, WorkerCallable<T> workerCallable) { | |
| if (workerFuture != null) { | |
| return workerFuture; | |
| } | |
| // We reset the state object back to its very initial state, since the host SkyFunction may have | |
| // been re-entered (for example b/330892334 and | |
| // https://github.com/bazelbuild/bazel/issues/21238), and/or the previous worker thread may have | |
| // been interrupted while the host SkyFunction was inactive. | |
| workerExecutorService = | |
| MoreExecutors.listeningDecorator( | |
| Executors.newThreadPerTaskExecutor( | |
| Thread.ofVirtual().name(workerThreadName).factory())); | |
| signalSemaphore.drainPermits(); | |
| delegateEnvQueue.clear(); | |
| // Start the worker. | |
| workerFuture = | |
| workerExecutorService.submit( | |
| () -> { | |
| var workerEnv = | |
| new WorkerSkyFunctionEnvironment( | |
| delegateEnvQueue.take(), this::signalForFreshEnv); | |
| return workerCallable.call(workerEnv); | |
| }); | |
| workerFuture.addListener(signalSemaphore::release, directExecutor()); | |
| return workerFuture; | |
| } | |
| /** | |
| * Releases a permit on the {@code signalSemaphore} and immediately expect a fresh Environment | |
| * back. This may only be called from the worker thread. | |
| */ | |
| private SkyFunction.Environment signalForFreshEnv() throws InterruptedException { | |
| signalSemaphore.release(); | |
| return delegateEnvQueue.take(); | |
| } | |
| /** | |
| * Closes the state object, and blocks until all pending async work is finished. The state object | |
| * will reset to a clean slate after this method finishes. | |
| */ | |
| // This may be called from any thread, including the host Skyframe thread and the | |
| // high-memory-pressure listener thread. | |
| @Override | |
| public synchronized void close() { | |
| if (workerFuture != null) { | |
| workerFuture.cancel(true); | |
| } | |
| workerFuture = null; | |
| if (workerExecutorService != null) { | |
| workerExecutorService.close(); // This blocks | |
| } | |
| } | |
| } |