コンテンツにスキップ

Idempotency Is Easy Until the Second Request Is Different

チェック

  • [ ] 本文を確認した
  • [ ] 概要を確認した
  • [ ] タグを確認した
  • [ ] inbox/ 直下へ移行した

概要

HTTP API の冪等性を、単なる Idempotency-Key とレスポンス再送の仕組みではなく、副作用、並行実行、外部プロバイダ、期限切れ、リクエスト内容差分まで含む契約として整理する記事。 特に「同じキーで違う内容のリクエストが来た場合」を、サーバーが明示的に扱うべき重要ケースとしている。 支払い API を例に、キーのスコープ、正規化済みコマンドのハッシュ、実行所有権、IN_PROGRESS、再送可能な失敗、復旧が必要な未知状態などを扱う。 バックエンドの副作用設計、決済、ジョブキュー、分散システムの再試行設計で参照しやすい内容。

本文

記事は、冪等性を「同じキーなら保存済みレスポンスを返す」という単純なリプレイキャッシュとして扱う危うさから始まる。 二度目のリクエストは、完了済みの再試行かもしれないが、最初のリクエストが実行中かもしれない。 ローカル DB には成功を書いたがイベント発行前に落ちたかもしれない。 外部決済プロバイダには受理されたが、自システムに結果を記録する前に死んだかもしれない。 また、同じ Idempotency-Key で金額などの内容が変わっているかもしれない。

記事が強調するのは、冪等性とは「効果」に関する性質であり、重複行を防ぐだけでは足りないという点。 一意制約で支払い行の重複を防げても、クライアントが正しい結果を得られなければ再試行契約としては不十分。 イベント、監査ログ、メール、外部 API 呼び出し、メトリクスなど、ビジネス上意味のある副作用も含めて考える必要がある。

POST /payments のような副作用 API では、永続化された冪等性レコードが少なくとも 3 つの問いに答える必要がある。 誰がこのキーを所有しているか。 最初のコマンドは何を意味していたか。 どの結果を再送できるか。 このため、キー、テナントやアカウントなどのスコープ、操作名、正規化済みリクエストのハッシュ、状態、レスポンスまたは作成リソース参照、期限、ロック期限などを持つテーブルが例示される。

同じキーで違うコマンドが来た場合、著者の推奨はハードエラー。 元の 10 EUR 支払いと、再送に見える 100 EUR 支払いを同じキーで受けたとき、元のレスポンスを黙って返すのはクライアントバグを隠す。 409 Conflict と安定した機械可読エラーを返し、サーバーがキーの意味を曖昧にしないことが重要。

リクエスト同一性の判定では、生の JSON バイト列をハッシュするのではなく、検証・正規化されたコマンドをハッシュする。 フィールド順、空白、デフォルト値、enum の大文字小文字、金額表現、API バージョン、パスパラメータなど、API が同一とみなす意味に合わせる。 このハッシュ計算自体も契約なので、デプロイやスキーマ変更で過去の再試行が別物扱いにならないよう注意が必要。

並行実行では、最初にレコードを読む実装ではなく、原子的な insert-first で実行所有権を獲得する。 同時に 2 台の API インスタンスへ同じリクエストが来た場合、どちらか一方だけが IN_PROGRESS を作成し、実行権を持つ。 既存レコードがあれば、ハッシュ差分、完了済み、実行中、期限切れ、復旧必要状態に応じて明示的に分岐する。

要点

  • 冪等性キーは、リクエスト同一性、結果再送、外部副作用まで含む API 契約である。
  • 同じキーで違う正規化済みコマンドが来たら、黙って再送せず明示的に拒否する。
  • キーはグローバルではなく、テナント、ユーザー、操作名など適切なスコープで一意にする。
  • IN_PROGRESS は内部状態ではなく、同時再試行時の公開設計に関わる。
  • insert-first と一意制約で実行所有権を原子的に取る。
  • 外部プロバイダ呼び出し後に落ちた状態は DB だけでは推論できず、照合・復旧フローが必要になる。

タグ

backend #api-design #idempotency #distributed-systems #database #payments