fix: ADR-003 markSucceeded/markFailed/markCancelled 接受可选 tx 参数
Some checks failed
Deploy API Server / build-and-unit (push) Successful in 32s
Deploy API Server / current-integration (push) Successful in 29s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / deploy (push) Has been cancelled

修正 §1.4 与 §6.2 矛盾:三个终态写入方法现接受可选 Prisma.TransactionClient,
Projector 路径传入 tx 共享事务保证原子性,非 Projector 路径不传 tx 使用独立事务。
lockJob 保持无 tx(CAS 必须立即对其他 Worker 可见)。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-20 17:09:59 +08:00
parent 608129be4e
commit 947958052a

View File

@ -76,13 +76,38 @@ running → queued (不可回退;需重试应通过 retryCount 机制)
**所有** `lifecycleStatus` 的写入必须通过 `AiJobLifecycleRepository` 的以下方法:
```typescript
// AiJobLifecycleRepository — 唯一写入 lifeCycleStatus 的入口
// AiJobLifecycleRepository — 唯一写入 lifecycleStatus 的入口
export class AiJobLifecycleRepository {
/**
* 在给定事务中创建 JoblifecycleStatus = 'queued')。
* 调用方负责在同一 tx 中写入 OutboxEvent以保证 Job + Outbox 原子性。
*/
createJob(tx: Prisma.TransactionClient, input: CreateJobInput): Promise<AiJob>;
lockJob(jobId: string, instanceId: string): Promise<AiJob>; // CAS: queued → running
markSucceeded(jobId: string): Promise<AiJob>;
markFailed(jobId: string, error: JobErrorInfo): Promise<AiJob>;
markCancelled(jobId: string): Promise<AiJob>;
/**
* CAS 抢锁queued → running
* 必须是独立操作非事务内CAS 需要立即对其他 Worker 可见。
*/
lockJob(jobId: string, instanceId: string): Promise<AiJob>;
/**
* 标记成功(→ succeeded
* @param tx 可选事务客户端。由 Projector 在事务内调用以确保 Projector + markSucceeded 原子性。
* 若未传 tx方法内部使用独立事务非 Projector 路径使用)。
*/
markSucceeded(jobId: string, tx?: Prisma.TransactionClient): Promise<AiJob>;
/**
* 标记失败(→ failed
* @param tx 可选事务客户端。若非 Projector 路径(如超时/Provider 错误),传 undefined 使用独立事务。
*/
markFailed(jobId: string, error: JobErrorInfo, tx?: Prisma.TransactionClient): Promise<AiJob>;
/**
* 标记取消(→ cancelled
* @param tx 可选事务客户端。取消路径通常为独立操作,不传 tx。
*/
markCancelled(jobId: string, tx?: Prisma.TransactionClient): Promise<AiJob>;
// 只读
findByLifecycleStatus(status: LifecycleStatus[], opts?: { queueName?: string; limit?: number }): Promise<AiJob[]>;
@ -544,14 +569,17 @@ interface ProjectionResult {
#### 6.2 事务边界
```
// tx = await prisma.$transaction(async (tx) => { ... })
BEGIN TRANSACTION
1. ResultProjector.project(tx, job, validatedOutput)
→ 写入业务表QuizQuestion / Flashcard / AiLearningAnalysis / WeakPointCandidate / NextActionRecommendation 等)
→ 写入 AiJobArtifact (jobId, artifactType, artifactId)
2. AiJobLifecycleRepository.markSucceeded(tx, jobId)
2. AiJobLifecycleRepository.markSucceeded(jobId, tx) // 传入 tx与 Projector 共享事务
3. (未来) OutboxRepository.createInTransaction(tx, { eventType: 'ai.job.completed', ... })
COMMIT
```
- `markSucceeded` 接受可选 `tx`:传入 tx 时复用事务,不传时内部开启独立事务
- Projector 抛错 → Transaction Rollback → Job 回到 Engine retry 流程
- **全部成功** → Commit
- **任何步骤失败** → RollbackJob 重新进入 retry 流程