書き込み口を2つにしたら、人が入れた修正が黙って消えた
Upstash のオーバーレイと、バッチによる直接更新が衝突。drain-first と base 照合で lost update を止めるまで。
症状: 直したはずの箇所が、元に戻っている
管理画面から自分で直した箇所が、しばらくすると元の文に戻っていました。 エラーは出ていません。ログにも何も残っていません。 「直し忘れたのかな」と思うくらいの、静かな消え方でした。
原因: 書き込み口が2つあった
この構成には、同じ segment に書き込む経路が2本あります。
校正で確定できなかった箇所
│ screen_verdict=unsure で CSV に書く
▼
data/medium_low_findings*.csv
│ build_review_queue.py(デプロイのたびに作り直す)
▼
review_queue テーブル(現在 8,493件)
│
▼
/admin/review で該当箇所に色が付く
│ 人が直す
▼
Upstash Redis の corrections(オーバーレイ)── 即座に本番の表示へ反映 ← 経路①
│ merge_overlay.py
▼
data/yuru.db(恒久化) ← 経路②(バッチが直接触る)
オーバーレイを噛ませているのは、DBの再配布を待たずに直せるようにするためです。 400MBのDBを毎回配るわけにいかないので、差分だけ Redis に置いて上書き表示しています。
ここで、まだマージしていないオーバーレイがある状態で、 こちらがバッチでDBを直接触ると、マージのときに後から来たほうが黙って勝ちます。 負けるのはたいてい手で直したほうでした。いちばん手間をかけたものから消えます。
対処1: 先に排出してから触る(drain-first)
# ❌ これをやると、未マージの修正の上に書いてしまう
$ python3 scripts/bulk_fix.py
# ✅ 必ず先に排出する
$ python3 scripts/merge_overlay.py # 人の修正を DB に取り込む
$ python3 scripts/bulk_fix.py # そのうえで機械の編集
対処2: base を持たせて、競合を残す
修正提案に base(提案者が見ていた文)を持たせて、
API・承認・マージの各段で照合します。不一致なら上書きせず、競合として保全します。
$ python3 scripts/feedback_queue.py conflicts
[0] 5HUPJcw-YXA:781
base : デカルト味は世界に ← 提案者が見ていた文
提案 : デカルトみは世界に
現在 : 「デカルトみ」は世界に ← その後こちらが変えた
$ python3 scripts/feedback_queue.py resolve 0 keep
ここは、機械に判定させて片方を消さないようにしています。両方の文を出して、自分で選ぶ形です。
もう1件: push と DB入れ替えを連結して20分落とした
# ❌ やってしまったこと
$ git push && bash scripts/upload_db.sh
# push が Vercel の再ビルドを起こす
# → その再ビルドが「古いDBを消して新しいDBを上げている最中」に当たる
# → 全ページ 500(約20分)
いまは DB の入れ替えを原子的にしています。
Step 0 : FTS 索引を作り直す
Step 0.5: 要確認台帳から review_queue を再構築
Step 1 : 出荷前検査5項目(1つでも中止条件に触れたら止める)
Step 2 : 一時名でアップロード → サイズ検証
Step 2.5: 数秒でリネームして世代交代(前世代は yuru-deploy-prev.db として保持)
Step 3 : 空コミットで Vercel を再ビルド
Step 4 : 出荷時点のスナップショットを控える(次回の検査の比較元)
そのうえで、コード変更の push とは連結しないようにしました。
再発防止
- UIより先に、書き込み口がいくつあるかを数える
- 2つ以上あるなら調停の規則を先に決める
- 競合は自分で判定せず、両方を見せて選ばせる
- デプロイは原子的に。一時名 → 検証 → リネーム、前世代を残す
← 「要確認」を付けて確認する仕組みにした。ただし漏れが… 追記専用の編集台帳とデプロイ前検査で、事故を出荷前に… → 目次へ