feat(postgresql): add service-owned admission and restore-incarnation rotation - #4334
Conversation
4973ca9 to
8f716c7
Compare
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
The control-plane coverage job runs without PostgreSQL, so the provider-backed integration suite cannot cover the new admission and rotation failure paths and coverage on new code stayed below the quality gate. Exercise those paths with a scripted provider instead: malformed memberships, verifier and authorization outages, invalid authenticated principals, invalid or unchanged incarnations, stored-incarnation drift, transaction failure, and protocol violation. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
8f716c7 to
7f6b334
Compare
The new service module and its suites were absent from the control-plane TypeScript program, so npm run typecheck:control-plane never checked them. Add them to the program and keep the real-path URL assertion type-safe. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
详细中文评审
Exact head: e4eb78457d06ed7ca755784b4b1eb52c28019f77(base origin/main = 5fdda4044)
动机
PostgreSQL authority store 之前只能由任何持有数据库连接的进程内调用方直接打开,没有服务层的认证与租户授权边界;而数据库从备份恢复后会沿用旧的 store_identity,恢复前签发的 provider revision token 仍然可读。这两点各自带来持续成本:每个部署都要自己发明凭据处理与租户校验(规则会漂移且无法被验证),每次恢复都要临时修补 identity。本 PR 把两件事收进一个 seam:openStore 返回类型化结果(未认证、未授权、验证不可用、身份不可用、身份不匹配),rotatePostgreSqlAuthorityStoreIdentity 在行锁下原子地把旧 incarnation 换成新 incarnation,使恢复前签发的 opaque revision token 冲突,而 head/commit/receipt 历史保持可读。只写一条 SQL 旋转 identity 的更小修法会让准入决策继续隐式,并且没有类型化失败与 ambiguous 语义,因此不够。
改动思路
入口是 PostgreSqlAuthorityService.openStore(postgresql_authority_service.ts:102)与 rotatePostgreSqlAuthorityStoreIdentity(postgresql_authority_store.ts:451)。权威状态是 loopx_control_plane.authority_store_metadata 单例行(schema_version + store_identity);决策归属保持三方清晰:认证与租户授权由部署注入的校验函数提供、由 service 拥有,LoopX authority 仍拥有领域决策,PostgreSQL 只拥有持久化 incarnation 身份。正向路径为:校验请求 → 认证 principal → 校验 principal id → 租户授权 → 构造 store → 读取并比对 store identity → 返回 opened 与绑定好的 store;负向路径在构造 store 之前就以类型化 reason code 返回,identity 比对是唯一在读取元数据之后发生的拒绝。
复用上,新模块没有另起一套 provider:它导入既有 store 的 database/options 类型,共享 POSTGRESQL_STORE_IDENTITY_PATTERN 与 POSTGRESQL_SCHEMA_VERSION,旋转复用既有 connect/rollback/oneRow/AuthorityStoreProtocolError 原语,因此没有第二条 SQL 写路径,也没有第二个状态源。凭据只作不透明转发,不入库、不进 provider 中性的 AuthorityStore 契约,准入与 provider 边界因此不互相污染。
具体改动
12 个文件、+1285/−2:生产 343 行(service 模块 212 行新文件 + store 131 行新增),测试与 fixture 807 行,文档 130 行,构建/类型检查 5 行。删除的 2 行只是 store 中两个常量由私有改为导出,值未变。分类上:生产改动是准入 seam 与旋转事务;测试改动包含确定性 seam 覆盖、一个真机套件与一份公开合成 fixture;文档是中英双语参考页与其索引条目;构建改动是一个 npm script 与 control-plane TypeScript program 的两个新条目。
关键代码讲解
PostgreSqlAuthorityService.openStore(postgresql_authority_service.ts:102):准入口。先做请求形状校验与 tenant/goal id 校验,再调用authenticatePrincipal,再校验返回的 principal id,再authorizeTenant,最后才构造 store 并做 incarnation 比对。关键不变量是"任何被拒绝的调用都不会拿到 store 句柄",以及注入函数抛错时返回*_unavailable而不是猜测通过。PostgreSqlAuthorityServiceOpenResult(postgresql_authority_service.ts:47):把失败原因建模为闭集 reason code 的判别联合(invalid_service_request、principal_unauthenticated、principal_verification_unavailable、tenant_unauthorized、tenant_authorization_unavailable、store_identity_unavailable、store_identity_mismatch),调用方只能按类型判断,不依赖文案,非法状态无法表达。rotatePostgreSqlAuthorityStoreIdentity(postgresql_authority_store.ts:451):BEGIN后以SELECT ... FOR UPDATE锁住元数据单例行,再在事务内复验 schema version 与期望 identity,通过才UPDATE store_identity并COMMIT;期望 identity 不匹配时抛类型化拒绝且不写入。commitStarted在COMMIT之前置位,因此丢失响应会返回ambiguous而不是伪成功,恢复路径明确要求"先读元数据再重试"。PostgreSQL service admits an authorized tenant and rotates a restored incarnation(postgresql_authority_service.integration.test.ts:50):真机套件,在一次性 PostgreSQL 上安装 schema、经 service 打开、旋转 incarnation,并断言恢复前的 revision 在旋转后冲突。缺 URL 时该文件只注册名称不同的 skip 占位,所以真机证据必须以名字断言,不能依赖"套件整体是绿的"。control-plane TypeScript program entry(tsconfig.control-plane.json:40):本轮评审修复的一处具体缺陷——新增模块与套件原本不在显式 include 列表内,npm run typecheck:control-plane根本没有读取它们;现在六个 PostgreSQL authority 文件都在 program 内(可用tsc --listFiles核对)。
对主干的风险
最强回归场景是"旋转结果被误判":若部署把丢失 COMMIT 响应的旋转当作成功,继续使用旧 revision token;或恢复后的库因为静默 no-op 继续接受陈旧 token。触发状态是 COMMIT 边界上的连接中断,或操作者用过期期望 identity 执行旋转。代码路径上,commitStarted 在 COMMIT 前置位使任何后续错误返回 ambiguous;期望 identity 在事务内被复验,过期或已旋转的 identity 会以 store_identity_mismatch 失败且不写入,因此这两条路都被显式阻断。影响面是数据库级 identity(不是单个 goal),所以文档把"先读元数据再重试"写成恢复流程而不是可选项。
第二类风险是范围风险,必须如实记录:scope_fit 显示本模块没有已发布运行时的调用方,参考页也明确声明它是 opt-in Stage 2B seam,不改变 file/SQLite 选择、不宣称 promotion。也就是说今天支撑这份契约的是两套套件加 ladder row,而不是生产流量;将来第一个部署适配器才是真正消费者,届时应补调用方层级的 parity 覆盖。此外,共享文件 postgresql_authority_store.ts 只改了常量可见性,store 自身 103 例套件在同 head 上保持 103 通过、0 失败、0 跳过,可用于判断默认关闭隔离未被破坏。
本轮评审发现并已在本 head 修掉的缺陷是 tsconfig 缺口(P2):tsc --listFiles 证实新增模块与套件不在 program 内,补入后立刻暴露出一个真实的 strict-mode 类型错误(真实路径断言把可能为 undefined 的环境变量当字符串用),两者在 e4eb78457 一并修复,因此不再是未决阻塞项。
我的整体评价
observable_semantics 判定为 intentional_change_validated:base 与 head 相比没有既有可观测行为被改写——provider 选择未变、元数据 schema 未变、identity 形状未变,唯一共享改动是常量导出可见性,而 store 套件在 head 上给出与 base 相同的 103/103 结果。default_off_isolation 为 isolated:模块只能被显式 import 与构造,没有任何自动安装、prompt 或调度副作用;authority_semantics 为 aligned:名称描述的是进程内准入边界,文档显式排除了网络访问、actor 所有权、lease 所有权、跨主机同步与 promotion 权限。change_proportionality 为 proportionate:机制 343 行生产代码换取一个可复用、可真机验证的边界,无需迁移。
验证(全部在同一 exact head):npm run typecheck:control-plane 干净;npm run test:postgresql-authority-service 10/10 通过(含 95ms 真机用例);npm run test:postgresql-authority-store 103/103 通过(28.7s,全新一次性库);ladder row s2b.postgresql_conformance_live 通过(tap_pass 103、tap_fail 0、tap_skipped 0、unverified 0);loopx canary premerge --from-git-diff 无失败命令、无 manual hold;change-quality 回执 cqr_19897393d56d0dd8cb23 验证为 valid。残留风险即上文无调用方 seam 这一点,已按 P3 记录。任何 head 变更(含 rebase)都会重启本评审,合并前需要对该 head 重新做一次 review 与 merge-readiness 检查。
English verdict: APPROVE — exact head e4eb78457d06ed7ca755784b4b1eb52c28019f77 (base 5fdda4044). Service-owned admission plus locked, atomic restore-incarnation rotation; reviewed against PostgreSQL 16.15 for real (service suite 10/10 including a 95 ms real-server case, store suite 103/103, ladder row pass with unverified 0) and clean typecheck. One P2 defect found and fixed in this head: the new module and its suites were missing from the control-plane TypeScript program, so tsc had never read them. Residual risk, recorded as P3: the seam has no shipped runtime caller, so a future deployment adapter should add caller-level parity coverage. Two non-blocking notes stay as follow-ups, not merge gates.
Self-merge decision recordReviewed exact head: Changed surfaces. Production: Checks that ran. Failures and skips. One Why this coverage is enough. The two changed behaviors are admission decisions and an atomic incarnation rotation, and both were exercised against a real PostgreSQL server rather than a mock, including the negative paths (rejected credential, unauthorized tenant, unavailable verifier, mismatched incarnation, wrong expected identity, lost COMMIT response). The repository-owned premerge gate passed with zero failures and zero manual holds, the change-quality receipt Merge uses the maintainer admin path because the ruleset requires an approving review and GitHub does not allow self-approval; the gate reports |
Summary
This is an independent PostgreSQL P-lane PR based directly on
main. It isnot stacked on or a replacement for #4317 (local provider routing) or #4328
(SQLite D2), and it does not change the file/SQLite default or add a runtime
caller/promotion claim.
PostgreSqlAuthorityServiceadmission seam.PostgreSQL connection is opened.
explicit post-COMMIT ambiguity.
integration test, and bilingual reference documentation.
Semantic changes and fixes
credentials stay in the service, while the provider receives only the
authorized tenant/goal context.
and identity-drift outcomes fail closed with stable reason codes and no
premature provider connection.
metadata-row lock. Existing heads, commits, events, receipts, and operation
IDs remain untouched; revision tokens from the prior incarnation conflict.
COMMITis reported asambiguous, requiring ametadata read before retry instead of guessing the outcome.
Validation
npm run typecheck:control-planenpm run test:postgresql-authority-service— 9/9, including the disposablereal PostgreSQL service path
npm run test:postgresql-authority-store— 53/53 against an isolated realPostgreSQL database
npm run test:control-plane— 1347/1347, 0 skipped, with isolatedPostgreSQL conformance and service databases
backend — source unchanged, provider heads exact, receipts found
loopx canary premerge --from-git-diff --git-diff-base origin/main— 17/17 checks passed, no manual holds
cqr_d8a4135e63438828d3d3— exact scope validThe real integration requires an isolated disposable database through
LOOPX_TEST_POSTGRES_SERVICE_URL; it never uses an active goal database.Duplication and scope
The implementation reuses the existing
AuthorityStorecontract, PostgreSQLmetadata schema, ID validation, and rollback/release lifecycle. The identity
and schema constants are exported once instead of being copied into a second
service contract. The new service does not introduce a provider registry,
generic pool framework, default-routing branch, or runtime authority caller.
Existing connection lifecycle code remains local to the provider because its
read/write transaction boundaries differ; the rotation helper reuses the
existing rollback/error discipline rather than adding another abstraction.
Follow-up boundary
Production transport authentication, tenant policy implementation, pool
capacity/failover qualification, runtime caller wiring, and provider promotion
remain deployment- or follow-up work. This PR establishes the service-owned
contract needed for that switchable PostgreSQL path without changing defaults.