From 4c2b8d02d7cbccc69e44ae35d4a5d9d8e3657405 Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 20 Jun 2026 12:40:26 +0800 Subject: [PATCH] docs(M-AI-02-GATE): update ADR-002 lifecycle spec + @map strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lifecycleStatus enum: created|queued|running|retrying|succeeded|failed|cancel_requested|cancelled - Legacy mapping: pending→queued, processing→running, completed→succeeded, failed→failed - @map approach documented for physical column compatibility Co-Authored-By: Claude --- .../adr-002-ai-job-database-expand.md | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/architecture/adr-002-ai-job-database-expand.md b/docs/architecture/adr-002-ai-job-database-expand.md index bdae5e5..2e3a3b7 100644 --- a/docs/architecture/adr-002-ai-job-database-expand.md +++ b/docs/architecture/adr-002-ai-job-database-expand.md @@ -31,6 +31,7 @@ M-AI-02-01 审计确认: | AiRuntimeJob 表 | **不删除、不修改、不迁移** | 0 行生产数据,M-AI-08 退场时一并清理 | | AiRuntimeResult 表 | **不修改** | M-AI-03 统一 Job Engine 后用新的 Result 模型 | | AiAnalysisResult 表 | **不修改** | 保留旧系统 A 的结果存储,M-AI-04 迁移后评估删除 | +| 列重命名策略 | **Prisma 逻辑名用新名,物理列用 `@map` 保持旧名** | `internalErrorMessage @map("errorMessage")`、`finishedAt @map("completedAt")`。旧版本应用通过物理列名兼容,新代码通过逻辑名访问 | ### 2. 统一 AiJob Schema @@ -188,30 +189,42 @@ model AiJob { | `sessionId` | `sessionId` | VARCHAR(191) | ✅ | - | 100% (9/9) | 保留但废弃(旧系统 A),M-AI-05 删除 | | `answerId` | `answerId` | VARCHAR(191) | ✅ | - | 78% (7/9) | 保留但废弃(旧系统 A),M-AI-05 删除 | -### 3. 状态映射表 +### 3. 统一生命周期状态 -两套旧状态统一为单一 `lifecycleStatus`: +新 lifecycleStatus 枚举(8 个值): -| 旧 AiAnalysisJob.status | 旧 AiRuntimeJob.status | 新 lifecycleStatus | 含义 | 进入条件 | 退出条件 | -|------------------------|----------------------|-------------------|------|---------|---------| -| `pending` | `pending` | **`pending`** | 等待消费 | API 创建 / retryable fail 回退 | 被 Runtime lock / BullMQ claim | -| - | `locked` | **`locked`** | 已被 Runtime 获取,等待执行确认 | Runtime POST /lock 成功 | heartbeat → running / lockUntil 超时 | -| `processing` | `running` | **`running`** | 正在执行 | heartbeat 确认(新)/ Worker process() 开始(旧) | 完成 → succeeded / 失败 → failed | -| `completed` | `succeeded` | **`succeeded`** | 执行成功 | API POST /result | 终态 | -| `failed` | `failed` | **`failed`** | 执行失败 | 不可重试错误 / 超过 maxAttempts | 终态(Admin 可重跑) | -| - | `cancelled` | **`cancelled`** | 已取消 | 用户/Admin 取消 pending job / cancelRequested 后 Runtime 确认 | 终态 | -| - | `expired` | **取消**(合并入 retry 逻辑) | - | lockUntil 超时 → back to `pending`(Reaper 处理) | - | +| 状态 | 含义 | +|------|------| +| `created` | Job 记录已创建,尚未入队 | +| `queued` | 已入队,等待 Worker 消费 | +| `running` | Worker 正在执行 | +| `retrying` | 执行失败,等待重试 | +| `succeeded` | 执行成功(终态) | +| `failed` | 执行失败,超过最大重试(终态) | +| `cancel_requested` | 用户/Admin 请求取消,等待 Worker 确认 | +| `cancelled` | 已取消(终态) | -> **为什么 `processing` → `running` 而非 `locked`**:旧系统的 `processing` 表示 Worker 正在执行(`ai-analysis.worker.ts:48`),语义等同于新系统 heartbeat 确认后的 `running`。新系统的 `locked` 是「已被 claim 但尚未确认执行开始」的中间态,在旧系统中由 BullMQ 内部管理,无显式状态对应。 -> -> **Reaper 安全影响**:若映射为 `locked`,JobReaperService 会将旧 Worker 正在执行的任务视为「无 heartbeat 的 locked job」而在 `lockUntil` 过期后错误清扫回 `pending`,导致重复执行。映射为 `running` 可避免此问题(Reaper 对 `running` 的清扫阈值远长于 `locked`)。 +**Legacy → 统一映射**(Shadow Write + 回填): + +| 旧 AiAnalysisJob.status | 旧 AiRuntimeJob.status | 新 lifecycleStatus | +|------------------------|----------------------|-------------------| +| `pending` | `pending` | **`queued`** | +| `processing` | `running` | **`running`** | +| `completed` | `succeeded` | **`succeeded`** | +| `failed` | `failed` | **`failed`** | +| - | `locked` | N/A(新系统内部状态,旧系统无对应) | +| - | `cancelled` | **`cancelled`** | + +> **为什么 `pending` → `queued`**:旧系统的 `pending` 表示「已创建且在 BullMQ 队列中等待」,语义更接近新系统的 `queued`。新系统的 `created` 预留给未来「创建记录」和「入队」分离的场景。 + +> **`lifecycleStatus` 过渡期 nullable**:旧代码写入时通过 Shadow Write 同步填充;M-AI-02-10 回填历史 NULL;M-AI-05 切换完成后改为 NOT NULL。 **迁移回填 SQL**(旧 `status` → 新 `lifecycleStatus`): ```sql UPDATE `AiAnalysisJob` SET `lifecycleStatus` = CASE `status` - WHEN 'pending' THEN 'pending' + WHEN 'pending' THEN 'queued' WHEN 'processing' THEN 'running' WHEN 'completed' THEN 'succeeded' WHEN 'failed' THEN 'failed'