Files
qinglong/docs/adr/ADR-0229-local-workflow-task-runtime-lifecycle.md

146 lines
7.5 KiB
Markdown
Raw Permalink 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-0229Local Workflow Task 单 cadence 运行生命周期
- 状态:Proposed
- 日期:2026-07-30
- 关联 RFCQL-RFC-0001 D-18、D-19、D-71、D-115、D-175、D-207、D-212、D-213
- 关联 ADRADR-0066—0072、ADR-0223—0228
## 背景
Workflow admission、frontier、Task Attempt admission、cancellation 和 recovery
已经在共享领域层及 SQLite/PostgreSQL adapter 中存在,但 Local application 仍只有
普通 Run-centric scheduler/execution lifecycle。直接把 Workflow aggregate Run 交给
普通 dispatcher 会把 Workflow ID/publication digest 当成 Task identity;普通
completion/control/recovery 又会提前终结父 Run。
Local application 已经拥有 scheduler cadence、execution control cadence、单 SQLite
operation authority、completion receipt 与受审 POSIX launcher。新增 Workflow timer、
数据库连接、表或 workspace package 会增加路由设备空闲成本,也会制造第二套停止顺序。
## 决策
### 1. 一个 scheduler cadence 承载完整 Workflow 前进顺序
`LocalWorkflowSchedulerCoordinator` 是无 timer 的组合器,每个 cycle 固定执行:
1. 整体 Workflow cancellation convergence
2. 普通 schedule
3. Workflow frontier
4. generation-bound Task Attempt admission
5. 有界 local dispatch。
它复用既有 `LocalSchedulerLifecycle`。Edge 每轮 cancellation 4×1、frontier 1×1、
Task admission 1×1、dispatch 1Standalone 分别为 32×4、16×4、16×4、dispatch
4。任何阶段失败都由原 lifecycle 隔离并在下一 cadence 重试,不增加 per-Workflow
timer、watcher、listener 或后台扫描。
### 2. Workflow Task 执行只推进 Attempt/StepRun
Local dispatch candidate 可以携带 `stepRunId`。只有 immutable Task admission ledger
与 exact ready StepRun 同时存在时,父 Run 为 `running` 的 Task Attempt 才可派发。
prepare、running、start-failure、completion、timeout 和 cancellation 都经窄
`LocalWorkflowTaskExecutionRepository` 在一个 `BEGIN IMMEDIATE` 中推进 Attempt、
StepRun、Run counter 与 Event/mutation;父 Workflow Run 保持 `running`,最终状态只
由 frontier 汇总。
普通 Run 仍要求 latest Attempt,并保持既有 `queued→dispatching→running→terminal`
语义。Workflow 分支不能改变普通 Run 的 retry、completion 或 cancellation contract。
### 3. 启动恢复按聚合类型分流
普通 startup recovery 明确排除存在 immutable Workflow admission 的父 Run。专用
Workflow Task recovery 以 Attempt 为单位:
- orphaned claimed Attempt 写 `lost`,并把 StepRun 刷新为新的 ready epoch
- starting/running 先读取 authenticated completion receipt
- 没有 receipt 时只接受 exact persisted process identity 的两次一致 live inspection
或可信 not-running evidence
- unknown、缺 handle、身份变化和平台不可验证一律失败关闭。
恢复不终结父 Workflow,也不拥有 timer。
### 4. completion identity 与 Workflow portable identity 统一
Workflow plan 已明确使用最长 36 字节的 portable Run identityTask admission 生成
`wta:<32 hex>` deterministic Attempt identity。Local completion receipt/journal 过去
只接受 UUIDv7,导致 adapter 单测通过而真实 launcher 永远在请求校验处失败。
receipt、journal 和文件分片统一接受
`[A-Za-z0-9][A-Za-z0-9._:-]{0,35}`。UUIDv7 是该集合的子集;空值、前导点、斜杠、
反斜杠、空白、NUL 和超长 identity 继续拒绝,因此没有扩大目录穿越 authority。
### 5. startedAt 绑定 spawn 前 durable barrier
launcher 在 spawn 前登记 journal,并把同一个 `handle.startedAtMs` 写进不可覆盖
receipt。coordinator 必须把 Attempt/StepRun 的 `startedAtMs` 保存为该 handle fact
不能在 spawn 后用新的 wall-clock observation 覆盖。否则合法 receipt 会因早于
Attempt start 而被 quarantine。
Event 时间仍必须不早于 Run/Attempt creation;时间回退继续失败关闭。
### 6. 停机先冻结 admission,再 drain execution
application stop 顺序保持:
1. `LocalSchedulerLifecycle.stopAndDrain()`,冻结新 Workflow/Task admission
2. `LocalExecutionControlLifecycle.stopAndDrain()`,对在途进程提交 shutdown intent、
exact stop 与 Step-scoped terminal/recovery
3. 关闭共享 SQLite storage 和 adoption fence。
并发 stop 复用同一个 promise,不产生第二轮 scheduler 或重复 Attempt。
## 不采用的方案
### 新建 `local-workflow` workspace package
拒绝。该能力没有独立部署或依赖边界;组合器、执行 port、SQLite adapter 分别属于既有
local-execution/local-sqlite/local-application。继续拆包会产生更多单文件 package。
### 为每个 Workflow 创建 timer 或 child Run
拒绝。前者增加路由器唤醒/RSS,后者复制 Run/取消/recovery authority,并破坏 same-Run
StepRun fence。
### 保留 receipt UUIDv7-only 并把 deterministic Attempt 强制改为随机 UUID
拒绝。会破坏 admission 的可重算 exact replay,也与已经受审的双方言 portable
identity contract 冲突。严格 portable 字符集在保持路径安全的同时覆盖两类 Run。
## 当前验证
1. `runtime-core` 419/419、`local-process` 18/18、`local-execution` 30/30、
`local-sqlite` 189/189
2. macOS application 全量 41 项为 38 pass、3 条 Linux 条件 skip、0 fail
3. 本机已有 `node:24-bookworm-slim` 镜像在 `--pull never --network none` 下执行真实
Linux product vertical:两步 Workflow 全部成功,每个 Step 恰好一个 Attempt
两条 admission,无重复 dispatch
4. 启动前用户取消的 Workflow 在 application 首轮 cadence 原子收敛为 cancelled
普通 Run recovery 扫描为 0,且没有启动 Attempt;
Linux 产品门还覆盖在途 Local Task:等待真实 PID/Attempt/StepRun running
持久化父 Workflow 取消意图,先停止进程,再将 Attempt/全部 StepRun/父 Workflow
精确收敛为 cancelled
5. PostgreSQL 18.4 arm64 physical HA 新增独立
`pluginPackageWorkflowTaskAttempt*` 五项 gate:原子提交、exact replay、
`remote_apply` 晋升前复制、runtime-only ACL、timeline 1→2 后存活全部为 true
6. HA 继续完成旧主 fencing、`pg_rewind` 只读同步重入、两个 fresh control replica
`gates.passed=true`,并使用 `QL3_HA_SKIP_IMAGE_PULL=true`
7. 真实 Local Workflow 已进入既有 Linux resource gate,没有新增 workspace
package、依赖或常驻 cadence。arm64 Node 24.18.0 的 128 MiB/0.5 CPU 档
`memory.peak=120217600`、Workflow process peak RSS `87449600`、RSS delta
`16908288`16 次 admission write-lock p95 `4.053 ms`256 MiB/1 CPU 档
`memory.peak=121344000`、Workflow process peak RSS `87949312`、RSS delta
`17408000`32 次 write-lock p95 `3.317 ms`。两档 `memory.events`
max/OOM 增量为零,并完成 Edge/Standalone admission 与
conclusive-stop/control-terminal 两组各 16 个
`SIGKILL → reopen → exact replay` 场景。
8. PostgreSQL 18.4 arm64 HA 已完成 Remote Workflow
`stop_requested → completion response loss/exact replay → parent convergence
response loss/exact replay → promotion 后再次 replay`Run/Attempt/StepRun
cancelled、Lease completed@v6,独立四项 gate 和总 `passed` 均为 true。
## 尚未关闭
1. 固定 Edge/Standalone 物理路由设备报告、闪存/FTL 写放大、真实 ENOSPC 与受控
突发断电门;CI `SIGKILL` crash matrix 明确不等同于物理断电;
2. Linux x64/arm64 的长期 CI 成功记录和实际低配设备基线。