Files
Agent-System-Dev/docs/recovery/u8-sqlite-instance-guard.md
T

66 lines
5.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# U8 SQLite 活动服务锁与耐久核验
状态:2026-10-12 用户批准恢复方案 r3 的 U8 后编写,随 `feature/u8-sqlite-instance-guard` 实施。
主记录:[服务重启与全量数据恢复方案](../../modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md) §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. 身份记录
两处记录,各管一种替换:
1. 锁文件内容(持有 fd 时读写):`v1 db_dev db_ino instance_id lock_gen`。`lock_gen` 是 16 字节随机值的十六进制。数据库 inode 与记录不一致,或记录还在而数据库文件消失,视为数据库被替换,**打开之前**拒绝。
2. 表 `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 入口已稳定,再补脚本级用例。