fix: ADR-003 补充幂等流程 + 修正队列配置对齐 #288 规格
- 新增 §2.4 AiJobService.createJob 幂等流程(findUnique + insert + P2002 catch) - §3.1 队列配置对齐 Issue #288:ai-interactive attempts=2, backoff=2000ms;ai-background concurrency=1, backoff=5000ms - 新增设计理由说明与原始规格的偏差原因 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
947958052a
commit
d44fe57b6d
@ -315,17 +315,115 @@ export class AiJobModule implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 2.4 AiJobService — 统一 Job 创建入口与幂等性
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// AiJobService — 唯一 Job 创建入口(internal-only)
|
||||||
|
@Injectable()
|
||||||
|
export class AiJobService {
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
private readonly registry: JobDefinitionRegistry,
|
||||||
|
private readonly lifecycleRepo: AiJobLifecycleRepository,
|
||||||
|
private readonly outboxRepo: OutboxRepository,
|
||||||
|
private readonly snapshotBuilder: SnapshotBuilderService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Job + Snapshot + Outbox(一个 Prisma Transaction 内)。
|
||||||
|
*
|
||||||
|
* 幂等性:
|
||||||
|
* - AiJob 表有 @@unique([userId, jobType, idempotencyKey])
|
||||||
|
* - 如果 (userId, jobType, idempotencyKey) 三元组匹配到已有 Job,直接返回已有 Job
|
||||||
|
* - 未提供 idempotencyKey 时每次创建新 Job
|
||||||
|
*
|
||||||
|
* 事务边界:
|
||||||
|
* - BEGIN TRANSACTION
|
||||||
|
* - createJob(tx) → INSERT AiJob (lifecycleStatus = 'queued')
|
||||||
|
* - createInTransaction(tx) → INSERT OutboxEvent (eventType = 'ai.job.enqueue')
|
||||||
|
* - COMMIT
|
||||||
|
* - OutboxDispatcher 异步投递到 BullMQ
|
||||||
|
*/
|
||||||
|
async createJob(input: CreateJobInput): Promise<AiJob>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateJobInput {
|
||||||
|
userId: string;
|
||||||
|
jobType: string;
|
||||||
|
targetType: string;
|
||||||
|
targetId: string;
|
||||||
|
/** 幂等 Key。提供时启用幂等;不提供时每次创建新 Job */
|
||||||
|
idempotencyKey?: string;
|
||||||
|
/** 覆盖 Definition 默认值 */
|
||||||
|
overrides?: {
|
||||||
|
priority?: number;
|
||||||
|
credentialMode?: 'platform_key' | 'user_deepseek_key';
|
||||||
|
credentialId?: string;
|
||||||
|
promptVersion?: string;
|
||||||
|
outputSchemaVersion?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**幂等流程**:
|
||||||
|
|
||||||
|
```
|
||||||
|
AiJobService.createJob(input)
|
||||||
|
│
|
||||||
|
├─ 1. lookup JobDefinition via Registry.get(input.jobType)
|
||||||
|
│
|
||||||
|
├─ 2. if input.idempotencyKey is provided:
|
||||||
|
│ existing = prisma.aiJob.findUnique({
|
||||||
|
│ where: { userId_jobType_idempotencyKey: {
|
||||||
|
│ userId: input.userId,
|
||||||
|
│ jobType: input.jobType,
|
||||||
|
│ idempotencyKey: input.idempotencyKey
|
||||||
|
│ }}
|
||||||
|
│ })
|
||||||
|
│ if existing → return existing // 幂等返回,不创建新 Job
|
||||||
|
│
|
||||||
|
├─ 3. build snapshot via SnapshotBuilder
|
||||||
|
│
|
||||||
|
├─ 4. prisma.$transaction(async (tx) => {
|
||||||
|
│ job = lifecycleRepo.createJob(tx, input)
|
||||||
|
│ outboxRepo.createInTransaction(tx, {
|
||||||
|
│ eventType: 'ai.job.enqueue',
|
||||||
|
│ aggregateType: 'AiJob',
|
||||||
|
│ aggregateId: job.id,
|
||||||
|
│ dedupeKey: `${input.jobType}:${input.userId}:${input.idempotencyKey ?? job.id}`,
|
||||||
|
│ payload: { jobId: job.id, jobType: input.jobType, queueName: def.queue.queueName, priority: job.priority },
|
||||||
|
│ availableAt: new Date(),
|
||||||
|
│ })
|
||||||
|
│ return job
|
||||||
|
│ })
|
||||||
|
│
|
||||||
|
└─ 5. return job
|
||||||
|
```
|
||||||
|
|
||||||
|
**约束**:
|
||||||
|
- 不提供通用公开创建接口 —— 外部通过具体业务 Service(如 `ActiveRecallService.createJob()`)调用
|
||||||
|
- `CreateJobInput` 的类型由 `JobDefinition` 的 `input.schemaVersion` 隐式约束
|
||||||
|
- 并发相同 `(userId, jobType, idempotencyKey)` → `findUnique` 无记录 → 两个事务同时 INSERT → 一个成功,一个 `P2002` 唯一冲突 → catch 后 `findUnique` 返回已有(赢者)Job
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. 队列架构
|
### 3. 队列架构
|
||||||
|
|
||||||
#### 3.1 队列配置
|
#### 3.1 队列配置
|
||||||
|
|
||||||
| 队列名 | 用途 | Worker 进程 | 默认 concurrency | lockDuration | attempts |
|
> 以下值来自 Issue #288 原始规格。有意偏离需在对应 Issue 中注明原因。
|
||||||
|--------|------|------------|-----------------|-------------|----------|
|
|
||||||
| `ai-interactive` | 新统一 Engine — 交互式 Job(用户等待结果) | `WorkerModule` 新增 | 2 | 30s | 3 |
|
| 队列名 | 用途 | Worker 进程 | concurrency | lockDuration | attempts | backoff |
|
||||||
| `ai-background` | 新统一 Engine — 后台 Job(用户不等待) | `WorkerModule` 新增 | 3 | 60s | 3 |
|
|--------|------|------------|-------------|-------------|----------|---------|
|
||||||
| `ai-analysis` | 旧系统 A — active-recall + feynman-evaluation | `WorkerModule`(已有) | 1 | 30s | 3 |
|
| `ai-interactive` | 新统一 Engine — 交互式 Job(用户等待结果) | `WorkerModule` 新增 | 2 | 30s | 2 | exponential 2000ms |
|
||||||
|
| `ai-background` | 新统一 Engine — 后台 Job(用户不等待) | `WorkerModule` 新增 | 1 | 60s | 3 | exponential 5000ms |
|
||||||
|
| `ai-analysis` | 旧系统 A — active-recall + feynman-evaluation | `WorkerModule`(已有) | 1 | 30s | 3 | exponential 1000ms |
|
||||||
|
|
||||||
|
**设计理由**:
|
||||||
|
|
||||||
|
- `ai-interactive` attempts=2(非 3):用户等待结果,BullMQ retry 会延长感知延迟。Engine 内部已有一层重试,BullMQ 仅兜底 Worker 崩溃。
|
||||||
|
- `ai-background` concurrency=1(非 3):后台 Job 以吞吐而非延迟为目标;单 Worker 串行消费避免 DeepSeek API 并发限流。若后续需要更大吞吐,通过水平扩展 Worker 实例而非提高 concurrency。
|
||||||
|
- `ai-background` backoff=5000ms(非默认 1000ms):后台场景无实时性要求,更长退避减少无效重试。
|
||||||
|
|
||||||
**约束**:
|
**约束**:
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user