SkyFrame StateMachines 指南

回報問題 查看來源 夜間 7.2 7.1 7.0 6.5 6.4

總覽

SkyFrame StateMachine 是「已拆解」的函式物件,位於 堆積。可在無須備援的情況下享有彈性和評估能力1。 您無法立即取得必要的值,不過是以非同步方式計算。 StateMachine 在等待期間無法連結執行緒資源,但必須 政策就會暫停並重新啟用解構函式可能會 以便略過先前的運算

StateMachine 可用來表示序列、分支版本、結構化邏輯 而且專為 SkyFrame 互動而設計。 StateMachine 可以組成大型 StateMachine,並與他人分享 子 StateMachine。並行的結構一律是結構體, 完全合乎邏輯每個並行子工作都是在單一共用父項中執行 SkyFunction 執行緒。

簡介

本節會短暫提起並介紹 StateMachinejava.com.google.devtools.build.skyframe.state 套件。

SkyFrame 重新啟動的簡短說明

SkyFrame 是對依附元件圖表執行平行評估的架構。 圖表中的每個節點都對應了具有 SkyKey 指定其參數,以及指定其結果的 SkyValue。 運算模型是,SkyFunction 可以根據 SkyKey 查詢 SkyValues, 觸發其他 SkyFunction 的週期性且平行評估作業。而不是 封鎖,在要求 SkyValue 尚未包含 預備工作,因為部分運算子圖不完整 SkyFunction 會觀察 null getValue 回應,且應傳回 null 而不是 SkyValue,表示由於缺少輸入內容而不完整。 SkyFrame 會在所有先前要求過的 SkyValues 時重新啟動 SkyFunction 可供使用。

SkyKeyComputeState 導入之前,傳統的處理方法 則是重新執行完整運算雖然這會是二次 以這種方式編寫的函式最終會完成 但會傳回 null 的查詢。有了 SkyKeyComputeState,就能 將手動指定的查核點資料與 SkyFunction 建立關聯,這樣就能省下大量成本 會出現 BERT 事件

StateMachine 是存在 SkyKeyComputeState 且消除中的物件 在 SkyFunction 重新啟動時幾乎所有重新計算 (假設 SkyKeyComputeState 不會因存取暫停並繼續作業而從快取中消失 執行掛鉤

SkyKeyComputeState 內的有狀態運算

從物件導向的設計的角度來看,因此最好考慮儲存 SkyKeyComputeState 內的運算物件,而不是純資料值。 在 Java 中,攜帶物件的行為最小描述為 功能介面,而這一切都滿足了。StateMachine 下文、充滿好奇心且定義如下2

@FunctionalInterface
public interface StateMachine {
  StateMachine step(Tasks tasks) throws InterruptedException;
}

Tasks 介面與 SkyFunction.Environment 類似,但 專為非同步作業設計,並新增支援邏輯並行子工作3

step 的傳回值是另一個 StateMachine,能夠指定規格 一致。step 會在DONE StateMachine 已完成。例如:

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;
  }
}

說明具有下列輸出內容的 StateMachine

hello
world

請注意,由於原因,方法參照 this::step2 也是 StateMachine step2 符合 StateMachine 的功能介面定義。方法 是最常在 物件中指定下一個狀態的 StateMachine

暫停與繼續

以符合直覺的方式將運算細分為 StateMachine 步驟,而非 單體函式,提供暫停暫停恢復 運算能力StateMachine.step 傳回時,系統會明確暫停訂閱 點。回傳的 StateMachine 值指定的接續值為 明確的 resume 點。因此可以避免重新計算 便能從中斷的地方接續進行。

回呼、接續與非同步運算

在技術術語中,StateMachine 做為「接續」, 來執行StateMachine 可以不必封鎖,而是改為 使用者從 step 函式 (傳輸 改回 Driver 執行個體。Driver可以 然後切換至準備就緒的 StateMachine,或恢復使用 SkyFrame。

傳統上,「回呼」和「接續」會形成一個概念。 不過,StateMachine 會保留兩者。

  • 回呼 - 說明要儲存非同步結果結果的位置 運算能力
  • 接續 - 指定下一個執行狀態。

叫用非同步作業時需要回呼, 實際作業不會在呼叫 方法後立即發生,如 也就是 SkyValue 查詢的情況回呼應盡可能保持簡單。

接續StateMachineStateMachine 的傳回值。 封裝所有非同步執行 運算作業解析這種結構化做法 可管理回呼。

工作

Tasks 介面提供 StateMachine 以及可查詢 SkyValues 的 API 以及排定並行子工作

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.
}

SkyValue 查詢

