ddeutil-workflow 0.0.64__py3-none-any.whl → 0.0.66__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 +1 -1
- ddeutil/workflow/__main__.py +1 -27
- ddeutil/workflow/api/routes/job.py +2 -2
- ddeutil/workflow/cli.py +66 -0
- ddeutil/workflow/conf.py +1 -9
- ddeutil/workflow/{exceptions.py → errors.py} +46 -11
- ddeutil/workflow/job.py +247 -120
- ddeutil/workflow/logs.py +1 -1
- ddeutil/workflow/params.py +11 -11
- ddeutil/workflow/result.py +84 -10
- ddeutil/workflow/reusables.py +15 -17
- ddeutil/workflow/stages.py +685 -450
- ddeutil/workflow/utils.py +33 -0
- ddeutil/workflow/workflow.py +177 -664
- {ddeutil_workflow-0.0.64.dist-info → ddeutil_workflow-0.0.66.dist-info}/METADATA +15 -13
- ddeutil_workflow-0.0.66.dist-info/RECORD +29 -0
- {ddeutil_workflow-0.0.64.dist-info → ddeutil_workflow-0.0.66.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.64.dist-info/RECORD +0 -28
- {ddeutil_workflow-0.0.64.dist-info → ddeutil_workflow-0.0.66.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.64.dist-info → ddeutil_workflow-0.0.66.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.64.dist-info → ddeutil_workflow-0.0.66.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -12,7 +12,7 @@ for execute on target machine instead of the current local machine.
|
|
12
12
|
making matrix values before execution parallelism stage execution.
|
13
13
|
|
14
14
|
The Job model does not implement `handler_execute` same as Stage model
|
15
|
-
because the job should raise only `
|
15
|
+
because the job should raise only `JobError` class from the execution
|
16
16
|
method.
|
17
17
|
"""
|
18
18
|
from __future__ import annotations
|
@@ -40,12 +40,18 @@ from pydantic.functional_validators import field_validator, model_validator
|
|
40
40
|
from typing_extensions import Self
|
41
41
|
|
42
42
|
from .__types import DictData, DictStr, Matrix, StrOrNone
|
43
|
-
from .
|
44
|
-
|
45
|
-
|
46
|
-
|
43
|
+
from .errors import JobCancelError, JobError, to_dict
|
44
|
+
from .result import (
|
45
|
+
CANCEL,
|
46
|
+
FAILED,
|
47
|
+
SKIP,
|
48
|
+
SUCCESS,
|
49
|
+
WAIT,
|
50
|
+
Result,
|
51
|
+
Status,
|
52
|
+
get_status_from_error,
|
53
|
+
validate_statuses,
|
47
54
|
)
|
48
|
-
from .result import CANCEL, FAILED, SKIP, SUCCESS, WAIT, Result, Status
|
49
55
|
from .reusables import has_template, param2template
|
50
56
|
from .stages import Stage
|
51
57
|
from .utils import cross_product, filter_func, gen_id
|
@@ -238,6 +244,7 @@ class SelfHostedArgs(BaseModel):
|
|
238
244
|
"""Self-Hosted arguments."""
|
239
245
|
|
240
246
|
host: str = Field(description="A host URL of the target self-hosted.")
|
247
|
+
token: SecretStr = Field(description="An API or Access token.")
|
241
248
|
|
242
249
|
|
243
250
|
class OnSelfHosted(BaseRunsOn): # pragma: no cov
|
@@ -250,6 +257,8 @@ class OnSelfHosted(BaseRunsOn): # pragma: no cov
|
|
250
257
|
|
251
258
|
|
252
259
|
class AzBatchArgs(BaseModel):
|
260
|
+
"""Azure Batch arguments."""
|
261
|
+
|
253
262
|
batch_account_name: str
|
254
263
|
batch_account_key: SecretStr
|
255
264
|
batch_account_url: str
|
@@ -431,11 +440,10 @@ class Job(BaseModel):
|
|
431
440
|
return stage
|
432
441
|
raise ValueError(f"Stage {stage_id!r} does not exists in this job.")
|
433
442
|
|
434
|
-
def check_needs(
|
435
|
-
self, jobs: dict[str, DictData]
|
436
|
-
) -> Status: # pragma: no cov
|
443
|
+
def check_needs(self, jobs: dict[str, DictData]) -> Status:
|
437
444
|
"""Return trigger status from checking job's need trigger rule logic was
|
438
|
-
valid. The return status should be SUCCESS
|
445
|
+
valid. The return status should be `SUCCESS`, `FAILED`, `WAIT`, or
|
446
|
+
`SKIP` status.
|
439
447
|
|
440
448
|
:param jobs: (dict[str, DictData]) A mapping of job ID and its context
|
441
449
|
data that return from execution process.
|
@@ -450,43 +458,98 @@ class Job(BaseModel):
|
|
450
458
|
def make_return(result: bool) -> Status:
|
451
459
|
return SUCCESS if result else FAILED
|
452
460
|
|
461
|
+
# NOTE: Filter all job result context only needed in this job.
|
453
462
|
need_exist: dict[str, Any] = {
|
454
|
-
need: jobs[need]
|
463
|
+
need: jobs[need] or {"status": SUCCESS}
|
464
|
+
for need in self.needs
|
465
|
+
if need in jobs
|
455
466
|
}
|
456
|
-
|
467
|
+
|
468
|
+
# NOTE: Return WAIT status if result context not complete, or it has any
|
469
|
+
# waiting status.
|
470
|
+
if len(need_exist) < len(self.needs) or any(
|
471
|
+
need_exist[job].get("status", SUCCESS) == WAIT for job in need_exist
|
472
|
+
):
|
457
473
|
return WAIT
|
458
|
-
|
474
|
+
|
475
|
+
# NOTE: Return SKIP status if all status are SKIP.
|
476
|
+
elif all(
|
477
|
+
need_exist[job].get("status", SUCCESS) == SKIP for job in need_exist
|
478
|
+
):
|
459
479
|
return SKIP
|
480
|
+
|
481
|
+
# NOTE: Return CANCEL status if any status is CANCEL.
|
482
|
+
elif any(
|
483
|
+
need_exist[job].get("status", SUCCESS) == CANCEL
|
484
|
+
for job in need_exist
|
485
|
+
):
|
486
|
+
return CANCEL
|
487
|
+
|
488
|
+
# NOTE: Return SUCCESS if all status not be WAIT or all SKIP.
|
460
489
|
elif self.trigger_rule == Rule.ALL_DONE:
|
461
490
|
return SUCCESS
|
491
|
+
|
462
492
|
elif self.trigger_rule == Rule.ALL_SUCCESS:
|
463
493
|
rs = all(
|
464
494
|
(
|
465
495
|
"errors" not in need_exist[job]
|
466
|
-
and
|
496
|
+
and need_exist[job].get("status", SUCCESS) == SUCCESS
|
467
497
|
)
|
468
498
|
for job in need_exist
|
469
499
|
)
|
470
500
|
elif self.trigger_rule == Rule.ALL_FAILED:
|
471
|
-
rs = all(
|
472
|
-
elif self.trigger_rule == Rule.ONE_SUCCESS:
|
473
|
-
rs = sum(
|
501
|
+
rs = all(
|
474
502
|
(
|
475
|
-
"errors"
|
476
|
-
|
503
|
+
"errors" in need_exist[job]
|
504
|
+
or need_exist[job].get("status", SUCCESS) == FAILED
|
477
505
|
)
|
478
506
|
for job in need_exist
|
479
|
-
)
|
507
|
+
)
|
508
|
+
|
509
|
+
elif self.trigger_rule == Rule.ONE_SUCCESS:
|
510
|
+
rs = (
|
511
|
+
sum(
|
512
|
+
(
|
513
|
+
"errors" not in need_exist[job]
|
514
|
+
and need_exist[job].get("status", SUCCESS) == SUCCESS
|
515
|
+
)
|
516
|
+
for job in need_exist
|
517
|
+
)
|
518
|
+
== 1
|
519
|
+
)
|
520
|
+
|
480
521
|
elif self.trigger_rule == Rule.ONE_FAILED:
|
481
|
-
rs =
|
522
|
+
rs = (
|
523
|
+
sum(
|
524
|
+
(
|
525
|
+
"errors" in need_exist[job]
|
526
|
+
or need_exist[job].get("status", SUCCESS) == FAILED
|
527
|
+
)
|
528
|
+
for job in need_exist
|
529
|
+
)
|
530
|
+
== 1
|
531
|
+
)
|
532
|
+
|
482
533
|
elif self.trigger_rule == Rule.NONE_SKIPPED:
|
483
534
|
rs = all(
|
484
|
-
|
535
|
+
need_exist[job].get("status", SUCCESS) != SKIP
|
536
|
+
for job in need_exist
|
485
537
|
)
|
538
|
+
|
486
539
|
elif self.trigger_rule == Rule.NONE_FAILED:
|
487
|
-
rs = all(
|
540
|
+
rs = all(
|
541
|
+
(
|
542
|
+
"errors" not in need_exist[job]
|
543
|
+
and need_exist[job].get("status", SUCCESS) != FAILED
|
544
|
+
)
|
545
|
+
for job in need_exist
|
546
|
+
)
|
547
|
+
|
488
548
|
else: # pragma: no cov
|
489
|
-
|
549
|
+
raise NotImplementedError(
|
550
|
+
f"Trigger rule {self.trigger_rule} does not implement on this "
|
551
|
+
f"`check_needs` method yet."
|
552
|
+
)
|
490
553
|
return make_return(rs)
|
491
554
|
|
492
555
|
def is_skipped(self, params: DictData) -> bool:
|
@@ -496,9 +559,9 @@ class Job(BaseModel):
|
|
496
559
|
:param params: (DictData) A parameter value that want to pass to condition
|
497
560
|
template.
|
498
561
|
|
499
|
-
:raise
|
562
|
+
:raise JobError: When it has any error raise from the eval
|
500
563
|
condition statement.
|
501
|
-
:raise
|
564
|
+
:raise JobError: When return type of the eval condition statement
|
502
565
|
does not return with boolean type.
|
503
566
|
|
504
567
|
:rtype: bool
|
@@ -519,7 +582,7 @@ class Job(BaseModel):
|
|
519
582
|
raise TypeError("Return type of condition does not be boolean")
|
520
583
|
return not rs
|
521
584
|
except Exception as e:
|
522
|
-
raise
|
585
|
+
raise JobError(f"{e.__class__.__name__}: {e}") from e
|
523
586
|
|
524
587
|
def set_outputs(
|
525
588
|
self,
|
@@ -561,7 +624,7 @@ class Job(BaseModel):
|
|
561
624
|
extract from the result context if it exists. If it does not found, it
|
562
625
|
will not set on the received context.
|
563
626
|
|
564
|
-
:raise
|
627
|
+
:raise JobError: If the job's ID does not set and the setting
|
565
628
|
default job ID flag does not set.
|
566
629
|
|
567
630
|
:param output: (DictData) A result data context that want to extract
|
@@ -575,34 +638,51 @@ class Job(BaseModel):
|
|
575
638
|
to["jobs"] = {}
|
576
639
|
|
577
640
|
if self.id is None and job_id is None:
|
578
|
-
raise
|
641
|
+
raise JobError(
|
579
642
|
"This job do not set the ID before setting execution output."
|
580
643
|
)
|
581
644
|
|
582
645
|
_id: str = self.id or job_id
|
583
|
-
output: DictData =
|
646
|
+
output: DictData = copy.deepcopy(output)
|
584
647
|
errors: DictData = (
|
585
|
-
{"errors": output.pop("errors"
|
648
|
+
{"errors": output.pop("errors")} if "errors" in output else {}
|
586
649
|
)
|
587
|
-
|
588
|
-
{"
|
589
|
-
if "skipped" in output
|
590
|
-
else {}
|
650
|
+
status: dict[str, Status] = (
|
651
|
+
{"status": output.pop("status")} if "status" in output else {}
|
591
652
|
)
|
592
|
-
|
593
653
|
if self.strategy.is_set():
|
594
|
-
to["jobs"][_id] = {"strategies": output
|
654
|
+
to["jobs"][_id] = {"strategies": output} | errors | status
|
595
655
|
elif len(k := output.keys()) > 1: # pragma: no cov
|
596
|
-
raise
|
656
|
+
raise JobError(
|
597
657
|
"Strategy output from execution return more than one ID while "
|
598
658
|
"this job does not set strategy."
|
599
659
|
)
|
600
660
|
else:
|
601
661
|
_output: DictData = {} if len(k) == 0 else output[list(k)[0]]
|
602
662
|
_output.pop("matrix", {})
|
603
|
-
to["jobs"][_id] =
|
663
|
+
to["jobs"][_id] = _output | errors | status
|
604
664
|
return to
|
605
665
|
|
666
|
+
def get_outputs(
|
667
|
+
self,
|
668
|
+
output: DictData,
|
669
|
+
*,
|
670
|
+
job_id: StrOrNone = None,
|
671
|
+
) -> DictData:
|
672
|
+
"""Get the outputs from jobs data. It will get this job ID or passing
|
673
|
+
custom ID from the job outputs mapping.
|
674
|
+
|
675
|
+
:param output: (DictData) A job outputs data that want to extract
|
676
|
+
:param job_id: (StrOrNone) A job ID if the `id` field does not set.
|
677
|
+
|
678
|
+
:rtype: DictData
|
679
|
+
"""
|
680
|
+
_id: str = self.id or job_id
|
681
|
+
if self.strategy.is_set():
|
682
|
+
return output.get("jobs", {}).get(_id, {}).get("strategies", {})
|
683
|
+
else:
|
684
|
+
return output.get("jobs", {}).get(_id, {})
|
685
|
+
|
606
686
|
def execute(
|
607
687
|
self,
|
608
688
|
params: DictData,
|
@@ -634,10 +714,11 @@ class Job(BaseModel):
|
|
634
714
|
)
|
635
715
|
|
636
716
|
result.trace.info(
|
637
|
-
f"[JOB]:
|
717
|
+
f"[JOB]: Routing for "
|
638
718
|
f"{''.join(self.runs_on.type.value.split('_')).title()}: "
|
639
719
|
f"{self.id!r}"
|
640
720
|
)
|
721
|
+
|
641
722
|
if self.runs_on.type == RunsOn.LOCAL:
|
642
723
|
return local_execute(
|
643
724
|
self,
|
@@ -658,13 +739,13 @@ class Job(BaseModel):
|
|
658
739
|
)
|
659
740
|
|
660
741
|
result.trace.error(
|
661
|
-
f"[JOB]:
|
742
|
+
f"[JOB]: Execution not support runs-on: {self.runs_on.type.value!r} "
|
662
743
|
f"yet."
|
663
744
|
)
|
664
745
|
return result.catch(
|
665
746
|
status=FAILED,
|
666
747
|
context={
|
667
|
-
"errors":
|
748
|
+
"errors": JobError(
|
668
749
|
f"Execute runs-on type: {self.runs_on.type.value!r} does "
|
669
750
|
f"not support yet."
|
670
751
|
).to_dict(),
|
@@ -672,6 +753,19 @@ class Job(BaseModel):
|
|
672
753
|
)
|
673
754
|
|
674
755
|
|
756
|
+
def mark_errors(context: DictData, error: JobError) -> None:
|
757
|
+
"""Make the errors context result with the refs value depends on the nested
|
758
|
+
execute func.
|
759
|
+
|
760
|
+
:param context: (DictData) A context data.
|
761
|
+
:param error: (JobError) A stage exception object.
|
762
|
+
"""
|
763
|
+
if "errors" in context:
|
764
|
+
context["errors"][error.refs] = error.to_dict()
|
765
|
+
else:
|
766
|
+
context["errors"] = error.to_dict(with_refs=True)
|
767
|
+
|
768
|
+
|
675
769
|
def local_execute_strategy(
|
676
770
|
job: Job,
|
677
771
|
strategy: DictData,
|
@@ -679,7 +773,7 @@ def local_execute_strategy(
|
|
679
773
|
*,
|
680
774
|
result: Optional[Result] = None,
|
681
775
|
event: Optional[Event] = None,
|
682
|
-
) -> Result:
|
776
|
+
) -> tuple[Status, Result]:
|
683
777
|
"""Local strategy execution with passing dynamic parameters from the
|
684
778
|
job execution and strategy matrix.
|
685
779
|
|
@@ -700,11 +794,11 @@ def local_execute_strategy(
|
|
700
794
|
:param event: (Event) An Event manager instance that use to cancel this
|
701
795
|
execution if it forces stopped by parent execution.
|
702
796
|
|
703
|
-
:raise
|
704
|
-
:raise
|
705
|
-
:raise
|
797
|
+
:raise JobError: If event was set.
|
798
|
+
:raise JobError: If stage execution raise any error as `StageError`.
|
799
|
+
:raise JobError: If the result from execution has `FAILED` status.
|
706
800
|
|
707
|
-
:rtype: Result
|
801
|
+
:rtype: tuple[Status, Result]
|
708
802
|
"""
|
709
803
|
result: Result = result or Result(
|
710
804
|
run_id=gen_id(job.id or "EMPTY", unique=True),
|
@@ -719,81 +813,92 @@ def local_execute_strategy(
|
|
719
813
|
|
720
814
|
context: DictData = copy.deepcopy(params)
|
721
815
|
context.update({"matrix": strategy, "stages": {}})
|
722
|
-
|
816
|
+
total_stage: int = len(job.stages)
|
817
|
+
skips: list[bool] = [False] * total_stage
|
818
|
+
for i, stage in enumerate(job.stages, start=0):
|
723
819
|
|
724
820
|
if job.extras:
|
725
821
|
stage.extras = job.extras
|
726
822
|
|
727
|
-
if stage.is_skipped(params=context):
|
728
|
-
result.trace.info(f"[JOB]: Skip Stage: {stage.iden!r}")
|
729
|
-
stage.set_outputs(output={"skipped": True}, to=context)
|
730
|
-
continue
|
731
|
-
|
732
823
|
if event and event.is_set():
|
733
|
-
error_msg: str =
|
824
|
+
error_msg: str = (
|
825
|
+
"Strategy execution was canceled from the event before "
|
826
|
+
"start stage execution."
|
827
|
+
)
|
734
828
|
result.catch(
|
735
829
|
status=CANCEL,
|
736
830
|
context={
|
737
831
|
strategy_id: {
|
832
|
+
"status": CANCEL,
|
738
833
|
"matrix": strategy,
|
739
834
|
"stages": filter_func(context.pop("stages", {})),
|
740
|
-
"errors":
|
835
|
+
"errors": JobCancelError(error_msg).to_dict(),
|
741
836
|
},
|
742
837
|
},
|
743
838
|
)
|
744
|
-
raise
|
839
|
+
raise JobCancelError(error_msg, refs=strategy_id)
|
840
|
+
|
841
|
+
result.trace.info(f"[JOB]: Execute Stage: {stage.iden!r}")
|
842
|
+
rs: Result = stage.handler_execute(
|
843
|
+
params=context,
|
844
|
+
run_id=result.run_id,
|
845
|
+
parent_run_id=result.parent_run_id,
|
846
|
+
event=event,
|
847
|
+
)
|
848
|
+
stage.set_outputs(rs.context, to=context)
|
745
849
|
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
850
|
+
if rs.status == SKIP:
|
851
|
+
skips[i] = True
|
852
|
+
continue
|
853
|
+
|
854
|
+
if rs.status == FAILED:
|
855
|
+
error_msg: str = (
|
856
|
+
f"Strategy execution was break because its nested-stage, "
|
857
|
+
f"{stage.iden!r}, failed."
|
753
858
|
)
|
754
|
-
stage.set_outputs(rs.context, to=context)
|
755
|
-
except StageException as e:
|
756
859
|
result.catch(
|
757
860
|
status=FAILED,
|
758
861
|
context={
|
759
862
|
strategy_id: {
|
863
|
+
"status": FAILED,
|
760
864
|
"matrix": strategy,
|
761
865
|
"stages": filter_func(context.pop("stages", {})),
|
762
|
-
"errors":
|
866
|
+
"errors": JobError(error_msg).to_dict(),
|
763
867
|
},
|
764
868
|
},
|
765
869
|
)
|
766
|
-
raise
|
767
|
-
message=f"Handler Error: {e.__class__.__name__}: {e}",
|
768
|
-
refs=strategy_id,
|
769
|
-
) from e
|
870
|
+
raise JobError(error_msg, refs=strategy_id)
|
770
871
|
|
771
|
-
|
872
|
+
elif rs.status == CANCEL:
|
772
873
|
error_msg: str = (
|
773
|
-
|
774
|
-
|
874
|
+
"Strategy execution was canceled from the event after "
|
875
|
+
"end stage execution."
|
775
876
|
)
|
776
877
|
result.catch(
|
777
|
-
status=
|
878
|
+
status=CANCEL,
|
778
879
|
context={
|
779
880
|
strategy_id: {
|
881
|
+
"status": CANCEL,
|
780
882
|
"matrix": strategy,
|
781
883
|
"stages": filter_func(context.pop("stages", {})),
|
782
|
-
"errors":
|
884
|
+
"errors": JobCancelError(error_msg).to_dict(),
|
783
885
|
},
|
784
886
|
},
|
785
887
|
)
|
786
|
-
raise
|
888
|
+
raise JobCancelError(error_msg, refs=strategy_id)
|
787
889
|
|
788
|
-
|
789
|
-
|
890
|
+
status: Status = SKIP if sum(skips) == total_stage else SUCCESS
|
891
|
+
result.catch(
|
892
|
+
status=status,
|
790
893
|
context={
|
791
894
|
strategy_id: {
|
895
|
+
"status": status,
|
792
896
|
"matrix": strategy,
|
793
897
|
"stages": filter_func(context.pop("stages", {})),
|
794
898
|
},
|
795
899
|
},
|
796
900
|
)
|
901
|
+
return status, result
|
797
902
|
|
798
903
|
|
799
904
|
def local_execute(
|
@@ -809,7 +914,7 @@ def local_execute(
|
|
809
914
|
step and run multithread on this metrics to the `stages` field of this job.
|
810
915
|
|
811
916
|
Important:
|
812
|
-
This method does not raise any `
|
917
|
+
This method does not raise any `JobError` because it allows run
|
813
918
|
parallel mode. If it raises error from strategy execution, it will catch
|
814
919
|
that error and store it in the `errors` key with list of error.
|
815
920
|
|
@@ -835,12 +940,22 @@ def local_execute(
|
|
835
940
|
extras=job.extras,
|
836
941
|
)
|
837
942
|
|
943
|
+
result.trace.info("[JOB]: Start Local executor.")
|
944
|
+
|
945
|
+
if job.desc:
|
946
|
+
result.trace.debug(f"[JOB]: Description:||{job.desc}||")
|
947
|
+
|
948
|
+
if job.is_skipped(params=params):
|
949
|
+
result.trace.info("[JOB]: Skip because job condition was valid.")
|
950
|
+
return result.catch(status=SKIP)
|
951
|
+
|
838
952
|
event: Event = event or Event()
|
839
|
-
|
840
|
-
ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
|
953
|
+
ls: str = "Fail-Fast" if job.strategy.fail_fast else "All-Completed"
|
841
954
|
workers: int = job.strategy.max_parallel
|
955
|
+
strategies: list[DictStr] = job.strategy.make()
|
956
|
+
len_strategy: int = len(strategies)
|
842
957
|
result.trace.info(
|
843
|
-
f"[JOB]:
|
958
|
+
f"[JOB]: ... Mode {ls}: {job.id!r} with {workers} "
|
844
959
|
f"worker{'s' if workers > 1 else ''}."
|
845
960
|
)
|
846
961
|
|
@@ -848,17 +963,14 @@ def local_execute(
|
|
848
963
|
return result.catch(
|
849
964
|
status=CANCEL,
|
850
965
|
context={
|
851
|
-
"errors":
|
852
|
-
"
|
966
|
+
"errors": JobCancelError(
|
967
|
+
"Execution was canceled from the event before start "
|
853
968
|
"local job execution."
|
854
969
|
).to_dict()
|
855
970
|
},
|
856
971
|
)
|
857
972
|
|
858
|
-
with ThreadPoolExecutor(
|
859
|
-
max_workers=workers, thread_name_prefix="job_strategy_exec_"
|
860
|
-
) as executor:
|
861
|
-
|
973
|
+
with ThreadPoolExecutor(workers, "jb_stg") as executor:
|
862
974
|
futures: list[Future] = [
|
863
975
|
executor.submit(
|
864
976
|
local_execute_strategy,
|
@@ -868,50 +980,57 @@ def local_execute(
|
|
868
980
|
result=result,
|
869
981
|
event=event,
|
870
982
|
)
|
871
|
-
for strategy in
|
983
|
+
for strategy in strategies
|
872
984
|
]
|
873
985
|
|
874
986
|
context: DictData = {}
|
875
|
-
|
987
|
+
statuses: list[Status] = [WAIT] * len_strategy
|
988
|
+
fail_fast: bool = False
|
876
989
|
|
877
|
-
if not
|
990
|
+
if not job.strategy.fail_fast:
|
878
991
|
done: Iterator[Future] = as_completed(futures)
|
879
992
|
else:
|
880
993
|
done, not_done = wait(futures, return_when=FIRST_EXCEPTION)
|
881
994
|
if len(list(done)) != len(futures):
|
882
995
|
result.trace.warning(
|
883
|
-
"[JOB]:
|
996
|
+
"[JOB]: Set the event for stop pending job-execution."
|
884
997
|
)
|
885
998
|
event.set()
|
886
999
|
for future in not_done:
|
887
1000
|
future.cancel()
|
888
|
-
time.sleep(0.075)
|
889
1001
|
|
890
|
-
|
891
|
-
(
|
892
|
-
|
893
|
-
|
1002
|
+
time.sleep(0.025)
|
1003
|
+
nd: str = (
|
1004
|
+
(
|
1005
|
+
f", {len(not_done)} strateg"
|
1006
|
+
f"{'ies' if len(not_done) > 1 else 'y'} not run!!!"
|
1007
|
+
)
|
1008
|
+
if not_done
|
1009
|
+
else ""
|
894
1010
|
)
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
result.trace.debug(f"[JOB]: ... Job was set Fail-Fast{nd}")
|
899
|
-
done: Iterator[Future] = as_completed(futures)
|
1011
|
+
result.trace.debug(f"[JOB]: ... Job was set Fail-Fast{nd}")
|
1012
|
+
done: Iterator[Future] = as_completed(futures)
|
1013
|
+
fail_fast: bool = True
|
900
1014
|
|
901
|
-
for future in done:
|
1015
|
+
for i, future in enumerate(done, start=0):
|
902
1016
|
try:
|
903
|
-
future.result()
|
904
|
-
except
|
905
|
-
|
1017
|
+
statuses[i], _ = future.result()
|
1018
|
+
except JobError as e:
|
1019
|
+
statuses[i] = get_status_from_error(e)
|
906
1020
|
result.trace.error(
|
907
|
-
f"[JOB]: {ls}
|
1021
|
+
f"[JOB]: {ls} Handler:||{e.__class__.__name__}: {e}"
|
908
1022
|
)
|
909
|
-
|
910
|
-
context["errors"][e.refs] = e.to_dict()
|
911
|
-
else:
|
912
|
-
context["errors"] = e.to_dict(with_refs=True)
|
1023
|
+
mark_errors(context, e)
|
913
1024
|
except CancelledError:
|
914
1025
|
pass
|
1026
|
+
|
1027
|
+
status: Status = validate_statuses(statuses)
|
1028
|
+
|
1029
|
+
# NOTE: Prepare status because it does not cancel from parent event but
|
1030
|
+
# cancel from failed item execution.
|
1031
|
+
if fail_fast and status == CANCEL:
|
1032
|
+
status = FAILED
|
1033
|
+
|
915
1034
|
return result.catch(status=status, context=context)
|
916
1035
|
|
917
1036
|
|
@@ -943,12 +1062,14 @@ def self_hosted_execute(
|
|
943
1062
|
extras=job.extras,
|
944
1063
|
)
|
945
1064
|
|
1065
|
+
result.trace.info("[JOB]: Start self-hosted executor.")
|
1066
|
+
|
946
1067
|
if event and event.is_set():
|
947
1068
|
return result.catch(
|
948
1069
|
status=CANCEL,
|
949
1070
|
context={
|
950
|
-
"errors":
|
951
|
-
"
|
1071
|
+
"errors": JobCancelError(
|
1072
|
+
"Execution was canceled from the event before start "
|
952
1073
|
"self-hosted execution."
|
953
1074
|
).to_dict()
|
954
1075
|
},
|
@@ -970,8 +1091,8 @@ def self_hosted_execute(
|
|
970
1091
|
return result.catch(status=FAILED, context={"errors": to_dict(e)})
|
971
1092
|
|
972
1093
|
if resp.status_code != 200:
|
973
|
-
raise
|
974
|
-
f"Job execution error from
|
1094
|
+
raise JobError(
|
1095
|
+
f"Job execution got error response from self-hosted: "
|
975
1096
|
f"{job.runs_on.args.host!r}"
|
976
1097
|
)
|
977
1098
|
|
@@ -1018,12 +1139,15 @@ def azure_batch_execute(
|
|
1018
1139
|
id_logic=(job.id or "EMPTY"),
|
1019
1140
|
extras=job.extras,
|
1020
1141
|
)
|
1142
|
+
|
1143
|
+
result.trace.info("[JOB]: Start Azure Batch executor.")
|
1144
|
+
|
1021
1145
|
if event and event.is_set():
|
1022
1146
|
return result.catch(
|
1023
1147
|
status=CANCEL,
|
1024
1148
|
context={
|
1025
|
-
"errors":
|
1026
|
-
"
|
1149
|
+
"errors": JobCancelError(
|
1150
|
+
"Execution was canceled from the event before start "
|
1027
1151
|
"azure-batch execution."
|
1028
1152
|
).to_dict()
|
1029
1153
|
},
|
@@ -1053,13 +1177,16 @@ def docker_execution(
|
|
1053
1177
|
id_logic=(job.id or "EMPTY"),
|
1054
1178
|
extras=job.extras,
|
1055
1179
|
)
|
1180
|
+
|
1181
|
+
result.trace.info("[JOB]: Start Docker executor.")
|
1182
|
+
|
1056
1183
|
if event and event.is_set():
|
1057
1184
|
return result.catch(
|
1058
1185
|
status=CANCEL,
|
1059
1186
|
context={
|
1060
|
-
"errors":
|
1061
|
-
"
|
1062
|
-
"
|
1187
|
+
"errors": JobCancelError(
|
1188
|
+
"Execution was canceled from the event before start "
|
1189
|
+
"start docker execution."
|
1063
1190
|
).to_dict()
|
1064
1191
|
},
|
1065
1192
|
)
|
ddeutil/workflow/logs.py
CHANGED
@@ -848,7 +848,7 @@ class FileAudit(BaseAudit):
|
|
848
848
|
"audit_path", extras=self.extras
|
849
849
|
) / self.filename_fmt.format(name=self.name, release=self.release)
|
850
850
|
|
851
|
-
def save(self, excluded: Optional[list[str]]) -> Self:
|
851
|
+
def save(self, excluded: Optional[list[str]] = None) -> Self:
|
852
852
|
"""Save logging data that receive a context data from a workflow
|
853
853
|
execution result.
|
854
854
|
|