このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
Cloudflare Workflowsは、長期間実行されるプロセス全体で、組み込みのリトライ処理と状態の永続性を備えた、耐久性のあるマルチステップアプリケーションを構築することができます。Workflowが実行されると、各ステップは外部システムを呼び出し、失敗を再試行し、再起動時に状態を保持することができます。しかし、1つのステップが失敗すると、完了したステップから以前の作業が一貫性のない、または部分的な状態になる可能性があります。
本日、Workflowsのサガロールバックを出荷します。これにより、障害が発生した場合に、ステップ自体でロールバックロジックを宣言できるようになります。
たとえば、2つの異なる銀行の口座間で資金移動を行うワークフローがあるとします。
A銀行の口座からの借方
B銀行の口座に振り込み
両方のアカウント所有者に確認メールを送信する
ステップ2(B銀行の口座への貸方)が失敗した場合はどうなりますか?A銀行で借方に成功すると、取引がコミットされ、お金がシステムを離れます。取引のオーケストレーターとして、A銀行のシステムで操作を単純に「元に戻す」ことはできません。この代わりに、最初の操作と逆の操作を意味する新しい操作を通じて、A銀行の口座に返金する必要があります。
このような操作と報酬ロジックの組み合わせは、サガパターンと呼ばれます。
今日まで、開発者は、��テップの直接的な定義以外に、何が成功し、何が失敗し、失敗した場合にどのような行動をとるべきかを追跡するために、独自の報酬ロジックを実装する必要がありました。これで、各step.do()に対して報酬ロジックを定義できるようになりました。ステップ自体内の引数として、ロールバックに対するワークフローの耐久性を維持します。
// track what completed so we know what to undo
let debitA;
let creditB;
try {
debitA = await step.do("debit-bank-a", () => bankA.debit(from, amount));
creditB = await step.do("credit-bank-b", () => bankB.credit(to, amount));
await step.do("notify", () => notifyBoth(from, to, amount));
} catch (error) {
// unwind in reverse. each undo is its own durable step,
// must be idempotent, and must keep going if one fails.
if (creditB) {
try {
await step.do("reverse-credit-b", () => bankB.debit(to, amount, creditB.id));
} catch (e) {
await alertOnCall("reverse-credit-b failed", e);
}
}
if (debitA) {
try {
await step.do("refund-debit-a", () => bankA.credit(from, amount, debitA.id));
} catch (e) {
await alertOnCall("refund-debit-a failed", e);
}
}
throw error;
}
ロールバックなし
// each step ships with its own undo. add a step,
// add its rollback right here. no growing catch
// block, no manual ordering, no replay logic.
await step.do("debit-bank-a", () => bankA.debit(from, amount), {
rollback: async ({ output }) => bankA.credit(from, amount, output.id),
});
await step.do("credit-bank-b", () => bankB.credit(to, amount), {
rollback: async ({ output }) => bankB.debit(to, amount, output.id),
});
await step.do("notify", () => notifyBoth(from, to, amount));
ロールバック機能あり
試してみる
ロールバックを使用するには、rollback関数を含むオプションオブジェクトを、step.do()の最後の引数として渡すだけです。
const debit = await step.do(
"debit-account-a",
async () => {
return await bankA.debit({
accountId: fromAccountId,
amount,
idempotencyKey: `${transferId}:debit-account-a`,
});
},
{
rollback: async () => {
await bankA.credit({
accountId: fromAccountId,
amount,
idempotencyKey: `${transferId}:rollback-debit-account-a`,
});
},
}
);
// The idempotency keys make both the forward operations and rollback operations safe to retry without duplicating the transfer
const credit = await step.do(
"credit-account-b",
async () => {
return await bankB.credit({
accountId: toAccountId,
amount,
idempotencyKey: `${transferId}:credit-account-b`,
});
},
{
rollback: async ({ output }) => {
if (output === undefined) {
return;
}
await bankB.debit({
accountId: toAccountId,
amount,
idempotencyKey: `${transferId}:rollback-credit-account-b`,
});
},
}
);
// If we fail here, we may want to revert all previous payments. Users should not have to wrap their code in complex try-catch logic just to revert two small payments (see below)
await step.do("send-confirmation", async () => {
await sendTransferConfirmation({ ... });
});
ロールバック関数は、通常のWorkflowステップと同様に、か部門である必要があります。料金を払い戻しる場合は、支払プロバイダーの識別キーを使用します。インベントリをリリースする場合、リリースを複数回呼び出すことができるようにします。
いずれかのステップが失敗すると、ロールバックハンドラは逆のステップ-開始順序で実行されます。それはシンプルに聞こえます。何かが失敗する時に、元に戻すステップを実行するだけです。実際には、APIと実行モデルを重要にする詳細がいくつかあります。
1.失敗したステップは、まだロールバックが必要である場合があります。失敗したstep.do()ロールバックハンドラを登録すれば、ロールバック対象になることができます。
ユーザーコードがエラーを検知してWorkflowが続行する場合は、ロールバックは開始されませんが、ステップエラーが検知されて、その後Workflowが別の理由で失敗した場合、ロールバックは以前登録されたハンドラに対してまだ実行できます。これは逆のstep-start順序で実行されます。
なぜでしょうか?このステップは、外部システムと部分的に対話している可能性があります。たとえば、決済プロバイダーは請求をキャプチャできますが、WorkflowsにchargeIdを返す前に、このステップが失敗することがあります。そのため、ロールバックハンドラーはoutputを受け取りますが、output === undefinedを処理しなければなりません。
2. ロールバックは、Workflowが失敗した場合にのみ始まります。ロールバックハンドラーを追加しても、すべてのステップエラーがロールバックをトリガーするわけではありません。ユーザーコードがエラーを検知して続行する場合、Workflowは続行します。ロールバックは、Workflow自体が最終的に障害に直面しつつある時点から始まります。
ロールバックが開始されると、Workflowsは条件を満たすstep.do()を見つけますロールバックハンドラを実行し、最終的なWorkflowの障害を記録します。
3. 順序は予測可能である必要があります。時系列のWorkflowsでは、ロールバック順序は当然のことのように感じられます:
在庫確保。
請求カード。
配送を作成します。
出荷に失敗した場合は、カードを返金し、在庫をリリースします。
パラレルステップを踏むことで、これはより巧妙になります。完了順序は開始順序と異なる場合があるため、Workflowsは、逆の完了順序ではなく、逆のステップ開始順序を使用します。
実用的なルールは次のとおりです。
ロールバックハンドラを使用して開始または完了したステップが対象です。
ロールバックハンドラーを登録されている場合、失敗した
step.do()も対象となります。ハンドラは完了順ではなく、逆のステップ開始順で実行されます。
APIの設計方法
想定される動作を想定したら、この新しいパターンをWorkflows APIに追加する必要がありました。ロールバックは、ロールバックオプションにたどり着くまでに、いくつかのイテレーションを経ました。
なぜAPIが流暢またはビルダーのAPIではないのか?
最初のアプローチは、流暢な形式でした。step.do(...).rollback(...) 読みやすいです。フォワードアクションと報酬は隣にあり、呼び出しサイトは通常のJavaScriptの連鎖のように見えます。
問題は、step.do() です。耐久性のあるステップを開始し、ステップ出力のPromiseを返すため、すでに重要な意味を持っています。Workersでは、Workers RPCがCap'n Proto のようなシステムから継承されたパターンであるpromise pipeliningをサポートしているため、promiseのような値が特に意味を持ちます。
約束パイプラインを使用すると、コードは将来の値が呼び出し元に完全に返される前に、将来の値に対してメソッドを呼び出すことができます。たとえば:
const session = api.authenticate(apiKey);
const name = await session.whoami();
ここでは、セッションはまだ実際のセッションオブジェクトではありません。これは、まもなく存在するセッションのハンドラーのようなものです。session.whoami()を呼び出すと、Workersは、この呼び出しをリモート側に早く送信して、「認証がセッションを作成したら、whoami()を呼び出してください」と言うことができます。
これにより、ラウンドトリップが節約できます。呼び出し元は、authenticate()が完全に終了するのを待ってからwhoami()を問い合わせる必要がありません。
Cloudflareは、流暢なAPIと考えました。
step.do("charge-card", chargeCard).rollback(refundCharge);
読者には、それが「charge-cardの結果に対して.rollback() を呼び出す」ことのように見えるかもしれません。ただし、ロールバックはステップの出力の一部ではありません。これは、step.do()の一部です。ステップが開始する前に登録されているため、Workflowsは、後のステップが失敗した場合にそのステップを修正する方法を知っています。
また、流暢なAPIは、ステップのタイミングを考えるのを難しくします。現在、step.do()呼び出されたときにステップを開始するため、開発者はステップを開始してから他の作業を行い、後で最初のステップを待つことができます。
const first = step.do("first", () => serviceA.call());
await step.do("second", () => serviceB.call());
await first;
現在の実行モデルでは、1つ目はすぐに始まり、2番目です。流暢なAPIはそれをさらに複雑にします。Workflowsは、.rollback() を実行するかどうかを確認するために待機する必要があります。完全なステップ定義を知る前に付加されます。それにより、ステップがエンジンに送信される時間が遅れる可能性があります。
先の例では、secondの完了後、firstはstep.do("first", ...)からではなく、await firstから開始される可能性があります。
このため、同時実行されるWorkflowsの理解が難しくなります。ステップのタイミングは、返されたPromiseが消費されるタイミングだけでなく、step.do()が呼び出される場所にも依存することになります。
また、ビルダースタイルのAPIも検討しました。
const charge = await step
.saga("charge")
.do(() => chargeCard())
.rollback(() => refundCharge())
.run();
ビルダーAPIは、Promiseの曖昧さを回避します。また、将来のステップレベルの選択肢が明確で、フォワードアクションとロールバックアクションが同じ収集ステップに属していることも明確になっています。
しかし、それはセレモニーを増すことにもなります。各ステップの最後には必ず.run()を記述する必要があります。.run()を書き忘れることは容易ですが、ツールを使わなければそのミスに気づくのは困難であり、単純な1ステップのケースでさえ、設定の連鎖のように見えてしまうようになります。また、新しいstep.saga()ビルダーが導入され、従来のstep.パターンから脱却しています。何よりも重要なのは、これによりstep.do()が、Workflowsの主要なプリミティブというよりは、むしろ古いAPIのように感じられてしまう点です。ロールバックの目的は、step.do()を拡張することであり、置き換えることではありませんでした。
ステップメタデータとしてロールバック
step.do(..., { rollback })
最終的に、ロールバックがステップのメタデータになる明示的な形式を選びました。
このように、各ロールバックは前進ステップ自体の中で定義されます。各ハンドラは、ロールバック開始の原因となったエラー、ステップコンテキスト、および出力を受け取ります。これらは、フォワードステップによって返された永続的な値(未定義である場合があります)、またはステップが値を永続化する前に失敗した場合は未定義となります。
ロールバックはライフサイクルイベントを発生させるため、代替が開始されたか、どのロールバックハンドラが失敗したか、そしてロールバックが正常に完了したかを把握できます。
重要なのは、元のWorkflowの障害が別個に残っていることです。ロールバックは障害発生後にWorkflowsが行うことであり、Workflowの障害が発生した理由ではありません。
WorkflowStepConfigを介して ステップ設定でカスタムの再試行動作とタイムアウト動作を定義できるのと同様に、rollbackConfigにロールバック固有の値を追加します。
{
rollback: async ({ output }) => {
await bankA.credit({ accountId: fromAccountId, amount, transferId: `${transferId}-reversal` });
},
rollbackConfig: {
retries: { limit: 10, delay: '30 seconds', backoff: 'exponential' },
timeout: '2 minutes',
},
}
これは、当社が求めていたライフサイクルイベントのメンタルモデルに一致しています。step.do()は、Workflowsが記録し、再試行し、そして後でログに表示する耐久性のある作業単位をすでに記述しています。ロールバックは、同じ作業単位の別のライフサイクル動作です。別のラッパーやビルダー内ではなく、ステップ定義と共に移動する必要があります。
ステップは、
step.do()が通常開始されるときに開始されます。返された約束は、依然としてステップの出力を表します。
同時実行Workflowコードは、同じ実行モデルを保持します。
ロールバックハンドラの横にあるロールバックライブのリトライとタイムアウトのオプション。
既存の
step.do()の呼び出しは、現在とまったく同じように動作し続けます。
この形は、流暢なAPIよりも若干明示的ですが、この明確さは便利です。オペレーションとその報酬は依然として一箇所にあり、APIは新しいステップビルダーや新しい種類のプロミスを導入するものではありません。すでにstep.do()を理解している開発者は、追加でoptionsオブジェクトを1つ学ぶだけで済みます。
これは魔法のようではありませんが、採用が簡単で、理解しやすいのです。
内部の仕組み
ロールバックは小さなAPIの追加のように感じられますが、各ステップについてWorkflowsが記録する必要があるものが変化します。
定期的なstep.do()すでに耐久性のあるレコードがあります。Workflowsは、ステップが開始されたか、完了したか、返されたか、またWorkflowが後で再開された場合に繰り返されるべきかどうかを記録します。
ロールバックは、そのレコードにもう1つ、ステップが報酬ロジックを登録済みかどうかを追加します。
つまり、Workflowsが失敗した場合に、Workflowsは2つの情報をまとめなければなりません。
1つ目は、耐久性のあるステップ履歴です。Workflowエンジンは、実行された内容、完了した内容、保存された出力内容、ロールバックが登録されたかどうかを把握するためのデータを保存します。
2つ目は、ロールバックハンドラ自体で、そのステップを補完するために書かれた関数です。Workflowsは、その関数のテキストをデータとして保存しません。その代わり、Workflowの実行中に、ハンドラへの呼び出し可能な参照を保持します。
Workers RPCでは、この種の呼び出し可能な参照をスタブと呼びます。スタブリ込みとは、システムの一部が、別の場所で実行中のコードを呼び出すことを可能にします。また、スタブには有効期限があり、呼び出しや実行のコンテキストが終了すると処分できるようになっています。その時点を超えてスタブを保持する必要がある場合、Workers RPCは、同じターゲットに別のハンドルを作成するdup()メソッドを提供します。
ロールバックには、このモデルが便利です。耐久性のあるステップ履歴に、報酬が必要なものを記録。ロールバックスタブにより、Workflowsは報酬コードを呼び出す方法を得ることができます。また、ロールバックハンドラーは、即時のstep.do()よりも長く存続する必要があるため、Workflowsは、ロールバックフェーズのハンドラへの独自の呼び出し可能な参照を保持します。
一般的なケースでは、Workflowが同じエンジン有効期間内にロールバックに入る時、Workflowsには既に必要なロールバックスタブがあります。耐久性のあるステップ履歴を使用して対象となるステップを見つけ、フォワード実行中に登録されたロールバックスタブを呼び出すことができます。
これは、再起動後にWorkflowsが回復する必要がある場合、より微妙になります。
ロールバックが必要な時にエンジンがエビクション、クラッシュ、または再起動した場合、Workflowsにはまだ耐久性のあるステップ履歴がありますが、メモリ内ロールバックスタブはなくなる可能性があります。Workflowsは、回復するために、リプレイを使用します。これは、完了したフォワードステップ本文を再実行することなく、Workflowコードを再実行できるリカバリーモードです。
リプレイが完了したstep.do()に達すると、Workflowsは、ステップ本文を再度実行する代わりに、永続化された結果を読み取ります。ロールバックリカバリーの場合、Workflowsは、ロールバックが付加されたステップのリビルドハンドラのみを必要とします。これらのstep.do()はロールバックオプションにより、呼び出し可能なスタブを再び登録��きます。
これにより、Workflowsは、元の外部のサイド効果を重複させることなく、必要なロールバックハンドラを回復することができます。
これらの要素が整っていれば、ハンドラがメモリ上にまだ存在している場合でも、リカバリ中に再構築する必要がある場合でも、ロールバックは正常に機能します。
ワークフローが失敗しそうになった場合、Workflowsはアプリケーションに対して、何が起きたのかを再現するよう要求することはありません。すでにステップ履歴があります。保持された記録を見て、重要な質問に答えることができます。
どのステップから始まりましたか?
完了したステップは?
失敗したステップのどれが、まだクリーンアップが必要かもしれませんか?
登録されたロールバックハンドラはどのステップか?
各ロールバックハンドラはどの出力を受信すべきか?
報酬はどのような順序で実行されるべきか?
次に、Workflowsは、ロールバックコンテキスト(オリジナルエラー、ステップコンテキスト、ステップ出力(永続化されている場合))を使用して、各ロールバックスタブを呼び出します。
順序の細部が重要です。通常のJavaScript、特にPromise.all()では、完了順序は、開始順序と同じではありません。ステップAが最初に始まり、ステップBが2番目に始まる場合、ステップBが最初に完了するかもしれません。ロールバックの際、Workflowsは、保存された開始順序を安定した信頼できる情報源として使用し、逆に展開します。
ロールバックハンドラーは、Workflowsの通常のステップ機械も実行します。つまり、報酬は、Workflowsに期待されるのと同じ運用プロパティ(リトライ、タイムアウト、ライフサイクルイベント、ログ、記録された最終的な結果)が得られるということです。ロールバックハンドラーが設定されたリトライ後に失敗し続けると、Workflowsはロールバックの結果を失敗として記録し、残りのロールバックハンドラの実行を停止し、Workflowインスタンスは最終的にErrored状態になります。
これは、サガロールバックとキャッチブロックの主な違いです。Catchブロックは、JavaScript実行の正確な時点でまだメモリにあるものだけがわかります。Workflowsのロールバックは、残っているステップ履歴を使用して、すでに何が起こったかを判断し、一般的なケースではすでにあるスタブを呼び出し、必要な場合はリカバリ中に不足しているスタブリを安全に再構築します。
また、それが理由で、このAPIではstep.do()自体にロールバック機能が実装されています。ロールバックは、別のグローバルなエラーハンドラーではありません。これは、Workflowsがすでに理解しているワークの耐久性のある単位にメタデータが付加されるものです。
今後の展開は?
ロールバックの最初のイテレーションには以下の内容が含まれます:
step.do()の明示的なステップごとのロールバックハンドラ順次ロールバック実行
修正のための設定再試行とタイムアウト
次に、以下を探ります。
waitForEventのロールバックサポート並列ロールバック実行のサポート
Python Workflowsのサポートをロールバックする
マルチステップのアプリケーションが途中で失敗したとき、最も大変なのは、失敗したことに気づいていないことが多いのです。すでに何が起こったのか、次に何が起こる必要があるのかを知ることです。
SaaSロールバックを使用すると、その答えを各ステップの横に直接配置することができます。Workflowsでマルチステップのアプリケーションを構築している場合、s頻度のロールバックを試し、次に必要な報酬パターンを教えてください。Workflowsドキュメントの利用を開始し、Cloudflareコミュニティでフィードバックを共有してください。




