worker.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. # -*- coding: utf-8 -*-
  2. """
  3. celery.apps.worker
  4. ~~~~~~~~~~~~~~~~~~
  5. This module is the 'program-version' of :mod:`celery.worker`.
  6. It does everything necessary to run that module
  7. as an actual application, like installing signal handlers,
  8. platform tweaks, and so on.
  9. """
  10. from __future__ import absolute_import, print_function
  11. import logging
  12. import os
  13. import platform as _platform
  14. import sys
  15. import warnings
  16. from functools import partial
  17. from billiard import current_process
  18. from kombu.utils.encoding import safe_str
  19. from celery import VERSION_BANNER, platforms, signals
  20. from celery.exceptions import SystemTerminate
  21. from celery.five import string, string_t
  22. from celery.loaders.app import AppLoader
  23. from celery.app import trace
  24. from celery.utils import cry, isatty
  25. from celery.utils.imports import qualname
  26. from celery.utils.log import get_logger, in_sighandler, set_in_sighandler
  27. from celery.utils.text import pluralize
  28. from celery.worker import WorkController
  29. logger = get_logger(__name__)
  30. is_jython = sys.platform.startswith('java')
  31. is_pypy = hasattr(sys, 'pypy_version_info')
  32. def active_thread_count():
  33. from threading import enumerate
  34. return sum(1 for t in enumerate()
  35. if not t.name.startswith('Dummy-'))
  36. def safe_say(msg):
  37. print('\n{0}'.format(msg), file=sys.__stderr__)
  38. ARTLINES = [
  39. ' --------------',
  40. '---- **** -----',
  41. '--- * *** * --',
  42. '-- * - **** ---',
  43. '- ** ----------',
  44. '- ** ----------',
  45. '- ** ----------',
  46. '- ** ----------',
  47. '- *** --- * ---',
  48. '-- ******* ----',
  49. '--- ***** -----',
  50. ' --------------',
  51. ]
  52. BANNER = """\
  53. {hostname} v{version}
  54. {platform}
  55. [config]
  56. .> broker: {conninfo}
  57. .> app: {app}
  58. .> concurrency: {concurrency}
  59. .> events: {events}
  60. [queues]
  61. {queues}
  62. """
  63. EXTRA_INFO_FMT = """
  64. [tasks]
  65. {tasks}
  66. """
  67. class Worker(WorkController):
  68. def on_before_init(self, **kwargs):
  69. trace.setup_worker_optimizations(self.app)
  70. # this signal can be used to set up configuration for
  71. # workers by name.
  72. signals.celeryd_init.send(
  73. sender=self.hostname, instance=self, conf=self.app.conf,
  74. )
  75. def on_after_init(self, purge=False, no_color=None,
  76. redirect_stdouts=None, redirect_stdouts_level=None,
  77. **kwargs):
  78. self.redirect_stdouts = self._getopt(
  79. 'redirect_stdouts', redirect_stdouts,
  80. )
  81. self.redirect_stdouts_level = self._getopt(
  82. 'redirect_stdouts_level', redirect_stdouts_level,
  83. )
  84. super(Worker, self).setup_defaults(**kwargs)
  85. self.purge = purge
  86. self.no_color = no_color
  87. self._isatty = isatty(sys.stdout)
  88. self.colored = self.app.log.colored(
  89. self.logfile,
  90. enabled=not no_color if no_color is not None else no_color
  91. )
  92. def on_init_blueprint(self):
  93. self._custom_logging = self.setup_logging()
  94. # apply task execution optimizations
  95. # -- This will finalize the app!
  96. trace.setup_worker_optimizations(self.app)
  97. def on_start(self):
  98. if not self._custom_logging and self.redirect_stdouts:
  99. self.app.log.redirect_stdouts(self.redirect_stdouts_level)
  100. WorkController.on_start(self)
  101. # this signal can be used to e.g. change queues after
  102. # the -Q option has been applied.
  103. signals.celeryd_after_setup.send(
  104. sender=self.hostname, instance=self, conf=self.app.conf,
  105. )
  106. if getattr(os, 'getuid', None) and os.getuid() == 0:
  107. warnings.warn(RuntimeWarning(
  108. 'Running the worker with superuser privileges is discouraged!',
  109. ))
  110. if self.purge:
  111. self.purge_messages()
  112. # Dump configuration to screen so we have some basic information
  113. # for when users sends bug reports.
  114. sys.__stdout__.write(
  115. str(self.colored.cyan(' \n', self.startup_info())) +
  116. str(self.colored.reset(self.extra_info() or '')) + '\n'
  117. )
  118. self.set_process_status('-active-')
  119. self.install_platform_tweaks(self)
  120. def on_consumer_ready(self, consumer):
  121. signals.worker_ready.send(sender=consumer)
  122. print('{0} ready.'.format(safe_str(self.hostname), ))
  123. def setup_logging(self, colorize=None):
  124. if colorize is None and self.no_color is not None:
  125. colorize = not self.no_color
  126. return self.app.log.setup(
  127. self.loglevel, self.logfile,
  128. redirect_stdouts=False, colorize=colorize,
  129. )
  130. def purge_messages(self):
  131. count = self.app.control.purge()
  132. print('purge: Erased {0} {1} from the queue.\n'.format(
  133. count, pluralize(count, 'message')))
  134. def tasklist(self, include_builtins=True, sep='\n', int_='celery.'):
  135. return sep.join(
  136. ' . {0}'.format(task) for task in sorted(self.app.tasks)
  137. if (not task.startswith(int_) if not include_builtins else task)
  138. )
  139. def extra_info(self):
  140. if self.loglevel <= logging.INFO:
  141. include_builtins = self.loglevel <= logging.DEBUG
  142. tasklist = self.tasklist(include_builtins=include_builtins)
  143. return EXTRA_INFO_FMT.format(tasks=tasklist)
  144. def startup_info(self):
  145. app = self.app
  146. concurrency = string(self.concurrency)
  147. appr = '{0}:0x{1:x}'.format(app.main or '__main__', id(app))
  148. if not isinstance(app.loader, AppLoader):
  149. loader = qualname(app.loader)
  150. if loader.startswith('celery.loaders'):
  151. loader = loader[14:]
  152. appr += ' ({0})'.format(loader)
  153. if self.autoscale:
  154. max, min = self.autoscale
  155. concurrency = '{{min={0}, max={1}}}'.format(min, max)
  156. pool = self.pool_cls
  157. if not isinstance(pool, string_t):
  158. pool = pool.__module__
  159. concurrency += ' ({0})'.format(pool.split('.')[-1])
  160. events = 'ON'
  161. if not self.send_events:
  162. events = 'OFF (enable -E to monitor this worker)'
  163. banner = BANNER.format(
  164. app=appr,
  165. hostname=safe_str(self.hostname),
  166. version=VERSION_BANNER,
  167. conninfo=self.app.connection().as_uri(),
  168. concurrency=concurrency,
  169. platform=safe_str(_platform.platform()),
  170. events=events,
  171. queues=app.amqp.queues.format(indent=0, indent_first=False),
  172. ).splitlines()
  173. # integrate the ASCII art.
  174. for i, x in enumerate(banner):
  175. try:
  176. banner[i] = ' '.join([ARTLINES[i], banner[i]])
  177. except IndexError:
  178. banner[i] = ' ' * 16 + banner[i]
  179. return '\n'.join(banner) + '\n'
  180. def install_platform_tweaks(self, worker):
  181. """Install platform specific tweaks and workarounds."""
  182. if self.app.IS_OSX:
  183. self.osx_proxy_detection_workaround()
  184. # Install signal handler so SIGHUP restarts the worker.
  185. if not self._isatty:
  186. # only install HUP handler if detached from terminal,
  187. # so closing the terminal window doesn't restart the worker
  188. # into the background.
  189. if self.app.IS_OSX:
  190. # OS X can't exec from a process using threads.
  191. # See http://github.com/celery/celery/issues#issue/152
  192. install_HUP_not_supported_handler(worker)
  193. else:
  194. install_worker_restart_handler(worker)
  195. install_worker_term_handler(worker)
  196. install_worker_term_hard_handler(worker)
  197. install_worker_int_handler(worker)
  198. install_cry_handler()
  199. install_rdb_handler()
  200. def osx_proxy_detection_workaround(self):
  201. """See http://github.com/celery/celery/issues#issue/161"""
  202. os.environ.setdefault('celery_dummy_proxy', 'set_by_celeryd')
  203. def set_process_status(self, info):
  204. return platforms.set_mp_process_title(
  205. 'celeryd',
  206. info='{0} ({1})'.format(info, platforms.strargv(sys.argv)),
  207. hostname=self.hostname,
  208. )
  209. def _shutdown_handler(worker, sig='TERM', how='Warm',
  210. exc=SystemExit, callback=None):
  211. def _handle_request(*args):
  212. with in_sighandler():
  213. from celery.worker import state
  214. if current_process()._name == 'MainProcess':
  215. if callback:
  216. callback(worker)
  217. safe_say('worker: {0} shutdown (MainProcess)'.format(how))
  218. if active_thread_count() > 1:
  219. setattr(state, {'Warm': 'should_stop',
  220. 'Cold': 'should_terminate'}[how], True)
  221. else:
  222. raise exc()
  223. _handle_request.__name__ = 'worker_' + how
  224. platforms.signals[sig] = _handle_request
  225. install_worker_term_handler = partial(
  226. _shutdown_handler, sig='SIGTERM', how='Warm', exc=SystemExit,
  227. )
  228. if not is_jython: # pragma: no cover
  229. install_worker_term_hard_handler = partial(
  230. _shutdown_handler, sig='SIGQUIT', how='Cold', exc=SystemTerminate,
  231. )
  232. else: # pragma: no cover
  233. install_worker_term_handler = \
  234. install_worker_term_hard_handler = lambda *a, **kw: None
  235. def on_SIGINT(worker):
  236. safe_say('worker: Hitting Ctrl+C again will terminate all running tasks!')
  237. install_worker_term_hard_handler(worker, sig='SIGINT')
  238. if not is_jython: # pragma: no cover
  239. install_worker_int_handler = partial(
  240. _shutdown_handler, sig='SIGINT', callback=on_SIGINT
  241. )
  242. else: # pragma: no cover
  243. install_worker_int_handler = lambda *a, **kw: None
  244. def _clone_current_worker():
  245. if os.fork() == 0:
  246. platforms.close_open_fds([
  247. sys.__stdin__, sys.__stdout__, sys.__stderr__,
  248. ])
  249. os.execv(sys.executable, [sys.executable] + sys.argv)
  250. def install_worker_restart_handler(worker, sig='SIGHUP'):
  251. def restart_worker_sig_handler(*args):
  252. """Signal handler restarting the current python program."""
  253. set_in_sighandler(True)
  254. safe_say('Restarting celery worker ({0})'.format(' '.join(sys.argv)))
  255. import atexit
  256. atexit.register(_clone_current_worker)
  257. from celery.worker import state
  258. state.should_stop = True
  259. platforms.signals[sig] = restart_worker_sig_handler
  260. def install_cry_handler(sig='SIGUSR1'):
  261. # Jython/PyPy does not have sys._current_frames
  262. if is_jython or is_pypy: # pragma: no cover
  263. return
  264. def cry_handler(*args):
  265. """Signal handler logging the stacktrace of all active threads."""
  266. with in_sighandler():
  267. safe_say(cry())
  268. platforms.signals[sig] = cry_handler
  269. def install_rdb_handler(envvar='CELERY_RDBSIG',
  270. sig='SIGUSR2'): # pragma: no cover
  271. def rdb_handler(*args):
  272. """Signal handler setting a rdb breakpoint at the current frame."""
  273. with in_sighandler():
  274. from celery.contrib.rdb import set_trace, _frame
  275. # gevent does not pass standard signal handler args
  276. frame = args[1] if args else _frame().f_back
  277. set_trace(frame)
  278. if os.environ.get(envvar):
  279. platforms.signals[sig] = rdb_handler
  280. def install_HUP_not_supported_handler(worker, sig='SIGHUP'):
  281. def warn_on_HUP_handler(signum, frame):
  282. with in_sighandler():
  283. safe_say('{sig} not supported: Restarting with {sig} is '
  284. 'unstable on this platform!'.format(sig=sig))
  285. platforms.signals[sig] = warn_on_HUP_handler