orionis 0.524.0__py3-none-any.whl → 0.526.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 +329 -182
- orionis/metadata/framework.py +1 -1
- orionis/services/introspection/dataclass/attributes.py +22 -7
- {orionis-0.524.0.dist-info → orionis-0.526.0.dist-info}/METADATA +1 -1
- {orionis-0.524.0.dist-info → orionis-0.526.0.dist-info}/RECORD +18 -16
- {orionis-0.524.0.dist-info → orionis-0.526.0.dist-info}/WHEEL +0 -0
- {orionis-0.524.0.dist-info → orionis-0.526.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.524.0.dist-info → orionis-0.526.0.dist-info}/top_level.txt +0 -0
- {orionis-0.524.0.dist-info → orionis-0.526.0.dist-info}/zip-safe +0 -0
|
@@ -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))
|
|
607
|
+
|
|
608
|
+
except Exception as e:
|
|
523
609
|
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
630
|
+
|
|
631
|
+
else:
|
|
544
632
|
|
|
545
|
-
|
|
546
|
-
|
|
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,14 +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
703
|
def __shutdownListener(
|
|
604
704
|
self,
|
|
605
|
-
event
|
|
705
|
+
event
|
|
606
706
|
) -> None:
|
|
607
707
|
"""
|
|
608
708
|
Handle the scheduler shutdown event for logging and invoking registered listeners.
|
|
@@ -629,14 +729,19 @@ class Scheduler(ISchedule):
|
|
|
629
729
|
now = self.__getCurrentTime()
|
|
630
730
|
|
|
631
731
|
# Check if a listener is registered for the scheduler shutdown event
|
|
632
|
-
|
|
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)
|
|
633
738
|
|
|
634
739
|
# Log an informational message indicating that the scheduler has shut down
|
|
635
740
|
self.__logger.info(f"Orionis Scheduler shut down successfully at {now}.")
|
|
636
741
|
|
|
637
742
|
def __errorListener(
|
|
638
743
|
self,
|
|
639
|
-
event
|
|
744
|
+
event
|
|
640
745
|
) -> None:
|
|
641
746
|
"""
|
|
642
747
|
Handle job error events for logging and error reporting.
|
|
@@ -663,14 +768,31 @@ class Scheduler(ISchedule):
|
|
|
663
768
|
self.__logger.error(f"Task {event.job_id} raised an exception: {event.exception}")
|
|
664
769
|
|
|
665
770
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
666
|
-
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
|
+
)
|
|
667
781
|
|
|
668
|
-
# Check if a listener is registered for the scheduler
|
|
669
|
-
|
|
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
|
+
)
|
|
670
792
|
|
|
671
793
|
def __submittedListener(
|
|
672
794
|
self,
|
|
673
|
-
event
|
|
795
|
+
event
|
|
674
796
|
) -> None:
|
|
675
797
|
"""
|
|
676
798
|
Handle job submission events for logging and error reporting.
|
|
@@ -696,12 +818,15 @@ class Scheduler(ISchedule):
|
|
|
696
818
|
# Log an informational message indicating that the job has been submitted
|
|
697
819
|
self.__logger.info(f"Task {event.job_id} submitted to executor.")
|
|
698
820
|
|
|
821
|
+
# Create entity for job submitted event
|
|
822
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
823
|
+
|
|
699
824
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
700
|
-
self.__taskCallableListener(
|
|
825
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_BEFORE)
|
|
701
826
|
|
|
702
827
|
def __executedListener(
|
|
703
828
|
self,
|
|
704
|
-
event
|
|
829
|
+
event
|
|
705
830
|
) -> None:
|
|
706
831
|
"""
|
|
707
832
|
Handle job execution events for logging and error reporting.
|
|
@@ -728,12 +853,15 @@ class Scheduler(ISchedule):
|
|
|
728
853
|
# Log an informational message indicating that the job has been executed
|
|
729
854
|
self.__logger.info(f"Task {event.job_id} executed.")
|
|
730
855
|
|
|
856
|
+
# Create entity for job executed event
|
|
857
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
858
|
+
|
|
731
859
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
732
|
-
self.__taskCallableListener(
|
|
860
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_AFTER)
|
|
733
861
|
|
|
734
862
|
def __missedListener(
|
|
735
863
|
self,
|
|
736
|
-
event
|
|
864
|
+
event
|
|
737
865
|
) -> None:
|
|
738
866
|
"""
|
|
739
867
|
Handle job missed events for debugging and error reporting.
|
|
@@ -760,12 +888,15 @@ class Scheduler(ISchedule):
|
|
|
760
888
|
# Log a warning indicating that the job was missed
|
|
761
889
|
self.__logger.warning(f"Task {event.job_id} was missed. It was scheduled to run at {event.scheduled_run_time}.")
|
|
762
890
|
|
|
891
|
+
# Create entity for job missed event
|
|
892
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
893
|
+
|
|
763
894
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
764
|
-
self.__taskCallableListener(
|
|
895
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MISSED)
|
|
765
896
|
|
|
766
897
|
def __maxInstancesListener(
|
|
767
898
|
self,
|
|
768
|
-
event
|
|
899
|
+
event
|
|
769
900
|
) -> None:
|
|
770
901
|
"""
|
|
771
902
|
Handle job max instances events for logging and error reporting.
|
|
@@ -792,46 +923,15 @@ class Scheduler(ISchedule):
|
|
|
792
923
|
# Log an error message indicating that the job exceeded maximum instances
|
|
793
924
|
self.__logger.error(f"Task {event.job_id} exceeded maximum instances")
|
|
794
925
|
|
|
795
|
-
#
|
|
796
|
-
self.
|
|
797
|
-
|
|
798
|
-
def __modifiedListener(
|
|
799
|
-
self,
|
|
800
|
-
event: JobModified
|
|
801
|
-
) -> None:
|
|
802
|
-
"""
|
|
803
|
-
Handle job modified events for logging and error reporting.
|
|
804
|
-
|
|
805
|
-
This method is triggered when a job is modified. It logs an informational
|
|
806
|
-
message indicating that the job has been modified successfully. If the application
|
|
807
|
-
is in debug mode, it also displays a message on the console. Additionally, if a
|
|
808
|
-
listener is registered for the modified job, it invokes the listener with the
|
|
809
|
-
event details.
|
|
810
|
-
|
|
811
|
-
Parameters
|
|
812
|
-
----------
|
|
813
|
-
event : JobModified
|
|
814
|
-
An instance of the JobModified event containing details about the modified job,
|
|
815
|
-
including its ID and other relevant information.
|
|
816
|
-
|
|
817
|
-
Returns
|
|
818
|
-
-------
|
|
819
|
-
None
|
|
820
|
-
This method does not return any value. It performs logging, error reporting,
|
|
821
|
-
and listener invocation for the job modified event.
|
|
822
|
-
"""
|
|
926
|
+
# Create entity for job max instances event
|
|
927
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
823
928
|
|
|
824
929
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
825
|
-
|
|
826
|
-
self.__logger.info(f"Task {event.job_id} has been paused.")
|
|
827
|
-
self.__taskCallableListener(event, ListeningEvent.JOB_ON_PAUSED)
|
|
828
|
-
else:
|
|
829
|
-
self.__logger.info(f"Task {event.job_id} has been resumed.")
|
|
830
|
-
self.__taskCallableListener(event, ListeningEvent.JOB_ON_RESUMED)
|
|
930
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MAXINSTANCES)
|
|
831
931
|
|
|
832
932
|
def __removedListener(
|
|
833
933
|
self,
|
|
834
|
-
event
|
|
934
|
+
event
|
|
835
935
|
) -> None:
|
|
836
936
|
"""
|
|
837
937
|
Handle job removal events for logging and invoking registered listeners.
|
|
@@ -857,8 +957,11 @@ class Scheduler(ISchedule):
|
|
|
857
957
|
# Log the removal of the job
|
|
858
958
|
self.__logger.info(f"Task {event.job_id} has been removed.")
|
|
859
959
|
|
|
960
|
+
# Create entity for job removed event
|
|
961
|
+
data_event = self.__getTaskFromSchedulerById(event.job_id, event.code)
|
|
962
|
+
|
|
860
963
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
861
|
-
self.__taskCallableListener(
|
|
964
|
+
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_REMOVED)
|
|
862
965
|
|
|
863
966
|
def __loadEvents(
|
|
864
967
|
self
|
|
@@ -996,25 +1099,34 @@ class Scheduler(ISchedule):
|
|
|
996
1099
|
|
|
997
1100
|
Raises
|
|
998
1101
|
------
|
|
999
|
-
|
|
1000
|
-
If the provided `func` is not an asynchronous function
|
|
1102
|
+
CLIOrionisRuntimeError
|
|
1103
|
+
If the provided `func` is not an asynchronous function or execution fails.
|
|
1001
1104
|
"""
|
|
1002
1105
|
|
|
1003
|
-
# Define a synchronous wrapper function
|
|
1004
1106
|
def sync_wrapper(*args, **kwargs) -> Any:
|
|
1107
|
+
|
|
1005
1108
|
try:
|
|
1109
|
+
|
|
1006
1110
|
# Try to get the current event loop
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
# If
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
return loop.run_until_complete(func(*args, **kwargs))
|
|
1014
|
-
except RuntimeError:
|
|
1015
|
-
# If no event loop exists, create a new one and run the coroutine
|
|
1016
|
-
return asyncio.run(func(*args, **kwargs))
|
|
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
|
|
1017
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
|
|
1018
1130
|
return sync_wrapper
|
|
1019
1131
|
|
|
1020
1132
|
def pauseEverythingAt(
|
|
@@ -1059,7 +1171,7 @@ class Scheduler(ISchedule):
|
|
|
1059
1171
|
if self.isRunning():
|
|
1060
1172
|
|
|
1061
1173
|
# Clear the set of previously paused jobs
|
|
1062
|
-
self.
|
|
1174
|
+
self.__pausedByPauseEverything.clear()
|
|
1063
1175
|
|
|
1064
1176
|
# Get all jobs from the scheduler
|
|
1065
1177
|
all_jobs = self.__scheduler.get_jobs()
|
|
@@ -1074,30 +1186,38 @@ class Scheduler(ISchedule):
|
|
|
1074
1186
|
# Pause only user jobs, not system jobs
|
|
1075
1187
|
for job in all_jobs:
|
|
1076
1188
|
|
|
1077
|
-
#
|
|
1189
|
+
# Check if the job is not a system job
|
|
1078
1190
|
if job.id not in system_job_ids:
|
|
1079
1191
|
|
|
1080
|
-
# Pause the job and add its ID to the set of paused jobs
|
|
1081
1192
|
try:
|
|
1082
1193
|
|
|
1083
1194
|
# Pause the job in the scheduler
|
|
1084
1195
|
self.__scheduler.pause_job(job.id)
|
|
1085
|
-
self.
|
|
1086
|
-
|
|
1087
|
-
# Execute the task callable listener
|
|
1088
|
-
self.__globalCallableListener(SchedulerPaused(EVENT_SCHEDULER_PAUSED), ListeningEvent.SCHEDULER_PAUSED)
|
|
1196
|
+
self.__pausedByPauseEverything.add(job.id)
|
|
1089
1197
|
|
|
1090
1198
|
# Get the current time in the configured timezone
|
|
1091
1199
|
now = self.__getCurrentTime()
|
|
1092
1200
|
|
|
1093
|
-
# Log an informational message indicating that the
|
|
1094
|
-
self.
|
|
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}.")
|
|
1095
1209
|
|
|
1096
1210
|
except Exception as e:
|
|
1097
1211
|
|
|
1098
1212
|
# Log a warning if pausing a job fails, but continue with others
|
|
1099
1213
|
self.__logger.warning(f"Failed to pause job '{job.id}': {str(e)}")
|
|
1100
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
|
+
|
|
1101
1221
|
# Log that all user jobs have been paused
|
|
1102
1222
|
self.__logger.info("All user jobs have been paused. System jobs remain active.")
|
|
1103
1223
|
|
|
@@ -1170,25 +1290,24 @@ class Scheduler(ISchedule):
|
|
|
1170
1290
|
if self.isRunning():
|
|
1171
1291
|
|
|
1172
1292
|
# Resume only jobs that were paused by pauseEverythingAt
|
|
1173
|
-
if self.
|
|
1293
|
+
if self.__pausedByPauseEverything:
|
|
1174
1294
|
|
|
1175
1295
|
# Iterate through the set of paused job IDs and resume each one
|
|
1176
|
-
for job_id in list(self.
|
|
1296
|
+
for job_id in list(self.__pausedByPauseEverything):
|
|
1177
1297
|
|
|
1178
1298
|
try:
|
|
1179
1299
|
|
|
1180
1300
|
# Resume the job and log the action
|
|
1181
1301
|
self.__scheduler.resume_job(job_id)
|
|
1182
|
-
self.__logger.info(f"User job '{job_id}' has been resumed.")
|
|
1183
1302
|
|
|
1184
|
-
#
|
|
1185
|
-
self.
|
|
1303
|
+
# Invoke the listener for the resumed job
|
|
1304
|
+
self.__taskCallableListener(
|
|
1305
|
+
self.__getTaskFromSchedulerById(job_id),
|
|
1306
|
+
ListeningEvent.JOB_ON_RESUMED
|
|
1307
|
+
)
|
|
1186
1308
|
|
|
1187
|
-
#
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
# Log an informational message indicating that the scheduler has been paused
|
|
1191
|
-
self.__logger.info(f"Orionis Scheduler paused successfully at {now}.")
|
|
1309
|
+
# Log an informational message indicating that the job has been resumed
|
|
1310
|
+
self.__logger.info(f"User job '{job_id}' has been resumed.")
|
|
1192
1311
|
|
|
1193
1312
|
except Exception as e:
|
|
1194
1313
|
|
|
@@ -1196,7 +1315,19 @@ class Scheduler(ISchedule):
|
|
|
1196
1315
|
self.__logger.warning(f"Failed to resume job '{job_id}': {str(e)}")
|
|
1197
1316
|
|
|
1198
1317
|
# Clear the set after resuming all jobs
|
|
1199
|
-
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}.")
|
|
1200
1331
|
|
|
1201
1332
|
# Log that all previously paused jobs have been resumed
|
|
1202
1333
|
self.__logger.info("All previously paused user jobs have been resumed.")
|
|
@@ -1272,18 +1403,26 @@ class Scheduler(ISchedule):
|
|
|
1272
1403
|
if not isinstance(wait, bool):
|
|
1273
1404
|
raise ValueError("The 'wait' parameter must be a boolean value.")
|
|
1274
1405
|
|
|
1275
|
-
# Define
|
|
1276
|
-
def schedule_shutdown():
|
|
1277
|
-
|
|
1406
|
+
# Define an async function to shut down the scheduler
|
|
1407
|
+
async def schedule_shutdown():
|
|
1278
1408
|
# Only shut down the scheduler if it is currently running
|
|
1279
1409
|
if self.isRunning():
|
|
1410
|
+
try:
|
|
1280
1411
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1412
|
+
# Log the shutdown initiation
|
|
1413
|
+
self.__logger.info("Initiating scheduled shutdown...")
|
|
1283
1414
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
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)
|
|
1423
|
+
|
|
1424
|
+
# Force stop if graceful shutdown fails
|
|
1425
|
+
self.forceStop()
|
|
1287
1426
|
|
|
1288
1427
|
try:
|
|
1289
1428
|
|
|
@@ -1297,7 +1436,7 @@ class Scheduler(ISchedule):
|
|
|
1297
1436
|
|
|
1298
1437
|
# Add a job to the scheduler to shut it down at the specified datetime
|
|
1299
1438
|
self.__scheduler.add_job(
|
|
1300
|
-
func=schedule_shutdown,
|
|
1439
|
+
func=self.wrapAsyncFunction(schedule_shutdown), # Function to shut down the scheduler
|
|
1301
1440
|
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1302
1441
|
id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
|
|
1303
1442
|
name="Shutdown Scheduler", # Descriptive name for the job
|
|
@@ -1588,7 +1727,7 @@ class Scheduler(ISchedule):
|
|
|
1588
1727
|
# Return False if the job could not be removed (e.g., it does not exist)
|
|
1589
1728
|
return False
|
|
1590
1729
|
|
|
1591
|
-
def events(self) ->
|
|
1730
|
+
def events(self) -> List[Dict]:
|
|
1592
1731
|
"""
|
|
1593
1732
|
Retrieve all scheduled jobs currently managed by the Scheduler.
|
|
1594
1733
|
|
|
@@ -1815,16 +1954,24 @@ class Scheduler(ISchedule):
|
|
|
1815
1954
|
None
|
|
1816
1955
|
This method does not return any value. It signals the scheduler to stop.
|
|
1817
1956
|
"""
|
|
1818
|
-
|
|
1819
1957
|
# Check if the stop event exists and has not already been set
|
|
1820
1958
|
if self._stop_event and not self._stop_event.is_set():
|
|
1821
1959
|
|
|
1822
|
-
|
|
1823
|
-
loop = asyncio.get_event_loop()
|
|
1960
|
+
try:
|
|
1824
1961
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1962
|
+
# Try to get the current running event loop
|
|
1963
|
+
loop = asyncio.get_running_loop()
|
|
1964
|
+
|
|
1965
|
+
# If the event loop is running, set the stop event in a thread-safe manner
|
|
1827
1966
|
loop.call_soon_threadsafe(self._stop_event.set)
|
|
1828
|
-
|
|
1829
|
-
|
|
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)}")
|
|
1830
1977
|
self._stop_event.set()
|