ddeutil-workflow 0.0.34__py3-none-any.whl → 0.0.36__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 +8 -3
- ddeutil/workflow/api/api.py +58 -14
- ddeutil/workflow/api/repeat.py +21 -11
- ddeutil/workflow/api/routes/__init__.py +9 -0
- ddeutil/workflow/api/routes/job.py +73 -0
- ddeutil/workflow/api/routes/logs.py +64 -0
- ddeutil/workflow/api/{route.py → routes/schedules.py} +3 -131
- ddeutil/workflow/api/routes/workflows.py +137 -0
- ddeutil/workflow/audit.py +9 -6
- ddeutil/workflow/{call.py → caller.py} +4 -4
- ddeutil/workflow/job.py +63 -24
- ddeutil/workflow/logs.py +326 -0
- ddeutil/workflow/params.py +87 -22
- ddeutil/workflow/result.py +17 -141
- ddeutil/workflow/scheduler.py +69 -41
- ddeutil/workflow/stages.py +68 -14
- ddeutil/workflow/utils.py +7 -1
- ddeutil/workflow/workflow.py +2 -16
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/METADATA +32 -27
- ddeutil_workflow-0.0.36.dist-info/RECORD +31 -0
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.34.dist-info/RECORD +0 -26
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/top_level.txt +0 -0
ddeutil/workflow/params.py
CHANGED
@@ -3,37 +3,42 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""Param
|
7
|
-
Workflow and Schedule objects
|
6
|
+
"""This module include all Param Pydantic Models that use for parsing an
|
7
|
+
incoming parameters that was passed to the Workflow and Schedule objects before
|
8
|
+
execution or release methods.
|
9
|
+
|
10
|
+
The Param model allow you to handle validation and preparation steps before
|
11
|
+
passing an input value to target execution method.
|
8
12
|
"""
|
9
13
|
from __future__ import annotations
|
10
14
|
|
11
15
|
import decimal
|
12
|
-
import logging
|
13
16
|
from abc import ABC, abstractmethod
|
14
17
|
from datetime import date, datetime
|
15
|
-
from typing import Any, Literal, Optional, Union
|
18
|
+
from typing import Annotated, Any, Literal, Optional, TypeVar, Union
|
16
19
|
|
17
20
|
from pydantic import BaseModel, Field
|
18
21
|
|
19
22
|
from .__types import TupleStr
|
20
23
|
from .exceptions import ParamValueException
|
21
|
-
from .utils import get_dt_now
|
22
|
-
|
23
|
-
logger = logging.getLogger("ddeutil.workflow")
|
24
|
+
from .utils import get_d_now, get_dt_now
|
24
25
|
|
25
26
|
__all__: TupleStr = (
|
26
27
|
"ChoiceParam",
|
27
28
|
"DatetimeParam",
|
29
|
+
"DateParam",
|
28
30
|
"IntParam",
|
29
31
|
"Param",
|
30
32
|
"StrParam",
|
31
33
|
)
|
32
34
|
|
35
|
+
T = TypeVar("T")
|
36
|
+
|
33
37
|
|
34
38
|
class BaseParam(BaseModel, ABC):
|
35
|
-
"""Base Parameter that use to make any Params
|
36
|
-
with the type field that made from literal string.
|
39
|
+
"""Base Parameter that use to make any Params Models. The parameter type
|
40
|
+
will dynamic with the setup type field that made from literal string.
|
41
|
+
"""
|
37
42
|
|
38
43
|
desc: Optional[str] = Field(
|
39
44
|
default=None, description="A description of parameter providing."
|
@@ -45,7 +50,7 @@ class BaseParam(BaseModel, ABC):
|
|
45
50
|
type: str = Field(description="A type of parameter.")
|
46
51
|
|
47
52
|
@abstractmethod
|
48
|
-
def receive(self, value: Optional[
|
53
|
+
def receive(self, value: Optional[T] = None) -> T:
|
49
54
|
raise NotImplementedError(
|
50
55
|
"Receive value and validate typing before return valid value."
|
51
56
|
)
|
@@ -72,17 +77,42 @@ class DefaultParam(BaseParam):
|
|
72
77
|
)
|
73
78
|
|
74
79
|
|
75
|
-
# TODO: Not implement this parameter yet
|
76
80
|
class DateParam(DefaultParam): # pragma: no cov
|
77
|
-
"""Date parameter."""
|
81
|
+
"""Date parameter model."""
|
78
82
|
|
79
83
|
type: Literal["date"] = "date"
|
84
|
+
default: date = Field(default_factory=get_d_now)
|
85
|
+
|
86
|
+
def receive(self, value: Optional[str | datetime | date] = None) -> date:
|
87
|
+
"""Receive value that match with date. If an input value pass with
|
88
|
+
None, it will use default value instead.
|
89
|
+
|
90
|
+
:param value: A value that want to validate with date parameter type.
|
91
|
+
|
92
|
+
:rtype: date
|
93
|
+
"""
|
94
|
+
if value is None:
|
95
|
+
return self.default
|
80
96
|
|
81
|
-
|
97
|
+
if isinstance(value, datetime):
|
98
|
+
return value.date()
|
99
|
+
elif isinstance(value, date):
|
100
|
+
return value
|
101
|
+
elif not isinstance(value, str):
|
102
|
+
raise ParamValueException(
|
103
|
+
f"Value that want to convert to date does not support for "
|
104
|
+
f"type: {type(value)}"
|
105
|
+
)
|
106
|
+
try:
|
107
|
+
return date.fromisoformat(value)
|
108
|
+
except ValueError:
|
109
|
+
raise ParamValueException(
|
110
|
+
f"Invalid the ISO format string for date: {value!r}"
|
111
|
+
) from None
|
82
112
|
|
83
113
|
|
84
114
|
class DatetimeParam(DefaultParam):
|
85
|
-
"""Datetime parameter."""
|
115
|
+
"""Datetime parameter model."""
|
86
116
|
|
87
117
|
type: Literal["datetime"] = "datetime"
|
88
118
|
default: datetime = Field(default_factory=get_dt_now)
|
@@ -93,6 +123,7 @@ class DatetimeParam(DefaultParam):
|
|
93
123
|
|
94
124
|
:param value: A value that want to validate with datetime parameter
|
95
125
|
type.
|
126
|
+
|
96
127
|
:rtype: datetime
|
97
128
|
"""
|
98
129
|
if value is None:
|
@@ -111,7 +142,7 @@ class DatetimeParam(DefaultParam):
|
|
111
142
|
return datetime.fromisoformat(value)
|
112
143
|
except ValueError:
|
113
144
|
raise ParamValueException(
|
114
|
-
f"Invalid the ISO format string: {value!r}"
|
145
|
+
f"Invalid the ISO format string for datetime: {value!r}"
|
115
146
|
) from None
|
116
147
|
|
117
148
|
|
@@ -169,9 +200,11 @@ class ChoiceParam(BaseParam):
|
|
169
200
|
"""Choice parameter."""
|
170
201
|
|
171
202
|
type: Literal["choice"] = "choice"
|
172
|
-
options: list[str] = Field(
|
203
|
+
options: Union[list[str], list[int]] = Field(
|
204
|
+
description="A list of choice parameters that able be str or int.",
|
205
|
+
)
|
173
206
|
|
174
|
-
def receive(self, value: str | None = None) -> str:
|
207
|
+
def receive(self, value: Union[str, int] | None = None) -> Union[str, int]:
|
175
208
|
"""Receive value that match with options.
|
176
209
|
|
177
210
|
:param value: A value that want to select from the options field.
|
@@ -188,9 +221,41 @@ class ChoiceParam(BaseParam):
|
|
188
221
|
return value
|
189
222
|
|
190
223
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
224
|
+
# TODO: Not implement this parameter yet
|
225
|
+
class MapParam(DefaultParam): # pragma: no cov
|
226
|
+
|
227
|
+
type: Literal["map"] = "map"
|
228
|
+
default: dict[Any, Any] = Field(default_factory=dict)
|
229
|
+
|
230
|
+
def receive(self, value: Optional[dict[Any, Any]] = None) -> dict[Any, Any]:
|
231
|
+
if value is None:
|
232
|
+
return self.default
|
233
|
+
|
234
|
+
|
235
|
+
# TODO: Not implement this parameter yet
|
236
|
+
class ArrayParam(DefaultParam): # pragma: no cov
|
237
|
+
|
238
|
+
type: Literal["array"] = "array"
|
239
|
+
default: list[Any] = Field(default_factory=list)
|
240
|
+
|
241
|
+
def receive(self, value: Optional[list[T]] = None) -> list[T]:
|
242
|
+
if value is None:
|
243
|
+
return self.default
|
244
|
+
if not isinstance(value, list):
|
245
|
+
raise ParamValueException(
|
246
|
+
f"Value that want to convert to array does not support for "
|
247
|
+
f"type: {type(value)}"
|
248
|
+
)
|
249
|
+
return value
|
250
|
+
|
251
|
+
|
252
|
+
Param = Annotated[
|
253
|
+
Union[
|
254
|
+
ChoiceParam,
|
255
|
+
DatetimeParam,
|
256
|
+
DateParam,
|
257
|
+
IntParam,
|
258
|
+
StrParam,
|
259
|
+
],
|
260
|
+
Field(discriminator="type"),
|
196
261
|
]
|
ddeutil/workflow/result.py
CHANGED
@@ -4,36 +4,29 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""This is the Result module. It is the data context transfer objects that use
|
7
|
-
by all object in this package.
|
7
|
+
by all object in this package. This module provide Result dataclass.
|
8
8
|
"""
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
-
import os
|
12
|
-
from abc import ABC, abstractmethod
|
13
11
|
from dataclasses import field
|
14
12
|
from datetime import datetime
|
15
13
|
from enum import IntEnum
|
16
|
-
from
|
17
|
-
from pathlib import Path
|
18
|
-
from threading import Event, get_ident
|
14
|
+
from threading import Event
|
19
15
|
from typing import Optional
|
20
16
|
|
21
17
|
from pydantic import ConfigDict
|
22
18
|
from pydantic.dataclasses import dataclass
|
19
|
+
from pydantic.functional_validators import model_validator
|
23
20
|
from typing_extensions import Self
|
24
21
|
|
25
22
|
from .__types import DictData, TupleStr
|
26
|
-
from .
|
27
|
-
from .utils import
|
28
|
-
|
29
|
-
logger = get_logger("ddeutil.workflow")
|
23
|
+
from .logs import TraceLog, get_dt_tznow, get_trace
|
24
|
+
from .utils import gen_id
|
30
25
|
|
31
26
|
__all__: TupleStr = (
|
32
27
|
"Result",
|
33
28
|
"Status",
|
34
|
-
"TraceLog",
|
35
29
|
"default_gen_id",
|
36
|
-
"get_dt_tznow",
|
37
30
|
)
|
38
31
|
|
39
32
|
|
@@ -46,14 +39,6 @@ def default_gen_id() -> str:
|
|
46
39
|
return gen_id("manual", unique=True)
|
47
40
|
|
48
41
|
|
49
|
-
def get_dt_tznow() -> datetime:
|
50
|
-
"""Return the current datetime object that passing the config timezone.
|
51
|
-
|
52
|
-
:rtype: datetime
|
53
|
-
"""
|
54
|
-
return get_dt_now(tz=config.tz)
|
55
|
-
|
56
|
-
|
57
42
|
class Status(IntEnum):
|
58
43
|
"""Status Int Enum object."""
|
59
44
|
|
@@ -62,111 +47,6 @@ class Status(IntEnum):
|
|
62
47
|
WAIT: int = 2
|
63
48
|
|
64
49
|
|
65
|
-
@dataclass(frozen=True)
|
66
|
-
class BaseTraceLog(ABC): # pragma: no cov
|
67
|
-
"""Base Trace Log dataclass object."""
|
68
|
-
|
69
|
-
run_id: str
|
70
|
-
parent_run_id: Optional[str] = None
|
71
|
-
|
72
|
-
@abstractmethod
|
73
|
-
def writer(self, message: str, is_err: bool = False) -> None: ...
|
74
|
-
|
75
|
-
@abstractmethod
|
76
|
-
def make_message(self, message: str) -> str: ...
|
77
|
-
|
78
|
-
def debug(self, message: str):
|
79
|
-
msg: str = self.make_message(message)
|
80
|
-
|
81
|
-
# NOTE: Write file if debug mode.
|
82
|
-
if config.debug:
|
83
|
-
self.writer(msg)
|
84
|
-
|
85
|
-
logger.debug(msg, stacklevel=2)
|
86
|
-
|
87
|
-
def info(self, message: str):
|
88
|
-
msg: str = self.make_message(message)
|
89
|
-
self.writer(msg)
|
90
|
-
logger.info(msg, stacklevel=2)
|
91
|
-
|
92
|
-
def warning(self, message: str):
|
93
|
-
msg: str = self.make_message(message)
|
94
|
-
self.writer(msg)
|
95
|
-
logger.warning(msg, stacklevel=2)
|
96
|
-
|
97
|
-
def error(self, message: str):
|
98
|
-
msg: str = self.make_message(message)
|
99
|
-
self.writer(msg, is_err=True)
|
100
|
-
logger.error(msg, stacklevel=2)
|
101
|
-
|
102
|
-
|
103
|
-
class TraceLog(BaseTraceLog): # pragma: no cov
|
104
|
-
"""Trace Log object that write file to the local storage."""
|
105
|
-
|
106
|
-
@property
|
107
|
-
def log_file(self) -> Path:
|
108
|
-
log_file: Path = (
|
109
|
-
config.log_path / f"run_id={self.parent_run_id or self.run_id}"
|
110
|
-
)
|
111
|
-
if not log_file.exists():
|
112
|
-
log_file.mkdir(parents=True)
|
113
|
-
return log_file
|
114
|
-
|
115
|
-
@property
|
116
|
-
def cut_id(self) -> str:
|
117
|
-
"""Combine cutting ID of parent running ID if it set."""
|
118
|
-
cut_run_id: str = cut_id(self.run_id)
|
119
|
-
if not self.parent_run_id:
|
120
|
-
return f"{cut_run_id} -> {' ' * 6}"
|
121
|
-
|
122
|
-
cut_parent_run_id: str = cut_id(self.parent_run_id)
|
123
|
-
return f"{cut_parent_run_id} -> {cut_run_id}"
|
124
|
-
|
125
|
-
def make_message(self, message: str) -> str:
|
126
|
-
return f"({self.cut_id}) {message}"
|
127
|
-
|
128
|
-
def writer(self, message: str, is_err: bool = False) -> None:
|
129
|
-
"""The path of logging data will store by format:
|
130
|
-
|
131
|
-
... ./logs/run_id=<run-id>/stdout.txt
|
132
|
-
... ./logs/run_id=<run-id>/stderr.txt
|
133
|
-
|
134
|
-
:param message:
|
135
|
-
:param is_err:
|
136
|
-
"""
|
137
|
-
if not config.enable_write_log:
|
138
|
-
return
|
139
|
-
|
140
|
-
frame_info: Traceback = getframeinfo(currentframe().f_back.f_back)
|
141
|
-
filename: str = frame_info.filename.split(os.path.sep)[-1]
|
142
|
-
lineno: int = frame_info.lineno
|
143
|
-
|
144
|
-
# NOTE: set process and thread IDs.
|
145
|
-
process: int = os.getpid()
|
146
|
-
thread: int = get_ident()
|
147
|
-
|
148
|
-
write_file: str = "stderr.txt" if is_err else "stdout.txt"
|
149
|
-
with (self.log_file / write_file).open(
|
150
|
-
mode="at", encoding="utf-8"
|
151
|
-
) as f:
|
152
|
-
msg_fmt: str = f"{config.log_format_file}\n"
|
153
|
-
print(msg_fmt)
|
154
|
-
f.write(
|
155
|
-
msg_fmt.format(
|
156
|
-
**{
|
157
|
-
"datetime": get_dt_tznow().strftime(
|
158
|
-
config.log_datetime_format
|
159
|
-
),
|
160
|
-
"process": process,
|
161
|
-
"thread": thread,
|
162
|
-
"message": message,
|
163
|
-
"filename": filename,
|
164
|
-
"lineno": lineno,
|
165
|
-
}
|
166
|
-
)
|
167
|
-
)
|
168
|
-
|
169
|
-
|
170
50
|
@dataclass(
|
171
51
|
config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
|
172
52
|
)
|
@@ -182,12 +62,12 @@ class Result:
|
|
182
62
|
status: Status = field(default=Status.WAIT)
|
183
63
|
context: DictData = field(default_factory=dict)
|
184
64
|
run_id: Optional[str] = field(default_factory=default_gen_id)
|
185
|
-
|
186
|
-
# NOTE: Ignore this field to compare another result model with __eq__.
|
187
65
|
parent_run_id: Optional[str] = field(default=None, compare=False)
|
188
|
-
event: Event = field(default_factory=Event, compare=False)
|
189
66
|
ts: datetime = field(default_factory=get_dt_tznow, compare=False)
|
190
67
|
|
68
|
+
event: Event = field(default_factory=Event, compare=False, repr=False)
|
69
|
+
trace: Optional[TraceLog] = field(default=None, compare=False, repr=False)
|
70
|
+
|
191
71
|
@classmethod
|
192
72
|
def construct_with_rs_or_id(
|
193
73
|
cls,
|
@@ -208,13 +88,12 @@ class Result:
|
|
208
88
|
result.set_parent_run_id(parent_run_id)
|
209
89
|
return result
|
210
90
|
|
211
|
-
|
212
|
-
|
91
|
+
@model_validator(mode="after")
|
92
|
+
def __prepare_trace(self) -> Self:
|
93
|
+
"""Prepare trace field that want to pass after its initialize step."""
|
94
|
+
if self.trace is None: # pragma: no cove
|
95
|
+
self.trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
|
213
96
|
|
214
|
-
:param running_id: A running ID that want to update on this model.
|
215
|
-
:rtype: Self
|
216
|
-
"""
|
217
|
-
self.run_id: str = running_id
|
218
97
|
return self
|
219
98
|
|
220
99
|
def set_parent_run_id(self, running_id: str) -> Self:
|
@@ -224,6 +103,7 @@ class Result:
|
|
224
103
|
:rtype: Self
|
225
104
|
"""
|
226
105
|
self.parent_run_id: str = running_id
|
106
|
+
self.trace: TraceLog = get_trace(self.run_id, running_id)
|
227
107
|
return self
|
228
108
|
|
229
109
|
def catch(
|
@@ -244,13 +124,9 @@ class Result:
|
|
244
124
|
self.__dict__["context"].update(context or {})
|
245
125
|
return self
|
246
126
|
|
247
|
-
|
248
|
-
|
249
|
-
"""Return TraceLog object that passing its running ID.
|
127
|
+
def alive_time(self) -> float: # pragma: no cov
|
128
|
+
"""Return total seconds that this object use since it was created.
|
250
129
|
|
251
|
-
:rtype:
|
130
|
+
:rtype: float
|
252
131
|
"""
|
253
|
-
return TraceLog(self.run_id, self.parent_run_id)
|
254
|
-
|
255
|
-
def alive_time(self) -> float: # pragma: no cov
|
256
132
|
return (get_dt_tznow() - self.ts).total_seconds()
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -4,18 +4,18 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""
|
7
|
-
The main schedule running is
|
8
|
-
multiprocess of
|
9
|
-
config by
|
7
|
+
The main schedule running is `schedule_runner` function that trigger the
|
8
|
+
multiprocess of `schedule_control` function for listing schedules on the
|
9
|
+
config by `Loader.finds(Schedule)`.
|
10
10
|
|
11
|
-
The
|
12
|
-
functions;
|
11
|
+
The `schedule_control` is the scheduler function that release 2 schedule
|
12
|
+
functions; `workflow_task`, and `workflow_monitor`.
|
13
13
|
|
14
|
-
|
15
|
-
|
14
|
+
`schedule_control` ---( Every minute at :02 )--> `schedule_task`
|
15
|
+
---( Every 5 minutes )--> `monitor`
|
16
16
|
|
17
|
-
The
|
18
|
-
for multithreading strategy. This
|
17
|
+
The `schedule_task` will run `task.release` method in threading object
|
18
|
+
for multithreading strategy. This `release` method will run only one crontab
|
19
19
|
value with the on field.
|
20
20
|
"""
|
21
21
|
from __future__ import annotations
|
@@ -134,7 +134,7 @@ class ScheduleWorkflow(BaseModel):
|
|
134
134
|
on: list[str] = [on]
|
135
135
|
|
136
136
|
if any(not isinstance(n, (dict, str)) for n in on):
|
137
|
-
raise TypeError("The
|
137
|
+
raise TypeError("The `on` key should be list of str or dict")
|
138
138
|
|
139
139
|
# NOTE: Pass on value to Loader and keep on model object to on
|
140
140
|
# field.
|
@@ -344,7 +344,7 @@ class Schedule(BaseModel):
|
|
344
344
|
tasks=self.tasks(
|
345
345
|
start_date_waiting, queue=queue, externals=externals
|
346
346
|
),
|
347
|
-
|
347
|
+
stop=stop_date,
|
348
348
|
queue=queue,
|
349
349
|
threads=threads,
|
350
350
|
result=result,
|
@@ -359,12 +359,16 @@ ReturnResultOrCancel = Callable[P, ResultOrCancel]
|
|
359
359
|
DecoratorCancelJob = Callable[[ReturnResultOrCancel], ReturnResultOrCancel]
|
360
360
|
|
361
361
|
|
362
|
-
def catch_exceptions(
|
362
|
+
def catch_exceptions(
|
363
|
+
cancel_on_failure: bool = False,
|
364
|
+
parent_run_id: str | None = None,
|
365
|
+
) -> DecoratorCancelJob:
|
363
366
|
"""Catch exception error from scheduler job that running with schedule
|
364
367
|
package and return CancelJob if this function raise an error.
|
365
368
|
|
366
369
|
:param cancel_on_failure: A flag that allow to return the CancelJob or not
|
367
370
|
it will raise.
|
371
|
+
:param parent_run_id:
|
368
372
|
|
369
373
|
:rtype: DecoratorCancelJob
|
370
374
|
"""
|
@@ -375,10 +379,17 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
375
379
|
|
376
380
|
@wraps(func)
|
377
381
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultOrCancel:
|
382
|
+
|
378
383
|
try:
|
379
384
|
return func(*args, **kwargs)
|
385
|
+
|
380
386
|
except Exception as err:
|
381
|
-
|
387
|
+
if parent_run_id:
|
388
|
+
(
|
389
|
+
Result(parent_run_id=parent_run_id).trace.exception(
|
390
|
+
str(err)
|
391
|
+
)
|
392
|
+
)
|
382
393
|
if cancel_on_failure:
|
383
394
|
return CancelJob
|
384
395
|
raise err
|
@@ -399,13 +410,13 @@ class ReleaseThread(TypedDict):
|
|
399
410
|
ReleaseThreads = dict[str, ReleaseThread]
|
400
411
|
|
401
412
|
|
402
|
-
@catch_exceptions(cancel_on_failure=True)
|
403
413
|
def schedule_task(
|
404
414
|
tasks: list[WorkflowTask],
|
405
415
|
stop: datetime,
|
406
416
|
queue: dict[str, ReleaseQueue],
|
407
417
|
threads: ReleaseThreads,
|
408
418
|
audit: type[Audit],
|
419
|
+
*,
|
409
420
|
parent_run_id: str | None = None,
|
410
421
|
) -> ResultOrCancel:
|
411
422
|
"""Schedule task function that generate thread of workflow task release
|
@@ -491,8 +502,14 @@ def schedule_task(
|
|
491
502
|
# job.
|
492
503
|
thread_name: str = f"{task.alias}|{release.date:%Y%m%d%H%M}"
|
493
504
|
thread: Thread = Thread(
|
494
|
-
target=catch_exceptions(
|
495
|
-
|
505
|
+
target=catch_exceptions(
|
506
|
+
cancel_on_failure=True,
|
507
|
+
)(task.release),
|
508
|
+
kwargs={
|
509
|
+
"release": release,
|
510
|
+
"queue": q,
|
511
|
+
"audit": audit,
|
512
|
+
},
|
496
513
|
name=thread_name,
|
497
514
|
daemon=True,
|
498
515
|
)
|
@@ -508,22 +525,28 @@ def schedule_task(
|
|
508
525
|
delay()
|
509
526
|
|
510
527
|
result.trace.debug(
|
511
|
-
f"[SCHEDULE]: End schedule task
|
512
|
-
f"{'=' *
|
528
|
+
f"[SCHEDULE]: End schedule task that run since "
|
529
|
+
f"{current_date:%Y-%m-%d %H:%M:%S} {'=' * 30}"
|
513
530
|
)
|
514
531
|
return result.catch(
|
515
532
|
status=Status.SUCCESS, context={"task_date": current_date}
|
516
533
|
)
|
517
534
|
|
518
535
|
|
519
|
-
def monitor(
|
536
|
+
def monitor(
|
537
|
+
threads: ReleaseThreads,
|
538
|
+
parent_run_id: str | None = None,
|
539
|
+
) -> None: # pragma: no cov
|
520
540
|
"""Monitoring function that running every five minute for track long-running
|
521
541
|
thread instance from the schedule_control function that run every minute.
|
522
542
|
|
523
543
|
:param threads: A mapping of Thread object and its name.
|
544
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
545
|
+
|
524
546
|
:type threads: ReleaseThreads
|
525
547
|
"""
|
526
|
-
|
548
|
+
result: Result = Result().set_parent_run_id(parent_run_id)
|
549
|
+
result.trace.debug("[MONITOR]: Start checking long running schedule task.")
|
527
550
|
|
528
551
|
snapshot_threads: list[str] = list(threads.keys())
|
529
552
|
for thread_name in snapshot_threads:
|
@@ -538,20 +561,20 @@ def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
|
538
561
|
|
539
562
|
def scheduler_pending(
|
540
563
|
tasks: list[WorkflowTask],
|
541
|
-
|
542
|
-
queue,
|
543
|
-
threads,
|
564
|
+
stop: datetime,
|
565
|
+
queue: dict[str, ReleaseQueue],
|
566
|
+
threads: ReleaseThreads,
|
544
567
|
result: Result,
|
545
568
|
audit: type[Audit],
|
546
569
|
) -> Result: # pragma: no cov
|
547
|
-
"""
|
570
|
+
"""Scheduler pending function.
|
548
571
|
|
549
|
-
:param tasks:
|
550
|
-
:param
|
551
|
-
:param queue:
|
552
|
-
:param threads:
|
553
|
-
:param result:
|
554
|
-
:param audit:
|
572
|
+
:param tasks: A list of WorkflowTask object.
|
573
|
+
:param stop: A stop datetime object that force stop running scheduler.
|
574
|
+
:param queue: A mapping of alias name and ReleaseQueue object.
|
575
|
+
:param threads: A mapping of alias name and Thread object.
|
576
|
+
:param result: A result object.
|
577
|
+
:param audit: An audit class that want to make audit object.
|
555
578
|
|
556
579
|
:rtype: Result
|
557
580
|
"""
|
@@ -569,9 +592,12 @@ def scheduler_pending(
|
|
569
592
|
scheduler.every(1)
|
570
593
|
.minutes.at(":02")
|
571
594
|
.do(
|
572
|
-
|
595
|
+
catch_exceptions(
|
596
|
+
cancel_on_failure=True,
|
597
|
+
parent_run_id=result.parent_run_id,
|
598
|
+
)(schedule_task),
|
573
599
|
tasks=tasks,
|
574
|
-
stop=
|
600
|
+
stop=stop,
|
575
601
|
queue=queue,
|
576
602
|
threads=threads,
|
577
603
|
audit=audit,
|
@@ -588,13 +614,14 @@ def scheduler_pending(
|
|
588
614
|
.do(
|
589
615
|
monitor,
|
590
616
|
threads=threads,
|
617
|
+
parent_run_id=result.parent_run_id,
|
591
618
|
)
|
592
619
|
.tag("monitor")
|
593
620
|
)
|
594
621
|
|
595
622
|
# NOTE: Start running schedule
|
596
623
|
result.trace.info(
|
597
|
-
f"[SCHEDULE]: Schedule with stopper: {
|
624
|
+
f"[SCHEDULE]: Schedule with stopper: {stop:%Y-%m-%d %H:%M:%S}"
|
598
625
|
)
|
599
626
|
|
600
627
|
while True:
|
@@ -611,7 +638,7 @@ def scheduler_pending(
|
|
611
638
|
"running in background."
|
612
639
|
)
|
613
640
|
delay(10)
|
614
|
-
monitor(threads)
|
641
|
+
monitor(threads, parent_run_id=result.parent_run_id)
|
615
642
|
|
616
643
|
break
|
617
644
|
|
@@ -681,7 +708,7 @@ def schedule_control(
|
|
681
708
|
|
682
709
|
scheduler_pending(
|
683
710
|
tasks=tasks,
|
684
|
-
|
711
|
+
stop=stop_date,
|
685
712
|
queue=queue,
|
686
713
|
threads=threads,
|
687
714
|
result=result,
|
@@ -707,15 +734,16 @@ def schedule_runner(
|
|
707
734
|
|
708
735
|
This function will get all workflows that include on value that was
|
709
736
|
created in config path and chuck it with application config variable
|
710
|
-
|
737
|
+
`WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS` env var to multiprocess executor
|
711
738
|
pool.
|
712
739
|
|
713
740
|
The current workflow logic that split to process will be below diagram:
|
714
741
|
|
715
|
-
MAIN ==> process 01 ==> schedule
|
716
|
-
|
717
|
-
==> schedule
|
718
|
-
|
742
|
+
MAIN ==> process 01 ==> schedule ==> thread 01 --> 01
|
743
|
+
==> thread 01 --> 02
|
744
|
+
==> schedule ==> thread 02 --> 01
|
745
|
+
==> thread 02 --> 02
|
746
|
+
==> ...
|
719
747
|
==> process 02 ==> ...
|
720
748
|
|
721
749
|
:rtype: Result
|
@@ -745,7 +773,7 @@ def schedule_runner(
|
|
745
773
|
|
746
774
|
# NOTE: Raise error when it has any error from schedule_control.
|
747
775
|
if err := future.exception():
|
748
|
-
|
776
|
+
result.trace.error(str(err))
|
749
777
|
raise WorkflowException(str(err)) from err
|
750
778
|
|
751
779
|
rs: Result = future.result(timeout=1)
|