fix: ADR-003 cancel_requested 信号语义 + BullMQ retry 范围 + CreateJobInput 补全
All checks were successful
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 1s
Deploy API Server / deploy (push) Successful in 1m2s

- §1.1.1: 明确 cancel_requested 不是独立生命周期状态,是 running 上的信号字段
- §5.5: Engine 内部 retry(业务级)vs BullMQ retry(进程崩溃)的分工表
- §2.4: CreateJobInput 对齐 #289 规格(新增 triggerType/parentJobId)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-20 17:12:35 +08:00
parent 88b35d0d18
commit 8b6c7cef72

View File

@ -48,6 +48,31 @@ M-AI-03 的目标是在 M-AI-02 Schema Expand 之上,纯代码层实现统一
| `failed` | 执行失败且重试耗尽 | 是 | `AiJobLifecycleRepository.markFailed()` | | `failed` | 执行失败且重试耗尽 | 是 | `AiJobLifecycleRepository.markFailed()` |
| `cancelled` | 用户/系统取消 | 是 | `AiJobLifecycleRepository.markCancelled()` | | `cancelled` | 用户/系统取消 | 是 | `AiJobLifecycleRepository.markCancelled()` |
#### 1.1.1 `cancel_requested` 不是独立状态
`cancel_requested` **不是** `lifecycleStatus` 枚举值。它是 `AiJob.cancelRequestedAt` 字段,作为 `running` 状态上的**取消信号**而非状态迁移目标。
流程:
```
用户调用 cancel API
├─ lifecycleStatus = 'queued'
│ → markCancelled(jobId) → cancelled直接终态
└─ lifecycleStatus = 'running'
→ 设置 cancelRequestedAt = now()lifecycleStatus 保持 running
→ Worker Heartbeat / Engine 执行间隙检测到 cancelRequestedAt != null
→ 抛出 JobCancelledError
→ Engine catch → markCancelled(jobId) → cancelled
```
**设计理由**
- 不引入中间状态 `cancel_requested` 避免状态机膨胀5 状态 + 1 信号 vs 6 状态)
- `cancelRequestedAt``running` 上的 boolean flag语义更轻量
- Engine 在每次 Provider 调用前后检查此字段,实现协作式取消
- §1.5 的 `STATUS_TO_LIFECYCLE``cancel_requested` 仅用于过渡期 Shadow Writelegacy `status` 列可能有此值,映射到新 `lifecycleStatus = 'cancelled'`
#### 1.2 允许迁移 #### 1.2 允许迁移
``` ```
@ -367,11 +392,20 @@ export class AiJobService {
async createJob(input: CreateJobInput): Promise<AiJob>; async createJob(input: CreateJobInput): Promise<AiJob>;
} }
// 对齐 Issue #289:事务化 AiJob 创建、Snapshot 与 Enqueue Outbox
interface CreateJobInput { interface CreateJobInput {
/** 用户 ID */
userId: string; userId: string;
/** Job 类型(对应 JobDefinition.jobType */
jobType: string; jobType: string;
/** 触发方式 */
triggerType: 'user_api' | 'admin_manual' | 'system_scheduled' | 'parent_job';
/** 分析目标类型 */
targetType: string; targetType: string;
/** 分析目标 ID */
targetId: string; targetId: string;
/** 父 Job IDJob 链场景) */
parentJobId?: string;
/** 幂等 Key。提供时启用幂等不提供时每次创建新 Job */ /** 幂等 Key。提供时启用幂等不提供时每次创建新 Job */
idempotencyKey?: string; idempotencyKey?: string;
/** 覆盖 Definition 默认值 */ /** 覆盖 Definition 默认值 */
@ -638,7 +672,16 @@ try {
| Execution Engine | 内部重试循环 | `JobDefinition.execution.maxRetries` | | Execution Engine | 内部重试循环 | `JobDefinition.execution.maxRetries` |
| Outbox | attemptCount < 3 pending>= 3 → dead | OutboxRepository | | Outbox | attemptCount < 3 pending>= 3 → dead | OutboxRepository |
**约束**BullMQ retry 与 Engine 内部 retry 不叠加。Engine 内部错误不抛出(不触发 BullMQ retry而是在 Engine 内完成所有重试后,一次性 `markFailed``markSucceeded` **约束**BullMQ retry 与 Engine 内部 retry 分工明确,不叠加。
| 层级 | 覆盖范围 | 触发条件 |
|------|---------|---------|
| **Engine 内部 retry** | 业务级错误Provider 超时/5xx、输出校验失败 | `JobDefinition.execution.maxRetries` |
| **BullMQ retry** | 进程级故障SIGKILL、OOM、Worker 崩溃、Redis 断连) | §3.1 `attempts` |
Engine 内部完成所有业务重试后,一次性 `markFailed``markSucceeded`。Engine 内部错误**不向 BullMQ 抛出**——BullMQ 仅看到 Worker 进程退出SIGKILL/OOM或未响应stalled此时触发 BullMQ retryWorker 重启后从 `markFailed``markSucceeded` 的幂等语义恢复。
**这解释了 §3.1 中 `ai-interactive` attempts=2 的设计**:交互式场景只需要 2 次机会覆盖进程崩溃;业务重试已在 Engine 内部闭环。
#### 5.6 ValidatedOutput 与 OutputHash #### 5.6 ValidatedOutput 与 OutputHash