runnable 0.4.0__py3-none-any.whl → 0.5.0__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.
- runnable/cli.py +1 -4
- runnable/context.py +0 -2
- runnable/datastore.py +0 -4
- runnable/defaults.py +1 -1
- runnable/entrypoints.py +3 -16
- runnable/executor.py +1 -41
- runnable/extensions/executor/__init__.py +4 -98
- runnable/extensions/executor/mocked/implementation.py +1 -26
- runnable/extensions/executor/retry/__init__.py +0 -0
- runnable/extensions/executor/retry/implementation.py +305 -0
- runnable/extensions/run_log_store/file_system/implementation.py +0 -2
- runnable/extensions/run_log_store/generic_chunked.py +0 -2
- runnable/sdk.py +0 -1
- {runnable-0.4.0.dist-info → runnable-0.5.0.dist-info}/METADATA +1 -1
- {runnable-0.4.0.dist-info → runnable-0.5.0.dist-info}/RECORD +18 -16
- {runnable-0.4.0.dist-info → runnable-0.5.0.dist-info}/entry_points.txt +1 -0
- {runnable-0.4.0.dist-info → runnable-0.5.0.dist-info}/LICENSE +0 -0
- {runnable-0.4.0.dist-info → runnable-0.5.0.dist-info}/WHEEL +0 -0
runnable/cli.py
CHANGED
@@ -41,8 +41,7 @@ def cli():
|
|
41
41
|
)
|
42
42
|
@click.option("--tag", default="", help="A tag attached to the run")
|
43
43
|
@click.option("--run-id", help="An optional run_id, one would be generated if not provided")
|
44
|
-
|
45
|
-
def execute(file, config_file, parameters_file, log_level, tag, run_id, use_cached): # pragma: no cover
|
44
|
+
def execute(file, config_file, parameters_file, log_level, tag, run_id): # pragma: no cover
|
46
45
|
"""
|
47
46
|
Execute a pipeline
|
48
47
|
|
@@ -59,7 +58,6 @@ def execute(file, config_file, parameters_file, log_level, tag, run_id, use_cach
|
|
59
58
|
[default: ]
|
60
59
|
--run-id TEXT An optional run_id, one would be generated if not
|
61
60
|
provided
|
62
|
-
--use-cached TEXT Provide the previous run_id to re-run.
|
63
61
|
"""
|
64
62
|
logger.setLevel(log_level)
|
65
63
|
entrypoints.execute(
|
@@ -67,7 +65,6 @@ def execute(file, config_file, parameters_file, log_level, tag, run_id, use_cach
|
|
67
65
|
pipeline_file=file,
|
68
66
|
tag=tag,
|
69
67
|
run_id=run_id,
|
70
|
-
use_cached=use_cached,
|
71
68
|
parameters_file=parameters_file,
|
72
69
|
)
|
73
70
|
|
runnable/context.py
CHANGED
runnable/datastore.py
CHANGED
@@ -169,9 +169,7 @@ class RunLog(BaseModel):
|
|
169
169
|
|
170
170
|
run_id: str
|
171
171
|
dag_hash: Optional[str] = None
|
172
|
-
use_cached: bool = False
|
173
172
|
tag: Optional[str] = ""
|
174
|
-
original_run_id: Optional[str] = ""
|
175
173
|
status: str = defaults.FAIL
|
176
174
|
steps: OrderedDict[str, StepLog] = Field(default_factory=OrderedDict)
|
177
175
|
parameters: Dict[str, Any] = Field(default_factory=dict)
|
@@ -659,9 +657,7 @@ class BufferRunLogstore(BaseRunLogStore):
|
|
659
657
|
self.run_log = RunLog(
|
660
658
|
run_id=run_id,
|
661
659
|
dag_hash=dag_hash,
|
662
|
-
use_cached=use_cached,
|
663
660
|
tag=tag,
|
664
|
-
original_run_id=original_run_id,
|
665
661
|
status=status,
|
666
662
|
)
|
667
663
|
return self.run_log
|
runnable/defaults.py
CHANGED
@@ -35,7 +35,7 @@ class ServiceConfig(TypedDict):
|
|
35
35
|
config: Mapping[str, Any]
|
36
36
|
|
37
37
|
|
38
|
-
class
|
38
|
+
class RunnableConfig(TypedDict, total=False):
|
39
39
|
run_log_store: Optional[ServiceConfig]
|
40
40
|
secrets: Optional[ServiceConfig]
|
41
41
|
catalog: Optional[ServiceConfig]
|
runnable/entrypoints.py
CHANGED
@@ -9,12 +9,12 @@ from rich import print
|
|
9
9
|
|
10
10
|
import runnable.context as context
|
11
11
|
from runnable import defaults, graph, utils
|
12
|
-
from runnable.defaults import
|
12
|
+
from runnable.defaults import RunnableConfig, ServiceConfig
|
13
13
|
|
14
14
|
logger = logging.getLogger(defaults.LOGGER_NAME)
|
15
15
|
|
16
16
|
|
17
|
-
def get_default_configs() ->
|
17
|
+
def get_default_configs() -> RunnableConfig:
|
18
18
|
"""
|
19
19
|
User can provide extensions as part of their code base, runnable-config.yaml provides the place to put them.
|
20
20
|
"""
|
@@ -37,7 +37,6 @@ def prepare_configurations(
|
|
37
37
|
configuration_file: str = "",
|
38
38
|
pipeline_file: str = "",
|
39
39
|
tag: str = "",
|
40
|
-
use_cached: str = "",
|
41
40
|
parameters_file: str = "",
|
42
41
|
force_local_executor: bool = False,
|
43
42
|
) -> context.Context:
|
@@ -51,7 +50,6 @@ def prepare_configurations(
|
|
51
50
|
pipeline_file (str): The config/dag file
|
52
51
|
run_id (str): The run id of the run.
|
53
52
|
tag (str): If a tag is provided at the run time
|
54
|
-
use_cached (str): Provide the run_id of the older run
|
55
53
|
|
56
54
|
Returns:
|
57
55
|
executor.BaseExecutor : A prepared executor as per the dag/config
|
@@ -64,7 +62,7 @@ def prepare_configurations(
|
|
64
62
|
if configuration_file:
|
65
63
|
templated_configuration = utils.load_yaml(configuration_file) or {}
|
66
64
|
|
67
|
-
configuration:
|
65
|
+
configuration: RunnableConfig = cast(RunnableConfig, templated_configuration)
|
68
66
|
|
69
67
|
# Run log settings, configuration over-rides everything
|
70
68
|
run_log_config: Optional[ServiceConfig] = configuration.get("run_log_store", None)
|
@@ -141,11 +139,6 @@ def prepare_configurations(
|
|
141
139
|
run_context.pipeline_file = pipeline_file
|
142
140
|
run_context.dag = dag
|
143
141
|
|
144
|
-
run_context.use_cached = False
|
145
|
-
if use_cached:
|
146
|
-
run_context.use_cached = True
|
147
|
-
run_context.original_run_id = use_cached
|
148
|
-
|
149
142
|
context.run_context = run_context
|
150
143
|
|
151
144
|
return run_context
|
@@ -156,7 +149,6 @@ def execute(
|
|
156
149
|
pipeline_file: str,
|
157
150
|
tag: str = "",
|
158
151
|
run_id: str = "",
|
159
|
-
use_cached: str = "",
|
160
152
|
parameters_file: str = "",
|
161
153
|
):
|
162
154
|
# pylint: disable=R0914,R0913
|
@@ -168,10 +160,8 @@ def execute(
|
|
168
160
|
pipeline_file (str): The config/dag file
|
169
161
|
run_id (str): The run id of the run.
|
170
162
|
tag (str): If a tag is provided at the run time
|
171
|
-
use_cached (str): The previous run_id to use.
|
172
163
|
parameters_file (str): The parameters being sent in to the application
|
173
164
|
"""
|
174
|
-
# Re run settings
|
175
165
|
run_id = utils.generate_run_id(run_id=run_id)
|
176
166
|
|
177
167
|
run_context = prepare_configurations(
|
@@ -179,7 +169,6 @@ def execute(
|
|
179
169
|
pipeline_file=pipeline_file,
|
180
170
|
run_id=run_id,
|
181
171
|
tag=tag,
|
182
|
-
use_cached=use_cached,
|
183
172
|
parameters_file=parameters_file,
|
184
173
|
)
|
185
174
|
print("Working with context:")
|
@@ -231,7 +220,6 @@ def execute_single_node(
|
|
231
220
|
pipeline_file=pipeline_file,
|
232
221
|
run_id=run_id,
|
233
222
|
tag=tag,
|
234
|
-
use_cached="",
|
235
223
|
parameters_file=parameters_file,
|
236
224
|
)
|
237
225
|
print("Working with context:")
|
@@ -416,7 +404,6 @@ def fan(
|
|
416
404
|
pipeline_file=pipeline_file,
|
417
405
|
run_id=run_id,
|
418
406
|
tag=tag,
|
419
|
-
use_cached="",
|
420
407
|
parameters_file=parameters_file,
|
421
408
|
)
|
422
409
|
print("Working with context:")
|
runnable/executor.py
CHANGED
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict
|
|
9
9
|
|
10
10
|
import runnable.context as context
|
11
11
|
from runnable import defaults
|
12
|
-
from runnable.datastore import DataCatalog,
|
12
|
+
from runnable.datastore import DataCatalog, StepLog
|
13
13
|
from runnable.defaults import TypeMapVariable
|
14
14
|
from runnable.graph import Graph
|
15
15
|
|
@@ -36,9 +36,6 @@ class BaseExecutor(ABC, BaseModel):
|
|
36
36
|
|
37
37
|
overrides: dict = {}
|
38
38
|
|
39
|
-
# TODO: This needs to go away
|
40
|
-
_previous_run_log: Optional[RunLog] = None
|
41
|
-
_single_step: str = ""
|
42
39
|
_local: bool = False # This is a flag to indicate whether the executor is local or not.
|
43
40
|
|
44
41
|
_context_step_log = None # type : StepLog
|
@@ -60,21 +57,6 @@ class BaseExecutor(ABC, BaseModel):
|
|
60
57
|
"""
|
61
58
|
...
|
62
59
|
|
63
|
-
# TODO: This needs to go away
|
64
|
-
@abstractmethod
|
65
|
-
def _set_up_for_re_run(self, parameters: Dict[str, Any]) -> None:
|
66
|
-
"""
|
67
|
-
Set up the executor for using a previous execution.
|
68
|
-
|
69
|
-
Retrieve the older run log, error out if it does not exist.
|
70
|
-
Sync the catalogs from the previous run log with the current one.
|
71
|
-
|
72
|
-
Update the parameters of this execution with the previous one. The previous one take precedence.
|
73
|
-
|
74
|
-
Args:
|
75
|
-
parameters (Dict[str, Any]): The parameters for the current execution.
|
76
|
-
"""
|
77
|
-
|
78
60
|
@abstractmethod
|
79
61
|
def _set_up_run_log(self, exists_ok=False):
|
80
62
|
"""
|
@@ -293,28 +275,6 @@ class BaseExecutor(ABC, BaseModel):
|
|
293
275
|
"""
|
294
276
|
...
|
295
277
|
|
296
|
-
# TODO: This needs to go away
|
297
|
-
@abstractmethod
|
298
|
-
def _is_step_eligible_for_rerun(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
299
|
-
"""
|
300
|
-
In case of a re-run, this method checks to see if the previous run step status to determine if a re-run is
|
301
|
-
necessary.
|
302
|
-
* True: If its not a re-run.
|
303
|
-
* True: If its a re-run and we failed in the last run or the corresponding logs do not exist.
|
304
|
-
* False: If its a re-run and we succeeded in the last run.
|
305
|
-
|
306
|
-
Most cases, this logic need not be touched
|
307
|
-
|
308
|
-
Args:
|
309
|
-
node (Node): The node to check against re-run
|
310
|
-
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of iterable..
|
311
|
-
Defaults to None.
|
312
|
-
|
313
|
-
Returns:
|
314
|
-
bool: Eligibility for re-run. True means re-run, False means skip to the next step.
|
315
|
-
"""
|
316
|
-
...
|
317
|
-
|
318
278
|
@abstractmethod
|
319
279
|
def send_return_code(self, stage="traversal"):
|
320
280
|
"""
|
@@ -3,12 +3,12 @@ import json
|
|
3
3
|
import logging
|
4
4
|
import os
|
5
5
|
from abc import abstractmethod
|
6
|
-
from typing import Any, Dict, List, Optional
|
6
|
+
from typing import Any, Dict, List, Optional
|
7
7
|
|
8
8
|
from rich import print
|
9
9
|
|
10
10
|
from runnable import context, defaults, exceptions, integration, parameters, utils
|
11
|
-
from runnable.datastore import DataCatalog,
|
11
|
+
from runnable.datastore import DataCatalog, StepLog
|
12
12
|
from runnable.defaults import TypeMapVariable
|
13
13
|
from runnable.executor import BaseExecutor
|
14
14
|
from runnable.experiment_tracker import get_tracked_data
|
@@ -40,20 +40,6 @@ class GenericExecutor(BaseExecutor):
|
|
40
40
|
def _context(self):
|
41
41
|
return context.run_context
|
42
42
|
|
43
|
-
@property
|
44
|
-
def step_decorator_run_id(self):
|
45
|
-
"""
|
46
|
-
TODO: Experimental feature, design is not mature yet.
|
47
|
-
|
48
|
-
This function is used by the decorator function.
|
49
|
-
The design idea is we can over-ride this method in different implementations to retrieve the run_id.
|
50
|
-
But is it really intrusive to ask to set the environmental variable runnable_RUN_ID?
|
51
|
-
|
52
|
-
Returns:
|
53
|
-
_type_: _description_
|
54
|
-
"""
|
55
|
-
return os.environ.get("runnable_RUN_ID", None)
|
56
|
-
|
57
43
|
def _get_parameters(self) -> Dict[str, Any]:
|
58
44
|
"""
|
59
45
|
Consolidate the parameters from the environment variables
|
@@ -72,28 +58,6 @@ class GenericExecutor(BaseExecutor):
|
|
72
58
|
params.update(parameters.get_user_set_parameters())
|
73
59
|
return params
|
74
60
|
|
75
|
-
def _set_up_for_re_run(self, parameters: Dict[str, Any]) -> None:
|
76
|
-
try:
|
77
|
-
attempt_run_log = self._context.run_log_store.get_run_log_by_id(
|
78
|
-
run_id=self._context.original_run_id, full=False
|
79
|
-
)
|
80
|
-
except exceptions.RunLogNotFoundError as e:
|
81
|
-
msg = (
|
82
|
-
f"Expected a run log with id: {self._context.original_run_id} "
|
83
|
-
"but it does not exist in the run log store. "
|
84
|
-
"If the original execution was in a different environment, ensure that it is available in the current "
|
85
|
-
"environment."
|
86
|
-
)
|
87
|
-
logger.exception(msg)
|
88
|
-
raise Exception(msg) from e
|
89
|
-
|
90
|
-
# Sync the previous run log catalog to this one.
|
91
|
-
self._context.catalog_handler.sync_between_runs(
|
92
|
-
previous_run_id=self._context.original_run_id, run_id=self._context.run_id
|
93
|
-
)
|
94
|
-
|
95
|
-
parameters.update(cast(RunLog, attempt_run_log).parameters)
|
96
|
-
|
97
61
|
def _set_up_run_log(self, exists_ok=False):
|
98
62
|
"""
|
99
63
|
Create a run log and put that in the run log store
|
@@ -115,22 +79,16 @@ class GenericExecutor(BaseExecutor):
|
|
115
79
|
raise
|
116
80
|
|
117
81
|
# Consolidate and get the parameters
|
118
|
-
|
119
|
-
|
120
|
-
# TODO: This needs to go away
|
121
|
-
if self._context.use_cached:
|
122
|
-
self._set_up_for_re_run(parameters=parameters)
|
82
|
+
params = self._get_parameters()
|
123
83
|
|
124
84
|
self._context.run_log_store.create_run_log(
|
125
85
|
run_id=self._context.run_id,
|
126
86
|
tag=self._context.tag,
|
127
87
|
status=defaults.PROCESSING,
|
128
88
|
dag_hash=self._context.dag_hash,
|
129
|
-
use_cached=self._context.use_cached,
|
130
|
-
original_run_id=self._context.original_run_id,
|
131
89
|
)
|
132
90
|
# Any interaction with run log store attributes should happen via API if available.
|
133
|
-
self._context.run_log_store.set_parameters(run_id=self._context.run_id, parameters=
|
91
|
+
self._context.run_log_store.set_parameters(run_id=self._context.run_id, parameters=params)
|
134
92
|
|
135
93
|
# Update run_config
|
136
94
|
run_config = utils.get_run_config()
|
@@ -409,17 +367,6 @@ class GenericExecutor(BaseExecutor):
|
|
409
367
|
self._execute_node(node, map_variable=map_variable, **kwargs)
|
410
368
|
return
|
411
369
|
|
412
|
-
# TODO: This needs to go away
|
413
|
-
# In single step
|
414
|
-
if (self._single_step and not node.name == self._single_step) or not self._is_step_eligible_for_rerun(
|
415
|
-
node, map_variable=map_variable
|
416
|
-
):
|
417
|
-
# If the node name does not match, we move on to the next node.
|
418
|
-
# If previous run was successful, move on to the next step
|
419
|
-
step_log.mock = True
|
420
|
-
step_log.status = defaults.SUCCESS
|
421
|
-
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
422
|
-
return
|
423
370
|
# We call an internal function to iterate the sub graphs and execute them
|
424
371
|
if node.is_composite:
|
425
372
|
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
@@ -543,47 +490,6 @@ class GenericExecutor(BaseExecutor):
|
|
543
490
|
run_log = self._context.run_log_store.get_run_log_by_id(run_id=self._context.run_id, full=True)
|
544
491
|
print(json.dumps(run_log.model_dump(), indent=4))
|
545
492
|
|
546
|
-
# TODO: This needs to go away
|
547
|
-
def _is_step_eligible_for_rerun(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
548
|
-
"""
|
549
|
-
In case of a re-run, this method checks to see if the previous run step status to determine if a re-run is
|
550
|
-
necessary.
|
551
|
-
* True: If its not a re-run.
|
552
|
-
* True: If its a re-run and we failed in the last run or the corresponding logs do not exist.
|
553
|
-
* False: If its a re-run and we succeeded in the last run.
|
554
|
-
|
555
|
-
Most cases, this logic need not be touched
|
556
|
-
|
557
|
-
Args:
|
558
|
-
node (Node): The node to check against re-run
|
559
|
-
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of iterable..
|
560
|
-
Defaults to None.
|
561
|
-
|
562
|
-
Returns:
|
563
|
-
bool: Eligibility for re-run. True means re-run, False means skip to the next step.
|
564
|
-
"""
|
565
|
-
if self._context.use_cached:
|
566
|
-
node_step_log_name = node._get_step_log_name(map_variable=map_variable)
|
567
|
-
logger.info(f"Scanning previous run logs for node logs of: {node_step_log_name}")
|
568
|
-
|
569
|
-
try:
|
570
|
-
previous_node_log = self._context.run_log_store.get_step_log(
|
571
|
-
internal_name=node_step_log_name, run_id=self._context.original_run_id
|
572
|
-
)
|
573
|
-
except exceptions.StepLogNotFoundError:
|
574
|
-
logger.warning(f"Did not find the node {node.name} in previous run log")
|
575
|
-
return True # We should re-run the node.
|
576
|
-
|
577
|
-
logger.info(f"The original step status: {previous_node_log.status}")
|
578
|
-
|
579
|
-
if previous_node_log.status == defaults.SUCCESS:
|
580
|
-
return False # We need not run the node
|
581
|
-
|
582
|
-
logger.info(f"The new execution should start executing graph from this node {node.name}")
|
583
|
-
return True
|
584
|
-
|
585
|
-
return True
|
586
|
-
|
587
493
|
def send_return_code(self, stage="traversal"):
|
588
494
|
"""
|
589
495
|
Convenience function used by pipeline to send return code to the caller of the cli
|
@@ -32,9 +32,6 @@ class MockedExecutor(GenericExecutor):
|
|
32
32
|
def _context(self):
|
33
33
|
return context.run_context
|
34
34
|
|
35
|
-
def _set_up_for_re_run(self, parameters: Dict[str, Any]) -> None:
|
36
|
-
raise Exception("MockedExecutor does not support re-run")
|
37
|
-
|
38
35
|
def execute_from_graph(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
39
36
|
"""
|
40
37
|
This is the entry point to from the graph execution.
|
@@ -85,7 +82,7 @@ class MockedExecutor(GenericExecutor):
|
|
85
82
|
# node is not patched, so mock it
|
86
83
|
step_log.mock = True
|
87
84
|
else:
|
88
|
-
# node is
|
85
|
+
# node is patched
|
89
86
|
# command as the patch value
|
90
87
|
executable_type = node_to_send.executable.__class__
|
91
88
|
executable = create_executable(
|
@@ -94,7 +91,6 @@ class MockedExecutor(GenericExecutor):
|
|
94
91
|
node_name=node.name,
|
95
92
|
)
|
96
93
|
node_to_send.executable = executable
|
97
|
-
pass
|
98
94
|
|
99
95
|
# Executor specific way to trigger a job
|
100
96
|
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
@@ -117,27 +113,6 @@ class MockedExecutor(GenericExecutor):
|
|
117
113
|
self.prepare_for_node_execution()
|
118
114
|
self.execute_node(node=node, map_variable=map_variable, **kwargs)
|
119
115
|
|
120
|
-
# TODO: This needs to go away
|
121
|
-
def _is_step_eligible_for_rerun(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
122
|
-
"""
|
123
|
-
In case of a re-run, this method checks to see if the previous run step status to determine if a re-run is
|
124
|
-
necessary.
|
125
|
-
* True: If its not a re-run.
|
126
|
-
* True: If its a re-run and we failed in the last run or the corresponding logs do not exist.
|
127
|
-
* False: If its a re-run and we succeeded in the last run.
|
128
|
-
|
129
|
-
Most cases, this logic need not be touched
|
130
|
-
|
131
|
-
Args:
|
132
|
-
node (Node): The node to check against re-run
|
133
|
-
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of iterable..
|
134
|
-
Defaults to None.
|
135
|
-
|
136
|
-
Returns:
|
137
|
-
bool: Eligibility for re-run. True means re-run, False means skip to the next step.
|
138
|
-
"""
|
139
|
-
return True
|
140
|
-
|
141
116
|
def _resolve_executor_config(self, node: BaseNode):
|
142
117
|
"""
|
143
118
|
The overrides section can contain specific over-rides to an global executor config.
|
File without changes
|
@@ -0,0 +1,305 @@
|
|
1
|
+
import copy
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
from functools import cached_property
|
5
|
+
from typing import Any, Dict, List, Optional
|
6
|
+
|
7
|
+
from rich import print
|
8
|
+
|
9
|
+
from runnable import context, defaults, exceptions, parameters, utils
|
10
|
+
from runnable.datastore import DataCatalog, RunLog
|
11
|
+
from runnable.defaults import TypeMapVariable
|
12
|
+
from runnable.experiment_tracker import get_tracked_data
|
13
|
+
from runnable.extensions.executor import GenericExecutor
|
14
|
+
from runnable.graph import Graph
|
15
|
+
from runnable.nodes import BaseNode
|
16
|
+
|
17
|
+
logger = logging.getLogger(defaults.LOGGER_NAME)
|
18
|
+
|
19
|
+
|
20
|
+
class RetryExecutor(GenericExecutor):
|
21
|
+
"""
|
22
|
+
The skeleton of an executor class.
|
23
|
+
Any implementation of an executor should inherit this class and over-ride accordingly.
|
24
|
+
|
25
|
+
This is a loaded base class which has a lot of methods already implemented for "typical" executions.
|
26
|
+
Look at the function docs to understand how to use them appropriately.
|
27
|
+
|
28
|
+
For any implementation:
|
29
|
+
1). Who/when should the run log be set up?
|
30
|
+
2). Who/When should the step log be set up?
|
31
|
+
|
32
|
+
"""
|
33
|
+
|
34
|
+
service_name: str = "retry"
|
35
|
+
service_type: str = "executor"
|
36
|
+
run_id: str
|
37
|
+
|
38
|
+
_local: bool = True
|
39
|
+
_original_run_log: Optional[RunLog] = None
|
40
|
+
|
41
|
+
@property
|
42
|
+
def _context(self):
|
43
|
+
return context.run_context
|
44
|
+
|
45
|
+
@cached_property
|
46
|
+
def original_run_log(self):
|
47
|
+
self.original_run_log = self._context.run_log_store.get_run_log_by_id(
|
48
|
+
run_id=self.run_id,
|
49
|
+
full=True,
|
50
|
+
)
|
51
|
+
|
52
|
+
def _set_up_for_re_run(self, params: Dict[str, Any]) -> None:
|
53
|
+
# Sync the previous run log catalog to this one.
|
54
|
+
self._context.catalog_handler.sync_between_runs(previous_run_id=self.run_id, run_id=self._context.run_id)
|
55
|
+
|
56
|
+
params.update(self.original_run_log.parameters)
|
57
|
+
|
58
|
+
def _set_up_run_log(self, exists_ok=False):
|
59
|
+
"""
|
60
|
+
Create a run log and put that in the run log store
|
61
|
+
|
62
|
+
If exists_ok, we allow the run log to be already present in the run log store.
|
63
|
+
"""
|
64
|
+
super()._set_up_run_log(exists_ok=exists_ok)
|
65
|
+
|
66
|
+
params = self._get_parameters()
|
67
|
+
|
68
|
+
self._set_up_for_re_run(params=params)
|
69
|
+
|
70
|
+
def _execute_node(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
71
|
+
"""
|
72
|
+
This is the entry point when we do the actual execution of the function.
|
73
|
+
DO NOT Over-ride this function.
|
74
|
+
|
75
|
+
While in interactive execution, we just compute, in 3rd party interactive execution, we need to reach
|
76
|
+
this function.
|
77
|
+
|
78
|
+
In most cases,
|
79
|
+
* We get the corresponding step_log of the node and the parameters.
|
80
|
+
* We sync the catalog to GET any data sets that are in the catalog
|
81
|
+
* We call the execute method of the node for the actual compute and retry it as many times as asked.
|
82
|
+
* If the node succeeds, we get any of the user defined metrics provided by the user.
|
83
|
+
* We sync the catalog to PUT any data sets that are in the catalog.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
node (Node): The node to execute
|
87
|
+
map_variable (dict, optional): If the node is of a map state, map_variable is the value of the iterable.
|
88
|
+
Defaults to None.
|
89
|
+
"""
|
90
|
+
step_log = self._context.run_log_store.get_step_log(node._get_step_log_name(map_variable), self._context.run_id)
|
91
|
+
"""
|
92
|
+
By now, all the parameters are part of the run log as a dictionary.
|
93
|
+
We set them as environment variables, serialized as json strings.
|
94
|
+
"""
|
95
|
+
params = self._context.run_log_store.get_parameters(run_id=self._context.run_id)
|
96
|
+
params_copy = copy.deepcopy(params)
|
97
|
+
# This is only for the API to work.
|
98
|
+
parameters.set_user_defined_params_as_environment_variables(params)
|
99
|
+
|
100
|
+
attempt = self.step_attempt_number
|
101
|
+
logger.info(f"Trying to execute node: {node.internal_name}, attempt : {attempt}")
|
102
|
+
|
103
|
+
attempt_log = self._context.run_log_store.create_attempt_log()
|
104
|
+
self._context_step_log = step_log
|
105
|
+
self._context_node = node
|
106
|
+
|
107
|
+
data_catalogs_get: Optional[List[DataCatalog]] = self._sync_catalog(step_log, stage="get")
|
108
|
+
try:
|
109
|
+
attempt_log = node.execute(
|
110
|
+
executor=self,
|
111
|
+
mock=step_log.mock,
|
112
|
+
map_variable=map_variable,
|
113
|
+
params=params,
|
114
|
+
**kwargs,
|
115
|
+
)
|
116
|
+
except Exception as e:
|
117
|
+
# Any exception here is a runnable exception as node suppresses exceptions.
|
118
|
+
msg = "This is clearly runnable fault, please report a bug and the logs"
|
119
|
+
logger.exception(msg)
|
120
|
+
raise Exception(msg) from e
|
121
|
+
finally:
|
122
|
+
attempt_log.attempt_number = attempt
|
123
|
+
step_log.attempts.append(attempt_log)
|
124
|
+
|
125
|
+
tracked_data = get_tracked_data()
|
126
|
+
|
127
|
+
self._context.experiment_tracker.publish_data(tracked_data)
|
128
|
+
parameters_out = attempt_log.output_parameters
|
129
|
+
|
130
|
+
if attempt_log.status == defaults.FAIL:
|
131
|
+
logger.exception(f"Node: {node} failed")
|
132
|
+
step_log.status = defaults.FAIL
|
133
|
+
else:
|
134
|
+
# Mock is always set to False, bad design??
|
135
|
+
# TODO: Stub nodes should not sync back data
|
136
|
+
# TODO: Errors in catalog syncing should point to Fail step
|
137
|
+
# TODO: Even for a failed execution, the catalog can happen
|
138
|
+
step_log.status = defaults.SUCCESS
|
139
|
+
self._sync_catalog(step_log, stage="put", synced_catalogs=data_catalogs_get)
|
140
|
+
step_log.user_defined_metrics = tracked_data
|
141
|
+
|
142
|
+
diff_parameters = utils.diff_dict(params_copy, parameters_out)
|
143
|
+
self._context.run_log_store.set_parameters(self._context.run_id, diff_parameters)
|
144
|
+
|
145
|
+
# Remove the step context
|
146
|
+
parameters.get_user_set_parameters(remove=True)
|
147
|
+
self._context_step_log = None
|
148
|
+
self._context_node = None # type: ignore
|
149
|
+
self._context_metrics = {}
|
150
|
+
|
151
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
152
|
+
|
153
|
+
def execute_from_graph(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
154
|
+
"""
|
155
|
+
This is the entry point to from the graph execution.
|
156
|
+
|
157
|
+
While the self.execute_graph is responsible for traversing the graph, this function is responsible for
|
158
|
+
actual execution of the node.
|
159
|
+
|
160
|
+
If the node type is:
|
161
|
+
* task : We can delegate to _execute_node after checking the eligibility for re-run in cases of a re-run
|
162
|
+
* success: We can delegate to _execute_node
|
163
|
+
* fail: We can delegate to _execute_node
|
164
|
+
|
165
|
+
For nodes that are internally graphs:
|
166
|
+
* parallel: Delegate the responsibility of execution to the node.execute_as_graph()
|
167
|
+
* dag: Delegate the responsibility of execution to the node.execute_as_graph()
|
168
|
+
* map: Delegate the responsibility of execution to the node.execute_as_graph()
|
169
|
+
|
170
|
+
Transpilers will NEVER use this method and will NEVER call ths method.
|
171
|
+
This method should only be used by interactive executors.
|
172
|
+
|
173
|
+
Args:
|
174
|
+
node (Node): The node to execute
|
175
|
+
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of iterable.
|
176
|
+
Defaults to None.
|
177
|
+
"""
|
178
|
+
step_log = self._context.run_log_store.create_step_log(node.name, node._get_step_log_name(map_variable))
|
179
|
+
|
180
|
+
self.add_code_identities(node=node, step_log=step_log)
|
181
|
+
|
182
|
+
step_log.step_type = node.node_type
|
183
|
+
step_log.status = defaults.PROCESSING
|
184
|
+
|
185
|
+
# Add the step log to the database as per the situation.
|
186
|
+
# If its a terminal node, complete it now
|
187
|
+
if node.node_type in ["success", "fail"]:
|
188
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
189
|
+
self._execute_node(node, map_variable=map_variable, **kwargs)
|
190
|
+
return
|
191
|
+
|
192
|
+
# In single step
|
193
|
+
if not self._is_step_eligible_for_rerun(node, map_variable=map_variable):
|
194
|
+
# If the node name does not match, we move on to the next node.
|
195
|
+
# If previous run was successful, move on to the next step
|
196
|
+
step_log.mock = True
|
197
|
+
step_log.status = defaults.SUCCESS
|
198
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
199
|
+
return
|
200
|
+
|
201
|
+
# We call an internal function to iterate the sub graphs and execute them
|
202
|
+
if node.is_composite:
|
203
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
204
|
+
node.execute_as_graph(map_variable=map_variable, **kwargs)
|
205
|
+
return
|
206
|
+
|
207
|
+
# Executor specific way to trigger a job
|
208
|
+
self._context.run_log_store.add_step_log(step_log, self._context.run_id)
|
209
|
+
self.execute_node(node=node, map_variable=map_variable, **kwargs)
|
210
|
+
|
211
|
+
def execute_graph(self, dag: Graph, map_variable: TypeMapVariable = None, **kwargs):
|
212
|
+
"""
|
213
|
+
The parallelization is controlled by the nodes and not by this function.
|
214
|
+
|
215
|
+
Transpilers should over ride this method to do the translation of dag to the platform specific way.
|
216
|
+
Interactive methods should use this to traverse and execute the dag.
|
217
|
+
- Use execute_from_graph to handle sub-graphs
|
218
|
+
|
219
|
+
Logically the method should:
|
220
|
+
* Start at the dag.start_at of the dag.
|
221
|
+
* Call the self.execute_from_graph(node)
|
222
|
+
* depending upon the status of the execution, either move to the success node or failure node.
|
223
|
+
|
224
|
+
Args:
|
225
|
+
dag (Graph): The directed acyclic graph to traverse and execute.
|
226
|
+
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of the iterable.
|
227
|
+
Defaults to None.
|
228
|
+
"""
|
229
|
+
current_node = dag.start_at
|
230
|
+
previous_node = None
|
231
|
+
logger.info(f"Running the execution with {current_node}")
|
232
|
+
|
233
|
+
while True:
|
234
|
+
working_on = dag.get_node_by_name(current_node)
|
235
|
+
|
236
|
+
if previous_node == current_node:
|
237
|
+
raise Exception("Potentially running in a infinite loop")
|
238
|
+
|
239
|
+
previous_node = current_node
|
240
|
+
|
241
|
+
logger.info(f"Creating execution log for {working_on}")
|
242
|
+
self.execute_from_graph(working_on, map_variable=map_variable, **kwargs)
|
243
|
+
|
244
|
+
_, next_node_name = self._get_status_and_next_node_name(
|
245
|
+
current_node=working_on, dag=dag, map_variable=map_variable
|
246
|
+
)
|
247
|
+
|
248
|
+
if working_on.node_type in ["success", "fail"]:
|
249
|
+
break
|
250
|
+
|
251
|
+
current_node = next_node_name
|
252
|
+
|
253
|
+
run_log = self._context.run_log_store.get_branch_log(
|
254
|
+
working_on._get_branch_log_name(map_variable), self._context.run_id
|
255
|
+
)
|
256
|
+
|
257
|
+
branch = "graph"
|
258
|
+
if working_on.internal_branch_name:
|
259
|
+
branch = working_on.internal_branch_name
|
260
|
+
|
261
|
+
logger.info(f"Finished execution of the {branch} with status {run_log.status}")
|
262
|
+
|
263
|
+
# get the final run log
|
264
|
+
if branch == "graph":
|
265
|
+
run_log = self._context.run_log_store.get_run_log_by_id(run_id=self._context.run_id, full=True)
|
266
|
+
print(json.dumps(run_log.model_dump(), indent=4))
|
267
|
+
|
268
|
+
def _is_step_eligible_for_rerun(self, node: BaseNode, map_variable: TypeMapVariable = None):
|
269
|
+
"""
|
270
|
+
In case of a re-run, this method checks to see if the previous run step status to determine if a re-run is
|
271
|
+
necessary.
|
272
|
+
* True: If its not a re-run.
|
273
|
+
* True: If its a re-run and we failed in the last run or the corresponding logs do not exist.
|
274
|
+
* False: If its a re-run and we succeeded in the last run.
|
275
|
+
|
276
|
+
Most cases, this logic need not be touched
|
277
|
+
|
278
|
+
Args:
|
279
|
+
node (Node): The node to check against re-run
|
280
|
+
map_variable (dict, optional): If the node if of a map state, this corresponds to the value of iterable..
|
281
|
+
Defaults to None.
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
bool: Eligibility for re-run. True means re-run, False means skip to the next step.
|
285
|
+
"""
|
286
|
+
|
287
|
+
node_step_log_name = node._get_step_log_name(map_variable=map_variable)
|
288
|
+
logger.info(f"Scanning previous run logs for node logs of: {node_step_log_name}")
|
289
|
+
|
290
|
+
try:
|
291
|
+
previous_attempt_log, _ = self.original_run_log.search_step_by_internal_name(node_step_log_name)
|
292
|
+
except exceptions.StepLogNotFoundError:
|
293
|
+
logger.warning(f"Did not find the node {node.name} in previous run log")
|
294
|
+
return True # We should re-run the node.
|
295
|
+
|
296
|
+
logger.info(f"The original step status: {previous_attempt_log.status}")
|
297
|
+
|
298
|
+
if previous_attempt_log.status == defaults.SUCCESS:
|
299
|
+
return False # We need not run the node
|
300
|
+
|
301
|
+
logger.info(f"The new execution should start executing graph from this node {node.name}")
|
302
|
+
return True
|
303
|
+
|
304
|
+
def execute_node(self, node: BaseNode, map_variable: TypeMapVariable = None, **kwargs):
|
305
|
+
self._execute_node(node, map_variable=map_variable, **kwargs)
|
runnable/sdk.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
runnable/__init__.py,sha256=v0QgHL7uvEWKecAOJ_bVYHYM9O5B4xCICCLZVBO6Ci8,923
|
2
2
|
runnable/catalog.py,sha256=OUaQ73DWfTsMmq2sKlBn0aDz031mupladNGVuF3pWm0,3985
|
3
|
-
runnable/cli.py,sha256=
|
4
|
-
runnable/context.py,sha256=
|
5
|
-
runnable/datastore.py,sha256=
|
6
|
-
runnable/defaults.py,sha256=
|
7
|
-
runnable/entrypoints.py,sha256=
|
3
|
+
runnable/cli.py,sha256=AZiZf2eRV7zMA7APg6dyTHWqK1--bQwdLiYP8olaKis,9589
|
4
|
+
runnable/context.py,sha256=BhCycb09KkEfA5KS6HpVAr_unLEuijfJmmZ_ZdclfhY,966
|
5
|
+
runnable/datastore.py,sha256=q2hyPTOZUI4rteU9zaY3AL4YIsYSIw4Bzrt2EIrDGUc,23420
|
6
|
+
runnable/defaults.py,sha256=clBTH8f5cXcl7xG3CISyn5P8Oph0wTtYxeTY7-N8CmM,4672
|
7
|
+
runnable/entrypoints.py,sha256=S3pPvd1guC3y6qE2MndzMaxySwR1n08QwcL5_CL5ibE,15125
|
8
8
|
runnable/exceptions.py,sha256=R__RzUWs7Ow7m7yqawi2w09XXI4OqmA67yeXkECM0xw,2419
|
9
|
-
runnable/executor.py,sha256=
|
9
|
+
runnable/executor.py,sha256=BSbhlki18gTvJpMW4ZMTNJE56bTEjqNngVVjRDQZaS0,14530
|
10
10
|
runnable/experiment_tracker.py,sha256=bX2Vr73f3bsdnWqxjMSSiKA-WwqkUHfUzJQqZoQBpvY,3668
|
11
11
|
runnable/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
12
|
runnable/extensions/catalog/__init__.py,sha256=uXZ6D-Myr_J4HnBA4F5Hd7LZ0IAjQiFQYxRhMzejhQc,761
|
@@ -15,7 +15,7 @@ runnable/extensions/catalog/file_system/implementation.py,sha256=UNrJFV_tyMpknFK
|
|
15
15
|
runnable/extensions/catalog/k8s_pvc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
16
|
runnable/extensions/catalog/k8s_pvc/implementation.py,sha256=oJDDI0APT7lrtjWmzYJRDHLGn3Vhbn2MdFSRYvFBUpY,436
|
17
17
|
runnable/extensions/catalog/k8s_pvc/integration.py,sha256=OfrHbNFN8sR-wsVa4os3ajmWJFSd5H4KOHGVAmjRZTQ,1850
|
18
|
-
runnable/extensions/executor/__init__.py,sha256=
|
18
|
+
runnable/extensions/executor/__init__.py,sha256=HMDZx8XRuEAvr_WDgueBjFDfkuEtlaD1U3lM4bbxW1U,26559
|
19
19
|
runnable/extensions/executor/argo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
20
|
runnable/extensions/executor/argo/implementation.py,sha256=wlDSD5RfZmrdQ65abXTZSdh4KUGv-IzQtbHVtDXNUgQ,43795
|
21
21
|
runnable/extensions/executor/argo/specification.yaml,sha256=wXQcm2gOQYqy-IOQIhucohS32ZrHKCfGA5zZ0RraPYc,1276
|
@@ -27,7 +27,9 @@ runnable/extensions/executor/local/implementation.py,sha256=r9dSf2lSBGHihbGNhq_G
|
|
27
27
|
runnable/extensions/executor/local_container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
28
|
runnable/extensions/executor/local_container/implementation.py,sha256=6kYMgdgE5JxZkVAidxsBSpqkHvyKMfEctgZWSZQEpXA,13979
|
29
29
|
runnable/extensions/executor/mocked/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
-
runnable/extensions/executor/mocked/implementation.py,sha256=
|
30
|
+
runnable/extensions/executor/mocked/implementation.py,sha256=3dggy9vdfyNjX90xlmFaL1YH-eSu9JH16zJ8pufRXYI,6244
|
31
|
+
runnable/extensions/executor/retry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
|
+
runnable/extensions/executor/retry/implementation.py,sha256=kORV-qtu0uhYJqnieAGlE0tHzw5k_qHGlDoUhIajoOg,13078
|
31
33
|
runnable/extensions/experiment_tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
32
34
|
runnable/extensions/experiment_tracker/mlflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
33
35
|
runnable/extensions/experiment_tracker/mlflow/implementation.py,sha256=sc1Wm1LCf7wBX0BYVx3YVdwsR72AE0qIrzl7cEfIl58,3045
|
@@ -41,8 +43,8 @@ runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py,sha256=atzdTy5H
|
|
41
43
|
runnable/extensions/run_log_store/db/implementation_FF.py,sha256=oEiG5ASWYYbwlBbnryKarQENB-L_yOsnZahbj2U0GdQ,5155
|
42
44
|
runnable/extensions/run_log_store/db/integration_FF.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
43
45
|
runnable/extensions/run_log_store/file_system/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
44
|
-
runnable/extensions/run_log_store/file_system/implementation.py,sha256=
|
45
|
-
runnable/extensions/run_log_store/generic_chunked.py,sha256=
|
46
|
+
runnable/extensions/run_log_store/file_system/implementation.py,sha256=PcaM8IKcj-b2iNE9Zup2eC6Y2-987uQzzb0skdd1QX4,4114
|
47
|
+
runnable/extensions/run_log_store/generic_chunked.py,sha256=rcY5f-MIYUUiM5iQnDHICOh7cKiOUSCeaxcBG9_fz-U,19390
|
46
48
|
runnable/extensions/run_log_store/k8s_pvc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
49
|
runnable/extensions/run_log_store/k8s_pvc/implementation.py,sha256=tLgXy9HUB_vlFVQ0Itk6PpNU3GlCOILN4vA3fm80jXI,542
|
48
50
|
runnable/extensions/run_log_store/k8s_pvc/integration.py,sha256=lxQg327mwC0ykhNp5Kg34a9g8o1DzJAhfkiqMGmsABs,1873
|
@@ -58,12 +60,12 @@ runnable/names.py,sha256=vn92Kv9ANROYSZX6Z4z1v_WA3WiEdIYmG6KEStBFZug,8134
|
|
58
60
|
runnable/nodes.py,sha256=7ztYtTf4GthbutwR56lDqu4ANDLrN5zHqJNvLD_PTOo,16458
|
59
61
|
runnable/parameters.py,sha256=IT-7OUbYmRQuVtzAsG6L7Q4TkGaovmsQ4ErStcSXwmQ,6434
|
60
62
|
runnable/pickler.py,sha256=rrFRc6SMrV6Pxd9r7aMtUou8z-HLL1un4QfH_non4XE,2679
|
61
|
-
runnable/sdk.py,sha256=
|
63
|
+
runnable/sdk.py,sha256=vHxVVOrGQyHSfMPR1kNwzcsOnbn8I9yaJVg6Moaovmc,22257
|
62
64
|
runnable/secrets.py,sha256=dakb7WRloWVo-KpQp6Vy4rwFdGi58BTlT4OifQY106I,2324
|
63
65
|
runnable/tasks.py,sha256=eG-L8mB5Kp4m-HChwvcZkXkueS9IA7VQ-tQKmr87lrQ,13604
|
64
66
|
runnable/utils.py,sha256=jfgx2_lYCCKUASM7vEGZXdizRFg6EvV9pyZSlhhMKMk,19801
|
65
|
-
runnable-0.
|
66
|
-
runnable-0.
|
67
|
-
runnable-0.
|
68
|
-
runnable-0.
|
69
|
-
runnable-0.
|
67
|
+
runnable-0.5.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
68
|
+
runnable-0.5.0.dist-info/METADATA,sha256=noaJZ19MCpWfKpJ9qOg38grkhUDJ7_teJUuwUV9V3yQ,16185
|
69
|
+
runnable-0.5.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
70
|
+
runnable-0.5.0.dist-info/entry_points.txt,sha256=_elJX0RSR4u9IWIl8fwYNL-YBXoYaanYd415TJBmTRE,1710
|
71
|
+
runnable-0.5.0.dist-info/RECORD,,
|
@@ -10,6 +10,7 @@ argo=runnable.extensions.executor.argo.implementation:ArgoExecutor
|
|
10
10
|
local=runnable.extensions.executor.local.implementation:LocalExecutor
|
11
11
|
local-container=runnable.extensions.executor.local_container.implementation:LocalContainerExecutor
|
12
12
|
mocked=runnable.extensions.executor.mocked.implementation:MockedExecutor
|
13
|
+
retry=runnable.extensions.executor.retry.implementation:RetryExecutor
|
13
14
|
|
14
15
|
[experiment_tracker]
|
15
16
|
do-nothing=runnable.experiment_tracker:DoNothingTracker
|
File without changes
|
File without changes
|