ddeutil-workflow 0.0.22__py3-none-any.whl → 0.0.24__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 +6 -0
- ddeutil/workflow/__init__.py +16 -14
- ddeutil/workflow/api.py +2 -2
- ddeutil/workflow/cli.py +2 -2
- ddeutil/workflow/conf.py +18 -2
- ddeutil/workflow/{on.py → cron.py} +3 -3
- ddeutil/workflow/job.py +2 -2
- ddeutil/workflow/params.py +176 -0
- ddeutil/workflow/result.py +102 -0
- ddeutil/workflow/route.py +1 -1
- ddeutil/workflow/scheduler.py +220 -173
- ddeutil/workflow/stage.py +6 -1
- ddeutil/workflow/utils.py +4 -245
- ddeutil/workflow/workflow.py +145 -133
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.24.dist-info}/METADATA +4 -3
- ddeutil_workflow-0.0.24.dist-info/RECORD +24 -0
- ddeutil_workflow-0.0.22.dist-info/RECORD +0 -22
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.24.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.24.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.24.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.22.dist-info → ddeutil_workflow-0.0.24.dist-info}/top_level.txt +0 -0
ddeutil/workflow/scheduler.py
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""
|
7
|
-
The main schedule running is ``
|
7
|
+
The main schedule running is ``schedule_runner`` function that trigger the
|
8
8
|
multiprocess of ``workflow_control`` function for listing schedules on the
|
9
9
|
config by ``Loader.finds(Schedule)``.
|
10
10
|
|
@@ -21,7 +21,6 @@ value with the on field.
|
|
21
21
|
from __future__ import annotations
|
22
22
|
|
23
23
|
import copy
|
24
|
-
import inspect
|
25
24
|
import logging
|
26
25
|
import time
|
27
26
|
from concurrent.futures import (
|
@@ -31,6 +30,7 @@ from concurrent.futures import (
|
|
31
30
|
)
|
32
31
|
from datetime import datetime, timedelta
|
33
32
|
from functools import wraps
|
33
|
+
from heapq import heappop
|
34
34
|
from textwrap import dedent
|
35
35
|
from threading import Thread
|
36
36
|
from typing import Callable, Optional
|
@@ -51,52 +51,55 @@ except ImportError: # pragma: no cov
|
|
51
51
|
|
52
52
|
from .__cron import CronRunner
|
53
53
|
from .__types import DictData, TupleStr
|
54
|
-
from .conf import Loader, config, get_logger
|
54
|
+
from .conf import FileLog, Loader, Log, config, get_logger
|
55
|
+
from .cron import On
|
55
56
|
from .exceptions import WorkflowException
|
56
|
-
from .on import On
|
57
57
|
from .utils import (
|
58
58
|
batch,
|
59
59
|
delay,
|
60
|
-
queue2str,
|
61
60
|
)
|
62
|
-
from .workflow import Workflow,
|
61
|
+
from .workflow import Workflow, WorkflowQueue, WorkflowRelease, WorkflowTask
|
63
62
|
|
64
63
|
P = ParamSpec("P")
|
65
64
|
logger = get_logger("ddeutil.workflow")
|
66
65
|
|
67
|
-
# NOTE: Adjust logging level on the schedule package.
|
66
|
+
# NOTE: Adjust logging level on the `schedule` package.
|
68
67
|
logging.getLogger("schedule").setLevel(logging.INFO)
|
69
68
|
|
70
69
|
|
71
70
|
__all__: TupleStr = (
|
72
71
|
"Schedule",
|
73
|
-
"
|
74
|
-
"
|
75
|
-
"
|
76
|
-
"
|
77
|
-
"
|
72
|
+
"WorkflowSchedule",
|
73
|
+
"schedule_task",
|
74
|
+
"monitor",
|
75
|
+
"schedule_control",
|
76
|
+
"schedule_runner",
|
78
77
|
)
|
79
78
|
|
80
79
|
|
81
|
-
class
|
82
|
-
"""Schedule
|
83
|
-
Schedule model. it should not use Workflow model directly because on the
|
80
|
+
class WorkflowSchedule(BaseModel):
|
81
|
+
"""Workflow Schedule Pydantic model that use to keep workflow model for
|
82
|
+
the Schedule model. it should not use Workflow model directly because on the
|
84
83
|
schedule config it can adjust crontab value that different from the Workflow
|
85
84
|
model.
|
86
85
|
"""
|
87
86
|
|
88
87
|
alias: Optional[str] = Field(
|
89
88
|
default=None,
|
90
|
-
description="An alias name of workflow.",
|
89
|
+
description="An alias name of workflow that use for schedule model.",
|
91
90
|
)
|
92
91
|
name: str = Field(description="A workflow name.")
|
93
92
|
on: list[On] = Field(
|
94
93
|
default_factory=list,
|
95
|
-
description="An override On
|
94
|
+
description="An override the list of On object values.",
|
96
95
|
)
|
97
|
-
|
96
|
+
values: DictData = Field(
|
98
97
|
default_factory=dict,
|
99
|
-
description=
|
98
|
+
description=(
|
99
|
+
"A value that want to pass to the workflow parameters when "
|
100
|
+
"calling release method."
|
101
|
+
),
|
102
|
+
alias="params",
|
100
103
|
)
|
101
104
|
|
102
105
|
@model_validator(mode="before")
|
@@ -105,10 +108,13 @@ class ScheduleWorkflow(BaseModel):
|
|
105
108
|
|
106
109
|
:rtype: DictData
|
107
110
|
"""
|
108
|
-
|
111
|
+
# VALIDATE: Prepare a workflow name that should not include space.
|
112
|
+
if name := values.get("name"):
|
113
|
+
values["name"] = name.replace(" ", "_")
|
109
114
|
|
115
|
+
# VALIDATE: Add default the alias field with the name.
|
110
116
|
if not values.get("alias"):
|
111
|
-
values["alias"] = values
|
117
|
+
values["alias"] = values.get("name")
|
112
118
|
|
113
119
|
cls.__bypass_on(values)
|
114
120
|
return values
|
@@ -135,6 +141,7 @@ class ScheduleWorkflow(BaseModel):
|
|
135
141
|
Loader(n, externals={}).data if isinstance(n, str) else n
|
136
142
|
for n in on
|
137
143
|
]
|
144
|
+
|
138
145
|
return data
|
139
146
|
|
140
147
|
@field_validator("on", mode="after")
|
@@ -150,19 +157,72 @@ class ScheduleWorkflow(BaseModel):
|
|
150
157
|
"The on fields should not contain duplicate on value."
|
151
158
|
)
|
152
159
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
160
|
+
if len(set_ons) > config.max_on_per_workflow:
|
161
|
+
raise ValueError(
|
162
|
+
f"The number of the on should not more than "
|
163
|
+
f"{config.max_on_per_workflow} crontab."
|
164
|
+
)
|
165
|
+
|
159
166
|
return value
|
160
167
|
|
168
|
+
def tasks(
|
169
|
+
self,
|
170
|
+
start_date: datetime,
|
171
|
+
queue: dict[str, WorkflowQueue],
|
172
|
+
*,
|
173
|
+
externals: DictData | None = None,
|
174
|
+
) -> list[WorkflowTask]:
|
175
|
+
"""Return the list of WorkflowTask object from the specific input
|
176
|
+
datetime that mapping with the on field.
|
177
|
+
|
178
|
+
This task creation need queue to tracking release date already
|
179
|
+
mapped or not.
|
180
|
+
|
181
|
+
:param start_date: A start date that get from the workflow schedule.
|
182
|
+
:param queue: A mapping of name and list of datetime for queue.
|
183
|
+
:param externals: An external parameters that pass to the Loader object.
|
184
|
+
|
185
|
+
:rtype: list[WorkflowTask]
|
186
|
+
:return: Return the list of WorkflowTask object from the specific
|
187
|
+
input datetime that mapping with the on field.
|
188
|
+
"""
|
189
|
+
workflow_tasks: list[WorkflowTask] = []
|
190
|
+
extras: DictData = externals or {}
|
191
|
+
|
192
|
+
# NOTE: Loading workflow model from the name of workflow.
|
193
|
+
wf: Workflow = Workflow.from_loader(self.name, externals=extras)
|
194
|
+
wf_queue: WorkflowQueue = queue[self.alias]
|
195
|
+
|
196
|
+
# IMPORTANT: Create the default 'on' value if it does not passing
|
197
|
+
# the on field to the Schedule object.
|
198
|
+
ons: list[On] = self.on or wf.on.copy()
|
199
|
+
|
200
|
+
for on in ons:
|
201
|
+
|
202
|
+
# NOTE: Create CronRunner instance from the start_date param.
|
203
|
+
runner: CronRunner = on.generate(start_date)
|
204
|
+
next_running_date = runner.next
|
205
|
+
|
206
|
+
while wf_queue.check_queue(next_running_date):
|
207
|
+
next_running_date = runner.next
|
208
|
+
|
209
|
+
workflow_tasks.append(
|
210
|
+
WorkflowTask(
|
211
|
+
alias=self.alias,
|
212
|
+
workflow=wf,
|
213
|
+
runner=runner,
|
214
|
+
values=self.values,
|
215
|
+
),
|
216
|
+
)
|
217
|
+
|
218
|
+
return workflow_tasks
|
219
|
+
|
161
220
|
|
162
221
|
class Schedule(BaseModel):
|
163
|
-
"""Schedule Pydantic
|
164
|
-
|
165
|
-
|
222
|
+
"""Schedule Pydantic model that use to run with any scheduler package.
|
223
|
+
|
224
|
+
It does not equal the on value in Workflow model but it use same logic
|
225
|
+
to running release date with crontab interval.
|
166
226
|
"""
|
167
227
|
|
168
228
|
desc: Optional[str] = Field(
|
@@ -171,9 +231,9 @@ class Schedule(BaseModel):
|
|
171
231
|
"A schedule description that can be string of markdown content."
|
172
232
|
),
|
173
233
|
)
|
174
|
-
workflows: list[
|
234
|
+
workflows: list[WorkflowSchedule] = Field(
|
175
235
|
default_factory=list,
|
176
|
-
description="A list of
|
236
|
+
description="A list of WorkflowSchedule models.",
|
177
237
|
)
|
178
238
|
|
179
239
|
@field_validator("desc", mode="after")
|
@@ -181,6 +241,7 @@ class Schedule(BaseModel):
|
|
181
241
|
"""Prepare description string that was created on a template.
|
182
242
|
|
183
243
|
:param value: A description string value that want to dedent.
|
244
|
+
|
184
245
|
:rtype: str
|
185
246
|
"""
|
186
247
|
return dedent(value)
|
@@ -217,55 +278,33 @@ class Schedule(BaseModel):
|
|
217
278
|
def tasks(
|
218
279
|
self,
|
219
280
|
start_date: datetime,
|
220
|
-
queue: dict[str,
|
281
|
+
queue: dict[str, WorkflowQueue],
|
221
282
|
*,
|
222
283
|
externals: DictData | None = None,
|
223
|
-
) -> list[
|
224
|
-
"""Return the list of
|
225
|
-
datetime that mapping with the on field.
|
284
|
+
) -> list[WorkflowTask]:
|
285
|
+
"""Return the list of WorkflowTask object from the specific input
|
286
|
+
datetime that mapping with the on field from workflow schedule model.
|
226
287
|
|
227
288
|
:param start_date: A start date that get from the workflow schedule.
|
228
289
|
:param queue: A mapping of name and list of datetime for queue.
|
290
|
+
:type queue: dict[str, WorkflowQueue]
|
229
291
|
:param externals: An external parameters that pass to the Loader object.
|
292
|
+
:type externals: DictData | None
|
230
293
|
|
231
|
-
:rtype: list[
|
232
|
-
:return: Return the list of
|
294
|
+
:rtype: list[WorkflowTask]
|
295
|
+
:return: Return the list of WorkflowTask object from the specific
|
233
296
|
input datetime that mapping with the on field.
|
234
297
|
"""
|
298
|
+
workflow_tasks: list[WorkflowTask] = []
|
235
299
|
|
236
|
-
|
237
|
-
workflow_tasks: list[WorkflowTaskData] = []
|
238
|
-
extras: DictData = externals or {}
|
239
|
-
|
240
|
-
for sch_wf in self.workflows:
|
241
|
-
|
242
|
-
wf: Workflow = Workflow.from_loader(sch_wf.name, externals=extras)
|
243
|
-
|
244
|
-
# NOTE: Create default list of release datetime.
|
245
|
-
if sch_wf.alias not in queue:
|
246
|
-
queue[sch_wf.alias]: list[datetime] = []
|
247
|
-
|
248
|
-
# IMPORTANT: Create the default 'on' value if it does not passing
|
249
|
-
# the on field to the Schedule object.
|
250
|
-
ons: list[On] = wf.on.copy() if len(sch_wf.on) == 0 else sch_wf.on
|
251
|
-
|
252
|
-
for on in ons:
|
253
|
-
|
254
|
-
# NOTE: Create CronRunner instance from the start_date param.
|
255
|
-
runner: CronRunner = on.generate(start_date)
|
256
|
-
next_running_date = runner.next
|
300
|
+
for workflow in self.workflows:
|
257
301
|
|
258
|
-
|
259
|
-
|
302
|
+
if workflow.alias not in queue:
|
303
|
+
queue[workflow.alias] = WorkflowQueue()
|
260
304
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
workflow=wf,
|
265
|
-
runner=runner,
|
266
|
-
params=sch_wf.params,
|
267
|
-
),
|
268
|
-
)
|
305
|
+
workflow_tasks.extend(
|
306
|
+
workflow.tasks(start_date, queue=queue, externals=externals)
|
307
|
+
)
|
269
308
|
|
270
309
|
return workflow_tasks
|
271
310
|
|
@@ -286,14 +325,6 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
286
325
|
|
287
326
|
def decorator(func: ReturnCancelJob) -> ReturnCancelJob: # pragma: no cov
|
288
327
|
try:
|
289
|
-
# NOTE: Check the function that want to handle is method or not.
|
290
|
-
if inspect.ismethod(func):
|
291
|
-
|
292
|
-
@wraps(func)
|
293
|
-
def wrapper(self, *args, **kwargs):
|
294
|
-
return func(self, *args, **kwargs)
|
295
|
-
|
296
|
-
return wrapper
|
297
328
|
|
298
329
|
@wraps(func)
|
299
330
|
def wrapper(*args, **kwargs):
|
@@ -311,36 +342,28 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
311
342
|
|
312
343
|
|
313
344
|
@catch_exceptions(cancel_on_failure=True) # pragma: no cov
|
314
|
-
def
|
315
|
-
|
345
|
+
def schedule_task(
|
346
|
+
tasks: list[WorkflowTask],
|
316
347
|
stop: datetime,
|
317
|
-
queue,
|
318
|
-
running,
|
348
|
+
queue: dict[str, WorkflowQueue],
|
319
349
|
threads: dict[str, Thread],
|
350
|
+
log: type[Log],
|
320
351
|
) -> CancelJob | None:
|
321
352
|
"""Workflow task generator that create release pair of workflow and on to
|
322
353
|
the threading in background.
|
323
354
|
|
324
355
|
This workflow task will start every minute at ':02' second.
|
325
356
|
|
326
|
-
:param
|
357
|
+
:param tasks: A list of WorkflowTask object.
|
327
358
|
:param stop: A stop datetime object that force stop running scheduler.
|
328
|
-
:param queue:
|
329
|
-
:param
|
330
|
-
:param
|
359
|
+
:param queue: A mapping of alias name and WorkflowQueue object.
|
360
|
+
:param threads: A mapping of alias name and Thread object.
|
361
|
+
:param log: A log class that want to making log object.
|
362
|
+
|
331
363
|
:rtype: CancelJob | None
|
332
364
|
"""
|
333
365
|
current_date: datetime = datetime.now(tz=config.tz)
|
334
|
-
|
335
366
|
if current_date > stop.replace(tzinfo=config.tz):
|
336
|
-
logger.info("[WORKFLOW]: Stop this schedule with datetime stopper.")
|
337
|
-
while len(threads) > 0:
|
338
|
-
logger.warning(
|
339
|
-
"[WORKFLOW]: Waiting workflow release thread that still "
|
340
|
-
"running in background."
|
341
|
-
)
|
342
|
-
time.sleep(15)
|
343
|
-
workflow_monitor(threads)
|
344
367
|
return CancelJob
|
345
368
|
|
346
369
|
# IMPORTANT:
|
@@ -355,48 +378,53 @@ def workflow_task_release(
|
|
355
378
|
# '00:02:00' --> '*/2 * * * *' --> running
|
356
379
|
# --> '*/35 * * * *' --> skip
|
357
380
|
#
|
358
|
-
for task in
|
381
|
+
for task in tasks:
|
382
|
+
|
383
|
+
q: WorkflowQueue = queue[task.alias]
|
384
|
+
|
385
|
+
# NOTE: Start adding queue and move the runner date in the WorkflowTask.
|
386
|
+
task.queue(stop, q, log=log)
|
359
387
|
|
360
388
|
# NOTE: Get incoming datetime queue.
|
361
|
-
logger.debug(
|
362
|
-
f"[WORKFLOW]: Current queue: {task.workflow.name!r} : "
|
363
|
-
f"{list(queue2str(queue[task.alias]))}"
|
364
|
-
)
|
389
|
+
logger.debug(f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}")
|
365
390
|
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
f"[WORKFLOW]: Skip schedule "
|
372
|
-
f"{task.runner.date:%Y-%m-%d %H:%M:%S} "
|
373
|
-
f"for : {task.workflow.name!r} : {task.runner.cron}"
|
391
|
+
# VALIDATE: Check the queue is empty or not.
|
392
|
+
if not q.is_queued:
|
393
|
+
logger.warning(
|
394
|
+
f"[WORKFLOW]: Queue is empty for : {task.alias!r} : "
|
395
|
+
f"{task.runner.cron}"
|
374
396
|
)
|
375
397
|
continue
|
376
398
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
399
|
+
# VALIDATE: Check this task is the first release in the queue or not.
|
400
|
+
current_date: datetime = current_date.replace(second=0, microsecond=0)
|
401
|
+
if (first_date := q.first_queue.date) != current_date:
|
402
|
+
logger.debug(
|
403
|
+
f"[WORKFLOW]: Skip schedule "
|
404
|
+
f"{first_date:%Y-%m-%d %H:%M:%S} "
|
405
|
+
f"for : {task.alias!r} : {task.runner.cron}"
|
381
406
|
)
|
382
407
|
continue
|
383
408
|
|
384
|
-
# NOTE:
|
385
|
-
queue
|
409
|
+
# NOTE: Pop the latest release and push it to running.
|
410
|
+
release: WorkflowRelease = heappop(q.queue)
|
411
|
+
q.push_running(release)
|
412
|
+
|
413
|
+
logger.info(
|
414
|
+
f"[WORKFLOW]: Start thread: '{task.alias}|{str(task.runner.cron)}|"
|
415
|
+
f"{release.date:%Y%m%d%H%M}'"
|
416
|
+
)
|
386
417
|
|
387
418
|
# NOTE: Create thread name that able to tracking with observe schedule
|
388
419
|
# job.
|
389
420
|
thread_name: str = (
|
390
|
-
f"{task.
|
391
|
-
f"{
|
421
|
+
f"{task.alias}|{str(task.runner.cron)}|"
|
422
|
+
f"{release.date:%Y%m%d%H%M}"
|
392
423
|
)
|
393
424
|
|
394
425
|
wf_thread: Thread = Thread(
|
395
426
|
target=catch_exceptions(cancel_on_failure=True)(task.release),
|
396
|
-
kwargs={
|
397
|
-
"queue": queue,
|
398
|
-
"running": running,
|
399
|
-
},
|
427
|
+
kwargs={"release": release, "queue": q, "log": log},
|
400
428
|
name=thread_name,
|
401
429
|
daemon=True,
|
402
430
|
)
|
@@ -407,20 +435,21 @@ def workflow_task_release(
|
|
407
435
|
|
408
436
|
delay()
|
409
437
|
|
410
|
-
logger.debug(f"[
|
438
|
+
logger.debug(f"[SCHEDULE]: End schedule release {'=' * 80}")
|
411
439
|
|
412
440
|
|
413
|
-
def
|
414
|
-
"""
|
415
|
-
|
441
|
+
def monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
|
442
|
+
"""Monitoring function that running every five minute for track long running
|
443
|
+
thread instance from the schedule_control function that run every minute.
|
416
444
|
|
417
445
|
:param threads: A mapping of Thread object and its name.
|
418
|
-
:
|
446
|
+
:type threads: dict[str, Thread]
|
419
447
|
"""
|
420
448
|
logger.debug(
|
421
449
|
"[MONITOR]: Start checking long running workflow release task."
|
422
450
|
)
|
423
|
-
|
451
|
+
|
452
|
+
snapshot_threads: list[str] = list(threads.keys())
|
424
453
|
for t_name in snapshot_threads:
|
425
454
|
|
426
455
|
# NOTE: remove the thread that running success.
|
@@ -428,18 +457,23 @@ def workflow_monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
|
|
428
457
|
threads.pop(t_name)
|
429
458
|
|
430
459
|
|
431
|
-
def
|
460
|
+
def schedule_control(
|
432
461
|
schedules: list[str],
|
433
462
|
stop: datetime | None = None,
|
434
463
|
externals: DictData | None = None,
|
464
|
+
*,
|
465
|
+
log: type[Log] | None = None,
|
435
466
|
) -> list[str]: # pragma: no cov
|
436
|
-
"""
|
467
|
+
"""Scheduler control function that running every minute.
|
437
468
|
|
438
469
|
:param schedules: A list of workflow names that want to schedule running.
|
439
470
|
:param stop: An datetime value that use to stop running schedule.
|
440
471
|
:param externals: An external parameters that pass to Loader.
|
472
|
+
:param log:
|
473
|
+
|
441
474
|
:rtype: list[str]
|
442
475
|
"""
|
476
|
+
# NOTE: Lazy import Scheduler object from the schedule package.
|
443
477
|
try:
|
444
478
|
from schedule import Scheduler
|
445
479
|
except ImportError:
|
@@ -447,30 +481,27 @@ def workflow_control(
|
|
447
481
|
"Should install schedule package before use this module."
|
448
482
|
) from None
|
449
483
|
|
484
|
+
log: type[Log] = log or FileLog
|
450
485
|
scheduler: Scheduler = Scheduler()
|
451
486
|
start_date: datetime = datetime.now(tz=config.tz)
|
487
|
+
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
452
488
|
|
453
|
-
#
|
454
|
-
|
455
|
-
|
456
|
-
#
|
457
|
-
wf_queue: dict[str, list[datetime]] = {}
|
458
|
-
thread_releases: dict[str, Thread] = {}
|
489
|
+
# IMPORTANT: Create main mapping of queue and thread object.
|
490
|
+
queue: dict[str, WorkflowQueue] = {}
|
491
|
+
threads: dict[str, Thread] = {}
|
459
492
|
|
460
|
-
start_date_waiting: datetime =
|
493
|
+
start_date_waiting: datetime = start_date.replace(
|
461
494
|
second=0, microsecond=0
|
462
|
-
)
|
495
|
+
) + timedelta(minutes=1)
|
463
496
|
|
464
|
-
# NOTE:
|
465
|
-
|
497
|
+
# NOTE: Start create workflow tasks from list of schedule name.
|
498
|
+
tasks: list[WorkflowTask] = []
|
466
499
|
for name in schedules:
|
467
500
|
schedule: Schedule = Schedule.from_loader(name, externals=externals)
|
468
|
-
|
469
|
-
# NOTE: Create a workflow task data instance from schedule object.
|
470
|
-
workflow_tasks.extend(
|
501
|
+
tasks.extend(
|
471
502
|
schedule.tasks(
|
472
503
|
start_date_waiting,
|
473
|
-
queue=
|
504
|
+
queue=queue,
|
474
505
|
externals=externals,
|
475
506
|
),
|
476
507
|
)
|
@@ -480,23 +511,33 @@ def workflow_control(
|
|
480
511
|
scheduler.every(1)
|
481
512
|
.minutes.at(":02")
|
482
513
|
.do(
|
483
|
-
|
484
|
-
|
485
|
-
stop=
|
486
|
-
queue=
|
487
|
-
threads=
|
514
|
+
schedule_task,
|
515
|
+
tasks=tasks,
|
516
|
+
stop=stop_date,
|
517
|
+
queue=queue,
|
518
|
+
threads=threads,
|
519
|
+
log=log,
|
488
520
|
)
|
489
521
|
.tag("control")
|
490
522
|
)
|
491
523
|
|
492
524
|
# NOTE: Checking zombie task with schedule job will start every 5 minute.
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
525
|
+
(
|
526
|
+
scheduler.every(5)
|
527
|
+
.minutes.at(":10")
|
528
|
+
.do(
|
529
|
+
monitor,
|
530
|
+
threads=threads,
|
531
|
+
)
|
532
|
+
.tag("monitor")
|
533
|
+
)
|
497
534
|
|
498
535
|
# NOTE: Start running schedule
|
499
|
-
logger.info(
|
536
|
+
logger.info(
|
537
|
+
f"[SCHEDULE]: Schedule: {schedules} with stopper: "
|
538
|
+
f"{stop_date:%Y-%m-%d %H:%M:%S}"
|
539
|
+
)
|
540
|
+
|
500
541
|
while True:
|
501
542
|
scheduler.run_pending()
|
502
543
|
time.sleep(1)
|
@@ -504,29 +545,35 @@ def workflow_control(
|
|
504
545
|
# NOTE: Break the scheduler when the control job does not exists.
|
505
546
|
if not scheduler.get_jobs("control"):
|
506
547
|
scheduler.clear("monitor")
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
548
|
+
|
549
|
+
while len(threads) > 0:
|
550
|
+
logger.warning(
|
551
|
+
"[SCHEDULE]: Waiting schedule release thread that still "
|
552
|
+
"running in background."
|
553
|
+
)
|
554
|
+
delay(15)
|
555
|
+
monitor(threads)
|
556
|
+
|
511
557
|
break
|
512
558
|
|
513
559
|
logger.warning(
|
514
|
-
f"Queue: {[list(
|
560
|
+
f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
|
515
561
|
)
|
516
562
|
return schedules
|
517
563
|
|
518
564
|
|
519
|
-
def
|
565
|
+
def schedule_runner(
|
520
566
|
stop: datetime | None = None,
|
521
567
|
externals: DictData | None = None,
|
522
568
|
excluded: list[str] | None = None,
|
523
569
|
) -> list[str]: # pragma: no cov
|
524
|
-
"""
|
525
|
-
|
570
|
+
"""Schedule runner function for start submit the ``schedule_control`` func
|
571
|
+
in multiprocessing pool with chunk of schedule config that exists in config
|
572
|
+
path by ``WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS``.
|
526
573
|
|
527
574
|
:param stop: A stop datetime object that force stop running scheduler.
|
528
|
-
:param excluded:
|
529
575
|
:param externals:
|
576
|
+
:param excluded: A list of schedule name that want to excluded from finding.
|
530
577
|
|
531
578
|
:rtype: list[str]
|
532
579
|
|
@@ -537,24 +584,21 @@ def workflow_runner(
|
|
537
584
|
|
538
585
|
The current workflow logic that split to process will be below diagram:
|
539
586
|
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
workflow task 02 01
|
546
|
-
--> thread of release
|
547
|
-
workflow task 02 02
|
548
|
-
==> ...
|
587
|
+
MAIN ==> process 01 ==> schedule --> thread of release task 01 01
|
588
|
+
--> thread of release task 01 02
|
589
|
+
==> schedule --> thread of release task 02 01
|
590
|
+
--> thread of release task 02 02
|
591
|
+
==> process 02
|
549
592
|
"""
|
550
|
-
|
593
|
+
results: list[str] = []
|
551
594
|
|
552
595
|
with ProcessPoolExecutor(
|
553
596
|
max_workers=config.max_schedule_process,
|
554
597
|
) as executor:
|
598
|
+
|
555
599
|
futures: list[Future] = [
|
556
600
|
executor.submit(
|
557
|
-
|
601
|
+
schedule_control,
|
558
602
|
schedules=[load[0] for load in loader],
|
559
603
|
stop=stop,
|
560
604
|
externals=(externals or {}),
|
@@ -565,10 +609,13 @@ def workflow_runner(
|
|
565
609
|
)
|
566
610
|
]
|
567
611
|
|
568
|
-
results: list[str] = []
|
569
612
|
for future in as_completed(futures):
|
613
|
+
|
614
|
+
# NOTE: Raise error when it has any error from schedule_control.
|
570
615
|
if err := future.exception():
|
571
616
|
logger.error(str(err))
|
572
617
|
raise WorkflowException(str(err)) from err
|
618
|
+
|
573
619
|
results.extend(future.result(timeout=1))
|
574
|
-
|
620
|
+
|
621
|
+
return results
|