# ADR-0118:Remote Worker Lease Control 与耐久 Timeout - 状态:Accepted(生产 route composition 已由 ADR-0119 接入、共享 Artifact adapter 已由 ADR-0120 实现、Worker execution composition 已由 ADR-0121 实现;完整 Worker 产品生命周期仍默认关闭) - 日期:2026-07-23 - 关联 RFC:QL-RFC-0001 D-25、D-71、D-85、D-108、D-111、D-115、D-116、D-117 - 关联 ADR:ADR-0012、ADR-0021、ADR-0109、ADR-0112、ADR-0116、ADR-0117 ## 背景 ADR-0117 已能认证上传 Artifact 并原子完成远端 Run,但 package Worker 在进程启动后没有持续的 Run Lease 控制链。只依赖 Offer 初始 expiry 会让长任务必然失租;只在 Worker 内存启动 timeout timer,则重启、休眠或时钟漂移后无法证明何时应停止。控制面若直接停止远端 PID,又会跨越节点 边界并可能误杀复用身份。取消、timeout、Session replacement 与 completion 还会竞争同一 Lease version,必须由一个完整 fence 和一个持久化顺序收敛。 低配路由设备不能为每个 Run 建 timer/socket;集群节点又需要多 Worker 实例和多 control replica 安全并行。因此控制协议必须是 caller-driven、有界、可重放,并保持数据库和本地进程各自唯一 authority。 ## 决策 ### 1. 不新增 package,新增 exact lease-control subpath wire contract 放在 `@qinglong/runtime-core/remote-worker-lease-control`,Cluster service、PostgreSQL repository 和 Worker adapter 分别放入既有 cluster-control、cluster-postgres、worker-runtime。它们 没有新的依赖、部署、权限或发布责任,不满足 D-85 的拆包条件。 `qinglong/remote-worker-lease-control@v1` request 最大 8 KiB,response 最大 4 KiB。Worker/Session 来自 path,body 携带 worker generation、Project、Run、Attempt、Offer、Lease generation、raw token 和 expected version。response 固定为 `renewed`、`stop_requested` 或 `terminal`,不回显 raw token 或 digest;续租响应必须恰为 expected version + 1。 Worker adapter 复用 ADR-0112 的 TLS 1.3 mTLS client、credential provider 和单 keep-alive Agent, 不创建第二个 socket authority。path/body identity、schema、byte cap、response authority 或 version 漂移全部 fail closed,临时 response buffer 消费后清零。 ### 2. PostgreSQL 拥有时间、timeout intent 和 Lease version repository 在 Attempt advisory lock 后,依次锁定当前 Worker Session、Run/Attempt 与 Run Lease, 最后读取数据库时间。它复验 Session/generation、Project、Run/Attempt 状态、Offer、Lease generation、 token digest、expected version 与 expiry;raw token 不进入 SQL、Event 或错误。 live `claimed|starting|running` authority 每次把 Lease 和 Attempt version 同时加一,并用数据库时间 写 renewed/expiry。若已有 user/policy/shutdown/reconcile/timeout cancel intent,则同一事务仍先续租, 再返回 durable stop request,让 Worker 有时间停止并提交 completion。若 Attempt deadline 已到且尚无 cancel intent,事务先原子写 `run.cancel_requested_at_ms/reason=timeout`、Run version/Event sequence 和 server-ID `run.cancel_requested` Event,再返回 stop request。已完整终态只做 exact terminal projection; 旧 Session、旧 version、过期 Lease 或状态漂移拒绝。 ### 3. starting ACK 原子固定 durable deadline timeout 只来自 Run 已 pin 的 immutable `task_execution_revisions.plan_json.timeoutMs`。starting ACK 在 既有 activation transaction 内 join 精确 Project/Task/task revision/executor revision,读取一次数据库 时间,并把 `deadline_at_ms = observedAtMs + timeoutMs` 与 Attempt `starting` 同时写入;Event 与 exact activation snapshot 投影同一 deadline。无 timeout 的 revision 必须保持 deadline 为 null。 Worker Processor 对 activation snapshot 做双向约束:revision 有 timeout 时 deadline 必须存在,revision 无 timeout 时 deadline 必须缺失。spawn 前再次 replay starting ACK;只有拿到 durable deadline 才把 `timeoutMs + executionDeadlineAtMs` 交给 Executor。Executor 对单边字段、负值或非安全整数 fail closed, 不再使用本地 timer 冒充控制面 timeout authority。 ### 4. Worker 先回放 completion,再续租或精确停机 `WorkerRemoteExecutionControlCoordinator.reconcile(offerId)` 是单项、无 timer、同 Offer coalesced 操作: 1. 读取并规范化唯一 inbox record;已 completion ACK 直接结束。 2. 先调用 receipt-first completion recovery。Artifact/completion transport 临时失败与 lease transport 隔离,不能阻止仍运行进程保有 authority。 3. 本地 Lease 已到期时不再访问控制面,只对已持久化 durable handle 调用受审 `LocalProcessController.stop()`;TERM/KILL 成功或已退出记录 `lease_lost_local_execution_stopped`,身份/信号无法确认记录 `lease_lost_local_execution_unverified`。 4. Session 与本地 Lease 仍有效时发送完整 fence。`renewed|stop_requested` 必须先以 inbox revision CAS 持久化新 Lease version/time,再执行 stop;这样 completion 永远使用最新 version。 5. `terminal` 先停止精确本地身份,再将 record 隔离为 `control_plane_terminal`,绝不由旧 Worker 写控制面假终态。 ### 5. Headless lifecycle 只做 caller-driven 有界监督 显式 headless lifecycle 现在要求注入 control coordinator。startup reconciliation 完成后,每个 tick 先按稳定 cursor 扫描最多 1–64 条 inbox record,串行监督 `launching|started|running_acknowledged`,再 决定是否 Pull。出现 lease loss、control-plane terminal 或 durable recovery fact 后立即停止 Pull。 监督扫描与 Pull 之间再次检查 AbortSignal,确保 shutdown 先中止请求再释放 journal owner。 lifecycle 不创建 timer、watcher、队列或额外连接。Edge 可以由外层单 cadence 使用较小 page,Node 可以增大到 64 或按 Worker 实例水平扩展;禁用时仍为零后台活动。外层 heartbeat/drain、共享 Artifact store 和完整 Worker Profile composition 未闭合前不得默认启用。 ### 6. 用户取消使用认证 mutation 与事务内 Policy fence Cluster Control 提供 `POST /api/v3/projects/{projectId}/runs/{runId}/cancellation`,权限固定为 `run.stop`。body 只接受 exact `qinglong/run-cancellation@v1` 与 bounded `mutationId`,reason 固定为 `user`,调用方不能伪造 `shutdown|policy|timeout`。通用 admission 必须在读 body 前完成 authentication、Project Policy 和 durable security audit;Agent 的 `run.stop` 仍返回 `require_approval`,不能用该路由绕过 Approval 状态机。 PostgreSQL repository 不能只信任 admission 的历史 allow:它在 serializable 短事务中依次锁定 Project、读取当前 RoleBinding、锁 Run,精确复验 Project/binding version、active 状态和 owner/admin/operator role。随后以数据库时间写 `cancel_requested_at_ms/reason=user`,递增 Run version/event sequence,并追加 server-ID、actor-bound `run.cancel_requested` Event。首次提交返回 202 `accepted`;已有意图和终态分别返回 200 `already_requested|already_terminal`,不重复 Event。 Project 漂移、撤权或角色变化在 Run 写入前返回 fence conflict;跨 Project 与不存在 Run 同样投影 404,避免资源枚举。该入口只提交 durable intent,不承诺进程已经停止;Remote Worker 在下一次 lease-control 看到 stop request。ADR-0119 已把该 mutation 固定注册进受审生产 registry;Worker ingress 与 headless 组合仍保持独立、默认关闭。 ### 7. 非执行状态由一个有界 convergence lifecycle 收敛 `ClusterRunCancellationConvergenceCoordinator` 每周期最多处理 1–128 条、最多 1–64 页,跨调用 coalesce,自身不持有 timer、连接或队列。Cluster Control 外层只创建一个全局 cadence;Edge/Standalone 后续也可复用 coordinator 并选择更小预算,不得创建 per-Run timer。 PostgreSQL repository 用 `FOR UPDATE SKIP LOCKED` 在短事务内选择已经有 durable cancel intent 且处于 `created|queued|waiting_approval|retry_wait|lost` 的 runtime Run。`lost` 不是终态,因为 retry authority 仍可把它推进 `retry_wait|queued`;取消 API 不得提前返回 `already_terminal`。最新 Attempt 若仍是 `claimed`,repository 先把 Attempt 收敛为 `cancelled|timed_out`,随后按连续 sequence/version 原子终结 Run 并写两条 reconciler Event;没有活动 Attempt 或最新 Attempt 已终态时只写 Run Event。 Event ID 只在实际锁到 candidate 后,按 domain + Run/Attempt + cancel timestamp 做域分离 SHA-256 并截取 128 bit,空闲 tick 不预生成 UUID;同一次意图重试得到同一稳定 ID。若发现已经跨过 start barrier 的 `starting|running` Attempt 或带 active Lease 的 claimed Attempt,repository 返回 `blocked`,绝不伪造 终态;这些状态只允许由 Worker completion 或受信 evidence recovery 收敛。多 replica 通过 SKIP LOCKED 分摊候选,无需新增 claim 表、migration、schema 或常驻连接。 ## 被否决的替代方案 1. **每 Run 一个 timeout/renewal timer**:资源随并发增长,休眠与重启丢失语义,拒绝。 2. **Worker 本地时钟决定 durable timeout**:多节点时钟漂移且不能认证重放,拒绝。 3. **控制面远程发送 PID signal**:跨节点 authority 且 PID 可复用,拒绝。 4. **stop 后再保存 Lease version**:完成回调会携带旧 version,被 fence 或产生不可解释窗口,拒绝。 5. **续租失败立即写 Run lost**:旧 Worker 无控制面终态写 authority,拒绝。 6. **为 lease client/coordinator 新建 package**:依赖与发布责任未分离,违反 D-85,拒绝。 7. **只在 admission 检查一次 Policy**:授权与 Run mutation 之间可被撤权竞态穿透,拒绝。 8. **允许调用方提交 cancel reason**:可伪造系统 shutdown、policy 或 timeout 事实,拒绝。 ## 验收证据 1. runtime exact wire 覆盖 renewed/stop/terminal、byte cap、未知字段和非法 shape。 2. ingress 先认证/audit,再 path-bind command;fenced 映射 409、storage unavailable 映射 503,response 不泄漏 token。 3. PostgreSQL repository 覆盖续租、既有取消、deadline timeout intent、terminal projection、旧 version rollback,且 raw token 不进入 SQL/Event。 4. starting ACK 用数据库时间写 deadline,无 timeout 时写 null;wire round-trip deadline。 5. Worker HTTPS adapter 验证完整 identity 和 version + 1;真实 TLS 1.3 mTLS 回归在同一默认 Agent 上连续执行 Artifact、completion 与 lease-control,并验证可清零凭据 Buffer 不会改变 socket 回收池键或挂起后续请求。 6. control coordinator 证明 completion-first、persist-before-stop、本地过期不访问控制面、成功/不确定 stop 证据分离、Session unavailable 和同 Offer coalescing。 7. headless lifecycle 证明 bounded supervise-before-Pull、recovery fail-closed、tick coalescing 与 Abort-before-release。 8. Executor 无 durable deadline 时不 spawn,有 deadline 时通过原有 reviewed spawn barrier。 9. exact 用户取消 route 在 admission 后传递完整 subject/policy fence;PostgreSQL 16 最小权限 runtime role 真实验证 accepted、exact replay、Event actor/payload 与撤权 fence rejection。 10. 未新增 workspace package、migration、schema、per-run timer、队列或常驻连接。 11. bounded convergence 覆盖 queued+claimed、lost+terminal Attempt、timeout、blocked start barrier、 page/coalescing/stop drain;PostgreSQL 16 最小权限 runtime role 真实验证 `run.cancel_requested → attempt.cancelled → run.cancelled` 原子序列。 12. ADR-0125 的本机 arm64 PostgreSQL 18.4 physical-promotion 门分别在用户 cancellation intent 和 cancellation convergence 的 driver-confirmed `COMMIT` 后终止 transaction backend。意图以同一 command 重放为 `already_requested`,终态收敛重放扫描 0 条;standby 在 promotion 前及 timeline 2 promoted primary 上均保持 Run version 4/event sequence 3、上述 3 条 Event、3 个 dedupe key 和 0 duplicate。故障范围是 PostgresClient 边界,不是 raw-wire packet-loss。