同じキーでも同じリクエストとは限らない
目標
キーの範囲・本文の正規化・再利用の衝突を分けて、再送を判定します。
なぜ重要なのか
2人の顧客がたまたま同じ冪等キーを使ったところ、ある顧客に別の顧客の応答が返されました。別のリクエストでは、同じキーで金額を変えたのに、以前の成功を返しました。冪等キーそのものだけを比べると、リクエストの意味とセキュリティ境界を見落とします。キーはテナントと作業の範囲に結び付け、本文の意味は、別途フィンガープリントで検査する必要があります。
ステップ
/root/work/idem-fingerprint-lab/service.pyで、valid_key(value)は、英字・数字・アンダースコア・ハイフンの1–64文字だけをそのまま返し、ほかの入力はValueErrorです。
最初に1回だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/idem-fingerprint-lab
test -e /root/work/idem-fingerprint-lab/service.py || cp /opt/fixtures/ten_labs/idem-fingerprint-lab/service.py /root/work/idem-fingerprint-lab/service.py
cd /root/work/idem-fingerprint-lab
-
/root/work/idem-fingerprint-lab/service.pyで、canonical(body)は、dictだけを受け取り、sort_keys=True、separators=(',',':')、ensure_ascii=False、allow_nan=FalseのJSON文字列で返します。シリアライズできない値は、ValueErrorに統一します。 -
/root/work/idem-fingerprint-lab/service.pyで、fingerprint(body)は、canonical(body)のUTF-8バイトにSHA-256を適用した、64文字のhex文字列です。 -
/root/work/idem-fingerprint-lab/service.pyで、scoped_key(tenant, method, path, key)は、tenantとkeyをvalid_keyで検証し、methodを大文字に変えます。pathは/で始まる文字列である必要があります。4つの値をJSON配列として、separators=(',',':')でエンコードして返します。 -
/root/work/idem-fingerprint-lab/service.pyで、classify(record, digest)は、record=Noneなら'new'、record['fingerprint']==digestなら'replay'、それ以外は'conflict'です。 -
/root/work/idem-fingerprint-lab/service.pyで、remember(records, key, digest, response)は、新しいキーに{fingerprint:digest, response:responseのdeepcopy}を保存します。すでにあればValueErrorで、既存の記録は保持します。 -
/root/work/idem-fingerprint-lab/service.pyで、replay(record)は、record['response']のディープコピーです。 -
/root/work/idem-fingerprint-lab/service.pyで、execute(records, tenant, method, path, key, body, action)は、scopeとフィンガープリントを計算します。newならaction()の結果をrememberしてコピーを返し、replayなら既存の応答のコピーを返し、conflictならValueErrorです。actionの例外は伝播させ、記録を残しません。
参考
- インターネットやパッケージのインストールなしで、既存のlab-dev環境で行います。
- 各ステップは、45秒の採点予算の中で実行されます。実際のsleepやネットワーク呼び出しを追加しないでください。
- 採点は、提出されたモジュールを新しく読み込み、独立した入力と一時DBで検査します。期待値を定数として返す代わりに、契約を実装してください。
- FastAPI公式ドキュメント・pytest公式ドキュメント・Python sqlite3
- 限界: このラボは、単一プロセスのメモリ上のディクショナリで、キーとリクエストの意味を分ける契約を学びます。プロセスの障害後の保持や複数ワーカーの並行性は、次のSQLiteのラボで扱います。ハッシュは暗号化ではなく、JSONの正規化がすべての言語の数値表現まで標準化する国際規格だとは主張しません。
キーの文法を検証する
/root/work/idem-fingerprint-lab/service.pyで、valid_key(value)は、英字・数字・アンダースコア・ハイフンの1–64文字だけをそのまま返し、ほかの入力はValueErrorです。
最初に1回だけ準備してください。既存のファイルは上書きしません。
mkdir -p /root/work/idem-fingerprint-lab
test -e /root/work/idem-fingerprint-lab/service.py || cp /opt/fixtures/ten_labs/idem-fingerprint-lab/service.py /root/work/idem-fingerprint-lab/service.py
cd /root/work/idem-fingerprint-lab
キーの長さと許可する文字を制限し、空のキーを、正常な再送として扱いません。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/01-contract.shで確認してください。
オブジェクトの順序は畳み、配列の順序は保持する
/root/work/idem-fingerprint-lab/service.pyで、canonical(body)は、dictだけを受け取り、sort_keys=True、separators=(',',':')、ensure_ascii=False、allow_nan=FalseのJSON文字列で返します。シリアライズできない値は、ValueErrorに統一します。
配列をソートすると、ユーザーがリクエストした作業の順序を変えてしまいます。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/02-contract.shで確認してください。
本文のフィンガープリントを計算する
/root/work/idem-fingerprint-lab/service.pyで、fingerprint(body)は、canonical(body)のUTF-8バイトにSHA-256を適用した、64文字のhex文字列です。
Pythonのhash()はプロセスごとに変わるので、保存するフィンガープリントとしては使いません。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/03-contract.shで確認してください。
テナントと作業でキーを区別する
/root/work/idem-fingerprint-lab/service.pyで、scoped_key(tenant, method, path, key)は、tenantとkeyをvalid_keyで検証し、methodを大文字に変えます。pathは/で始まる文字列である必要があります。4つの値をJSON配列として、separators=(',',':')でエンコードして返します。
単純な区切り文字での連結よりも、構造をエンコードするほうが、境界が明確です。パスの大文字小文字は保持します。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/04-contract.shで確認してください。
3つの判定を区別する
/root/work/idem-fingerprint-lab/service.pyで、classify(record, digest)は、record=Noneなら'new'、record['fingerprint']==digestなら'replay'、それ以外は'conflict'です。
キーが存在するという理由だけで、すべての再リクエストを成功として再生しません。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/05-contract.shで確認してください。
応答を保存するときにコピーする
/root/work/idem-fingerprint-lab/service.pyで、remember(records, key, digest, response)は、新しいキーに{fingerprint:digest, response:responseのdeepcopy}を保存します。すでにあればValueErrorで、既存の記録は保持します。
応答の中のリストもコピーしなければ、入れ子の状態が共有されます。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/06-contract.shで確認してください。
応答を読むときもコピーする
/root/work/idem-fingerprint-lab/service.pyで、replay(record)は、record['response']のディープコピーです。
最初の応答を修正した呼び出し側が、次の再送の結果まで変えられないようにします。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/07-contract.shで確認してください。
業務関数を1回だけ呼び出す
/root/work/idem-fingerprint-lab/service.pyで、execute(records, tenant, method, path, key, body, action)は、scopeとフィンガープリントを計算します。newならaction()の結果をrememberしてコピーを返し、replayなら既存の応答のコピーを返し、conflictならValueErrorです。actionの例外は伝播させ、記録を残しません。
業務関数の呼び出し回数と、失敗のあとに残った記録まで検査して初めて、再送の契約がわかります。
保存したあと、bash /opt/lab/checks/idem-fingerprint-lab/08-contract.shで確認してください。