提交 72db5e2e authored 作者: 陈泽健's avatar 陈泽健

fix(scheduler): 新增僵尸执行常驻看门狗,杜绝执行线程死亡卡死调度

背景:5.44 出现执行线程死亡但进程存活的「僵尸执行」——DB 记录卡 running
29 小时,内存锁已释放但 DB 层 running 守卫(scheduler_service.py)永久挡死
调度,定时任务 6 小时触发全部被跳过。启动期 recover_interrupted_executions
只兜进程重启,无法兜线程死亡。

实现(execution_service.py + main.py):
- 新增常驻 watchdog_loop,每 60s 扫描 UI running/pending 执行
- 双活信号:内存心跳(每用例完成后 touch)+ DB 结果数跨扫描增长
  (非绝对数量,存量结果不算存活)
- 30 分钟无活信号判僵尸,标记 failed + 释放内存锁(set_running_execution)
- 安全测试执行不纳入看门狗;5 分钟启动缓冲避免误杀

验证:
- 新增 tests/test_watchdog_stale_execution.py(9 例,全绿)
- 5.202/5.60 已部署并清理各自 1 个僵尸执行(330/0 条结果),自动触发新回归
- 5.44 等待当前 332 用例回归跑完后部署
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 11e1dd85
# HANDOFF — UI自动化测试交接文档
> **生成时间**: 2026-08-27
> **生成时间**: 2026-08-29
> **当前分支**: `platform-auto-test`
> **最近提交**: `c73ffd58` feat(perf): AI 分析报告资源使用分析四维度增强(Java 进程 + MySQL)
> **状态**: 🟢 **会话43:薄弱模块 R7 执行器修复正式部署三台——`playwright_executor.py`(commit `a63d090c`,R7 版)按安全流程(备份→上传→SHA-256→原子替换→docker restart→健康检查→容器内 AST/SHA 比对)部署到 5.44/5.202/5.60,三台全部验证通过。至此「执行器代码修复未部署」遗留闭环,远端薄弱模块 NO-BODY 问题解除。**
> **最近提交**: `3a29883b` feat(smart-locate): P1/P2 智能定位统一执行路径 + micro-app 递归穿透与状态确认
> **状态**: 🟢 **会话44:僵尸执行常驻看门狗上线——5.44 复现「DB 卡 running 29 小时挡死调度」(进程存活、线程死亡,启动恢复无法覆盖),新增 `watchdog_loop` 常驻协程(60s 扫描/30 分钟无进展判定/双信号活性:内存心跳 + DB 结果数增长),部署 5.202/5.60 完成(顺带清理两台 8/27 遗留僵尸),5.44 等当日回归跑完后自动部署(后台长轮询挂起)。会话 43 的 P1/P2 部署待办同时闭环。**
---
## 📊 当前状态(会话 44,2026-08-29)
**僵尸执行常驻看门狗**:会话 42 的 `recover_interrupted_executions()` 只兜「进程重启」场景;8/29 排查发现 5.44 一条 `exec_23393d`(8/28 09:16 定时回归,332 用例跑完 221 条后线程死亡)卡 running **29 小时**,期间调度器 DB 守卫每轮跳过(日志刷「到点但 DB 中仍有 UI 执行在跑」),且 `POST /run` 返回 `triggered` 但内层静默跳过(外层只查内存锁)。手动 cancel 后 4 秒调度恢复并触发新一轮——证实根因链。
### ✅ 修复内容(会话 44,2026-08-29)
| 项目 | 说明 |
|------|------|
| 制品 | `execution_service.py`(看门狗实现)+ `main.py`(lifespan 启动/停止) |
| 看门狗参数 | 扫描间隔 60s;静默阈值 1800s(30 分钟无进展判僵尸);运行存活下限 300s(启动窗口期不判罚) |
| 双信号活性判定 | ① 内存心跳:工作线程每完成一条用例 `touch_execution_heartbeat()`;② DB 结果计数**增长**(跨进程兜底;关键设计:**存量结果不算增长**——首次观测只记基线,防止把死执行误判为活跃,即 5.44 事件形态) |
| 判罚动作 | 执行 + 关联 pending/running 用例结果标记 failed + 清心跳 + 释放内存锁 → 调度立即恢复 |
| 防误伤 | security 执行不纳入;pending 需超静默阈值才判;running 需「存活超下限 + 双信号全静默」 |
| 测试 | `tests/test_watchdog_stale_execution.py` 9 个用例(含增长信号跨扫描语义);全量 333 passed |
### ✅ 部署结果(会话 44,2026-08-29,`deploy_watchdog.py`)
| 服务器 | 结果 | 附带清理 |
|--------|------|----------|
| 5.202 | ✅ 看门狗已启动(日志确认) | 启动恢复清掉 8/27 22:39 僵尸 1 条 + 330 条卡住结果;调度恢复后新一轮回归已自动触发(真实执行) |
| 5.60 | ✅ 看门狗已启动(日志确认) | 启动恢复清掉 8/27 22:40 僵尸 1 条;新回归已自动触发 |
| 5.44 | ⏳ 等待中 | 14:43 清僵尸后调度恢复、新一轮 332 用例回归正在健康执行(chromium 活跃、结果增长,~2 分钟/条),**不可打断**;后台长轮询(8h)等 running=0 自动部署 |
**遗留观察(非本次范围)**:「每日定时自动回归测试」实际 `interval=6h`,但单轮 332 用例需 8 小时以上(8/26 那轮跑了 19.4h)——执行窗口远大于调度周期,即使无僵尸,除首轮外每 6h 的触发都会被运行守卫跳过。如需高频回归需缩减用例集或拆分任务。
---
......@@ -493,7 +520,7 @@ cd frontend && npm run build
- [ ] **预定2.0(meetingV2)独立页面探测与用例生成**(首页无独立直达入口,需从功能中心 Tab/`[data-id]` 入口探测)
- [x] ~~**定时任务僵尸执行 + 重叠守卫**~~(会话 42 已修复并部署三台:`recover_interrupted_executions()` 启动恢复 + DB 级重叠兜底守卫 + lifespan 调用;清理 5.44/5.202/5.60 的 18/14/68 条 running 僵尸,对应 2606/1814/401 用例结果标 failed;324 测试全绿)
- [x] ~~**薄弱模块 R7 修复部署三台服务器(5.44/5.202/5.60)**~~(会话 43 完成:`playwright_executor.py` commit `a63d090c` 按安全流程部署三台,容器内 SHA 一致、健康检查通过;会话 40 用例数据 + 本次代码修复完整生效,远端薄弱模块 NO-BODY 解除。`deploy_executor_r7.py` 新增 `--artifact` 参数支持指定部署制品)
- [ ] **P1/P2 执行器代码部署三台(待部署)**(本地工作树含 `playwright_executor.py` P1b/P2 增强 + `smart_locate_service.py` + `smart_locate.py` + 前端 dist,须前端 `npm run build` 后一并部署
- [x] ~~**P1/P2 执行器代码部署三台**~~(会话 43 已完成:`deploy_p1p2.py` 部署 `playwright_executor.py` P1b/P2 增强 + `smart_locate_service.py` + `smart_locate.py` + 前端 dist 三台,6/6 验证通过;已提交 `3a29883b`
- [ ] **monitor 内层路由被忽略**(微应用固定渲染设备列表,忽略 URL 内层 Maintenancelist/Filemanage 路由;相关用例已按实际渲染断言,如需真正内页需进一步探测侧边栏交互方案)
- [ ] 完善二级菜单映射表(`menu_mapping.py`
- [ ] 智能定位 API 与用例执行路径统一(当前两者逻辑分离)
......
......@@ -145,6 +145,17 @@ async def lifespan(app: FastAPI):
except Exception as e:
logger.warning(f"定时任务调度引擎启动失败: {e}")
# 启动僵尸执行看门狗:进程常驻期间周期性标记「长时间无进展」的
# running/pending 执行,避免容器未重启但执行线程异常退出时,
# DB 记录卡在 running 挡死定时任务(5.44 复现:挡死调度 29 小时)。
watchdog_task = None
try:
from app.services.execution_service import start_watchdog
watchdog_task = start_watchdog()
logger.info("僵尸执行看门狗已启动")
except Exception as e:
logger.warning(f"僵尸执行看门狗启动失败: {e}")
yield
# 关闭时
......@@ -168,6 +179,15 @@ async def lifespan(app: FastAPI):
pass
logger.info("定时任务调度引擎已停止")
# 停止僵尸执行看门狗
if watchdog_task:
watchdog_task.cancel()
try:
await watchdog_task
except asyncio.CancelledError:
pass
logger.info("僵尸执行看门狗已停止")
# 创建 FastAPI 应用实例
app = FastAPI(
......
此差异已折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论