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:
+27
-2
@@ -13,12 +13,13 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from app.api.deps import DbSession, JobRepoDep
|
||||
from app.application.services.job_executor import new_id, run_job_background
|
||||
from app.application.services.job_executor import new_id, run_job_background, terminate_active
|
||||
from app.domain.entities.research import (
|
||||
BacktestResult,
|
||||
FactorTestReport,
|
||||
@@ -69,6 +70,30 @@ def create_job(
|
||||
return {"job_id": job.id, "status": job.status}
|
||||
|
||||
|
||||
@router.get("", summary="Job 列表")
|
||||
def list_jobs(
|
||||
job_repo: JobRepoDep,
|
||||
kind: Annotated[str | None, Query(description="按类型过滤(backtest/factor_test)")] = None,
|
||||
limit: Annotated[int, Query(ge=1, le=200)] = 20,
|
||||
) -> list[dict]:
|
||||
return [_job_view(j) for j in job_repo.list_recent(kind=kind, limit=limit)]
|
||||
|
||||
|
||||
@router.post("/{job_id}/cancel", summary="取消 Job(queued/running)")
|
||||
def cancel_job(job_id: str, session: DbSession, job_repo: JobRepoDep) -> dict:
|
||||
job = job_repo.get(job_id)
|
||||
if job is None:
|
||||
raise HTTPException(status_code=404, detail=f"Job {job_id} 不存在")
|
||||
if job.status not in (JobStatus.QUEUED, JobStatus.RUNNING):
|
||||
return {"job_id": job_id, "status": job.status, "cancelled": False}
|
||||
job.status = JobStatus.CANCELLED
|
||||
job.stage = None
|
||||
job_repo.update(job)
|
||||
session.commit()
|
||||
terminate_active(job_id) # 终止研究子进程(若有);父进程兜底已跳过 CANCELLED
|
||||
return {"job_id": job_id, "status": JobStatus.CANCELLED, "cancelled": True}
|
||||
|
||||
|
||||
@router.get("/{job_id}", summary="查询 Job 状态与结果")
|
||||
def get_job(job_id: str, job_repo: JobRepoDep) -> dict:
|
||||
job = job_repo.get(job_id)
|
||||
|
||||
Reference in New Issue
Block a user