proxy-pool/docs/adr/006-postgresql-admin-state.md
2026-08-02 15:14:20 +08:00

7.3 KiB
Raw Permalink 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)
    DisableRouting(context.Context, DisableRoutingCommand) (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 在同一事务提交。

配置修订

配置提交只保存管理面恢复所需的非敏感事实:配置版本、完整已解析配置的 HMAC-SHA-256 指纹、来源、Upstream 启用状态和 Routing 候选/当前选择。HMAC 使用 独立外部高熵密钥,覆盖 Secret 轮换以驱动多副本收敛,同时避免普通摘要成为 低熵 Secret 的离线校验器。HMAC 密钥、已解析 Secret、Provider Token、Proxy 凭据、配置正文和完整运行时对象均不进入 PostgreSQL。

配置发布携带事务返回的全局 revision。本地 config.Store 只接受严格递增 revision因此并发提交或 Supervisor 同步的迟到旧版本不能覆盖较新运行配置。

配置重载以一个事务替换管理快照。新 Routing 的当前 Upstream 必须属于其候选集, 所有引用的 Upstream 必须存在,名称与列表必须非空且唯一。校验失败发生在事务前, 旧修订继续生效。

Routing CAS

SwitchRouting 在单条事务中锁定目标 Routing并同时校验

  1. Routing 存在且启用;
  2. expectedCurrent 等于权威当前值;
  3. target 属于该 Routing 的候选 Upstream
  4. 目标与当前值不同。

并发使用同一 expected 值时最多一个请求成功。目标等于当前值且 expected 匹配时 返回 changed=false,仍写审计但不写 Outbox。

DisableRouting 用于 Sequential 的末端 stop。它同样锁定目标 Routing并要求 expectedCurrent 仍等于权威当前值;满足条件时保留 current_upstream、仅将 enabled 置为 false。这样延迟的 Provider Empty 观察不会覆盖人工切换或配置重载。 已停用且 expected 匹配时返回 changed=false 并审计,不再产生 Outbox真实停用写入 routing.disabled 事件。该行为与 SwitchRouting 共享同一个全局 revision 分配边界。

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 个并发请求最多一个成功terminal stop 的 100 个 并发请求仅一个真实停用,其余为幂等审计 no-op。
  • 任一审计/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违反短效活动池与数据最小化边界。