
To be able to easily plug-in future types of ways to get which topics (and tasks) workers exist on (and can perform) and to identify and keep this information up-to date refactor the functionality that currently does this using periodic messages into a finder type and a periodic function that exists on it (that will be periodically activated by an updated and improved periodic worker). Part of blueprint wbe-worker-info Change-Id: Ib3ae29758af3d244b4ac4624ac380caf88b159fd
180 lines
6.7 KiB
Python
180 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2015 Yahoo! Inc. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import heapq
|
|
import inspect
|
|
|
|
from oslo_utils import reflection
|
|
import six
|
|
|
|
from taskflow import logging
|
|
from taskflow.utils import misc
|
|
from taskflow.utils import threading_utils as tu
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
# Find a monotonic providing time (or fallback to using time.time()
|
|
# which isn't *always* accurate but will suffice).
|
|
_now = misc.find_monotonic(allow_time_time=True)
|
|
|
|
# Attributes expected on periodic tagged/decorated functions or methods...
|
|
_PERIODIC_ATTRS = tuple([
|
|
'_periodic',
|
|
'_periodic_spacing',
|
|
'_periodic_run_immediately',
|
|
])
|
|
|
|
|
|
def periodic(spacing, run_immediately=True):
|
|
"""Tags a method/function as wanting/able to execute periodically."""
|
|
|
|
if spacing <= 0:
|
|
raise ValueError("Periodicity/spacing must be greater than"
|
|
" zero instead of %s" % spacing)
|
|
|
|
def wrapper(f):
|
|
f._periodic = True
|
|
f._periodic_spacing = spacing
|
|
f._periodic_run_immediately = run_immediately
|
|
|
|
@six.wraps(f)
|
|
def decorator(*args, **kwargs):
|
|
return f(*args, **kwargs)
|
|
|
|
return decorator
|
|
|
|
return wrapper
|
|
|
|
|
|
class PeriodicWorker(object):
|
|
"""Calls a collection of callables periodically (sleeping as needed...).
|
|
|
|
NOTE(harlowja): typically the :py:meth:`.start` method is executed in a
|
|
background thread so that the periodic callables are executed in
|
|
the background/asynchronously (using the defined periods to determine
|
|
when each is called).
|
|
"""
|
|
|
|
@classmethod
|
|
def create(cls, objects, exclude_hidden=True):
|
|
"""Automatically creates a worker by analyzing object(s) methods.
|
|
|
|
Only picks up methods that have been tagged/decorated with
|
|
the :py:func:`.periodic` decorator (does not match against private
|
|
or protected methods unless explicitly requested to).
|
|
"""
|
|
callables = []
|
|
for obj in objects:
|
|
for (name, member) in inspect.getmembers(obj):
|
|
if name.startswith("_") and exclude_hidden:
|
|
continue
|
|
if reflection.is_bound_method(member):
|
|
consume = True
|
|
for attr_name in _PERIODIC_ATTRS:
|
|
if not hasattr(member, attr_name):
|
|
consume = False
|
|
break
|
|
if consume:
|
|
callables.append(member)
|
|
return cls(callables)
|
|
|
|
def __init__(self, callables, tombstone=None):
|
|
if tombstone is None:
|
|
self._tombstone = tu.Event()
|
|
else:
|
|
# Allows someone to share an event (if they so want to...)
|
|
self._tombstone = tombstone
|
|
almost_callables = list(callables)
|
|
for cb in almost_callables:
|
|
if not six.callable(cb):
|
|
raise ValueError("Periodic callback must be callable")
|
|
for attr_name in _PERIODIC_ATTRS:
|
|
if not hasattr(cb, attr_name):
|
|
raise ValueError("Periodic callback missing required"
|
|
" attribute '%s'" % attr_name)
|
|
self._callables = tuple((cb, reflection.get_callable_name(cb))
|
|
for cb in almost_callables)
|
|
self._schedule = []
|
|
self._immediates = []
|
|
now = _now()
|
|
for i, (cb, cb_name) in enumerate(self._callables):
|
|
spacing = getattr(cb, '_periodic_spacing')
|
|
next_run = now + spacing
|
|
heapq.heappush(self._schedule, (next_run, i))
|
|
for (cb, cb_name) in reversed(self._callables):
|
|
if getattr(cb, '_periodic_run_immediately', False):
|
|
self._immediates.append((cb, cb_name))
|
|
|
|
def __len__(self):
|
|
return len(self._callables)
|
|
|
|
@staticmethod
|
|
def _safe_call(cb, cb_name, kind='periodic'):
|
|
try:
|
|
cb()
|
|
except Exception:
|
|
LOG.warn("Failed to call %s callable '%s'",
|
|
kind, cb_name, exc_info=True)
|
|
|
|
def start(self):
|
|
"""Starts running (will not stop/return until the tombstone is set).
|
|
|
|
NOTE(harlowja): If this worker has no contained callables this raises
|
|
a runtime error and does not run since it is impossible to periodically
|
|
run nothing.
|
|
"""
|
|
if not self._callables:
|
|
raise RuntimeError("A periodic worker can not start"
|
|
" without any callables")
|
|
while not self._tombstone.is_set():
|
|
if self._immediates:
|
|
cb, cb_name = self._immediates.pop()
|
|
LOG.debug("Calling immediate callable '%s'", cb_name)
|
|
self._safe_call(cb, cb_name, kind='immediate')
|
|
else:
|
|
# Figure out when we should run next (by selecting the
|
|
# minimum item from the heap, where the minimum should be
|
|
# the callable that needs to run next and has the lowest
|
|
# next desired run time).
|
|
now = _now()
|
|
next_run, i = heapq.heappop(self._schedule)
|
|
when_next = next_run - now
|
|
if when_next <= 0:
|
|
cb, cb_name = self._callables[i]
|
|
spacing = getattr(cb, '_periodic_spacing')
|
|
LOG.debug("Calling periodic callable '%s' (it runs every"
|
|
" %s seconds)", cb_name, spacing)
|
|
self._safe_call(cb, cb_name)
|
|
# Run again someday...
|
|
next_run = now + spacing
|
|
heapq.heappush(self._schedule, (next_run, i))
|
|
else:
|
|
# Gotta wait...
|
|
heapq.heappush(self._schedule, (next_run, i))
|
|
self._tombstone.wait(when_next)
|
|
|
|
def stop(self):
|
|
"""Sets the tombstone (this stops any further executions)."""
|
|
self._tombstone.set()
|
|
|
|
def reset(self):
|
|
"""Resets the tombstone and re-queues up any immediate executions."""
|
|
self._tombstone.clear()
|
|
self._immediates = []
|
|
for (cb, cb_name) in reversed(self._callables):
|
|
if getattr(cb, '_periodic_run_immediately', False):
|
|
self._immediates.append((cb, cb_name))
|