Ringkasan
StateMachine
Skyframe adalah objek fungsi yang diuraikan yang berada di
heap. Vertex AI mendukung fleksibilitas dan evaluasi tanpa redundansi1 saat nilai yang diperlukan tidak segera tersedia, tetapi dikomputasi secara asinkron. StateMachine
tidak dapat mengikat resource thread selagi menunggu, tetapi harus
ditangguhkan dan dilanjutkan. Dekonstruksi ini mengekspos titik entri ulang yang eksplisit sehingga komputasi sebelumnya dapat dilewati.
StateMachine
dapat digunakan untuk mengekspresikan urutan, percabangan, konkurensi logis yang terstruktur, dan disesuaikan secara khusus untuk interaksi Skyframe. StateMachine
dapat dikomposisi menjadi StateMachine
yang lebih besar dan membagikan sub-StateMachine
. Konkurensi selalu hierarkis berdasarkan konstruksi dan murni logis. Setiap subtugas serentak berjalan di satu thread SkyFunction induk bersama.
Pengantar
Bagian ini secara singkat memotivasi dan memperkenalkan StateMachine
, yang ada dalam
paket
java.com.google.devtools.build.skyframe.state
.
Pengenalan singkat tentang memulai ulang Skyframe
Skyframe adalah kerangka kerja yang melakukan evaluasi paralel atas grafik dependensi.
Setiap node dalam grafik sesuai dengan evaluasi SkyFunction dengan SkyKey yang menentukan parameternya, dan SkyValue yang menentukan hasilnya. Model
komputasi sedemikian rupa sehingga SkyFunction dapat mencari SkyValues melalui SkyKey,
yang memicu evaluasi paralel rekursif untuk SkyFunction tambahan. Alih-alih
memblokir, yang akan mengikat thread, saat SkyValue yang diminta belum
siap karena beberapa subgrafik komputasi tidak lengkap, SkyFunction yang meminta
mengamati respons null
getValue
dan seharusnya menampilkan null
,
bukan SkyValue, yang menandakan bahwa SkyValue tidak lengkap karena input yang tidak ada.
Skyframe memulai ulang SkyFunctions saat semua SkyValues yang diminta sebelumnya
tersedia.
Sebelum diperkenalkannya SkyKeyComputeState
, cara tradisional untuk menangani
mulai ulang adalah dengan menjalankan ulang komputasi sepenuhnya. Meskipun memiliki kerumitan kuadrat, fungsi yang ditulis dengan cara ini pada akhirnya akan selesai karena setiap dijalankan ulang, lebih sedikit pencarian yang menampilkan null
. Dengan SkyKeyComputeState
, Anda dapat mengaitkan data check-point yang ditentukan secara manual dengan SkyFunction, sehingga menghemat perhitungan ulang yang signifikan.
StateMachine
adalah objek yang ada di dalam SkyKeyComputeState
dan secara virtual menghilangkan
semua penghitungan ulang saat SkyFunction dimulai ulang (dengan asumsi bahwa
SkyKeyComputeState
tidak keluar dari cache) dengan mengekspos hook eksekusi
penangguhan dan lanjutkan.
Komputasi stateful di dalam SkyKeyComputeState
Dari sudut pandang desain berorientasi objek, sebaiknya pertimbangkan untuk menyimpan
objek komputasi di dalam SkyKeyComputeState
, bukan nilai data murni.
Di Java, deskripsi minimum dari perilaku yang membawa objek adalah antarmuka fungsional dan ternyata sudah cukup. StateMachine
memiliki definisi berikut2 yang rekursif dan aneh.
@FunctionalInterface
public interface StateMachine {
StateMachine step(Tasks tasks) throws InterruptedException;
}
Antarmuka Tasks
setara dengan SkyFunction.Environment
, tetapi
dirancang untuk asinkron dan menambahkan dukungan untuk subtugas serentak secara logis3.
Nilai yang ditampilkan step
adalah StateMachine
lain, yang memungkinkan spesifikasi
urutan langkah, secara induktif. step
menampilkan DONE
saat
StateMachine
selesai. Contoh:
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;
}
}
menjelaskan StateMachine
dengan output berikut.
hello
world
Perhatikan bahwa referensi metode this::step2
juga merupakan StateMachine
karena
step2
memenuhi definisi antarmuka fungsional StateMachine
. Referensi
metode adalah cara paling umum untuk menentukan status berikutnya dalam
StateMachine
.
Secara intuitif, memecah komputasi menjadi langkah-langkah StateMachine
, bukan
fungsi monolitik, akan memberikan hook yang diperlukan untuk suspend dan suspend
komputasi. Saat StateMachine.step
ditampilkan, ada titik penangguhan
eksplisit. Kelanjutan yang ditentukan oleh nilai StateMachine
yang ditampilkan adalah
titik resume yang eksplisit. Dengan demikian, komputasi ulang dapat dihindari karena
komputasi dapat dilanjutkan tepat dari tempat terakhirnya.
Callback, kelanjutan, dan komputasi asinkron
Dalam istilah teknis, StateMachine
berfungsi sebagai continuation, yang menentukan
komputasi berikutnya yang akan dijalankan. Daripada memblokir, StateMachine
dapat
suspend secara sukarela dengan kembali dari fungsi step
, yang mentransfer
kontrol kembali ke instance Driver
. Kemudian, Driver
dapat
beralih ke StateMachine
yang siap atau melepaskan kontrol kembali ke Skyframe.
Biasanya, callbacks dan callbacks digabungkan menjadi satu konsep.
Namun, StateMachine
mempertahankan perbedaan di antara keduanya.
- Callback - menjelaskan tempat menyimpan hasil komputasi asinkron.
- Lanjutan - menentukan status eksekusi berikutnya.
Callback diperlukan saat memanggil operasi asinkron, yang berarti bahwa operasi sebenarnya tidak akan terjadi secara langsung setelah memanggil metode, seperti dalam kasus pencarian SkyValue. Callback harus dibuat sesederhana mungkin.
Lanjutan adalah nilai StateMachine
yang ditampilkan dari StateMachine
dan
mengenkapsulasi eksekusi kompleks yang mengikutinya setelah semua komputasi
asinkron diselesaikan. Pendekatan terstruktur ini membantu menjaga kompleksitas
callback tetap dapat dikelola.
Tugas
Antarmuka Tasks
menyediakan StateMachine
dengan API untuk mencari SkyValues
melalui SkyKey dan menjadwalkan subtugas serentak.
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.
}
Pencarian SkyValue
StateMachine
menggunakan overload Tasks.lookUp
untuk mencari SkyValues. Keduanya
mirip dengan SkyFunction.Environment.getValue
dan
SkyFunction.Environment.getValueOrThrow
, serta memiliki semantik penanganan pengecualian
yang serupa. Implementasi tersebut tidak langsung melakukan pencarian, tetapi
akan mengelompokkan4 pencarian sebanyak mungkin sebelum melakukannya. Nilai ini
mungkin tidak segera tersedia, misalnya, memerlukan mulai ulang Skyframe,
sehingga pemanggil menentukan tindakan yang harus dilakukan dengan nilai yang dihasilkan menggunakan callback.
Prosesor StateMachine
(Driver
dan penghubungan ke
SkyFrame) menjamin bahwa nilai tersebut tersedia sebelum
status berikutnya dimulai. Contohnya sebagai berikut.
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;
}
}
Pada contoh di atas, langkah pertama melakukan pencarian untuk new Key()
, dengan meneruskan
this
sebagai konsumen. Hal ini memungkinkan karena DoesLookup
mengimplementasikan
Consumer<SkyValue>
.
Berdasarkan kontrak, sebelum DoesLookup.processValue
status berikutnya dimulai, semua
pencarian DoesLookup.step
selesai. Oleh karena itu, value
tersedia saat
diakses di processValue
.
Subtugas
Tasks.enqueue
meminta eksekusi subtugas serentak secara logis.
Subtugas juga merupakan StateMachine
dan dapat melakukan apa pun yang dapat dilakukan StateMachine
reguler, termasuk membuat lebih banyak subtugas secara rekursif atau mencari SkyValues.
Mirip dengan lookUp
, driver mesin status memastikan bahwa semua subtugas
sudah selesai sebelum melanjutkan ke langkah berikutnya. Contohnya sebagai berikut.
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.
}
}
}
Meskipun Subtask1
dan Subtask2
secara logis serentak, semuanya berjalan dalam
satu thread sehingga update i
"serentak" tidak memerlukan
sinkronisasi apa pun.
Konkurensi terstruktur
Karena setiap lookUp
dan enqueue
harus di-resolve sebelum berlanjut ke status berikutnya, artinya konkurensi secara alami terbatas pada struktur hierarki. Anda
dapat membuat konkurensi 5 hierarkis seperti yang ditunjukkan pada contoh
berikut.
Sulit untuk membedakan dari UML bahwa struktur konkurensi membentuk hierarki. Ada tampilan alternatif yang menampilkan struktur hierarki dengan lebih baik.
Konkurensi terstruktur jauh lebih mudah untuk dipertimbangkan.
Pola aliran komposisi dan kontrol
Bagian ini menampilkan contoh cara menyusun beberapa StateMachine
dan solusi untuk masalah alur kontrol tertentu.
Status berurutan
Ini adalah pola alur kontrol yang paling umum dan mudah. Contoh
hal ini ditunjukkan dalam Komputasi stateful di dalam
SkyKeyComputeState
.
Percabangan
Status percabangan di StateMachine
dapat dicapai dengan menampilkan nilai yang berbeda
menggunakan alur kontrol Java reguler, seperti ditunjukkan dalam contoh berikut.
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;
}
…
}
Sangat umum bagi cabang tertentu untuk menampilkan DONE
, untuk penyelesaian lebih awal.
Komposisi berurutan lanjutan
Karena struktur kontrol StateMachine
tidak memiliki memori, berbagi definisi StateMachine
sebagai subtugas terkadang dapat terasa canggung. Misalkan M1 dan M2 adalah instance StateMachine
yang berbagi StateMachine
, S, dengan M1 dan M2 adalah urutan <A, S, B> dan <X, S, Y>. Masalahnya adalah S tidak tahu apakah akan
melanjutkan ke B atau Y setelah selesai dan StateMachine
tidak bisa menyimpan
stack panggilan. Bagian ini meninjau beberapa teknik untuk mencapai hal ini.
StateMachine
sebagai elemen urutan terminal
Hal ini tidak menyelesaikan masalah awal yang ditimbulkan. Class ini hanya menunjukkan komposisi
berurutan jika StateMachine
bersama adalah terminal dalam urutan.
// 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();
}
}
Metode ini berfungsi meskipun S sendiri merupakan mesin status yang kompleks.
Subtugas untuk komposisi berurutan
Karena subtugas dalam antrean dijamin akan selesai sebelum status berikutnya, terkadang ada kemungkinan untuk sedikit menyalahgunakan6 mekanisme subtugas.
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;
}
}
Injeksi runAfter
Terkadang, menyalahgunakan Tasks.enqueue
tidak mungkin dilakukan karena ada
subtugas paralel atau panggilan Tasks.lookUp
lainnya yang harus diselesaikan sebelum S
dieksekusi. Dalam hal ini, memasukkan parameter runAfter
ke dalam S dapat digunakan untuk
memberi tahu S tentang tindakan yang harus dilakukan berikutnya.
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;
}
}
Pendekatan ini lebih bersih daripada menyalahgunakan subtugas. Namun, menerapkan ini terlalu
leluasa, misalnya, dengan membuat beberapa StateMachine
bertingkat menggunakan runAfter
, akan menjadi jalan menuju Callback Hell. Sebaiknya pisahkan runAfter
berurutan
dengan status berurutan biasa.
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
dapat diganti dengan kode berikut.
private StateMachine step1(Tasks tasks) {
doStep1();
return new S(/* runAfter= */ this::intermediateStep);
}
private StateMachine intermediateStep(Tasks tasks) {
return new T(/* runAfter= */ this::nextStep);
}
Alternatif Dilarang: runAfterUnlessError
Dalam draf sebelumnya, kita telah mempertimbangkan runAfterUnlessError
yang akan membatalkan error di awal. Hal ini dimotivasi oleh fakta bahwa error sering kali diperiksa
dua kali, sekali oleh StateMachine
yang memiliki referensi runAfter
dan
sekali oleh mesin runAfter
itu sendiri.
Setelah beberapa pertimbangan, kami memutuskan bahwa keseragaman kode lebih
penting daripada menghapus duplikat pemeriksaan error. Akan membingungkan jika
mekanisme runAfter
tidak berfungsi secara konsisten dengan
mekanisme tasks.enqueue
, yang selalu memerlukan pemeriksaan error.
Delegasi langsung
Setiap kali ada transisi status formal, loop Driver
utama akan maju.
Sesuai dengan kontrak, status maju berarti semua pencarian dan subtugas SkyValue yang diantrekan sebelumnya
diselesaikan sebelum status berikutnya dieksekusi. Terkadang, logika
delegasi StateMachine
membuat fase maju tidak diperlukan atau
kontraproduktif. Misalnya, jika step
pertama dari delegasi melakukan
pencarian SkyKey yang dapat diparalelkan dengan pencarian status yang mendelegasikan,
maka kemajuan fase akan membuatnya berurutan. Sebaiknya lakukan delegasi langsung, seperti yang ditunjukkan pada contoh di bawah ini.
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;
}
}
Aliran data
Fokus dari diskusi sebelumnya adalah pada pengelolaan alur kontrol. Bagian ini menjelaskan penerapan nilai data.
Mengimplementasikan callback Tasks.lookUp
Ada contoh penerapan callback Tasks.lookUp
di pencarian
SkyValue. Bagian ini memberikan alasan dan menyarankan
pendekatan untuk menangani beberapa SkyValues.
Tasks.lookUp
callback
Metode Tasks.lookUp
menggunakan callback, sink
, sebagai parameter.
void lookUp(SkyKey key, Consumer<SkyValue> sink);
Pendekatan idiomatisnya adalah menggunakan lambda Java untuk menerapkan hal ini:
tasks.lookUp(key, value -> myValue = (MyValueClass)value);
dengan myValue
menjadi variabel anggota dari instance StateMachine
yang melakukan
pencarian. Namun, lambda memerlukan alokasi memori tambahan dibandingkan dengan
mengimplementasikan antarmuka Consumer<SkyValue>
dalam implementasi
StateMachine
. Lambda masih berguna saat ada beberapa pencarian yang
akan ambigu.
Ada juga error yang menangani overload Tasks.lookUp
, yang setara dengan
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);
}
Contoh implementasi ditunjukkan di bawah ini.
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.
…
}
}
Seperti pencarian tanpa penanganan error, meminta class StateMachine
secara langsung
mengimplementasikan callback akan menghemat alokasi memori untuk lambda.
Penanganan error memberikan detail yang lebih lengkap, tetapi pada dasarnya, tidak ada banyak perbedaan antara penerapan error dan nilai normal.
Memakai beberapa SkyValues
Beberapa pencarian SkyValue sering kali diperlukan. Pendekatan yang paling berhasil dalam waktu adalah mengaktifkan jenis SkyValue. Berikut adalah contoh yang telah disederhanakan dari kode produksi prototipe.
@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);
}
Implementasi callback Consumer<SkyValue>
dapat dibagikan secara jelas
karena jenis nilainya berbeda. Jika tidak demikian, beralih kembali ke
penerapan berbasis lambda atau instance dalam class dalam penuh yang mengimplementasikan
callback yang sesuai dapat dilakukan.
Menyebarkan nilai antara StateMachine
Sejauh ini, dokumen ini hanya menjelaskan cara mengatur pekerjaan di subtugas, tetapi subtugas juga perlu melaporkan nilai kembali ke pemanggil. Karena subtugas secara logis asinkron, hasilnya dikomunikasikan kembali ke pemanggil menggunakan callback. Agar ini berfungsi, subtugas menentukan antarmuka sink yang dimasukkan melalui konstruktornya.
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;
}
}
Pemanggil StateMachine
akan terlihat seperti berikut.
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;
}
}
Contoh sebelumnya menunjukkan beberapa hal. Caller
harus menyebarkan
hasilnya kembali dan menentukan Caller.ResultSink
-nya sendiri. Caller
menerapkan
callback BarProducer.ResultSink
. Setelah dilanjutkan, processResult
akan memeriksa apakah
value
bernilai null untuk menentukan apakah terjadi error atau tidak. Ini adalah pola perilaku
umum setelah menerima output dari subtugas atau pencarian SkyValue.
Perlu diperhatikan bahwa implementasi acceptBarError
akan segera meneruskan hasilnya ke
Caller.ResultSink
, seperti yang diperlukan oleh Error bubbling.
Alternatif untuk StateMachine
level teratas dijelaskan dalam Driver
dan
menjembatani ke SkyFunctions.
Penanganan error
Ada beberapa contoh penanganan error yang sudah ada di callback
Tasks.lookUp
dan Menyebarkan nilai di antara
StateMachines
. Pengecualian, selain
InterruptedException
tidak ditampilkan, tetapi diteruskan melalui
callback sebagai nilai. Callback tersebut sering kali memiliki semantik eksklusif atau semantik, dengan
persis salah satu nilai atau error yang diteruskan.
Bagian berikutnya menjelaskan interaksi yang halus, tetapi penting dengan penanganan error Skyframe.
Kesalahan saat mengeluarkan air (--nokeep_go)
Selama error bubling, SkyFunction dapat dimulai ulang meskipun tidak semua SkyValues yang diminta tersedia. Dalam kasus tersebut, status berikutnya tidak akan pernah
dicapai karena kontrak API Tasks
. Namun, StateMachine
tetap
harus menyebarkan pengecualian.
Karena penyebaran harus terjadi terlepas dari apakah status berikutnya tercapai atau tidak, callback penanganan error harus melakukan tugas ini. Untuk StateMachine
bagian dalam,
hal ini dicapai dengan memanggil callback induk.
Di StateMachine
tingkat atas, yang berinteraksi dengan SkyFunction, hal ini dapat dilakukan dengan memanggil metode setException
dari ValueOrExceptionProducer
.
ValueOrExceptionProducer.tryProduceValue
kemudian akan menampilkan pengecualian, meskipun
ada SkyValues yang tidak ada.
Jika Driver
digunakan secara langsung, sebaiknya periksa
error yang disebarkan dari SkyFunction, meskipun mesin belum selesai
diproses.
Penanganan Peristiwa
Untuk SkyFunction yang perlu memunculkan peristiwa, StoredEventHandler
dimasukkan ke SkyKeyComputeState dan selanjutnya dimasukkan ke StateMachine
yang memerlukannya. Secara historis, StoredEventHandler
diperlukan karena Skyframe menghapus
peristiwa tertentu, kecuali jika peristiwa tersebut di-replay, tetapi hal ini kemudian diperbaiki.
Injeksi StoredEventHandler
dipertahankan karena menyederhanakan
implementasi peristiwa yang dikeluarkan dari callback penanganan error.
Driver
dan menjembatani ke SkyFunctions
Driver
bertanggung jawab untuk mengelola eksekusi StateMachine
,
dimulai dengan StateMachine
root yang ditentukan. Karena StateMachine
dapat
mengantrekan subtugas StateMachine
secara rekursif, satu Driver
dapat mengelola
banyak subtugas. Subtugas ini membuat struktur pohon, hasil dari
Konkurensi terstruktur. Driver
mengelompokkan pencarian SkyValue
di seluruh subtugas untuk meningkatkan efisiensi.
Ada sejumlah class yang dibangun di sekitar Driver
, dengan API berikut.
public final class Driver {
public Driver(StateMachine root);
public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}
Driver
menggunakan StateMachine
root tunggal sebagai parameter. Memanggil
Driver.drive
akan mengeksekusi StateMachine
sejauh yang dapat dilakukan tanpa
memulai ulang Skyframe. Metode ini menampilkan benar (true) saat StateMachine
selesai, dan salah (false) jika tidak, menunjukkan bahwa tidak semua nilai tersedia.
Driver
mempertahankan status StateMachine
secara serentak dan cocok untuk penyematan di SkyKeyComputeState
.
Membuat instance Driver
secara langsung
Implementasi StateMachine
secara konvensional mengomunikasikan hasilnya melalui
callback. Anda dapat langsung membuat instance Driver
seperti yang ditunjukkan dalam
contoh berikut.
Driver
disematkan dalam implementasi SkyKeyComputeState
bersama dengan
implementasi ResultSink
yang sesuai yang akan ditentukan sedikit
lebih jauh ke bawah. Di tingkat teratas, objek State
adalah penerima yang sesuai untuk
hasil komputasi karena dijamin aktif lebih lama dibandingkan 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;
}
}
Kode di bawah membuat sketsa 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;
}
}
Kemudian, kode untuk melakukan komputasi {i>database<i} dengan lambat dapat terlihat seperti berikut.
@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;
}
Menyematkan Driver
Jika StateMachine
menghasilkan nilai dan tidak meningkatkan pengecualian, menyematkan
Driver
adalah kemungkinan penerapan lain, seperti yang ditunjukkan dalam contoh berikut.
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.
}
SkyFunction mungkin memiliki kode yang terlihat seperti berikut (dengan State
adalah jenis SkyKeyComputeState
khusus fungsi).
@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;
}
Menyematkan Driver
dalam implementasi StateMachine
lebih cocok untuk
gaya coding sinkron Skyframe.
StateMachine yang dapat menghasilkan pengecualian
Jika tidak, ada class ValueOrExceptionProducer
dan ValueOrException2Producer
yang dapat disematkan SkyKeyComputeState
, yang memiliki API sinkron untuk mencocokkan
kode SkyFunction sinkron.
Class abstrak ValueOrExceptionProducer
menyertakan metode berikut.
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. }
}
Ini mencakup instance Driver
tersemat dan sangat mirip dengan
class ResultProducer
dalam driver Embedding dan antarmuka
dengan SkyFunction dengan cara yang sama. Daripada menentukan ResultSink
,
implementasi memanggil setValue
atau setException
saat salah satu dari hal tersebut terjadi.
Jika keduanya terjadi, pengecualian akan diprioritaskan. Metode tryProduceValue
menjembatani kode callback asinkron ke kode sinkron dan menampilkan
pengecualian jika ditetapkan.
Seperti disebutkan sebelumnya, selama bubble error, mungkin saja terjadi error
meski mesin belum selesai, karena tidak semua input tersedia. Untuk
mengakomodasi hal ini, tryProduceValue
akan menampilkan pengecualian yang telah ditetapkan, bahkan sebelum
mesin selesai.
Epilogue: Akhirnya menghapus callback
StateMachine
adalah cara yang sangat efisien, tetapi memerlukan banyak boilerplate untuk melakukan
komputasi asinkron. Lanjutan (terutama dalam bentuk Runnable
yang diteruskan ke ListenableFuture
) tersebar luas di bagian tertentu kode Bazel,
tetapi tidak umum dalam analisis SkyFunctions. Analisis sebagian besar terikat CPU dan
tidak ada API asinkron yang efisien untuk I/O disk. Akhirnya, sebaiknya
optimalkan callback jauh karena memiliki kurva pembelajaran dan menghambat
keterbacaan.
Salah satu alternatif yang paling menjanjikan adalah thread virtual Java. Alih-alih
menulis callback, semuanya diganti dengan panggilan pemblokir
yang sinkron. Hal ini memungkinkan karena mengikat resource thread virtual, tidak seperti
thread platform, seharusnya murah. Namun, meskipun dengan thread virtual,
mengganti operasi sinkron sederhana dengan pembuatan thread dan primitif sinkronisasi
terlalu mahal. Kami melakukan migrasi dari StateMachine
ke
thread virtual Java yang jauh lebih lambat, sehingga menyebabkan
peningkatan latensi analisis menyeluruh hampir 3x lipat. Karena thread virtual masih
merupakan fitur pratinjau, ada kemungkinan migrasi ini dapat dilakukan
di lain waktu saat performa meningkat.
Pendekatan lain yang perlu dipertimbangkan adalah menunggu coroutine Loom, jika tersedia. Keuntungannya di sini adalah mengurangi overhead sinkronisasi menggunakan multitasking yang kooperatif.
Jika semua cara tersebut gagal, penulisan ulang bytecode tingkat rendah juga dapat menjadi alternatif yang tepat. Dengan pengoptimalan yang memadai, Anda dapat mencapai performa yang mendekati kode callback yang ditulis tangan.
Lampiran
Callback Neraka
Callback hell adalah masalah yang terkenal dalam kode asinkron yang menggunakan callback. Hal ini berasal dari fakta bahwa kelanjutan untuk langkah berikutnya berada dalam langkah sebelumnya. Jika ada banyak langkah, penyusunan bertingkat ini bisa sangat dalam. Jika digabungkan dengan alur kontrol, kode menjadi tidak dapat dikelola.
class CallbackHell implements StateMachine {
@Override
public StateMachine step(Tasks task) {
doA();
return (t, l) -> {
doB();
return (t1, l2) -> {
doC();
return DONE;
};
};
}
}
Salah satu keuntungan penerapan bertingkat adalah frame stack langkah luar dapat dipertahankan. Di Java, variabel lambda yang diambil harus sudah final secara efektif sehingga penggunaan variabel tersebut dapat merepotkan. Penyusunan bertingkat dalam dihindari dengan menampilkan referensi metode sebagai kelanjutan, bukan lambda, seperti yang ditunjukkan berikut ini.
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;
}
}
Callback callback juga dapat terjadi jika pola injeksi runAfter
digunakan terlalu padat, tetapi hal ini dapat dihindari dengan menyeberangi injeksi
dengan langkah-langkah yang berurutan.
Contoh: Pencarian SkyValue berantai
Sering kali logika aplikasi memerlukan rantai dependen pencarian SkyValue, misalnya, jika SkyKey kedua bergantung pada SkyValue pertama. Memikirkan hal ini secara naif, hal ini akan menghasilkan struktur callback yang kompleks dan bersarang.
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;
}
Namun, karena kelanjutan ditetapkan sebagai referensi metode, kode akan terlihat sesuai prosedur di seluruh transisi status: step2
mengikuti step1
. Perlu diperhatikan bahwa di sini, lambda digunakan untuk menetapkan value2
. Hal ini membuat urutan kode sesuai dengan
pengurutan komputasi dari atas ke bawah.
Tips Lain-Lain
Keterbacaan: Pengurutan Eksekusi
Untuk meningkatkan keterbacaan, usahakan untuk mempertahankan implementasi StateMachine.step
dalam urutan eksekusi dan implementasi callback segera setelah itu
diteruskan dalam kode. Hal ini tidak selalu memungkinkan jika cabang alur kontrol
dijalankan. Komentar tambahan mungkin dapat membantu dalam kasus tersebut.
Dalam Contoh: Pencarian SkyValue berantai, referensi metode perantara dibuat untuk mencapai hal ini. Hal ini mengorbankan sejumlah kecil performa untuk keterbacaan, yang mungkin bermanfaat di sini.
Hipotesis Generasi
Objek Java berumur menengah memecahkan hipotesis generasi pengumpul sampah Java, yang didesain untuk menangani objek yang aktif dalam waktu sangat singkat atau objek yang hidup selamanya. Menurut definisi, objek dalam
SkyKeyComputeState
melanggar hipotesis ini. Objek tersebut, yang berisi
hierarki yang dibangun dari semua StateMachine
yang masih berjalan, yang di-root pada Driver
memiliki
masa aktif menengah saat ditangguhkan, menunggu komputasi asinkron
selesai.
Tampaknya tidak terlalu buruk di JDK19, tetapi saat menggunakan StateMachine
, terkadang ada
kemungkinan untuk mengamati peningkatan waktu GC, meskipun dengan penurunan dramatis pada
sampah memori yang sebenarnya dihasilkan. Karena memiliki masa aktif menengah,
StateMachine
dapat dipromosikan ke generasi lama, sehingga menyebabkannya terisi lebih cepat, sehingga
memerlukan GC utama atau GC penuh yang lebih mahal untuk dibersihkan.
Tindakan pencegahan awal adalah meminimalkan penggunaan variabel StateMachine
, tetapi tidak selalu memungkinkan, misalnya, jika nilai diperlukan di beberapa status. Jika memungkinkan, variabel step
stack lokal adalah variabel generasi muda dan di-GC secara efisien.
Untuk variabel StateMachine
, menguraikannya menjadi subtugas dan mengikuti
pola yang direkomendasikan untuk Menyebarkan nilai di antara
StateMachine
juga akan membantu. Perhatikan bahwa saat
mengikuti pola, hanya StateMachine
turunan yang memiliki referensi ke StateMachine
induk
dan bukan sebaliknya. Artinya, saat turunan menyelesaikan dan
memperbarui induk menggunakan callback hasil, turunan tersebut secara alami akan keluar dari
cakupan dan memenuhi syarat untuk GC.
Terakhir, dalam beberapa kasus, variabel StateMachine
diperlukan di status sebelumnya, tetapi tidak di status berikutnya. Ada baiknya Anda melakukan null dari referensi objek besar
setelah diketahui bahwa objek tersebut tidak diperlukan lagi.
Penamaan negara bagian
Saat memberi nama metode, biasanya Anda dapat memberi nama metode untuk perilaku
yang terjadi dalam metode tersebut. Cara melakukan ini kurang jelas dalam
StateMachine
karena tidak ada stack. Misalnya, metode foo
memanggil bar
sub-metode. Dalam StateMachine
, hal ini dapat diterjemahkan ke
urutan status foo
, yang diikuti oleh bar
. foo
tidak lagi menyertakan perilaku
bar
. Akibatnya, nama metode untuk status cenderung lebih sempit cakupannya, sehingga berpotensi mencerminkan perilaku lokal.
Diagram pohon konkurensi
Berikut adalah tampilan alternatif diagram dalam Selaraskan terstruktur yang menggambarkan struktur hierarki dengan lebih baik. Blok membentuk pohon kecil.
-
Berbeda dengan konvensi Skyframe, yaitu memulai ulang dari awal saat nilai tidak tersedia. ↩
-
Perhatikan bahwa
step
diizinkan untuk menampilkanInterruptedException
, tetapi contohnya menghilangkannya. Ada beberapa metode rendah dalam kode Bazel yang menampilkan pengecualian ini dan disebarkan hinggaDriver
, yang akan dijelaskan nanti, yang menjalankanStateMachine
. Tidak masalah jika tidak mendeklarasikannya untuk ditampilkan jika tidak diperlukan.↩ -
Subtugas serentak dimotivasi oleh
ConfiguredTargetFunction
yang melakukan pekerjaan independen untuk setiap dependensi. Alih-alih memanipulasi struktur data kompleks yang memproses semua dependensi sekaligus, yang menyebabkan inefisiensi, setiap dependensi memilikiStateMachine
independennya sendiri.↩ -
Beberapa panggilan
tasks.lookUp
dalam satu langkah akan ditumpuk bersama. Pengelompokan tambahan dapat dibuat dengan pencarian yang terjadi dalam subtugas serentak. ↩ -
Secara konsep mirip dengan konkurensi terstruktur Java jeps/428. ↩
-
Cara ini mirip dengan membuat thread dan menggabungkannya untuk mencapai komposisi berurutan. ↩