Files
qinglong/docs/adr/ADR-0022-pinned-task-execution-plan-materialization.md
T

98 lines
5.4 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.
# ADR-0022Pinned Task Revision 与本地执行计划物化
- 状态:Proposed
- 日期:2026-07-18
- 关联:QL-RFC-0001、ADR-0003、ADR-0014、ADR-0021、ADR-0023、ADR-0024
## 上下文
自动重试创建 Attempt N+1 后,控制面必须重建与原 Run 相同 Task revision 的执行意图,同时为新 Attempt 创建新的日志、Secret 和 completion capability。直接读取“当前 Crontab”有四个不可接受的问题:
- Task 在 Attempt 1 之后可能已编辑或删除;
- 当前行无法证明与 Run 中的 `task_revision` 一致;
- 把 Secret 环境或 callback token 存入 Task revision 会扩大静态泄漏面;
- 把 Attempt 1 的日志、handle 或 callback capability复制给 Attempt N+1 会破坏隔离和 fencing。
edge 必须能用本地存储完成精确解析,cluster-control 则需要共享数据库和 Artifact;两种部署不能因此产生两套 plan 语义。
## 决策
### 1. Task revision 只保存不可变执行模板
`PinnedTaskExecutionRevision` 固定:
- `projectId/taskId/taskRevision`
- `executorType`
- command、working directory、environment policy、timeout、termination grace 和 resource policy
- 一个不透明、不可变的 `contextRef`
模板不包含 Run/Attempt ID,也不包含 Secret 明文、callback token、进程身份或可写日志句柄。命令和资源字段复用共享 `ExecutionSpec` 边界与大小限制,未知字段在物化时剥离。
### 2. 精确 revision 解析,禁止回退 latest
`TaskExecutionRevisionSource.resolve()` 只接受冻结的 Project/Task/revision 三元组。找不到精确 revision 时返回 unavailable;禁止读取最新版本、当前 Crontab 或名称相同的 Task 继续执行。
返回记录必须再次与持久化 candidate 的 Project、Task、revision 和 executor type 完全比较。漂移在任何 Secret、output 或 Executor 副作用前拒绝。
### 3. Attempt 身份由 Dispatcher 注入
物化器只从 candidate 注入 `runId/attemptId/projectId/taskId/taskRevision`,再通过共享 `cloneExecutionSpec()` 校验和深拷贝。Revision source 返回对象即使在运行时夹带同名身份字段,也不能覆盖 candidate。
这样同一 Task revision 可以确定性生成 Attempt 1、Attempt N+1 或远程 offer 的业务执行模板,同时每个 Attempt 保持不同的 ownership capability。
### 4. 动态能力由独立 context materializer 创建
`LocalExecutionContextMaterializer` 接受冻结 candidate 和 `contextRef`,为当前 Attempt 创建:
- 只在内存存在的 Secret 环境;
- attempt-scoped output sink
- 可选的取消 signal
- 非阻塞、幂等资源清理回调。
`contextRef` 是解析配方或受控引用,不是 Secret 本身。环境规范化限制最多 256 项、总计 256 KiB、单值 64 KiB;名称、NUL、输出 sink 和 signal capability 在激活前校验。环境复制到无原型、只读对象,避免调用方后续修改和 `__proto__` 污染。
context 缺失时 plan unavailable,不回退到继承宿主环境。校验失败时先调用可选清理,再向上报告;清理失败不能覆盖原始校验错误,也不能把 Secret 写入日志。
### 5. edge 与 cluster 只替换 adapter
- edge/standalone 可以用本地不可变 Task revision 表或内容寻址文件,并用本地 SecretStore/Artifact 日志实现 context materializer
- cluster-control 使用 PostgreSQL revision metadata、对象 Artifact 和集中 Secret provider
- 两者共用 candidate identity、template 校验、context 上限和 `ExecutionSpec` 物化逻辑。
当前 `next` 已按 ADR-0023/0024 提供 append-only SQLite Task revision/context recipe migration、Repository contract、内存 Secret provider port 和私有文件 Artifact allocator;仍没有生产 SecretStore、PostgreSQL/object Artifact adapter、容量 retention 或 startup 装配,因此保持 production unreachable。
## 影响
正面影响:
- Task 编辑不会静默改变历史 Run 或自动重试的执行内容;
- Secret 与静态 revision 分离;
- Attempt N+1 获得新的日志和 callback authority
- edge 和 cluster 能共享严格相同的 fail-closed 物化语义。
代价与风险:
- 必须实现 Task revision retention,不能只保存当前 Task 行;
- `contextRef` 的备份、权限和迁移需要与 Secret/Artifact ADR 对齐;
- revision 可读但 Artifact 已丢失时,Run 会保持 unavailable,必须有告警和运维处置;
- 256 KiB 环境上限可能要求大型输入改用 Artifact,而不是环境变量。
## 未选择的方案
1. **重试时读取当前 Crontab**revision 漂移,拒绝。
2. **把完整 ExecutionContext JSON 存入 Run**Secret 和 capability 静态泄漏,拒绝。
3. **复制旧 Attempt 的日志、handle 和 callback token**:破坏 Attempt 隔离与 fencing,拒绝。
4. **找不到 revision 时回退 latest**:执行内容不可审计,拒绝。
5. **edge 与 cluster 各写一套 spec builder**:验证和限制会漂移,拒绝。
## 验证要求
- revision source 只收到冻结的精确三元组;
- revision 缺失时不调用 context materializer
- revision/executor 漂移在 context 和 Executor 副作用前拒绝;
- source 夹带身份字段不能覆盖 candidate;
- command/resource policy 和环境均深拷贝;
- context 缺失不回退,校验失败调用清理;
- 环境项数、单值、总字节、NUL、输出 sink 和 prototype pollution 有边界测试;
- Node 22、Node 24 全量回归通过。