SessionDB 会话存储与 SQLite 高并发锁自愈
深度解析 hermes_state.py、网络文件系统 WAL 降级与 Jitter Retry 随机避堵算法
📊 图 4-1:SQLite 库的写冲突 Jitter Retry 与 schema 在轨自愈流转图
💾 1. SQLite state.db 核心表关系与外键设计
会话状态(如 Turn 历史消息、变量快照、Kanban 子任务生命周期)被统一持久化在 SQLite 中(通常命名为 state.db)。底层数据表拓扑关系如下 ER 图所示:
📊 图 4-2:SQLite state.db 核心数据表外键关系 ER 图
核心数据表职责:
sessions表:主表。存储会话全局状态(如session_idPK,status,metadataJSON)。messages表:保存对话树。其session_id作为 FK 级联关联主表。每一行存储 Turn 角色(role)、内容(content BLOB)和唯一序列号。variables表:存储用户在会话期间绑定的环境变量及快照,在后台任务与重试时提供上下文自愈。compression_locks表:高并发对话轨迹压缩互斥锁表,确保同一个 `session` 绝对不会有两个后台线程同时做压缩重构。
☁️ 2. 网络文件系统的 WAL 兼容降级 (apply_wal_with_fallback)
如果用户的工程被部署在 NFS、SMB、WSL 宿主机虚拟挂载盘 上,底层往往缺少对 SQLite WAL 模式所要求的共享内存映射文件(.shm)和 byte-range locks 字节范围锁协议的支持,这会抛出:
sqlite3.OperationalError: locking protocol (or: not authorized)
为了解决这一云挂载环境的致命痛点,在 hermes_state.py 中实现了降级机制:
# apply_wal_with_fallback 实现细节
def apply_wal_with_fallback(conn: sqlite3.Connection, db_label: str) -> str:
try:
conn.execute("PRAGMA journal_mode=WAL")
return "wal"
except sqlite3.OperationalError as exc:
msg = str(exc).lower()
if any(marker in msg for marker in ("locking protocol", "not authorized")):
# 自动向 DELETE 模式回退降级,关闭共享内存映射
conn.execute("PRAGMA journal_mode=DELETE")
_log_wal_fallback_once(db_label, exc)
return "delete"
raise
⏳ 3. 应用层 Jitter Retry 与锁 convoy 避堵算法
普通的 SQLite 重试机制会引起“惊群效应(Convoy Pattern)”——多个阻塞的线程同时被唤醒抢占同一把锁,导致数据库持续死锁。Hermes 在 SessionDB 中利用应用层随机抖动(Jitter)算法打破了队列 convoy:
- BEGIN IMMEDIATE 显式抢占:
在执行任何写 SQL 前,连接立即宣告
BEGIN IMMEDIATE。这使得 SQLite 写锁冲突在事务一开始就被拦截,防止事务执行到一半时发生冲突回滚。 - 应用层 Jitter(随机退避):
一旦发生
locked报错,代码自动释放当前的 Python thread lock 并开启随机休眠:# Jitter 避堵逻辑核心 _WRITE_RETRY_MIN_S = 0.020 # 20ms _WRITE_RETRY_MAX_S = 0.150 # 150ms # 锁冲突时,释放 Python 锁并睡眠 20ms 到 150ms 之间随机时间,打破惊群排队 jitter = random.uniform(_WRITE_RETRY_MIN_S, _WRITE_RETRY_MAX_S) time.sleep(jitter)
🔧 4. sqlite_master Schema 损坏的在轨直接手术
在极端意外情况下,元数据表 sqlite_master 会出现异常的主键冲突,普通开发人员往往无从下手。Hermes 在 SessionDB 内部封装了在轨修复手术:
- 排他自愈锁 (`_repair_attempt_lock`):确保在整修期间,没有其他并发的后台线程可以读写该 DB 块。
- 可写 Schema PRAGMA 写入:
# PRAGMA 强制修复 schema conn.execute("PRAGMA writable_schema=ON") # 1. 尝试 Strategy 1:执行去重手术,仅保存 rowid 最低的那条原始定义 conn.execute(""" DELETE FROM sqlite_master WHERE rowid NOT IN ( SELECT MIN(rowid) FROM sqlite_master GROUP BY type, name ) """) # 2. 如果损坏依然存在,执行 Strategy 2:直接强制清理 FTS 虚表定义元数据 conn.execute("DELETE FROM sqlite_master WHERE name LIKE 'messages_fts%'") conn.execute("PRAGMA writable_schema=OFF")
🔗 本章子任务深入剖析专栏
为深度掌握 SessionDB,推荐阅读以下子技术专题: