65 lines
3.3 KiB
Python
65 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
||
"""fts_stale 运行期兜底 watch(kanban t_a88ff8ac 方案 b 落地, 2026-09-07)
|
||
|
||
设计决策(为什么选方案 b 只读版):
|
||
- 既有测试不变量(tests/state/test_fts_runtime_rebuild.py): live write/search 不得
|
||
rebuild 全量 FTS, 只有 SessionDB open 才恢复。方案 a(独立进程模拟 reopen) 在本环境
|
||
有 P1-1 风险(gateway 在线时第二写入者 → state.db WAL 突破损坏, 本环境反复损坏根因),
|
||
方案 c(改源码允许受控时机重建) 风险面最大。
|
||
- 方案 b 落地 = 部署层「检测 + 告警排队」: 只读查 fts_stale 标记与 FTS 漂移, 若 stale
|
||
持续存在 → 输出告警(no_agent watchdog 模式, 健康静默) 通知人工在停机窗口触发重建
|
||
(gateway 停止后 SessionDB open 自然走 _recover_stale_fts)。不引入任何活库写面。
|
||
- 该方案保留既有不变量不动, 消灭「fts_stale 后无 open 则无限期 LIKE 降级(无声)」——
|
||
改为「有 open 则恢复, 无 open 则 30 分钟内有告警」。
|
||
"""
|
||
import sqlite3, os, sys
|
||
|
||
db = os.path.expanduser("~/.hermes/state.db")
|
||
|
||
def main():
|
||
try:
|
||
# 只读连接: 绝不写活库(P1-1 规避: gateway 在线时第二写入者 = 损坏)
|
||
conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=15)
|
||
cur = conn.cursor()
|
||
# 1. fts_stale 持久化标记(state_meta key, hermes_state_common.py:778)
|
||
try:
|
||
stale = cur.execute(
|
||
"SELECT 1 FROM state_meta WHERE key = 'fts_stale' LIMIT 1"
|
||
).fetchone() is not None
|
||
except sqlite3.OperationalError:
|
||
stale = False # 表不存在(新库/极端情况)不算 stale
|
||
# 2. FTS 行数 vs messages 行数漂移(粗略健康信号)
|
||
n_msg = n_fts = drift = None
|
||
try:
|
||
n_msg = cur.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
|
||
n_fts = cur.execute("SELECT COUNT(*) FROM messages_fts").fetchone()[0]
|
||
drift = n_msg - n_fts
|
||
except sqlite3.OperationalError:
|
||
pass
|
||
# 3. 完整性快检(只读安全)
|
||
try:
|
||
integrity = cur.execute("PRAGMA quick_check").fetchone()[0]
|
||
except sqlite3.OperationalError:
|
||
integrity = "err"
|
||
conn.close()
|
||
|
||
stale_ok = "✅ 无 stale 标记" if not stale else "⚠️ fts_stale 标记存在"
|
||
drift_ok = ""
|
||
if drift is not None and drift > 200:
|
||
drift_ok = f"; ⚠️ FTS 漂移大 (messages={n_msg} fts={n_fts} drift={drift})"
|
||
healthy = (not stale) and (drift is None or drift <= 200) and integrity == "ok"
|
||
|
||
if not healthy:
|
||
# watchdog 模式: 只有异常才输出(cron no_agent 空输出不发送)
|
||
print(f"[FTS-WATCH] {stale_ok}{drift_ok} integrity={integrity}")
|
||
print("处理: 请勿在线 rebuild(live write 不 rebuild 不变量 + P1-1 第二写入者风险)。")
|
||
print(" 在停机窗口(gateway 停止后)执行: hermes 下次 SessionDB open 会自动走 _recover_stale_fts 重建。")
|
||
print(" 或手动: 停 gateway → sqlite3 重建 FTS → 起 gateway。详见 RECOVERY-RULES.md。")
|
||
# 健康时完全静默(无输出 = no_agent cron 不投递)
|
||
|
||
except Exception as e:
|
||
print(f"[FTS-WATCH-ERR] {e}")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|