worker.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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
  11. import logging
  12. import os
  13. import platform as _platform
  14. import socket
  15. import sys
  16. import warnings
  17. from functools import partial
  18. from billiard import cpu_count, current_process
  19. from kombu.utils.encoding import safe_str
  20. from celery import VERSION_BANNER, platforms, signals
  21. from celery.app import app_or_default
  22. from celery.app.abstract import configurated, from_config
  23. from celery.exceptions import ImproperlyConfigured, SystemTerminate
  24. from celery.loaders.app import AppLoader
  25. from celery.task import trace
  26. from celery.utils import cry, isatty, worker_direct
  27. from celery.utils.imports import qualname
  28. from celery.utils.log import get_logger, mlevel, set_in_sighandler
  29. from celery.utils.text import pluralize
  30. from celery.worker import WorkController
  31. try:
  32. from greenlet import GreenletExit
  33. IGNORE_ERRORS = (GreenletExit, )
  34. except ImportError: # pragma: no cover
  35. IGNORE_ERRORS = ()
  36. logger = get_logger(__name__)
  37. is_jython = sys.platform.startswith('java')
  38. is_pypy = hasattr(sys, 'pypy_version_info')
  39. def active_thread_count():
  40. from threading import enumerate
  41. # must use .getName on Python 2.5
  42. return sum(1 for t in enumerate()
  43. if not t.getName().startswith('Dummy-'))
  44. def safe_say(msg):
  45. sys.__stderr__.write('\n%s\n' % msg)
  46. ARTLINES = [
  47. ' --------------',
  48. '---- **** -----',
  49. '--- * *** * --',
  50. '-- * - **** ---',
  51. '- ** ----------',
  52. '- ** ----------',
  53. '- ** ----------',
  54. '- ** ----------',
  55. '- *** --- * ---',
  56. '-- ******* ----',
  57. '--- ***** -----',
  58. ' --------------',
  59. ]
  60. BANNER = """\
  61. celery@%(hostname)s v%(version)s
  62. %(platform)s
  63. [config]
  64. .> broker: %(conninfo)s
  65. .> app: %(app)s
  66. .> concurrency: %(concurrency)s
  67. .> events: %(events)s
  68. [queues]
  69. %(queues)s
  70. """
  71. EXTRA_INFO_FMT = """
  72. [Tasks]
  73. %(tasks)s
  74. """
  75. UNKNOWN_QUEUE = """\
  76. Trying to select queue subset of %r, but queue %s is not
  77. defined in the CELERY_QUEUES setting.
  78. If you want to automatically declare unknown queues you can
  79. enable the CELERY_CREATE_MISSING_QUEUES setting.
  80. """
  81. class Worker(configurated):
  82. WorkController = WorkController
  83. app = None
  84. inherit_confopts = (WorkController, )
  85. loglevel = from_config('log_level')
  86. redirect_stdouts = from_config()
  87. redirect_stdouts_level = from_config()
  88. def __init__(self, hostname=None, purge=False, beat=False,
  89. queues=None, include=None, app=None, pidfile=None,
  90. autoscale=None, autoreload=False, no_execv=False,
  91. no_color=None, **kwargs):
  92. self.app = app = app_or_default(app or self.app)
  93. self.hostname = hostname or socket.gethostname()
  94. # this signal can be used to set up configuration for
  95. # workers by name.
  96. signals.celeryd_init.send(sender=self.hostname, instance=self,
  97. conf=self.app.conf)
  98. self.setup_defaults(kwargs, namespace='celeryd')
  99. if not self.concurrency:
  100. try:
  101. self.concurrency = cpu_count()
  102. except NotImplementedError:
  103. self.concurrency = 2
  104. self.purge = purge
  105. self.beat = beat
  106. self.use_queues = [] if queues is None else queues
  107. self.queues = None
  108. self.include = include
  109. self.pidfile = pidfile
  110. self.autoscale = None
  111. self.autoreload = autoreload
  112. self.no_color = no_color
  113. self.no_execv = no_execv
  114. if autoscale:
  115. max_c, _, min_c = autoscale.partition(',')
  116. self.autoscale = [int(max_c), min_c and int(min_c) or 0]
  117. self._isatty = isatty(sys.stdout)
  118. self.colored = app.log.colored(
  119. self.logfile,
  120. enabled=not no_color if no_color is not None else no_color
  121. )
  122. if isinstance(self.use_queues, basestring):
  123. self.use_queues = self.use_queues.split(',')
  124. if self.include:
  125. if isinstance(self.include, basestring):
  126. self.include = self.include.split(',')
  127. app.conf.CELERY_INCLUDE = (
  128. tuple(app.conf.CELERY_INCLUDE) + tuple(self.include))
  129. self.loglevel = mlevel(self.loglevel)
  130. def run(self):
  131. self.init_queues()
  132. self.app.loader.init_worker()
  133. # this signal can be used to e.g. change queues after
  134. # the -Q option has been applied.
  135. signals.celeryd_after_setup.send(sender=self.hostname, instance=self,
  136. conf=self.app.conf)
  137. if getattr(os, 'getuid', None) and os.getuid() == 0:
  138. warnings.warn(RuntimeWarning(
  139. 'Running celeryd with superuser privileges is discouraged!'))
  140. if self.purge:
  141. self.purge_messages()
  142. # Dump configuration to screen so we have some basic information
  143. # for when users sends bug reports.
  144. print(str(self.colored.cyan(' \n', self.startup_info())) +
  145. str(self.colored.reset(self.extra_info() or '')))
  146. self.set_process_status('-active-')
  147. self.setup_logging()
  148. # apply task execution optimizations
  149. trace.setup_worker_optimizations(self.app)
  150. try:
  151. self.run_worker()
  152. except IGNORE_ERRORS:
  153. pass
  154. def on_consumer_ready(self, consumer):
  155. signals.worker_ready.send(sender=consumer)
  156. print('celery@%s ready.' % safe_str(self.hostname))
  157. def init_queues(self):
  158. try:
  159. self.app.select_queues(self.use_queues)
  160. except KeyError, exc:
  161. raise ImproperlyConfigured(UNKNOWN_QUEUE % (self.use_queues, exc))
  162. if self.app.conf.CELERY_WORKER_DIRECT:
  163. self.app.amqp.queues.select_add(worker_direct(self.hostname))
  164. def setup_logging(self, colorize=None):
  165. if colorize is None and self.no_color is not None:
  166. colorize = not self.no_color
  167. self.app.log.setup(self.loglevel, self.logfile,
  168. self.redirect_stdouts, self.redirect_stdouts_level,
  169. colorize=colorize)
  170. def purge_messages(self):
  171. count = self.app.control.purge()
  172. print('purge: Erased %d %s from the queue.\n' % (
  173. count, pluralize(count, 'message')))
  174. def tasklist(self, include_builtins=True, sep='\n', int_='celery.'):
  175. return sep.join(
  176. ' . %s' % task for task in sorted(self.app.tasks)
  177. if (not task.startswith(int_) if not include_builtins else task)
  178. )
  179. def extra_info(self):
  180. if self.loglevel <= logging.INFO:
  181. include_builtins = self.loglevel <= logging.DEBUG
  182. tasklist = self.tasklist(include_builtins=include_builtins)
  183. return EXTRA_INFO_FMT % {'tasks': tasklist}
  184. def startup_info(self):
  185. app = self.app
  186. concurrency = unicode(self.concurrency)
  187. appr = '%s:0x%x' % (app.main or '__main__', id(app))
  188. if not isinstance(app.loader, AppLoader):
  189. loader = qualname(app.loader)
  190. if loader.startswith('celery.loaders'):
  191. loader = loader[14:]
  192. appr += ' (%s)' % loader
  193. if self.autoscale:
  194. max, min = self.autoscale
  195. concurrency = '{min=%s, max=%s}' % (min, max)
  196. pool = self.pool_cls
  197. if not isinstance(pool, basestring):
  198. pool = pool.__module__
  199. concurrency += ' (%s)' % pool.split('.')[-1]
  200. events = 'ON'
  201. if not self.send_events:
  202. events = 'OFF (enable -E to monitor this worker)'
  203. banner = (BANNER % {
  204. 'app': appr,
  205. 'hostname': self.hostname,
  206. 'version': VERSION_BANNER,
  207. 'conninfo': self.app.connection().as_uri(),
  208. 'concurrency': concurrency,
  209. 'platform': safe_str(_platform.platform()),
  210. 'events': events,
  211. 'queues': app.amqp.queues.format(indent=0, indent_first=False),
  212. }).splitlines()
  213. # integrate the ASCII art.
  214. for i, x in enumerate(banner):
  215. try:
  216. banner[i] = ' '.join([ARTLINES[i], banner[i]])
  217. except IndexError:
  218. banner[i] = ' ' * 16 + banner[i]
  219. return '\n'.join(banner) + '\n'
  220. def run_worker(self):
  221. worker = self.WorkController(
  222. app=self.app,
  223. hostname=self.hostname,
  224. ready_callback=self.on_consumer_ready, beat=self.beat,
  225. autoscale=self.autoscale, autoreload=self.autoreload,
  226. no_execv=self.no_execv,
  227. pidfile=self.pidfile,
  228. **self.confopts_as_dict()
  229. )
  230. self.install_platform_tweaks(worker)
  231. signals.worker_init.send(sender=worker)
  232. worker.start()
  233. def install_platform_tweaks(self, worker):
  234. """Install platform specific tweaks and workarounds."""
  235. if self.app.IS_OSX:
  236. self.osx_proxy_detection_workaround()
  237. # Install signal handler so SIGHUP restarts the worker.
  238. if not self._isatty:
  239. # only install HUP handler if detached from terminal,
  240. # so closing the terminal window doesn't restart celeryd
  241. # into the background.
  242. if self.app.IS_OSX:
  243. # OS X can't exec from a process using threads.
  244. # See http://github.com/celery/celery/issues#issue/152
  245. install_HUP_not_supported_handler(worker)
  246. else:
  247. install_worker_restart_handler(worker)
  248. install_worker_term_handler(worker)
  249. install_worker_term_hard_handler(worker)
  250. install_worker_int_handler(worker)
  251. install_cry_handler()
  252. install_rdb_handler()
  253. def osx_proxy_detection_workaround(self):
  254. """See http://github.com/celery/celery/issues#issue/161"""
  255. os.environ.setdefault('celery_dummy_proxy', 'set_by_celeryd')
  256. def set_process_status(self, info):
  257. return platforms.set_mp_process_title(
  258. 'celeryd',
  259. info='%s (%s)' % (info, platforms.strargv(sys.argv)),
  260. hostname=self.hostname,
  261. )
  262. def _shutdown_handler(worker, sig='TERM', how='Warm',
  263. exc=SystemExit, callback=None):
  264. def _handle_request(*args):
  265. set_in_sighandler(True)
  266. try:
  267. from celery.worker import state
  268. if current_process()._name == 'MainProcess':
  269. if callback:
  270. callback(worker)
  271. safe_say('celeryd: %s shutdown (MainProcess)' % how)
  272. if active_thread_count() > 1:
  273. setattr(state, {'Warm': 'should_stop',
  274. 'Cold': 'should_terminate'}[how], True)
  275. else:
  276. raise exc()
  277. finally:
  278. set_in_sighandler(False)
  279. _handle_request.__name__ = 'worker_' + how
  280. platforms.signals[sig] = _handle_request
  281. install_worker_term_handler = partial(
  282. _shutdown_handler, sig='SIGTERM', how='Warm', exc=SystemExit,
  283. )
  284. if not is_jython:
  285. install_worker_term_hard_handler = partial(
  286. _shutdown_handler, sig='SIGQUIT', how='Cold', exc=SystemTerminate,
  287. )
  288. else:
  289. install_worker_term_handler = \
  290. install_worker_term_hard_handler = lambda *a, **kw: None
  291. def on_SIGINT(worker):
  292. safe_say('celeryd: Hitting Ctrl+C again will terminate all running tasks!')
  293. install_worker_term_hard_handler(worker, sig='SIGINT')
  294. if not is_jython:
  295. install_worker_int_handler = partial(
  296. _shutdown_handler, sig='SIGINT', callback=on_SIGINT
  297. )
  298. else:
  299. install_worker_int_handler = lambda *a, **kw: None
  300. def _clone_current_worker():
  301. if os.fork() == 0:
  302. platforms.close_open_fds([
  303. sys.__stdin__, sys.__stdout__, sys.__stderr__,
  304. ])
  305. os.execv(sys.executable, [sys.executable] + sys.argv)
  306. def install_worker_restart_handler(worker, sig='SIGHUP'):
  307. def restart_worker_sig_handler(*args):
  308. """Signal handler restarting the current python program."""
  309. set_in_sighandler(True)
  310. safe_say('Restarting celeryd (%s)' % (' '.join(sys.argv), ))
  311. import atexit
  312. atexit.register(_clone_current_worker)
  313. from celery.worker import state
  314. state.should_stop = True
  315. platforms.signals[sig] = restart_worker_sig_handler
  316. def install_cry_handler():
  317. # Jython/PyPy does not have sys._current_frames
  318. if is_jython or is_pypy: # pragma: no cover
  319. return
  320. def cry_handler(*args):
  321. """Signal handler logging the stacktrace of all active threads."""
  322. set_in_sighandler(True)
  323. try:
  324. safe_say(cry())
  325. finally:
  326. set_in_sighandler(False)
  327. platforms.signals['SIGUSR1'] = cry_handler
  328. def install_rdb_handler(envvar='CELERY_RDBSIG',
  329. sig='SIGUSR2'): # pragma: no cover
  330. def rdb_handler(*args):
  331. """Signal handler setting a rdb breakpoint at the current frame."""
  332. set_in_sighandler(True)
  333. try:
  334. _, frame = args
  335. from celery.contrib import rdb
  336. rdb.set_trace(frame)
  337. finally:
  338. set_in_sighandler(False)
  339. if os.environ.get(envvar):
  340. platforms.signals[sig] = rdb_handler
  341. def install_HUP_not_supported_handler(worker, sig='SIGHUP'):
  342. def warn_on_HUP_handler(*args):
  343. set_in_sighandler(True)
  344. try:
  345. safe_say('%(sig)s not supported: Restarting with %(sig)s is '
  346. 'unstable on this platform!' % {'sig': sig})
  347. finally:
  348. set_in_sighandler(False)
  349. platforms.signals[sig] = warn_on_HUP_handler