冪等性エンジン:トランザクションの整合性を確保する

SchemaBridge Team · 2025-12-22 · Idempotency, Consistency, Transactions

at-least-once 実行、受信時の重複排除、宛先側のキー。分散ワークフローで重複が実際にどこで止まり、どこがあなたの責任なのかを解説します。

「二重請求」の悪夢:分散システムがリトライを嫌う理由

不安定なネットワークと一時的なクラウドリソースの世界では、障害は頻繁に起こるだけでなく、常態です。シニアエンジニアなら誰しも「孤立したアクション」の悪夢を経験しています。小さなネットワークの不具合から始まり、データ破損や金銭的責任で終わるシナリオです。分散システムの典型的な障害シナリオは次のとおりです。

1. リクエスト:オーケストレーターが決済 API に「課金」リクエストを、あるいは配送業者に「出荷」リクエストを送信します。

2. 成功:サードパーティ API はリクエストを正常に処理し、顧客のカードに課金するか、配送ラベルを印刷します。

3. パーティション:戻りの経路で短いネットワーク障害が発生します。API からのレスポンス、つまり肝心の確認応答がワーカーマシンに届きません。

4. リトライ:ワーカーはタイムアウトを検知し、標準的な慣行に従って処理が失敗したとみなします。リトライポリシーに従い、リクエストを再送します。

5. 重複:API はこの特定の意図に対する一意のアイデンティティを与えられていないため、リクエストを再び処理します。新しいトランザクションとみなすのです。顧客は二重に課金されるか、同じ注文に対して配送ラベルが 2 枚発行されます。

これはコードロジックの失敗ではなく、アイデンティティの失敗です。特定の意図を時間と空間を越えて一意に識別する手段がなければ、システムは接続をリトライするたびに顧客のデータとお金を賭けていることになります。

分散トランザクションは死んだ:冪等性よ永遠に

モノリシックなアーキテクチャでは、一貫性を確保するために Two-Phase Commit (2PC) やグローバルな分散ロックに頼ります。これらのツールにより、複数の操作を単一の真実の単位として扱えます。しかし、SaaS API、サーバーレスワーカー、ポリグロットなマイクロサービスが分散する世界では、グローバルトランザクションは幻想です。スケールせず、膨大なレイテンシーを招き、ほとんどのサードパーティプロバイダーはサポートしていません。組織の境界を越えては実現不可能なリソースの「ロック」を必要とします。

分散一貫性への唯一現実的な道は冪等性です。数学的には、ある操作を複数回適用しても最初の適用以降に結果が変わらない場合、その操作は冪等です。代数的には f(x) = f(f(x)) です。エンジニアリングの観点では、システムが何度失敗してリトライしても、最終結果は常に正しいということです。

誠実な契約:ガード付きの at-least-once

オーケストレーターが約束できることとできないことを正確に整理しましょう。SchemaBridge は作業を at-least-once で配信します。HTTP 呼び出しが成功した後、結果を記録する前にワーカーがクラッシュすることがあります。メッセージが再配信されることもあります。claim のリースが期限切れになり、別のワーカーに引き継がれることもあります。いずれの場合も、ステップを失うよりも再実行するほうが安全な振る舞いです。つまり、ステップがまれに 2 回実行されることがあります。

プラットフォームが行うのは、その「まれに」をできる限りまれにすることで、それを 2 つのレイヤーで行います。

レイヤー 1:受信トリガーの重複排除

エントリーポイントの各トリガーには実行 ID が割り当てられ、デフォルトではその ID はコンテンツから導出されます。ワークスペース、エントリーポイントの vertex、リクエストのペイロードに対する決定論的な SHA-256 です。同じ Webhook 配信が 2 回届いても、2 つ目の実行を開始するのではなく同じ実行に解決されます。送信側がリトライするときに、まさに望ましい挙動です。アイデンティティは 3 つのつまみで制御できます。

| つまみ | 場所 | 使う場面 |

| :--- | :--- | :--- |

| idempotencyFields | エントリーポイント vertex の設定 | ボディ全体ではなくビジネス識別子(例:order.id)をキーに重複排除したい場合 |

| clientIdempotencyId | トリガー呼び出しのクエリパラメーター | 呼び出し側が重複排除キーを管理する場合 |

