Ringkasan
StateMachine
Skyframe adalah objek fungsi dekonstruksi yang berada di
heap. Fitur ini mendukung fleksibilitas dan evaluasi tanpa redundansi1 jika
nilai yang diperlukan tidak langsung tersedia, tetapi dihitung secara asinkron. StateMachine
tidak dapat mengikat resource thread saat menunggu, tetapi harus
ditangguhkan dan dilanjutkan. Dengan demikian, dekonstruksi mengekspos titik masuk ulang
eksplisit sehingga komputasi sebelumnya dapat dilewati.
StateMachine
dapat digunakan untuk mengekspresikan urutan, pencabangan, konkurensi logis
terstruktur, dan disesuaikan khusus untuk interaksi Skyframe.
StateMachine
dapat disusun menjadi StateMachine
yang lebih besar dan sub-StateMachine
bersama. Konkurensi selalu hierarkis dengan konstruksi dan
benar-benar logis. Setiap subtugas serentak berjalan di satu thread induk
SkyFunction yang sama.
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 tersebut sedemikian rupa sehingga SkyFunction dapat mencari SkyValues dengan SkyKey,
yang memicu evaluasi paralel berulang dari SkyFunction tambahan. Bukannya
pemblokiran, yang akan mengikat thread, ketika SkyValue yang diminta belum
siap karena beberapa subgrafik komputasi tidak lengkap, SkyFunction yang meminta
mengamati respons null
getValue
dan harus menampilkan null
,
bukan SkyValue, yang menandakan bahwa itu tidak lengkap karena input yang hilang.
Skyframe memulai ulang SkyFunctions saat semua SkyValues yang diminta sebelumnya tersedia.
Sebelum SkyKeyComputeState
diperkenalkan, cara tradisional untuk menangani
mulai ulang adalah dengan menjalankan kembali komputasi sepenuhnya. Meskipun ini memiliki kerumitan kuadrat, fungsi yang ditulis dengan cara ini pada akhirnya selesai karena setiap pencarian yang dijalankan ulang lebih sedikit akan menampilkan null
. Dengan SkyKeyComputeState
, Anda dapat
mengaitkan data check-point yang ditentukan secara manual dengan SkyFunction sehingga dapat menghemat
komputasi yang signifikan.
StateMachine
adalah objek yang berada di dalam SkyKeyComputeState
dan menghilangkan
semua komputasi secara virtual saat SkyFunction dimulai ulang (dengan asumsi
SkyKeyComputeState
tidak keluar dari cache) dengan mengekspos hook eksekusi
yang ditangguhkan dan dilanjutkan.
Komputasi stateful di dalam SkyKeyComputeState
Dari sudut pandang desain yang berorientasi objek, sebaiknya simpan
objek komputasi di dalam SkyKeyComputeState
, bukan nilai data murni.
Di Java, deskripsi minimum objek yang membawa objek adalah
antarmuka fungsional dan ternyata sudah cukup. StateMachine
memiliki
definisi berikut yang aneh dan rekursif2.
@FunctionalInterface
public interface StateMachine {
StateMachine step(Tasks tasks) throws InterruptedException;
}
Antarmuka Tasks
sejalan dengan SkyFunction.Environment
, tetapi
didesain untuk asinkron dan menambahkan dukungan untuk subtugas serentak yang 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;
}
}
mendeskripsikan 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, menyediakan hook yang diperlukan untuk menangguhkan dan melanjutkan
komputasi. Saat StateMachine.step
ditampilkan, ada titik penangguhan
eksplisit. Kelanjutan yang ditentukan oleh nilai StateMachine
yang ditampilkan adalah
titik resume eksplisit. Dengan demikian, komputasi dapat dihindari karena
komputasi dapat dilanjutkan di bagian yang terakhir ditinggalkan.
Callback, kelanjutan, dan komputasi asinkron
Dalam istilah teknis, StateMachine
berfungsi sebagai kelanjutan, yang menentukan
komputasi berikutnya yang akan dijalankan. Daripada memblokir, StateMachine
dapat
secara menangguhkan 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.
Secara tradisional, callback dan kelanjutan digabungkan ke dalam satu konsep.
Namun, StateMachine
mempertahankan perbedaan di antara keduanya.
- Callback - menjelaskan tempat untuk menyimpan hasil komputasi asinkron.
- Kelanjutan - menentukan status eksekusi berikutnya.
Callback diperlukan saat memanggil operasi asinkron, yang berarti bahwa operasi yang sebenarnya tidak terjadi segera setelah memanggil metode, seperti dalam kasus pencarian SkyValue. Callback harus dibuat sesederhana mungkin.
Kelanjutan adalah nilai yang ditampilkan StateMachine
dari StateMachine
dan
mengenkapsulasi eksekusi kompleks yang mengikuti setelah semua komputasi
asinkron diselesaikan. Pendekatan terstruktur ini membantu menjaga kompleksitas
callback dapat dikelola.
Tugas
Antarmuka Tasks
memberi StateMachine
API untuk mencari SkyValues
oleh 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
sesuai dengan SkyFunction.Environment.getValue
dan
SkyFunction.Environment.getValueOrThrow
, serta memiliki semantik penanganan
pengecualian yang serupa. Implementasi tidak langsung melakukan pencarian, tetapi akan mengelompokkan4 pencarian sebanyak mungkin sebelum melakukannya. Nilai tersebut mungkin tidak langsung tersedia, misalnya, memerlukan mulai ulang Skyframe, sehingga pemanggil menentukan apa yang harus dilakukan dengan nilai yang dihasilkan menggunakan callback.
Prosesor StateMachine
(Driver
dan terhubung ke
SkyFrame) menjamin bahwa nilai tersebut tersedia sebelum
status berikutnya dimulai. Contohnya adalah:
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()
, meneruskan this
sebagai konsumen. Hal ini memungkinkan karena DoesLookup
mengimplementasikan
Consumer<SkyValue>
.
Berdasarkan kontrak, sebelum status DoesLookup.processValue
berikutnya dimulai, semua
penelusuran DoesLookup.step
selesai. Oleh karena itu, value
tersedia saat
diakses di processValue
.
Subtugas
Tasks.enqueue
meminta eksekusi subtugas serentak yang logis.
Subtugas juga merupakan StateMachine
dan dapat melakukan apa pun yang dapat dilakukan StateMachine
reguler, termasuk membuat lebih banyak subtugas atau mencari SkyValues secara berulang.
Sama seperti lookUp
, driver mesin status memastikan bahwa semua subtugas
selesai sebelum melanjutkan ke langkah berikutnya. Contohnya adalah:
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
secara serentak tidak memerlukan sinkronisasi.
Serentak dan terstruktur
Karena setiap lookUp
dan enqueue
harus diselesaikan sebelum maju ke status
berikutnya, ini berarti bahwa konkurensi secara alami terbatas pada struktur hierarki. Anda
dapat membuat konkurensi hierarkis5 seperti yang ditunjukkan pada contoh
berikut.
Sulit untuk membedakan dari UML bahwa struktur serentak membentuk hierarki. Ada tampilan alternatif yang lebih menunjukkan struktur hierarki.
Konkurensi terstruktur jauh lebih mudah untuk dipahami.
Pola alur dan komposisi
Bagian ini menampilkan 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. Contohnya
ditunjukkan dalam Komputasi stateful di dalam
SkyKeyComputeState
.
Cabang
Status cabang di StateMachine
dapat dicapai dengan menampilkan nilai
yang berbeda menggunakan alur kontrol Java reguler, seperti yang ditunjukkan pada 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 dapat terasa canggung. Biarkan M1 dan
M2 menjadi instance StateMachine
yang berbagi StateMachine
, S,
dengan M1 dan M2 berturut-turut 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 cukup menyimpan
stack panggilan. Bagian ini meninjau beberapa teknik untuk mencapai hal ini.
StateMachine
sebagai elemen urutan terminal
Ini tidak menyelesaikan masalah awal yang dikemukakan. Ini hanya menunjukkan komposisi
berurutan ketika StateMachine
bersama menjadi 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();
}
}
Ini berfungsi meskipun S adalah mesin status yang kompleks.
Subtugas untuk komposisi berurutan
Karena subtugas yang diantrekan dijamin akan selesai sebelum status berikutnya, terkadang Anda dapat 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
Kadang, 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 S dapat digunakan untuk
memberi tahu S tentang apa yang harus dilakukan 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 bersih daripada menyalahgunakan subtugas. Namun, menerapkan ini
secara bebas, misalnya dengan menyarangkan beberapa StateMachine
dengan runAfter
, adalah
cara menuju Callback Hell. Sebaiknya pisahkan runAfter
berurutan dengan status berurutan biasa.
return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))
dapat diganti dengan parameter 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 Terlarang: runAfterUnlessError
Dalam draf sebelumnya, kami telah mempertimbangkan runAfterUnlessError
yang akan dibatalkan
lebih awal karena error. Hal ini didasarkan pada fakta bahwa error sering kali akhirnya
diperiksa dua kali, sekali oleh StateMachine
yang memiliki referensi runAfter
dan
satu kali oleh mesin runAfter
itu sendiri.
Setelah beberapa pertimbangan, kami memutuskan bahwa keseragaman kode lebih
penting dibandingkan dengan menghapus duplikat pemeriksaan error. Akan menjadi 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 maju.
Sesuai kontrak, memajukan status berarti semua pencarian dan subtugas SkyValue yang sebelumnya diantrekan diselesaikan sebelum status berikutnya dieksekusi. Terkadang logika
StateMachine
delegasi membuat progres fase tidak diperlukan atau
kontraproduktif. Misalnya, jika step
pertama yang didelegasikan melakukan
pencarian SkyKey yang dapat diparalelkan dengan pencarian status delegasi,
penundaan fase akan membuatnya berurutan. Akan lebih masuk akal untuk melakukan delegasi langsung, seperti yang ditunjukkan pada 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 tentang pengelolaan alur kontrol. Bagian ini menjelaskan penyebaran nilai data.
Mengimplementasikan callback Tasks.lookUp
Ada contoh penerapan callback Tasks.lookUp
dalam 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 menerapkan hal ini:
tasks.lookUp(key, value -> myValue = (MyValueClass)value);
dengan myValue
yang menjadi variabel anggota 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 serupa 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 penerapannya 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 secara langsung
mengimplementasikan callback akan menyimpan alokasi memori untuk lambda.
Penanganan error memberikan sedikit lebih banyak detail, tetapi pada dasarnya, tidak ada banyak perbedaan antara penyebaran error dan nilai normal.
Memakai beberapa SkyValues
Beberapa pencarian SkyValue sering kali diperlukan. Pendekatan yang paling sering digunakan 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 dengan jelas
karena jenis nilainya berbeda. Jika tidak, Anda dapat kembali ke
implementasi berbasis lambda atau instance class dalam penuh yang menerapkan
callback yang sesuai.
Memperluas nilai antara StateMachine
Sejauh ini, dokumen ini hanya menjelaskan cara mengatur pekerjaan dalam subtugas, tetapi subtugas juga perlu melaporkan nilai kembali ke pemanggil. Karena subtugas bersifat logika asinkron, hasilnya akan dikomunikasikan kembali kepada 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;
}
}
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 kembali
hasilnya dan menentukan Caller.ResultSink
-nya sendiri. Caller
mengimplementasikan callback BarProducer.ResultSink
. Setelah dimulai ulang, processResult
akan memeriksa apakah
value
bernilai null untuk menentukan apakah terjadi error. Ini adalah pola perilaku yang umum setelah menerima output dari subtugas atau pencarian SkyValue.
Perlu diperhatikan bahwa implementasi acceptBarError
akan meneruskan hasilnya ke
Caller.ResultSink
, seperti yang diwajibkan oleh Error bubling.
Alternatif untuk StateMachine
level teratas dijelaskan dalam Driver
dan terhubung ke SkyFunctions.
Penanganan error
Ada beberapa contoh penanganan error yang sudah ada di callback
Tasks.lookUp
dan Menerapkan 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 salah satu nilai atau error yang diteruskan.
Bagian berikutnya menjelaskan interaksi yang samar, tetapi penting dengan penanganan error Skyframe.
Terjadi error dalam balon (--nokeep_getting)
Selama balon error, SkyFunction dapat dimulai ulang meskipun tidak semua
SkyValues yang diminta tersedia. Dalam kasus seperti itu, status berikutnya tidak akan pernah tercapai karena kontrak API Tasks
. Namun, StateMachine
harus
tetap menyebarkan pengecualian.
Karena propagasi harus dilakukan, terlepas dari apakah status berikutnya tercapai atau tidak,
callback penanganan error harus melakukan tugas ini. Untuk StateMachine
dalam,
ini dicapai dengan memanggil callback induk.
Di StateMachine
level teratas, yang berinteraksi dengan SkyFunction, ini dapat
dilakukan dengan memanggil metode setException
dari ValueOrExceptionProducer
.
ValueOrExceptionProducer.tryProduceValue
kemudian akan menampilkan pengecualian, meskipun
tidak ada SkyValues yang hilang.
Jika Driver
digunakan secara langsung, penting untuk memeriksa
error yang disebarkan dari SkyFunction, meskipun mesin belum selesai
memproses.
Penanganan Peristiwa
Untuk SkyFunctions yang perlu memunculkan peristiwa, StoredEventHandler
diinjeksikan
ke SkyKeyComputeState dan selanjutnya dimasukkan ke dalam StateMachine
yang
memerlukannya. Secara historis, StoredEventHandler
diperlukan karena Skyframe menghapus
peristiwa tertentu kecuali jika diputar ulang, tetapi masalah ini kemudian diperbaiki.
Injeksi StoredEventHandler
dipertahankan karena menyederhanakan
implementasi peristiwa yang dikeluarkan dari callback penanganan error.
Driver
dan terhubung ke SkyFunctions
Driver
bertanggung jawab untuk mengelola eksekusi StateMachine
,
dimulai dengan root StateMachine
yang ditentukan. Karena StateMachine
dapat
mengantrekan subtugas StateMachine
secara berulang, satu Driver
dapat mengelola
banyak subtugas. Subtugas ini membuat struktur hierarki, hasil dari
Konkurensi terstruktur. Driver
mengelompokkan pencarian SkyValue
di seluruh subtugas untuk meningkatkan efisiensi.
Ada sejumlah class yang dibuat 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 root StateMachine
sebagai parameter. Memanggil
Driver.drive
akan mengeksekusi StateMachine
sejauh yang dapat dilakukan tanpa
memulai ulang Skyframe. Metode ini menampilkan nilai benar (true) saat StateMachine
selesai dan jika tidak, menunjukkan bahwa tidak semua nilai tersedia.
Driver
mempertahankan status StateMachine
serentak, dan sangat cocok untuk disematkan di SkyKeyComputeState
.
Membuat instance langsung Driver
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
beserta
implementasi ResultSink
yang sesuai untuk ditentukan sedikit lebih
bawah. Di tingkat atas, objek State
adalah penerima yang sesuai untuk
hasil komputasi karena dijamin akan aktif 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;
}
}
Kode untuk menghitung hasil secara lambat akan 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 memberikan pengecualian, menyematkan
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
sebagai
jenis SkyKeyComputeState
dari fungsi tertentu).
@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;
}
Penyematan Driver
dalam implementasi StateMachine
lebih cocok untuk
gaya coding sinkron Skyframe.
StateMachines yang dapat menghasilkan pengecualian
Jika tidak, ada class ValueOrExceptionProducer
dan ValueOrException2Producer
yang bisa disematkan SkyKeyComputeState
yang memiliki API sinkron agar cocok dengan
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. }
}
Library ini mencakup instance Driver
tersemat dan sangat mirip dengan
class ResultProducer
dalam Driver tersemat dan antarmuka
dengan SkyFunction dengan cara yang serupa. Bukannya menentukan ResultSink
,
implementasi akan memanggil setValue
atau setException
jika salah satunya terjadi.
Jika keduanya terjadi, pengecualian akan diprioritaskan. Metode tryProduceValue
menghubungkan
kode callback asinkron ke kode sinkron dan menampilkan
pengecualian saat kode tersebut ditetapkan.
Seperti disebutkan sebelumnya, selama error bubling, mungkin saja error terjadi
meskipun mesin belum selesai karena tidak semua input tersedia. Untuk
mengakomodasi ini, tryProduceValue
akan menampilkan pengecualian yang ditetapkan, bahkan sebelum
mesin selesai.
Epilog: Pada akhirnya, menghapus callback
StateMachine
adalah cara yang sangat efisien, tetapi intensif menggunakan boilerplate untuk melakukan
komputasi asinkron. Kelanjutan (terutama dalam bentuk Runnable
yang diteruskan ke ListenableFuture
) tersebar luas di bagian kode Bazel tertentu,
tetapi tidak umum dalam analisis SkyFunctions. Analisis sebagian besar terikat dengan CPU dan
tidak ada API asinkron yang efisien untuk I/O disk. Pada akhirnya, sebaiknya
optimalkan callback jauhnya karena callback tersebut memiliki kurva pembelajaran dan mengurangi
keterbacaan.
Salah satu alternatif yang paling menjanjikan adalah thread virtual Java. Alih-alih harus menulis callback, semuanya diganti dengan panggilan sinkron yang memblokir. Hal ini memungkinkan karena mengikat resource thread virtual, tidak seperti
thread platform, seharusnya murah. Namun, bahkan dengan thread virtual,
mengganti operasi sinkron sederhana dengan pembuatan thread dan
primitif sinkronisasi terlalu mahal. Kami melakukan migrasi dari StateMachine
ke
thread virtual Java dan urutannya jauh lebih lambat, sehingga menghasilkan
hampir 3x peningkatan latensi analisis menyeluruh. Karena thread virtual
masih menjadi fitur pratinjau, migrasi ini mungkin dapat dilakukan di
masa mendatang saat performa meningkat.
Pendekatan lain yang perlu dipertimbangkan adalah menunggu coroutine Loom, jika ada. Keuntungannya di sini adalah Anda dapat mengurangi overhead sinkronisasi dengan menggunakan multitasking kooperatif.
Jika semuanya gagal, penulisan ulang bytecode level rendah juga dapat menjadi alternatif yang mungkin dilakukan. Dengan pengoptimalan yang memadai, Anda dapat mencapai performa yang mendekati kode callback yang ditulis tangan.
Lampiran
Penalaan Balik
Hell callback adalah masalah terkenal dalam kode asinkron yang menggunakan callback. Hal ini didasarkan pada fakta bahwa kelanjutan untuk langkah berikutnya berada dalam langkah sebelumnya. Jika ada banyak langkah, tingkatan ini bisa sangat mendalam. 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 dari implementasi bertingkat adalah bahwa frame stack langkah luar dapat dipertahankan. Di Java, variabel lambda yang diambil harus benar-benar final sehingga menggunakan variabel tersebut bisa jadi rumit. Penyarangan mendalam 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;
}
}
Hell callback juga dapat terjadi jika pola injeksi runAfter
digunakan terlalu padat, tetapi hal ini dapat dihindari dengan menyisipkan injeksi
dengan langkah berurutan.
Contoh: Pencarian SkyValue berantai
Sering kali logika aplikasi memerlukan rantai pencarian SkyValue yang dependen, misalnya, jika SkyKey kedua bergantung pada SkyValue pertama. Mempertimbangkan hal ini secara naif, hal ini akan menghasilkan struktur callback yang kompleks dan bertingkat.
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
prosedur di seluruh transisi status: step2
mengikuti step1
. Perhatikan bahwa di sini, lambda digunakan untuk menetapkan value2
. Hal ini membuat pengurutan kode sesuai
dengan urutan komputasi dari atas ke bawah.
Tips Lainnya
Keterbacaan: Urutan Eksekusi
Untuk meningkatkan keterbacaan, usahakan untuk mempertahankan implementasi StateMachine.step
dalam urutan eksekusi dan implementasi callback tepat setelah mengikutinya
dalam meneruskan kode. Hal ini tidak selalu mungkin dilakukan di mana cabang kontrol
beralur. Komentar tambahan mungkin berguna dalam kasus seperti itu.
Dalam Contoh: Pencarian SkyValue yang Dirantai, referensi metode perantara dibuat untuk mencapai ini. Hal ini menukar sejumlah kecil performa untuk keterbacaan, yang mungkin cukup bermanfaat di sini.
Hipotesis Generasi
Objek Java berumur sedang merusak hipotesis generasi pembersih sampah memori Java, yang dirancang untuk menangani objek yang hidup dalam
waktu sangat singkat atau objek yang hidup selamanya. Pada dasarnya, objek di
SkyKeyComputeState
melanggar hipotesis ini. Objek tersebut, yang berisi
hierarki yang dikonstruksi dari semua StateMachine
yang masih berjalan, yang di-root di Driver
memiliki
masa aktif menengah saat ditangguhkan, menunggu komputasi asinkron
selesai.
Hal ini tampaknya tidak terlalu buruk di JDK19, tetapi saat menggunakan StateMachine
, terkadang
Anda dapat mengamati peningkatan waktu GC, bahkan dengan penurunan drastis pada
sampah memori sebenarnya yang dihasilkan. Karena memiliki masa aktif
sedang, StateMachine
dapat dipromosikan ke generasi lama, yang membuatnya terisi lebih cepat, sehingga
harus membersihkan GC utama atau yang lebih mahal.
Tindakan pencegahan awal adalah untuk 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 yang efisien.
Untuk variabel StateMachine
, mengelompokkan hal-hal ke dalam subtugas dan mengikuti
pola yang direkomendasikan untuk Menerapkan nilai antara
StateMachine
juga berguna. Perhatikan bahwa saat
mengikuti pola, hanya StateMachine
turunan yang memiliki referensi ke StateMachine
induk dan bukan sebaliknya. Ini berarti bahwa saat turunan menyelesaikan dan
memperbarui induk menggunakan callback hasil, turunan tersebut tentu saja akan berada di luar
cakupan dan memenuhi syarat untuk GC.
Terakhir, dalam beberapa kasus, variabel StateMachine
diperlukan di status sebelumnya, tetapi tidak di status berikutnya. Ada baiknya untuk membatalkan referensi objek besar
setelah diketahui bahwa objek tersebut tidak lagi diperlukan.
Penamaan status
Saat memberi nama metode, biasanya Anda dapat memberi nama metode untuk perilaku
yang terjadi dalam metode tersebut. Cara melakukannya di StateMachine
tidak terlalu jelas karena tidak ada stack. Misalnya, metode foo
memanggil sub-metode bar
. Dalam StateMachine
, kode ini dapat diterjemahkan ke
urutan status foo
, diikuti dengan bar
. foo
tidak lagi menyertakan perilaku
bar
. Akibatnya, nama metode untuk status cenderung lebih sempit dalam cakupan,
yang berpotensi mencerminkan perilaku lokal.
Diagram hierarki serentak
Berikut adalah tampilan alternatif dari diagram dalam Konkurensi terstruktur yang lebih menggambarkan struktur pohon. Blok membentuk pohon kecil.
-
Berbeda dengan konvensi Skyframe untuk memulai ulang dari awal saat nilai tidak tersedia. ↩
-
Perhatikan bahwa
step
diizinkan untuk menampilkanInterruptedException
, tetapi contoh menghilangkannya. Ada beberapa metode rendah dalam kode Bazel yang menampilkan pengecualian ini dan menyebarkannya keDriver
, untuk dijelaskan nanti, yang menjalankanStateMachine
. Tidak masalah untuk tidak mendeklarasikannya saat tidak diperlukan.↩ -
Subtugas serentak dimotivasi oleh
ConfiguredTargetFunction
yang melakukan pekerjaan independen untuk setiap dependensi. Daripada memanipulasi struktur data kompleks yang memproses semua dependensi sekaligus, sehingga menyebabkan ketidakefisienan, setiap dependensi memilikiStateMachine
tersendiri.↩ -
Beberapa panggilan
tasks.lookUp
dalam satu langkah dikelompokkan bersama. Pengelompokan tambahan dapat dibuat oleh pencarian yang terjadi dalam subtugas serentak. ↩ -
Ini secara konsep mirip dengan konkurensi terstruktur Java jeps/428. ↩
-
Caranya mirip dengan menghasilkan thread dan menggabungkannya untuk mencapai komposisi berurutan. ↩