orionis 0.523.0__py3-none-any.whl → 0.525.0__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.
- orionis/console/base/scheduler_event_listener.py +82 -53
- orionis/console/contracts/schedule_event_listener.py +58 -174
- orionis/console/entities/event_job.py +60 -0
- orionis/console/entities/scheduler_error.py +19 -0
- orionis/console/entities/scheduler_event_data.py +1 -6
- orionis/console/entities/scheduler_paused.py +2 -2
- orionis/console/entities/scheduler_resumed.py +2 -2
- orionis/console/entities/scheduler_shutdown.py +3 -3
- orionis/console/entities/scheduler_started.py +3 -2
- orionis/console/tasks/schedule.py +374 -244
- orionis/metadata/framework.py +1 -1
- {orionis-0.523.0.dist-info → orionis-0.525.0.dist-info}/METADATA +1 -1
- {orionis-0.523.0.dist-info → orionis-0.525.0.dist-info}/RECORD +17 -15
- {orionis-0.523.0.dist-info → orionis-0.525.0.dist-info}/WHEEL +0 -0
- {orionis-0.523.0.dist-info → orionis-0.525.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.523.0.dist-info → orionis-0.525.0.dist-info}/top_level.txt +0 -0
- {orionis-0.523.0.dist-info → orionis-0.525.0.dist-info}/zip-safe +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Dict, List, Optional, Union
|
|
4
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, Union
|
|
5
5
|
import pytz
|
|
6
6
|
from apscheduler.triggers.date import DateTrigger
|
|
7
7
|
from apscheduler.events import (
|
|
@@ -14,7 +14,6 @@ from apscheduler.events import (
|
|
|
14
14
|
EVENT_JOB_EXECUTED,
|
|
15
15
|
EVENT_JOB_MISSED,
|
|
16
16
|
EVENT_JOB_MAX_INSTANCES,
|
|
17
|
-
EVENT_JOB_MODIFIED,
|
|
18
17
|
EVENT_JOB_REMOVED
|
|
19
18
|
)
|
|
20
19
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
|
|
@@ -25,15 +24,8 @@ from orionis.console.contracts.event import IEvent
|
|
|
25
24
|
from orionis.console.contracts.reactor import IReactor
|
|
26
25
|
from orionis.console.contracts.schedule import ISchedule
|
|
27
26
|
from orionis.console.contracts.schedule_event_listener import IScheduleEventListener
|
|
28
|
-
from orionis.console.entities.
|
|
29
|
-
from orionis.console.entities.
|
|
30
|
-
from orionis.console.entities.job_max_instances import JobMaxInstances
|
|
31
|
-
from orionis.console.entities.job_missed import JobMissed
|
|
32
|
-
from orionis.console.entities.job_modified import JobModified
|
|
33
|
-
from orionis.console.entities.job_pause import JobPause
|
|
34
|
-
from orionis.console.entities.job_removed import JobRemoved
|
|
35
|
-
from orionis.console.entities.job_resume import JobResume
|
|
36
|
-
from orionis.console.entities.job_submitted import JobSubmitted
|
|
27
|
+
from orionis.console.entities.event_job import EventJob
|
|
28
|
+
from orionis.console.entities.scheduler_error import SchedulerError
|
|
37
29
|
from orionis.console.entities.scheduler_paused import SchedulerPaused
|
|
38
30
|
from orionis.console.entities.scheduler_resumed import SchedulerResumed
|
|
39
31
|
from orionis.console.entities.scheduler_shutdown import SchedulerShutdown
|
|
@@ -111,10 +103,10 @@ class Scheduler(ISchedule):
|
|
|
111
103
|
self.__listeners: Dict[str, callable] = {}
|
|
112
104
|
|
|
113
105
|
# Initialize set to track jobs paused by pauseEverythingAt
|
|
114
|
-
self.
|
|
106
|
+
self.__pausedByPauseEverything: set = set()
|
|
115
107
|
|
|
116
108
|
# Add this line to the existing __init__ method
|
|
117
|
-
self.
|
|
109
|
+
self._stopEvent: Optional[asyncio.Event] = None
|
|
118
110
|
|
|
119
111
|
def __getCurrentTime(
|
|
120
112
|
self
|
|
@@ -297,6 +289,113 @@ class Scheduler(ISchedule):
|
|
|
297
289
|
# Return the Event instance for further scheduling configuration
|
|
298
290
|
return self.__events[signature]
|
|
299
291
|
|
|
292
|
+
def __getTaskFromSchedulerById(
|
|
293
|
+
self,
|
|
294
|
+
id: str,
|
|
295
|
+
code: int = None
|
|
296
|
+
) -> Optional[EventJob]:
|
|
297
|
+
"""
|
|
298
|
+
Retrieve a scheduled job from the AsyncIOScheduler by its unique ID.
|
|
299
|
+
|
|
300
|
+
This method fetches a job from the AsyncIOScheduler using its unique identifier (ID).
|
|
301
|
+
It extracts the job's attributes and creates a `Job` entity containing the relevant
|
|
302
|
+
details. If the job does not exist, the method returns `None`.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
id : str
|
|
307
|
+
The unique identifier (ID) of the job to retrieve. This must be a non-empty string.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
Optional[Job]
|
|
312
|
+
A `Job` entity containing the details of the scheduled job if it exists.
|
|
313
|
+
If the job does not exist, returns `None`.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
# Extract event data from the internal events list if available
|
|
317
|
+
event_data: dict = {}
|
|
318
|
+
for job in self.events():
|
|
319
|
+
if id == job.get('signature'):
|
|
320
|
+
event_data = job
|
|
321
|
+
break
|
|
322
|
+
|
|
323
|
+
# Retrieve the job data from the scheduler using the provided job ID
|
|
324
|
+
data = self.__scheduler.get_job(id)
|
|
325
|
+
|
|
326
|
+
# If no job is found, return EventJob with default values
|
|
327
|
+
_id = data.id if data and hasattr(data, 'id') else None
|
|
328
|
+
if not _id and code == EVENT_JOB_MISSED:
|
|
329
|
+
_id = event_data.get('signature', None)
|
|
330
|
+
elif not _id:
|
|
331
|
+
return EventJob()
|
|
332
|
+
|
|
333
|
+
# Extract the job name if available
|
|
334
|
+
_name = data.name if data and hasattr(data, 'name') else None
|
|
335
|
+
|
|
336
|
+
# Extract the job function if available
|
|
337
|
+
_func = data.func if data and hasattr(data, 'func') else None
|
|
338
|
+
|
|
339
|
+
# Extract the job arguments if available
|
|
340
|
+
_args = data.args if data and hasattr(data, 'args') else tuple(event_data.get('args', []))
|
|
341
|
+
|
|
342
|
+
# Extract the job trigger if available
|
|
343
|
+
_trigger = data.trigger if data and hasattr(data, 'trigger') else None
|
|
344
|
+
|
|
345
|
+
# Extract the job executor if available
|
|
346
|
+
_executor = data.executor if data and hasattr(data, 'executor') else None
|
|
347
|
+
|
|
348
|
+
# Extract the job jobstore if available
|
|
349
|
+
_jobstore = data.jobstore if data and hasattr(data, 'jobstore') else None
|
|
350
|
+
|
|
351
|
+
# Extract the job misfire_grace_time if available
|
|
352
|
+
_misfire_grace_time = data.misfire_grace_time if data and hasattr(data, 'misfire_grace_time') else None
|
|
353
|
+
|
|
354
|
+
# Extract the job max_instances if available
|
|
355
|
+
_max_instances = data.max_instances if data and hasattr(data, 'max_instances') else 0
|
|
356
|
+
|
|
357
|
+
# Extract the job coalesce if available
|
|
358
|
+
_coalesce = data.coalesce if data and hasattr(data, 'coalesce') else False
|
|
359
|
+
|
|
360
|
+
# Extract the job next_run_time if available
|
|
361
|
+
_next_run_time = data.next_run_time if data and hasattr(data, 'next_run_time') else None
|
|
362
|
+
|
|
363
|
+
# Extract additional event data if available
|
|
364
|
+
_purpose = event_data.get('purpose', None)
|
|
365
|
+
|
|
366
|
+
# Extract additional event data if available
|
|
367
|
+
_start_date = event_data.get('start_date', None)
|
|
368
|
+
|
|
369
|
+
# Extract additional event data if available
|
|
370
|
+
_end_date = event_data.get('end_date', None)
|
|
371
|
+
|
|
372
|
+
# Extract additional event data if available
|
|
373
|
+
_details = event_data.get('details', None)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# Create and return a Job entity based on the retrieved job data
|
|
377
|
+
return EventJob(
|
|
378
|
+
id=_id,
|
|
379
|
+
code=code if code is not None else 0,
|
|
380
|
+
name=_name,
|
|
381
|
+
func=_func,
|
|
382
|
+
args=_args,
|
|
383
|
+
trigger=_trigger,
|
|
384
|
+
executor=_executor,
|
|
385
|
+
jobstore=_jobstore,
|
|
386
|
+
misfire_grace_time=_misfire_grace_time,
|
|
387
|
+
max_instances=_max_instances,
|
|
388
|
+
coalesce=_coalesce,
|
|
389
|
+
next_run_time=_next_run_time,
|
|
390
|
+
exception = None,
|
|
391
|
+
traceback = None,
|
|
392
|
+
retval = None,
|
|
393
|
+
purpose = _purpose,
|
|
394
|
+
start_date = _start_date,
|
|
395
|
+
end_date = _end_date,
|
|
396
|
+
details = _details
|
|
397
|
+
)
|
|
398
|
+
|
|
300
399
|
def __subscribeListeners(
|
|
301
400
|
self
|
|
302
401
|
) -> None:
|
|
@@ -325,12 +424,11 @@ class Scheduler(ISchedule):
|
|
|
325
424
|
self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
|
|
326
425
|
self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
|
|
327
426
|
self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
|
|
328
|
-
self.__scheduler.add_listener(self.__modifiedListener, EVENT_JOB_MODIFIED)
|
|
329
427
|
self.__scheduler.add_listener(self.__removedListener, EVENT_JOB_REMOVED)
|
|
330
428
|
|
|
331
429
|
def __globalCallableListener(
|
|
332
430
|
self,
|
|
333
|
-
event_data: Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown,
|
|
431
|
+
event_data: Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, SchedulerError]],
|
|
334
432
|
listening_vent: ListeningEvent
|
|
335
433
|
) -> None:
|
|
336
434
|
"""
|
|
@@ -342,7 +440,7 @@ class Scheduler(ISchedule):
|
|
|
342
440
|
|
|
343
441
|
Parameters
|
|
344
442
|
----------
|
|
345
|
-
event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown,
|
|
443
|
+
event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, ...]]
|
|
346
444
|
The event data associated with the global scheduler event. This can include details about the event,
|
|
347
445
|
such as its type and context. If no specific data is available, this parameter can be None.
|
|
348
446
|
listening_vent : ListeningEvent
|
|
@@ -370,46 +468,40 @@ class Scheduler(ISchedule):
|
|
|
370
468
|
# Check if a listener is registered for the specified event
|
|
371
469
|
if scheduler_event in self.__listeners:
|
|
372
470
|
|
|
373
|
-
#
|
|
471
|
+
# Get the listener for the specified event
|
|
374
472
|
listener = self.__listeners[scheduler_event]
|
|
375
473
|
|
|
376
|
-
#
|
|
474
|
+
# If is Callable
|
|
377
475
|
if callable(listener):
|
|
378
476
|
|
|
379
477
|
try:
|
|
380
478
|
|
|
381
|
-
# If the listener is a coroutine,
|
|
479
|
+
# If the listener is a coroutine, handle it properly
|
|
382
480
|
if asyncio.iscoroutinefunction(listener):
|
|
383
481
|
|
|
384
|
-
# Try to get the running event loop
|
|
385
482
|
try:
|
|
386
483
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
# Try to get the current running event loop
|
|
390
|
-
loop = asyncio.get_running_loop()
|
|
391
|
-
loop.create_task(listener(event_data, self))
|
|
392
|
-
|
|
393
|
-
except RuntimeError:
|
|
484
|
+
# Try to get the current running event loop
|
|
485
|
+
loop = asyncio.get_running_loop()
|
|
394
486
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
asyncio.set_event_loop(loop)
|
|
398
|
-
loop.create_task(listener(event_data, self))
|
|
487
|
+
# Schedule the coroutine as a task in the current loop
|
|
488
|
+
loop.create_task(listener(event_data, self))
|
|
399
489
|
|
|
400
|
-
# If no event loop is running, log a warning instead of creating one
|
|
401
490
|
except RuntimeError:
|
|
402
491
|
|
|
403
|
-
#
|
|
404
|
-
|
|
492
|
+
# No running event loop, create a new one
|
|
493
|
+
try:
|
|
405
494
|
|
|
406
|
-
|
|
407
|
-
|
|
495
|
+
# Create and run the coroutine in a new event loop
|
|
496
|
+
asyncio.run(listener(event_data, self))
|
|
408
497
|
|
|
409
|
-
|
|
410
|
-
|
|
498
|
+
except Exception as e:
|
|
499
|
+
|
|
500
|
+
# Handle exceptions during coroutine execution
|
|
501
|
+
error_msg = f"Failed to run async listener for '{scheduler_event}': {str(e)}"
|
|
502
|
+
self.__logger.error(error_msg)
|
|
503
|
+
raise CLIOrionisRuntimeError(error_msg) from e
|
|
411
504
|
|
|
412
|
-
# Otherwise, invoke the listener directly as a regular function
|
|
413
505
|
else:
|
|
414
506
|
|
|
415
507
|
# Call the regular listener function directly
|
|
@@ -421,18 +513,14 @@ class Scheduler(ISchedule):
|
|
|
421
513
|
if isinstance(e, CLIOrionisRuntimeError):
|
|
422
514
|
raise e
|
|
423
515
|
|
|
424
|
-
# Construct
|
|
516
|
+
# Construct and log error message
|
|
425
517
|
error_msg = f"An error occurred while invoking the listener for event '{scheduler_event}': {str(e)}"
|
|
426
|
-
|
|
427
|
-
# Log the error message
|
|
428
518
|
self.__logger.error(error_msg)
|
|
429
|
-
|
|
430
|
-
# Raise a runtime error if listener invocation fails
|
|
431
|
-
raise CLIOrionisRuntimeError(error_msg)
|
|
519
|
+
raise CLIOrionisRuntimeError(error_msg) from e
|
|
432
520
|
|
|
433
521
|
def __taskCallableListener(
|
|
434
522
|
self,
|
|
435
|
-
event_data:
|
|
523
|
+
event_data: EventJob,
|
|
436
524
|
listening_vent: ListeningEvent
|
|
437
525
|
) -> None:
|
|
438
526
|
"""
|
|
@@ -445,7 +533,7 @@ class Scheduler(ISchedule):
|
|
|
445
533
|
|
|
446
534
|
Parameters
|
|
447
535
|
----------
|
|
448
|
-
event_data :
|
|
536
|
+
event_data : EventJob
|
|
449
537
|
The event data associated with the task/job event. This includes details about the job,
|
|
450
538
|
such as its ID, exception (if any), and other context. If no specific data is available,
|
|
451
539
|
this parameter can be None.
|
|
@@ -466,23 +554,29 @@ class Scheduler(ISchedule):
|
|
|
466
554
|
|
|
467
555
|
# Validate that the provided event is an instance of ListeningEvent
|
|
468
556
|
if not isinstance(listening_vent, ListeningEvent):
|
|
469
|
-
|
|
557
|
+
error_msg = "The event must be an instance of ListeningEvent."
|
|
558
|
+
self.__logger.error(error_msg)
|
|
559
|
+
raise CLIOrionisValueError(error_msg)
|
|
470
560
|
|
|
471
|
-
# Validate that event_data is not None and has a
|
|
472
|
-
if event_data
|
|
561
|
+
# Validate that event_data is not None and has a id attribute
|
|
562
|
+
if not isinstance(event_data, EventJob) or not hasattr(event_data, 'id') or not event_data.id:
|
|
473
563
|
return
|
|
474
564
|
|
|
475
565
|
# Retrieve the global identifier for the event from the ListeningEvent enum
|
|
476
566
|
scheduler_event = listening_vent.value
|
|
477
567
|
|
|
478
568
|
# Check if a listener is registered for the specific job ID in the event data
|
|
479
|
-
if event_data.
|
|
569
|
+
if event_data.id in self.__listeners:
|
|
480
570
|
|
|
481
571
|
# Retrieve the listener for the specific job ID
|
|
482
|
-
listener = self.__listeners[event_data.
|
|
572
|
+
listener = self.__listeners[event_data.id]
|
|
483
573
|
|
|
484
574
|
# Check if the listener is an instance of IScheduleEventListener
|
|
485
|
-
if
|
|
575
|
+
if issubclass(listener, IScheduleEventListener):
|
|
576
|
+
|
|
577
|
+
# Initialize the listener if it's a class
|
|
578
|
+
if isinstance(listener, type):
|
|
579
|
+
listener = listener()
|
|
486
580
|
|
|
487
581
|
# Check if the listener has a method corresponding to the event type
|
|
488
582
|
if hasattr(listener, scheduler_event) and callable(getattr(listener, scheduler_event)):
|
|
@@ -490,44 +584,37 @@ class Scheduler(ISchedule):
|
|
|
490
584
|
# Retrieve the method from the listener
|
|
491
585
|
listener_method = getattr(listener, scheduler_event)
|
|
492
586
|
|
|
493
|
-
# Try to invoke the listener method
|
|
494
587
|
try:
|
|
495
588
|
|
|
496
|
-
#
|
|
589
|
+
# If the listener_method is a coroutine, handle it properly
|
|
497
590
|
if asyncio.iscoroutinefunction(listener_method):
|
|
498
591
|
|
|
499
|
-
# Try to get the running event loop
|
|
500
592
|
try:
|
|
501
593
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
# Try to get the current running event loop
|
|
505
|
-
loop = asyncio.get_running_loop()
|
|
506
|
-
loop.create_task(listener_method(event_data, self))
|
|
507
|
-
|
|
508
|
-
except RuntimeError:
|
|
594
|
+
# Try to get the current running event loop
|
|
595
|
+
loop = asyncio.get_running_loop()
|
|
509
596
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
asyncio.set_event_loop(loop)
|
|
513
|
-
loop.create_task(listener_method(event_data, self))
|
|
597
|
+
# Schedule the coroutine as a task in the current loop
|
|
598
|
+
loop.create_task(listener_method(event_data, self))
|
|
514
599
|
|
|
515
|
-
# If no event loop is running, log a warning
|
|
516
600
|
except RuntimeError:
|
|
517
601
|
|
|
518
|
-
#
|
|
519
|
-
|
|
602
|
+
# No running event loop, create a new one
|
|
603
|
+
try:
|
|
520
604
|
|
|
521
|
-
|
|
522
|
-
|
|
605
|
+
# Create and run the coroutine in a new event loop
|
|
606
|
+
asyncio.run(listener_method(event_data, self))
|
|
523
607
|
|
|
524
|
-
|
|
525
|
-
|
|
608
|
+
except Exception as e:
|
|
609
|
+
|
|
610
|
+
# Handle exceptions during coroutine execution
|
|
611
|
+
error_msg = f"Failed to run async listener_method for '{scheduler_event}': {str(e)}"
|
|
612
|
+
self.__logger.error(error_msg)
|
|
613
|
+
raise CLIOrionisRuntimeError(error_msg) from e
|
|
526
614
|
|
|
527
|
-
# Call the regular listener method directly
|
|
528
615
|
else:
|
|
529
616
|
|
|
530
|
-
#
|
|
617
|
+
# Call the regular listener_method function directly
|
|
531
618
|
listener_method(event_data, self)
|
|
532
619
|
|
|
533
620
|
except Exception as e:
|
|
@@ -536,18 +623,24 @@ class Scheduler(ISchedule):
|
|
|
536
623
|
if isinstance(e, CLIOrionisRuntimeError):
|
|
537
624
|
raise e
|
|
538
625
|
|
|
539
|
-
# Construct
|
|
540
|
-
error_msg =
|
|
541
|
-
|
|
542
|
-
# Log the error message
|
|
626
|
+
# Construct and log error message
|
|
627
|
+
error_msg = f"An error occurred while invoking the listener_method for event '{scheduler_event}': {str(e)}"
|
|
543
628
|
self.__logger.error(error_msg)
|
|
629
|
+
raise CLIOrionisRuntimeError(error_msg) from e
|
|
544
630
|
|
|
545
|
-
|
|
546
|
-
|
|
631
|
+
else:
|
|
632
|
+
|
|
633
|
+
# Log a warning if the listener does not have the required method
|
|
634
|
+
self.__logger.warning(f"The listener for job ID '{event_data.id}' does not have a method for event '{scheduler_event}'.")
|
|
635
|
+
|
|
636
|
+
else:
|
|
637
|
+
|
|
638
|
+
# Log a warning if the listener is not an instance of IScheduleEventListener
|
|
639
|
+
self.__logger.warning(f"The listener for job ID '{event_data.id}' is not an instance of IScheduleEventListener.")
|
|
547
640
|
|
|
548
641
|
def __startedListener(
|
|
549
642
|
self,
|
|
550
|
-
event
|
|
643
|
+
event
|
|
551
644
|
) -> None:
|
|
552
645
|
"""
|
|
553
646
|
Handle the scheduler started event for logging and invoking registered listeners.
|
|
@@ -595,82 +688,21 @@ class Scheduler(ISchedule):
|
|
|
595
688
|
self.__rich_console.line()
|
|
596
689
|
|
|
597
690
|
# Check if a listener is registered for the scheduler started event
|
|
598
|
-
|
|
691
|
+
event_data = SchedulerStarted(
|
|
692
|
+
code=event.code if hasattr(event, 'code') else 0,
|
|
693
|
+
time=now,
|
|
694
|
+
tasks=self.events()
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
# If a listener is registered for this event, invoke the listener with the event details
|
|
698
|
+
self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_STARTED)
|
|
599
699
|
|
|
600
700
|
# Log an informational message indicating that the scheduler has started
|
|
601
701
|
self.__logger.info(f"Orionis Scheduler started successfully at {now}.")
|
|
602
702
|
|
|
603
|
-
def __pausedListener(
|
|
604
|
-
self,
|
|
605
|
-
event: SchedulerPaused
|
|
606
|
-
) -> None:
|
|
607
|
-
"""
|
|
608
|
-
Handle the scheduler paused event for logging and invoking registered listeners.
|
|
609
|
-
|
|
610
|
-
This method is triggered when the scheduler is paused. It logs an informational
|
|
611
|
-
message indicating that the scheduler has been paused successfully and displays
|
|
612
|
-
a formatted message on the rich console. If a listener is registered for the
|
|
613
|
-
scheduler paused event, it invokes the listener with the event details.
|
|
614
|
-
|
|
615
|
-
Parameters
|
|
616
|
-
----------
|
|
617
|
-
event : SchedulerPaused
|
|
618
|
-
An event object containing details about the scheduler paused event.
|
|
619
|
-
|
|
620
|
-
Returns
|
|
621
|
-
-------
|
|
622
|
-
None
|
|
623
|
-
This method does not return any value. It performs logging, displays
|
|
624
|
-
a message on the console, and invokes any registered listener for the
|
|
625
|
-
scheduler paused event.
|
|
626
|
-
"""
|
|
627
|
-
|
|
628
|
-
# Get the current time in the configured timezone
|
|
629
|
-
now = self.__getCurrentTime()
|
|
630
|
-
|
|
631
|
-
# Check if a listener is registered for the scheduler paused event
|
|
632
|
-
self.__globalCallableListener(event, ListeningEvent.SCHEDULER_PAUSED)
|
|
633
|
-
|
|
634
|
-
# Log an informational message indicating that the scheduler has been paused
|
|
635
|
-
self.__logger.info(f"Orionis Scheduler paused successfully at {now}.")
|
|
636
|
-
|
|
637
|
-
def __resumedListener(
|
|
638
|
-
self,
|
|
639
|
-
event: SchedulerResumed
|
|
640
|
-
) -> None:
|
|
641
|
-
"""
|
|
642
|
-
Handle the scheduler resumed event for logging and invoking registered listeners.
|
|
643
|
-
|
|
644
|
-
This method is triggered when the scheduler resumes from a paused state. It logs an informational
|
|
645
|
-
message indicating that the scheduler has resumed successfully and displays a formatted message
|
|
646
|
-
on the rich console. If a listener is registered for the scheduler resumed event, it invokes
|
|
647
|
-
the listener with the event details.
|
|
648
|
-
|
|
649
|
-
Parameters
|
|
650
|
-
----------
|
|
651
|
-
event : SchedulerResumed
|
|
652
|
-
An event object containing details about the scheduler resumed event.
|
|
653
|
-
|
|
654
|
-
Returns
|
|
655
|
-
-------
|
|
656
|
-
None
|
|
657
|
-
This method does not return any value. It performs logging, displays
|
|
658
|
-
a message on the console, and invokes any registered listener for the
|
|
659
|
-
scheduler resumed event.
|
|
660
|
-
"""
|
|
661
|
-
|
|
662
|
-
# Get the current time in the configured timezone
|
|
663
|
-
now = self.__getCurrentTime()
|
|
664
|
-
|
|
665
|
-
# Check if a listener is registered for the scheduler resumed event
|
|
666
|
-
self.__globalCallableListener(event, ListeningEvent.SCHEDULER_RESUMED)
|
|
667
|
-
|
|
668
|
-
# Log an informational message indicating that the scheduler has resumed
|
|
669
|
-
self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
|
|
670
|
-
|
|
671
703
|
def __shutdownListener(
|
|
672
704
|
self,
|
|
673
|
-
event
|
|
705
|
+
event
|
|
674
706
|
) -> None:
|
|
675
707
|
"""
|
|
676
708
|
Handle the scheduler shutdown event for logging and invoking registered listeners.
|
|
@@ -697,14 +729,19 @@ class Scheduler(ISchedule):
|
|
|
697
729
|
now = self.__getCurrentTime()
|
|
698
730
|
|
|
699
731
|
# Check if a listener is registered for the scheduler shutdown event
|
|
700
|
-
|
|
732
|
+
event_data = SchedulerShutdown(
|
|
733
|
+
code=event.code if hasattr(event, 'code') else 0,
|
|
734
|
+
time=now,
|
|
735
|
+
tasks=self.events()
|
|
736
|
+
)
|
|
737
|
+
self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_SHUTDOWN)
|
|
701
738
|
|
|
702
739
|
# Log an informational message indicating that the scheduler has shut down
|
|
703
740
|
self.__logger.info(f"Orionis Scheduler shut down successfully at {now}.")
|
|
704
741
|
|
|
705
742
|
def __errorListener(
|
|
706
743
|
self,
|
|
707
|
-
event
|
|
744
|
+
event
|
|
708
745
|
) -> None:
|
|
709
746
|
"""
|
|
710
747
|
Handle job error events for logging and error reporting.
|
|
@@ -731,14 +768,31 @@ class Scheduler(ISchedule):
|
|
|
731
768
|
self.__logger.error(f"Task {event.job_id} raised an exception: {event.exception}")
|
|
732
769
|
|
|
733
770
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
734
|
-
self.
|
|
771
|
+
event_data = self.__getTaskFromSchedulerById(event.job_id)
|
|
772
|
+
event_data.code = event.code if hasattr(event, 'code') else 0
|
|
773
|
+
event_data.exception = event.exception if hasattr(event, 'exception') else None
|
|
774
|
+
event_data.traceback = event.traceback if hasattr(event, 'traceback') else None
|
|
775
|
+
|
|
776
|
+
# Call the task-specific listener for job errors
|
|
777
|
+
self.__taskCallableListener(
|
|
778
|
+
event_data,
|
|
779
|
+
ListeningEvent.JOB_ON_FAILURE
|
|
780
|
+
)
|
|
735
781
|
|
|
736
|
-
# Check if a listener is registered for the scheduler
|
|
737
|
-
|
|
782
|
+
# Check if a listener is registered for the scheduler error event
|
|
783
|
+
event_data = SchedulerError(
|
|
784
|
+
code=event.code if hasattr(event, 'code') else 0,
|
|
785
|
+
exception=event.exception if hasattr(event, 'exception') else None,
|
|
786
|
+
traceback=event.traceback if hasattr(event, 'traceback') else None,
|
|
787
|
+
)
|
|
788
|
+
self.__globalCallableListener(
|
|
789
|
+
event_data,
|
|
790
|
+
ListeningEvent.SCHEDULER_ERROR
|
|
791
|
+
)
|
|
738
792
|
|
|
739
793
|
def __submittedListener(
|
|
740
794
|
self,
|
|
741
|
-
event
|
|
795
|
+
event
|
|
742
796
|
) -> None:
|
|
743
797
|
"""
|
|
744
798
|
Handle job submission events for logging and error reporting.
|
|
@@ -764,12 +818,15 @@ class Scheduler(ISchedule):
|
|
|
764
818
|
# Log an informational message indicating that the job has been submitted
|
|
765
819
|
self.__logger.info(f"Task {event.job_id} submitted to executor.")
|
|
766
820
|
|
|
821
|
+
# Create entity for job submitted event
|
|
822
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
823
|
+
|
|
767
824
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
768
|
-
self.__taskCallableListener(
|
|
825
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_BEFORE)
|
|
769
826
|
|
|
770
827
|
def __executedListener(
|
|
771
828
|
self,
|
|
772
|
-
event
|
|
829
|
+
event
|
|
773
830
|
) -> None:
|
|
774
831
|
"""
|
|
775
832
|
Handle job execution events for logging and error reporting.
|
|
@@ -796,12 +853,15 @@ class Scheduler(ISchedule):
|
|
|
796
853
|
# Log an informational message indicating that the job has been executed
|
|
797
854
|
self.__logger.info(f"Task {event.job_id} executed.")
|
|
798
855
|
|
|
856
|
+
# Create entity for job executed event
|
|
857
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
858
|
+
|
|
799
859
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
800
|
-
self.__taskCallableListener(
|
|
860
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_AFTER)
|
|
801
861
|
|
|
802
862
|
def __missedListener(
|
|
803
863
|
self,
|
|
804
|
-
event
|
|
864
|
+
event
|
|
805
865
|
) -> None:
|
|
806
866
|
"""
|
|
807
867
|
Handle job missed events for debugging and error reporting.
|
|
@@ -828,12 +888,15 @@ class Scheduler(ISchedule):
|
|
|
828
888
|
# Log a warning indicating that the job was missed
|
|
829
889
|
self.__logger.warning(f"Task {event.job_id} was missed. It was scheduled to run at {event.scheduled_run_time}.")
|
|
830
890
|
|
|
891
|
+
# Create entity for job missed event
|
|
892
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
893
|
+
|
|
831
894
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
832
|
-
self.__taskCallableListener(
|
|
895
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MISSED)
|
|
833
896
|
|
|
834
897
|
def __maxInstancesListener(
|
|
835
898
|
self,
|
|
836
|
-
event
|
|
899
|
+
event
|
|
837
900
|
) -> None:
|
|
838
901
|
"""
|
|
839
902
|
Handle job max instances events for logging and error reporting.
|
|
@@ -860,46 +923,15 @@ class Scheduler(ISchedule):
|
|
|
860
923
|
# Log an error message indicating that the job exceeded maximum instances
|
|
861
924
|
self.__logger.error(f"Task {event.job_id} exceeded maximum instances")
|
|
862
925
|
|
|
863
|
-
#
|
|
864
|
-
self.
|
|
865
|
-
|
|
866
|
-
def __modifiedListener(
|
|
867
|
-
self,
|
|
868
|
-
event: JobModified
|
|
869
|
-
) -> None:
|
|
870
|
-
"""
|
|
871
|
-
Handle job modified events for logging and error reporting.
|
|
872
|
-
|
|
873
|
-
This method is triggered when a job is modified. It logs an informational
|
|
874
|
-
message indicating that the job has been modified successfully. If the application
|
|
875
|
-
is in debug mode, it also displays a message on the console. Additionally, if a
|
|
876
|
-
listener is registered for the modified job, it invokes the listener with the
|
|
877
|
-
event details.
|
|
878
|
-
|
|
879
|
-
Parameters
|
|
880
|
-
----------
|
|
881
|
-
event : JobModified
|
|
882
|
-
An instance of the JobModified event containing details about the modified job,
|
|
883
|
-
including its ID and other relevant information.
|
|
884
|
-
|
|
885
|
-
Returns
|
|
886
|
-
-------
|
|
887
|
-
None
|
|
888
|
-
This method does not return any value. It performs logging, error reporting,
|
|
889
|
-
and listener invocation for the job modified event.
|
|
890
|
-
"""
|
|
926
|
+
# Create entity for job max instances event
|
|
927
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
891
928
|
|
|
892
929
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
893
|
-
|
|
894
|
-
self.__logger.info(f"Task {event.job_id} has been paused.")
|
|
895
|
-
self.__taskCallableListener(event, ListeningEvent.JOB_ON_PAUSED)
|
|
896
|
-
else:
|
|
897
|
-
self.__logger.info(f"Task {event.job_id} has been resumed.")
|
|
898
|
-
self.__taskCallableListener(event, ListeningEvent.JOB_ON_RESUMED)
|
|
930
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MAXINSTANCES)
|
|
899
931
|
|
|
900
932
|
def __removedListener(
|
|
901
933
|
self,
|
|
902
|
-
event
|
|
934
|
+
event
|
|
903
935
|
) -> None:
|
|
904
936
|
"""
|
|
905
937
|
Handle job removal events for logging and invoking registered listeners.
|
|
@@ -925,8 +957,11 @@ class Scheduler(ISchedule):
|
|
|
925
957
|
# Log the removal of the job
|
|
926
958
|
self.__logger.info(f"Task {event.job_id} has been removed.")
|
|
927
959
|
|
|
960
|
+
# Create entity for job removed event
|
|
961
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
962
|
+
|
|
928
963
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
929
|
-
self.__taskCallableListener(
|
|
964
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_REMOVED)
|
|
930
965
|
|
|
931
966
|
def __loadEvents(
|
|
932
967
|
self
|
|
@@ -1038,6 +1073,62 @@ class Scheduler(ISchedule):
|
|
|
1038
1073
|
# Register the listener for the specified event in the internal listeners dictionary
|
|
1039
1074
|
self.__listeners[event] = listener
|
|
1040
1075
|
|
|
1076
|
+
def wrapAsyncFunction(
|
|
1077
|
+
self,
|
|
1078
|
+
func: Callable[..., Awaitable[Any]]
|
|
1079
|
+
) -> Callable[..., Any]:
|
|
1080
|
+
"""
|
|
1081
|
+
Wrap an asynchronous function to be called in a synchronous context.
|
|
1082
|
+
|
|
1083
|
+
This method takes an asynchronous function (a coroutine) and returns a synchronous
|
|
1084
|
+
wrapper function that can be called in a non-async context. The wrapper function
|
|
1085
|
+
ensures that the asynchronous function is executed within the appropriate event loop.
|
|
1086
|
+
|
|
1087
|
+
Parameters
|
|
1088
|
+
----------
|
|
1089
|
+
func : Callable[..., Awaitable[Any]]
|
|
1090
|
+
The asynchronous function (coroutine) to be wrapped. This function should be
|
|
1091
|
+
defined using the `async def` syntax and return an awaitable object.
|
|
1092
|
+
|
|
1093
|
+
Returns
|
|
1094
|
+
-------
|
|
1095
|
+
Callable[..., Any]
|
|
1096
|
+
A synchronous wrapper function that can be called in a non-async context.
|
|
1097
|
+
When invoked, this wrapper will execute the original asynchronous function
|
|
1098
|
+
within the appropriate event loop.
|
|
1099
|
+
|
|
1100
|
+
Raises
|
|
1101
|
+
------
|
|
1102
|
+
CLIOrionisRuntimeError
|
|
1103
|
+
If the provided `func` is not an asynchronous function or execution fails.
|
|
1104
|
+
"""
|
|
1105
|
+
|
|
1106
|
+
def sync_wrapper(*args, **kwargs) -> Any:
|
|
1107
|
+
|
|
1108
|
+
try:
|
|
1109
|
+
|
|
1110
|
+
# Try to get the current event loop
|
|
1111
|
+
try:
|
|
1112
|
+
|
|
1113
|
+
# If we're already in an event loop, create a task
|
|
1114
|
+
loop = asyncio.get_running_loop()
|
|
1115
|
+
task = loop.create_task(func(*args, **kwargs))
|
|
1116
|
+
return task
|
|
1117
|
+
|
|
1118
|
+
except RuntimeError:
|
|
1119
|
+
|
|
1120
|
+
# No running loop, so we can run the coroutine directly
|
|
1121
|
+
return asyncio.run(func(*args, **kwargs))
|
|
1122
|
+
|
|
1123
|
+
except Exception as e:
|
|
1124
|
+
|
|
1125
|
+
# Log the error and re-raise
|
|
1126
|
+
self.__logger.error(f"Error executing async function: {str(e)}")
|
|
1127
|
+
raise CLIOrionisRuntimeError(f"Failed to execute async function: {str(e)}") from e
|
|
1128
|
+
|
|
1129
|
+
# Return the synchronous wrapper function
|
|
1130
|
+
return sync_wrapper
|
|
1131
|
+
|
|
1041
1132
|
def pauseEverythingAt(
|
|
1042
1133
|
self,
|
|
1043
1134
|
at: datetime
|
|
@@ -1073,14 +1164,14 @@ class Scheduler(ISchedule):
|
|
|
1073
1164
|
if not isinstance(at, datetime):
|
|
1074
1165
|
raise ValueError("The 'at' parameter must be a datetime object.")
|
|
1075
1166
|
|
|
1076
|
-
# Define
|
|
1077
|
-
def schedule_pause():
|
|
1167
|
+
# Define an async function to pause the scheduler
|
|
1168
|
+
async def schedule_pause():
|
|
1078
1169
|
|
|
1079
1170
|
# Only pause jobs if the scheduler is currently running
|
|
1080
1171
|
if self.isRunning():
|
|
1081
1172
|
|
|
1082
1173
|
# Clear the set of previously paused jobs
|
|
1083
|
-
self.
|
|
1174
|
+
self.__pausedByPauseEverything.clear()
|
|
1084
1175
|
|
|
1085
1176
|
# Get all jobs from the scheduler
|
|
1086
1177
|
all_jobs = self.__scheduler.get_jobs()
|
|
@@ -1095,29 +1186,38 @@ class Scheduler(ISchedule):
|
|
|
1095
1186
|
# Pause only user jobs, not system jobs
|
|
1096
1187
|
for job in all_jobs:
|
|
1097
1188
|
|
|
1098
|
-
#
|
|
1189
|
+
# Check if the job is not a system job
|
|
1099
1190
|
if job.id not in system_job_ids:
|
|
1100
1191
|
|
|
1101
|
-
# Pause the job and add its ID to the set of paused jobs
|
|
1102
1192
|
try:
|
|
1103
1193
|
|
|
1104
1194
|
# Pause the job in the scheduler
|
|
1105
1195
|
self.__scheduler.pause_job(job.id)
|
|
1106
|
-
self.
|
|
1196
|
+
self.__pausedByPauseEverything.add(job.id)
|
|
1197
|
+
|
|
1198
|
+
# Get the current time in the configured timezone
|
|
1199
|
+
now = self.__getCurrentTime()
|
|
1107
1200
|
|
|
1108
|
-
#
|
|
1109
|
-
self.
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1201
|
+
# Log an informational message indicating that the job has been paused
|
|
1202
|
+
self.__taskCallableListener(
|
|
1203
|
+
self.__getTaskFromSchedulerById(job.id),
|
|
1204
|
+
ListeningEvent.JOB_ON_PAUSED
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
# Log the pause action
|
|
1208
|
+
self.__logger.info(f"Job '{job.id}' paused successfully at {now}.")
|
|
1115
1209
|
|
|
1116
1210
|
except Exception as e:
|
|
1117
1211
|
|
|
1118
1212
|
# Log a warning if pausing a job fails, but continue with others
|
|
1119
1213
|
self.__logger.warning(f"Failed to pause job '{job.id}': {str(e)}")
|
|
1120
1214
|
|
|
1215
|
+
# Execute the global callable listener after all jobs are paused
|
|
1216
|
+
self.__globalCallableListener(SchedulerPaused(
|
|
1217
|
+
code=EVENT_SCHEDULER_PAUSED,
|
|
1218
|
+
time=self.__getCurrentTime()
|
|
1219
|
+
), ListeningEvent.SCHEDULER_PAUSED)
|
|
1220
|
+
|
|
1121
1221
|
# Log that all user jobs have been paused
|
|
1122
1222
|
self.__logger.info("All user jobs have been paused. System jobs remain active.")
|
|
1123
1223
|
|
|
@@ -1133,7 +1233,7 @@ class Scheduler(ISchedule):
|
|
|
1133
1233
|
|
|
1134
1234
|
# Add a job to the scheduler to pause it at the specified datetime
|
|
1135
1235
|
self.__scheduler.add_job(
|
|
1136
|
-
func=schedule_pause,
|
|
1236
|
+
func=self.wrapAsyncFunction(schedule_pause), # Function to pause the scheduler
|
|
1137
1237
|
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1138
1238
|
id="scheduler_pause_at", # Unique job ID for pausing the scheduler
|
|
1139
1239
|
name="Pause Scheduler", # Descriptive name for the job
|
|
@@ -1183,29 +1283,31 @@ class Scheduler(ISchedule):
|
|
|
1183
1283
|
if not isinstance(at, datetime):
|
|
1184
1284
|
raise ValueError("The 'at' parameter must be a datetime object.")
|
|
1185
1285
|
|
|
1186
|
-
# Define
|
|
1187
|
-
def schedule_resume():
|
|
1286
|
+
# Define an async function to resume the scheduler
|
|
1287
|
+
async def schedule_resume():
|
|
1188
1288
|
|
|
1189
1289
|
# Only resume jobs if the scheduler is currently running
|
|
1190
1290
|
if self.isRunning():
|
|
1191
1291
|
|
|
1192
1292
|
# Resume only jobs that were paused by pauseEverythingAt
|
|
1193
|
-
if self.
|
|
1293
|
+
if self.__pausedByPauseEverything:
|
|
1194
1294
|
|
|
1195
1295
|
# Iterate through the set of paused job IDs and resume each one
|
|
1196
|
-
for job_id in list(self.
|
|
1296
|
+
for job_id in list(self.__pausedByPauseEverything):
|
|
1197
1297
|
|
|
1198
1298
|
try:
|
|
1199
1299
|
|
|
1200
1300
|
# Resume the job and log the action
|
|
1201
1301
|
self.__scheduler.resume_job(job_id)
|
|
1202
|
-
self.__logger.info(f"User job '{job_id}' has been resumed.")
|
|
1203
1302
|
|
|
1204
|
-
#
|
|
1205
|
-
self.
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
)
|
|
1303
|
+
# Invoke the listener for the resumed job
|
|
1304
|
+
self.__taskCallableListener(
|
|
1305
|
+
self.__getTaskFromSchedulerById(job_id),
|
|
1306
|
+
ListeningEvent.JOB_ON_RESUMED
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
# Log an informational message indicating that the job has been resumed
|
|
1310
|
+
self.__logger.info(f"User job '{job_id}' has been resumed.")
|
|
1209
1311
|
|
|
1210
1312
|
except Exception as e:
|
|
1211
1313
|
|
|
@@ -1213,7 +1315,19 @@ class Scheduler(ISchedule):
|
|
|
1213
1315
|
self.__logger.warning(f"Failed to resume job '{job_id}': {str(e)}")
|
|
1214
1316
|
|
|
1215
1317
|
# Clear the set after resuming all jobs
|
|
1216
|
-
self.
|
|
1318
|
+
self.__pausedByPauseEverything.clear()
|
|
1319
|
+
|
|
1320
|
+
# Execute the global callable listener after all jobs are resumed
|
|
1321
|
+
self.__globalCallableListener(SchedulerResumed(
|
|
1322
|
+
code=EVENT_SCHEDULER_RESUMED,
|
|
1323
|
+
time=self.__getCurrentTime()
|
|
1324
|
+
), ListeningEvent.SCHEDULER_RESUMED)
|
|
1325
|
+
|
|
1326
|
+
# Get the current time in the configured timezone
|
|
1327
|
+
now = self.__getCurrentTime()
|
|
1328
|
+
|
|
1329
|
+
# Log an informational message indicating that the scheduler has been resumed
|
|
1330
|
+
self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
|
|
1217
1331
|
|
|
1218
1332
|
# Log that all previously paused jobs have been resumed
|
|
1219
1333
|
self.__logger.info("All previously paused user jobs have been resumed.")
|
|
@@ -1230,7 +1344,7 @@ class Scheduler(ISchedule):
|
|
|
1230
1344
|
|
|
1231
1345
|
# Add a job to the scheduler to resume it at the specified datetime
|
|
1232
1346
|
self.__scheduler.add_job(
|
|
1233
|
-
func=schedule_resume,
|
|
1347
|
+
func=self.wrapAsyncFunction(schedule_resume), # Function to resume the scheduler
|
|
1234
1348
|
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1235
1349
|
id="scheduler_resume_at", # Unique job ID for resuming the scheduler
|
|
1236
1350
|
name="Resume Scheduler", # Descriptive name for the job
|
|
@@ -1289,18 +1403,26 @@ class Scheduler(ISchedule):
|
|
|
1289
1403
|
if not isinstance(wait, bool):
|
|
1290
1404
|
raise ValueError("The 'wait' parameter must be a boolean value.")
|
|
1291
1405
|
|
|
1292
|
-
# Define
|
|
1293
|
-
def schedule_shutdown():
|
|
1294
|
-
|
|
1406
|
+
# Define an async function to shut down the scheduler
|
|
1407
|
+
async def schedule_shutdown():
|
|
1295
1408
|
# Only shut down the scheduler if it is currently running
|
|
1296
1409
|
if self.isRunning():
|
|
1410
|
+
try:
|
|
1297
1411
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1412
|
+
# Log the shutdown initiation
|
|
1413
|
+
self.__logger.info("Initiating scheduled shutdown...")
|
|
1414
|
+
|
|
1415
|
+
# Call the async shutdown method
|
|
1416
|
+
await self.shutdown(wait=wait)
|
|
1417
|
+
|
|
1418
|
+
except Exception as e:
|
|
1419
|
+
|
|
1420
|
+
# Log any errors that occur during shutdown
|
|
1421
|
+
error_msg = f"Error during scheduled shutdown: {str(e)}"
|
|
1422
|
+
self.__logger.error(error_msg)
|
|
1300
1423
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
self._stop_event.set()
|
|
1424
|
+
# Force stop if graceful shutdown fails
|
|
1425
|
+
self.forceStop()
|
|
1304
1426
|
|
|
1305
1427
|
try:
|
|
1306
1428
|
|
|
@@ -1314,7 +1436,7 @@ class Scheduler(ISchedule):
|
|
|
1314
1436
|
|
|
1315
1437
|
# Add a job to the scheduler to shut it down at the specified datetime
|
|
1316
1438
|
self.__scheduler.add_job(
|
|
1317
|
-
func=schedule_shutdown,
|
|
1439
|
+
func=self.wrapAsyncFunction(schedule_shutdown), # Function to shut down the scheduler
|
|
1318
1440
|
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1319
1441
|
id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
|
|
1320
1442
|
name="Shutdown Scheduler", # Descriptive name for the job
|
|
@@ -1605,7 +1727,7 @@ class Scheduler(ISchedule):
|
|
|
1605
1727
|
# Return False if the job could not be removed (e.g., it does not exist)
|
|
1606
1728
|
return False
|
|
1607
1729
|
|
|
1608
|
-
def events(self) ->
|
|
1730
|
+
def events(self) -> List[Dict]:
|
|
1609
1731
|
"""
|
|
1610
1732
|
Retrieve all scheduled jobs currently managed by the Scheduler.
|
|
1611
1733
|
|
|
@@ -1832,16 +1954,24 @@ class Scheduler(ISchedule):
|
|
|
1832
1954
|
None
|
|
1833
1955
|
This method does not return any value. It signals the scheduler to stop.
|
|
1834
1956
|
"""
|
|
1835
|
-
|
|
1836
1957
|
# Check if the stop event exists and has not already been set
|
|
1837
1958
|
if self._stop_event and not self._stop_event.is_set():
|
|
1838
1959
|
|
|
1839
|
-
|
|
1840
|
-
|
|
1960
|
+
try:
|
|
1961
|
+
|
|
1962
|
+
# Try to get the current running event loop
|
|
1963
|
+
loop = asyncio.get_running_loop()
|
|
1841
1964
|
|
|
1842
|
-
|
|
1843
|
-
if loop.is_running():
|
|
1965
|
+
# If the event loop is running, set the stop event in a thread-safe manner
|
|
1844
1966
|
loop.call_soon_threadsafe(self._stop_event.set)
|
|
1845
|
-
|
|
1846
|
-
|
|
1967
|
+
|
|
1968
|
+
except RuntimeError:
|
|
1969
|
+
|
|
1970
|
+
# No running event loop, set the stop event directly
|
|
1971
|
+
self._stop_event.set()
|
|
1972
|
+
|
|
1973
|
+
except Exception as e:
|
|
1974
|
+
|
|
1975
|
+
# Log the error but still try to set the event
|
|
1976
|
+
self.__logger.warning(f"Error setting stop event through event loop: {str(e)}")
|
|
1847
1977
|
self._stop_event.set()
|