Coverage for gramex\services\scheduler.py : 97%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1'''Gramex scheduling service'''
3import time
4import tornado.ioloop
5from crontab import CronTab
6from gramex.transforms import build_transform
7from gramex.config import app_log, ioloop_running
10class Task(object):
11 '''Run a task. Then schedule it at the next occurrance.'''
13 def __init__(self, name, schedule, threadpool, ioloop=None):
14 '''
15 Create a new task based on a schedule in ioloop (default to current).
17 The schedule configuration accepts:
19 - startup: True to run at startup, '*' to run on every config change
20 - minutes, hours, dates, months, weekdays, years: cron schedule
21 - thread: True to run in a separate thread
22 '''
23 self.name = name
24 self.utc = schedule.get('utc', False)
25 self.thread = schedule.get('thread', False)
26 if 'function' not in schedule: 26 ↛ 27line 26 didn't jump to line 27, because the condition on line 26 was never true
27 raise ValueError('schedule %s has no function:' % name)
28 if callable(schedule['function']):
29 self.function = schedule['function']
30 else:
31 self.function = build_transform(schedule, vars={}, filename='schedule:%s' % name)
32 self.ioloop = ioloop or tornado.ioloop.IOLoop.current()
33 self._call_later(None)
35 if self.thread:
36 fn = self.function
38 def on_done(future):
39 exception = future.exception(timeout=0)
40 if exception:
41 app_log.error('%s (thread): %s', name, exception)
43 def run_function(*args, **kwargs):
44 future = threadpool.submit(fn, *args, **kwargs)
45 future.add_done_callback(on_done)
46 return future
48 self.function = run_function
50 # Run on schedule if any of the schedule periods are specified
51 periods = 'minutes hours dates months weekdays years'.split()
52 if any(schedule.get(key) for key in periods):
53 # Convert all valid values into strings (e.g. 30 => '30'), and ignore any spaces
54 cron = (str(schedule.get(key, '*')).replace(' ', '') for key in periods)
55 self.cron_str = ' '.join(cron)
56 self.cron = CronTab(self.cron_str)
57 self.call_later()
58 elif not schedule.get('startup'):
59 app_log.warning('schedule:%s has no schedule nor startup', name)
61 # Run now if the task is to be run on startup. Don't re-run if the config was reloaded
62 startup = schedule.get('startup')
63 if startup == '*' or (startup is True and not ioloop_running(self.ioloop)):
64 self.function()
66 def run(self, *args, **kwargs):
67 '''Run task. Then set up next callback.'''
68 app_log.info('Running %s', self.name)
69 try:
70 self.result = self.function(*args, **kwargs)
71 finally:
72 # Run again, if not stopped via self.stop() or end of schedule
73 if self.callback is not None: 73 ↛ exitline 73 didn't return from function 'run', because the condition on line 73 was never false
74 self.call_later()
76 def stop(self):
77 '''Suspend task, clearing any pending callbacks'''
78 if self.callback is not None:
79 app_log.debug('Stopping %s', self.name)
80 self.ioloop.remove_timeout(self.callback)
81 self._call_later(None)
83 def call_later(self):
84 '''Schedule next run automatically. Clears any previous scheduled runs'''
85 delay = self.cron.next(default_utc=self.utc) if hasattr(self, 'cron') else None
86 self._call_later(delay)
87 if delay is not None:
88 app_log.debug('Scheduling %s after %.0fs', self.name, delay)
89 else:
90 app_log.debug('No further schedule for %s', self.name)
92 def _call_later(self, delay):
93 '''Schedule next run after delay seconds. If delay is None, no more runs.'''
94 if delay is not None:
95 if self.callback is not None:
96 self.ioloop.remove_timeout(self.callback)
97 self.callback = self.ioloop.call_later(delay, self.run)
98 self.next = time.time() + delay
99 else:
100 self.callback, self.next = None, None