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:
ericwyuan
2026-09-13 09:34:24 +08:00
parent a494d60361
commit 3b5f7db51d
4 changed files with 134 additions and 5 deletions

View File

@@ -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 个测试绿。