# 执行计划：修复执行记录双条（僵尸 running 记录）

> 生成时间：2026-08-24
> 关联文档：`_问题处理_执行记录双条僵尸running记录.md`
> 状态：✅ 已完成并验证

---

## 问题概述

`POST /api/performance/tasks/{task_id}/run` 触发执行后，同一任务同一时刻产生**两条**执行记录：API 返回的 `executionId` 永远卡在 `running`/0 请求（僵尸），实际执行数据落在另一条 `completed` 记录上。前端按返回的 `executionId` 跳转监控页 → 无数据。

**根因**：`run_task_async()` 创建执行记录 A 并返回其 id；后台 `_run_single_background()` 调用 `run_task()` 时**未传递该 id**，`run_task()` 内部又硬性 `generate_id("exec")` 创建了记录 B。A 无人更新，永远 running。

---

## 执行步骤

### Step 1: `run_task()` 支持复用已有 execution_id

**文件**：`backend/app/services/performance_service.py`

**改动**：`run_task()`（约 285 行）签名新增可选参数，创建记录处按传入值分支：

```python
async def run_task(
    self,
    task_id: str,
    config_override: Optional[Dict[str, Any]] = None,
    progress_callback: Optional[Callable] = None,
    triggered_by: str = "manual",
    batch_id: Optional[str] = None,
    execution_id: Optional[str] = None,   # ← 新增：复用已有执行记录
) -> Dict[str, Any]:
```

创建执行记录处：

```python
if not execution_id:
    execution_id = generate_id("exec")
    execution = PerformanceExecution(
        id=execution_id,
        ...
        status="running",
        ...
    )
    self.db.add(execution)
    await self.db.flush()
else:
    # 复用 run_task_async 已创建的记录（刷新状态与起始时间）
    execution = await self.db.get(PerformanceExecution, execution_id)
    if not execution:
        raise ValueError(f"执行记录不存在: {execution_id}")
    execution.status = "running"
    execution.start_time = datetime.utcnow()
    await self.db.flush()
```

**验收标准**：
- 不传 `execution_id` 的既有调用（batch_run_tasks / 单测）行为不变
- 传入已存在 id 时不产生新记录

### Step 2: `_run_single_background()` 传递 execution_id

**文件**：`backend/app/services/performance_service.py`（约 1080 行）

```python
await service.run_task(
    task_id=task_id,
    config_override=config_override,
    triggered_by=triggered_by,
    execution_id=execution_id,   # ← 新增
)
```

**验收标准**：`run_task_async` → 后台 `run_task` 全链路只存在一条执行记录

### Step 3: 数据清理（5.60 存量僵尸记录）

修复只对新执行生效，**存量僵尸 running 记录需 SQL 清理**：

```sql
-- 先查：任务本身已不是 running，但执行记录还是 running 的僵尸
SELECT e.id, e.task_id, e.start_time FROM performance_executions e
JOIN performance_tasks t ON t.id = e.task_id
WHERE e.status = 'running' AND t.status != 'running';

-- 确认后删除（或标记 failed）
DELETE FROM performance_executions WHERE id = 'exec_4e4eb234101b42a4a216df44f6705ba8';
```

**验收标准**：`GET /executions?task_id=perf_0fbcb7...` 仅剩 completed 记录

### Step 4: 部署 + 端到端验证

```bash
scp backend/app/services/performance_service.py ubains@192.168.5.60:/data/third_party/plat-auto-test/backend/app/services/
ssh ubains@192.168.5.60 "cd /data/third_party/plat-auto-test/deploy && docker compose restart app"
# 重跑任务验证只产生一条执行记录，且 API 返回的 executionId 就是最终记录
```

**验收标准**：
- run 返回的 `executionId` 对应记录在执行结束后变为 `completed` 且有统计数据
- 执行历史下拉无重复/僵尸条目

---

## 改动清单

| 文件 | 类型 | 说明 |
|------|------|------|
| `backend/app/services/performance_service.py` | **MODIFY** | `run_task()` +`execution_id` 参数（复用分支）；`_run_single_background()` 透传 |
| 5.60 MySQL `performance_executions` | **DATA** | 删除存量僵尸 running 记录（`exec_4e4e...`） |
| `Docs/.../_问题处理_执行记录双条僵尸running记录.md` | **NEW** | 问题处理文档 |
| `Docs/.../_执行计划_修复执行记录双条僵尸running.md` | **NEW** | 本执行计划 |

**无需改动**：
- `routers/performance.py`（run 端点签名不变，仍走 `run_task_async`）
- 前端（返回契约不变）
- `PerformanceExecution` 模型

---

## 验证结果

| 验证项 | 结果 | 说明 |
|--------|------|------|
| 本地语法检查 | ✅ | 模块导入无错误 |
| 5.60 重跑任务单记录 | ✅ | run 返回 id == 最终 completed 记录 id |
| 存量僵尸清理 | ✅ | `exec_4e4e...` 已删除 |
| 兼容性（batch 批量路径） | ✅ | batch_run_tasks 未传 execution_id，行为不变 |

---

## 教训

1. "前台建记录返回 id + 后台另起会话执行"的模式里，**后台必须透传 id 复用记录**，否则必然双条。
2. 修 bug 时顺手写清理 SQL：代码修复只管增量，存量脏数据要一并处理，否则前端历史列表长期脏。
3. API 返回的标识符要与最终落库实体一一对应——返回前先问"这个 id 后面会不会被更新"。

---

*本文档由 Claude Code 生成*
