proxy-pool/docs/adr/006-postgresql-admin-state.md
2026-07-29 21:50:21 +08:00

153 lines
6.2 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-006PostgreSQL 管理面采用事务深模块
## 状态
接受2026-07-29。
## 背景
Admin HTTP Handler 已定义 Upstream 启停、Routing 手工切换、配置重载和状态查询,
但权威管理状态、审计与 Outbox 还没有生产存储实现。PostgreSQL 只属于控制面;
Proxy 地址、凭据、生命周期、Worker 所有权、逐次提取记录和短期幂等结果都不能
进入 PostgreSQL。
管理写入存在一个不可拆分的不变量业务状态、Admin 审计和 Outbox 必须在同一
PostgreSQL 事务内提交。若把它们暴露成多个 Repository调用方容易漏写审计、
漏发事件或在失败时留下部分状态。
## 决策
### 模块与 seam
新增 `domain/adminstate` 公用契约。调用方按用途依赖窄接口,一个 Adapter 可以
同时实现全部接口:
```go
type Mutator interface {
SetUpstreamEnabled(context.Context, SetUpstreamCommand) (MutationResult, error)
SwitchRouting(context.Context, SwitchRoutingCommand) (MutationResult, error)
CommitConfig(context.Context, CommitConfigCommand) (MutationResult, error)
}
type SnapshotReader interface {
Snapshot(context.Context) (Snapshot, error)
}
type AuditReader interface {
ReadAudit(context.Context, AuditQuery) ([]AuditRecord, error)
}
type Outbox interface {
Claim(context.Context, ClaimCommand) ([]Event, error)
Acknowledge(context.Context, AcknowledgeCommand) error
}
```
三个 mutation 方法保留现有 Admin 语义事务、修订号、CAS、审计和 Outbox 都
隐藏在模块实现内部。不会暴露 `BeginTx`、SQL executor 或五个可被错误组合的浅
Repository。
`MemoryStore` 是并发安全参考 AdapterPostgreSQL Adapter 必须运行同一套公用
行为契约。Controller Admin 应用层只负责 DTO 映射、配置解析/校验和运行态发布,
不自行拼装数据库事务。
所有命令和查询在领域类型上提供公用 `Validate()`Memory 与 PostgreSQL
Adapter 必须在访问存储前调用同一方法,不复制名称、引用、分页或租约校验。
### 全局修订
每次产生管理状态变化时分配单调递增的全局 `revision`
- Upstream 目标状态已满足时返回 `changed=false`,不增加修订。
- Routing 的 `expectedCurrent` 不匹配时返回冲突,不写任何记录。
- 相同配置版本和校验和再次提交时返回 `changed=false`
- 相同配置版本对应不同校验和时返回冲突。
- `MutationResult.version` 是已提交的全局管理修订,不是 YAML 格式版本或 Worker
Snapshot 版本。
每次合法 Admin mutation 都写审计,包括 `changed=false`;只有真实状态变化写
Outbox。状态变化、审计和 Outbox 在同一事务提交。
### 配置修订
配置提交只保存管理面恢复所需的非敏感事实配置版本、SHA-256 校验和、来源、
Upstream 启用状态和 Routing 候选/当前选择。已解析 Secret、Provider Token、
Proxy 凭据和完整运行时对象不进入 PostgreSQL。
配置重载以一个事务替换管理快照。新 Routing 的当前 Upstream 必须属于其候选集,
所有引用的 Upstream 必须存在,名称与列表必须非空且唯一。校验失败发生在事务前,
旧修订继续生效。
### Routing CAS
`SwitchRouting` 在单条事务中锁定目标 Routing并同时校验
1. Routing 存在且启用;
2. `expectedCurrent` 等于权威当前值;
3. `target` 属于该 Routing 的候选 Upstream
4. 目标与当前值不同。
并发使用同一 expected 值时最多一个请求成功。目标等于当前值且 expected 匹配时
返回 `changed=false`,仍写审计但不写 Outbox。
### Outbox 消费
Outbox 使用有界 claim/ack而不是无界全表扫描
- `Claim` 要求稳定 consumer ID、当前时间、正租期和有界 limit。
- 未发布且未被有效租约占用的事件按序 claim。
- `Acknowledge` 只允许当前 consumer 在租期内确认自己 claim 的事件。
- 发布失败不 ACK租期过后可由其他 consumer 重试。
事件 payload 只包含管理资源名、目标状态、修订和必要原因,不包含 Secret、
Proxy、Client 或完整配置正文。
### Schema 边界
首个迁移只创建:
```text
control_revisions
config_revisions
upstream_admin_state
routing_admin_state
admin_audit_log
admin_outbox
```
迁移和集成测试必须断言不存在 Proxy 明细、Worker ownership、提取记录或短期
幂等表。PostgreSQL 故障只使管理写入失败关闭,不改变 Redis Extract 的可用性。
## 测试与验收
MemoryStore 与 PostgreSQL Adapter 运行相同契约,至少覆盖:
- 配置首次提交、相同重放、校验和冲突和非法引用零写入。
- Upstream enable/disable 幂等、修订单调、审计必写、Outbox 仅在变更时写。
- Routing CAS、目标校验和 100 个并发请求最多一个成功。
- 任一审计/Outbox 写故障导致状态完全回滚。
- Outbox 有界 claim、租约到期重试、错误 consumer ACK 拒绝和顺序稳定。
- 审计按 ID 稳定分页并保留 Actor、资源、动作、修订和 UTC 时间Routing no-op
写审计但不写 Outbox。
- 批量 ACK 先完整校验所有事件再提交,任一未知或冲突 ID 不得部分发布。
- 上下文取消、错误脱敏和 Snapshot 防止调用方修改内部状态。
- 真实 PostgreSQL 重复迁移、事务回滚和禁止数据表边界。
所有测试命令最长 60 秒。100,000 QPS 属于 Gateway 集群目标,不以管理面数据库
测试推导吞吐结论。
## 后果
收益调用方无法绕过事务不变量Memory/PostgreSQL 行为一致;管理数据边界可
通过 Schema 自动验证Outbox 重试有界且可观测。
代价PostgreSQL Adapter 内部实现较深配置提交需要完整管理快照Outbox
dispatcher 需要租约续期或确保单批发布时间小于 claim TTL。
## 不采用的方案
- 五个公开 Repository 加公开事务管理器:接口浅,调用方容易产生部分提交。
- 将所有命令塞入一个弱类型 `Command`:入口最少,但 Go 调用方需要运行时判断
联合字段,错误更晚暴露。
- 把 Proxy 或提取事实写入 PostgreSQL违反短效活动池与数据最小化边界。