22import threading
33import logging
44import queue
5-
5+ from splitio . optional . loaders import asyncio
66
77__TASK_STOP__ = 0
88__TASK_FORCE_RUN__ = 1
99
1010_LOGGER = logging .getLogger (__name__ )
1111
12-
1312def _safe_run (func ):
1413 """
1514 Execute a function wrapped in a try-except block.
@@ -30,6 +29,26 @@ def _safe_run(func):
3029 _LOGGER .debug ('Original traceback:' , exc_info = True )
3130 return False
3231
32+ async def _safe_run_async (func ):
33+ """
34+ Execute a function wrapped in a try-except block.
35+
36+ If anything goes wrong returns false instead of propagating the exception.
37+
38+ :param func: Function to be executed, receives no arguments and it's return
39+ value is ignored.
40+ """
41+ try :
42+ await func ()
43+ return True
44+ except Exception : # pylint: disable=broad-except
45+ # Catch any exception that might happen to avoid the periodic task
46+ # from ending and allowing for a recovery, as well as preventing
47+ # an exception from propagating and breaking the main thread
48+ _LOGGER .error ('Something went wrong when running passed function.' )
49+ _LOGGER .debug ('Original traceback:' , exc_info = True )
50+ return False
51+
3352
3453class AsyncTask (object ): # pylint: disable=too-many-instance-attributes
3554 """
@@ -166,3 +185,133 @@ def force_execution(self):
166185 def running (self ):
167186 """Return whether the task is running or not."""
168187 return self ._running
188+
189+
190+ class AsyncTaskAsync (object ): # pylint: disable=too-many-instance-attributes
191+ """
192+ Asyncrhonous controllable task async class.
193+
194+ This class creates is used to wrap around a function to treat it as a
195+ periodic task. This task can be stopped, it's execution can be forced, and
196+ it's status (whether it's running or not) can be obtained from the task
197+ object.
198+ It also allows for "on init" and "on stop" functions to be passed.
199+ """
200+
201+
202+ def __init__ (self , main , period , on_init = None , on_stop = None ):
203+ """
204+ Class constructor.
205+
206+ :param main: Main function to be executed periodically
207+ :type main: callable
208+ :param period: How many seconds to wait between executions
209+ :type period: int
210+ :param on_init: Function to be executed ONCE before the main one
211+ :type on_init: callable
212+ :param on_stop: Function to be executed ONCE after the task has finished
213+ :type on_stop: callable
214+ """
215+ self ._on_init = on_init
216+ self ._main = main
217+ self ._on_stop = on_stop
218+ self ._period = period
219+ self ._messages = asyncio .Queue ()
220+ self ._running = False
221+ self ._completion_event = None
222+
223+ async def _execution_wrapper (self ):
224+ """
225+ Execute user defined function in separate thread.
226+
227+ It will execute the "on init" hook is available. If an exception is
228+ raised it will abort execution, otherwise it will enter an infinite
229+ loop in which the main function is executed every <period> seconds.
230+ After stop has been called the "on stop" hook will be invoked if
231+ available.
232+
233+ All custom functions are run within a _safe_run() function which
234+ prevents exceptions from being propagated.
235+ """
236+ try :
237+ if self ._on_init is not None :
238+ if not await _safe_run_async (self ._on_init ):
239+ _LOGGER .error ("Error running task initialization function, aborting execution" )
240+ self ._running = False
241+ return
242+ self ._running = True
243+
244+ while self ._running :
245+ try :
246+ msg = self ._messages .get_nowait ()
247+ if msg == __TASK_STOP__ :
248+ _LOGGER .debug ("Stop signal received. finishing task execution" )
249+ break
250+ elif msg == __TASK_FORCE_RUN__ :
251+ _LOGGER .debug ("Force execution signal received. Running now" )
252+ if not await _safe_run_async (self ._main ):
253+ _LOGGER .error ("An error occurred when executing the task. "
254+ "Retrying after perio expires" )
255+ continue
256+ except asyncio .QueueEmpty :
257+ # If no message was received, the timeout has expired
258+ # and we're ready for a new execution
259+ pass
260+ except asyncio .CancelledError :
261+ break
262+
263+ await asyncio .sleep (self ._period )
264+ if not await _safe_run_async (self ._main ):
265+ _LOGGER .error (
266+ "An error occurred when executing the task. "
267+ "Retrying after period expires"
268+ )
269+ finally :
270+ await self ._cleanup ()
271+
272+ async def _cleanup (self ):
273+ """Execute on_stop callback, set event if needed, update status."""
274+ if self ._on_stop is not None :
275+ if not await _safe_run_async (self ._on_stop ):
276+ _LOGGER .error ("An error occurred when executing the task's OnStop hook. " )
277+
278+ self ._running = False
279+ self ._completion_event .set ()
280+
281+ def start (self ):
282+ """Start the async task."""
283+ if self ._running :
284+ _LOGGER .warning ("Task is already running. Ignoring .start() call" )
285+ return
286+ # Start execution
287+ self ._completion_event = asyncio .Event ()
288+ asyncio .get_running_loop ().create_task (self ._execution_wrapper ())
289+
290+ async def stop (self , wait_for_completion = False ):
291+ """
292+ Send a signal to the thread in order to stop it. If the task is not running do nothing.
293+
294+ Optionally accept an event to be set upon task completion.
295+
296+ :param event: Event to set when the task completes.
297+ :type event: threading.Event
298+ """
299+ if not self ._running :
300+ return
301+
302+ # Queue is of infinite size, should not raise an exception
303+ self ._messages .put_nowait (__TASK_STOP__ )
304+
305+ if wait_for_completion :
306+ await self ._completion_event .wait ()
307+
308+ def force_execution (self ):
309+ """Force an execution of the task without waiting for the period to end."""
310+ if not self ._running :
311+ return
312+ # Queue is of infinite size, should not raise an exception
313+ self ._messages .put_nowait (__TASK_FORCE_RUN__ )
314+
315+ def running (self ):
316+ """Return whether the task is running or not."""
317+ return self ._running
0 commit comments