|
|
|
@ -16,6 +16,7 @@ from src.config import (
|
|
|
|
RAMP_MODEL_NAME,
|
|
|
|
RAMP_MODEL_NAME,
|
|
|
|
RAMP_POLL_INTERVAL_SECONDS,
|
|
|
|
RAMP_POLL_INTERVAL_SECONDS,
|
|
|
|
RAMP_POLL_TIMEOUT_SECONDS,
|
|
|
|
RAMP_POLL_TIMEOUT_SECONDS,
|
|
|
|
|
|
|
|
RAMP_STALE_TIMEOUT_SECONDS,
|
|
|
|
RAMP_TARGET_SCHEMA,
|
|
|
|
RAMP_TARGET_SCHEMA,
|
|
|
|
SIMULATION_ENGINE,
|
|
|
|
SIMULATION_ENGINE,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
@ -255,9 +256,7 @@ def _rbd_reliability_to_ramp_override(reliability: dict) -> dict:
|
|
|
|
"distribution": _clean_dict({"type": "Weibull", "shape": beta, "scale": eta}),
|
|
|
|
"distribution": _clean_dict({"type": "Weibull", "shape": beta, "scale": eta}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elif distribution == "Exponential":
|
|
|
|
elif distribution == "Exponential":
|
|
|
|
exp_eta = eta
|
|
|
|
exp_eta = eta if eta is not None else beta
|
|
|
|
if beta and beta > 0:
|
|
|
|
|
|
|
|
exp_eta = 1 / beta
|
|
|
|
|
|
|
|
override["model"] = {
|
|
|
|
override["model"] = {
|
|
|
|
"type": "NonRepairable",
|
|
|
|
"type": "NonRepairable",
|
|
|
|
"distribution": _clean_dict({"type": "Exponential", "eta": exp_eta}),
|
|
|
|
"distribution": _clean_dict({"type": "Exponential", "eta": exp_eta}),
|
|
|
|
@ -374,6 +373,51 @@ async def _fetch_ramp_model_asset_names(client: httpx.AsyncClient, model_id: str
|
|
|
|
return {_norm_key(asset.get("name")) for asset in assets if asset.get("name")}
|
|
|
|
return {_norm_key(asset.get("name")) for asset in assets if asset.get("name")}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _find_ramp_task(tasks_payload: dict, task_id: str) -> dict | None:
|
|
|
|
|
|
|
|
for task in tasks_payload.get("tasks", []):
|
|
|
|
|
|
|
|
if str(task.get("id")) == str(task_id):
|
|
|
|
|
|
|
|
return task
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _fetch_ramp_task_status(client: httpx.AsyncClient, task_id: str) -> dict:
|
|
|
|
|
|
|
|
"""Fetch task status, falling back to the lighter task-list endpoint.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RAMP's status endpoint includes the result payload for completed tasks. When
|
|
|
|
|
|
|
|
the engine is busy that endpoint can occasionally time out even for running
|
|
|
|
|
|
|
|
tasks, while /tasks still returns the persisted progress row quickly.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
status_response = await client.get(f"{_simulation_api_base()}/status/{task_id}")
|
|
|
|
|
|
|
|
status_response.raise_for_status()
|
|
|
|
|
|
|
|
return status_response.json()
|
|
|
|
|
|
|
|
except (httpx.ReadTimeout, httpx.ConnectTimeout, httpx.ConnectError) as status_error:
|
|
|
|
|
|
|
|
log.warning("RAMP status request failed for task %s: %s", task_id, status_error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tasks_response = await client.get(f"{_simulation_api_base()}/tasks")
|
|
|
|
|
|
|
|
tasks_response.raise_for_status()
|
|
|
|
|
|
|
|
task = _find_ramp_task(tasks_response.json(), task_id)
|
|
|
|
|
|
|
|
if task:
|
|
|
|
|
|
|
|
return task
|
|
|
|
|
|
|
|
raise RuntimeError(f"RAMP task {task_id} was not found in /tasks fallback.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _fetch_completed_ramp_result(client: httpx.AsyncClient, task_id: str) -> dict:
|
|
|
|
|
|
|
|
response = await client.get(f"{_simulation_api_base()}/status/{task_id}", timeout=120.0)
|
|
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
|
|
result = response.json().get("result")
|
|
|
|
|
|
|
|
if not isinstance(result, dict):
|
|
|
|
|
|
|
|
raise RuntimeError(f"RAMP task {task_id} completed without a result payload.")
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _cancel_ramp_task(client: httpx.AsyncClient, task_id: str) -> None:
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
await client.post(f"{_simulation_api_base()}/cancel/{task_id}")
|
|
|
|
|
|
|
|
except Exception as cancel_error:
|
|
|
|
|
|
|
|
log.warning("Failed to cancel stale RAMP task %s: %s", task_id, cancel_error)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _filter_overrides_to_model(overrides: dict, asset_names: set[str]) -> tuple[dict, list[str]]:
|
|
|
|
def _filter_overrides_to_model(overrides: dict, asset_names: set[str]) -> tuple[dict, list[str]]:
|
|
|
|
"""Keep only overrides whose asset exists in the RAMP model.
|
|
|
|
"""Keep only overrides whose asset exists in the RAMP model.
|
|
|
|
|
|
|
|
|
|
|
|
@ -612,10 +656,36 @@ async def save_ramp_simulation_result(
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
simulation.status = "completed"
|
|
|
|
simulation.status = "completed"
|
|
|
|
|
|
|
|
simulation.error = None
|
|
|
|
simulation.completed_at = datetime.now()
|
|
|
|
simulation.completed_at = datetime.now()
|
|
|
|
await db_session.commit()
|
|
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def record_ramp_task_start(
|
|
|
|
|
|
|
|
*,
|
|
|
|
|
|
|
|
db_session: DbSession,
|
|
|
|
|
|
|
|
simulation_id: UUID,
|
|
|
|
|
|
|
|
model_id: str,
|
|
|
|
|
|
|
|
ramp_task_id: str,
|
|
|
|
|
|
|
|
payload: dict,
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
|
|
simulation = await get_simulation_by_id(db_session=db_session, simulation_id=simulation_id)
|
|
|
|
|
|
|
|
simulation.status = "running"
|
|
|
|
|
|
|
|
simulation.input = {
|
|
|
|
|
|
|
|
**(simulation.input or {}),
|
|
|
|
|
|
|
|
"engine": "ramp",
|
|
|
|
|
|
|
|
"ramp_model_id": model_id,
|
|
|
|
|
|
|
|
"ramp_task_id": ramp_task_id,
|
|
|
|
|
|
|
|
"ramp_payload": {
|
|
|
|
|
|
|
|
key: value
|
|
|
|
|
|
|
|
for key, value in payload.items()
|
|
|
|
|
|
|
|
if key != "distribution_overrides"
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
"ramp_distribution_override_count": len(payload.get("distribution_overrides") or {}),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
await db_session.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def execute_ramp_simulation(
|
|
|
|
async def execute_ramp_simulation(
|
|
|
|
*,
|
|
|
|
*,
|
|
|
|
db_session: DbSession,
|
|
|
|
db_session: DbSession,
|
|
|
|
@ -653,19 +723,60 @@ async def execute_ramp_simulation(
|
|
|
|
task_id = response.json().get("task_id")
|
|
|
|
task_id = response.json().get("task_id")
|
|
|
|
if not task_id:
|
|
|
|
if not task_id:
|
|
|
|
raise RuntimeError(f"RAMP execute response did not include task_id: {response.text}")
|
|
|
|
raise RuntimeError(f"RAMP execute response did not include task_id: {response.text}")
|
|
|
|
|
|
|
|
await record_ramp_task_start(
|
|
|
|
|
|
|
|
db_session=db_session,
|
|
|
|
|
|
|
|
simulation_id=simulation_id,
|
|
|
|
|
|
|
|
model_id=model_id,
|
|
|
|
|
|
|
|
ramp_task_id=task_id,
|
|
|
|
|
|
|
|
payload=payload,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
deadline = time.monotonic() + RAMP_POLL_TIMEOUT_SECONDS
|
|
|
|
deadline = time.monotonic() + RAMP_POLL_TIMEOUT_SECONDS
|
|
|
|
last_status = None
|
|
|
|
last_status = None
|
|
|
|
|
|
|
|
last_progress = None
|
|
|
|
|
|
|
|
last_updated_at = None
|
|
|
|
|
|
|
|
last_change_at = time.monotonic()
|
|
|
|
|
|
|
|
last_successful_poll_at = time.monotonic()
|
|
|
|
while time.monotonic() < deadline:
|
|
|
|
while time.monotonic() < deadline:
|
|
|
|
status_response = await client.get(f"{_simulation_api_base()}/status/{task_id}")
|
|
|
|
try:
|
|
|
|
status_response.raise_for_status()
|
|
|
|
status_payload = await _fetch_ramp_task_status(client, task_id)
|
|
|
|
status_payload = status_response.json()
|
|
|
|
last_successful_poll_at = time.monotonic()
|
|
|
|
|
|
|
|
except httpx.HTTPError as poll_error:
|
|
|
|
|
|
|
|
if time.monotonic() - last_successful_poll_at > RAMP_STALE_TIMEOUT_SECONDS:
|
|
|
|
|
|
|
|
await _cancel_ramp_task(client, task_id)
|
|
|
|
|
|
|
|
raise TimeoutError(
|
|
|
|
|
|
|
|
f"RAMP task {task_id} status polling failed for "
|
|
|
|
|
|
|
|
f"{RAMP_STALE_TIMEOUT_SECONDS}s: {poll_error}"
|
|
|
|
|
|
|
|
) from poll_error
|
|
|
|
|
|
|
|
log.warning("Transient RAMP poll failure for task %s: %s", task_id, poll_error)
|
|
|
|
|
|
|
|
await asyncio.sleep(RAMP_POLL_INTERVAL_SECONDS)
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
previous_marker = (last_status, last_progress, last_updated_at)
|
|
|
|
last_status = status_payload.get("status")
|
|
|
|
last_status = status_payload.get("status")
|
|
|
|
|
|
|
|
progress = status_payload.get("progress")
|
|
|
|
|
|
|
|
updated_at = status_payload.get("updated_at")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (last_status, progress, updated_at) != previous_marker:
|
|
|
|
|
|
|
|
last_change_at = time.monotonic()
|
|
|
|
|
|
|
|
last_progress = progress
|
|
|
|
|
|
|
|
last_updated_at = updated_at
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
|
|
last_status == "running"
|
|
|
|
|
|
|
|
and RAMP_STALE_TIMEOUT_SECONDS > 0
|
|
|
|
|
|
|
|
and time.monotonic() - last_change_at > RAMP_STALE_TIMEOUT_SECONDS
|
|
|
|
|
|
|
|
):
|
|
|
|
|
|
|
|
await _cancel_ramp_task(client, task_id)
|
|
|
|
|
|
|
|
raise TimeoutError(
|
|
|
|
|
|
|
|
f"RAMP task {task_id} made no progress for "
|
|
|
|
|
|
|
|
f"{RAMP_STALE_TIMEOUT_SECONDS}s. Last progress: {progress}."
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if last_status == "completed":
|
|
|
|
if last_status == "completed":
|
|
|
|
result = status_payload.get("result")
|
|
|
|
result = status_payload.get("result")
|
|
|
|
if not isinstance(result, dict):
|
|
|
|
if not isinstance(result, dict):
|
|
|
|
raise RuntimeError(f"RAMP task {task_id} completed without a result payload.")
|
|
|
|
result = await _fetch_completed_ramp_result(client, task_id)
|
|
|
|
await save_ramp_simulation_result(
|
|
|
|
await save_ramp_simulation_result(
|
|
|
|
db_session=db_session,
|
|
|
|
db_session=db_session,
|
|
|
|
simulation_id=simulation_id,
|
|
|
|
simulation_id=simulation_id,
|
|
|
|
@ -682,4 +793,5 @@ async def execute_ramp_simulation(
|
|
|
|
|
|
|
|
|
|
|
|
await asyncio.sleep(RAMP_POLL_INTERVAL_SECONDS)
|
|
|
|
await asyncio.sleep(RAMP_POLL_INTERVAL_SECONDS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await _cancel_ramp_task(client, task_id)
|
|
|
|
raise TimeoutError(f"RAMP task timed out after {RAMP_POLL_TIMEOUT_SECONDS}s. Last status: {last_status}")
|
|
|
|
raise TimeoutError(f"RAMP task timed out after {RAMP_POLL_TIMEOUT_SECONDS}s. Last status: {last_status}")
|
|
|
|
|