StateMachine 會使用 Tasks.lookUp 超載查詢 SkyValues。這些 類似 SkyFunction.Environment.getValueSkyFunction.Environment.getValueOrThrow 和類似的例外狀況處理功能 語意實作不會立即執行查詢,而 而是在執行這項作業前,盡可能批次處理4查詢次數。這個鍵 可能無法立即使用,例如需要重新啟動 SkyFrame 因此,呼叫端會使用回呼指定如何處理結果值。

StateMachine 處理器 (Driver,且橋接到 SkyFrame) 保證這個值會在 從下一個狀態開始範例如下。

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;
  }
}

在上述範例中,第一個步驟會查詢 new Key(),並傳遞 this 為消費者。可以這麼做的原因是 DoesLookup 導入 Consumer<SkyValue>

根據合約,在下一個狀態 DoesLookup.processValue 開始之前,所有 DoesLookup.step的查詢結果已完成。因此,value 在下列情況下可供使用: 您可在 processValue 中存取。

子工作

Tasks.enqueue 會要求在邏輯上同時執行子工作。 子工作也是 StateMachine,可以執行任何一般的 StateMachine 作業 包括以遞迴方式建立更多子工作或查詢 SkyValues 與 lookUp 類似,狀態機器驅動程式可確保所有子工作 完成,才能繼續進行下一個步驟。範例如下。

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.
    }
  }
}

雖然 Subtask1Subtask2 在邏輯上並行,但一切都是在 就能讓「並行」更新 i 不需要任何 以及同步處理功能

結構化並行

由於每個 lookUpenqueue 都必須解析為下一個字元,才能前往下一個 這表示並行作業自然受限於樹狀結構。是 可以建立階層5並行,如下所示 範例。

結構化並行

UML 可得知並行結構形成了樹狀結構。 有替代檢視模式能更有效地顯示 樹狀結構的名稱

非結構化並行

結構化並行就是容易理解的。

組合和控制流程模式

本節舉例說明如何組合多個 StateMachine 特定控制流程問題的相關解決方案

依序狀態

這是最常見也最直接的控制流程模式。評估 如此便可使用 SkyKeyComputeState

分支版本

StateMachine 中的分支狀態可透過傳回不同的 值,如以下範例所示。

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;
  }
  …
}

某些分支版本在提早完成時傳回 DONE 是很常見的情況。

進階依序組合

StateMachine 控制結構無記憶體,因此會共用 StateMachine 這些子任務有時可能會讓人尷尬。讓 M1M2 是共用 StateMachineSStateMachine 執行個體。 其中 M1M2 是序列 <A, S, B><X、S、Y> 和問題在於 S 無法判斷 順利完成 BY,但 StateMachine 未能即時保留 呼叫堆疊本節會說明達成這個目標的幾種技巧。

使用 StateMachine 做為終端機序列元素

無法解決最初提出的問題。只示範 共用的 StateMachine 是序列中的終端時,可構成的組成。

// 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();
  }
}

即使 S 本身是複雜的狀態機器,還是可以運作。

依序組合的子工作

由於已排入佇列的子工作保證會在下一個狀態前完成,因此不會 但有時會稍微濫用子工作機制6

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;
  }
}

runAfter注射

有時候無法濫用 Tasks.enqueue,因為 平行子工作或 Tasks.lookUp 呼叫,必須在 S 之前完成 執行作業。在此情況下,將 runAfter 參數插入 S 即可用來 告知 S 後續步驟。

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;
  }
}

此方法比濫用子工作更簡潔。不過,您同時要套用 從整體上來說,透過 runAfter 建立多個 StateMachine 的巢狀結構就是 Callback Hell 的道路。建議依循循序漸進的方式 具有一般序列狀態的 runAfter

  return new S(/* runAfter= */ new T(/* runAfter= */ this::nextStep))

可以替換成下列內容

  private StateMachine step1(Tasks tasks) {
     doStep1();
     return new S(/* runAfter= */ this::intermediateStep);
  }

  private StateMachine intermediateStep(Tasks tasks) {
    return new T(/* runAfter= */ this::nextStep);
  }

已禁止的替代方案:runAfterUnlessError

在先前的草稿中,我們考慮了會取消的runAfterUnlessError 及早發生錯誤事實上,錯誤通常會因為錯誤而 由包含 runAfter 參照的 StateMachine 執行兩次 由 runAfter 機器本身建立

經過審慎評估後,我們判斷程式碼的統一性 而不是複製錯誤檢查的重要性如果 runAfter 機制無法與 tasks.enqueue 機制,一律需要檢查錯誤。

直接委派

