| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- from __future__ import annotations
- import json
- import logging
- import os
- import signal
- import subprocess
- import sys
- from dataclasses import dataclass
- from pathlib import Path
- from typing import Any
- from supply_infra.config import InfraSettings, get_infra_settings
- from supply_infra.pipeline.contracts import StepResult
- logger = logging.getLogger(__name__)
- _REPO_ROOT = Path(__file__).resolve().parents[2]
- _MAX_RESULT_BYTES = 100_000
- @dataclass(frozen=True)
- class StepExecutionRequest:
- run_id: str
- step_run_id: str
- step_key: str
- timeout_seconds: int
- def _log_directory(settings: InfraSettings, run_id: str) -> Path:
- base = Path(settings.pipeline_log_dir)
- if not base.is_absolute():
- base = _REPO_ROOT / base
- path = base / run_id
- path.mkdir(parents=True, exist_ok=True)
- return path
- def _read_result(path: Path) -> dict[str, Any]:
- if not path.is_file():
- return {"success": False, "error_code": "missing_result", "error": "Result file missing"}
- raw = path.read_bytes()
- if len(raw) > _MAX_RESULT_BYTES:
- return {
- "success": False,
- "error_code": "result_too_large",
- "error": f"Step result exceeds {_MAX_RESULT_BYTES} bytes",
- "result_bytes": len(raw),
- }
- try:
- payload = json.loads(raw.decode("utf-8"))
- except (UnicodeDecodeError, json.JSONDecodeError) as exc:
- return {
- "success": False,
- "error_code": "invalid_result_json",
- "error": str(exc),
- }
- if not isinstance(payload, dict):
- return {
- "success": False,
- "error_code": "invalid_result_type",
- "error": "Step result must be a JSON object",
- }
- return payload
- def run_step_subprocess(
- request: StepExecutionRequest,
- *,
- settings: InfraSettings | None = None,
- ) -> StepResult:
- active = settings or get_infra_settings()
- directory = _log_directory(active, request.run_id)
- stem = f"{request.step_key}-attempt-{request.step_run_id}"
- log_path = directory / f"{stem}.log"
- result_path = directory / f"{stem}.result.json"
- result_path.unlink(missing_ok=True)
- command = [
- sys.executable,
- "-m",
- "supply_infra.pipeline.step_cli",
- request.step_run_id,
- "--result-file",
- str(result_path),
- ]
- env = os.environ.copy()
- env["PROCESS_ROLE"] = "step"
- logger.info("Starting pipeline step: step_run_id=%s", request.step_run_id)
- timed_out = False
- exit_code: int | None = None
- with log_path.open("a", encoding="utf-8") as log_file:
- try:
- process = subprocess.Popen(
- command,
- cwd=str(_REPO_ROOT),
- env=env,
- stdout=log_file,
- stderr=subprocess.STDOUT,
- text=True,
- start_new_session=True,
- )
- except Exception as exc:
- logger.exception("Failed to launch pipeline step: %s", request.step_run_id)
- return StepResult(
- success=False,
- payload={},
- error_code="step_launch_failed",
- error_message=str(exc),
- log_uri=str(log_path),
- )
- try:
- exit_code = process.wait(timeout=request.timeout_seconds)
- except subprocess.TimeoutExpired:
- timed_out = True
- os.killpg(process.pid, signal.SIGTERM)
- try:
- exit_code = process.wait(timeout=min(active.pipeline_shutdown_grace_seconds, 30))
- except subprocess.TimeoutExpired:
- os.killpg(process.pid, signal.SIGKILL)
- exit_code = process.wait()
- if timed_out:
- return StepResult(
- success=False,
- payload={},
- error_code="step_timed_out",
- error_message=f"Step timed out after {request.timeout_seconds}s",
- exit_code=exit_code,
- log_uri=str(log_path),
- timed_out=True,
- )
- payload = _read_result(result_path)
- result_path.unlink(missing_ok=True)
- success = exit_code == 0 and payload.get("success") is not False
- return StepResult(
- success=success,
- payload=payload,
- error_code=(
- None
- if success
- else str(payload.get("error_code") or "step_failed")
- ),
- error_message=(
- None
- if success
- else str(payload.get("error") or f"Step exited with code {exit_code}")
- ),
- exit_code=exit_code,
- log_uri=str(log_path),
- )
|