feat(ql3): add postgres cancellation dispatch

This commit is contained in:
whyour
2026-08-19 06:24:11 +08:00
parent 36035ac43e
commit 1809fbb8d3
29 changed files with 2265 additions and 133 deletions
@@ -1,6 +1,6 @@
# ADR-0005Durable Cancellation Dispatch、Lease 与 Fencing
- 状态:Proposed
- 状态:AcceptedLocal 与 PostgreSQL Repository 已实现;Cluster 生产启动拓扑待接入)
- 日期:2026-07-18
- 决策范围:跨进程取消派发、崩溃恢复、并发 Worker、退避和审计事件
- 关联:QL-RFC-0001、ADR-0001、ADR-0003、ADR-0004
@@ -30,7 +30,7 @@ version non-negative integer
dispatch_count non-negative integer
next_attempt_at_ms nullable
lease_owner nullable
lease_token nullable
lease_token_digest nullable
lease_expires_at_ms nullable
last_result nullable
last_dispatched_at_ms nullable
@@ -49,9 +49,9 @@ Worker 对唯一 active Attempt 执行原子 claim
3. 不存在 dispatch 时创建 `pending`,绑定该 Attempt。
4. 既有记录绑定其他 Attempt 时 fail closed,不重新绑定或选择“最新 PID”。
5. `dispatched``blocked` 不再 claim;未到 `next_attempt_at_ms` 返回 not-due;未过期 lease 返回 leased。
6. 到期或可派发时,以 version CAS 更新为 leased,递增 version 和 dispatch_count,写入新的 owner、不可预测 token 和 expiry
6. 到期或可派发时,以 version CAS 更新为 leased,递增 version 和 dispatch_count,写入新的 owner、domain-separated SHA-256 token digest 和 expiry;原始 token 只随成功 claim 返回给当前调用者,不进入 durable record
SQLite adapter 使用短 `IMMEDIATE` 事务串行化写竞争PostgreSQL adapter 必须提供等价的行锁或条件更新语义;实现方式可以不同,行为契约不得改变。
Repository 是租约、到期与退避的时间 authority。调用方只提交已有 `cancel_requested_at_ms` 事实、lease duration 或 retry delay,不得提交“当前时间”、lease expiry 或绝对 retry timestamp。SQLite adapter 使用注入 clock 与`IMMEDIATE` 事务串行化写竞争PostgreSQL adapter 使用 `transaction_timestamp()`,按 Run→Attempt→CancellationDispatch 顺序取行锁。实现方式可以不同,行为契约不得改变。
### 2.3 发出副作用
@@ -67,7 +67,7 @@ SQLite adapter 使用短 `IMMEDIATE` 事务串行化写竞争。PostgreSQL adapt
### 2.4 Result 事务与 fencing
结果提交必须同时匹配 run ID、attempt ID、lease owner、lease token 和 expected dispatch version。任何一项过期都拒绝写入。
结果提交必须同时匹配 run ID、attempt ID、lease owner、原始 lease token 的 digest 和 expected dispatch version。任何一项过期都拒绝写入。
同一事务中:
@@ -99,7 +99,7 @@ lease 到期后其他 Worker 可以重新 claim。重试仍必须执行完整身
### 2.6 退避与 Supervisor
首版退避为 `min(max, base * 2^(dispatch_count-1))`,指数有上限。Repository 持久化绝对 `next_attempt_at_ms`,进程重启不会清空退避。
首版退避为 `min(max, base * 2^(dispatch_count-1))`,指数有上限。调用方只提交有硬上限的 `retryDelayMs`Repository 依据自身时间计算并持久化绝对 `next_attempt_at_ms`,进程重启不会清空退避。
Supervisor 一次只执行有界 cycle
@@ -151,15 +151,14 @@ PID 可复用,可能终止无关进程,禁止。
## 5. 当前孵化边界
`next` 已实现 `0005-run-cancellation-dispatch`Repository 端口、临时 Sequelize/SQLite adapter、lease expiry 接管、fencing、退避、结果事件、Dispatcher、有界 Supervisor 和默认惰性的 lifecycle runner,并覆盖双 Worker、崩溃接管、事务回滚、无重叠调度和有界 shutdown 测试
`next` 已实现 profile-neutral canonical contract、`0005-run-cancellation-dispatch`legacy Sequelize/SQLite adapter、PostgreSQL `pg-0066-cancellation-dispatch`/capability v65 adapter、lease expiry 接管、fencing、退避、结果事件、Dispatcher、有界 Supervisor 和默认惰性的 lifecycle runner。PostgreSQL 结果事务按 Run→Attempt→dispatch 锁序完成 dispatch 更新、Run version CAS 与 RunEvent 追加;runtime 角色只取得新表的 SELECT/INSERT/UPDATE
HTTP worker 已通过默认关闭的 manual-only manifest bootstrap 接入 Supervisor:只有 accepted 且全部 gate 通过时才启动,失败或 shutdown 时有界停止。以下工作仍未完成,因此它仍只允许显式 canary,不得扩大到默认生产流量:
HTTP worker 已通过默认关闭的 manual-only manifest bootstrap 接入 Local Supervisor:只有 accepted 且全部 gate 通过时才启动,失败或 shutdown 时有界停止。以下工作仍未完成,因此它仍只允许显式 canary,不得扩大到默认生产流量:
- PostgreSQL adapter 与真实多连接并发压力测试。
- ADR-0007 的 completion receipt、direct-file log、CompletionService 与周期 completion supervisortimeout lifecycle 已接入)。
- 用户可见的运行指标、blocked 诊断和处置入口。
- 固定 edge 设备的数据库写放大、RSS、时延和磁盘基准。
- 与 rollout manifest、回滚 runbook 和运维告警的最终接线。
- cluster-control 对 PostgreSQL CancellationDispatch 的生产启动/停止拓扑与运维告警接线。
- 首次真实目标实例完整激活/回滚仪式与共享 config 多写者 authority。
## 6. 验证门禁
@@ -173,3 +172,5 @@ HTTP worker 已通过默认关闭的 manual-only manifest bootstrap 接入该 Su
8. identity/PID/process-group 不一致时零 signal。
9. page、cycle 和退避均有硬上限。
10. Event 与日志不包含 handle、命令、环境和 Secret。
11. PostgreSQL 双连接只能产生一个 claim winnerraw token 不落库,数据库时间决定 lease/retry 到期。
12. v65 事实经 WAL 到达 standby,提升为新 Primary 后仍可读取;旧 owner/token/version 继续被 fencing。
@@ -0,0 +1,64 @@
# ADR-0456:数据库计时的 PostgreSQL CancellationDispatch
- 状态:Accepted
- 日期:2026-08-19
- 关联 RFCQL-RFC-0001 D-363、PR-5
- 关联 ADRADR-0001、ADR-0005、ADR-0041、ADR-0384
- AmendsADR-0005 的 PostgreSQL adapter、时间 authority 与 token 持久化边界
## 上下文
ADR-0005 已在 Local Profile 建立 durable cancellation dispatch,但 Cluster Profile 不能直接复用 SQLite 的单写者事务或进程时钟。多个 cluster-control 副本可能同时扫描同一 Run;节点时钟漂移会让租约提前接管或永久延后;把 raw lease token 持久化又会扩大数据库快照、备份与只读诊断面的能力泄漏。
QingLong 3.0 还必须同时服务低配路由和集群节点。公共协议需要同构,部署依赖与运行 authority 必须按 Profile 隔离:Edge 不应因 Cluster 能力引入 `pg`、连接池或常驻协调器,Cluster 也不能用进程内锁冒充多副本共识。
## 决策
1. `CancellationDispatch` 的 canonical contract 位于 `@qinglong/runtime-core/cancellation-dispatch` 显式子路径,不从 runtime-core 根入口导出。它定义 exact-shape command/record/result、硬上限、状态不变量、结果分类和 domain-separated SHA-256 token digest;不拥有数据库连接、timer、worker 或部署 Profile。
2. claim command 只携带 Run/Attempt、已有的 `cancel_requested_at_ms`、owner、raw token 和有上限的 lease duration。result command只携带精确 fence、结果枚举、event ID,以及 retryable 结果所需的有上限 delay。调用方不得提交当前时间、lease expiry 或绝对 retry timestamp。
3. PostgreSQL Repository 在事务中以 `transaction_timestamp()` 取得唯一时间事实。lease expiry、`updated_at_ms``last_dispatched_at_ms` 和 retry due time均由数据库时间计算;Local adapter 保持注入/default clock,以便低成本确定性测试和单设备运行。
4. raw lease token 仅在成功 claim 的返回值中出现。durable record 与 PostgreSQL 表只保存 `sha256("qinglong.cancellation-dispatch-lease.v1\\0" || token)`;后续 result 在事务内重新计算 digest 比对。read/list、WAL、备份和诊断面不得恢复该 capability。
5. `pg-0066-cancellation-dispatch` 把 PostgreSQL capability 提升到 v65,创建 `ql3.run_cancellation_dispatches`。主键为 Run IDAttempt 通过 `(attempt_id, run_id)` 复合外键固定绑定同一 Run;CHECK 约束状态、counter、lease/retry/terminal shape,索引只支持 bounded due 与 expired-lease recovery。
6. runtime role 对新表只有 SELECT、INSERT、UPDATE,没有 DELETE、TRUNCATE、REFERENCES、TRIGGER 或 schema createmigration owner 继续独占 DDL。readiness、Drizzle schema、reviewed SQL、migration checksum 与 catalog privilege 行必须保持锁步。
7. claim 的锁序固定为 Run→Attempt→CancellationDispatch。先验证 runtime-owned active Run、精确 cancel timestamp 与同 Run active Attempt,再创建或锁定 dispatch;跨 Attempt 重绑定失败关闭。正常 lease 未过期、不 due、terminal 和 blocked 都不会产生新 owner。
8. recordResult 使用同一锁序并精确验证 run/attempt/owner/token digest/expected version。在一个事务中更新 dispatch、对 Run version 做 CAS、分配 event sequence 并追加低敏 RunEvent;任一步失败全部回滚。stale fence 不能覆盖新 owner。
9. PostgreSQL adapter 只通过 `@qinglong/cluster-postgres/cancellation-dispatch` 和受审 runtime entrypoint 发布,不从 package 根入口扩张。它不自动创建连接池、扫描器、timer、listener 或 cluster-control 进程;生产启动/停止拓扑是后续独立决策。
10. Local legacy 表暂时保留既有列名与迁移兼容性,由 adapter 在 canonical record 边界投影 digest。D-363 不把这一点表述为 Local 数据库存量已经完成 raw-token 迁移;若要修改既有 SQLite durable layout,必须单独设计兼容迁移和回滚门。
## 被拒绝的替代方案
### 使用 cluster-control 进程时钟
拒绝。多副本时钟漂移会破坏 lease 与 retry 的单一到期语义,主库提升后也无法证明旧节点计算的绝对时间仍可信。
### 在表中保存 raw lease token
拒绝。token 是一次短期写能力,不是诊断事实。持久化 raw capability 会让只读快照、复制链和备份获得不必要的可重放材料。
### 只用唯一索引或进程锁去重
拒绝。它们不能同时表达过期接管、owner fencing、结果原子事件与多副本崩溃恢复。
### 把 adapter 从包根入口导出并自动启动
拒绝。根入口扩张会污染轻量依赖闭包,自动启动会在未决的生产拓扑之前引入常驻扫描与连接 authority。
## 资源、安全与部署影响
- Edge/Standalone 基础产物不新增 `pg` 或 cluster package;最小 Edge artifact 仍约 2.59 MiB。
- Cluster 新增一张当前状态表、两个恢复索引和短事务;无新 workspace package、生产依赖、Kubernetes 对象、端口、timer 或常驻进程。
- raw lease token 不进入 canonical record、PostgreSQL row、WAL 或事件;事件仍只含 Attempt、dispatch count 与固定结果枚举。
- 行锁顺序与 5 秒 statement timeout、1 秒 lock timeout、10 秒 idle-in-transaction timeout 共同限制锁等待;这不是无限并发压力证明,生产指标与容量门仍需完成。
## 验证
- runtime-core 契约 `5/5`PostgreSQL schema/migration/readiness 聚焦 `75/75`v65 checksum、CHECK/FK/index 与最小权限通过。
- 完整 backend`1,487 pass / 0 fail / 2 conditional skip`18-package 最新 clean/build 退出 0,随后 18-package 顺序测试单次退出 0。
- package boundary、Edge import、cluster dependency、cluster deployment、service-manager bridge 审计均零 findingworkspace package 精确为 18,新增实现位于明确子域而非 `src` 根平铺。
- `14/14` Local Profile artifact audit 通过;基础 Edge/Standalone 为 `2,589,998 / 2,590,076` bytes,没有 PostgreSQL 依赖泄漏。
- PostgreSQL 18.6 arm64 HA 门 `144/144`:双连接单 claim、数据库时钟、digest-only durable token、expired takeover、stale fence、retry due、事务回滚、WAL standby 可见和 promotion 后读取均通过;timeline `1→2`,报告 SHA-256 为 `b168b25023f7aad623153d22e41cccfe5f511a6985dc75c9e9e20073f980d5cb`
- HA 临时 Docker 容器在门结束后全部清理。该证据不冒充 CloudNativePG、多节点网络分区或生产容量证明。
## 后续
下一阶段把 PostgreSQL CancellationDispatch Repository 接入 cluster-control 的明确生产 composition、单一 cadence、availability withdrawal、shutdown drain、指标与 blocked 处置面;随后补 CloudNativePG live failover、多副本压力、固定 x64/arm64 资源门。Local 侧如需消除 legacy raw-token 存量,另开兼容迁移 ADR,不与 Cluster rollout 混合。