每次有正式狀態轉換時,主要 Driver 迴圈都會推進。 根據合約,進階狀態表示所有先前加入佇列的 SkyValue 查詢和子工作都會在執行下一個狀態前解析。在某些情況下 委派 StateMachine 會導致系統非必要階段,或 會造成不良影響例如,如果委派項目的第一個 step 執行 可與委派狀態查詢平行處理 SkyKey 查詢 階段進展會使按鈕依序排列 執行直接委派,如下例所示。

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;
  }
}

資料流程

前文討論的重點在於管理控制流程。這個 部分說明瞭資料值的傳播。

實作 Tasks.lookUp 回呼

以下範例說明如何在 SkyValue 中實作 Tasks.lookUp 回呼 查詢。本節提供原因和建議 處理多個 SkyValue 的方法。

Tasks.lookUp 回呼

Tasks.lookUp 方法會將回呼 sink 做為參數。

  void lookUp(SkyKey key, Consumer<SkyValue> sink);

慣用的方法是使用 Java lambda 來實作:

  tasks.lookUp(key, value -> myValue = (MyValueClass)value);

其中 myValue 是執行以下動作的 StateMachine 執行個體成員變數: 。然而,相較於 在 StateMachine 中實作 Consumer<SkyValue> 介面 。只有在多次查詢時,lambda 仍非常實用 是模稜兩可的

還有錯誤處理 Tasks.lookUp 超載,類似 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);
  }

以下是實作範例:

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.
    …
  }
}

和不含錯誤處理的查詢一樣,請直接使用 StateMachine 類別 實作回呼可節省 lambda 的記憶體配置。

「錯誤處理」可提供更多細節,但基本上, 錯誤和正常值的傳播卻沒有太大的差異。

使用多個 SkyValue

通常需要多次查詢 SkyValue。這種方法適用於 開啟 SkyValue 的類型。以下範例使用 簡化了原型製作程式碼。

  @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);
  }

Consumer<SkyValue> 回呼實作可以明確共用 因為值類型不同如果事實並非如此 以 lambda 為基礎的實作,或實作 可用的回呼。

StateMachine 秒之間傳播值

目前為止,本文件只說明如何排列子工作中的工作內容, 子工作也需要將值回報給呼叫端。因為子工作 其結果會透過邏輯非同步的方式傳回呼叫端 回呼。如要執行這項作業,子工作會定義 透過其建構函式插入。

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 看起來會如下所示。

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;
  }
}

上述範例示範了幾個要點。Caller必須傳播 並定義其自己的 Caller.ResultSinkCaller 會導入 BarProducer.ResultSink 回呼。在繼續時,processResult 會檢查是否 value 為空值,判斷是否發生錯誤。這是很常見的行為 模式,藉此接受子工作或 SkyValue 查詢的輸出。

請注意,實作 acceptBarError 會主動將結果轉送至 Caller.ResultSink (依錯誤泡泡要求)。

如需頂層 StateMachine 的替代方案,請參閱 Driver 和 連結至 SkyFunctions

處理錯誤

Tasks.lookUp 已有一些錯誤處理的例子: 回呼StateMachines。除了 InterruptedException 不會擲回,但會傳遞到 回呼做為值。這類回呼通常具有專屬或語意,且 只傳回其中一個值或錯誤。

下一節將介紹一些細微但重要的互動,並與 SkyFrame 互動 錯誤處理機制

錯誤啟動 (--nokeep_going)

在錯誤啟動期間,即使未要求所有要求,SkyFunction 仍可能會重新啟動 可使用 SkyValues。在這種情況下,後續狀態一律不會 已因 Tasks API 合約而達成。不過,StateMachine 應 仍會傳播例外狀況

因為無論達到下一個狀態,傳播作業都必須進行 錯誤處理回呼必須執行這項工作。對於內部 StateMachine: 方法是叫用父項回呼。

在頂層 StateMachine (與 SkyFunction 的介面) 中,這可 方法是呼叫 ValueOrExceptionProducersetException 方法。 ValueOrExceptionProducer.tryProduceValue 也會擲回例外狀況,即使 缺少 SkyValues。

如果是直接使用 Driver,請務必檢查 即使機器尚未完成,仍可從 SkyFunction 傳播錯誤 和資料處理之間

事件處理

針對需要發出事件的 SkyFunctions,系統會插入 StoredEventHandler 並進一步插入需要用到 SkyKeyComputeState 的 StateMachine 具體做法是指示 Kubernetes 建立並維護 一或多個代表這些 Pod 的物件過去由於 SkyFrame 下降,因此需要使用 StoredEventHandler 的特定事件,除非這些事件再次發生,不過之後又修正了。 保留 StoredEventHandler 插入,因為這麼做簡化了 處理錯誤處理回呼所產生的事件。

