はじめに: マルチステップワークフローの課題

分散システムにおいて、複数のステップを経て完了する処理を考えます。各ステップがすべて成功すれば問題はありませんが、途中のステップで障害が発生した場合、それまでに完了したステップの結果をどう扱うべきでしょうか。 単に「失敗」として処理を終えるだけでは、外部システムに影響を及ぼした状態が残ってしまいます。

例えば、銀行口座間の送金ワークフローを想像してみてください。

  1. A銀行から出金
  2. B銀行に入金
  3. 両口座の所有者にメール送信

ステップ2が失敗した場合、ステップ1で出金されたお金はどうなるでしょうか。A銀行のシステム上でその取引を直接「取り消す」ことはできません。新しい逆方向の操作(補償処理)によって元の状態に戻す、つまりA銀行へ再入金するという操作が必要になります。

このように、順方向の操作とそれに対応する補償ロジックをペアとして管理するパターンを、Sagaパターンと呼びます。本稿では、Cloudflare Workflowsに新しく追加された、このSagaパターンを実現するための公式ロールバック機能について、その設計思想と実装を詳しく解説します。

Cloudflare edge server handling durable workflow requests Technical Structure Concept

本論 1: 従来方式の問題点と新しいrollbackオプション

従来方式: 手動補償ロジックの複雑さ

これまでは、開発者が自前で補償ロジックを実装する必要がありました。失敗時にどのステップが成功したかを追跡し、逆順で補償処理を実行する、という複雑なコードを書く必要があったのです。

// 従来方式: 成功したステップを手動で追跡し、逆順で補償
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) {
  // 逆順で補償実行。各補償は独立したdurable stepであり、
  // 冪等性を持ち、一つが失敗しても続行する必要がある。
  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;
}

この方式には、以下のような大きな問題点があります。

  • コードの複雑化: 成功したステップを追跡する変数と分岐処理のロジックが増えます。
  • ロジックの分散: 各ステップの定義と補償ロジックが分離しており、ワークフロー全体の流れを把握しづらくなります。
  • ミスの可能性: 新しいステップを追加する際に、補償ロジックを書き忘れる可能性があります。

新しい方式: step.do()へのrollbackオプション追加

Cloudflare Workflowsは、step.do()関数にrollbackオプションを提供することで、この問題を解決します。各ステップの定義内に補償ロジックを一緒に宣言できるようになりました。

// 新しい方式: 各ステップに補償ロジックを一緒に定義
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));

このようにすることで、各ステップの順方向の操作と逆方向の補償操作が一つの単位としてまとまります。 新しいステップを追加する際は、そのステップのロールバックロジックも一緒に追加すればよいため、記述漏れのリスクが減ります。また、ステップが失敗すると、Cloudflare Workflowsが自動的に逆順で補償ハンドラーを実行します。

実際の使用例: 冪等性の保証

補償関数を作成する際に最も重要なのは**冪等性(Idempotency)**です。ネットワークエラーなどにより補償関数が複数回呼び出される可能性があるため、同じリクエストを複数回送っても同じ結果になることを保証する必要があります。決済ゲートウェイや在庫システムが提供するIdempotency-Keyを活用するのが一般的です。

const transferId = "transfer-12345"; // ワークフロー実行ID

