Ringkasan
StateMachine
Skyframe adalah objek fungsi yang diuraikan yang berada di
dari heap tersebut. Privacy Sandbox mendukung fleksibilitas dan evaluasi tanpa redundansi1 saat
nilai yang diperlukan tidak langsung tersedia, tetapi dikomputasi secara asinkron. Tujuan
StateMachine
tidak dapat mengikat resource thread selagi menunggu, tetapi harus melakukannya
ditangguhkan dan dilanjutkan. Dengan demikian, dekonstruksi mengekspos entri kembali yang eksplisit
poin sehingga perhitungan sebelumnya
dapat dilewati.
StateMachine
dapat digunakan untuk mengekspresikan urutan, percabangan, logika terstruktur
konkurensi dan disesuaikan secara khusus untuk interaksi Skyframe.
StateMachine
dapat disusun menjadi StateMachine
yang lebih besar dan dapat dibagikan
sub-StateMachine
. Konkurensi selalu hierarkis berdasarkan konstruksi dan
sepenuhnya logis. Setiap subtugas serentak berjalan di satu induk bersama
Thread SkyFunction.
Pengantar
Bagian ini secara singkat memotivasi dan memperkenalkan StateMachine
, yang dapat ditemukan dalam
java.com.google.devtools.build.skyframe.state
paket.
Pengenalan singkat tentang memulai ulang Skyframe
Skyframe adalah kerangka kerja yang melakukan evaluasi paralel atas grafik dependensi.
Setiap {i>node<i} dalam grafik sesuai dengan evaluasi SkyFunction dengan
SkyKey yang menentukan parameternya dan SkyValue yang menentukan hasilnya. Tujuan
sedemikian rupa sehingga SkyFunction
dapat mencari SkyValues melalui SkyKey,
memicu evaluasi rekursif paralel dari SkyFunction tambahan. Daripada fokus pada
pemblokiran, yang akan mengikat thread, jika SkyValue yang diminta belum
siap karena beberapa subgrafik komputasi tidak lengkap, permintaan
SkyFunction mengamati respons null
getValue
dan akan menampilkan null
bukan SkyValue, menandakan bahwa data tidak lengkap karena
input yang ada tidak ada.
Skyframe memulai ulang SkyFunctions saat semua SkyValues yang diminta sebelumnya
tersedia.
Sebelum diperkenalkannya SkyKeyComputeState
, cara tradisional penanganan
{i>restart<i} adalah untuk sepenuhnya
menjalankan kembali komputasi. Meskipun model ini memiliki fungsi kuadrat
kompleksitas yang lebih tinggi, fungsi yang ditulis dengan cara ini
akhirnya 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
untuk rekomputasi.
StateMachine
adalah objek yang ada di dalam SkyKeyComputeState
dan menghilangkannya
hampir semua penghitungan ulang saat SkyFunction dimulai ulang (dengan asumsi bahwa
SkyKeyComputeState
tidak keluar dari cache) dengan mengekspos penangguhan dan melanjutkan
hook eksekusi.
Komputasi stateful di dalam SkyKeyComputeState
Dari sudut pandang desain berorientasi objek, adalah
masuk akal untuk mempertimbangkan penyimpanan
objek komputasi di dalam SkyKeyComputeState
, bukan nilai data murni.
Di Java, deskripsi minimum dari perilaku yang membawa objek adalah
antarmuka fungsional dan ternyata memadai. StateMachine
memiliki
definisi rekursif berikut ini2.
@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
jika
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
. Metode
referensi 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 berperforma tinggi. Saat StateMachine.step
ditampilkan kembali, ada penangguhan eksplisit
poin. Kelanjutan yang ditentukan oleh nilai StateMachine
yang ditampilkan adalah
titik resume eksplisit. Dengan demikian,
komputasi ulang dapat dihindari karena
aktivitas komputasi dapat dilanjutkan
dari tempat terakhir yang ditinggalkan.
Callback, kelanjutan, dan komputasi asinkron
Dalam istilah teknis, StateMachine
berfungsi sebagai kelanjutan, yang menentukan
komputasi selanjutnya
yang akan dieksekusi. Daripada memblokir, StateMachine
dapat
menangguhkan secara sukarela dengan kembali dari fungsi step
, yang mentransfer
kontrol kembali ke instance Driver
. Driver
dapat
lalu alihkan ke StateMachine
siap atau lepaskan kontrol kembali ke Skyframe.
Biasanya, callback dan kelanjutan digabungkan menjadi satu konsep.
Namun, StateMachine
mempertahankan perbedaan di antara keduanya.
- Callback - menjelaskan tempat menyimpan hasil asinkron komputasi berperforma tinggi.
- Lanjutan - menentukan status eksekusi berikutnya.
Callback diperlukan saat memanggil operasi asinkron, yang berarti operasi yang sebenarnya tidak terjadi segera setelah memanggil metode, seperti dalam pencarian {i>SkyValue<i}. Callback harus dibuat sesederhana mungkin.
Lanjutan adalah nilai StateMachine
yang ditampilkan dari StateMachine
dan
mengenkapsulasi eksekusi kompleks yang terjadi setelah semua
penyelesaian komputasi. Pendekatan terstruktur ini membantu
menjaga kompleksitas
agar callback dapat dikelola.
Tugas
Antarmuka Tasks
menyediakan StateMachine
dengan API untuk mencari SkyValues
oleh SkyKey dan untuk 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. Mereka adalah
setara dengan SkyFunction.Environment.getValue
dan
SkyFunction.Environment.getValueOrThrow
dan memiliki penanganan pengecualian yang serupa
semantik. Implementasi ini tidak langsung melakukan pencarian, tetapi
sebagai gantinya, mengelompokkan4 sebanyak mungkin pencarian sebelum melakukannya. Nilainya
mungkin tidak segera tersedia, misalnya, memerlukan {i>restart<i} Skyframe,
sehingga pemanggil harus menentukan tindakan yang harus dilakukan dengan nilai yang dihasilkan menggunakan callback.
Prosesor StateMachine
(Driver
dan menjembatani ke
SkyFrame) menjamin bahwa nilai 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()
, yang 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 StateMachine
reguler
bisa dilakukan, termasuk membuat lebih banyak
subtugas secara rekursif atau mencari SkyValues.
Mirip dengan lookUp
, driver mesin status
memastikan bahwa semua subtugas
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
thread tunggal sehingga "konkurensi" pembaruan i
tidak memerlukan
sinkronisasi.
Serentak dan terstruktur
Karena setiap lookUp
dan enqueue
harus di-resolve sebelum melanjutkan ke yang berikutnya
, itu berarti konkurensi secara alami terbatas pada struktur pohon. Penting
pembuatan konkurensi hierarkis5 seperti ditunjukkan di bawah ini
contoh.
Sulit untuk membedakan dari UML bahwa struktur konkurensi membentuk hierarki. Ada tampilan alternatif yang menampilkan dengan lebih baik struktur pohon.
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 berbagai
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 lebih awal.
Komposisi berurutan lanjutan
Karena struktur kontrol StateMachine
tidak memiliki memori, membagikan StateMachine
definisi sebagai subtugas
terkadang bisa terasa canggung. Misalkan M1 dan
M2 adalah instance StateMachine
yang memiliki StateMachine
, S,
dengan M1 dan M2 adalah urutan <A, S, B> dan
<X, S, Y>. Masalahnya, S tidak tahu apakah harus
lanjutkan ke B atau Y setelah selesai dan StateMachine
tidak cukup mempertahankan
stack panggilan. Bagian ini meninjau beberapa teknik untuk mencapai hal ini.
StateMachine
sebagai elemen urutan terminal
Hal ini tidak menyelesaikan masalah awal yang ditimbulkan. Ini hanya menunjukkan model
komposisi 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 dalam antrean dijamin selesai sebelum status berikutnya, terkadang 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
Terkadang, menyalahgunakan Tasks.enqueue
tidak mungkin karena ada hal lain
subtugas paralel atau panggilan Tasks.lookUp
yang harus diselesaikan sebelum S
akan dijalankan. 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 juga
secara bebas, misalnya, dengan membuat beberapa StateMachine
bertingkat menggunakan runAfter
,
jalan menuju Callback Hell. Lebih baik memisahkan secara berurutan
runAfter
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, kami telah mempertimbangkan runAfterUnlessError
yang akan membatalkan
sejak dini. Hal ini dimotivasi oleh fakta bahwa
kesalahan sering kali menghasilkan
diperiksa dua kali, sekali oleh StateMachine
yang memiliki referensi runAfter
dan
sekali oleh komputer runAfter
itu sendiri.
Setelah beberapa muslihat, kami memutuskan bahwa keseragaman kode lebih
lebih penting daripada menghapus duplikat
dalam pemeriksaan {i>error<i}. 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 SkyValue yang sebelumnya diantrekan
pencarian dan subtugas diselesaikan sebelum
status berikutnya dieksekusi. Terkadang logika
dari delegasi StateMachine
membuat fase maju tidak diperlukan atau
kontraproduktif. Misalnya, jika step
pertama dari delegasi menjalankan
Pencarian SkyKey yang dapat diparalelkan dengan pencarian status yang mendelegasikan
kemudian fase maju akan
membuatnya berurutan. Ini bisa lebih masuk akal untuk
melakukan delegasi langsung, seperti yang ditunjukkan dalam 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. Ini menjelaskan penyebaran nilai data.
Mengimplementasikan callback Tasks.lookUp
Ada contoh penerapan callback Tasks.lookUp
di SkyValue
pencarian. Bagian ini memberikan alasan dan saran
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
menerapkan antarmuka Consumer<SkyValue>
di StateMachine
terlepas dari implementasi layanan. Lambda masih berguna bila ada beberapa pencarian yang
bisa jadi ambigu.
Ada juga kelebihan penanganan error dari 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, memiliki class StateMachine
secara langsung
mengimplementasikan callback akan menyimpan alokasi memori untuk lambda.
Penanganan error memberikan detail lebih lengkap, tetapi pada dasarnya, tidak ada banyak perbedaan antara penyebaran kesalahan dan nilai normal.
Memakai beberapa SkyValues
Beberapa pencarian SkyValue sering kali diperlukan. Pendekatan yang banyak digunakan waktunya adalah mengaktifkan jenis SkyValue. Berikut ini adalah contoh 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 belum tentu, kembali ke
implementasi berbasis lambda atau instance kelas dalam lengkap yang mengimplementasikan
callback yang tepat layak.
Menyebarkan 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 asinkron secara logis, hasilnya dikomunikasikan kembali ke pemanggil menggunakan callback. Agar ini berhasil, subtugas mendefinisikan antarmuka sink yang yang diinjeksikan 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
mengimplementasikan metode
Callback BarProducer.ResultSink
. Setelah dilanjutkan, processResult
akan memeriksa apakah
value
bernilai null untuk menentukan apakah terjadi error. Ini adalah perilaku umum
setelah menerima {i>output<i} 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
menghubungkan ke SkyFunctions.
Penanganan error
Ada beberapa contoh penanganan error yang sudah tersedia di Tasks.lookUp
callback dan Menyebarkan nilai antara
StateMachines
. Pengecualian, selain
InterruptedException
tidak ditampilkan, tetapi diteruskan melalui
callback sebagai nilai. Callback tersebut sering kali memiliki semantik eksklusif, dengan
dengan tepat satu nilai atau
kesalahan yang diteruskan.
Bagian selanjutnya menjelaskan interaksi yang kecil, namun penting dengan Skyframe dalam penanganan error.
Kesalahan saat mengeluarkan air (--nokeep_go)
Selama kesalahan menggelegar, SkyFunction dapat dimulai ulang bahkan jika tidak semua diminta
SkyValues tersedia. Dalam kasus tersebut, status berikutnya tidak akan pernah
tercapai karena kontrak API Tasks
. Namun, StateMachine
harus
masih menyebarkan pengecualian.
Karena propagasi harus terjadi terlepas dari
apakah status berikutnya tercapai,
callback penanganan error harus melakukan tugas ini. Untuk StateMachine
bagian dalam,
ini dapat dilakukan dengan memanggil callback induk.
Di StateMachine
tingkat atas, yang berinteraksi dengan SkyFunction, dapat
dilakukan dengan memanggil metode setException
dari ValueOrExceptionProducer
.
ValueOrExceptionProducer.tryProduceValue
kemudian akan menampilkan pengecualian, bahkan
jika ada SkyValues yang hilang.
Jika Driver
digunakan secara langsung, Anda harus memeriksa
menyebarkan kesalahan dari SkyFunction, meskipun mesin belum selesai
diproses.
Penanganan Peristiwa
Untuk SkyFunctions yang perlu memunculkan peristiwa, StoredEventHandler
dimasukkan
ke SkyKeyComputeState dan dimasukkan lebih lanjut ke StateMachine
yang memerlukan
mereka. Sebelumnya, StoredEventHandler
diperlukan karena penurunan Skyframe
peristiwa tertentu kecuali jika mereka 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 mengelola eksekusi StateMachine
,
yang diawali dengan StateMachine
root tertentu. Seperti yang dapat dilakukan StateMachine
mengantrekan StateMachine
subtugas secara rekursif, satu Driver
dapat mengelola
banyak subtugas. Sub-tugas ini membuat struktur pohon, hasil dari
Konkurensi terstruktur. Driver
mengelompokkan SkyValue
pencarian 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. Menelepon
Driver.drive
menjalankan StateMachine
sejauh yang dapat dilakukan tanpa
Mulai ulang Skyframe. Metode ini menampilkan benar (true) saat StateMachine
selesai dan salah (false)
sebaliknya, menunjukkan bahwa tidak semua nilai tersedia.
Driver
mempertahankan status StateMachine
secara serentak dan juga
cocok untuk penyematan di SkyKeyComputeState
.
Membuat instance Driver
secara langsung
Implementasi StateMachine
mengomunikasikan hasilnya secara konvensional melalui
callback. Anda dapat langsung membuat instance Driver
seperti yang ditunjukkan dalam
contoh berikut.
Driver
disematkan dalam implementasi SkyKeyComputeState
bersama dengan
implementasi dari ResultSink
yang sesuai akan didefinisikan sedikit lebih jauh
ke bawah. Di tingkat atas, objek State
adalah penerima yang sesuai untuk
dari 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, penyematan
Driver
adalah kemungkinan implementasi 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
jenis khusus fungsi SkyKeyComputeState
).
@Nullable // Null when a Skyframe restart is needed.
Result computeResult(SkyFunction.Environment env, State state)
throws InterruptedException {
if (state.result != null) {
return state.result;
}
if (state.resultProducer == null) {
state.resultProducer = new ResultProducer(new Parameters());
}
var result = state.resultProducer.tryProduceValue(env);
if (result == null) {
return null;
}
state.resultProducer = null;
return state.result = result;
}
Menyematkan Driver
dalam implementasi StateMachine
lebih cocok untuk
Gaya coding sinkron Skyframe.
StateMachine yang dapat menghasilkan pengecualian
Jika tidak, ada ValueOrExceptionProducer
yang dapat disematkan SkyKeyComputeState
dan ValueOrException2Producer
yang memiliki API sinkron yang akan dicocokkan
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 mencakup instance Driver
yang disematkan dan sangat mirip dengan
Class ResultProducer
dalam driver penyematan 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 ketika salah satunya ditetapkan.
Seperti disebutkan sebelumnya, selama buih error, mungkin saja terjadi error
bahkan jika mesin belum selesai karena
tidak semua input tersedia. Kepada
mengakomodasi hal ini, tryProduceValue
akan menampilkan pengecualian yang ditetapkan, bahkan sebelum
mesin selesai.
Epilogue: Akhirnya menghapus callback
StateMachine
adalah cara yang sangat efisien, tetapi membutuhkan boilerplate untuk melakukan
komputasi asinkron. Lanjutan (terutama dalam bentuk Runnable
detik
diteruskan ke ListenableFuture
) tersebar luas di bagian-bagian tertentu kode Bazel,
tetapi tidak umum digunakan
dalam analisis SkyFunctions. Analisis sebagian besar
terikat CPU dan
tidak ada API asinkron yang efisien untuk I/O disk. Pada akhirnya, itu akan menjadi
bagus untuk mengoptimalkan callback jauh karena memiliki kurva pembelajaran dan menghambat
keterbacaan.
Salah satu alternatif yang paling menjanjikan adalah thread virtual Java. Daripada fokus pada
harus menulis callback, semuanya diganti dengan
panggilan telepon. Hal ini dimungkinkan karena mengikat resource thread virtual, tidak seperti
thread platform, seharusnya murah. Namun, bahkan dengan thread virtual,
mengganti operasi sinkron sederhana dengan pembuatan dan sinkronisasi thread
primitif terlalu mahal. Kami melakukan migrasi dari StateMachine
ke
Thread virtual Java dan jauh lebih lambat, sehingga
peningkatan hampir 3x lipat dalam hal
analisis end-to-end. Karena {i>thread<i} virtual
masih merupakan fitur pratinjau, migrasi ini mungkin dapat dilakukan di
di kemudian hari ketika kinerja meningkat.
Pendekatan lain yang perlu dipertimbangkan adalah menunggu coroutine Loom, jika diperlukan tersedia. Keuntungannya di sini adalah adanya upaya untuk mengurangi {i>overhead<i} sinkronisasi dengan menggunakan tugas {i>multitasking<i} yang bekerja sama.
Jika semuanya gagal, penulisan ulang bytecode tingkat rendah juga dapat menjadi kemungkinan masalah alternatif. Dengan pengoptimalan yang memadai, Anda mungkin dapat mencapai 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 disarangkan di 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 tersarang adalah bahwa {i>stack frame<i} dari tingkat eksternal dapat dipertahankan. Di Java, variabel lambda yang diambil harus berupa final secara efektif sehingga menggunakan variabel-variabel tersebut bisa merepotkan. Bersarang (nesting) yang mendalam adalah dihindari dengan menampilkan referensi metode sebagai kelanjutan, bukan lambda, ditampilkan 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 callback juga dapat terjadi jika injeksi runAfter
pola yang digunakan terlalu padat, tetapi hal ini dapat dihindari dengan
dengan langkah-langkah
yang berurutan.
Contoh: Pencarian SkyValue berantai
Sering kali logika aplikasi membutuhkan rantai dependen Pencarian SkyValue, misalnya, jika SkyKey kedua bergantung pada SkyValue pertama. Memikirkan hal ini secara naif, hal ini akan menghasilkan struktur callback.
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
prosedural di seluruh transisi status: step2
mengikuti step1
. Perhatikan bahwa di sini,
lambda digunakan untuk menetapkan value2
. Ini membuat urutan kode sesuai dengan
urutan 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 mereka
yang diteruskan dalam kode. Hal ini tidak selalu memungkinkan
jika alur kontrol
yang berbeda. Komentar tambahan mungkin dapat membantu dalam kasus tersebut.
Di Contoh: Pencarian SkyValue berantai, referensi metode perantara dibuat untuk mencapai hal ini. Hal ini memperdagangkan peningkatan performa untuk keterbacaan, yang mungkin bermanfaat di sini.
Hipotesis Generasi
Objek Java yang memiliki masa aktif sedang mematahkan hipotesis generasi Java
pembersih sampah memori, yang dirancang untuk menangani objek yang dapat
waktu yang singkat atau objek
yang hidup selamanya. Menurut definisi, objek di
SkyKeyComputeState
melanggar hipotesis ini. Objek tersebut, yang berisi atribut
hierarki yang dibuat dari semua StateMachine
yang masih berjalan, yang di-root pada Driver
memiliki
masa hidup menengah saat ditangguhkan, menunggu komputasi asinkron
untuk diselesaikan.
Tampaknya tidak terlalu buruk di JDK19, tetapi saat menggunakan StateMachine
, terkadang
mungkin untuk mengamati peningkatan waktu GC, bahkan dengan penurunan
sampah memori aktual yang dihasilkan. Karena StateMachine
memiliki masa aktif menengah
mereka bisa dipromosikan ke generasi lama, sehingga menyebabkannya terisi lebih cepat, sehingga
sehingga membutuhkan GC besar atau penuh
yang lebih mahal untuk melakukan pembersihan.
Tindakan pencegahan awal adalah meminimalkan penggunaan variabel StateMachine
, tetapi
tidak selalu mungkin, misalnya, jika suatu nilai diperlukan dalam
negara bagian. Jika memungkinkan, variabel step
stack lokal adalah generasi muda
variabel dan secara efisien melakukan GC.
Untuk variabel StateMachine
, uraikan semuanya menjadi subtugas dan ikuti
pola yang disarankan untuk Menyebarkan nilai antara
StateMachine
juga berguna. Perhatikan bahwa ketika
dengan mengikuti pola, hanya StateMachine
turunan yang memiliki referensi ke induk
StateMachine
, dan bukan sebaliknya. Ini berarti bahwa ketika
anak-anak menyelesaikan dan
memperbarui induk menggunakan callback hasil, turunan secara alami keluar dari
cakupan dan memenuhi syarat untuk GC.
Terakhir, dalam beberapa kasus, variabel StateMachine
diperlukan di status sebelumnya
tetapi tidak di negara bagian yang lain. Akan sangat bermanfaat untuk {i>null<i} referensi
setelah diketahui bahwa mereka tidak lagi diperlukan.
Penamaan negara bagian
Saat menamai metode, biasanya Anda dapat menamai metode untuk perilaku tersebut
yang terjadi dalam metode tersebut. Kurang jelas bagaimana
melakukan ini di
StateMachine
karena tidak ada stack. Misalnya, metode foo
memanggil sub-metode bar
. Dalam StateMachine
, hal ini dapat diterjemahkan ke dalam bahasa
urutan status foo
, diikuti oleh bar
. foo
tidak lagi menyertakan perilaku ini
bar
. Akibatnya, nama metode untuk
keadaan cenderung lebih sempit cakupannya,
berpotensi mencerminkan perilaku lokal.
Diagram pohon konkurensi
Berikut ini adalah tampilan alternatif diagram pada menu Terstruktur konkurensi yang lebih menggambarkan struktur pohon. Blok membentuk pohon kecil.
-
Berbeda dengan konvensi Skyframe tentang {i>restart <i}dari awal saat nilai tersebut tidak tersedia. ↩
-
Perhatikan bahwa
step
diizinkan untuk menampilkanInterruptedException
, tetapi contoh menghilangkannya. Ada beberapa metode rendah dalam kode Bazel yang menampilkan pengecualian ini dan menyebar keDriver
, yang akan dijelaskan nanti, yang menjalankanStateMachine
. Tidak masalah jika tidak mendeklarasikannya untuk ditampilkan saat dan 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, menyebabkan inefisiensi, setiap dependensi memilikiStateMachine
.↩ -
Beberapa panggilan
tasks.lookUp
dalam satu langkah akan ditumpuk bersama. Pengelompokan tambahan dapat dibuat dengan pencarian yang terjadi dalam waktu serentak subtugas. ↩ -
Secara konsep mirip dengan konkurensi terstruktur Java jeps/428. ↩
-
Cara ini mirip dengan membuat thread dan menggabungkannya untuk mencapai komposisi berurutan. ↩