Package horizons :: Module scheduler
[hide private]
[frames] | no frames]

Source Code for Module horizons.scheduler

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import logging 
 23  from collections import deque 
 24   
 25  import horizons.main 
 26  from horizons.constants import GAME 
 27  from horizons.util.living import LivingObject 
 28  from horizons.util.python.singleton import ManualConstructionSingleton 
 29   
 30   
31 -class Scheduler(LivingObject, metaclass=ManualConstructionSingleton):
32 """"Class providing timed callbacks. 33 Master of time. 34 35 TODO: 36 - Refactor to use a data structure that is suitable for iteration (ticking) as well as 37 searching/deleting by instance, possibly also by callback. 38 Suggestion: { tick -> { instance -> [callback] }} (basically a k-d tree) 39 40 41 @param timer: Timer instance the schedular registers itself with. 42 """ 43 44 log = logging.getLogger("scheduler") 45 46 # the tick with this id is actually executed, and no tick with a smaller number can occur 47 FIRST_TICK_ID = 0 48
49 - def __init__(self, timer):
50 """ 51 @param timer: Timer obj 52 """ 53 super().__init__() 54 self.schedule = {} 55 self.additional_cur_tick_schedule = [] # jobs to be executed at the same tick they were added 56 self.calls_by_instance = {} # for get_classinst_calls 57 self.cur_tick = self.__class__.FIRST_TICK_ID - 1 # before ticking 58 self.timer = timer 59 self.timer.add_call(self.tick)
60
61 - def end(self):
62 self.log.debug("Scheduler end; len: %s", len(self.schedule)) 63 self.schedule = None 64 self.timer.remove_call(self.tick) 65 self.timer = None 66 super().end()
67
68 - def tick(self, tick_id):
69 """Threads main loop 70 @param tick_id: int id of the tick. 71 """ 72 assert tick_id == self.cur_tick + 1 73 self.cur_tick = tick_id 74 75 if GAME.MAX_TICKS is not None and tick_id >= GAME.MAX_TICKS: 76 horizons.main.quit() 77 return 78 79 if self.cur_tick in self.schedule: 80 self.log.debug("Scheduler: tick %s, cbs: %s", self.cur_tick, len(self.schedule[self.cur_tick])) 81 82 # use iteration method that works in case the list is altered during iteration 83 # this can happen for e.g. rem_all_classinst_calls 84 cur_schedule = self.schedule[self.cur_tick] 85 while cur_schedule: 86 callback = cur_schedule.popleft() 87 # TODO: some system-level unit tests fail if this list is not processed in the correct order 88 # (i.e. if e.g. pop() was used here). This is an indication of invalid assumptions 89 # in the program and should be fixed. 90 91 if hasattr(callback, "invalid"): 92 self.log.debug("S(t:%s): %s: INVALID", tick_id, callback) 93 continue 94 self.log.debug("S(t:%s): %s", tick_id, callback) 95 callback.callback() 96 assert callback.loops >= -1 97 if callback.loops != 0: 98 self.add_object(callback, readd=True) 99 else: # gone for good 100 if callback.class_instance in self.calls_by_instance: 101 # this can already be removed by e.g. rem_all_classinst_calls 102 if callback.finish_callback is not None: 103 callback.finish_callback() 104 105 try: 106 self.calls_by_instance[callback.class_instance].remove(callback) 107 except ValueError: 108 pass # also the callback can be deleted by e.g. rem_call 109 del self.schedule[self.cur_tick] 110 111 self.log.debug("Scheduler: finished tick %s", self.cur_tick) 112 113 # run jobs added in the loop above 114 self._run_additional_jobs() 115 116 assert (not self.schedule) or next(iter(self.schedule.keys())) > self.cur_tick
117
118 - def before_ticking(self):
119 """Called after game load and before game has started. 120 Callbacks with run_in=0 are used as generic "do this as soon as the current context 121 is finished". If this is done during load, it is supposed to mean tick -1, since it 122 does not belong to the first tick. This method simulates this. 123 """ 124 self._run_additional_jobs()
125
126 - def _run_additional_jobs(self):
127 for callback in self.additional_cur_tick_schedule: 128 assert callback.loops == 0 # can't loop with no delay 129 callback.callback() 130 self.additional_cur_tick_schedule = []
131
132 - def add_object(self, callback_obj, readd=False):
133 """Adds a new CallbackObject instance to the callbacks list for the first time 134 @param callback_obj: CallbackObject type object, containing all necessary information 135 @param readd: Whether this object is added another time (looped) 136 """ 137 if callback_obj.loops > 0: 138 callback_obj.loops -= 1 139 if callback_obj.run_in == 0: # run in the current tick 140 self.additional_cur_tick_schedule.append(callback_obj) 141 else: # default: run in future tick 142 interval = callback_obj.loop_interval if readd else callback_obj.run_in 143 tick_key = self.cur_tick + interval 144 if tick_key not in self.schedule: 145 self.schedule[tick_key] = deque() 146 callback_obj.tick = tick_key 147 self.schedule[tick_key].append(callback_obj) 148 if not readd: # readded calls haven't been removed here 149 if callback_obj.class_instance not in self.calls_by_instance: 150 self.calls_by_instance[callback_obj.class_instance] = [] 151 self.calls_by_instance[callback_obj.class_instance].append(callback_obj)
152
153 - def add_new_object(self, callback, class_instance, run_in=1, loops=1, loop_interval=None, finish_callback=None):
154 """Creates a new CallbackObject instance and calls the self.add_object() function. 155 @param callback: lambda function callback, which is called run_in ticks. 156 @param class_instance: class instance the function belongs to. 157 @param run_in: int number of ticks after which the callback is called. Defaults to 1, run next tick. 158 @param loops: How often the callback is called. -1 = infinite times. Defaults to 1, run once. 159 @param loop_interval: Delay between subsequent loops in ticks. Defaults to run_in.""" 160 callback_obj = _CallbackObject(self, callback, class_instance, run_in, loops, loop_interval, finish_callback=finish_callback) 161 self.add_object(callback_obj)
162
163 - def rem_object(self, callback_obj):
164 """Removes a CallbackObject from all callback lists 165 @param callback_obj: CallbackObject to remove 166 @return: int, number of removed calls 167 """ 168 removed_objs = 0 169 if self.schedule is not None: 170 for key in self.schedule: 171 while callback_obj in self.schedule[key]: 172 self.schedule[key].remove(callback_obj) 173 self.calls_by_instance[callback_obj.class_instance].remove(callback_obj) 174 removed_objs += 1 175 176 if not self.calls_by_instance[callback_obj.class_instance]: 177 del self.calls_by_instance[callback_obj.class_instance] 178 179 return removed_objs
180
181 - def rem_all_classinst_calls(self, class_instance):
182 """Removes all callbacks from the scheduler that belong to the class instance class_inst.""" 183 """ 184 for key in self.schedule: 185 callback_objects = self.schedule[key] 186 for i in xrange(len(callback_objects) - 1, -1, -1): 187 if callback_objects[i].class_instance is class_instance: 188 del callback_objects[i] 189 """ 190 if class_instance in self.calls_by_instance: 191 for callback_obj in self.calls_by_instance[class_instance]: 192 callback_obj.invalid = True # don't remove, finding them all takes too long 193 del self.calls_by_instance[class_instance] 194 195 # filter additional callbacks as well 196 self.additional_cur_tick_schedule = \ 197 [cb for cb in self.additional_cur_tick_schedule 198 if cb.class_instance is not class_instance]
199
200 - def rem_call(self, instance, callback):
201 """Removes all callbacks of 'instance' that are 'callback' 202 @param instance: the instance that would execute the call 203 @param callback: the function to remove 204 @return: int, number of removed calls 205 """ 206 assert callable(callback) 207 removed_calls = 0 208 for key in self.schedule: 209 callback_objects = self.schedule[key] 210 for i in range(len(callback_objects) - 1, -1, -1): 211 if (callback_objects[i].class_instance is instance 212 and callback_objects[i].callback == callback 213 and not hasattr(callback_objects[i], "invalid")): 214 del callback_objects[i] 215 removed_calls += 1 216 217 test = 0 218 if removed_calls > 0: # there also must be calls in the calls_by_instance dict 219 for i in range(len(self.calls_by_instance[instance]) - 1, -1, -1): 220 obj = self.calls_by_instance[instance][i] 221 if obj.callback == callback: 222 del self.calls_by_instance[instance][i] 223 test += 1 224 assert test == removed_calls, "{}, {}".format(test, removed_calls) 225 if not self.calls_by_instance[instance]: 226 del self.calls_by_instance[instance] 227 228 for i in range(len(self.additional_cur_tick_schedule) - 1, -1, -1): 229 if self.additional_cur_tick_schedule[i].class_instance is instance and \ 230 self.additional_cur_tick_schedule[i].callback == callback: 231 del callback_objects[i] 232 removed_calls += 1 233 234 return removed_calls
235
236 - def get_classinst_calls(self, instance, callback=None):
237 """Returns all CallbackObjects of instance. 238 Optionally, a specific callback can be specified. 239 @param instance: the instance to execute the call 240 @param callback: None to get all calls of instance, 241 else only calls that execute callback. 242 @return: dict, entries: { CallbackObject: remaining_ticks_to_executing } 243 """ 244 calls = {} 245 if instance in self.calls_by_instance: 246 for callback_obj in self.calls_by_instance[instance]: 247 if callback is None or callback_obj.callback == callback: 248 calls[callback_obj] = callback_obj.tick - self.cur_tick 249 return calls
250
251 - def get_remaining_ticks(self, instance, callback, assert_present=True):
252 """Returns in how many ticks a callback is executed. You must specify 1 single call. 253 @param *: just like get_classinst_calls 254 @param assert_present: assert that there must be sucha call 255 @return int or possbile None if not assert_present""" 256 calls = self.get_classinst_calls(instance, callback) 257 if assert_present: 258 assert len(calls) == 1, 'got {:i} calls for {} {}: {}'\ 259 .format(len(calls), instance, callback, [str(i) for i in calls]) 260 return next(iter(calls.values())) 261 else: 262 return next(iter(calls.values())) if calls else None
263
264 - def get_ticks(self, seconds):
265 """Call propagated to time instance""" 266 return self.timer.get_ticks(seconds)
267
268 - def get_ticks_of_month(self):
270 271
272 -class _CallbackObject:
273 """Class used by the TimerManager Class to organize callbacks."""
274 - def __init__(self, scheduler, callback, class_instance, run_in, loops, loop_interval, finish_callback=None):
275 """Creates the CallbackObject instance. 276 @param scheduler: reference to the scheduler, necessary to react properly on weak reference callbacks 277 @see Scheduler.add_new_object 278 """ 279 assert run_in >= 0, "Can't schedule callbacks in the past, run_in must be a non negative number" 280 assert (loops > 0) or (loops == -1), \ 281 "Loop count must be a positive number or -1 for infinite repeat" 282 assert callable(callback) 283 assert loop_interval is None or loop_interval > 0 284 285 self.callback = callback 286 self.finish_callback = finish_callback 287 288 self.run_in = run_in 289 self.loops = loops 290 self.loop_interval = loop_interval if loop_interval is not None else run_in 291 self.class_instance = class_instance
292
293 - def __str__(self):
294 cb = str(self.callback) 295 if "_move_tick" in cb: # very crude measure to reduce log noise 296 return "(_move_tick,{})".format(self.class_instance.worldid) 297 298 return "SchedCb({} on {})".format(cb, self.class_instance)
299