esd-services-api-client 2.1.3__py3-none-any.whl → 2.2.1__py3-none-any.whl
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.
- esd_services_api_client/_version.py +1 -1
- esd_services_api_client/nexus/README.md +62 -37
- esd_services_api_client/nexus/abstractions/algrorithm_cache.py +100 -0
- esd_services_api_client/nexus/abstractions/input_object.py +63 -0
- esd_services_api_client/nexus/abstractions/nexus_object.py +18 -10
- esd_services_api_client/nexus/algorithms/_baseline_algorithm.py +14 -6
- esd_services_api_client/nexus/algorithms/_remote_algorithm.py +118 -0
- esd_services_api_client/nexus/algorithms/forked_algorithm.py +124 -0
- esd_services_api_client/nexus/algorithms/minimalistic.py +8 -1
- esd_services_api_client/nexus/algorithms/recursive.py +5 -1
- esd_services_api_client/nexus/core/app_core.py +9 -0
- esd_services_api_client/nexus/core/app_dependencies.py +19 -0
- esd_services_api_client/nexus/exceptions/cache_errors.py +49 -0
- esd_services_api_client/nexus/exceptions/startup_error.py +15 -0
- esd_services_api_client/nexus/input/__init__.py +0 -1
- esd_services_api_client/nexus/input/input_processor.py +11 -58
- esd_services_api_client/nexus/input/input_reader.py +9 -5
- esd_services_api_client/nexus/telemetry/__init__.py +0 -0
- esd_services_api_client/nexus/telemetry/recorder.py +97 -0
- {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/METADATA +1 -1
- {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/RECORD +23 -17
- esd_services_api_client/nexus/input/_functions.py +0 -89
- {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/LICENSE +0 -0
- {esd_services_api_client-2.1.3.dist-info → esd_services_api_client-2.2.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,124 @@
|
|
1
|
+
"""
|
2
|
+
Remotely executed algorithm
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023-2024. ECCO Sneaks & Data
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
import asyncio
|
21
|
+
from abc import abstractmethod
|
22
|
+
from functools import partial
|
23
|
+
|
24
|
+
from adapta.metrics import MetricsProvider
|
25
|
+
from adapta.utils.decorators import run_time_metrics_async
|
26
|
+
|
27
|
+
from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
|
28
|
+
from esd_services_api_client.nexus.abstractions.nexus_object import (
|
29
|
+
NexusObject,
|
30
|
+
TPayload,
|
31
|
+
AlgorithmResult,
|
32
|
+
)
|
33
|
+
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
34
|
+
from esd_services_api_client.nexus.algorithms._remote_algorithm import RemoteAlgorithm
|
35
|
+
from esd_services_api_client.nexus.input.input_processor import (
|
36
|
+
InputProcessor,
|
37
|
+
)
|
38
|
+
|
39
|
+
|
40
|
+
class ForkedAlgorithm(NexusObject[TPayload, AlgorithmResult]):
|
41
|
+
"""
|
42
|
+
Forked algorithm is an algorithm that returns a result (main scenario run) and then fires off one or more forked runs
|
43
|
+
with different configurations as specified in fork class implementation.
|
44
|
+
|
45
|
+
Forked algorithm only awaits scheduling of forked runs, but never their results.
|
46
|
+
|
47
|
+
Q: How do I spawn a ForkedAlgorithm run as a remote algorithm w/o ending in an infinite loop?
|
48
|
+
A: Provide class names for forks from your algorithm configuration and construct forks with locate(fork_class)(**kwargs) calls.
|
49
|
+
|
50
|
+
Q: Can I build execution trees with this?
|
51
|
+
A: Yes, they will look like this (F(N) - Forked with N forks):
|
52
|
+
|
53
|
+
graph TB
|
54
|
+
F3["F(3)"] --> F2["F(2)"]
|
55
|
+
F3 --> F0["F(0)"]
|
56
|
+
F3 --> F1["F(1)"]
|
57
|
+
F2 --> F1_1["F(1)"]
|
58
|
+
F2 --> F0_1["F(0)"]
|
59
|
+
F1 --> F0_2["F(0)"]
|
60
|
+
F1_1 --> F0_3["F(0)"]
|
61
|
+
"""
|
62
|
+
|
63
|
+
def __init__(
|
64
|
+
self,
|
65
|
+
metrics_provider: MetricsProvider,
|
66
|
+
logger_factory: LoggerFactory,
|
67
|
+
forks: list[RemoteAlgorithm],
|
68
|
+
*input_processors: InputProcessor,
|
69
|
+
cache: InputCache,
|
70
|
+
):
|
71
|
+
super().__init__(metrics_provider, logger_factory)
|
72
|
+
self._input_processors = input_processors
|
73
|
+
self._forks = forks
|
74
|
+
self._cache = cache
|
75
|
+
|
76
|
+
@abstractmethod
|
77
|
+
async def _run(self, **kwargs) -> AlgorithmResult:
|
78
|
+
"""
|
79
|
+
Core logic for this algorithm. Implementing this method is mandatory.
|
80
|
+
"""
|
81
|
+
|
82
|
+
@property
|
83
|
+
def _metric_tags(self) -> dict[str, str]:
|
84
|
+
return {"algorithm": self.__class__.alias()}
|
85
|
+
|
86
|
+
async def run(self, **kwargs) -> AlgorithmResult:
|
87
|
+
"""
|
88
|
+
Coroutine that executes the algorithm logic.
|
89
|
+
"""
|
90
|
+
|
91
|
+
@run_time_metrics_async(
|
92
|
+
metric_name="algorthm_run",
|
93
|
+
on_finish_message_template="Finished running algorithm {algorithm} in {elapsed:.2f}s seconds",
|
94
|
+
template_args={
|
95
|
+
"algorithm": self.__class__.alias().upper(),
|
96
|
+
},
|
97
|
+
)
|
98
|
+
async def _measured_run(**run_args) -> AlgorithmResult:
|
99
|
+
return await self._run(**run_args)
|
100
|
+
|
101
|
+
if len(self._forks) > 0:
|
102
|
+
self._logger.info(
|
103
|
+
"This algorithm has forks attached: {forks}. They will be executed after the main run",
|
104
|
+
forks=",".join([fork.alias() for fork in self._forks]),
|
105
|
+
)
|
106
|
+
else:
|
107
|
+
self._logger.info(
|
108
|
+
"This algorithm supports forks but none were injected. Proceeding with a main run only"
|
109
|
+
)
|
110
|
+
|
111
|
+
results = await self._cache.resolve(*self._input_processors, **kwargs)
|
112
|
+
|
113
|
+
run_result = await partial(
|
114
|
+
_measured_run,
|
115
|
+
**results,
|
116
|
+
metric_tags=self._metric_tags,
|
117
|
+
metrics_provider=self._metrics_provider,
|
118
|
+
logger=self._logger,
|
119
|
+
)()
|
120
|
+
|
121
|
+
# now await callback scheduling
|
122
|
+
await asyncio.wait([fork.run(**kwargs) for fork in self._forks])
|
123
|
+
|
124
|
+
return run_result
|
@@ -22,6 +22,7 @@ from abc import ABC
|
|
22
22
|
from adapta.metrics import MetricsProvider
|
23
23
|
from injector import inject
|
24
24
|
|
25
|
+
from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
|
25
26
|
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
26
27
|
from esd_services_api_client.nexus.abstractions.nexus_object import TPayload
|
27
28
|
from esd_services_api_client.nexus.algorithms._baseline_algorithm import (
|
@@ -41,5 +42,11 @@ class MinimalisticAlgorithm(BaselineAlgorithm[TPayload], ABC):
|
|
41
42
|
metrics_provider: MetricsProvider,
|
42
43
|
logger_factory: LoggerFactory,
|
43
44
|
*input_processors: InputProcessor,
|
45
|
+
cache: InputCache,
|
44
46
|
):
|
45
|
-
super().__init__(
|
47
|
+
super().__init__(
|
48
|
+
metrics_provider,
|
49
|
+
logger_factory,
|
50
|
+
*input_processors,
|
51
|
+
cache=cache,
|
52
|
+
)
|
@@ -23,6 +23,7 @@ from abc import abstractmethod
|
|
23
23
|
from adapta.metrics import MetricsProvider
|
24
24
|
from injector import inject
|
25
25
|
|
26
|
+
from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
|
26
27
|
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
27
28
|
from esd_services_api_client.nexus.abstractions.nexus_object import (
|
28
29
|
TPayload,
|
@@ -45,8 +46,11 @@ class RecursiveAlgorithm(BaselineAlgorithm[TPayload]):
|
|
45
46
|
metrics_provider: MetricsProvider,
|
46
47
|
logger_factory: LoggerFactory,
|
47
48
|
*input_processors: InputProcessor,
|
49
|
+
cache: InputCache,
|
48
50
|
):
|
49
|
-
super().__init__(
|
51
|
+
super().__init__(
|
52
|
+
metrics_provider, logger_factory, *input_processors, cache=cache
|
53
|
+
)
|
50
54
|
|
51
55
|
@abstractmethod
|
52
56
|
async def _is_finished(self, **kwargs) -> bool:
|
@@ -58,6 +58,7 @@ from esd_services_api_client.nexus.input.payload_reader import (
|
|
58
58
|
AlgorithmPayloadReader,
|
59
59
|
AlgorithmPayload,
|
60
60
|
)
|
61
|
+
from esd_services_api_client.nexus.telemetry.recorder import TelemetryRecorder
|
61
62
|
|
62
63
|
|
63
64
|
def is_transient_exception(exception: Optional[BaseException]) -> Optional[bool]:
|
@@ -244,9 +245,11 @@ class Nexus:
|
|
244
245
|
"""
|
245
246
|
Activates the run sequence.
|
246
247
|
"""
|
248
|
+
|
247
249
|
self._injector = Injector(self._configurator.injection_binds)
|
248
250
|
|
249
251
|
algorithm: BaselineAlgorithm = self._injector.get(self._algorithm_class)
|
252
|
+
telemetry_recorder: TelemetryRecorder = self._injector.get(TelemetryRecorder)
|
250
253
|
|
251
254
|
async with algorithm as instance:
|
252
255
|
self._algorithm_run_task = asyncio.create_task(
|
@@ -266,6 +269,12 @@ class Nexus:
|
|
266
269
|
if len(on_complete_tasks) > 0:
|
267
270
|
await asyncio.wait(on_complete_tasks)
|
268
271
|
|
272
|
+
# record telemetry
|
273
|
+
async with telemetry_recorder as recorder:
|
274
|
+
await recorder.record(
|
275
|
+
run_id=self._run_args.request_id, **algorithm.inputs
|
276
|
+
)
|
277
|
+
|
269
278
|
# dispose of QES instance gracefully as it might hold open connections
|
270
279
|
qes = self._injector.get(QueryEnabledStore)
|
271
280
|
qes.close()
|
@@ -28,6 +28,7 @@ from adapta.storage.query_enabled_store import QueryEnabledStore
|
|
28
28
|
from injector import Module, singleton, provider
|
29
29
|
|
30
30
|
from esd_services_api_client.crystal import CrystalConnector
|
31
|
+
from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
|
31
32
|
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
32
33
|
from esd_services_api_client.nexus.abstractions.socket_provider import (
|
33
34
|
ExternalSocketProvider,
|
@@ -43,6 +44,7 @@ from esd_services_api_client.nexus.input.input_reader import InputReader
|
|
43
44
|
from esd_services_api_client.nexus.input.payload_reader import (
|
44
45
|
AlgorithmPayload,
|
45
46
|
)
|
47
|
+
from esd_services_api_client.nexus.telemetry.recorder import TelemetryRecorder
|
46
48
|
|
47
49
|
|
48
50
|
@final
|
@@ -165,6 +167,21 @@ class ExternalSocketsModule(Module):
|
|
165
167
|
)
|
166
168
|
|
167
169
|
|
170
|
+
@final
|
171
|
+
class CacheModule(Module):
|
172
|
+
"""
|
173
|
+
Storage client module.
|
174
|
+
"""
|
175
|
+
|
176
|
+
@singleton
|
177
|
+
@provider
|
178
|
+
def provide(self) -> InputCache:
|
179
|
+
"""
|
180
|
+
Dependency provider.
|
181
|
+
"""
|
182
|
+
return InputCache()
|
183
|
+
|
184
|
+
|
168
185
|
@final
|
169
186
|
class ServiceConfigurator:
|
170
187
|
"""
|
@@ -178,6 +195,8 @@ class ServiceConfigurator:
|
|
178
195
|
QueryEnabledStoreModule(),
|
179
196
|
StorageClientModule(),
|
180
197
|
ExternalSocketsModule(),
|
198
|
+
CacheModule(),
|
199
|
+
type(f"{TelemetryRecorder.__name__}Module", (Module,), {})(),
|
181
200
|
]
|
182
201
|
|
183
202
|
@property
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""
|
2
|
+
Cache module exceptions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Copyright (c) 2023-2024. ECCO Sneaks & Data
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
from esd_services_api_client.nexus.exceptions._nexus_error import (
|
21
|
+
FatalNexusError,
|
22
|
+
TransientNexusError,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
class FatalCachingError(FatalNexusError):
|
27
|
+
"""
|
28
|
+
Cache-level exception that shuts down the Nexus.
|
29
|
+
"""
|
30
|
+
|
31
|
+
def __init__(self, failed_object: str):
|
32
|
+
super().__init__()
|
33
|
+
self._failed_object = failed_object
|
34
|
+
|
35
|
+
def __str__(self) -> str:
|
36
|
+
return f"Nexus object with alias '{self._failed_object}' failed the caching operation that cannot be retried. Review traceback for more information"
|
37
|
+
|
38
|
+
|
39
|
+
class TransientCachingError(TransientNexusError):
|
40
|
+
"""
|
41
|
+
Cache-level exception that will initiate a retry with the Nexus re-activation.
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, failed_object):
|
45
|
+
super().__init__()
|
46
|
+
self._failed_object = failed_object
|
47
|
+
|
48
|
+
def __str__(self) -> str:
|
49
|
+
return f"Nexus object with alias '{self._failed_object}' failed the caching operation and it will be retried. Review traceback for more information"
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
App startup exceptions.
|
3
3
|
"""
|
4
|
+
from typing import Type
|
4
5
|
|
5
6
|
# Copyright (c) 2023-2024. ECCO Sneaks & Data
|
6
7
|
#
|
@@ -46,3 +47,17 @@ class FatalStartupConfigurationError(FatalNexusError):
|
|
46
47
|
|
47
48
|
def __str__(self) -> str:
|
48
49
|
return f"Algorithm initialization failed due to a missing configuration entry: {self._missing_entry}."
|
50
|
+
|
51
|
+
|
52
|
+
class FatalAlgorithmConfigurationError(FatalNexusError):
|
53
|
+
"""
|
54
|
+
Service configuration error that shuts down the Nexus.
|
55
|
+
"""
|
56
|
+
|
57
|
+
def __init__(self, message: str, algorithm_class: Type):
|
58
|
+
super().__init__()
|
59
|
+
self._message = message
|
60
|
+
self._type_name = str(algorithm_class)
|
61
|
+
|
62
|
+
def __str__(self) -> str:
|
63
|
+
return f"Algorithm {self._type_name} misconfigured: {self._message}."
|
@@ -1,7 +1,6 @@
|
|
1
1
|
"""
|
2
2
|
Input processing.
|
3
3
|
"""
|
4
|
-
import asyncio
|
5
4
|
|
6
5
|
# Copyright (c) 2023-2024. ECCO Sneaks & Data
|
7
6
|
#
|
@@ -25,22 +24,17 @@ from typing import Optional
|
|
25
24
|
from adapta.metrics import MetricsProvider
|
26
25
|
from adapta.utils.decorators import run_time_metrics_async
|
27
26
|
|
27
|
+
from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
|
28
|
+
from esd_services_api_client.nexus.abstractions.input_object import InputObject
|
28
29
|
from esd_services_api_client.nexus.abstractions.nexus_object import (
|
29
|
-
NexusObject,
|
30
30
|
TPayload,
|
31
31
|
TResult,
|
32
32
|
)
|
33
33
|
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
34
|
-
from esd_services_api_client.nexus.input._functions import (
|
35
|
-
resolve_readers,
|
36
|
-
resolve_reader_exc_type,
|
37
|
-
)
|
38
34
|
from esd_services_api_client.nexus.input.input_reader import InputReader
|
39
35
|
|
40
|
-
_processor_cache = {}
|
41
|
-
|
42
36
|
|
43
|
-
class InputProcessor(
|
37
|
+
class InputProcessor(InputObject[TPayload, TResult]):
|
44
38
|
"""
|
45
39
|
Base class for raw data processing into algorithm input.
|
46
40
|
"""
|
@@ -51,24 +45,23 @@ class InputProcessor(NexusObject[TPayload, TResult]):
|
|
51
45
|
payload: TPayload,
|
52
46
|
metrics_provider: MetricsProvider,
|
53
47
|
logger_factory: LoggerFactory,
|
48
|
+
cache: InputCache
|
54
49
|
):
|
55
50
|
super().__init__(metrics_provider, logger_factory)
|
56
51
|
self._readers = readers
|
57
52
|
self._payload = payload
|
58
53
|
self._result: Optional[TResult] = None
|
59
|
-
|
60
|
-
async def _read_input(self) -> dict[str, TResult]:
|
61
|
-
return await resolve_readers(*self._readers)
|
54
|
+
self._cache = cache
|
62
55
|
|
63
56
|
@property
|
64
|
-
def
|
57
|
+
def data(self) -> Optional[TResult]:
|
65
58
|
"""
|
66
59
|
Data returned by this processor
|
67
60
|
"""
|
68
61
|
return self._result
|
69
62
|
|
70
63
|
@abstractmethod
|
71
|
-
async def _process_input(self, **kwargs) ->
|
64
|
+
async def _process_input(self, **kwargs) -> TResult:
|
72
65
|
"""
|
73
66
|
Input processing logic. Implement this method to prepare data for your algorithm code.
|
74
67
|
"""
|
@@ -77,7 +70,7 @@ class InputProcessor(NexusObject[TPayload, TResult]):
|
|
77
70
|
def _metric_tags(self) -> dict[str, str]:
|
78
71
|
return {"processor": self.__class__.alias()}
|
79
72
|
|
80
|
-
async def
|
73
|
+
async def process(self, **kwargs) -> TResult:
|
81
74
|
"""
|
82
75
|
Input processing coroutine. Do not override this method.
|
83
76
|
"""
|
@@ -89,8 +82,9 @@ class InputProcessor(NexusObject[TPayload, TResult]):
|
|
89
82
|
"processor": self.__class__.alias().upper(),
|
90
83
|
},
|
91
84
|
)
|
92
|
-
async def _process(**_) ->
|
93
|
-
|
85
|
+
async def _process(**_) -> TResult:
|
86
|
+
readers = await self._cache.resolve(*self._readers)
|
87
|
+
return await self._process_input(**(kwargs | readers))
|
94
88
|
|
95
89
|
if self._result is None:
|
96
90
|
self._result = await partial(
|
@@ -101,44 +95,3 @@ class InputProcessor(NexusObject[TPayload, TResult]):
|
|
101
95
|
)()
|
102
96
|
|
103
97
|
return self._result
|
104
|
-
|
105
|
-
|
106
|
-
async def resolve_processors(
|
107
|
-
*processors: InputProcessor[TPayload, TResult], **kwargs
|
108
|
-
) -> dict[str, dict[str, TResult]]:
|
109
|
-
"""
|
110
|
-
Concurrently resolve `result` property of all processors by invoking their `process_input` method.
|
111
|
-
"""
|
112
|
-
|
113
|
-
def get_result(alias: str, completed_task: asyncio.Task) -> dict[str, TResult]:
|
114
|
-
reader_exc = completed_task.exception()
|
115
|
-
if reader_exc:
|
116
|
-
raise resolve_reader_exc_type(reader_exc)(alias, reader_exc) from reader_exc
|
117
|
-
|
118
|
-
return completed_task.result()
|
119
|
-
|
120
|
-
async def _process(input_processor: InputProcessor):
|
121
|
-
async with input_processor as instance:
|
122
|
-
result = await instance.process_input(**kwargs)
|
123
|
-
_processor_cache[input_processor.__class__.alias()] = result
|
124
|
-
return result
|
125
|
-
|
126
|
-
cached = {
|
127
|
-
processor.__class__.alias(): processor.result
|
128
|
-
for processor in processors
|
129
|
-
if processor.__class__.alias() in _processor_cache
|
130
|
-
}
|
131
|
-
if len(cached) == len(processors):
|
132
|
-
return cached
|
133
|
-
|
134
|
-
process_tasks: dict[str, asyncio.Task] = {
|
135
|
-
processor.__class__.alias(): asyncio.create_task(_process(processor))
|
136
|
-
for processor in processors
|
137
|
-
if processor.__class__.alias() not in _processor_cache
|
138
|
-
}
|
139
|
-
if len(process_tasks) > 0:
|
140
|
-
await asyncio.wait(fs=process_tasks.values())
|
141
|
-
|
142
|
-
return {
|
143
|
-
alias: get_result(alias, task) for alias, task in process_tasks.items()
|
144
|
-
} | cached
|
@@ -26,15 +26,16 @@ from adapta.process_communication import DataSocket
|
|
26
26
|
from adapta.storage.query_enabled_store import QueryEnabledStore
|
27
27
|
from adapta.utils.decorators import run_time_metrics_async
|
28
28
|
|
29
|
+
from esd_services_api_client.nexus.abstractions.algrorithm_cache import InputCache
|
30
|
+
from esd_services_api_client.nexus.abstractions.input_object import InputObject
|
29
31
|
from esd_services_api_client.nexus.abstractions.nexus_object import (
|
30
|
-
NexusObject,
|
31
32
|
TPayload,
|
32
33
|
TResult,
|
33
34
|
)
|
34
35
|
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
35
36
|
|
36
37
|
|
37
|
-
class InputReader(
|
38
|
+
class InputReader(InputObject[TPayload, TResult]):
|
38
39
|
"""
|
39
40
|
Base class for a raw data reader.
|
40
41
|
"""
|
@@ -47,6 +48,7 @@ class InputReader(NexusObject[TPayload, TResult]):
|
|
47
48
|
payload: TPayload,
|
48
49
|
*readers: "InputReader",
|
49
50
|
socket: Optional[DataSocket] = None,
|
51
|
+
cache: InputCache
|
50
52
|
):
|
51
53
|
super().__init__(metrics_provider, logger_factory)
|
52
54
|
self.socket = socket
|
@@ -54,6 +56,7 @@ class InputReader(NexusObject[TPayload, TResult]):
|
|
54
56
|
self._data: Optional[TResult] = None
|
55
57
|
self._readers = readers
|
56
58
|
self._payload = payload
|
59
|
+
self._cache = cache
|
57
60
|
|
58
61
|
@property
|
59
62
|
def data(self) -> Optional[TResult]:
|
@@ -63,7 +66,7 @@ class InputReader(NexusObject[TPayload, TResult]):
|
|
63
66
|
return self._data
|
64
67
|
|
65
68
|
@abstractmethod
|
66
|
-
async def _read_input(self) -> TResult:
|
69
|
+
async def _read_input(self, **kwargs) -> TResult:
|
67
70
|
"""
|
68
71
|
Actual data reader logic. Implementing this method is mandatory for the reader to work
|
69
72
|
"""
|
@@ -72,7 +75,7 @@ class InputReader(NexusObject[TPayload, TResult]):
|
|
72
75
|
def _metric_tags(self) -> dict[str, str]:
|
73
76
|
return {"entity": self.__class__.alias()}
|
74
77
|
|
75
|
-
async def
|
78
|
+
async def process(self, **_) -> TResult:
|
76
79
|
"""
|
77
80
|
Coroutine that reads the data from external store and converts it to a dataframe, or generates data locally. Do not override this method.
|
78
81
|
"""
|
@@ -88,7 +91,8 @@ class InputReader(NexusObject[TPayload, TResult]):
|
|
88
91
|
| ({"data_path": self.socket.data_path} if self.socket else {}),
|
89
92
|
)
|
90
93
|
async def _read(**_) -> TResult:
|
91
|
-
|
94
|
+
readers = await self._cache.resolve(*self._readers)
|
95
|
+
return await self._read_input(**readers)
|
92
96
|
|
93
97
|
if self._data is None:
|
94
98
|
self._data = await partial(
|
File without changes
|
@@ -0,0 +1,97 @@
|
|
1
|
+
"""
|
2
|
+
Telemetry recording module.
|
3
|
+
"""
|
4
|
+
import asyncio
|
5
|
+
import os
|
6
|
+
from functools import partial
|
7
|
+
from typing import final
|
8
|
+
|
9
|
+
import pandas as pd
|
10
|
+
from adapta.metrics import MetricsProvider
|
11
|
+
from adapta.process_communication import DataSocket
|
12
|
+
from adapta.storage.blob.base import StorageClient
|
13
|
+
from adapta.storage.models.format import (
|
14
|
+
DictJsonSerializationFormat,
|
15
|
+
DataFrameParquetSerializationFormat,
|
16
|
+
)
|
17
|
+
from injector import inject, singleton
|
18
|
+
|
19
|
+
from esd_services_api_client.nexus.abstractions.logger_factory import LoggerFactory
|
20
|
+
from esd_services_api_client.nexus.abstractions.nexus_object import NexusCoreObject
|
21
|
+
|
22
|
+
|
23
|
+
@final
|
24
|
+
@singleton
|
25
|
+
class TelemetryRecorder(NexusCoreObject):
|
26
|
+
"""
|
27
|
+
Class for instantiating a telemetry recorder that will save all algorithm inputs (run method arguments) to a persistent location.
|
28
|
+
"""
|
29
|
+
|
30
|
+
async def _context_open(self):
|
31
|
+
pass
|
32
|
+
|
33
|
+
async def _context_close(self):
|
34
|
+
pass
|
35
|
+
|
36
|
+
@inject
|
37
|
+
def __init__(
|
38
|
+
self,
|
39
|
+
storage_client: StorageClient,
|
40
|
+
metrics_provider: MetricsProvider,
|
41
|
+
logger_factory: LoggerFactory,
|
42
|
+
):
|
43
|
+
super().__init__(metrics_provider, logger_factory)
|
44
|
+
self._storage_client = storage_client
|
45
|
+
self._telemetry_base_path = os.getenv("NEXUS__TELEMETRY_PATH")
|
46
|
+
|
47
|
+
async def record(self, run_id: str, **telemetry_args):
|
48
|
+
"""
|
49
|
+
Record all data in telemetry args for the provided run_id.
|
50
|
+
"""
|
51
|
+
|
52
|
+
async def _record(
|
53
|
+
entity_to_record: pd.DataFrame | dict,
|
54
|
+
entity_name: str,
|
55
|
+
**_,
|
56
|
+
) -> None:
|
57
|
+
self._logger.debug(
|
58
|
+
"Recording telemetry for {entity_name} in the run {run_id}",
|
59
|
+
entity_name=entity_name,
|
60
|
+
run_id=run_id,
|
61
|
+
)
|
62
|
+
self._storage_client.save_data_as_blob(
|
63
|
+
data=entity_to_record,
|
64
|
+
blob_path=DataSocket(
|
65
|
+
alias="telemetry",
|
66
|
+
data_path=f"{self._telemetry_base_path}/{entity_name}/{run_id}",
|
67
|
+
data_format="null",
|
68
|
+
).parse_data_path(),
|
69
|
+
serialization_format=DictJsonSerializationFormat
|
70
|
+
if isinstance(entity_to_record, dict)
|
71
|
+
else DataFrameParquetSerializationFormat,
|
72
|
+
overwrite=True,
|
73
|
+
)
|
74
|
+
|
75
|
+
telemetry_tasks = [
|
76
|
+
asyncio.create_task(
|
77
|
+
partial(
|
78
|
+
_record,
|
79
|
+
entity_to_record=telemetry_value,
|
80
|
+
entity_name=telemetry_key,
|
81
|
+
run_id=run_id,
|
82
|
+
)()
|
83
|
+
)
|
84
|
+
for telemetry_key, telemetry_value in telemetry_args.items()
|
85
|
+
]
|
86
|
+
|
87
|
+
done, pending = await asyncio.wait(telemetry_tasks)
|
88
|
+
if len(pending) > 0:
|
89
|
+
self._logger.warning(
|
90
|
+
"Some telemetry recording operations did not complete within specified time. This run might lack observability coverage."
|
91
|
+
)
|
92
|
+
for done_telemetry_task in done:
|
93
|
+
telemetry_exc = done_telemetry_task.exception()
|
94
|
+
if telemetry_exc:
|
95
|
+
self._logger.warning(
|
96
|
+
"Telemetry recoding failed", exception=telemetry_exc
|
97
|
+
)
|