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

fix(execution): 修复看门狗对 pending 执行的时区误杀 (UTC vs 本地时钟)

- 根因:Execution.created_at 由 datetime.utcnow 写入 (UTC),但 watchdog_scan_once 此前统一用 datetime.now() (本地时区) 相减,导致在 UTC+8 环境下未设置 start_time 的 pending 执行 age 虚高 8 小时,新创建执行在 25-60 秒内被误杀标记为 failed。
- 修复:按字段实际口径自适应基准时间——无 start_time 时用 datetime.utcnow() 比较 created_at;有 start_time 时用 datetime.now() 比较。
- 单测:补充 test_pending_no_start_time_fresh_utc_not_marked 与 test_pending_no_start_time_stale_utc_marks_failed 2 条回归用例,watchdog 测试 11/11 全过。
- 文档:HANDOFF_UI自动化.md 归档会话 58 看板用例 5/5 回归收尾与看门狗误杀排查记录。
Co-Authored-By: 's avatarClaude Code <noreply@anthropic.com>
上级 2feb7deb
......@@ -3,7 +3,15 @@
> **生成时间**: 2026-09-08
> **当前分支**: `platform-auto-test`
> **最近提交**: `f363abdd` docs(handoff): 会话53 系统配置500 修复记录 + 被测系统配置数据库缺列运维文档归档(本会话改动已提交推送)
> **状态**: 🟢 **会话57 完成(2026-09-08):双窗口工作合并——本窗口会话 54/55/56(报告中心放开失败/取消执行 → 全量部署 5.44/5.202 → 钉钉通知版面重构)与另一窗口会话 54(执行进度 total_cases 显示修复,提交 `14e3c365`/`b227d759`)在 git rebase 中合并;编号冲突已在本节说明,两线工作均已完成并保留**(历史会话见下章节)
> **状态**: 🟢 **会话58 完成(2026-09-08):看板用例 5/5 回归收尾 + 看门狗时区误杀修复(执行中心线,编号自会话57 顺延)**(历史会话见下章节)
> - **看门狗时区误杀(P0)**:MySQL 下 `created_at`(utcnow,UTC)vs `datetime.now()`(本地)→ pending 执行 age 虚高 8 小时 → 刚创建未启动的执行 25 秒即被看门狗标记 failed(实锤 exec_fa9d786f)。修复 `watchdog_scan_once` pending 分支按字段实际口径选时间基准(无 start_time 用 utcnow 比较),新增 2 个回归测试(watchdog 测试 11/11,全量 468 passed)。
> - **看板用例(case_6699d6dd)收尾**(承接会话 56 待办,已完成):三步修复——① 步骤 3 断言 expected「今日会议」→「新建会议」(既定方案);② 步骤 1 wait `.nav_right_list` → `.block` 15s;③ ⚠️ **新踩坑**:步骤名含「新建会议」会被页面直达(page_url_service 按步骤名关键词识别)误导航到 CreateMeeting 页,改名「断言看板容器文本」避开(expected 值不参与识别,断言保留「新建会议」)。最终 `exec_f319fb82` **5/5 通过**(登录×3 + 巡检报表 + 看板 1.02s)。
> - **5.44 容器 crash(P0,与执行中心线并行发现)**:`services/recorder_engine.py` 在 5.44 缺失 → main.py import recorder 失败 → 容器 crash loop。本线先上传补齐恢复 `/health` 200,另一线(下方会话 55)随后以递归同步全量根治。5.44 容器 /tmp/re.py(临时脚本残留)遮蔽标准库 re 已删除。
## ⚠️ 待办(会话 58 更新)
- **5.60 / 5.202 核对看门狗时区修复**:需核对两台机器容器内 `execution_service.py` 是否已含 pending/utcnow 分支(若另一线会话 55 的递归同步发生在本地改动之后则已带上,未确认)——时区 bug 在所有 MySQL 部署同样存在,任何创建后未立即启动的 pending 执行会被误杀
- **5.60 录制器真实环境验收**(用户自行验证)
- 清理诊断脚本(diag_544_*.py、dump_544_cases.py、dump9_cases.json、tmp_wait_deploy_544.py 等)
---
......
......@@ -215,7 +215,17 @@ async def watchdog_scan_once(
continue
if e.case_type != "ui":
continue # 防御:security 等类型不纳入监控
age = (now - started).total_seconds()
# created_at 由 datetime.utcnow 写入(UTC),start_time 由 datetime.now
# 写入(本地时区)。pending 尚无 start_time 时若仍用本地时钟相减,
# age 会虚高一个时区偏移(UTC+8 为 8 小时),刚创建未启动的 pending
# 执行会被立即误判为僵尸(2026-09-08 5.44 复现:创建后 25 秒即被
# 看门狗标记 failed)。此处按字段实际口径选基准:无 start_time →
# started 必为 UTC 的 created_at,用 utcnow 比较;有 start_time
# (UI 链路为本地时间)用本地 now 比较。
if e.start_time is None:
age = (datetime.utcnow() - started).total_seconds()
else:
age = (now - started).total_seconds()
# 执行在不同阶段用不同判罚起点:pending 未启动即算空转;
# running 需要存活时间 + 静默时间叠加,避免误判慢启动用例。
......
......@@ -139,6 +139,41 @@ class TestWatchdogScanOnce:
assert recovered == 1
assert affected == 1
def test_pending_no_start_time_fresh_utc_not_marked(self):
"""pending 无 start_time:created_at(UTC) 刚创建 → 不标记(5.44 时区误杀回归)
MySQL 部署下 created_at 由 datetime.utcnow 写入(UTC),pending 执行
start_time=None。若看门狗用本地时钟(UTC+8)相减,age 虚高 8 小时,
刚创建未启动的执行会被立即判僵尸(2026-09-08 5.44 复现)。
"""
from datetime import datetime as dt
fresh = Execution(id="e_fresh_utc", status="pending", case_type="ui")
fresh.created_at = dt.utcnow() # 刚创建(UTC 口径)
fresh.start_time = None
session = _make_session([fresh], result_count=0)
with _patch_sessionmaker(session):
recovered, affected = run(watchdog_scan_once(
stale_seconds=1800, running_floor_seconds=300
))
assert recovered == 0
assert affected == 0
def test_pending_no_start_time_stale_utc_marks_failed(self):
"""pending 无 start_time:created_at(UTC) 已超静默阈值 → 标记"""
from datetime import datetime as dt, timedelta as td
old = Execution(id="e_old_utc", status="pending", case_type="ui")
old.created_at = dt.utcnow() - td(hours=2)
old.start_time = None
session = _make_session([old], result_count=0)
with _patch_sessionmaker(session):
recovered, affected = run(watchdog_scan_once(
stale_seconds=1800, running_floor_seconds=300
))
assert recovered == 1
assert affected == 1
def test_running_within_floor_not_affected(self):
"""运行中执行:启动窗口期内(age < floor)→ 不标记"""
young = _mk_exec("e_young", "running", timedelta(minutes=-1))
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论