First version of a reworked tickerhandler. It will now also repeat normal functions in a module, not just methods on a database object. This means a backwards incompatible change, and API - old tickerhandler repeats will not restore properly with this. Currently untested.

This commit is contained in:
Griatch 2016-03-20 23:12:00 +01:00
parent 8090d92d85
commit 77b178bf28

View file

@ -54,6 +54,7 @@ a custom handler one can make a custom `AT_STARTSTOP_MODULE` entry to
call the handler's `save()` and `restore()` methods when the server reboots. call the handler's `save()` and `restore()` methods when the server reboots.
""" """
import inspect
from builtins import object from builtins import object
from future.utils import listvalues from future.utils import listvalues
@ -63,15 +64,15 @@ from evennia.scripts.scripts import ExtendedLoopingCall
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.utils.logger import log_trace, log_err from evennia.utils.logger import log_trace, log_err
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj
from evennia.utils import variable_from_module
_GA = object.__getattribute__ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
_ERROR_ADD_INTERVAL = \ _ERROR_ADD_TICKER = \
"""TickerHandler: Tried to add a ticker with invalid interval: """TickerHandler: Tried to add an invalid ticker:
obj={obj}, interval={interval}, args={args}, kwargs={kwargs} {storekey}
store_key={store_key}
Ticker was not added.""" Ticker was not added."""
class Ticker(object): class Ticker(object):
@ -98,14 +99,22 @@ class Ticker(object):
kwargs is used here to identify which hook method to call. kwargs is used here to identify which hook method to call.
""" """
for store_key, (obj, args, kwargs) in self.subscriptions.items(): for store_key, (args, kwargs) in self.subscriptions.iteritems():
hook_key = yield kwargs.pop("_hook_key", "at_tick") callback = yield kwargs.pop("_callback", "at_tick")
if not obj or not obj.pk:
# object was deleted between calls
self.remove(store_key)
continue
try: try:
yield _GA(obj, hook_key)(*args, **kwargs) if callable(callback):
# call directly
yield callback(*args, **kwargs)
return
# try object method
obj = yield kwargs.pop("_obj", None)
if not obj or not obj.pk:
# object was deleted between calls
self.remove(store_key)
continue
else:
yield _GA(obj, callback)(*args, **kwargs)
except ObjectDoesNotExist: except ObjectDoesNotExist:
log_trace() log_trace()
self.remove(store_key) self.remove(store_key)
@ -113,7 +122,7 @@ class Ticker(object):
log_trace() log_trace()
finally: finally:
# make sure to re-store # make sure to re-store
kwargs["_hook_key"] = hook_key kwargs["_callback"] = callback
def __init__(self, interval): def __init__(self, interval):
""" """
@ -138,34 +147,27 @@ class Ticker(object):
""" """
subs = self.subscriptions subs = self.subscriptions
if None in subs.values():
# clean out objects that may have been deleted
subs = dict((store_key, obj) for store_key, obj in subs if obj)
self.subscriptions = subs
if self.task.running: if self.task.running:
if not subs: if not subs:
self.task.stop() self.task.stop()
elif subs: elif subs:
self.task.start(self.interval, now=False, start_delay=start_delay) self.task.start(self.interval, now=False, start_delay=start_delay)
def add(self, store_key, obj, *args, **kwargs): def add(self, store_key, *args, **kwargs):
""" """
Sign up a subscriber to this ticker. Sign up a subscriber to this ticker.
Args: Args:
store_key (str): Unique storage hash for this ticker subscription. store_key (str): Unique storage hash for this ticker subscription.
obj (Object): Object subscribing to this ticker.
args (any, optional): Arguments to call the hook method with. args (any, optional): Arguments to call the hook method with.
Kwargs: Kwargs:
_start_delay (int): If set, this will be _start_delay (int): If set, this will be
used to delay the start of the trigger instead of used to delay the start of the trigger instead of
`interval`. `interval`.
_hooK_key (str): This carries the name of the hook method
to call. It is passed on as-is from this method.
""" """
start_delay = kwargs.pop("_start_delay", None) start_delay = kwargs.pop("_start_delay", None)
self.subscriptions[store_key] = (obj, args, kwargs) self.subscriptions[store_key] = (args, kwargs)
self.validate(start_delay=start_delay) self.validate(start_delay=start_delay)
def remove(self, store_key): def remove(self, store_key):
@ -204,46 +206,33 @@ class TickerPool(object):
""" """
self.tickers = {} self.tickers = {}
def add(self, store_key, obj, interval, *args, **kwargs): def add(self, store_key, *args, **kwargs):
""" """
Add new ticker subscriber. Add new ticker subscriber.
Args: Args:
store_key (str): Unique storage hash. store_key (str): Unique storage hash.
obj (Object): Object subscribing.
interval (int): How often to call the ticker.
args (any, optional): Arguments to send to the hook method. args (any, optional): Arguments to send to the hook method.
Kwargs:
_start_delay (int): If set, this will be
used to delay the start of the trigger instead of
`interval`. It is passed on as-is from this method.
_hooK_key (str): This carries the name of the hook method
to call. It is passed on as-is from this method.
""" """
_, _, _, interval, _ = store_key
if not interval: if not interval:
log_err(_ERROR_ADD_INTERVAL.format(store_key=store_key, obj=obj, log_err(_ERROR_ADD_TICKER.format(store_key=store_key))
interval=interval, args=args, kwargs=kwargs))
return return
if interval not in self.tickers: if interval not in self.tickers:
self.tickers[interval] = self.ticker_class(interval) self.tickers[interval] = self.ticker_class(interval)
self.tickers[interval].add(store_key, obj, *args, **kwargs) self.tickers[interval].add(store_key, *args, **kwargs)
def remove(self, store_key, interval): def remove(self, store_key):
""" """
Remove subscription from pool. Remove subscription from pool.
Args: Args:
store_key (str): Unique storage hash. store_key (str): Unique storage hash to remove
interval (int): Ticker interval.
Notes:
A given subscription is uniquely identified both
via its `store_key` and its `interval`.
""" """
_, _, _, interval, _ = store_key
if interval in self.tickers: if interval in self.tickers:
self.tickers[interval].remove(store_key) self.tickers[interval].remove(store_key)
@ -285,33 +274,65 @@ class TickerHandler(object):
self.save_name = save_name self.save_name = save_name
self.ticker_pool = self.ticker_pool_class() self.ticker_pool = self.ticker_pool_class()
def _store_key(self, obj, interval, idstring=""): def _get_callback(callback):
"""
Analyze callback and determine its consituents
Args:
callback (function or method): This is either a stand-alone
function or class method on a typeclassed entitye (that is,
an entity that can be saved to the database).
Returns:
ret (tuple): This is a tuple of the form `(obj, path, callfunc)`,
where `obj` is the database object the callback is defined on
if it's a method (otherwise `None`) and vice-versa, `path` is
the python-path to the stand-alone function (`None` if a method).
The `callfunc` is either the name of the method to call or the
callable function object itself.
"""
outobj, outpath, outcallfunc = None, None, None
if callable(callback):
if inspect.ismethod(callback):
outobj = callback.im_self
outcallfunc = callback.im_func.func_name
elif inspect.isfunction(callback):
outpath = "%s.%s" % (callback.__module__, callback.func_name)
outcallfunc = callback
else:
raise TypeError("%s is not a callable function or method." % callback)
return outobj, outpath, outcallfunc
def _store_key(self, obj, path, interval, callfunc, idstring=""):
""" """
Tries to create a store_key for the object. Returns a tuple Tries to create a store_key for the object. Returns a tuple
(isdb, store_key) where isdb is a boolean True if obj was a (isdb, store_key) where isdb is a boolean True if obj was a
database object, False otherwise. database object, False otherwise.
Args: Args:
obj (Object): Subscribing object. obj (Object or None): Subscribing object if any.
interval (int): Ticker interval path (str or None): Python-path to callable, if any.
interval (int): Ticker interval.
callfunc (callable or str): This is either the callable function or
the name of the method to call. Note that the callable is never
stored in the key; that is uniquely identified with the python-path.
idstring (str, optional): Additional separator between idstring (str, optional): Additional separator between
different subscription types. different subscription types.
Returns:
isdb_and_store_key (tuple): A tuple `(obj, path, interval,
methodname, idstring)` that uniquely identifies the
ticker. `path` is `None` and `methodname` is the name of
the method if `obj_or_path` is a database object.
Vice-versa, `obj` and `methodname` are `None` if
`obj_or_path` is a python-path.
""" """
if hasattr(obj, "db_key"): outobj = pack_dbobj(obj) if obj and hasattr(obj, "db_key") else None
# create a store_key using the database representation outpath = path if isinstance(basestring, path) else None
objkey = pack_dbobj(obj) methodname = callfunc if callfunc and isinstance(basestring, callfunc) else None
isdb = True return (outobj, methodname, outpath, interval, idstring)
else:
# non-db object, look for a property "key" on it, otherwise
# use its memory location.
try:
objkey = _GA(obj, "key")
except AttributeError:
objkey = id(obj)
isdb = False
# return sidb and store_key
return isdb, (objkey, interval, idstring)
def save(self): def save(self):
""" """
@ -346,82 +367,82 @@ class TickerHandler(object):
# load stored command instructions and use them to re-initialize handler # load stored command instructions and use them to re-initialize handler
ticker_storage = ServerConfig.objects.conf(key=self.save_name) ticker_storage = ServerConfig.objects.conf(key=self.save_name)
if ticker_storage: if ticker_storage:
# the dbunserialize will convert all serialized dbobjs to real objects
self.ticker_storage = dbunserialize(ticker_storage) self.ticker_storage = dbunserialize(ticker_storage)
for store_key, (args, kwargs) in self.ticker_storage.items(): for store_key, (args, kwargs) in self.ticker_storage.iteritems():
obj, interval, idstring = store_key try:
_, store_key = self._store_key(obj, interval, idstring) obj, methodname, path, interval, idstring = store_key
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) if obj and methodname:
kwargs["_callable"] = methodname
kwargs["_obj"] = obj
elif path:
modname, varname = path.rsplit(".", 1)
callback = variable_from_module(modname, varname)
kwargs["_callable"] = callback
kwargs["_obj"] = None
except Exception as err:
# this suggests a malformed save or missing objects
log_err("%s\nTickerhandler: Removing malformed ticker: %s" % (err, str(store_key)))
continue
self.ticker_pool.add(store_key, *args, **kwargs)
def add(self, obj, interval, idstring="", hook_key="at_tick", *args, **kwargs): def add(self, interval=60, callback=None, idstring="", *args, **kwargs):
""" """
Add object to tickerhandler Add object to tickerhandler
Args: Args:
obj (Object): The object to subscribe to the ticker. interval (int, optional): Interval in seconds between calling
interval (int): Interval in seconds between calling `callable(*args, **kwargs)`
`hook_key` below. callable (callable function or method, optional): This
should either be a stand-alone function or a method on a
typeclassed entity (that is, one that can be saved to the
database).
idstring (str, optional): Identifier for separating idstring (str, optional): Identifier for separating
this ticker-subscription from others with the same this ticker-subscription from others with the same
interval. Allows for managing multiple calls with interval. Allows for managing multiple calls with
the same time interval the same time interval and callback.
hook_key (str, optional): The name of the hook method
on `obj` to call every `interval` seconds. Defaults to
`at_tick(*args, **kwargs`. All hook methods must
always accept *args, **kwargs.
args, kwargs (optional): These will be passed into the args, kwargs (optional): These will be passed into the
method given by `hook_key` every time it is called. callback every time it is called.
Notes: Notes:
The combination of `obj`, `interval` and `idstring` The callback will be identified by type and stored either as
together uniquely defines the ticker subscription. They as combination of serialized database object + methodname or
must all be supplied in order to unsubscribe from it as a python-path to the module + funcname. These strings will
later. be combined iwth `interval` and `idstring` to define a
unique storage key for saving. These must thus all be supplied
when wanting to modify/remove the ticker later.
""" """
isdb, store_key = self._store_key(obj, interval, idstring) obj, path, callfunc = self._get_callback(callback)
if isdb: store_key = self._store_key(obj, path, interval, callfunc, idstring)
self.ticker_storage[store_key] = (args, kwargs) self.ticker_storage[store_key] = (args, kwargs)
self.save() self.save()
kwargs["_hook_key"] = hook_key kwargs["_obj"] = obj
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) kwargs["_callable"] = callfunc # either method-name or callable
self.ticker_pool.add(store_key, *args, **kwargs)
def remove(self, obj, interval=None, idstring=""): def remove(self, interval=60, callback=None, idstring=""):
""" """
Remove object from ticker or only remove it from tickers with Remove object from ticker or only remove it from tickers with
a given interval. a given interval.
Args: Args:
obj (Object): The object subscribing to the ticker. interval (int, optional): Interval of ticker to remove.
interval (int, optional): Interval of ticker to remove. If callback (callable function or method): Either a function or
`None`, all tickers on this object matching `idstring` the method of a typeclassed object.
will be removed, regardless of their `interval` setting.
idstring (str, optional): Identifier id of ticker to remove. idstring (str, optional): Identifier id of ticker to remove.
""" """
if interval: obj, path, callfunc = self._get_callback(callback)
isdb, store_key = self._store_key(obj, interval, idstring) store_key = self._store_key(obj, path, interval, callfunc, idstring)
if isdb: to_remove = self.ticker_storage.pop(store_key, None)
self.ticker_storage.pop(store_key, None) if to_remove:
self.save() self.ticker_pool.remove(store_key)
self.ticker_pool.remove(store_key, interval) self.save()
else:
# remove all objects with any intervals
intervals = list(self.ticker_pool.tickers)
should_save = False
for interval in intervals:
isdb, store_key = self._store_key(obj, interval, idstring)
if isdb:
self.ticker_storage.pop(store_key, None)
should_save = True
self.ticker_pool.remove(store_key, interval)
if should_save:
self.save()
def clear(self, interval=None): def clear(self, interval=None):
""" """
Stop/remove all tickers from handler. Stop/remove tickers from handler.
Args: Args:
interval (int): Only stop tickers with this interval. interval (int): Only stop tickers with this interval.
@ -464,6 +485,5 @@ class TickerHandler(object):
if ticker: if ticker:
return listvalues(ticker.subscriptions) return listvalues(ticker.subscriptions)
# main tickerhandler # main tickerhandler
TICKER_HANDLER = TickerHandler() TICKER_HANDLER = TickerHandler()