fred-oss 0.29.0__tar.gz → 0.31.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.
Files changed (94) hide show
  1. {fred_oss-0.29.0/src/main/fred_oss.egg-info → fred_oss-0.31.0}/PKG-INFO +1 -1
  2. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/impl.py +9 -9
  3. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/utils.py +8 -8
  4. fred_oss-0.31.0/src/main/fred/version +1 -0
  5. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/interface.py +4 -4
  6. fred_oss-0.31.0/src/main/fred/worker/runner/backend.py +31 -0
  7. fred_oss-0.31.0/src/main/fred/worker/runner/client.py +144 -0
  8. fred_oss-0.31.0/src/main/fred/worker/runner/handler.py +191 -0
  9. fred_oss-0.31.0/src/main/fred/worker/runner/model/_handler.py +21 -0
  10. fred_oss-0.31.0/src/main/fred/worker/runner/model/_item.py +46 -0
  11. fred_oss-0.31.0/src/main/fred/worker/runner/model/_request.py +41 -0
  12. fred_oss-0.31.0/src/main/fred/worker/runner/model/_runner_spec.py +88 -0
  13. fred_oss-0.31.0/src/main/fred/worker/runner/model/catalog.py +16 -0
  14. fred_oss-0.31.0/src/main/fred/worker/runner/model/interface.py +10 -0
  15. fred_oss-0.31.0/src/main/fred/worker/runner/plugins/_local.py +34 -0
  16. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/plugins/catalog.py +4 -1
  17. fred_oss-0.31.0/src/main/fred/worker/runner/plugins/interface.py +131 -0
  18. fred_oss-0.31.0/src/main/fred/worker/runner/rest/routers/__init__.py +0 -0
  19. fred_oss-0.31.0/src/main/fred/worker/runner/settings.py +17 -0
  20. fred_oss-0.31.0/src/main/fred/worker/runner/signal.py +13 -0
  21. fred_oss-0.31.0/src/main/fred/worker/runner/status.py +27 -0
  22. {fred_oss-0.29.0 → fred_oss-0.31.0/src/main/fred_oss.egg-info}/PKG-INFO +1 -1
  23. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred_oss.egg-info/SOURCES.txt +11 -0
  24. fred_oss-0.29.0/src/main/fred/version +0 -1
  25. fred_oss-0.29.0/src/main/fred/worker/runner/client.py +0 -101
  26. fred_oss-0.29.0/src/main/fred/worker/runner/handler.py +0 -136
  27. fred_oss-0.29.0/src/main/fred/worker/runner/plugins/_local.py +0 -26
  28. fred_oss-0.29.0/src/main/fred/worker/runner/plugins/interface.py +0 -152
  29. {fred_oss-0.29.0 → fred_oss-0.31.0}/MANIFEST.in +0 -0
  30. {fred_oss-0.29.0 → fred_oss-0.31.0}/NOTICE.txt +0 -0
  31. {fred_oss-0.29.0 → fred_oss-0.31.0}/README.md +0 -0
  32. {fred_oss-0.29.0 → fred_oss-0.31.0}/requirements.txt +0 -0
  33. {fred_oss-0.29.0 → fred_oss-0.31.0}/setup.cfg +0 -0
  34. {fred_oss-0.29.0 → fred_oss-0.31.0}/setup.py +0 -0
  35. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/cli/__init__.py +0 -0
  36. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/cli/__main__.py +0 -0
  37. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/cli/interface.py +0 -0
  38. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/cli/main.py +0 -0
  39. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/__init__.py +0 -0
  40. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/comp/__init__.py +0 -0
  41. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/comp/_keyval.py +0 -0
  42. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/comp/_queue.py +0 -0
  43. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/comp/catalog.py +0 -0
  44. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/comp/interface.py +0 -0
  45. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/service/__init__.py +0 -0
  46. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/service/_redis.py +0 -0
  47. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/service/_stdlib.py +0 -0
  48. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/service/catalog.py +0 -0
  49. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/service/interface.py +0 -0
  50. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/dao/service/utils.py +0 -0
  51. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/__init__.py +0 -0
  52. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/callback/__init__.py +0 -0
  53. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/callback/_function.py +0 -0
  54. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/callback/catalog.py +0 -0
  55. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/callback/interface.py +0 -0
  56. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/result.py +0 -0
  57. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/future/settings.py +0 -0
  58. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/__init__.py +0 -0
  59. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/cli_ext.py +0 -0
  60. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/runtime.py +0 -0
  61. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/runtimes/__init__.py +0 -0
  62. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/runtimes/scanner.py +0 -0
  63. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/runtimes/sync.py +0 -0
  64. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/wrappers/__init__.py +0 -0
  65. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/databricks/wrappers/dbutils.py +0 -0
  66. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/runpod/__init__.py +0 -0
  67. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/runpod/cli_ext.py +0 -0
  68. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/integrations/runpod/helper.py +0 -0
  69. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/maturity.py +0 -0
  70. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/monad/__init__.py +0 -0
  71. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/monad/_either.py +0 -0
  72. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/monad/catalog.py +0 -0
  73. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/monad/interface.py +0 -0
  74. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/settings.py +0 -0
  75. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/utils/__init__.py +0 -0
  76. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/utils/dateops.py +0 -0
  77. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/utils/runtime.py +0 -0
  78. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/version.py +0 -0
  79. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/__init__.py +0 -0
  80. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/__init__.py +0 -0
  81. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/info.py +0 -0
  82. {fred_oss-0.29.0/src/main/fred/worker/runner/plugins → fred_oss-0.31.0/src/main/fred/worker/runner/model}/__init__.py +0 -0
  83. {fred_oss-0.29.0/src/main/fred/worker/runner/rest → fred_oss-0.31.0/src/main/fred/worker/runner/plugins}/__init__.py +0 -0
  84. {fred_oss-0.29.0/src/main/fred/worker/runner/rest/routers → fred_oss-0.31.0/src/main/fred/worker/runner/rest}/__init__.py +0 -0
  85. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/rest/cli_ext.py +0 -0
  86. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/rest/routers/_runner.py +0 -0
  87. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/rest/routers/catalog.py +0 -0
  88. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/rest/routers/interface.py +0 -0
  89. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/rest/server.py +0 -0
  90. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred/worker/runner/utils.py +0 -0
  91. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred_oss.egg-info/dependency_links.txt +0 -0
  92. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred_oss.egg-info/entry_points.txt +0 -0
  93. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred_oss.egg-info/requires.txt +0 -0
  94. {fred_oss-0.29.0 → fred_oss-0.31.0}/src/main/fred_oss.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fred-oss
