beat.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import time
  2. import shelve
  3. import threading
  4. import multiprocessing
  5. from datetime import datetime
  6. from UserDict import UserDict
  7. from celery import log
  8. from celery import conf
  9. from celery import registry as _registry
  10. from celery.utils.info import humanize_seconds
  11. class SchedulingError(Exception):
  12. """An error occured while scheduling a task."""
  13. class ScheduleEntry(object):
  14. """An entry in the scheduler.
  15. :param task: see :attr:`task`.
  16. :keyword last_run_at: see :attr:`last_run_at`.
  17. :keyword total_run_count: see :attr:`total_run_count`.
  18. .. attribute:: task
  19. The task class.
  20. .. attribute:: last_run_at
  21. The time and date of when this task was last run.
  22. .. attribute:: total_run_count
  23. Total number of times this periodic task has been executed.
  24. """
  25. def __init__(self, name, last_run_at=None, total_run_count=None):
  26. self.name = name
  27. self.last_run_at = last_run_at or datetime.now()
  28. self.total_run_count = total_run_count or 0
  29. def next(self):
  30. """Returns a new instance of the same class, but with
  31. its date and count fields updated."""
  32. return self.__class__(self.name,
  33. datetime.now(),
  34. self.total_run_count + 1)
  35. def is_due(self, task):
  36. """See :meth:`celery.task.base.PeriodicTask.is_due`."""
  37. return task.is_due(self.last_run_at)
  38. class Scheduler(UserDict):
  39. """Scheduler for periodic tasks.
  40. :keyword registry: see :attr:`registry`.
  41. :keyword schedule: see :attr:`schedule`.
  42. :keyword logger: see :attr:`logger`.
  43. :keyword max_interval: see :attr:`max_interval`.
  44. .. attribute:: registry
  45. The task registry to use.
  46. .. attribute:: schedule
  47. The schedule dict/shelve.
  48. .. attribute:: logger
  49. The logger to use.
  50. .. attribute:: max_interval
  51. Maximum time to sleep between re-checking the schedule.
  52. """
  53. def __init__(self, registry=None, schedule=None, logger=None,
  54. max_interval=None):
  55. self.registry = registry or _registry.TaskRegistry()
  56. self.data = schedule or {}
  57. self.logger = logger or log.get_default_logger()
  58. self.max_interval = max_interval or conf.CELERYBEAT_MAX_LOOP_INTERVAL
  59. self.cleanup()
  60. self.schedule_registry()
  61. def tick(self):
  62. """Run a tick, that is one iteration of the scheduler.
  63. Executes all due tasks."""
  64. debug = self.logger.debug
  65. error = self.logger.error
  66. remaining_times = []
  67. for entry in self.schedule.values():
  68. is_due, next_time_to_run = self.is_due(entry)
  69. if is_due:
  70. debug("Scheduler: Sending due task %s" % entry.name)
  71. try:
  72. result = self.apply_async(entry)
  73. except SchedulingError, exc:
  74. error("Scheduler: %s" % exc)
  75. else:
  76. debug("%s sent. id->%s" % (entry.name, result.task_id))
  77. if next_time_to_run:
  78. remaining_times.append(next_time_to_run)
  79. return min(remaining_times + [self.max_interval])
  80. def get_task(self, name):
  81. return self.registry[name]
  82. def is_due(self, entry):
  83. return entry.is_due(self.get_task(entry.name))
  84. def apply_async(self, entry):
  85. # Update timestamps and run counts before we actually execute,
  86. # so we have that done if an exception is raised (doesn't schedule
  87. # forever.)
  88. entry = self.schedule[entry.name] = entry.next()
  89. task = self.get_task(entry.name)
  90. try:
  91. result = task.apply_async()
  92. except Exception, exc:
  93. raise SchedulingError("Couldn't apply scheduled task %s: %s" % (
  94. task.name, exc))
  95. return result
  96. def schedule_registry(self):
  97. """Add the current contents of the registry to the schedule."""
  98. for name, task in self.registry.periodic().items():
  99. if name not in self.schedule:
  100. self.logger.debug("Scheduler: "
  101. "Added periodic task %s to schedule" % name)
  102. self.schedule.setdefault(name, ScheduleEntry(task.name))
  103. def cleanup(self):
  104. for task_name, entry in self.schedule.items():
  105. if task_name not in self.registry:
  106. self.schedule.pop(task_name, None)
  107. @property
  108. def schedule(self):
  109. return self.data
  110. class ClockService(object):
  111. scheduler_cls = Scheduler
  112. registry = _registry.tasks
  113. open_schedule = lambda self, filename: shelve.open(filename)
  114. def __init__(self, logger=None,
  115. max_interval=conf.CELERYBEAT_MAX_LOOP_INTERVAL,
  116. schedule_filename=conf.CELERYBEAT_SCHEDULE_FILENAME):
  117. self.logger = logger or log.get_default_logger()
  118. self.max_interval = max_interval
  119. self.schedule_filename = schedule_filename
  120. self._shutdown = threading.Event()
  121. self._stopped = threading.Event()
  122. self._schedule = None
  123. self._scheduler = None
  124. self._in_sync = False
  125. silence = self.max_interval < 60 and 10 or 1
  126. self.debug = log.SilenceRepeated(self.logger.debug,
  127. max_iterations=silence)
  128. def start(self):
  129. self.logger.info("ClockService: Starting...")
  130. self.logger.debug("ClockService: "
  131. "Ticking with max interval->%s, schedule->%s" % (
  132. humanize_seconds(self.max_interval),
  133. self.schedule_filename))
  134. try:
  135. while True:
  136. if self._shutdown.isSet():
  137. break
  138. interval = self.scheduler.tick()
  139. self.debug("ClockService: Waking up %s." % (
  140. humanize_seconds(interval, prefix="in ")))
  141. time.sleep(interval)
  142. except (KeyboardInterrupt, SystemExit):
  143. self.sync()
  144. finally:
  145. self.sync()
  146. def sync(self):
  147. if self._schedule is not None and not self._in_sync:
  148. self.logger.debug("ClockService: Syncing schedule to disk...")
  149. self._schedule.sync()
  150. self._schedule.close()
  151. self._in_sync = True
  152. self._stopped.set()
  153. def stop(self, wait=False):
  154. self.logger.info("ClockService: Shutting down...")
  155. self._shutdown.set()
  156. wait and self._stopped.wait() # block until shutdown done.
  157. @property
  158. def schedule(self):
  159. if self._schedule is None:
  160. filename = self.schedule_filename
  161. self._schedule = self.open_schedule(filename=filename)
  162. return self._schedule
  163. @property
  164. def scheduler(self):
  165. if self._scheduler is None:
  166. self._scheduler = self.scheduler_cls(schedule=self.schedule,
  167. registry=self.registry,
  168. logger=self.logger,
  169. max_interval=self.max_interval)
  170. return self._scheduler
  171. def EmbeddedClockService(*args, **kwargs):
  172. """Return embedded clock service.
  173. :keyword thread: Run threaded instead of as a separate process.
  174. Default is ``False``.
  175. """
  176. class _Threaded(threading.Thread):
  177. """Embedded clock service using threading."""
  178. def __init__(self, *args, **kwargs):
  179. super(_Threaded, self).__init__()
  180. self.clockservice = ClockService(*args, **kwargs)
  181. self.setDaemon(True)
  182. def run(self):
  183. self.clockservice.start()
  184. def stop(self):
  185. self.clockservice.stop(wait=True)
  186. class _Process(multiprocessing.Process):
  187. """Embedded clock service using multiprocessing."""
  188. def __init__(self, *args, **kwargs):
  189. super(_Process, self).__init__()
  190. self.clockservice = ClockService(*args, **kwargs)
  191. self.daemon = True
  192. def run(self):
  193. self.clockservice.start()
  194. def stop(self):
  195. self.clockservice.stop()
  196. if kwargs.pop("thread", False):
  197. # Need short max interval to be able to stop thread
  198. # in reasonable time.
  199. kwargs.setdefault("max_interval", 1)
  200. return _Threaded(*args, **kwargs)
  201. return _Process(*args, **kwargs)