fred-oss 0.29.0__tar.gz → 0.30.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fred_oss-0.29.0/src/main/fred_oss.egg-info → fred_oss-0.30.0}/PKG-INFO +1 -1
- fred_oss-0.30.0/src/main/fred/version +1 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/interface.py +4 -4
- fred_oss-0.30.0/src/main/fred/worker/runner/backend.py +31 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/client.py +140 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/handler.py +191 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/model/_handler.py +21 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/model/_item.py +46 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/model/_request.py +41 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/model/_runner_spec.py +88 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/model/catalog.py +16 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/model/interface.py +10 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/plugins/_local.py +34 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/plugins/catalog.py +4 -1
- fred_oss-0.30.0/src/main/fred/worker/runner/plugins/interface.py +131 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/rest/routers/__init__.py +0 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/settings.py +17 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/signal.py +13 -0
- fred_oss-0.30.0/src/main/fred/worker/runner/status.py +27 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0/src/main/fred_oss.egg-info}/PKG-INFO +1 -1
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/SOURCES.txt +11 -0
- fred_oss-0.29.0/src/main/fred/version +0 -1
- fred_oss-0.29.0/src/main/fred/worker/runner/client.py +0 -101
- fred_oss-0.29.0/src/main/fred/worker/runner/handler.py +0 -136
- fred_oss-0.29.0/src/main/fred/worker/runner/plugins/_local.py +0 -26
- fred_oss-0.29.0/src/main/fred/worker/runner/plugins/interface.py +0 -152
- {fred_oss-0.29.0 → fred_oss-0.30.0}/MANIFEST.in +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/NOTICE.txt +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/README.md +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/requirements.txt +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/setup.cfg +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/setup.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/__main__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/interface.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/cli/main.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/_keyval.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/_queue.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/catalog.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/comp/interface.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/_redis.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/_stdlib.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/catalog.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/interface.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/dao/service/utils.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/_function.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/catalog.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/callback/interface.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/impl.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/result.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/settings.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/future/utils.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/cli_ext.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtime.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtimes/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtimes/scanner.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/runtimes/sync.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/wrappers/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/runpod/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/runpod/cli_ext.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/integrations/runpod/helper.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/maturity.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/_either.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/catalog.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/monad/interface.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/settings.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/utils/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/utils/dateops.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/utils/runtime.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/version.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/info.py +0 -0
- {fred_oss-0.29.0/src/main/fred/worker/runner/plugins → fred_oss-0.30.0/src/main/fred/worker/runner/model}/__init__.py +0 -0
- {fred_oss-0.29.0/src/main/fred/worker/runner/rest → fred_oss-0.30.0/src/main/fred/worker/runner/plugins}/__init__.py +0 -0
- {fred_oss-0.29.0/src/main/fred/worker/runner/rest/routers → fred_oss-0.30.0/src/main/fred/worker/runner/rest}/__init__.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/cli_ext.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/routers/_runner.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/routers/catalog.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/routers/interface.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/rest/server.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred/worker/runner/utils.py +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/dependency_links.txt +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/entry_points.txt +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/requires.txt +0 -0
- {fred_oss-0.29.0 → fred_oss-0.30.0}/src/main/fred_oss.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.30.0
|
|
@@ -139,14 +139,14 @@ class HandlerInterface:
|
|
|
139
139
|
return json.loads(metadata_serialized)
|
|
140
140
|
|
|
141
141
|
@overload
|
|
142
|
-
def run(self, event: dict, as_future: bool = True) -> Future[dict]:
|
|
142
|
+
def run(self, event: dict, as_future: bool = True, future_id: Optional[str] = None) -> Future[dict]:
|
|
143
143
|
...
|
|
144
144
|
|
|
145
145
|
@overload
|
|
146
|
-
def run(self, event: dict, as_future: bool = False) -> dict:
|
|
146
|
+
def run(self, event: dict, as_future: bool = False, future_id: Optional[str] = None) -> dict:
|
|
147
147
|
...
|
|
148
148
|
|
|
149
|
-
def run(self, event: dict, as_future: bool = False) -> dict | Future[dict]:
|
|
149
|
+
def run(self, event: dict, as_future: bool = False, future_id: Optional[str] = None) -> dict | Future[dict]:
|
|
150
150
|
"""Process an incoming event and return a structured response.
|
|
151
151
|
The event is expected to be a dictionary with at least an 'id' and 'input' keys.
|
|
152
152
|
The 'input' key should contain the payload to be processed.
|
|
@@ -158,7 +158,7 @@ class HandlerInterface:
|
|
|
158
158
|
If requested as a Future, returns a Future that will resolve to the response dictionary.
|
|
159
159
|
"""
|
|
160
160
|
if as_future:
|
|
161
|
-
return Future(function=lambda: self.run(event=event, as_future=False))
|
|
161
|
+
return Future(function=lambda: self.run(event=event, as_future=False), future_id=future_id)
|
|
162
162
|
# Extract payload and event ID
|
|
163
163
|
payload = event.get("input", {})
|
|
164
164
|
job_event_identifier = event.get("id")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from fred.settings import logger_manager
|
|
4
|
+
from fred.dao.comp.catalog import FredKeyVal, FredQueue
|
|
5
|
+
from fred.dao.service.catalog import ServiceCatalog
|
|
6
|
+
|
|
7
|
+
logger = logger_manager.get_logger(name=__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=False)
|
|
11
|
+
class RunnerBackend:
|
|
12
|
+
keyval: FredKeyVal
|
|
13
|
+
queue: FredQueue
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def auto(cls, service_name: str, **kwargs) -> 'RunnerBackend':
|
|
17
|
+
service = ServiceCatalog[service_name.upper()]
|
|
18
|
+
match service:
|
|
19
|
+
case ServiceCatalog.REDIS:
|
|
20
|
+
from fred.dao.service.utils import get_redis_configs_from_payload
|
|
21
|
+
service_kwargs = get_redis_configs_from_payload(kwargs)
|
|
22
|
+
case ServiceCatalog.STDLIB:
|
|
23
|
+
service_kwargs = {}
|
|
24
|
+
case _:
|
|
25
|
+
logger.error(f"Unknown service '{service_name}'... will attempt to use provided kwargs as-is.")
|
|
26
|
+
service_kwargs = kwargs
|
|
27
|
+
components = service.component_catalog(**service_kwargs)
|
|
28
|
+
return cls(
|
|
29
|
+
keyval=components.KEYVAL.value,
|
|
30
|
+
queue=components.QUEUE.value,
|
|
31
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fred.future import Future
|
|
6
|
+
from fred.future.result import FutureResult
|
|
7
|
+
from fred.dao.comp.catalog import FredQueue
|
|
8
|
+
from fred.worker.runner.status import RunnerStatus
|
|
9
|
+
from fred.worker.runner.signal import RunnerSignal
|
|
10
|
+
from fred.worker.runner.backend import RunnerBackend
|
|
11
|
+
from fred.worker.runner.model.catalog import RunnerModelCatalog
|
|
12
|
+
from fred.worker.runner.settings import (
|
|
13
|
+
FRD_RUNNER_BACKEND,
|
|
14
|
+
FRD_RUNNER_REQUEST_QUEUE,
|
|
15
|
+
FRD_RUNNER_RESPONSE_QUEUE,
|
|
16
|
+
)
|
|
17
|
+
from fred.settings import logger_manager
|
|
18
|
+
|
|
19
|
+
logger = logger_manager.get_logger(name=__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class RunnerClient:
|
|
24
|
+
_runner_backend: RunnerBackend
|
|
25
|
+
req_queue: FredQueue
|
|
26
|
+
res_queue: FredQueue
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def auto(
|
|
31
|
+
cls,
|
|
32
|
+
queue_slug: Optional[str] = None,
|
|
33
|
+
service_name: Optional[str] = None,
|
|
34
|
+
**kwargs
|
|
35
|
+
) -> "RunnerClient":
|
|
36
|
+
queue_slug = queue_slug or kwargs.pop("queue_slug", None) or (
|
|
37
|
+
logger.warning("Queue slug not specified; defaulting to: 'demo'")
|
|
38
|
+
or "demo"
|
|
39
|
+
)
|
|
40
|
+
queue_name_request = FRD_RUNNER_REQUEST_QUEUE or f"req:{queue_slug}"
|
|
41
|
+
queue_name_response = FRD_RUNNER_RESPONSE_QUEUE or f"res:{queue_slug}"
|
|
42
|
+
runner_backend = RunnerBackend.auto(
|
|
43
|
+
service_name=service_name or FRD_RUNNER_BACKEND,
|
|
44
|
+
**kwargs
|
|
45
|
+
)
|
|
46
|
+
return cls(
|
|
47
|
+
_runner_backend=runner_backend,
|
|
48
|
+
req_queue=runner_backend.queue(name=queue_name_request),
|
|
49
|
+
res_queue=runner_backend.queue(name=queue_name_response),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def PING(self):
|
|
54
|
+
return self.signal(RunnerSignal.PING)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def STOP(self):
|
|
58
|
+
return self.signal(RunnerSignal.STOP)
|
|
59
|
+
|
|
60
|
+
def signal(self, signal: str | RunnerSignal):
|
|
61
|
+
signal = RunnerSignal[signal.upper()] if isinstance(signal, str) else signal
|
|
62
|
+
return signal.send(self.req_queue)
|
|
63
|
+
|
|
64
|
+
def runner_status(self, runner_id: str) -> RunnerStatus:
|
|
65
|
+
runner_status=self._runner_backend.keyval(
|
|
66
|
+
key=RunnerStatus.get_key(runner_id=runner_id)
|
|
67
|
+
)
|
|
68
|
+
if not (value := runner_status.get()):
|
|
69
|
+
logger.warning(f"No status found for runner_id: '{runner_id}'")
|
|
70
|
+
return RunnerStatus.UNDEFINED
|
|
71
|
+
return RunnerStatus.parse_value(value=value)
|
|
72
|
+
|
|
73
|
+
def send(
|
|
74
|
+
self,
|
|
75
|
+
item: dict,
|
|
76
|
+
req_uuid_hash: bool = False,
|
|
77
|
+
item_uuid_hash: bool = False,
|
|
78
|
+
) -> str:
|
|
79
|
+
item_instance = RunnerModelCatalog.ITEM.value.uuid(payload=item, uuid_hash=item_uuid_hash)
|
|
80
|
+
request = item_instance.as_request(
|
|
81
|
+
use_hash=req_uuid_hash,
|
|
82
|
+
request_id=item.get("request_id"),
|
|
83
|
+
)
|
|
84
|
+
request.dispatch(request_queue=self.req_queue)
|
|
85
|
+
return request.request_id
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def fetch_status(request_id: str) -> Optional[str]:
|
|
89
|
+
return FutureResult._get_status_key(future_id=request_id).get()
|
|
90
|
+
|
|
91
|
+
def pullsync(
|
|
92
|
+
self,
|
|
93
|
+
request_id: str,
|
|
94
|
+
retry_sync: int = 10,
|
|
95
|
+
retry_delay: float = 0.5,
|
|
96
|
+
**kwargs,
|
|
97
|
+
) -> Future:
|
|
98
|
+
# TODO: Once the broadcast method is implemented, we can add a warning notice regarding
|
|
99
|
+
# the pullsync metho adding extra load to the backend service
|
|
100
|
+
future = Future(
|
|
101
|
+
function=self._is_ready_for_pullsync,
|
|
102
|
+
request_id=request_id,
|
|
103
|
+
retry_sync=retry_sync,
|
|
104
|
+
retry_delay=retry_delay,
|
|
105
|
+
fail=True, # Always fail inside the future to propagate the exception
|
|
106
|
+
)
|
|
107
|
+
return future.flat_map(
|
|
108
|
+
lambda _: Future.pullsync(future_id=request_id, **kwargs)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _is_ready_for_pullsync(
|
|
112
|
+
self,
|
|
113
|
+
request_id: str,
|
|
114
|
+
retry_sync: int = 10,
|
|
115
|
+
retry_delay: float = 0.5,
|
|
116
|
+
fail: bool = False,
|
|
117
|
+
) -> bool:
|
|
118
|
+
if not self.fetch_status(request_id=request_id):
|
|
119
|
+
logger.info(f"No status found for request_id '{request_id}'")
|
|
120
|
+
retry_kwargs = {
|
|
121
|
+
"request_id": request_id,
|
|
122
|
+
"retry_sync": retry_sync - 1,
|
|
123
|
+
"retry_delay": retry_delay,
|
|
124
|
+
"fail": fail,
|
|
125
|
+
}
|
|
126
|
+
if retry_sync:
|
|
127
|
+
time.sleep(retry_delay)
|
|
128
|
+
return self._is_ready_for_pullsync(**retry_kwargs)
|
|
129
|
+
elif fail:
|
|
130
|
+
logger.error(f"Failed to fetch status for request_id '{request_id}' after retries.")
|
|
131
|
+
raise ValueError(f"No status found for request_id '{request_id}'")
|
|
132
|
+
else:
|
|
133
|
+
return False
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
def fetch_result(self, request_id: str, now: bool = False, timeout: Optional[float] = None) -> Optional[dict]:
|
|
137
|
+
future = self.pullsync(request_id=request_id)
|
|
138
|
+
if now:
|
|
139
|
+
return future.getwhatevernow()
|
|
140
|
+
return future.wait_and_resolve(timeout=timeout)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from fred.future import Future
|
|
7
|
+
from fred.monad.catalog import EitherMonad
|
|
8
|
+
from fred.utils.dateops import datetime_utcnow
|
|
9
|
+
from fred.dao.comp.catalog import FredQueue
|
|
10
|
+
from fred.worker.runner.settings import FRD_RUNNER_BACKEND
|
|
11
|
+
from fred.worker.runner.backend import RunnerBackend
|
|
12
|
+
from fred.worker.runner.model.catalog import RunnerModelCatalog
|
|
13
|
+
from fred.worker.interface import HandlerInterface
|
|
14
|
+
from fred.worker.runner.status import RunnerStatus
|
|
15
|
+
|
|
16
|
+
from fred.settings import logger_manager
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logger_manager.get_logger(name=__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=False)
|
|
23
|
+
class RunnerHandler(HandlerInterface):
|
|
24
|
+
|
|
25
|
+
def __post_init__(self):
|
|
26
|
+
super().__post_init__()
|
|
27
|
+
logger.info("Fred-Runner outer-handler initialized using Fred-Worker interface.")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _runner_process(
|
|
31
|
+
item: dict,
|
|
32
|
+
runner: HandlerInterface,
|
|
33
|
+
item_id: str,
|
|
34
|
+
request_id: str,
|
|
35
|
+
) -> Future[dict]:
|
|
36
|
+
logger.info(f"Processing item '{item_id}' provided by request-id: {request_id}")
|
|
37
|
+
return runner.run(
|
|
38
|
+
event={
|
|
39
|
+
"id": item_id,
|
|
40
|
+
"input": item
|
|
41
|
+
},
|
|
42
|
+
as_future=True,
|
|
43
|
+
future_id=request_id,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _runner_loop(
|
|
47
|
+
self,
|
|
48
|
+
runner: HandlerInterface,
|
|
49
|
+
req_queue: FredQueue,
|
|
50
|
+
lifespan: int,
|
|
51
|
+
timeout: int,
|
|
52
|
+
res_queue: Optional[FredQueue] = None,
|
|
53
|
+
) -> dict:
|
|
54
|
+
start_time = datetime_utcnow()
|
|
55
|
+
last_processed_time = datetime_utcnow()
|
|
56
|
+
while (elapsed_seconds := (datetime_utcnow() - start_time).total_seconds()):
|
|
57
|
+
if elapsed_seconds > lifespan:
|
|
58
|
+
logger.info("Lifespan exceeded; exiting runner loop.")
|
|
59
|
+
break
|
|
60
|
+
if (idle_seconds := (datetime_utcnow() - last_processed_time).total_seconds()) > timeout:
|
|
61
|
+
logger.info(f"Idle time ({idle_seconds}) exceeded timeout ({timeout}); exiting runner loop.")
|
|
62
|
+
break
|
|
63
|
+
# Fetch item from Redis queue
|
|
64
|
+
try:
|
|
65
|
+
message = req_queue.pop()
|
|
66
|
+
# If no item, iterate again
|
|
67
|
+
if not message:
|
|
68
|
+
continue
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Error fetching item from Redis queue '{req_queue}': {e}")
|
|
71
|
+
continue
|
|
72
|
+
# Handle special signals
|
|
73
|
+
match message:
|
|
74
|
+
case "STOP" | "SHUTDOWN" | "TERMINATE":
|
|
75
|
+
logger.info("Received STOP signal; exiting runner loop.")
|
|
76
|
+
break
|
|
77
|
+
case "PING":
|
|
78
|
+
logger.info("Received PING signal; continuing.")
|
|
79
|
+
last_processed_time = datetime_utcnow()
|
|
80
|
+
continue
|
|
81
|
+
case _:
|
|
82
|
+
pass
|
|
83
|
+
try:
|
|
84
|
+
item = json.loads(message)
|
|
85
|
+
item_id = item.get("item_id") or (
|
|
86
|
+
logger.warning("No item_id provided in item-payload; generating a new one using UUID5 hash.")
|
|
87
|
+
or str(uuid.uuid5(uuid.NAMESPACE_OID, message))
|
|
88
|
+
)
|
|
89
|
+
# The request_id is used as the future_id for tracking purposes
|
|
90
|
+
request_id = item.get("request_id") or (
|
|
91
|
+
logger.warning(f"No request_id provided in item-payload; using the item_id '{item_id}' instead.")
|
|
92
|
+
or item_id
|
|
93
|
+
)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
logger.error(f"Error decoding or parsing item from Redis: {e}")
|
|
96
|
+
continue
|
|
97
|
+
# Process item using runner and handle result
|
|
98
|
+
future = self._runner_process(item=item, runner=runner, item_id=item_id, request_id=request_id).map(
|
|
99
|
+
lambda res: res if isinstance(res, str) else json.dumps(res, default=str)
|
|
100
|
+
)
|
|
101
|
+
match future.wait():
|
|
102
|
+
case EitherMonad.Right(value):
|
|
103
|
+
if res_queue:
|
|
104
|
+
logger.debug(f"Processed item ID '{item_id}' on request ID '{request_id}' and pushed result to response queue.")
|
|
105
|
+
res_queue.add(value)
|
|
106
|
+
case EitherMonad.Left(error):
|
|
107
|
+
logger.error(f"Error processing item ID '{item_id}' on request ID '{request_id}': {error}")
|
|
108
|
+
continue
|
|
109
|
+
case _:
|
|
110
|
+
logger.error(f"Unexpected result processing item ID '{item_id}' on request ID '{request_id}': {future}")
|
|
111
|
+
continue
|
|
112
|
+
last_processed_time = datetime_utcnow()
|
|
113
|
+
return {
|
|
114
|
+
"started_at": start_time.isoformat(),
|
|
115
|
+
"stopped_at": datetime_utcnow().isoformat(),
|
|
116
|
+
"total_elapsed_seconds": (datetime_utcnow() - start_time).total_seconds(),
|
|
117
|
+
"last_processed_at": last_processed_time.isoformat(),
|
|
118
|
+
"idle_seconds": (datetime_utcnow() - last_processed_time).total_seconds(),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def handler(self, payload: dict) -> dict:
|
|
123
|
+
# Configure the backend service abstraction (e.g., Redis)
|
|
124
|
+
runner_backend = RunnerBackend.auto(
|
|
125
|
+
service_name=payload.pop("runner_backend", None) or FRD_RUNNER_BACKEND,
|
|
126
|
+
**payload.pop("runner_backend_configs", {}),
|
|
127
|
+
)
|
|
128
|
+
# Outer handler model instance
|
|
129
|
+
spec = RunnerModelCatalog.RUNNER_SPEC.value.from_payload(payload=payload)
|
|
130
|
+
|
|
131
|
+
# Determine request and response queues to use for this runner instance
|
|
132
|
+
req_queue = runner_backend.queue(name=spec.request_queue_name)
|
|
133
|
+
res_queue = runner_backend.queue(name=spec.response_queue_name) \
|
|
134
|
+
if spec.use_response_queue else None
|
|
135
|
+
# Get runner (inner handler) instance and ID
|
|
136
|
+
# The 'outer' handler delegates the processing of tasks to the 'inner' handler
|
|
137
|
+
# and is responsible for managing the lifecycle and updating the status of the runner
|
|
138
|
+
runner = spec.inner.get_handler()
|
|
139
|
+
runner_id = spec.runner_id
|
|
140
|
+
runner_status = runner_backend.keyval(
|
|
141
|
+
key=RunnerStatus.get_key(runner_id=runner_id)
|
|
142
|
+
)
|
|
143
|
+
logger.info(f"Starting runner with ID '{runner_id}' using request-queue '{req_queue.name}'")
|
|
144
|
+
runner_status.set(
|
|
145
|
+
value=RunnerStatus.STARTED.get_val(),
|
|
146
|
+
expire=None,
|
|
147
|
+
)
|
|
148
|
+
runner_loop = Future(
|
|
149
|
+
function=self._runner_loop,
|
|
150
|
+
runner=runner,
|
|
151
|
+
req_queue=req_queue,
|
|
152
|
+
lifespan=spec.lifetime,
|
|
153
|
+
timeout=spec.timeout,
|
|
154
|
+
res_queue=res_queue,
|
|
155
|
+
# The runner_id is used as the future_id for tracking purposes
|
|
156
|
+
future_id=runner_id,
|
|
157
|
+
)
|
|
158
|
+
runner_status.set(
|
|
159
|
+
value=RunnerStatus.RUNNING.get_val(),
|
|
160
|
+
expire=None,
|
|
161
|
+
)
|
|
162
|
+
results = {
|
|
163
|
+
"runner_id": runner_id,
|
|
164
|
+
"runner_started_at": datetime_utcnow().isoformat(),
|
|
165
|
+
"runner_pending_requests": req_queue.size(),
|
|
166
|
+
}
|
|
167
|
+
match runner_loop.wait():
|
|
168
|
+
case EitherMonad.Right(meta):
|
|
169
|
+
results["runner_status"] = "success"
|
|
170
|
+
results["metadata"] = meta
|
|
171
|
+
case EitherMonad.Left(error):
|
|
172
|
+
results["runner_status"] = "failure"
|
|
173
|
+
results["metadata"] = {
|
|
174
|
+
"error": str(error)
|
|
175
|
+
}
|
|
176
|
+
case _:
|
|
177
|
+
results["runner_status"] = "unexpected"
|
|
178
|
+
results["metadata"] = {
|
|
179
|
+
"error": "Unexpected result from runner loop"
|
|
180
|
+
}
|
|
181
|
+
results["pending_requests"] = pending_requests = req_queue.size()
|
|
182
|
+
runner_status.set(
|
|
183
|
+
value=RunnerStatus.STOPPED.get_val(str(pending_requests)),
|
|
184
|
+
expire=3600, # Keep the stopped status for 1 hour
|
|
185
|
+
)
|
|
186
|
+
if pending_requests:
|
|
187
|
+
logger.warning(f"Runner '{runner_id}' stopped with {pending_requests} pending items still in the queue: '{req_queue.name}'")
|
|
188
|
+
else:
|
|
189
|
+
logger.info(f"Runner '{runner_id}' stopped with no pending items in the queue: '{req_queue.name}'")
|
|
190
|
+
|
|
191
|
+
return results
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from fred.settings import logger_manager
|
|
4
|
+
from fred.worker.interface import HandlerInterface
|
|
5
|
+
from fred.worker.runner.model.interface import ModelInterface
|
|
6
|
+
|
|
7
|
+
logger = logger_manager.get_logger(name=__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=False)
|
|
11
|
+
class Handler(ModelInterface):
|
|
12
|
+
classname: str
|
|
13
|
+
classpath: str
|
|
14
|
+
kwargs: dict
|
|
15
|
+
|
|
16
|
+
def get_handler(self) -> HandlerInterface:
|
|
17
|
+
return HandlerInterface.find_handler(
|
|
18
|
+
handler_classname=self.classname,
|
|
19
|
+
import_pattern=self.classpath,
|
|
20
|
+
**self.kwargs,
|
|
21
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic.dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from fred.utils.dateops import datetime_utcnow
|
|
8
|
+
from fred.worker.runner.model._request import RunnerRequest
|
|
9
|
+
from fred.worker.runner.model.interface import ModelInterface
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class RunnerItem(ModelInterface):
|
|
14
|
+
item_id: str
|
|
15
|
+
item_created_at: str
|
|
16
|
+
item_payload: dict
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def uuid(cls, payload: dict, uuid_hash: bool = False) -> "RunnerItem":
|
|
20
|
+
item_id = payload.get("item_id") or (
|
|
21
|
+
str(uuid.uuid5(uuid.NAMESPACE_DNS, json.dumps(payload, default=str)))
|
|
22
|
+
if uuid_hash else str(uuid.uuid4())
|
|
23
|
+
)
|
|
24
|
+
return cls(
|
|
25
|
+
item_id=item_id,
|
|
26
|
+
item_created_at=datetime_utcnow().isoformat(),
|
|
27
|
+
item_payload=payload
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def as_payload(self) -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"item_id": self.item_id,
|
|
33
|
+
"item_created_at": self.item_created_at,
|
|
34
|
+
**self.item_payload,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def as_request(
|
|
38
|
+
self,
|
|
39
|
+
use_hash: bool = False,
|
|
40
|
+
request_id: Optional[str] = None
|
|
41
|
+
) -> RunnerRequest:
|
|
42
|
+
return RunnerRequest.uuid(
|
|
43
|
+
payload=self.as_payload(),
|
|
44
|
+
request_id=request_id,
|
|
45
|
+
uuid_hash=use_hash,
|
|
46
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic.dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from fred.dao.comp.catalog import FredQueue
|
|
8
|
+
from fred.worker.runner.model.interface import ModelInterface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class RunnerRequest(ModelInterface):
|
|
13
|
+
request_id: str
|
|
14
|
+
payload: dict
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def uuid(
|
|
18
|
+
cls,
|
|
19
|
+
payload: dict,
|
|
20
|
+
uuid_hash: bool = False,
|
|
21
|
+
request_id: Optional[str] = None,
|
|
22
|
+
) -> "RunnerRequest":
|
|
23
|
+
request_id = request_id or (
|
|
24
|
+
str(uuid.uuid5(uuid.NAMESPACE_OID, json.dumps(payload, default=str)))
|
|
25
|
+
if uuid_hash else str(uuid.uuid4())
|
|
26
|
+
)
|
|
27
|
+
return cls(request_id=request_id, payload=payload)
|
|
28
|
+
|
|
29
|
+
def as_payload(self) -> dict:
|
|
30
|
+
return {
|
|
31
|
+
"request_id": self.request_id,
|
|
32
|
+
**self.payload,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
def dispatch(self, request_queue: FredQueue, **kwargs):
|
|
36
|
+
serialization_kwargs = {
|
|
37
|
+
"default": str,
|
|
38
|
+
**kwargs
|
|
39
|
+
}
|
|
40
|
+
request = json.dumps(self.as_payload(), **serialization_kwargs)
|
|
41
|
+
return request_queue.add(request)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from fred.settings import logger_manager
|
|
5
|
+
from fred.utils.dateops import datetime_utcnow
|
|
6
|
+
from fred.worker.runner.model._handler import Handler
|
|
7
|
+
from fred.worker.runner.model.interface import ModelInterface
|
|
8
|
+
from fred.worker.runner.settings import (
|
|
9
|
+
FRD_RUNNER_REQUEST_QUEUE,
|
|
10
|
+
FRD_RUNNER_RESPONSE_QUEUE,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = logger_manager.get_logger(name=__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=False)
|
|
18
|
+
class RunnerSpec(ModelInterface):
|
|
19
|
+
runner_id: str
|
|
20
|
+
created_at: str
|
|
21
|
+
queue_slug: str
|
|
22
|
+
inner: Handler
|
|
23
|
+
use_response_queue: bool = False
|
|
24
|
+
lifetime: int = 3600 # Default to 1 hour if not specified
|
|
25
|
+
timeout: int = 30 # Default to 30 seconds if not specified
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def auto(cls, **kwargs) -> "RunnerSpec":
|
|
29
|
+
kwargs["runner_id"] = kwargs.get("runner_id", str(uuid.uuid4()))
|
|
30
|
+
return cls.from_payload(payload=kwargs)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_payload(cls, payload: dict) -> "RunnerSpec":
|
|
34
|
+
inner_handler_kwargs = {
|
|
35
|
+
"classname": payload.pop("handler_classname", None) or (
|
|
36
|
+
logger.warning("No handler_classname provided; defaulting to: 'HandlerInterface'")
|
|
37
|
+
or "HandlerInterface"
|
|
38
|
+
),
|
|
39
|
+
"classpath": payload.pop("handler_classpath", None) or (
|
|
40
|
+
logger.warning("No handler_classpath provided; defaulting to: 'fred.worker.interface'")
|
|
41
|
+
or "fred.worker.interface"
|
|
42
|
+
),
|
|
43
|
+
"kwargs": payload.pop("handler_kwargs", {}),
|
|
44
|
+
**payload.pop("handler_configs", {})
|
|
45
|
+
}
|
|
46
|
+
runner_id = payload.pop("runner_id", None) or (
|
|
47
|
+
logger.warning("No runner_id provided; generating a new one using UUID4.")
|
|
48
|
+
or str(uuid.uuid4())
|
|
49
|
+
)
|
|
50
|
+
return cls(
|
|
51
|
+
runner_id=runner_id,
|
|
52
|
+
created_at=datetime_utcnow().isoformat(),
|
|
53
|
+
queue_slug=payload.pop("queue_slug", None) or (
|
|
54
|
+
logger.warning("Queue slug not specified; defaulting to: 'demo'")
|
|
55
|
+
or "demo"
|
|
56
|
+
),
|
|
57
|
+
inner=Handler(**inner_handler_kwargs),
|
|
58
|
+
use_response_queue=payload.pop("use_response_queue", False),
|
|
59
|
+
lifetime=payload.pop("lifetime", 3600),
|
|
60
|
+
timeout=payload.pop("timeout", 30),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def as_dict(self) -> dict:
|
|
64
|
+
return {
|
|
65
|
+
"runner_id": self.runner_id,
|
|
66
|
+
"created_at": self.created_at,
|
|
67
|
+
"queue_slug": self.queue_slug,
|
|
68
|
+
"handler_classname": self.inner.classname,
|
|
69
|
+
"handler_classpath": self.inner.classpath,
|
|
70
|
+
"handler_kwargs": self.inner.kwargs,
|
|
71
|
+
"use_response_queue": self.use_response_queue,
|
|
72
|
+
"lifetime": self.lifetime,
|
|
73
|
+
"timeout": self.timeout,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def as_event(self) -> dict:
|
|
77
|
+
return {
|
|
78
|
+
"id": self.runner_id,
|
|
79
|
+
"input": self.as_dict(),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def request_queue_name(self) -> str:
|
|
84
|
+
return FRD_RUNNER_REQUEST_QUEUE or f"req:{self.queue_slug}"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def response_queue_name(self) -> str:
|
|
88
|
+
return FRD_RUNNER_RESPONSE_QUEUE or f"res:{self.queue_slug}"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
|
|
3
|
+
from fred.worker.runner.model._runner_spec import RunnerSpec
|
|
4
|
+
from fred.worker.runner.model._request import RunnerRequest
|
|
5
|
+
from fred.worker.runner.model._item import RunnerItem
|
|
6
|
+
from fred.worker.runner.model._handler import Handler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RunnerModelCatalog(enum.Enum):
|
|
10
|
+
RUNNER_SPEC = RunnerSpec
|
|
11
|
+
REQUEST = RunnerRequest
|
|
12
|
+
HANDLER = Handler
|
|
13
|
+
ITEM = RunnerItem
|
|
14
|
+
|
|
15
|
+
def __call__(self, *args, **kwargs):
|
|
16
|
+
return self.value(*args, **kwargs)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from fred.settings import logger_manager
|
|
4
|
+
from fred.worker.runner.info import RunnerInfo
|
|
5
|
+
from fred.worker.runner.handler import RunnerHandler
|
|
6
|
+
from fred.worker.runner.model._runner_spec import RunnerSpec
|
|
7
|
+
from fred.worker.runner.plugins.interface import PluginInterface
|
|
8
|
+
|
|
9
|
+
logger = logger_manager.get_logger(name=__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True, slots=True)
|
|
13
|
+
class LocalPlugin(PluginInterface):
|
|
14
|
+
|
|
15
|
+
def _execute(
|
|
16
|
+
self,
|
|
17
|
+
spec: RunnerSpec,
|
|
18
|
+
**kwargs
|
|
19
|
+
):
|
|
20
|
+
"""Execute the runner locally by directly invoking the handler's run method.
|
|
21
|
+
This method bypasses any queuing mechanism and runs the handler in the current process.
|
|
22
|
+
Args:
|
|
23
|
+
spec (RunnerSpec): The specification of the runner to execute.
|
|
24
|
+
**kwargs: Additional keyword arguments that may be used for execution.
|
|
25
|
+
"""
|
|
26
|
+
outer_handler_classname = kwargs.pop("outer_handler_classname", "RunnerHandler")
|
|
27
|
+
outer_handler_classpath = kwargs.pop("outer_handler_classpath", "fred.worker.runner.handler")
|
|
28
|
+
outer_handler_init_kwargs = kwargs.pop("outer_handler_init_kwargs", {})
|
|
29
|
+
outer_handler = RunnerHandler.find_handler(
|
|
30
|
+
handler_classname=outer_handler_classname,
|
|
31
|
+
import_pattern=outer_handler_classpath,
|
|
32
|
+
**outer_handler_init_kwargs,
|
|
33
|
+
)
|
|
34
|
+
outer_handler.run(event=spec.as_event(), as_future=False)
|
|
@@ -10,6 +10,9 @@ logger = logger_manager.get_logger(name=__name__)
|
|
|
10
10
|
class PluginCatalog(enum.Enum):
|
|
11
11
|
"""Enum for the different plugins available in FRED."""
|
|
12
12
|
|
|
13
|
-
LOCAL = LocalPlugin
|
|
13
|
+
LOCAL = LocalPlugin
|
|
14
14
|
RUNPOD = None # Placeholder for future RunPod plugin
|
|
15
15
|
LAMBDA = None # Placeholder for future AWS Lambda plugin
|
|
16
|
+
|
|
17
|
+
def __call__(self, *args, **kwargs):
|
|
18
|
+
return self.value.auto(*args, **kwargs)
|