78 lines
2.8 KiB
Bash
Executable File
78 lines
2.8 KiB
Bash
Executable File
#!/bin/bash
|
||
# snapshot-state-db.sh — state.db 定时健康快照(2026-09-03 创建)
|
||
# 2026-09-09 修复:原用 /usr/bin/sqlite3 (CLI 3.45.1,有 WAL-reset bug #69784) 对 WAL 库做
|
||
# .backup + journal_mode=WAL → 反复触发 delivery_obligations 索引损坏(9/7/9/8/9/9 三连坏真凶)。
|
||
# hermes_state.py 明确定义 3.7.0~3.51.2 为 vulnerable,3.45.1 在危险区间。
|
||
# 现全部改用 python3 sqlite3 (3.53.1,修复版,与 gateway 同版本)。
|
||
# 保留最近 8 份;损坏恢复时用最近的快照。
|
||
set -uo pipefail
|
||
|
||
DB=/home/muc/.hermes/state.db
|
||
SNAP_DIR=/home/muc/.hermes/backups/state-db-snap
|
||
KEEP=8
|
||
TS=$(date +%Y%m%d-%H%M)
|
||
LOG=/tmp/state-db-snap.log
|
||
|
||
mkdir -p "$SNAP_DIR"
|
||
|
||
# 0. 版本自检:拒绝 vulnerable 老 CLI(防止将来 PATH 变化回退到 /usr/bin/sqlite3)
|
||
PYVER=$(python3 -c 'import sqlite3; print(sqlite3.sqlite_version)')
|
||
case "$PYVER" in
|
||
3.51.3|3.51.*|3.5[2-9].*|3.6[0-9].*|4.*)
|
||
;;
|
||
*)
|
||
echo "[$(date '+%H:%M:%S')] ❌ python3 sqlite3 版本 $PYVER 有 WAL-reset bug,拒绝快照" | tee -a "$LOG"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# 1. 在线一致性快照(python3 同版本 .backup,与 gateway 一致)
|
||
if ! python3 - "$DB" "$SNAP_DIR/state-$TS.db" <<'PY' >>"$LOG" 2>&1
|
||
import sqlite3, sys
|
||
src = sqlite3.connect(sys.argv[1], timeout=30)
|
||
dst = sqlite3.connect(sys.argv[2], timeout=30)
|
||
try:
|
||
src.backup(dst)
|
||
finally:
|
||
dst.close(); src.close()
|
||
PY
|
||
then
|
||
echo "[$(date '+%H:%M:%S')] ❌ state.db 快照失败" | tee -a "$LOG"
|
||
rm -f "$SNAP_DIR/state-$TS.db"
|
||
exit 1
|
||
fi
|
||
|
||
# 2. 验证快照健康(python3 只读)
|
||
CHECK=$(python3 - "$SNAP_DIR/state-$TS.db" <<'PY' 2>&1
|
||
import sqlite3, sys
|
||
c = sqlite3.connect(f"file:{sys.argv[1]}?mode=ro", uri=True, timeout=30)
|
||
print(c.execute("PRAGMA integrity_check").fetchone()[0])
|
||
c.close()
|
||
PY
|
||
)
|
||
if [ "$CHECK" != "ok" ]; then
|
||
echo "[$(date '+%H:%M:%S')] ⚠️ 快照 integrity=$CHECK(删除坏快照)" | tee -a "$LOG"
|
||
rm -f "$SNAP_DIR/state-$TS.db"
|
||
exit 1
|
||
fi
|
||
|
||
# 2.5 快照切 WAL 模式(2026-09-06 模式15:.backup 产物默认 delete 头,直接拿去恢复会触发
|
||
# gateway 启动 delete→WAL 转换风暴 → FTS5 撕裂;让快照生而 WAL,恢复链全程无转换)
|
||
JM=$(python3 - "$SNAP_DIR/state-$TS.db" <<'PY' 2>&1
|
||
import sqlite3, sys
|
||
c = sqlite3.connect(sys.argv[1], timeout=30)
|
||
print(c.execute("PRAGMA journal_mode=WAL").fetchone()[0])
|
||
c.close()
|
||
PY
|
||
)
|
||
if [ "$JM" != "wal" ]; then
|
||
echo "[$(date '+%H:%M:%S')] ⚠️ 快照切 WAL 失败 ($JM),删除该快照" | tee -a "$LOG"
|
||
rm -f "$SNAP_DIR/state-$TS.db"
|
||
exit 1
|
||
fi
|
||
|
||
# 3. 清理旧快照(保留最近 KEEP 份)
|
||
ls -1t "$SNAP_DIR"/state-*.db 2>/dev/null | tail -n +$((KEEP+1)) | xargs -r rm -f
|
||
|
||
echo "[$(date '+%H:%M:%S')] ✅ 快照 state-$TS.db (integrity=ok, 保留 $(ls "$SNAP_DIR"/state-*.db 2>/dev/null | wc -l) 份)" >> "$LOG"
|