Driver 並橋接至 SkyFunctions

Driver 負責管理 StateMachine 的執行作業。 開頭為指定的根 StateMachine。因為 StateMachine 可以 子工作 StateMachine 會以遞迴方式排入佇列,單一 Driver 可以管理 許多子工作這些子工作會建立樹狀結構,結果是 結構化並行Driver 會批次處理 SkyValue 查詢所有子工作的查詢,進而提升效率。

許多使用 Driver 建構的類別,且使用下列 API。

public final class Driver {
  public Driver(StateMachine root);
  public boolean drive(SkyFunction.Environment env) throws InterruptedException;
}

Driver 會將單一根層級 StateMachine 做為參數。撥號中 Driver.drive 會盡可能執行 StateMachine,不使用 重新啟動 SkyFrame。如果 StateMachine 完成且傳回 false,則會傳回 true 否則代表無法使用所有值。

Driver 會保留 StateMachine 的並行狀態,因此很好 適用於 SkyKeyComputeState 的嵌入功能。

直接為 Driver 執行個體化

StateMachine 導入作業會傳統透過以下項目傳達成果: 回呼函式。您可以直接將 Driver 例項化,如 範例。

Driver 已嵌入 SkyKeyComputeState 實作中, 實作對應的 ResultSink,以便進一步定義 下降。在頂層,State 物件是該物件的適當接收器 計算結果,因為系統保證會超過 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;
  }
}

下方程式碼是 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;
  }
}

然後延遲計算結果的程式碼可能如下所示。

@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;
}

正在嵌入「Driver

如果 StateMachine 產生值,且未引發任何例外狀況,嵌入 Driver 是另一個可行的實作,如以下範例所示。

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 的程式碼可能如下所示 (其中 State 是 函式專屬類型 (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;
}

StateMachine 實作中嵌入 Driver 更適合 SkyFrame 的同步編碼樣式。

可能會產生例外狀況的 StateMachines

否則,也有 SkyKeyComputeState 可以嵌入 ValueOrExceptionProducerValueOrException2Producer 類別具有同步 API 以供比對 同步的 SkyFunction 程式碼

ValueOrExceptionProducer 抽象類別包含下列方法。

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. }
}

這包含嵌入式 Driver 執行個體,類似於 嵌入驅動程式和介面中的 ResultProducer 類別 以類似的方式使用 SkyFunction與其定義 ResultSink 發生其中一種情形時,系統會呼叫 setValuesetException。 當這兩種情況發生時,以例外狀況為優先。tryProduceValue 方法 將非同步回呼程式碼橋接至同步程式碼,並擲回 但當您設定這類物件時

如先前所述,在錯誤泡泡期間,可能發生錯誤 即使是機器也可能尚未完成 因為並非所有輸入都能取得目的地: 因此,tryProduceValue 會擲回任何已設定的例外狀況,即使 都沒問題

Epilogue:最終移除回呼

StateMachine 是效率極高,但需要大量樣化的樣板來執行 非同步運算接續 (尤其是 Runnable 的形式) 傳遞至 ListenableFuture) 在 Bazel 程式碼的某些部分廣泛, 但對於 SkyFunctions 的分析而言較不重要分析主要取決於 CPU 限制 沒有高效率的非同步 API 用於磁碟 I/O。最終 最好將回呼最佳化,因為回呼會經過學習,且會阻礙 且可讀性高

Java 虛擬執行緒是最具潛力的替代方案之一。而不是 不必編寫回呼,所有內容都會替換為同步、封鎖 呼叫。這可能是因為此資源連結虛擬執行緒資源,與 所謂平台執行緒,就應節省成本。但即使有了虛擬執行緒 以建立執行緒和同步處理作業取代簡易的同步作業 基元太貴了我們已將 StateMachine 秒的資料遷移至 Java 虛擬執行緒和數量級的處理速度較慢, 端對端分析的延遲時間幾乎是 3 倍。因為虛擬執行緒 這項遷移作業可在某個位置執行 等到績效改善時再新增日期。

另一個考量方式是等待「Loom」協同程式 (如果有的話) 可供使用。這種做法的好處是或許可以減少 以便進行多工處理。

如果其他方法都故障,低階位元碼重寫也可以是可行的做法 。若最佳化程度夠高,或許也能 改用手動編寫回呼程式碼的效能。

附錄

回歸地獄

