blob: bac7db19eb3054274421457ed4ba2adfb9617554 [file] [log] [blame]
// Copyright 2022 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.state;
import com.google.devtools.build.skyframe.SkyFunction;
import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState;
import com.google.devtools.build.skyframe.SkyKey;
import com.google.devtools.build.skyframe.SkyValue;
import java.util.function.Consumer;
import javax.annotation.Nullable;
/**
* A simple state machine with structured concurrency.
*
* <p>This is used to implement {@link SkyFunction}s with logical concurrency within {@link
* SkyKeyComputeState}. All execution is singly-threaded. It can be used in places where further
* stateful decomposition of a computation is desirable but more {@code Skyframe} entries would
* create too much overhead. However, the key motivation is to facilitate logical concurrency.
*
* <p>For example, consider a {@link SkyFunction} that processes dependencies. Each dependency
* requires a sequence of processing steps, some of which have {@code Skyframe} lookups. Let {@code
* A = <A1, A2, A3>} and {@code B = <B1, B2, B3>} be sequences of dependent {@code SkyKey}s. {@code
* A3} depends on {@code A2} depends on {@code A1} and similarly for {@code B3, B2, B1}. The
* processing of {@code A} and {@code B} are independent and therefore logically concurrent.
*
* <p>One conventional approach is to implement the {@code SkyFunction} to make groups like {@code
* (A1,B1), (A2,B2), (A3,B3)}. Grouping is more efficient than one-at-a-time lookups because in
* builds where lookups can be performed remotely, such implementations can parallelize over groups
* but process single queries sequentially to avoid wasted speculative work.
*
* <p>However, this manual batching may creates false dependencies that lead to unnecessary latency.
* In the example, {@code A2} can't be evaluated until {@code B1} is available. If both {@code B1}
* and {@code A2} are slow, the critical path is unnecessarily lengthened by forcing {@code A2} to
* happen after {@code B1} instead of allowing {@code A2} to proceed once {@code A1} is available.
* From the perspective of restarts, if both {@code B1} and {@code A2} require restarts, evaluation
* requires 2 restarts. However, by running concurrently, the restart of {@code B1} and {@code A2}
* can be grouped together, reducing it to 1 restart.
*
* <p>The {@link Driver} class and {@link Driver#drive} are used to run a state machine.
*
* <p>A guide is available at <a href="https://bazel.build/contribute/statemachine-guide"/>.
*/
@FunctionalInterface
public interface StateMachine {
/** A sentinel value returned when a {@code StateMachine} is done. */
public static final StateMachine DONE =
t -> {
throw new IllegalStateException("Sentinel DONE state should not be executed.");
};
/**
* Step performs the next computation.
*
* <p>{@link Tasks#lookup} may be used to request {@link SkyKey}s. The next step will not be
* executed until all requested {@link SkyValue}s are available and their associated callbacks
* have been called. Similarly, {@link Tasks#enqueue} can be used to spawn a concurrent
* subcomputation, which must also complete before the next computation step.
*
* <p>Note that recursive decomposition within subtasks is possible and can be used to capture
* fine-grain dependency structures. This is required to correctly model the example in the class
* description. {@code <A1, A2, A3>} and {@code <B1, B2, B3>} become concurrent, multi-step,
* subtasks.
*
* @param tasks an interface for adding subtasks, which may be either {@link SkyKey} lookups or
* child state machines. The {@code tasks} handle is associated with this state machine and
* other state machines should not use it.
* @return an instance indicating the next computation or {@link #DONE} on completion.
*/
StateMachine step(Tasks tasks) throws InterruptedException;
/**
* Tasks allows registering logically parallel subtasks.
*
* <p>Completion of the current step waits until all subtasks are complete.
*/
interface Tasks {
/**
* Enqueues a subtask for logically concurrent evaluation.
*
* <p>The next step will not be executed until the subtask completes. If more than one subtask
* is enqueued, the next step waits on all subtasks.
*/
void enqueue(StateMachine subtask);
/**
* A lookup that handles no exceptions.
*
* <p>This lookup is logically concurrent with other subtasks. The state machine {@link Driver}
* may defer the callback until after a {@code Skyframe} restart if it is not immediately
* available.
*
* <p>Unhandled exceptions eventually set a fail fast condition over the entire state machine
* tree and no further processing occurs afterwards.
*
* <p>IMPLEMENTATION: if an unhandled exception occurs immediately (without a restart) on a
* lookup, {@link Driver} observes unavailability and returns an incomplete status. The driver
* cannot distinguish here between a result that is not yet computed and an unhandled exception.
*
* <p>After a restart, all previously requested values should be available, so observing
* unavailability implies an unhandled exception, triggering fail-fast.
*/
void lookUp(SkyKey key, Consumer<SkyValue> sink);
/**
* A lookup that handles exceptions of the specified type.
*
* <p>The callback could be deferred until the next {@code Skyframe} restart if the queried key
* is not immediately available.
*/
<E extends Exception> void lookUp(
SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);
/** A lookup that handles exceptions of the specified 2 types. */
<E1 extends Exception, E2 extends Exception> void lookUp(
SkyKey key,
Class<E1> exceptionClass1,
Class<E2> exceptionClass2,
ValueOrException2Sink<E1, E2> sink);
/** A lookup that handles exceptions of the specified 3 types. */
<E1 extends Exception, E2 extends Exception, E3 extends Exception> void lookUp(
SkyKey key,
Class<E1> exceptionClass1,
Class<E2> exceptionClass2,
Class<E3> exceptionClass3,
ValueOrException3Sink<E1, E2, E3> sink);
}
/**
* Receives the result of a lookup.
*
* <p>Exactly one of {@code value} or {@code exception} will be non-null.
*/
@FunctionalInterface
interface ValueOrExceptionSink<E extends Exception> {
void acceptValueOrException(@Nullable SkyValue value, @Nullable E exception);
}
/**
* Receives the result of a lookup.
*
* <p>Exactly one of {@code value}, {@code e1} or {@code e2} will be non-null.
*/
@FunctionalInterface
interface ValueOrException2Sink<E1 extends Exception, E2 extends Exception> {
void acceptValueOrException2(@Nullable SkyValue value, @Nullable E1 e1, @Nullable E2 e2);
}
/**
* Receives the result of a lookup.
*
* <p>Exactly one of {@code value}, {@code e1}, {@code e2} or {@code e3} will be non-null.
*/
@FunctionalInterface
interface ValueOrException3Sink<
E1 extends Exception, E2 extends Exception, E3 extends Exception> {
void acceptValueOrException3(
@Nullable SkyValue value, @Nullable E1 e1, @Nullable E2 e2, @Nullable E3 e3);
}
}