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

冪等性 — 二度押しても決済は一度だけ

冪等なリクエストにも再試行の予算が必要

TT Labで続きを見る

目標

ステータスコード・リクエストの安全性・最大回数・全体の時間制限を、一緒に守る実行器を作ります。

なぜ重要なのか

正常なリクエストが1回成功しても、境界値と障害からの復旧は保証されません。このラボは、各関数の契約を小さく実装またはテストしたあと、実際の実行につなげます。コードが存在するかどうかやレポートの文言だけを見るのではなく、結果・例外・保存状態を検査します。前のステップのコードを維持しながら、次のステップに進んでください。

ステップ

  1. /root/work/idem-retry-budget-lab/service.pyで、retryable(status)は429、502、503、504にだけTrueを返してください。残りの状態はFalseです。この一覧はラボのポリシーであり、すべてのサービスの正解ではありません。最初の準備は、次のコマンドで行います。
mkdir -p /root/work/idem-retry-budget-lab
cp /opt/fixtures/practice_depth/idem-retry-budget-lab/* /root/work/idem-retry-budget-lab/
cd /root/work/idem-retry-budget-lab
  1. /root/work/idem-retry-budget-lab/service.pyで、replay_safe(method, key=None)は、大文字小文字を区別しないGET・HEAD・PUT・DELETE、または空でないkeyがあるPOSTだけにTrueを返してください。PATCHはこの契約では許可しません。
  2. /root/work/idem-retry-budget-lab/service.pyで、remaining(deadline, now)は、deadline-nowと0のうち大きいほうを返してください。引数は単調時計の秒単位の実数です。
  3. /root/work/idem-retry-budget-lab/service.pyで、backoff(attempt, base=0.5, cap=4.0)は、min(cap, base*2**attempt)を返してください。attemptは0–30、base・capは負であってはならず、違反するとValueErrorです。
  4. /root/work/idem-retry-budget-lab/service.pyで、jitter(delay, fraction)は、delay*fractionを返してください。delay>=0、0<=fraction<=1である必要があり、違反するとValueErrorです。
  5. /root/work/idem-retry-budget-lab/service.pyで、wait_delay(local, retry_after)は、localとRetry-Afterの整数秒のうち大きいほうを返してください。None・負の数・非整数・日付などサポートしない値は、0秒と解釈します。
  6. /root/work/idem-retry-budget-lab/service.pyで、can_attempt(attempts, maximum, wait, budget)は、attempts < maximum、wait>=0、budget>waitをすべて満たすときだけTrueです。attemptsは、すでに行った総呼び出し数です。
  7. /root/work/idem-retry-budget-lab/service.pyで、execute(send, clock, sleep, method='GET', key=None, timeout=5.0, maximum=3, random_fraction=lambda:0.5)を実装してください。send(残り秒数)は、{status: int, retry_after: 任意の値}を返します。最初にdeadlineを固定し、上のポリシーに従ってリトライし、それ以上できなければ最後の応答を返します。timeout<=0またはmaximum<1はValueError、呼び出しの直前に予算を使い切っていればTimeoutErrorです。

参考

リトライする応答を絞る

/root/work/idem-retry-budget-lab/service.pyで、retryable(status)は429、502、503、504にだけTrueを返してください。残りの状態はFalseです。この一覧はラボのポリシーであり、すべてのサービスの正解ではありません。最初の準備は、次のコマンドで行います。

mkdir -p /root/work/idem-retry-budget-lab
cp /opt/fixtures/practice_depth/idem-retry-budget-lab/* /root/work/idem-retry-budget-lab/
cd /root/work/idem-retry-budget-lab

400・401・409のようなエラーを繰り返しリクエストしても、入力・権限・競合は自然には解決されません。

採点はbash /opt/lab/checks/idem-retry-budget-lab/01-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

リクエストを再送してよいか判断する

/root/work/idem-retry-budget-lab/service.pyで、replay_safe(method, key=None)は、大文字小文字を区別しないGET・HEAD・PUT・DELETE、または空でないkeyがあるPOSTだけにTrueを返してください。PATCHはこの契約では許可しません。

サーバーがキーの重複排除を提供するという前提を区別してください。ヘッダーに文字列を入れただけで安全になるわけではありません。

採点はbash /opt/lab/checks/idem-retry-budget-lab/02-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

1つの締め切り時刻を共有する

/root/work/idem-retry-budget-lab/service.pyで、remaining(deadline, now)は、deadline-nowと0のうち大きいほうを返してください。引数は単調時計の秒単位の実数です。

試行のたびに新しいtimeoutを発行すると、全体の制限が延びてしまいます。

採点はbash /opt/lab/checks/idem-retry-budget-lab/03-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

バックオフに上限を置く

/root/work/idem-retry-budget-lab/service.pyで、backoff(attempt, base=0.5, cap=4.0)は、min(cap, base*2**attempt)を返してください。attemptは0–30、base・capは負であってはならず、違反するとValueErrorです。

attempt=0は、最初の失敗のあとの待機です。指数の計算が際限なく大きくならないように、範囲を検査します。

採点はbash /opt/lab/checks/idem-retry-budget-lab/04-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

ジッターを決定的にテストする

/root/work/idem-retry-budget-lab/service.pyで、jitter(delay, fraction)は、delay*fractionを返してください。delay>=0、0<=fraction<=1である必要があり、違反するとValueErrorです。

乱数の生成そのものを関数の中に隠さず、fractionを注入すれば、両端と中間を再現できます。

採点はbash /opt/lab/checks/idem-retry-budget-lab/05-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

サーバーの待機より早くリトライしない

/root/work/idem-retry-budget-lab/service.pyで、wait_delay(local, retry_after)は、localとRetry-Afterの整数秒のうち大きいほうを返してください。None・負の数・非整数・日付などサポートしない値は、0秒と解釈します。

サーバーが7秒を要求すれば、ローカルのバックオフが0.5秒でも7秒です。残りの予算が短ければ、次のステップで諦めます。

採点はbash /opt/lab/checks/idem-retry-budget-lab/06-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

試行回数と待機の予算を一緒に見る

/root/work/idem-retry-budget-lab/service.pyで、can_attempt(attempts, maximum, wait, budget)は、attempts < maximum、wait>=0、budget>waitをすべて満たすときだけTrueです。attemptsは、すでに行った総呼び出し数です。

待機のあとの残り時間がちょうど0なら、新しいリクエストを開始できません。

採点はbash /opt/lab/checks/idem-retry-budget-lab/07-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。

偽の時計で実行器全体を検証する

/root/work/idem-retry-budget-lab/service.pyで、execute(send, clock, sleep, method='GET', key=None, timeout=5.0, maximum=3, random_fraction=lambda:0.5)を実装してください。send(残り秒数)は、{status: int, retry_after: 任意の値}を返します。最初にdeadlineを固定し、上のポリシーに従ってリトライし、それ以上できなければ最後の応答を返します。timeout<=0またはmaximum<1はValueError、呼び出しの直前に予算を使い切っていればTimeoutErrorです。

応答のコストも時計に含めます。最初の失敗はbackoff(0)です。sleepのあとにも再び時間を確認し、sendに渡すtimeoutを縮めます。

採点はbash /opt/lab/checks/idem-retry-budget-lab/08-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。