Files
tick-stock-panel/backend/app/services/lab_schedule.py
T
shy3130 b573ca43eb feat(mining): 挖掘锁撞锁降级启动 + 持有者诊断 + 一键强制释放
复现的原始问题: 旧实例挂死在 shutdown (热重载撞上挖掘运行) 或 worker
成为孤儿后, `.mining_process.lock` 被无限期占用, 后续所有后端实例
在 lifespan 抢锁时直接启动失败, 用户只能对着笼统报错手动找进程。

- 锁文件记录持有者 {pid, started_at, role, host} (flock 仍是唯一
  事实, 内容只是诊断线索; 旧格式 "0" 兼容, 视为持有者未知);
  新增非阻塞探测, 无需抢锁即可判断是否被持有。
- 撞锁降级启动: lifespan 抢锁失败不再上抛, 面板照常可用; 挖掘
  启动入口 (手动/自动/周度调度) 返回 409 并指引到挖掘页; 桌面版
  预检从「秒退失败页」改为留日志继续启动; 守护复验不受影响。
- 挖掘页锁占用横幅: 显示持有者 pid/角色/挖掘存储静默时长; 满足
  陈旧判定时可一键「强制停止并接管」(SIGTERM→SIGKILL→当前后端
  获取锁, 挖掘入口立即恢复)。
- 陈旧判定全部 fail-closed: 持有者信息可读、进程存活且 pid 未被
  复用 (POSIX 比对进程实际启动时间)、无进行中的挖掘运行、挖掘
  存储 10 分钟内无写入 — 任一不满足即拒绝并给出原因。不做启动
  时自动清理, 终止进程必须人工确认。

测试: 新增 test_mining_lock_admin (含真实外部持锁进程的强停回路与
五类拒绝路径), 重写 lifespan 降级契约与桌面预检继续启动契约;
后端全量 3239 通过, 前端 308 通过, ruff 零新增; 沙盒 E2E 完整
复刻「持锁→降级→强停→接管→恢复」事故场景通过。

Signed-off-by: shy3130 <shy3130@qq.com>
2026-10-26 00:01:20 +08:00

35 lines
1.4 KiB
Python

"""自动策略实验室调度单入口 (P3): 周度挖掘 + 每日守护一次挂载。
替代 daily_pipeline 直接调用两个调度函数的旧接线; 原入口
(run_weekly_mining / run_daily_guardian) 保持独立可用, 便于单独触发与测试。
"""
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
def run_scheduled_lab(app_state: Any) -> dict[str, Any]:
"""收盘管道成功后依次触发周度挖掘与每日守护复验 (各自独立防重)。"""
from app.services.guardian_schedule import run_daily_guardian
from app.services.mining_schedule import run_weekly_mining
results: dict[str, Any] = {"mining": None, "guardian": None}
try:
if not getattr(app_state, "mining_lock_acquired", True):
# 降级模式 (挖掘锁被其他进程占用): 跳过周度挖掘而不是报错,
# 守护复验不受影响
logger.warning("scheduled lab mining skipped: mining lock unavailable")
results["mining"] = {"skipped": "mining lock unavailable"}
else:
results["mining"] = run_weekly_mining(app_state)
except Exception:
logger.exception("scheduled lab mining enqueue failed")
try:
results["guardian"] = run_daily_guardian(app_state)
except Exception:
logger.exception("scheduled lab guardian enqueue failed")
return results