ddeutil-workflow 0.0.36__py3-none-any.whl → 0.0.38__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/__init__.py +4 -1
- ddeutil/workflow/api/api.py +3 -1
- ddeutil/workflow/api/log.py +59 -0
- ddeutil/workflow/api/repeat.py +1 -1
- ddeutil/workflow/api/routes/job.py +4 -2
- ddeutil/workflow/api/routes/logs.py +126 -17
- ddeutil/workflow/api/routes/schedules.py +6 -6
- ddeutil/workflow/api/routes/workflows.py +9 -7
- ddeutil/workflow/caller.py +9 -3
- ddeutil/workflow/conf.py +0 -60
- ddeutil/workflow/context.py +59 -0
- ddeutil/workflow/exceptions.py +14 -1
- ddeutil/workflow/job.py +310 -277
- ddeutil/workflow/logs.py +6 -1
- ddeutil/workflow/result.py +1 -1
- ddeutil/workflow/scheduler.py +11 -4
- ddeutil/workflow/stages.py +368 -111
- ddeutil/workflow/utils.py +27 -49
- ddeutil/workflow/workflow.py +137 -72
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/METADATA +12 -6
- ddeutil_workflow-0.0.38.dist-info/RECORD +33 -0
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.36.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info/licenses}/LICENSE +0 -0
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -27,7 +27,7 @@ from threading import Event
|
|
27
27
|
from typing import Annotated, Any, Literal, Optional, Union
|
28
28
|
|
29
29
|
from ddeutil.core import freeze_args
|
30
|
-
from pydantic import BaseModel, ConfigDict, Field
|
30
|
+
from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag
|
31
31
|
from pydantic.functional_validators import field_validator, model_validator
|
32
32
|
from typing_extensions import Self
|
33
33
|
|
@@ -43,7 +43,6 @@ from .stages import Stage
|
|
43
43
|
from .templates import has_template
|
44
44
|
from .utils import (
|
45
45
|
cross_product,
|
46
|
-
dash2underscore,
|
47
46
|
filter_func,
|
48
47
|
gen_id,
|
49
48
|
)
|
@@ -60,6 +59,8 @@ __all__: TupleStr = (
|
|
60
59
|
"RunsOnSelfHosted",
|
61
60
|
"RunsOnK8s",
|
62
61
|
"make",
|
62
|
+
"local_execute_strategy",
|
63
|
+
"local_execute",
|
63
64
|
)
|
64
65
|
|
65
66
|
|
@@ -153,7 +154,7 @@ class Strategy(BaseModel):
|
|
153
154
|
|
154
155
|
fail_fast: bool = Field(
|
155
156
|
default=False,
|
156
|
-
|
157
|
+
alias="fail-fast",
|
157
158
|
)
|
158
159
|
max_parallel: int = Field(
|
159
160
|
default=1,
|
@@ -162,7 +163,7 @@ class Strategy(BaseModel):
|
|
162
163
|
"The maximum number of executor thread pool that want to run "
|
163
164
|
"parallel"
|
164
165
|
),
|
165
|
-
|
166
|
+
alias="max-parallel",
|
166
167
|
)
|
167
168
|
matrix: Matrix = Field(
|
168
169
|
default_factory=dict,
|
@@ -179,18 +180,6 @@ class Strategy(BaseModel):
|
|
179
180
|
description="A list of exclude matrix that want to filter-out.",
|
180
181
|
)
|
181
182
|
|
182
|
-
@model_validator(mode="before")
|
183
|
-
def __prepare_keys(cls, values: DictData) -> DictData:
|
184
|
-
"""Rename key that use dash to underscore because Python does not
|
185
|
-
support this character exist in any variable name.
|
186
|
-
|
187
|
-
:param values: A parsing values to these models
|
188
|
-
:rtype: DictData
|
189
|
-
"""
|
190
|
-
dash2underscore("max-parallel", values)
|
191
|
-
dash2underscore("fail-fast", values)
|
192
|
-
return values
|
193
|
-
|
194
183
|
def is_set(self) -> bool:
|
195
184
|
"""Return True if this strategy was set from yaml template.
|
196
185
|
|
@@ -228,6 +217,10 @@ class RunsOnType(str, Enum):
|
|
228
217
|
|
229
218
|
|
230
219
|
class BaseRunsOn(BaseModel): # pragma: no cov
|
220
|
+
"""Base Runs-On Model for generate runs-on types via inherit this model
|
221
|
+
object and override execute method.
|
222
|
+
"""
|
223
|
+
|
231
224
|
model_config = ConfigDict(use_enum_values=True)
|
232
225
|
|
233
226
|
type: Literal[RunsOnType.LOCAL]
|
@@ -243,12 +236,17 @@ class RunsOnLocal(BaseRunsOn): # pragma: no cov
|
|
243
236
|
type: Literal[RunsOnType.LOCAL] = Field(default=RunsOnType.LOCAL)
|
244
237
|
|
245
238
|
|
239
|
+
class SelfHostedArgs(BaseModel):
|
240
|
+
host: str
|
241
|
+
|
242
|
+
|
246
243
|
class RunsOnSelfHosted(BaseRunsOn): # pragma: no cov
|
247
244
|
"""Runs-on self-hosted."""
|
248
245
|
|
249
246
|
type: Literal[RunsOnType.SELF_HOSTED] = Field(
|
250
247
|
default=RunsOnType.SELF_HOSTED
|
251
248
|
)
|
249
|
+
args: SelfHostedArgs = Field(alias="with")
|
252
250
|
|
253
251
|
|
254
252
|
class RunsOnK8s(BaseRunsOn): # pragma: no cov
|
@@ -257,13 +255,17 @@ class RunsOnK8s(BaseRunsOn): # pragma: no cov
|
|
257
255
|
type: Literal[RunsOnType.K8S] = Field(default=RunsOnType.K8S)
|
258
256
|
|
259
257
|
|
258
|
+
def get_discriminator_runs_on(model: dict[str, Any]) -> str:
|
259
|
+
return model.get("type", "local")
|
260
|
+
|
261
|
+
|
260
262
|
RunsOn = Annotated[
|
261
263
|
Union[
|
262
|
-
|
263
|
-
RunsOnSelfHosted,
|
264
|
-
|
264
|
+
Annotated[RunsOnK8s, Tag(RunsOnType.K8S)],
|
265
|
+
Annotated[RunsOnSelfHosted, Tag(RunsOnType.SELF_HOSTED)],
|
266
|
+
Annotated[RunsOnLocal, Tag(RunsOnType.LOCAL)],
|
265
267
|
],
|
266
|
-
|
268
|
+
Discriminator(get_discriminator_runs_on),
|
267
269
|
]
|
268
270
|
|
269
271
|
|
@@ -308,7 +310,7 @@ class Job(BaseModel):
|
|
308
310
|
runs_on: RunsOn = Field(
|
309
311
|
default_factory=RunsOnLocal,
|
310
312
|
description="A target node for this job to use for execution.",
|
311
|
-
|
313
|
+
alias="runs-on",
|
312
314
|
)
|
313
315
|
stages: list[Stage] = Field(
|
314
316
|
default_factory=list,
|
@@ -317,7 +319,7 @@ class Job(BaseModel):
|
|
317
319
|
trigger_rule: TriggerRules = Field(
|
318
320
|
default=TriggerRules.all_success,
|
319
321
|
description="A trigger rule of tracking needed jobs.",
|
320
|
-
|
322
|
+
alias="trigger-rule",
|
321
323
|
)
|
322
324
|
needs: list[str] = Field(
|
323
325
|
default_factory=list,
|
@@ -328,18 +330,6 @@ class Job(BaseModel):
|
|
328
330
|
description="A strategy matrix that want to generate.",
|
329
331
|
)
|
330
332
|
|
331
|
-
@model_validator(mode="before")
|
332
|
-
def __prepare_keys__(cls, values: DictData) -> DictData:
|
333
|
-
"""Rename key that use dash to underscore because Python does not
|
334
|
-
support this character exist in any variable name.
|
335
|
-
|
336
|
-
:param values: A passing value that coming for initialize this object.
|
337
|
-
:rtype: DictData
|
338
|
-
"""
|
339
|
-
dash2underscore("runs-on", values)
|
340
|
-
dash2underscore("trigger-rule", values)
|
341
|
-
return values
|
342
|
-
|
343
333
|
@field_validator("desc", mode="after")
|
344
334
|
def ___prepare_desc__(cls, value: str) -> str:
|
345
335
|
"""Prepare description string that was created on a template.
|
@@ -430,15 +420,14 @@ class Job(BaseModel):
|
|
430
420
|
|
431
421
|
:rtype: DictData
|
432
422
|
"""
|
423
|
+
if "jobs" not in to:
|
424
|
+
to["jobs"] = {}
|
425
|
+
|
433
426
|
if self.id is None and not config.job_default_id:
|
434
427
|
raise JobException(
|
435
428
|
"This job do not set the ID before setting execution output."
|
436
429
|
)
|
437
430
|
|
438
|
-
# NOTE: Create jobs key to receive an output from the job execution.
|
439
|
-
if "jobs" not in to:
|
440
|
-
to["jobs"] = {}
|
441
|
-
|
442
431
|
# NOTE: If the job ID did not set, it will use index of jobs key
|
443
432
|
# instead.
|
444
433
|
_id: str = self.id or str(len(to["jobs"]) + 1)
|
@@ -447,278 +436,322 @@ class Job(BaseModel):
|
|
447
436
|
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
448
437
|
)
|
449
438
|
|
450
|
-
|
451
|
-
{"strategies": output, **errors}
|
452
|
-
|
453
|
-
|
454
|
-
|
439
|
+
if self.strategy.is_set():
|
440
|
+
to["jobs"][_id] = {"strategies": output, **errors}
|
441
|
+
else:
|
442
|
+
_output = output.get(next(iter(output), "FIRST"), {})
|
443
|
+
_output.pop("matrix", {})
|
444
|
+
to["jobs"][_id] = {**_output, **errors}
|
455
445
|
return to
|
456
446
|
|
457
|
-
def
|
447
|
+
def execute(
|
458
448
|
self,
|
459
|
-
strategy: DictData,
|
460
449
|
params: DictData,
|
461
450
|
*,
|
451
|
+
run_id: str | None = None,
|
452
|
+
parent_run_id: str | None = None,
|
462
453
|
result: Result | None = None,
|
463
454
|
event: Event | None = None,
|
464
455
|
) -> Result:
|
465
|
-
"""Job
|
466
|
-
|
467
|
-
|
468
|
-
This execution is the minimum level of execution of this job model.
|
469
|
-
It different with `self.execute` because this method run only one
|
470
|
-
strategy and return with context of this strategy data.
|
471
|
-
|
472
|
-
The result of this execution will return result with strategy ID
|
473
|
-
that generated from the `gen_id` function with an input strategy value.
|
474
|
-
|
475
|
-
:raise JobException: If it has any error from `StageException` or
|
476
|
-
`UtilException`.
|
456
|
+
"""Job execution with passing dynamic parameters from the workflow
|
457
|
+
execution. It will generate matrix values at the first step and run
|
458
|
+
multithread on this metrics to the `stages` field of this job.
|
477
459
|
|
478
|
-
:param
|
479
|
-
|
480
|
-
:param
|
460
|
+
:param params: An input parameters that use on job execution.
|
461
|
+
:param run_id: A job running ID for this execution.
|
462
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
481
463
|
:param result: (Result) A result object for keeping context and status
|
482
464
|
data.
|
483
|
-
:param event: An event manager that pass to the
|
465
|
+
:param event: (Event) An event manager that pass to the
|
466
|
+
PoolThreadExecutor.
|
484
467
|
|
485
468
|
:rtype: Result
|
486
469
|
"""
|
487
|
-
|
488
|
-
result
|
470
|
+
result: Result = Result.construct_with_rs_or_id(
|
471
|
+
result,
|
472
|
+
run_id=run_id,
|
473
|
+
parent_run_id=parent_run_id,
|
474
|
+
id_logic=(self.id or "not-set"),
|
475
|
+
)
|
476
|
+
|
477
|
+
if self.runs_on.type == RunsOnType.LOCAL:
|
478
|
+
return local_execute(
|
479
|
+
job=self,
|
480
|
+
params=params,
|
481
|
+
result=result,
|
482
|
+
event=event,
|
483
|
+
)
|
484
|
+
elif self.runs_on.type == RunsOnType.SELF_HOSTED: # pragma: no cov
|
485
|
+
pass
|
486
|
+
elif self.runs_on.type == RunsOnType.K8S: # pragma: no cov
|
487
|
+
pass
|
488
|
+
|
489
|
+
# pragma: no cov
|
490
|
+
result.trace.error(
|
491
|
+
f"[JOB]: Job executor does not support for runs-on type: "
|
492
|
+
f"{self.runs_on.type} yet"
|
493
|
+
)
|
494
|
+
raise NotImplementedError(
|
495
|
+
f"The job runs-on other type: {self.runs_on.type} does not "
|
496
|
+
f"support yet."
|
497
|
+
)
|
498
|
+
|
499
|
+
|
500
|
+
def local_execute_strategy(
|
501
|
+
job: Job,
|
502
|
+
strategy: DictData,
|
503
|
+
params: DictData,
|
504
|
+
*,
|
505
|
+
result: Result | None = None,
|
506
|
+
event: Event | None = None,
|
507
|
+
raise_error: bool = False,
|
508
|
+
) -> Result:
|
509
|
+
"""Local job strategy execution with passing dynamic parameters from the
|
510
|
+
workflow execution to strategy matrix.
|
511
|
+
|
512
|
+
This execution is the minimum level of execution of this job model.
|
513
|
+
It different with `self.execute` because this method run only one
|
514
|
+
strategy and return with context of this strategy data.
|
515
|
+
|
516
|
+
The result of this execution will return result with strategy ID
|
517
|
+
that generated from the `gen_id` function with an input strategy value.
|
518
|
+
|
519
|
+
:raise JobException: If it has any error from `StageException` or
|
520
|
+
`UtilException`.
|
521
|
+
|
522
|
+
:param job: (Job) A job model that want to execute.
|
523
|
+
:param strategy: A strategy metrix value that use on this execution.
|
524
|
+
This value will pass to the `matrix` key for templating.
|
525
|
+
:param params: A dynamic parameters that will deepcopy to the context.
|
526
|
+
:param result: (Result) A result object for keeping context and status
|
527
|
+
data.
|
528
|
+
:param event: (Event) An event manager that pass to the PoolThreadExecutor.
|
529
|
+
:param raise_error: (bool) A flag that all this method raise error
|
530
|
+
|
531
|
+
:rtype: Result
|
532
|
+
"""
|
533
|
+
if result is None:
|
534
|
+
result: Result = Result(run_id=gen_id(job.id or "not-set", unique=True))
|
535
|
+
|
536
|
+
strategy_id: str = gen_id(strategy)
|
537
|
+
|
538
|
+
# PARAGRAPH:
|
539
|
+
#
|
540
|
+
# Create strategy execution context and update a matrix and copied
|
541
|
+
# of params. So, the context value will have structure like;
|
542
|
+
#
|
543
|
+
# {
|
544
|
+
# "params": { ... }, <== Current input params
|
545
|
+
# "jobs": { ... }, <== Current input params
|
546
|
+
# "matrix": { ... } <== Current strategy value
|
547
|
+
# "stages": { ... } <== Catching stage outputs
|
548
|
+
# }
|
549
|
+
#
|
550
|
+
context: DictData = copy.deepcopy(params)
|
551
|
+
context.update({"matrix": strategy, "stages": {}})
|
552
|
+
|
553
|
+
if strategy:
|
554
|
+
result.trace.info(f"[JOB]: Execute Strategy ID: {strategy_id}")
|
555
|
+
result.trace.info(f"[JOB]: ... Matrix: {strategy_id}")
|
556
|
+
|
557
|
+
# IMPORTANT: The stage execution only run sequentially one-by-one.
|
558
|
+
for stage in job.stages:
|
559
|
+
|
560
|
+
if stage.is_skipped(params=context):
|
561
|
+
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
562
|
+
continue
|
489
563
|
|
490
|
-
|
564
|
+
if event and event.is_set():
|
565
|
+
error_msg: str = (
|
566
|
+
"Job strategy was canceled from event that had set before "
|
567
|
+
"strategy execution."
|
568
|
+
)
|
569
|
+
return result.catch(
|
570
|
+
status=Status.FAILED,
|
571
|
+
context={
|
572
|
+
strategy_id: {
|
573
|
+
"matrix": strategy,
|
574
|
+
"stages": context.pop("stages", {}),
|
575
|
+
"errors": JobException(error_msg).to_dict(),
|
576
|
+
},
|
577
|
+
},
|
578
|
+
)
|
491
579
|
|
492
580
|
# PARAGRAPH:
|
493
581
|
#
|
494
|
-
#
|
495
|
-
#
|
582
|
+
# This step will add the stage result to `stages` key in that
|
583
|
+
# stage id. It will have structure like;
|
496
584
|
#
|
497
585
|
# {
|
498
|
-
# "params": { ... },
|
499
|
-
# "jobs": { ... },
|
500
|
-
# "matrix": { ... }
|
501
|
-
# "stages": { ... }
|
586
|
+
# "params": { ... },
|
587
|
+
# "jobs": { ... },
|
588
|
+
# "matrix": { ... },
|
589
|
+
# "stages": { { "stage-id-01": { "outputs": { ... } } }, ... }
|
502
590
|
# }
|
503
591
|
#
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
#
|
508
|
-
|
592
|
+
# IMPORTANT:
|
593
|
+
# This execution change all stage running IDs to the current job
|
594
|
+
# running ID, but it still trac log to the same parent running ID
|
595
|
+
# (with passing `run_id` and `parent_run_id` to the stage
|
596
|
+
# execution arguments).
|
597
|
+
#
|
598
|
+
try:
|
599
|
+
stage.set_outputs(
|
600
|
+
stage.handler_execute(
|
601
|
+
params=context,
|
602
|
+
run_id=result.run_id,
|
603
|
+
parent_run_id=result.parent_run_id,
|
604
|
+
).context,
|
605
|
+
to=context,
|
606
|
+
)
|
607
|
+
except (StageException, UtilException) as err:
|
608
|
+
result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
|
609
|
+
if raise_error or config.job_raise_error:
|
610
|
+
raise JobException(
|
611
|
+
f"Stage execution error: {err.__class__.__name__}: "
|
612
|
+
f"{err}"
|
613
|
+
) from None
|
614
|
+
|
615
|
+
return result.catch(
|
616
|
+
status=Status.FAILED,
|
617
|
+
context={
|
618
|
+
strategy_id: {
|
619
|
+
"matrix": strategy,
|
620
|
+
"stages": context.pop("stages", {}),
|
621
|
+
"errors": err.to_dict(),
|
622
|
+
},
|
623
|
+
},
|
624
|
+
)
|
509
625
|
|
510
|
-
|
511
|
-
|
512
|
-
continue
|
626
|
+
# NOTE: Remove the current stage object for saving memory.
|
627
|
+
del stage
|
513
628
|
|
514
|
-
|
629
|
+
return result.catch(
|
630
|
+
status=Status.SUCCESS,
|
631
|
+
context={
|
632
|
+
strategy_id: {
|
633
|
+
"matrix": strategy,
|
634
|
+
"stages": filter_func(context.pop("stages", {})),
|
635
|
+
},
|
636
|
+
},
|
637
|
+
)
|
515
638
|
|
516
|
-
# NOTE: Logging a matrix that pass on this stage execution.
|
517
|
-
if strategy:
|
518
|
-
result.trace.info(f"[JOB]: ... Matrix: {strategy}")
|
519
639
|
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
640
|
+
def local_execute(
|
641
|
+
job: Job,
|
642
|
+
params: DictData,
|
643
|
+
*,
|
644
|
+
run_id: str | None = None,
|
645
|
+
parent_run_id: str | None = None,
|
646
|
+
result: Result | None = None,
|
647
|
+
event: Event | None = None,
|
648
|
+
raise_error: bool = False,
|
649
|
+
) -> Result:
|
650
|
+
"""Local job execution with passing dynamic parameters from the workflow
|
651
|
+
execution. It will generate matrix values at the first step and run
|
652
|
+
multithread on this metrics to the `stages` field of this job.
|
653
|
+
|
654
|
+
This method does not raise any JobException if it runs with
|
655
|
+
multi-threading strategy.
|
656
|
+
|
657
|
+
:param job: (Job) A job model that want to execute.
|
658
|
+
:param params: (DictData) An input parameters that use on job execution.
|
659
|
+
:param run_id: (str) A job running ID for this execution.
|
660
|
+
:param parent_run_id: (str) A parent workflow running ID for this release.
|
661
|
+
:param result: (Result) A result object for keeping context and status
|
662
|
+
data.
|
663
|
+
:param event: (Event) An event manager that pass to the PoolThreadExecutor.
|
664
|
+
:param raise_error: (bool) A flag that all this method raise error to the
|
665
|
+
strategy execution.
|
666
|
+
|
667
|
+
:rtype: Result
|
668
|
+
"""
|
669
|
+
result: Result = Result.construct_with_rs_or_id(
|
670
|
+
result,
|
671
|
+
run_id=run_id,
|
672
|
+
parent_run_id=parent_run_id,
|
673
|
+
id_logic=(job.id or "not-set"),
|
674
|
+
)
|
675
|
+
event: Event = Event() if event is None else event
|
676
|
+
|
677
|
+
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
678
|
+
# for-loop to control strategy execution sequentially.
|
679
|
+
if (not job.strategy.is_set()) or job.strategy.max_parallel == 1:
|
680
|
+
|
681
|
+
for strategy in job.strategy.make():
|
682
|
+
|
683
|
+
# TODO: stop and raise error if the event was set.
|
684
|
+
local_execute_strategy(
|
685
|
+
job=job,
|
686
|
+
strategy=strategy,
|
687
|
+
params=params,
|
688
|
+
result=result,
|
689
|
+
event=event,
|
690
|
+
raise_error=raise_error,
|
691
|
+
)
|
546
692
|
|
547
|
-
|
548
|
-
#
|
549
|
-
# I do not use below syntax because `params` dict be the
|
550
|
-
# reference memory pointer, and it was changed when I action
|
551
|
-
# anything like update or re-construct this.
|
552
|
-
#
|
553
|
-
# ... params |= stage.execute(params=params)
|
554
|
-
#
|
555
|
-
# This step will add the stage result to `stages` key in
|
556
|
-
# that stage id. It will have structure like;
|
557
|
-
#
|
558
|
-
# {
|
559
|
-
# "params": { ... },
|
560
|
-
# "jobs": { ... },
|
561
|
-
# "matrix": { ... },
|
562
|
-
# "stages": { { "stage-id-1": ... }, ... }
|
563
|
-
# }
|
564
|
-
#
|
565
|
-
# IMPORTANT:
|
566
|
-
# This execution change all stage running IDs to the current job
|
567
|
-
# running ID, but it still trac log to the same parent running ID
|
568
|
-
# (with passing `run_id` and `parent_run_id` to the stage
|
569
|
-
# execution arguments).
|
570
|
-
#
|
571
|
-
try:
|
572
|
-
stage.set_outputs(
|
573
|
-
stage.handler_execute(
|
574
|
-
params=context,
|
575
|
-
run_id=result.run_id,
|
576
|
-
parent_run_id=result.parent_run_id,
|
577
|
-
).context,
|
578
|
-
to=context,
|
579
|
-
)
|
580
|
-
except (StageException, UtilException) as err:
|
581
|
-
result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
|
582
|
-
if config.job_raise_error:
|
583
|
-
raise JobException(
|
584
|
-
f"Stage execution error: {err.__class__.__name__}: "
|
585
|
-
f"{err}"
|
586
|
-
) from None
|
587
|
-
|
588
|
-
return result.catch(
|
589
|
-
status=1,
|
590
|
-
context={
|
591
|
-
strategy_id: {
|
592
|
-
"matrix": strategy,
|
593
|
-
"stages": context.pop("stages", {}),
|
594
|
-
"errors": {
|
595
|
-
"class": err,
|
596
|
-
"name": err.__class__.__name__,
|
597
|
-
"message": f"{err.__class__.__name__}: {err}",
|
598
|
-
},
|
599
|
-
},
|
600
|
-
},
|
601
|
-
)
|
693
|
+
return result.catch(status=Status.SUCCESS)
|
602
694
|
|
603
|
-
|
604
|
-
|
695
|
+
fail_fast_flag: bool = job.strategy.fail_fast
|
696
|
+
ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
|
605
697
|
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
"matrix": strategy,
|
611
|
-
"stages": filter_func(context.pop("stages", {})),
|
612
|
-
},
|
613
|
-
},
|
614
|
-
)
|
698
|
+
result.trace.info(
|
699
|
+
f"[JOB]: Start multithreading: {job.strategy.max_parallel} threads "
|
700
|
+
f"with {ls} mode."
|
701
|
+
)
|
615
702
|
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
703
|
+
# IMPORTANT: Start running strategy execution by multithreading because
|
704
|
+
# it will run by strategy values without waiting previous execution.
|
705
|
+
with ThreadPoolExecutor(
|
706
|
+
max_workers=job.strategy.max_parallel,
|
707
|
+
thread_name_prefix="job_strategy_exec_",
|
708
|
+
) as executor:
|
709
|
+
|
710
|
+
futures: list[Future] = [
|
711
|
+
executor.submit(
|
712
|
+
local_execute_strategy,
|
713
|
+
job=job,
|
714
|
+
strategy=strategy,
|
715
|
+
params=params,
|
716
|
+
result=result,
|
717
|
+
event=event,
|
718
|
+
raise_error=raise_error,
|
719
|
+
)
|
720
|
+
for strategy in job.strategy.make()
|
721
|
+
]
|
627
722
|
|
628
|
-
:
|
629
|
-
:
|
630
|
-
:param parent_run_id: A parent workflow running ID for this release.
|
631
|
-
:param result: (Result) A result object for keeping context and status
|
632
|
-
data.
|
723
|
+
context: DictData = {}
|
724
|
+
status: Status = Status.SUCCESS
|
633
725
|
|
634
|
-
:
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
parent_run_id=parent_run_id,
|
726
|
+
if not fail_fast_flag:
|
727
|
+
done = as_completed(futures, timeout=1800)
|
728
|
+
else:
|
729
|
+
done, not_done = wait(
|
730
|
+
futures, timeout=1800, return_when=FIRST_EXCEPTION
|
640
731
|
)
|
641
|
-
elif parent_run_id: # pragma: no cov
|
642
|
-
result.set_parent_run_id(parent_run_id)
|
643
|
-
|
644
|
-
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
645
|
-
# for-loop to control strategy execution sequentially.
|
646
|
-
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
647
|
-
|
648
|
-
for strategy in self.strategy.make():
|
649
|
-
result: Result = self.execute_strategy(
|
650
|
-
strategy=strategy,
|
651
|
-
params=params,
|
652
|
-
result=result,
|
653
|
-
)
|
654
732
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
event: Event = Event()
|
659
|
-
|
660
|
-
# IMPORTANT: Start running strategy execution by multithreading because
|
661
|
-
# it will run by strategy values without waiting previous execution.
|
662
|
-
with ThreadPoolExecutor(
|
663
|
-
max_workers=self.strategy.max_parallel,
|
664
|
-
thread_name_prefix="job_strategy_exec_",
|
665
|
-
) as executor:
|
666
|
-
|
667
|
-
futures: list[Future] = [
|
668
|
-
executor.submit(
|
669
|
-
self.execute_strategy,
|
670
|
-
strategy=strategy,
|
671
|
-
params=params,
|
672
|
-
result=result,
|
673
|
-
event=event,
|
674
|
-
)
|
675
|
-
for strategy in self.strategy.make()
|
676
|
-
]
|
677
|
-
|
678
|
-
context: DictData = {}
|
679
|
-
status: Status = Status.SUCCESS
|
680
|
-
fail_fast_flag: bool = self.strategy.fail_fast
|
681
|
-
|
682
|
-
if fail_fast_flag:
|
683
|
-
# NOTE: Get results from a collection of tasks with a timeout
|
684
|
-
# that has the first exception.
|
685
|
-
done, not_done = wait(
|
686
|
-
futures, timeout=1800, return_when=FIRST_EXCEPTION
|
733
|
+
if len(done) != len(futures):
|
734
|
+
result.trace.warning(
|
735
|
+
"[JOB]: Set the event for stop running stage."
|
687
736
|
)
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
737
|
+
event.set()
|
738
|
+
for future in not_done:
|
739
|
+
future.cancel()
|
740
|
+
|
741
|
+
nd: str = (
|
742
|
+
f", the strategies do not run is {not_done}" if not_done else ""
|
743
|
+
)
|
744
|
+
result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
|
745
|
+
|
746
|
+
for future in done:
|
747
|
+
try:
|
748
|
+
future.result()
|
749
|
+
except JobException as err:
|
750
|
+
status = Status.FAILED
|
751
|
+
result.trace.error(
|
752
|
+
f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
|
753
|
+
f"\n\t{err}"
|
692
754
|
)
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
# and cancel any scheduled tasks.
|
697
|
-
if len(done) != len(futures):
|
698
|
-
event.set()
|
699
|
-
for future in not_done:
|
700
|
-
future.cancel()
|
701
|
-
else:
|
702
|
-
done = as_completed(futures, timeout=1800)
|
703
|
-
|
704
|
-
for future in done:
|
705
|
-
try:
|
706
|
-
future.result()
|
707
|
-
except JobException as err:
|
708
|
-
status = Status.FAILED
|
709
|
-
ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
|
710
|
-
result.trace.error(
|
711
|
-
f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
|
712
|
-
f"\n\t{err}"
|
713
|
-
)
|
714
|
-
context.update(
|
715
|
-
{
|
716
|
-
"errors": {
|
717
|
-
"class": err,
|
718
|
-
"name": err.__class__.__name__,
|
719
|
-
"message": f"{err.__class__.__name__}: {err}",
|
720
|
-
},
|
721
|
-
},
|
722
|
-
)
|
723
|
-
|
724
|
-
return result.catch(status=status, context=context)
|
755
|
+
context.update({"errors": err.to_dict()})
|
756
|
+
|
757
|
+
return result.catch(status=status, context=context)
|