diff --git a/platypush/message/request/__init__.py b/platypush/message/request/__init__.py index 50838a8f..174a91fd 100644 --- a/platypush/message/request/__init__.py +++ b/platypush/message/request/__init__.py @@ -190,7 +190,7 @@ class Request(Message): - group: ${group_name} # will be expanded as "Kitchen lights") """ - def _thread_func(n_tries): + def _thread_func(n_tries, errors=None): if self.action.startswith('procedure.'): context['n_tries'] = n_tries response = self._execute_procedure(**context) @@ -207,18 +207,25 @@ class Request(Message): response = plugin.run(method=method_name, **args) if response and response.is_error(): - raise RuntimeError('Response processed with errors: {}'.format(response)) - - logger.info('Processed response from plugin {}: {}'. - format(plugin, str(response))) + logger.warning(('Response processed with errors from ' + + 'action {}.{}: {}').format( + plugin, self.action, str(response))) + else: + logger.info('Processed response from action {}.{}: {}'. + format(plugin, self.action, str(response))) except Exception as e: # Retry mechanism - response = Response(output=None, errors=[str(e), traceback.format_exc()]) - logger.exception(e) - if n_tries: + logger.warning(('Uncaught exception while processing response ' + + 'from action {}.{}: {}').format( + plugin, self.action, str(e))) + + errors = errors or [] + errors.append(str(e)) + response = Response(output=None, errors=errors) + if n_tries-1 > 0: logger.info('Reloading plugin {} and retrying'.format(module_name)) get_plugin(module_name, reload=True) - response = _thread_func(n_tries-1) + response = _thread_func(n_tries=n_tries-1, errors=errors) finally: self._send_response(response) return response diff --git a/platypush/plugins/__init__.py b/platypush/plugins/__init__.py index fa9b56b2..22c7f03c 100644 --- a/platypush/plugins/__init__.py +++ b/platypush/plugins/__init__.py @@ -29,7 +29,8 @@ def action(f): except Exception as e: if isinstance(args[0], Plugin): args[0].logger.exception(e) - errors.append(str(e) + '\n' + traceback.format_exc()) + raise e + # errors.append(str(e) + '\n' + traceback.format_exc()) return Response(output=output, errors=errors) diff --git a/platypush/plugins/utils.py b/platypush/plugins/utils.py index f1627790..90f0807b 100644 --- a/platypush/plugins/utils.py +++ b/platypush/plugins/utils.py @@ -1,6 +1,8 @@ +import threading import time from platypush.plugins import Plugin, action +from platypush.procedure import Procedure class UtilsPlugin(Plugin): @@ -8,6 +10,17 @@ class UtilsPlugin(Plugin): A plugin for general-purpose util methods """ + _DEFAULT_TIMEOUT_PREFIX = '_PlatypushTimeout_' + _timeout_hndl_idx = 0 + _timeout_hndl_idx_lock = threading.RLock() + + _DEFAULT_INTERVAL_PREFIX = '_PlatypushInterval_' + _interval_hndl_idx = 0 + _interval_hndl_idx_lock = threading.RLock() + + _pending_intervals = {} + _pending_intervals_lock = threading.RLock() + @action def sleep(self, seconds): """ @@ -19,6 +32,114 @@ class UtilsPlugin(Plugin): time.sleep(seconds) + @action + def set_timeout(self, seconds, actions, name=None, **args): + """ + Define a set of actions to run after the specified amount of `seconds`. + + :param seconds: Number of seconds before running the timeout procedure + :type seconds: float + + :param actions: List of actions to be executed after the timeout expires + :type actions: list[dict] + + :param name: Set an optional name for this timeout. It is advised to set + a name if you are planning to programmatically cancel the timeout in + your business logic. + :type name: str + + :param args: Optional arguments/context to pass to the timeout function + """ + + with self._timeout_hndl_idx_lock: + self._timeout_hndl_idx += 1 + if not name: + name = self._DEFAULT_TIMEOUT_PREFIX + str(self._timeout_hndl_idx) + if name in self._pending_timeouts: + return (None, + "A timeout named '{}' is already awaiting".format(name)) + + + procedure = Procedure.build(name=name, requests=actions, _async=False) + self._pending_timeouts[name] = procedure + + def _proc_wrapper(**kwargs): + procedure.execute(**kwargs) + + with self._pending_timeouts_lock: + self._pending_timeouts[name] = threading.Timer(seconds, + _proc_wrapper, + kwargs=args) + self._pending_timeouts[name].start() + + @action + def clear_timeout(self, name): + timer = None + + with self._pending_timeouts_lock: + if name not in self._pending_timeouts: + return + timer = self._pending_timeouts.pop(name) + + timer.cancel() + + + @action + def set_interval(self, seconds, actions, name=None, **args): + """ + Define a set of actions to run each specified amount of `seconds`. + + :param seconds: Number of seconds between two runs of the interval + procedure + :type seconds: float + + :param actions: List of actions to be executed at each interval + :type actions: list[dict] + + :param name: Set an optional name for this interval. It is advised to + set a name if you are planning to programmatically cancel the + interval in your business logic. + :type name: str + + :param args: Optional arguments/context to pass to the interval function + """ + + with self._interval_hndl_idx_lock: + self._interval_hndl_idx += 1 + if not name: + name = self._DEFAULT_INTERVAL_PREFIX + \ + str(self._interval_hndl_idx) + + if name in self._pending_intervals: + return (None, + "An interval named '{}' is already running".format(name)) + + + procedure = Procedure.build(name=name, requests=actions, _async=False) + self._pending_intervals[name] = procedure + + def _proc_wrapper(**kwargs): + while True: + with self._pending_intervals_lock: + if name not in self._pending_intervals: + return + + procedure.execute(**kwargs) + time.sleep(seconds) + + with self._pending_intervals_lock: + self._pending_intervals[name] = threading.Thread( + target=_proc_wrapper, kwargs=args) + self._pending_intervals[name].start() + + @action + def clear_interval(self, name): + interval = None + + with self._pending_intervals_lock: + if name not in self._pending_intervals: + return + del self._pending_intervals[name] + # vim:sw=4:ts=4:et: -