Merge branch 'master' of https://github.com/davewiththenicehat/evennia into davewiththenicehat-master

This commit is contained in:
Griatch 2021-05-09 13:16:29 +02:00
commit cec566be79
4 changed files with 695 additions and 56 deletions

View file

@ -70,11 +70,11 @@ class SlowExit(DefaultExit):
traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed))
# create a delayed movement
deferred = utils.delay(move_delay, move_callback)
t = utils.delay(move_delay, move_callback)
# we store the deferred on the character, this will allow us
# to abort the movement. We must use an ndb here since
# deferreds cannot be pickled.
traversing_object.ndb.currently_moving = deferred
traversing_object.ndb.currently_moving = t
#

View file

@ -6,6 +6,7 @@ from datetime import datetime, timedelta
from twisted.internet import reactor
from twisted.internet.task import deferLater
from twisted.internet.defer import CancelledError as DefCancelledError
from evennia.server.models import ServerConfig
from evennia.utils.logger import log_err
from evennia.utils.dbserialize import dbserialize, dbunserialize
@ -13,31 +14,213 @@ from evennia.utils.dbserialize import dbserialize, dbunserialize
TASK_HANDLER = None
class TaskHandler(object):
def handle_error(*args, **kwargs):
"""Handle errors within deferred objects."""
for arg in args:
# suppress cancel errors
if arg.type == DefCancelledError:
continue
raise arg
class TaskHandlerTask:
"""An object to represent a single TaskHandler task.
Instance Attributes:
task_id (int): the global id for this task
deferred (deferred): a reference to this task's deferred
Property Attributes:
paused (bool): check if the deferred instance of a task has been paused.
called(self): A task attribute to check if the deferred instance of a task has been called.
Methods:
pause(): Pause the callback of a task.
unpause(): Process all callbacks made since pause() was called.
do_task(): Execute the task (call its callback).
call(): Call the callback of this task.
remove(): Remove a task without executing it.
cancel(): Stop a task from automatically executing.
active(): Check if a task is active (has not been called yet).
exists(): Check if a task exists.
get_id(): Returns the global id for this task. For use with
"""
A light singleton wrapper allowing to access permanent tasks.
def __init__(self, task_id):
self.task_id = task_id
self.deferred = TASK_HANDLER.get_deferred(task_id)
def get_deferred(self):
"""Return the instance of the deferred the task id is using.
Returns:
bool or deferred: An instance of a deferred or False if there is no task with the id.
None is returned if there is no deferred affiliated with this id.
"""
return TASK_HANDLER.get_deferred(self.task_id)
def pause(self):
"""Pause the callback of a task.
To resume use TaskHandlerTask.unpause
"""
d = self.deferred
if d:
d.pause()
def unpause(self):
"""Unpause a task, run the task if it has passed delay time."""
d = self.deferred
if d:
d.unpause()
@property
def paused(self):
"""A task attribute to check if the deferred instance of a task has been paused.
This exists to mock usage of a twisted deferred object.
Returns:
bool or None: True if the task was properly paused. None if the task does not have
a deferred instance.
"""
d = self.deferred
if d:
return d.paused
else:
return None
def do_task(self):
"""Execute the task (call its callback).
If calling before timedelay, cancel the deferred instance affliated to this task.
Remove the task from the dictionary of current tasks on a successful
callback.
Returns:
bool or any: Set to `False` if the task does not exist in task
handler. Otherwise it will be the return of the task's callback.
"""
return TASK_HANDLER.do_task(self.task_id)
def call(self):
"""Call the callback of a task.
Leave the task unaffected otherwise.
This does not use the task's deferred instance.
The only requirement is that the task exist in task handler.
Returns:
bool or any: Set to `False` if the task does not exist in task
handler. Otherwise it will be the return of the task's callback.
"""
return TASK_HANDLER.call_task(self.task_id)
def remove(self):
"""Remove a task without executing it.
Deletes the instance of the task's deferred.
Args:
task_id (int): an existing task ID.
Returns:
bool: True if the removal completed successfully.
"""
return TASK_HANDLER.remove(self.task_id)
def cancel(self):
"""Stop a task from automatically executing.
This will not remove the task.
Returns:
bool: True if the cancel completed successfully.
False if the cancel did not complete successfully.
"""
return TASK_HANDLER.cancel(self.task_id)
def active(self):
"""Check if a task is active (has not been called yet).
Returns:
bool: True if a task is active (has not been called yet). False if
it is not (has been called) or if the task does not exist.
"""
return TASK_HANDLER.active(self.task_id)
@property
def called(self):
"""
A task attribute to check if the deferred instance of a task has been called.
This exists to mock usage of a twisted deferred object.
It will not set to True if Task.call has been called. This only happens if
task's deferred instance calls the callback.
Returns:
bool: True if the deferred instance of this task has called the callback.
False if the deferred instnace of this task has not called the callback.
"""
d = self.deferred
if d:
return d.called
else:
return None
def exists(self):
"""Check if a task exists.
Most task handler methods check for existence for you.
Returns:
bool: True the task exists False if it does not.
"""
return TASK_HANDLER.exists(self.task_id)
def get_id(self):
""" Returns the global id for this task. For use with
`evennia.scripts.taskhandler.TASK_HANDLER`.
Returns:
task_id (int): global task id for this task.
"""
return self.task_id
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.
the task.
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.
Task handler will automatically remove uncalled but canceled from task
handler. By default this will not occur until a canceled task
has been uncalled for 60 second after the time it should have been called.
To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0
stale tasks will not be automatically removed.
This is not done on a timer. I is done as new tasks are added or the load method is called.
"""
def __init__(self):
self.tasks = {}
self.to_save = {}
self.clock = reactor
# number of seconds before an uncalled canceled task is removed from TaskHandler
self.stale_timeout = 60
self._now = False # used in unit testing to manually set now time
def load(self):
"""Load from the ServerConfig.
Note:
This should be automatically called when Evennia starts.
It populates `self.tasks` according to the ServerConfig.
This should be automatically called when Evennia starts.
It populates `self.tasks` according to the ServerConfig.
"""
to_save = False
@ -58,16 +241,42 @@ class TaskHandler(object):
continue
callback = getattr(obj, method)
self.tasks[task_id] = (date, callback, args, kwargs)
self.tasks[task_id] = (date, callback, args, kwargs, True, None)
if self.stale_timeout > 0: # cleanup stale tasks.
self.clean_stale_tasks()
if to_save:
self.save()
def clean_stale_tasks(self):
"""remove uncalled but canceled from task handler.
By default this will not occur until a canceled task
has been uncalled for 60 second after the time it should have been called.
To adjust this time use TASK_HANDLER.stale_timeout.
"""
clean_ids = []
for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items():
if not self.active(task_id):
stale_date = date + timedelta(seconds=self.stale_timeout)
# if a now time is provided use it (intended for unit testing)
now = self._now if self._now else datetime.now()
# the task was canceled more than stale_timeout seconds ago
if now > stale_date:
clean_ids.append(task_id)
for task_id in clean_ids:
self.remove(task_id)
return True
def save(self):
"""Save the tasks in ServerConfig."""
for task_id, (date, callback, args, kwargs) in self.tasks.items():
for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items():
if task_id in self.to_save:
continue
if not persistent:
continue
if getattr(callback, "__self__", None):
# `callback` is an instance method
@ -93,31 +302,49 @@ class TaskHandler(object):
ServerConfig.objects.conf("delayed_tasks", self.to_save)
def add(self, timedelay, callback, *args, **kwargs):
"""Add a new persistent task in the configuration.
"""Add a new task.
If the persistent kwarg is truthy:
The callback, args and values for kwarg will be serialized. Type
and attribute errors during the serialization will be logged,
but will not throw exceptions.
For persistent tasks do not use memory references in the callback
function or arguments. After a restart those memory references are no
longer accurate.
Args:
timedelay (int or float): time in sedconds before calling the callback.
timedelay (int or float): time in seconds before calling the callback.
callback (function or instance method): the callback itself
any (any): any additional positional arguments to send to the callback
*args: positional arguments to pass to callback.
**kwargs: keyword arguments to pass to callback.
persistent (bool, optional): persist the task (stores it).
persistent key and value is removed from kwargs it will
not be passed to callback.
Keyword Args:
persistent (bool, optional): persist the task (store it).
any (any): additional keyword arguments to send to the callback
Returns:
TaskHandlerTask: An object to represent a task.
Reference evennia.scripts.taskhandler.TaskHandlerTask for complete details.
"""
persistent = kwargs.get("persistent", False)
if persistent:
del kwargs["persistent"]
now = datetime.now()
delta = timedelta(seconds=timedelay)
# set the completion time
# Only used on persistent tasks after a restart
now = datetime.now()
delta = timedelta(seconds=timedelay)
comp_time = now + delta
# get an open task id
used_ids = list(self.tasks.keys())
task_id = 1
while task_id in used_ids:
task_id += 1
# Choose a free task_id
# record the task to the tasks dictionary
persistent = kwargs.get("persistent", False)
if "persistent" in kwargs:
del kwargs["persistent"]
if persistent:
safe_args = []
safe_kwargs = {}
used_ids = list(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:
@ -144,59 +371,219 @@ class TaskHandler(object):
else:
safe_kwargs[key] = value
self.tasks[task_id] = (now + delta, callback, safe_args, safe_kwargs)
self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, persistent, None)
self.save()
callback = self.do_task
args = [task_id]
kwargs = {}
else: # this is a non-persitent task
self.tasks[task_id] = (comp_time, callback, args, kwargs, persistent, None)
return deferLater(reactor, timedelay, callback, *args, **kwargs)
# defer the task
callback = self.do_task
args = [task_id]
kwargs = {}
d = deferLater(self.clock, timedelay, callback, *args, **kwargs)
d.addErrback(handle_error)
def remove(self, task_id):
"""Remove a persistent task without executing it.
# some tasks may complete before the deferred can be added
if task_id in self.tasks:
task = self.tasks.get(task_id)
task = list(task)
task[4] = persistent
task[5] = d
self.tasks[task_id] = task
else: # the task already completed
return False
if self.stale_timeout > 0:
self.clean_stale_tasks()
return TaskHandlerTask(task_id)
def exists(self, task_id):
"""Check if a task exists.
Most task handler methods check for existence for you.
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.
Returns:
bool: True the task exists False if it does not.
"""
del self.tasks[task_id]
if task_id in self.tasks:
return True
else:
return False
def active(self, task_id):
"""Check if a task is active (has not been called yet).
Args:
task_id (int): an existing task ID.
Returns:
bool: True if a task is active (has not been called yet). False if
it is not (has been called) or if the task does not exist.
"""
if task_id in self.tasks:
# if the task has not been run, cancel it
deferred = self.get_deferred(task_id)
return not (deferred and deferred.called)
else:
return False
def cancel(self, task_id):
"""Stop a task from automatically executing.
This will not remove the task.
Args:
task_id (int): an existing task ID.
Returns:
bool: True if the cancel completed successfully.
False if the cancel did not complete successfully.
"""
if task_id in self.tasks:
# if the task has not been run, cancel it
d = self.get_deferred(task_id)
if d: # it is remotely possible for a task to not have a deferred
if d.called:
return False
else: # the callback has not been called yet.
d.cancel()
return True
else: # this task has no deferred instance
return False
else:
return False
def remove(self, task_id):
"""Remove a task without executing it.
Deletes the instance of the task's deferred.
Args:
task_id (int): an existing task ID.
Returns:
bool: True if the removal completed successfully.
"""
d = None
# delete the task from the tasks dictionary
if task_id in self.tasks:
# if the task has not been run, cancel it
self.cancel(task_id)
del self.tasks[task_id] # delete the task from the tasks dictionary
# remove the task from the persistent dictionary and ServerConfig
if task_id in self.to_save:
del self.to_save[task_id]
self.save() # remove from ServerConfig.objects
# delete the instance of the deferred
if d:
del d
return True
self.save()
def clear(self, save=True, cancel=True):
"""clear all tasks.
By default tasks are canceled and removed from the database also.
Args:
save=True (bool): Should changes to persistent tasks be saved to database.
cancel=True (bool): Cancel scheduled tasks before removing it from task handler.
Returns:
True (bool): if the removal completed successfully.
"""
if self.tasks:
for task_id in self.tasks.keys():
if cancel:
self.cancel(task_id)
self.tasks = {}
if self.to_save:
self.to_save = {}
if save:
self.save()
return True
def call_task(self, task_id):
"""Call the callback of a task.
Leave the task unaffected otherwise.
This does not use the task's deferred instance.
The only requirement is that the task exist in task handler.
Args:
task_id (int): an existing task ID.
Returns:
bool or any: Set to `False` if the task does not exist in task
handler. Otherwise it will be the return of the task's callback.
"""
if task_id in self.tasks:
date, callback, args, kwargs, persistent, d = self.tasks.get(task_id)
else: # the task does not exist
return False
return callback(*args, **kwargs)
def do_task(self, task_id):
"""Execute the task (call its callback).
If calling before timedelay cancel the deferred instance affliated to this task.
Remove the task from the dictionary of current tasks on a successful
callback.
Args:
task_id (int): a valid task ID.
Note:
This will also remove it from the list of current tasks.
Returns:
bool or any: Set to `False` if the task does not exist in task
handler. Otherwise it will be the return of the task's callback.
"""
date, callback, args, kwargs = self.tasks.pop(task_id)
if task_id in self.to_save:
del self.to_save[task_id]
callback_return = False
if task_id in self.tasks:
date, callback, args, kwargs, persistent, d = self.tasks.get(task_id)
else: # the task does not exist
return False
if d: # it is remotely possible for a task to not have a deferred
if not d.called: # the task's deferred has not been called yet
d.cancel() # cancel the automated callback
else: # this task has no deferred, and should not be called
return False
callback_return = callback(*args, **kwargs)
self.remove(task_id)
return callback_return
self.save()
callback(*args, **kwargs)
def get_deferred(self, task_id):
"""
Return the instance of the deferred the task id is using.
Args:
task_id (int): a valid task ID.
Returns:
bool or deferred: An instance of a deferred or False if there is no task with the id.
None is returned if there is no deferred affiliated with this id.
"""
if task_id in self.tasks:
return self.tasks[task_id][5]
else:
return None
def create_delays(self):
"""Create the delayed tasks for the persistent tasks.
Note:
This method should be automatically called when Evennia starts.
This method should be automatically called when Evennia starts.
"""
now = datetime.now()
for task_id, (date, callbac, args, kwargs) in self.tasks.items():
for task_id, (date, callback, args, kwargs, _, _) in self.tasks.items():
self.tasks[task_id] = date, callback, args, kwargs, True, None
seconds = max(0, (date - now).total_seconds())
deferLater(reactor, seconds, self.do_task, task_id)
d = deferLater(self.clock, seconds, self.do_task, task_id)
d.addErrback(handle_error)
# some tasks may complete before the deferred can be added
if self.tasks.get(task_id, False):
self.tasks[task_id] = date, callback, args, kwargs, True, d
# Create the soft singleton

View file

@ -6,13 +6,16 @@ TODO: Not nearly all utilities are covered yet.
"""
import os.path
import mock
from datetime import datetime, timedelta
from django.test import TestCase
from datetime import datetime
from twisted.internet import task
from evennia.utils.ansi import ANSIString
from evennia.utils import utils
from evennia.utils.test_resources import EvenniaTest
class TestIsIter(TestCase):
@ -292,3 +295,241 @@ class LatinifyTest(TestCase):
byte_str = utils.to_bytes(self.example_str)
result = utils.latinify(byte_str)
self.assertEqual(result, self.expected_output)
_TASK_HANDLER = None
def dummy_func(obj):
"""
Used in TestDelay.
A function that:
can be serialized
uses no memory references
uses evennia objects
"""
# get a reference of object
from evennia.objects.models import ObjectDB
obj = ObjectDB.objects.object_search(obj)
obj = obj[0]
# make changes to object
obj.ndb.dummy_var = 'dummy_func ran'
return True
class TestDelay(EvenniaTest):
"""
Test utils.delay.
"""
def setUp(self):
super().setUp()
# get a reference of TASK_HANDLER
self.timedelay = 5
global _TASK_HANDLER
if _TASK_HANDLER is None:
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
_TASK_HANDLER.clock = task.Clock()
self.char1.ndb.dummy_var = False
def tearDown(self):
super().tearDown()
_TASK_HANDLER.clear()
def test_call_early(self):
# call a task early with call
for pers in (True, False):
t = utils.delay(self.timedelay, dummy_func, self.char1.dbref, persistent=pers)
result = t.call()
self.assertTrue(result)
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
self.assertTrue(t.exists())
self.assertTrue(t.active())
self.char1.ndb.dummy_var = False
def test_do_task(self):
# call the task early with do_task
for pers in (True, False):
t = utils.delay(self.timedelay, dummy_func, self.char1.dbref, persistent=pers)
# call the task early to test Task.call and TaskHandler.call_task
result = t.do_task()
self.assertTrue(result)
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
self.assertFalse(t.exists())
self.char1.ndb.dummy_var = False
def test_deferred_call(self):
# wait for deferred to call
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
self.assertTrue(t.active())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
self.assertFalse(t.exists())
self.char1.ndb.dummy_var = False
def test_short_deferred_call(self):
# wait for deferred to call with a very short time
timedelay = .1
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
self.assertTrue(t.active())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
self.assertFalse(t.exists())
self.char1.ndb.dummy_var = False
def test_active(self):
timedelay = self.timedelay
t = utils.delay(timedelay, dummy_func, self.char1.dbref)
self.assertTrue(_TASK_HANDLER.active(t.get_id()))
self.assertTrue(t.active())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertFalse(_TASK_HANDLER.active(t.get_id()))
self.assertFalse(t.active())
def test_called(self):
timedelay = self.timedelay
t = utils.delay(timedelay, dummy_func, self.char1.dbref)
self.assertFalse(t.called)
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertTrue(t.called)
def test_cancel(self):
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
self.assertTrue(t.active())
success = t.cancel()
self.assertFalse(t.active())
self.assertTrue(success)
self.assertTrue(t.exists())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertEqual(self.char1.ndb.dummy_var, False)
def test_remove(self):
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
self.assertTrue(t.active())
success = t.remove()
self.assertTrue(success)
self.assertFalse(t.active())
self.assertFalse(t.exists())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertEqual(self.char1.ndb.dummy_var, False)
def test_remove_canceled(self):
# remove a canceled task
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
self.assertTrue(t.active())
success = t.cancel()
self.assertTrue(success)
self.assertTrue(t.exists())
self.assertFalse(t.active())
success = t.remove()
self.assertTrue(success)
self.assertFalse(t.exists())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertEqual(self.char1.ndb.dummy_var, False)
def test_pause_unpause(self):
# remove a canceled task
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
self.assertTrue(t.active())
t.pause()
self.assertTrue(t.paused)
t.unpause()
self.assertFalse(t.paused)
self.assertEqual(self.char1.ndb.dummy_var, False)
t.pause()
_TASK_HANDLER.clock.advance(timedelay) # make time pass
self.assertEqual(self.char1.ndb.dummy_var, False)
t.unpause()
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')
self.char1.ndb.dummy_var = False
def test_auto_stale_task_removal(self):
# automated removal of stale tasks.
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
t.cancel()
self.assertFalse(t.active())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
if pers:
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
# Make task handler's now time, after the stale timeout
_TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1)
# add a task to test automatic removal
t2 = utils.delay(timedelay, dummy_func, self.char1.dbref)
if pers:
self.assertFalse(t.get_id() in _TASK_HANDLER.to_save)
self.assertFalse(t.get_id() in _TASK_HANDLER.tasks)
self.assertEqual(self.char1.ndb.dummy_var, False)
_TASK_HANDLER.clear()
def test_manual_stale_task_removal(self):
# manual removal of stale tasks.
timedelay = self.timedelay
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
t.cancel()
self.assertFalse(t.active())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
if pers:
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
# Make task handler's now time, after the stale timeout
_TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1)
_TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method
if pers:
self.assertFalse(t.get_id() in _TASK_HANDLER.to_save)
self.assertFalse(t.get_id() in _TASK_HANDLER.tasks)
self.assertEqual(self.char1.ndb.dummy_var, False)
_TASK_HANDLER.clear()
def test_disable_stale_removal(self):
# manual removal of stale tasks.
timedelay = self.timedelay
_TASK_HANDLER.stale_timeout = 0
for pers in (False, True):
t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers)
t.cancel()
self.assertFalse(t.active())
_TASK_HANDLER.clock.advance(timedelay) # make time pass
if pers:
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
# Make task handler's now time, after the stale timeout
_TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1)
t2 = utils.delay(timedelay, dummy_func, self.char1.dbref)
if pers:
self.assertTrue(t.get_id() in _TASK_HANDLER.to_save)
self.assertTrue(t.get_id() in _TASK_HANDLER.tasks)
self.assertEqual(self.char1.ndb.dummy_var, False)
# manual removal should still work
_TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method
if pers:
self.assertFalse(t.get_id() in _TASK_HANDLER.to_save)
self.assertFalse(t.get_id() in _TASK_HANDLER.tasks)
_TASK_HANDLER.clear()
def test_server_restart(self):
# emulate a server restart
timedelay = self.timedelay
utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True)
_TASK_HANDLER.clear(False) # remove all tasks from task handler, do not save this change.
_TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time
self.assertEqual(self.char1.ndb.dummy_var, False) # task has not run
_TASK_HANDLER.load() # load persistent tasks from database.
_TASK_HANDLER.create_delays() # create new deffered instances from persistent tasks
_TASK_HANDLER.clock.advance(timedelay) # Clock must advance to trigger, even if past timedelay
self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran')

View file

@ -1022,7 +1022,7 @@ _TASK_HANDLER = None
def delay(timedelay, callback, *args, **kwargs):
"""
Delay the return of a value.
Delay the calling of a callback (function).
Args:
timedelay (int or float): The delay in seconds
@ -1040,15 +1040,26 @@ def delay(timedelay, callback, *args, **kwargs):
commandhandler callback chain, the callback chain can be
defined directly in the command body and don't need to be
specified here.
Reference twisted.internet.defer.Deferred
if persistent kwarg is truthy:
task_id (int): the task's id intended for use with
evennia.scripts.taskhandler.TASK_HANDLER's do_task and remove methods.
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,
and other keyword arguments will be saved (serialized) 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).
Keep in mind that persistent tasks arguments and callback should not
use memory references.
If persistent is set to True the delay function will return an int
which is the task's id itended for use with TASK_HANDLER's do_task
and remove methods.
All task's whose time delays have passed will be called on server startup.
"""
global _TASK_HANDLER