ddeutil-workflow 0.0.7__py3-none-any.whl → 0.0.8__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/app.py +4 -0
- ddeutil/workflow/exceptions.py +1 -4
- ddeutil/workflow/log.py +49 -0
- ddeutil/workflow/pipeline.py +327 -167
- ddeutil/workflow/stage.py +191 -97
- ddeutil/workflow/utils.py +94 -16
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/METADATA +17 -92
- ddeutil_workflow-0.0.8.dist-info/RECORD +20 -0
- ddeutil_workflow-0.0.7.dist-info/RECORD +0 -20
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.7.dist-info → ddeutil_workflow-0.0.8.dist-info}/top_level.txt +0 -0
ddeutil/workflow/pipeline.py
CHANGED
@@ -29,7 +29,12 @@ from pydantic.functional_validators import model_validator
|
|
29
29
|
from typing_extensions import Self
|
30
30
|
|
31
31
|
from .__types import DictData, DictStr, Matrix, MatrixExclude, MatrixInclude
|
32
|
-
from .exceptions import
|
32
|
+
from .exceptions import (
|
33
|
+
JobException,
|
34
|
+
PipelineException,
|
35
|
+
StageException,
|
36
|
+
UtilException,
|
37
|
+
)
|
33
38
|
from .loader import Loader
|
34
39
|
from .on import On
|
35
40
|
from .scheduler import CronRunner
|
@@ -39,6 +44,7 @@ from .utils import (
|
|
39
44
|
Result,
|
40
45
|
cross_product,
|
41
46
|
dash2underscore,
|
47
|
+
filter_func,
|
42
48
|
gen_id,
|
43
49
|
get_diff_sec,
|
44
50
|
)
|
@@ -54,7 +60,7 @@ class Strategy(BaseModel):
|
|
54
60
|
... 'fail-fast': False,
|
55
61
|
... 'matrix': {
|
56
62
|
... 'first': [1, 2, 3],
|
57
|
-
... 'second': ['foo', 'bar']
|
63
|
+
... 'second': ['foo', 'bar'],
|
58
64
|
... },
|
59
65
|
... 'include': [{'first': 4, 'second': 'foo'}],
|
60
66
|
... 'exclude': [{'first': 1, 'second': 'bar'}],
|
@@ -82,6 +88,10 @@ class Strategy(BaseModel):
|
|
82
88
|
dash2underscore("fail-fast", values)
|
83
89
|
return values
|
84
90
|
|
91
|
+
def is_set(self) -> bool:
|
92
|
+
"""Return True if this strategy was set from yaml template."""
|
93
|
+
return len(self.matrix) > 0
|
94
|
+
|
85
95
|
def make(self) -> list[DictStr]:
|
86
96
|
"""Return List of product of matrix values that already filter with
|
87
97
|
exclude and add include.
|
@@ -138,18 +148,25 @@ class Job(BaseModel):
|
|
138
148
|
Data Validate:
|
139
149
|
>>> job = {
|
140
150
|
... "runs-on": None,
|
141
|
-
... "strategy": {
|
151
|
+
... "strategy": {
|
152
|
+
... "max-parallel": 1,
|
153
|
+
... "matrix": {
|
154
|
+
... "first": [1, 2, 3],
|
155
|
+
... "second": ['foo', 'bar'],
|
156
|
+
... },
|
157
|
+
... },
|
142
158
|
... "needs": [],
|
143
159
|
... "stages": [
|
144
160
|
... {
|
145
161
|
... "name": "Some stage",
|
146
162
|
... "run": "print('Hello World')",
|
147
163
|
... },
|
164
|
+
... ...
|
148
165
|
... ],
|
149
166
|
... }
|
150
167
|
"""
|
151
168
|
|
152
|
-
|
169
|
+
id: Optional[str] = Field(default=None)
|
153
170
|
desc: Optional[str] = Field(default=None)
|
154
171
|
runs_on: Optional[str] = Field(default=None)
|
155
172
|
stages: list[Stage] = Field(
|
@@ -164,6 +181,9 @@ class Job(BaseModel):
|
|
164
181
|
default_factory=Strategy,
|
165
182
|
description="A strategy matrix that want to generate.",
|
166
183
|
)
|
184
|
+
run_id: Optional[str] = Field(
|
185
|
+
default=None, description="A running job ID.", repr=False
|
186
|
+
)
|
167
187
|
|
168
188
|
@model_validator(mode="before")
|
169
189
|
def __prepare_keys(cls, values: DictData) -> DictData:
|
@@ -173,6 +193,12 @@ class Job(BaseModel):
|
|
173
193
|
dash2underscore("runs-on", values)
|
174
194
|
return values
|
175
195
|
|
196
|
+
@model_validator(mode="after")
|
197
|
+
def __prepare_running_id(self):
|
198
|
+
if self.run_id is None:
|
199
|
+
self.run_id = gen_id(self.id or "", unique=True)
|
200
|
+
return self
|
201
|
+
|
176
202
|
def stage(self, stage_id: str) -> Stage:
|
177
203
|
"""Return stage model that match with an input stage ID."""
|
178
204
|
for stage in self.stages:
|
@@ -180,9 +206,8 @@ class Job(BaseModel):
|
|
180
206
|
return stage
|
181
207
|
raise ValueError(f"Stage ID {stage_id} does not exists")
|
182
208
|
|
183
|
-
|
184
|
-
|
185
|
-
if len(output) > 1:
|
209
|
+
def set_outputs(self, output: DictData) -> DictData:
|
210
|
+
if len(output) > 1 and self.strategy.is_set():
|
186
211
|
return {"strategies": output}
|
187
212
|
|
188
213
|
return output[next(iter(output))]
|
@@ -194,26 +219,32 @@ class Job(BaseModel):
|
|
194
219
|
*,
|
195
220
|
event: Event | None = None,
|
196
221
|
) -> Result:
|
197
|
-
"""Strategy execution with passing dynamic parameters from the
|
198
|
-
|
222
|
+
"""Job Strategy execution with passing dynamic parameters from the
|
223
|
+
pipeline execution to strategy matrix.
|
199
224
|
|
200
|
-
|
201
|
-
|
225
|
+
This execution is the minimum level execution of job model.
|
226
|
+
|
227
|
+
:param strategy: A metrix strategy value.
|
228
|
+
:param params: A dynamic parameters.
|
202
229
|
:param event: An manger event that pass to the PoolThreadExecutor.
|
203
230
|
:rtype: Result
|
231
|
+
|
232
|
+
:raise JobException: If it has any error from StageException or
|
233
|
+
UtilException.
|
204
234
|
"""
|
205
|
-
_stop_rs: Result = Result(
|
206
|
-
status=1,
|
207
|
-
context={
|
208
|
-
gen_id(strategy): {
|
209
|
-
"matrix": strategy,
|
210
|
-
"stages": {},
|
211
|
-
"error": "Event stopped",
|
212
|
-
},
|
213
|
-
},
|
214
|
-
)
|
215
235
|
if event and event.is_set():
|
216
|
-
return
|
236
|
+
return Result(
|
237
|
+
status=1,
|
238
|
+
context={
|
239
|
+
gen_id(strategy): {
|
240
|
+
"matrix": strategy,
|
241
|
+
"stages": {},
|
242
|
+
"error": {
|
243
|
+
"message": "Process Event stopped before execution"
|
244
|
+
},
|
245
|
+
},
|
246
|
+
},
|
247
|
+
)
|
217
248
|
|
218
249
|
# NOTE: Create strategy execution context and update a matrix and copied
|
219
250
|
# of params. So, the context value will have structure like;
|
@@ -229,16 +260,25 @@ class Job(BaseModel):
|
|
229
260
|
|
230
261
|
# IMPORTANT: The stage execution only run sequentially one-by-one.
|
231
262
|
for stage in self.stages:
|
263
|
+
|
264
|
+
# IMPORTANT: Change any stage running IDs to this job running ID.
|
265
|
+
stage.run_id = self.run_id
|
266
|
+
|
232
267
|
_st_name: str = stage.id or stage.name
|
233
268
|
|
234
|
-
if stage.
|
235
|
-
logging.info(
|
269
|
+
if stage.is_skipped(params=context):
|
270
|
+
logging.info(
|
271
|
+
f"({self.run_id}) [JOB]: Skip the stage: {_st_name!r}"
|
272
|
+
)
|
236
273
|
continue
|
237
|
-
|
274
|
+
|
275
|
+
logging.info(
|
276
|
+
f"({self.run_id}) [JOB]: Start execute the stage: {_st_name!r}"
|
277
|
+
)
|
238
278
|
|
239
279
|
# NOTE: Logging a matrix that pass on this stage execution.
|
240
280
|
if strategy:
|
241
|
-
logging.info(f"[
|
281
|
+
logging.info(f"({self.run_id}) [JOB]: Matrix: {strategy}")
|
242
282
|
|
243
283
|
# NOTE:
|
244
284
|
# I do not use below syntax because `params` dict be the
|
@@ -258,23 +298,41 @@ class Job(BaseModel):
|
|
258
298
|
# }
|
259
299
|
#
|
260
300
|
if event and event.is_set():
|
261
|
-
return
|
262
|
-
|
263
|
-
|
301
|
+
return Result(
|
302
|
+
status=1,
|
303
|
+
context={
|
304
|
+
gen_id(strategy): {
|
305
|
+
"matrix": strategy,
|
306
|
+
"stages": filter_func(context.pop("stages", {})),
|
307
|
+
"error": {
|
308
|
+
"message": (
|
309
|
+
"Process Event stopped before execution"
|
310
|
+
),
|
311
|
+
},
|
312
|
+
},
|
313
|
+
},
|
314
|
+
)
|
315
|
+
try:
|
316
|
+
rs: Result = stage.execute(params=context)
|
264
317
|
stage.set_outputs(rs.context, params=context)
|
265
|
-
|
266
|
-
|
267
|
-
f"
|
268
|
-
f"{stage.name}."
|
318
|
+
except (StageException, UtilException) as err:
|
319
|
+
logging.error(
|
320
|
+
f"({self.run_id}) [JOB]: {err.__class__.__name__}: {err}"
|
269
321
|
)
|
270
|
-
|
271
|
-
|
322
|
+
raise JobException(
|
323
|
+
f"Get stage execution error: {err.__class__.__name__}: "
|
324
|
+
f"{err}"
|
325
|
+
) from None
|
272
326
|
return Result(
|
273
327
|
status=0,
|
274
328
|
context={
|
275
329
|
gen_id(strategy): {
|
276
330
|
"matrix": strategy,
|
277
|
-
|
331
|
+
# NOTE: (WF001) filter own created function from stages
|
332
|
+
# value, because it does not dump with pickle when you
|
333
|
+
# execute with multiprocess.
|
334
|
+
#
|
335
|
+
"stages": filter_func(context.pop("stages", {})),
|
278
336
|
},
|
279
337
|
},
|
280
338
|
)
|
@@ -288,17 +346,20 @@ class Job(BaseModel):
|
|
288
346
|
:rtype: Result
|
289
347
|
"""
|
290
348
|
strategy_context: DictData = {}
|
291
|
-
rs = Result(context=strategy_context)
|
292
349
|
|
293
|
-
|
350
|
+
# NOTE: Normal Job execution.
|
351
|
+
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
294
352
|
for strategy in self.strategy.make():
|
295
353
|
rs: Result = self.strategy_execute(
|
296
354
|
strategy, params=copy.deepcopy(params)
|
297
355
|
)
|
298
356
|
strategy_context.update(rs.context)
|
299
|
-
return
|
357
|
+
return Result(
|
358
|
+
status=0,
|
359
|
+
context=strategy_context,
|
360
|
+
)
|
300
361
|
|
301
|
-
#
|
362
|
+
# WARNING: (WF001) I got error that raise when use
|
302
363
|
# ``ProcessPoolExecutor``;
|
303
364
|
# ---
|
304
365
|
# _pickle.PicklingError: Can't pickle
|
@@ -308,84 +369,109 @@ class Job(BaseModel):
|
|
308
369
|
with Manager() as manager:
|
309
370
|
event: Event = manager.Event()
|
310
371
|
|
372
|
+
# NOTE: Start process pool executor for running strategy executor in
|
373
|
+
# parallel mode.
|
311
374
|
with ProcessPoolExecutor(
|
312
375
|
max_workers=self.strategy.max_parallel
|
313
|
-
) as
|
314
|
-
|
315
|
-
|
376
|
+
) as executor:
|
377
|
+
features: list[Future] = [
|
378
|
+
executor.submit(
|
316
379
|
self.strategy_execute,
|
317
|
-
|
380
|
+
strategy,
|
318
381
|
params=copy.deepcopy(params),
|
319
382
|
event=event,
|
320
383
|
)
|
321
|
-
for
|
384
|
+
for strategy in self.strategy.make()
|
322
385
|
]
|
323
386
|
if self.strategy.fail_fast:
|
387
|
+
rs = self.__catch_fail_fast(event, features)
|
388
|
+
else:
|
389
|
+
rs = self.__catch_all_completed(features)
|
390
|
+
return Result(
|
391
|
+
status=0,
|
392
|
+
context=rs.context,
|
393
|
+
)
|
324
394
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
395
|
+
def __catch_fail_fast(self, event: Event, features: list[Future]) -> Result:
|
396
|
+
"""Job parallel pool features catching with fail-fast mode. That will
|
397
|
+
stop all not done features if it receive the first exception from all
|
398
|
+
running features.
|
399
|
+
|
400
|
+
:param event:
|
401
|
+
:param features: A list of features.
|
402
|
+
:rtype: Result
|
403
|
+
"""
|
404
|
+
strategy_context: DictData = {}
|
405
|
+
# NOTE: Get results from a collection of tasks with a
|
406
|
+
# timeout that has the first exception.
|
407
|
+
done, not_done = wait(
|
408
|
+
features, timeout=1800, return_when=FIRST_EXCEPTION
|
409
|
+
)
|
410
|
+
nd: str = (
|
411
|
+
f", the strategies do not run is {not_done}" if not_done else ""
|
412
|
+
)
|
413
|
+
logging.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
|
414
|
+
|
415
|
+
# NOTE: Stop all running tasks
|
416
|
+
event.set()
|
417
|
+
|
418
|
+
# NOTE: Cancel any scheduled tasks
|
419
|
+
for future in features:
|
420
|
+
future.cancel()
|
421
|
+
|
422
|
+
status: int = 0
|
423
|
+
for f in done:
|
424
|
+
if f.exception():
|
425
|
+
status = 1
|
426
|
+
logging.error(
|
427
|
+
f"({self.run_id}) [JOB]: One stage failed with: "
|
428
|
+
f"{f.exception()}, shutting down this feature."
|
429
|
+
)
|
430
|
+
elif f.cancelled():
|
431
|
+
continue
|
432
|
+
else:
|
433
|
+
rs: Result = f.result(timeout=60)
|
434
|
+
strategy_context.update(rs.context)
|
435
|
+
return Result(
|
436
|
+
status=status,
|
437
|
+
context=strategy_context,
|
438
|
+
)
|
439
|
+
|
440
|
+
def __catch_all_completed(self, features: list[Future]) -> Result:
|
441
|
+
"""Job parallel pool features catching with all-completed mode.
|
442
|
+
|
443
|
+
:param features: A list of features.
|
444
|
+
"""
|
445
|
+
strategy_context: DictData = {}
|
446
|
+
status: int = 0
|
447
|
+
for feature in as_completed(features):
|
448
|
+
try:
|
449
|
+
rs: Result = feature.result(timeout=60)
|
450
|
+
strategy_context.update(rs.context)
|
451
|
+
except PickleError as err:
|
452
|
+
# NOTE: (WF001) I do not want to fix this issue because
|
453
|
+
# it does not make sense and over-engineering with
|
454
|
+
# this bug fix process.
|
455
|
+
raise JobException(
|
456
|
+
f"PyStage that create object on locals does use "
|
457
|
+
f"parallel in strategy execution;\n\t{err}"
|
458
|
+
) from None
|
459
|
+
except TimeoutError:
|
460
|
+
status = 1
|
461
|
+
logging.warning("Task is hanging. Attempting to kill.")
|
462
|
+
feature.cancel()
|
463
|
+
if not feature.cancelled():
|
464
|
+
logging.warning("Failed to cancel the task.")
|
465
|
+
else:
|
466
|
+
logging.warning("Task canceled successfully.")
|
467
|
+
except JobException as err:
|
468
|
+
status = 1
|
469
|
+
logging.error(
|
470
|
+
f"({self.run_id}) [JOB]: Get stage exception with "
|
471
|
+
f"fail-fast does not set;\n{err.__class__.__name__}:\n\t"
|
472
|
+
f"{err}"
|
473
|
+
)
|
474
|
+
return Result(status=status, context=strategy_context)
|
389
475
|
|
390
476
|
|
391
477
|
class Pipeline(BaseModel):
|
@@ -414,6 +500,9 @@ class Pipeline(BaseModel):
|
|
414
500
|
default_factory=dict,
|
415
501
|
description="A mapping of job ID and job model that already loaded.",
|
416
502
|
)
|
503
|
+
run_id: Optional[str] = Field(
|
504
|
+
default=None, description="A running job ID.", repr=False
|
505
|
+
)
|
417
506
|
|
418
507
|
@classmethod
|
419
508
|
def from_loader(
|
@@ -474,7 +563,7 @@ class Pipeline(BaseModel):
|
|
474
563
|
return values
|
475
564
|
|
476
565
|
@model_validator(mode="after")
|
477
|
-
def
|
566
|
+
def __validate_jobs_need_and_prepare_running_id(self):
|
478
567
|
for job in self.jobs:
|
479
568
|
if not_exist := [
|
480
569
|
need for need in self.jobs[job].needs if need not in self.jobs
|
@@ -483,6 +572,13 @@ class Pipeline(BaseModel):
|
|
483
572
|
f"This needed jobs: {not_exist} do not exist in this "
|
484
573
|
f"pipeline."
|
485
574
|
)
|
575
|
+
|
576
|
+
# NOTE: update a job id with its job id from pipeline template
|
577
|
+
self.jobs[job].id = job
|
578
|
+
|
579
|
+
if self.run_id is None:
|
580
|
+
self.run_id = gen_id(self.name, unique=True)
|
581
|
+
|
486
582
|
return self
|
487
583
|
|
488
584
|
def job(self, name: str) -> Job:
|
@@ -602,7 +698,7 @@ class Pipeline(BaseModel):
|
|
602
698
|
self,
|
603
699
|
job: str,
|
604
700
|
params: DictData,
|
605
|
-
):
|
701
|
+
) -> Result:
|
606
702
|
"""Job Executor that use on pipeline executor.
|
607
703
|
:param job: A job ID that want to execute.
|
608
704
|
:param params: A params that was parameterized from pipeline execution.
|
@@ -613,18 +709,19 @@ class Pipeline(BaseModel):
|
|
613
709
|
f"The job ID: {job} does not exists on {self.name!r} pipeline."
|
614
710
|
)
|
615
711
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
712
|
+
try:
|
713
|
+
logging.info(f"({self.run_id}) [PIPELINE]: Start execute: {job!r}")
|
714
|
+
job_obj: Job = self.jobs[job]
|
715
|
+
j_rs: Result = job_obj.execute(params=params)
|
716
|
+
except JobException as err:
|
717
|
+
raise PipelineException(
|
718
|
+
f"The job ID: {job} get raise error: {err.__class__.__name__}:"
|
719
|
+
f"\n{err}"
|
720
|
+
) from None
|
721
|
+
return Result(
|
722
|
+
status=j_rs.status,
|
723
|
+
context={job: job_obj.set_outputs(j_rs.context)},
|
724
|
+
)
|
628
725
|
|
629
726
|
def execute(
|
630
727
|
self,
|
@@ -666,68 +763,131 @@ class Pipeline(BaseModel):
|
|
666
763
|
logging.warning("[PIPELINE]: This pipeline does not have any jobs")
|
667
764
|
return Result(status=0, context=params)
|
668
765
|
|
669
|
-
# NOTE:
|
766
|
+
# NOTE: Create a job queue that keep the job that want to running after
|
670
767
|
# it dependency condition.
|
671
768
|
jq: Queue = Queue()
|
672
769
|
for job_id in self.jobs:
|
673
770
|
jq.put(job_id)
|
674
771
|
|
772
|
+
# NOTE: Create start timestamp
|
675
773
|
ts: float = time.monotonic()
|
676
|
-
not_time_out_flag: bool = True
|
677
774
|
|
678
775
|
# NOTE: Create result context that will pass this context to any
|
679
776
|
# execution dependency.
|
680
777
|
rs: Result = Result(context=self.parameterize(params))
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
futures: list[Future] = []
|
688
|
-
while not jq.empty() and (
|
689
|
-
not_time_out_flag := ((time.monotonic() - ts) < timeout)
|
690
|
-
):
|
691
|
-
job_id: str = jq.get()
|
692
|
-
logging.info(
|
693
|
-
f"[PIPELINE]: Start execute the job: {job_id!r}"
|
694
|
-
)
|
695
|
-
job: Job = self.jobs[job_id]
|
696
|
-
if any(
|
697
|
-
need not in rs.context["jobs"] for need in job.needs
|
698
|
-
):
|
699
|
-
jq.put(job_id)
|
700
|
-
futures.append(
|
701
|
-
executor.submit(
|
702
|
-
self.job_execute,
|
703
|
-
job_id,
|
704
|
-
params=copy.deepcopy(rs.context),
|
705
|
-
),
|
778
|
+
try:
|
779
|
+
rs.receive(
|
780
|
+
self.__exec_non_threading(rs, jq, ts, timeout=timeout)
|
781
|
+
if (
|
782
|
+
worker := int(
|
783
|
+
os.getenv("WORKFLOW_CORE_MAX_JOB_PARALLEL", "1")
|
706
784
|
)
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
f"[CORE]: Run {self.name} with non-threading job executor"
|
785
|
+
)
|
786
|
+
== 1
|
787
|
+
else self.__exec_threading(
|
788
|
+
rs, jq, ts, worker=worker, timeout=timeout
|
789
|
+
)
|
713
790
|
)
|
714
|
-
|
791
|
+
return rs
|
792
|
+
except PipelineException as err:
|
793
|
+
rs.context.update({"error": {"message": str(err)}})
|
794
|
+
rs.status = 1
|
795
|
+
return rs
|
796
|
+
|
797
|
+
def __exec_threading(
|
798
|
+
self,
|
799
|
+
rs: Result,
|
800
|
+
job_queue: Queue,
|
801
|
+
ts: float,
|
802
|
+
*,
|
803
|
+
worker: int = 1,
|
804
|
+
timeout: int = 600,
|
805
|
+
) -> Result:
|
806
|
+
"""Pipeline threading execution."""
|
807
|
+
not_time_out_flag: bool = True
|
808
|
+
|
809
|
+
# IMPORTANT: The job execution can run parallel and waiting by
|
810
|
+
# needed.
|
811
|
+
with ThreadPoolExecutor(max_workers=worker) as executor:
|
812
|
+
futures: list[Future] = []
|
813
|
+
while not job_queue.empty() and (
|
715
814
|
not_time_out_flag := ((time.monotonic() - ts) < timeout)
|
716
815
|
):
|
717
|
-
job_id: str =
|
718
|
-
logging.info(f"[PIPELINE]: Start execute the job: {job_id!r}")
|
816
|
+
job_id: str = job_queue.get()
|
719
817
|
job: Job = self.jobs[job_id]
|
720
|
-
if any(need not in rs.context["jobs"] for need in job.needs):
|
721
|
-
jq.put(job_id)
|
722
818
|
|
723
|
-
|
724
|
-
|
819
|
+
# IMPORTANT:
|
820
|
+
# Change any job running IDs to this pipeline running ID.
|
821
|
+
job.run_id = self.run_id
|
822
|
+
|
823
|
+
if any(need not in rs.context["jobs"] for need in job.needs):
|
824
|
+
job_queue.put(job_id)
|
825
|
+
time.sleep(0.5)
|
826
|
+
continue
|
827
|
+
|
828
|
+
futures.append(
|
829
|
+
executor.submit(
|
830
|
+
self.job_execute,
|
831
|
+
job_id,
|
832
|
+
params=copy.deepcopy(rs.context),
|
833
|
+
),
|
725
834
|
)
|
726
|
-
|
835
|
+
|
836
|
+
for future in as_completed(futures):
|
837
|
+
if err := future.exception():
|
838
|
+
logging.error(f"{err}")
|
839
|
+
raise PipelineException(f"{err}")
|
840
|
+
|
841
|
+
# NOTE: Update job result to pipeline result.
|
842
|
+
rs.receive_jobs(future.result(timeout=20))
|
727
843
|
|
728
844
|
if not not_time_out_flag:
|
729
|
-
logging.warning(
|
730
|
-
|
731
|
-
|
845
|
+
logging.warning(
|
846
|
+
f"({self.run_id}) [PIPELINE]: Execution of pipeline was timeout"
|
847
|
+
)
|
848
|
+
raise PipelineException(
|
849
|
+
f"Execution of pipeline: {self.name} was timeout"
|
850
|
+
)
|
851
|
+
rs.status = 0
|
852
|
+
return rs
|
853
|
+
|
854
|
+
def __exec_non_threading(
|
855
|
+
self,
|
856
|
+
rs: Result,
|
857
|
+
job_queue: Queue,
|
858
|
+
ts: float,
|
859
|
+
*,
|
860
|
+
timeout: int = 600,
|
861
|
+
) -> Result:
|
862
|
+
"""Pipeline non-threading execution."""
|
863
|
+
not_time_out_flag: bool = True
|
864
|
+
logging.info(f"[CORE]: Run {self.name} with non-threading job executor")
|
865
|
+
while not job_queue.empty() and (
|
866
|
+
not_time_out_flag := ((time.monotonic() - ts) < timeout)
|
867
|
+
):
|
868
|
+
job_id: str = job_queue.get()
|
869
|
+
job: Job = self.jobs[job_id]
|
870
|
+
|
871
|
+
# IMPORTANT:
|
872
|
+
# Change any job running IDs to this pipeline running ID.
|
873
|
+
job.run_id = self.run_id
|
874
|
+
|
875
|
+
# NOTE:
|
876
|
+
if any(need not in rs.context["jobs"] for need in job.needs):
|
877
|
+
job_queue.put(job_id)
|
878
|
+
time.sleep(0.5)
|
879
|
+
continue
|
880
|
+
|
881
|
+
# NOTE: Start job execution.
|
882
|
+
job_rs = self.job_execute(job_id, params=copy.deepcopy(rs.context))
|
883
|
+
rs.context["jobs"].update(job_rs.context)
|
884
|
+
|
885
|
+
if not not_time_out_flag:
|
886
|
+
logging.warning(
|
887
|
+
f"({self.run_id}) [PIPELINE]: Execution of pipeline was timeout"
|
888
|
+
)
|
889
|
+
raise PipelineException(
|
890
|
+
f"Execution of pipeline: {self.name} was timeout"
|
891
|
+
)
|
732
892
|
rs.status = 0
|
733
893
|
return rs
|