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

統合とデプロイ

応答を失う決済APIを相手にする

TT Labで続きを見る

目標

タイムアウトのあとのリトライが、どのように二重決済を生むかを自分で作ってみて、冪等キーで防ぎます。そして、キーを付けたのにうまくいかない場合まで確認します。

環境

/opt/app/pay.pyは、顧客企業の決済APIを真似たものです。直す対象ではなく、相手側です。

mkdir -p /root/idem
nohup python3 /opt/app/pay.py > /tmp/pay.log 2>&1 &
sleep 1
curl -s http://127.0.0.1:8021/health
POST /payments    결제 하나를 기록한다
                  Idempotency-Key 헤더가 있고 이미 본 키면
                  새로 기록하지 않고 저장해 둔 결과를 그대로 돌려준다
GET  /payments    지금까지 기록된 결제 전부
POST /reset       상태 초기화

このコードブロックの韓国語は、各エンドポイントの説明です。POST /paymentsは決済を1件記録し、Idempotency-Keyヘッダーがあって、すでに見たキーなら、新しく記録せず、保存しておいた結果をそのまま返します。GET /paymentsは、これまでに記録されたすべての決済を返します。POST /resetは状態を初期化します。

偶数番目のリクエストは、処理を終えたあと、応答だけを6秒遅らせます。2秒でタイムアウトをかけると、クライアントは失敗と見なしますが、サーバーにはすでに記録されています。これが「不確実な結果」です。

作るもの

すべて/root/idem/の下です。

naive.sh      멱등키 없이. 타임아웃이면 다시 보낸다
naive.txt     그 결과와 왜 그런지
safe.sh       비즈니스 행위 하나에 키 하나. 재시도에도 같은 키
newkey.sh     시도마다 새 키를 만든다 (일부러 틀린 형태)
newkey.txt    그 결과와 키를 무엇 단위로 만들어야 하는지
backoff.sh    대기 시간 간격을 낸다
retryable.sh  상태를 받아 재시도 여부를 답한다
payments.csv  조사할 결제 기록
forensics.txt 중복을 찾고 원인을 지목한 결과
report.md     정리

このコードブロックの韓国語は、各ファイルの説明です。naive.shは冪等キーなしで、タイムアウトなら再送する、naive.txtはその結果と理由、safe.shはビジネス行為1つにキー1つで、リトライにも同じキー、newkey.shは試行ごとに新しいキーを作る(わざと間違った形)、newkey.txtはその結果と、キーを何の単位で作るべきか、backoff.shは待機時間の間隔を出す、retryable.shはステータスを受け取ってリトライの可否を答える、payments.csvは調査する決済記録、forensics.txtは重複を見つけて原因を指摘した結果、report.mdはまとめです。

採点方法

採点ツールが、毎回サーバーを初期化し、作成したスクリプトを直接実行したあと、記録を数えます。書いた数字ではなく、実際に残った記録を見ます。

naive.sh   기록 > 주문  이어야 한다 (중복이 생겨야 한다)
safe.sh    기록 = 주문  이어야 한다 (정확히 한 번)
newkey.sh  기록 > 주문  이어야 한다 (키가 무력해진다)
backoff.sh 간격이 늘고, 두 번 돌리면 달라야 한다

このコードブロックの韓国語は、naive.shは記録が注文より多くなる必要がある(重複が生じる必要がある)、safe.shは記録が注文と等しくなる必要がある(正確に1回)、newkey.shは記録が注文より多くなる必要がある(キーが無力になる)、backoff.shは間隔が増え、2回実行すると異なる必要がある、という意味です。

ステップ

  1. 決済APIを起動します。
  2. naive.sh: 冪等キーなしで、注文を4件以上決済し、タイムアウトなら再送します。結果をnaive.txtに書きます。
  3. safe.sh: 注文ごとに1つのキーを決め、リトライするときに同じキーを再び使います。
  4. newkey.sh: キーを付けますが、試行ごとに新しく作ります。newkey.txtに、なぜ役に立たないかを書きます。
  5. backoff.sh: リトライの間隔を4つ以上出します。指数的に増え、ランダム性が混ざっている必要があります。
  6. retryable.sh: ステータス(500、429、400、timeoutなど)を引数として受け取り、yesまたはnoを出力します。
  7. payments.csvを作成し、重複を探します。重複した件の時刻の間隔が、原因を語ります。
  8. まとめます。

参考

ステップ7の材料は、次のように作ります。

cat > /root/idem/payments.csv <<'CSV'
payment_id,order_id,created_at
PAY-1,ORD-1,2026-09-07T10:00:00Z
PAY-2,ORD-2,2026-09-07T10:00:05Z
PAY-3,ORD-2,2026-09-07T10:00:06Z
PAY-4,ORD-2,2026-09-07T10:00:08Z
PAY-5,ORD-2,2026-09-07T10:00:12Z
PAY-6,ORD-3,2026-09-07T10:01:00Z
PAY-7,ORD-4,2026-09-07T10:02:00Z
PAY-8,ORD-4,2026-09-07T10:02:01Z
PAY-9,ORD-4,2026-09-07T10:02:03Z
PAY-10,ORD-4,2026-09-07T10:02:07Z
CSV

間隔が1秒・2秒・4秒と倍になっているのが見えれば、ほぼ確定です。

応答を失うAPIを起動する

決済APIを起動します。

/opt/app/pay.pyをバックグラウンドで起動し、/healthが200かを見ます。開始前に/resetで状態を空にします。

タイムアウトを失敗と見なすと

naive.sh: 冪等キーなしで、注文を4件以上決済し、タイムアウトなら再送します。結果をnaive.txtに書きます。

curl --max-time 2でタイムアウトをかけ、失敗したら||でもう一度送ります。注文4件以上。そのあと、GET /paymentsで実際の記録を数えてみてください。

冪等キーで正確に1回

safe.sh: 注文ごとに1つのキーを決め、リトライするときに同じキーを再び使います。

注文ごとにキーを1つ決め、リトライにも同じキーを使います。-H "Idempotency-Key: $K"を、両方の呼び出しに付けてください。

キーを付けたのにうまくいかない場合

newkey.sh: キーを付けますが、試行ごとに新しく作ります。newkey.txtに、なぜ役に立たないかを書きます。

試行ごとに新しいキーを作ってみてください。サーバーは別のリクエストと見なして、また記録します。キーは、「HTTPリクエスト1つ」ではなく「ビジネス行為1つ」に対して作ります。

間隔を広げて、ばらつかせる

backoff.sh: リトライの間隔を4つ以上出します。指数的に増え、ランダム性が混ざっている必要があります。

間隔が毎回大きくなる必要があり(指数バックオフ)、2回実行すると値が違う必要があります(ジッター)。ジッターがないと、すべてのクライアントが同じ時刻に同時にリトライします。

何をリトライし、何をリトライしないか

retryable.sh: ステータス(500、429、400、timeoutなど)を引数として受け取り、yesまたはnoを出力します。

リクエストが誤っているもの(400・422)と、認証情報の問題(401・403)は、何回送っても同じです。429はリトライしますが、Retry-Afterを守ります。

間隔が原因を語る

payments.csvを作成し、重複を探します。重複した件の時刻の間隔が、原因を語ります。

重複した注文を探し、その記録の時刻の差を見てください。1秒・2秒・4秒のように倍に増えるなら、クライアントのリトライです。

まとめ

まとめます。

タイムアウトが何を意味するか、キーを何の単位で作るか、バックオフとジッターがそれぞれ何を防ぐかを、書いてください。