ddeutil-workflow 0.0.23__py3-none-any.whl → 0.0.25__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 +8 -7
- ddeutil/workflow/api/__init__.py +1 -0
- ddeutil/workflow/{api.py → api/api.py} +43 -21
- ddeutil/workflow/{repeat.py → api/repeat.py} +2 -2
- ddeutil/workflow/{route.py → api/route.py} +81 -62
- ddeutil/workflow/cli.py +33 -55
- ddeutil/workflow/conf.py +38 -45
- ddeutil/workflow/{on.py → cron.py} +1 -1
- ddeutil/workflow/exceptions.py +3 -0
- ddeutil/workflow/scheduler.py +212 -165
- ddeutil/workflow/stage.py +5 -0
- ddeutil/workflow/utils.py +7 -5
- ddeutil/workflow/workflow.py +149 -149
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.25.dist-info}/METADATA +33 -35
- ddeutil_workflow-0.0.25.dist-info/RECORD +25 -0
- ddeutil_workflow-0.0.23.dist-info/RECORD +0 -24
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.25.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.25.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.25.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.23.dist-info → ddeutil_workflow-0.0.25.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,9 +30,10 @@ from concurrent.futures import (
|
|
31
30
|
)
|
32
31
|
from datetime import datetime, timedelta
|
33
32
|
from functools import wraps
|
33
|
+
from heapq import heappop, heappush
|
34
34
|
from textwrap import dedent
|
35
35
|
from threading import Thread
|
36
|
-
from typing import Callable, Optional
|
36
|
+
from typing import Callable, Optional, TypedDict
|
37
37
|
|
38
38
|
from pydantic import BaseModel, Field
|
39
39
|
from pydantic.functional_validators import field_validator, model_validator
|
@@ -51,30 +51,31 @@ 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
72
|
"WorkflowSchedule",
|
74
|
-
"
|
75
|
-
"
|
76
|
-
"
|
77
|
-
"
|
73
|
+
"schedule_task",
|
74
|
+
"monitor",
|
75
|
+
"schedule_control",
|
76
|
+
"schedule_runner",
|
77
|
+
"ReleaseThreads",
|
78
|
+
"ReleaseThread",
|
78
79
|
)
|
79
80
|
|
80
81
|
|
@@ -166,6 +167,58 @@ class WorkflowSchedule(BaseModel):
|
|
166
167
|
|
167
168
|
return value
|
168
169
|
|
170
|
+
def tasks(
|
171
|
+
self,
|
172
|
+
start_date: datetime,
|
173
|
+
queue: dict[str, WorkflowQueue],
|
174
|
+
*,
|
175
|
+
externals: DictData | None = None,
|
176
|
+
) -> list[WorkflowTask]:
|
177
|
+
"""Return the list of WorkflowTask object from the specific input
|
178
|
+
datetime that mapping with the on field.
|
179
|
+
|
180
|
+
This task creation need queue to tracking release date already
|
181
|
+
mapped or not.
|
182
|
+
|
183
|
+
:param start_date: A start date that get from the workflow schedule.
|
184
|
+
:param queue: A mapping of name and list of datetime for queue.
|
185
|
+
:param externals: An external parameters that pass to the Loader object.
|
186
|
+
|
187
|
+
:rtype: list[WorkflowTask]
|
188
|
+
:return: Return the list of WorkflowTask object from the specific
|
189
|
+
input datetime that mapping with the on field.
|
190
|
+
"""
|
191
|
+
workflow_tasks: list[WorkflowTask] = []
|
192
|
+
extras: DictData = externals or {}
|
193
|
+
|
194
|
+
# NOTE: Loading workflow model from the name of workflow.
|
195
|
+
wf: Workflow = Workflow.from_loader(self.name, externals=extras)
|
196
|
+
wf_queue: WorkflowQueue = queue[self.alias]
|
197
|
+
|
198
|
+
# IMPORTANT: Create the default 'on' value if it does not passing
|
199
|
+
# the on field to the Schedule object.
|
200
|
+
ons: list[On] = self.on or wf.on.copy()
|
201
|
+
|
202
|
+
for on in ons:
|
203
|
+
|
204
|
+
# NOTE: Create CronRunner instance from the start_date param.
|
205
|
+
runner: CronRunner = on.generate(start_date)
|
206
|
+
next_running_date = runner.next
|
207
|
+
|
208
|
+
while wf_queue.check_queue(next_running_date):
|
209
|
+
next_running_date = runner.next
|
210
|
+
|
211
|
+
workflow_tasks.append(
|
212
|
+
WorkflowTask(
|
213
|
+
alias=self.alias,
|
214
|
+
workflow=wf,
|
215
|
+
runner=runner,
|
216
|
+
values=self.values,
|
217
|
+
),
|
218
|
+
)
|
219
|
+
|
220
|
+
return workflow_tasks
|
221
|
+
|
169
222
|
|
170
223
|
class Schedule(BaseModel):
|
171
224
|
"""Schedule Pydantic model that use to run with any scheduler package.
|
@@ -214,7 +267,7 @@ class Schedule(BaseModel):
|
|
214
267
|
loader: Loader = Loader(name, externals=(externals or {}))
|
215
268
|
|
216
269
|
# NOTE: Validate the config type match with current connection model
|
217
|
-
if loader.type != cls:
|
270
|
+
if loader.type != cls.__name__:
|
218
271
|
raise ValueError(f"Type {loader.type} does not match with {cls}")
|
219
272
|
|
220
273
|
loader_data: DictData = copy.deepcopy(loader.data)
|
@@ -227,57 +280,33 @@ class Schedule(BaseModel):
|
|
227
280
|
def tasks(
|
228
281
|
self,
|
229
282
|
start_date: datetime,
|
230
|
-
queue: dict[str,
|
283
|
+
queue: dict[str, WorkflowQueue],
|
231
284
|
*,
|
232
285
|
externals: DictData | None = None,
|
233
|
-
) -> list[
|
234
|
-
"""Return the list of
|
235
|
-
datetime that mapping with the on field.
|
236
|
-
|
237
|
-
This task creation need queue to tracking release date already
|
238
|
-
mapped or not.
|
286
|
+
) -> list[WorkflowTask]:
|
287
|
+
"""Return the list of WorkflowTask object from the specific input
|
288
|
+
datetime that mapping with the on field from workflow schedule model.
|
239
289
|
|
240
290
|
:param start_date: A start date that get from the workflow schedule.
|
241
291
|
:param queue: A mapping of name and list of datetime for queue.
|
292
|
+
:type queue: dict[str, WorkflowQueue]
|
242
293
|
:param externals: An external parameters that pass to the Loader object.
|
294
|
+
:type externals: DictData | None
|
243
295
|
|
244
|
-
:rtype: list[
|
245
|
-
:return: Return the list of
|
296
|
+
:rtype: list[WorkflowTask]
|
297
|
+
:return: Return the list of WorkflowTask object from the specific
|
246
298
|
input datetime that mapping with the on field.
|
247
299
|
"""
|
248
|
-
workflow_tasks: list[
|
249
|
-
extras: DictData = externals or {}
|
300
|
+
workflow_tasks: list[WorkflowTask] = []
|
250
301
|
|
251
|
-
for
|
302
|
+
for workflow in self.workflows:
|
252
303
|
|
253
|
-
|
254
|
-
|
304
|
+
if workflow.alias not in queue:
|
305
|
+
queue[workflow.alias] = WorkflowQueue()
|
255
306
|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
# IMPORTANT: Create the default 'on' value if it does not passing
|
261
|
-
# the on field to the Schedule object.
|
262
|
-
ons: list[On] = sch_wf.on or wf.on.copy()
|
263
|
-
|
264
|
-
for on in ons:
|
265
|
-
|
266
|
-
# NOTE: Create CronRunner instance from the start_date param.
|
267
|
-
runner: CronRunner = on.generate(start_date)
|
268
|
-
next_running_date = runner.next
|
269
|
-
|
270
|
-
while next_running_date in queue[sch_wf.alias]:
|
271
|
-
next_running_date = runner.next
|
272
|
-
|
273
|
-
workflow_tasks.append(
|
274
|
-
WorkflowTaskData(
|
275
|
-
alias=sch_wf.alias,
|
276
|
-
workflow=wf,
|
277
|
-
runner=runner,
|
278
|
-
params=sch_wf.values,
|
279
|
-
),
|
280
|
-
)
|
307
|
+
workflow_tasks.extend(
|
308
|
+
workflow.tasks(start_date, queue=queue, externals=externals)
|
309
|
+
)
|
281
310
|
|
282
311
|
return workflow_tasks
|
283
312
|
|
@@ -298,14 +327,6 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
298
327
|
|
299
328
|
def decorator(func: ReturnCancelJob) -> ReturnCancelJob: # pragma: no cov
|
300
329
|
try:
|
301
|
-
# NOTE: Check the function that want to handle is method or not.
|
302
|
-
if inspect.ismethod(func):
|
303
|
-
|
304
|
-
@wraps(func)
|
305
|
-
def wrapper(self, *args, **kwargs):
|
306
|
-
return func(self, *args, **kwargs)
|
307
|
-
|
308
|
-
return wrapper
|
309
330
|
|
310
331
|
@wraps(func)
|
311
332
|
def wrapper(*args, **kwargs):
|
@@ -322,37 +343,37 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
322
343
|
return decorator
|
323
344
|
|
324
345
|
|
325
|
-
|
326
|
-
|
327
|
-
|
346
|
+
class ReleaseThread(TypedDict):
|
347
|
+
thread: Thread
|
348
|
+
start_date: datetime
|
349
|
+
|
350
|
+
|
351
|
+
ReleaseThreads = dict[str, ReleaseThread]
|
352
|
+
|
353
|
+
|
354
|
+
@catch_exceptions(cancel_on_failure=True)
|
355
|
+
def schedule_task(
|
356
|
+
tasks: list[WorkflowTask],
|
328
357
|
stop: datetime,
|
329
|
-
queue,
|
330
|
-
|
331
|
-
|
358
|
+
queue: dict[str, WorkflowQueue],
|
359
|
+
threads: ReleaseThreads,
|
360
|
+
log: type[Log],
|
332
361
|
) -> CancelJob | None:
|
333
362
|
"""Workflow task generator that create release pair of workflow and on to
|
334
363
|
the threading in background.
|
335
364
|
|
336
365
|
This workflow task will start every minute at ':02' second.
|
337
366
|
|
338
|
-
:param
|
367
|
+
:param tasks: A list of WorkflowTask object.
|
339
368
|
:param stop: A stop datetime object that force stop running scheduler.
|
340
|
-
:param queue:
|
341
|
-
:param
|
342
|
-
:param
|
369
|
+
:param queue: A mapping of alias name and WorkflowQueue object.
|
370
|
+
:param threads: A mapping of alias name and Thread object.
|
371
|
+
:param log: A log class that want to making log object.
|
372
|
+
|
343
373
|
:rtype: CancelJob | None
|
344
374
|
"""
|
345
375
|
current_date: datetime = datetime.now(tz=config.tz)
|
346
|
-
|
347
376
|
if current_date > stop.replace(tzinfo=config.tz):
|
348
|
-
logger.info("[WORKFLOW]: Stop this schedule with datetime stopper.")
|
349
|
-
while len(threads) > 0:
|
350
|
-
logger.warning(
|
351
|
-
"[WORKFLOW]: Waiting workflow release thread that still "
|
352
|
-
"running in background."
|
353
|
-
)
|
354
|
-
time.sleep(15)
|
355
|
-
workflow_monitor(threads)
|
356
377
|
return CancelJob
|
357
378
|
|
358
379
|
# IMPORTANT:
|
@@ -367,91 +388,104 @@ def workflow_task_release(
|
|
367
388
|
# '00:02:00' --> '*/2 * * * *' --> running
|
368
389
|
# --> '*/35 * * * *' --> skip
|
369
390
|
#
|
370
|
-
for task in
|
391
|
+
for task in tasks:
|
392
|
+
|
393
|
+
q: WorkflowQueue = queue[task.alias]
|
394
|
+
|
395
|
+
# NOTE: Start adding queue and move the runner date in the WorkflowTask.
|
396
|
+
task.queue(stop, q, log=log)
|
371
397
|
|
372
398
|
# NOTE: Get incoming datetime queue.
|
373
|
-
logger.debug(
|
374
|
-
f"[WORKFLOW]: Current queue: {task.workflow.name!r} : "
|
375
|
-
f"{list(queue2str(queue[task.alias]))}"
|
376
|
-
)
|
399
|
+
logger.debug(f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}")
|
377
400
|
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
f"[WORKFLOW]: Skip schedule "
|
384
|
-
f"{task.runner.date:%Y-%m-%d %H:%M:%S} "
|
385
|
-
f"for : {task.workflow.name!r} : {task.runner.cron}"
|
401
|
+
# VALIDATE: Check the queue is empty or not.
|
402
|
+
if not q.is_queued:
|
403
|
+
logger.warning(
|
404
|
+
f"[WORKFLOW]: Queue is empty for : {task.alias!r} : "
|
405
|
+
f"{task.runner.cron}"
|
386
406
|
)
|
387
407
|
continue
|
388
408
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
409
|
+
# VALIDATE: Check this task is the first release in the queue or not.
|
410
|
+
current_release: datetime = current_date.replace(
|
411
|
+
second=0, microsecond=0
|
412
|
+
)
|
413
|
+
if (first_date := q.first_queue.date) != current_release:
|
414
|
+
logger.debug(
|
415
|
+
f"[WORKFLOW]: Skip schedule "
|
416
|
+
f"{first_date:%Y-%m-%d %H:%M:%S} for : {task.alias!r}"
|
393
417
|
)
|
394
418
|
continue
|
395
419
|
|
396
|
-
# NOTE:
|
397
|
-
queue
|
420
|
+
# NOTE: Pop the latest release and push it to running.
|
421
|
+
release: WorkflowRelease = heappop(q.queue)
|
422
|
+
heappush(q.running, release)
|
398
423
|
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
f"{task.workflow.name}|{str(task.runner.cron)}|"
|
403
|
-
f"{task.runner.date:%Y%m%d%H%M}"
|
424
|
+
logger.info(
|
425
|
+
f"[WORKFLOW]: Start thread: '{task.alias}|"
|
426
|
+
f"{release.date:%Y%m%d%H%M}'"
|
404
427
|
)
|
405
428
|
|
406
|
-
|
429
|
+
# NOTE: Create thread name that able to tracking with observe schedule
|
430
|
+
# job.
|
431
|
+
thread_name: str = f"{task.alias}|{release.date:%Y%m%d%H%M}"
|
432
|
+
thread: Thread = Thread(
|
407
433
|
target=catch_exceptions(cancel_on_failure=True)(task.release),
|
408
|
-
kwargs={
|
409
|
-
"queue": queue,
|
410
|
-
"running": running,
|
411
|
-
},
|
434
|
+
kwargs={"release": release, "queue": q, "log": log},
|
412
435
|
name=thread_name,
|
413
436
|
daemon=True,
|
414
437
|
)
|
415
438
|
|
416
|
-
threads[thread_name] =
|
439
|
+
threads[thread_name] = {
|
440
|
+
"thread": thread,
|
441
|
+
"start_date": datetime.now(tz=config.tz),
|
442
|
+
}
|
417
443
|
|
418
|
-
|
444
|
+
thread.start()
|
419
445
|
|
420
446
|
delay()
|
421
447
|
|
422
|
-
logger.debug(f"[
|
448
|
+
logger.debug(f"[SCHEDULE]: End schedule release {'=' * 80}")
|
423
449
|
|
424
450
|
|
425
|
-
def
|
426
|
-
"""
|
427
|
-
|
451
|
+
def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
452
|
+
"""Monitoring function that running every five minute for track long running
|
453
|
+
thread instance from the schedule_control function that run every minute.
|
428
454
|
|
429
455
|
:param threads: A mapping of Thread object and its name.
|
430
|
-
:
|
456
|
+
:type threads: ReleaseThreads
|
431
457
|
"""
|
432
458
|
logger.debug(
|
433
459
|
"[MONITOR]: Start checking long running workflow release task."
|
434
460
|
)
|
435
|
-
|
461
|
+
|
462
|
+
snapshot_threads: list[str] = list(threads.keys())
|
436
463
|
for t_name in snapshot_threads:
|
437
464
|
|
465
|
+
thread_release: ReleaseThread = threads[t_name]
|
466
|
+
|
438
467
|
# NOTE: remove the thread that running success.
|
439
|
-
if not
|
468
|
+
if not thread_release["thread"].is_alive():
|
440
469
|
threads.pop(t_name)
|
441
470
|
|
442
471
|
|
443
|
-
def
|
472
|
+
def schedule_control(
|
444
473
|
schedules: list[str],
|
445
474
|
stop: datetime | None = None,
|
446
475
|
externals: DictData | None = None,
|
476
|
+
*,
|
477
|
+
log: type[Log] | None = None,
|
447
478
|
) -> list[str]: # pragma: no cov
|
448
|
-
"""
|
479
|
+
"""Scheduler control function that running every minute.
|
449
480
|
|
450
481
|
:param schedules: A list of workflow names that want to schedule running.
|
451
482
|
:param stop: An datetime value that use to stop running schedule.
|
452
483
|
:param externals: An external parameters that pass to Loader.
|
484
|
+
:param log:
|
485
|
+
|
453
486
|
:rtype: list[str]
|
454
487
|
"""
|
488
|
+
# NOTE: Lazy import Scheduler object from the schedule package.
|
455
489
|
try:
|
456
490
|
from schedule import Scheduler
|
457
491
|
except ImportError:
|
@@ -459,30 +493,27 @@ def workflow_control(
|
|
459
493
|
"Should install schedule package before use this module."
|
460
494
|
) from None
|
461
495
|
|
496
|
+
log: type[Log] = log or FileLog
|
462
497
|
scheduler: Scheduler = Scheduler()
|
463
498
|
start_date: datetime = datetime.now(tz=config.tz)
|
499
|
+
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
464
500
|
|
465
|
-
#
|
466
|
-
|
467
|
-
|
468
|
-
#
|
469
|
-
wf_queue: dict[str, list[datetime]] = {}
|
470
|
-
thread_releases: dict[str, Thread] = {}
|
501
|
+
# IMPORTANT: Create main mapping of queue and thread object.
|
502
|
+
queue: dict[str, WorkflowQueue] = {}
|
503
|
+
threads: ReleaseThreads = {}
|
471
504
|
|
472
|
-
start_date_waiting: datetime =
|
505
|
+
start_date_waiting: datetime = start_date.replace(
|
473
506
|
second=0, microsecond=0
|
474
|
-
)
|
507
|
+
) + timedelta(minutes=1)
|
475
508
|
|
476
|
-
# NOTE:
|
477
|
-
|
509
|
+
# NOTE: Start create workflow tasks from list of schedule name.
|
510
|
+
tasks: list[WorkflowTask] = []
|
478
511
|
for name in schedules:
|
479
512
|
schedule: Schedule = Schedule.from_loader(name, externals=externals)
|
480
|
-
|
481
|
-
# NOTE: Create a workflow task data instance from schedule object.
|
482
|
-
workflow_tasks.extend(
|
513
|
+
tasks.extend(
|
483
514
|
schedule.tasks(
|
484
515
|
start_date_waiting,
|
485
|
-
queue=
|
516
|
+
queue=queue,
|
486
517
|
externals=externals,
|
487
518
|
),
|
488
519
|
)
|
@@ -492,23 +523,33 @@ def workflow_control(
|
|
492
523
|
scheduler.every(1)
|
493
524
|
.minutes.at(":02")
|
494
525
|
.do(
|
495
|
-
|
496
|
-
|
497
|
-
stop=
|
498
|
-
queue=
|
499
|
-
threads=
|
526
|
+
schedule_task,
|
527
|
+
tasks=tasks,
|
528
|
+
stop=stop_date,
|
529
|
+
queue=queue,
|
530
|
+
threads=threads,
|
531
|
+
log=log,
|
500
532
|
)
|
501
533
|
.tag("control")
|
502
534
|
)
|
503
535
|
|
504
536
|
# NOTE: Checking zombie task with schedule job will start every 5 minute.
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
537
|
+
(
|
538
|
+
scheduler.every(5)
|
539
|
+
.minutes.at(":10")
|
540
|
+
.do(
|
541
|
+
monitor,
|
542
|
+
threads=threads,
|
543
|
+
)
|
544
|
+
.tag("monitor")
|
545
|
+
)
|
509
546
|
|
510
547
|
# NOTE: Start running schedule
|
511
|
-
logger.info(
|
548
|
+
logger.info(
|
549
|
+
f"[SCHEDULE]: Schedule: {schedules} with stopper: "
|
550
|
+
f"{stop_date:%Y-%m-%d %H:%M:%S}"
|
551
|
+
)
|
552
|
+
|
512
553
|
while True:
|
513
554
|
scheduler.run_pending()
|
514
555
|
time.sleep(1)
|
@@ -516,29 +557,35 @@ def workflow_control(
|
|
516
557
|
# NOTE: Break the scheduler when the control job does not exists.
|
517
558
|
if not scheduler.get_jobs("control"):
|
518
559
|
scheduler.clear("monitor")
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
560
|
+
|
561
|
+
while len(threads) > 0:
|
562
|
+
logger.warning(
|
563
|
+
"[SCHEDULE]: Waiting schedule release thread that still "
|
564
|
+
"running in background."
|
565
|
+
)
|
566
|
+
delay(15)
|
567
|
+
monitor(threads)
|
568
|
+
|
523
569
|
break
|
524
570
|
|
525
571
|
logger.warning(
|
526
|
-
f"Queue: {[list(
|
572
|
+
f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
|
527
573
|
)
|
528
574
|
return schedules
|
529
575
|
|
530
576
|
|
531
|
-
def
|
577
|
+
def schedule_runner(
|
532
578
|
stop: datetime | None = None,
|
533
579
|
externals: DictData | None = None,
|
534
580
|
excluded: list[str] | None = None,
|
535
581
|
) -> list[str]: # pragma: no cov
|
536
|
-
"""
|
537
|
-
|
582
|
+
"""Schedule runner function for start submit the ``schedule_control`` func
|
583
|
+
in multiprocessing pool with chunk of schedule config that exists in config
|
584
|
+
path by ``WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS``.
|
538
585
|
|
539
586
|
:param stop: A stop datetime object that force stop running scheduler.
|
540
|
-
:param excluded:
|
541
587
|
:param externals:
|
588
|
+
:param excluded: A list of schedule name that want to excluded from finding.
|
542
589
|
|
543
590
|
:rtype: list[str]
|
544
591
|
|
@@ -549,24 +596,21 @@ def workflow_runner(
|
|
549
596
|
|
550
597
|
The current workflow logic that split to process will be below diagram:
|
551
598
|
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
workflow task 02 01
|
558
|
-
--> thread of release
|
559
|
-
workflow task 02 02
|
560
|
-
==> ...
|
599
|
+
MAIN ==> process 01 ==> schedule --> thread of release task 01 01
|
600
|
+
--> thread of release task 01 02
|
601
|
+
==> schedule --> thread of release task 02 01
|
602
|
+
--> thread of release task 02 02
|
603
|
+
==> process 02
|
561
604
|
"""
|
562
|
-
|
605
|
+
results: list[str] = []
|
563
606
|
|
564
607
|
with ProcessPoolExecutor(
|
565
608
|
max_workers=config.max_schedule_process,
|
566
609
|
) as executor:
|
610
|
+
|
567
611
|
futures: list[Future] = [
|
568
612
|
executor.submit(
|
569
|
-
|
613
|
+
schedule_control,
|
570
614
|
schedules=[load[0] for load in loader],
|
571
615
|
stop=stop,
|
572
616
|
externals=(externals or {}),
|
@@ -577,10 +621,13 @@ def workflow_runner(
|
|
577
621
|
)
|
578
622
|
]
|
579
623
|
|
580
|
-
results: list[str] = []
|
581
624
|
for future in as_completed(futures):
|
625
|
+
|
626
|
+
# NOTE: Raise error when it has any error from schedule_control.
|
582
627
|
if err := future.exception():
|
583
628
|
logger.error(str(err))
|
584
629
|
raise WorkflowException(str(err)) from err
|
630
|
+
|
585
631
|
results.extend(future.result(timeout=1))
|
586
|
-
|
632
|
+
|
633
|
+
return results
|
ddeutil/workflow/stage.py
CHANGED
@@ -346,6 +346,11 @@ class EmptyStage(BaseStage):
|
|
346
346
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
347
347
|
)
|
348
348
|
if self.sleep > 0:
|
349
|
+
if self.sleep > 30:
|
350
|
+
logger.info(
|
351
|
+
f"({cut_id(run_id)}) [STAGE]: ... sleep "
|
352
|
+
f"({self.sleep} seconds)"
|
353
|
+
)
|
349
354
|
time.sleep(self.sleep)
|
350
355
|
return Result(status=0, context={}, run_id=run_id)
|
351
356
|
|
ddeutil/workflow/utils.py
CHANGED
@@ -74,6 +74,12 @@ def get_diff_sec(
|
|
74
74
|
)
|
75
75
|
|
76
76
|
|
77
|
+
def wait_a_minute(now: datetime, second: float = 2) -> None: # pragma: no cov
|
78
|
+
"""Wait with sleep to the next minute with an offset second value."""
|
79
|
+
future = now.replace(second=0, microsecond=0) + timedelta(minutes=1)
|
80
|
+
time.sleep((future - now).total_seconds() + second)
|
81
|
+
|
82
|
+
|
77
83
|
def delay(second: float = 0) -> None: # pragma: no cov
|
78
84
|
"""Delay time that use time.sleep with random second value between
|
79
85
|
0.00 - 0.99 seconds.
|
@@ -326,7 +332,7 @@ def get_args_from_filter(
|
|
326
332
|
|
327
333
|
if func_name not in filters:
|
328
334
|
raise UtilException(
|
329
|
-
f"The post-filter: {func_name} does not support yet."
|
335
|
+
f"The post-filter: {func_name!r} does not support yet."
|
330
336
|
)
|
331
337
|
|
332
338
|
if isinstance((f_func := filters[func_name]), list) and (args or kwargs):
|
@@ -577,10 +583,6 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
|
577
583
|
yield chain((first_el,), chunk_it)
|
578
584
|
|
579
585
|
|
580
|
-
def queue2str(queue: list[datetime]) -> Iterator[str]: # pragma: no cov
|
581
|
-
return (f"{q:%Y-%m-%d %H:%M:%S}" for q in queue)
|
582
|
-
|
583
|
-
|
584
586
|
def cut_id(run_id: str, *, num: int = 6):
|
585
587
|
"""Cutting running ID with length.
|
586
588
|
|