def add(self, cb, *args, **kwargs): """Adds a new periodic callback to the current worker. Returns a :py:class:`.Watcher` if added successfully or the value ``None`` if not (or raises a ``ValueError`` if the callback is not correctly formed and/or decorated). :param cb: a callable object/method/function previously decorated with the :py:func:`.periodic` decorator :type cb: callable """ if not six.callable(cb): raise ValueError("Periodic callback %r must be callable" % cb) missing_attrs = _check_attrs(cb) if missing_attrs: raise ValueError("Periodic callback %r missing required" " attributes %s" % (cb, missing_attrs)) if not cb._is_periodic: return None now = self._now_func() with self._waiter: cb_index = len(self._works) cb_metrics = self._INITIAL_METRICS.copy() work = Work(utils.get_callback_name(cb), cb, args, kwargs) watcher = Watcher(cb_metrics, work) self._works.append(work) self._watchers.append((cb_metrics, watcher)) if cb._periodic_run_immediately: self._immediates.append(cb_index) else: next_run = self._initial_schedule_strategy(cb, now) self._schedule.push(next_run, cb_index) self._waiter.notify_all() return watcher
def add(self, cb, *args, **kwargs): """Adds a new periodic callback to the current worker. Returns a :py:class:`.Watcher` if added successfully or the value ``None`` if not (or raises a ``ValueError`` if the callback is not correctly formed and/or decorated). :param cb: a callable object/method/function previously decorated with the :py:func:`.periodic` decorator :type cb: callable """ if not six.callable(cb): raise ValueError("Periodic callback %r must be callable" % cb) missing_attrs = _check_attrs(cb) if missing_attrs: raise ValueError("Periodic callback %r missing required" " attributes %s" % (cb, missing_attrs)) if not cb._is_periodic: return None now = self._now_func() with self._waiter: cb_index = len(self._callables) cb_name = utils.get_callback_name(cb) cb_metrics = self._INITIAL_METRICS.copy() watcher = Watcher(cb_metrics) self._callables.append((cb, cb_name, args, kwargs)) self._watchers.append((cb_metrics, watcher)) if cb._periodic_run_immediately: self._immediates.append(cb_index) else: next_run = self._initial_schedule_strategy(cb, now) self._schedule.push(next_run, cb_index) self._waiter.notify_all() return watcher
def _on_failure_log(log, cb, kind, spacing, exc_info, traceback=None): cb_name = utils.get_callback_name(cb) if all(exc_info) or not traceback: log.error("Failed to call %s '%s' (it runs every %0.2f" " seconds)", kind, cb_name, spacing, exc_info=exc_info) else: log.error("Failed to call %s '%s' (it runs every %0.2f" " seconds):\n%s", kind, cb_name, spacing, traceback)
def _on_failure_log(log, cb, kind, spacing, exc_info, traceback=None): cb_name = utils.get_callback_name(cb) if all(exc_info) or not traceback: log.error("Failed to call %s '%s' (it runs every %0.2f" " seconds)", kind, cb_name, spacing, exc_info=exc_info) else: log.error( "Failed to call %s '%s' (it runs every %0.2f" " seconds):\n%s", kind, cb_name, spacing, traceback)
def __init__(self, callables, log=None, executor_factory=None, cond_cls=threading.Condition, event_cls=threading.Event, schedule_strategy='last_started', now_func=utils.now, on_failure=None): """Creates a new worker using the given periodic callables. :param callables: a iterable of tuple objects previously decorated with the :py:func:`.periodic` decorator, each item in the iterable is expected to be in the format of ``(cb, args, kwargs)`` where ``cb`` is the decorated function and ``args`` and ``kwargs`` are any positional and keyword arguments to send into the callback when it is activated (both ``args`` and ``kwargs`` may be provided as none to avoid using them) :type callables: iterable :param log: logger to use when creating a new worker (defaults to the module logger if none provided), it is currently only used to report callback failures (if they occur) :type log: logger :param executor_factory: factory callable that can be used to generate executor objects that will be used to run the periodic callables (if none is provided one will be created that uses the :py:class:`~futurist.SynchronousExecutor` class) :type executor_factory: ExecutorFactory or any callable :param cond_cls: callable object that can produce ``threading.Condition`` (or compatible/equivalent) objects :type cond_cls: callable :param event_cls: callable object that can produce ``threading.Event`` (or compatible/equivalent) objects :type event_cls: callable :param schedule_strategy: string to select one of the built-in strategies that can return the next time a callable should run :type schedule_strategy: string :param now_func: callable that can return the current time offset from some point (used in calculating elapsed times and next times to run); preferably this is monotonically increasing :type now_func: callable :param on_failure: callable that will be called whenever a periodic function fails with an error, it will be provided four positional arguments and one keyword argument, the first positional argument being the callable that failed, the second being the type of activity under which it failed (``IMMEDIATE`` or ``PERIODIC``), the third being the spacing that the callable runs at and the fourth ``exc_info`` tuple of the failure. The keyword argument ``traceback`` will also be provided that may be be a string that caused the failure (this is required for executors which run out of process, as those can not *currently* transfer stack frames across process boundaries); if no callable is provided then a default failure logging function will be used instead (do note that any user provided callable should not raise exceptions on being called) :type on_failure: callable """ if on_failure is not None and not six.callable(on_failure): raise ValueError("On failure callback %r must be" " callable" % on_failure) self._tombstone = event_cls() self._waiter = cond_cls() self._dead = event_cls() self._active = event_cls() self._cond_cls = cond_cls self._watchers = [] self._works = [] for (cb, args, kwargs) in callables: if not six.callable(cb): raise ValueError("Periodic callback %r must be callable" % cb) missing_attrs = _check_attrs(cb) if missing_attrs: raise ValueError("Periodic callback %r missing required" " attributes %s" % (cb, missing_attrs)) if cb._is_periodic: # Ensure these aren't none and if so replace them with # something more appropriate... if args is None: args = self._NO_OP_ARGS if kwargs is None: kwargs = self._NO_OP_KWARGS.copy() cb_metrics = self._INITIAL_METRICS.copy() work = Work(utils.get_callback_name(cb), cb, args, kwargs) watcher = Watcher(cb_metrics, work) self._works.append(work) self._watchers.append((cb_metrics, watcher)) try: strategy = self.BUILT_IN_STRATEGIES[schedule_strategy] self._schedule_strategy = strategy[0] self._initial_schedule_strategy = strategy[1] except KeyError: valid_strategies = sorted(self.BUILT_IN_STRATEGIES.keys()) raise ValueError("Scheduling strategy '%s' must be one of" " %s selectable strategies" % (schedule_strategy, valid_strategies)) self._immediates, self._schedule = _build( now_func, self._works, self._initial_schedule_strategy) self._log = log or LOG if executor_factory is None: executor_factory = lambda: futurist.SynchronousExecutor() if on_failure is None: on_failure = functools.partial(_on_failure_log, self._log) self._on_failure = on_failure self._executor_factory = executor_factory self._now_func = now_func
def __init__(self, callables, log=None, executor_factory=None, cond_cls=threading.Condition, event_cls=threading.Event, schedule_strategy='last_started', now_func=utils.now, on_failure=None): """Creates a new worker using the given periodic callables. :param callables: a iterable of tuple objects previously decorated with the :py:func:`.periodic` decorator, each item in the iterable is expected to be in the format of ``(cb, args, kwargs)`` where ``cb`` is the decorated function and ``args`` and ``kwargs`` are any positional and keyword arguments to send into the callback when it is activated (both ``args`` and ``kwargs`` may be provided as none to avoid using them) :type callables: iterable :param log: logger to use when creating a new worker (defaults to the module logger if none provided), it is currently only used to report callback failures (if they occur) :type log: logger :param executor_factory: factory callable that can be used to generate executor objects that will be used to run the periodic callables (if none is provided one will be created that uses the :py:class:`~futurist.SynchronousExecutor` class) :type executor_factory: ExecutorFactory or any callable :param cond_cls: callable object that can produce ``threading.Condition`` (or compatible/equivalent) objects :type cond_cls: callable :param event_cls: callable object that can produce ``threading.Event`` (or compatible/equivalent) objects :type event_cls: callable :param schedule_strategy: string to select one of the built-in strategies that can return the next time a callable should run :type schedule_strategy: string :param now_func: callable that can return the current time offset from some point (used in calculating elapsed times and next times to run); preferably this is monotonically increasing :type now_func: callable :param on_failure: callable that will be called whenever a periodic function fails with an error, it will be provided four positional arguments and one keyword argument, the first positional argument being the callable that failed, the second being the type of activity under which it failed (``IMMEDIATE`` or ``PERIODIC``), the third being the spacing that the callable runs at and the fourth ``exc_info`` tuple of the failure. The keyword argument ``traceback`` will also be provided that may be be a string that caused the failure (this is required for executors which run out of process, as those can not *currently* transfer stack frames across process boundaries); if no callable is provided then a default failure logging function will be used instead (do note that any user provided callable should not raise exceptions on being called) :type on_failure: callable """ if on_failure is not None and not six.callable(on_failure): raise ValueError("On failure callback %r must be" " callable" % on_failure) self._tombstone = event_cls() self._waiter = cond_cls() self._dead = event_cls() self._active = event_cls() self._cond_cls = cond_cls self._watchers = [] self._callables = [] for (cb, args, kwargs) in callables: if not six.callable(cb): raise ValueError("Periodic callback %r must be callable" % cb) missing_attrs = _check_attrs(cb) if missing_attrs: raise ValueError("Periodic callback %r missing required" " attributes %s" % (cb, missing_attrs)) if cb._is_periodic: # Ensure these aren't none and if so replace them with # something more appropriate... if args is None: args = self._NO_OP_ARGS if kwargs is None: kwargs = self._NO_OP_KWARGS cb_name = utils.get_callback_name(cb) cb_metrics = self._INITIAL_METRICS.copy() watcher = Watcher(cb_metrics) self._callables.append((cb, cb_name, args, kwargs)) self._watchers.append((cb_metrics, watcher)) try: strategy = self.BUILT_IN_STRATEGIES[schedule_strategy] self._schedule_strategy = strategy[0] self._initial_schedule_strategy = strategy[1] except KeyError: valid_strategies = sorted(self.BUILT_IN_STRATEGIES.keys()) raise ValueError("Scheduling strategy '%s' must be one of" " %s selectable strategies" % (schedule_strategy, valid_strategies)) self._immediates, self._schedule = _build( now_func, self._callables, self._initial_schedule_strategy) self._log = log or LOG if executor_factory is None: executor_factory = lambda: futurist.SynchronousExecutor() if on_failure is None: on_failure = functools.partial(_on_failure_log, self._log) self._on_failure = on_failure self._executor_factory = executor_factory self._now_func = now_func