3
- Version: 0.29.0
3
+ Version: 0.31.0
4
4
  Summary: FREDOSS
5
5
  Home-page: https://fred.fahera.mx
6
6
  Author: Fahera Research, Education, and Development
@@ -239,9 +239,9 @@ class Future(MonadInterface[A]):
239
239
  def pullsync(
240
240
  cls,
241
241
  future_id: str,
242
- delay: float = 0.001,
243
- delay_incr: float = 0.001,
244
- delay_max: float = 30,
242
+ retry_delay: float = 0.2,
243
+ retry_backoff_rate: float = 0.1,
244
+ retry_delay_max: float = 15,
245
245
  timeout: float = FRD_FUTURE_DEFAULT_EXPIRATION,
246
246
  on_complete: Optional[CallbackInterface] = None,
247
247
  **kwargs
@@ -252,9 +252,9 @@ class Future(MonadInterface[A]):
252
252
 
253
253
  Args:
254
254
  future_id (str): The unique identifier of the future to be pulled.
255
- delay (float): Initial delay between checks for the future's completion.
256
- delay_incr (float): Incremental increase in delay after each check.
257
- delay_max (float): Maximum delay between checks.
255
+ retry_delay (float): Initial delay between checks for the future's completion.
256
+ retry_backoff_rate (float): Incremental increase in delay after each check.
257
+ retry_delay_max (float): Maximum delay between checks.
258
258
  timeout (float): Maximum time to wait for the future to complete.
259
259
  on_complete (Optional[CallbackInterface]): An optional callback to be executed
260
260
  when the future completes.
@@ -267,9 +267,9 @@ class Future(MonadInterface[A]):
267
267
  return cls(
268
268
  function=lambda: pull_future_result(
269
269
  future_id=future_id,
270
- delay=delay,
271
- delay_incr=delay_incr,
272
- delay_max=delay_max,
270
+ retry_delay=retry_delay,
271
+ retry_backoff_rate=retry_backoff_rate,
272
+ retry_delay_max=retry_delay_max,
273
273
  timeout=timeout,
274
274
  ),
275
275
  on_complete=on_complete,
@@ -8,9 +8,9 @@ A = TypeVar("A")
8
8
 
9
9
  def pull_future_result(
10
10
  future_id: str,
11
- delay: float = 0.001,
12
- delay_incr: float = 0.001,
13
- delay_max: float = 30,
11
+ retry_delay: float = 0.2,
12
+ retry_backoff_rate: float = 0.1,
13
+ retry_delay_max: float = 15,
14
14
  timeout: float = FRD_FUTURE_DEFAULT_EXPIRATION,
15
15
  ) -> A:
16
16
  from fred.future.impl import FutureResult, FutureUndefinedInProgress, FutureUndefinedPending
@@ -25,15 +25,15 @@ def pull_future_result(
25
25
  return value.resolve()
26
26
  case FutureUndefinedPending() | FutureUndefinedInProgress():
27
27
  # If the future is not yet defined, wait for it to complete
28
- time.sleep(delay)
28
+ time.sleep(retry_delay)
29
29
  # Increase the delay for the next check to avoid busy waiting (exponential backoff) capped at delay_max
30
30
  return pull_future_result(
31
31
  future_id=future_id,
32
- delay=(delay + delay_incr) if delay < delay_max else delay_max,
33
- delay_incr=delay,
34
- delay_max=delay_max,
32
+ retry_delay=min(retry_delay * (1 + retry_backoff_rate), retry_delay_max),
33
+ retry_backoff_rate=retry_backoff_rate,
34
+ retry_delay_max=retry_delay_max,
35
35
  # TODO: Consider using a more precise timeout mechanism based on elapsed time
36
- timeout=timeout - delay,
36
+ timeout=timeout - retry_delay,
37
37
  )
38
38
  case _:
39
39
  raise ValueError(f"Unknown future state for ID '{future_id}'")
@@ -0,0 +1 @@
1
+ 0.31.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,144 @@
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.2,
96
+ retry_backoff_rate: float = 0.1,
97
+ **kwargs,
98
+ ) -> Future:
99
+ # TODO: Once the broadcast method is implemented, we can add a warning notice regarding
100
+ # the pullsync metho adding extra load to the backend service
101
+ future = Future(
102
+ function=self._is_ready_for_pullsync,
103
+ request_id=request_id,
104
+ retry_sync=retry_sync,
105
+ retry_delay=retry_delay,
106
+ retry_backoff_rate=retry_backoff_rate,
107
+ fail=True, # Always fail inside the future to propagate the exception
108
+ )
109
+ return future.flat_map(
110
+ lambda _: Future.pullsync(future_id=request_id, **kwargs)
111
+ )
112
+
113
+ def _is_ready_for_pullsync(
114
+ self,
115
+ request_id: str,
116
+ retry_sync: int = 10,
117
+ retry_delay: float = 0.2,
118
+ retry_backoff_rate: float = 0.1,
119
+ fail: bool = False,
120
+ ) -> bool:
121
+ if not self.fetch_status(request_id=request_id):
122
+ logger.info(f"No status found for request_id '{request_id}'")
123
+ retry_kwargs = {
124
+ "request_id": request_id,
125
+ "retry_sync": retry_sync - 1,
126
+ "retry_delay": retry_delay * (1 + retry_backoff_rate), # Exponential backoff
127
+ "retry_backoff_rate": retry_backoff_rate,
128
+ "fail": fail,
129
+ }
130
+ if retry_sync:
131
+ time.sleep(retry_delay)
132
+ return self._is_ready_for_pullsync(**retry_kwargs)
133
+ elif fail:
134
+ logger.error(f"Failed to fetch status for request_id '{request_id}' after retries.")
135
+ raise ValueError(f"No status found for request_id '{request_id}'")
136
+ else:
137
+ return False
138
+ return True
139
+
140
+ def fetch_result(self, request_id: str, now: bool = False, timeout: Optional[float] = None) -> Optional[dict]:
141
+ future = self.pullsync(request_id=request_id)
142
+ if now:
143
+ return future.getwhatevernow()
144
+ 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}"