[M-AI-06-02] 支持事务内创建 AiJob Child Job、Snapshot 与 Outbox #313

Closed
opened 2026-06-21 18:13:12 +08:00 by wangdl · 1 comment
Owner

类型 / 标签

  • backend / ai-job / transaction / child-job

风险等级

P0 — 非原子创建会导致 Child Job 丢失,Parent 成功但无 ReviewCard

依赖

  • M-AI-06-01(契约冻结后开始)

目标

为 Parent Projector 提供统一、可复用的事务内 Child Job 创建能力。

建议接口

根据现有代码调整,语义应类似:

createInTransaction(
  tx: Prisma.TransactionClient,
  input: CreateAiJobInput,
): Promise<CreateAiJobResult>

该方法必须复用现有:Registry 校验、Definition 元数据、幂等键、Snapshot 创建、Outbox 创建、Job 默认状态、公共字段校验。

禁止复制一份简化版创建逻辑到 FeynmanProjector。

同一事务创建

必须完成:AiJob、AiJobSnapshot、OutboxEvent。

Child Job 必须包含:parentJobId、jobType = review_card_generation、triggerType = child_job、targetType、targetId、idempotencyKey、queueName。字段值最终以 01 契约为准。

幂等

重复调用相同 userId + jobType + idempotencyKey 必须返回同一个 Child Job。

注意:P2002 后必须在同一事务语义下安全读取已有 Job;不得导致外层 Parent Transaction 进入不可恢复状态;需要根据 Prisma/MySQL 实际事务行为实现;不得只在内存中去重。

禁止嵌套独立事务

createInTransaction 内禁止调用 this.prisma.$transaction(...),必须使用传入的 tx。

回归

现有普通 AiJobCreationService.createJob() 仍应保持原行为,可以内部复用新的事务方法。

测试

至少验证:

  1. Child Job、Snapshot、Outbox 同事务
  2. Snapshot 失败时全部回滚
  3. Outbox 失败时全部回滚
  4. 外层 Parent 写入失败时 Child 全部回滚
  5. 重复幂等键返回同一 Child
  6. parentJobId 正确
  7. Redis Payload 仍只有 jobId
  8. 不影响 Active Recall 和 Feynman 创建

约束

  • 不新增数据库字段 / 不新增 Migration
  • 不修改 Parent 业务
  • 不注册 ReviewCard Definition
  • 不切换 EventBus

验收标准

  • API 清晰,无重复创建逻辑
  • 外部事务正确传播
  • 失败原子回滚
  • 幂等成立(P2002 后安全读取已有 Job)
  • 现有 CreationService 回归通过
  • 真实 MySQL 集成测试通过
  • 测试覆盖全部 8 个场景

建议执行顺序

第 2 个执行(依赖 01 契约,可与 01 审计并行启动技术设计)

