opsjob.py 2.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
  1. # -*- coding: utf-8 -*-
  2. r"""运维动作(重算/清产物)的**当前状态**读取口(单一实现)。
  3. 为什么需要它: 2026-09-19 用户报"重算后页面没有输出"。实况是——门户里点了「清除产物 → 执行重算」,
  4. 产物被清掉后重算要跑很久(本次 ④b 振动侧摄入要处理 150 GB 的 CMS 导出), 期间各页面只能显示
  5. "无产物"。但那句话现在只说"要么还没放数据, 要么刚清过产物", **没说"重算正在跑"** ——
  6. 于是同一现象被读成"系统坏了"。
  7. 状态真源: `scripts/_ops_run.py` 写的 `run/ops_job.json`(每 15 s 心跳更新 mtime)。
  8. 判定"在跑"= `status == 'running'` **且** 文件 mtime 在 `fresh_s` 秒内(只看 pid 会被复用骗过)。
  9. """
  10. from __future__ import annotations
  11. try:
  12. from ._root import install_root as _install_root
  13. except ImportError: # 直接当脚本跑(python <本文件>)时没有包上下文
  14. from _root import install_root as _install_root
  15. import json
  16. import pathlib
  17. import time
  18. ROOT = _install_root(__file__)
  19. JOB = ROOT / 'run' / 'ops_job.json'
  20. def current(fresh_s: int = 90) -> dict:
  21. """→ dict(running=bool, kind=, cmd=, started=, elapsed_s=, age_s=, stale=bool, note=)。读不到就 running=False。"""
  22. out = dict(running=False, kind=None, cmd=None, started=None, elapsed_s=None, age_s=None,
  23. stale=False, note=None, job_file=str(JOB))
  24. if not JOB.is_file():
  25. return out
  26. try:
  27. j = json.loads(JOB.read_text(encoding='utf-8'))
  28. except Exception as e:
  29. return {**out, 'note': f'任务文件读不出 ({type(e).__name__})'}
  30. age = time.time() - JOB.stat().st_mtime
  31. out.update(kind=j.get('kind'), cmd=j.get('cmd'), started=j.get('started'), age_s=round(age),
  32. status=j.get('status'), rc=j.get('rc'))
  33. running = (j.get('status') == 'running') and age < fresh_s
  34. out['running'] = bool(running)
  35. out['stale'] = (j.get('status') == 'running') and age >= fresh_s
  36. if out.get('started'):
  37. try:
  38. t0 = time.mktime(time.strptime(out['started'], '%Y-%m-%d %H:%M:%S'))
  39. out['elapsed_s'] = round(time.time() - t0)
  40. except Exception:
  41. pass
  42. if out['stale']:
  43. out['note'] = (f"任务文件写着 running 但已 {round(age / 60, 1)} 分钟没心跳 —— 可能是被强杀, "
  44. f"不是正在跑")
  45. return out
  46. def running_text() -> str:
  47. """给人看的一句话(页面/接口用): 在跑就写明"起点 + 已跑多久", 否则空串。"""
  48. s = current()
  49. if not s['running']:
  50. return ''
  51. mins = (s['elapsed_s'] or 0) / 60
  52. return (f"⚠ 检测到**重算正在进行中**({s['kind']},起于 {s['started']},已跑 {mins:.0f} 分钟)—— "
  53. f"产物是一步步长出来的,页面会在对应步骤跑完后自己回来(无需重启服务)。")