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

6.0 KiB
Raw Blame History

ADR-006PostgreSQL 管理面采用事务深模块

状态

接受2026-07-29。

背景

Admin HTTP Handler 已定义 Upstream 启停、Routing 手工切换、配置重载和状态查询, 但权威管理状态、审计与 Outbox 还没有生产存储实现。PostgreSQL 只属于控制面; Proxy 地址、凭据、生命周期、Worker 所有权、逐次提取记录和短期幂等结果都不能 进入 PostgreSQL。

管理写入存在一个不可拆分的不变量业务状态、Admin 审计和 Outbox 必须在同一 PostgreSQL 事务内提交。若把它们暴露成多个 Repository调用方容易漏写审计、 漏发事件或在失败时留下部分状态。

决策

模块与 seam

新增 domain/adminstate 公用契约。调用方按用途依赖窄接口,一个 Adapter 可以 同时实现全部接口:

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 边界

首个迁移只创建:

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 拒绝和顺序稳定。
  • 上下文取消、错误脱敏和 Snapshot 防止调用方修改内部状态。
  • 真实 PostgreSQL 重复迁移、事务回滚和禁止数据表边界。

所有测试命令最长 60 秒。100,000 QPS 属于 Gateway 集群目标,不以管理面数据库 测试推导吞吐结论。

后果

收益调用方无法绕过事务不变量Memory/PostgreSQL 行为一致;管理数据边界可 通过 Schema 自动验证Outbox 重试有界且可观测。

代价PostgreSQL Adapter 内部实现较深配置提交需要完整管理快照Outbox dispatcher 需要租约续期或确保单批发布时间小于 claim TTL。

不采用的方案

  • 五个公开 Repository 加公开事务管理器:接口浅,调用方容易产生部分提交。
  • 将所有命令塞入一个弱类型 Command:入口最少,但 Go 调用方需要运行时判断 联合字段,错误更晚暴露。
  • 把 Proxy 或提取事实写入 PostgreSQL违反短效活动池与数据最小化边界。