取り消しも新しい変更
一言でいうと
元に戻すことは、過去を消す作業ではなく、現在の状態を確認して行う新しい変更です。元の変更と補償のレシートを分け、あとで別の人が変更した行は上書きしません。
なぜ必要なのか
雨の予報でキャンセルした祭りが、再び開催されます。担当者が、朝のキャンセルを元に戻してほしいと言います。バックアップファイルでテーブルを丸ごと復元すると、キャンセルのあとに入った新しい注文や、別の担当者の数量変更まで消えてしまう可能性があります。以前作ったバックアップがあるという事実だけで、いまそれで上書きしてよいという許可が生まれるわけではありません。何を元に戻してよいのか、改めて境界を決める必要があります。
今回の課題は、キャンセルした注文をpendingに復旧する、小さな補償です。実際の決済のキャンセルや顧客への通知まで元に戻す例ではありません。返金、発送、外部通知には、それぞれ別のAPIと復旧ポリシーが必要です。ここでは、1つのPostgreSQLトランザクションに収められる、注文の状態と補償記録を扱い、その境界の中で、部分的な変更と二重実行を防ぎます。
どう動くのか
まず、changesの元の承認内容と、auditの行ごとの前後バージョンを照合します。承認には注文1のバージョン3があり、監査には3から4に変わったと残っている必要があります。補償計画は、現在のテーブルを新たに承認するのではなく、この記録からキャンセル直後の期待バージョン4を計算します。監査の行がなかったり、異なる数量を記録していたりしたら、根拠が一致しないので止まります。現在の注文がcancelledだからというだけの理由で、どの変更の結果なのかを推測しません。
復旧のUPDATEには、顧客・ID・キャンセル直後のバージョン・数量・cancelled状態をすべて入れます。一致すればpendingに変更し、バージョンをもう一度増やします。バージョン3をキャンセルして4になったなら、補償のあとは5です。以前のバージョン3に下げると、古い承認資料がまた合っているように見えるおそれがあります。値を昔の姿に戻すとしても、そのあとに新しい出来事があったという事実は、バージョンに残す必要があります。
원 승인 주문 1 / pending / revision 3
취소 확정 주문 1 / cancelled / revision 4
보상 확정 주문 1 / pending / revision 5
数値は無限ではありません。PostgreSQLのintegerの上限に達すると、キャンセルと補償を増やし続けられません。元の承認バージョンが大きすぎれば、補償計画の段階から拒否します。実サービスでは、より大きな型や別の識別戦略で設計できますが、このラボで、エラーを隠してバージョンを循環させたり減らしたりするのは、解決策ではありません。
補償には、undo_idと元の変更original_id、理由reasonを残します。undo_recordsは、undo_idが一意で、original_idも一意です。同じ元の変更を、異なる補償IDで重複して復旧できないようにするためです。同じ補償ID・元の変更・理由の再呼び出しはFalseで完了しますが、理由や元の変更が違えばConflictです。Falseは失敗ではなく、すでに実行した同一の要求だという結果です。
補償レシートの挿入、すべての注文の復旧、undo_auditの記録は、1つのトランザクションです。2つ目の注文で衝突したら、1つ目の注文の復旧と未完成のレシートもなくなっている必要があります。元のchangesとauditは変更しません。補償のために元の記録を削除すると、なぜ現在の状態がそうなったのかを説明するつながりが切れます。過去の監査が消された状態で、新しい成功記録だけを残すのは、安全な補償ではありません。
現場での姿
補償をコミットした直後にプログラムが落ちると、ユーザーは成功応答を受け取れません。同じIDでもう一度要求したら、補償レシートを読んで、新しい変更なしで完了する必要があります。別の補償IDに変えて再要求すると、元の変更の一意性制約と内容の照合が、衝突を知らせてくれます。この違いを説明できてはじめて、運用者がエラーをなくそうとして、番号だけを新しく作り続ける行動を避けられます。
レシートは、補償当時の事実です。補償のあとに別の担当者が注文を変更したり削除したりしていたら、現在の状態と違っている可能性があります。inspect_undoは当時の記録を返し、reconcile_undoは現在の対象を、一致・変更・欠落に分けます。同じ数量でもバージョンが高ければその後の変更であり、別IDの新しい注文で、元の注文の欠落を相殺しません。現在の照合は読み取りだけを行い、新しい承認なしに是正しません。
実務では、顧客に、キャンセルのレシート、補償のレシート、現在の照合結果を区別して伝えます。補償が失敗したなら、どの前提が変わったのかを説明して、再判断を依頼します。このプログラムは、権限のある担当者が呼び出す前提であり、tenant文字列を受け取ること自体は権限の検証ではありません。実サービスの認証、ロール、監査の保存とアクセス制御は、別の境界です。
次のラボですること
リクエストの検証、元の記録の照合、単一行の復旧、時間上限付きのバッチ、アトミックな補償、記録の照会、接続の所有、制限付きの再試行を、8ステップで実装します。クライアントをコミットの前後の3か所で実際に終了させ、同一・別の補償IDを同時に実行してみます。既存のDBイメージのfsyncとfull_page_writesは切られているので、クライアント終了の検証を、サーバー電源障害に対する耐久性の保証にまで拡大しません。
参考: PostgreSQL UPDATE、一意性制約、トランザクション分離。補償のスキーマ・理由・バージョンの範囲は、このレッスンが決めた契約であり、外部システムの自動キャンセルの保証ではありません。