[错误报告] 3.3.5 删除番剧规则时 SQLite 持续 database is locked,连带影响 rename scheduler 和 auth refresh
确认
- 当前版本为最新版本:3.3.5
- 已搜索现有 issue,未找到相同的
database is locked/ SQLite 并发问题 - 问题类型:程序运行问题
当前程序版本
3.3.5(Docker: ghcr.io/estrellaxd/auto_bangumi:latest)
问题描述
在 WebUI 删除番剧规则时,后端连续出现 sqlite3.OperationalError: database is locked。
这次并不是一次瞬时锁冲突:日志中约 30 秒内反复出现相同错误(共观察到 19 处),并且不仅删除规则失败,后台 rename scheduler 和浏览器 session refresh 的数据库写入也同时失败。
重启容器后数据库恢复正常:
{"status":"ok","version":"3.3.5","db_ok":true}因此目前看起来不像数据库损坏,而更像运行时 SQLite writer contention / 某个写事务持锁时间过长。
环境排查
已排除两个比较常见的外部原因:
- 只有一个 AutoBangumi 容器在运行,没有多个实例共用同一数据库。
/app/databind mount 位于本机 ext4 文件系统,不是 SMB/NFS/FUSE:
TARGET SOURCE FSTYPE OPTIONS
/mnt/store /dev/md0 ext4 rw,relatime,stripe=8191容器挂载:
/mnt/store/ab-data/config -> /app/config
/mnt/store/ab-data/data -> /app/data发生问题时的关键日志
删除番剧关联的 torrent 记录时:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is locked
[SQL: DELETE FROM torrent WHERE torrent.id = ?]
[parameters: [(699,), (711,), (723,), (737,), (751,), (777,), (791,), (804,) ... (841,), (845,)]]同一时间 rename scheduler 也无法写数据库:
ERROR module.core.scheduler: rename tick failed: (sqlite3.OperationalError) database is locked
[SQL: UPDATE rename_operation SET notified_at=?, updated_at=?
WHERE rename_operation.id = ?
AND rename_operation.notified_at IS NULL RETURNING id]稍后 auth session refresh 连 BEGIN IMMEDIATE 都无法获取写锁:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) database is locked
[SQL: BEGIN IMMEDIATE]代码观察
3.3.5 已将数据库全面切换为 AsyncSession + aiosqlite,module/database/engine.py 中也明确说明现在会有多个 aiosqlite connection 并发写 SQLite,并通过 WAL + 5 秒 busy timeout 缓解:
cursor.execute("PRAGMA journal_mode=WAL")
cursor.execute("PRAGMA busy_timeout=5000")相关代码:
backend/src/module/database/engine.py
SQLite 即使在 WAL 下仍然只有一个 writer,因此如果某个写事务持锁超过 5 秒,其它 API / scheduler writer 都会抛 database is locked。
另外,删除番剧的实现似乎会放大竞争窗口。
TorrentDatabase.delete_by_bangumi_id() 当前先 SELECT 所有 Torrent ORM 对象,再逐个 session.delete(),最终 commit:
statement = select(Torrent).where(Torrent.bangumi_id == bangumi_id)
result = await self.session.execute(statement)
torrents = list(result.scalars().all())
for t in torrents:
await self.session.delete(t)
if count > 0:
await self.session.commit()相关代码:
backend/src/module/database/torrent.py
而同文件的 delete_orphans() 已经使用直接集合 DELETE:
await self.session.execute(
delete(Torrent).where(Torrent.bangumi_id.is_(None))
)
await self.session.commit()因此 delete_by_bangumi_id() 是否也可以改为单条 SQL,例如:
result = await self.session.execute(
delete(Torrent)
.where(Torrent.bangumi_id == bangumi_id)
.execution_options(synchronize_session=False)
)
await self.session.commit()这应该至少可以缩短删除时的事务/flush 路径。
另外 TorrentManager.delete_rule() 当前会依次进行多个各自 commit 的数据库操作:
await self.db.torrent.delete_by_bangumi_id(int(_id))
await self.db.bangumi.delete_one(int(_id))
await self._disable_orphan_sub_rss(data)相关代码:
backend/src/module/manager/torrent.py
与此同时 rename operation、notification inbox、auth refresh、RSS scheduler 等也都有各自独立的 SQLite 写事务。
可能的改进方向
以下只是根据日志和代码的初步判断,不确定最初持锁的具体事务是哪一个:
- 将
delete_by_bangumi_id()改成单条集合 DELETE,减少 ORM delete/flush 开销和竞争窗口。 - 考虑给 SQLite writer 增加进程级 async serialization(锁应覆盖完整 write transaction,而不是只覆盖 commit)。
- 或者对
SQLITE_BUSY/SQLITE_LOCKED做事务级 rollback + retry/backoff;不能只 retry commit。 - 增加真实临时 SQLite 文件上的并发回归测试,例如同时执行:
delete_by_bangumi_id()rename_operation.mark_notified()- auth
BEGIN IMMEDIATE/ refresh
- 仅提高
busy_timeout可以缓解,但可能只是隐藏长事务问题。
预期行为
删除番剧规则时,即使后台 rename/RSS/auth 等任务恰好也在写数据库,也不应该让多个 API / scheduler 在几十秒内连续报 database is locked。至少应该能够通过内部串行化或重试恢复,而不需要重启容器。
实际行为
一次删除操作附近出现大量 SQLite write lock 错误,影响删除规则、rename scheduler 和 auth session refresh。重启容器后恢复,/health 返回 db_ok: true。
Source: EstrellaXD/Auto_Bangumi