Ringkasan
StateMachine
Skyframe adalah objek fungsi yang didekonsektur dan berada di
heap. Fungsi ini mendukung evaluasi yang fleksibel dan tanpa redundansi1 saat
nilai yang diperlukan tidak langsung tersedia, tetapi dihitung secara asinkron. StateMachine
tidak dapat mengikat resource thread selagi menunggu, tetapi harus
ditangguhkan dan dilanjutkan. Dengan demikian, dekonstruksi mengekspos titik re-entry eksplisit
sehingga komputasi sebelumnya dapat dilewati.
StateMachine
dapat digunakan untuk mengekspresikan urutan, cabang, konkurensi
logis terstruktur, dan disesuaikan secara khusus untuk interaksi Skyframe.
StateMachine
dapat disusun menjadi StateMachine
yang lebih besar dan berbagi
sub-StateMachine
. Serentak selalu bersifat hierarkis berdasarkan konstruksi dan
sepenuhnya logis. Setiap subtugas serentak berjalan di satu thread SkyFunction induk
bersama.
Pengantar
Bagian ini secara singkat memotivasi dan memperkenalkan StateMachine
, yang ditemukan dalam
paket
java.com.google.devtools.build.skyframe.state
.
Pengantar singkat tentang mulai ulang Skyframe
Skyframe adalah framework yang melakukan evaluasi paralel 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 menurut SkyKey, yang memicu evaluasi paralel dan rekursif dari SkyFunction tambahan. Alih-alih
pemblokiran, yang akan mengikat thread, saat SkyValue yang diminta belum
siap karena beberapa subgrafik komputasi tidak lengkap, SkyFunction
yang meminta akan mengamati respons null
getValue
dan harus menampilkan null
,
bukan SkyValue, yang menandakan bahwa SkyValue tidak lengkap karena input 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 titik pemeriksaan yang ditentukan secara manual dengan SkyFunction, sehingga menghemat
komputasi ulang yang signifikan.
StateMachine
adalah objek yang berada di dalam SkyKeyComputeState
dan menghilangkan
hampir semua komputasi ulang saat SkyFunction dimulai ulang (dengan asumsi bahwa
SkyKeyComputeState
tidak keluar dari cache) dengan mengekspos hook eksekusi
jeda 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 objek yang membawa perilaku adalah
antarmuka fungsional dan ternyata sudah cukup. StateMachine
memiliki
definisi rekursif yang aneh berikut2.
@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 di
StateMachine
.
Secara intuitif, membagi komputasi menjadi beberapa langkah StateMachine
, bukan
fungsi monolitik, akan menyediakan hook yang diperlukan untuk menangguhkan dan melanjutkan
komputasi. Saat StateMachine.step
ditampilkan, ada titik penangguhan
yang eksplisit. Lanjutan yang ditentukan oleh nilai StateMachine
yang ditampilkan adalah
titik lanjutkan 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 lanjutan, yang menentukan
komputasi berikutnya yang akan dieksekusi. Alih-alih memblokir, StateMachine
dapat
menangguhkan secara sukarela dengan kembali dari fungsi step
, yang mentransfer
kontrol kembali ke instance Driver
. Driver
kemudian dapat
beralih ke StateMachine
yang siap atau melepaskan kontrol kembali ke Skyframe.
Secara tradisional, callback dan lanjutan digabungkan menjadi satu konsep.
Namun, StateMachine
mempertahankan perbedaan antara keduanya.
- Callback - menjelaskan tempat untuk menyimpan hasil komputasi asinkron.
- Lanjutan - menentukan status eksekusi berikutnya.
Callback diperlukan saat memanggil operasi asinkron, yang berarti bahwa operasi sebenarnya tidak langsung terjadi setelah memanggil metode, seperti dalam kasus pencarian SkyValue. Callback harus dibuat sesederhana mungkin.
Lanjutan adalah nilai yang ditampilkan StateMachine
dari StateMachine
dan
mengaitkan eksekusi kompleks yang mengikuti 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
berdasarkan 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 tidak langsung melakukan pencarian, tetapi
sebagai gantinya, mengelompokkan4 sebanyak mungkin pencarian sebelum melakukannya. Nilai
mungkin tidak langsung tersedia, misalnya, memerlukan mulai ulang Skyframe,
sehingga pemanggil menentukan tindakan yang akan dilakukan dengan nilai yang dihasilkan menggunakan callback.
Prosesor StateMachine
(Driver
dan jembatan ke
SkyFrame) menjamin bahwa nilai tersedia sebelum
status berikutnya dimulai. Berikut adalah contohnya.
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()
, yang meneruskan
this
sebagai konsumen. Hal ini dimungkinkan karena DoesLookup
mengimplementasikan
Consumer<SkyValue>
.
Berdasarkan kontrak, sebelum status berikutnya DoesLookup.processValue
dimulai, semua
pencarian DoesLookup.step
akan selesai. Oleh karena itu, value
tersedia saat
diakses di processValue
.
Subtugas
Tasks.enqueue
meminta eksekusi subtugas yang 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.
Sama seperti lookUp
, driver mesin status memastikan bahwa semua subtugas telah selesai sebelum melanjutkan ke langkah berikutnya. Berikut adalah contohnya.
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 bersifat serentak, semuanya berjalan dalam
satu thread sehingga update "serentak" i
tidak memerlukan
sinkronisasi apa pun.
Serentak dan terstruktur
Karena setiap lookUp
dan enqueue
harus diselesaikan sebelum melanjutkan ke status
berikutnya, ini berarti bahwa konkurensi secara alami terbatas pada struktur hierarki. Anda
dapat membuat konkurensi hierarkis5 seperti yang ditunjukkan dalam contoh
berikut.
Sulit untuk mengetahui dari UML bahwa struktur konkurensi membentuk hierarki. Ada tampilan alternatif yang menampilkan struktur hierarki dengan lebih baik.
Konkurensi terstruktur jauh lebih mudah dipahami.
Pola alur kontrol dan komposisi
Bagian ini menyajikan contoh cara beberapa StateMachine
dapat disusun
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 cabang di StateMachine
dapat dicapai dengan menampilkan nilai
yang berbeda menggunakan alur kontrol Java reguler, seperti yang 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 awal.
Komposisi berurutan lanjutan
Karena struktur kontrol StateMachine
tidak memiliki memori, berbagi definisi StateMachine
sebagai subtugas terkadang bisa terasa canggung. Misalkan M1 dan
M2 adalah instance StateMachine
yang berbagi StateMachine
, S,
dengan M1 dan M2 masing-masing adalah urutan <A, S, B> dan
<X, S, Y>. Masalahnya adalah S tidak tahu apakah harus
melanjutkan ke B atau Y setelah selesai dan StateMachine
tidak cukup menyimpan
stack panggilan. Bagian ini meninjau beberapa teknik untuk mencapai hal ini.
StateMachine
sebagai elemen urutan terminal
Tindakan ini tidak menyelesaikan masalah awal yang diajukan. Ini hanya menunjukkan komposisi
berurutan saat 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 yang diantrekan dijamin akan selesai sebelum status berikutnya, terkadang mungkin ada sedikit penyalahgunaan6 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, penyalahgunaan Tasks.enqueue
tidak mungkin dilakukan karena ada subtugas paralel
atau panggilan Tasks.lookUp
lain yang harus diselesaikan sebelum S
dieksekusi. Dalam hal ini, memasukkan parameter runAfter
ke dalam S dapat digunakan untuk
memberi tahu S tentang tindakan selanjutnya.
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 rapi daripada menyalahgunakan subtugas. Namun, menerapkannya terlalu
longgar, misalnya, dengan menyusun beberapa StateMachine
dengan runAfter
, adalah
jalan menuju Callback Hell. Sebaiknya pisahkan runAfter
berurutan
dengan status berurutan biasa.
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
dapat diganti dengan yang 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
kesalahan sejak 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
StateMachine
delegasi membuat kemajuan fase tidak diperlukan atau
berlawanan. Misalnya, jika step
pertama dari delegasi melakukan
pencarian SkyKey yang dapat diparalelkan dengan pencarian status delegasi,
maka kemajuan fase akan membuatnya berurutan. Akan lebih baik jika Anda
melakukan delegasi langsung, seperti yang ditunjukkan dalam contoh di bawah.
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 diskusi sebelumnya adalah mengelola 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.
Callback Tasks.lookUp
Metode Tasks.lookUp
menggunakan callback, sink
, sebagai parameter.
void lookUp(SkyKey key, Consumer<SkyValue> sink);
Pendekatan idiomatis adalah menggunakan lambda Java untuk menerapkannya:
tasks.lookUp(key, value -> myValue = (MyValueClass)value);
dengan myValue
sebagai variabel anggota instance StateMachine
yang melakukan
lookup. Namun, lambda memerlukan alokasi memori tambahan dibandingkan dengan
menerapkan antarmuka Consumer<SkyValue>
dalam penerapan
StateMachine
. Lambda masih berguna jika 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 ditampilkan 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, memiliki class StateMachine
yang langsung menerapkan callback akan menghemat alokasi memori untuk lamba.
Penanganan error memberikan sedikit detail lebih lanjut, tetapi pada dasarnya, tidak ada banyak perbedaan antara penyebaran error dan nilai normal.
Menggunakan beberapa SkyValues
Beberapa pencarian SkyValue sering kali diperlukan. Pendekatan yang sering kali berhasil 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 tidak ambigu
karena jenis nilainya berbeda. Jika tidak demikian, kembali ke
penerapan berbasis lambda atau instance class dalam penuh yang mengimplementasikan
callback yang sesuai dapat dilakukan.
Memperluas nilai di antara StateMachine
Sejauh ini, dokumen ini hanya menjelaskan cara mengatur pekerjaan dalam subtugas, tetapi subtugas juga perlu melaporkan nilai kembali ke pemanggil. Karena subtugas secara logis asinkron, hasilnya dikomunikasikan kembali ke pemanggil menggunakan callback. Agar 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;
}
}
StateMachine
pemanggil kemudian 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
mengimplementasikan
callback BarProducer.ResultSink
. Setelah dilanjutkan, processResult
akan memeriksa apakah
value
null untuk menentukan apakah terjadi error. Ini adalah pola perilaku
umum setelah menerima output dari subtugas atau pencarian SkyValue.
Perhatikan bahwa penerapan acceptBarError
dengan cepat meneruskan hasilnya ke
Caller.ResultSink
, seperti yang diwajibkan oleh Pembentukan error.
Alternatif untuk StateMachine
level teratas dijelaskan dalam Driver
dan
melakukan bridging 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, dengan
salah satu nilai atau error yang diteruskan.
Bagian berikutnya menjelaskan interaksi yang halus, tetapi penting dengan penanganan error Skyframe.
Pembentukan error (--nokeep_going)
Selama penyebaran error, SkyFunction dapat dimulai ulang meskipun tidak semua SkyValues yang diminta tersedia. Dalam kasus seperti itu, status berikutnya tidak akan
dicapai karena kontrak API Tasks
. Namun, StateMachine
tetap
harus menyebarkan pengecualian.
Karena penyebaran harus terjadi terlepas dari apakah status berikutnya tercapai,
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, Anda harus memeriksa error yang di-propagate dari SkyFunction, meskipun mesin belum selesai memproses.
Penanganan Peristiwa
Untuk SkyFunctions yang perlu memunculkan peristiwa, StoredEventHandler
dimasukkan ke dalam SkyKeyComputeState dan selanjutnya dimasukkan ke dalam StateMachine
yang memerlukannya. Secara historis, StoredEventHandler
diperlukan karena Skyframe menghapus
peristiwa tertentu kecuali jika peristiwa tersebut diputar ulang, tetapi hal ini kemudian diperbaiki.
Injeksi StoredEventHandler
dipertahankan karena menyederhanakan
implementasi peristiwa yang dikeluarkan dari callback penanganan error.
Driver
dan jembatan ke SkyFunctions
Driver
bertanggung jawab mengelola eksekusi StateMachine
,
yang dimulai dengan StateMachine
root yang ditentukan. Karena StateMachine
dapat
mengantrekan StateMachine
subtugas 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 satu StateMachine
root sebagai parameter. Memanggil
Driver.drive
akan mengeksekusi StateMachine
sejauh mungkin tanpa
memulai ulang Skyframe. Fungsi ini menampilkan true jika StateMachine
selesai dan false jika tidak, yang menunjukkan bahwa tidak semua nilai tersedia.
Driver
mempertahankan status serentak StateMachine
dan sangat cocok
untuk disematkan di SkyKeyComputeState
.
Membuat instance Driver
secara langsung
Implementasi StateMachine
secara konvensional menyampaikan hasilnya melalui
callback. Anda dapat membuat instance Driver
secara langsung seperti yang ditunjukkan dalam
contoh berikut.
Driver
disematkan dalam implementasi SkyKeyComputeState
bersama dengan
implementasi ResultSink
yang sesuai untuk ditentukan sedikit lebih jauh
ke bawah. Di tingkat teratas, objek State
adalah penerima yang sesuai untuk
hasil komputasi karena dijamin akan lebih lama dari 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;
}
}
Maka, 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 menimbulkan pengecualian, penyematan
Driver
adalah kemungkinan implementasi lainnya, 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. }
}
Class ini menyertakan instance Driver
tersemat dan sangat mirip dengan
class ResultProducer
di Menyemat driver dan antarmuka
dengan SkyFunction dengan cara yang serupa. Sebagai ganti menentukan ResultSink
,
implementasi memanggil setValue
atau setException
saat salah satunya terjadi.
Jika keduanya terjadi, pengecualian akan diprioritaskan. Metode tryProduceValue
menghubungkan kode callback asinkron ke kode sinkron dan menampilkan
pengecualian saat kode ditetapkan.
Seperti yang telah disebutkan sebelumnya, selama error bubbling, error dapat terjadi
meskipun mesin belum selesai karena tidak semua input tersedia. Untuk
menampung hal ini, tryProduceValue
akan menampilkan pengecualian yang ditetapkan, bahkan sebelum
komputer selesai.
Epilogue: Akhirnya menghapus callback
StateMachine
adalah cara yang sangat efisien, tetapi intensif boilerplate untuk melakukan
komputasi asinkron. Lanjutan (terutama dalam bentuk Runnable
yang diteruskan ke ListenableFuture
) tersebar luas di bagian tertentu dari kode Bazel,
tetapi tidak umum di SkyFunctions analisis. Analisis sebagian besar terikat CPU dan
tidak ada API asinkron yang efisien untuk I/O disk. Pada akhirnya, sebaiknya
optimalkan callback karena memiliki kurva belajar dan menghambat
keterbacaan.
Salah satu alternatif yang paling menjanjikan adalah thread virtual Java. Daripada
harus menulis callback, semuanya diganti dengan panggilan sinkron yang memblokir. Hal ini dimungkinkan karena mengikat resource thread virtual, tidak seperti
thread platform, seharusnya murah. Namun, meskipun dengan thread virtual,
mengganti operasi sinkron sederhana dengan primitif pembuatan dan sinkronisasi
thread terlalu mahal. Kami melakukan migrasi dari StateMachine
ke
thread virtual Java dan migrasi tersebut jauh lebih lambat, sehingga
menyebabkan peningkatan latensi analisis menyeluruh hampir 3x lipat. Karena thread virtual
masih merupakan fitur pratinjau, migrasi ini mungkin dapat dilakukan pada
tanggal lain saat performa meningkat.
Pendekatan lain yang perlu dipertimbangkan adalah menunggu coroutine Loom, jika tersedia. Keuntungannya adalah Anda mungkin dapat mengurangi overhead sinkronisasi dengan menggunakan multitasking kooperatif.
Jika semua upaya di atas tidak berhasil, penulisan ulang bytecode tingkat rendah juga dapat menjadi alternatif yang layak. Dengan pengoptimalan yang memadai, Anda mungkin dapat mencapai performa yang mendekati kode callback yang ditulis tangan.
Lampiran
Callback Hell
Callback hell adalah masalah 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 implementasi bertingkat adalah frame stack dari langkah luar dapat dipertahankan. Di Java, variabel lambda yang diambil harus sudah final secara efektif sehingga penggunaan variabel tersebut dapat merepotkan. Penyusunan bertingkat dihindari dengan menampilkan referensi metode sebagai kelanjutan, bukan lambda seperti yang ditunjukkan sebagai berikut.
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 hell juga dapat terjadi jika pola injeksi runAfter
digunakan terlalu padat, tetapi hal ini dapat dihindari dengan menyelingi injeksi
dengan langkah-langkah 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 bertingkat banyak.
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 ditentukan sebagai referensi metode, kode tersebut terlihat
prosedural di seluruh transisi status: step2
mengikuti step1
. Perlu diperhatikan bahwa di sini, lambda digunakan untuk menetapkan value2
. Hal ini membuat pengurutan kode cocok dengan
pengurutan komputasi dari atas ke bawah.
Tips Lainnya
Keterbacaan: Pengurutan Eksekusi
Untuk meningkatkan keterbacaan, usahakan untuk mempertahankan implementasi StateMachine.step
dalam urutan eksekusi dan implementasi callback segera setelah
diteruskan dalam kode. Hal ini tidak selalu dapat dilakukan jika alur kontrol
bercabang. Komentar tambahan mungkin akan membantu dalam kasus tersebut.
Di Contoh: Pencarian SkyValue berantai, referensi metode perantara dibuat untuk mencapai hal ini. Hal ini mengorbankan sedikit performa untuk keterbacaan, yang mungkin sepadan 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
pohon yang dibuat dari semua StateMachine
yang masih berjalan, yang berakar di Driver
memiliki
masa aktif perantara saat ditangguhkan, menunggu komputasi asinkron
selesai.
Hal ini tampaknya tidak terlalu buruk di JDK19, tetapi saat menggunakan StateMachine
, terkadang
dapat diamati peningkatan waktu GC, bahkan dengan penurunan drastis dalam
sampah yang sebenarnya dihasilkan. Karena StateMachine
memiliki masa aktif menengah,
StateMachine
dapat dipromosikan ke generasi lama, sehingga mengisi lebih cepat, sehingga
memerlukan GC utama atau penuh yang lebih mahal untuk dibersihkan.
Langkah 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 GC secara efisien.
Untuk variabel StateMachine
, membaginya 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, bukan sebaliknya. Artinya, saat turunan menyelesaikan dan
memperbarui induk menggunakan callback hasil, turunan akan secara alami keluar dari
cakupan dan memenuhi syarat untuk GC.
Terakhir, dalam beberapa kasus, variabel StateMachine
diperlukan dalam status sebelumnya,
tetapi tidak dalam status berikutnya. Ada baiknya Anda melakukan null dari referensi objek besar
setelah diketahui bahwa objek tersebut tidak diperlukan lagi.
Penamaan status
Saat memberi nama metode, biasanya Anda dapat memberi nama metode untuk perilaku
yang terjadi dalam metode tersebut. Cara melakukannya di
StateMachine
kurang jelas karena tidak ada stack. Misalnya, metode foo
memanggil bar
sub-metode. Dalam StateMachine
, ini dapat diterjemahkan menjadi
urutan status foo
, diikuti dengan bar
. foo
tidak lagi menyertakan perilaku
bar
. Akibatnya, nama metode untuk status cenderung memiliki cakupan yang lebih sempit,
yang berpotensi mencerminkan perilaku lokal.
Diagram hierarki serentak
Berikut adalah tampilan alternatif diagram dalam Konkurensi terstruktur yang menggambarkan struktur hierarki dengan lebih baik. Blok membentuk pohon kecil.
-
Berbeda dengan konvensi Skyframe yang 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 diperluas hinggaDriver
, yang akan dijelaskan nanti, yang menjalankanStateMachine
. Anda tidak perlu mendeklarasikannya untuk ditampilkan jika tidak diperlukan. ↩ -
Subtugas serentak dimotivasi oleh
ConfiguredTargetFunction
yang melakukan pekerjaan independen untuk setiap dependensi. Daripada memanipulasi struktur data kompleks yang memproses semua dependensi sekaligus, yang menyebabkan inefisiensi, setiap dependensi memilikiStateMachine
independennya sendiri. ↩ -
Beberapa panggilan
tasks.lookUp
dalam satu langkah dikelompokkan bersama. Pengelompokan tambahan dapat dibuat oleh pencarian yang terjadi dalam subtugas serentak. ↩ -
Secara konsep mirip dengan konkurensi terstruktur Java jeps/428. ↩
-
Melakukan hal ini mirip dengan membuat thread dan menggabungkannya untuk mencapai komposisi berurutan. ↩