ddeutil-workflow 0.0.73__py3-none-any.whl → 0.0.75__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 +20 -12
- 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 +71 -26
- ddeutil/workflow/errors.py +86 -19
- ddeutil/workflow/event.py +268 -169
- ddeutil/workflow/job.py +331 -192
- ddeutil/workflow/params.py +43 -11
- ddeutil/workflow/result.py +96 -70
- ddeutil/workflow/reusables.py +56 -6
- ddeutil/workflow/stages.py +1059 -572
- ddeutil/workflow/traces.py +205 -124
- ddeutil/workflow/utils.py +58 -19
- ddeutil/workflow/workflow.py +435 -296
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/METADATA +27 -17
- ddeutil_workflow-0.0.75.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.75.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.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,18 @@ 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
|
28
|
-
from zoneinfo import ZoneInfo
|
37
|
+
from threading import Event as ThreadEvent
|
38
|
+
from typing import Any, Optional, Union
|
29
39
|
|
30
40
|
from pydantic import BaseModel, Field
|
31
41
|
from pydantic.functional_validators import field_validator, model_validator
|
32
42
|
from typing_extensions import Self
|
33
43
|
|
34
|
-
from . import get_status_from_error
|
35
44
|
from .__types import DictData
|
36
|
-
from .audits import Audit,
|
45
|
+
from .audits import Audit, get_audit_model
|
37
46
|
from .conf import YamlParser, dynamic
|
38
47
|
from .errors import WorkflowCancelError, WorkflowError, WorkflowTimeoutError
|
39
|
-
from .event import
|
48
|
+
from .event import Event
|
40
49
|
from .job import Job
|
41
50
|
from .params import Param
|
42
51
|
from .result import (
|
@@ -47,17 +56,32 @@ from .result import (
|
|
47
56
|
WAIT,
|
48
57
|
Result,
|
49
58
|
Status,
|
59
|
+
catch,
|
60
|
+
get_status_from_error,
|
50
61
|
validate_statuses,
|
51
62
|
)
|
52
63
|
from .reusables import has_template, param2template
|
64
|
+
from .traces import Trace, get_trace
|
53
65
|
from .utils import (
|
66
|
+
UTC,
|
54
67
|
gen_id,
|
68
|
+
get_dt_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_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_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"))
|
241
|
+
Args:
|
242
|
+
data: A description string value that want to dedent.
|
200
243
|
|
201
|
-
|
202
|
-
|
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.
|
211
|
-
|
212
|
-
:param value: A list of on object.
|
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
|
-
)
|
221
|
-
|
222
|
-
# WARNING:
|
223
|
-
# if '* * * * *' in set_ons and len(set_ons) > 1:
|
224
|
-
# raise ValueError(
|
225
|
-
# "If it has every minute cronjob on value, it should have "
|
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
|
-
)
|
247
|
+
return dedent(data.lstrip("\n"))
|
234
248
|
|
235
|
-
|
236
|
-
|
237
|
-
|
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,16 +363,21 @@ 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
|
+
if dt.tzinfo is None:
|
373
|
+
dt = dt.replace(tzinfo=UTC)
|
374
|
+
|
375
|
+
release: datetime = replace_sec(dt.astimezone(UTC))
|
354
376
|
if not self.on:
|
355
377
|
return release
|
356
378
|
|
357
|
-
for on in self.on:
|
358
|
-
if release == on.cronjob.schedule(release).next:
|
379
|
+
for on in self.on.schedule:
|
380
|
+
if release == on.cronjob.schedule(release, tz=UTC).next:
|
359
381
|
return release
|
360
382
|
raise WorkflowError(
|
361
383
|
"Release datetime does not support for this workflow"
|
@@ -366,12 +388,10 @@ class Workflow(BaseModel):
|
|
366
388
|
release: datetime,
|
367
389
|
params: DictData,
|
368
390
|
*,
|
369
|
-
release_type: ReleaseType = NORMAL,
|
370
391
|
run_id: Optional[str] = None,
|
371
|
-
|
392
|
+
release_type: ReleaseType = NORMAL,
|
372
393
|
audit: type[Audit] = None,
|
373
394
|
override_log_name: Optional[str] = None,
|
374
|
-
result: Optional[Result] = None,
|
375
395
|
timeout: int = 600,
|
376
396
|
excluded: Optional[list[str]] = None,
|
377
397
|
) -> Result:
|
@@ -393,90 +413,92 @@ class Workflow(BaseModel):
|
|
393
413
|
:param params: A workflow parameter that pass to execute method.
|
394
414
|
:param release_type:
|
395
415
|
:param run_id: (str) A workflow running ID.
|
396
|
-
:param parent_run_id: (str) A parent workflow running ID.
|
397
416
|
:param audit: An audit class that want to save the execution result.
|
398
417
|
:param override_log_name: (str) An override logging name that use
|
399
418
|
instead the workflow name.
|
400
|
-
:param result: (Result) A result object for keeping context and status
|
401
|
-
data.
|
402
419
|
:param timeout: (int) A workflow execution time out in second unit.
|
403
420
|
:param excluded: (list[str]) A list of key that want to exclude from
|
404
421
|
audit data.
|
405
422
|
|
406
423
|
:rtype: Result
|
407
424
|
"""
|
408
|
-
audit: type[Audit] = audit or get_audit(extras=self.extras)
|
409
425
|
name: str = override_log_name or self.name
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
parent_run_id=
|
414
|
-
|
415
|
-
|
426
|
+
|
427
|
+
# NOTE: Generate the parent running ID with not None value.
|
428
|
+
if run_id:
|
429
|
+
parent_run_id: str = run_id
|
430
|
+
run_id: str = gen_id(name, unique=True)
|
431
|
+
else:
|
432
|
+
run_id: str = gen_id(name, unique=True)
|
433
|
+
parent_run_id: str = run_id
|
434
|
+
|
435
|
+
context: DictData = {}
|
436
|
+
trace: Trace = get_trace(
|
437
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
416
438
|
)
|
417
439
|
release: datetime = self.validate_release(dt=release)
|
418
|
-
|
419
|
-
f"[RELEASE]: Start {name!r} : {release:%Y-%m-%d %H:%M:%S}"
|
420
|
-
)
|
421
|
-
tz: ZoneInfo = dynamic("tz", extras=self.extras)
|
440
|
+
trace.info(f"[RELEASE]: Start {name!r} : {release:%Y-%m-%d %H:%M:%S}")
|
422
441
|
values: DictData = param2template(
|
423
442
|
params,
|
424
443
|
params={
|
425
444
|
"release": {
|
426
445
|
"logical_date": release,
|
427
|
-
"execute_date":
|
428
|
-
"run_id":
|
446
|
+
"execute_date": get_dt_now(),
|
447
|
+
"run_id": run_id,
|
429
448
|
}
|
430
449
|
},
|
431
450
|
extras=self.extras,
|
432
451
|
)
|
433
452
|
rs: Result = self.execute(
|
434
453
|
params=values,
|
435
|
-
parent_run_id
|
454
|
+
run_id=parent_run_id,
|
436
455
|
timeout=timeout,
|
437
456
|
)
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
)
|
442
|
-
result.trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
|
457
|
+
catch(context, status=rs.status, updated=rs.context)
|
458
|
+
trace.info(f"[RELEASE]: End {name!r} : {release:%Y-%m-%d %H:%M:%S}")
|
459
|
+
trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
|
443
460
|
(
|
444
|
-
audit(
|
461
|
+
(audit or get_audit_model(extras=self.extras))(
|
445
462
|
name=name,
|
446
463
|
release=release,
|
447
464
|
type=release_type,
|
448
|
-
context=
|
449
|
-
parent_run_id=
|
450
|
-
run_id=
|
451
|
-
execution_time=
|
465
|
+
context=context,
|
466
|
+
parent_run_id=parent_run_id,
|
467
|
+
run_id=run_id,
|
468
|
+
execution_time=rs.info.get("execution_time", 0),
|
452
469
|
extras=self.extras,
|
453
470
|
).save(excluded=excluded)
|
454
471
|
)
|
455
|
-
return
|
472
|
+
return Result(
|
473
|
+
run_id=run_id,
|
474
|
+
parent_run_id=parent_run_id,
|
456
475
|
status=rs.status,
|
457
|
-
context=
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
"
|
476
|
+
context=catch(
|
477
|
+
context,
|
478
|
+
status=rs.status,
|
479
|
+
updated={
|
480
|
+
"params": params,
|
481
|
+
"release": {
|
482
|
+
"type": release_type,
|
483
|
+
"logical_date": release,
|
484
|
+
},
|
485
|
+
**{"jobs": context.pop("jobs", {})},
|
486
|
+
**(context["errors"] if "errors" in context else {}),
|
462
487
|
},
|
463
|
-
|
464
|
-
|
465
|
-
result.context["errors"]
|
466
|
-
if "errors" in result.context
|
467
|
-
else {}
|
468
|
-
),
|
469
|
-
},
|
488
|
+
),
|
489
|
+
extras=self.extras,
|
470
490
|
)
|
471
491
|
|
472
492
|
def execute_job(
|
473
493
|
self,
|
474
494
|
job: Job,
|
475
495
|
params: DictData,
|
496
|
+
run_id: str,
|
497
|
+
context: DictData,
|
476
498
|
*,
|
477
|
-
|
478
|
-
event: Optional[
|
479
|
-
) -> tuple[Status,
|
499
|
+
parent_run_id: Optional[str] = None,
|
500
|
+
event: Optional[ThreadEvent] = None,
|
501
|
+
) -> tuple[Status, DictData]:
|
480
502
|
"""Job execution with passing dynamic parameters from the main workflow
|
481
503
|
execution to the target job object via job's ID.
|
482
504
|
|
@@ -487,42 +509,48 @@ class Workflow(BaseModel):
|
|
487
509
|
This method do not raise any error, and it will handle all exception
|
488
510
|
from the job execution.
|
489
511
|
|
490
|
-
:
|
491
|
-
|
492
|
-
|
493
|
-
|
512
|
+
Args:
|
513
|
+
job: (Job) A job model that want to execute.
|
514
|
+
params: (DictData) A parameter data.
|
515
|
+
run_id: A running stage ID.
|
516
|
+
context: A context data.
|
517
|
+
parent_run_id: A parent running ID. (Default is None)
|
518
|
+
event: (Event) An Event manager instance that use to cancel this
|
494
519
|
execution if it forces stopped by parent execution.
|
495
520
|
|
496
|
-
:
|
521
|
+
Returns:
|
522
|
+
tuple[Status, DictData]: The pair of status and result context data.
|
497
523
|
"""
|
498
|
-
|
499
|
-
|
524
|
+
trace: Trace = get_trace(
|
525
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
526
|
+
)
|
500
527
|
if event and event.is_set():
|
501
528
|
error_msg: str = (
|
502
529
|
"Job execution was canceled because the event was set "
|
503
530
|
"before start job execution."
|
504
531
|
)
|
505
|
-
return CANCEL,
|
532
|
+
return CANCEL, catch(
|
533
|
+
context=context,
|
506
534
|
status=CANCEL,
|
507
|
-
|
535
|
+
updated={
|
508
536
|
"errors": WorkflowCancelError(error_msg).to_dict(),
|
509
537
|
},
|
510
538
|
)
|
511
539
|
|
512
|
-
|
540
|
+
trace.info(f"[WORKFLOW]: Execute Job: {job.id!r}")
|
513
541
|
rs: Result = job.execute(
|
514
542
|
params=params,
|
515
|
-
run_id=
|
516
|
-
parent_run_id=result.parent_run_id,
|
543
|
+
run_id=parent_run_id,
|
517
544
|
event=event,
|
518
545
|
)
|
519
546
|
job.set_outputs(rs.context, to=params)
|
520
547
|
|
521
548
|
if rs.status == FAILED:
|
522
549
|
error_msg: str = f"Job execution, {job.id!r}, was failed."
|
523
|
-
return FAILED,
|
550
|
+
return FAILED, catch(
|
551
|
+
context=context,
|
524
552
|
status=FAILED,
|
525
|
-
|
553
|
+
updated={
|
526
554
|
"errors": WorkflowError(error_msg).to_dict(),
|
527
555
|
**params,
|
528
556
|
},
|
@@ -533,23 +561,25 @@ class Workflow(BaseModel):
|
|
533
561
|
f"Job execution, {job.id!r}, was canceled from the event after "
|
534
562
|
f"end job execution."
|
535
563
|
)
|
536
|
-
return CANCEL,
|
564
|
+
return CANCEL, catch(
|
565
|
+
context=context,
|
537
566
|
status=CANCEL,
|
538
|
-
|
567
|
+
updated={
|
539
568
|
"errors": WorkflowCancelError(error_msg).to_dict(),
|
540
569
|
**params,
|
541
570
|
},
|
542
571
|
)
|
543
572
|
|
544
|
-
return rs.status,
|
573
|
+
return rs.status, catch(
|
574
|
+
context=context, status=rs.status, updated=params
|
575
|
+
)
|
545
576
|
|
546
577
|
def execute(
|
547
578
|
self,
|
548
579
|
params: DictData,
|
549
580
|
*,
|
550
581
|
run_id: Optional[str] = None,
|
551
|
-
|
552
|
-
event: Optional[Event] = None,
|
582
|
+
event: Optional[ThreadEvent] = None,
|
553
583
|
timeout: float = 3600,
|
554
584
|
max_job_parallel: int = 2,
|
555
585
|
) -> Result:
|
@@ -598,7 +628,6 @@ class Workflow(BaseModel):
|
|
598
628
|
|
599
629
|
:param params: A parameter data that will parameterize before execution.
|
600
630
|
:param run_id: (Optional[str]) A workflow running ID.
|
601
|
-
:param parent_run_id: (Optional[str]) A parent workflow running ID.
|
602
631
|
:param event: (Event) An Event manager instance that use to cancel this
|
603
632
|
execution if it forces stopped by parent execution.
|
604
633
|
:param timeout: (float) A workflow execution time out in second unit
|
@@ -611,24 +640,30 @@ class Workflow(BaseModel):
|
|
611
640
|
:rtype: Result
|
612
641
|
"""
|
613
642
|
ts: float = time.monotonic()
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
extras=self.extras,
|
643
|
+
parent_run_id: Optional[str] = run_id
|
644
|
+
run_id: str = gen_id(self.name, extras=self.extras)
|
645
|
+
trace: Trace = get_trace(
|
646
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
619
647
|
)
|
620
648
|
context: DictData = self.parameterize(params)
|
621
|
-
event:
|
649
|
+
event: ThreadEvent = event or ThreadEvent()
|
622
650
|
max_job_parallel: int = dynamic(
|
623
651
|
"max_job_parallel", f=max_job_parallel, extras=self.extras
|
624
652
|
)
|
625
|
-
|
653
|
+
trace.info(
|
626
654
|
f"[WORKFLOW]: Execute: {self.name!r} ("
|
627
655
|
f"{'parallel' if max_job_parallel > 1 else 'sequential'} jobs)"
|
628
656
|
)
|
629
657
|
if not self.jobs:
|
630
|
-
|
631
|
-
return
|
658
|
+
trace.warning(f"[WORKFLOW]: {self.name!r} does not set jobs")
|
659
|
+
return Result(
|
660
|
+
run_id=run_id,
|
661
|
+
parent_run_id=parent_run_id,
|
662
|
+
status=SUCCESS,
|
663
|
+
context=catch(context, status=SUCCESS),
|
664
|
+
info={"execution_time": time.monotonic() - ts},
|
665
|
+
extras=self.extras,
|
666
|
+
)
|
632
667
|
|
633
668
|
job_queue: Queue = Queue()
|
634
669
|
for job_id in self.jobs:
|
@@ -642,20 +677,30 @@ class Workflow(BaseModel):
|
|
642
677
|
timeout: float = dynamic(
|
643
678
|
"max_job_exec_timeout", f=timeout, extras=self.extras
|
644
679
|
)
|
645
|
-
|
680
|
+
catch(context, status=WAIT)
|
646
681
|
if event and event.is_set():
|
647
|
-
return
|
682
|
+
return Result(
|
683
|
+
run_id=run_id,
|
684
|
+
parent_run_id=parent_run_id,
|
648
685
|
status=CANCEL,
|
649
|
-
context=
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
686
|
+
context=catch(
|
687
|
+
context,
|
688
|
+
status=CANCEL,
|
689
|
+
updated={
|
690
|
+
"errors": WorkflowCancelError(
|
691
|
+
"Execution was canceled from the event was set "
|
692
|
+
"before workflow execution."
|
693
|
+
).to_dict(),
|
694
|
+
},
|
695
|
+
),
|
696
|
+
info={"execution_time": time.monotonic() - ts},
|
697
|
+
extras=self.extras,
|
655
698
|
)
|
656
699
|
|
657
700
|
with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
|
658
701
|
futures: list[Future] = []
|
702
|
+
backoff_sleep = 0.01 # Start with smaller sleep time
|
703
|
+
consecutive_waits = 0 # Track consecutive wait states
|
659
704
|
|
660
705
|
while not job_queue.empty() and (
|
661
706
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
@@ -665,21 +710,37 @@ class Workflow(BaseModel):
|
|
665
710
|
if (check := job.check_needs(context["jobs"])) == WAIT:
|
666
711
|
job_queue.task_done()
|
667
712
|
job_queue.put(job_id)
|
668
|
-
|
713
|
+
consecutive_waits += 1
|
714
|
+
# Exponential backoff up to 0.15s max
|
715
|
+
backoff_sleep = min(backoff_sleep * 1.5, 0.15)
|
716
|
+
time.sleep(backoff_sleep)
|
669
717
|
continue
|
670
|
-
|
671
|
-
|
718
|
+
|
719
|
+
# Reset backoff when we can proceed
|
720
|
+
consecutive_waits = 0
|
721
|
+
backoff_sleep = 0.01
|
722
|
+
|
723
|
+
if check == FAILED: # pragma: no cov
|
724
|
+
return Result(
|
725
|
+
run_id=run_id,
|
726
|
+
parent_run_id=parent_run_id,
|
672
727
|
status=FAILED,
|
673
|
-
context=
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
728
|
+
context=catch(
|
729
|
+
context,
|
730
|
+
status=FAILED,
|
731
|
+
updated={
|
732
|
+
"status": FAILED,
|
733
|
+
"errors": WorkflowError(
|
734
|
+
f"Validate job trigger rule was failed "
|
735
|
+
f"with {job.trigger_rule.value!r}."
|
736
|
+
).to_dict(),
|
737
|
+
},
|
738
|
+
),
|
739
|
+
info={"execution_time": time.monotonic() - ts},
|
740
|
+
extras=self.extras,
|
680
741
|
)
|
681
742
|
elif check == SKIP: # pragma: no cov
|
682
|
-
|
743
|
+
trace.info(
|
683
744
|
f"[JOB]: Skip job: {job_id!r} from trigger rule."
|
684
745
|
)
|
685
746
|
job.set_outputs(output={"status": SKIP}, to=context)
|
@@ -693,7 +754,9 @@ class Workflow(BaseModel):
|
|
693
754
|
self.execute_job,
|
694
755
|
job=job,
|
695
756
|
params=context,
|
696
|
-
|
757
|
+
run_id=run_id,
|
758
|
+
context=context,
|
759
|
+
parent_run_id=parent_run_id,
|
697
760
|
event=event,
|
698
761
|
),
|
699
762
|
)
|
@@ -706,7 +769,9 @@ class Workflow(BaseModel):
|
|
706
769
|
self.execute_job,
|
707
770
|
job=job,
|
708
771
|
params=context,
|
709
|
-
|
772
|
+
run_id=run_id,
|
773
|
+
context=context,
|
774
|
+
parent_run_id=parent_run_id,
|
710
775
|
event=event,
|
711
776
|
)
|
712
777
|
)
|
@@ -726,7 +791,7 @@ class Workflow(BaseModel):
|
|
726
791
|
else: # pragma: no cov
|
727
792
|
job_queue.put(job_id)
|
728
793
|
futures.insert(0, future)
|
729
|
-
|
794
|
+
trace.warning(
|
730
795
|
f"[WORKFLOW]: ... Execution non-threading not "
|
731
796
|
f"handle: {future}."
|
732
797
|
)
|
@@ -749,44 +814,58 @@ class Workflow(BaseModel):
|
|
749
814
|
for i, s in enumerate(sequence_statuses, start=0):
|
750
815
|
statuses[total + 1 + skip_count + i] = s
|
751
816
|
|
752
|
-
|
753
|
-
|
817
|
+
st: Status = validate_statuses(statuses)
|
818
|
+
return Result(
|
819
|
+
run_id=run_id,
|
820
|
+
parent_run_id=parent_run_id,
|
821
|
+
status=st,
|
822
|
+
context=catch(context, status=st),
|
823
|
+
info={"execution_time": time.monotonic() - ts},
|
824
|
+
extras=self.extras,
|
754
825
|
)
|
755
826
|
|
756
827
|
event.set()
|
757
828
|
for future in futures:
|
758
829
|
future.cancel()
|
759
830
|
|
760
|
-
|
831
|
+
trace.error(
|
761
832
|
f"[WORKFLOW]: {self.name!r} was timeout because it use exec "
|
762
833
|
f"time more than {timeout} seconds."
|
763
834
|
)
|
764
835
|
|
765
836
|
time.sleep(0.0025)
|
766
837
|
|
767
|
-
return
|
838
|
+
return Result(
|
839
|
+
run_id=run_id,
|
840
|
+
parent_run_id=parent_run_id,
|
768
841
|
status=FAILED,
|
769
|
-
context=
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
842
|
+
context=catch(
|
843
|
+
context,
|
844
|
+
status=FAILED,
|
845
|
+
updated={
|
846
|
+
"errors": WorkflowTimeoutError(
|
847
|
+
f"{self.name!r} was timeout because it use exec time "
|
848
|
+
f"more than {timeout} seconds."
|
849
|
+
).to_dict(),
|
850
|
+
},
|
851
|
+
),
|
852
|
+
info={"execution_time": time.monotonic() - ts},
|
853
|
+
extras=self.extras,
|
775
854
|
)
|
776
855
|
|
777
856
|
def rerun(
|
778
857
|
self,
|
779
858
|
context: DictData,
|
780
859
|
*,
|
781
|
-
|
782
|
-
event: Optional[
|
860
|
+
run_id: Optional[str] = None,
|
861
|
+
event: Optional[ThreadEvent] = None,
|
783
862
|
timeout: float = 3600,
|
784
863
|
max_job_parallel: int = 2,
|
785
864
|
) -> Result:
|
786
865
|
"""Re-Execute workflow with passing the error context data.
|
787
866
|
|
788
867
|
:param context: A context result that get the failed status.
|
789
|
-
:param
|
868
|
+
:param run_id: (Optional[str]) A workflow running ID.
|
790
869
|
:param event: (Event) An Event manager instance that use to cancel this
|
791
870
|
execution if it forces stopped by parent execution.
|
792
871
|
:param timeout: (float) A workflow execution time out in second unit
|
@@ -796,36 +875,49 @@ class Workflow(BaseModel):
|
|
796
875
|
:param max_job_parallel: (int) The maximum workers that use for job
|
797
876
|
execution in `ThreadPoolExecutor` object. (Default: 2 workers)
|
798
877
|
|
799
|
-
|
878
|
+
Returns
|
879
|
+
Result: Return Result object that create from execution context with
|
880
|
+
return mode.
|
800
881
|
"""
|
801
882
|
ts: float = time.monotonic()
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
extras=self.extras,
|
883
|
+
parent_run_id: str = run_id
|
884
|
+
run_id: str = gen_id(self.name, extras=self.extras)
|
885
|
+
trace: Trace = get_trace(
|
886
|
+
run_id, parent_run_id=parent_run_id, extras=self.extras
|
807
887
|
)
|
808
888
|
if context["status"] == SUCCESS:
|
809
|
-
|
889
|
+
trace.info(
|
810
890
|
"[WORKFLOW]: Does not rerun because it already executed with "
|
811
891
|
"success status."
|
812
892
|
)
|
813
|
-
return
|
893
|
+
return Result(
|
894
|
+
run_id=run_id,
|
895
|
+
parent_run_id=parent_run_id,
|
896
|
+
status=SUCCESS,
|
897
|
+
context=catch(context=context, status=SUCCESS),
|
898
|
+
extras=self.extras,
|
899
|
+
)
|
814
900
|
|
815
901
|
err = context["errors"]
|
816
|
-
|
902
|
+
trace.info(f"[WORKFLOW]: Previous error: {err}")
|
817
903
|
|
818
|
-
event:
|
904
|
+
event: ThreadEvent = event or ThreadEvent()
|
819
905
|
max_job_parallel: int = dynamic(
|
820
906
|
"max_job_parallel", f=max_job_parallel, extras=self.extras
|
821
907
|
)
|
822
|
-
|
908
|
+
trace.info(
|
823
909
|
f"[WORKFLOW]: Execute: {self.name!r} ("
|
824
910
|
f"{'parallel' if max_job_parallel > 1 else 'sequential'} jobs)"
|
825
911
|
)
|
826
912
|
if not self.jobs:
|
827
|
-
|
828
|
-
return
|
913
|
+
trace.warning(f"[WORKFLOW]: {self.name!r} does not set jobs")
|
914
|
+
return Result(
|
915
|
+
run_id=run_id,
|
916
|
+
parent_run_id=parent_run_id,
|
917
|
+
status=SUCCESS,
|
918
|
+
context=catch(context=context, status=SUCCESS),
|
919
|
+
extras=self.extras,
|
920
|
+
)
|
829
921
|
|
830
922
|
# NOTE: Prepare the new context for rerun process.
|
831
923
|
jobs: DictData = context.get("jobs")
|
@@ -845,8 +937,14 @@ class Workflow(BaseModel):
|
|
845
937
|
total_job += 1
|
846
938
|
|
847
939
|
if total_job == 0:
|
848
|
-
|
849
|
-
return
|
940
|
+
trace.warning("[WORKFLOW]: It does not have job to rerun.")
|
941
|
+
return Result(
|
942
|
+
run_id=run_id,
|
943
|
+
parent_run_id=parent_run_id,
|
944
|
+
status=SUCCESS,
|
945
|
+
context=catch(context=context, status=SUCCESS),
|
946
|
+
extras=self.extras,
|
947
|
+
)
|
850
948
|
|
851
949
|
not_timeout_flag: bool = True
|
852
950
|
statuses: list[Status] = [WAIT] * total_job
|
@@ -856,20 +954,29 @@ class Workflow(BaseModel):
|
|
856
954
|
"max_job_exec_timeout", f=timeout, extras=self.extras
|
857
955
|
)
|
858
956
|
|
859
|
-
|
957
|
+
catch(new_context, status=WAIT)
|
860
958
|
if event and event.is_set():
|
861
|
-
return
|
959
|
+
return Result(
|
960
|
+
run_id=run_id,
|
961
|
+
parent_run_id=parent_run_id,
|
862
962
|
status=CANCEL,
|
863
|
-
context=
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
963
|
+
context=catch(
|
964
|
+
new_context,
|
965
|
+
status=CANCEL,
|
966
|
+
updated={
|
967
|
+
"errors": WorkflowCancelError(
|
968
|
+
"Execution was canceled from the event was set "
|
969
|
+
"before workflow execution."
|
970
|
+
).to_dict(),
|
971
|
+
},
|
972
|
+
),
|
973
|
+
extras=self.extras,
|
869
974
|
)
|
870
975
|
|
871
976
|
with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
|
872
977
|
futures: list[Future] = []
|
978
|
+
backoff_sleep = 0.01
|
979
|
+
consecutive_waits = 0
|
873
980
|
|
874
981
|
while not job_queue.empty() and (
|
875
982
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
@@ -879,21 +986,37 @@ class Workflow(BaseModel):
|
|
879
986
|
if (check := job.check_needs(new_context["jobs"])) == WAIT:
|
880
987
|
job_queue.task_done()
|
881
988
|
job_queue.put(job_id)
|
882
|
-
|
989
|
+
consecutive_waits += 1
|
990
|
+
|
991
|
+
# NOTE: Exponential backoff up to 0.15s max.
|
992
|
+
backoff_sleep = min(backoff_sleep * 1.5, 0.15)
|
993
|
+
time.sleep(backoff_sleep)
|
883
994
|
continue
|
884
|
-
|
885
|
-
|
995
|
+
|
996
|
+
# NOTE: Reset backoff when we can proceed
|
997
|
+
consecutive_waits = 0
|
998
|
+
backoff_sleep = 0.01
|
999
|
+
|
1000
|
+
if check == FAILED: # pragma: no cov
|
1001
|
+
return Result(
|
1002
|
+
run_id=run_id,
|
1003
|
+
parent_run_id=parent_run_id,
|
886
1004
|
status=FAILED,
|
887
|
-
context=
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
1005
|
+
context=catch(
|
1006
|
+
new_context,
|
1007
|
+
status=FAILED,
|
1008
|
+
updated={
|
1009
|
+
"status": FAILED,
|
1010
|
+
"errors": WorkflowError(
|
1011
|
+
f"Validate job trigger rule was failed "
|
1012
|
+
f"with {job.trigger_rule.value!r}."
|
1013
|
+
).to_dict(),
|
1014
|
+
},
|
1015
|
+
),
|
1016
|
+
extras=self.extras,
|
894
1017
|
)
|
895
1018
|
elif check == SKIP: # pragma: no cov
|
896
|
-
|
1019
|
+
trace.info(
|
897
1020
|
f"[JOB]: Skip job: {job_id!r} from trigger rule."
|
898
1021
|
)
|
899
1022
|
job.set_outputs(output={"status": SKIP}, to=new_context)
|
@@ -907,7 +1030,9 @@ class Workflow(BaseModel):
|
|
907
1030
|
self.execute_job,
|
908
1031
|
job=job,
|
909
1032
|
params=new_context,
|
910
|
-
|
1033
|
+
run_id=run_id,
|
1034
|
+
context=context,
|
1035
|
+
parent_run_id=parent_run_id,
|
911
1036
|
event=event,
|
912
1037
|
),
|
913
1038
|
)
|
@@ -920,7 +1045,9 @@ class Workflow(BaseModel):
|
|
920
1045
|
self.execute_job,
|
921
1046
|
job=job,
|
922
1047
|
params=new_context,
|
923
|
-
|
1048
|
+
run_id=run_id,
|
1049
|
+
context=context,
|
1050
|
+
parent_run_id=parent_run_id,
|
924
1051
|
event=event,
|
925
1052
|
)
|
926
1053
|
)
|
@@ -940,7 +1067,7 @@ class Workflow(BaseModel):
|
|
940
1067
|
else: # pragma: no cov
|
941
1068
|
job_queue.put(job_id)
|
942
1069
|
futures.insert(0, future)
|
943
|
-
|
1070
|
+
trace.warning(
|
944
1071
|
f"[WORKFLOW]: ... Execution non-threading not "
|
945
1072
|
f"handle: {future}."
|
946
1073
|
)
|
@@ -963,27 +1090,39 @@ class Workflow(BaseModel):
|
|
963
1090
|
for i, s in enumerate(sequence_statuses, start=0):
|
964
1091
|
statuses[total + 1 + skip_count + i] = s
|
965
1092
|
|
966
|
-
|
967
|
-
|
1093
|
+
st: Status = validate_statuses(statuses)
|
1094
|
+
return Result(
|
1095
|
+
run_id=run_id,
|
1096
|
+
parent_run_id=parent_run_id,
|
1097
|
+
status=st,
|
1098
|
+
context=catch(new_context, status=st),
|
1099
|
+
extras=self.extras,
|
968
1100
|
)
|
969
1101
|
|
970
1102
|
event.set()
|
971
1103
|
for future in futures:
|
972
1104
|
future.cancel()
|
973
1105
|
|
974
|
-
|
1106
|
+
trace.error(
|
975
1107
|
f"[WORKFLOW]: {self.name!r} was timeout because it use exec "
|
976
1108
|
f"time more than {timeout} seconds."
|
977
1109
|
)
|
978
1110
|
|
979
1111
|
time.sleep(0.0025)
|
980
1112
|
|
981
|
-
return
|
1113
|
+
return Result(
|
1114
|
+
run_id=run_id,
|
1115
|
+
parent_run_id=parent_run_id,
|
982
1116
|
status=FAILED,
|
983
|
-
context=
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
1117
|
+
context=catch(
|
1118
|
+
new_context,
|
1119
|
+
status=FAILED,
|
1120
|
+
updated={
|
1121
|
+
"errors": WorkflowTimeoutError(
|
1122
|
+
f"{self.name!r} was timeout because it use exec time "
|
1123
|
+
f"more than {timeout} seconds."
|
1124
|
+
).to_dict(),
|
1125
|
+
},
|
1126
|
+
),
|
1127
|
+
extras=self.extras,
|
989
1128
|
)
|