Let Reprocess stop a live or stuck running pipeline, then enqueue.
A docker rebuild can leave status=running with no worker; the old 409 blocked retry. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -204,11 +204,20 @@ async def reprocess(project_id: str, request: Request, req: ReprocessRequest | N
|
|||||||
owner_user_id, meta = await resolve_or_404(request, project_id)
|
owner_user_id, meta = await resolve_or_404(request, project_id)
|
||||||
if not meta.has_bom or not meta.has_netlist:
|
if not meta.has_bom or not meta.has_netlist:
|
||||||
raise HTTPException(400, "Upload BOM and netlist before reprocessing")
|
raise HTTPException(400, "Upload BOM and netlist before reprocessing")
|
||||||
if meta.status not in _REPROCESS_OK_FROM:
|
|
||||||
|
if _project_active(meta):
|
||||||
|
await _interrupt_active_pipeline(storage, owner_user_id, project_id)
|
||||||
|
meta = proj_svc.get_project(storage, owner_user_id, project_id) or meta
|
||||||
|
|
||||||
|
if meta.status == proj_svc.STATUS_PAUSED:
|
||||||
|
allowed = _REPROCESS_OK_FROM | {proj_svc.STATUS_PAUSED}
|
||||||
|
else:
|
||||||
|
allowed = _REPROCESS_OK_FROM
|
||||||
|
|
||||||
|
if meta.status not in allowed and not _project_active(meta):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
409,
|
409,
|
||||||
f"Reprocess is for complete / error / cancelled runs "
|
f"Cannot reprocess from status={meta.status}.",
|
||||||
f"(status={meta.status}). Pause uses resume; a live run uses cancel.",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
body = req or ReprocessRequest()
|
body = req or ReprocessRequest()
|
||||||
@@ -221,7 +230,9 @@ async def reprocess(project_id: str, request: Request, req: ReprocessRequest | N
|
|||||||
try:
|
try:
|
||||||
proj_svc.transition_status(
|
proj_svc.transition_status(
|
||||||
storage, owner_user_id, project_id,
|
storage, owner_user_id, project_id,
|
||||||
from_status=_REPROCESS_OK_FROM,
|
from_status=allowed | {
|
||||||
|
proj_svc.STATUS_QUEUED, proj_svc.STATUS_RUNNING,
|
||||||
|
},
|
||||||
to_status=proj_svc.STATUS_QUEUED,
|
to_status=proj_svc.STATUS_QUEUED,
|
||||||
cancel_requested=False,
|
cancel_requested=False,
|
||||||
execution_name=None,
|
execution_name=None,
|
||||||
@@ -480,6 +491,36 @@ async def status(project_id: str, request: Request):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def _interrupt_active_pipeline(
|
||||||
|
storage, user_id: str, project_id: str,
|
||||||
|
) -> None:
|
||||||
|
"""Cancel a queued/running worker so a new run can be enqueued.
|
||||||
|
|
||||||
|
If the worker is already dead (docker rebuild, OOM) the status can
|
||||||
|
stay ``running``; force it to cancelled after a short wait.
|
||||||
|
"""
|
||||||
|
proj_svc.request_cancel(storage, user_id, project_id)
|
||||||
|
await _await_terminal(storage, user_id, project_id, timeout_s=10.0)
|
||||||
|
meta = proj_svc.get_project(storage, user_id, project_id)
|
||||||
|
if meta is None:
|
||||||
|
return
|
||||||
|
if _project_active(meta) and meta.execution_name:
|
||||||
|
job_runner.cancel_execution(meta.execution_name)
|
||||||
|
await _await_terminal(storage, user_id, project_id, timeout_s=5.0)
|
||||||
|
meta = proj_svc.get_project(storage, user_id, project_id) or meta
|
||||||
|
if _project_active(meta):
|
||||||
|
try:
|
||||||
|
proj_svc.transition_status(
|
||||||
|
storage, user_id, project_id,
|
||||||
|
from_status={proj_svc.STATUS_RUNNING, proj_svc.STATUS_QUEUED},
|
||||||
|
to_status=proj_svc.STATUS_CANCELLED,
|
||||||
|
pipeline_state={"error": "Superseded by reprocess"},
|
||||||
|
cancel_requested=False,
|
||||||
|
)
|
||||||
|
except proj_svc.StatusConflict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def _await_terminal(
|
async def _await_terminal(
|
||||||
storage, user_id: str, project_id: str, *, timeout_s: float,
|
storage, user_id: str, project_id: str, *, timeout_s: float,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
+24
-7
@@ -96,17 +96,34 @@ def test_reprocess_all_clears_kept_refs(tmp_path, monkeypatch):
|
|||||||
assert fresh.completed_review_refs == []
|
assert fresh.completed_review_refs == []
|
||||||
|
|
||||||
|
|
||||||
def test_reprocess_rejects_running(tmp_path, monkeypatch):
|
def test_reprocess_stops_running_then_enqueues(tmp_path, monkeypatch):
|
||||||
monkeypatch.setattr(
|
captured: dict = {}
|
||||||
"backend.services.job_runner.enqueue_pipeline",
|
|
||||||
lambda *a, **k: "x",
|
async def fake_await(*a, **k):
|
||||||
)
|
return None
|
||||||
|
|
||||||
|
monkeypatch.setattr("backend.routers.pipeline._await_terminal", fake_await)
|
||||||
|
monkeypatch.setattr("backend.services.job_runner.cancel_execution", lambda *a, **k: None)
|
||||||
|
|
||||||
|
def fake_enqueue(project_id, user_id, *, resume=False, free=False):
|
||||||
|
captured["resume"] = resume
|
||||||
|
return "local/projects/x"
|
||||||
|
|
||||||
|
monkeypatch.setattr("backend.services.job_runner.enqueue_pipeline", fake_enqueue)
|
||||||
|
|
||||||
client = _client(tmp_path)
|
client = _client(tmp_path)
|
||||||
meta = client.post("/api/projects", json={"name": "board"}).json()
|
meta = client.post("/api/projects", json={"name": "board"}).json()
|
||||||
pid = meta["id"]
|
pid = meta["id"]
|
||||||
storage = client.app.state.storage
|
storage = client.app.state.storage
|
||||||
proj_svc.update_project(
|
proj_svc.update_project(
|
||||||
storage, "local", pid, status="running", has_bom=True, has_netlist=True,
|
storage, "local", pid,
|
||||||
|
status="running",
|
||||||
|
has_bom=True,
|
||||||
|
has_netlist=True,
|
||||||
|
completed_review_refs=["U1"],
|
||||||
)
|
)
|
||||||
resp = client.post(f"/api/pipeline/{pid}/reprocess", json={"mode": "failed"})
|
resp = client.post(f"/api/pipeline/{pid}/reprocess", json={"mode": "failed"})
|
||||||
assert resp.status_code == 409
|
assert resp.status_code == 202, resp.text
|
||||||
|
assert captured["resume"] is True
|
||||||
|
fresh = proj_svc.get_project(storage, "local", pid)
|
||||||
|
assert fresh.status == "queued"
|
||||||
|
|||||||
Reference in New Issue
Block a user