feat(ql3): observe system crond runs idempotently

This commit is contained in:
whyour
2026-08-18 06:33:23 +08:00
parent 6831ea3de5
commit 0ad96d38a6
16 changed files with 858 additions and 73 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)
- Amended by[ADR-0445](./ADR-0445-schedule-service-origin-shadow-run-coverage.md)、[ADR-0446](./ADR-0446-system-crond-stable-shadow-admission.md)
## 1. 决策摘要
@@ -145,15 +145,16 @@ owner 在接受触发时写入执行上下文,并贯穿日志、指标和回
当前孵化实现只开放观察型 Shadow,不通过该环境变量提供 primary:
QL3_SHADOW_ORIGINS=manual,scheduled_node,subscription,system,script
QL3_SHADOW_ORIGINS=manual,scheduled_node,scheduled_system,subscription,system,script
- 未设置或设置为空时全部为 off。
- ADR-0445 后当前接受 `manual``scheduled_node``subscription``system``script`;未知 origin 被忽略并记录有界配置告警,
`scheduled_system``once``boot``grpc` 仍不开放。
- ADR-0446 后当前接受 `manual``scheduled_node``scheduled_system``subscription``system``script`;未知 origin 被忽略并记录
有界配置告警,`once``boot``grpc` 仍不开放。
- 配置在进程内首次使用时读取;edge 不启动 watcher,变更后需要通过既有进程重启或未来的显式 reload 生效。
- 兼容观察器在实际 HTTP/gRPC worker 中按需加载;关闭时不构造 Shadow 事实或任务摘要、不增加 ChildProcess 监听器、不初始化 Repository、不创建后台任务,也不引入额外数据库写入。
- 所有已开放 origin 都只监听 Legacy 已创建的同一个 ChildProcess。Shadow 代码不得调用 Executor 或第二次 spawn
`subscription/system/script` 仅在 `ScheduleService` 已选中执行且 `onBefore` 成功后 accepted。
- `manual/scheduled_node/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。
@@ -5,6 +5,7 @@
- 关联 RFCQL-RFC-0001 D-02、D-353、PR-4
- 关联 ADRADR-0001、ADR-0002、ADR-0003
- AmendsADR-0002 的当前 Alpha Shadow origin allowlist,不改变 Legacy owner 或 Primary 门禁
- Follow-up[ADR-0446](./ADR-0446-system-crond-stable-shadow-admission.md) 已完成本文保留的 `scheduled_system` 稳定准入 Gate
## 上下文
@@ -0,0 +1,81 @@
# ADR-0446System Crond 稳定 Shadow 准入与回调重放
- 状态:Accepted
- 日期:2026-08-18
- 关联 RFCQL-RFC-0001 D-02、D-354、PR-4
- 关联 ADRADR-0001、ADR-0002、ADR-0445
- AmendsADR-0002 的 Alpha Shadow origin allowlist 与 system crond callback 关联规则
## 上下文
system crond 不由 Node worker spawn。它执行 `crontab.list` 中的 Shell 命令,`task.sh/share.sh` 再分别向
`/open/crons/status` 发送 running 与 idle callback。旧 callback 只有 Cron ID、PID、log path 和秒级开始时间:running callback 丢失时,finish
无法证明 accepted identity;重复、乱序或 HTTP response loss 又可能把同一次执行创建为多个 Shadow Run。因此 D-353 明确拒绝从结束事实直接伪造
`scheduled_system` Run。
这条边界还必须区分 Node scheduler 和面板手动执行。三者最终都可能进入同一 Shell 脚本;仅凭 `ID`、PID 或 `real_log_path` 猜测来源会把同一次手动
执行同时记为 `manual``scheduled_system`
## 决策
1. 只有 `CronService.setCrontab` 在实际 system scheduler 模式写出的命令增加
`QL_EXECUTION_ORIGIN=scheduled_system`。Node scheduler 注册命令、manual/boot `runSingle` 和直接 Shell 调用不带该标记。
2. Shell 仅在标记存在时生成一次 `legacy-system:<start-seconds>:<uuid-v4>` execution ID,并在 start/finish callback 复用。UUID 优先读取 Linux
kernel random UUID,其次使用 `uuidgen`,最后使用已存在的 Node runtime;三者都不可用或输出非法时不发送 ID,Legacy 执行仍继续且不创建
`scheduled_system` Shadow Run。
3. `/open/crons/status` 只接受严格小写 UUIDv4 和正整数秒时间的可选 `execution_id`。旧客户端没有该字段时继续走原 Cron ID/PID/log correlation
不改变 2.x 请求兼容性。
4. 带稳定 ID 的 callback 使用专用 detached observation,不注册虚构 ChildProcess,也不进入易歧义的本机 registry。running 映射为
accepted→spawned→runningfinish 映射为 accepted→spawned→exited,因此 start request/response 丢失后,finish-only 仍能形成完整的终态聚合。
5. Shadow Run 固定 `executionOwner=legacy`、origin/trigger type `scheduled_system``triggeredBy=legacy:system-crond`、Project `default`
`legacy-cron:<id>` task identity。accepted/scheduled 时间从 execution ID 内的开始秒派生,task revision 摘要与 manual/node Cron 使用相同字段集合。
6. 带 request ID 的 `LegacyShadowRunWriter` 使用 request ID 和 accepted time 派生稳定 UUIDv7 形态的 Run/Attempt ID,并写既有
`(project_id,idempotency_key)` 唯一键。重放只有在 Run、Attempt、task revision、Cron ID、origin、request ID 与创建时间全部一致时才复用;任一
漂移都失败开放,不创建第二个 Run,也不改 Legacy callback 结果。
7. `QL3_SHADOW_ORIGINS` 增加 `scheduled_system`,但默认仍为 off;本决定不开放 Primary,不调用 Executor,不增加网络重试、timer、watcher 或后台
reconciler。
## Response-loss 与乱序语义
- start 成功且 response 丢失:finish 使用同一 execution ID,复用既有 Run 并终结。
- start request 未到达:finish-only 创建同一确定性 Run/Attempt 后直接终结。
- finish 成功且 response 丢失后重放:唯一键和确定性 ID 命中 exact replay,终态与 Event 数不增加。
- finish 先于迟到 start:迟到 start 命中已终态聚合,writer 的幂等状态推进保持终态不变。
- 相同 ID 携带不同任务定义:exact replay 校验失败,Shadow 记录有界 accept failureLegacy 状态更新与任务结果不受影响。
- 无 ID、ID 非法或来源未显式标记:不创建 `scheduled_system` Run;无 ID 的既有 callback correlation 保持原行为。
## 部署与资源影响
- 不新增 workspace package、生产依赖、schema、migration、表、索引、端口、Kubernetes object 或常驻进程。
- 复用 Run 表既有 idempotency unique indexSQLite 与 PostgreSQL Repository contract 不变。
- 默认关闭时后端只多一次缓存 Set 查询;system crond 命令仍可执行,Shell 只在显式标记下读取一个 UUID。
- Edge/路由设备优先读取 `/proc/sys/kernel/random/uuid`,不额外启动 Node;只有缺少 kernel UUID 与 `uuidgen` 时才使用已有 Node runtime 作为
兼容 fallback。
- 不增加 callback retry,避免低配设备网络阻塞扩大;本决定保证重放安全和 finish-only 收敛,不把“最终一定送达”伪装成已解决问题。
## 被拒绝的替代方案
### 继续使用 Cron ID、PID 与 log path
拒绝。它们在并发、PID 复用、`/dev/null` 日志和进程重启后都不能证明一次 execution identity。
### 每个 callback 在后端生成新 ID
拒绝。start/finish 以及 response-loss 重放会产生不同 Run,无法幂等收敛。
### 为 Shell callback 注册虚构 ChildProcess
拒绝。Node 不拥有 system crond 子进程;伪造 handle 会污染取消、恢复和 owner 语义。
### 默认启用 scheduled_system Shadow
拒绝。低写入寿命设备必须显式选择迁移观测成本,且 Primary 与对账门仍未完成。
## 验证
- Shell contract 覆盖显式 origin 才生成 ID、UUID 格式和 callback JSON 原样复用;
- SQLite 集成覆盖 running→finish、start response-loss replay、finish-only、重复终态和 task revision drift
- Bridge/registry/correlation/ChildProcess/ScheduleService/rollout 聚焦回归 38/38 通过,包含真实隔离 crontab 文件写入与 system/node 模式差异;
- `build:back`、4 个 Shell 文件语法检查与完整 backend 回归通过(1,415 pass、2 条条件 skip、0 fail);
- 18-package clean build/test 退出 014/14 静态审计与 14/14 artifact 档位均 compatibleartifact 字节与 D-353 相同;
- 物理 PostgreSQL HA/K3s 只在数据库或部署面变化时重跑,本决定不以相邻阶段证据冒充新运行。
+2 -1
View File
@@ -448,7 +448,8 @@
| [ADR-0442](./ADR-0442-catalog-ready-terminal-release-tag-publication.md) | Catalog-ready 的终态 Release Tag 发布与闭合收据 | Superseded by ADR-0443bounded promotion/closure 机制保留) |
| [ADR-0443](./ADR-0443-deployment-ready-terminal-release-finalization.md) | Deployment-ready 的终态 Release Finalization | Accepted(首份真实 GHCR deployment-ready finalization 待实际 release tag |
| [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` 幂等准入待独立 Gate |
| [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 |
## 规则