node:sqlite 备份报 not an error:检查首步与写事务重叠
备份刚开始就遇到源连接写事务时,Node 可能以 ERR_SQLITE_ERROR、errcode 0 拒绝。先结束这笔写事务再启动备份,不能把失败文件视为可用副本。
调用 node:sqlite 的 backup(),Promise 却以 ERR_SQLITE_ERROR 拒绝,errcode 为 0,消息甚至是 not an error。这不是成功提示。若备份启动时,同一个源连接还持有写事务,应先核对备份第一步与事务提交的先后顺序。
这条表现出现在 Node 22.22.1、24.21.0。正式 24.22.1 与 26.12.1 的备份实现仍保留相应路径,不能把更新到这两个版本当成已经修好。这里针对的是异步备份与源连接自己的写事务重叠,不是所有 SQLite 锁错误。
第一页还没复制,剩余页数也可能是零
底层 sqlite3_backup_step() 在第一步遇到 SQLITE_BUSY 或 SQLITE_LOCKED 时,可以尚未更新剩余页数。Node 会先放过这两个可重试状态,却只在剩余页数非零时重新安排工作。
于是,剩余页数仍为零、状态又不是 SQLITE_DONE 的分支进入拒绝路径。它从目标连接取错误信息,而目标连接可能没有错误,最终出现 errcode 0 和 not an error。这里的零不能说明备份已经完成,也不能直接还原第一步的真实 SQLite 状态。
保存完整异常及应用已有的事务日志,核对源连接是否在调用 backup 前执行了写入,提交是否在备份启动之后。仅看到 WAL 模式或一个 locked 字样,还不足以确认这条路径;磁盘、权限、目标打开失败等错误仍要分别处理。
让源写事务先结束,再启动备份
由拥有这笔事务的业务代码按原有规则提交或回滚,然后再安排备份。不能在保持该写事务打开的同时 await backup(),又指望 await 之后的代码去释放它。也不要为了备份直接提交一笔本来应当回滚的业务操作。
应用有多条写入路径时,应由统一调度协调源连接的写事务与备份启动,而不是只在某个备份函数前加固定延时。延时并不证明事务已经结束,负载变化后仍可能重叠。
确实需要重试时,先确认当前异常与上述重叠一致,等待这笔事务真正结束,再使用有次数和总时限的重试。不能把所有 ERR_SQLITE_ERROR 或所有 errcode 0 都当成 BUSY;I/O、内存、只读等失败不能套同一策略。遇到不同异常应保留并交给原有错误处理。
失败目标不能接着当备份使用
backup 的目标路径如果已经存在,会被覆盖。排查和重试应使用专门的新目标,不要拿唯一的历史备份或源数据库当输出。备份被拒绝后,不把已经出现的目标文件视为完成,也不要因为消息写着 not an error 就自动标记任务成功。
按项目原有恢复验收,在隔离数据中检查备份可打开、内容和一致性满足要求,再让它进入正式备份轮换。验收时保留写事务与首次备份启动重叠的条件,并确认调度改动后备份确实在事务结束后开始。无需删除源库的 WAL 或 SHM 文件,也不应通过吞掉 Promise 拒绝让任务看起来恢复。


