# 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 事务中: 1. 创建 `ql3.run_retry_policies`; 2. 创建有界 due index 和 Run lost-retry index; 3. 只允许从精确的 `control-core` v1 推进到 v2; 4. 写入 capability: ```json { "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_attempts` 1–16; - `retry_on_lost`; - `unknown/idempotent/deduplicated` safety; - 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)` 必须: 1. 从 Pool 取得一个 client; 2. `BEGIN` 并使用 `READ COMMITTED`; 3. 设置 transaction-local statement、lock 和 idle-transaction timeout; 4. 在同一 client 上完成全部 Repository work; 5. `COMMIT`,失败时 best-effort `ROLLBACK`; 6. 始终 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-core` v2、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、lazy `openDatabase()` 与 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 矩阵验证。 ## 未选择的方案 1. **PostgreSQL 暂不保存 RetryPolicy**:破坏端口和恢复事实源,拒绝。 2. **放宽 cluster execution owner 为 legacy**:把 2.x 兼容 ownership 带入集群,拒绝。 3. **在核心根包直接依赖 `pg`**:破坏 edge 安装隔离,拒绝。 4. **所有 bigint 直接 `Number()`**:可能静默丢精度,拒绝。 5. **所有 23505 都映射为同一种 duplicate**:掩盖主键或未知约束损坏,拒绝。 6. **用 SERIALIZABLE 代替所有 CAS**:增加重试成本且不能替代领域 version/fencing,拒绝。 ## 验证要求 - `pg-0001/0002` checksum 不变,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。