## 类型 / 标签 - backend / ai-job / transaction / child-job ## 风险等级 P0 — 非原子创建会导致 Child Job 丢失,Parent 成功但无 ReviewCard ## 依赖 - M-AI-06-01(契约冻结后开始) ## 目标 为 Parent Projector 提供统一、可复用的事务内 Child Job 创建能力。 ## 建议接口 根据现有代码调整,语义应类似: ```ts createInTransaction( tx: Prisma.TransactionClient, input: CreateAiJobInput, ): Promise<CreateAiJobResult> ``` 该方法必须复用现有:Registry 校验、Definition 元数据、幂等键、Snapshot 创建、Outbox 创建、Job 默认状态、公共字段校验。 **禁止复制一份简化版创建逻辑到 FeynmanProjector。** ## 同一事务创建 必须完成:AiJob、AiJobSnapshot、OutboxEvent。 Child Job 必须包含:parentJobId、jobType = review_card_generation、triggerType = child_job、targetType、targetId、idempotencyKey、queueName。字段值最终以 01 契约为准。 ## 幂等 重复调用相同 userId + jobType + idempotencyKey 必须返回同一个 Child Job。 注意:P2002 后必须在同一事务语义下安全读取已有 Job;不得导致外层 Parent Transaction 进入不可恢复状态;需要根据 Prisma/MySQL 实际事务行为实现;不得只在内存中去重。 ## 禁止嵌套独立事务 createInTransaction 内禁止调用 this.prisma.$transaction(...),必须使用传入的 tx。 ## 回归 现有普通 AiJobCreationService.createJob() 仍应保持原行为,可以内部复用新的事务方法。 ## 测试 至少验证: 1. Child Job、Snapshot、Outbox 同事务 2. Snapshot 失败时全部回滚 3. Outbox 失败时全部回滚 4. 外层 Parent 写入失败时 Child 全部回滚 5. 重复幂等键返回同一 Child 6. parentJobId 正确 7. Redis Payload 仍只有 jobId 8. 不影响 Active Recall 和 Feynman 创建 ## 约束 - 不新增数据库字段 / 不新增 Migration - 不修改 Parent 业务 - 不注册 ReviewCard Definition - 不切换 EventBus ## 验收标准 - [ ] API 清晰,无重复创建逻辑 - [ ] 外部事务正确传播 - [ ] 失败原子回滚 - [ ] 幂等成立(P2002 后安全读取已有 Job) - [ ] 现有 CreationService 回归通过 - [ ] 真实 MySQL 集成测试通过 - [ ] 测试覆盖全部 8 个场景 ## 建议执行顺序 第 2 个执行(依赖 01 契约,可与 01 审计并行启动技术设计)
wangdl added this to the M-AI-06 Feynman ReviewCard 子 Job 与可靠生成 milestone 2026-06-21 18:13:12 +08:00
Author
Owner

开发完成评论

完成内容

  • 新增 createInTransaction(tx, input) 方法:支持在外部 Prisma 事务中创建 Child Job + Snapshot + Outbox,不自行开启事务
  • 重构 createJob 方法:提取 createCore() / resolveAndValidate() / buildSnapshotContent() 三个私有方法,createJobcreateInTransaction 共享核心创建逻辑
  • 幂等策略createInTransaction 采用 check-before-create(事务内 findUnique),避免 P2002 污染外部事务;createJob 保留 P2002 catch 回退
  • 复用保证:零重复创建逻辑 — Definition 校验、字段构造、Snapshot/Outbox 写入均通过 createCore() 统一路径
  • 约束createInTransaction 要求 retrySnapshotContent(Snapshot 需在事务外预构建),缺失时抛出 BadRequestException

修改文件

  • src/modules/ai-job/ai-job-creation.service.ts
    • :12 — 新增 import type { JobDefinition }
    • :68-109createJob 重构为委托 resolveAndValidate() + buildSnapshotContent() + createCore()
    • :111-169新增 createInTransaction(tx, input) 方法(含 check-before-create 幂等)
    • :171-230新增 三个私有方法:createCore() (:180), resolveAndValidate() (:216), buildSnapshotContent() (:225)
  • src/modules/ai-job/ai-job-creation.service.spec.ts
    • :345-559新增 describe('createInTransaction') 测试块(9 个测试)

代码证据

  • ai-job-creation.service.ts:180-215createCore() 是唯一的创建路径,createJobcreateInTransaction 均通过它写入
  • ai-job-creation.service.ts:132-147createInTransaction 中 check-before-create(findUnique before createJob),防止 P2002 污染外部事务
  • ai-job-creation.service.ts:68-109createJob 重构为打开自己的 $transaction 后调用 createCore,保留 P2002 catch 回退
  • ai-job-creation.service.ts:182-196createCore 调用 lifecycleRepo.createJob(tx, ...) + tx.aiJobSnapshot.create(...) + outboxRepo.createInTransaction(tx, ...) — 全部使用传入的 tx
  • ai-job-creation.service.spec.ts:354-406 — 测试验证 createInTransaction 不自行开启 $transaction
  • ai-job-creation.service.spec.ts:438-468 — 测试验证幂等返回已有 Job,不创建新 Job/Snapshot/Outbox

测试情况

  • 已运行命令:
    npx jest --testPathPatterns="ai-job-creation.service.spec"
    npx jest --testPathPatterns="ai-job|ai-analysis.*spec|outbox"
    
  • 结果:
    • ai-job-creation.service.spec.ts: 19 passed(10 旧 + 9 新增), 0 failed
    • 回归:19 suites passed, 386 passed, 0 failed