回呼堆積是使用回呼的非同步程式碼中惡名昭彰的問題。 原因在於後續步驟的接續步驟為巢狀結構 即可。如果有多個步驟 深度。如果加上控制流程,程式碼就無法管理。

class CallbackHell implements StateMachine {
  @Override
  public StateMachine step(Tasks task) {
    doA();
    return (t, l) -> {
      doB();
      return (t1, l2) -> {
        doC();
        return DONE;
      };
    };
  }
}

巢狀結構的一個優點是, 外部步驟可保留下來。在 Java 中,擷取的 lambda 變數必須是 最終,這樣的變數使用起來可能很麻煩。深層巢狀結構是 會避免透過傳回方法參照做為接續的結果,而非 lambda 如下所示。

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;
  }
}

如果 runAfter 插入,也可能發生回呼回歸法 模式的使用過於密集,但可以透過穿插插入即可避免 依序步驟

範例:鏈結 SkyValue 查詢

大多數情況下,應用程式邏輯需要依附元件鏈結 例如,如果第二個 SkyKey 依賴第一個 SkyValue,則執行 SkyValue 查詢。 以單純的方式思考,這種做法會使巢狀結構的階層更加複雜 回呼結構。

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;
}

不過,由於接續會指定為方法參照,因此程式碼看起來是 狀態轉換的程序:step2 遵循 step1。請注意,這裡的 lambda 的用途是指派 value2。如此一來,程式碼的順序與 計算順序。

其他訣竅

可讀性:執行作業排序

為提升可讀性,請努力保留 StateMachine.step 實作項目 依照執行順序和回呼實作。 。因此控制流程並非面面俱到 如果是這種情況,其他意見可能有所幫助。

範例:鏈結 SkyValue 查詢中, 為達成此目標,我們建立了中繼方法參考。這意味著 但值得注意的是

生成假設

中型 Java 物件會破壞 Java 的生成假設 垃圾收集器專為處理 只能長短時間,或是永遠存在根據定義 SkyKeyComputeState違反這個假設。這類物件,包含 所有仍在執行中的 StateMachine (根層級為 Driver) 的建構樹狀結構,已 因其暫停而暫停的中階生命週期,等待非同步運算 以完成。

在 JDK19 中似乎不太糟,但使用 StateMachine 時,有時可能會 可以觀察到 GC 時間的增加 實際產生的垃圾資訊因為 StateMachine 的生命週期中位數 只要將模型升級為舊版,就能更快裝滿 並不需要用到更昂貴的主要或完整的 GC 清理。

初期預防措施是盡量減少使用 StateMachine 變數,但 並非每次都可行,例如在多個多個 州。如果可以,本機堆疊 step 變數為年輕世代 GC 的程式碼

如果是 StateMachine 變數,請將項目分成多個子工作,並遵循 將值傳播於 StateMachines 也提供相關資訊。請留意 遵循這個模式,只有子項 StateMachine 參照了父項 StateMachine,反之亦然。也就是說,隨著孩子 使用結果回呼來更新父項,而子項自然地退出 且符合使用 GC 的資格

最後,在某些情況下,先前的狀態需要使用 StateMachine 變數 但之後狀態不會出現對大型大型檔案的參照來說,可能很有利 已知不再需要的物件

命名狀態

為方法命名時,通常可以為行為命名方法 發生什麼情況實作起來不太清楚 StateMachine,因為沒有堆疊。例如,假設 foo 方法 會呼叫子方法 bar。在 StateMachine 中,這個值可轉譯為 狀態序列 foo,後面接著 barfoo 不再包含行為 bar。因此,狀態的方法名稱通常範圍較小 可能反映出當地行為

並行樹狀圖

以下是「結構化圖片」中圖表的替代檢視模式 以更合適的方式呈現樹狀結構的結構。 方塊會形成一棵小樹。

結構化並行 3D


  1. SkyFrame 自從如何從頭開始 但無法使用這些值。 

  2. 請注意,step 可以擲回 InterruptedException,但 範例會省略此項。Bazel 程式碼中有幾個低的方法 並套用至 Driver,稍後將說明 執行 StateMachine 的指令您可以在 無謂

  3. 同時執行的子工作是由「ConfiguredTargetFunction」提供,: 會為每個依附元件執行「獨立」工作。與其進行遷移 處理所有依附元件的複雜資料結構 如果效率不彰,每個依附元件都有各自的 StateMachine

  4. 單一步驟中的多個 tasks.lookUp 呼叫會批次處理。 並行查詢則可建立額外的批次 子工作。 

  5. 這在概念上與 Java 的結構化並行類似 jeps/428。 

  6. 這與產生執行緒並彙整,藉此達到目的 序列組成