Files
qinglong/docs/adr/ADR-0041-postgresql-run-repository-contract-parity.md
T

8.3 KiB
Raw Blame History

ADR-0041PostgreSQL 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 省略 RetryPolicycluster-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
{
  "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 116
  • retry_on_lost
  • unknown/idempotent/deduplicated safety
  • 086400000 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 errorserialization/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 接受 defaultExecutionOwnerSQLite 兼容 adapter 使用 legacyPostgreSQL 使用 runtime。不得建立删减 PostgreSQL 专属 contract 来规避端口能力。

当前孵化状态

next 已具备:

  • pg-0003-run-retry-policycontrol-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。