153 lines
3.9 KiB
Python
Executable File
153 lines
3.9 KiB
Python
Executable File
"""
|
|
Pipeline control — retry / force-node / pause / resume / state.
|
|
|
|
Wraps an in-memory engine (set by main.py) and emits events on the bus
|
|
so the UI live stream picks them up.
|
|
"""
|
|
|
|
from fastapi import APIRouter
|
|
from pydantic import BaseModel
|
|
from typing import Any, Optional
|
|
import threading
|
|
|
|
from backend.api.api import bus
|
|
from backend.events.schema import EventType
|
|
|
|
router = APIRouter(prefix="/api/pipeline", tags=["pipeline"])
|
|
|
|
# F-15: guards for module-level mutable state used from async handlers.
|
|
_engine_lock = threading.Lock()
|
|
_state_lock = threading.Lock()
|
|
|
|
# In-memory engine reference (set by main.py)
|
|
_ENGINE: Optional[Any] = None
|
|
_PIPELINE_STATE: dict = {
|
|
"state": "idle", # idle | running | paused | complete | failed
|
|
"active_actions": [],
|
|
"completed": 0,
|
|
"failed": 0,
|
|
"queued": 0,
|
|
"build_id": None,
|
|
}
|
|
|
|
|
|
class ActionBody(BaseModel):
|
|
action: str
|
|
node: Optional[str] = None
|
|
|
|
|
|
class ForceBody(BaseModel):
|
|
action: str
|
|
node: str
|
|
|
|
|
|
def set_engine(engine: Any) -> None:
|
|
global _ENGINE
|
|
with _engine_lock:
|
|
_ENGINE = engine
|
|
|
|
|
|
def get_state_snapshot() -> dict:
|
|
with _state_lock:
|
|
return dict(_PIPELINE_STATE)
|
|
|
|
|
|
# -----------------------------
|
|
# GET STATE
|
|
# -----------------------------
|
|
@router.get("/state")
|
|
def get_state():
|
|
return get_state_snapshot()
|
|
|
|
|
|
# -----------------------------
|
|
# RETRY ACTION
|
|
# -----------------------------
|
|
@router.post("/retry")
|
|
def retry_action(body: ActionBody):
|
|
bus.emit(
|
|
EventType.PIPELINE_UPDATE,
|
|
action=body.action,
|
|
node=body.node,
|
|
state="retrying",
|
|
reason="manual_retry",
|
|
)
|
|
# In a real engine this would re-queue the action.
|
|
# For now we just record the intent.
|
|
return {"status": "retry_queued", "action": body.action, "node": body.node}
|
|
|
|
|
|
# -----------------------------
|
|
# FORCE NODE
|
|
# -----------------------------
|
|
@router.post("/force-node")
|
|
def force_node(body: ForceBody):
|
|
bus.emit(
|
|
EventType.PIPELINE_UPDATE,
|
|
action=body.action,
|
|
node=body.node,
|
|
state="forced",
|
|
reason="manual_force",
|
|
)
|
|
return {"status": "forced", "action": body.action, "node": body.node}
|
|
|
|
|
|
# -----------------------------
|
|
# PAUSE
|
|
# -----------------------------
|
|
@router.post("/pause")
|
|
def pause() -> dict:
|
|
with _state_lock:
|
|
_PIPELINE_STATE["state"] = "paused"
|
|
eng = _ENGINE # brief read; lock not strictly needed for read of immutable ref
|
|
if eng and hasattr(eng, "pause"):
|
|
eng.pause()
|
|
bus.emit(EventType.PIPELINE_UPDATE, state="paused", reason="manual_pause")
|
|
return {"status": "paused"}
|
|
|
|
|
|
# -----------------------------
|
|
# RESUME
|
|
# -----------------------------
|
|
@router.post("/resume")
|
|
def resume() -> dict:
|
|
with _state_lock:
|
|
_PIPELINE_STATE["state"] = "running"
|
|
eng = _ENGINE
|
|
if eng and hasattr(eng, "resume"):
|
|
eng.resume()
|
|
bus.emit(EventType.PIPELINE_UPDATE, state="running", reason="manual_resume")
|
|
return {"status": "running"}
|
|
|
|
|
|
# -----------------------------
|
|
# Legacy wrapper for api.py facade
|
|
# -----------------------------
|
|
def pipeline_control_endpoint(action: str, payload: Optional[dict] = None,
|
|
bus: Optional[Any] = None) -> dict:
|
|
"""Legacy dispatcher.
|
|
|
|
``action`` is one of: ``retry``, ``force``, ``pause``, ``resume``,
|
|
``state``. F-11 remediation: the if/elif chain is replaced with a
|
|
dispatch table so adding a new action is a one-line entry.
|
|
"""
|
|
payload = payload or {}
|
|
|
|
def _retry():
|
|
return retry_action(ActionBody(**payload))
|
|
|
|
def _force():
|
|
return force_node(ForceBody(**payload))
|
|
|
|
handlers = {
|
|
"retry": _retry,
|
|
"force": _force,
|
|
"pause": pause,
|
|
"resume": resume,
|
|
"state": get_state_snapshot,
|
|
}
|
|
fn = handlers.get(action)
|
|
if fn is None:
|
|
return {"error": f"unknown action: {action}"}
|
|
return fn()
|