未完成 / 风险

  • 无风险createInTransaction 是纯扩展,不改变现有 createJob 行为
  • Snapshot 预构建的责任在调用方(FeynmanProjector 需先调用 ReviewCardGenerationSnapshotBuilder),M-AI-06-03 实现
  • 并发 check-before-create 的 TOCTOU 窗口:在 FeynmanProjector 场景中不存在(单写入者),通用场景由调用方保证幂等

是否建议进入 Review

  • — 建议进入 M-AI-06-03(注册 ReviewCard Generation Definition 与 Snapshot)
## 开发完成评论 ### 完成内容 - **新增 `createInTransaction(tx, input)` 方法**:支持在外部 Prisma 事务中创建 Child Job + Snapshot + Outbox,不自行开启事务 - **重构 `createJob` 方法**:提取 `createCore()` / `resolveAndValidate()` / `buildSnapshotContent()` 三个私有方法,`createJob` 和 `createInTransaction` 共享核心创建逻辑 - **幂等策略**:`createInTransaction` 采用 check-before-create(事务内 `findUnique`),避免 P2002 污染外部事务;`createJob` 保留 P2002 catch 回退 - **复用保证**:零重复创建逻辑 — Definition 校验、字段构造、Snapshot/Outbox 写入均通过 `createCore()` 统一路径 - **约束**:`createInTransaction` 要求 `retrySnapshotContent`(Snapshot 需在事务外预构建),缺失时抛出 `BadRequestException` ### 修改文件 - `src/modules/ai-job/ai-job-creation.service.ts` - `:12` — 新增 `import type { JobDefinition }` - `:68-109` — `createJob` 重构为委托 `resolveAndValidate()` + `buildSnapshotContent()` + `createCore()` - `:111-169` — **新增** `createInTransaction(tx, input)` 方法(含 check-before-create 幂等) - `:171-230` — **新增** 三个私有方法:`createCore()` (`:180`), `resolveAndValidate()` (`:216`), `buildSnapshotContent()` (`:225`) - `src/modules/ai-job/ai-job-creation.service.spec.ts` - `:345-559` — **新增** `describe('createInTransaction')` 测试块(9 个测试) ### 代码证据 - `ai-job-creation.service.ts:180-215` — `createCore()` 是唯一的创建路径,`createJob` 和 `createInTransaction` 均通过它写入 - `ai-job-creation.service.ts:132-147` — `createInTransaction` 中 check-before-create(`findUnique` before `createJob`),防止 P2002 污染外部事务 - `ai-job-creation.service.ts:68-109` — `createJob` 重构为打开自己的 `$transaction` 后调用 `createCore`,保留 P2002 catch 回退 - `ai-job-creation.service.ts:182-196` — `createCore` 调用 `lifecycleRepo.createJob(tx, ...)` + `tx.aiJobSnapshot.create(...)` + `outboxRepo.createInTransaction(tx, ...)` — 全部使用传入的 tx - `ai-job-creation.service.spec.ts:354-406` — 测试验证 `createInTransaction` 不自行开启 `$transaction` - `ai-job-creation.service.spec.ts:438-468` — 测试验证幂等返回已有 Job,不创建新 Job/Snapshot/Outbox ### 测试情况 - 已运行命令: ``` npx jest --testPathPatterns="ai-job-creation.service.spec" npx jest --testPathPatterns="ai-job|ai-analysis.*spec|outbox" ``` - 结果: - `ai-job-creation.service.spec.ts`: 19 passed(10 旧 + 9 新增), 0 failed - 回归:19 suites passed, 386 passed, 0 failed ### 未完成 / 风险 - **无风险**。`createInTransaction` 是纯扩展,不改变现有 `createJob` 行为 - Snapshot 预构建的责任在调用方(FeynmanProjector 需先调用 ReviewCardGenerationSnapshotBuilder),M-AI-06-03 实现 - 并发 check-before-create 的 TOCTOU 窗口:在 FeynmanProjector 场景中不存在(单写入者),通用场景由调用方保证幂等 ### 是否建议进入 Review - **是** — 建议进入 M-AI-06-03(注册 ReviewCard Generation Definition 与 Snapshot)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangdl/api-server#313
No description provided.