Hide keyboard shortcuts

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''' 

2 

3import time 

4import tornado.ioloop 

5from crontab import CronTab 

6from gramex.transforms import build_transform 

7from gramex.config import app_log, ioloop_running 

8 

9 

10class Task(object): 

11 '''Run a task. Then schedule it at the next occurrance.''' 

12 

13 def __init__(self, name, schedule, threadpool, ioloop=None): 

14 ''' 

15 Create a new task based on a schedule in ioloop (default to current). 

16 

17 The schedule configuration accepts: 

18 

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) 

34 

35 if self.thread: 

36 fn = self.function 

37 

38 def on_done(future): 

39 exception = future.exception(timeout=0) 

40 if exception: 

41 app_log.error('%s (thread): %s', name, exception) 

42 

43 def run_function(*args, **kwargs): 

44 future = threadpool.submit(fn, *args, **kwargs) 

45 future.add_done_callback(on_done) 

46 return future 

47 

48 self.function = run_function 

49 

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) 

60 

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() 

65 

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() 

75 

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) 

82 

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) 

91 

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