← 返回首页
功能执行事务与幂等架构
# 功能执行事务与幂等架构
## 目标
普通记录和真实功能执行必须把数据库中的主记录、不可变修订、功能执行历史、审计事件、幂等预占与幂等成功结果视为一个提交单元。任何一步失败时,这些数据库写入全部回滚;同一个 owner 与 `Idempotency-Key` 的相同 operation 并发请求最多执行一次工作;同 owner 的键已绑定其他 operation 时返回 HTTP 409 `IDEMPOTENCY_SCOPE_CONFLICT`。
幂等语义不是“再次读取当前最新记录”。首次成功后,即使记录继续产生新修订、当前模块规则发生变化,或重试请求正文已经不可再通过校验,同键重放仍必须恢复首次成功时的精确结果。
现实外部系统不属于本地数据库事务。`record-action` 只要声明直接外部调用,就必须失败关闭并改用受控 Outbox;不能在事务中调用不可回滚的 Provider 后再声称数据库回滚能够撤销现实副作用。
## 事务传播
`DATABASE_TOKEN` 提供事务感知的 Drizzle 代理。`IdempotencyService` 建立外层事务后,`AsyncLocalStorage` 把仓储、审计和幂等服务的数据库调用路由到同一事务执行器。仓储内部再次调用 `transaction` 时加入现有事务,不另取连接。
```text
IdempotencyService.runPrepared
│
├── 无 key:开启事务 -> prepare -> work
│
└── 有 key:
读取有效成功缓存
├── hit:按最小引用精确恢复首次结果,直接返回
└── miss:原子插入 PROCESSING 预占
│
├── prepare(无副作用)
├── 主记录 + revision
├── feature_execution(如有)
├── audit_event
└── SUCCEEDED + 策略化结果 envelope
│
├── 全部成功:COMMIT
└── 任一步失败:ROLLBACK
```
`prepare` 只能读取当前 owner/module 记录并执行确定性校验或规范化,不得写数据库、审计、Outbox 或调用 Provider。只有成功取得数据库预占的请求会执行 `prepare`。因此:
- 成功 cache hit 不读取当前记录、不重复模块策略,也不执行写入和审计;
- cache miss 的策略错误发生在预占之后,但错误会让预占和全部业务副作用在同一事务中共同回滚;
- PostgreSQL 唯一约束仲裁跨进程竞争,同进程 `inFlight` Promise 只用于避免重复工作,不代替数据库约束。
真实 PostgreSQL 使用 Drizzle/`pg` 事务。`pg-mem` 不能证明唯一索引等待、锁、真实回滚或查询计划;这些合同只能由一次性 Testcontainer PostgreSQL 验证,未执行时必须记录为 `not_run`。
## 幂等键状态机
幂等唯一边界固定为:
```text
owner_id + idempotency_key
```
`operation` 是已占用键的业务作用域校验值,不是唯一索引的一部分;必须标明模块和动作,并在适用时包含记录边界,不能只使用功能编号。当前唯一索引由 `0030_idempotency_global_owner_key.sql` 固定为 owner 与键;不同 owner 互不影响,有效期沿用 24 小时,同键重放不会延长首次有效期。状态流转如下:
1. 有效 `SUCCEEDED`:按当前操作的结果策略恢复首次结果;
2. 有效 `PROCESSING`:返回 `IDEMPOTENCY_IN_PROGRESS`(HTTP 409);
3. 已过期记录:在同一事务删除后重新竞争预占;
4. 不存在记录:插入 `PROCESSING`;
5. 业务成功:更新为 `SUCCEEDED` 并写入结果 envelope;
6. 任一步失败:事务回滚,不保留失败缓存,原键可以安全重试。
跨进程并发插入依靠 PostgreSQL 唯一约束等待。首次事务提交后,竞争者读取并恢复成功结果;首次事务回滚后,竞争者可以取得预占并执行。异常遗留且尚未过期的 `PROCESSING` 不得被当作成功或重复执行。
## 最小结果 envelope
新写入的 `idempotency_record.response` 使用固定 envelope:
```json
{
"schemaVersion": 1,
"policy": "record-snapshot-reference-v1",
"result": {
"recordId": "01J...",
"revision": 2
}
}
```
结果策略由 `IdempotencyResultPolicy<T>` 定义:
- `serialize`:把完整业务结果转换为可持久化 JSON;
- `restore`:在当前认证 owner 与路由 moduleCode 边界内恢复完整结果;
- `policy` 名称变化代表恢复语义变化,名称不匹配必须失败关闭。
Record Kernel 的普通记录只保存 `recordId + revision`;功能执行只额外保存 `executionId`。重放通过 `findRevision(owner,module,record,revision)` 与 `findExecution(owner,module,record,executionId)` 精确读取,不返回记录后来产生的最新 revision。
禁止把以下内容写入新幂等结果:
- title、summary、完整 payload 或 snapshot;
- feature input/output 正文;
- MEM-001 `originalContent`、`currentContent`;
- Cookie、Token、Authorization、连接串、Provider 响应或私钥。
历史未封装结果仅保留读取兼容,不作为新写入格式。Identity 旧通用写链已在 ModuleRegistry、Capture 和 v1 HTTP 处理器进入幂等/仓储前失败关闭;`0020_identity_legacy_idempotency_redaction.sql` 在 `0019` 台账状态为 `passed` 后,仅按 `operation='identity:create'` 且 `response.policy='raw-json-v1'` 精确清理历史回执,不触碰 typed、审计或 legacy 实体数据。迁移只输出删除计数,重复执行安全。
## 修订与执行一致性
`record_revision` 的 owner 与父记录由数据库复合外键保护;module 一致性由仓储查询的父记录 join 与不可变 snapshot 校验共同失败关闭。指定 revision 的 snapshot 必须同时满足:
- 列 `owner_id`、`record_id`、`revision` 与查询边界一致;
- snapshot 的 `ownerId`、`moduleCode`、`id`、`revision` 与父记录和列值一致;
- snapshot payload 通过受控 JSON 边界。
`feature_execution` 已通过 `(owner_id,module_code,record_id)` 复合外键绑定父记录,并按 owner/module/record/executionId 精确读取。
当前设计没有在 0009 为 `record_revision` 增加新的 module 列或执行历史数据回填。若后续需要数据库层 module 复合外键,应在独立 Expand/Copy/Verify 批次中采用对既有数据安全的迁移,不得在公共加固批次直接修改业务数据。
## 外部阻塞与 Outbox
`blocked_external` 只表示系统持久化了“当前无法执行现实外部动作”的事实,不表示现实动作成功。响应保持 `{ record, execution }`:
| 字段 | 约束 |
| --- | --- |
| `execution.mode` | `blocked_external` |
| `execution.status` | `BLOCKED` |
| `record.revision` | 保持请求前修订号 |
| 审计动作 | `FEATURE_<featureId>_BLOCKED` |
直接外部调用必须改为受控 Outbox:业务事务只提交待发送事实和幂等标识,独立 worker 在事务外执行 Provider 调用。Batch 003 只建立失败关闭边界,不实现新的 Outbox。
## 验收证据
- `apps/api/test/unit/idempotency-result-policy.spec.ts`:最小引用、策略名和精确恢复;
- `apps/api/test/unit/record-kernel-policy-order.spec.ts`:cache hit 跳过当前状态与策略,cache miss 保持副作用顺序;
- `apps/api/test/e2e/reliability-hardening.e2e.spec.ts`:同键并发、回滚和成功缓存优先;
- `apps/api/test/e2e/record-kernel-v2.e2e.spec.ts`:记录继续变化后仍恢复首次 revision,且幂等表不保存正文;
- `apps/api/test/e2e/memory-text.e2e.spec.ts`:MEM-001 只保存 `recordId + revision`;
- `apps/api/test/integration/record-kernel.postgres.spec.ts`:一次性 PostgreSQL 中验证 owner/module 隔离、并发乐观锁、revision 漂移与精确回放;仅在 `RUN_TESTCONTAINERS=true` 且 Docker 可用时执行。
## 2026-10-08:事务感知进程内共享保护(源码候选)
本段只追加本批候选边界,不改写前文与历史证据。本批测试、类型、构建、可执行门禁、发布和真实运行均为 `not_run`;前置已发布基线为 `e2eede44993c2d2ee63fee96d33488c34441dbbf`。平台机制覆盖不代表 inventory 中每个模块的业务流程已验收,Catalog 与既有 partial 不提升。
内建 `DATABASE_TOKEN` 代理使用 `database-transaction-context.ts` 的内部 WeakMap 登记确切代理和现有 ALS 的实时 reader。`runPrepared` 在自身 `runReserved` 事务及任何 `inFlight` 访问之前读取:只有已登记代理当前链已有 ambient transaction,或调用方原本显式 `shareInFlightResult:false`,才绕过进程内结果的发布与加入。这样 outer follower 不加入已有 standalone flight,outer leader 也不向其它请求发布内部 callback 结果;后续仍使用既有数据库预占/回放。
这不是全局默认翻转。已登记但无事务和未登记 custom provider 分别返回 false/undefined,均保留默认共享;显式 true 不能覆盖已检测到的外层事务。standalone 自己打开事务后再在 work 内调用维护/仓储 transaction,不会被重新分类。检测按每个确切 proxy 和每条 async chain 隔离,不建立全局布尔值、深度或新 ALS,不向业务公开事务对象。
无 key、`runExternalPrepared`、owner/key 仲裁、operation/fingerprint、PROCESSING、策略/envelope、有效期与回放不续期均保持。root CREATE/PATCH、loan 与 Health 原有显式 false 及证据不动;未登记提供方仍需显式选项或另行验证能力。Review 的 `commitment-review-reference-v2` 保持不可变引用、owner-scoped 恢复与正文不落回执。PATCH 事务外预检窗口、应用自建共享 map、逃逸事务的未等待工作和 generic/seed 公共可达性不由本批证明。
候选测试拟通过真实 Nest provider factory、生产 ALS proxy、真实 Drizzle 和正常原型的惰性 `SyntheticPool` 验证:独立客户端、严格 SQL/参数/投影/rowMode、staged/committed 隔离、owner/key 等待和显式 COMMIT 门;不启动 Nest、真实 Pool、迁移或 pg-mem,不复制代理或手工登记测试上下文。真实 Commitment `changeStatus`、`createReview` 仅作为两条领域桥接。H0/有效 RED/GREEN 尚待实际记录;有限客户端在合并 staged 前拒绝 COMMIT,不是服务器已提交但确认丢失的证明。
真实 PostgreSQL MVCC/唯一索引等待/约束/维护排空、网络提交歧义、HTTP/认证、浏览器、Uni/native、Provider 与部署均未验收。设计、实施步骤和后续实际记录分别见 `docs/设计与计划/设计规格/2026-10-08-事务感知幂等共享保护设计.md`、`docs/设计与计划/实施计划/2026-10-08-事务感知幂等共享保护实施计划.md`、`docs/验证/2026-10-08-事务感知幂等共享保护证据.md`。
### 首次生产工厂有限模型实际续证
实际截止(UTC):2026-10-08T11:15:32.147868+00:00。上文 not_run 保留为初始编写时点。首次 baseline H0 因 helper 第 333 行 TS2345 导致 3 suites 编译失败、0 cases,exit 1;这不是产品 RED。仅两表判断改成等价字符串比较,经独立复核后重跑,生产、SQL 白名单、断言和超时未改变。
修订 B2 基线 H0 实际 33 pass、22 filtered;原宽 `RED ` 选择实际 7 fail、10 额外通过、27 filtered,原记录保留。随后同一字节以 `\bRED ` 精确重放 7 项目标 RED(5 core、2 domain),0 pass、37 filtered、0 runtime error;所有失败明确显示 COMMIT 前 `ok:true` 的公开成功 payload。过滤项不计验收 skip,宽/精确 RED 不重复计缺陷。
验证者随后采用已审阅三个平台文件与 direct-registry spec,helper/三份基线 spec 未改;候选完整 4 suites、58/58 pass,0 fail/skip,exit 0:core 28、domain 16、registry 3、fixture controls 11。H0 和宽 RED 额外通过不重复累加。B2 epoch SHA-256 为 `af853d49c31bc7032c8da5321deeb471060bdcd45c7104ce040737ec05120113`;G 为 `107fe2b3a04c8a77a95d89821fb576a32c172f4382f5075f1cdfff7197a31561`,修订后四次运行的源码/模式 guard 均 unchanged。
真实 factory/proxy/Drizzle 有限模型门已取得,真实 PostgreSQL/HTTP 与全部 inventory 业务验收仍未证明。相关回归、API types/build、离线/Node、最终六文档采用及其最终字节门禁在此截止尚待独立终态;发布/远端/CI/最终包不能由 focused GREEN 推断。精确命令、初始失败、修订 hash 与全部 record/report 索引见本批证据第七节;Catalog/partial 不提升。
### 最终文档采用前源码门禁终态
实际截止(UTC):2026-10-08T11:26:20.512334+00:00。新增 58 与相关 179 合计 15 suites/237 个不同 API cases,0 fail/skip;H0、重复 RED 与宽选择器额外通过不重复计数。API types、实际 Nest build 与必须的 asset finalizer、原十四项离线和 integrity 均 exit 0;原文档保留 45/45;Node domain 5 pass、general 820 pass/3 个原 Constitution 报告缺失 skip、0 fail。全部 22 个候选 G 门禁在同一 3782 路径源码上完成,source/mode guards unchanged。
Canonical `validation/code-validation-summary.json` SHA-256 为 `1b70749ceea0a4a4480dacbda384f3d276b4f7726bbd99145dc2c8c4615d3a82`,predoc metadata freeze 为 `cc06850b5bf4e2625c5aaa386a1139a3cdebfb8538b6f999a97a58685e363926`,绑定 26 条实际运行记录。最初 metadata/hash-string preflight 诊断未启动 Jest,原 helper TS2345 和宽/精确 RED 全部保留;仅前述一行 helper 修复,没有后续生产、断言、SQL 或超时修订。首次 H0 原记录只有 hash guard,后来关闭边界模式核对单独保留;修订后 B2/G 的前后 mode guards 不倒写成早期证据。
以上均为本六文档正式采用前的源码门禁;原文档 45/45 不代证新增字节。六文档复核/采用、采用后门禁、本批发布/远端/CI/最终包仍待独立记录。本后端批次未改前端源码、未重跑其全量/构建;真实 PostgreSQL/HTTP、浏览器/Uni/native、Provider 与部署仍 not_run。有限模型、未知提供方及全部 inventory 业务验收的限制不变,Catalog/partial 不提升。完整实际命令、hash 和历史说明见本批证据第八节。