diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 9ccb0ea307..214d1e3f8a 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -32,18 +32,13 @@ class TaskHandler(object): 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 creates the task. It saves the delay time, callback, - arguments and kwa to the database. Each of these variables are - serialized to do this. - 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. - - Dev notes: - deferLater creates an instance of IDelayedCall using reactor.callLater. - deferLater uses the cancel method on the IDelayedCall instance to create + 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. """ @@ -51,6 +46,9 @@ class TaskHandler(object): 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. @@ -80,11 +78,37 @@ class TaskHandler(object): callback = getattr(obj, method) 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.""" + """ + Save the tasks in ServerConfig. + """ + for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): if task_id in self.to_save: continue @@ -209,6 +233,8 @@ class TaskHandler(object): self.tasks[task_id] = task else: # the task already completed return False + if self.stale_timeout > 0: + self.clean_stale_tasks() return Task(task_id) def exists(self, task_id): @@ -392,10 +418,28 @@ TASK_HANDLER = TaskHandler() class Task: """ - A light + A 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 + Propert Attributes: + paused (bool): check if the deferral of a task has been paused. + called(self): A task attribute to check if the deferral 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). + 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 """ def __init__(self, task_id): self.task_id = task_id + self.deferred = TASK_HANDLER.get_deferred(task_id) def get_deferred(self): """ @@ -412,7 +456,7 @@ class Task: Pause the callback of a task. To resume use Task.unpause """ - d = TASK_HANDLER.get_deferred(self.task_id) + d = self.deferred if d: d.pause() @@ -420,7 +464,7 @@ class Task: """ Process all callbacks made since pause() was called. """ - d = TASK_HANDLER.get_deferred(self.task_id) + d = self.deferred if d: d.unpause() @@ -434,7 +478,7 @@ class Task: This will return None if the deferred object for the task does not exist or if the task no longer exists. """ - d = TASK_HANDLER.get_deferred(self.task_id) + d = self.deferred if d: return d.paused else: @@ -511,7 +555,11 @@ class Task: It will not set to false if Task.call has been called. """ - return not TASK_HANDLER.active(self.task_id) + d = self.deferred + if d: + return d.called + else: + return None def exists(self): """ diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 9bf252d0a8..53ec661521 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -7,6 +7,7 @@ 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 @@ -413,5 +414,53 @@ class TestDelay(EvenniaTest): t.unpause() self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False - # restart condictions - # cancel a diferall directly, without calling task handler's cancel + # test automated removal of stale tasks. + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + t.cancel() + task_id = t.get_id() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertTrue(task_id in _TASK_HANDLER.to_save) + self.assertTrue(task_id in _TASK_HANDLER.tasks) + # add a task to test automatic removal + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout + t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertFalse(task_id in _TASK_HANDLER.to_save) + self.assertFalse(task_id in _TASK_HANDLER.tasks) + self.assertEqual(self.char1.ndb.dummy_var, False) + # test manual cleanup + t2.cancel() + _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=30) # set TaskHandler's time to 30 seconnds from now + task_id = t2.get_id() + # test before stale_timeout time + _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method + # still in the task handler because stale timeout has not been reached + self.assertTrue(task_id in _TASK_HANDLER.to_save) + self.assertTrue(task_id in _TASK_HANDLER.tasks) + # advance past stale timeout + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout + _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method + self.assertFalse(task_id in _TASK_HANDLER.to_save) + self.assertFalse(task_id in _TASK_HANDLER.tasks) + self.char1.ndb.dummy_var = False + _TASK_HANDLER._now = False + # if _TASK_HANDLER.stale_timeout is 0 or less, automatic cleanup should not run + _TASK_HANDLER.stale_timeout = 0 + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + t.cancel() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # advance twisted's reactor past callback time + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + # add a task to test automatic removal + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout + t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + 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) + t.remove() + t2.remove() + self.char1.ndb.dummy_var = False + _TASK_HANDLER._now = False + # replicate a restart