# 计划执行 — 定时任务并发执行失败修复

> 版本：1.0 ｜ 日期：2026-07-27 ｜ 作者：czj
> 关联问题文档：`PRD_问题处理_定时任务并发执行失败.md`

---

## 修复目标

1. 修复 5.44 定时任务不执行问题（P0）
2. 修复 schedules.json 并发写入状态丢失问题（P1）
3. 修复 APScheduler 线程阻塞问题（P1）

---

## 改动文件

| 文件 | 改动内容 |
|------|---------|
| `skill/code/web/service_monitor/services/schedule_service.py` | 三处修复（见下文） |

---

## 修复步骤

### 步骤 1：添加 schedules.json 读写锁

**位置**：`schedule_service.py` 模块级变量区（第 28 行附近）

**改动**：新增 `_schedules_lock = threading.Lock()`

**涉及函数加锁**：
- `_save_schedules()` — 写操作加锁
- `_update_current_status()` — 读-改-写加锁
- `update_run_status()` — 读-改-写加锁

**注意**：`_load_schedules()` 是纯读操作，不需要加锁（JSON 文件读取是原子的）。但读-改-写模式（先 load 再 save）必须整体加锁。

```python
# 新增模块级锁
_schedules_lock = threading.Lock()

# _save_schedules 加锁
def _save_schedules(schedules: list) -> None:
    with _schedules_lock:
        ensure_dirs()
        SCHEDULES_FILE.write_text(
            json.dumps(schedules, ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

# _update_current_status 加锁
def _update_current_status(schedule_id: str, status: str) -> None:
    with _schedules_lock:
        schedules = _load_schedules()
        for i, s in enumerate(schedules):
            if s.get("id") == schedule_id:
                schedules[i]["current_status"] = status
                _save_schedules(schedules)
                return
    raise ValueError("定时任务不存在")

# update_run_status 加锁
def update_run_status(schedule_id: str, status: str, report_id: str, run_at: str) -> dict:
    with _schedules_lock:
        schedules = _load_schedules()
        for i, s in enumerate(schedules):
            if s.get("id") == schedule_id:
                schedules[i]["last_run_at"] = run_at
                schedules[i]["last_run_status"] = status
                schedules[i]["last_report_id"] = report_id
                schedules[i]["next_run_at"] = _calc_next_run(s["cron"])
                _save_schedules(schedules)
                return schedules[i]
    raise ValueError("定时任务不存在")
```

---

### 步骤 2：`_add_job()` 添加关键参数

**位置**：`schedule_service.py` 第 451-457 行

**改动**：添加 `misfire_grace_time`、`coalesce`、`max_instances` 参数

```python
_scheduler.add_job(
    func=_execute_scheduled_job,
    trigger=trigger,
    id=job_id,
    args=[sched["id"]],
    replace_existing=True,
    misfire_grace_time=3600,  # 允许 1 小时内的错过执行（默认 1 秒太短）
    coalesce=True,            # 错过多次只执行一次
    max_instances=1,          # 同一 job 不并发（显式声明）
)
```

**参数说明**：
- `misfire_grace_time=3600`：如果任务错过触发时间，1 小时内仍会执行。解决因 GIL 竞争、线程繁忙等导致的短暂延迟跳过问题
- `coalesce=True`：如果任务连续错过多次触发（如服务重启期间），只执行一次，不堆积
- `max_instances=1`：同一 job 同一时间只允许一个实例运行，防止并发重复执行

---

### 步骤 3：`_execute_scheduled_job` 改为后台线程执行

**位置**：`schedule_service.py` 第 512-521 行

**改动**：将同步调用改为 `threading.Thread` 后台执行，与 `run_now()` 保持一致

```python
def _execute_scheduled_job(schedule_id: str) -> None:
    """定时任务执行入口（APScheduler 线程中调用），含 enabled 检查。"""
    logger.info("定时任务触发: %s", schedule_id)

    sched = get_schedule(schedule_id)
    if not sched or not sched.get("enabled"):
        logger.warning("定时任务不存在或已禁用: %s", schedule_id)
        return

    # 检查是否已在执行中（防止定时触发与手动触发并发）
    if sched.get("current_status") == "running":
        logger.warning("定时任务已在执行中，跳过: %s", schedule_id)
        return

    # 使用后台线程执行，不阻塞 APScheduler 调度线程
    t = threading.Thread(target=_run_job_body, args=(schedule_id,), daemon=True)
    t.start()
```

**改动说明**：
- 新增 `current_status == "running"` 检查，防止定时触发与手动触发并发执行同一任务
- 使用 `threading.Thread` 后台执行，APScheduler 线程立即释放，可以继续调度其他任务
- `daemon=True` 确保主进程退出时线程自动终止

---

## 验证步骤

### 本地验证

1. 启动服务，创建两个定时任务（间隔 1 分钟）
2. 确认两个任务都能正常触发执行
3. 手动触发一个正在执行的任务，确认被拒绝（"已在执行中"）
4. 检查 `schedules.json` 状态更新正确

### 部署后验证

1. 部署到 5.60
2. 检查健康状态：`curl -s http://192.168.5.60:8088/api/health`
3. 检查 scheduler 状态：`curl -s http://192.168.5.60:8088/api/service-monitor/scheduler/status`
4. 确认 5.44 和 5.202 两个定时任务都已注册
5. 等待下一个工作日 8:40，确认 5.44 任务正常执行
6. 等待 9:00，确认 5.202 任务正常执行
7. 检查 `schedules.json` 中两个任务的状态更新均正确

### 单元测试

```bash
cd skill/code && python -m pytest -v
```

确认 218 个测试全绿，无回归。

---

## 回滚方案

如果修复后出现问题，回滚步骤：
1. 恢复 `schedule_service.py` 到修复前版本
2. 重新部署
3. 手动检查 `schedules.json` 中任务状态是否正确

---

## 文档信息

- 创建时间：2026-07-27
- 创建人：Claude
- 关联问题文档：`PRD_问题处理_定时任务并发执行失败.md`
