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/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
- serialization_alias="fail-fast",
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
- serialization_alias="max-parallel",
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
- RunsOnLocal,
263
- RunsOnSelfHosted,
264
- RunsOnK8s,
264
+ Annotated[RunsOnK8s, Tag(RunsOnType.K8S)],
265
+ Annotated[RunsOnSelfHosted, Tag(RunsOnType.SELF_HOSTED)],
266
+ Annotated[RunsOnLocal, Tag(RunsOnType.LOCAL)],
265
267
  ],
266
- Field(discriminator="type"),
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
- serialization_alias="runs-on",
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
- serialization_alias="trigger-rule",
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
- to["jobs"][_id] = (
451
- {"strategies": output, **errors}
452
- if self.strategy.is_set()
453
- else {**output.get(next(iter(output), "DUMMY"), {}), **errors}
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 execute_strategy(
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 Strategy execution with passing dynamic parameters from the
466
- workflow execution to strategy matrix.
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 strategy: A strategy metrix value that use on this execution.
479
- This value will pass to the `matrix` key for templating.
480
- :param params: A dynamic parameters that will deepcopy to the context.
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 PoolThreadExecutor.
465
+ :param event: (Event) An event manager that pass to the
466
+ PoolThreadExecutor.
484
467
 
485
468
  :rtype: Result
486
469
  """
487
- if result is None: # pragma: no cov
488
- result: Result = Result(run_id=gen_id(self.id or "", unique=True))
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
- strategy_id: str = gen_id(strategy)
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
- # Create strategy execution context and update a matrix and copied
495
- # of params. So, the context value will have structure like;
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": { ... }, <== Current input params
499
- # "jobs": { ... }, <== Current input params
500
- # "matrix": { ... } <== Current strategy value
501
- # "stages": { ... } <== Catching stage outputs
586
+ # "params": { ... },
587
+ # "jobs": { ... },
588
+ # "matrix": { ... },
589
+ # "stages": { { "stage-id-01": { "outputs": { ... } } }, ... }
502
590
  # }
503
591
  #
504
- context: DictData = copy.deepcopy(params)
505
- context.update({"matrix": strategy, "stages": {}})
506
-
507
- # IMPORTANT: The stage execution only run sequentially one-by-one.
508
- for stage in self.stages:
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
- if stage.is_skipped(params=context):
511
- result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
512
- continue
626
+ # NOTE: Remove the current stage object for saving memory.
627
+ del stage
513
628
 
514
- result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
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
- # NOTE: Force stop this execution if event was set from main
521
- # execution.
522
- if event and event.is_set():
523
- error_msg: str = (
524
- "Job strategy was canceled from event that had set before "
525
- "strategy execution."
526
- )
527
- return result.catch(
528
- status=1,
529
- context={
530
- strategy_id: {
531
- "matrix": strategy,
532
- # NOTE: If job strategy executor use multithreading,
533
- # it will not filter function object from context.
534
- # ---
535
- # "stages": filter_func(context.pop("stages", {})),
536
- #
537
- "stages": context.pop("stages", {}),
538
- "errors": {
539
- "class": JobException(error_msg),
540
- "name": "JobException",
541
- "message": error_msg,
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
- # PARAGRAPH:
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
- # NOTE: Remove the current stage object for saving memory.
604
- del stage
695
+ fail_fast_flag: bool = job.strategy.fail_fast
696
+ ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
605
697
 
606
- return result.catch(
607
- status=Status.SUCCESS,
608
- context={
609
- strategy_id: {
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
- def execute(
617
- self,
618
- params: DictData,
619
- *,
620
- run_id: str | None = None,
621
- parent_run_id: str | None = None,
622
- result: Result | None = None,
623
- ) -> Result:
624
- """Job execution with passing dynamic parameters from the workflow
625
- execution. It will generate matrix values at the first step and run
626
- multithread on this metrics to the `stages` field of this job.
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
- :param params: An input parameters that use on job execution.
629
- :param run_id: A job running ID for this execution.
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
- :rtype: Result
635
- """
636
- if result is None: # pragma: no cov
637
- result: Result = Result(
638
- run_id=(run_id or gen_id(self.id or "", unique=True)),
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
- return result.catch(status=Status.SUCCESS)
656
-
657
- # NOTE: Create event for cancel executor by trigger stop running event.
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
- nd: str = (
689
- f", the strategies do not run is {not_done}"
690
- if not_done
691
- else ""
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
- result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
694
-
695
- # NOTE: Stop all running tasks with setting the event manager
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)