feat(ql3): observe boot runs without false origins

This commit is contained in:
whyour
2026-08-18 06:54:25 +08:00
parent 0ad96d38a6
commit 1ff5aed844
7 changed files with 382 additions and 20 deletions
@@ -5,7 +5,7 @@
- 决策者:QingLong Maintainers
- 关联 RFC[QL-RFC-0001](../QINGLONG_3_0_ARCHITECTURE_RFC.md)
- 前置决策:[ADR-0001](./ADR-0001-run-state-and-transaction-boundaries.md)
- Amended by[ADR-0445](./ADR-0445-schedule-service-origin-shadow-run-coverage.md)、[ADR-0446](./ADR-0446-system-crond-stable-shadow-admission.md)
- Amended by[ADR-0445](./ADR-0445-schedule-service-origin-shadow-run-coverage.md)、[ADR-0446](./ADR-0446-system-crond-stable-shadow-admission.md)、[ADR-0447](./ADR-0447-boot-shadow-and-non-origin-boundaries.md)
## 1. 决策摘要
@@ -17,7 +17,8 @@ QingLong 3.0 采用按执行来源渐进切换的三态兼容模式:
- shadow2.x 的 Crontab、RunningInstance、Shell 回调和日志仍是用户可见事实源;系统旁路创建 Run、RunAttempt 和 RunEvent,用于验证,不参与调度与结果判定。
- primaryRun 聚合成为事实源,2.x 字段变为兼容投影;所有外部副作用由新 Runtime 发起,Legacy 路径不得再次执行同一任务。
切换按 manual、scheduled、once、boot、grpc 等 execution origin 独立进行,不允许一次性全局切换。任何单次执行在被接受时即固定 owner 为 legacy 或 runtime,运行中不得切换执行引擎。
切换按已被入口事实证明的 manual、scheduled、boot 与内部任务 execution origin 独立进行,不允许一次性全局切换。`once``grpc` 等保留值只有在未来具备独立
trigger/admission identity 后才形成切换单元,不能由 schedule 或 transport 名称推导。任何单次执行在被接受时即固定 owner 为 legacy 或 runtime,运行中不得切换执行引擎。
Shadow 写入必须 fail-open:新模型写入失败只产生有界日志、指标和对账记录,不改变 2.x 的执行结果。Primary 路径在产生 spawn、派发或其他外部副作用前必须 fail-closed;只有能够证明外部副作用尚未发生时,才允许受控回退到 Legacy。
@@ -145,18 +146,19 @@ owner 在接受触发时写入执行上下文,并贯穿日志、指标和回
当前孵化实现只开放观察型 Shadow,不通过该环境变量提供 primary:
QL3_SHADOW_ORIGINS=manual,scheduled_node,scheduled_system,subscription,system,script
QL3_SHADOW_ORIGINS=manual,scheduled_node,scheduled_system,boot,subscription,system,script
- 未设置或设置为空时全部为 off。
- ADR-0446 后当前接受 `manual``scheduled_node``scheduled_system``subscription``system``script`;未知 origin 被忽略并记录
有界配置告警`once``boot``grpc`不开放
- ADR-0447 后当前接受 `manual``scheduled_node``scheduled_system``boot``subscription``system``script`;未知 origin 被忽略并记录
有界配置告警`once``grpc`是保留领域值:现有 `@once` 只是 schedule 标记,gRPC 只是传输入口,两者都不能单凭该字段推导 execution origin
- 配置在进程内首次使用时读取;edge 不启动 watcher,变更后需要通过既有进程重启或未来的显式 reload 生效。
- 兼容观察器在实际 HTTP/gRPC worker 中按需加载;关闭时不构造 Shadow 事实或任务摘要、不增加 ChildProcess 监听器、不初始化 Repository、不创建后台任务,也不引入额外数据库写入。
- `manual/scheduled_node/subscription/system/script` 只监听 Legacy 已创建的同一个 ChildProcess。`scheduled_system` 不持有 Node ChildProcess
- `manual/scheduled_node/boot/subscription/system/script` 只监听 Legacy 已创建的同一个 ChildProcess。`scheduled_system` 不持有 Node ChildProcess
只接受 system crond 显式标记后由 Shell start/finish 共用的稳定 execution IDfinish-only 回调可以幂等补齐 accepted→terminal 聚合。Shadow
代码不得调用 Executor 或第二次 spawn`subscription/system/script` 仅在 `ScheduleService` 已选中执行且 `onBefore` 成功后 accepted。
- 任意初始化、接受或后续写入失败都退化为 no-op,只记录不含命令、环境变量和 Secret 的稳定错误类型与有界计数。
- `boot` 虽复用 `runSingle`,仍携带独立 origin,当前不在允许列表中,不能被误记为 manual。
- `bootTask` 只把启用的 `@boot` 条目以固定 `boot` origin 交给 `runSingle`;普通 HTTP/gRPC `run` 仍使用 `manual``@once` 不会因 schedule
字符串被改记为 `once`gRPC 请求也不会因 transport 被改记为 `grpc`
该环境变量是 Alpha 兼容桥,不替代最终可审计的 `RuntimeRolloutConfig`。进入 primary 前必须改用具备配置校验、审计和 owner 固化语义的正式配置面。
@@ -488,12 +490,13 @@ Shadow 阶段至少记录:
2. manual shadow
3. scheduled_node shadow
4. scheduled_system shadow
5. once/boot/grpc shadow
6. manual primary for opt-in users
7. manual primary default
8. scheduled_node primary
9. scheduled_system primary
10. remove Legacy as execution owner
5. boot shadow
6. future source-proven trigger shadow(只有独立 once/grpc trigger 存在时)
7. manual primary for opt-in users
8. manual primary default
9. scheduled_node primary
10. scheduled_system primary
11. remove Legacy as execution owner
每一步必须独立通过门禁,不能因为 manual 路径稳定就直接切换 scheduled/system crontab。
@@ -0,0 +1,67 @@
# ADR-0447Boot Shadow 准入与非 Origin 边界
- 状态:Accepted
- 日期:2026-08-18
- 关联 RFCQL-RFC-0001 D-02、D-355、PR-4
- 关联 ADRADR-0001、ADR-0002、ADR-0445、ADR-0446
- AmendsADR-0002 的 Alpha Shadow allowlist 与 `once/boot/grpc` 裁决
## 上下文
Run domain 为未来触发协议保留了 `once``boot``grpc` execution origin,但 2.x 代码里的相似名称并不都代表独立的执行所有权边界:
- `CronService.bootTask` 在启动后筛选启用的 `@boot` Crontab,并显式调用 `runSingle(id, 'boot')`;这是一条真实、独立且由 Legacy Node worker
创建 ChildProcess 的触发路径。
- `@once` 当前只被 `isSpecialSchedule` 排除出 system/node 自动调度,没有独立自动触发器。用户从 HTTP 或 gRPC API 运行这类 Crontab 时仍进入
`CronService.run`,语义是 manual。
- gRPC `runCrons` 只是传输适配器,委托同一个 `CronService.run(ids)`transport 不能替代 actor、trigger 与 execution owner。
如果仅因为领域枚举或 schedule/transport 名称存在就同时开放三类 origin,会把同一次 manual 执行错误分类,污染 Shadow 完整率和未来 Primary 门禁。
## 决策
1. `QL3_SHADOW_ORIGINS` 增加 `boot`。默认仍为 off,只有显式列出 `boot` 时才构造事实、加载 Shadow Repository 或增加 ChildProcess listener。
2. 复用现有 `runSingle` 的唯一 ChildProcess,不调用 Executor、不再次 spawnRun 固定 `executionOwner=legacy``origin/triggerType=boot`
`triggeredBy=legacy:boot``legacy-cron:<id>` task identity。
3. `bootTask` 只准入启用的 `@boot` 条目;disabled、`@once` 与普通 cron 表达式不进入 boot 路径。一次进程启动中的每个匹配条目只调用一次
`runSingle(id, 'boot')`,现有并发限制和 Legacy 状态仍是执行事实源。
4. 不把 `@once` schedule 推导为 `once` origin。当前没有独立 once trigger/admission identity;由用户 API 发起的 `@once` Crontab 仍是 manual。
5. 不把 gRPC transport 推导为 `grpc` origin。现有 `runCrons` 与 HTTP `/crons/run` 共用 `CronService.run`;未来只有出现具备独立认证 actor、
accepted identity 和 owner 决策的 gRPC trigger 时,才可另提准入 Gate。
6. `once``grpc` 保留在共享 domain/schema vocabulary 中,避免破坏持久化兼容和未来协议,但 Legacy Shadow allowlist 拒绝它们并产生有界、低敏配置告警。
## 资源与部署影响
- 不新增 package、生产依赖、schema、migration、表、索引、timer、watcher、线程、端口或部署对象。
- 默认关闭时只多一个缓存 Set 成员,没有数据库读取、任务摘要、listener 或写入。
- 启用时只为本来就会执行的 boot ChildProcess 写现有 Run/Attempt/Event;不扫描历史 Crontab,不产生后台对账循环。
- 路由设备与 standalone 使用相同的惰性进程内路径;cluster 节点不会因此获得新的本机 owner,也不会把 transport 当作跨节点 authority。
## 被拒绝的替代方案
### 同时开放 once、boot、grpc
拒绝。只有 boot 有独立的真实触发路径;其余两个名称不能证明 execution origin。
### 根据 `cron.schedule === '@once'` 改写 origin
拒绝。schedule 描述任务定义,不描述本次触发者;用户手动执行 `@once` 任务仍是 manual。
### 根据请求来自 gRPC 改写 origin
拒绝。传输协议不是 owner。HTTP 与 gRPC 当前调用同一 service 方法,按 transport 分裂会让相同行为产生不同 Run 语义。
### 为 boot 另建 Executor 或 scheduler
拒绝。Shadow 只能观察 Legacy 已创建的进程;第二个执行器会违反单 owner 和零双跑约束。
## 验证
- 环境边界验证 boot 可显式启用,而 once/grpc 继续拒绝;未知配置只产生有界告警。
- `bootTask` 合同验证只选择 enabled `@boot`,并以固定 `boot` origin 单次派发。
- 真实 ChildProcess + SQLite 集成验证一个 legacy-owned boot Run/Attempt、成功终态与八个顺序 Event,且不保存原始 command/credential 文本。
- `@once` 真实执行验证继续产生 manual factgRPC `runCrons` 验证只委托 `CronService.run`,没有传入或推导 grpc origin。
- Legacy Shadow 聚焦测试 42/42、`build:back`、完整 backend 1,419 pass + 2 条条件 skip/0 fail、18-package clean
build/test、14/14 静态审计与 14/14 artifact 档位全部通过;artifact 字节与 D-354 一致。
- 本阶段没有修改数据库 schema/adapter、容器或 Kubernetes 拓扑,因此不重跑物理 PostgreSQL HA/K3s 门;完整证据与各档位字节记录在
QL-RFC-0001 D-355。
+1
View File
@@ -450,6 +450,7 @@
| [ADR-0444](./ADR-0444-fail-closed-release-tag-finalizer-and-replay-rehearsal.md) | Fail-closed Release Tag Finalizer 与重放演练 | Accepted(首份真实 GHCR response-loss 重放待实际 release tag |
| [ADR-0445](./ADR-0445-schedule-service-origin-shadow-run-coverage.md) | ScheduleService 执行来源的 Shadow Run 覆盖 | Accepted`scheduled_system` 后续由 ADR-0446 完成) |
| [ADR-0446](./ADR-0446-system-crond-stable-shadow-admission.md) | System Crond 稳定 Shadow 准入与回调重放 | Accepted |
| [ADR-0447](./ADR-0447-boot-shadow-and-non-origin-boundaries.md) | Boot Shadow 准入与 once/gRPC 非 Origin 边界 | Accepted |
## 规则