feat(job): D1 Job 阶段上报(v3 §23)+ 列表/取消端点
- ResearchService.run_backtest/run_factor_test 支持 on_stage 回调(data_loading →
factor_calculation/backtesting → analysis);job_executor 经独立短会话把 stage 写库
(子进程模式同样走 DB),收尾回读最后阶段避免覆盖
- /api/jobs:GET 列表(kind/limit)、POST /{id}/cancel(queued/running → CANCELLED +
终止子进程 terminate_active;父进程兜底已跳过 CANCELLED)
- SSE /jobs/{id}/events 现会携带 stage
- tests/test_job_stages.py:成功 job 终态 stage=analysis;executor 取消 queued/幂等/
已完成不可取消;API 列表+终态不可取消+404;全量 pytest 通过
This commit is contained in:
@@ -95,10 +95,23 @@ def _execute_inner(
|
||||
service = ResearchService(
|
||||
stock_repo_factory(session), daily_repo_factory(session), engine
|
||||
)
|
||||
|
||||
def _set_stage(name: str) -> None:
|
||||
"""阶段上报(v3 §23):独立短会话写 job.stage 并 commit(子进程同样走 DB)。"""
|
||||
try:
|
||||
with session_factory() as st_sess:
|
||||
st = job_repo_factory(st_sess).get(job_id)
|
||||
if st is not None and st.status == JobStatus.RUNNING:
|
||||
st.stage = name
|
||||
job_repo_factory(st_sess).update(st)
|
||||
st_sess.commit()
|
||||
except Exception: # noqa: BLE001 —— 阶段上报失败不阻断执行
|
||||
pass
|
||||
|
||||
if spec.type == "backtest":
|
||||
result = service.run_backtest(spec)
|
||||
result = service.run_backtest(spec, on_stage=_set_stage)
|
||||
else:
|
||||
result = service.run_factor_test(spec)
|
||||
result = service.run_factor_test(spec, on_stage=_set_stage)
|
||||
|
||||
result_json = json.dumps(result.model_dump(mode="json"), ensure_ascii=False)
|
||||
experiment = ExperimentRecord(
|
||||
@@ -122,6 +135,14 @@ def _execute_inner(
|
||||
job.error = f"{type(exc).__name__}: {exc}"
|
||||
finally:
|
||||
job.finished_at = datetime.now()
|
||||
# 回读最后一次阶段上报(_set_stage 经独立会话写库),避免被本会话覆盖
|
||||
try:
|
||||
with session_factory() as last_sess:
|
||||
last = job_repo_factory(last_sess).get(job_id)
|
||||
if last is not None:
|
||||
job.stage = last.stage
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
job_repo.update(job)
|
||||
session.commit()
|
||||
|
||||
@@ -353,6 +374,42 @@ def _run_in_subprocess(
|
||||
)
|
||||
|
||||
|
||||
def terminate_active(job_id: str) -> bool:
|
||||
"""终止该 Job 的活动子进程(如有)。返回是否找到并终止。"""
|
||||
with _active_lock:
|
||||
proc = _active_jobs.get(job_id)
|
||||
if proc is None:
|
||||
return False
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
proc.terminate()
|
||||
return True
|
||||
|
||||
|
||||
def cancel_job(
|
||||
job_id: str,
|
||||
*,
|
||||
session_factory: Callable | None = None,
|
||||
job_repo_factory: Callable | None = None,
|
||||
) -> bool:
|
||||
"""取消 queued/running Job(置 CANCELLED 并终止子进程);不可取消返回 False。"""
|
||||
facts = default_factories()
|
||||
sf = session_factory or facts["session_factory"]
|
||||
jr = job_repo_factory or facts["job_repo_factory"]
|
||||
with sf() as session:
|
||||
repo = jr(session)
|
||||
job = repo.get(job_id)
|
||||
if job is None or job.status not in (JobStatus.QUEUED, JobStatus.RUNNING):
|
||||
return False
|
||||
job.status = JobStatus.CANCELLED
|
||||
job.stage = None
|
||||
repo.update(job)
|
||||
session.commit()
|
||||
terminate_active(job_id)
|
||||
return True
|
||||
|
||||
|
||||
def mark_stale_jobs_failed(
|
||||
*,
|
||||
session_factory: Callable | None = None,
|
||||
|
||||
Reference in New Issue
Block a user