Files
qinglong/docs/adr/ADR-0014-bounded-dispatcher-and-claimed-execution-offer.md
T

131 lines
9.7 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-0014:有界 Dispatcher 与 Claimed Execution Offer
- 状态:Proposed
- 日期:2026-07-18
- 关联:QL-RFC-0001、ADR-0012、ADR-0013
## 上下文
Run candidate、Worker 列表、Placement 结果、Run Lease 和 `ExecutionSpec` 已分别建立边界,但如果由 transport、定时器或 API 临时拼接,仍可能产生以下错误:
- 在 claim 前把命令、工作目录或资源策略暴露给未获授权的 Worker;
- 把 Placement 命中误认为执行权,导致多个 Dispatcher 重复下发同一 Attempt
- 在路由设备上无界读取 Run、Worker 或构造大量 `ExecutionSpec`
- 使用请求体中的 Worker 身份,而不是 Registry 中刚参与 Placement 的 session/generation
- candidate 的 executor type、Placement 和 `ExecutionSpec` 身份不一致;
- claim 与消息发送之间的崩溃窗口没有被明确建模。
因此需要一个与 transport 无关、单次运行且有硬预算的应用层 Dispatcher,先稳定控制面授权顺序,再选择 HTTP、gRPC 或消息协议。
## 决策
### 1. Dispatcher 每次只执行一个有界周期
`RunDispatcher.dispatchOnce()` 不创建 timer,也不拥有后台生命周期。调用方负责调度下一周期。默认预算面向 edge:
- active lease recovery:每页 8 条,最多 2 页;
- candidate:每页 8 条,最多 2 页;
- Worker:每页 8 条,最多 2 页;
- claim:全周期最多尝试 8 次。
实现硬上限为 recovery/candidate/Worker 每页 64 条、页数 16、单周期参与 Placement 的 Worker 总数 64、claim 尝试 64。配置若使 Worker 页乘积超过 64,启动前直接拒绝。standalone 或未来 cluster-control 可以调高默认值,但不能绕过硬上限;扩展吞吐应通过更多短周期和数据库级公平性完成,而不是扩大单周期内存。
### 2. 授权顺序固定
每个周期严格执行:
1. 用同一个 `observedAtMs` 优先读取 active lease recovery;只有 Run 仍为 `dispatching`、Attempt 仍为 `claimed`、未取消、lease 未过期且 Worker session/generation 仍为当前 online/draining Session 时才可恢复;
2. recovery 没有可交付项后才读取第一页 candidate;没有 candidate 时不查询 Worker
3. 有 candidate 后才按稳定 cursor 读取有界 Worker 快照;
4. 每次只调用 `RunDispatchPlanSource.prepare(candidate)` 构造一个 plan
5.`ExecutionSpec` 做共享领域校验、显式深拷贝和未知字段剥离,并验证 `runId/attemptId/projectId/taskId/taskRevision` 与持久化 candidate 完全一致;
6. 新 claim 的 Placement 必须包含 candidate 的 executor type;未声明时由 Dispatcher 加为 required,冲突时拒绝;
7. 按 Placement score、可用槽位和 Worker ID 确定性排序;
8. 使用所选 Registry record 的 Worker ID、session ID 和 generation 原子 claim
9. 只有 lease recovery、`claimed` 或同一 capability 的 `idempotent` 结果才能生成 `ClaimedExecutionOffer`
plan 可以在可信控制面内存中于 claim 前构造,但不得作为返回值、日志、Event、Trace attribute 或 transport 消息暴露。claim 失败、竞争丢失、无匹配节点或预算耗尽时只返回低基数状态和计数,不返回 `ExecutionSpec`
### 3. Offer 是受 Lease 约束的内部 capability
`ClaimedExecutionOffer` 只包含:
- candidate 的有界身份和排序事实;
- 由 Attempt ID、lease generation 和 Worker session authority 派生的稳定 `offerId`、规范化 `ExecutionSpec` SHA-256,以及 `new_claim/lease_recovery` 来源;
- 目标 Worker 的 ID、session ID、generation
- 已提交的 Run Lease,包括不透明 token
- 清洗并深拷贝后的 `ExecutionSpec`
- 新 claim 的 Placement score;恢复不依赖重新计算 score。
原始 lease token 和命令内容不得进入普通日志。offer 的消费者必须把它发送给完全相同的认证 Worker principal;不能改派给另一个 session,也不能以消息系统的 delivery ACK 代替 ADR-0013 的 starting/running ACK。
### 4. 分页与竞争 fail closed
- candidate 重复 Attempt、cursor 不前进或端口返回超页数据时拒绝本周期;
- recovery 重复 Attempt、过期 lease、非前进 cursor 或超页数据时拒绝本周期;
- Worker 重复、顺序倒退、truncated 页缺失前进 cursor 或总数超限时拒绝;
- Worker 在快照后失联或容量耗尽时,可以在全局 claim 预算内尝试下一个已匹配 Worker;
- Attempt 已被租用、变为不可领取或 claim CAS version 冲突时停止处理该 candidate,不对同一 Attempt 继续改派;
- 扫描或 claim 预算耗尽必须显式返回 `truncated` 和对应 reason,不能伪报为“没有任务”。
### 5. Run Lease 是恢复 authority,但不是 transport outbox
不新增与 Run Lease 并列的 `DispatchOffers` authority 表。已提交的 active Run Lease 已原子绑定 Attempt、Worker session/generation、lease generation/token 和 expiryDispatcher 重启后先从该事实有界恢复,并按 Run 中固定的 Project/Task/revision 重新 materialize plan。相同 lease generation 始终得到相同 `offerId`,因此 claim 提交后、首次发送前崩溃不必等待 lease 过期。
恢复只覆盖 Attempt 尚为 `claimed` 的窗口。Worker 已 ACK `starting` 后不再重发 `ExecutionSpec`Worker 应重试 ADR-0013 的后续 ACK。取消、lease expiry 或 Session replacement 都立即取消恢复资格。plan 无法按原 revision 重建或 `ExecutionSpec` identity 漂移时 fail closed。
当前仍没有持久化 delivery attempt、网络发送或 delivery ACK。控制面可能在 starting ACK 前重复返回相同 offer。ADR-0021 已增加默认不可达的 Worker 本地持久化 inbox:按 `offerId` 幂等去重、拒绝同 ID 异 `ExecutionSpec` digest,并在 spawn 前写入 crash barrier。delivery ACK 仍只表示收包,不能代替 starting/running 事实。
ADR-0108 已为 PostgreSQL cluster 增加一个更窄的认证 Worker Pull 适配:Worker 自带稳定 `offerId` 与高熵 lease token,服务端用数据库时钟有界读取 candidate,只对该认证 Session 执行 Placement 和原子 claim;数据库仅保存 token digest,同一请求可从 durable Lease 重建丢失响应。该适配复用本 ADR 的授权顺序和预算,但不把 SQLite Dispatcher 的进程内 Worker 全表选择、控制面派生 token 或 transport 生命周期搬入 cluster。
生产 transport 必须另行决定:
- delivery attempt、退避、重连和可观测状态;
- 认证 transport 如何调用 ADR-0021 inbox,以及 journal 单 owner、retention 和恢复扫描;
- offer 未送达时主动 release 还是等待 lease expiry
- Artifact 是否能按 pinned Task revision 确定性重建;
- token 的加密传输、内存生命周期和脱敏审计。
ADR-0109 已完成 PostgreSQL starting/running/start-failure ACK;在认证 ExecutionSpec transport、runtime 内部 port 和 Worker inbox 网络装配完成前,当前 Dispatcher/Pull port 必须保持默认生产不可达。
## 影响
正面影响:
- candidate discovery、Placement、claim 和 offer 的权限边界可独立测试;
- edge 无新增常驻 timer、连接或无界集合;
- Worker session fencing 来自持久化快照,不信任 transport 请求体;
- `ExecutionSpec` 的共享校验同时服务 LocalProcess 和未来 Remote Worker,避免两套规则漂移;
- cluster-control 可以复用同一应用语义,只替换 PostgreSQL Repository 和 transport。
代价与风险:
- plan 在 claim 前构造,昂贵的 Artifact 解析必须保持惰性和有界;后续可以拆成 claim 前 metadata plan 与 claim 后 payload materialization
- 单周期公平性依赖 candidate keyset 顺序及外部 cadence,仍需多 Project/Quota 调度策略;
- starting ACK 前可能重复投递,transport 和 Worker 必须实现稳定 ID 去重与退避;
- SQLite 只能验证单控制面行为,不能证明多副本 claim 正确性。
## 未选择的方案
1. **先发送 ExecutionSpec,再由 Worker claim**:未授权节点能看到任务内容,并可能先执行后 claim。
2. **Placement 命中即更新为 running**:没有 Run Lease、starting ACK 或稳定 executor handle。
3. **一次加载全部 Worker 和 candidate**:在 edge 上失去内存上限,也放大集群热点。
4. **把 Dispatcher 直接写进 HTTP handler**:网络重试和身份字段会污染领域授权顺序。
5. **claim 成功后立即接入生产 headless runtime**delivery、Artifact 和 ACK 恢复协议尚未完成。
6. **新建与 Lease 并列的 durable offer authority 表**:会复制 Attempt/Worker/lease 状态并引入双重真相;delivery telemetry 后续可以单独持久化,但不能取代 Lease authority。
## 验证要求
- 第一页无 candidate 时 Worker、plan 和 claim 调用次数均为 0
- required/preferred Placement 选择正确 Worker,并用该 record 的 session/generation claim
- candidate/Worker 跨页查找受预算限制,重复或不前进 cursor fail closed
- 容量竞争可尝试下一 Worker,Attempt 所有权竞争停止当前 candidate;
- claim/扫描预算耗尽返回准确 reason 与 `truncated`
- 无效或身份不一致的 `ExecutionSpec` 在 claim 前拒绝;
- 未 claim 的结果不包含 offer,序列化结果不能泄漏命令;
- `ExecutionSpec` 深拷贝,plan source 后续修改不能改变 offer
- active lease 在新 candidate 前恢复,重启前后 `offerId` 相同;
- recovery 只接受 pinned Task identity、当前 Worker Session 和未过期 lease;取消或 Session replacement 后不再投递;
- recovery plan 缺失、revision 漂移、重复 cursor 和页数耗尽均显式 fail closed
- Node 22、Node 24 全量回归通过;未来 PostgreSQL contract suite 覆盖多 Dispatcher 竞争。