Add the persistent utils.delay

This commit is contained in:
Vincent Le Goff 2017-07-20 21:37:34 +02:00
parent f7830a5c29
commit 87b6cee596
3 changed files with 201 additions and 1 deletions

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 persistent tasks
from evennia.utils.persistent import PERSISTENT_TASKS
PERSISTENT_TASKS.load()
PERSISTENT_TASKS.create_delays()
# delete the temporary setting
ServerConfig.objects.conf("server_restart_mode", delete=True)

173
evennia/utils/persistent.py Normal file
View file

@ -0,0 +1,173 @@
"""
Module containing persistent features and the persistent differed
tasks, generated by utils.delay(persistent=True).
"""
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
PERSISTENT_TASKS = None
class PersistentTasks(object):
"""A light singleton wrapper allowing to access permanent tasks.
Permament tasks are created by `utils.delay` when the `persistent`
keyword argument is set to True. Tasks are saved in ServerConfig.
It's easier to access these tasks (should it be necessary) using
`evennia.utils.persistent.PERSISTSENT_TASKS`, which contains one
instance of this class, and use its `add and `remove` methods.
"""
def __init__(self):
self.tasks = {}
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 unserialized dictionary of tasks
for task_id, (date, callback, args, kwargs) in tasks.items():
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."""
to_save = {}
for task_id, (date, callback, args, kwargs) in self.tasks.items():
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
to_save[task_id] = (date, safe_callback, args, kwargs)
to_save = dbserialize(to_save)
ServerConfig.objects.conf("delayed_tasks", 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:
any (any): additional keyword arguments to send to the callback
Note:
This doesn't create any delay at this time to call the task,
but the task is written in ServerConfig along with the
others.
"""
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()
return task_id
def remove(self, task_id):
"""Remove a task without executing it.
Args:
task_id (int): an existing task ID.
"""
del self.tasks[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)
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 = (date - now).total_seconds()
if seconds < 0:
seconds = 0
task.deferLater(reactor, seconds, self.do_task, task_id)
# Create the soft singleton
PERSISTENT_TASKS = PersistentTasks()

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
_PERSISTENT_TASKS = None
def delay(timedelay, callback, *args, **kwargs):
"""
Delay the return of a value.
@ -930,6 +931,8 @@ def delay(timedelay, callback, *args, **kwargs):
arguments after `timedelay` seconds.
args (any, optional): Will be used as arguments to callback
Kwargs:
persistent (bool, optional): should make the delay persistent
over a reboot or reload
any (any): Will be used to call the callback.
Returns:
@ -939,7 +942,26 @@ def delay(timedelay, callback, *args, **kwargs):
defined directly in the command body and don't need to be
specified here.
Note:
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).
"""
global _PERSISTENT_TASKS
persistent = kwargs.get("persistent", False)
if persistent:
del kwargs["persistent"]
# Do some imports here to avoid circular import and speed things up
if _PERSISTENT_TASKS is None:
from evennia.utils.persistent import PERSISTENT_TASKS as _PERSISTENT_TASKS
task_id = _PERSISTENT_TASKS.add(timedelay, callback, *args, **kwargs)
callback = _PERSISTENT_TASKS.do_task
args = [task_id]
kwargs = {}
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)