Files
qinglong/docs/adr/ADR-0226-bounded-plugin-package-workflow-frontier.md
T

149 lines
7.5 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-0226:有界 Plugin Package Workflow Frontier
- 状态:Proposed
- 日期:2026-07-30
- 关联 RFCQL-RFC-0001 D-03、D-207、D-212
- 关联 ADRADR-0156、ADR-0223、ADR-0224、ADR-0225
## 背景
ADR-0223 至 ADR-0225 已把 generation-bound Workflow DAG 原子展开为一个
runtime-owned Workflow Run、全部 StepRun、Event/mutation 与 admission receipt。
准入后的 Workflow Run 固定为 `running`,而 Task root StepRun 为 `ready`、其余
StepRun 为 `pending`
首次 PostgreSQL HA 重跑暴露出一个恢复边界错误:通用 Cluster Run recovery 把没有
RunAttempt 的 Workflow 聚合 Run 识别成孤儿执行。该判断对普通 Task Run 成立,但
Workflow Run 本身是编排聚合,不应直接进入普通 Task dispatcher,也不应伪造一个
RunAttempt 或终态绕过恢复。
同时,低配 Edge 不能为每个 dormant Workflow 建立 timer、watcher 或内存队列;Cluster
多副本也不能依赖进程内 frontier。
## 决策
### 1. 通用恢复器排除 Workflow 聚合
PostgreSQL 通用 recovery source 必须排除
`trigger_type='plugin_package_workflow'`。这不是忽略孤儿工作,而是明确 ownership:
- 普通 Task Run/RunAttempt 继续由既有 Cluster recovery 处理;
- Workflow 聚合 Run 只由专属 Workflow frontier/recovery 处理;
- Workflow 的 Task Step 后续仍必须通过既有 RunAttempt/Executor fence 启动,不能把
聚合 Run 作为普通 Task 分发。
### 2. 纯 frontier 契约留在现有 runtime-core
新增显式 subpath
`@qinglong/runtime-core/plugin-package-workflow-frontier`,不新增 workspace package、
第三方依赖、进程或连接。一次 resolution 只接受:
- exact normalized immutable plan
- exact runtime-owned `plugin_package_workflow` Run fence
- 与 plan 一一对应的最多 128 个 durable StepRun
- 单调的存储 observation time。
契约对完整 StepRun 集合、Run/Project/Workflow/publication identity、Task
definition ref/digest 和 record digest 失败关闭。
### 3. 一次纯计算收敛依赖 frontier
resolution 使用确定性规则:
1. pending Step 的所有 `needs` 都为 `succeeded` 时生成 `pending → ready` mutation
2. 任一祖先为 `failed|skipped|cancelled|timed_out` 时,依赖失败可在同一 pass 传递,
下游生成 `pending → skipped`result code 固定为
`dependency_not_succeeded`
3. mutation 按 canonical Step key 顺序绑定连续 Run version/event sequence
4. frontier event ID 不超过双方言共有的 36 字节,mutation ID 由 plan、StepRun
identity/version 与目标状态做 domain-separated digest
5. resolution 返回 projected ready StepRun ID,但不把读取结果解释为执行 claim。
当全部 required StepRun 终结时,纯契约给出固定的 Run terminal 建议:
`failed/skipped → failed`,其次为 `timed_out``cancelled`,全部成功才为
`succeeded`。terminal transition 同时绑定 expected Run version/event sequence、
固定 error code、finished time 和不超过 36 字节的确定性 RunEvent identitystorage
adapter 必须把它与本轮 StepRun mutation 原子提交。
### 4. 双方言使用同一 port,不新增 package
同一显式 subpath 公开最多 64 条的 keyset candidate page 和
`listCandidates/advance` repository port
- SQLite adapter 位于既有 `@qinglong/local-sqlite`,复用单
`LocalSqliteOperationAuthority``BEGIN IMMEDIATE`candidate SQL 只返回可推进
pending dependency 或全部 StepRun 已终结但聚合 Run 未终结的 Workflow,不为 root
ready 或等待中的 Workflow 制造空写;
- PostgreSQL adapter 位于既有 `@qinglong/cluster-postgres`,由 caller 驱动有界
keyset page`advance` 使用短 `SERIALIZABLE` transaction,锁定聚合 Run 和完整
StepRun 集合,使用数据库 transaction clock,并对 serialization/deadlock 做有界
fresh-client retry
- 两者都先验证 immutable plan、Run identity 和所有 StepRun canonical JSON/mirror
再按 StepRun version/digest/status CAS;一轮只更新一次聚合 Runversion/event
sequence 按 mutation 数加 terminal event 数整体推进;
- StepRun、RunEvent、StepRunMutation 和 terminal Run/Event 同事务提交,任一 drift、
fence 或写入冲突整体回滚;已终态 aggregate 重放只读收敛为 `settled`
没有新增 migration、workspace package、第三方依赖、连接、timer、watcher 或后台
scanner。ready Task 的执行 admission 仍必须在后续原子绑定 StepRun 与
RunAttempt/Executor fencefrontier 的 ready projection 本身不是执行 claim。
### 5. admission replay 只冻结准入证据,不冻结当前执行状态
双方言 admission exact replay 继续严格验证 immutable plan、receipt、初始 RunEvent、
初始 StepRun mutation 与 admission step ledger,但不再要求当前 Run/StepRun 永远停在
准入初态。当前 aggregate 只允许合法运行/终态、identity 不变、canonical record
完整且 version/event sequence 单调;初始 Event 查询固定截止 admission final sequence。
因此合法 frontier/Executor 推进后仍可 inspect/exact replay,而历史准入证据缺失或漂移
仍失败关闭。
## 不采用方案
### 把 Workflow 聚合 Run 交给普通 dispatcher
拒绝。聚合 Run 的 `taskId` 是 Workflow IDrevision 是 publication digest,不是一个
普通 Task execution revision;直接分发会错误解释定义并把一个 DAG 当成一次 Task。
### 为恢复器伪造 RunAttempt 或把聚合 Run提前置为终态
拒绝。伪造 Attempt 会制造不存在的执行事实;提前终结会隐藏尚未执行的 ready/pending
StepRun。
### 每 Workflow 常驻 coordinator
拒绝。它给低配路由器增加空闲内存、唤醒与写放大,并让 Cluster 正确性依赖进程内
ownership。
### 新建 workflow package
拒绝。纯契约没有独立部署、重依赖或权限域,另拆会违反 D-207 的 package 粒度标准。
## 当前验证
1. 通用 PostgreSQL recovery SQL 影响评估为 LOW,新增 Workflow trigger exclusion
2. runtime-core 405/405,包含 frontier 纯契约 6/6 与后续 Attempt admission 4/4
3. `local-sqlite` 180/180;真实 SQLite frontier 覆盖 dependency ready、失败向下游
skipped、成功/失败 aggregate terminal、bounded page、drift rollback、settled 与
admission-after-progress exact replay
4. `cluster-postgres` 223 pass、1 条无测试 URL 条件 skipmock 契约覆盖
`SERIALIZABLE`、Run/StepRun row lock、单 aggregate CAS、terminal 原子写、
40001 fresh-client retry、settled 与显式 subpath
5. PostgreSQL 18.4 arm64 physical HA 在禁止 image pull 下完整通过,包含
`remote_apply`、timeline 1→2、旧主 fencing、promotion、`pg_rewind` 和两 fresh
control replicas;新增
`pluginPackageWorkflowFrontierTerminalizesAtomically`
`pluginPackageWorkflowFrontierExactlyReplays`
`pluginPackageWorkflowFrontierSurvivesPromotion` 三项 gate,总
`passed=true`
6. HA durable facts 为 1 个 Workflow admission、5 个 Workflow RunEvent、3 个
StepRunMutationpromotion 后 frontier 返回同一 `succeeded/settled` winner
admission 仍 exact replay
7. workspace 仍为 20 个 package,未新增生产依赖或常驻资源。
## 尚未关闭
1. ready Task StepRun 到 RunAttempt/Executor 的原子执行 admission
2. Workflow 取消、超时和 Task post-start recovery
3. frontier caller 与 Local application/Cluster control 生命周期装配;
4. Edge/Standalone 真实断电、资源门和产品 API/CLI/UI。