ddeutil-workflow 0.0.73__py3-none-any.whl → 0.0.74__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/__cron.py +14 -8
- ddeutil/workflow/__init__.py +119 -10
- ddeutil/workflow/__types.py +53 -41
- ddeutil/workflow/api/__init__.py +74 -3
- ddeutil/workflow/api/routes/job.py +15 -29
- ddeutil/workflow/api/routes/logs.py +9 -9
- ddeutil/workflow/api/routes/workflows.py +3 -3
- ddeutil/workflow/audits.py +70 -55
- ddeutil/workflow/cli.py +1 -15
- ddeutil/workflow/conf.py +63 -18
- ddeutil/workflow/errors.py +86 -19
- ddeutil/workflow/event.py +268 -169
- ddeutil/workflow/job.py +331 -192
- ddeutil/workflow/params.py +37 -7
- ddeutil/workflow/result.py +96 -70
- ddeutil/workflow/reusables.py +56 -6
- ddeutil/workflow/stages.py +1059 -572
- ddeutil/workflow/traces.py +199 -120
- ddeutil/workflow/utils.py +60 -8
- ddeutil/workflow/workflow.py +424 -290
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.74.dist-info}/METADATA +27 -17
- ddeutil_workflow-0.0.74.dist-info/RECORD +30 -0
- ddeutil_workflow-0.0.73.dist-info/RECORD +0 -30
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.74.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.74.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.74.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.74.dist-info}/top_level.txt +0 -0
ddeutil/workflow/workflow.py
CHANGED
@@ -3,14 +3,25 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""Workflow
|
7
|
-
ReleaseQueue, and Workflow models.
|
6
|
+
"""Workflow Core Module.
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
This module contains the core workflow orchestration functionality, including
|
9
|
+
the Workflow model, release management, and workflow execution strategies.
|
10
|
+
|
11
|
+
The workflow system implements timeout strategy at the workflow execution layer
|
12
|
+
because the main purpose is to use Workflow as an orchestrator for complex
|
13
|
+
job execution scenarios.
|
13
14
|
|
15
|
+
Classes:
|
16
|
+
Workflow: Main workflow orchestration class
|
17
|
+
ReleaseType: Enumeration for different release types
|
18
|
+
|
19
|
+
Constants:
|
20
|
+
NORMAL: Normal release execution
|
21
|
+
RERUN: Re-execution of failed workflows
|
22
|
+
EVENT: Event-triggered execution
|
23
|
+
FORCE: Force execution regardless of conditions
|
24
|
+
"""
|
14
25
|
import copy
|
15
26
|
import time
|
16
27
|
from concurrent.futures import (
|
@@ -23,20 +34,19 @@ from enum import Enum
|
|
23
34
|
from pathlib import Path
|
24
35
|
from queue import Queue
|
25
36
|
from textwrap import dedent
|
26
|
-
from threading import Event
|
27
|
-
from typing import Any, Optional
|
37
|
+
from threading import Event as ThreadEvent
|
38
|
+
from typing import Any, Optional, Union
|
28
39
|
from zoneinfo import ZoneInfo
|
29
40
|
|
30
41
|
from pydantic import BaseModel, Field
|
31
42
|
from pydantic.functional_validators import field_validator, model_validator
|
32
43
|
from typing_extensions import Self
|
33
44
|
|
34
|
-
from . import get_status_from_error
|
35
45
|
from .__types import DictData
|
36
|
-
from .audits import Audit,
|
46
|
+
from .audits import Audit, get_audit_model
|
37
47
|
from .conf import YamlParser, dynamic
|
38
48
|
from .errors import WorkflowCancelError, WorkflowError, WorkflowTimeoutError
|
39
|
-
from .event import
|
49
|
+
from .event import Event
|
40
50
|
from .job import Job
|
41
51
|
from .params import Param
|
42
52
|
from .result import (
|
@@ -47,17 +57,31 @@ from .result import (
|
|
47
57
|
WAIT,
|
48
58
|
Result,
|
49
59
|
Status,
|
60
|
+
catch,
|
61
|
+
get_status_from_error,
|
50
62
|
validate_statuses,
|
51
63
|
)
|
52
64
|
from .reusables import has_template, param2template
|
65
|
+
from .traces import Trace, get_trace
|
53
66
|
from .utils import (
|
54
67
|
gen_id,
|
68
|
+
get_dt_ntz_now,
|
55
69
|
replace_sec,
|
56
70
|
)
|
57
71
|
|
58
72
|
|
59
73
|
class ReleaseType(str, Enum):
|
60
|
-
"""Release
|
74
|
+
"""Release type enumeration for workflow execution modes.
|
75
|
+
|
76
|
+
This enum defines the different types of workflow releases that can be
|
77
|
+
triggered, each with specific behavior and use cases.
|
78
|
+
|
79
|
+
Attributes:
|
80
|
+
NORMAL: Standard workflow release execution
|
81
|
+
RERUN: Re-execution of previously failed workflow
|
82
|
+
EVENT: Event-triggered workflow execution
|
83
|
+
FORCE: Forced execution bypassing normal conditions
|
84
|
+
"""
|
61
85
|
|
62
86
|
NORMAL = "normal"
|
63
87
|
RERUN = "rerun"
|
@@ -72,19 +96,43 @@ FORCE = ReleaseType.FORCE
|
|
72
96
|
|
73
97
|
|
74
98
|
class Workflow(BaseModel):
|
75
|
-
"""
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
99
|
+
"""Main workflow orchestration model for job and schedule management.
|
100
|
+
|
101
|
+
The Workflow class is the core component of the workflow orchestration system.
|
102
|
+
It manages job execution, scheduling via cron expressions, parameter handling,
|
103
|
+
and provides comprehensive execution capabilities for complex workflows.
|
104
|
+
|
105
|
+
This class extends Pydantic BaseModel to provide robust data validation and
|
106
|
+
serialization while maintaining lightweight performance characteristics.
|
107
|
+
|
108
|
+
Attributes:
|
109
|
+
extras (dict): Extra parameters for overriding configuration values
|
110
|
+
name (str): Unique workflow identifier
|
111
|
+
desc (str, optional): Workflow description supporting markdown content
|
112
|
+
params (dict[str, Param]): Parameter definitions for the workflow
|
113
|
+
on (list[Crontab]): Schedule definitions using cron expressions
|
114
|
+
jobs (dict[str, Job]): Collection of jobs within this workflow
|
115
|
+
|
116
|
+
Example:
|
117
|
+
Create and execute a workflow:
|
118
|
+
|
119
|
+
```python
|
120
|
+
workflow = Workflow.from_conf('my-workflow')
|
121
|
+
result = workflow.execute({
|
122
|
+
'param1': 'value1',
|
123
|
+
'param2': 'value2'
|
124
|
+
})
|
125
|
+
```
|
126
|
+
|
127
|
+
Note:
|
128
|
+
Workflows can be executed immediately or scheduled for background
|
129
|
+
execution using the cron-like scheduling system.
|
81
130
|
"""
|
82
131
|
|
83
132
|
extras: DictData = Field(
|
84
133
|
default_factory=dict,
|
85
134
|
description="An extra parameters that want to override config values.",
|
86
135
|
)
|
87
|
-
|
88
136
|
name: str = Field(description="A workflow name.")
|
89
137
|
desc: Optional[str] = Field(
|
90
138
|
default=None,
|
@@ -96,14 +144,28 @@ class Workflow(BaseModel):
|
|
96
144
|
default_factory=dict,
|
97
145
|
description="A parameters that need to use on this workflow.",
|
98
146
|
)
|
99
|
-
on:
|
147
|
+
on: Event = Field(
|
100
148
|
default_factory=list,
|
101
|
-
description="
|
149
|
+
description="An events for this workflow.",
|
102
150
|
)
|
103
151
|
jobs: dict[str, Job] = Field(
|
104
152
|
default_factory=dict,
|
105
153
|
description="A mapping of job ID and job model that already loaded.",
|
106
154
|
)
|
155
|
+
created_at: datetime = Field(
|
156
|
+
default_factory=get_dt_ntz_now,
|
157
|
+
description=(
|
158
|
+
"A created datetime of this workflow template when loading from "
|
159
|
+
"file."
|
160
|
+
),
|
161
|
+
)
|
162
|
+
updated_dt: datetime = Field(
|
163
|
+
default_factory=get_dt_ntz_now,
|
164
|
+
description=(
|
165
|
+
"A updated datetime of this workflow template when loading from "
|
166
|
+
"file."
|
167
|
+
),
|
168
|
+
)
|
107
169
|
|
108
170
|
@classmethod
|
109
171
|
def from_conf(
|
@@ -111,20 +173,38 @@ class Workflow(BaseModel):
|
|
111
173
|
name: str,
|
112
174
|
*,
|
113
175
|
path: Optional[Path] = None,
|
114
|
-
extras: DictData
|
176
|
+
extras: Optional[DictData] = None,
|
115
177
|
) -> Self:
|
116
|
-
"""Create Workflow instance from
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
:
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
:
|
178
|
+
"""Create Workflow instance from configuration file.
|
179
|
+
|
180
|
+
Loads workflow configuration from YAML files and creates a validated
|
181
|
+
Workflow instance. The configuration loader searches for workflow
|
182
|
+
definitions in the specified path or default configuration directories.
|
183
|
+
|
184
|
+
Args:
|
185
|
+
name: Workflow name to load from configuration
|
186
|
+
path: Optional custom configuration path to search
|
187
|
+
extras: Additional parameters to override configuration values
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
Self: Validated Workflow instance loaded from configuration
|
191
|
+
|
192
|
+
Raises:
|
193
|
+
ValueError: If workflow type doesn't match or configuration invalid
|
194
|
+
FileNotFoundError: If workflow configuration file not found
|
195
|
+
|
196
|
+
Example:
|
197
|
+
```python
|
198
|
+
# Load from default config path
|
199
|
+
workflow = Workflow.from_conf('data-pipeline')
|
200
|
+
|
201
|
+
# Load with custom path and extras
|
202
|
+
workflow = Workflow.from_conf(
|
203
|
+
'data-pipeline',
|
204
|
+
path=Path('./custom-configs'),
|
205
|
+
extras={'environment': 'production'}
|
206
|
+
)
|
207
|
+
```
|
128
208
|
"""
|
129
209
|
load: YamlParser = YamlParser(name, path=path, extras=extras, obj=cls)
|
130
210
|
|
@@ -138,105 +218,38 @@ class Workflow(BaseModel):
|
|
138
218
|
if extras:
|
139
219
|
data["extras"] = extras
|
140
220
|
|
141
|
-
cls.__bypass_on__(data, path=load.path, extras=extras)
|
142
221
|
return cls.model_validate(obj=data)
|
143
222
|
|
144
|
-
@
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
) -> DictData:
|
151
|
-
"""Bypass the on data to loaded config data.
|
152
|
-
|
153
|
-
:param data: (DictData) A data to construct to this Workflow model.
|
154
|
-
:param path: (Path) A config path.
|
155
|
-
:param extras: (DictData) An extra parameters that want to override core
|
156
|
-
config values.
|
157
|
-
|
158
|
-
:rtype: DictData
|
159
|
-
"""
|
160
|
-
if on := data.pop("on", []):
|
161
|
-
if isinstance(on, str):
|
162
|
-
on: list[str] = [on]
|
163
|
-
if any(not isinstance(i, (dict, str)) for i in on):
|
164
|
-
raise TypeError("The `on` key should be list of str or dict")
|
165
|
-
|
166
|
-
# NOTE: Pass on value to SimLoad and keep on model object to the on
|
167
|
-
# field.
|
168
|
-
data["on"] = [
|
169
|
-
(
|
170
|
-
YamlParser(n, path=path, extras=extras).data
|
171
|
-
if isinstance(n, str)
|
172
|
-
else n
|
173
|
-
)
|
174
|
-
for n in on
|
175
|
-
]
|
176
|
-
return data
|
177
|
-
|
178
|
-
@model_validator(mode="before")
|
179
|
-
def __prepare_model_before__(cls, data: Any) -> Any:
|
223
|
+
@field_validator(
|
224
|
+
"params",
|
225
|
+
mode="before",
|
226
|
+
json_schema_input_type=Union[dict[str, Param], dict[str, str]],
|
227
|
+
)
|
228
|
+
def __prepare_params(cls, data: Any) -> Any:
|
180
229
|
"""Prepare the params key in the data model before validating."""
|
181
|
-
if isinstance(data, dict)
|
182
|
-
data
|
183
|
-
|
184
|
-
|
185
|
-
if isinstance(params[p], str)
|
186
|
-
else params[p]
|
187
|
-
)
|
188
|
-
for p in params
|
230
|
+
if isinstance(data, dict):
|
231
|
+
data = {
|
232
|
+
k: ({"type": v} if isinstance(v, str) else v)
|
233
|
+
for k, v in data.items()
|
189
234
|
}
|
190
235
|
return data
|
191
236
|
|
192
237
|
@field_validator("desc", mode="after")
|
193
|
-
def __dedent_desc__(cls,
|
238
|
+
def __dedent_desc__(cls, data: str) -> str:
|
194
239
|
"""Prepare description string that was created on a template.
|
195
240
|
|
196
|
-
:
|
197
|
-
|
198
|
-
"""
|
199
|
-
return dedent(value.lstrip("\n"))
|
200
|
-
|
201
|
-
@field_validator("on", mode="after")
|
202
|
-
def __on_no_dup_and_reach_limit__(
|
203
|
-
cls,
|
204
|
-
value: list[Crontab],
|
205
|
-
) -> list[Crontab]:
|
206
|
-
"""Validate the on fields should not contain duplicate values and if it
|
207
|
-
contains the every minute value more than one value, it will remove to
|
208
|
-
only one value.
|
209
|
-
|
210
|
-
:raise ValueError: If it has some duplicate value.
|
241
|
+
Args:
|
242
|
+
data: A description string value that want to dedent.
|
211
243
|
|
212
|
-
:
|
213
|
-
|
214
|
-
:rtype: list[Crontab]
|
244
|
+
Returns:
|
245
|
+
str: The de-dented description string.
|
215
246
|
"""
|
216
|
-
|
217
|
-
if len(set_ons) != len(value):
|
218
|
-
raise ValueError(
|
219
|
-
"The on fields should not contain duplicate on value."
|
220
|
-
)
|
247
|
+
return dedent(data.lstrip("\n"))
|
221
248
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
# "only one value in the on field."
|
227
|
-
# )
|
228
|
-
set_tz: set[str] = {on.tz for on in value}
|
229
|
-
if len(set_tz) > 1:
|
230
|
-
raise ValueError(
|
231
|
-
f"The on fields should not contain multiple timezone, "
|
232
|
-
f"{list(set_tz)}."
|
233
|
-
)
|
234
|
-
|
235
|
-
if len(set_ons) > 10:
|
236
|
-
raise ValueError(
|
237
|
-
"The number of the on should not more than 10 crontabs."
|
238
|
-
)
|
239
|
-
return value
|
249
|
+
@field_validator("created_at", "updated_dt", mode="after")
|
250
|
+
def __convert_tz__(cls, dt: datetime) -> datetime:
|
251
|
+
"""Replace timezone of datetime type to no timezone."""
|
252
|
+
return dt.replace(tzinfo=None)
|
240
253
|
|
241
254
|
@model_validator(mode="after")
|
242
255
|
def __validate_jobs_need__(self) -> Self:
|
@@ -277,13 +290,15 @@ class Workflow(BaseModel):
|
|
277
290
|
or job's ID. This method will pass an extra parameter from this model
|
278
291
|
to the returned Job model.
|
279
292
|
|
280
|
-
:
|
281
|
-
job
|
293
|
+
Args:
|
294
|
+
name: A job name or ID that want to get from a mapping of
|
295
|
+
job models.
|
282
296
|
|
283
|
-
:
|
297
|
+
Returns:
|
298
|
+
Job: A job model that exists on this workflow by input name.
|
284
299
|
|
285
|
-
:
|
286
|
-
|
300
|
+
Raises:
|
301
|
+
ValueError: If a name or ID does not exist on the jobs field.
|
287
302
|
"""
|
288
303
|
if name not in self.jobs:
|
289
304
|
raise ValueError(
|
@@ -306,15 +321,17 @@ class Workflow(BaseModel):
|
|
306
321
|
... "jobs": {}
|
307
322
|
... }
|
308
323
|
|
309
|
-
:
|
310
|
-
|
324
|
+
Args:
|
325
|
+
params: A parameter data that receive from workflow
|
326
|
+
execute method.
|
311
327
|
|
312
|
-
:
|
313
|
-
|
328
|
+
Returns:
|
329
|
+
DictData: The parameter value that validate with its parameter fields and
|
330
|
+
adding jobs key to this parameter.
|
314
331
|
|
315
|
-
:
|
316
|
-
|
317
|
-
|
332
|
+
Raises:
|
333
|
+
WorkflowError: If parameter value that want to validate does
|
334
|
+
not include the necessary parameter that had required flag.
|
318
335
|
"""
|
319
336
|
# VALIDATE: Incoming params should have keys that set on this workflow.
|
320
337
|
check_key: list[str] = [
|
@@ -346,15 +363,17 @@ class Workflow(BaseModel):
|
|
346
363
|
millisecond to 0 and replaced timezone to None before checking it match
|
347
364
|
with the set `on` field.
|
348
365
|
|
349
|
-
:
|
366
|
+
Args:
|
367
|
+
dt: A datetime object that want to validate.
|
350
368
|
|
351
|
-
:
|
369
|
+
Returns:
|
370
|
+
datetime: The validated release datetime.
|
352
371
|
"""
|
353
372
|
release: datetime = replace_sec(dt.replace(tzinfo=None))
|
354
373
|
if not self.on:
|
355
374
|
return release
|
356
375
|
|
357
|
-
for on in self.on:
|
376
|
+
for on in self.on.schedule:
|
358
377
|
if release == on.cronjob.schedule(release).next:
|
359
378
|
return release
|
360
379
|
raise WorkflowError(
|
@@ -368,10 +387,8 @@ class Workflow(BaseModel):
|
|
368
387
|
*,
|
369
388
|
release_type: ReleaseType = NORMAL,
|
370
389
|
run_id: Optional[str] = None,
|
371
|
-
parent_run_id: Optional[str] = None,
|
372
390
|
audit: type[Audit] = None,
|
373
391
|
override_log_name: Optional[str] = None,
|
374
|
-
result: Optional[Result] = None,
|
375
392
|
timeout: int = 600,
|
376
393
|
excluded: Optional[list[str]] = None,
|
377
394
|
) -> Result:
|
@@ -393,31 +410,28 @@ class Workflow(BaseModel):
|
|
393
410
|
:param params: A workflow parameter that pass to execute method.
|
394
411
|
:param release_type:
|
395
412
|
:param run_id: (str) A workflow running ID.
|
396
|
-
:param parent_run_id: (str) A parent workflow running ID.
|
397
413
|
:param audit: An audit class that want to save the execution result.
|
398
414
|
:param override_log_name: (str) An override logging name that use
|
399
415
|
instead the workflow name.
|
400
|
-
:param result: (Result) A result object for keeping context and status
|
401
|
-
data.
|
402
416
|
:param timeout: (int) A workflow execution time out in second unit.
|
403
417
|
:param excluded: (list[str]) A list of key that want to exclude from
|
404
418
|
audit data.
|
405
419
|
|
406
420
|
:rtype: Result
|
407
421
|
"""
|
408
|
-
audit: type[Audit] = audit or get_audit(extras=self.extras)
|
409
422
|
name: str = override_log_name or self.name
|
410
|
-
|
411
|
-
|
412
|
-
run_id=
|
413
|
-
|
414
|
-
|
415
|
-
|
423
|
+
if run_id:
|
424
|
+
parent_run_id: str = run_id
|
425
|
+
run_id: str = gen_id(name, unique=True)
|
426
|
+
else:
|
427
|
+
run_id: str = gen_id(name, unique=True)
|
428
|
+
parent_run_id: str = run_id
|
429
|
+
context: DictData = {}
|
430
|
+
trace: Trace = get_trace(
|
431
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
416
432
|
)
|
417
433
|
release: datetime = self.validate_release(dt=release)
|
418
|
-
|
419
|
-
f"[RELEASE]: Start {name!r} : {release:%Y-%m-%d %H:%M:%S}"
|
420
|
-
)
|
434
|
+
trace.info(f"[RELEASE]: Start {name!r} : {release:%Y-%m-%d %H:%M:%S}")
|
421
435
|
tz: ZoneInfo = dynamic("tz", extras=self.extras)
|
422
436
|
values: DictData = param2template(
|
423
437
|
params,
|
@@ -425,58 +439,61 @@ class Workflow(BaseModel):
|
|
425
439
|
"release": {
|
426
440
|
"logical_date": release,
|
427
441
|
"execute_date": datetime.now(tz=tz),
|
428
|
-
"run_id":
|
442
|
+
"run_id": run_id,
|
429
443
|
}
|
430
444
|
},
|
431
445
|
extras=self.extras,
|
432
446
|
)
|
433
447
|
rs: Result = self.execute(
|
434
448
|
params=values,
|
435
|
-
parent_run_id
|
449
|
+
run_id=parent_run_id,
|
436
450
|
timeout=timeout,
|
437
451
|
)
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
)
|
442
|
-
result.trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
|
452
|
+
catch(context, status=rs.status, updated=rs.context)
|
453
|
+
trace.info(f"[RELEASE]: End {name!r} : {release:%Y-%m-%d %H:%M:%S}")
|
454
|
+
trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
|
443
455
|
(
|
444
|
-
audit(
|
456
|
+
(audit or get_audit_model(extras=self.extras))(
|
445
457
|
name=name,
|
446
458
|
release=release,
|
447
459
|
type=release_type,
|
448
|
-
context=
|
449
|
-
parent_run_id=
|
450
|
-
run_id=
|
451
|
-
execution_time=
|
460
|
+
context=context,
|
461
|
+
parent_run_id=parent_run_id,
|
462
|
+
run_id=run_id,
|
463
|
+
execution_time=rs.info.get("execution_time", 0),
|
452
464
|
extras=self.extras,
|
453
465
|
).save(excluded=excluded)
|
454
466
|
)
|
455
|
-
return
|
467
|
+
return Result(
|
468
|
+
run_id=run_id,
|
469
|
+
parent_run_id=parent_run_id,
|
456
470
|
status=rs.status,
|
457
|
-
context=
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
"
|
471
|
+
context=catch(
|
472
|
+
context,
|
473
|
+
status=rs.status,
|
474
|
+
updated={
|
475
|
+
"params": params,
|
476
|
+
"release": {
|
477
|
+
"type": release_type,
|
478
|
+
"logical_date": release,
|
479
|
+
},
|
480
|
+
**{"jobs": context.pop("jobs", {})},
|
481
|
+
**(context["errors"] if "errors" in context else {}),
|
462
482
|
},
|
463
|
-
|
464
|
-
|
465
|
-
result.context["errors"]
|
466
|
-
if "errors" in result.context
|
467
|
-
else {}
|
468
|
-
),
|
469
|
-
},
|
483
|
+
),
|
484
|
+
extras=self.extras,
|
470
485
|
)
|
471
486
|
|
472
487
|
def execute_job(
|
473
488
|
self,
|
474
489
|
job: Job,
|
475
490
|
params: DictData,
|
491
|
+
run_id: str,
|
492
|
+
context: DictData,
|
476
493
|
*,
|
477
|
-
|
478
|
-
event: Optional[
|
479
|
-
) -> tuple[Status,
|
494
|
+
parent_run_id: Optional[str] = None,
|
495
|
+
event: Optional[ThreadEvent] = None,
|
496
|
+
) -> tuple[Status, DictData]:
|
480
497
|
"""Job execution with passing dynamic parameters from the main workflow
|
481
498
|
execution to the target job object via job's ID.
|
482
499
|
|
@@ -487,42 +504,48 @@ class Workflow(BaseModel):
|
|
487
504
|
This method do not raise any error, and it will handle all exception
|
488
505
|
from the job execution.
|
489
506
|
|
490
|
-
:
|
491
|
-
|
492
|
-
|
493
|
-
|
507
|
+
Args:
|
508
|
+
job: (Job) A job model that want to execute.
|
509
|
+
params: (DictData) A parameter data.
|
510
|
+
run_id: A running stage ID.
|
511
|
+
context: A context data.
|
512
|
+
parent_run_id: A parent running ID. (Default is None)
|
513
|
+
event: (Event) An Event manager instance that use to cancel this
|
494
514
|
execution if it forces stopped by parent execution.
|
495
515
|
|
496
|
-
:
|
516
|
+
Returns:
|
517
|
+
tuple[Status, DictData]: The pair of status and result context data.
|
497
518
|
"""
|
498
|
-
|
499
|
-
|
519
|
+
trace: Trace = get_trace(
|
520
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
521
|
+
)
|
500
522
|
if event and event.is_set():
|
501
523
|
error_msg: str = (
|
502
524
|
"Job execution was canceled because the event was set "
|
503
525
|
"before start job execution."
|
504
526
|
)
|
505
|
-
return CANCEL,
|
527
|
+
return CANCEL, catch(
|
528
|
+
context=context,
|
506
529
|
status=CANCEL,
|
507
|
-
|
530
|
+
updated={
|
508
531
|
"errors": WorkflowCancelError(error_msg).to_dict(),
|
509
532
|
},
|
510
533
|
)
|
511
534
|
|
512
|
-
|
535
|
+
trace.info(f"[WORKFLOW]: Execute Job: {job.id!r}")
|
513
536
|
rs: Result = job.execute(
|
514
537
|
params=params,
|
515
|
-
run_id=
|
516
|
-
parent_run_id=result.parent_run_id,
|
538
|
+
run_id=parent_run_id,
|
517
539
|
event=event,
|
518
540
|
)
|
519
541
|
job.set_outputs(rs.context, to=params)
|
520
542
|
|
521
543
|
if rs.status == FAILED:
|
522
544
|
error_msg: str = f"Job execution, {job.id!r}, was failed."
|
523
|
-
return FAILED,
|
545
|
+
return FAILED, catch(
|
546
|
+
context=context,
|
524
547
|
status=FAILED,
|
525
|
-
|
548
|
+
updated={
|
526
549
|
"errors": WorkflowError(error_msg).to_dict(),
|
527
550
|
**params,
|
528
551
|
},
|
@@ -533,23 +556,25 @@ class Workflow(BaseModel):
|
|
533
556
|
f"Job execution, {job.id!r}, was canceled from the event after "
|
534
557
|
f"end job execution."
|
535
558
|
)
|
536
|
-
return CANCEL,
|
559
|
+
return CANCEL, catch(
|
560
|
+
context=context,
|
537
561
|
status=CANCEL,
|
538
|
-
|
562
|
+
updated={
|
539
563
|
"errors": WorkflowCancelError(error_msg).to_dict(),
|
540
564
|
**params,
|
541
565
|
},
|
542
566
|
)
|
543
567
|
|
544
|
-
return rs.status,
|
568
|
+
return rs.status, catch(
|
569
|
+
context=context, status=rs.status, updated=params
|
570
|
+
)
|
545
571
|
|
546
572
|
def execute(
|
547
573
|
self,
|
548
574
|
params: DictData,
|
549
575
|
*,
|
550
576
|
run_id: Optional[str] = None,
|
551
|
-
|
552
|
-
event: Optional[Event] = None,
|
577
|
+
event: Optional[ThreadEvent] = None,
|
553
578
|
timeout: float = 3600,
|
554
579
|
max_job_parallel: int = 2,
|
555
580
|
) -> Result:
|
@@ -598,7 +623,6 @@ class Workflow(BaseModel):
|
|
598
623
|
|
599
624
|
:param params: A parameter data that will parameterize before execution.
|
600
625
|
:param run_id: (Optional[str]) A workflow running ID.
|
601
|
-
:param parent_run_id: (Optional[str]) A parent workflow running ID.
|
602
626
|
:param event: (Event) An Event manager instance that use to cancel this
|
603
627
|
execution if it forces stopped by parent execution.
|
604
628
|
:param timeout: (float) A workflow execution time out in second unit
|
@@ -611,24 +635,30 @@ class Workflow(BaseModel):
|
|
611
635
|
:rtype: Result
|
612
636
|
"""
|
613
637
|
ts: float = time.monotonic()
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
extras=self.extras,
|
638
|
+
parent_run_id: Optional[str] = run_id
|
639
|
+
run_id: str = gen_id(self.name, extras=self.extras)
|
640
|
+
trace: Trace = get_trace(
|
641
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
619
642
|
)
|
620
643
|
context: DictData = self.parameterize(params)
|
621
|
-
event:
|
644
|
+
event: ThreadEvent = event or ThreadEvent()
|
622
645
|
max_job_parallel: int = dynamic(
|
623
646
|
"max_job_parallel", f=max_job_parallel, extras=self.extras
|
624
647
|
)
|
625
|
-
|
648
|
+
trace.info(
|
626
649
|
f"[WORKFLOW]: Execute: {self.name!r} ("
|
627
650
|
f"{'parallel' if max_job_parallel > 1 else 'sequential'} jobs)"
|
628
651
|
)
|
629
652
|
if not self.jobs:
|
630
|
-
|
631
|
-
return
|
653
|
+
trace.warning(f"[WORKFLOW]: {self.name!r} does not set jobs")
|
654
|
+
return Result(
|
655
|
+
run_id=run_id,
|
656
|
+
parent_run_id=parent_run_id,
|
657
|
+
status=SUCCESS,
|
658
|
+
context=catch(context, status=SUCCESS),
|
659
|
+
info={"execution_time": time.monotonic() - ts},
|
660
|
+
extras=self.extras,
|
661
|
+
)
|
632
662
|
|
633
663
|
job_queue: Queue = Queue()
|
634
664
|
for job_id in self.jobs:
|
@@ -642,20 +672,30 @@ class Workflow(BaseModel):
|
|
642
672
|
timeout: float = dynamic(
|
643
673
|
"max_job_exec_timeout", f=timeout, extras=self.extras
|
644
674
|
)
|
645
|
-
|
675
|
+
catch(context, status=WAIT)
|
646
676
|
if event and event.is_set():
|
647
|
-
return
|
677
|
+
return Result(
|
678
|
+
run_id=run_id,
|
679
|
+
parent_run_id=parent_run_id,
|
648
680
|
status=CANCEL,
|
649
|
-
context=
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
681
|
+
context=catch(
|
682
|
+
context,
|
683
|
+
status=CANCEL,
|
684
|
+
updated={
|
685
|
+
"errors": WorkflowCancelError(
|
686
|
+
"Execution was canceled from the event was set "
|
687
|
+
"before workflow execution."
|
688
|
+
).to_dict(),
|
689
|
+
},
|
690
|
+
),
|
691
|
+
info={"execution_time": time.monotonic() - ts},
|
692
|
+
extras=self.extras,
|
655
693
|
)
|
656
694
|
|
657
695
|
with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
|
658
696
|
futures: list[Future] = []
|
697
|
+
backoff_sleep = 0.01 # Start with smaller sleep time
|
698
|
+
consecutive_waits = 0 # Track consecutive wait states
|
659
699
|
|
660
700
|
while not job_queue.empty() and (
|
661
701
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
@@ -665,21 +705,37 @@ class Workflow(BaseModel):
|
|
665
705
|
if (check := job.check_needs(context["jobs"])) == WAIT:
|
666
706
|
job_queue.task_done()
|
667
707
|
job_queue.put(job_id)
|
668
|
-
|
708
|
+
consecutive_waits += 1
|
709
|
+
# Exponential backoff up to 0.15s max
|
710
|
+
backoff_sleep = min(backoff_sleep * 1.5, 0.15)
|
711
|
+
time.sleep(backoff_sleep)
|
669
712
|
continue
|
670
|
-
|
671
|
-
|
713
|
+
|
714
|
+
# Reset backoff when we can proceed
|
715
|
+
consecutive_waits = 0
|
716
|
+
backoff_sleep = 0.01
|
717
|
+
|
718
|
+
if check == FAILED: # pragma: no cov
|
719
|
+
return Result(
|
720
|
+
run_id=run_id,
|
721
|
+
parent_run_id=parent_run_id,
|
672
722
|
status=FAILED,
|
673
|
-
context=
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
723
|
+
context=catch(
|
724
|
+
context,
|
725
|
+
status=FAILED,
|
726
|
+
updated={
|
727
|
+
"status": FAILED,
|
728
|
+
"errors": WorkflowError(
|
729
|
+
f"Validate job trigger rule was failed "
|
730
|
+
f"with {job.trigger_rule.value!r}."
|
731
|
+
).to_dict(),
|
732
|
+
},
|
733
|
+
),
|
734
|
+
info={"execution_time": time.monotonic() - ts},
|
735
|
+
extras=self.extras,
|
680
736
|
)
|
681
737
|
elif check == SKIP: # pragma: no cov
|
682
|
-
|
738
|
+
trace.info(
|
683
739
|
f"[JOB]: Skip job: {job_id!r} from trigger rule."
|
684
740
|
)
|
685
741
|
job.set_outputs(output={"status": SKIP}, to=context)
|
@@ -693,7 +749,9 @@ class Workflow(BaseModel):
|
|
693
749
|
self.execute_job,
|
694
750
|
job=job,
|
695
751
|
params=context,
|
696
|
-
|
752
|
+
run_id=run_id,
|
753
|
+
context=context,
|
754
|
+
parent_run_id=parent_run_id,
|
697
755
|
event=event,
|
698
756
|
),
|
699
757
|
)
|
@@ -706,7 +764,9 @@ class Workflow(BaseModel):
|
|
706
764
|
self.execute_job,
|
707
765
|
job=job,
|
708
766
|
params=context,
|
709
|
-
|
767
|
+
run_id=run_id,
|
768
|
+
context=context,
|
769
|
+
parent_run_id=parent_run_id,
|
710
770
|
event=event,
|
711
771
|
)
|
712
772
|
)
|
@@ -726,7 +786,7 @@ class Workflow(BaseModel):
|
|
726
786
|
else: # pragma: no cov
|
727
787
|
job_queue.put(job_id)
|
728
788
|
futures.insert(0, future)
|
729
|
-
|
789
|
+
trace.warning(
|
730
790
|
f"[WORKFLOW]: ... Execution non-threading not "
|
731
791
|
f"handle: {future}."
|
732
792
|
)
|
@@ -749,44 +809,58 @@ class Workflow(BaseModel):
|
|
749
809
|
for i, s in enumerate(sequence_statuses, start=0):
|
750
810
|
statuses[total + 1 + skip_count + i] = s
|
751
811
|
|
752
|
-
|
753
|
-
|
812
|
+
st: Status = validate_statuses(statuses)
|
813
|
+
return Result(
|
814
|
+
run_id=run_id,
|
815
|
+
parent_run_id=parent_run_id,
|
816
|
+
status=st,
|
817
|
+
context=catch(context, status=st),
|
818
|
+
info={"execution_time": time.monotonic() - ts},
|
819
|
+
extras=self.extras,
|
754
820
|
)
|
755
821
|
|
756
822
|
event.set()
|
757
823
|
for future in futures:
|
758
824
|
future.cancel()
|
759
825
|
|
760
|
-
|
826
|
+
trace.error(
|
761
827
|
f"[WORKFLOW]: {self.name!r} was timeout because it use exec "
|
762
828
|
f"time more than {timeout} seconds."
|
763
829
|
)
|
764
830
|
|
765
831
|
time.sleep(0.0025)
|
766
832
|
|
767
|
-
return
|
833
|
+
return Result(
|
834
|
+
run_id=run_id,
|
835
|
+
parent_run_id=parent_run_id,
|
768
836
|
status=FAILED,
|
769
|
-
context=
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
837
|
+
context=catch(
|
838
|
+
context,
|
839
|
+
status=FAILED,
|
840
|
+
updated={
|
841
|
+
"errors": WorkflowTimeoutError(
|
842
|
+
f"{self.name!r} was timeout because it use exec time "
|
843
|
+
f"more than {timeout} seconds."
|
844
|
+
).to_dict(),
|
845
|
+
},
|
846
|
+
),
|
847
|
+
info={"execution_time": time.monotonic() - ts},
|
848
|
+
extras=self.extras,
|
775
849
|
)
|
776
850
|
|
777
851
|
def rerun(
|
778
852
|
self,
|
779
853
|
context: DictData,
|
780
854
|
*,
|
781
|
-
|
782
|
-
event: Optional[
|
855
|
+
run_id: Optional[str] = None,
|
856
|
+
event: Optional[ThreadEvent] = None,
|
783
857
|
timeout: float = 3600,
|
784
858
|
max_job_parallel: int = 2,
|
785
859
|
) -> Result:
|
786
860
|
"""Re-Execute workflow with passing the error context data.
|
787
861
|
|
788
862
|
:param context: A context result that get the failed status.
|
789
|
-
:param
|
863
|
+
:param run_id: (Optional[str]) A workflow running ID.
|
790
864
|
:param event: (Event) An Event manager instance that use to cancel this
|
791
865
|
execution if it forces stopped by parent execution.
|
792
866
|
:param timeout: (float) A workflow execution time out in second unit
|
@@ -796,36 +870,49 @@ class Workflow(BaseModel):
|
|
796
870
|
:param max_job_parallel: (int) The maximum workers that use for job
|
797
871
|
execution in `ThreadPoolExecutor` object. (Default: 2 workers)
|
798
872
|
|
799
|
-
|
873
|
+
Returns
|
874
|
+
Result: Return Result object that create from execution context with
|
875
|
+
return mode.
|
800
876
|
"""
|
801
877
|
ts: float = time.monotonic()
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
extras=self.extras,
|
878
|
+
parent_run_id: str = run_id
|
879
|
+
run_id: str = gen_id(self.name, extras=self.extras)
|
880
|
+
trace: Trace = get_trace(
|
881
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
807
882
|
)
|
808
883
|
if context["status"] == SUCCESS:
|
809
|
-
|
884
|
+
trace.info(
|
810
885
|
"[WORKFLOW]: Does not rerun because it already executed with "
|
811
886
|
"success status."
|
812
887
|
)
|
813
|
-
return
|
888
|
+
return Result(
|
889
|
+
run_id=run_id,
|
890
|
+
parent_run_id=parent_run_id,
|
891
|
+
status=SUCCESS,
|
892
|
+
context=catch(context=context, status=SUCCESS),
|
893
|
+
extras=self.extras,
|
894
|
+
)
|
814
895
|
|
815
896
|
err = context["errors"]
|
816
|
-
|
897
|
+
trace.info(f"[WORKFLOW]: Previous error: {err}")
|
817
898
|
|
818
|
-
event:
|
899
|
+
event: ThreadEvent = event or ThreadEvent()
|
819
900
|
max_job_parallel: int = dynamic(
|
820
901
|
"max_job_parallel", f=max_job_parallel, extras=self.extras
|
821
902
|
)
|
822
|
-
|
903
|
+
trace.info(
|
823
904
|
f"[WORKFLOW]: Execute: {self.name!r} ("
|
824
905
|
f"{'parallel' if max_job_parallel > 1 else 'sequential'} jobs)"
|
825
906
|
)
|
826
907
|
if not self.jobs:
|
827
|
-
|
828
|
-
return
|
908
|
+
trace.warning(f"[WORKFLOW]: {self.name!r} does not set jobs")
|
909
|
+
return Result(
|
910
|
+
run_id=run_id,
|
911
|
+
parent_run_id=parent_run_id,
|
912
|
+
status=SUCCESS,
|
913
|
+
context=catch(context=context, status=SUCCESS),
|
914
|
+
extras=self.extras,
|
915
|
+
)
|
829
916
|
|
830
917
|
# NOTE: Prepare the new context for rerun process.
|
831
918
|
jobs: DictData = context.get("jobs")
|
@@ -845,8 +932,14 @@ class Workflow(BaseModel):
|
|
845
932
|
total_job += 1
|
846
933
|
|
847
934
|
if total_job == 0:
|
848
|
-
|
849
|
-
return
|
935
|
+
trace.warning("[WORKFLOW]: It does not have job to rerun.")
|
936
|
+
return Result(
|
937
|
+
run_id=run_id,
|
938
|
+
parent_run_id=parent_run_id,
|
939
|
+
status=SUCCESS,
|
940
|
+
context=catch(context=context, status=SUCCESS),
|
941
|
+
extras=self.extras,
|
942
|
+
)
|
850
943
|
|
851
944
|
not_timeout_flag: bool = True
|
852
945
|
statuses: list[Status] = [WAIT] * total_job
|
@@ -856,20 +949,29 @@ class Workflow(BaseModel):
|
|
856
949
|
"max_job_exec_timeout", f=timeout, extras=self.extras
|
857
950
|
)
|
858
951
|
|
859
|
-
|
952
|
+
catch(new_context, status=WAIT)
|
860
953
|
if event and event.is_set():
|
861
|
-
return
|
954
|
+
return Result(
|
955
|
+
run_id=run_id,
|
956
|
+
parent_run_id=parent_run_id,
|
862
957
|
status=CANCEL,
|
863
|
-
context=
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
958
|
+
context=catch(
|
959
|
+
new_context,
|
960
|
+
status=CANCEL,
|
961
|
+
updated={
|
962
|
+
"errors": WorkflowCancelError(
|
963
|
+
"Execution was canceled from the event was set "
|
964
|
+
"before workflow execution."
|
965
|
+
).to_dict(),
|
966
|
+
},
|
967
|
+
),
|
968
|
+
extras=self.extras,
|
869
969
|
)
|
870
970
|
|
871
971
|
with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
|
872
972
|
futures: list[Future] = []
|
973
|
+
backoff_sleep = 0.01
|
974
|
+
consecutive_waits = 0
|
873
975
|
|
874
976
|
while not job_queue.empty() and (
|
875
977
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
@@ -879,21 +981,37 @@ class Workflow(BaseModel):
|
|
879
981
|
if (check := job.check_needs(new_context["jobs"])) == WAIT:
|
880
982
|
job_queue.task_done()
|
881
983
|
job_queue.put(job_id)
|
882
|
-
|
984
|
+
consecutive_waits += 1
|
985
|
+
|
986
|
+
# NOTE: Exponential backoff up to 0.15s max.
|
987
|
+
backoff_sleep = min(backoff_sleep * 1.5, 0.15)
|
988
|
+
time.sleep(backoff_sleep)
|
883
989
|
continue
|
884
|
-
|
885
|
-
|
990
|
+
|
991
|
+
# NOTE: Reset backoff when we can proceed
|
992
|
+
consecutive_waits = 0
|
993
|
+
backoff_sleep = 0.01
|
994
|
+
|
995
|
+
if check == FAILED: # pragma: no cov
|
996
|
+
return Result(
|
997
|
+
run_id=run_id,
|
998
|
+
parent_run_id=parent_run_id,
|
886
999
|
status=FAILED,
|
887
|
-
context=
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
1000
|
+
context=catch(
|
1001
|
+
new_context,
|
1002
|
+
status=FAILED,
|
1003
|
+
updated={
|
1004
|
+
"status": FAILED,
|
1005
|
+
"errors": WorkflowError(
|
1006
|
+
f"Validate job trigger rule was failed "
|
1007
|
+
f"with {job.trigger_rule.value!r}."
|
1008
|
+
).to_dict(),
|
1009
|
+
},
|
1010
|
+
),
|
1011
|
+
extras=self.extras,
|
894
1012
|
)
|
895
1013
|
elif check == SKIP: # pragma: no cov
|
896
|
-
|
1014
|
+
trace.info(
|
897
1015
|
f"[JOB]: Skip job: {job_id!r} from trigger rule."
|
898
1016
|
)
|
899
1017
|
job.set_outputs(output={"status": SKIP}, to=new_context)
|
@@ -907,7 +1025,9 @@ class Workflow(BaseModel):
|
|
907
1025
|
self.execute_job,
|
908
1026
|
job=job,
|
909
1027
|
params=new_context,
|
910
|
-
|
1028
|
+
run_id=run_id,
|
1029
|
+
context=context,
|
1030
|
+
parent_run_id=parent_run_id,
|
911
1031
|
event=event,
|
912
1032
|
),
|
913
1033
|
)
|
@@ -920,7 +1040,9 @@ class Workflow(BaseModel):
|
|
920
1040
|
self.execute_job,
|
921
1041
|
job=job,
|
922
1042
|
params=new_context,
|
923
|
-
|
1043
|
+
run_id=run_id,
|
1044
|
+
context=context,
|
1045
|
+
parent_run_id=parent_run_id,
|
924
1046
|
event=event,
|
925
1047
|
)
|
926
1048
|
)
|
@@ -940,7 +1062,7 @@ class Workflow(BaseModel):
|
|
940
1062
|
else: # pragma: no cov
|
941
1063
|
job_queue.put(job_id)
|
942
1064
|
futures.insert(0, future)
|
943
|
-
|
1065
|
+
trace.warning(
|
944
1066
|
f"[WORKFLOW]: ... Execution non-threading not "
|
945
1067
|
f"handle: {future}."
|
946
1068
|
)
|
@@ -963,27 +1085,39 @@ class Workflow(BaseModel):
|
|
963
1085
|
for i, s in enumerate(sequence_statuses, start=0):
|
964
1086
|
statuses[total + 1 + skip_count + i] = s
|
965
1087
|
|
966
|
-
|
967
|
-
|
1088
|
+
st: Status = validate_statuses(statuses)
|
1089
|
+
return Result(
|
1090
|
+
run_id=run_id,
|
1091
|
+
parent_run_id=parent_run_id,
|
1092
|
+
status=st,
|
1093
|
+
context=catch(new_context, status=st),
|
1094
|
+
extras=self.extras,
|
968
1095
|
)
|
969
1096
|
|
970
1097
|
event.set()
|
971
1098
|
for future in futures:
|
972
1099
|
future.cancel()
|
973
1100
|
|
974
|
-
|
1101
|
+
trace.error(
|
975
1102
|
f"[WORKFLOW]: {self.name!r} was timeout because it use exec "
|
976
1103
|
f"time more than {timeout} seconds."
|
977
1104
|
)
|
978
1105
|
|
979
1106
|
time.sleep(0.0025)
|
980
1107
|
|
981
|
-
return
|
1108
|
+
return Result(
|
1109
|
+
run_id=run_id,
|
1110
|
+
parent_run_id=parent_run_id,
|
982
1111
|
status=FAILED,
|
983
|
-
context=
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
1112
|
+
context=catch(
|
1113
|
+
new_context,
|
1114
|
+
status=FAILED,
|
1115
|
+
updated={
|
1116
|
+
"errors": WorkflowTimeoutError(
|
1117
|
+
f"{self.name!r} was timeout because it use exec time "
|
1118
|
+
f"more than {timeout} seconds."
|
1119
|
+
).to_dict(),
|
1120
|
+
},
|
1121
|
+
),
|
1122
|
+
extras=self.extras,
|
989
1123
|
)
|