Descripción general
Un StateMachine
de Skyframe es un objeto de función deconstruido que reside en
el montón. Admite flexibilidad y evaluación sin redundancia1 cuando
los valores requeridos no están disponibles de inmediato, pero se procesan de forma asíncrona. El
StateMachine
no puede vincular un recurso de subprocesos mientras espera, sino que debe
suspenderse y reanudarse. Así, la deconstrucción expone el reingreso explícito
y puntos para que se puedan omitir
los procesamientos previos.
Los elementos StateMachine
se pueden usar para expresar secuencias, ramificaciones, lógicas estructuradas
y están adaptados específicamente para la interacción de Skyframe.
Los elementos StateMachine
se pueden componer en StateMachine
más grandes y se pueden compartir
sub-StateMachine
. La simultaneidad siempre es jerárquica por construcción
puramente lógico. Cada subtarea simultánea se ejecuta en el único superior compartido
Subproceso de SkyFunction.
Introducción
En esta sección, se motivan y presentan brevemente los StateMachine
, que se encuentran en el
java.com.google.devtools.build.skyframe.state
.
Una breve introducción a los reinicios de Skyframe
Skyframe es un framework que realiza evaluaciones paralelas de grafos de dependencias.
Cada nodo del gráfico corresponde a la evaluación de una SkyFunction con un
SkyKey especifica sus parámetros y SkyValue especifica su resultado. El
de manera que una SkyFunction pueda buscar SkyValues por SkyKey
lo que activa una evaluación recursiva y paralela de SkyFunctions adicionales. En lugar de
bloqueo, lo que enlazaría un subproceso cuando un SkyValue solicitado aún no esté
listo porque algún subgrafo de procesamiento está incompleto, la solicitud
SkyFunction observa una respuesta null
getValue
y debe mostrar null
.
en lugar de un SkyValue, lo que indica que está incompleto debido a entradas faltantes.
Skyframe reinicia SkyFunctions cuando todos los SkyValues solicitados anteriormente
que estén disponibles.
Antes de la introducción de SkyKeyComputeState
, la forma tradicional de controlar
un reinicio fue volver a ejecutar el cálculo por completo. Aunque tiene datos cuadráticos
complejidad, las funciones escritas de esta manera se completan porque cada repetición,
menos búsquedas muestran null
. Con SkyKeyComputeState
, es posible
asociar datos de puntos de control especificados a mano con una SkyFunction, lo que ahorra un
la reprocesamiento.
Los elementos StateMachine
son objetos que residen en SkyKeyComputeState
y eliminan
Prácticamente todo el reprocesamiento cuando se reinicia una SkyFunction (suponiendo que
SkyKeyComputeState
no queda fuera de la caché) mediante la exposición de las suspensiones y las reanudaciones.
hooks de ejecución.
Cálculos con estado dentro de SkyKeyComputeState
Desde el punto de vista del diseño orientado a objetos, tiene sentido considerar almacenar
objetos de procesamiento dentro de SkyKeyComputeState
en lugar de valores de datos puros.
En Java, la descripción mínima de un comportamiento que transporta un objeto es un
una interfaz funcional y resulta suficiente. Un StateMachine
tiene
la siguiente definición, curiosamente recursiva:2.
@FunctionalInterface
public interface StateMachine {
StateMachine step(Tasks tasks) throws InterruptedException;
}
La interfaz Tasks
es análoga a SkyFunction.Environment
, pero es
Está diseñada para la asincrónica y agrega compatibilidad con subtareas simultáneas de forma lógica.3
El valor que se muestra de step
es otro StateMachine
, lo que permite la especificación
de una secuencia de pasos, de forma inductiva. step
muestra DONE
cuando el
Se completó StateMachine
. Por ejemplo:
class HelloWorld implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
System.out.println("hello");
return this::step2; // The next step is HelloWorld.step2.
}
private StateMachine step2(Tasks tasks) {
System.out.println("world");
// DONE is special value defined in the `StateMachine` interface signaling
// that the computation is done.
return DONE;
}
}
describe un StateMachine
con el siguiente resultado.
hello
world
Ten en cuenta que la referencia del método this::step2
también es StateMachine
debido a lo siguiente:
step2
según la definición de la interfaz funcional de StateMachine
. Método
las referencias son la forma más común de especificar el siguiente estado de una
StateMachine
De manera intuitiva, desglosar un cálculo en StateMachine
pasos, en lugar de un
monolithic, proporciona los hooks necesarios para suspender y reanudar una
el procesamiento. Cuando StateMachine.step
devuelve, hay una suspensión explícita.
punto. La continuación especificada por el valor StateMachine
devuelto es una
punto de reanudación explícito. Así se puede evitar el reprocesamiento
el procesamiento puede retomarse
exactamente donde lo dejó.
Devoluciones de llamada, continuaciones y procesamiento asíncrono
En términos técnicos, un StateMachine
funciona como una continuación que determina la
el procesamiento posterior que se ejecutará. En lugar de bloquear, un StateMachine
puede
suspender de forma voluntaria a partir de la función step
, que transfiere
control de vuelta a una instancia de Driver
. El Driver
puede
Luego, cambia a un StateMachine
listo o deja el control de Skyframe.
Tradicionalmente, las devoluciones de llamada y las continuaciones se combinan en un solo concepto.
Sin embargo, los objetos StateMachine
mantienen una distinción entre los dos.
- Devolución de llamada: Describe dónde almacenar el resultado de una llamada el procesamiento.
- Continuation: Especifica el siguiente estado de ejecución.
Se requieren devoluciones de llamada cuando se invoca una operación asíncrona, lo que significa que La operación real no ocurre inmediatamente después de que se llama al método, como el caso de una búsqueda de SkyValue. Las devoluciones de llamada deben ser lo más sencillas posible.
Las Continuations son los valores mostrados de StateMachine
de StateMachine
y
encapsula la ejecución compleja que sigue una vez que ocurre
de procesamiento resuelven. Este enfoque estructurado ayuda a mantener la complejidad
las devoluciones de llamada.
Tasks
La interfaz Tasks
proporciona elementos StateMachine
con una API para buscar SkyValues.
con SkyKey y para programar subtareas simultáneas.
interface Tasks {
void enqueue(StateMachine subtask);
void lookUp(SkyKey key, Consumer<SkyValue> sink);
<E extends Exception>
void lookUp(SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);
// lookUp overloads for 2 and 3 exception types exist, but are elided here.
}
Búsquedas de SkyValue
Los objetos StateMachine
usan sobrecargas de Tasks.lookUp
para buscar SkyValues. Son
análogo a SkyFunction.Environment.getValue
y
SkyFunction.Environment.getValueOrThrow
y tienen un control de excepciones similar
semántica. La implementación no realiza la búsqueda de inmediato, pero
en su lugar, organiza4 tantas búsquedas como sea posible antes de hacerlo. El valor
podría no estar disponible de inmediato, por ejemplo, si requieres
un reinicio de Skyframe,
para que el llamador especifique qué hacer con el valor resultante usando una devolución de llamada.
El procesador StateMachine
(Driver
y el modo puente a
SkyFrame) garantiza que el valor esté disponible antes de
comienza el siguiente estado. A continuación, se incluye un ejemplo.
class DoesLookup implements StateMachine, Consumer<SkyValue> {
private Value value;
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new Key(), (Consumer<SkyValue>) this);
return this::processValue;
}
// The `lookUp` call in `step` causes this to be called before `processValue`.
@Override // Implementation of Consumer<SkyValue>.
public void accept(SkyValue value) {
this.value = (Value)value;
}
private StateMachine processValue(Tasks tasks) {
System.out.println(value); // Prints the string representation of `value`.
return DONE;
}
}
En el ejemplo anterior, el primer paso realiza una búsqueda de new Key()
y pasa lo siguiente:
this
como consumidor. Esto es posible porque DoesLookup
implementa
Consumer<SkyValue>
Por contrato, antes de que comience el siguiente estado DoesLookup.processValue
, todos los
se completaron las búsquedas de DoesLookup.step
. Por lo tanto, value
está disponible cuando
se accede en processValue
.
Subtareas
Tasks.enqueue
solicita la ejecución de subtareas simultáneas de forma lógica.
Las subtareas también son StateMachine
y pueden realizar cualquier acción de StateMachine
normal
lo que puedes hacer, incluso crear más subtareas de forma recurrente o buscar SkyValues.
Al igual que con lookUp
, el controlador de la máquina de estado garantiza que todas las subtareas
completar antes de continuar con el siguiente paso. A continuación, se incluye un ejemplo.
class Subtasks implements StateMachine {
private int i = 0;
@Override
public StateMachine step(Tasks tasks) {
tasks.enqueue(new Subtask1());
tasks.enqueue(new Subtask2());
// The next step is Subtasks.processResults. It won't be called until both
// Subtask1 and Subtask 2 are complete.
return this::processResults;
}
private StateMachine processResults(Tasks tasks) {
System.out.println(i); // Prints "3".
return DONE; // Subtasks is done.
}
private class Subtask1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
i += 1;
return DONE; // Subtask1 is done.
}
}
private class Subtask2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
i += 2;
return DONE; // Subtask2 is done.
}
}
}
Aunque Subtask1
y Subtask2
son simultáneos de forma lógica, todo se ejecuta en un
un solo subproceso para que la función "concurrent" la actualización de i
no necesita
y la sincronización.
Simultaneidad estructurada
Dado que cada lookUp
y enqueue
deben resolverse antes de pasar a la siguiente
significa que la simultaneidad se limita naturalmente a las estructuras de árbol. Es
es posible crear simultaneidad5 jerárquica, como se muestra en
ejemplo.
Es difícil saber a partir del UML que la estructura de simultaneidad forma un árbol. Hay una vista alternativa que muestra mejor el estructura de árbol.
La simultaneidad estructurada es mucho más fácil de razonar.
Patrones de composición y flujo de control
En esta sección, se presentan ejemplos de cómo se pueden componer varios StateMachine
y soluciones a ciertos problemas
del flujo de control.
Estados secuenciales
Este es el patrón de flujo de control más común y directo. Un ejemplo de
esto se muestra en Cálculos con estado dentro
SkyKeyComputeState
Ramificación
Los estados de ramificación en StateMachine
se pueden lograr mostrando diferentes
con el flujo de control normal de Java, como se muestra en el siguiente ejemplo.
class Branch implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
// Returns different state machines, depending on condition.
if (shouldUseA()) {
return this::performA;
}
return this::performB;
}
…
}
Es muy común que ciertas ramas muestren DONE
para la finalización anticipada.
Composición secuencial avanzada
Como la estructura de control de StateMachine
no tiene memoria, se comparte StateMachine
las definiciones como subtareas a veces pueden ser incómodas. Deja que M1 y
M2 son instancias de StateMachine
que comparten un StateMachine
, S y
M1 y M2 son las secuencias <A, S, B> y
<X, S, Y> respectivamente. El problema es que S no sabe si debe
continúa en B o Y después de que se completa y los StateMachine
no mantienen un
en la pila de llamadas. En esta sección, se revisan algunas técnicas para lograrlo.
StateMachine
como elemento de secuencia de la terminal
Esto no resuelve el problema planteado inicial. Solo demuestra secuencias
composición cuando el StateMachine
compartido es la terminal en la secuencia.
// S is the shared state machine.
class S implements StateMachine { … }
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
return new S();
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
return new S();
}
}
Esto funciona incluso si S es en sí misma una máquina de estados compleja.
Subtarea para la composición secuencial
Dado que se garantiza que las subtareas en cola se completen antes del siguiente estado, es a veces es posible abusar levemente6 del mecanismo de subtareas.
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
// S starts after `step` returns and by contract must complete before `doB`
// begins. It is effectively sequential, inducing the sequence < A, S, B >.
tasks.enqueue(new S());
return this::doB;
}
private StateMachine doB(Tasks tasks) {
performB();
return DONE;
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
// Similarly, this induces the sequence < X, S, Y>.
tasks.enqueue(new S());
return this::doY;
}
private StateMachine doY(Tasks tasks) {
performY();
return DONE;
}
}
Inyección de runAfter
A veces, es imposible abusar de Tasks.enqueue
porque hay otros
subtareas paralelas o llamadas de Tasks.lookUp
que deben completarse antes del S
que se ejecute. En este caso, puedes insertar un parámetro runAfter
en S para
informar a S qué hacer a continuación.
class S implements StateMachine {
// Specifies what to run after S completes.
private final StateMachine runAfter;
@Override
public StateMachine step(Tasks tasks) {
… // Performs some computations.
return this::processResults;
}
@Nullable
private StateMachine processResults(Tasks tasks) {
… // Does some additional processing.
// Executes the state machine defined by `runAfter` after S completes.
return runAfter;
}
}
class M1 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performA();
// Passes `this::doB` as the `runAfter` parameter of S, resulting in the
// sequence < A, S, B >.
return new S(/* runAfter= */ this::doB);
}
private StateMachine doB(Tasks tasks) {
performB();
return DONE;
}
}
class M2 implements StateMachine {
@Override
public StateMachine step(Tasks tasks) {
performX();
// Passes `this::doY` as the `runAfter` parameter of S, resulting in the
// sequence < X, S, Y >.
return new S(/* runAfter= */ this::doY);
}
private StateMachine doY(Tasks tasks) {
performY();
return DONE;
}
}
Este enfoque es más limpio que abusar de subtareas. Sin embargo, si aplicamos esto también
liberalmente, por ejemplo, al anidar varios StateMachine
con runAfter
, se
la ruta al infierno de devolución de llamada. Es mejor dividir las secuencias
En su lugar, objetos runAfter
con estados secuenciales ordinarios.
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
se puede reemplazar por lo siguiente.
private StateMachine step1(Tasks tasks) {
doStep1();
return new S(/* runAfter= */ this::intermediateStep);
}
private StateMachine intermediateStep(Tasks tasks) {
return new T(/* runAfter= */ this::nextStep);
}
Alternativa Prohibida: runAfterUnlessError
En un borrador anterior, habíamos considerado un runAfterUnlessError
que se anularía.
los errores desde el principio. Esto fue motivado por el hecho
de que los errores a menudo terminan
dos veces, una por la StateMachine
que tiene una referencia runAfter
y
una vez por la propia máquina runAfter
.
Después de un poco de deliberación, decidimos que la uniformidad del código es más
importante que anular
la comprobación de errores. Sería confuso si el
El mecanismo runAfter
no funcionaba de manera consistente con el
Mecanismo tasks.enqueue
, que siempre requiere comprobación de errores.
Delegación directa
Cada vez que hay una transición de estado formal, el bucle Driver
principal avanza.
Según el contrato, el avance de los estados significa que todos los SkyValue que se colocaron en cola anteriormente
las búsquedas y subtareas se resuelven antes de que se ejecute el siguiente estado. A veces, la lógica
de un delegado StateMachine
hace que un avance de fase sea innecesario o
puede ser contraproducente. Por ejemplo, si el primer step
del delegado realiza
Búsquedas de SkyKey que se pueden paralelizar con búsquedas del estado de delegación
entonces un avance de fase los volvería secuenciales. Podría tener más sentido
realizar la delegación directa, como se muestra en el siguiente ejemplo.
class Parent implements StateMachine {
@Override
public StateMachine step(Tasks tasks ) {
tasks.lookUp(new Key1(), this);
// Directly delegates to `Delegate`.
//
// The (valid) alternative:
// return new Delegate(this::afterDelegation);
// would cause `Delegate.step` to execute after `step` completes which would
// cause lookups of `Key1` and `Key2` to be sequential instead of parallel.
return new Delegate(this::afterDelegation).step(tasks);
}
private StateMachine afterDelegation(Tasks tasks) {
…
}
}
class Delegate implements StateMachine {
private final StateMachine runAfter;
Delegate(StateMachine runAfter) {
this.runAfter = runAfter;
}
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new Key2(), this);
return …;
}
// Rest of implementation.
…
private StateMachine complete(Tasks tasks) {
…
return runAfter;
}
}
Flujo de datos
El enfoque del debate anterior ha estado en la administración del flujo de control. Esta describe la propagación de los valores de datos.
Cómo implementar devoluciones de llamada de Tasks.lookUp
Hay un ejemplo de cómo implementar una devolución de llamada Tasks.lookUp
en SkyValue.
búsquedas. En esta sección, se proporcionan los motivos y se sugiere
para manejar varios SkyValues.
Tasks.lookUp
devoluciones de llamada
El método Tasks.lookUp
toma una devolución de llamada, sink
, como parámetro.
void lookUp(SkyKey key, Consumer<SkyValue> sink);
El enfoque idiomático sería usar una lambda de Java para implementar lo siguiente:
tasks.lookUp(key, value -> myValue = (MyValueClass)value);
myValue
es una variable de miembro de la instancia StateMachine
que realiza lo siguiente:
búsqueda. Sin embargo, la lambda requiere una asignación de memoria adicional en comparación con
Implementa la interfaz Consumer<SkyValue>
en StateMachine
para implementarlos. La lambda sigue siendo útil
cuando hay varias búsquedas
sería ambiguo.
También hay sobrecargas de manejo de errores de Tasks.lookUp
, que son análogas a
SkyFunction.Environment.getValueOrThrow
<E extends Exception> void lookUp(
SkyKey key, Class<E> exceptionClass, ValueOrExceptionSink<E> sink);
interface ValueOrExceptionSink<E extends Exception> {
void acceptValueOrException(@Nullable SkyValue value, @Nullable E exception);
}
A continuación, se muestra un ejemplo de implementación.
class PerformLookupWithError extends StateMachine, ValueOrExceptionSink<MyException> {
private MyValue value;
private MyException error;
@Override
public StateMachine step(Tasks tasks) {
tasks.lookUp(new MyKey(), MyException.class, ValueOrExceptionSink<MyException>) this);
return this::processResult;
}
@Override
public acceptValueOrException(@Nullable SkyValue value, @Nullable MyException exception) {
if (value != null) {
this.value = (MyValue)value;
return;
}
if (exception != null) {
this.error = exception;
return;
}
throw new IllegalArgumentException("Both parameters were unexpectedly null.");
}
private StateMachine processResult(Tasks tasks) {
if (exception != null) {
// Handles the error.
…
return DONE;
}
// Processes `value`, which is non-null.
…
}
}
Al igual que con las búsquedas sin manejo de errores, tener la clase StateMachine
directamente
implementar la devolución de llamada guarda una asignación de memoria para la lamba.
El manejo de errores proporciona más detalles, pero, en esencia, no hay mucha diferencia entre la propagación de los errores y los valores normales.
Cómo consumir varios SkyValues
A menudo, se requieren varias búsquedas de SkyValue. Un enfoque que funciona gran parte del es cambiar el tipo de SkyValue. El siguiente es un ejemplo que tiene se simplificó a partir del código de producción del prototipo.
@Nullable
private StateMachine fetchConfigurationAndPackage(Tasks tasks) {
var configurationKey = configuredTarget.getConfigurationKey();
if (configurationKey != null) {
tasks.lookUp(configurationKey, (Consumer<SkyValue>) this);
}
var packageId = configuredTarget.getLabel().getPackageIdentifier();
tasks.lookUp(PackageValue.key(packageId), (Consumer<SkyValue>) this);
return this::constructResult;
}
@Override // Implementation of `Consumer<SkyValue>`.
public void accept(SkyValue value) {
if (value instanceof BuildConfigurationValue) {
this.configurationValue = (BuildConfigurationValue) value;
return;
}
if (value instanceof PackageValue) {
this.pkg = ((PackageValue) value).getPackage();
return;
}
throw new IllegalArgumentException("unexpected value: " + value);
}
La implementación de devolución de llamada Consumer<SkyValue>
se puede compartir sin ambigüedades.
porque los tipos de valores son diferentes. Cuando ese no sea el caso, recurrir
implementaciones basadas en lambda o instancias completas de clase interna que implementan el
devoluciones de llamada adecuadas es viable.
Propaga valores entre StateMachine
s
Hasta ahora, en este documento solo se explicó cómo organizar el trabajo en una subtarea, pero las subtareas también deben informar los valores al emisor. Dado que las subtareas son lógicamente asíncronos, sus resultados se comunican al emisor mediante el uso de una devolución de llamada. Para hacer esto, la subtarea define una interfaz de receptor que es insertada a través de su constructor.
class BarProducer implements StateMachine {
// Callers of BarProducer implement the following interface to accept its
// results. Exactly one of the two methods will be called by the time
// BarProducer completes.
interface ResultSink {
void acceptBarValue(Bar value);
void acceptBarError(BarException exception);
}
private final ResultSink sink;
BarProducer(ResultSink sink) {
this.sink = sink;
}
… // StateMachine steps that end with this::complete.
private StateMachine complete(Tasks tasks) {
if (hasError()) {
sink.acceptBarError(getError());
return DONE;
}
sink.acceptBarValue(getValue());
return DONE;
}
}
Un llamador StateMachine
debería verse de la siguiente manera.
class Caller implements StateMachine, BarProducer.ResultSink {
interface ResultSink {
void acceptCallerValue(Bar value);
void acceptCallerError(BarException error);
}
private final ResultSink sink;
private Bar value;
Caller(ResultSink sink) {
this.sink = sink;
}
@Override
@Nullable
public StateMachine step(Tasks tasks) {
tasks.enqueue(new BarProducer((BarProducer.ResultSink) this));
return this::processResult;
}
@Override
public void acceptBarValue(Bar value) {
this.value = value;
}
@Override
public void acceptBarError(BarException error) {
sink.acceptCallerError(error);
}
private StateMachine processResult(Tasks tasks) {
// Since all enqueued subtasks resolve before `processResult` starts, one of
// the `BarResultSink` callbacks must have been called by this point.
if (value == null) {
return DONE; // There was a previously reported error.
}
var finalResult = computeResult(value);
sink.acceptCallerValue(finalResult);
return DONE;
}
}
En el ejemplo anterior, se demuestran algunas cosas. Caller
debe propagar su
y define su propio Caller.ResultSink
. Caller
implementa
BarProducer.ResultSink
de devoluciones de llamada. Después de la reanudación, processResult
verifica si
value
es nulo para determinar si se produjo un error. Este es un comportamiento habitual
después de aceptar el resultado de una subtarea o una búsqueda de SkyValue.
Ten en cuenta que la implementación de acceptBarError
reenvía el resultado con anticipación a
Caller.ResultSink
, como lo requiere Error bubbble.
Las alternativas para StateMachine
de nivel superior se describen en Driver
y
el modo puente con SkyFunctions.
Manejo de errores
Hay un par de ejemplos de manejo de errores que ya están en Tasks.lookUp
de devoluciones de llamada y Propaga valores entre
StateMachines
Excepciones a las
No se arrojan InterruptedException
, sino que se pasan por
las devoluciones de llamada como valores. Estas devoluciones de llamada a menudo tienen semánticas exclusivas o semánticas, con
exactamente uno de un valor o error que se está pasando.
En la siguiente sección, se describe una interacción sutil pero importante con Skyframe y el manejo de errores.
Error de burbuja (--nokeep_going)
Durante la burbuja de error, es posible que se reinicie una SkyFunction incluso si no se solicitaron todas
SkyValues está disponible. En tales casos, el estado posterior nunca será
se alcanzó debido al contrato de la API de Tasks
. Sin embargo, StateMachine
debe
propagarán la excepción.
Dado que la propagación debe ocurrir independientemente de si se alcanza el siguiente estado,
la devolución de llamada de manejo de errores debe realizar esta tarea. Para un StateMachine
interno,
esto se logra invocando la devolución de llamada superior.
En el StateMachine
de nivel superior, que interactúa con SkyFunction, esto puede
llamando al método setException
de ValueOrExceptionProducer
.
ValueOrExceptionProducer.tryProduceValue
arrojará la excepción, incluso
si faltan SkyValues.
Si se usa directamente un Driver
, es esencial que verifiques
errores propagados desde SkyFunction, incluso si la máquina no ha finalizado
el procesamiento de datos.
Control de eventos
En el caso de SkyFunctions que necesitan emitir eventos, se inserta un StoredEventHandler
.
en SkyKeyComputeState y se las inserta en StateMachine
que requieren
de ellos. Históricamente, se necesitaba StoredEventHandler
debido a la caída de Skyframe
ciertos eventos, a menos que se vuelvan a reproducir, pero esto se solucionó posteriormente.
La inyección de StoredEventHandler
se conserva porque simplifica la
implementación de eventos emitidos a partir de devoluciones de llamada de manejo de errores.
Driver
y el modo puente a SkyFunctions
Un Driver
es responsable de administrar la ejecución de las StateMachine
.
que comienza con una raíz especificada StateMachine
. Como pueden hacer los StateMachine
Colocar StateMachine
subtareas en cola de forma recursiva; una sola Driver
puede administrar
varias subtareas. Estas subtareas crean una estructura de árbol, un resultado de
Simultaneidad estructurada. El Driver
agrupa SkyValue en lotes
y búsquedas en subtareas para mejorar la eficiencia.
Hay una serie de clases compiladas en torno a Driver
con la siguiente API.
public final class Driver {
public Driver(StateMachine root);
public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}
Driver
toma una raíz única StateMachine
como parámetro. Llamando
Driver.drive
ejecuta StateMachine
todo lo que puede sin una
Se reinició Skyframe. El resultado es verdadero cuando se completa StateMachine
y falso
de lo contrario, lo que indica que no todos los valores estaban disponibles.
Driver
mantiene el estado simultáneo de StateMachine
y está bien.
adecuado para incorporarse en SkyKeyComputeState
.
Crea una instancia de Driver
directamente
Por lo general, las implementaciones de StateMachine
comunican sus resultados a través de
devoluciones de llamada. Es posible crear directamente una instancia de Driver
, como se muestra en el
siguiente ejemplo.
El Driver
está incorporado en la implementación de SkyKeyComputeState
junto con
una implementación del ResultSink
correspondiente que se definirá un poco más
fuera de servicio. En el nivel superior, el objeto State
es un receptor apropiado del
resultado del procesamiento, ya que se garantiza que sobreviva a Driver
.
class State implements SkyKeyComputeState, ResultProducer.ResultSink {
// The `Driver` instance, containing the full tree of all `StateMachine`
// states. Responsible for calling `StateMachine.step` implementations when
// asynchronous values are available and performing batched SkyFrame lookups.
//
// Non-null while `result` is being computed.
private Driver resultProducer;
// Variable for storing the result of the `StateMachine`
//
// Will be non-null after the computation completes.
//
private ResultType result;
// Implements `ResultProducer.ResultSink`.
//
// `ResultProducer` propagates its final value through a callback that is
// implemented here.
@Override
public void acceptResult(ResultType result) {
this.result = result;
}
}
En el siguiente código, se esboza el ResultProducer
.
class ResultProducer implements StateMachine {
interface ResultSink {
void acceptResult(ResultType value);
}
private final Parameters parameters;
private final ResultSink sink;
… // Other internal state.
ResultProducer(Parameters parameters, ResultSink sink) {
this.parameters = parameters;
this.sink = sink;
}
@Override
public StateMachine step(Tasks tasks) {
… // Implementation.
return this::complete;
}
private StateMachine complete(Tasks tasks) {
sink.acceptResult(getResult());
return DONE;
}
}
Entonces, el código para calcular de forma diferida el resultado podría verse de la siguiente manera.
@Nullable
private Result computeResult(State state, Skyfunction.Environment env)
throws InterruptedException {
if (state.result != null) {
return state.result;
}
if (state.resultProducer == null) {
state.resultProducer = new Driver(new ResultProducer(
new Parameters(), (ResultProducer.ResultSink)state));
}
if (state.resultProducer.drive(env)) {
// Clears the `Driver` instance as it is no longer needed.
state.resultProducer = null;
}
return state.result;
}
Incorporando Driver
Si StateMachine
produce un valor y no genera excepciones, la incorporación
Driver
es otra implementación posible, como se muestra en el siguiente ejemplo.
class ResultProducer implements StateMachine {
private final Parameters parameters;
private final Driver driver;
private ResultType result;
ResultProducer(Parameters parameters) {
this.parameters = parameters;
this.driver = new Driver(this);
}
@Nullable // Null when a Skyframe restart is needed.
public ResultType tryProduceValue( SkyFunction.Environment env)
throws InterruptedException {
if (!driver.drive(env)) {
return null;
}
return result;
}
@Override
public StateMachine step(Tasks tasks) {
… // Implementation.
}
La SkyFunction puede tener un código como el siguiente (donde State
es
el tipo específico de función de SkyKeyComputeState
).
@Nullable // Null when a Skyframe restart is needed.
Result computeResult(SkyFunction.Environment env, State state)
throws InterruptedException {
if (state.result != null) {
return state.result;
}
if (state.resultProducer == null) {
state.resultProducer = new ResultProducer(new Parameters());
}
var result = state.resultProducer.tryProduceValue(env);
if (result == null) {
return null;
}
state.resultProducer = null;
return state.result = result;
}
Incorporar Driver
en la implementación de StateMachine
es una mejor opción para
Estilo de programación síncrono de Skyframe.
StateMachines que pueden producir excepciones
De lo contrario, hay ValueOrExceptionProducer
que se pueden incorporar con SkyKeyComputeState
.
y ValueOrException2Producer
que tienen APIs síncronas para coincidir
código síncrono de SkyFunction.
La clase abstracta ValueOrExceptionProducer
incluye los siguientes métodos.
public abstract class ValueOrExceptionProducer<V, E extends Exception>
implements StateMachine {
@Nullable
public final V tryProduceValue(Environment env)
throws InterruptedException, E {
… // Implementation.
}
protected final void setValue(V value) { … // Implementation. }
protected final void setException(E exception) { … // Implementation. }
}
Incluye una instancia Driver
incorporada y se parece mucho a la
Clase ResultProducer
en las interfaces y el controlador de incorporaciones
con la SkyFunction de manera similar. En lugar de definir un ResultSink
,
Las implementaciones llaman a setValue
o setException
cuando se produce alguna de estas situaciones.
Cuando se producen ambos casos, la excepción tiene prioridad. El método tryProduceValue
conecta el código de devolución de llamada asíncrono con el código síncrono y arroja un
de error cuando se establece uno.
Como se señaló antes, durante la burbuja de errores, es posible que ocurra un error.
incluso si la máquina aún no está lista
porque no todas las entradas están disponibles. Para
tryProduceValue
arroja las excepciones establecidas, incluso antes de que
de la máquina virtual haya finalizado.
Epílogo: Con el tiempo, se quitan las devoluciones de llamada
Los elementos StateMachine
son una forma de tener un rendimiento muy eficiente, pero con un uso intensivo de código estándar.
procesamiento asíncrono. Continuaciones (particularmente en forma de Runnable
)
pasados a ListenableFuture
) son generalizados en ciertas partes del código de Bazel.
pero que no son frecuentes
en el análisis de SkyFunctions. El análisis se basa, en su mayoría, en la CPU
no hay APIs asíncronas eficientes para E/S de disco. Con el tiempo, sería
es bueno optimizar las devoluciones de llamada de ausencia, ya que tienen una curva de aprendizaje e impiden
la legibilidad.
Una de las alternativas más prometedoras son los subprocesos virtuales de Java. En lugar de
tener que escribir devoluciones de llamada, todo se reemplaza por síncrono, lo que bloquea
llamadas. Esto es posible porque vinculas un recurso de subproceso virtual, a diferencia de
de la plataforma de prueba
se supone que es económico. Sin embargo, incluso con subprocesos virtuales,
Reemplaza las operaciones síncronas simples por creación y sincronización de subprocesos
primitivas es demasiado costosa. Realizamos una migración de StateMachine
a
subprocesos virtuales de Java y eran órdenes de magnitud más lentos, lo que llevaba a una
casi triplicó la latencia del análisis de extremo a extremo. Como los subprocesos virtuales son
todavía es una función de vista previa, es posible que esta migración se pueda realizar
más adelante cuando mejore el rendimiento.
Otro enfoque que debes considerar es esperar las corrutinas de Loom, si alguna vez que estén disponibles. La ventaja es que podrías reducir la sobrecarga de sincronización mediante el uso cooperativo de tareas múltiples.
Si todo lo demás falla, la reescritura de código de bytes de bajo nivel también podría ser una opción alternativa. Con una optimización suficiente, es posible lograr que se aproxima al código de devolución de llamada escrito a mano.
Apéndice
Infierno de devolución de llamada
El infierno de las devoluciones de llamadas es un problema infame en el código asíncrono que usa devoluciones de llamada. Deriva del hecho de que la continuación de un paso siguiente está anidada en el paso anterior. Si hay muchos pasos, este anidado puede ser profundas. Si se combina con el flujo de control, el código se vuelve inmanejable.
class CallbackHell implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return (t, l) -> {
doB();
return (t1, l2) -> {
doC();
return DONE;
};
};
}
}
Una de las ventajas de las implementaciones anidadas es que el marco de pila de la el paso externo se puede preservar. En Java, las variables lambda capturadas se deben de manera efectiva, por lo que usar esas variables puede resultar engorroso. El anidamiento profundo es evitando mostrando referencias de métodos como continuaciones en lugar de lambdas como como se muestra a continuación.
class CallbackHellAvoided implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return this::step2;
}
private StateMachine step2(Tasks tasks) {
doB();
return this::step3;
}
private StateMachine step3(Tasks tasks) {
doC();
return DONE;
}
}
También puede ocurrir un infierno de devolución de llamada si la inyección runAfter
este patrón se usa demasiado, pero se puede evitar con inyecciones intercaladas
con pasos secuenciales.
Ejemplo: Búsquedas de SkyValue encadenadas
Suele suceder que la lógica de la aplicación requiera cadenas dependientes Búsquedas de SkyValue, por ejemplo, si una segunda SkyKey depende del primer SkyValue Si se piensa en esto de forma ingenua, daría como resultado un entorno complejo de devolución de llamada.
private ValueType1 value1;
private ValueType2 value2;
private StateMachine step1(...) {
tasks.lookUp(key1, (Consumer<SkyValue>) this); // key1 has type KeyType1.
return this::step2;
}
@Override
public void accept(SkyValue value) {
this.value1 = (ValueType1) value;
}
private StateMachine step2(...) {
KeyType2 key2 = computeKey(value1);
tasks.lookup(key2, this::acceptValueType2);
return this::step3;
}
private void acceptValueType2(SkyValue value) {
this.value2 = (ValueType2) value;
}
Sin embargo, dado que las continuaciones se especifican como referencias de métodos, el código parece
procedimental entre transiciones de estado: step2
sigue a step1
. Ten en cuenta que aquí
se usa lambda para asignar value2
. Esto hace que el orden del código coincida con
de procesamiento de arriba a abajo.
Sugerencias varias
Legibilidad: orden de la ejecución
Para mejorar la legibilidad, esfuérzate por mantener las implementaciones de StateMachine.step
en orden de ejecución e implementaciones de devolución de llamada inmediatamente después de dónde
se pasan en el código. Esto no siempre es posible cuando el flujo de control
ramas. Los comentarios adicionales pueden ser útiles en esos casos.
En Example: Chained SkyValue lookups, se muestra un se crea una referencia de método intermedio para lograr esto. Esto intercambia un pequeño el rendimiento para una mayor legibilidad, lo que probablemente valga la pena.
Hipótesis generacional
Los objetos Java de duración media rompen la hipótesis generacional de Java.
de elementos no utilizados, diseñado para manejar objetos que viven durante
poco tiempo u objetos que permanecen para siempre. Por definición, los objetos en
SkyKeyComputeState
infringe esta hipótesis. Dichos objetos, que contienen las
árbol construido de todos los StateMachine
que aún están en ejecución y que tienen la raíz en Driver
tienen
una vida útil intermedia, ya que se suspenden, a la espera de procesamientos asíncronos
en completarse.
Parece menos malo en JDK19, pero al usar StateMachine
, a veces
es posible observar un aumento en el tiempo de GC, incluso con disminuciones drásticas en
por los elementos no utilizados reales. Dado que los StateMachine
tienen una vida útil intermedia
podrían pasar a la antigua generación, lo que haría que se llene más rápido,
con GC mayores o completas más costosas para la limpieza.
La precaución inicial es minimizar el uso de variables StateMachine
, pero
no siempre es factible, por ejemplo, si se necesita un valor en varios
estados. Cuando es posible, las variables step
de la pila local son de generación joven.
variables y GC eficientemente.
Para las variables StateMachine
, dividirlo en subtareas y seguir
el patrón recomendado para Propagar valores entre
StateMachine
s también es útil. Observa que cuando
que siguen el patrón, solo los elementos StateMachine
secundarios tienen referencias al elemento superior.
StateMachine
y no al revés. Esto significa que,
a medida que los niños completen y
los elementos superiores con devoluciones de llamada de resultados, los elementos secundarios se eliminan naturalmente
el alcance y ser apto para GC.
Por último, en algunos casos, se necesita una variable StateMachine
en estados anteriores.
pero no en estados posteriores. Puede ser beneficioso anular referencias de modelos
una vez que se sabe que ya no son necesarios.
Estados de nombres
Cuando se nombra un método, por lo general, es posible nombrar un método para el comportamiento
que se lleva a cabo en ese método. No está tan claro cómo hacer esto en
StateMachine
porque no hay una pila. Por ejemplo, supongamos que el método foo
llama a un submétodo bar
. En un StateMachine
, esto se podría traducir al
la secuencia de estado foo
seguida de bar
. foo
ya no incluye el comportamiento
bar
Como resultado, los nombres de métodos para los estados suelen tener un alcance más limitado,
lo que podría reflejar
el comportamiento local.
Diagrama de árbol de simultaneidad
La siguiente es una vista alternativa del diagrama en Estructurados simultaneidad que describe mejor la estructura de árbol. Los bloques forman un pequeño árbol.
-
En contraste con la convención de Skyframe de reiniciar desde el principio, cuando no están disponibles. ↩
-
Ten en cuenta que
step
puede arrojarInterruptedException
, pero el los ejemplos omiten esto. Hay algunos métodos bajos en el código de Bazel que arrojan esta excepción y se propaga hasta elDriver
, que se describirá más adelante, que ejecutaStateMachine
. Es correcto no declarar que se arroje cuando innecesariamente.↩ -
Las subtareas simultáneas fueron motivadas por la
ConfiguredTargetFunction
, que realiza un trabajo independiente para cada dependencia. En lugar de manipular con estructuras de datos complejas que procesan todas las dependencias a la vez lo que introduce ineficiencias, cada dependencia tiene su propia infraestructuraStateMachine
↩ -
Varias llamadas
tasks.lookUp
en un solo paso se agrupan en lotes. Se pueden crear lotes adicionales a través de las búsquedas que se producen dentro del subtareas. ↩ -
Esto es conceptualmente similar a la simultaneidad estructurada de Java. jeps/428. ↩
-
Esto es similar a generar un subproceso y unirlo para lograr composición secuencial. ↩