mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
131 lines
9.7 KiB
Markdown
131 lines
9.7 KiB
Markdown
# 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 和 expiry;Dispatcher 重启后先从该事实有界恢复,并按 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 竞争。
|