marsh-lib 0.2.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.
- marsh/__init__.py +2 -0
- marsh/bash/__init__.py +3 -0
- marsh/bash/bash_factory.py +74 -0
- marsh/bash/bash_grammar.py +44 -0
- marsh/bash/bash_runner_decorators.py +0 -0
- marsh/bash/bash_script.py +71 -0
- marsh/constants.py +5 -0
- marsh/core/__init__.py +8 -0
- marsh/core/authenticator.py +7 -0
- marsh/core/cmd_run_decorator.py +289 -0
- marsh/core/command_grammar.py +45 -0
- marsh/core/connector.py +77 -0
- marsh/core/conveyor.py +105 -0
- marsh/core/executor.py +296 -0
- marsh/core/expression.py +208 -0
- marsh/core/script.py +38 -0
- marsh/dag/__init__.py +11 -0
- marsh/dag/dag.py +748 -0
- marsh/dag/node.py +52 -0
- marsh/dag/startable.py +48 -0
- marsh/docker/__init__.py +2 -0
- marsh/docker/docker_command_grammar.py +31 -0
- marsh/docker/docker_executor.py +181 -0
- marsh/exceptions.py +16 -0
- marsh/logger.py +116 -0
- marsh/modifier_functions/__init__.py +2 -0
- marsh/modifier_functions/case_conversion.py +17 -0
- marsh/modifier_functions/readers.py +11 -0
- marsh/powershell/__init__.py +0 -0
- marsh/processor_functions/__init__.py +3 -0
- marsh/processor_functions/printers.py +63 -0
- marsh/processor_functions/raisers.py +8 -0
- marsh/processor_functions/redirections.py +39 -0
- marsh/signals.py +1 -0
- marsh/ssh/__init__.py +3 -0
- marsh/ssh/ssh_command_grammar.py +50 -0
- marsh/ssh/ssh_connector.py +71 -0
- marsh/ssh/ssh_factory.py +93 -0
- marsh/utils/__init__.py +16 -0
- marsh/utils/output_streams.py +13 -0
- marsh_lib-0.2.0.dist-info/LICENSE +21 -0
- marsh_lib-0.2.0.dist-info/METADATA +380 -0
- marsh_lib-0.2.0.dist-info/RECORD +44 -0
- marsh_lib-0.2.0.dist-info/WHEEL +4 -0
marsh/dag/dag.py
ADDED
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
import multiprocessing
|
|
4
|
+
from concurrent.futures import as_completed, Future, ThreadPoolExecutor, ProcessPoolExecutor
|
|
5
|
+
from graphlib import TopologicalSorter
|
|
6
|
+
from typing import Dict, Optional, Set, List, Tuple, Any
|
|
7
|
+
|
|
8
|
+
from .startable import Startable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Dag(Startable):
|
|
12
|
+
"""
|
|
13
|
+
Base class representing a Directed Acyclic Graph (DAG) of Startable objects.
|
|
14
|
+
|
|
15
|
+
The `Dag` class manages a collection of `Startable` objects and defines their dependencies. Subclasses
|
|
16
|
+
of `Dag` implement different strategies for executing the startable objects (e.g., synchronously, asynchronously,
|
|
17
|
+
using threads or processes).
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
_startables (dict): A dictionary mapping startable names to `Startable` objects.
|
|
21
|
+
_graph (dict): A dictionary representing the dependency graph of startables.
|
|
22
|
+
_sorter (Optional[TopologicalSorter]): A topological sorter used to order the startables based on dependencies.
|
|
23
|
+
_last_startable (Optional[List[Startable]]): The list of the most recently added startables.
|
|
24
|
+
|
|
25
|
+
Methods:
|
|
26
|
+
add(dependent, *dependencies): Adds a dependent `Startable` and its dependencies to the DAG.
|
|
27
|
+
remove(startable): Removes a `Startable` from the DAG.
|
|
28
|
+
do(*startables): Adds the specified startables to the DAG and marks them as the last startables.
|
|
29
|
+
then(*dependents): Specifies the dependents of the last startables, adding them to the DAG.
|
|
30
|
+
when(*dependencies): Specifies the dependencies for the last startables.
|
|
31
|
+
start(): Starts the DAG execution (method to be overriden by subclasses).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, name):
|
|
35
|
+
"""
|
|
36
|
+
Initializes a DAG with the specified name.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name (str): The name of the DAG.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(name)
|
|
42
|
+
self._startables: Dict[str, Startable] = {}
|
|
43
|
+
self._graph: Dict[str, Set[str]] = {}
|
|
44
|
+
self._sorter: Optional[TopologicalSorter] = None
|
|
45
|
+
self._last_startable: Optional[List[Startable]] = []
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def startables(self) -> List[Startable]:
|
|
49
|
+
"""
|
|
50
|
+
Returns a list of all `Startable` objects in the DAG.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List[Startable]: A list of all `Startable` objects managed by the DAG.
|
|
54
|
+
"""
|
|
55
|
+
return list(self._startables.values())
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def names(self) -> List[str]:
|
|
59
|
+
"""
|
|
60
|
+
Returns a list of the names of all `Startable` objects in the DAG.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List[str]: A list of names of all `Startable` objects.
|
|
64
|
+
"""
|
|
65
|
+
return list(self._startables.keys())
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def sorted_names(self) -> List[str]:
|
|
69
|
+
"""
|
|
70
|
+
Returns a list of startable names in topologically sorted order.
|
|
71
|
+
|
|
72
|
+
This property depends on the dependency graph of the startables.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List[str]: A list of startable names sorted by their dependencies.
|
|
76
|
+
"""
|
|
77
|
+
sorter = self.sorter
|
|
78
|
+
if sorter is None: return []
|
|
79
|
+
return list(sorter.static_order())
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def sorter(self) -> Optional[TopologicalSorter]:
|
|
83
|
+
"""
|
|
84
|
+
Returns a TopologicalSorter instance based on the DAG's dependency graph.
|
|
85
|
+
|
|
86
|
+
This property computes a `TopologicalSorter` when the graph is not empty.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Optional[TopologicalSorter]: The topological sorter instance or `None` if the graph is empty.
|
|
90
|
+
"""
|
|
91
|
+
return TopologicalSorter(graph=self._graph) if self._graph else None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def graph(self) -> Dict[str, Set[str]]:
|
|
95
|
+
"""
|
|
96
|
+
Returns the dependency graph of startables in the DAG.
|
|
97
|
+
|
|
98
|
+
The graph is represented as a dictionary, where each key is the dependent startable name, and each value
|
|
99
|
+
is a set of names of other startable dependencies of the dependent startable.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dict[str, Set[str]]: The dependency graph of startables.
|
|
103
|
+
"""
|
|
104
|
+
return self._graph
|
|
105
|
+
|
|
106
|
+
def reset(self) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Resets the DAG, clearing all startables and dependencies.
|
|
109
|
+
|
|
110
|
+
This method clears the `_startables`, `_graph`, and `_last_startable` attributes.
|
|
111
|
+
It also resets the topological sorter.
|
|
112
|
+
"""
|
|
113
|
+
self._startables.clear()
|
|
114
|
+
self._sorter = TopologicalSorter()
|
|
115
|
+
self._graph.clear()
|
|
116
|
+
self._last_startable = []
|
|
117
|
+
|
|
118
|
+
def add(self, dependent: Startable, *dependencies: Startable) -> "Dag":
|
|
119
|
+
"""
|
|
120
|
+
Adds a dependent `Startable` and its dependencies to the DAG.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
dependent (Startable): The `Startable` to be added as a dependent in the DAG.
|
|
124
|
+
*dependencies (Startable): The `Startable` objects that the dependent depends on.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dag: The updated `Dag` instance with the newly added dependent and its dependencies.
|
|
128
|
+
"""
|
|
129
|
+
# Update the Startable Dictionary
|
|
130
|
+
self._startables[dependent.name] = dependent
|
|
131
|
+
|
|
132
|
+
for dependency in dependencies:
|
|
133
|
+
self._startables[dependency.name] = dependency
|
|
134
|
+
|
|
135
|
+
# Make sure that if the dependency is not yet registered to the graph,
|
|
136
|
+
# then add them with with default empty set for their own dependencies.
|
|
137
|
+
self._graph[dependency.name] = self._graph.get(dependency.name, set())
|
|
138
|
+
|
|
139
|
+
# Update the Graph Dictionary
|
|
140
|
+
# dependency_set: set[str] = self._graph.get(dependent.name, set())
|
|
141
|
+
# self._graph[dependent.name] = dependency_set | set([dependency.name for dependency in dependencies])
|
|
142
|
+
self._graph.setdefault(dependent.name, set()).update([d.name for d in dependencies])
|
|
143
|
+
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def remove(self, startable: Startable) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Removes a `Startable` from the DAG.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
startable (Startable): The `Startable` to be removed from the DAG.
|
|
152
|
+
"""
|
|
153
|
+
try:
|
|
154
|
+
del self._startables[startable.name]
|
|
155
|
+
except KeyError:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# Remove the startable from Graph Dictionary
|
|
159
|
+
try:
|
|
160
|
+
del self._graph[startable.name]
|
|
161
|
+
except KeyError:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Remove the startable from some dependency set of other startable in graph dictionary
|
|
165
|
+
for name, dependency_set in self._graph.items():
|
|
166
|
+
if startable.name in dependency_set:
|
|
167
|
+
self._graph[name].remove(startable.name)
|
|
168
|
+
|
|
169
|
+
def do(self, *startables: Startable) -> "Dag":
|
|
170
|
+
"""
|
|
171
|
+
Adds the specified `Startable` objects to the DAG and marks them as the last added startables.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
*startables (Startable): The `Startable` objects to be added to the DAG.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Dag: The updated `Dag` instance with the newly added startables.
|
|
178
|
+
"""
|
|
179
|
+
for startable in startables:
|
|
180
|
+
self.add(startable)
|
|
181
|
+
|
|
182
|
+
self._last_startable = startables
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def then(self, *dependents: Startable) -> "Dag":
|
|
186
|
+
"""
|
|
187
|
+
Specifies the dependents of the last added startables, adding them to the DAG.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
*dependents (Startable): The `Startable` objects that depend on the last added startables.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dag: The updated `Dag` instance with the newly added dependents.
|
|
194
|
+
"""
|
|
195
|
+
assert dependents, "Dependents cannot be empty."
|
|
196
|
+
|
|
197
|
+
for dependent in dependents:
|
|
198
|
+
self.add(dependent, *self._last_startable)
|
|
199
|
+
|
|
200
|
+
self._last_startable = dependents # Updated to avoid side effects
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def when(self, *dependencies: Startable) -> "Dag":
|
|
204
|
+
"""
|
|
205
|
+
Specifies the dependencies for the last added startables.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
*dependencies (Startable): The `Startable` objects that the last added startables depend on.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Dag: The updated `Dag` instance with the newly added dependencies.
|
|
212
|
+
"""
|
|
213
|
+
assert dependencies, "Dependencies cannot be empty."
|
|
214
|
+
|
|
215
|
+
for dependent in self._last_startable:
|
|
216
|
+
self.add(dependent, *dependencies)
|
|
217
|
+
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
def start(self) -> Dict[str, Tuple[bytes, bytes] | dict]:
|
|
221
|
+
"""
|
|
222
|
+
Starts the DAG execution.
|
|
223
|
+
|
|
224
|
+
This method should be implemented by subclasses to define how the startables are executed.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
NotImplementedError: This method must be implemented by subclasses.
|
|
228
|
+
"""
|
|
229
|
+
raise NotImplementedError("Add implementation to Dag.start")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class SyncDag(Dag):
|
|
233
|
+
"""
|
|
234
|
+
A synchronous implementation of the `Dag` class.
|
|
235
|
+
|
|
236
|
+
The `SyncDag` class executes all startable objects in the DAG sequentially, ensuring that each startable
|
|
237
|
+
is executed only after its dependencies have been completed.
|
|
238
|
+
|
|
239
|
+
Methods:
|
|
240
|
+
start(): Executes the startables sequentially and returns the results as a dictionary.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def start(self) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Starts the DAG execution synchronously.
|
|
246
|
+
|
|
247
|
+
Executes each startable in topologically sorted order and returns the results in a dictionary.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dict[str, Any]: A dictionary mapping startable names to their results.
|
|
251
|
+
"""
|
|
252
|
+
results = {}
|
|
253
|
+
for name in self.sorted_names:
|
|
254
|
+
startable = self._startables[name]
|
|
255
|
+
result = startable.start()
|
|
256
|
+
results[startable.name] = result
|
|
257
|
+
|
|
258
|
+
return results
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class AsyncDag(Dag):
|
|
262
|
+
"""
|
|
263
|
+
An asynchronous implementation of the `Dag` class using asyncio.
|
|
264
|
+
|
|
265
|
+
The `AsyncDag` class executes startable objects concurrently, using asyncio to manage coroutines and tasks
|
|
266
|
+
for parallel execution, while respecting the dependencies defined by the DAG structure.
|
|
267
|
+
|
|
268
|
+
Attributes:
|
|
269
|
+
_max_coroutines (int): The maximum number of concurrent coroutines allowed.
|
|
270
|
+
_timeout (float): The maximum time (in seconds) to wait for each startable.
|
|
271
|
+
|
|
272
|
+
Methods:
|
|
273
|
+
_worker(sorter, startable, semaphore, lock): Worker function that runs the startable asynchronously.
|
|
274
|
+
_main_loop(): Main loop that manages execution and ensures tasks are completed in dependency order.
|
|
275
|
+
start(): Starts the DAG execution asynchronously and returns the results as a dictionary.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
def __init__(self, name: str, max_coroutines: int = 20, timeout: float = 600):
|
|
279
|
+
"""
|
|
280
|
+
Initializes a AsyncDag with the specified name, maximum number of coroutines, and timeout.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
name (str): The name of the AsyncDag.
|
|
284
|
+
max_coroutines (int, optional): The maximum number of concurrent coroutines. Defaults to 20.
|
|
285
|
+
timeout (float, optional): The time limit (seconds) for running a startable. Defaults to 600.
|
|
286
|
+
"""
|
|
287
|
+
super().__init__(name)
|
|
288
|
+
self._max_coroutines = max_coroutines
|
|
289
|
+
self._timeout = timeout
|
|
290
|
+
|
|
291
|
+
async def _worker(self,
|
|
292
|
+
sorter: TopologicalSorter,
|
|
293
|
+
startable: Startable,
|
|
294
|
+
semaphore: asyncio.Semaphore,
|
|
295
|
+
lock: asyncio.Lock,
|
|
296
|
+
) -> Dict[str, Any] | Tuple[bytes, bytes]:
|
|
297
|
+
"""
|
|
298
|
+
Worker function to execute a startable asynchronously with a semaphore.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
sorter (TopologicalSorter): The sorter to update as tasks are completed.
|
|
302
|
+
startable (Startable): The startable to execute.
|
|
303
|
+
semaphore (asyncio.Semaphore): Limits the number of concurrent coroutines.
|
|
304
|
+
lock (asyncio.Lock): Ensures thread safety when updating the sorter.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Tuple[str, Any]: A tuple containing the startable name and its result.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
async with semaphore:
|
|
311
|
+
try:
|
|
312
|
+
result = await asyncio.wait_for(asyncio.to_thread(startable.start), self._timeout)
|
|
313
|
+
except TimeoutError:
|
|
314
|
+
raise TimeoutError(f"{startable.name} exceeded {self._timeout} seconds.")
|
|
315
|
+
|
|
316
|
+
# Ensure only one task updates the TopologicalSorter at a time
|
|
317
|
+
async with lock:
|
|
318
|
+
sorter.done(startable.name)
|
|
319
|
+
|
|
320
|
+
return startable.name, result
|
|
321
|
+
|
|
322
|
+
async def _main_loop(self) -> Dict[str, Any]:
|
|
323
|
+
"""
|
|
324
|
+
Main loop to execute startables concurrently in topological order.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Dict[str, Any]: A dictionary mapping startable names to their results.
|
|
328
|
+
"""
|
|
329
|
+
sorter = self.sorter
|
|
330
|
+
sorter.prepare()
|
|
331
|
+
|
|
332
|
+
semaphore = asyncio.Semaphore(self._max_coroutines) # Limit concurrent coroutines
|
|
333
|
+
lock = asyncio.Lock()
|
|
334
|
+
tasks = []
|
|
335
|
+
results = []
|
|
336
|
+
|
|
337
|
+
while sorter.is_active():
|
|
338
|
+
ready_names = sorter.get_ready()
|
|
339
|
+
for name in ready_names:
|
|
340
|
+
startable = self._startables[name]
|
|
341
|
+
task = asyncio.create_task(self._worker(sorter, startable, semaphore, lock))
|
|
342
|
+
tasks.append(task)
|
|
343
|
+
|
|
344
|
+
# Wait for at least one task to finish before continuing
|
|
345
|
+
if tasks:
|
|
346
|
+
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
347
|
+
tasks = list(pending) # Keep only pending tasks for the next iteration
|
|
348
|
+
|
|
349
|
+
# Collect results from completed tasks
|
|
350
|
+
for completed_task in done:
|
|
351
|
+
result = await completed_task
|
|
352
|
+
results.append(result)
|
|
353
|
+
|
|
354
|
+
# Ensure any remaining tasks are awaited and results collected
|
|
355
|
+
if tasks:
|
|
356
|
+
for task in tasks:
|
|
357
|
+
result = await task
|
|
358
|
+
results.append(result)
|
|
359
|
+
|
|
360
|
+
return {name: result for name, result in results}
|
|
361
|
+
|
|
362
|
+
def start(self) -> Dict[str, Any]:
|
|
363
|
+
"""
|
|
364
|
+
Starts the asynchronous DAG execution.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
Dict[str, Any]: A dictionary mapping startable names to their results.
|
|
368
|
+
"""
|
|
369
|
+
return asyncio.run(self._main_loop())
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class ThreadPoolDag(Dag):
|
|
373
|
+
"""
|
|
374
|
+
A `Dag` implementation using a thread pool.
|
|
375
|
+
|
|
376
|
+
The `ThreadPoolDag` class executes startables concurrently using a thread pool, limiting the number of threads
|
|
377
|
+
running concurrently to the specified maximum.
|
|
378
|
+
|
|
379
|
+
Attributes:
|
|
380
|
+
_max_workers (int): The maximum number of workers in the thread pool.
|
|
381
|
+
|
|
382
|
+
Methods:
|
|
383
|
+
_worker(sorter, startable, lock, results): Worker function to execute a startable in a thread.
|
|
384
|
+
start(): Starts the DAG execution using the thread pool and returns the results as a dictionary.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
def __init__(self, name: str, max_workers: int = 6):
|
|
388
|
+
"""
|
|
389
|
+
Initializes the thread pool DAG with the given name and maximum number of worker threads.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
name (str): The name of the DAG.
|
|
393
|
+
max_workers (int, optional): The maximum number of worker threads. Defaults to 6.
|
|
394
|
+
"""
|
|
395
|
+
super().__init__(name)
|
|
396
|
+
self._max_workers = max_workers
|
|
397
|
+
|
|
398
|
+
def _worker(self,
|
|
399
|
+
sorter: TopologicalSorter,
|
|
400
|
+
startable: Startable,
|
|
401
|
+
lock: threading.Lock,
|
|
402
|
+
results: Dict[str, Any]
|
|
403
|
+
) -> None:
|
|
404
|
+
"""
|
|
405
|
+
Worker function to execute a startable in a thread.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
sorter (TopologicalSorter): The sorter to update as tasks are completed.
|
|
409
|
+
startable (Startable): The startable to execute.
|
|
410
|
+
lock (threading.Lock): Ensures thread safety when updating the sorter.
|
|
411
|
+
results (dict): A dictionary to store the results of executed startables.
|
|
412
|
+
"""
|
|
413
|
+
|
|
414
|
+
result = startable.start()
|
|
415
|
+
|
|
416
|
+
with lock:
|
|
417
|
+
sorter.done(startable.name)
|
|
418
|
+
results[startable.name] = result
|
|
419
|
+
|
|
420
|
+
def start(self) -> Dict[str, Any]:
|
|
421
|
+
"""
|
|
422
|
+
Starts the DAG execution using a thread pool.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Dict[str, Any]: A dictionary mapping startable names to their results.
|
|
426
|
+
"""
|
|
427
|
+
sorter = self.sorter
|
|
428
|
+
sorter.prepare()
|
|
429
|
+
|
|
430
|
+
lock = threading.Lock()
|
|
431
|
+
results = {}
|
|
432
|
+
|
|
433
|
+
with ThreadPoolExecutor(max_workers=self._max_workers) as executor:
|
|
434
|
+
futures: list[Future] = []
|
|
435
|
+
while sorter.is_active():
|
|
436
|
+
ready_names = []
|
|
437
|
+
with lock:
|
|
438
|
+
ready_names = sorter.get_ready()
|
|
439
|
+
|
|
440
|
+
for name in ready_names:
|
|
441
|
+
startable = self._startables[name]
|
|
442
|
+
future = executor.submit(self._worker, sorter, startable, lock, results)
|
|
443
|
+
futures.append(future)
|
|
444
|
+
|
|
445
|
+
for future in as_completed(futures):
|
|
446
|
+
future.result()
|
|
447
|
+
|
|
448
|
+
return results
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class ThreadDag(Dag):
|
|
452
|
+
def __init__(self, name: str, max_workers: int = 6):
|
|
453
|
+
"""
|
|
454
|
+
Initializes the threaded DAG with the given name and maximum number of threads.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
name (str): The name of the DAG.
|
|
458
|
+
max_workers (int, optional): The maximum number of threads to run concurrently. Defaults to 6.
|
|
459
|
+
"""
|
|
460
|
+
super().__init__(name)
|
|
461
|
+
self._max_workers = max_workers
|
|
462
|
+
|
|
463
|
+
def _worker(self,
|
|
464
|
+
sorter: TopologicalSorter,
|
|
465
|
+
startable: Startable,
|
|
466
|
+
results: Dict[str, Any],
|
|
467
|
+
lock: threading.Lock,
|
|
468
|
+
semaphore: threading.Semaphore
|
|
469
|
+
) -> None:
|
|
470
|
+
"""
|
|
471
|
+
Worker method to execute a task in a thread and update results.
|
|
472
|
+
|
|
473
|
+
This method runs each task in a separate thread and updates the results and topological sorter.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
sorter (TopologicalSorter): The topological sorter for the DAG.
|
|
477
|
+
startable (Startable): The task to be executed.
|
|
478
|
+
results (Dict[str, Any]): A dictionary to store task results.
|
|
479
|
+
lock (threading.Lock): Lock to ensure thread-safe updates.
|
|
480
|
+
semaphore (threading.Semaphore): Semaphore to limit the number of concurrent threads.
|
|
481
|
+
"""
|
|
482
|
+
with semaphore:
|
|
483
|
+
result = startable.start()
|
|
484
|
+
|
|
485
|
+
with lock:
|
|
486
|
+
results[startable.name] = result
|
|
487
|
+
|
|
488
|
+
with lock:
|
|
489
|
+
sorter.done(startable.name)
|
|
490
|
+
|
|
491
|
+
def start(self) -> Dict[str, Any]:
|
|
492
|
+
"""
|
|
493
|
+
Starts the execution of tasks in the DAG using threads.
|
|
494
|
+
|
|
495
|
+
This method manages the execution of tasks using threads. It controls the maximum number of concurrent threads
|
|
496
|
+
using a semaphore and collects results in a shared dictionary.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Dict[str, Any]: A dictionary mapping task names to their results.
|
|
500
|
+
"""
|
|
501
|
+
sorter = self.sorter
|
|
502
|
+
sorter.prepare()
|
|
503
|
+
|
|
504
|
+
lock = threading.Lock()
|
|
505
|
+
semaphore = threading.Semaphore(self._max_workers) # Control the Number of Threads
|
|
506
|
+
|
|
507
|
+
results: dict[str, Any] = {}
|
|
508
|
+
workers: list[threading.Thread] = []
|
|
509
|
+
|
|
510
|
+
# Start the Loop
|
|
511
|
+
while sorter.is_active():
|
|
512
|
+
with lock:
|
|
513
|
+
ready_names = sorter.get_ready()
|
|
514
|
+
|
|
515
|
+
if ready_names:
|
|
516
|
+
for name in ready_names:
|
|
517
|
+
# Dispatch a worker only if there's room in the semaphore
|
|
518
|
+
startable = self._startables[name]
|
|
519
|
+
thread = threading.Thread(
|
|
520
|
+
target=self._worker,
|
|
521
|
+
args=(sorter, startable, results, lock, semaphore),
|
|
522
|
+
daemon=True
|
|
523
|
+
)
|
|
524
|
+
thread.start()
|
|
525
|
+
workers.append(thread)
|
|
526
|
+
|
|
527
|
+
# Wait for all threads to finish
|
|
528
|
+
for thread in workers:
|
|
529
|
+
thread.join()
|
|
530
|
+
|
|
531
|
+
return results
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class MultiprocessDag(Dag):
|
|
535
|
+
"""
|
|
536
|
+
A Directed Acyclic Graph (DAG) that executes tasks concurrently using multiple worker processes.
|
|
537
|
+
|
|
538
|
+
This class utilizes multiple processes to execute tasks in parallel, allowing for parallel computation across
|
|
539
|
+
multiple CPU cores. Each task is processed by one of the available worker processes, while a marker thread
|
|
540
|
+
tracks completed tasks and updates the DAG sorter to ensure task dependencies are respected. Tasks are
|
|
541
|
+
submitted to a queue, executed, and results are stored in a shared namespace.
|
|
542
|
+
|
|
543
|
+
Attributes:
|
|
544
|
+
_max_processes (int): The maximum number of worker processes to use for concurrent execution. Default is 4.
|
|
545
|
+
"""
|
|
546
|
+
|
|
547
|
+
def __init__(self, name: str, max_processes=4):
|
|
548
|
+
"""
|
|
549
|
+
Initializes the multiprocess DAG with the given name and maximum number of worker processes.
|
|
550
|
+
|
|
551
|
+
This class executes tasks concurrently using multiple processes, allowing parallel execution across different
|
|
552
|
+
CPUs.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
name (str): The name of the DAG.
|
|
556
|
+
max_processes (int, optional): The maximum number of worker processes. Defaults to 4.
|
|
557
|
+
"""
|
|
558
|
+
super().__init__(name)
|
|
559
|
+
self._max_processes = max_processes
|
|
560
|
+
|
|
561
|
+
@staticmethod
|
|
562
|
+
def _worker(ns, lock, startable_queue, done_queue) -> None:
|
|
563
|
+
"""
|
|
564
|
+
Worker method that processes tasks from the startable_queue and stores results in the shared namespace.
|
|
565
|
+
|
|
566
|
+
Each worker takes tasks from the queue, processes them, and stores the result in a thread-safe dictionary.
|
|
567
|
+
Once the task is completed, it is put into the done_queue to be marked as done for sorter.
|
|
568
|
+
|
|
569
|
+
Args:
|
|
570
|
+
ns (multiprocessing.Namespace): Shared namespace to store task results.
|
|
571
|
+
lock (multiprocessing.Lock): A lock to synchronize access to the shared results.
|
|
572
|
+
startable_queue (multiprocessing.Queue): Queue of startable tasks to be processed.
|
|
573
|
+
done_queue (multiprocessing.Queue): Queue to track completed tasks.
|
|
574
|
+
"""
|
|
575
|
+
while True:
|
|
576
|
+
if not startable_queue.empty():
|
|
577
|
+
startable = startable_queue.get()
|
|
578
|
+
result = startable.start()
|
|
579
|
+
|
|
580
|
+
with lock:
|
|
581
|
+
ns.results[startable.name] = result
|
|
582
|
+
|
|
583
|
+
done_queue.put(startable)
|
|
584
|
+
startable_queue.task_done()
|
|
585
|
+
|
|
586
|
+
@staticmethod
|
|
587
|
+
def _marker(sorter, lock, done_queue, stop_event) -> None:
|
|
588
|
+
"""
|
|
589
|
+
Marker thread that tracks task completion and updates the sorter.
|
|
590
|
+
|
|
591
|
+
The marker thread continually monitors the done_queue for completed tasks and updates the DAG sorter
|
|
592
|
+
to mark tasks as done, ensuring the DAG is correctly processed.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
sorter (TopologicalSorter): The sorter managing task dependencies.
|
|
596
|
+
lock (multiprocessing.Lock): A lock to synchronize access to the sorter.
|
|
597
|
+
done_queue (multiprocessing.Queue): Queue tracking tasks that have been completed.
|
|
598
|
+
stop_event (multiprocessing.Event): Event to signal when the process should stop.
|
|
599
|
+
"""
|
|
600
|
+
while not (stop_event.is_set() and done_queue.empty() and not sorter.is_active()):
|
|
601
|
+
if not done_queue.empty():
|
|
602
|
+
startable = done_queue.get()
|
|
603
|
+
with lock:
|
|
604
|
+
sorter.done(startable.name)
|
|
605
|
+
done_queue.task_done()
|
|
606
|
+
|
|
607
|
+
def start(self) -> Dict[str, Any]:
|
|
608
|
+
"""
|
|
609
|
+
Starts the execution of tasks in the DAG using multiple processes and a marker thread.
|
|
610
|
+
|
|
611
|
+
The method prepares the DAG, starts worker processes to handle tasks in parallel, and uses a marker thread
|
|
612
|
+
to track task completion. It ensures that all tasks are processed and results are collected.
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
name (str): The name of the DAG.
|
|
616
|
+
|
|
617
|
+
Returns:
|
|
618
|
+
Dict[str, Any]: A dictionary mapping task names to their results.
|
|
619
|
+
"""
|
|
620
|
+
sorter = self.sorter
|
|
621
|
+
sorter.prepare()
|
|
622
|
+
|
|
623
|
+
with multiprocessing.Manager() as manager:
|
|
624
|
+
# Use Namespace to share results
|
|
625
|
+
ns = manager.Namespace()
|
|
626
|
+
ns.results = manager.dict() # Dictionary of Results
|
|
627
|
+
|
|
628
|
+
# Mutex
|
|
629
|
+
lock = manager.Lock()
|
|
630
|
+
|
|
631
|
+
# Queues
|
|
632
|
+
startable_queue = manager.Queue()
|
|
633
|
+
done_queue = manager.Queue()
|
|
634
|
+
|
|
635
|
+
# Events
|
|
636
|
+
stop_event = manager.Event()
|
|
637
|
+
|
|
638
|
+
# Start the Worker Processes
|
|
639
|
+
workers: list[multiprocessing.Process] = []
|
|
640
|
+
for _ in range(self._max_processes):
|
|
641
|
+
process = multiprocessing.Process(
|
|
642
|
+
target=self._worker,
|
|
643
|
+
args=(ns, lock, startable_queue, done_queue),
|
|
644
|
+
daemon=True
|
|
645
|
+
)
|
|
646
|
+
process.start()
|
|
647
|
+
workers.append(process)
|
|
648
|
+
|
|
649
|
+
# Start the Marker Thread
|
|
650
|
+
marker_thread = threading.Thread(
|
|
651
|
+
target=self._marker,
|
|
652
|
+
args=(sorter, lock, done_queue, stop_event),
|
|
653
|
+
daemon=True
|
|
654
|
+
)
|
|
655
|
+
marker_thread.start()
|
|
656
|
+
|
|
657
|
+
# Loop until all startables are processed and completed
|
|
658
|
+
while sorter.is_active():
|
|
659
|
+
with lock:
|
|
660
|
+
ready_names = sorter.get_ready()
|
|
661
|
+
|
|
662
|
+
if ready_names:
|
|
663
|
+
for name in ready_names:
|
|
664
|
+
startable = self._startables[name]
|
|
665
|
+
startable_queue.put(startable)
|
|
666
|
+
|
|
667
|
+
# Wait until all startables are processed
|
|
668
|
+
startable_queue.join()
|
|
669
|
+
|
|
670
|
+
# Wait until all startables are marked
|
|
671
|
+
done_queue.join()
|
|
672
|
+
|
|
673
|
+
# Set the Stop Event to signal the Processes and Thread
|
|
674
|
+
stop_event.set()
|
|
675
|
+
|
|
676
|
+
# Wait until marker is done
|
|
677
|
+
marker_thread.join()
|
|
678
|
+
|
|
679
|
+
# Terminate all worker processes
|
|
680
|
+
for process in workers:
|
|
681
|
+
process.terminate()
|
|
682
|
+
|
|
683
|
+
return dict(ns.results)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
class ProcessPoolDag(Dag):
|
|
687
|
+
"""
|
|
688
|
+
A Directed Acyclic Graph (DAG) that executes tasks concurrently using a process pool.
|
|
689
|
+
|
|
690
|
+
This class uses a `ProcessPoolExecutor` to execute tasks concurrently in a pool of worker processes. Tasks
|
|
691
|
+
are submitted to the pool and executed in parallel. The results are gathered as they complete, and the task
|
|
692
|
+
dependencies are handled by the DAG sorter to ensure tasks execute in the correct order.
|
|
693
|
+
|
|
694
|
+
Attributes:
|
|
695
|
+
_max_processes (int): The maximum number of processes to use in the pool for concurrent execution. Default is 4.
|
|
696
|
+
"""
|
|
697
|
+
|
|
698
|
+
def __init__(self, name: str, max_processes: int = 4):
|
|
699
|
+
"""
|
|
700
|
+
Initializes the process pool DAG with the given name and maximum number of worker processes in the pool.
|
|
701
|
+
|
|
702
|
+
This class uses a process pool to execute tasks concurrently. Each task is submitted to the pool, and the results
|
|
703
|
+
are tracked as they are completed.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
name (str): The name of the DAG.
|
|
707
|
+
max_processes (int, optional): The maximum number of processes to run concurrently in the process pool.
|
|
708
|
+
Defaults to 4.
|
|
709
|
+
"""
|
|
710
|
+
super().__init__(name)
|
|
711
|
+
self._max_processes = max_processes
|
|
712
|
+
|
|
713
|
+
def start(self) -> Dict[str, Any]:
|
|
714
|
+
"""
|
|
715
|
+
Starts the execution of tasks in the DAG using a process pool.
|
|
716
|
+
|
|
717
|
+
The method submits tasks to the process pool, waits for their completion, and collects results. It ensures that
|
|
718
|
+
tasks are executed concurrently and the results are properly handled and sorted.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
name (str): The name of the DAG.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Dict[str, Any]: A dictionary mapping task names to their results.
|
|
725
|
+
"""
|
|
726
|
+
sorter = self.sorter
|
|
727
|
+
sorter.prepare()
|
|
728
|
+
|
|
729
|
+
results = {}
|
|
730
|
+
with ProcessPoolExecutor(max_workers=self._max_processes) as pool:
|
|
731
|
+
futures: dict[Future, Startable] = {}
|
|
732
|
+
|
|
733
|
+
while sorter.is_active():
|
|
734
|
+
ready_names = sorter.get_ready()
|
|
735
|
+
for name in ready_names:
|
|
736
|
+
startable = self._startables[name]
|
|
737
|
+
futures[pool.submit(startable.start)] = startable
|
|
738
|
+
|
|
739
|
+
# Check completed futures without blocking others
|
|
740
|
+
completed_futures = [f for f in futures if f.done()] # Polling
|
|
741
|
+
for future in completed_futures:
|
|
742
|
+
startable = futures[future]
|
|
743
|
+
result = future.result()
|
|
744
|
+
results[startable.name] = result
|
|
745
|
+
sorter.done(startable.name)
|
|
746
|
+
del futures[future]
|
|
747
|
+
|
|
748
|
+
return results
|