ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.34__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 +16 -10
- ddeutil/workflow/api/route.py +2 -2
- ddeutil/workflow/audit.py +28 -37
- ddeutil/workflow/{hook.py → call.py} +27 -27
- ddeutil/workflow/conf.py +47 -12
- ddeutil/workflow/job.py +80 -118
- ddeutil/workflow/result.py +126 -25
- ddeutil/workflow/scheduler.py +165 -150
- ddeutil/workflow/{stage.py → stages.py} +103 -37
- ddeutil/workflow/utils.py +20 -2
- ddeutil/workflow/workflow.py +137 -112
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/METADATA +18 -17
- ddeutil_workflow-0.0.34.dist-info/RECORD +26 -0
- ddeutil_workflow-0.0.33.dist-info/RECORD +0 -26
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/top_level.txt +0 -0
ddeutil/workflow/scheduler.py
CHANGED
@@ -5,16 +5,16 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""
|
7
7
|
The main schedule running is ``schedule_runner`` function that trigger the
|
8
|
-
multiprocess of ``
|
8
|
+
multiprocess of ``schedule_control`` function for listing schedules on the
|
9
9
|
config by ``Loader.finds(Schedule)``.
|
10
10
|
|
11
|
-
The ``
|
11
|
+
The ``schedule_control`` is the scheduler function that release 2 schedule
|
12
12
|
functions; ``workflow_task``, and ``workflow_monitor``.
|
13
13
|
|
14
|
-
``
|
15
|
-
--- Every 5 minutes --> ``
|
14
|
+
``schedule_control`` --- Every minute at :02 --> ``schedule_task``
|
15
|
+
--- Every 5 minutes --> ``monitor``
|
16
16
|
|
17
|
-
The ``
|
17
|
+
The ``schedule_task`` will run ``task.release`` method in threading object
|
18
18
|
for multithreading strategy. This ``release`` method will run only one crontab
|
19
19
|
value with the on field.
|
20
20
|
"""
|
@@ -55,7 +55,7 @@ from .audit import Audit, get_audit
|
|
55
55
|
from .conf import Loader, config, get_logger
|
56
56
|
from .cron import On
|
57
57
|
from .exceptions import ScheduleException, WorkflowException
|
58
|
-
from .result import Result
|
58
|
+
from .result import Result, Status
|
59
59
|
from .utils import batch, delay
|
60
60
|
from .workflow import Release, ReleaseQueue, Workflow, WorkflowTask
|
61
61
|
|
@@ -314,25 +314,19 @@ class Schedule(BaseModel):
|
|
314
314
|
*,
|
315
315
|
stop: datetime | None = None,
|
316
316
|
externals: DictData | None = None,
|
317
|
-
|
318
|
-
|
317
|
+
audit: type[Audit] | None = None,
|
318
|
+
parent_run_id: str | None = None,
|
319
|
+
) -> Result: # pragma: no cov
|
319
320
|
"""Pending this schedule tasks with the schedule package.
|
320
321
|
|
321
322
|
:param stop: A datetime value that use to stop running schedule.
|
322
323
|
:param externals: An external parameters that pass to Loader.
|
323
|
-
:param
|
324
|
-
writing its release
|
324
|
+
:param audit: An audit class that use on the workflow task release for
|
325
|
+
writing its release audit context.
|
326
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
325
327
|
"""
|
326
|
-
|
327
|
-
|
328
|
-
except ImportError:
|
329
|
-
raise ImportError(
|
330
|
-
"Should install schedule package before use this method."
|
331
|
-
) from None
|
332
|
-
|
333
|
-
# NOTE: Get default logging.
|
334
|
-
log: type[Audit] = log or get_audit()
|
335
|
-
scheduler: Scheduler = Scheduler()
|
328
|
+
audit: type[Audit] = audit or get_audit()
|
329
|
+
result: Result = Result().set_parent_run_id(parent_run_id)
|
336
330
|
|
337
331
|
# NOTE: Create the start and stop datetime.
|
338
332
|
start_date: datetime = datetime.now(tz=config.tz)
|
@@ -346,66 +340,23 @@ class Schedule(BaseModel):
|
|
346
340
|
second=0, microsecond=0
|
347
341
|
) + timedelta(minutes=1)
|
348
342
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
stop=stop_date,
|
359
|
-
queue=queue,
|
360
|
-
threads=threads,
|
361
|
-
log=log,
|
362
|
-
)
|
363
|
-
.tag("control")
|
364
|
-
)
|
365
|
-
|
366
|
-
# NOTE: Checking zombie task with schedule job will start every 5 minute at
|
367
|
-
# :10 seconds.
|
368
|
-
(
|
369
|
-
scheduler.every(5)
|
370
|
-
.minutes.at(":10")
|
371
|
-
.do(
|
372
|
-
monitor,
|
373
|
-
threads=threads,
|
374
|
-
)
|
375
|
-
.tag("monitor")
|
376
|
-
)
|
377
|
-
|
378
|
-
# NOTE: Start running schedule
|
379
|
-
logger.info(
|
380
|
-
f"[SCHEDULE]: Schedule with stopper: {stop_date:%Y-%m-%d %H:%M:%S}"
|
343
|
+
scheduler_pending(
|
344
|
+
tasks=self.tasks(
|
345
|
+
start_date_waiting, queue=queue, externals=externals
|
346
|
+
),
|
347
|
+
stop_date=stop_date,
|
348
|
+
queue=queue,
|
349
|
+
threads=threads,
|
350
|
+
result=result,
|
351
|
+
audit=audit,
|
381
352
|
)
|
382
353
|
|
383
|
-
|
384
|
-
scheduler.run_pending()
|
385
|
-
time.sleep(1)
|
354
|
+
return result.catch(status=Status.SUCCESS)
|
386
355
|
|
387
|
-
# NOTE: Break the scheduler when the control job does not exist.
|
388
|
-
if not scheduler.get_jobs("control"):
|
389
|
-
scheduler.clear("monitor")
|
390
356
|
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
"running in background."
|
395
|
-
)
|
396
|
-
delay(10)
|
397
|
-
monitor(threads)
|
398
|
-
|
399
|
-
break
|
400
|
-
|
401
|
-
logger.warning(
|
402
|
-
f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
|
403
|
-
)
|
404
|
-
|
405
|
-
|
406
|
-
ResultOrCancelJob = Union[type[CancelJob], Result]
|
407
|
-
ReturnCancelJob = Callable[P, ResultOrCancelJob]
|
408
|
-
DecoratorCancelJob = Callable[[ReturnCancelJob], ReturnCancelJob]
|
357
|
+
ResultOrCancel = Union[type[CancelJob], Result]
|
358
|
+
ReturnResultOrCancel = Callable[P, ResultOrCancel]
|
359
|
+
DecoratorCancelJob = Callable[[ReturnResultOrCancel], ReturnResultOrCancel]
|
409
360
|
|
410
361
|
|
411
362
|
def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
@@ -418,10 +369,12 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
418
369
|
:rtype: DecoratorCancelJob
|
419
370
|
"""
|
420
371
|
|
421
|
-
def decorator(
|
372
|
+
def decorator(
|
373
|
+
func: ReturnResultOrCancel,
|
374
|
+
) -> ReturnResultOrCancel: # pragma: no cov
|
422
375
|
|
423
376
|
@wraps(func)
|
424
|
-
def wrapper(*args: P.args, **kwargs: P.kwargs) ->
|
377
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultOrCancel:
|
425
378
|
try:
|
426
379
|
return func(*args, **kwargs)
|
427
380
|
except Exception as err:
|
@@ -438,8 +391,9 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
438
391
|
class ReleaseThread(TypedDict):
|
439
392
|
"""TypeDict for the release thread."""
|
440
393
|
|
441
|
-
thread: Thread
|
394
|
+
thread: Optional[Thread]
|
442
395
|
start_date: datetime
|
396
|
+
release_date: datetime
|
443
397
|
|
444
398
|
|
445
399
|
ReleaseThreads = dict[str, ReleaseThread]
|
@@ -451,8 +405,9 @@ def schedule_task(
|
|
451
405
|
stop: datetime,
|
452
406
|
queue: dict[str, ReleaseQueue],
|
453
407
|
threads: ReleaseThreads,
|
454
|
-
|
455
|
-
|
408
|
+
audit: type[Audit],
|
409
|
+
parent_run_id: str | None = None,
|
410
|
+
) -> ResultOrCancel:
|
456
411
|
"""Schedule task function that generate thread of workflow task release
|
457
412
|
method in background. This function do the same logic as the workflow poke
|
458
413
|
method, but it runs with map of schedules and the on values.
|
@@ -464,10 +419,12 @@ def schedule_task(
|
|
464
419
|
:param stop: A stop datetime object that force stop running scheduler.
|
465
420
|
:param queue: A mapping of alias name and ReleaseQueue object.
|
466
421
|
:param threads: A mapping of alias name and Thread object.
|
467
|
-
:param
|
422
|
+
:param audit: An audit class that want to make audit object.
|
423
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
468
424
|
|
469
|
-
:rtype:
|
425
|
+
:rtype: ResultOrCancel
|
470
426
|
"""
|
427
|
+
result: Result = Result().set_parent_run_id(parent_run_id)
|
471
428
|
current_date: datetime = datetime.now(tz=config.tz)
|
472
429
|
if current_date > stop.replace(tzinfo=config.tz):
|
473
430
|
return CancelJob
|
@@ -490,14 +447,16 @@ def schedule_task(
|
|
490
447
|
q: ReleaseQueue = queue[task.alias]
|
491
448
|
|
492
449
|
# NOTE: Start adding queue and move the runner date in the WorkflowTask.
|
493
|
-
task.queue(stop, q,
|
450
|
+
task.queue(stop, q, audit=audit)
|
494
451
|
|
495
452
|
# NOTE: Get incoming datetime queue.
|
496
|
-
|
453
|
+
result.trace.debug(
|
454
|
+
f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}"
|
455
|
+
)
|
497
456
|
|
498
457
|
# VALIDATE: Check the queue is empty or not.
|
499
458
|
if not q.is_queued:
|
500
|
-
|
459
|
+
result.trace.warning(
|
501
460
|
f"[WORKFLOW]: Queue is empty for : {task.alias!r} : "
|
502
461
|
f"{task.runner.cron}"
|
503
462
|
)
|
@@ -508,7 +467,7 @@ def schedule_task(
|
|
508
467
|
second=0, microsecond=0
|
509
468
|
)
|
510
469
|
if (first_date := q.first_queue.date) > current_release:
|
511
|
-
|
470
|
+
result.trace.debug(
|
512
471
|
f"[WORKFLOW]: Skip schedule "
|
513
472
|
f"{first_date:%Y-%m-%d %H:%M:%S} for : {task.alias!r}"
|
514
473
|
)
|
@@ -523,7 +482,7 @@ def schedule_task(
|
|
523
482
|
release: Release = heappop(q.queue)
|
524
483
|
heappush(q.running, release)
|
525
484
|
|
526
|
-
|
485
|
+
result.trace.info(
|
527
486
|
f"[WORKFLOW]: Start thread: '{task.alias}|"
|
528
487
|
f"{release.date:%Y%m%d%H%M}'"
|
529
488
|
)
|
@@ -533,7 +492,7 @@ def schedule_task(
|
|
533
492
|
thread_name: str = f"{task.alias}|{release.date:%Y%m%d%H%M}"
|
534
493
|
thread: Thread = Thread(
|
535
494
|
target=catch_exceptions(cancel_on_failure=True)(task.release),
|
536
|
-
kwargs={"release": release, "queue": q, "
|
495
|
+
kwargs={"release": release, "queue": q, "audit": audit},
|
537
496
|
name=thread_name,
|
538
497
|
daemon=True,
|
539
498
|
)
|
@@ -541,13 +500,20 @@ def schedule_task(
|
|
541
500
|
threads[thread_name] = {
|
542
501
|
"thread": thread,
|
543
502
|
"start_date": datetime.now(tz=config.tz),
|
503
|
+
"release_date": release.date,
|
544
504
|
}
|
545
505
|
|
546
506
|
thread.start()
|
547
507
|
|
548
508
|
delay()
|
549
509
|
|
550
|
-
|
510
|
+
result.trace.debug(
|
511
|
+
f"[SCHEDULE]: End schedule task at {current_date:%Y-%m-%d %H:%M:%S} "
|
512
|
+
f"{'=' * 80}"
|
513
|
+
)
|
514
|
+
return result.catch(
|
515
|
+
status=Status.SUCCESS, context={"task_date": current_date}
|
516
|
+
)
|
551
517
|
|
552
518
|
|
553
519
|
def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
@@ -560,69 +526,44 @@ def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
|
560
526
|
logger.debug("[MONITOR]: Start checking long running schedule task.")
|
561
527
|
|
562
528
|
snapshot_threads: list[str] = list(threads.keys())
|
563
|
-
for
|
529
|
+
for thread_name in snapshot_threads:
|
564
530
|
|
565
|
-
thread_release: ReleaseThread = threads[
|
531
|
+
thread_release: ReleaseThread = threads[thread_name]
|
566
532
|
|
567
533
|
# NOTE: remove the thread that running success.
|
568
|
-
|
569
|
-
|
534
|
+
thread = thread_release["thread"]
|
535
|
+
if thread and (not thread_release["thread"].is_alive()):
|
536
|
+
thread_release["thread"] = None
|
570
537
|
|
571
538
|
|
572
|
-
def
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
the background.
|
539
|
+
def scheduler_pending(
|
540
|
+
tasks: list[WorkflowTask],
|
541
|
+
stop_date,
|
542
|
+
queue,
|
543
|
+
threads,
|
544
|
+
result: Result,
|
545
|
+
audit: type[Audit],
|
546
|
+
) -> Result: # pragma: no cov
|
547
|
+
"""
|
582
548
|
|
583
|
-
:param
|
584
|
-
:param
|
585
|
-
:param
|
586
|
-
:param
|
587
|
-
|
549
|
+
:param tasks:
|
550
|
+
:param stop_date:
|
551
|
+
:param queue:
|
552
|
+
:param threads:
|
553
|
+
:param result:
|
554
|
+
:param audit:
|
588
555
|
|
589
|
-
:rtype:
|
556
|
+
:rtype: Result
|
590
557
|
"""
|
591
|
-
# NOTE: Lazy import Scheduler object from the schedule package.
|
592
558
|
try:
|
593
559
|
from schedule import Scheduler
|
594
560
|
except ImportError:
|
595
561
|
raise ImportError(
|
596
|
-
"Should install schedule package before use this
|
562
|
+
"Should install schedule package before use this method."
|
597
563
|
) from None
|
598
564
|
|
599
|
-
# NOTE: Get default logging.
|
600
|
-
log: type[Audit] = log or get_audit()
|
601
565
|
scheduler: Scheduler = Scheduler()
|
602
566
|
|
603
|
-
# NOTE: Create the start and stop datetime.
|
604
|
-
start_date: datetime = datetime.now(tz=config.tz)
|
605
|
-
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
606
|
-
|
607
|
-
# IMPORTANT: Create main mapping of queue and thread object.
|
608
|
-
queue: dict[str, ReleaseQueue] = {}
|
609
|
-
threads: ReleaseThreads = {}
|
610
|
-
|
611
|
-
start_date_waiting: datetime = start_date.replace(
|
612
|
-
second=0, microsecond=0
|
613
|
-
) + timedelta(minutes=1)
|
614
|
-
|
615
|
-
tasks: list[WorkflowTask] = []
|
616
|
-
for name in schedules:
|
617
|
-
schedule: Schedule = Schedule.from_loader(name, externals=externals)
|
618
|
-
tasks.extend(
|
619
|
-
schedule.tasks(
|
620
|
-
start_date_waiting,
|
621
|
-
queue=queue,
|
622
|
-
externals=externals,
|
623
|
-
),
|
624
|
-
)
|
625
|
-
|
626
567
|
# NOTE: This schedule job will start every minute at :02 seconds.
|
627
568
|
(
|
628
569
|
scheduler.every(1)
|
@@ -633,7 +574,8 @@ def schedule_control(
|
|
633
574
|
stop=stop_date,
|
634
575
|
queue=queue,
|
635
576
|
threads=threads,
|
636
|
-
|
577
|
+
audit=audit,
|
578
|
+
parent_run_id=result.parent_run_id,
|
637
579
|
)
|
638
580
|
.tag("control")
|
639
581
|
)
|
@@ -651,9 +593,8 @@ def schedule_control(
|
|
651
593
|
)
|
652
594
|
|
653
595
|
# NOTE: Start running schedule
|
654
|
-
|
655
|
-
f"[SCHEDULE]: Schedule
|
656
|
-
f"{stop_date:%Y-%m-%d %H:%M:%S}"
|
596
|
+
result.trace.info(
|
597
|
+
f"[SCHEDULE]: Schedule with stopper: {stop_date:%Y-%m-%d %H:%M:%S}"
|
657
598
|
)
|
658
599
|
|
659
600
|
while True:
|
@@ -664,8 +605,8 @@ def schedule_control(
|
|
664
605
|
if not scheduler.get_jobs("control"):
|
665
606
|
scheduler.clear("monitor")
|
666
607
|
|
667
|
-
while len(threads) > 0:
|
668
|
-
|
608
|
+
while len([t for t in threads.values() if t["thread"]]) > 0:
|
609
|
+
result.trace.warning(
|
669
610
|
"[SCHEDULE]: Waiting schedule release thread that still "
|
670
611
|
"running in background."
|
671
612
|
)
|
@@ -674,17 +615,87 @@ def schedule_control(
|
|
674
615
|
|
675
616
|
break
|
676
617
|
|
677
|
-
|
618
|
+
result.trace.warning(
|
678
619
|
f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
|
679
620
|
)
|
680
|
-
return
|
621
|
+
return result.catch(
|
622
|
+
status=Status.SUCCESS,
|
623
|
+
context={
|
624
|
+
"threads": [
|
625
|
+
{
|
626
|
+
"name": thread,
|
627
|
+
"start_date": threads[thread]["start_date"],
|
628
|
+
"release_date": threads[thread]["release_date"],
|
629
|
+
}
|
630
|
+
for thread in threads
|
631
|
+
],
|
632
|
+
},
|
633
|
+
)
|
634
|
+
|
635
|
+
|
636
|
+
def schedule_control(
|
637
|
+
schedules: list[str],
|
638
|
+
stop: datetime | None = None,
|
639
|
+
externals: DictData | None = None,
|
640
|
+
*,
|
641
|
+
audit: type[Audit] | None = None,
|
642
|
+
parent_run_id: str | None = None,
|
643
|
+
) -> Result: # pragma: no cov
|
644
|
+
"""Scheduler control function that run the chuck of schedules every minute
|
645
|
+
and this function release monitoring thread for tracking undead thread in
|
646
|
+
the background.
|
647
|
+
|
648
|
+
:param schedules: A list of workflow names that want to schedule running.
|
649
|
+
:param stop: A datetime value that use to stop running schedule.
|
650
|
+
:param externals: An external parameters that pass to Loader.
|
651
|
+
:param audit: An audit class that use on the workflow task release for
|
652
|
+
writing its release audit context.
|
653
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
654
|
+
|
655
|
+
:rtype: Result
|
656
|
+
"""
|
657
|
+
audit: type[Audit] = audit or get_audit()
|
658
|
+
result: Result = Result().set_parent_run_id(parent_run_id)
|
659
|
+
|
660
|
+
# NOTE: Create the start and stop datetime.
|
661
|
+
start_date: datetime = datetime.now(tz=config.tz)
|
662
|
+
stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
|
663
|
+
|
664
|
+
# IMPORTANT: Create main mapping of queue and thread object.
|
665
|
+
queue: dict[str, ReleaseQueue] = {}
|
666
|
+
threads: ReleaseThreads = {}
|
667
|
+
|
668
|
+
start_date_waiting: datetime = start_date.replace(
|
669
|
+
second=0, microsecond=0
|
670
|
+
) + timedelta(minutes=1)
|
671
|
+
|
672
|
+
tasks: list[WorkflowTask] = []
|
673
|
+
for name in schedules:
|
674
|
+
tasks.extend(
|
675
|
+
Schedule.from_loader(name, externals=externals).tasks(
|
676
|
+
start_date_waiting,
|
677
|
+
queue=queue,
|
678
|
+
externals=externals,
|
679
|
+
),
|
680
|
+
)
|
681
|
+
|
682
|
+
scheduler_pending(
|
683
|
+
tasks=tasks,
|
684
|
+
stop_date=stop_date,
|
685
|
+
queue=queue,
|
686
|
+
threads=threads,
|
687
|
+
result=result,
|
688
|
+
audit=audit,
|
689
|
+
)
|
690
|
+
|
691
|
+
return result.catch(status=Status.SUCCESS, context={"schedules": schedules})
|
681
692
|
|
682
693
|
|
683
694
|
def schedule_runner(
|
684
695
|
stop: datetime | None = None,
|
685
696
|
externals: DictData | None = None,
|
686
697
|
excluded: list[str] | None = None,
|
687
|
-
) ->
|
698
|
+
) -> Result: # pragma: no cov
|
688
699
|
"""Schedule runner function it the multiprocess controller function for
|
689
700
|
split the setting schedule to the `schedule_control` function on the
|
690
701
|
process pool. It chunks schedule configs that exists in config
|
@@ -707,9 +718,10 @@ def schedule_runner(
|
|
707
718
|
--> thread of release task 02 02
|
708
719
|
==> process 02 ==> ...
|
709
720
|
|
710
|
-
:rtype:
|
721
|
+
:rtype: Result
|
711
722
|
"""
|
712
|
-
|
723
|
+
result: Result = Result()
|
724
|
+
context: DictData = {"schedules": [], "threads": []}
|
713
725
|
|
714
726
|
with ProcessPoolExecutor(
|
715
727
|
max_workers=config.max_schedule_process,
|
@@ -721,6 +733,7 @@ def schedule_runner(
|
|
721
733
|
schedules=[load[0] for load in loader],
|
722
734
|
stop=stop,
|
723
735
|
externals=(externals or {}),
|
736
|
+
parent_run_id=result.parent_run_id,
|
724
737
|
)
|
725
738
|
for loader in batch(
|
726
739
|
Loader.finds(Schedule, excluded=excluded),
|
@@ -735,6 +748,8 @@ def schedule_runner(
|
|
735
748
|
logger.error(str(err))
|
736
749
|
raise WorkflowException(str(err)) from err
|
737
750
|
|
738
|
-
|
751
|
+
rs: Result = future.result(timeout=1)
|
752
|
+
context["schedule"].extend(rs.context.get("schedules", []))
|
753
|
+
context["threads"].extend(rs.context.get("threads", []))
|
739
754
|
|
740
|
-
return
|
755
|
+
return result.catch(status=0, context=context)
|