Files
qinglong/docs/adr/ADR-0037-postgresql-cluster-control-storage-incubation.md
T

234 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-0037PostgreSQL Cluster-control 存储孵化与跨方言契约
- 状态:Proposed
- 日期:2026-07-19
- 关联:QL-RFC-0001、ADR-0004、ADR-0012、ADR-0014、ADR-0031、ADR-0032、ADR-0033、ADR-0036
## 上下文
QingLong 3.0 已明确 edge/standalone 使用本机 SQLitecluster-control 使用 PostgreSQL。但当前 `next` 的实现事实仍是:
- 生产依赖只有 Sequelize + SQLite driver,没有 `pg`、Drizzle ORM 或 Drizzle Kit
- migration runner 的 `MigrationContext`、history model 和 transaction 都绑定 Sequelize
- `0001``0024` 的 checksum 由 SQLite manifest 计算,部分 migration 还扩展 2.x legacy 表;
- 至少 20 个 legacy-sequelize adapter 显式拒绝非 SQLite,其中 11 个包含 `SQLITE_BUSY`、短 `BEGIN IMMEDIATE` 或等价单机重试假设;
- local completion journal、local Secret envelope、local Artifact retention、legacy owner bootstrap 和 legacy panel identity 不是 cluster-control 应复制的 schema
- cluster-control Profile 当前拒绝装配 SQLite + LocalProcess Primary,这是正确的 fail-closed 状态。
如果为了“双方言”把 PostgreSQL SQL 直接加入已应用的 `0001``0024` checksum,现有 SQLite 数据库会在升级时得到 checksum mismatch。反过来,如果 PostgreSQL 复用这些 migration ID 但记录不同 checksum,同一 ID 将不再代表同一不可变变更。把现有 Sequelize adapter 切换 dialect 也不能自动获得多副本 claim、row lock、SQLSTATE retry 和 shared evidence 语义。
因此 PostgreSQL 不能作为现有 SQLite runner 的一个连接字符串选项加入;它需要独立、可审查的 migration stream 和 adapter 实现,但必须通过相同领域 Repository contract。
## 决策
### 1. 冻结 SQLite migration identity
`0001``0024` 继续属于 `sqlite-main` stream,其 ID 与 checksum 不因 PostgreSQL 实现而变化。后续 SQLite migration 也只对自身方言 SQL/manifest 计算 checksum。
PostgreSQL 使用全局可区分的 ID,例如:
```text
pg-0001-schema-history
pg-0002-run-core
pg-0003-worker-dispatch
pg-0004-project-identity-policy
pg-0005-approval-action-recovery
```
一个 PostgreSQL 数据库只记录 `pg-*` stream;一个 SQLite 数据库只记录现有数字 stream。禁止在同一数据库伪装应用另一方言 migration,也禁止通过修改旧 checksum“升级”migration 内容。
跨方言兼容由单独的 `schema contract version` 表达,而不是要求 migration ID 相同。contract version 只在一组 migration 全部完成后推进,用于 ql-core readiness;它不替代逐条 checksum 历史。
这项决策补充并修正 ADR-0004 中“一个 checksum 同时覆盖两个方言 SQL”的早期目标。单一不可变 history 语义保留,但具体 migration stream 按方言分开。
### 2. PostgreSQL 从空库建立 control-plane-only schema
首版只支持空 PostgreSQL 数据库或受支持的 `pg-*` 3.x migration,不把 2.x SQLite 文件当作可原地升级的 PostgreSQL 数据库。baseline 分阶段创建:
1. migration history、schema metadata 与 runtime capability
2. Run、RunAttempt、RunEvent、Task revision、cancellation、retry
3. Worker registry、Run dispatch lease 与多副本 queue 索引;
4. Project、stable Identity、RoleBinding 与 Policy 审计事实;
5. ApprovalRequest、ApprovedAction dispatch/execution/recovery/receipt/authorization fact。
cluster-control baseline 不创建:
- 2.x Crontab/Env/Subscription/RunningInstance 等 legacy projection
- local completion receipt journal
- local execution context recipe
- local Secret envelope/key
- local Artifact retention/checkpoint
- local owner bootstrap challenge
- legacy panel singleton identity binding。
这些能力必须分别由共享 Artifact/object store、KMS/Secret provider、OIDC/mTLS/API identity 和集群运维 bootstrap 协议替代。表名相同不代表可以复制 local-only security model。
### 3. Migration runtime 与应用 runtime 分权
PostgreSQL migration 默认由部署级 Job 使用 migration role 执行;应用 pod 使用无 DDL 权限的 runtime role。若支持应用内 migration,必须先取得固定、版本化 advisory lock,并满足:
- lock key、等待上限和 owner metadata 固定;
- lock 未取得时 pod 保持 not-ready,不并行执行 DDL
- 每条 migration 在单事务可行时原子提交;需要 `CREATE INDEX CONCURRENTLY` 等非事务步骤时拆为显式 prepare/commit migration,并有可恢复 marker
- history row 只在全部步骤成功后写入;
- checksum mismatch、未知 ahead migration 或 contract version 不兼容都阻止 readiness
- runtime role 只能只读验证 history/capability,不能自动修复 schema。
生产 PostgreSQL backup、PITR 和 restore 属于部署平台;应用只调用有界 backup precondition hook,不读取或复制数据目录。
### 4. 共享 Repository contract,不共享并发 SQL
领域 port、状态机、错误码、幂等键和 cursor 语义保持统一;SQLite 和 PostgreSQL adapter 使用不同算法。
PostgreSQL 至少采用:
- queue-like batch claim`SELECT ... FOR UPDATE SKIP LOCKED` 或等价的有界 claim + `UPDATE ... RETURNING`
- 单资源 mutation:锁定 aggregate/control row,再按 expected version CAS
- 不存在子资源的首次创建:通过 parent row、唯一约束或专用 lock row 串行化,不能依赖 SQLite 的 database-wide write lock
- immutable append:唯一 `(aggregate_id, version|sequence|mutation_id)` 约束;
- lease fencingowner、token、generation/attempt、version 全部进入条件写;
- Policy fenceProject 与 RoleBinding version 在同一事务复验;
- Approved RunRun/Attempt/Event/receipt 在同一 PostgreSQL transaction
- human recoveryresolution、execution/control terminal 与 authorization fact 在同一 transaction。
`SKIP LOCKED` 只用于 queue claim,不用于普通列表、Policy read 或审计查询。cursor 仍使用稳定 keyset,不暴露 offset 全表扫描。
### 5. 现有逐条 claim port 先保正确性,再扩展批量能力
当前 dispatcher/reconciler 的流程是 `listDue -> claim(id)`。PostgreSQL 可先用条件 `UPDATE ... WHERE version/status/due RETURNING` 保证只有一个 winner,从而保持领域正确性;但高并发下会产生额外 round trip 和热点竞争。
cluster 性能实现允许增加显式 `claimDueBatch` port,由 adapter 在一个短事务内 SKIP LOCKED 并返回已 fenced 的 bounded snapshots。应用层必须把单条与批量 claim 归一为相同领域结果,并通过同一 mutation replay/terminal winner contract。禁止让 service 拼接 PostgreSQL SQL 或感知 SQLSTATE。
是否增加批量 port 由 benchmark 决定,不在未测量前破坏现有单机接口。
### 6. SQLSTATE 只能在 adapter 边界映射
PostgreSQL adapter 将驱动错误映射为稳定 Repository 错误,至少覆盖:
- serialization failure/deadlock:有界全事务 retry,耗尽后统一 unavailable/retry-exhausted
- lock timeout/query canceledbusy/timeout
- unique/foreign-key/check violation:按具体约束名映射 idempotency conflict、fence conflict 或 corruption
- connection reset/failover/read-only primarydatabase unavailable
- bigint/JSON/boolean 解码漂移:corrupt persisted statefail closed。
领域 service 不读取错误字符串、SQLSTATE、constraint name 或 driver class。retry 必须有次数和总时间上限,不能在事务 callback 内执行外部副作用。
### 7. 连接与查询默认有界
cluster-control adapter factory 必须显式配置:
- bounded pool min/max、acquire timeout 和 idle lifetime
- statement timeout、lock timeout、idle-in-transaction timeout
- application name、UTC session 与明确 search path
- TLS 验证策略和 Secret-ref 连接配置;
- bigint 转换:只接受 JavaScript safe integer 范围,越界 fail closed
- query log 默认不记录参数,尤其不记录 token、Secret、prompt 或 Tool payload
- shutdown 先停止 admission/lifecycle、drain in-flight transaction,再有界关闭 pool。
edge/standalone 构建和启动不得 import、初始化或连接 PostgreSQL。依赖采用 profile lazy load 或独立 package/entrypoint,避免路由设备为 cluster driver 支付启动 RSS。
### 8. Contract suite 分四层
每个进入 cluster-control 的 Repository 必须通过:
1. **领域等价层**SQLite 与 PostgreSQL 对相同 command 返回相同 record、错误和重放语义;
2. **双连接竞态层**create、CAS、claim、renew、completion、revoke/consume/resolve 只有一个 winner
3. **多副本压力层**:至少三个独立 pool 并发 claim,无重复副作用,吞吐、lock wait、retry 有界;
4. **故障层**:事务中断、连接 reset、primary failover、migration leader 竞争和 pod kill 后可恢复。
Approved Action 首批必须覆盖:
- approval consume + dispatch/execution baseline 原子性;
- pre-start takeover 与 post-start recovery-required
- normal completion、automatic evidence resolution、human resolution 的 terminal winner
- `run.create` Run/Attempt/Event/receipt 原子提交;
- Role revoke/Project archive 与 manual recovery authorization race
- recovery-first lifecycle 在一个副本失败时不扩大副作用。
### 9. 依赖选择是单独门禁
本 ADR 不直接安装 `pg`、Drizzle ORM 或 Drizzle Kit。具体 exact version、Node/PostgreSQL 支持线和独立 package 交付边界由 ADR-0038 决定;依赖落锁前仍必须由 Maintainer 接受:
- 固定 Node 24 patch 与 PostgreSQL 最低/最高支持版本;
- exact `pg`、Drizzle ORM/Kit 版本和升级策略;
- Docker/Testcontainers 或外部 CI service 的测试拓扑;
- migration Job 与应用内 advisory-lock 两种部署方式的首选项;
- runtime/migration 最小权限 role
- 基准和 failover 环境的 owner。
依赖加入后必须证明 edge/standalone 默认路径不 import cluster bundle,并记录安装体积、冷启动和 RSS 差异。
### 10. 实施顺序
1. 把 migration runner 抽象为 dialect-neutral history/transaction port,同时让现有 SQLite suite 零语义变化;
2. 建立 `pg-*` SQL 文件、checksum 与只读 schema capability audit
3. 先实现 Run core + WorkerRegistry + RunDispatchLease,验证多副本 claim/lease/fencing
4. 实现 Project/Identity/Policy 与 Approval/Approved Action
5. 实现 shared Artifact、Secret/KMS、plan/receipt provider
6. 通过多副本、failover、backup/restore 和资源门禁后,才允许 cluster-control readiness 成功。
任一阶段不得让未实现的 port 回退到 SQLite、进程内 Map 或本地文件唯一事实。
### 11. 当前孵化状态
`next` 已完成第一步的兼容切片,并开始第二步的 driver-neutral Store 切片:
- 新增不依赖数据库驱动的 migration stream core,统一校验 stream/dialect、ID scheme、checksum scheme、重放、事务内并发复验和 history 原子写入;
- ID scheme 显式区分既有 `sqlite-numbered` 与未来 `postgres-prefixed`,不重写任何 SQLite migration ID
- checksum scheme 对既有 SQLite 使用 `legacy-opaque` 保持 runner 兼容,对未来 PostgreSQL 强制 64 位小写 SHA-256
- `SequelizeSqliteMigrationStreamStore` 把现有 Sequelize transaction/query interface 映射到通用 core,并保持 `SchemaMigrations(id,checksum,applied_at)` 行结构;
-`runMigrations()` 签名、`Migration` 接口、注册列表和 `[migration] Applied ...` 日志格式不变;
- 非 SQLite dialect 在 history 访问前拒绝,避免把 SQLite migration stream 误用于 cluster-control
- contract tests 覆盖旧 history 精确重放、checksum mismatch、损坏 stream/dialect、并发 leader winner、migration/history 同回滚和完整 legacy fixture 升级。
- `PostgresMigrationStreamStore` 只依赖 QingLong 自有 Pool/Client 端口,以固定 transaction advisory lock 串行 history bootstrap 和 migration;竞争者 fail closedwork/history 同事务回滚;
- PostgreSQL transaction 固定 statement/lock/idle-in-transaction timeouthistory bigint 时间超出 JavaScript safe integer 时按损坏状态拒绝。
- 通用 core 会在执行前枚举完整 history,未知 migration、重复 ID、非前缀缺口、stream/dialect 漂移和 checksum mismatch 均 fail closedSQLite 默认完整 runner 同样执行 downgrade/ahead 审计,自定义 migration 子集只作为 scoped fixture seam。
- `pg-0001-schema-capability``pg-0002-run-core``pg-0003-run-retry-policy` 已定义首批不可变 streamcapability 对 history 的 deferred FK 保证 `control-core=1/2` 只和对应 `pg-0002/0003` history 同事务提交。
- ADR-0039 的只读 readiness auditor 已固定 PostgreSQL 1618、完整 history、capability、精确 schema object 和最小权限 runtime-role 五层 gate;未知 `ql3` 对象和过度授权同样 fail closed。
后续切片已按 ADR-0038/0039/0041/0042/0043/0044 落入独立 package`@qinglong/runtime-core` 公开 migration/activation/RunRepository contract`@qinglong/cluster-postgres` 拥有真实 lazy `pg.Pool`、Run Repository、Drizzle schema、migration 和 readiness`@qinglong/cluster-control` 在 readiness 后创建真实 Repository 并执行有序激活。package-local fake-client tests 与本机 PostgreSQL 13 真实 shared contract 已通过,PostgreSQL 16/18 × x64/arm64 最小权限 integration 也已接入 CI。远端矩阵成功证据、多 Pool 竞争/failover、完整 control-plane application stack 和独立发布产物仍未完成,因此 cluster-control 尚不可作为生产 ready 控制面。
## 影响
正面影响:
- 不修改已经应用的 SQLite migration checksum
- cluster-control schema 不携带本机/legacy 数据模型;
- 领域状态机保持一致,同时允许真正的 PostgreSQL 并发算法;
- migration、runtime 和运维权限边界清晰;
- edge 不为集群 driver 付出依赖、连接或常驻内存。
代价与风险:
- 需要维护两条 migration stream 和 schema contract version
- PostgreSQL adapter 不能通过“换 dialect”自动得到,工作量较大;
- 双方言 contract、多副本和 failover CI 增加维护成本;
- SQLite 到 PostgreSQL 迁移必须是独立离线产品,不是简单复制数据库文件;
- 在首个 PostgreSQL vertical slice 完成前,cluster-control 继续不可用。
## 未选择的方案
1. **给 `00010024` checksum 追加 PostgreSQL SQL**:会破坏已应用 SQLite history,拒绝。
2. **相同 migration ID 记录不同方言 checksum**ID 失去不可变语义,拒绝。
3. **Sequelize 连接改成 postgres 即完成适配**:锁、重试、schema 和 local-only 表语义错误,拒绝。
4. **cluster-control 共享 NFS SQLite**:多副本 locking/可见性/故障语义不受支持,拒绝。
5. **所有 Profile 都安装并初始化 pg Pool**:增加 edge RSS、Secret 和连接面,拒绝。
6. **每个 pod 自动跑 migration 且失败继续 ready**DDL 竞争和 schema drift 会污染运行时,拒绝。
7. **一个巨大 PostgreSQL baseline 一次创建全部能力**:难以审查、回滚和按 vertical slice 验证,拒绝。
8. **在 service 中判断 SQLSTATE**:泄漏 driver 语义并破坏 Repository contract,拒绝。
9. **先实现在线 SQLite→PostgreSQL 双写**:跨库事务、回放和回退复杂度过高,拒绝。
## 验证要求
- 已有 SQLite `00010024` checksum 在 PostgreSQL 工作开始后保持不变;
- PostgreSQL migration ID 使用独立前缀,history 重放、checksum mismatch 和 ahead schema fail closed
- baseline 只包含声明的 cluster-control 表,local/legacy 表缺席;
- migration leader 竞争只有一个 writer,其余 pod not-ready
- runtime role 无 DDL 权限,migration role 不作为常驻应用凭据;
- Repository contract 在 SQLite/PostgreSQL 返回同一领域结果和稳定错误;
- 三 pool 并发 claim/renew/complete 无重复 winner
- serialization/deadlock/lock timeout retry 有次数和总时长上限;
- kill/failover 后 lease/recovery 状态可收敛,不从连接错误推导副作用结果;
- PostgreSQL bundle 在 edge/standalone 默认入口的 import graph 中不可达;
- cluster-control 在任一 schema capability、shared Artifact、Secret/KMS 或 adapter 缺失时拒绝 ready。