suite.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. from __future__ import absolute_import, print_function, unicode_literals
  2. import platform
  3. import random
  4. import socket
  5. import sys
  6. from collections import namedtuple
  7. from itertools import count
  8. from time import sleep
  9. from kombu.utils.compat import OrderedDict
  10. from celery import group, VERSION_BANNER
  11. from celery.exceptions import TimeoutError
  12. from celery.five import range, values, monotonic
  13. from celery.utils.debug import blockdetection
  14. from celery.utils.text import pluralize, truncate
  15. from celery.utils.timeutils import humanize_seconds
  16. from .app import (
  17. marker, _marker, add, any_, exiting, kill, sleeping,
  18. sleeping_ignore_limits, segfault, any_returning,
  19. )
  20. from .data import BIG, SMALL
  21. from .fbi import FBI
  22. BANNER = """\
  23. Celery stress-suite v{version}
  24. {platform}
  25. [config]
  26. .> broker: {conninfo}
  27. [toc: {total} {TESTS} total]
  28. {toc}
  29. """
  30. F_PROGRESS = """\
  31. {0.index}: {0.test.__name__}({0.iteration}/{0.total_iterations}) \
  32. rep#{0.repeats} runtime: {runtime}/{elapsed} \
  33. """
  34. Progress = namedtuple('Progress', (
  35. 'test', 'iteration', 'total_iterations',
  36. 'index', 'repeats', 'runtime', 'elapsed', 'completed',
  37. ))
  38. Inf = float('Inf')
  39. class StopSuite(Exception):
  40. pass
  41. def pstatus(p):
  42. return F_PROGRESS.format(
  43. p,
  44. runtime=humanize_seconds(monotonic() - p.runtime, now='0 seconds'),
  45. elapsed=humanize_seconds(monotonic() - p.elapsed, now='0 seconds'),
  46. )
  47. class Speaker(object):
  48. def __init__(self, gap=5.0):
  49. self.gap = gap
  50. self.last_noise = monotonic() - self.gap * 2
  51. def beep(self):
  52. now = monotonic()
  53. if now - self.last_noise >= self.gap:
  54. self.emit()
  55. self.last_noise = now
  56. def emit(self):
  57. print('\a', file=sys.stderr, end='')
  58. def testgroup(*funs):
  59. return OrderedDict((fun.__name__, fun) for fun in funs)
  60. class Suite(object):
  61. def __init__(self, app, block_timeout=30 * 60):
  62. self.app = app
  63. self.connerrors = self.app.connection().recoverable_connection_errors
  64. self.block_timeout = block_timeout
  65. self.progress = None
  66. self.speaker = Speaker()
  67. self.fbi = FBI(app)
  68. self.groups = {
  69. 'all': testgroup(
  70. self.manyshort,
  71. self.termbysig,
  72. self.bigtasks,
  73. self.bigtasksbigvalue,
  74. self.smalltasks,
  75. self.timelimits,
  76. self.always_timeout,
  77. self.timelimits_soft,
  78. self.revoketermfast,
  79. self.revoketermslow,
  80. self.alwayskilled,
  81. self.alwaysexits,
  82. ),
  83. 'green': testgroup(
  84. self.manyshort,
  85. self.bigtasks,
  86. self.bigtasksbigvalue,
  87. self.smalltasks,
  88. self.alwaysexits,
  89. self.group_with_exit,
  90. ),
  91. }
  92. def run(self, names=None, iterations=50, offset=0,
  93. numtests=None, list_all=False, repeat=0, group='all',
  94. diag=False, no_join=False, **kw):
  95. self.no_join = no_join
  96. self.fbi.enable(diag)
  97. tests = self.filtertests(group, names)[offset:numtests or None]
  98. if list_all:
  99. return print(self.testlist(tests))
  100. print(self.banner(tests))
  101. print('+ Enabling events')
  102. self.app.control.enable_events()
  103. it = count() if repeat == Inf else range(int(repeat) or 1)
  104. for i in it:
  105. marker(
  106. 'Stresstest suite start (repetition {0})'.format(i + 1),
  107. '+',
  108. )
  109. for j, test in enumerate(tests):
  110. self.runtest(test, iterations, j + 1, i + 1)
  111. marker(
  112. 'Stresstest suite end (repetition {0})'.format(i + 1),
  113. '+',
  114. )
  115. def filtertests(self, group, names):
  116. tests = self.groups[group]
  117. try:
  118. return ([tests[n] for n in names] if names
  119. else list(values(tests)))
  120. except KeyError as exc:
  121. raise KeyError('Unknown test name: {0}'.format(exc))
  122. def testlist(self, tests):
  123. return ',\n'.join(
  124. '.> {0}) {1}'.format(i + 1, t.__name__)
  125. for i, t in enumerate(tests)
  126. )
  127. def banner(self, tests):
  128. app = self.app
  129. return BANNER.format(
  130. app='{0}:0x{1:x}'.format(app.main or '__main__', id(app)),
  131. version=VERSION_BANNER,
  132. conninfo=app.connection().as_uri(),
  133. platform=platform.platform(),
  134. toc=self.testlist(tests),
  135. TESTS=pluralize(len(tests), 'test'),
  136. total=len(tests),
  137. )
  138. def manyshort(self):
  139. self.join(group(add.si(i, i) for i in range(1000))(),
  140. timeout=10, propagate=True)
  141. def runtest(self, fun, n=50, index=0, repeats=1):
  142. print('{0}: [[[{1}({2})]]]'.format(repeats, fun.__name__, n))
  143. with blockdetection(self.block_timeout):
  144. with self.fbi.investigation():
  145. runtime = elapsed = monotonic()
  146. i = 0
  147. failed = False
  148. self.progress = Progress(
  149. fun, i, n, index, repeats, elapsed, runtime, 0,
  150. )
  151. _marker.delay(pstatus(self.progress))
  152. try:
  153. for i in range(n):
  154. runtime = monotonic()
  155. self.progress = Progress(
  156. fun, i + 1, n, index, repeats, runtime, elapsed, 0,
  157. )
  158. try:
  159. fun()
  160. except StopSuite:
  161. raise
  162. except Exception as exc:
  163. print('-> {0!r}'.format(exc))
  164. print(pstatus(self.progress))
  165. else:
  166. print(pstatus(self.progress))
  167. except Exception:
  168. failed = True
  169. self.speaker.beep()
  170. raise
  171. finally:
  172. print('{0} {1} iterations in {2}s'.format(
  173. 'failed after' if failed else 'completed',
  174. i + 1, humanize_seconds(monotonic() - elapsed),
  175. ))
  176. if not failed:
  177. self.progress = Progress(
  178. fun, i + 1, n, index, repeats, runtime, elapsed, 1,
  179. )
  180. def always_timeout(self):
  181. self.join(
  182. group(sleeping.s(1).set(time_limit=0.1)
  183. for _ in range(100))(),
  184. timeout=10, propagate=True,
  185. )
  186. def termbysig(self):
  187. self._evil_groupmember(kill)
  188. def group_with_exit(self):
  189. self._evil_groupmember(exiting)
  190. def termbysegfault(self):
  191. self._evil_groupmember(segfault)
  192. def timelimits(self):
  193. self._evil_groupmember(sleeping, 2, time_limit=1)
  194. def timelimits_soft(self):
  195. self._evil_groupmember(sleeping_ignore_limits, 2,
  196. soft_time_limit=1, time_limit=1.1)
  197. def alwayskilled(self):
  198. g = group(kill.s() for _ in range(10))
  199. self.join(g(), timeout=10)
  200. def alwaysexits(self):
  201. g = group(exiting.s() for _ in range(10))
  202. self.join(g(), timeout=10)
  203. def _evil_groupmember(self, evil_t, *eargs, **opts):
  204. g1 = group(add.s(2, 2).set(**opts), evil_t.s(*eargs).set(**opts),
  205. add.s(4, 4).set(**opts), add.s(8, 8).set(**opts))
  206. g2 = group(add.s(3, 3).set(**opts), add.s(5, 5).set(**opts),
  207. evil_t.s(*eargs).set(**opts), add.s(7, 7).set(**opts))
  208. self.join(g1(), timeout=10)
  209. self.join(g2(), timeout=10)
  210. def bigtasksbigvalue(self):
  211. g = group(any_returning.s(BIG, sleep=0.3) for i in range(8))
  212. r = g()
  213. try:
  214. self.join(r, timeout=10)
  215. finally:
  216. # very big values so remove results from backend
  217. try:
  218. r.forget()
  219. except NotImplementedError:
  220. pass
  221. def bigtasks(self, wait=None):
  222. self._revoketerm(wait, False, False, BIG)
  223. def smalltasks(self, wait=None):
  224. self._revoketerm(wait, False, False, SMALL)
  225. def revoketermfast(self, wait=None):
  226. self._revoketerm(wait, True, False, SMALL)
  227. def revoketermslow(self, wait=5):
  228. self._revoketerm(wait, True, True, BIG)
  229. def _revoketerm(self, wait=None, terminate=True,
  230. joindelay=True, data=BIG):
  231. g = group(any_.s(data, sleep=wait) for i in range(8))
  232. r = g()
  233. if terminate:
  234. if joindelay:
  235. sleep(random.choice(range(4)))
  236. r.revoke(terminate=True)
  237. self.join(r, timeout=10)
  238. def missing_results(self, r):
  239. return [res.id for res in r if res.id not in res.backend._cache]
  240. def join(self, r, propagate=False, max_retries=10, **kwargs):
  241. if self.no_join:
  242. return
  243. received = []
  244. def on_result(task_id, value):
  245. received.append(task_id)
  246. for i in range(max_retries) if max_retries else count(0):
  247. received[:] = []
  248. try:
  249. return r.get(callback=on_result, propagate=propagate, **kwargs)
  250. except (socket.timeout, TimeoutError) as exc:
  251. waiting_for = self.missing_results(r)
  252. self.speaker.beep()
  253. marker(
  254. 'Still waiting for {0}/{1}: [{2}]: {3!r}'.format(
  255. len(r) - len(received), len(r),
  256. truncate(', '.join(waiting_for)), exc), '!',
  257. )
  258. self.fbi.diag(waiting_for)
  259. except self.connerrors as exc:
  260. self.speaker.beep()
  261. marker('join: connection lost: {0!r}'.format(exc), '!')
  262. raise StopSuite('Test failed: Missing task results')
  263. def dump_progress(self):
  264. return pstatus(self.progress) if self.progress else 'No test running'