仕組み
Durable Workflow は、Laravel のキュージョブとイベントソーシングによる永続化で、永続コルーチンを実現します。ワークフローは Fiber ベースのヘルパー呼び出しで停止し、永続履歴に従ってリプレイされます。
ランタイム
ワークフローは、handle() メソッドで activity()、await()、timer()、sideEffect()、child()、all([...]) などを直接呼び出すクラスです。各呼び出しは、対応する永続ステップが完了するまでワークフローを停止し、記録された結果を使って再開します。
各ステップは永続履歴イベントを生成します。エンジンはワークフローが起きるたびに履歴をリプレイし、イベント列から状態を再構築して、次の未実行ステップへ進みます。このため、Worker の再起動、デプロイ、マシン障害をまたいでも進行状況を失いません。
WorkflowStub::make() は、公開ワークフローインスタンス ID を予約します。開始時に最初の実行とワークフロータスクを作成します。各実行には独自の run ID があります。signal()、cancel()、terminate() などは、インスタンスの現在の実行を対象にします。
イベントソーシング
イベントソーシングは、保存されたイベント列から現在の状態を再構築します。実行イベントの完全な履歴を保持し、Worker がクラッシュした場合もワークフローの再開に使えます。
コルーチン
コルーチンは、停止して再開できる関数です。永続的な停止点を、activity()、await()、timer()、sideEffect() などの Fiber ベースの呼び出しで表します。
ユーザーコードは通常の handle() メソッドにあり、これらを直接呼び出します。ランタイムは、ステップがすでに永続的に完了したか確認します。完了済みなら履歴の結果を返します。未完了なら次のアクティビティ、タイマー、子ワークフローの作業をキューに入れ、そのステップの完了または失敗まで停止します。
アクティビティ
ワークフローは複数のアクティビティを呼び出し、結果を組み合わせます。アクティビティ呼び出しに到達すると停止し、完了後に続行します。
ワークフロー Worker がクラッシュした場合、確定済みイベントをリプレイして現在の状態を再構築します。同じ入力と出力を使って続行し、決定性を保ちます。未処理のワークフロー失敗は、その実行を終端状態にします。リプレイは失敗済みの実行をリトライしません。
v2 の通常のアクティビティは、永続的なキュー作業です。明示的なローカルアクティビティは、永続履歴とリトライの意味を保ちながら、ワークフロー Worker のプロセス内で短い処理を実行します。通常のアクティビティは任意の互換 Worker で実行できます。同じ Worker のローカルリソースが必要な複数ステップには、Worker セッションの明示的なリースを使います。アクティビティをキューに入れず、一回だけ記録するリプレイに安全な値が必要なら、sideEffect(...)を使います。全体の契約はアクティビティ実行モデルを参照してください。
実行保証
ワークフローとアクティビティでは、繰り返し実行の意味が異なります。
- ワークフローコードはリプレイされます。 ワークフロータスクの再配信は、永続履歴から状態を再構築し、決定的なコードを再び実行します。記録済みの外部副作用は繰り返しません。
- アクティビティは少なくとも一回のキュー作業です。 リトライ、リース期限切れ、Worker の喪失により、同じ論理アクティビティが再配信または再び観測されることがあります。重複配信は分散システムの正常な動作です。
- アクティビティ識別子は永続的です。
activity_execution_idはリトライや再配信をまたぐ論理的なアクティビティを、activity_attempt_idは個別の試行を識別します。外部の冪等キーには前者を使い、下流システムが試行を区別する必要がある場合だけ後者を使います。
公開 v2 契約は、実行保証と冪等性、アクティビティ実行モデル、障害と復旧を参照してください。
キュー
キュージョブは、後でバックグラウンド実行する処理です。Laravel は Amazon SQS、Redis、リレーショナルデータベースのキューに対応します。ワークフローとアクティビティはどちらもキュージョブですが、動作が異なります。ワークフローは通常、複数回ディスパッチされます。実行してアクティビティを派遣し、いったん終了して、アクティビティの完了後に再び実行されます。アクティビティは少なくとも一回のタスクです。一般的には一回の試行で成功しますが、リトライ、リース期限切れ、Worker の喪失で、同じ論理アクティビティが複数回配信されることがあります。
例
use Workflow\V2\Workflow;
use function Workflow\V2\{activity, all};
class MyWorkflow extends Workflow
{
public function handle(): array
{
return [
activity(TestActivity::class),
activity(TestOtherActivity::class),
all([
fn () => activity(TestParallelActivity::class),
fn () => activity(TestParallelOtherActivity::class),
]),
];
}
}
シーケンス図
直列と並列のアクティビティを通じて、ワークフローがどのように進むかを示します。
- ワークフローをキュージョブとしてディスパッチします。
- 最初のアクティビティ
TestActivityをディスパッチし、ワークフロージョブを終了します。アクティビティが結果をデータベースに保存し、ワークフローを再びディスパッチします。 - ワークフローはイベントソーシングのリプレイループに入り、データベースのイベント列から状態を再構築します。ワークフローは常駐プロセスではなく、アクティビティ実行中は終了し、完了後に再びディスパッチされます。
- リプレイ後、次の
TestOtherActivityをディスパッチします。アクティビティが完了すると結果を保存し、ワークフローを再びディスパッチします。 - 再び履歴をリプレイして状態を再構築します。
TestParallelActivityとTestParallelOtherActivityを並列にディスパッチします。両方が結果を保存し、ワークフローへ制御を戻します。- 最後に履歴をリプレイして状態を再構築し、ワークフローを完了します。
決定性
起きるたびに履歴をリプレイするため、同じ履歴から同じコマンドを生成する必要があります。制約で、コードのルールと、Workflow\V2\Workflow の Workflow::now()、sideEffect()、getVersion() など、非決定的になりうる処理を安全に扱う機能を確認してください。