冪等キー:再試行を安全にする一回の書き込み

ネットワークのタイムアウトはサーバが何もしなかった証拠ではありません。冪等キーで同一リクエストを識別し、再実行せず最初の結果を返します。

クライアントが支払いを送信し、接続がタイムアウトし、自動で再試行する。最初の試行が実は成功していたら、代金は二度動きます。タイムアウトは失敗ではありません。 これがすべての再試行の前提です。

キーがすること

クライアントは業務上の意図ごとに一つの一意なキー(UUID)を生成し、再試行時に同じキーを使います。

POST /payments
Idempotency-Key: 6f4a1e2c-9b3d-4c8a-9f21-7e5d0b3a1c88

サーバの処理はこうです。その値を一意制約とする行を挿入します。最初のリクエストが実際に実行され結果を保存し、以降の同じキーのリクエストは保存済みの結果を返します。

実装の要点

async function handle(key: string, body: Body) {
  try {
    const row = await db.insert('idempotency', { key, state: 'in_progress' });
  } catch (e) {
    if (!isUniqueViolation(e)) throw e;
    // 既存:保存済み結果を返すか、処理中と伝える
    const existing = await db.get('idempotency', key);
    if (existing.state === 'done') return existing.response;
    return conflict('in progress');
  }
  const result = await doWork(body);
  await db.update('idempotency', key, { state: 'done', response: result });
  return result;
}

見落としがちな三点です。

要点 欠けると
データベース層の一意インデックス 並行要求が両方挿入し、両方実行される
元の応答本文の保存 再試行で再計算が必要になり意味を失う
in_progress 状態の保持 実行中と完了を区別できない

スコープと有効期限

キーは認証主体とエンドポイントに紐づけます。でなければ、あるユーザーのキーが別のユーザーの記録に当たります。

有効期限は業務判断です。支払いは通常一日から数日保持します。短すぎると本当の再試行を取りこぼし、長すぎると表が無限に育ちます。清掃は業務トランザクションの外で行い、実行中の記録を消さないようにします。

リクエスト本文の指紋

厳格な実装では本文のハッシュも保存し、同じキーで内容が違う誤用を検出します。

if (existing.requestHash !== sha256(body)) {
  return unprocessable('key reused with different payload');
}

ここは 409 より 422 が適します。状態の衝突ではなく、クライアントの実装上の誤りです。

不要な場面

もともと冪等な操作にキーは不要です。GET、全体置換の PUT、DELETE。資源を作る POST、金銭を動かすもの、メッセージを送るものには必要です。 判断基準は「二回実行すると副作用が二度起きるか」です。

冪等キーは再試行を安全にするのではなく、「これは同じリクエストだ」とサーバが認識できるようにするものです。

← 記事一覧に戻る

コメント

…