[错误报告] 3.3.5 删除番剧规则时 SQLite 持续 database is locked,连带影响 rename scheduler 和 auth refresh

Author: lrkkrCreated Aug 28, 2026Updated Aug 28, 2026

确认

  • 当前版本为最新版本: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 的数据库写入也同时失败。

重启容器后数据库恢复正常:

json
{"status":"ok","version":"3.3.5","db_ok":true}

因此目前看起来不像数据库损坏,而更像运行时 SQLite writer contention / 某个写事务持锁时间过长。

环境排查

已排除两个比较常见的外部原因:

  1. 只有一个 AutoBangumi 容器在运行,没有多个实例共用同一数据库。
  2. /app/data bind 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 + aiosqlitemodule/database/engine.py 中也明确说明现在会有多个 aiosqlite connection 并发写 SQLite,并通过 WAL + 5 秒 busy timeout 缓解:

python
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:

python
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:

python
await self.session.execute(
    delete(Torrent).where(Torrent.bangumi_id.is_(None))
)
await self.session.commit()

因此 delete_by_bangumi_id() 是否也可以改为单条 SQL,例如:

python
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 的数据库操作:

python
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 写事务。

可能的改进方向

以下只是根据日志和代码的初步判断,不确定最初持锁的具体事务是哪一个:

  1. delete_by_bangumi_id() 改成单条集合 DELETE,减少 ORM delete/flush 开销和竞争窗口。
  2. 考虑给 SQLite writer 增加进程级 async serialization(锁应覆盖完整 write transaction,而不是只覆盖 commit)。
  3. 或者对 SQLITE_BUSY / SQLITE_LOCKED 做事务级 rollback + retry/backoff;不能只 retry commit。
  4. 增加真实临时 SQLite 文件上的并发回归测试,例如同时执行:
    • delete_by_bangumi_id()
    • rename_operation.mark_notified()
    • auth BEGIN IMMEDIATE / refresh
  5. 仅提高 busy_timeout 可以缓解,但可能只是隐藏长事务问题。

预期行为

删除番剧规则时,即使后台 rename/RSS/auth 等任务恰好也在写数据库,也不应该让多个 API / scheduler 在几十秒内连续报 database is locked。至少应该能够通过内部串行化或重试恢复,而不需要重启容器。

实际行为

一次删除操作附近出现大量 SQLite write lock 错误,影响删除规则、rename scheduler 和 auth session refresh。重启容器后恢复,/health 返回 db_ok: true