| forceNewExecution | テスト実行とストリーミング実行の呼び出しのみ(そこではデフォルトで有効) | 本番トリガーでは使えません。本番では idempotencyFields か、呼び出しごとに一意な clientIdempotencyId を使います |

{
  "type": "ENTRY_POINT",
  "config": {
    "idempotencyFields": "order.id"
  }
}

裏返せば、これは実際の落とし穴でもあります。2 つの正当な実行がバイト単位で同一のペイロードを持ちうる場合(「空のボディで夜間レポートを実行する」など)、オプトアウトしない限り 1 つにまとめられてしまいます。スケジュール(cron)トリガーは例外で、発火のたびに新しいランダム ID が割り当てられます。スケジュールは決して重複排除されるべきものではないからです。また、明示的な重複排除は読み取り後に書き込むルックアップであり、データベースの一意性制約ではありません。そのため、同じキーを持つ真に同時のトリガー 2 つが両方とも通過する可能性があることに注意してください。

レイヤー 2:ステップレベルのガード

1 つの実行の中では、すでに COMPLETED のステップは再実行されません。ワーカーはステップを実行する前にアトミックに claim(DISPATCHED → EXECUTING)する必要があり、終端への遷移はガードされ、claim エポックによってリースを引き継がれたワーカーは締め出されます。これらのガードは一般的な重複経路を止めますが、いずれもサードパーティ API の内部に手を伸ばして、すでに行われた呼び出しを取り消すことはできません。

レイヤー 3 はあなたの担当:宛先側のキー

この最後のギャップこそ、プラットフォーム自身の動作契約がはっきりと次のように述べている理由です。副作用は宛先側で冪等にし、オーケストレーターがそれを正確に 1 回だけ実行することに頼らないでください。

幸い、本格的な API(決済、配送、メッセージング)の多くは冪等性キーを受け付けます。そして Gateway ステップは、実行のデータから導出したキーを送信できます。SchemaBridge ではヘッダー値をステップの入力に対する式にできるため、キーはタイムスタンプやランダム値からではなく、安定したビジネス識別子から計算されます。

{
  "type": "GATEWAY",
  "config": {
    "method": "POST",
    "url": "https://api.payments.example/v1/charges",
    "headers": {
      "Idempotency-Key": "= 'charge-' & payload.order.id",
      "Authorization": "Bearer {{ secrets.PAYMENTS_KEY }}"
    }
  }
}

キーは注文だけに依存するため、このステップが再実行されても(再配信後、オペレーターによる Retry、過負荷時の自動リトライのいずれでも)同じキーが送信され、宛先は再度課金するのではなく最初の結果を返します。1 つ注意点があります。Start over(ワークフロー全体の再実行)は新しい実行を作成しますが、キーを実行 ID ではなくビジネスデータから導出していれば、その場合でもキーは安定したままです。だからこそ、エンジンの識別子よりもビジネス識別子のほうがキーとして優れているのです。

レガシー API への対処:「Read-Verify-Write」パターン

残念ながら、多くのレガシーシステムやニッチな SaaS プロバイダーは冪等性キーをサポートしていません。Idempotency-Key ヘッダーがないのです。このような「冪等でない」エンドポイントに対しては、通常のステップを組み合わせて Read-Verify-Write シーケンスを構成できます。

1. 検証ステップ(Read):まず Gateway ステップが下流システムに問い合わせ、レコードがすでに存在するかを確認します(例:GET /orders?external_id=123。キーとして使ったであろうものと同じビジネス識別子を使います)。

2. 条件分岐:条件付きの edge がレスポンスを確認します。注文が存在すれば、実行は「処理済み」の経路を進みます。存在しなければ先へ進みます。

3. アクションステップ(Write):検証ステップが何も見つけなかった場合にのみ、実行は実際の書き込み(POST /orders)に進みます。

ウィンドウを把握する

Read-Verify-Write は重複のウィンドウを狭めますが、閉じはしません。書き込みの後、成功を記録する前にワーカーがクラッシュすると、書き込みステップだけが単独で再実行されます。先に戻って再検証することはありません。したがって、冪等性をまったくサポートしない宛先については、宛先側の自然な一意性制約(競合時に拒否される external_id など)も探し、競合レスポンスを「処理済み」として扱ってください。デフォルトでは 4xx は Gateway ステップを失敗させます。clientErrorMode を complete に設定すると、クライアントエラーのレスポンスが、次のステップがルーティングに使えるデータになります。

