Merge branch 'persistent_delay' of git://github.com/vlegoff/evennia into vlegoff-persistent_delay

This commit is contained in:
Griatch 2017-08-10 22:42:31 +02:00
commit 19bb66665b
3 changed files with 212 additions and 3 deletions

View file

@ -0,0 +1,188 @@
"""
Module containing the task handler for Evennia deferred tasks, persistent or not.
"""
from datetime import datetime, timedelta
from twisted.internet import reactor, task
from evennia.server.models import ServerConfig
from evennia.utils.logger import log_trace, log_err
from evennia.utils.dbserialize import dbserialize, dbunserialize
TASK_HANDLER = None
class TaskHandler(object):
"""
A light singleton wrapper allowing to access permanent tasks.
When `utils.delay` is called, the task handler is used to create
the task. If `utils.delay` is called with `persistent=True`, the
task handler stores the new task and saves.
It's easier to access these tasks (should it be necessary) using
`evennia.scripts.taskhandler.TASK_HANDLER`, which contains one
instance of this class, and use its `add` and `remove` methods.
"""
def __init__(self):
self.tasks = {}
self.to_save = {}
def load(self):
"""Load from the ServerConfig.
Note:
This should be automatically called when Evennia starts.
It populates `self.tasks` according to the ServerConfig.
"""
value = ServerConfig.objects.conf("delayed_tasks", default={})
if isinstance(value, basestring):
tasks = dbunserialize(value)
else:
tasks = value
# At this point, `tasks` contains a dictionary of still-serialized tasks
for task_id, value in tasks.items():
date, callback, args, kwargs = dbunserialize(value)
if isinstance(callback, tuple):
# `callback` can be an object and name for instance methods
obj, method = callback
callback = getattr(obj, method)
self.tasks[task_id] = (date, callback, args, kwargs)
def save(self):
"""Save the tasks in ServerConfig."""
for task_id, (date, callback, args, kwargs) in self.tasks.items():
if task_id in self.to_save:
continue
if getattr(callback, "__self__", None):
# `callback` is an instance method
obj = callback.__self__
name = callback.__name__
callback = (obj, name)
# Check if callback can be pickled. args and kwargs have been checked
safe_callback = None
try:
dbserialize(callback)
except (TypeError, AttributeError):
raise ValueError("the specified callback {} cannot be pickled. " \
"It must be a top-level function in a module or an " \
"instance method.".format(callback))
else:
safe_callback = callback
self.to_save[task_id] = dbserialize((date, safe_callback, args, kwargs))
ServerConfig.objects.conf("delayed_tasks", self.to_save)
def add(self, timedelay, callback, *args, **kwargs):
"""Add a new persistent task in the configuration.
Args:
timedelay (int or float): time in sedconds before calling the callback.
callback (function or instance method): the callback itself
any (any): any additional positional arguments to send to the callback
Kwargs:
persistent (bool, optional): persist the task (store it).
any (any): additional keyword arguments to send to the callback
"""
persistent = kwargs.get("persistent", False)
if persistent:
del kwargs["persistent"]
now = datetime.now()
delta = timedelta(seconds=timedelay)
# Choose a free task_id
safe_args = []
safe_kwargs = {}
used_ids = self.tasks.keys()
task_id = 1
while task_id in used_ids:
task_id += 1
# Check that args and kwargs contain picklable information
for arg in args:
try:
dbserialize(arg)
except (TypeError, AttributeError):
logger.log_err("The positional argument {} cannot be " \
"pickled and will not be present in the arguments " \
"fed to the callback {}".format(arg, callback))
else:
safe_args.append(arg)
for key, value in kwargs.items():
try:
dbserialize(value)
except (TypeError, AttributeError):
logger.log_err("The {} keyword argument {} cannot be " \
"pickled and will not be present in the arguments " \
"fed to the callback {}".format(key, value, callback))
else:
safe_kwargs[key] = value
self.tasks[task_id] = (now + delta, callback, safe_args, safe_kwargs)
self.save()
callback = self.do_task
args = [task_id]
kwargs = {}
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)
def remove(self, task_id):
"""Remove a persistent task without executing it.
Args:
task_id (int): an existing task ID.
Note:
A non-persistent task doesn't have a task_id, it is not stored
in the TaskHandler.
"""
del self.tasks[task_id]
if task_id in self.to_save:
del self.to_save[task_id]
self.save()
def do_task(self, task_id):
"""Execute the task (call its callback).
Args:
task_id (int): a valid task ID.
Note:
This will also remove it from the list of current tasks.
"""
date, callback, args, kwargs = self.tasks.pop(task_id)
if task_id in self.to_save:
del self.to_save[task_id]
self.save()
callback(*args, **kwargs)
def create_delays(self):
"""Create the delayed tasks for the persistent tasks.
Note:
This method should be automatically called when Evennia starts.
"""
now = datetime.now()
for task_id, (date, callbac, args, kwargs) in self.tasks.items():
seconds = max(0, (date - now).total_seconds())
task.deferLater(reactor, seconds, self.do_task, task_id)
# Create the soft singleton
TASK_HANDLER = TaskHandler()

View file

@ -455,6 +455,11 @@ class Evennia(object):
# (this also starts any that didn't yet start)
ScriptDB.objects.validate(init_mode=mode)
# start the task handler
from evennia.scripts.taskhandler import TASK_HANDLER
TASK_HANDLER.load()
TASK_HANDLER.create_delays()
# delete the temporary setting
ServerConfig.objects.conf("server_restart_mode", delete=True)

View file

@ -44,7 +44,6 @@ _DA = object.__delattr__
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
def is_iter(iterable):
"""
Checks if an object behaves iterably.
@ -920,6 +919,8 @@ def uses_database(name="sqlite3"):
return engine == "django.db.backends.%s" % name
_TASK_HANDLER = None
def delay(timedelay, callback, *args, **kwargs):
"""
Delay the return of a value.
@ -930,7 +931,9 @@ def delay(timedelay, callback, *args, **kwargs):
arguments after `timedelay` seconds.
args (any, optional): Will be used as arguments to callback
Kwargs:
any (any): Will be used to call the callback.
persistent (bool, optional): should make the delay persistent
over a reboot or reload
any (any): Will be used to call the callback.
Returns:
deferred (deferred): Will fire fire with callback after
@ -939,8 +942,21 @@ def delay(timedelay, callback, *args, **kwargs):
defined directly in the command body and don't need to be
specified here.
Note:
The task handler (`evennia.scripts.taskhandler.TASK_HANDLEr`) will
be called for persistent or non-persistent tasks.
If persistent is set to True, the callback, its arguments
and other keyword arguments will be saved in the database,
assuming they can be. The callback will be executed even after
a server restart/reload, taking into account the specified delay
(and server down time).
"""
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)
global _TASK_HANDLER
# Do some imports here to avoid circular import and speed things up
if _TASK_HANDLER is None:
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
return _TASK_HANDLER.add(timedelay, callback, *args, **kwargs)
_TYPECLASSMODELS = None