8.3 KiB
ADR-0041:PostgreSQL Run Repository 契约对等
- 状态:Proposed
- 日期:2026-07-19
- 关联:QL-RFC-0001、ADR-0001、ADR-0037、ADR-0038、ADR-0039、ADR-0040
上下文
pg-0002-run-core 建立了 Run、RunAttempt 和 RunEvent,但应用层 RunRepository 还要求 durable RunRetryPolicy:
- 随 Run admission 保存重试安全声明;
- 按 version compare-and-set 推进下一次重试时间;
- 在控制面重启后继续判断 lost retry;
- 与 Run/Attempt/Event 处于同一数据库事务边界。
如果 PostgreSQL adapter 省略 RetryPolicy,cluster-control 只能实现一个缩小版 Repository。上层 service 会被迫按方言分叉,或在内存中保存唯一重试事实,两者都违反跨 Profile 的 Runtime Kernel 和数据库事实源不变量。
另一个差异是 SQLite 兼容 contract 默认构造 execution_owner=legacy,而 PostgreSQL cluster schema 正确地只允许 runtime。为了让 contract 真正跨方言,测试数据必须参数化 owner,不能放宽 cluster 约束。
决策
1. control-core v2 表示完整首版 RunRepository 存储能力
新增不可变 migration pg-0003-run-retry-policy,在同一 migration 事务中:
- 创建
ql3.run_retry_policies; - 创建有界 due index 和 Run lost-retry index;
- 只允许从精确的
control-corev1 推进到 v2; - 写入 capability:
{
"contract_name": "control-core",
"contract_version": 2,
"migration_id": "pg-0003-run-retry-policy",
"capabilities": {
"run_core": 1,
"run_retry_policy": 1
}
}
若 v1 capability 的 version、migration ID 或 JSON 能力不精确匹配,migration 必须失败并回滚。不得修改 pg-0001/0002 的 SQL 或 checksum。
2. RetryPolicy 与领域约束保持一致
PostgreSQL 表固定保存:
max_attempts1–16;retry_on_lost;unknown/idempotent/deduplicatedsafety;- 0–86400000 ms 且 max 不小于 base 的 backoff;
- nullable、非负的
next_attempt_at_ms; - version、created/updated 时间与 Run 外键。
删除 Run 时 RetryPolicy 可级联删除;runtime role 对该表只有 SELECT/INSERT/UPDATE,没有 DELETE,常驻进程也不是 owner。
3. 公共端口不直接依赖 pg
@qinglong/runtime-core 提供 driver-neutral PostgresPool/Client/Queryable 结构接口、Run domain、Repository port 和稳定错误契约。独立 @qinglong/cluster-postgres 同时拥有 PostgreSQL SQL adapter 与真实 pg.Pool binding;公共 core 不含 SQL、driver 或具体 Profile。
因此:
- edge/standalone 根 importer 不安装或加载
pg; - Repository SQL、row codec 和事务边界在 cluster adapter 内评审,稳定错误与端口语义由 runtime-core 定义;
- driver connection、TLS、pool sizing、type parser 和 shutdown 属于 cluster package。
4. 每次业务事务使用一个 client
transaction(work) 必须:
- 从 Pool 取得一个 client;
BEGIN并使用READ COMMITTED;- 设置 transaction-local statement、lock 和 idle-transaction timeout;
- 在同一 client 上完成全部 Repository work;
COMMIT,失败时 best-effortROLLBACK;- 始终 release client。
work callback 抛出的业务错误必须原样返回;BEGIN/config/COMMIT 的 driver 错误映射为稳定 Repository error。不能在一个事务中混用 pool.query() 与 transaction client。
5. CAS、bigint 和唯一错误 fail closed
- Run CAS 精确匹配
id + expected version,新 version 必须恰好加一; - Attempt CAS 精确匹配
id + status + callback_sequence; - RetryPolicy CAS 精确匹配
run_id + expected version,新 version 必须恰好加一; - affected rows 只能是 0 或 1,多行视为存储损坏;
- PostgreSQL bigint string 只能转换为 safe integer,越界或非法值不得静默截断;
- RunEvent JSON 必须是 object,序列化后仍受 16 KiB 上限;
- 只将已知 idempotency、Attempt number、Event sequence/dedupe constraint 映射为稳定 duplicate error;其他 SQLSTATE 23 归为 constraint error;serialization/deadlock/lock/connection shutdown 归为 retryable busy。
6. 共享 contract 必须覆盖完整端口
RunRepository contract 至少验证:
- Run/Attempt/Event 原子写入与失败回滚;
- Run/Attempt CAS;
- RetryPolicy insert/read/version CAS;
- 稳定 duplicate error;
- 有界 Event 与 cancellation recovery page;
- oversized payload 在 SQL 前拒绝。
contract 接受 defaultExecutionOwner:SQLite 兼容 adapter 使用 legacy,PostgreSQL 使用 runtime。不得建立删减 PostgreSQL 专属 contract 来规避端口能力。
当前孵化状态
next 已具备:
pg-0003-run-retry-policy、control-corev2、6 表/18 索引严格 schema contract;- readiness 对 capability v2、RetryPolicy 表和最小权限矩阵的检查;
- 本机一次性 PostgreSQL 13 对三条 migration、deferred history/capability 和最终表集合的真实 DDL/事务 smoke;
- 本机 PostgreSQL 服务端已对 Repository 生成的 13 条参数化读写/CAS SQL 执行
PREPARE,验证占位符和参数类型推断;该检查曾发现并推动pg-0003增量补齐runs.legacy_cron_id; - 独立
@qinglong/runtime-core的 Run domain、RetryPolicy、Repository port 和稳定错误契约; - 独立
@qinglong/cluster-postgres的 PostgreSQL Run Repository、lazyopenDatabase()与 package-local fake-client tests; - 独立
@qinglong/cluster-control组合根已把 readiness、真实 Repository 创建、activation 与 Pool close ownership 串联; - transaction/CAS/row codec/SQLSTATE/payload 的 fake-client 测试;
- 扩展后的 SQLite RunRepository contract,首次真实覆盖 RetryPolicy。
共享 contract 允许每个 adapter 注入自己的错误 constructor/常量,避免 legacy 根兼容副本与 runtime-core package 因 JavaScript class identity 不同而产生伪失败;默认仍保持 SQLite legacy contract 不变。独立 @qinglong/cluster-postgres 已在本机 PostgreSQL 13 上用真实 driver 运行完整共享 Repository contract 和 migration/runtime 双角色权限验证;PostgreSQL 16/18 × x64/arm64 matrix 已接入 CI。远端矩阵成功证据、双连接竞争/failover 和完整 production control-plane stack 仍未完成,因此 cluster-control 继续保持 production unreachable。
影响
正面影响:
- cluster-control 不需要缩小版 Repository 或内存 RetryPolicy;
- SQLite/PostgreSQL 共享同一应用层事务和 CAS 语义;
- bigint 与 driver error 不泄漏为不稳定平台行为;
- edge 依赖隔离不因 PostgreSQL adapter 进入核心源码而失效。
代价与风险:
- SQL row codec 字段较多,schema、migration 和 contract 必须保持 lockstep;
- fake client 只能证明编排和映射,不能替代真实 PostgreSQL 并发语义;
- READ COMMITTED 下的正确性依赖所有状态写继续使用显式 CAS;
- concrete Pool 必须配置连接级 read timeout/type parser/shutdown,并接受 PostgreSQL 16/18 矩阵验证。
未选择的方案
- PostgreSQL 暂不保存 RetryPolicy:破坏端口和恢复事实源,拒绝。
- 放宽 cluster execution owner 为 legacy:把 2.x 兼容 ownership 带入集群,拒绝。
- 在核心根包直接依赖
pg:破坏 edge 安装隔离,拒绝。 - 所有 bigint 直接
Number():可能静默丢精度,拒绝。 - 所有 23505 都映射为同一种 duplicate:掩盖主键或未知约束损坏,拒绝。
- 用 SERIALIZABLE 代替所有 CAS:增加重试成本且不能替代领域 version/fencing,拒绝。
验证要求
pg-0001/0002checksum 不变,v1 数据库只能由pg-0003推进到 v2;- capability 漂移时
pg-0003全事务回滚且不留下 RetryPolicy 表/history; - schema readiness 精确验证 6 表、18 索引、39 个 CHECK、7 个 FK 和六表权限矩阵;
- SQLite 与 PostgreSQL 均运行同一完整 RunRepository contract;
- PostgreSQL 16/18 上验证 transaction rollback、Run/Attempt/RetryPolicy CAS 和 duplicate mapping;
- 两个 Pool client 竞争时只有一个 CAS 获胜;
- statement/lock/idle timeout 和 serialization/deadlock 映射为 bounded retryable error;
- bigint 越界、非法 enum、非 object JSON 和多行 identity 结果全部 fail closed;
- edge import/tarball/SBOM 审计继续证明未包含
pg/Drizzle/cluster package。