トランザクショナルアウトボックスで注文とイベントを守る
目標
SQLiteのトランザクション・障害の注入・再送・コンシューマーの重複排除を、実際のファイルの状態で検証します。
なぜ重要なのか
正常なリクエストが1回成功しても、境界値と障害からの復旧は保証されません。このラボは、各関数の契約を小さく実装またはテストしたあと、実際の実行につなげます。コードが存在するかどうかやレポートの文言だけを見るのではなく、結果・例外・保存状態を検査します。前のステップのコードを維持しながら、次のステップに進んでください。
ステップ
/root/work/idem-outbox-lab/service.pyで、init_db(path)は、orders(id TEXT PRIMARY KEY, amount INTEGER NOT NULL)、outbox(id TEXT PRIMARY KEY, payload TEXT NOT NULL, sent INTEGER NOT NULL DEFAULT 0)、consumed(id TEXT PRIMARY KEY)、totals(name TEXT PRIMARY KEY, amount INTEGER NOT NULL)を作成し、totalsにsales=0を重複なく入れてください。最初の準備は、次のコマンドで行います。
mkdir -p /root/work/idem-outbox-lab
cp /opt/fixtures/practice_depth/idem-outbox-lab/* /root/work/idem-outbox-lab/
cd /root/work/idem-outbox-lab
/root/work/idem-outbox-lab/service.pyで、canonical(order_id, amount)は、空でない文字列のidと正のint金額だけを受け取り、{id, amount}を、sort_keys=True、separators=(',', ':')、ensure_ascii=FalseのJSONで返してください。boolは金額として受け付けず、不正な入力はValueErrorです。/root/work/idem-outbox-lab/service.pyで、enqueue(path, order_id, amount, fault=lambda:None)は、BEGIN IMMEDIATEで、注文の挿入 → fault() → outboxの挿入をコミットしてTrueを返してください。同じid・金額の再リクエストはFalse、金額の衝突はValueErrorです。faultが例外を出したら、両方ともロールバックします。/root/work/idem-outbox-lab/service.pyで、pending(path, limit=10)は、sent=0の(id, payload)タプルを、id昇順で最大limit個返してください。limitはboolを除く整数の1–100で、違反するとValueErrorです。/root/work/idem-outbox-lab/service.pyで、acknowledge(path, event_id)は、sent=0の行をsent=1に変えたらTrue、ないか、すでに送信済みならFalseを返してください。/root/work/idem-outbox-lab/service.pyで、dispatch(path, publish, limit=10)は、pendingの各行をpublish(id, payload)に渡し、成功したあとにacknowledgeしてください。発行の例外は伝播させ、未送信の状態を保持します。成功して処理した個数を返します。/root/work/idem-outbox-lab/service.pyで、consume(path, event_id, payload)は、JSONのid・金額を検証し、event_idの一致を確認してください。同じトランザクションで、consumedにidを記録し、totalsのsalesに金額を加算します。最初の処理はTrue、重複はFalseで、不正な値はValueErrorです。/root/work/idem-outbox-lab/service.pyで、total(path)は、salesの合計を返してください。publishがconsumeを成功させた直後にConnectionErrorを出すようにしてから、dispatchをもう一度実行してみてください。イベントは2回配信されても、合計は1回しか増えず、outboxは最終的にsent=1である必要があります。
参考
- パッケージはイメージにインストールされています。インターネット接続やpip installは必要ありません。
- 準備のコピーは、最初に1回だけ行ってください。もう一度コピーすると、作業ファイルが初期化されます。
- 採点は、作成したservice.pyを読み込んで、別のidと一時ストレージで実行します。結果を定数として埋め込まないでください。
- 各ステップの採点は45秒以内で実行されます。無限ループや実際の待機を作らないでください。
注文・イベント・消費の記録を分ける
/root/work/idem-outbox-lab/service.pyで、init_db(path)は、orders(id TEXT PRIMARY KEY, amount INTEGER NOT NULL)、outbox(id TEXT PRIMARY KEY, payload TEXT NOT NULL, sent INTEGER NOT NULL DEFAULT 0)、consumed(id TEXT PRIMARY KEY)、totals(name TEXT PRIMARY KEY, amount INTEGER NOT NULL)を作成し、totalsにsales=0を重複なく入れてください。最初の準備は、次のコマンドで行います。
mkdir -p /root/work/idem-outbox-lab
cp /opt/fixtures/practice_depth/idem-outbox-lab/* /root/work/idem-outbox-lab/
cd /root/work/idem-outbox-lab
CREATE IF NOT EXISTSとINSERT OR IGNOREを使って、再起動のときに合計を初期化しないようにします。
採点はbash /opt/lab/checks/idem-outbox-lab/01-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
検証済みのイベント本文を作る
/root/work/idem-outbox-lab/service.pyで、canonical(order_id, amount)は、空でない文字列のidと正のint金額だけを受け取り、{id, amount}を、sort_keys=True、separators=(',', ':')、ensure_ascii=FalseのJSONで返してください。boolは金額として受け付けず、不正な入力はValueErrorです。
JSON文字列の空白とキーの順序を正規化します。floatやboolを、黙って整数に変えてはいけません。
採点はbash /opt/lab/checks/idem-outbox-lab/02-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
2つの書き込みの間の障害をロールバックする
/root/work/idem-outbox-lab/service.pyで、enqueue(path, order_id, amount, fault=lambda:None)は、BEGIN IMMEDIATEで、注文の挿入 → fault() → outboxの挿入をコミットしてTrueを返してください。同じid・金額の再リクエストはFalse、金額の衝突はValueErrorです。faultが例外を出したら、両方ともロールバックします。
withの接続ブロックの中で、例外を握りつぶしてはいけません。fault呼び出しの前にコミットすると、注文だけが残る事故が再現されます。
採点はbash /opt/lab/checks/idem-outbox-lab/03-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
未送信のイベントを、制限されたバッチで読む
/root/work/idem-outbox-lab/service.pyで、pending(path, limit=10)は、sent=0の(id, payload)タプルを、id昇順で最大limit個返してください。limitはboolを除く整数の1–100で、違反するとValueErrorです。
バッチの上限は、障害からの復旧時に、一度にメモリを使い果たさないようにします。ORDER BYを省略しないでください。
採点はbash /opt/lab/checks/idem-outbox-lab/04-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
発行の確認を冪等に記録する
/root/work/idem-outbox-lab/service.pyで、acknowledge(path, event_id)は、sent=0の行をsent=1に変えたらTrue、ないか、すでに送信済みならFalseを返してください。
UPDATEの条件とrowcountで、最初の確認と重複した確認を区別します。
採点はbash /opt/lab/checks/idem-outbox-lab/05-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
発行に成功したあとにだけ完了を記録する
/root/work/idem-outbox-lab/service.pyで、dispatch(path, publish, limit=10)は、pendingの各行をpublish(id, payload)に渡し、成功したあとにacknowledgeしてください。発行の例外は伝播させ、未送信の状態を保持します。成功して処理した個数を返します。
acknowledgeをpublishの前に置くと、障害のときにイベントを永遠に失います。空のバッチは0です。
採点はbash /opt/lab/checks/idem-outbox-lab/06-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
消費の記録と効果をアトミックにまとめる
/root/work/idem-outbox-lab/service.pyで、consume(path, event_id, payload)は、JSONのid・金額を検証し、event_idの一致を確認してください。同じトランザクションで、consumedにidを記録し、totalsのsalesに金額を加算します。最初の処理はTrue、重複はFalseで、不正な値はValueErrorです。
BEGIN IMMEDIATEで、重複の確認と効果の反映をまとめます。consumedだけを先にコミットすると、効果が消えることがあります。
採点はbash /opt/lab/checks/idem-outbox-lab/07-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。
配信2回・効果1回を再現する
/root/work/idem-outbox-lab/service.pyで、total(path)は、salesの合計を返してください。publishがconsumeを成功させた直後にConnectionErrorを出すようにしてから、dispatchをもう一度実行してみてください。イベントは2回配信されても、合計は1回しか増えず、outboxは最終的にsent=1である必要があります。
送信の成功と確認の記録の間の隙間は残ります。この隙間を隠さず、コンシューマーの重複排除で耐えられるかを確認します。
採点はbash /opt/lab/checks/idem-outbox-lab/08-contract.shで直接再現できます。ファイルを保存してから、もう一度実行してください。