TT Lab
はじめる
学ぶ 学習パス コース

本番バックエンドAPIキャップストーン

リトライは新しい命令ではない

TT Labで続きを見る

一言でいうと

クライアントがレスポンスを受け取れずに同じリクエストを再送しても、サーバーは同じコマンドとして認識しなければなりません。トランザクションと組織の範囲の冪等キーを併用すると、注文は1回だけ作成され、リトライは最初に作られたリソースを返します。

なぜタイムアウトが重複注文を生むのか

サーバーがコミットした直後にネットワークが切れると、クライアントは成功を知りません。無条件にリトライすると、2つ目の注文ができます。「リクエストを1回だけ送りなさい」という解決策は、分散システムでは守れません。代わりに、クライアントがIdempotency-Keyを送り、サーバーが組織と一緒に保存します。最初の書き込みとキーの記録は同じトランザクションでなければならず、どちらか一方だけが成功する状態があってはなりません。

PostgreSQLのINSERT ... ON CONFLICT (org_id, idempotency_key)は、競合するリクエストも、データベースの直列化の地点でまとめます。先に問い合わせて後から挿入するコードは、2つのリクエストが同時に「なし」を見て、両方とも挿入する競合状態が生じます。衝突した経路は、新しい入力の金額で既存の注文を上書きしてはいけません。元の注文を返して、同じコマンドの結果が安定して維持されなければなりません。

現場での失敗の扱い方

ドライバーの接続とカーソルはコンテキストマネージャーで閉じ、書き込みは明示的なtransactionの境界に置きます。例外が起きたらrollbackを保証し、例外メッセージにSQLの引数や接続文字列を入れません。統合テストは、同じキーを異なる金額で2回送り、最初のレスポンスが201、リトライが200で、IDと元の金額が同じであることを確認します。別の組織は、同じキーを独立して使えなければなりません。

リトライを安全にする3つの仕組み

リトライは、分散システムでは避けられません。問題は、リトライしてよいかどうかを、呼び出す 側が知ることができないという点です。レスポンスを受け取れなかったことと、処理されなかったことは違います。

冪等キーは、リクエスト側が作ります。サーバーが作ると、リトライのたびに変わってしまうので、意味が ありません。クライアントがUUIDを1つ作って、最初の試行から最後のリトライまで同じ値を 送ります。

create table payment_requests(
  org_id      bigint not null,
  idem_key    uuid not null,
  request_sha bytea not null,
  status      text not null check(status in ('처리중','완료','실패')),
  response    jsonb,
  created_at  timestamptz not null default now(),
  primary key (org_id, idem_key));

このコードブロックのstatusの韓国語の値は、順に「処理中」「完了」「失敗」という意味です。

同じキーで異なる内容が来たら、拒否します。request_shaを一緒に保存する理由です。 キーは同じなのに金額が違うなら、それはリトライではなくバグなので、黙って 成功させるのが最悪です。

先に行を確保してから、仕事をします。insert ... on conflict do nothing returning idが 空なら、誰かがすでに始めています。そのときは、その行の状態を見て、完了なら保存された レスポンスをそのまま返し、処理中なら409で返して、しばらく後にもう一度問い合わせさせます。 サーバーで待たせると、その接続が溜まって、別の問題になります。

外部の呼び出しとデータベースのトランザクションを、ひとまとめにしません。決済ゲートウェイを 呼び出している間、トランザクションを開いたままにすると、その遅い呼び出しの分だけロックが維持され、接続が塞がれます。 順序を分けます。短いトランザクションで意図を記録し、トランザクションの外で外部を 呼び出し、再び短いトランザクションで結果を書き込みます。途中で落ちると、意図だけが残った 行が残るので、それを見つけて、相手に状態を問い合わせる復旧の作業も一緒に作ります。

リトライには、必ず上限とジッターを付けます。全員が同じ間隔でリトライすると、障害が 回復する瞬間にまた崩れます。指数バックオフにランダムな要素を混ぜ、合計の試行時間を 決め、その後は失敗として処理して、人が見るようにします。永遠にリトライするキューは、 障害を隠す装置になります。

実務での判断基準

冪等性は、「重複でエラーになる」よりも強い契約です。ユーザーが結果を安全に再び受け取れなければなりません。キーの保持期間、リクエスト本文のハッシュの衝突ポリシー、古いキーの整理も、実際のサービスでは明示します。今回のキャップストーンは、最も重要な作成の境界と衝突の意味を先に証明し、次のモジュールで、これを実際のHTTPステータスと本文の契約として外部に公開します。