multi.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. # -*- coding: utf-8 -*-
  2. """
  3. .. program:: celery multi
  4. Examples
  5. ========
  6. .. code-block:: console
  7. $ # Single worker with explicit name and events enabled.
  8. $ celery multi start Leslie -E
  9. $ # Pidfiles and logfiles are stored in the current directory
  10. $ # by default. Use --pidfile and --logfile argument to change
  11. $ # this. The abbreviation %n will be expanded to the current
  12. $ # node name.
  13. $ celery multi start Leslie -E --pidfile=/var/run/celery/%n.pid
  14. --logfile=/var/log/celery/%n%I.log
  15. $ # You need to add the same arguments when you restart,
  16. $ # as these are not persisted anywhere.
  17. $ celery multi restart Leslie -E --pidfile=/var/run/celery/%n.pid
  18. --logfile=/var/run/celery/%n%I.log
  19. $ # To stop the node, you need to specify the same pidfile.
  20. $ celery multi stop Leslie --pidfile=/var/run/celery/%n.pid
  21. $ # 3 workers, with 3 processes each
  22. $ celery multi start 3 -c 3
  23. celery worker -n celery1@myhost -c 3
  24. celery worker -n celery2@myhost -c 3
  25. celery worker -n celery3@myhost -c 3
  26. $ # start 3 named workers
  27. $ celery multi start image video data -c 3
  28. celery worker -n image@myhost -c 3
  29. celery worker -n video@myhost -c 3
  30. celery worker -n data@myhost -c 3
  31. $ # specify custom hostname
  32. $ celery multi start 2 --hostname=worker.example.com -c 3
  33. celery worker -n celery1@worker.example.com -c 3
  34. celery worker -n celery2@worker.example.com -c 3
  35. $ # specify fully qualified nodenames
  36. $ celery multi start foo@worker.example.com bar@worker.example.com -c 3
  37. $ # fully qualified nodenames but using the current hostname
  38. $ celery multi start foo@%h bar@%h
  39. $ # Advanced example starting 10 workers in the background:
  40. $ # * Three of the workers processes the images and video queue
  41. $ # * Two of the workers processes the data queue with loglevel DEBUG
  42. $ # * the rest processes the default' queue.
  43. $ celery multi start 10 -l INFO -Q:1-3 images,video -Q:4,5 data
  44. -Q default -L:4,5 DEBUG
  45. $ # You can show the commands necessary to start the workers with
  46. $ # the 'show' command:
  47. $ celery multi show 10 -l INFO -Q:1-3 images,video -Q:4,5 data
  48. -Q default -L:4,5 DEBUG
  49. $ # Additional options are added to each celery worker' comamnd,
  50. $ # but you can also modify the options for ranges of, or specific workers
  51. $ # 3 workers: Two with 3 processes, and one with 10 processes.
  52. $ celery multi start 3 -c 3 -c:1 10
  53. celery worker -n celery1@myhost -c 10
  54. celery worker -n celery2@myhost -c 3
  55. celery worker -n celery3@myhost -c 3
  56. $ # can also specify options for named workers
  57. $ celery multi start image video data -c 3 -c:image 10
  58. celery worker -n image@myhost -c 10
  59. celery worker -n video@myhost -c 3
  60. celery worker -n data@myhost -c 3
  61. $ # ranges and lists of workers in options is also allowed:
  62. $ # (-c:1-3 can also be written as -c:1,2,3)
  63. $ celery multi start 5 -c 3 -c:1-3 10
  64. celery worker -n celery1@myhost -c 10
  65. celery worker -n celery2@myhost -c 10
  66. celery worker -n celery3@myhost -c 10
  67. celery worker -n celery4@myhost -c 3
  68. celery worker -n celery5@myhost -c 3
  69. $ # lists also works with named workers
  70. $ celery multi start foo bar baz xuzzy -c 3 -c:foo,bar,baz 10
  71. celery worker -n foo@myhost -c 10
  72. celery worker -n bar@myhost -c 10
  73. celery worker -n baz@myhost -c 10
  74. celery worker -n xuzzy@myhost -c 3
  75. """
  76. from __future__ import absolute_import, print_function, unicode_literals
  77. import errno
  78. import os
  79. import shlex
  80. import signal
  81. import sys
  82. from collections import OrderedDict, defaultdict, namedtuple
  83. from functools import partial
  84. from subprocess import Popen
  85. from time import sleep
  86. from kombu.utils import cached_property
  87. from kombu.utils.encoding import from_utf8
  88. from celery import VERSION_BANNER
  89. from celery.five import items
  90. from celery.platforms import Pidfile, IS_WINDOWS
  91. from celery.utils import term
  92. from celery.utils.nodenames import (
  93. gethostname, host_format, node_format, nodesplit,
  94. )
  95. from celery.utils.text import pluralize
  96. __all__ = ['MultiTool']
  97. SIGNAMES = {sig for sig in dir(signal)
  98. if sig.startswith('SIG') and '_' not in sig}
  99. SIGMAP = {getattr(signal, name): name for name in SIGNAMES}
  100. USAGE = """\
  101. usage: {prog_name} start <node1 node2 nodeN|range> [worker options]
  102. {prog_name} stop <n1 n2 nN|range> [-SIG (default: -TERM)]
  103. {prog_name} restart <n1 n2 nN|range> [-SIG] [worker options]
  104. {prog_name} kill <n1 n2 nN|range>
  105. {prog_name} show <n1 n2 nN|range> [worker options]
  106. {prog_name} get hostname <n1 n2 nN|range> [-qv] [worker options]
  107. {prog_name} names <n1 n2 nN|range>
  108. {prog_name} expand template <n1 n2 nN|range>
  109. {prog_name} help
  110. additional options (must appear after command name):
  111. * --nosplash: Don't display program info.
  112. * --quiet: Don't show as much output.
  113. * --verbose: Show more output.
  114. * --no-color: Don't display colors.
  115. """
  116. CELERY_EXE = 'celery'
  117. multi_args_t = namedtuple(
  118. 'multi_args_t', ('name', 'argv', 'expander', 'namespace'),
  119. )
  120. def main():
  121. sys.exit(MultiTool().execute_from_commandline(sys.argv))
  122. def celery_exe(*args):
  123. return ' '.join((CELERY_EXE,) + args)
  124. class MultiTool(object):
  125. retcode = 0 # Final exit code.
  126. def __init__(self, env=None, fh=None, quiet=False, verbose=False,
  127. no_color=False, nosplash=False, stdout=None, stderr=None):
  128. """fh is an old alias to stdout."""
  129. self.stdout = self.fh = stdout or fh or sys.stdout
  130. self.stderr = stderr or sys.stderr
  131. self.env = env
  132. self.nosplash = nosplash
  133. self.quiet = quiet
  134. self.verbose = verbose
  135. self.no_color = no_color
  136. self.prog_name = 'celery multi'
  137. self.commands = {'start': self.start,
  138. 'show': self.show,
  139. 'stop': self.stop,
  140. 'stopwait': self.stopwait,
  141. 'stop_verify': self.stopwait, # compat alias
  142. 'restart': self.restart,
  143. 'kill': self.kill,
  144. 'names': self.names,
  145. 'expand': self.expand,
  146. 'get': self.get,
  147. 'help': self.help}
  148. def execute_from_commandline(self, argv, cmd='celery worker'):
  149. argv = list(argv) # don't modify callers argv.
  150. # Reserve the --nosplash|--quiet|-q/--verbose options.
  151. if '--nosplash' in argv:
  152. self.nosplash = argv.pop(argv.index('--nosplash'))
  153. if '--quiet' in argv:
  154. self.quiet = argv.pop(argv.index('--quiet'))
  155. if '-q' in argv:
  156. self.quiet = argv.pop(argv.index('-q'))
  157. if '--verbose' in argv:
  158. self.verbose = argv.pop(argv.index('--verbose'))
  159. if '--no-color' in argv:
  160. self.no_color = argv.pop(argv.index('--no-color'))
  161. self.prog_name = os.path.basename(argv.pop(0))
  162. if not argv or argv[0][0] == '-':
  163. return self.error()
  164. try:
  165. self.commands[argv[0]](argv[1:], cmd)
  166. except KeyError:
  167. self.error('Invalid command: {0}'.format(argv[0]))
  168. return self.retcode
  169. def say(self, m, newline=True, file=None):
  170. print(m, file=file or self.stdout, end='\n' if newline else '')
  171. def carp(self, m, newline=True, file=None):
  172. return self.say(m, newline, file or self.stderr)
  173. def names(self, argv, cmd):
  174. p = NamespacedOptionParser(argv)
  175. self.say('\n'.join(
  176. n.name for n in multi_args(p, cmd)),
  177. )
  178. def get(self, argv, cmd):
  179. wanted = argv[0]
  180. p = NamespacedOptionParser(argv[1:])
  181. for node in multi_args(p, cmd):
  182. if node.name == wanted:
  183. self.say(' '.join(node.argv))
  184. return
  185. def show(self, argv, cmd):
  186. p = NamespacedOptionParser(argv)
  187. self.with_detacher_default_options(p)
  188. self.say('\n'.join(
  189. ' '.join([sys.executable] + n.argv) for n in multi_args(p, cmd)),
  190. )
  191. def start(self, argv, cmd):
  192. self.splash()
  193. p = NamespacedOptionParser(argv)
  194. self.with_detacher_default_options(p)
  195. retcodes = []
  196. self.note('> Starting nodes...')
  197. for node in multi_args(p, cmd):
  198. self.note('\t> {0}: '.format(node.name), newline=False)
  199. retcode = self.waitexec(node.argv, path=p.options['--executable'])
  200. self.note(retcode and self.FAILED or self.OK)
  201. retcodes.append(retcode)
  202. self.retcode = int(any(retcodes))
  203. def with_detacher_default_options(self, p):
  204. _setdefaultopt(p.options, ['--pidfile', '-p'], '%n.pid')
  205. _setdefaultopt(p.options, ['--logfile', '-f'], '%n%I.log')
  206. p.options.setdefault(
  207. '--cmd',
  208. '-m {0}'.format(celery_exe('worker', '--detach')),
  209. )
  210. _setdefaultopt(p.options, ['--executable'], sys.executable)
  211. def signal_node(self, nodename, pid, sig):
  212. try:
  213. os.kill(pid, sig)
  214. except OSError as exc:
  215. if exc.errno != errno.ESRCH:
  216. raise
  217. self.note('Could not signal {0} ({1}): No such process'.format(
  218. nodename, pid))
  219. return False
  220. return True
  221. def node_alive(self, pid):
  222. try:
  223. os.kill(pid, 0)
  224. except OSError as exc:
  225. if exc.errno == errno.ESRCH:
  226. return False
  227. raise
  228. return True
  229. def shutdown_nodes(self, nodes, sig=signal.SIGTERM, retry=None,
  230. callback=None):
  231. if not nodes:
  232. return
  233. P = set(nodes)
  234. def on_down(node):
  235. P.discard(node)
  236. if callback:
  237. callback(*node)
  238. self.note(self.colored.blue('> Stopping nodes...'))
  239. for node in list(P):
  240. if node in P:
  241. nodename, _, pid = node
  242. self.note('\t> {0}: {1} -> {2}'.format(
  243. nodename, SIGMAP[sig][3:], pid))
  244. if not self.signal_node(nodename, pid, sig):
  245. on_down(node)
  246. def note_waiting():
  247. left = len(P)
  248. if left:
  249. pids = ', '.join(str(pid) for _, _, pid in P)
  250. self.note(self.colored.blue(
  251. '> Waiting for {0} {1} -> {2}...'.format(
  252. left, pluralize(left, 'node'), pids)), newline=False)
  253. if retry:
  254. note_waiting()
  255. its = 0
  256. while P:
  257. for node in P:
  258. its += 1
  259. self.note('.', newline=False)
  260. nodename, _, pid = node
  261. if not self.node_alive(pid):
  262. self.note('\n\t> {0}: {1}'.format(nodename, self.OK))
  263. on_down(node)
  264. note_waiting()
  265. break
  266. if P and not its % len(P):
  267. sleep(float(retry))
  268. self.note('')
  269. def getpids(self, p, cmd, callback=None):
  270. _setdefaultopt(p.options, ['--pidfile', '-p'], '%n.pid')
  271. nodes = []
  272. for node in multi_args(p, cmd):
  273. try:
  274. pidfile_template = _getopt(
  275. p.namespaces[node.namespace], ['--pidfile', '-p'],
  276. )
  277. except KeyError:
  278. pidfile_template = _getopt(p.options, ['--pidfile', '-p'])
  279. pid = None
  280. pidfile = node.expander(pidfile_template)
  281. try:
  282. pid = Pidfile(pidfile).read_pid()
  283. except ValueError:
  284. pass
  285. if pid:
  286. nodes.append((node.name, tuple(node.argv), pid))
  287. else:
  288. self.note('> {0.name}: {1}'.format(node, self.DOWN))
  289. if callback:
  290. callback(node.name, node.argv, pid)
  291. return nodes
  292. def kill(self, argv, cmd):
  293. self.splash()
  294. p = NamespacedOptionParser(argv)
  295. for nodename, _, pid in self.getpids(p, cmd):
  296. self.note('Killing node {0} ({1})'.format(nodename, pid))
  297. self.signal_node(nodename, pid, signal.SIGKILL)
  298. def stop(self, argv, cmd, retry=None, callback=None):
  299. self.splash()
  300. p = NamespacedOptionParser(argv)
  301. return self._stop_nodes(p, cmd, retry=retry, callback=callback)
  302. def _stop_nodes(self, p, cmd, retry=None, callback=None):
  303. restargs = p.args[len(p.values):]
  304. self.shutdown_nodes(self.getpids(p, cmd, callback=callback),
  305. sig=findsig(restargs),
  306. retry=retry,
  307. callback=callback)
  308. def restart(self, argv, cmd):
  309. self.splash()
  310. p = NamespacedOptionParser(argv)
  311. self.with_detacher_default_options(p)
  312. retvals = []
  313. def on_node_shutdown(nodename, argv, pid):
  314. self.note(self.colored.blue(
  315. '> Restarting node {0}: '.format(nodename)), newline=False)
  316. retval = self.waitexec(argv, path=p.options['--executable'])
  317. self.note(retval and self.FAILED or self.OK)
  318. retvals.append(retval)
  319. self._stop_nodes(p, cmd, retry=2, callback=on_node_shutdown)
  320. self.retval = int(any(retvals))
  321. def stopwait(self, argv, cmd):
  322. self.splash()
  323. p = NamespacedOptionParser(argv)
  324. self.with_detacher_default_options(p)
  325. return self._stop_nodes(p, cmd, retry=2)
  326. stop_verify = stopwait # compat
  327. def expand(self, argv, cmd=None):
  328. template = argv[0]
  329. p = NamespacedOptionParser(argv[1:])
  330. for node in multi_args(p, cmd):
  331. self.say(node.expander(template))
  332. def help(self, argv, cmd=None):
  333. self.say(__doc__)
  334. def usage(self):
  335. self.splash()
  336. self.say(USAGE.format(prog_name=self.prog_name))
  337. def splash(self):
  338. if not self.nosplash:
  339. c = self.colored
  340. self.note(c.cyan('celery multi v{0}'.format(VERSION_BANNER)))
  341. def waitexec(self, argv, path=sys.executable):
  342. args = ' '.join([path] + list(argv))
  343. argstr = shlex.split(from_utf8(args), posix=not IS_WINDOWS)
  344. pipe = Popen(argstr, env=self.env)
  345. self.info(' {0}'.format(' '.join(argstr)))
  346. retcode = pipe.wait()
  347. if retcode < 0:
  348. self.note('* Child was terminated by signal {0}'.format(-retcode))
  349. return -retcode
  350. elif retcode > 0:
  351. self.note('* Child terminated with errorcode {0}'.format(retcode))
  352. return retcode
  353. def error(self, msg=None):
  354. if msg:
  355. self.carp(msg)
  356. self.usage()
  357. self.retcode = 1
  358. return 1
  359. def info(self, msg, newline=True):
  360. if self.verbose:
  361. self.note(msg, newline=newline)
  362. def note(self, msg, newline=True):
  363. if not self.quiet:
  364. self.say(str(msg), newline=newline)
  365. @cached_property
  366. def colored(self):
  367. return term.colored(enabled=not self.no_color)
  368. @cached_property
  369. def OK(self):
  370. return str(self.colored.green('OK'))
  371. @cached_property
  372. def FAILED(self):
  373. return str(self.colored.red('FAILED'))
  374. @cached_property
  375. def DOWN(self):
  376. return str(self.colored.magenta('DOWN'))
  377. def _args_for_node(p, name, prefix, suffix, cmd, append, options):
  378. name, nodename, expand = _get_nodename(
  379. name, prefix, suffix, options)
  380. if nodename in p.namespaces:
  381. ns = nodename
  382. else:
  383. ns = name
  384. argv = ([expand(cmd)] +
  385. [format_opt(opt, expand(value))
  386. for opt, value in items(p.optmerge(ns, options))] +
  387. [p.passthrough])
  388. if append:
  389. argv.append(expand(append))
  390. return multi_args_t(nodename, argv, expand, name)
  391. def multi_args(p, cmd='celery worker', append='', prefix='', suffix=''):
  392. names = p.values
  393. options = dict(p.options)
  394. ranges = len(names) == 1
  395. if ranges:
  396. try:
  397. names, prefix = _get_ranges(names)
  398. except ValueError:
  399. pass
  400. cmd = options.pop('--cmd', cmd)
  401. append = options.pop('--append', append)
  402. hostname = options.pop('--hostname',
  403. options.pop('-n', gethostname()))
  404. prefix = options.pop('--prefix', prefix) or ''
  405. suffix = options.pop('--suffix', suffix) or hostname
  406. suffix = '' if suffix in ('""', "''") else suffix
  407. _update_ns_opts(p, names)
  408. _update_ns_ranges(p, ranges)
  409. return (_args_for_node(p, name, prefix, suffix, cmd, append, options)
  410. for name in names)
  411. def _get_ranges(names):
  412. noderange = int(names[0])
  413. names = [str(n) for n in range(1, noderange + 1)]
  414. prefix = 'celery'
  415. return names, prefix
  416. def _update_ns_opts(p, names):
  417. # Numbers in args always refers to the index in the list of names.
  418. # (e.g. `start foo bar baz -c:1` where 1 is foo, 2 is bar, and so on).
  419. for ns_name, ns_opts in list(items(p.namespaces)):
  420. if ns_name.isdigit():
  421. ns_index = int(ns_name) - 1
  422. if ns_index < 0:
  423. raise KeyError('Indexes start at 1 got: %r' % (ns_name,))
  424. try:
  425. p.namespaces[names[ns_index]].update(ns_opts)
  426. except IndexError:
  427. raise KeyError('No node at index %r' % (ns_name,))
  428. def _update_ns_ranges(p, ranges):
  429. for ns_name, ns_opts in list(items(p.namespaces)):
  430. if ',' in ns_name or (ranges and '-' in ns_name):
  431. for subns in parse_ns_range(ns_name, ranges):
  432. p.namespaces[subns].update(ns_opts)
  433. p.namespaces.pop(ns_name)
  434. def _get_nodename(name, prefix, suffix, options):
  435. hostname = suffix
  436. if '@' in name:
  437. nodename = options['-n'] = host_format(name)
  438. shortname, hostname = nodesplit(nodename)
  439. name = shortname
  440. else:
  441. shortname = '%s%s' % (prefix, name)
  442. nodename = options['-n'] = host_format(
  443. '{0}@{1}'.format(shortname, hostname),
  444. )
  445. expand = partial(
  446. node_format, nodename=nodename, N=shortname, d=hostname,
  447. h=nodename, i='%i', I='%I',
  448. )
  449. return name, nodename, expand
  450. class NamespacedOptionParser(object):
  451. def __init__(self, args):
  452. self.args = args
  453. self.options = OrderedDict()
  454. self.values = []
  455. self.passthrough = ''
  456. self.namespaces = defaultdict(lambda: OrderedDict())
  457. self.parse()
  458. def parse(self):
  459. rargs = list(self.args)
  460. pos = 0
  461. while pos < len(rargs):
  462. arg = rargs[pos]
  463. if arg == '--':
  464. self.passthrough = ' '.join(rargs[pos:])
  465. break
  466. elif arg[0] == '-':
  467. if arg[1] == '-':
  468. self.process_long_opt(arg[2:])
  469. else:
  470. value = None
  471. if len(rargs) > pos + 1 and rargs[pos + 1][0] != '-':
  472. value = rargs[pos + 1]
  473. pos += 1
  474. self.process_short_opt(arg[1:], value)
  475. else:
  476. self.values.append(arg)
  477. pos += 1
  478. def process_long_opt(self, arg, value=None):
  479. if '=' in arg:
  480. arg, value = arg.split('=', 1)
  481. self.add_option(arg, value, short=False)
  482. def process_short_opt(self, arg, value=None):
  483. self.add_option(arg, value, short=True)
  484. def optmerge(self, ns, defaults=None):
  485. if defaults is None:
  486. defaults = self.options
  487. return OrderedDict(defaults, **self.namespaces[ns])
  488. def add_option(self, name, value, short=False, ns=None):
  489. prefix = short and '-' or '--'
  490. dest = self.options
  491. if ':' in name:
  492. name, ns = name.split(':')
  493. dest = self.namespaces[ns]
  494. dest[prefix + name] = value
  495. def quote(v):
  496. return "\\'".join("'" + p + "'" for p in v.split("'"))
  497. def format_opt(opt, value):
  498. if not value:
  499. return opt
  500. if opt.startswith('--'):
  501. return '{0}={1}'.format(opt, value)
  502. return '{0} {1}'.format(opt, value)
  503. def parse_ns_range(ns, ranges=False):
  504. ret = []
  505. for space in ',' in ns and ns.split(',') or [ns]:
  506. if ranges and '-' in space:
  507. start, stop = space.split('-')
  508. ret.extend(
  509. str(n) for n in range(int(start), int(stop) + 1)
  510. )
  511. else:
  512. ret.append(space)
  513. return ret
  514. def findsig(args, default=signal.SIGTERM):
  515. for arg in reversed(args):
  516. if len(arg) == 2 and arg[0] == '-':
  517. try:
  518. return int(arg[1])
  519. except ValueError:
  520. pass
  521. if arg[0] == '-':
  522. maybe_sig = 'SIG' + arg[1:]
  523. if maybe_sig in SIGNAMES:
  524. return getattr(signal, maybe_sig)
  525. return default
  526. def _getopt(d, alt):
  527. for opt in alt:
  528. try:
  529. return d[opt]
  530. except KeyError:
  531. pass
  532. raise KeyError(alt[0])
  533. def _setdefaultopt(d, alt, value):
  534. for opt in alt[1:]:
  535. try:
  536. return d[opt]
  537. except KeyError:
  538. pass
  539. return d.setdefault(alt[0], value)
  540. if __name__ == '__main__': # pragma: no cover
  541. main()