mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
132 lines
7.6 KiB
Markdown
132 lines
7.6 KiB
Markdown
# ADR-0363:有界 Application Shutdown Receipt 与物理 Service Stop 证据
|
||
|
||
- 状态:Proposed(产品 receipt、Owner consumer、真实 manager 组合门与物理 recorder 协议已完成;固定实机报告待采集)
|
||
- 日期:2026-08-11
|
||
- 关联:QL-RFC-0001 D-196、D-274、D-275、ADR-0204、ADR-0206、ADR-0362
|
||
|
||
## 上下文
|
||
|
||
root service bridge 的 stop outcome 能证明 systemd/OpenRC 最终观察到 service
|
||
inactive、原 receipted PID/start identity 已消失,但不能单独证明 Application 完成了
|
||
有序 drain。manager 超时后 SIGKILL、进程崩溃或强制退出都可能产生相同的“PID
|
||
不存在”终态。native application recorder 可以从 stdout 看到 `stopped`,真实
|
||
init-managed release 则不能依赖 journal/syslog 的持久化与轮转策略。
|
||
|
||
因此物理 Edge 的 graceful-stop Gate 需要一个由 Application 在资源释放成功后发布的
|
||
持久事实,再与 root bridge outcome 和实时进程终态交叉验证。
|
||
|
||
## 决策
|
||
|
||
### 1. Shutdown receipt 属于现有 Local Application 生产进程
|
||
|
||
`@qinglong/local-application` 仅在收到首个 SIGINT/SIGTERM 且
|
||
`application.stop()` 返回 `stopped` 后发布:
|
||
|
||
```text
|
||
<absolute-config-path>.stopped.json
|
||
```
|
||
|
||
schema 为 `qinglong/local-application-shutdown-receipt@v1`,最大 4 KiB,包含:
|
||
|
||
- instance、Profile、signal 与固定 `stopResult=stopped`;
|
||
- 原 startup receipt SHA-256;
|
||
- 同一 boot ID、PID/start ticks、Node executable/version;
|
||
- stop 完成时的 Linux boot age;
|
||
- domain-separated SHA-256。
|
||
|
||
`timed_out`、非 Linux、startup receipt 未成功发布或 shutdown receipt 发布失败均不能
|
||
形成 graceful 事实。Linux 发布失败会让 Application CLI 失败关闭。
|
||
|
||
### 2. Startup/shutdown 共享一个文件安全协议
|
||
|
||
同 package 的 `production-process/lifecycleReceiptFile.ts` 统一实现:
|
||
|
||
- current real/effective UID 必须一致;
|
||
- parent 必须为当前 UID 控制且 group/other 不可写;
|
||
- deterministic stage 使用 `O_EXCL|O_NOFOLLOW`,只复用 owner `0600`、single-link、
|
||
有界旧 stage;
|
||
- write→fsync→parent identity recheck→atomic rename→final inode/mode/link/size recheck;
|
||
- directory fsync 在不支持的文件系统上保持 best effort。
|
||
|
||
每次启动和停止各只替换一个 current 小文件,不追加历史,不启动 timer、watcher、
|
||
listener 或清理 daemon。实现留在现有 `@qinglong/local-application` 的
|
||
`production-process/` 领域目录,不新增 package 或依赖。
|
||
|
||
### 3. Owner cutover stop 必须验证三种独立事实
|
||
|
||
adopted service stop 只有同时满足下列条件才可推进 `target_stopped`:
|
||
|
||
1. root bridge outcome 绑定当前 stop intent 且 manager state 为 `stopped`;
|
||
2. shutdown receipt 以 SIGTERM、startup receipt digest、boot、PID/start、Node 和
|
||
`stoppedBootAgeMs >= activeBootAgeMs` 绑定上一代 active record;
|
||
3. exact receipted PID/start identity 已不存在。
|
||
|
||
缺失或漂移的 shutdown receipt 进入 `manual_required`,原因固定为
|
||
`application_shutdown_receipt_unproved`。service cutover record 新增
|
||
`shutdownReceiptDigest`,使 instance head 的 source record 对 graceful 事实形成摘要
|
||
链。PID 消失不再单独足以推进 lineage。
|
||
|
||
### 4. 物理 stop recorder 复用同一双 authority 协议
|
||
|
||
非 package 命令 `evidence:physical-edge-direct-service-stop` 提供:
|
||
|
||
- `prepare`:读取并复验同 boot 的 direct start session/report、实时 receipt/PID 和
|
||
active/enabled init 状态,以 Owner 发布 fresh stop intent,输出固定 root bridge
|
||
command;可选 `--root-command-output` 在 Owner data path 内以 `0600`、no-replace
|
||
写出与 stdout 完全相同的 command handoff;
|
||
- `resume`:以 Owner 消费 stopped outcome,验证 shutdown receipt、旧进程身份消失、
|
||
service inactive 但仍 enabled,发布 `physical_edge_direct_service_stop_candidate`。
|
||
|
||
direct start 的 `prepare` 同样提供 `--root-command-output`。运维流程要求 operator
|
||
先把该 Owner handoff 精确安装到 canonical root-owned `0700` 目录中的 root-owned
|
||
`0600` 文件并复核摘要,再以 root 调用 `ql3-service-bridge run --command-file`;禁止
|
||
用手工 `systemctl`/`rc-service` 绕过 intent、barrier、outcome 与 Owner resume。start
|
||
bridge 必须返回 `active`,stop bridge 必须返回 `stopped`,然后才能分别 resume。
|
||
|
||
session/report 绑定 direct session、active report、startup receipt、boot、PID/start、
|
||
stop intent/outcome/observation digest。统一 physical importer 只在 stop report 与同一
|
||
direct start report 精确配对时增加
|
||
`init_managed_graceful_application_stop`;否则该项继续留在
|
||
`remainingRequiredEvidence`。
|
||
|
||
### 5. 证据边界
|
||
|
||
报告永久 `supported:false`。它不证明 disable/descriptor removal、突然断电恢复、
|
||
firmware shutdown、whole-device flash 写放大、release signature、Standalone 或
|
||
Cluster。graceful stop 也不能替代 power-loss Gate。
|
||
|
||
## 验收证据
|
||
|
||
- 2026-08-11 的真实 arm64 Local image 门发现 Linux runtime observation 的字段插入顺序与 receipt verifier 的 canonical 顺序不同,导致同一语义对象产生不同 digest;builder 已改为逐字段 canonical materialization,并增加 Linux 属性顺序回归。shutdown/process 专项 15 pass/1 Linux skip;修复后的同一镜像在 Edge 128 MiB/64 PIDs 与 Standalone 256 MiB/256 PIDs、read-only root、network none 下均完成 active→graceful stop,SQLite integrity 为 `ok`。这修复的是既有 D-275 产品路径,不改变 receipt schema、authority 或固定物理设备剩余 Gate。
|
||
- Local Application startup/shutdown/process 专项:17 pass/2 条 Linux 条件 skip;
|
||
- Owner service cutover/intent/contract:14/14,包含无 shutdown receipt 必须
|
||
`manual_required`;
|
||
- 真实 systemd/OpenRC × UID 0/10001 fresh+adopted 组合门全部通过,四组 adopted
|
||
active→SIGTERM shutdown receipt→target_stopped;报告 SHA-256
|
||
`28d5c29f91747f88b099c4918648199c21338fa48b9840c48d8027e70f14c954`,容器零残留;
|
||
- direct stop recorder/report/importer 与既有 physical 回归 35 pass/1 Linux skip;
|
||
- direct start/stop/aggregation handoff 与运维顺序专项 15 pass/1 Linux skip,回归固定
|
||
start prepare→root bridge→start resume→stop prepare→root bridge→stop resume;
|
||
- service bridge 的 systemd/OpenRC × UID 0/10001 四容器组合门再次全绿,覆盖 root
|
||
command file、exact replay、active/stopped observation、Owner outcome 与 adopted
|
||
cutover;报告 SHA-256
|
||
`0120972416d679ef3749e81e99f65ead5ca45bd29f43e4a1e624d675d8f4fc9a`,容器、卷、
|
||
网络零残留;
|
||
- 完整 backend 回归 1,155 pass/2 条条件 skip、0 fail;本机遗留 SQLite binding 使用
|
||
项目锁定的 node-gyp 12.4.0 构建并验证加载,未修改生产依赖声明;
|
||
- workspace 仍为 17 package;Local Application 15 source 中 13 nested、2 个根 entry,
|
||
无 shallow/single-source package、无新增生产依赖或常驻资源。
|
||
|
||
固定物理 systemd/OpenRC 设备尚未执行 start prepare→root bridge→reboot→resume→stop
|
||
prepare→root bridge→stop resume,因此本 ADR 与 D-275 保持 Proposed。
|
||
|
||
## 不采用方案
|
||
|
||
- **只看 PID 消失**:无法区分 graceful drain、SIGKILL 与 crash。
|
||
- **只看 systemd/OpenRC stopped**:manager 不拥有 Application 内部资源释放事实。
|
||
- **解析 journal/syslog**:OpenRC/嵌入式日志持久化与轮转不统一。
|
||
- **新增 shutdown daemon/socket**:增加低配设备常驻内存、端口和 authority。
|
||
- **为两个 receipt 新建 package**:它们只有一个生产者并共享同一文件安全边界,拆包
|
||
只会增加发布碎片。
|
||
- **把 graceful stop 当作断电证明**:两者故障模型相反,不能互相替代。
|