fix: ADR-003 markSucceeded/markFailed/markCancelled 接受可选 tx 参数
修正 §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:
parent
608129be4e
commit
947958052a
@ -76,13 +76,38 @@ running → queued (不可回退;需重试应通过 retryCount 机制)
|
|||||||
**所有** `lifecycleStatus` 的写入必须通过 `AiJobLifecycleRepository` 的以下方法:
|
**所有** `lifecycleStatus` 的写入必须通过 `AiJobLifecycleRepository` 的以下方法:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// AiJobLifecycleRepository — 唯一写入 lifeCycleStatus 的入口
|
// AiJobLifecycleRepository — 唯一写入 lifecycleStatus 的入口
|
||||||
export class AiJobLifecycleRepository {
|
export class AiJobLifecycleRepository {
|
||||||
|
/**
|
||||||
|
* 在给定事务中创建 Job(lifecycleStatus = 'queued')。
|
||||||
|
* 调用方负责在同一 tx 中写入 OutboxEvent,以保证 Job + Outbox 原子性。
|
||||||
|
*/
|
||||||
createJob(tx: Prisma.TransactionClient, input: CreateJobInput): Promise<AiJob>;
|
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>;
|
* CAS 抢锁(queued → running)。
|
||||||
markCancelled(jobId: string): Promise<AiJob>;
|
* 必须是独立操作(非事务内):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[]>;
|
findByLifecycleStatus(status: LifecycleStatus[], opts?: { queueName?: string; limit?: number }): Promise<AiJob[]>;
|
||||||
@ -544,14 +569,17 @@ interface ProjectionResult {
|
|||||||
#### 6.2 事务边界
|
#### 6.2 事务边界
|
||||||
|
|
||||||
```
|
```
|
||||||
|
// tx = await prisma.$transaction(async (tx) => { ... })
|
||||||
BEGIN TRANSACTION
|
BEGIN TRANSACTION
|
||||||
1. ResultProjector.project(tx, job, validatedOutput)
|
1. ResultProjector.project(tx, job, validatedOutput)
|
||||||
→ 写入业务表(QuizQuestion / Flashcard / AiLearningAnalysis / WeakPointCandidate / NextActionRecommendation 等)
|
→ 写入业务表(QuizQuestion / Flashcard / AiLearningAnalysis / WeakPointCandidate / NextActionRecommendation 等)
|
||||||
→ 写入 AiJobArtifact (jobId, artifactType, artifactId)
|
→ 写入 AiJobArtifact (jobId, artifactType, artifactId)
|
||||||
2. AiJobLifecycleRepository.markSucceeded(tx, jobId)
|
2. AiJobLifecycleRepository.markSucceeded(jobId, tx) // 传入 tx,与 Projector 共享事务
|
||||||
3. (未来) OutboxRepository.createInTransaction(tx, { eventType: 'ai.job.completed', ... })
|
3. (未来) OutboxRepository.createInTransaction(tx, { eventType: 'ai.job.completed', ... })
|
||||||
COMMIT
|
COMMIT
|
||||||
```
|
```
|
||||||
|
- `markSucceeded` 接受可选 `tx`:传入 tx 时复用事务,不传时内部开启独立事务
|
||||||
|
- Projector 抛错 → Transaction Rollback → Job 回到 Engine retry 流程
|
||||||
|
|
||||||
- **全部成功** → Commit
|
- **全部成功** → Commit
|
||||||
- **任何步骤失败** → Rollback,Job 重新进入 retry 流程
|
- **任何步骤失败** → Rollback,Job 重新进入 retry 流程
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user