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

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

## 63 statements 62 run 1 missing 0 excluded 2 partial

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

3 import time

4 import tornado.ioloop

5 from crontab import CronTab

6 from gramex.transforms import build_transform

7 from gramex.config import app_log, ioloop_running

8

9

10 class 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