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.
@@ -4,7 +4,7 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  """
7
- The main schedule running is ``workflow_runner`` function that trigger the
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, WorkflowTaskData
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
- "ScheduleWorkflow",
74
- "workflow_task_release",
75
- "workflow_monitor",
76
- "workflow_control",
77
- "workflow_runner",
72
+ "WorkflowSchedule",
73
+ "schedule_task",
74
+ "monitor",
75
+ "schedule_control",
76
+ "schedule_runner",
78
77
  )
79
78
 
80
79
 
81
- class ScheduleWorkflow(BaseModel):
82
- """Schedule Workflow Pydantic model that use to keep workflow model for the
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 instance value.",
94
+ description="An override the list of On object values.",
96
95
  )
97
- params: DictData = Field(
96
+ values: DictData = Field(
98
97
  default_factory=dict,
99
- description="A parameters that want to use in workflow execution.",
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
- values["name"] = values["name"].replace(" ", "_")
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["name"]
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
- # WARNING:
154
- # if '* * * * *' in set_ons and len(set_ons) > 1:
155
- # raise ValueError(
156
- # "If it has every minute cronjob on value, it should has only "
157
- # "one value in the on field."
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 Model that use to run with scheduler package. It does
164
- not equal the on value in Workflow model but it use same logic to running
165
- release date with crontab interval.
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[ScheduleWorkflow] = Field(
234
+ workflows: list[WorkflowSchedule] = Field(
175
235
  default_factory=list,
176
- description="A list of ScheduleWorkflow models.",
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, list[datetime]],
281
+ queue: dict[str, WorkflowQueue],
221
282
  *,
222
283
  externals: DictData | None = None,
223
- ) -> list[WorkflowTaskData]:
224
- """Return the list of WorkflowTaskData object from the specific input
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[WorkflowTaskData]
232
- :return: Return the list of WorkflowTaskData object from the specific
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
- # NOTE: Create pair of workflow and on.
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
- while next_running_date in queue[sch_wf.alias]:
259
- next_running_date = runner.next
302
+ if workflow.alias not in queue:
303
+ queue[workflow.alias] = WorkflowQueue()
260
304
 
261
- workflow_tasks.append(
262
- WorkflowTaskData(
263
- alias=sch_wf.alias,
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 workflow_task_release(
315
- workflow_tasks: list[WorkflowTaskData],
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 workflow_tasks:
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 running:
330
- :param threads:
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 workflow_tasks:
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
- if (
367
- len(queue[task.alias]) > 0
368
- and task.runner.date != queue[task.alias][0]
369
- ):
370
- logger.debug(
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
- elif len(queue[task.alias]) == 0:
378
- logger.warning(
379
- f"[WORKFLOW]: Queue is empty for : {task.workflow.name!r} : "
380
- f"{task.runner.cron}"
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: Remove this datetime from queue.
385
- queue[task.alias].pop(0)
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.workflow.name}|{str(task.runner.cron)}|"
391
- f"{task.runner.date:%Y%m%d%H%M}"
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"[WORKFLOW]: {'=' * 100}")
438
+ logger.debug(f"[SCHEDULE]: End schedule release {'=' * 80}")
411
439
 
412
440
 
413
- def workflow_monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
414
- """Workflow schedule for monitoring long running thread from the schedule
415
- control.
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
- :rtype: None
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
- snapshot_threads = list(threads.keys())
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 workflow_control(
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
- """Workflow scheduler control.
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
- # NOTE: Design workflow queue caching.
454
- # ---
455
- # {"workflow-name": [<release-datetime>, <release-datetime>, ...]}
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 = (start_date + timedelta(minutes=1)).replace(
493
+ start_date_waiting: datetime = start_date.replace(
461
494
  second=0, microsecond=0
462
- )
495
+ ) + timedelta(minutes=1)
463
496
 
464
- # NOTE: Create pair of workflow and on from schedule model.
465
- workflow_tasks: list[WorkflowTaskData] = []
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=wf_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
- workflow_task_release,
484
- workflow_tasks=workflow_tasks,
485
- stop=(stop or (start_date + config.stop_boundary_delta)),
486
- queue=wf_queue,
487
- threads=thread_releases,
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
- scheduler.every(5).minutes.at(":10").do(
494
- workflow_monitor,
495
- threads=thread_releases,
496
- ).tag("monitor")
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(f"[WORKFLOW]: Start schedule: {schedules}")
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
- logger.warning(
508
- f"[WORKFLOW]: Workflow release thread: {thread_releases}"
509
- )
510
- logger.warning("[WORKFLOW]: Does not have any schedule jobs !!!")
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(queue2str(wf_queue[wf])) for wf in wf_queue]}"
560
+ f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
515
561
  )
516
562
  return schedules
517
563
 
518
564
 
519
- def workflow_runner(
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
- """Workflow application that running multiprocessing schedule with chunk of
525
- workflows that exists in config path.
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
- PIPELINES ==> process 01 ==> schedule --> thread of release
541
- workflow task 01 01
542
- --> thread of release
543
- workflow task 01 02
544
- ==> process 02 ==> schedule --> thread of release
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
- excluded: list[str] = excluded or []
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
- workflow_control,
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
- return results
620
+
621
+ return results