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:
@@ -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" # 已完成的不受影响
|
||||
|
||||
@@ -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]]:
|
||||
"""跑一轮「到点的作业」,返回每轮结果。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user