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

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

同じキーでも同じリクエストとは限らない

TT Labで続きを見る

目標

キーの範囲・本文の正規化・再利用の衝突を分けて、再送を判定します。

なぜ重要なのか

2人の顧客がたまたま同じ冪等キーを使ったところ、ある顧客に別の顧客の応答が返されました。別のリクエストでは、同じキーで金額を変えたのに、以前の成功を返しました。冪等キーそのものだけを比べると、リクエストの意味とセキュリティ境界を見落とします。キーはテナントと作業の範囲に結び付け、本文の意味は、別途フィンガープリントで検査する必要があります。

ステップ

  1. /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
  1. /root/work/idem-fingerprint-lab/service.pyで、canonical(body)は、dictだけを受け取り、sort_keys=True、separators=(',',':')、ensure_ascii=False、allow_nan=FalseのJSON文字列で返します。シリアライズできない値は、ValueErrorに統一します。

  2. /root/work/idem-fingerprint-lab/service.pyで、fingerprint(body)は、canonical(body)のUTF-8バイトにSHA-256を適用した、64文字のhex文字列です。

  3. /root/work/idem-fingerprint-lab/service.pyで、scoped_key(tenant, method, path, key)は、tenantとkeyをvalid_keyで検証し、methodを大文字に変えます。pathは/で始まる文字列である必要があります。4つの値をJSON配列として、separators=(',',':')でエンコードして返します。

  4. /root/work/idem-fingerprint-lab/service.pyで、classify(record, digest)は、record=Noneなら'new'、record['fingerprint']==digestなら'replay'、それ以外は'conflict'です。

  5. /root/work/idem-fingerprint-lab/service.pyで、remember(records, key, digest, response)は、新しいキーに{fingerprint:digest, response:responseのdeepcopy}を保存します。すでにあればValueErrorで、既存の記録は保持します。

  6. /root/work/idem-fingerprint-lab/service.pyで、replay(record)は、record['response']のディープコピーです。

  7. /root/work/idem-fingerprint-lab/service.pyで、execute(records, tenant, method, path, key, body, action)は、scopeとフィンガープリントを計算します。newならaction()の結果をrememberしてコピーを返し、replayなら既存の応答のコピーを返し、conflictならValueErrorです。actionの例外は伝播させ、記録を残しません。

参考

キーの文法を検証する

/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で確認してください。