「キー管理」の罠:自前の冪等性がスケールで破綻する理由

多くのエンジニアリングチームは、プライマリデータベースに「冪等性テーブル」を構築しようとします。これは、最終的に開発速度と信頼性を損なう 3 つの重大な問題を生みます。

1. 書き込みのボトルネック:すべての API 呼び出しで、トークンを記録するためのデータベース書き込みが必要になります。高負荷下では、冪等性テーブルが競合の発生点になります。

2. クリーンアップの複雑さ、ゴミ問題:冪等性キーは永遠に必要なわけではありません。古いキーを削除するバックグラウンドプロセスや TTL(Time-To-Live)が必要です。削除が積極的すぎると、遅くてリトライされたタスクで二重請求のリスクが生じます。遅すぎると、テーブルは際限なく肥大化します。

3. 分散状態の不整合:データベースへの書き込みは成功したのに API 呼び出しが失敗したらどうなるでしょうか。あるいは、API 呼び出しの後、データベースを「完了」に更新する前にワーカーがクラッシュしたら? 解決に手作業の介入を要する不整合が残ります。

解決策は、自分の側でより賢いテーブルを作ることではなく、副作用を実行するシステムに重複排除を任せることです。冪等性キーを尊重する宛先は、自身の操作についてストレージ、有効期限、クラッシュのウィンドウをすでに解決しています。あなたの仕事は、安定したキーを送ることです。

クライアント側とサーバー側のトークン生成

トークンはどこで生成すべきでしょうか。

SchemaBridge のワークフローでは、意図の発生源は実行自体のデータです。そこで、つまり Gateway ステップのヘッダーでキーを導出すれば、エンジンがとりうるあらゆるリトライ経路でキーが安定します。

比較表:重複はどこで止まるか

| 重複の発生源 | 止める仕組み | 設定する人 |

| :--- | :--- | :--- |

| 送信側が同じ Webhook をリトライ | コンテンツ由来の実行 ID / idempotencyFields / clientIdempotencyId | SchemaBridge(キーはあなたが選択) |

| 実行内でステップが再配信 | ステップレベルのガード(完了済みのショートサーキット、アトミックな claim、claim エポック) | SchemaBridge |

| 呼び出しがすでに成功した後にステップが再実行 | 宛先側の冪等性キー | あなた(Gateway ステップで設定) |

| 宛先が冪等性をサポートしていない | Read-Verify-Write と宛先側の一意性制約 | あなた |

エキスパートのヒント:冪等性チェックリスト

1. タイムスタンプは決して使わない:キーは時刻ではなくデータに基づくべきです。

2. キーにスコープを付ける:操作ごとにキーへプレフィックス(charge-、ship-)を付け、同じ注文 ID を共有していても「配送」のキーが「請求」のキーと衝突しないようにします。

3. 409 Conflict を処理する:API がすでに見たキーや識別子に対して 409(Conflict)を返した場合、入力が一致していれば、ワークフローは通常それを「処理済み」として扱うべきです。

4. 繰り返されるペイロードについて事前に決める:2 つの本物の実行が同一のボディを持ちうる場合は、idempotencyFields を設定するか、呼び出しごとに一意な clientIdempotencyId を渡してください。そうしないと、2 つ目の実行は重複排除で消えてしまいます。(forceNewExecution はテスト実行とストリーミング実行の呼び出しにしかありません。)

5. キーをテンプレートの一部にする:開発者が冪等性キーの追加を覚えておかなければならないなら、いつか必ず忘れます。その API 用に再利用する Gateway 設定にキーを組み込んでおきましょう。

結論:アイデンティティは真実の背骨である

分散システムでは、ネットワークも、時計も、レスポンスも信頼できません。本当に信頼できる唯一のものはアイデンティティです。

SchemaBridge は、自らが制御する境界ではアイデンティティを提供します(コンテンツ由来の実行 ID、明示的な重複排除キー、ステップレベルのガード)。そして制御できない境界、つまりサードパーティ側の副作用については誠実にそう伝えます。ビジネスデータから安定したキーを導出し、すべての呼び出しでそれを送信すれば、「二重請求の悪夢」は後から発覚するインシデントではなく、一度下した設計上の判断になります。

Building the Bridge シリーズの一部です。あわせて読みたい:並列ブランチ間で状態を同期する方法を解説した マージを極める。

詳しく見る