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

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

送金の途中で失敗しても残高を保つ

TT Labで続きを見る

目標

減算・加算・送金の記録をアトミックにまとめ、同時の残高競合を再現します。

なぜ重要なのか

送信口座からはお金を引いたものの、受信口座に足す前にプロセスが死にました。リトライには送金idがなかったので、またお金を引きました。逆に、送金の記録だけを先にコミットすると、リトライは完了したと判断して、入金が永遠に抜け落ちます。金額の計算を整数で正確に行っても、トランザクションの境界が間違っていれば、結果は間違います。

ステップ

  1. /root/work/idem-ledger-transfer-lab/service.pyで、init_db(path)は、accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL)とtransfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL)を、冪等に作成します。

最初に1回だけ準備してください。既存のファイルは上書きしません。

mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab
  1. /root/work/idem-ledger-transfer-lab/service.pyで、add_account(path, account_id, amount)は、boolを除く0以上のintだけを許可し、INSERTで口座を作ります。すでにある口座は、sqlite3.IntegrityErrorで拒否し、残高は保持します。

  2. /root/work/idem-ledger-transfer-lab/service.pyで、balance(path, account_id)は、口座の残高を返し、なければKeyErrorです。

  3. /root/work/idem-ledger-transfer-lab/service.pyで、validate_transfer(source,target,amount)は、source!=targetで、amountがboolを除く正のintならNone、そうでなければValueErrorです。

  4. /root/work/idem-ledger-transfer-lab/service.pyで、transfer(path,tx_id,source,target,amount,fault=lambda:None)は、同じid/内容ならFalse、idの衝突・ない口座・不足した残高はValueErrorです。新しい送金は、減算→fault()→入金→transfersへの挿入を、アトミックに実行してTrueです。

  5. /root/work/idem-ledger-transfer-lab/service.pyで、history(path)は、transfersをid昇順の(id,source,target,amount)タプルのリストで返します。

  6. /root/work/idem-ledger-transfer-lab/service.pyで、total_balance(path)は、accounts.balanceの合計です。口座がなければ0です。

  7. /root/work/idem-ledger-transfer-lab/service.pyで、compete(path,source,target,amount)は、互いに異なるid race-a/race-bで、同じ金額のtransferを、2つのスレッドで実行します。ValueErrorだけをFalseに変換した結果のリストを返します。残高が1回分しかなければ、成功は1つです。

参考

元帳テーブルを作る

/root/work/idem-ledger-transfer-lab/service.pyで、init_db(path)は、accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL)とtransfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL)を、冪等に作成します。

最初に1回だけ準備してください。既存のファイルは上書きしません。

mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab

同じ送金idが2回コミットされないように、一意性をDBに置きます。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/01-contract.shで確認してください。

口座を1回だけ作る

/root/work/idem-ledger-transfer-lab/service.pyで、add_account(path, account_id, amount)は、boolを除く0以上のintだけを許可し、INSERTで口座を作ります。すでにある口座は、sqlite3.IntegrityErrorで拒否し、残高は保持します。

初期化のUPSERTが、実際の残高を初期値に戻してしまわないようにします。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/02-contract.shで確認してください。

ない口座と0ウォンを区別する

/root/work/idem-ledger-transfer-lab/service.pyで、balance(path, account_id)は、口座の残高を返し、なければKeyErrorです。

ないことを0に変えると、間違った口座への送金を進められてしまいます。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/03-contract.shで確認してください。

送金の入力を検証する

/root/work/idem-ledger-transfer-lab/service.pyで、validate_transfer(source,target,amount)は、source!=targetで、amountがboolを除く正のintならNone、そうでなければValueErrorです。

同一口座の送金と、負の送金を、事前に拒否します。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/04-contract.shで確認してください。

3つの書き込みを1つのトランザクションにまとめる

/root/work/idem-ledger-transfer-lab/service.pyで、transfer(path,tx_id,source,target,amount,fault=lambda:None)は、同じid/内容ならFalse、idの衝突・ない口座・不足した残高はValueErrorです。新しい送金は、減算→fault()→入金→transfersへの挿入を、アトミックに実行してTrueです。

減算のあとに強制的に例外を出して、残高と送金の記録が、どちらも元のままかを検査します。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/05-contract.shで確認してください。

送金履歴を決定的に読む

/root/work/idem-ledger-transfer-lab/service.pyで、history(path)は、transfersをid昇順の(id,source,target,amount)タプルのリストで返します。

入力の順序と取得の順序を混同しないように、ORDER BYを明示します。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/06-contract.shで確認してください。

保存量を確認する

/root/work/idem-ledger-transfer-lab/service.pyで、total_balance(path)は、accounts.balanceの合計です。口座がなければ0です。

個々のリクエストの戻り値と、全体の金額の保存を、別々に検証します。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/07-contract.shで確認してください。

同時の送金が残高を超えない

/root/work/idem-ledger-transfer-lab/service.pyで、compete(path,source,target,amount)は、互いに異なるid race-a/race-bで、同じ金額のtransferを、2つのスレッドで実行します。ValueErrorだけをFalseに変換した結果のリストを返します。残高が1回分しかなければ、成功は1つです。

トランザクションの外で残高を先に検査すると、2つのリクエストが同じ残高を見て、両方とも成功することがあります。

保存したあと、bash /opt/lab/checks/idem-ledger-transfer-lab/08-contract.shで確認してください。