File size: 2,536 Bytes
1a36c6b
da839c2
 
 
1a36c6b
da839c2
 
 
 
1a36c6b
 
 
da839c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a36c6b
 
 
da839c2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a36c6b
da839c2
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"""Where jobs execute: Modal (default) or RunPod serverless.

Both backends expose the same two calls, so the API does not care which one is configured:

    submit(job_input, execution_timeout_s, function) -> {"id": ..., "status": "IN_QUEUE"}
    status(job_id) -> {"status", "output"?, "error"?, "executionTime"?, "delayTime"?}

Times are in milliseconds. The worker also reports its own wall time as ``output["worker_seconds"]``,
which is used when a backend does not report execution time itself (Modal).

`function` names the worker function (see worker/modal_app.py). Modal deploys all of them; RunPod's single
endpoint image only has the MuJoCo ones (``RunPodClient.FUNCTIONS``).
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import Protocol

from .config import Settings

TERMINAL = {"COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT"}
BACKENDS = ("modal", "runpod")

log = logging.getLogger("rosdiff.runs")


class BackendError(RuntimeError):
    """The backend could not be reached or refused the request. The run's status is left unchanged."""


class SimBackend(Protocol):
    name: str

    def supports(self, function: str) -> bool: ...

    def submit(self, job_input: dict, execution_timeout_s: int, function: str) -> dict: ...

    def status(self, job_id: str) -> dict: ...


def modal_credentials_present() -> bool:
    if os.environ.get("MODAL_TOKEN_ID") and os.environ.get("MODAL_TOKEN_SECRET"):
        return True
    return Path(os.environ.get("MODAL_CONFIG_PATH", Path.home() / ".modal.toml")).is_file()


def make_backend(settings: Settings) -> SimBackend | None:
    """The configured backend, or None (with a log line saying why) when runs are not set up."""
    if settings.sim_backend == "modal":
        if not modal_credentials_present():
            log.warning("simulation runs disabled: set MODAL_TOKEN_ID/MODAL_TOKEN_SECRET or run `modal token new`")
            return None
        from .modal_client import ModalClient

        return ModalClient(settings.modal_app)
    if settings.sim_backend == "runpod":
        if not (settings.runpod_api_key and settings.runpod_endpoint_id):
            log.warning("simulation runs disabled: RUNPOD_API_KEY and RUNPOD_ENDPOINT_ID are not set")
            return None
        from .runpod_client import RunPodClient

        return RunPodClient(settings.runpod_api_key, settings.runpod_endpoint_id)
    raise ValueError(f"SIM_BACKEND must be one of {', '.join(BACKENDS)}, got {settings.sim_backend!r}")