fix(automation): 启动时收尾卡在 running 的运行记录

作业是在调度线程里同步跑的:进程一重启(部署、崩溃、手工重启),那一轮就再没
机会写回结果,automation_runs 会永远停在 running,页面上一直显示「执行中」。
登录任务早有孤儿清理,运行记录原先漏了。

线上就是这么暴露的:部署时重建容器打断了正在跑的 ck_relogin 第 14 轮,重启清理
把 20 个在途账号标成了「重新登录中断(旧 Cookie 已保留)」(这部分是对的),
但 run 14 本身永远卡在 running,detail 为空。

新增 cleanup_orphan_runs(),在启动清理链里调用:把所有 running 的运行记录收尾为
failed + finished_at + 「服务重启中断」。如实记为 failed 而不是 success —— 那轮
确实没跑完;被中断的账号由各自的孤儿清理负责标记并按退避重试。

新增回归测试:只有 running 的被收尾,已完成的不受影响。

(另一件事一并说明:上一次部署我报告「零中断」是验证过早 —— 检查时部署的 up -d
还没执行到。实际情况是 mysql 未被重建(达到了本次改动目的),但应用容器因镜像
变化被正常重建,因此打断了那轮重登。)
This commit is contained in:
yml2213
2026-10-02 21:35:42 +08:00
parent ada3915856
commit 34d667cba0
3 changed files with 58 additions and 0 deletions
+33
View File
@@ -582,3 +582,36 @@ class TestFailureClassification(_Base):
batch_size=200,
concurrency=3,
)
class TestOrphanRunCleanup(_Base):
"""回归:进程重启后,卡在 running 的运行记录必须被收尾。
作业在调度线程里同步跑,进程一重启那轮就没机会写回结果,记录会永远停在
running,页面上一直显示「执行中」。登录任务早有孤儿清理,运行记录原先漏了
—— 线上部署时打断了一轮,run 14 就是这么卡住的。
"""
def test_running_runs_are_closed_on_startup(self):
from web.backend.services.automation.scheduler import cleanup_orphan_runs
stale = AutomationRun(
job_key="ck_relogin", started_at=datetime.now(UTC), status="running"
)
done = AutomationRun(
job_key="ck_health",
started_at=datetime.now(UTC),
finished_at=datetime.now(UTC),
status="success",
)
self.session.add_all([stale, done])
self.session.commit()
cleaned = cleanup_orphan_runs(self.session)
self.session.refresh(stale)
self.session.refresh(done)
assert cleaned == 1
assert stale.status == "failed"
assert stale.finished_at is not None
assert stale.message == "服务重启中断"
assert done.status == "success" # 已完成的不受影响
+5
View File
@@ -66,6 +66,11 @@ async def lifespan(app: FastAPI):
cleaned_relogin = cleanup_orphan_relogin_tasks(db)
if cleaned_relogin:
logger.info(f"启动清理 CK 重登残留任务: {cleaned_relogin} 条")
from .services.automation.scheduler import cleanup_orphan_runs
cleaned_runs = cleanup_orphan_runs(db)
if cleaned_runs:
logger.info(f"启动收尾中断的自动化运行记录: {cleaned_runs} 条")
cleaned_login = cleanup_orphan_login_tasks(db)
if cleaned_login:
logger.info(f"启动清理登录残留任务: {cleaned_login} 条")
@@ -118,6 +118,26 @@ def _due(job: AutomationJob, now: datetime) -> bool:
return next_run_at is None or next_run_at <= now
def cleanup_orphan_runs(db: Session, message: str = "服务重启中断") -> int:
"""把上次进程留下的 ``running`` 运行记录收尾。
作业是在调度线程里同步跑的:进程一重启(部署、崩溃、手工重启),那轮作业就
再没机会写回结果,``automation_runs`` 会永远停在 ``running``,页面上一直显示
「执行中」。登录任务有自己的孤儿清理,运行记录原先漏了。
收尾为 ``failed`` 而不是 ``success``:那轮确实没跑完,如实记录;被中断的账号
由各自的孤儿清理负责标记,并会按退避重试。
"""
orphans = db.query(AutomationRun).filter(AutomationRun.status == "running").all()
for run in orphans:
run.status = "failed"
run.finished_at = datetime.now(UTC)
run.message = message
if orphans:
db.commit()
return len(orphans)
def run_due_jobs(db: Session, *, now: datetime | None = None) -> list[dict[str, Any]]:
"""跑一轮「到点的作业」,返回每轮结果。