orionis 0.618.0__py3-none-any.whl → 0.620.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/commands/scheduler_work.py +31 -6
- orionis/console/contracts/command.py +18 -0
- orionis/console/contracts/schedule.py +2 -84
- orionis/console/core/reactor.py +12 -19
- orionis/console/entities/scheduler_error.py +11 -6
- orionis/console/entities/scheduler_event_data.py +6 -6
- orionis/console/entities/scheduler_paused.py +13 -5
- orionis/console/entities/scheduler_resumed.py +8 -7
- orionis/console/entities/scheduler_shutdown.py +11 -15
- orionis/console/entities/scheduler_started.py +11 -14
- orionis/console/fluent/command.py +2 -1
- orionis/console/tasks/schedule.py +853 -847
- orionis/metadata/framework.py +1 -1
- orionis/services/log/handlers/size_rotating.py +4 -1
- orionis/services/log/handlers/timed_rotating.py +4 -1
- {orionis-0.618.0.dist-info → orionis-0.620.0.dist-info}/METADATA +1 -1
- {orionis-0.618.0.dist-info → orionis-0.620.0.dist-info}/RECORD +20 -20
- {orionis-0.618.0.dist-info → orionis-0.620.0.dist-info}/WHEEL +0 -0
- {orionis-0.618.0.dist-info → orionis-0.620.0.dist-info}/licenses/LICENCE +0 -0
- {orionis-0.618.0.dist-info → orionis-0.620.0.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Dict, List, Optional, Union
|
|
5
5
|
import pytz
|
|
6
6
|
from apscheduler.events import (
|
|
7
7
|
EVENT_JOB_ERROR,
|
|
@@ -16,7 +16,6 @@ from apscheduler.events import (
|
|
|
16
16
|
EVENT_SCHEDULER_STARTED,
|
|
17
17
|
)
|
|
18
18
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler as APSAsyncIOScheduler
|
|
19
|
-
from apscheduler.triggers.date import DateTrigger
|
|
20
19
|
from rich.console import Console
|
|
21
20
|
from rich.panel import Panel
|
|
22
21
|
from rich.text import Text
|
|
@@ -48,96 +47,98 @@ class Schedule(ISchedule):
|
|
|
48
47
|
rich_console: Console
|
|
49
48
|
) -> None:
|
|
50
49
|
"""
|
|
51
|
-
Initialize a new instance of the
|
|
50
|
+
Initialize a new instance of the Schedule class.
|
|
52
51
|
|
|
53
52
|
This constructor sets up the internal state required for scheduling commands,
|
|
54
|
-
including references to the application instance, AsyncIOScheduler, the
|
|
53
|
+
including references to the application instance, the AsyncIOScheduler, the
|
|
55
54
|
command reactor, and job tracking structures. It also initializes properties
|
|
56
|
-
for managing the current scheduling context.
|
|
55
|
+
for managing the current scheduling context, logging, and event listeners.
|
|
57
56
|
|
|
58
57
|
Parameters
|
|
59
58
|
----------
|
|
60
59
|
reactor : IReactor
|
|
61
|
-
An instance
|
|
62
|
-
|
|
60
|
+
An instance implementing the IReactor interface, used to retrieve available
|
|
61
|
+
commands and execute scheduled jobs.
|
|
62
|
+
app : IApplication
|
|
63
|
+
The application container instance, used for configuration, dependency injection,
|
|
64
|
+
and service resolution.
|
|
65
|
+
rich_console : Console
|
|
66
|
+
An instance of Rich's Console for advanced output formatting.
|
|
63
67
|
|
|
64
68
|
Returns
|
|
65
69
|
-------
|
|
66
70
|
None
|
|
67
|
-
This method does not return any value. It initializes the
|
|
71
|
+
This method does not return any value. It initializes the Schedule instance
|
|
72
|
+
and prepares all required internal structures for scheduling and event handling.
|
|
68
73
|
"""
|
|
69
74
|
|
|
70
|
-
# Store the application instance for configuration access.
|
|
75
|
+
# Store the application instance for configuration and service access.
|
|
71
76
|
self.__app: IApplication = app
|
|
72
77
|
|
|
73
78
|
# Store the rich console instance for advanced output formatting.
|
|
74
79
|
self.__rich_console = rich_console
|
|
75
80
|
|
|
76
|
-
#
|
|
77
|
-
self.
|
|
78
|
-
timezone=pytz.timezone(self.__app.config('app.timezone', 'UTC'))
|
|
79
|
-
)
|
|
81
|
+
# Save the timezone configuration from the application settings.
|
|
82
|
+
self.__tz = pytz.timezone(self.__app.config('app.timezone') or 'UTC')
|
|
80
83
|
|
|
81
|
-
#
|
|
82
|
-
|
|
83
|
-
for name in ["apscheduler", "apscheduler.scheduler", "apscheduler.executors.default"]:
|
|
84
|
-
logger = logging.getLogger(name)
|
|
85
|
-
logger.handlers.clear()
|
|
86
|
-
logger.propagate = False
|
|
87
|
-
logger.disabled = True
|
|
84
|
+
# Initialize the AsyncIOScheduler with the configured timezone.
|
|
85
|
+
self.__scheduler = APSAsyncIOScheduler(timezone=self.__tz)
|
|
88
86
|
|
|
89
|
-
#
|
|
87
|
+
# Disable APScheduler's internal logging to avoid duplicate/conflicting logs.
|
|
88
|
+
self.__disableLogging()
|
|
89
|
+
|
|
90
|
+
# Initialize the logger using the application's dependency injection.
|
|
90
91
|
self.__logger: ILogger = self.__app.make(ILogger)
|
|
91
92
|
|
|
92
|
-
# Store the reactor instance for command management.
|
|
93
|
+
# Store the reactor instance for command management and job execution.
|
|
93
94
|
self.__reactor: IReactor = reactor
|
|
94
95
|
|
|
95
96
|
# Retrieve and store all available commands from the reactor.
|
|
96
97
|
self.__available_commands = self.__getCommands()
|
|
97
98
|
|
|
98
|
-
# Initialize the
|
|
99
|
+
# Initialize the dictionary to keep track of scheduled events.
|
|
99
100
|
self.__events: Dict[str, IEvent] = {}
|
|
100
101
|
|
|
101
|
-
# Initialize the
|
|
102
|
+
# Initialize the list to keep track of all scheduled job entities.
|
|
102
103
|
self.__jobs: List[EventEntity] = []
|
|
103
104
|
|
|
104
|
-
# Initialize the
|
|
105
|
+
# Initialize the dictionary to manage event listeners.
|
|
105
106
|
self.__listeners: Dict[str, callable] = {}
|
|
106
107
|
|
|
107
|
-
# Initialize set to track jobs paused by pauseEverythingAt
|
|
108
|
+
# Initialize a set to track jobs paused by pauseEverythingAt.
|
|
108
109
|
self.__pausedByPauseEverything: set = set()
|
|
109
110
|
|
|
110
|
-
#
|
|
111
|
+
# Initialize the asyncio event used to signal scheduler shutdown.
|
|
111
112
|
self._stopEvent: Optional[asyncio.Event] = None
|
|
112
113
|
|
|
113
|
-
# Retrieve and initialize the catch instance
|
|
114
|
+
# Retrieve and initialize the catch instance for exception handling.
|
|
114
115
|
self.__catch: ICatch = app.make(ICatch)
|
|
115
116
|
|
|
116
|
-
def
|
|
117
|
+
def __disableLogging(
|
|
117
118
|
self
|
|
118
|
-
) ->
|
|
119
|
+
) -> None:
|
|
119
120
|
"""
|
|
120
|
-
|
|
121
|
+
Disable APScheduler logging to prevent conflicts and duplicate log messages.
|
|
121
122
|
|
|
122
|
-
This method
|
|
123
|
-
|
|
123
|
+
This method disables logging for the APScheduler library and its key subcomponents.
|
|
124
|
+
It clears all handlers attached to the APScheduler loggers, disables log propagation,
|
|
125
|
+
and turns off the loggers entirely. This is useful in applications that have their
|
|
126
|
+
own logging configuration and want to avoid duplicate or unwanted log output from
|
|
127
|
+
APScheduler.
|
|
124
128
|
|
|
125
129
|
Returns
|
|
126
130
|
-------
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
None
|
|
132
|
+
This method does not return any value. It modifies the logging configuration
|
|
133
|
+
of APScheduler loggers in place.
|
|
130
134
|
"""
|
|
131
135
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# Format the current time as a string
|
|
140
|
-
return now.strftime("%Y-%m-%d %H:%M:%S")
|
|
136
|
+
# List of APScheduler logger names to disable
|
|
137
|
+
for name in ["apscheduler", "apscheduler.scheduler", "apscheduler.executors.default"]:
|
|
138
|
+
logger = logging.getLogger(name)
|
|
139
|
+
logger.handlers.clear() # Remove all handlers to prevent output
|
|
140
|
+
logger.propagate = False # Prevent log messages from propagating to ancestor loggers
|
|
141
|
+
logger.disabled = True # Disable the logger entirely
|
|
141
142
|
|
|
142
143
|
def __getCommands(
|
|
143
144
|
self
|
|
@@ -163,24 +164,124 @@ class Schedule(ISchedule):
|
|
|
163
164
|
# Iterate over all jobs provided by the reactor's info method
|
|
164
165
|
for job in self.__reactor.info():
|
|
165
166
|
|
|
167
|
+
signature: str = job.get('signature', None)
|
|
168
|
+
description: str = job.get('description', 'No description available.')
|
|
169
|
+
|
|
170
|
+
# Skip invalid or special method signatures
|
|
171
|
+
if not signature or (signature.startswith('__') and signature.endswith('__')):
|
|
172
|
+
continue
|
|
173
|
+
|
|
166
174
|
# Store each job's signature and description in the commands dictionary
|
|
167
|
-
commands[
|
|
168
|
-
'signature':
|
|
169
|
-
'description':
|
|
175
|
+
commands[signature] = {
|
|
176
|
+
'signature': signature,
|
|
177
|
+
'description': description
|
|
170
178
|
}
|
|
171
179
|
|
|
172
180
|
# Return the commands dictionary
|
|
173
181
|
return commands
|
|
174
182
|
|
|
183
|
+
def __getCurrentTime(
|
|
184
|
+
self
|
|
185
|
+
) -> str:
|
|
186
|
+
"""
|
|
187
|
+
Retrieve the current date and time as a formatted string in the configured timezone.
|
|
188
|
+
|
|
189
|
+
This method obtains the current date and time using the timezone specified in the application's
|
|
190
|
+
configuration (defaulting to UTC if not set). The result is formatted as a string in the
|
|
191
|
+
"YYYY-MM-DD HH:MM:SS" format, which is suitable for logging, display, or timestamping events.
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
str
|
|
196
|
+
The current date and time as a string in the format "YYYY-MM-DD HH:MM:SS", localized to the
|
|
197
|
+
scheduler's configured timezone.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
# Get the current time in the scheduler's configured timezone
|
|
201
|
+
now = datetime.now(self.__tz)
|
|
202
|
+
|
|
203
|
+
# Log the timezone currently assigned to the scheduler for traceability
|
|
204
|
+
self.__logger.info(
|
|
205
|
+
f"Timezone assigned to the scheduler: {self.__app.config('app.timezone') or 'UTC'}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Return the formatted current time string
|
|
209
|
+
return now.strftime("%Y-%m-%d %H:%M:%S")
|
|
210
|
+
|
|
211
|
+
def __getNow(
|
|
212
|
+
self
|
|
213
|
+
) -> datetime:
|
|
214
|
+
"""
|
|
215
|
+
Retrieve the current date and time as a timezone-aware datetime object.
|
|
216
|
+
|
|
217
|
+
This method returns the current date and time, localized to the timezone configured
|
|
218
|
+
for the scheduler instance. The timezone is determined by the application's configuration
|
|
219
|
+
(typically set during initialization). This is useful for ensuring that all time-related
|
|
220
|
+
operations within the scheduler are consistent and respect the application's timezone settings.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
datetime
|
|
225
|
+
A timezone-aware `datetime` object representing the current date and time
|
|
226
|
+
in the scheduler's configured timezone.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
# Return the current datetime localized to the scheduler's timezone
|
|
230
|
+
return datetime.now(self.__tz)
|
|
231
|
+
|
|
232
|
+
def __getAttribute(
|
|
233
|
+
self,
|
|
234
|
+
obj: Any,
|
|
235
|
+
attr: str,
|
|
236
|
+
default: Any = None
|
|
237
|
+
) -> Any:
|
|
238
|
+
"""
|
|
239
|
+
Safely retrieve an attribute from an object, returning a default value if the attribute does not exist.
|
|
240
|
+
|
|
241
|
+
This method attempts to access the specified attribute of the given object. If the attribute
|
|
242
|
+
exists, its value is returned. If the attribute does not exist, the provided default value
|
|
243
|
+
is returned instead. This prevents `AttributeError` exceptions when accessing attributes
|
|
244
|
+
that may not be present on the object.
|
|
245
|
+
|
|
246
|
+
Parameters
|
|
247
|
+
----------
|
|
248
|
+
obj : Any
|
|
249
|
+
The object from which to retrieve the attribute.
|
|
250
|
+
attr : str
|
|
251
|
+
The name of the attribute to retrieve.
|
|
252
|
+
default : Any, optional
|
|
253
|
+
The value to return if the attribute does not exist. Defaults to None.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
Any
|
|
258
|
+
The value of the specified attribute if it exists on the object; otherwise, the value
|
|
259
|
+
of `default`.
|
|
260
|
+
|
|
261
|
+
Notes
|
|
262
|
+
-----
|
|
263
|
+
This method is a wrapper around Python's built-in `getattr()` function, providing a safe
|
|
264
|
+
way to access attributes that may or may not exist on an object.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
# If the object is None, return the default value immediately
|
|
268
|
+
if obj is None:
|
|
269
|
+
return default
|
|
270
|
+
|
|
271
|
+
# Use Python's built-in getattr to safely retrieve the attribute,
|
|
272
|
+
# returning the default value if the attribute is not found.
|
|
273
|
+
return getattr(obj, attr, default)
|
|
274
|
+
|
|
175
275
|
def __isAvailable(
|
|
176
276
|
self,
|
|
177
277
|
signature: str
|
|
178
278
|
) -> bool:
|
|
179
279
|
"""
|
|
180
|
-
Check if a command with the given signature is available.
|
|
280
|
+
Check if a command with the given signature is available among registered commands.
|
|
181
281
|
|
|
182
|
-
This method
|
|
183
|
-
|
|
282
|
+
This method determines whether the provided command signature exists in the internal
|
|
283
|
+
dictionary of available commands. It is used to validate if a command can be scheduled
|
|
284
|
+
or executed by the scheduler.
|
|
184
285
|
|
|
185
286
|
Parameters
|
|
186
287
|
----------
|
|
@@ -190,46 +291,51 @@ class Schedule(ISchedule):
|
|
|
190
291
|
Returns
|
|
191
292
|
-------
|
|
192
293
|
bool
|
|
193
|
-
True if the command with the specified signature exists
|
|
194
|
-
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
# Iterate through all available command signatures
|
|
198
|
-
for command in self.__available_commands.keys():
|
|
294
|
+
Returns True if the command with the specified signature exists in the available
|
|
295
|
+
commands dictionary; otherwise, returns False.
|
|
199
296
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
297
|
+
Notes
|
|
298
|
+
-----
|
|
299
|
+
The method performs a simple membership check in the internal commands dictionary.
|
|
300
|
+
It does not validate the format or correctness of the signature itself, only its
|
|
301
|
+
presence among the registered commands.
|
|
302
|
+
"""
|
|
203
303
|
|
|
204
|
-
#
|
|
205
|
-
return
|
|
304
|
+
# Check if the signature exists in the available commands dictionary
|
|
305
|
+
return signature in self.__available_commands
|
|
206
306
|
|
|
207
307
|
def __getDescription(
|
|
208
308
|
self,
|
|
209
309
|
signature: str
|
|
210
310
|
) -> Optional[str]:
|
|
211
311
|
"""
|
|
212
|
-
Retrieve the description
|
|
312
|
+
Retrieve the description for a command based on its signature.
|
|
213
313
|
|
|
214
|
-
This method
|
|
215
|
-
|
|
216
|
-
|
|
314
|
+
This method searches the internal dictionary of available commands for the provided
|
|
315
|
+
command signature and returns the corresponding description if found. If the signature
|
|
316
|
+
does not exist in the available commands, the method returns None.
|
|
217
317
|
|
|
218
318
|
Parameters
|
|
219
319
|
----------
|
|
220
320
|
signature : str
|
|
221
|
-
The unique signature identifying the command.
|
|
321
|
+
The unique signature identifying the command whose description is to be retrieved.
|
|
222
322
|
|
|
223
323
|
Returns
|
|
224
324
|
-------
|
|
225
325
|
Optional[str]
|
|
226
|
-
The description
|
|
326
|
+
The description string associated with the command signature if it exists;
|
|
327
|
+
otherwise, returns None.
|
|
328
|
+
|
|
329
|
+
Notes
|
|
330
|
+
-----
|
|
331
|
+
This method is useful for displaying human-readable information about commands
|
|
332
|
+
when scheduling or listing tasks.
|
|
227
333
|
"""
|
|
228
334
|
|
|
229
335
|
# Attempt to retrieve the command entry from the available commands dictionary
|
|
230
336
|
command_entry = self.__available_commands.get(signature)
|
|
231
337
|
|
|
232
|
-
#
|
|
338
|
+
# If the command entry exists, return its description; otherwise, return None
|
|
233
339
|
return command_entry['description'] if command_entry else None
|
|
234
340
|
|
|
235
341
|
def command(
|
|
@@ -238,13 +344,12 @@ class Schedule(ISchedule):
|
|
|
238
344
|
args: Optional[List[str]] = None
|
|
239
345
|
) -> 'IEvent':
|
|
240
346
|
"""
|
|
241
|
-
Prepare an Event instance for a given command signature and its arguments.
|
|
347
|
+
Prepare and register an Event instance for a given command signature and its arguments.
|
|
242
348
|
|
|
243
|
-
This method validates the provided command signature and arguments, ensuring
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
Event
|
|
247
|
-
arguments, and description.
|
|
349
|
+
This method validates the provided command signature and arguments, ensuring that the command
|
|
350
|
+
exists among the registered commands and that the arguments are in the correct format. If validation
|
|
351
|
+
passes, it creates and registers an Event object representing the scheduled command, including its
|
|
352
|
+
signature, arguments, and description. The Event instance is stored internally for later scheduling.
|
|
248
353
|
|
|
249
354
|
Parameters
|
|
250
355
|
----------
|
|
@@ -255,36 +360,53 @@ class Schedule(ISchedule):
|
|
|
255
360
|
|
|
256
361
|
Returns
|
|
257
362
|
-------
|
|
258
|
-
|
|
259
|
-
|
|
363
|
+
IEvent
|
|
364
|
+
The Event instance containing the command signature, arguments, and its description.
|
|
365
|
+
This instance is also stored internally for further scheduling configuration.
|
|
260
366
|
|
|
261
367
|
Raises
|
|
262
368
|
------
|
|
263
|
-
|
|
369
|
+
CLIOrionisValueError
|
|
264
370
|
If the command signature is not a non-empty string, if the arguments are not a list
|
|
265
371
|
of strings or None, or if the command does not exist among the registered commands.
|
|
266
372
|
"""
|
|
267
373
|
|
|
268
374
|
# Prevent adding new commands while the scheduler is running
|
|
269
375
|
if self.isRunning():
|
|
270
|
-
self.__raiseException(
|
|
376
|
+
self.__raiseException(
|
|
377
|
+
CLIOrionisValueError(
|
|
378
|
+
"Cannot add new commands while the scheduler is running. Please stop the scheduler before adding new commands."
|
|
379
|
+
)
|
|
380
|
+
)
|
|
271
381
|
|
|
272
382
|
# Validate that the command signature is a non-empty string
|
|
273
383
|
if not isinstance(signature, str) or not signature.strip():
|
|
274
|
-
raise CLIOrionisValueError(
|
|
384
|
+
raise CLIOrionisValueError(
|
|
385
|
+
"The command signature must be a non-empty string. Please provide a valid command signature."
|
|
386
|
+
)
|
|
275
387
|
|
|
276
388
|
# Ensure that arguments are either a list of strings or None
|
|
277
|
-
if args is not None
|
|
278
|
-
|
|
389
|
+
if args is not None:
|
|
390
|
+
if not isinstance(args, list):
|
|
391
|
+
raise CLIOrionisValueError(
|
|
392
|
+
"Arguments must be provided as a list of strings or None. Please check your arguments."
|
|
393
|
+
)
|
|
394
|
+
for arg in args:
|
|
395
|
+
if not isinstance(arg, str):
|
|
396
|
+
raise CLIOrionisValueError(
|
|
397
|
+
f"Invalid argument '{arg}'. Each argument must be a string."
|
|
398
|
+
)
|
|
279
399
|
|
|
280
400
|
# Check if the command is available in the registered commands
|
|
281
401
|
if not self.__isAvailable(signature):
|
|
282
|
-
raise CLIOrionisValueError(
|
|
402
|
+
raise CLIOrionisValueError(
|
|
403
|
+
f"The command '{signature}' is not available or does not exist. Please check the command signature."
|
|
404
|
+
)
|
|
283
405
|
|
|
284
406
|
# Import Event here to avoid circular dependency issues
|
|
285
407
|
from orionis.console.fluent.event import Event
|
|
286
408
|
|
|
287
|
-
# Store the command and its arguments for scheduling
|
|
409
|
+
# Store the command and its arguments for scheduling in the internal events dictionary
|
|
288
410
|
self.__events[signature] = Event(
|
|
289
411
|
signature=signature,
|
|
290
412
|
args=args or [],
|
|
@@ -319,51 +441,50 @@ class Schedule(ISchedule):
|
|
|
319
441
|
"""
|
|
320
442
|
|
|
321
443
|
# Extract event data from the internal events list if available
|
|
322
|
-
event_data: dict =
|
|
323
|
-
for job in self.events():
|
|
324
|
-
if id == job.get('signature'):
|
|
325
|
-
event_data = job
|
|
326
|
-
break
|
|
444
|
+
event_data: dict = self.event(id)
|
|
327
445
|
|
|
328
446
|
# Retrieve the job data from the scheduler using the provided job ID
|
|
329
447
|
data = self.__scheduler.get_job(id)
|
|
330
448
|
|
|
331
449
|
# If no job is found, return EventJob with default values
|
|
332
|
-
_id =
|
|
450
|
+
_id = self.__getAttribute(data, 'id', None)
|
|
333
451
|
if not _id and code in (EVENT_JOB_MISSED, EVENT_JOB_REMOVED):
|
|
334
|
-
_id = event_data.get('signature'
|
|
452
|
+
_id = event_data.get('signature')
|
|
335
453
|
elif not _id:
|
|
336
454
|
return EventJob()
|
|
337
455
|
|
|
456
|
+
# Extract the job code if available
|
|
457
|
+
_code = code if code is not None else 0
|
|
458
|
+
|
|
338
459
|
# Extract the job name if available
|
|
339
|
-
_name =
|
|
460
|
+
_name = self.__getAttribute(data, 'name', None)
|
|
340
461
|
|
|
341
462
|
# Extract the job function if available
|
|
342
|
-
_func =
|
|
463
|
+
_func = self.__getAttribute(data, 'func', None)
|
|
343
464
|
|
|
344
465
|
# Extract the job arguments if available
|
|
345
|
-
_args =
|
|
466
|
+
_args = self.__getAttribute(data, 'args', tuple(event_data.get('args', [])))
|
|
346
467
|
|
|
347
468
|
# Extract the job trigger if available
|
|
348
|
-
_trigger =
|
|
469
|
+
_trigger = self.__getAttribute(data, 'trigger', None)
|
|
349
470
|
|
|
350
471
|
# Extract the job executor if available
|
|
351
|
-
_executor =
|
|
472
|
+
_executor = self.__getAttribute(data, 'executor', None)
|
|
352
473
|
|
|
353
474
|
# Extract the job jobstore if available
|
|
354
|
-
_jobstore =
|
|
475
|
+
_jobstore = self.__getAttribute(data, 'jobstore', None)
|
|
355
476
|
|
|
356
477
|
# Extract the job misfire_grace_time if available
|
|
357
|
-
_misfire_grace_time =
|
|
478
|
+
_misfire_grace_time = self.__getAttribute(data, 'misfire_grace_time', None)
|
|
358
479
|
|
|
359
480
|
# Extract the job max_instances if available
|
|
360
|
-
_max_instances =
|
|
481
|
+
_max_instances = self.__getAttribute(data, 'max_instances', 0)
|
|
361
482
|
|
|
362
483
|
# Extract the job coalesce if available
|
|
363
|
-
_coalesce =
|
|
484
|
+
_coalesce = self.__getAttribute(data, 'coalesce', False)
|
|
364
485
|
|
|
365
486
|
# Extract the job next_run_time if available
|
|
366
|
-
_next_run_time =
|
|
487
|
+
_next_run_time = self.__getAttribute(data, 'next_run_time', None)
|
|
367
488
|
|
|
368
489
|
# Extract additional event data if available
|
|
369
490
|
_purpose = event_data.get('purpose', None)
|
|
@@ -380,7 +501,7 @@ class Schedule(ISchedule):
|
|
|
380
501
|
# Create and return a Job entity based on the retrieved job data
|
|
381
502
|
return EventJob(
|
|
382
503
|
id=_id,
|
|
383
|
-
code=
|
|
504
|
+
code=_code,
|
|
384
505
|
name=_name,
|
|
385
506
|
func=_func,
|
|
386
507
|
args=_args,
|
|
@@ -404,30 +525,59 @@ class Schedule(ISchedule):
|
|
|
404
525
|
self
|
|
405
526
|
) -> None:
|
|
406
527
|
"""
|
|
407
|
-
Subscribe to
|
|
528
|
+
Subscribe internal handlers to APScheduler events for monitoring and control.
|
|
408
529
|
|
|
409
|
-
This method
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
530
|
+
This method attaches internal listener methods to the AsyncIOScheduler instance for a variety of
|
|
531
|
+
scheduler and job-related events. These listeners enable the scheduler to respond to lifecycle
|
|
532
|
+
changes (such as start, shutdown, pause, and resume) and job execution events (such as submission,
|
|
533
|
+
execution, errors, missed runs, max instance violations, and removals).
|
|
413
534
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
are monitored effectively.
|
|
535
|
+
Each listener is responsible for logging, invoking registered callbacks, and handling errors or
|
|
536
|
+
missed jobs as appropriate. This setup ensures that the scheduler's state and job execution
|
|
537
|
+
are monitored and managed effectively.
|
|
417
538
|
|
|
418
539
|
Returns
|
|
419
540
|
-------
|
|
420
541
|
None
|
|
421
|
-
This method does not return any value. It
|
|
542
|
+
This method does not return any value. It registers event listeners on the scheduler instance.
|
|
543
|
+
|
|
544
|
+
Notes
|
|
545
|
+
-----
|
|
546
|
+
The listeners are attached to the following APScheduler events:
|
|
547
|
+
- Scheduler started
|
|
548
|
+
- Scheduler shutdown
|
|
549
|
+
- Job error
|
|
550
|
+
- Job submitted
|
|
551
|
+
- Job executed
|
|
552
|
+
- Job missed
|
|
553
|
+
- Job max instances exceeded
|
|
554
|
+
- Job removed
|
|
555
|
+
|
|
556
|
+
These listeners enable the scheduler to handle and react to all critical scheduling events.
|
|
422
557
|
"""
|
|
423
558
|
|
|
559
|
+
# Register listener for when the scheduler starts
|
|
424
560
|
self.__scheduler.add_listener(self.__startedListener, EVENT_SCHEDULER_STARTED)
|
|
561
|
+
|
|
562
|
+
# Register listener for when the scheduler shuts down
|
|
425
563
|
self.__scheduler.add_listener(self.__shutdownListener, EVENT_SCHEDULER_SHUTDOWN)
|
|
564
|
+
|
|
565
|
+
# Register listener for job execution errors
|
|
426
566
|
self.__scheduler.add_listener(self.__errorListener, EVENT_JOB_ERROR)
|
|
567
|
+
|
|
568
|
+
# Register listener for when a job is submitted to its executor
|
|
427
569
|
self.__scheduler.add_listener(self.__submittedListener, EVENT_JOB_SUBMITTED)
|
|
570
|
+
|
|
571
|
+
# Register listener for when a job has finished executing
|
|
428
572
|
self.__scheduler.add_listener(self.__executedListener, EVENT_JOB_EXECUTED)
|
|
573
|
+
|
|
574
|
+
# Register listener for when a job is missed (not run at its scheduled time)
|
|
429
575
|
self.__scheduler.add_listener(self.__missedListener, EVENT_JOB_MISSED)
|
|
576
|
+
|
|
577
|
+
# Register listener for when a job exceeds its maximum allowed concurrent instances
|
|
430
578
|
self.__scheduler.add_listener(self.__maxInstancesListener, EVENT_JOB_MAX_INSTANCES)
|
|
579
|
+
|
|
580
|
+
# Register listener for when a job is removed from the scheduler
|
|
431
581
|
self.__scheduler.add_listener(self.__removedListener, EVENT_JOB_REMOVED)
|
|
432
582
|
|
|
433
583
|
def __globalCallableListener(
|
|
@@ -438,50 +588,56 @@ class Schedule(ISchedule):
|
|
|
438
588
|
"""
|
|
439
589
|
Invoke registered listeners for global scheduler events.
|
|
440
590
|
|
|
441
|
-
This method
|
|
442
|
-
or
|
|
443
|
-
|
|
591
|
+
This method is responsible for handling global scheduler events such as when the scheduler starts, pauses, resumes,
|
|
592
|
+
shuts down, or encounters an error. It checks if a listener is registered for the specified event type and, if so,
|
|
593
|
+
invokes the listener with the provided event data and the current scheduler instance. The listener can be either a
|
|
594
|
+
coroutine or a regular callable.
|
|
444
595
|
|
|
445
596
|
Parameters
|
|
446
597
|
----------
|
|
447
|
-
event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown,
|
|
448
|
-
The event data associated with the global scheduler event. This
|
|
449
|
-
such as its type
|
|
598
|
+
event_data : Optional[Union[SchedulerStarted, SchedulerPaused, SchedulerResumed, SchedulerShutdown, SchedulerError]]
|
|
599
|
+
The event data associated with the global scheduler event. This may include details about the event,
|
|
600
|
+
such as its type, code, time, or context. If no specific data is available, this parameter can be None.
|
|
450
601
|
listening_vent : ListeningEvent
|
|
451
602
|
An instance of the ListeningEvent enum representing the global scheduler event to handle.
|
|
452
603
|
|
|
453
604
|
Returns
|
|
454
605
|
-------
|
|
455
606
|
None
|
|
456
|
-
This method does not return any value. It
|
|
457
|
-
if one exists.
|
|
607
|
+
This method does not return any value. It executes the registered listener for the specified event if one exists.
|
|
458
608
|
|
|
459
609
|
Raises
|
|
460
610
|
------
|
|
461
611
|
CLIOrionisValueError
|
|
462
612
|
If the provided `listening_vent` is not an instance of ListeningEvent.
|
|
613
|
+
|
|
614
|
+
Notes
|
|
615
|
+
-----
|
|
616
|
+
This method is intended for internal use to centralize the invocation of global event listeners.
|
|
617
|
+
It ensures that only valid event types are processed and that exceptions raised by listeners are
|
|
618
|
+
properly handled through the application's error handling mechanism.
|
|
463
619
|
"""
|
|
464
620
|
|
|
465
|
-
#
|
|
621
|
+
# Ensure the provided event is a valid ListeningEvent instance
|
|
466
622
|
if not isinstance(listening_vent, ListeningEvent):
|
|
467
623
|
self.__raiseException(CLIOrionisValueError("The event must be an instance of ListeningEvent."))
|
|
468
624
|
|
|
469
|
-
# Retrieve the
|
|
625
|
+
# Retrieve the string identifier for the event from the ListeningEvent enum
|
|
470
626
|
scheduler_event = listening_vent.value
|
|
471
627
|
|
|
472
|
-
# Check if a listener is registered for
|
|
628
|
+
# Check if a listener is registered for this global event
|
|
473
629
|
if scheduler_event in self.__listeners:
|
|
474
630
|
|
|
475
|
-
#
|
|
631
|
+
# Retrieve the listener callable for the event
|
|
476
632
|
listener = self.__listeners[scheduler_event]
|
|
477
633
|
|
|
478
|
-
# If is
|
|
634
|
+
# If the listener is callable, invoke it with event data and the scheduler instance
|
|
479
635
|
if callable(listener):
|
|
480
|
-
|
|
481
|
-
# Invoke the listener, handling both coroutine and regular functions
|
|
482
636
|
try:
|
|
637
|
+
# Execute the listener, handling both coroutine and regular functions
|
|
483
638
|
self.__app.invoke(listener, event_data, self)
|
|
484
639
|
except BaseException as e:
|
|
640
|
+
# Handle any exceptions raised by the listener using the application's error handler
|
|
485
641
|
self.__raiseException(e)
|
|
486
642
|
|
|
487
643
|
def __taskCallableListener(
|
|
@@ -492,10 +648,11 @@ class Schedule(ISchedule):
|
|
|
492
648
|
"""
|
|
493
649
|
Invoke registered listeners for specific task/job events.
|
|
494
650
|
|
|
495
|
-
This method
|
|
496
|
-
missed jobs, and max instance violations. It checks if a
|
|
497
|
-
specific job ID associated with the event and
|
|
498
|
-
|
|
651
|
+
This method is responsible for handling task/job-specific events such as job errors,
|
|
652
|
+
executions, submissions, missed jobs, and max instance violations. It checks if a
|
|
653
|
+
listener is registered for the specific job ID associated with the event and, if so,
|
|
654
|
+
invokes the appropriate method on the listener. The listener can be either a class
|
|
655
|
+
implementing `IScheduleEventListener` or a callable.
|
|
499
656
|
|
|
500
657
|
Parameters
|
|
501
658
|
----------
|
|
@@ -504,7 +661,7 @@ class Schedule(ISchedule):
|
|
|
504
661
|
such as its ID, exception (if any), and other context. If no specific data is available,
|
|
505
662
|
this parameter can be None.
|
|
506
663
|
listening_vent : ListeningEvent
|
|
507
|
-
An instance of the ListeningEvent enum representing the task/job event to handle.
|
|
664
|
+
An instance of the `ListeningEvent` enum representing the task/job event to handle.
|
|
508
665
|
|
|
509
666
|
Returns
|
|
510
667
|
-------
|
|
@@ -515,48 +672,56 @@ class Schedule(ISchedule):
|
|
|
515
672
|
Raises
|
|
516
673
|
------
|
|
517
674
|
CLIOrionisValueError
|
|
518
|
-
If the provided `listening_vent` is not an instance of ListeningEvent
|
|
675
|
+
If the provided `listening_vent` is not an instance of `ListeningEvent`.
|
|
676
|
+
If the listener for the job ID is not a subclass of `IScheduleEventListener`.
|
|
677
|
+
|
|
678
|
+
Notes
|
|
679
|
+
-----
|
|
680
|
+
This method is intended for internal use to centralize the invocation of task/job event listeners.
|
|
681
|
+
It ensures that only valid event types and listeners are processed, and that exceptions raised
|
|
682
|
+
by listeners are properly handled through the application's error handling mechanism.
|
|
519
683
|
"""
|
|
520
684
|
|
|
521
|
-
#
|
|
685
|
+
# Ensure the provided event is a valid ListeningEvent instance
|
|
522
686
|
if not isinstance(listening_vent, ListeningEvent):
|
|
523
687
|
self.__raiseException(CLIOrionisValueError("The event must be an instance of ListeningEvent."))
|
|
524
688
|
|
|
525
|
-
# Validate that event_data is
|
|
689
|
+
# Validate that event_data is a valid EventJob with a non-empty id
|
|
526
690
|
if not isinstance(event_data, EventJob) or not hasattr(event_data, 'id') or not event_data.id:
|
|
527
691
|
return
|
|
528
692
|
|
|
529
|
-
# Retrieve the
|
|
693
|
+
# Retrieve the string identifier for the event from the ListeningEvent enum
|
|
530
694
|
scheduler_event = listening_vent.value
|
|
531
695
|
|
|
532
|
-
# Check if a listener is registered for
|
|
696
|
+
# Check if a listener is registered for this specific job ID
|
|
533
697
|
if event_data.id in self.__listeners:
|
|
534
698
|
|
|
535
699
|
# Retrieve the listener for the specific job ID
|
|
536
700
|
listener = self.__listeners[event_data.id]
|
|
537
701
|
|
|
538
|
-
#
|
|
702
|
+
# If the listener is a subclass of IScheduleEventListener, invoke the appropriate method
|
|
539
703
|
if issubclass(listener, IScheduleEventListener):
|
|
540
|
-
|
|
541
704
|
try:
|
|
542
|
-
|
|
543
|
-
# Initialize the listener if it's a class
|
|
705
|
+
# If the listener is a class, instantiate it using the application container
|
|
544
706
|
if isinstance(listener, type):
|
|
545
707
|
listener = self.__app.make(listener)
|
|
546
708
|
|
|
547
|
-
# Check if the listener has a method corresponding to the event type
|
|
709
|
+
# Check if the listener has a method corresponding to the event type and is callable
|
|
548
710
|
if hasattr(listener, scheduler_event) and callable(getattr(listener, scheduler_event)):
|
|
711
|
+
# Call the event method on the listener, passing event data and the scheduler instance
|
|
549
712
|
self.__app.call(listener, scheduler_event, event_data, self)
|
|
550
713
|
|
|
551
714
|
except BaseException as e:
|
|
552
|
-
|
|
553
|
-
# If an error occurs while invoking the listener, raise an exception
|
|
715
|
+
# Handle any exceptions raised by the listener using the application's error handler
|
|
554
716
|
self.__raiseException(e)
|
|
555
717
|
|
|
556
718
|
else:
|
|
557
|
-
|
|
558
719
|
# If the listener is not a subclass of IScheduleEventListener, raise an exception
|
|
559
|
-
self.__raiseException(
|
|
720
|
+
self.__raiseException(
|
|
721
|
+
CLIOrionisValueError(
|
|
722
|
+
f"The listener for job ID '{event_data.id}' must be a subclass of IScheduleEventListener."
|
|
723
|
+
)
|
|
724
|
+
)
|
|
560
725
|
|
|
561
726
|
def __startedListener(
|
|
562
727
|
self,
|
|
@@ -609,16 +774,15 @@ class Schedule(ISchedule):
|
|
|
609
774
|
|
|
610
775
|
# Check if a listener is registered for the scheduler started event
|
|
611
776
|
event_data = SchedulerStarted(
|
|
612
|
-
code=
|
|
613
|
-
time=
|
|
614
|
-
tasks=self.events()
|
|
777
|
+
code=self.__getAttribute(event, 'code', 0),
|
|
778
|
+
time=self.__getNow()
|
|
615
779
|
)
|
|
616
780
|
|
|
617
781
|
# If a listener is registered for this event, invoke the listener with the event details
|
|
618
782
|
self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_STARTED)
|
|
619
783
|
|
|
620
784
|
# Log an informational message indicating that the scheduler has started
|
|
621
|
-
self.__logger.info(f"Orionis Scheduler started successfully at {now}.")
|
|
785
|
+
self.__logger.info(f"Orionis Scheduler started successfully at: {now}.")
|
|
622
786
|
|
|
623
787
|
def __shutdownListener(
|
|
624
788
|
self,
|
|
@@ -650,10 +814,11 @@ class Schedule(ISchedule):
|
|
|
650
814
|
|
|
651
815
|
# Check if a listener is registered for the scheduler shutdown event
|
|
652
816
|
event_data = SchedulerShutdown(
|
|
653
|
-
code=
|
|
654
|
-
time=
|
|
655
|
-
tasks=self.events()
|
|
817
|
+
code=self.__getAttribute(event, 'code', 0),
|
|
818
|
+
time=self.__getNow()
|
|
656
819
|
)
|
|
820
|
+
|
|
821
|
+
# If a listener is registered for this event, invoke the listener with the event details
|
|
657
822
|
self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_SHUTDOWN)
|
|
658
823
|
|
|
659
824
|
# Log an informational message indicating that the scheduler has shut down
|
|
@@ -664,78 +829,105 @@ class Schedule(ISchedule):
|
|
|
664
829
|
event
|
|
665
830
|
) -> None:
|
|
666
831
|
"""
|
|
667
|
-
Handle job error events for logging and
|
|
832
|
+
Handle job error events for logging, error reporting, and listener invocation.
|
|
668
833
|
|
|
669
|
-
This method is triggered when a job
|
|
670
|
-
message
|
|
671
|
-
|
|
672
|
-
|
|
834
|
+
This method is triggered when a scheduled job raises an exception during execution.
|
|
835
|
+
It logs an error message with the job ID and exception details, updates the job event
|
|
836
|
+
data with error information, and invokes any registered listeners for both the specific
|
|
837
|
+
job error and the global scheduler error event. The method also delegates exception
|
|
838
|
+
handling to the application's error catching mechanism.
|
|
673
839
|
|
|
674
840
|
Parameters
|
|
675
841
|
----------
|
|
676
842
|
event : JobError
|
|
677
|
-
An
|
|
678
|
-
|
|
843
|
+
An event object containing details about the errored job, including its ID,
|
|
844
|
+
exception, traceback, and event code.
|
|
679
845
|
|
|
680
846
|
Returns
|
|
681
847
|
-------
|
|
682
848
|
None
|
|
683
849
|
This method does not return any value. It performs logging, error reporting,
|
|
684
|
-
and
|
|
850
|
+
and invokes any registered listeners for the job error and scheduler error events.
|
|
851
|
+
|
|
852
|
+
Notes
|
|
853
|
+
-----
|
|
854
|
+
- The method updates the job event data with the exception and traceback.
|
|
855
|
+
- Both job-specific and global error listeners are invoked if registered.
|
|
856
|
+
- All exceptions are delegated to the application's error handling system.
|
|
685
857
|
"""
|
|
858
|
+
|
|
859
|
+
# Extract job ID, event code, exception, and traceback from the event object
|
|
860
|
+
event_id = self.__getAttribute(event, 'job_id', None)
|
|
861
|
+
event_code = self.__getAttribute(event, 'code', 0)
|
|
862
|
+
event_exception = self.__getAttribute(event, 'exception', None)
|
|
863
|
+
event_traceback = self.__getAttribute(event, 'traceback', None)
|
|
864
|
+
|
|
686
865
|
# Log an error message indicating that the job raised an exception
|
|
687
|
-
self.__logger.error(f"Task '{
|
|
866
|
+
self.__logger.error(f"Task '{event_id}' raised an exception: {event_exception}")
|
|
688
867
|
|
|
689
|
-
#
|
|
690
|
-
job_event_data = self.__getTaskFromSchedulerById(
|
|
691
|
-
job_event_data.code =
|
|
692
|
-
job_event_data.exception =
|
|
693
|
-
job_event_data.traceback =
|
|
868
|
+
# Retrieve the job event data and update it with error details
|
|
869
|
+
job_event_data = self.__getTaskFromSchedulerById(event_id)
|
|
870
|
+
job_event_data.code = event_code
|
|
871
|
+
job_event_data.exception = event_exception
|
|
872
|
+
job_event_data.traceback = event_traceback
|
|
694
873
|
|
|
695
|
-
#
|
|
874
|
+
# Invoke the task-specific listener for job errors, if registered
|
|
696
875
|
self.__taskCallableListener(job_event_data, ListeningEvent.JOB_ON_FAILURE)
|
|
697
876
|
|
|
698
|
-
#
|
|
877
|
+
# Prepare the global scheduler error event data
|
|
699
878
|
event_data = SchedulerError(
|
|
700
|
-
code=
|
|
701
|
-
|
|
702
|
-
|
|
879
|
+
code=event_code,
|
|
880
|
+
time=self.__getNow(),
|
|
881
|
+
exception=event_exception,
|
|
882
|
+
traceback=event_traceback
|
|
703
883
|
)
|
|
884
|
+
|
|
885
|
+
# Invoke the global listener for scheduler errors, if registered
|
|
704
886
|
self.__globalCallableListener(event_data, ListeningEvent.SCHEDULER_ERROR)
|
|
705
887
|
|
|
706
|
-
#
|
|
707
|
-
self.__raiseException(
|
|
888
|
+
# Delegate exception handling to the application's error catching mechanism
|
|
889
|
+
self.__raiseException(event_exception)
|
|
708
890
|
|
|
709
891
|
def __submittedListener(
|
|
710
892
|
self,
|
|
711
893
|
event
|
|
712
894
|
) -> None:
|
|
713
895
|
"""
|
|
714
|
-
Handle job submission events for logging and
|
|
896
|
+
Handle job submission events for logging and invoking registered listeners.
|
|
715
897
|
|
|
716
|
-
This method is triggered when a job is submitted to its executor
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
898
|
+
This internal method is triggered when a job is submitted to its executor by the scheduler.
|
|
899
|
+
It logs an informational message about the job submission, creates an event entity representing
|
|
900
|
+
the job submission, and invokes any registered listeners for the submitted job. This allows
|
|
901
|
+
for custom pre-execution logic or notifications to be handled externally.
|
|
720
902
|
|
|
721
903
|
Parameters
|
|
722
904
|
----------
|
|
723
905
|
event : JobSubmitted
|
|
724
|
-
An
|
|
725
|
-
|
|
906
|
+
An event object containing details about the submitted job, such as its ID and
|
|
907
|
+
any associated event code.
|
|
726
908
|
|
|
727
909
|
Returns
|
|
728
910
|
-------
|
|
729
911
|
None
|
|
730
|
-
This method does not return any value. It performs logging
|
|
731
|
-
|
|
912
|
+
This method does not return any value. It performs logging and invokes any
|
|
913
|
+
registered listener for the job submission event.
|
|
914
|
+
|
|
915
|
+
Notes
|
|
916
|
+
-----
|
|
917
|
+
This method is intended for internal use to centralize the handling of job submission
|
|
918
|
+
events. It ensures that job submissions are logged and that any custom listeners
|
|
919
|
+
associated with the job are properly notified.
|
|
732
920
|
"""
|
|
733
921
|
|
|
734
|
-
#
|
|
735
|
-
self.
|
|
922
|
+
# Extract job ID and code from the event object, using default values if not present
|
|
923
|
+
event_id = self.__getAttribute(event, 'job_id', None)
|
|
924
|
+
event_code = self.__getAttribute(event, 'code', 0)
|
|
736
925
|
|
|
737
|
-
#
|
|
738
|
-
|
|
926
|
+
# Log an informational message indicating that the job has been submitted to the executor
|
|
927
|
+
self.__logger.info(f"Task '{event_id}' submitted to executor.")
|
|
928
|
+
|
|
929
|
+
# Create an event entity for the submitted job, including its ID and code
|
|
930
|
+
data_event = self.__getTaskFromSchedulerById(event_id, event_code)
|
|
739
931
|
|
|
740
932
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
741
933
|
self.__taskCallableListener(data_event, ListeningEvent.JOB_BEFORE)
|
|
@@ -745,32 +937,41 @@ class Schedule(ISchedule):
|
|
|
745
937
|
event
|
|
746
938
|
) -> None:
|
|
747
939
|
"""
|
|
748
|
-
Handle job execution events for logging and
|
|
940
|
+
Handle job execution events for logging, error reporting, and listener invocation.
|
|
749
941
|
|
|
750
|
-
This method is triggered when a job
|
|
751
|
-
message indicating
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if a listener is registered for the executed job, it invokes the listener with the event details.
|
|
942
|
+
This method is triggered when a job has finished executing in the scheduler. It logs an informational
|
|
943
|
+
message indicating the successful execution of the job. If the job execution resulted in an exception,
|
|
944
|
+
the error is logged and reported using the application's error handling system. Additionally, if a
|
|
945
|
+
listener is registered for the executed job, this method invokes the listener with the event details.
|
|
755
946
|
|
|
756
947
|
Parameters
|
|
757
948
|
----------
|
|
758
949
|
event : JobExecuted
|
|
759
|
-
An
|
|
760
|
-
|
|
950
|
+
An event object containing details about the executed job, including its ID, return value,
|
|
951
|
+
exception (if any), and traceback.
|
|
761
952
|
|
|
762
953
|
Returns
|
|
763
954
|
-------
|
|
764
955
|
None
|
|
765
|
-
This method does not return any value. It performs logging, error reporting,
|
|
766
|
-
|
|
956
|
+
This method does not return any value. It performs logging, error reporting, and invokes
|
|
957
|
+
any registered listener for the executed job.
|
|
958
|
+
|
|
959
|
+
Notes
|
|
960
|
+
-----
|
|
961
|
+
This method is intended for internal use to centralize the handling of job execution events.
|
|
962
|
+
It ensures that job executions are logged, errors are reported, and any custom listeners
|
|
963
|
+
associated with the job are properly notified.
|
|
767
964
|
"""
|
|
768
965
|
|
|
966
|
+
# Extract the job ID and event code from the event object, using default values if not present
|
|
967
|
+
event_id = self.__getAttribute(event, 'job_id', None)
|
|
968
|
+
event_code = self.__getAttribute(event, 'code', 0)
|
|
969
|
+
|
|
769
970
|
# Log an informational message indicating that the job has been executed
|
|
770
|
-
self.__logger.info(f"Task '{
|
|
971
|
+
self.__logger.info(f"Task '{event_id}' executed.")
|
|
771
972
|
|
|
772
|
-
# Create entity for
|
|
773
|
-
data_event = self.__getTaskFromSchedulerById(
|
|
973
|
+
# Create an event entity for the executed job, including its ID and code
|
|
974
|
+
data_event = self.__getTaskFromSchedulerById(event_id, event_code)
|
|
774
975
|
|
|
775
976
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
776
977
|
self.__taskCallableListener(data_event, ListeningEvent.JOB_AFTER)
|
|
@@ -780,32 +981,46 @@ class Schedule(ISchedule):
|
|
|
780
981
|
event
|
|
781
982
|
) -> None:
|
|
782
983
|
"""
|
|
783
|
-
Handle job missed events for
|
|
984
|
+
Handle job missed events for logging, reporting, and invoking registered listeners.
|
|
784
985
|
|
|
785
|
-
This method is triggered when a scheduled job is missed.
|
|
786
|
-
message indicating the missed job and its scheduled run time
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
event details.
|
|
986
|
+
This method is triggered when a scheduled job is missed (i.e., it was not executed at its scheduled time)
|
|
987
|
+
by the scheduler. It logs a warning message indicating the missed job and its scheduled run time, creates
|
|
988
|
+
an event entity for the missed job, and invokes any registered listeners for the missed job event. This
|
|
989
|
+
ensures that missed executions are tracked and that any custom logic associated with missed jobs is executed.
|
|
790
990
|
|
|
791
991
|
Parameters
|
|
792
992
|
----------
|
|
793
993
|
event : JobMissed
|
|
794
|
-
An
|
|
795
|
-
|
|
994
|
+
An event object containing details about the missed job, including its ID (`job_id`), event code (`code`),
|
|
995
|
+
and the scheduled run time (`scheduled_run_time`).
|
|
796
996
|
|
|
797
997
|
Returns
|
|
798
998
|
-------
|
|
799
999
|
None
|
|
800
|
-
This method does not return any value. It performs logging,
|
|
801
|
-
|
|
1000
|
+
This method does not return any value. It performs logging, reporting, and invokes any registered
|
|
1001
|
+
listener for the missed job event.
|
|
1002
|
+
|
|
1003
|
+
Notes
|
|
1004
|
+
-----
|
|
1005
|
+
- This method is intended for internal use to centralize the handling of missed job events.
|
|
1006
|
+
- It ensures that missed jobs are logged and that any custom listeners associated with the job are properly notified.
|
|
1007
|
+
- The event entity created provides structured information to listeners for further processing.
|
|
802
1008
|
"""
|
|
803
1009
|
|
|
804
|
-
#
|
|
805
|
-
self.
|
|
1010
|
+
# Extract the job ID from the event object, or None if not present
|
|
1011
|
+
event_id = self.__getAttribute(event, 'job_id', None)
|
|
1012
|
+
# Extract the event code from the event object, defaulting to 0 if not present
|
|
1013
|
+
event_code = self.__getAttribute(event, 'code', 0)
|
|
1014
|
+
# Extract the scheduled run time from the event object, or 'Unknown' if not present
|
|
1015
|
+
event_scheduled_run_time = self.__getAttribute(event, 'scheduled_run_time', 'Unknown')
|
|
1016
|
+
|
|
1017
|
+
# Log a warning indicating that the job was missed and when it was scheduled to run
|
|
1018
|
+
self.__logger.warning(
|
|
1019
|
+
f"Task '{event_id}' was missed. It was scheduled to run at: {event_scheduled_run_time}."
|
|
1020
|
+
)
|
|
806
1021
|
|
|
807
|
-
# Create entity for
|
|
808
|
-
data_event = self.__getTaskFromSchedulerById(
|
|
1022
|
+
# Create an event entity for the missed job, including its ID and code
|
|
1023
|
+
data_event = self.__getTaskFromSchedulerById(event_id, event_code)
|
|
809
1024
|
|
|
810
1025
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
811
1026
|
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MISSED)
|
|
@@ -815,32 +1030,42 @@ class Schedule(ISchedule):
|
|
|
815
1030
|
event
|
|
816
1031
|
) -> None:
|
|
817
1032
|
"""
|
|
818
|
-
Handle job max instances events for logging and
|
|
1033
|
+
Handle job max instances events for logging, error reporting, and listener invocation.
|
|
819
1034
|
|
|
820
|
-
This method is triggered when a job execution exceeds the maximum allowed
|
|
821
|
-
|
|
822
|
-
the
|
|
823
|
-
|
|
824
|
-
|
|
1035
|
+
This method is triggered when a job execution exceeds the maximum allowed concurrent instances.
|
|
1036
|
+
It logs an error message indicating the job ID that exceeded its instance limit, creates an event
|
|
1037
|
+
entity for the affected job, and invokes any registered listener for this job's max instances event.
|
|
1038
|
+
This allows for custom handling, notification, or recovery logic to be executed in response to
|
|
1039
|
+
the max instances violation.
|
|
825
1040
|
|
|
826
1041
|
Parameters
|
|
827
1042
|
----------
|
|
828
1043
|
event : JobMaxInstances
|
|
829
|
-
An
|
|
830
|
-
|
|
1044
|
+
An event object containing details about the job that exceeded the maximum allowed instances,
|
|
1045
|
+
including its job ID and event code.
|
|
831
1046
|
|
|
832
1047
|
Returns
|
|
833
1048
|
-------
|
|
834
1049
|
None
|
|
835
|
-
This method does not return any value. It performs logging, error reporting,
|
|
836
|
-
|
|
1050
|
+
This method does not return any value. It performs logging, error reporting, and invokes
|
|
1051
|
+
any registered listener for the job max instances event.
|
|
1052
|
+
|
|
1053
|
+
Notes
|
|
1054
|
+
-----
|
|
1055
|
+
This method is intended for internal use to centralize the handling of job max instances events.
|
|
1056
|
+
It ensures that such events are logged and that any custom listeners associated with the job
|
|
1057
|
+
are properly notified. No value is returned.
|
|
837
1058
|
"""
|
|
838
1059
|
|
|
839
|
-
#
|
|
840
|
-
self.
|
|
1060
|
+
# Extract the job ID and event code from the event object, using default values if not present
|
|
1061
|
+
event_id = self.__getAttribute(event, 'job_id', None)
|
|
1062
|
+
event_code = self.__getAttribute(event, 'code', 0)
|
|
1063
|
+
|
|
1064
|
+
# Log an error message indicating that the job exceeded maximum concurrent instances
|
|
1065
|
+
self.__logger.error(f"Task '{event_id}' exceeded maximum instances")
|
|
841
1066
|
|
|
842
|
-
# Create entity for job max instances
|
|
843
|
-
data_event = self.__getTaskFromSchedulerById(
|
|
1067
|
+
# Create an event entity for the job that exceeded max instances
|
|
1068
|
+
data_event = self.__getTaskFromSchedulerById(event_id, event_code)
|
|
844
1069
|
|
|
845
1070
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
846
1071
|
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_MAXINSTANCES)
|
|
@@ -853,28 +1078,37 @@ class Schedule(ISchedule):
|
|
|
853
1078
|
Handle job removal events for logging and invoking registered listeners.
|
|
854
1079
|
|
|
855
1080
|
This method is triggered when a job is removed from the scheduler. It logs an informational
|
|
856
|
-
message indicating that the job has been removed
|
|
857
|
-
|
|
858
|
-
removed job,
|
|
1081
|
+
message indicating that the job has been removed. If the application is in debug mode, it
|
|
1082
|
+
may display a message on the console. Additionally, if a listener is registered for the
|
|
1083
|
+
removed job, this method invokes the listener with the event details.
|
|
859
1084
|
|
|
860
1085
|
Parameters
|
|
861
1086
|
----------
|
|
862
1087
|
event : JobRemoved
|
|
863
|
-
An
|
|
864
|
-
including its ID and other relevant information.
|
|
1088
|
+
An event object containing details about the removed job, such as its ID and event code.
|
|
865
1089
|
|
|
866
1090
|
Returns
|
|
867
1091
|
-------
|
|
868
1092
|
None
|
|
869
1093
|
This method does not return any value. It performs logging and invokes any registered
|
|
870
1094
|
listener for the job removal event.
|
|
1095
|
+
|
|
1096
|
+
Notes
|
|
1097
|
+
-----
|
|
1098
|
+
- The method retrieves the job ID and event code from the event object.
|
|
1099
|
+
- It logs the removal of the job and creates an event entity for the removed job.
|
|
1100
|
+
- If a listener is registered for the job, it is invoked with the event details.
|
|
871
1101
|
"""
|
|
872
1102
|
|
|
1103
|
+
# Retrieve the job ID and event code from the event object
|
|
1104
|
+
event_id = self.__getAttribute(event, 'job_id', None)
|
|
1105
|
+
event_code = self.__getAttribute(event, 'code', 0)
|
|
1106
|
+
|
|
873
1107
|
# Log the removal of the job
|
|
874
|
-
self.__logger.info(f"Task '{
|
|
1108
|
+
self.__logger.info(f"Task '{event_id}' has been removed.")
|
|
875
1109
|
|
|
876
|
-
# Create entity for
|
|
877
|
-
data_event = self.__getTaskFromSchedulerById(
|
|
1110
|
+
# Create an event entity for the removed job
|
|
1111
|
+
data_event = self.__getTaskFromSchedulerById(event_id, event_code)
|
|
878
1112
|
|
|
879
1113
|
# If a listener is registered for this job ID, invoke the listener with the event details
|
|
880
1114
|
self.__taskCallableListener(data_event, ListeningEvent.JOB_ON_REMOVED)
|
|
@@ -883,36 +1117,64 @@ class Schedule(ISchedule):
|
|
|
883
1117
|
self
|
|
884
1118
|
) -> None:
|
|
885
1119
|
"""
|
|
886
|
-
Load all scheduled events from the AsyncIOScheduler into the internal jobs
|
|
1120
|
+
Load all scheduled events from the AsyncIOScheduler into the internal jobs list.
|
|
1121
|
+
|
|
1122
|
+
This method synchronizes the internal jobs list (`self.__jobs`) with the events currently
|
|
1123
|
+
registered in the AsyncIOScheduler. For each event in the internal events dictionary,
|
|
1124
|
+
it converts the event to its entity representation, adds it to the jobs list, and schedules
|
|
1125
|
+
it in the AsyncIOScheduler with the appropriate configuration. If a listener is associated
|
|
1126
|
+
with the event, it is also registered. This ensures that all scheduled jobs are properly
|
|
1127
|
+
tracked and managed by both the internal state and the scheduler.
|
|
887
1128
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1129
|
+
Parameters
|
|
1130
|
+
----------
|
|
1131
|
+
None
|
|
891
1132
|
|
|
892
1133
|
Returns
|
|
893
1134
|
-------
|
|
894
1135
|
None
|
|
895
|
-
This method does not return any value. It updates the internal jobs
|
|
1136
|
+
This method does not return any value. It updates the internal jobs list and
|
|
1137
|
+
registers jobs with the AsyncIOScheduler.
|
|
1138
|
+
|
|
1139
|
+
Raises
|
|
1140
|
+
------
|
|
1141
|
+
CLIOrionisRuntimeError
|
|
1142
|
+
If an error occurs while loading or scheduling an event, a runtime error is raised
|
|
1143
|
+
with a descriptive message.
|
|
1144
|
+
|
|
1145
|
+
Notes
|
|
1146
|
+
-----
|
|
1147
|
+
- Events are only loaded if the internal jobs list is empty.
|
|
1148
|
+
- Each event must implement a `toEntity` method to be converted to an entity.
|
|
1149
|
+
- Jobs are added to the scheduler with their respective configuration, and listeners
|
|
1150
|
+
are registered if present.
|
|
896
1151
|
"""
|
|
897
1152
|
|
|
898
|
-
# Only load events if the jobs list is empty
|
|
1153
|
+
# Only load events if the jobs list is empty to avoid duplicate scheduling
|
|
899
1154
|
if not self.__jobs:
|
|
900
1155
|
|
|
901
|
-
# Iterate through all scheduled
|
|
1156
|
+
# Iterate through all scheduled events in the internal events dictionary
|
|
902
1157
|
for signature, event in self.__events.items():
|
|
903
1158
|
|
|
904
1159
|
try:
|
|
905
|
-
# Convert the event to its entity representation
|
|
906
|
-
entity: EventEntity = event.toEntity()
|
|
907
1160
|
|
|
908
|
-
#
|
|
1161
|
+
# Ensure the event has a toEntity method for conversion
|
|
1162
|
+
if not hasattr(event, 'toEntity'):
|
|
1163
|
+
continue
|
|
1164
|
+
|
|
1165
|
+
# Convert the event to its entity representation (EventEntity)
|
|
1166
|
+
to_entity = getattr(event, 'toEntity')
|
|
1167
|
+
entity: EventEntity = to_entity()
|
|
1168
|
+
|
|
1169
|
+
# Add the job entity to the internal jobs list
|
|
909
1170
|
self.__jobs.append(entity)
|
|
910
1171
|
|
|
911
|
-
#
|
|
1172
|
+
# Helper function to create a job function that calls the reactor
|
|
912
1173
|
def create_job_func(cmd, args_list):
|
|
1174
|
+
# Returns a lambda that will call the command with its arguments
|
|
913
1175
|
return lambda: self.__reactor.call(cmd, args_list)
|
|
914
1176
|
|
|
915
|
-
# Add the job to the
|
|
1177
|
+
# Add the job to the AsyncIOScheduler with the specified configuration
|
|
916
1178
|
self.__scheduler.add_job(
|
|
917
1179
|
func=create_job_func(signature, list(entity.args)),
|
|
918
1180
|
trigger=entity.trigger,
|
|
@@ -923,22 +1185,21 @@ class Schedule(ISchedule):
|
|
|
923
1185
|
misfire_grace_time=entity.misfire_grace_time
|
|
924
1186
|
)
|
|
925
1187
|
|
|
926
|
-
# If
|
|
1188
|
+
# If the event entity has an associated listener, register it
|
|
927
1189
|
if entity.listener:
|
|
928
1190
|
self.setListener(signature, entity.listener)
|
|
929
1191
|
|
|
930
|
-
# Log the successful loading of the scheduled event
|
|
1192
|
+
# Log the successful loading of the scheduled event for debugging
|
|
931
1193
|
self.__logger.debug(f"Scheduled event '{signature}' loaded successfully.")
|
|
932
1194
|
|
|
933
1195
|
except Exception as e:
|
|
934
|
-
|
|
935
|
-
# Construct the error message
|
|
1196
|
+
# Construct an error message for failed event loading
|
|
936
1197
|
error_msg = f"Failed to load scheduled event '{signature}': {str(e)}"
|
|
937
1198
|
|
|
938
1199
|
# Log the error message
|
|
939
1200
|
self.__logger.error(error_msg)
|
|
940
1201
|
|
|
941
|
-
# Raise a runtime error
|
|
1202
|
+
# Raise a runtime error to signal failure in loading the scheduled event
|
|
942
1203
|
raise CLIOrionisRuntimeError(error_msg)
|
|
943
1204
|
|
|
944
1205
|
def __raiseException(
|
|
@@ -946,42 +1207,43 @@ class Schedule(ISchedule):
|
|
|
946
1207
|
exception: BaseException
|
|
947
1208
|
) -> None:
|
|
948
1209
|
"""
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
This private method serves as a centralized exception handler for the scheduler,
|
|
952
|
-
delegating exception processing to the application's error catching mechanism.
|
|
953
|
-
It ensures that all exceptions occurring within the scheduler context are
|
|
954
|
-
properly handled according to the application's error handling policies.
|
|
1210
|
+
Centralized exception handling for the scheduler, delegating to the application's error catching system.
|
|
955
1211
|
|
|
956
|
-
|
|
957
|
-
the application's
|
|
958
|
-
|
|
1212
|
+
This private method provides a unified mechanism for handling exceptions that occur within the scheduler's
|
|
1213
|
+
context. It forwards the exception to the application's error catching mechanism, ensuring consistent
|
|
1214
|
+
processing of errors according to the application's global error handling policies. This may include
|
|
1215
|
+
logging, reporting, or re-raising the exception, depending on the application's configuration.
|
|
959
1216
|
|
|
960
1217
|
Parameters
|
|
961
1218
|
----------
|
|
962
1219
|
exception : BaseException
|
|
963
|
-
The exception instance
|
|
964
|
-
|
|
965
|
-
exceptions, custom application exceptions, and runtime errors.
|
|
1220
|
+
The exception instance raised during scheduler or command execution. This can be any subclass of
|
|
1221
|
+
BaseException, including system, runtime, or custom exceptions.
|
|
966
1222
|
|
|
967
1223
|
Returns
|
|
968
1224
|
-------
|
|
969
1225
|
None
|
|
970
|
-
This method
|
|
971
|
-
|
|
972
|
-
exception depending on the configured error handling behavior.
|
|
1226
|
+
This method always returns None. It delegates the exception to the application's error
|
|
1227
|
+
handling system for further processing.
|
|
973
1228
|
|
|
974
1229
|
Notes
|
|
975
1230
|
-----
|
|
976
|
-
This method is intended for internal use within the scheduler
|
|
977
|
-
|
|
978
|
-
various actions such as logging, reporting, or re-raising the exception based
|
|
979
|
-
on the application's configuration.
|
|
1231
|
+
This method is intended for internal use within the scheduler. The actual handling of the exception
|
|
1232
|
+
(such as logging or propagation) is determined by the application's error catching implementation.
|
|
980
1233
|
"""
|
|
981
1234
|
|
|
982
|
-
#
|
|
983
|
-
|
|
984
|
-
|
|
1235
|
+
# Create a CLIRequest object representing the current scheduler context
|
|
1236
|
+
request = CLIRequest(
|
|
1237
|
+
command="schedule:work",
|
|
1238
|
+
args={}
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
# Delegate the exception to the application's error catching system
|
|
1242
|
+
self.__catch.exception(
|
|
1243
|
+
self,
|
|
1244
|
+
request,
|
|
1245
|
+
exception
|
|
1246
|
+
)
|
|
985
1247
|
|
|
986
1248
|
def setListener(
|
|
987
1249
|
self,
|
|
@@ -989,36 +1251,44 @@ class Schedule(ISchedule):
|
|
|
989
1251
|
listener: Union[IScheduleEventListener, callable]
|
|
990
1252
|
) -> None:
|
|
991
1253
|
"""
|
|
992
|
-
Register a listener
|
|
1254
|
+
Register a listener for a specific scheduler event or job.
|
|
993
1255
|
|
|
994
|
-
This method
|
|
995
|
-
|
|
996
|
-
event
|
|
997
|
-
|
|
1256
|
+
This method allows you to associate a callback or an instance of
|
|
1257
|
+
`IScheduleEventListener` with a scheduler event. The event can be a global
|
|
1258
|
+
scheduler event (such as 'scheduler_started', 'scheduler_paused', etc.) or
|
|
1259
|
+
a specific job ID. When the specified event occurs, the registered listener
|
|
1260
|
+
will be invoked with the event data.
|
|
998
1261
|
|
|
999
1262
|
Parameters
|
|
1000
1263
|
----------
|
|
1001
|
-
event : str
|
|
1002
|
-
The name of the event to listen for. This can be a
|
|
1003
|
-
or a
|
|
1264
|
+
event : str or ListeningEvent
|
|
1265
|
+
The name of the event to listen for. This can be a string representing
|
|
1266
|
+
a global event name (e.g., 'scheduler_started') or a job ID, or an
|
|
1267
|
+
instance of `ListeningEvent`.
|
|
1004
1268
|
listener : IScheduleEventListener or callable
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1269
|
+
The listener to be registered. This can be a callable function or an
|
|
1270
|
+
instance of `IScheduleEventListener`. The listener should accept the
|
|
1271
|
+
event object as its parameter.
|
|
1008
1272
|
|
|
1009
1273
|
Returns
|
|
1010
1274
|
-------
|
|
1011
1275
|
None
|
|
1012
|
-
This method does not return any value. It registers the listener for
|
|
1276
|
+
This method does not return any value. It registers the listener for
|
|
1277
|
+
the specified event.
|
|
1013
1278
|
|
|
1014
1279
|
Raises
|
|
1015
1280
|
------
|
|
1016
1281
|
CLIOrionisValueError
|
|
1017
|
-
If the event name is not a non-empty string or if the listener is not
|
|
1018
|
-
or an instance of IScheduleEventListener
|
|
1282
|
+
If the event name is not a non-empty string, or if the listener is not
|
|
1283
|
+
callable or an instance of `IScheduleEventListener`.
|
|
1284
|
+
|
|
1285
|
+
Notes
|
|
1286
|
+
-----
|
|
1287
|
+
- If the event parameter is a `ListeningEvent`, its value is used as the event name.
|
|
1288
|
+
- The listener will be stored internally and invoked when the corresponding event occurs.
|
|
1019
1289
|
"""
|
|
1020
1290
|
|
|
1021
|
-
# If the event is an instance of ListeningEvent, extract its value
|
|
1291
|
+
# If the event is an instance of ListeningEvent, extract its string value
|
|
1022
1292
|
if isinstance(event, ListeningEvent):
|
|
1023
1293
|
event = event.value
|
|
1024
1294
|
|
|
@@ -1028,428 +1298,167 @@ class Schedule(ISchedule):
|
|
|
1028
1298
|
|
|
1029
1299
|
# Validate that the listener is either callable or an instance of IScheduleEventListener
|
|
1030
1300
|
if not callable(listener) and not isinstance(listener, IScheduleEventListener):
|
|
1031
|
-
raise CLIOrionisValueError(
|
|
1301
|
+
raise CLIOrionisValueError(
|
|
1302
|
+
"Listener must be a callable function or an instance of IScheduleEventListener."
|
|
1303
|
+
)
|
|
1032
1304
|
|
|
1033
1305
|
# Register the listener for the specified event in the internal listeners dictionary
|
|
1034
1306
|
self.__listeners[event] = listener
|
|
1035
1307
|
|
|
1036
|
-
def
|
|
1037
|
-
self
|
|
1038
|
-
func: Callable[..., Awaitable[Any]]
|
|
1039
|
-
) -> Callable[..., Any]:
|
|
1040
|
-
"""
|
|
1041
|
-
Wrap an asynchronous function to be executed in a synchronous context.
|
|
1042
|
-
|
|
1043
|
-
This method creates a synchronous wrapper around an asynchronous function (coroutine)
|
|
1044
|
-
that enables its execution within non-async contexts. The wrapper leverages the
|
|
1045
|
-
Coroutine utility class to handle the complexities of asyncio event loop management
|
|
1046
|
-
and provides proper error handling with detailed logging and custom exception
|
|
1047
|
-
propagation.
|
|
1048
|
-
|
|
1049
|
-
The wrapper is particularly useful when integrating asynchronous functions with
|
|
1050
|
-
synchronous APIs or frameworks that do not natively support async operations,
|
|
1051
|
-
such as the APScheduler job execution environment.
|
|
1052
|
-
|
|
1053
|
-
Parameters
|
|
1054
|
-
----------
|
|
1055
|
-
func : Callable[..., Awaitable[Any]]
|
|
1056
|
-
The asynchronous function (coroutine) to be wrapped. This function must be
|
|
1057
|
-
defined using the `async def` syntax and return an awaitable object. The
|
|
1058
|
-
function can accept any number of positional and keyword arguments.
|
|
1059
|
-
|
|
1060
|
-
Returns
|
|
1061
|
-
-------
|
|
1062
|
-
Callable[..., Any]
|
|
1063
|
-
A synchronous wrapper function that executes the original asynchronous
|
|
1064
|
-
function and returns its result. The wrapper function accepts the same
|
|
1065
|
-
arguments as the original async function and forwards them appropriately.
|
|
1066
|
-
The return type depends on what the wrapped asynchronous function returns.
|
|
1067
|
-
|
|
1068
|
-
Raises
|
|
1069
|
-
------
|
|
1070
|
-
CLIOrionisRuntimeError
|
|
1071
|
-
If the asynchronous function execution fails or if the provided `func`
|
|
1072
|
-
parameter is not a valid asynchronous function. The original exception
|
|
1073
|
-
is wrapped to provide additional context for debugging.
|
|
1074
|
-
|
|
1075
|
-
Notes
|
|
1076
|
-
-----
|
|
1077
|
-
This method relies on the Coroutine utility class from the orionis.services.asynchrony
|
|
1078
|
-
module to handle the execution of the wrapped asynchronous function. The wrapper
|
|
1079
|
-
uses the instance logger for comprehensive error reporting and debugging information.
|
|
1080
|
-
"""
|
|
1081
|
-
|
|
1082
|
-
def sync_wrapper(*args, **kwargs) -> Any:
|
|
1083
|
-
"""
|
|
1084
|
-
Synchronous wrapper function that executes asynchronous functions in a thread-safe manner.
|
|
1085
|
-
|
|
1086
|
-
This wrapper provides a uniform interface for executing asynchronous functions within
|
|
1087
|
-
synchronous contexts by leveraging the Coroutine utility class. It handles the complexity
|
|
1088
|
-
of managing async/await patterns and provides proper error handling with detailed logging
|
|
1089
|
-
and custom exception propagation.
|
|
1090
|
-
|
|
1091
|
-
The wrapper is particularly useful when integrating asynchronous functions with
|
|
1092
|
-
synchronous APIs or frameworks that do not natively support async operations.
|
|
1093
|
-
|
|
1094
|
-
Parameters
|
|
1095
|
-
----------
|
|
1096
|
-
*args : tuple
|
|
1097
|
-
Variable length argument list to pass to the wrapped asynchronous function.
|
|
1098
|
-
These arguments are forwarded directly to the original function.
|
|
1099
|
-
**kwargs : dict
|
|
1100
|
-
Arbitrary keyword arguments to pass to the wrapped asynchronous function.
|
|
1101
|
-
These keyword arguments are forwarded directly to the original function.
|
|
1102
|
-
|
|
1103
|
-
Returns
|
|
1104
|
-
-------
|
|
1105
|
-
Any
|
|
1106
|
-
The return value from the executed asynchronous function. The type depends
|
|
1107
|
-
on what the wrapped function returns and can be any Python object.
|
|
1108
|
-
|
|
1109
|
-
Raises
|
|
1110
|
-
------
|
|
1111
|
-
CLIOrionisRuntimeError
|
|
1112
|
-
When the asynchronous function execution fails, wrapping the original
|
|
1113
|
-
exception with additional context and error details for better debugging.
|
|
1114
|
-
|
|
1115
|
-
Notes
|
|
1116
|
-
-----
|
|
1117
|
-
This function relies on the Coroutine utility class to handle the execution
|
|
1118
|
-
of the wrapped asynchronous function and uses the instance logger for comprehensive
|
|
1119
|
-
error reporting and debugging information.
|
|
1120
|
-
"""
|
|
1121
|
-
|
|
1122
|
-
# Execute the asynchronous function using the container's invoke method
|
|
1123
|
-
try:
|
|
1124
|
-
self.__app.invoke(func, *args, **kwargs)
|
|
1125
|
-
|
|
1126
|
-
# If an error occurs during execution, raise a custom exception
|
|
1127
|
-
except Exception as e:
|
|
1128
|
-
self.__raiseException(e)
|
|
1129
|
-
|
|
1130
|
-
# Return the synchronous wrapper function
|
|
1131
|
-
return sync_wrapper
|
|
1132
|
-
|
|
1133
|
-
def pauseEverythingAt(
|
|
1134
|
-
self,
|
|
1135
|
-
at: datetime
|
|
1136
|
-
) -> None:
|
|
1137
|
-
"""
|
|
1138
|
-
Schedule the scheduler to pause all operations at a specific datetime.
|
|
1139
|
-
|
|
1140
|
-
This method schedules a job that pauses the AsyncIOScheduler at the specified datetime.
|
|
1141
|
-
The job is added to the scheduler with a 'date' trigger, ensuring it executes exactly
|
|
1142
|
-
at the given time. If a pause job already exists, it is replaced to avoid conflicts.
|
|
1143
|
-
|
|
1144
|
-
Parameters
|
|
1145
|
-
----------
|
|
1146
|
-
at : datetime
|
|
1147
|
-
The datetime at which the scheduler should be paused. Must be a valid
|
|
1148
|
-
datetime object.
|
|
1149
|
-
|
|
1150
|
-
Returns
|
|
1151
|
-
-------
|
|
1152
|
-
None
|
|
1153
|
-
This method does not return any value. It schedules a job to pause the
|
|
1154
|
-
scheduler at the specified datetime.
|
|
1155
|
-
|
|
1156
|
-
Raises
|
|
1157
|
-
------
|
|
1158
|
-
ValueError
|
|
1159
|
-
If the 'at' parameter is not a valid datetime object or is not in the future.
|
|
1160
|
-
CLIOrionisRuntimeError
|
|
1161
|
-
If the scheduler is not running or if an error occurs during job scheduling.
|
|
1162
|
-
"""
|
|
1163
|
-
|
|
1164
|
-
# Validate that the 'at' parameter is a datetime object
|
|
1165
|
-
if not isinstance(at, datetime):
|
|
1166
|
-
CLIOrionisValueError("The 'at' parameter must be a datetime object.")
|
|
1167
|
-
|
|
1168
|
-
# Define an async function to pause the scheduler
|
|
1169
|
-
async def schedule_pause():
|
|
1170
|
-
|
|
1171
|
-
# Only pause jobs if the scheduler is currently running
|
|
1172
|
-
if self.isRunning():
|
|
1173
|
-
|
|
1174
|
-
# Clear the set of previously paused jobs
|
|
1175
|
-
self.__pausedByPauseEverything.clear()
|
|
1176
|
-
|
|
1177
|
-
# Get all jobs from the scheduler
|
|
1178
|
-
all_jobs = self.__scheduler.get_jobs()
|
|
1179
|
-
|
|
1180
|
-
# Filter out system jobs (pause, resume, shutdown tasks)
|
|
1181
|
-
system_job_ids = {
|
|
1182
|
-
"scheduler_pause_at",
|
|
1183
|
-
"scheduler_resume_at",
|
|
1184
|
-
"scheduler_shutdown_at"
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
# Pause only user jobs, not system jobs
|
|
1188
|
-
for job in all_jobs:
|
|
1189
|
-
|
|
1190
|
-
# Check if the job is not a system job
|
|
1191
|
-
if job.id not in system_job_ids:
|
|
1192
|
-
|
|
1193
|
-
try:
|
|
1194
|
-
|
|
1195
|
-
# Pause the job in the scheduler
|
|
1196
|
-
self.__scheduler.pause_job(job.id)
|
|
1197
|
-
self.__pausedByPauseEverything.add(job.id)
|
|
1198
|
-
|
|
1199
|
-
# Get the current time in the configured timezone
|
|
1200
|
-
now = self.__getCurrentTime()
|
|
1201
|
-
|
|
1202
|
-
# Log an informational message indicating that the job has been paused
|
|
1203
|
-
self.__taskCallableListener(
|
|
1204
|
-
self.__getTaskFromSchedulerById(job.id),
|
|
1205
|
-
ListeningEvent.JOB_ON_PAUSED
|
|
1206
|
-
)
|
|
1207
|
-
|
|
1208
|
-
# Log the pause action
|
|
1209
|
-
self.__logger.info(f"Job '{job.id}' paused successfully at {now}.")
|
|
1210
|
-
|
|
1211
|
-
except Exception as e:
|
|
1212
|
-
|
|
1213
|
-
# If an error occurs while pausing the job, raise an exception
|
|
1214
|
-
self.__raiseException(e)
|
|
1215
|
-
|
|
1216
|
-
# Execute the global callable listener after all jobs are paused
|
|
1217
|
-
self.__globalCallableListener(SchedulerPaused(
|
|
1218
|
-
code=EVENT_SCHEDULER_PAUSED,
|
|
1219
|
-
time=self.__getCurrentTime()
|
|
1220
|
-
), ListeningEvent.SCHEDULER_PAUSED)
|
|
1221
|
-
|
|
1222
|
-
# Log that all user jobs have been paused
|
|
1223
|
-
self.__logger.info("All user jobs have been paused. System jobs remain active.")
|
|
1224
|
-
|
|
1225
|
-
try:
|
|
1226
|
-
|
|
1227
|
-
# Remove any existing pause job to avoid conflicts
|
|
1228
|
-
try:
|
|
1229
|
-
self.__scheduler.remove_job("scheduler_pause_at")
|
|
1230
|
-
|
|
1231
|
-
# If the job doesn't exist, it's fine to proceed
|
|
1232
|
-
finally:
|
|
1233
|
-
pass
|
|
1234
|
-
|
|
1235
|
-
# Add a job to the scheduler to pause it at the specified datetime
|
|
1236
|
-
self.__scheduler.add_job(
|
|
1237
|
-
func=self.wrapAsyncFunction(schedule_pause), # Function to pause the scheduler
|
|
1238
|
-
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1239
|
-
id="scheduler_pause_at", # Unique job ID for pausing the scheduler
|
|
1240
|
-
name="Pause Scheduler", # Descriptive name for the job
|
|
1241
|
-
replace_existing=True # Replace any existing job with the same ID
|
|
1242
|
-
)
|
|
1243
|
-
|
|
1244
|
-
# Log the scheduled pause
|
|
1245
|
-
self.__logger.info(f"Scheduler pause scheduled for {at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
1246
|
-
|
|
1247
|
-
except Exception as e:
|
|
1248
|
-
|
|
1249
|
-
# Handle exceptions that may occur during job scheduling
|
|
1250
|
-
raise CLIOrionisRuntimeError(f"Failed to schedule scheduler pause: {str(e)}") from e
|
|
1251
|
-
|
|
1252
|
-
def resumeEverythingAt(
|
|
1253
|
-
self,
|
|
1254
|
-
at: datetime
|
|
1308
|
+
def pause(
|
|
1309
|
+
self
|
|
1255
1310
|
) -> None:
|
|
1256
1311
|
"""
|
|
1257
|
-
|
|
1312
|
+
Pause all user jobs managed by the scheduler.
|
|
1258
1313
|
|
|
1259
|
-
This method
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
----------
|
|
1265
|
-
at : datetime
|
|
1266
|
-
The datetime at which the scheduler should be resumed. Must be a valid
|
|
1267
|
-
datetime object.
|
|
1314
|
+
This method pauses all currently scheduled user jobs in the AsyncIOScheduler if the scheduler is running.
|
|
1315
|
+
It iterates through all jobs, attempts to pause each one, and tracks which jobs were paused by adding their
|
|
1316
|
+
IDs to an internal set. For each successfully paused job, the method logs the action and invokes any
|
|
1317
|
+
registered listeners for the pause event. After all jobs are paused, a global listener for the scheduler
|
|
1318
|
+
pause event is also invoked.
|
|
1268
1319
|
|
|
1269
1320
|
Returns
|
|
1270
1321
|
-------
|
|
1271
1322
|
None
|
|
1272
|
-
This method does not return any value. It
|
|
1273
|
-
|
|
1323
|
+
This method does not return any value. It performs the pausing of all user jobs and triggers
|
|
1324
|
+
the appropriate listeners and logging.
|
|
1274
1325
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1326
|
+
Notes
|
|
1327
|
+
-----
|
|
1328
|
+
- Only jobs with valid IDs are paused.
|
|
1329
|
+
- System jobs are not explicitly filtered out; all jobs returned by the scheduler are considered user jobs.
|
|
1330
|
+
- If an error occurs while pausing a job, the exception is handled by the application's error handler.
|
|
1331
|
+
- The set of paused jobs is cleared before pausing to ensure only currently paused jobs are tracked.
|
|
1281
1332
|
"""
|
|
1282
1333
|
|
|
1283
|
-
#
|
|
1284
|
-
if
|
|
1285
|
-
raise CLIOrionisValueError("The 'at' parameter must be a datetime object.")
|
|
1286
|
-
|
|
1287
|
-
# Define an async function to resume the scheduler
|
|
1288
|
-
async def schedule_resume():
|
|
1289
|
-
|
|
1290
|
-
# Only resume jobs if the scheduler is currently running
|
|
1291
|
-
if self.isRunning():
|
|
1292
|
-
|
|
1293
|
-
# Resume only jobs that were paused by pauseEverythingAt
|
|
1294
|
-
if self.__pausedByPauseEverything:
|
|
1295
|
-
|
|
1296
|
-
# Iterate through the set of paused job IDs and resume each one
|
|
1297
|
-
for job_id in list(self.__pausedByPauseEverything):
|
|
1298
|
-
|
|
1299
|
-
try:
|
|
1300
|
-
|
|
1301
|
-
# Resume the job and log the action
|
|
1302
|
-
self.__scheduler.resume_job(job_id)
|
|
1334
|
+
# Only pause jobs if the scheduler is currently running
|
|
1335
|
+
if self.isRunning():
|
|
1303
1336
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
self.__getTaskFromSchedulerById(job_id),
|
|
1307
|
-
ListeningEvent.JOB_ON_RESUMED
|
|
1308
|
-
)
|
|
1337
|
+
# Clear the set of previously paused jobs to avoid stale entries
|
|
1338
|
+
self.__pausedByPauseEverything.clear()
|
|
1309
1339
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1340
|
+
# Retrieve all jobs currently managed by the scheduler
|
|
1341
|
+
all_jobs = self.__scheduler.get_jobs()
|
|
1312
1342
|
|
|
1313
|
-
|
|
1343
|
+
# Iterate through each job and attempt to pause it
|
|
1344
|
+
for job in all_jobs:
|
|
1345
|
+
try:
|
|
1346
|
+
# Get the job ID safely
|
|
1347
|
+
job_id = self.__getAttribute(job, 'id', None)
|
|
1314
1348
|
|
|
1315
|
-
|
|
1316
|
-
|
|
1349
|
+
# Skip jobs without a valid ID
|
|
1350
|
+
if not job_id:
|
|
1351
|
+
continue
|
|
1317
1352
|
|
|
1318
|
-
#
|
|
1319
|
-
self.
|
|
1353
|
+
# Pause the job in the scheduler
|
|
1354
|
+
self.__scheduler.pause_job(job_id)
|
|
1320
1355
|
|
|
1321
|
-
#
|
|
1322
|
-
self.
|
|
1323
|
-
code=EVENT_SCHEDULER_RESUMED,
|
|
1324
|
-
time=self.__getCurrentTime()
|
|
1325
|
-
), ListeningEvent.SCHEDULER_RESUMED)
|
|
1356
|
+
# Track the paused job's ID
|
|
1357
|
+
self.__pausedByPauseEverything.add(job_id)
|
|
1326
1358
|
|
|
1327
|
-
# Get the current time in the configured timezone
|
|
1359
|
+
# Get the current time in the configured timezone for logging
|
|
1328
1360
|
now = self.__getCurrentTime()
|
|
1329
1361
|
|
|
1330
|
-
#
|
|
1331
|
-
self.
|
|
1332
|
-
|
|
1333
|
-
# Log that all previously paused jobs have been resumed
|
|
1334
|
-
self.__logger.info("All previously paused user jobs have been resumed.")
|
|
1362
|
+
# Retrieve event data for the paused job
|
|
1363
|
+
event_data = self.__getTaskFromSchedulerById(job_id)
|
|
1335
1364
|
|
|
1336
|
-
|
|
1365
|
+
# Invoke the listener for the paused job event
|
|
1366
|
+
self.__taskCallableListener(
|
|
1367
|
+
event_data,
|
|
1368
|
+
ListeningEvent.JOB_ON_PAUSED
|
|
1369
|
+
)
|
|
1337
1370
|
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
self.__scheduler.remove_job("scheduler_resume_at")
|
|
1371
|
+
# Log the pause action for this job
|
|
1372
|
+
self.__logger.info(f"Task '{job_id}' paused successfully at {now}.")
|
|
1341
1373
|
|
|
1342
|
-
|
|
1343
|
-
finally:
|
|
1344
|
-
pass
|
|
1345
|
-
|
|
1346
|
-
# Add a job to the scheduler to resume it at the specified datetime
|
|
1347
|
-
self.__scheduler.add_job(
|
|
1348
|
-
func=self.wrapAsyncFunction(schedule_resume), # Function to resume the scheduler
|
|
1349
|
-
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1350
|
-
id="scheduler_resume_at", # Unique job ID for resuming the scheduler
|
|
1351
|
-
name="Resume Scheduler", # Descriptive name for the job
|
|
1352
|
-
replace_existing=True # Replace any existing job with the same ID
|
|
1353
|
-
)
|
|
1374
|
+
except Exception as e:
|
|
1354
1375
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1376
|
+
# Handle any errors that occur while pausing a job
|
|
1377
|
+
self.__raiseException(e)
|
|
1357
1378
|
|
|
1358
|
-
|
|
1379
|
+
# After all jobs are paused, invoke the global listener for scheduler pause
|
|
1380
|
+
self.__globalCallableListener(SchedulerPaused(
|
|
1381
|
+
code=EVENT_SCHEDULER_PAUSED,
|
|
1382
|
+
time=self.__getNow()
|
|
1383
|
+
), ListeningEvent.SCHEDULER_PAUSED)
|
|
1359
1384
|
|
|
1360
|
-
#
|
|
1361
|
-
|
|
1385
|
+
# Log that all tasks have been paused
|
|
1386
|
+
self.__logger.info("All tasks have been paused.")
|
|
1362
1387
|
|
|
1363
|
-
def
|
|
1364
|
-
self
|
|
1365
|
-
at: datetime,
|
|
1366
|
-
wait: bool = True
|
|
1388
|
+
def resume(
|
|
1389
|
+
self
|
|
1367
1390
|
) -> None:
|
|
1368
1391
|
"""
|
|
1369
|
-
|
|
1392
|
+
Resume all user jobs that were previously paused by the scheduler.
|
|
1370
1393
|
|
|
1371
|
-
This method
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
at : datetime
|
|
1378
|
-
The datetime at which the scheduler should be shut down. Must be a valid
|
|
1379
|
-
datetime object.
|
|
1380
|
-
wait : bool, optional
|
|
1381
|
-
Whether to wait for currently running jobs to complete before shutdown.
|
|
1382
|
-
Default is True.
|
|
1394
|
+
This method resumes only those jobs that were paused using the `pause` method,
|
|
1395
|
+
as tracked by the internal set `__pausedByPauseEverything`. It iterates through
|
|
1396
|
+
each paused job, attempts to resume it, and triggers any registered listeners
|
|
1397
|
+
for the resumed job event. After all jobs are resumed, a global listener for the
|
|
1398
|
+
scheduler resume event is also invoked. The set of paused jobs is cleared after
|
|
1399
|
+
resumption to ensure accurate tracking.
|
|
1383
1400
|
|
|
1384
1401
|
Returns
|
|
1385
1402
|
-------
|
|
1386
1403
|
None
|
|
1387
|
-
This method does not return any value. It
|
|
1388
|
-
scheduler
|
|
1404
|
+
This method does not return any value. It resumes all jobs that were paused
|
|
1405
|
+
by the scheduler, triggers the appropriate listeners, and logs the actions.
|
|
1389
1406
|
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
CLIOrionisRuntimeError
|
|
1396
|
-
If the scheduler is not running or if an error occurs during job scheduling.
|
|
1407
|
+
Notes
|
|
1408
|
+
-----
|
|
1409
|
+
- Only jobs that were paused by the `pause` method are resumed.
|
|
1410
|
+
- If an error occurs while resuming a job, the exception is handled by the application's error handler.
|
|
1411
|
+
- After resuming all jobs, the global scheduler resumed event is triggered.
|
|
1397
1412
|
"""
|
|
1398
1413
|
|
|
1399
|
-
#
|
|
1400
|
-
if
|
|
1401
|
-
raise CLIOrionisValueError("The 'at' parameter must be a datetime object.")
|
|
1414
|
+
# Only resume jobs if the scheduler is currently running
|
|
1415
|
+
if self.isRunning():
|
|
1402
1416
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
raise CLIOrionisValueError("The 'wait' parameter must be a boolean value.")
|
|
1417
|
+
# Resume only jobs that were paused by the pause method
|
|
1418
|
+
if self.__pausedByPauseEverything:
|
|
1406
1419
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
# Only shut down the scheduler if it is currently running
|
|
1410
|
-
if self.isRunning():
|
|
1411
|
-
try:
|
|
1420
|
+
# Iterate through the set of paused job IDs and resume each one
|
|
1421
|
+
for job_id in list(self.__pausedByPauseEverything):
|
|
1412
1422
|
|
|
1413
|
-
|
|
1414
|
-
self.__logger.info("Initiating scheduled shutdown...")
|
|
1423
|
+
try:
|
|
1415
1424
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1425
|
+
# Resume the job in the scheduler
|
|
1426
|
+
self.__scheduler.resume_job(job_id)
|
|
1418
1427
|
|
|
1419
|
-
|
|
1428
|
+
# Retrieve event data for the resumed job
|
|
1429
|
+
event_data = self.__getTaskFromSchedulerById(job_id)
|
|
1420
1430
|
|
|
1421
|
-
|
|
1422
|
-
|
|
1431
|
+
# Invoke the listener for the resumed job event
|
|
1432
|
+
self.__taskCallableListener(
|
|
1433
|
+
event_data,
|
|
1434
|
+
ListeningEvent.JOB_ON_RESUMED
|
|
1435
|
+
)
|
|
1423
1436
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1437
|
+
# Log an informational message indicating that the job has been resumed
|
|
1438
|
+
self.__logger.info(f"Task '{job_id}' has been resumed.")
|
|
1426
1439
|
|
|
1427
|
-
|
|
1440
|
+
except Exception as e:
|
|
1428
1441
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
self.__scheduler.remove_job("scheduler_shutdown_at")
|
|
1442
|
+
# Handle any errors that occur while resuming a job
|
|
1443
|
+
self.__raiseException(e)
|
|
1432
1444
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
pass
|
|
1436
|
-
|
|
1437
|
-
# Add a job to the scheduler to shut it down at the specified datetime
|
|
1438
|
-
self.__scheduler.add_job(
|
|
1439
|
-
func=self.wrapAsyncFunction(schedule_shutdown), # Function to shut down the scheduler
|
|
1440
|
-
trigger=DateTrigger(run_date=at), # Trigger type is 'date' for one-time execution
|
|
1441
|
-
id="scheduler_shutdown_at", # Unique job ID for shutting down the scheduler
|
|
1442
|
-
name="Shutdown Scheduler", # Descriptive name for the job
|
|
1443
|
-
replace_existing=True # Replace any existing job with the same ID
|
|
1444
|
-
)
|
|
1445
|
+
# Clear the set after resuming all jobs to avoid stale entries
|
|
1446
|
+
self.__pausedByPauseEverything.clear()
|
|
1445
1447
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
+
# Execute the global callable listener after all jobs are resumed
|
|
1449
|
+
self.__globalCallableListener(SchedulerResumed(
|
|
1450
|
+
code=EVENT_SCHEDULER_RESUMED,
|
|
1451
|
+
time=self.__getNow()
|
|
1452
|
+
), ListeningEvent.SCHEDULER_RESUMED)
|
|
1448
1453
|
|
|
1449
|
-
|
|
1454
|
+
# Get the current time in the configured timezone for logging
|
|
1455
|
+
now = self.__getCurrentTime()
|
|
1450
1456
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1457
|
+
# Log an informational message indicating that the scheduler has been resumed
|
|
1458
|
+
self.__logger.info(f"Orionis Scheduler resumed successfully at {now}.")
|
|
1459
|
+
|
|
1460
|
+
# Log that all previously paused jobs have been resumed
|
|
1461
|
+
self.__logger.info("All previously task have been resumed.")
|
|
1453
1462
|
|
|
1454
1463
|
async def start(self) -> None:
|
|
1455
1464
|
"""
|
|
@@ -1553,16 +1562,20 @@ class Schedule(ISchedule):
|
|
|
1553
1562
|
|
|
1554
1563
|
# Validate the wait parameter
|
|
1555
1564
|
if not isinstance(wait, bool):
|
|
1556
|
-
self.__raiseException(
|
|
1565
|
+
self.__raiseException(
|
|
1566
|
+
CLIOrionisValueError(
|
|
1567
|
+
"The 'wait' parameter must be a boolean value (True or False) to indicate whether to wait for running jobs to finish before shutting down the scheduler."
|
|
1568
|
+
)
|
|
1569
|
+
)
|
|
1557
1570
|
|
|
1558
1571
|
# If the scheduler is not running, there's nothing to shut down
|
|
1559
1572
|
if not self.isRunning():
|
|
1573
|
+
self.__logger.info("The scheduler is already stopped. No shutdown action is required.")
|
|
1560
1574
|
return
|
|
1561
1575
|
|
|
1562
1576
|
try:
|
|
1563
|
-
|
|
1564
1577
|
# Log the shutdown process
|
|
1565
|
-
self.__logger.info(f"
|
|
1578
|
+
self.__logger.info(f"Starting Orionis Scheduler shutdown process (wait={wait})...")
|
|
1566
1579
|
|
|
1567
1580
|
# Shut down the AsyncIOScheduler
|
|
1568
1581
|
self.__scheduler.shutdown(wait=wait)
|
|
@@ -1576,37 +1589,45 @@ class Schedule(ISchedule):
|
|
|
1576
1589
|
await asyncio.sleep(0.1)
|
|
1577
1590
|
|
|
1578
1591
|
# Log the successful shutdown
|
|
1579
|
-
self.__logger.info("Scheduler
|
|
1592
|
+
self.__logger.info("Orionis Scheduler has been shut down successfully.")
|
|
1580
1593
|
|
|
1581
1594
|
except Exception as e:
|
|
1582
|
-
|
|
1583
1595
|
# Handle exceptions that may occur during shutdown
|
|
1584
|
-
self.__raiseException(
|
|
1596
|
+
self.__raiseException(
|
|
1597
|
+
CLIOrionisRuntimeError(
|
|
1598
|
+
f"Error while attempting to shut down Orionis Scheduler: {str(e)}"
|
|
1599
|
+
)
|
|
1600
|
+
)
|
|
1585
1601
|
|
|
1586
1602
|
def pauseTask(self, signature: str) -> bool:
|
|
1587
1603
|
"""
|
|
1588
1604
|
Pause a scheduled job in the AsyncIO scheduler.
|
|
1589
1605
|
|
|
1590
|
-
This method
|
|
1591
|
-
It validates the provided signature
|
|
1592
|
-
|
|
1593
|
-
|
|
1606
|
+
This method attempts to pause a job managed by the AsyncIOScheduler, identified by its unique signature.
|
|
1607
|
+
It first validates that the provided signature is a non-empty string. If the job exists and is successfully
|
|
1608
|
+
paused, the method logs the action and returns True. If the job does not exist or an error occurs during
|
|
1609
|
+
the pause operation, the method returns False.
|
|
1594
1610
|
|
|
1595
1611
|
Parameters
|
|
1596
1612
|
----------
|
|
1597
1613
|
signature : str
|
|
1598
|
-
The unique signature (ID) of the job to pause.
|
|
1614
|
+
The unique signature (ID) of the job to pause. Must be a non-empty string.
|
|
1599
1615
|
|
|
1600
1616
|
Returns
|
|
1601
1617
|
-------
|
|
1602
1618
|
bool
|
|
1603
1619
|
True if the job was successfully paused.
|
|
1604
|
-
False if the job does not exist or an error occurred.
|
|
1620
|
+
False if the job does not exist or an error occurred during the pause operation.
|
|
1605
1621
|
|
|
1606
1622
|
Raises
|
|
1607
1623
|
------
|
|
1608
1624
|
CLIOrionisValueError
|
|
1609
1625
|
If the `signature` parameter is not a non-empty string.
|
|
1626
|
+
|
|
1627
|
+
Notes
|
|
1628
|
+
-----
|
|
1629
|
+
This method is intended for use with jobs managed by the AsyncIOScheduler. It does not raise
|
|
1630
|
+
an exception if the job does not exist; instead, it returns False to indicate failure.
|
|
1610
1631
|
"""
|
|
1611
1632
|
|
|
1612
1633
|
# Validate that the signature is a non-empty string
|
|
@@ -1614,19 +1635,18 @@ class Schedule(ISchedule):
|
|
|
1614
1635
|
self.__raiseException(CLIOrionisValueError("Signature must be a non-empty string."))
|
|
1615
1636
|
|
|
1616
1637
|
try:
|
|
1617
|
-
|
|
1618
1638
|
# Attempt to pause the job with the given signature
|
|
1619
1639
|
self.__scheduler.pause_job(signature)
|
|
1620
1640
|
|
|
1621
1641
|
# Log the successful pausing of the job
|
|
1622
|
-
self.__logger.info(f"
|
|
1642
|
+
self.__logger.info(f"Pause '{signature}' has been paused.")
|
|
1623
1643
|
|
|
1624
1644
|
# Return True to indicate the job was successfully paused
|
|
1625
1645
|
return True
|
|
1626
1646
|
|
|
1627
1647
|
except Exception:
|
|
1628
1648
|
|
|
1629
|
-
# Return False if the job could not be paused (e.g., it does not exist)
|
|
1649
|
+
# Return False if the job could not be paused (e.g., it does not exist or another error occurred)
|
|
1630
1650
|
return False
|
|
1631
1651
|
|
|
1632
1652
|
def resumeTask(self, signature: str) -> bool:
|
|
@@ -1634,25 +1654,30 @@ class Schedule(ISchedule):
|
|
|
1634
1654
|
Resume a paused job in the AsyncIO scheduler.
|
|
1635
1655
|
|
|
1636
1656
|
This method attempts to resume a job that was previously paused in the AsyncIOScheduler.
|
|
1637
|
-
It validates the provided job signature
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
the method returns False.
|
|
1657
|
+
It first validates that the provided job signature is a non-empty string. If the job exists
|
|
1658
|
+
and is currently paused, the method resumes the job, logs the action, and returns True.
|
|
1659
|
+
If the job does not exist or an error occurs during the resume operation, the method returns False.
|
|
1641
1660
|
|
|
1642
1661
|
Parameters
|
|
1643
1662
|
----------
|
|
1644
1663
|
signature : str
|
|
1645
|
-
The unique signature (ID) of the job to resume.
|
|
1664
|
+
The unique signature (ID) of the job to resume. Must be a non-empty string.
|
|
1646
1665
|
|
|
1647
1666
|
Returns
|
|
1648
1667
|
-------
|
|
1649
1668
|
bool
|
|
1650
|
-
True if the job was successfully resumed
|
|
1669
|
+
True if the job was successfully resumed.
|
|
1670
|
+
False if the job does not exist or an error occurred during the resume operation.
|
|
1651
1671
|
|
|
1652
1672
|
Raises
|
|
1653
1673
|
------
|
|
1654
1674
|
CLIOrionisValueError
|
|
1655
1675
|
If the `signature` parameter is not a non-empty string.
|
|
1676
|
+
|
|
1677
|
+
Notes
|
|
1678
|
+
-----
|
|
1679
|
+
This method is intended for use with jobs managed by the AsyncIOScheduler. It does not raise
|
|
1680
|
+
an exception if the job does not exist; instead, it returns False to indicate failure.
|
|
1656
1681
|
"""
|
|
1657
1682
|
|
|
1658
1683
|
# Validate that the signature is a non-empty string
|
|
@@ -1660,45 +1685,51 @@ class Schedule(ISchedule):
|
|
|
1660
1685
|
self.__raiseException(CLIOrionisValueError("Signature must be a non-empty string."))
|
|
1661
1686
|
|
|
1662
1687
|
try:
|
|
1688
|
+
|
|
1663
1689
|
# Attempt to resume the job with the given signature
|
|
1664
1690
|
self.__scheduler.resume_job(signature)
|
|
1665
1691
|
|
|
1666
1692
|
# Log the successful resumption of the job
|
|
1667
|
-
self.__logger.info(f"
|
|
1693
|
+
self.__logger.info(f"Task '{signature}' has been resumed.")
|
|
1668
1694
|
|
|
1669
1695
|
# Return True to indicate the job was successfully resumed
|
|
1670
1696
|
return True
|
|
1671
1697
|
|
|
1672
1698
|
except Exception:
|
|
1673
1699
|
|
|
1674
|
-
# Return False if the job could not be resumed (e.g., it does not exist)
|
|
1700
|
+
# Return False if the job could not be resumed (e.g., it does not exist or another error occurred)
|
|
1675
1701
|
return False
|
|
1676
1702
|
|
|
1677
1703
|
def removeTask(self, signature: str) -> bool:
|
|
1678
1704
|
"""
|
|
1679
|
-
Remove a scheduled job from the AsyncIO scheduler.
|
|
1705
|
+
Remove a scheduled job from the AsyncIO scheduler by its signature.
|
|
1680
1706
|
|
|
1681
|
-
This method
|
|
1682
|
-
It validates the provided signature
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1707
|
+
This method attempts to remove a job from the AsyncIOScheduler using its unique signature (ID).
|
|
1708
|
+
It first validates that the provided signature is a non-empty string. If the job exists,
|
|
1709
|
+
it is removed from both the scheduler and the internal jobs list. The method logs the removal
|
|
1710
|
+
and returns True if successful. If the job does not exist or an error occurs during removal,
|
|
1711
|
+
the method returns False.
|
|
1686
1712
|
|
|
1687
1713
|
Parameters
|
|
1688
1714
|
----------
|
|
1689
1715
|
signature : str
|
|
1690
|
-
The unique signature (ID) of the job to remove.
|
|
1716
|
+
The unique signature (ID) of the job to remove. Must be a non-empty string.
|
|
1691
1717
|
|
|
1692
1718
|
Returns
|
|
1693
1719
|
-------
|
|
1694
1720
|
bool
|
|
1695
|
-
True if the job was successfully removed from the scheduler.
|
|
1696
|
-
False if the job does not exist or an error occurred.
|
|
1721
|
+
True if the job was successfully removed from both the scheduler and the internal jobs list.
|
|
1722
|
+
False if the job does not exist or an error occurred during removal.
|
|
1697
1723
|
|
|
1698
1724
|
Raises
|
|
1699
1725
|
------
|
|
1700
1726
|
CLIOrionisValueError
|
|
1701
1727
|
If the `signature` parameter is not a non-empty string.
|
|
1728
|
+
|
|
1729
|
+
Notes
|
|
1730
|
+
-----
|
|
1731
|
+
This method ensures that both the scheduler and the internal jobs list remain consistent
|
|
1732
|
+
after a job is removed. No exception is raised if the job does not exist; instead, False is returned.
|
|
1702
1733
|
"""
|
|
1703
1734
|
|
|
1704
1735
|
# Validate that the signature is a non-empty string
|
|
@@ -1710,43 +1741,47 @@ class Schedule(ISchedule):
|
|
|
1710
1741
|
# Attempt to remove the job from the scheduler using its signature
|
|
1711
1742
|
self.__scheduler.remove_job(signature)
|
|
1712
1743
|
|
|
1713
|
-
#
|
|
1744
|
+
# Remove the job from the internal jobs list if it exists
|
|
1714
1745
|
for job in self.__jobs:
|
|
1715
1746
|
if job.signature == signature:
|
|
1716
|
-
self.__jobs.remove(job)
|
|
1747
|
+
self.__jobs.remove(job)
|
|
1717
1748
|
break
|
|
1718
1749
|
|
|
1719
1750
|
# Log the successful removal of the job
|
|
1720
|
-
self.__logger.info(f"
|
|
1751
|
+
self.__logger.info(f"Task '{signature}' has been removed from the scheduler.")
|
|
1721
1752
|
|
|
1722
1753
|
# Return True to indicate the job was successfully removed
|
|
1723
1754
|
return True
|
|
1724
1755
|
|
|
1725
1756
|
except Exception:
|
|
1726
1757
|
|
|
1727
|
-
# Return False if the job could not be removed (e.g., it does not exist)
|
|
1758
|
+
# Return False if the job could not be removed (e.g., it does not exist or another error occurred)
|
|
1728
1759
|
return False
|
|
1729
1760
|
|
|
1730
1761
|
def events(self) -> List[Dict]:
|
|
1731
1762
|
"""
|
|
1732
|
-
Retrieve all scheduled jobs
|
|
1763
|
+
Retrieve a list of all scheduled jobs managed by the scheduler.
|
|
1733
1764
|
|
|
1734
|
-
This method
|
|
1735
|
-
|
|
1736
|
-
|
|
1765
|
+
This method ensures that all scheduled events are loaded into the internal jobs list,
|
|
1766
|
+
then iterates through each job to collect its details in a dictionary format. Each
|
|
1767
|
+
dictionary contains information such as the command signature, arguments, purpose,
|
|
1768
|
+
random delay, start and end dates, and any additional job details.
|
|
1737
1769
|
|
|
1738
1770
|
Returns
|
|
1739
1771
|
-------
|
|
1740
|
-
|
|
1741
|
-
A list where each
|
|
1742
|
-
|
|
1743
|
-
- '
|
|
1744
|
-
- '
|
|
1745
|
-
- '
|
|
1746
|
-
- '
|
|
1747
|
-
- '
|
|
1748
|
-
- '
|
|
1749
|
-
|
|
1772
|
+
List[dict]
|
|
1773
|
+
A list of dictionaries, where each dictionary represents a scheduled job and contains:
|
|
1774
|
+
- 'signature' (str): The command signature of the job.
|
|
1775
|
+
- 'args' (list): The arguments passed to the command.
|
|
1776
|
+
- 'purpose' (str): The description or purpose of the job.
|
|
1777
|
+
- 'random_delay' (int): The random delay associated with the job, if any.
|
|
1778
|
+
- 'start_date' (str): The formatted start date and time of the job, or 'Not Applicable' if not set.
|
|
1779
|
+
- 'end_date' (str): The formatted end date and time of the job, or 'Not Applicable' if not set.
|
|
1780
|
+
- 'details' (str): Additional details about the job.
|
|
1781
|
+
Notes
|
|
1782
|
+
-----
|
|
1783
|
+
This method guarantees that the returned list reflects the current state of all jobs
|
|
1784
|
+
managed by this scheduler instance. If no jobs are scheduled, an empty list is returned.
|
|
1750
1785
|
"""
|
|
1751
1786
|
|
|
1752
1787
|
# Ensure all events are loaded into the internal jobs list
|
|
@@ -1758,13 +1793,18 @@ class Schedule(ISchedule):
|
|
|
1758
1793
|
# Iterate over each job in the internal jobs list
|
|
1759
1794
|
for job in self.__jobs:
|
|
1760
1795
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1796
|
+
# Safely extract job details with default values if attributes are missing
|
|
1797
|
+
signature: str = self.__getAttribute(job, 'signature', '')
|
|
1798
|
+
args: list = self.__getAttribute(job, 'args', [])
|
|
1799
|
+
purpose: str = self.__getAttribute(job, 'purpose', 'No Description')
|
|
1800
|
+
random_delay: int = self.__getAttribute(job, 'random_delay', 0)
|
|
1801
|
+
start_date: datetime = self.__getAttribute(job, 'start_date', None)
|
|
1802
|
+
end_date: datetime = self.__getAttribute(job, 'end_date', None)
|
|
1803
|
+
details: str = self.__getAttribute(job, 'details', 'Not Available')
|
|
1804
|
+
|
|
1805
|
+
# Format the start and end dates as strings, or mark as 'Not Applicable' if not set
|
|
1806
|
+
formatted_start = start_date.strftime('%Y-%m-%d %H:%M:%S') if start_date else 'Not Applicable'
|
|
1807
|
+
formatted_end = end_date.strftime('%Y-%m-%d %H:%M:%S') if end_date else 'Not Applicable'
|
|
1768
1808
|
|
|
1769
1809
|
# Append a dictionary with relevant job details to the events list
|
|
1770
1810
|
events.append({
|
|
@@ -1772,188 +1812,154 @@ class Schedule(ISchedule):
|
|
|
1772
1812
|
'args': args,
|
|
1773
1813
|
'purpose': purpose,
|
|
1774
1814
|
'random_delay': random_delay,
|
|
1775
|
-
'start_date':
|
|
1776
|
-
'end_date':
|
|
1815
|
+
'start_date': formatted_start,
|
|
1816
|
+
'end_date': formatted_end,
|
|
1777
1817
|
'details': details
|
|
1778
1818
|
})
|
|
1779
1819
|
|
|
1780
1820
|
# Return the list of scheduled job details
|
|
1781
1821
|
return events
|
|
1782
1822
|
|
|
1783
|
-
def
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
This method attempts to remove a job from the scheduler that was set to pause
|
|
1788
|
-
the scheduler at a specific time. If the job exists, it is removed, and a log entry
|
|
1789
|
-
is created to indicate the cancellation. If no such job exists, the method returns False.
|
|
1790
|
-
|
|
1791
|
-
Returns
|
|
1792
|
-
-------
|
|
1793
|
-
bool
|
|
1794
|
-
True if the scheduled pause job was successfully cancelled.
|
|
1795
|
-
False if no pause job was found or an error occurred during the cancellation process.
|
|
1823
|
+
def event(
|
|
1824
|
+
self,
|
|
1825
|
+
signature: str
|
|
1826
|
+
) -> Optional[Dict]:
|
|
1796
1827
|
"""
|
|
1797
|
-
|
|
1828
|
+
Retrieve the details of a specific scheduled job by its signature.
|
|
1798
1829
|
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
# Attempt to remove the pause job with the specific ID
|
|
1805
|
-
# if it exists
|
|
1806
|
-
try:
|
|
1807
|
-
self.__scheduler.remove_job("scheduler_pause_at")
|
|
1808
|
-
finally:
|
|
1809
|
-
pass
|
|
1810
|
-
|
|
1811
|
-
# Log the successful cancellation of the pause operation
|
|
1812
|
-
self.__logger.info("Scheduled pause operation cancelled.")
|
|
1813
|
-
|
|
1814
|
-
# Return True to indicate the pause job was successfully cancelled
|
|
1815
|
-
return True
|
|
1816
|
-
|
|
1817
|
-
finally:
|
|
1818
|
-
|
|
1819
|
-
# Return False if the pause job does not exist or an error occurred
|
|
1820
|
-
return False
|
|
1821
|
-
|
|
1822
|
-
def cancelScheduledResume(self) -> bool:
|
|
1823
|
-
"""
|
|
1824
|
-
Cancel a previously scheduled resume operation.
|
|
1830
|
+
This method searches the internal jobs list for a job whose signature matches
|
|
1831
|
+
the provided value. If a matching job is found, it returns a dictionary containing
|
|
1832
|
+
the job's details, such as its arguments, purpose, random delay, start and end dates,
|
|
1833
|
+
and additional details. If no job with the given signature exists, the method returns None.
|
|
1825
1834
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1835
|
+
Parameters
|
|
1836
|
+
----------
|
|
1837
|
+
signature : str
|
|
1838
|
+
The unique signature (ID) of the job to retrieve. Must be a non-empty string.
|
|
1829
1839
|
|
|
1830
1840
|
Returns
|
|
1831
1841
|
-------
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
# Attempt to remove the resume job with the specific ID
|
|
1844
|
-
# if it exists
|
|
1845
|
-
try:
|
|
1846
|
-
self.__scheduler.remove_job("scheduler_resume_at")
|
|
1847
|
-
finally:
|
|
1848
|
-
pass
|
|
1849
|
-
|
|
1850
|
-
# Log the successful cancellation of the resume operation
|
|
1851
|
-
self.__logger.info("Scheduled resume operation cancelled.")
|
|
1852
|
-
|
|
1853
|
-
# Return True to indicate the resume job was successfully cancelled
|
|
1854
|
-
return True
|
|
1855
|
-
|
|
1856
|
-
finally:
|
|
1857
|
-
|
|
1858
|
-
# Return False if the resume job does not exist or an error occurred
|
|
1859
|
-
return False
|
|
1860
|
-
|
|
1861
|
-
def cancelScheduledShutdown(self) -> bool:
|
|
1862
|
-
"""
|
|
1863
|
-
Cancel a previously scheduled shutdown operation.
|
|
1864
|
-
|
|
1865
|
-
This method attempts to remove a job from the scheduler that was set to shut down
|
|
1866
|
-
the scheduler at a specific time. If the job exists, it is removed, and a log entry
|
|
1867
|
-
is created to indicate the cancellation. If no such job exists, the method returns False.
|
|
1842
|
+
dict or None
|
|
1843
|
+
If a job with the specified signature is found, returns a dictionary with the following keys:
|
|
1844
|
+
- 'signature': str, the job's signature.
|
|
1845
|
+
- 'args': list, the arguments passed to the job.
|
|
1846
|
+
- 'purpose': str, the description or purpose of the job.
|
|
1847
|
+
- 'random_delay': int, the random delay associated with the job (if any).
|
|
1848
|
+
- 'start_date': str, the formatted start date and time of the job, or 'Not Applicable' if not set.
|
|
1849
|
+
- 'end_date': str, the formatted end date and time of the job, or 'Not Applicable' if not set.
|
|
1850
|
+
- 'details': str, additional details about the job.
|
|
1851
|
+
If no job with the given signature is found, returns None.
|
|
1868
1852
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
False if no shutdown job was found or an error occurred during the cancellation process.
|
|
1853
|
+
Notes
|
|
1854
|
+
-----
|
|
1855
|
+
This method ensures that all events are loaded before searching for the job.
|
|
1856
|
+
The returned dictionary provides a summary of the job's configuration and metadata.
|
|
1874
1857
|
"""
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
# Remove any listener associated with the shutdown event
|
|
1878
|
-
listener = ListeningEvent.SCHEDULER_SHUTDOWN.value
|
|
1879
|
-
if listener in self.__listeners:
|
|
1880
|
-
del self.__listeners[listener]
|
|
1881
|
-
|
|
1882
|
-
# Attempt to remove the shutdown job with the specific ID
|
|
1883
|
-
# if it exists
|
|
1884
|
-
try:
|
|
1885
|
-
self.__scheduler.remove_job("scheduler_shutdown_at")
|
|
1886
|
-
finally:
|
|
1887
|
-
pass
|
|
1888
|
-
|
|
1889
|
-
# Log the successful cancellation of the shutdown operation
|
|
1890
|
-
self.__logger.info("Scheduled shutdown operation cancelled.")
|
|
1858
|
+
# Ensure all events are loaded into the internal jobs list
|
|
1859
|
+
self.__loadEvents()
|
|
1891
1860
|
|
|
1892
|
-
|
|
1893
|
-
|
|
1861
|
+
# Validate the signature parameter
|
|
1862
|
+
if not isinstance(signature, str) or not signature.strip():
|
|
1863
|
+
return None
|
|
1894
1864
|
|
|
1895
|
-
|
|
1865
|
+
# Search for the job with the matching signature
|
|
1866
|
+
for job in self.__jobs:
|
|
1867
|
+
# Get the job's signature attribute safely
|
|
1868
|
+
job_signature = self.__getAttribute(job, 'signature', '')
|
|
1869
|
+
|
|
1870
|
+
# If a matching job is found, return its details in a dictionary
|
|
1871
|
+
if job_signature == signature:
|
|
1872
|
+
|
|
1873
|
+
# Extract job details safely with default values
|
|
1874
|
+
args: list = self.__getAttribute(job, 'args', [])
|
|
1875
|
+
purpose: str = self.__getAttribute(job, 'purpose', 'No Description')
|
|
1876
|
+
random_delay: int = self.__getAttribute(job, 'random_delay', 0)
|
|
1877
|
+
start_date: datetime = self.__getAttribute(job, 'start_date', None)
|
|
1878
|
+
end_date: datetime = self.__getAttribute(job, 'end_date', None)
|
|
1879
|
+
details: str = self.__getAttribute(job, 'details', 'Not Available')
|
|
1880
|
+
|
|
1881
|
+
# Return the job details as a dictionary
|
|
1882
|
+
return {
|
|
1883
|
+
'signature': job_signature,
|
|
1884
|
+
'args': args,
|
|
1885
|
+
'purpose': purpose,
|
|
1886
|
+
'random_delay': random_delay,
|
|
1887
|
+
'start_date': start_date.strftime('%Y-%m-%d %H:%M:%S') if start_date else 'Not Applicable',
|
|
1888
|
+
'end_date': end_date.strftime('%Y-%m-%d %H:%M:%S') if end_date else 'Not Applicable',
|
|
1889
|
+
'details': details
|
|
1890
|
+
}
|
|
1896
1891
|
|
|
1897
|
-
|
|
1898
|
-
|
|
1892
|
+
# Return None if no job with the given signature is found
|
|
1893
|
+
return None
|
|
1899
1894
|
|
|
1900
1895
|
def isRunning(self) -> bool:
|
|
1901
1896
|
"""
|
|
1902
|
-
|
|
1897
|
+
Check if the scheduler is currently running.
|
|
1903
1898
|
|
|
1904
|
-
This method
|
|
1905
|
-
whether
|
|
1906
|
-
started and has not been paused or shut down.
|
|
1899
|
+
This method inspects the internal state of the AsyncIOScheduler instance to determine
|
|
1900
|
+
whether the scheduler is actively running. The scheduler is considered running if it
|
|
1901
|
+
has been started and has not been paused or shut down.
|
|
1907
1902
|
|
|
1908
1903
|
Returns
|
|
1909
1904
|
-------
|
|
1910
1905
|
bool
|
|
1911
|
-
True if the
|
|
1906
|
+
True if the AsyncIOScheduler is currently running; False otherwise.
|
|
1912
1907
|
"""
|
|
1913
1908
|
|
|
1914
|
-
# Return the
|
|
1909
|
+
# Return True if the scheduler is running, otherwise False
|
|
1915
1910
|
return self.__scheduler.running
|
|
1916
1911
|
|
|
1917
1912
|
def forceStop(self) -> None:
|
|
1918
1913
|
"""
|
|
1919
|
-
Forcefully stop the scheduler immediately
|
|
1914
|
+
Forcefully stop the scheduler immediately, bypassing graceful shutdown.
|
|
1920
1915
|
|
|
1921
|
-
This method shuts down the AsyncIOScheduler instance without waiting for currently
|
|
1922
|
-
running jobs to finish. It is intended for emergency situations where an
|
|
1923
|
-
|
|
1924
|
-
the scheduler
|
|
1925
|
-
shutdown procedures.
|
|
1916
|
+
This method immediately shuts down the AsyncIOScheduler instance without waiting for any currently
|
|
1917
|
+
running jobs to finish. It is intended for emergency or critical situations where an abrupt stop
|
|
1918
|
+
is required, such as unrecoverable errors or forced application termination. In addition to shutting
|
|
1919
|
+
down the scheduler, it also signals the internal stop event to interrupt the scheduler's main loop,
|
|
1920
|
+
allowing the application to proceed with its shutdown procedures.
|
|
1926
1921
|
|
|
1927
1922
|
Returns
|
|
1928
1923
|
-------
|
|
1929
1924
|
None
|
|
1930
|
-
This method does not return any value. It
|
|
1931
|
-
signals the stop event.
|
|
1925
|
+
This method does not return any value. It performs a forceful shutdown of the scheduler and
|
|
1926
|
+
signals the stop event to ensure the main loop is interrupted.
|
|
1927
|
+
|
|
1928
|
+
Notes
|
|
1929
|
+
-----
|
|
1930
|
+
- This method should be used with caution, as it does not wait for running jobs to complete.
|
|
1931
|
+
- After calling this method, the scheduler will be stopped and any pending or running jobs may be interrupted.
|
|
1932
1932
|
"""
|
|
1933
1933
|
|
|
1934
|
-
#
|
|
1934
|
+
# If the scheduler is currently running, shut it down immediately without waiting for jobs to finish
|
|
1935
1935
|
if self.__scheduler.running:
|
|
1936
|
-
# Shut down the scheduler immediately without waiting for jobs to complete
|
|
1937
1936
|
self.__scheduler.shutdown(wait=False)
|
|
1938
1937
|
|
|
1939
|
-
#
|
|
1938
|
+
# If the stop event exists and has not already been set, signal it to interrupt the main loop
|
|
1940
1939
|
if self._stop_event and not self._stop_event.is_set():
|
|
1941
|
-
# Signal the stop event to interrupt the scheduler's main loop
|
|
1942
1940
|
self._stop_event.set()
|
|
1943
1941
|
|
|
1944
1942
|
def stop(self) -> None:
|
|
1945
1943
|
"""
|
|
1946
|
-
|
|
1944
|
+
Signal the scheduler to stop synchronously by setting the internal stop event.
|
|
1947
1945
|
|
|
1948
|
-
This method
|
|
1949
|
-
It
|
|
1950
|
-
event loop is running, the stop event is set in a thread-safe manner
|
|
1951
|
-
the stop event is set directly.
|
|
1946
|
+
This method is used to request a graceful shutdown of the scheduler from a synchronous context.
|
|
1947
|
+
It sets the internal asyncio stop event, which will cause the scheduler's main loop to exit.
|
|
1948
|
+
If an asyncio event loop is currently running, the stop event is set in a thread-safe manner
|
|
1949
|
+
using `call_soon_threadsafe`. If no event loop is running, the stop event is set directly.
|
|
1950
|
+
This method is safe to call from both asynchronous and synchronous contexts.
|
|
1952
1951
|
|
|
1953
1952
|
Returns
|
|
1954
1953
|
-------
|
|
1955
1954
|
None
|
|
1956
|
-
This method does not return any value. It signals the scheduler to stop
|
|
1955
|
+
This method does not return any value. It only signals the scheduler to stop by setting
|
|
1956
|
+
the internal stop event.
|
|
1957
|
+
|
|
1958
|
+
Notes
|
|
1959
|
+
-----
|
|
1960
|
+
- If the stop event is already set or does not exist, this method does nothing.
|
|
1961
|
+
- Any exceptions encountered while setting the stop event are logged as warnings, but the
|
|
1962
|
+
method will still attempt to set the event directly.
|
|
1957
1963
|
"""
|
|
1958
1964
|
# Check if the stop event exists and has not already been set
|
|
1959
1965
|
if self._stop_event and not self._stop_event.is_set():
|
|
@@ -1973,6 +1979,6 @@ class Schedule(ISchedule):
|
|
|
1973
1979
|
|
|
1974
1980
|
except Exception as e:
|
|
1975
1981
|
|
|
1976
|
-
# Log
|
|
1982
|
+
# Log any unexpected error but still try to set the event directly
|
|
1977
1983
|
self.__logger.warning(f"Error setting stop event through event loop: {str(e)}")
|
|
1978
1984
|
self._stop_event.set()
|