新学期の在庫を昨日の番号で読まない
一言でいうと
番号が同じでも、別の世代のイベントかもしれません。復旧は、許可した世代のスナップショットをインストールし、その次の番号から有限回で追いかける過程です。
なぜ必要なのか
学園祭の初日の在庫記録は、番号800まで進みました。2日目には倉庫を新しく開け、ログの番号を0から始めました。表示板に残った800という数字だけを送ると、サーバーはこれから起こるイベントまで、すでに処理したと誤解するかもしれません。あるいは、レプリカが新しいログを、過去の重複として捨てるかもしれません。番号が一意である範囲を明示しなかったために起こる問題です。
ここではepochを、day-Aやday-Bのような世代の識別子として使います。1つの世代の中では番号を再利用せず、明示的に新しい世代を始めるときにだけ、別のepochを付与します。ラボの文字列は、説明を簡単にするための例であり、日付さえ同じなら安全だという意味ではありません。実際のシステムには、復元・再生成・本番環境を区別する識別ポリシーが必要です。epochを、任意のユーザー入力で選ばせると、別の顧客の状態を取り込むというセキュリティ上の問題にもなりえます。
どう動くのか
install_snapshotには、snapshotのほかにexpected_epochが入ります。snapshot.epochがexpected_epochと同じでなければ、インストールできません。expected_epochは、すでに信頼した接続設定から渡される値であり、検査しようとするJSONからそのまま取り出すと、検査が循環します。正しい形式の文字列であるという事実も、その倉庫を読む権限がある証拠にはなりません。認証・権限・転送の保護は、実際のサービスが別に保証する必要があります。
同じ世代のより古いスナップショットは、Conflictとして拒否します。同じlastでtotalが違えば、やはり衝突です。同じ世代・同じlast・同じtotalなら、再インストールの必要がないのでFalseを返し、残っている個々のイベントも消しません。許可された別の世代のスナップショットなら、番号が小さくても置き換えられます。day-Aの800よりday-Bの0が小さいという数字の比較だけで、新しい世代を拒否してはいけません。
スナップショットをインストールすると、それ以前の個々のイベントは、もうレプリカに残っていません。例えば、total=13、last=5のスナップショットは、0から5までの合計を教えてくれるだけで、番号4のdeltaが何だったかは教えてくれません。あとから(4,2)が入ってきたとき、同じ内容の重複だと証明できないので、CoveredBySnapshotとして分類します。過去のすべての番号を黙って無視すると、内容の衝突を隠してしまうことがあります。逆に、残しておいた個々のイベントの番号とdeltaがすべて同じなら、検証できる重複であり、効果を再び加算しません。
新しいバッチは、連続した番号でなければなりません。最後の適用が5なら、最初の新しい番号は6です。6と8だけが来たバッチを受け取って、カーソルを8まで上げると、7が消えます。また、6の効果をコミットしたあとで8でエラーを出すと、呼び出し側がバッチ全体が失敗したと考えて再試行するときに、混乱が生じます。このラボは、最大16個のバッチ全体を1つのトランザクションで適用し、形式と内部の順序を先に検査します。処理記録と在庫と最後の番号を、同じ境界に束ねます。
現場での姿
sync_onceは、1回の同期の試行です。サーバーの世代が期待値かを確認し、レプリカのカーソルで再生を要求します。範囲外か世代が違えば、スナップショットを受け取ってインストールしたあとで、snapshot.lastより大きいイベントだけを読みます。最大16個を適用した状態を返し、最新のサーバー状態に完全に到達したとは約束しません。呼び出し側は、結果のカーソルと、観測した時点のサーバーのカーソルを比べて、追いかけ続けるか、ユーザーに遅延を表示するかを決めます。
特に、スナップショットをインストールするあいだに、元のログが再び切り詰められることがあります。最初にスナップショットがlast=20を含んでいたのに、インストールのあとで元データが番号21を書き、それまで削除したなら、21を再生できません。この場合、今回のsync_onceはResyncRequiredをもう一度伝えます。すでにインストールしたlast=20の有効な状態は消さず、次の有限の呼び出しで、より新しいスナップショットを受け取らせます。失敗を隠したり、catchの中で延々とリトライしたりすると、元データの速い保管の切り詰めに追いつけないまま、リソースだけを使います。
sync_filesは、異なる2つのローカルファイルを開いて閉じる境界を担当します。パスの文字列が違っても、ハードリンクで同じファイルである可能性があるので、実際のファイルの同一性も確認します。元データがなければ、空のDBを作って、復旧が成功したかのように進めることはしません。レプリカを開くのに失敗しても、すでに開いた元データの接続は閉じます。ファイルパスは、学習者が制御する使い捨てのフォルダだという前提であり、攻撃者が同時にリンクを書き換える本番サーバーの、パス競合への防御まで実装したものではありません。
次のラボですること
ステップ8の総合ラボで、apply_batch・sync_once・sync_filesを完成させます。最後に、実際の子プロセスを終了コード73で終わらせ、独立した接続でDBを開き直します。コミットの前後ごとに、カーソル・在庫・個々のイベントが、以前の全体か新しい全体かを比べます。同じファイルの再起動は検証しますが、ディスクの消失、サーバー間の合意、自動フェイルオーバー、TLSとユーザー権限は検証しません。次の課題に拡張するときも、検証した境界を広げる作業が必要です。
参考: Pythonのsqlite3の接続とトランザクション、Pythonのos.path.samefile。epoch・衝突の分類・有限の復旧ループは、今回の学習プロトコルの契約です。