5.1 KiB
U8 SQLite 活动服务锁与耐久核验
状态:2026-10-12 用户批准恢复方案 r3 的 U8 后编写,随 feature/u8-sqlite-instance-guard 实施。
主记录:服务重启与全量数据恢复方案 §7 U8、KTD13、KTD14。
本文件只定本增量的可判定行为。账户 CLI 不迁移、不建库的合同由账户方案 U7 实现。
1. 锁对象
活动服务在打开 SQLite(迁移发生在打开内部)之前取得锁,并持有到存储关闭之后。PostgreSQL 档不使用这把锁,沿用迁移咨询锁与执行所有权。
| 规则 | 行为 |
|---|---|
| 路径 | 先 Abs,已存在的路径再 EvalSymlinks。数据库文件若是符号链接,锁放在解析后的真实文件旁边 |
| 锁文件 | <db>.lock。O_RDWR|O_CREAT|O_CLOEXEC|O_NOFOLLOW,模式 0600。永不 unlink。数据目录不存在时,活动服务在取锁前创建该目录 |
| 互斥 | flock(LOCK_EX|LOCK_NB)。占用返回「活动服务已在运行」,调用方不得打开库 |
| 替换检测 | 取锁后比较 fd 与路径的 device/inode,不一致则关闭并重试,仍不一致则拒绝 |
| 硬链接 | 解析后的数据库 nlink>1 直接拒绝,不能用 adopt 绕过 |
| 平台 | unix 实现。其他平台的 SQLite 档拒绝启动。PostgreSQL 档不受此限制 |
| 范围 | 只挡活动服务与 migrate。user、apikey、identity 不取这把锁 |
svc 的 runtime/<name>/.start.lock 只串行化脚本生命周期,挡不住直接执行二进制。本锁补的是这条缺口。
2. 身份记录
两处记录,各管一种替换:
- 锁文件内容(持有 fd 时读写):
v1 db_dev db_ino instance_id lock_gen。lock_gen是 16 字节随机值的十六进制。数据库 inode 与记录不一致,或记录还在而数据库文件消失,视为数据库被替换,打开之前拒绝。 - 表
instance_guard(迁移 v36,单行):记录锁文件的 device/inode、instance_id 与lock_gen。守卫建立之后,锁文件 inode 与表不一致,或表里已有lock_gen而锁文件记录缺失/不一致,视为锁根丢失或锁文件被重建(含 inode 被回收后内容丢失),打开并迁移之后拒绝,且不改写表。
尚无锁文件记录、表中也无行:视为首次建立(含旧库升级),写入两边。这是升级路径,不是 adopt。
同时换掉数据库文件和锁文件、新库里也没有本表:无法与真正的首次建立区分。此时若设置了 AGENT_SYSTEM_INSTANCE_ID,仍由既有身份校验拒绝错库。该剩余风险写入任务卡,不在本增量用第二套锁文件解决。
instance_id 取 AGENT_SYSTEM_INSTANCE_ID。表和锁文件都已有非空值且与本次不一致时拒绝。本次为空则不比较,保留表内原值。
显式 --adopt-instance-guard(仅 serve 与 migrate)在已持锁的前提下重写两边记录。它只接受「数据库文件还在但 inode 已变」和「锁记录损坏」。记录还在而数据库文件消失时,adopt 也不打开、不新建空库。锁文件若是指向数据库的符号链接,或与数据库同一 inode,直接拒绝且不写入。硬链接拒绝不受该标志影响。
身份校验在写入守卫之前完成。AGENT_SYSTEM_INSTANCE_ID 已设置但与库内身份不符时,不留下本次的 instance id。
3. 耐久
SQLite DSN 增加 synchronous(FULL)。打开后在同一应用连接读回 journal_mode=wal 且 synchronous=2,否则关闭并拒绝。
PostgreSQL 从应用连接读 pg_settings:fsync、full_page_writes、synchronous_commit 任一为 off,或读不到,则拒绝。synchronous_commit=local 不是 off,不拒绝。
写失败必须把错误返回给调用方。测试用 RLIMIT_FSIZE 制造失败,并忽略 SIGXFSZ,以免进程被信号杀死而看起来像成功。
4. migrate 子命令
agent-system migrate --data <dir>:SQLite 先取活动服务锁,有服务占用则拒绝,再打开(打开即迁移)并走第 2 节的建立/拒绝规则。
agent-system migrate --dsn <dsn> 或环境变量 AGENT_SYSTEM_DSN:不取文件锁,迁移仍用 PostgreSQL 咨询锁排队。
5. 验收
| 编号 | 判据 |
|---|---|
| T8-1 | 两个进程启动同一 SQLite,后者在打开前失败;前者的 running 与 waiting_hitl 行保持原状态 |
| T8-2 | 符号链接与相对路径进入同一把锁;硬链接拒绝;数据库替换拒绝;锁文件删除后重建拒绝 |
| T8-3 | 持锁进程被 SIGKILL 后,同数据目录可以再次取锁并打开 |
| T8-4 | 本增量:服务持锁期间另一进程的短事务可提交;缺库的 OpenExisting 不建目录;migrate 与持锁服务互斥。账户 CLI 不迁移归 U7 |
| T8-5 | 换端口、工作目录或路径拼写仍落在同一锁文件上 |
| T8-6 | pragma 读自应用连接;超限写入返回错误 |
tests/test_svc_storage_ownership.py 依赖 svc 启动路径。本任务不改 scripts/svc.sh,该文件留到账户 U5;若届时 svc 入口已稳定,再补脚本级用例。