// 出金ステップ
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`,
      });
    },
  }
);

// 入金ステップ
const credit = await step.do(
  "credit-account-b",
  async () => {
    return await bankB.credit({
      accountId: toAccountId,
      amount,
      idempotencyKey: `${transferId}:credit-account-b`,
    });
  },
  {
    rollback: async ({ output }) => {
      // ステップが失敗しoutputがundefinedの場合があるため、必ず処理する
      if (output === undefined) {
        return;
      }
      await bankB.debit({
        accountId: toAccountId,
        amount,
        idempotencyKey: `${transferId}:rollback-credit-account-b`,
      });
    },
  }
);

// 通知ステップ (失敗時は前のステップをロールバック)
await step.do("send-confirmation", async () => {
  await sendTransferConfirmation({ ... });
});

コード解説:

  • idempotencyKey: 各順方向・逆方向の操作に一意のキーを付与し、リトライ時の重複実行を防ぎます。transferIdにステップ名を組み合わせて使用しています。
  • output引数: rollback関数は{ output }オブジェクトを引数として受け取ります。outputは順方向のステップが成功した場合に返した値です。ステップが失敗しoutputundefinedになる可能性があるため、この処理は必須です。

Cloudflare Workflows saga rollback diagram showing step compensation Algorithm Concept Visual

本論 2: 設計思想、注意点、そして発展的なヒント

なぜrollbackオプションなのか?(API設計思想)

Cloudflareチームは、ロールバック機能を追加するにあたり、いくつかのAPIデザインを検討したとのことです。

  • Fluent API (step.do().rollback()): チェーンが自然に見えますが、step.do()が返すPromiseに.rollback()メソッドを追加することは、Promiseパイプライン(Promise Pipelining)のようなWorkers RPCの重要な機能と衝突する可能性があります。Promiseが実際の結果値の代わりに未来の結果を指す「ハンドル」として使用される状況では、.rollback()がPromiseのメソッドなのか、それとも別の意味なのかが曖昧になります。また、step.do()が呼び出されるタイミングとPromiseが実際に消費されるタイミングがずれる可能性があり、ステップの開始タイミングを予測しづらくなります。
  • Builder API (step.saga().do().rollback().run()): 明確ですが、すべてのステップに.run()を付ける必要があるという煩わしさがあります。また、step.do()を主力APIとして維持したいという思想とも合致しません。

結局、step.do(..., { rollback })というメタデータ方式が選択されました。これは、既存のstep.do()の動作を変更せずに、各ステップのライフサイクルにロールバックという新しい動作を追加する最も自然な方法だからです。

この技術の限界または注意点(批判的視点)

  • ロールバックは「ベストエフォート」です。 ロールバックハンドラー自体が失敗する可能性があり、設定されたリトライ回数をすべて使い果たすと、ワークフローはErrored状態で終了します。ロールバックが常に成功するとは限りません。
  • output === undefinedの処理: 失敗したステップのロールバックハンドラーは、outputとしてundefinedを受け取る可能性があります。これは、ステップが外部システムとやり取りした後、その結果をWorkflowsに返す前に失敗した可能性があるためです。したがって、ロールバックハンドラーはoutputundefinedの場合でも安全に動作するように設計する必要があります。
  • 複雑なトランザクションには不向き: Sagaパターンは、各ステップが独立したトランザクションを持つ分散環境に適しています。単一のデータベース内で強い整合性が求められるトランザクションの場合は、従来のACIDトランザクションを使用する方が適切な場合があります。

技術の適用文脈(日本市場向け)

日本のFinTech、EC、マイクロサービス環境においても、複数のサービスにまたがるワークフローを実装する際にSagaパターンは非常に有用です。例えば、大手ECサイトや金融サービスにおける「注文 -> 決済 -> 在庫引当 -> 配送開始」といったプロセスを管理する際、特定のステップが失敗した場合に前のステップを補償する必要があります。このようなケースでCloudflare Workflowsのロールバック機能を使用すれば、別途状態管理サーバーや複雑な補償ロジックを実装する必要なく、各ステップにロールバックハンドラーを定義するだけで済みます。特に、AWS Step FunctionsやAzure Durable Functionsを利用中の方は、これらと類似した機能をCloudflareのサーバーレス環境でも利用できる点を参考にしてみてください。

次のステップとしての学習指針

  1. 冪等性(Idempotency)の習得: ロールバックロジックの核心は冪等性です。様々な外部API(決済、メール、在庫)で冪等性を保証する方法を学びましょう。
  2. Sagaパターンの様々な変形: Sagaパターンには、Choreography(イベント駆動型)とOrchestration(中央集権型)の2つの主要な実装方法があります。Cloudflare WorkflowsはOrchestration方式に近いです。それぞれの方式の長所と短所を比較してみてください。
  3. ワークフローエンジンの比較: AWS Step Functions、Temporal、Camundaなど、他のワークフローエンジンとCloudflare Workflowsの違いを比較分析してみてください。特に、耐久性(Durability)を保証する方法と実行モデルの違いを理解することが重要です。

Developer configuring Cloudflare Workflows rollback API in code editor Developer Related Image

結論: 実務適用のアドバイスとまとめ

Cloudflare WorkflowsのSagaロールバック機能は、分散トランザクション処理の複雑さをかなりの部分抽象化してくれます。step.do()rollbackオプションを追加するだけで、従来手動で実装していた多くの補償ロジックを置き換えることができます。

実務適用時に必ず覚えておくべき点:

  • ロールバックハンドラーは必ず冪等性を持つように設計してください。
  • outputundefinedになり得る状況を常に考慮してください。
  • ロールバック自体もワークフローの一部であるため、リトライやタイムアウト設定を適切に構成してください。

この機能は、「失敗」を単にエラーとして処理するのではなく、システムを一貫した状態に戻すための戦略を設計することを可能にします。マルチステップアプリケーションを構築しているなら、この機会にSagaパターンの導入を検討してみてはいかがでしょうか。

あわせて読みたい記事:

参考資料: Cloudflare公式ブログ (英語)

本コンテンツは、信頼性の高い情報源をもとにAIツールを活用して作成され、編集者によるレビューを経て公開されています。専門家によるアドバイスの代替となるものではありません。