fix(fam-edge): SQLite 改每线程一条连接,修好 DiskGuard 等一切并发写
OracleDB 原来在 __init__ 里建一条 check_same_thread=False 的连接给全进程共用, 而 VideoQueue / PersonService / DiskGuard 三个后台线程 + gunicorn 的请求线程都在 并发读写它。sqlite3 的连接对象本来就不是可并发共享的,事务状态互相踩踏,线上 累计刷出三类错误(同一个根因): database is locked 71604 次 cannot start a transaction within a transaction 378 次 no more rows available 92 次(堆栈落在 commit()) 最严重的后果是磁盘守护形同虚设:DiskGuard 的清理被打断 2837 次,成功仅 128 次, 剩余空间在 2.7GB 和 16GB 之间来回荡——赶上 rclone 集中下载(实测 5 分钟写入 12.6GB)就掉进危险区。NAS 推来的运动事件被 500 打回也是它(失败批次不推进游标, 下一轮补推,所以没丢事件)。 改法:_conn 改成 @property,从 threading.local() 取当前线程的连接,没有就新建 (WAL + busy_timeout=10000)。做成 property 是因为外部调用方(api_gateway 的 activity 端点)也在直接用 db._conn.execute(...),这样全部现有调用点原样工作。 close() 相应改成收掉所有线程开过的连接。_write_lock 保留,复合写语义不变。 测试:新增 8 线程 × 25 轮并发读写、close 收连接两个用例;在旧代码上稳定复现 同族错误 cannot commit transaction - SQL statements in progress。 线上验证:重启后 DiskGuard 立刻跑通一轮(清 40 个文件,剩余回到 15.3GB), 三类 SQLite 错误在重启后的日志里均为 0。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
31
PROGRESS.md
31
PROGRESS.md
@@ -612,3 +612,34 @@ people 59;外网 `/` `/timeline` 200、`/login` 302、`/api/*` 未登录 401
|
||||
|
||||
**待办**:NAS 侧部署 fam-notifier(只能用户手动,密码登录);chat_history 一次性迁移
|
||||
(`fam-core/scripts/import_chat_history.py`,幂等);frpc.toml 里的 8000 映射可删。
|
||||
|
||||
## 修复 fam-edge 多线程共用 SQLite 连接(2026-09-13)
|
||||
|
||||
**触发**:用户问「甲骨文剩余硬盘小于 10G 会删东西的逻辑怎么没了?」。查下来逻辑一直在、
|
||||
也没被迁云动过(`disk_guard.py` 最后一次改动还是 8/28 新增它那次),但它 95% 的检查在空转:
|
||||
|
||||
```
|
||||
DiskGuard「本轮清理完成」 128 次
|
||||
DiskGuard「检查异常」 2837 次 ← 22 倍
|
||||
```
|
||||
|
||||
失败原因全是 `cannot start a transaction within a transaction`。表现就是磁盘剩余空间在
|
||||
2.7GB 和 16GB 之间来回荡——清理能不能成功全靠运气,赶上 rclone 集中下载
|
||||
(实测 5 分钟写入 12.6GB)就掉进危险区。
|
||||
|
||||
**根因**:`OracleDB.__init__` 建一条 `sqlite3.connect(check_same_thread=False)` 的连接
|
||||
给全进程共用,而 VideoQueue / PersonService / DiskGuard 三个后台线程 + gunicorn 的 4 个
|
||||
请求线程都在并发读写它。sqlite3 的连接对象本来就不是可并发共享的,事务状态互相踩踏。
|
||||
同一个根因在线上刷出三类错误,累计:`database is locked` 71604 次、
|
||||
`cannot start a transaction within a transaction` 378 次、`no more rows available` 92 次
|
||||
(后者堆栈落在 `self._conn.commit()`,是游标被别的线程重置的典型症状)。
|
||||
运动事件推送被 500 打回也是它——NAS 侧失败批次不推进游标,下一轮补推,所以没丢事件。
|
||||
|
||||
**修复**:`_conn` 改成 `@property`,从 `threading.local()` 取当前线程的连接,没有就新建
|
||||
(WAL + busy_timeout=10000)。`close()` 相应改成收掉所有线程开过的连接。因为外部调用方
|
||||
(如 `api_gateway` 的 activity 端点)也在直接用 `db._conn.execute(...)`,做成 property
|
||||
可以让全部现有调用点原样工作,不用逐个改。`_write_lock` 保留,复合写语义不变。
|
||||
|
||||
**测试**:新增 2 个用例(8 线程 × 25 轮并发读写、close 要收掉所有连接)。在旧代码上
|
||||
稳定复现同族错误 `cannot commit transaction - SQL statements in progress`,修复后通过;
|
||||
fam-edge 全套 159 个测试绿。
|
||||
|
||||
Reference in New Issue
Block a user