Files
qinglong/docs/adr/ADR-0039-postgresql-schema-capability-and-runtime-readiness.md

163 lines
7.8 KiB
Markdown
Raw Permalink 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-0039PostgreSQL Schema Capability 与 Runtime Readiness
- 状态:Proposed
- 日期:2026-07-19
- 关联:QL-RFC-0001、ADR-0037、ADR-0038
## 上下文
数据库连接成功只能证明网络、认证和一个 session 可用,不能证明当前 cluster-control 副本可以安全处理 Run。以下状态都必须阻止 ready:
- PostgreSQL 版本不在支持范围;
- 数据库 history 来自更新代码、存在 gap、checksum 或 dialect 漂移;
- migration 已部分执行,但 schema capability 尚未推进;
- 表、列或关键索引缺失;
- 同名对象被手工修改或第三方对象混入 QingLong 核心 schema
- runtime role 是表 owner、拥有 schema CREATE/DDL,或缺少业务所需 DML
- runtime role 可以修改 migration history、schema capability 或 immutable RunEvent。
如果 readiness 只检查 `SELECT 1`,旧 pod 可能在滚动升级期间处理新 schema,未迁移 pod 可能开始 claim,过度授权 runtime 凭据也会长期进入常驻服务。
## 决策
### 1. Readiness 是有顺序的 fail-closed capability gate
cluster-control 在创建 Repository、启动 dispatcher/reconciler 或开放 admission 前,必须依次只读验证:
1. PostgreSQL `server_version_num` 和稳定 runtime User
2. 完整 `postgresql-main` history
3. `control-core` schema capability
4. `ql3` schema 的表、列和关键索引;
5. runtime role 的 schema/table privilege 与 ownership。
任一步查询失败、结果行数不确定、类型不符合或内容漂移都保持 not-ready。auditor 不执行修复、DDL、GRANT、migration 或外部副作用。
### 2. History 必须是当前代码的完整前缀
readiness 与 migration runner 复用同一个纯 history auditor
- ID 必须属于当前 `pg-*` stream
- 已应用集合必须是代码 migration 列表的连续前缀;
- stream、dialect、ID、64 位 SHA-256 和非负 safe-integer 时间全部精确;
- 未知 ahead ID、gap、重复记录或 checksum mismatch 都不允许 ready。
不允许只比较最后一个 migration ID,也不允许因数据库“比代码新”而继续运行旧 pod。
### 3. Capability 只代表完整 vertical slice
历史首个 capability v1 固定为:
```json
{
"contract_name": "control-core",
"contract_version": 1,
"migration_id": "pg-0002-run-core",
"capabilities": { "run_core": 1 }
}
```
capability 对 migration history 使用 deferred FK。`pg-0002` 的全部 DDL、capability update 和 history insert 在同一事务成功后,commit 才能通过。字段缺失、额外 capability key、版本或 migration ID 不一致均不允许 ready。
ADR-0041 的 `pg-0003-run-retry-policy` 补齐完整 RunRepository 所需的 RetryPolicy 后,当前 capability 精确推进为 v2
```json
{
"contract_name": "control-core",
"contract_version": 2,
"migration_id": "pg-0003-run-retry-policy",
"capabilities": { "run_core": 1, "run_retry_policy": 1 }
}
```
`pg-0003` 只接受精确 v1 前置状态;推进失败时新表、索引、capability 和 history 同事务回滚。
### 4. `ql3` 是严格拥有的核心 schema
当前 contract version 2 精确拥有:
- `schema_migrations`
- `schema_capabilities`
- `runs`
- `run_attempts`
- `run_events`
- `run_retry_policies`
这些表的 required columns 和关键索引由 typed schema contract 固定。缺失或额外表/列/索引均视为 drift。插件、运维工具和未来独立模块必须使用自己的 schema 或经新 migration/contract version 纳入,不能向 `ql3` 静默加对象。
### 5. Runtime role 使用精确的最小权限
runtime role 必须:
-`ql3``USAGE`、没有 `CREATE`
- 不是任何核心表 owner
- 对 history/capability 只有 `SELECT`
- 对 Run/Attempt 有 `SELECT/INSERT/UPDATE`,没有 `DELETE`
- 对 RunRetryPolicy 有 `SELECT/INSERT/UPDATE`,没有 `DELETE`
- 对 immutable RunEvent 有 `SELECT/INSERT`,没有 `UPDATE/DELETE`
权限比要求少会导致运行时中途失败;权限比要求多也会 readiness 失败。migration role 由独立 Job 使用,不作为常驻 runtime 凭据。
### 6. 失败事实必须低敏且稳定
readiness error 只暴露稳定 code 和 bounded finding,例如:
- `server_version_unsupported`
- `migration_history_invalid`
- `capability_invalid`
- `schema_contract_invalid`
- `runtime_role_invalid`
不得输出连接 URI、口令、SQL 参数、Run payload 或 Secret。数据库连接/查询错误由外层映射为 unavailable,不从错误推导 migration 或副作用结果。
## 当前孵化状态
`next` 已实现 driver-neutral `assertPostgresSchemaReady()` 和 contract v2
- 支持 PostgreSQL 1618
- 复用 migration core 的 ahead/gap/checksum 审计;
- 精确验证 capability、6 张表、columns、18 个索引、39 个命名 CHECK 和 7 个命名 FK
- 精确验证 schema CREATE、table owner 和六张表的 DML 矩阵;
- SQL contract 与 reviewed `pg-0001/0002/0003` migration 有静态 lockstep test
- fake-query tests 覆盖成功、版本过低、capability 漂移、未知对象/约束和过度授权 role。
auditor 已绑定 `@qinglong/cluster-postgres` 的真实 lazy `pg.Pool` integration,并在独立 migration/runtime role 下执行完整 history、catalog 与权限 readiness;该测试已进入 PostgreSQL 16/18 × x64/arm64 CI matrix。ADR-0045 的 HTTP application host 已把 `/readyz` 绑定到本次 activationcatalog/recovery/lifecycle 未全部完成时固定返回 503,完成后才与 `/api/v3` admission 一起切换。远端 PostgreSQL 矩阵成功证据、认证业务 router 和 Kubernetes 部署实测尚未完成,因此 cluster-control 仍不可发布为 ready。
runtime readiness 现在使用 metadata-only migration manifest 审计 ID/checksum,不加载 `pg-*` DDL/up 模块;manifest 与 executable stream 由精确 lockstep test 约束。该拆分隔离常驻 runtime 与 migration role 的代码入口,但不改变完整 history fail-closed 语义。
## 影响
正面影响:
- 滚动升级和 downgrade 不会让不兼容 pod 接受流量;
- migration 完成与业务 capability 可分别证明;
- 最小权限成为可执行发布门禁,不只是一段运维文档;
- 插件和第三方对象不会静默污染核心 schema。
代价与风险:
- 每次 contract version 变更都要同步 migration、manifest、GRANT 和 readiness test
- 对额外对象严格失败要求插件使用独立 schema;
- 云数据库权限模型差异需要在支持矩阵内逐个验证;
- readiness 查询必须有 timeout 和缓存/节流,不能在每个 HTTP 请求重复执行 catalog 扫描。
## 未选择的方案
1. **只执行 `SELECT 1`**:不能证明 schema 或权限,拒绝。
2. **只比较最后 migration ID**:无法发现 gap、checksum 和 dialect 漂移,拒绝。
3. **发现 drift 自动运行 migration**runtime/migration 角色混权,拒绝。
4. **忽略未知 `ql3` 对象**:无法区分插件对象、手工 drift 和 ahead schema,拒绝。
5. **runtime 使用 owner/superuser**:破坏最小权限和 blast-radius 边界,拒绝。
6. **readiness 失败后回退 SQLite**:会形成双事实源,拒绝。
## 验证要求
- PostgreSQL 16/18 上 success report 与 catalog 事实一致;
- 15/19/未知未来 major 均 fail closed
- ahead、gap、checksum、stream、dialect 和 history 类型损坏均 not-ready
- capability/history 任一事务回滚后 contract version 不推进;
- 任一 required table/column/index 缺失或额外对象出现均 not-ready;
- runtime role 多一个或少一个 privilege 都 not-ready
- migration role 可以迁移但不能被 runtime entrypoint 接受;
- auditor query timeout、连接失败和 failover 均保持 not-ready,恢复后可重新收敛;
- readiness 成功前没有 Repository admission、dispatcher timer、claim 或用户副作用。