test_canvas.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. from __future__ import absolute_import, unicode_literals
  2. from datetime import datetime, timedelta
  3. import pytest
  4. from celery import chain, chord, group
  5. from celery.exceptions import TimeoutError
  6. from celery.result import AsyncResult, GroupResult, ResultSet
  7. from .conftest import flaky, get_active_redis_channels, get_redis_connection
  8. from .tasks import (add, add_chord_to_chord, add_replaced, add_to_all,
  9. add_to_all_to_chord, collect_ids, delayed_sum,
  10. delayed_sum_with_soft_guard, identity, ids, print_unicode,
  11. redis_echo, second_order_replace1, tsum)
  12. TIMEOUT = 120
  13. class test_chain:
  14. @flaky
  15. def test_simple_chain(self, manager):
  16. c = add.s(4, 4) | add.s(8) | add.s(16)
  17. assert c().get(timeout=TIMEOUT) == 32
  18. @flaky
  19. def test_single_chain(self, manager):
  20. c = chain(add.s(3, 4))()
  21. assert c.get(timeout=TIMEOUT) == 7
  22. @flaky
  23. def test_complex_chain(self, manager):
  24. c = (
  25. add.s(2, 2) | (
  26. add.s(4) | add_replaced.s(8) | add.s(16) | add.s(32)
  27. ) |
  28. group(add.s(i) for i in range(4))
  29. )
  30. res = c()
  31. assert res.get(timeout=TIMEOUT) == [64, 65, 66, 67]
  32. @flaky
  33. def test_group_results_in_chain(self, manager):
  34. # This adds in an explicit test for the special case added in commit
  35. # 1e3fcaa969de6ad32b52a3ed8e74281e5e5360e6
  36. c = (
  37. group(
  38. add.s(1, 2) | group(
  39. add.s(1), add.s(2)
  40. )
  41. )
  42. )
  43. res = c()
  44. assert res.get(timeout=TIMEOUT) == [4, 5]
  45. @flaky
  46. def test_chain_inside_group_receives_arguments(self, manager):
  47. c = (
  48. add.s(5, 6) |
  49. group((add.s(1) | add.s(2), add.s(3)))
  50. )
  51. res = c()
  52. assert res.get(timeout=TIMEOUT) == [14, 14]
  53. @flaky
  54. def test_eager_chain_inside_task(self, manager):
  55. from .tasks import chain_add
  56. prev = chain_add.app.conf.task_always_eager
  57. chain_add.app.conf.task_always_eager = True
  58. chain_add.apply_async(args=(4, 8), throw=True).get()
  59. chain_add.app.conf.task_always_eager = prev
  60. @flaky
  61. def test_group_chord_group_chain(self, manager):
  62. from celery.five import bytes_if_py2
  63. if not manager.app.conf.result_backend.startswith('redis'):
  64. raise pytest.skip('Requires redis result backend.')
  65. redis_connection = get_redis_connection()
  66. redis_connection.delete('redis-echo')
  67. before = group(redis_echo.si('before {}'.format(i)) for i in range(3))
  68. connect = redis_echo.si('connect')
  69. after = group(redis_echo.si('after {}'.format(i)) for i in range(2))
  70. result = (before | connect | after).delay()
  71. result.get(timeout=TIMEOUT)
  72. redis_messages = list(map(
  73. bytes_if_py2,
  74. redis_connection.lrange('redis-echo', 0, -1)
  75. ))
  76. before_items = \
  77. set(map(bytes_if_py2, (b'before 0', b'before 1', b'before 2')))
  78. after_items = set(map(bytes_if_py2, (b'after 0', b'after 1')))
  79. assert set(redis_messages[:3]) == before_items
  80. assert redis_messages[3] == b'connect'
  81. assert set(redis_messages[4:]) == after_items
  82. redis_connection.delete('redis-echo')
  83. @flaky
  84. def test_second_order_replace(self, manager):
  85. from celery.five import bytes_if_py2
  86. if not manager.app.conf.result_backend.startswith('redis'):
  87. raise pytest.skip('Requires redis result backend.')
  88. redis_connection = get_redis_connection()
  89. redis_connection.delete('redis-echo')
  90. result = second_order_replace1.delay()
  91. result.get(timeout=TIMEOUT)
  92. redis_messages = list(map(
  93. bytes_if_py2,
  94. redis_connection.lrange('redis-echo', 0, -1)
  95. ))
  96. expected_messages = [b'In A', b'In B', b'In/Out C', b'Out B', b'Out A']
  97. assert redis_messages == expected_messages
  98. @flaky
  99. def test_parent_ids(self, manager, num=10):
  100. assert manager.inspect().ping()
  101. c = chain(ids.si(i=i) for i in range(num))
  102. c.freeze()
  103. res = c()
  104. try:
  105. res.get(timeout=TIMEOUT)
  106. except TimeoutError:
  107. print(manager.inspect.active())
  108. print(manager.inspect.reserved())
  109. print(manager.inspect.stats())
  110. raise
  111. self.assert_ids(res, num - 1)
  112. def assert_ids(self, res, size):
  113. i, root = size, res
  114. while root.parent:
  115. root = root.parent
  116. node = res
  117. while node:
  118. root_id, parent_id, value = node.get(timeout=30)
  119. assert value == i
  120. if node.parent:
  121. assert parent_id == node.parent.id
  122. assert root_id == root.id
  123. node = node.parent
  124. i -= 1
  125. def test_chord_soft_timeout_recuperation(self, manager):
  126. """Test that if soft timeout happens in task but is managed by task,
  127. chord still get results normally
  128. """
  129. if not manager.app.conf.result_backend.startswith('redis'):
  130. raise pytest.skip('Requires redis result backend.')
  131. c = chord([
  132. # return 3
  133. add.s(1, 2),
  134. # return 0 after managing soft timeout
  135. delayed_sum_with_soft_guard.s(
  136. [100], pause_time=2
  137. ).set(
  138. soft_time_limit=1
  139. ),
  140. ])
  141. result = c(delayed_sum.s(pause_time=0)).get()
  142. assert result == 3
  143. def test_chain_error_handler_with_eta(self, manager):
  144. try:
  145. manager.app.backend.ensure_chords_allowed()
  146. except NotImplementedError as e:
  147. raise pytest.skip(e.args[0])
  148. eta = datetime.utcnow() + timedelta(seconds=10)
  149. c = chain(
  150. group(
  151. add.s(1, 2),
  152. add.s(3, 4),
  153. ),
  154. tsum.s()
  155. ).on_error(print_unicode.s()).apply_async(eta=eta)
  156. result = c.get()
  157. assert result == 10
  158. class test_result_set:
  159. @flaky
  160. def test_result_set(self, manager):
  161. assert manager.inspect().ping()
  162. rs = ResultSet([add.delay(1, 1), add.delay(2, 2)])
  163. assert rs.get(timeout=TIMEOUT) == [2, 4]
  164. class test_group:
  165. @flaky
  166. def test_empty_group_result(self, manager):
  167. if not manager.app.conf.result_backend.startswith('redis'):
  168. raise pytest.skip('Requires redis result backend.')
  169. task = group([])
  170. result = task.apply_async()
  171. GroupResult.save(result)
  172. task = GroupResult.restore(result.id)
  173. assert task.results == []
  174. @flaky
  175. def test_parent_ids(self, manager):
  176. assert manager.inspect().ping()
  177. g = (
  178. ids.si(i=1) |
  179. ids.si(i=2) |
  180. group(ids.si(i=i) for i in range(2, 50))
  181. )
  182. res = g()
  183. expected_root_id = res.parent.parent.id
  184. expected_parent_id = res.parent.id
  185. values = res.get(timeout=TIMEOUT)
  186. for i, r in enumerate(values):
  187. root_id, parent_id, value = r
  188. assert root_id == expected_root_id
  189. assert parent_id == expected_parent_id
  190. assert value == i + 2
  191. @flaky
  192. def test_nested_group(self, manager):
  193. assert manager.inspect().ping()
  194. c = group(
  195. add.si(1, 10),
  196. group(
  197. add.si(1, 100),
  198. group(
  199. add.si(1, 1000),
  200. add.si(1, 2000),
  201. ),
  202. ),
  203. )
  204. res = c()
  205. assert res.get(timeout=TIMEOUT) == [11, 101, 1001, 2001]
  206. def assert_ids(r, expected_value, expected_root_id, expected_parent_id):
  207. root_id, parent_id, value = r.get(timeout=TIMEOUT)
  208. assert expected_value == value
  209. assert root_id == expected_root_id
  210. assert parent_id == expected_parent_id
  211. class test_chord:
  212. @flaky
  213. def test_redis_subscribed_channels_leak(self, manager):
  214. if not manager.app.conf.result_backend.startswith('redis'):
  215. raise pytest.skip('Requires redis result backend.')
  216. manager.app.backend.result_consumer.on_after_fork()
  217. initial_channels = get_active_redis_channels()
  218. initial_channels_count = len(initial_channels)
  219. total_chords = 10
  220. async_results = [
  221. chord([add.s(5, 6), add.s(6, 7)])(delayed_sum.s())
  222. for _ in range(total_chords)
  223. ]
  224. manager.assert_result_tasks_in_progress_or_completed(async_results)
  225. channels_before = get_active_redis_channels()
  226. channels_before_count = len(channels_before)
  227. assert set(channels_before) != set(initial_channels)
  228. assert channels_before_count > initial_channels_count
  229. # The total number of active Redis channels at this point
  230. # is the number of chord header tasks multiplied by the
  231. # total chord tasks, plus the initial channels
  232. # (existing from previous tests).
  233. chord_header_task_count = 2
  234. assert channels_before_count <= \
  235. chord_header_task_count * total_chords + initial_channels_count
  236. result_values = [
  237. result.get(timeout=TIMEOUT)
  238. for result in async_results
  239. ]
  240. assert result_values == [24] * total_chords
  241. channels_after = get_active_redis_channels()
  242. channels_after_count = len(channels_after)
  243. assert channels_after_count == initial_channels_count
  244. assert set(channels_after) == set(initial_channels)
  245. @flaky
  246. def test_replaced_nested_chord(self, manager):
  247. try:
  248. manager.app.backend.ensure_chords_allowed()
  249. except NotImplementedError as e:
  250. raise pytest.skip(e.args[0])
  251. c1 = chord([
  252. chord(
  253. [add.s(1, 2), add_replaced.s(3, 4)],
  254. add_to_all.s(5),
  255. ) | tsum.s(),
  256. chord(
  257. [add_replaced.s(6, 7), add.s(0, 0)],
  258. add_to_all.s(8),
  259. ) | tsum.s(),
  260. ], add_to_all.s(9))
  261. res1 = c1()
  262. assert res1.get(timeout=TIMEOUT) == [29, 38]
  263. @flaky
  264. def test_add_to_chord(self, manager):
  265. if not manager.app.conf.result_backend.startswith('redis'):
  266. raise pytest.skip('Requires redis result backend.')
  267. c = group([add_to_all_to_chord.s([1, 2, 3], 4)]) | identity.s()
  268. res = c()
  269. assert res.get() == [0, 5, 6, 7]
  270. @flaky
  271. def test_add_chord_to_chord(self, manager):
  272. if not manager.app.conf.result_backend.startswith('redis'):
  273. raise pytest.skip('Requires redis result backend.')
  274. c = group([add_chord_to_chord.s([1, 2, 3], 4)]) | identity.s()
  275. res = c()
  276. assert res.get() == [0, 5 + 6 + 7]
  277. @flaky
  278. def test_group_chain(self, manager):
  279. if not manager.app.conf.result_backend.startswith('redis'):
  280. raise pytest.skip('Requires redis result backend.')
  281. c = (
  282. add.s(2, 2) |
  283. group(add.s(i) for i in range(4)) |
  284. add_to_all.s(8)
  285. )
  286. res = c()
  287. assert res.get(timeout=TIMEOUT) == [12, 13, 14, 15]
  288. @flaky
  289. def test_nested_group_chain(self, manager):
  290. try:
  291. manager.app.backend.ensure_chords_allowed()
  292. except NotImplementedError as e:
  293. raise pytest.skip(e.args[0])
  294. if not manager.app.backend.supports_native_join:
  295. raise pytest.skip('Requires native join support.')
  296. c = chain(
  297. add.si(1, 0),
  298. group(
  299. add.si(1, 100),
  300. chain(
  301. add.si(1, 200),
  302. group(
  303. add.si(1, 1000),
  304. add.si(1, 2000),
  305. ),
  306. ),
  307. ),
  308. add.si(1, 10),
  309. )
  310. res = c()
  311. assert res.get(timeout=TIMEOUT) == 11
  312. @flaky
  313. def test_single_task_header(self, manager):
  314. try:
  315. manager.app.backend.ensure_chords_allowed()
  316. except NotImplementedError as e:
  317. raise pytest.skip(e.args[0])
  318. c1 = chord([add.s(2, 5)], body=add_to_all.s(9))
  319. res1 = c1()
  320. assert res1.get(timeout=TIMEOUT) == [16]
  321. c2 = group([add.s(2, 5)]) | add_to_all.s(9)
  322. res2 = c2()
  323. assert res2.get(timeout=TIMEOUT) == [16]
  324. def test_empty_header_chord(self, manager):
  325. try:
  326. manager.app.backend.ensure_chords_allowed()
  327. except NotImplementedError as e:
  328. raise pytest.skip(e.args[0])
  329. c1 = chord([], body=add_to_all.s(9))
  330. res1 = c1()
  331. assert res1.get(timeout=TIMEOUT) == []
  332. c2 = group([]) | add_to_all.s(9)
  333. res2 = c2()
  334. assert res2.get(timeout=TIMEOUT) == []
  335. @flaky
  336. def test_nested_chord(self, manager):
  337. try:
  338. manager.app.backend.ensure_chords_allowed()
  339. except NotImplementedError as e:
  340. raise pytest.skip(e.args[0])
  341. c1 = chord([
  342. chord([add.s(1, 2), add.s(3, 4)], add.s([5])),
  343. chord([add.s(6, 7)], add.s([10]))
  344. ], add_to_all.s(['A']))
  345. res1 = c1()
  346. assert res1.get(timeout=TIMEOUT) == [[3, 7, 5, 'A'], [13, 10, 'A']]
  347. c2 = group([
  348. group([add.s(1, 2), add.s(3, 4)]) | add.s([5]),
  349. group([add.s(6, 7)]) | add.s([10]),
  350. ]) | add_to_all.s(['A'])
  351. res2 = c2()
  352. assert res2.get(timeout=TIMEOUT) == [[3, 7, 5, 'A'], [13, 10, 'A']]
  353. c = group([
  354. group([
  355. group([
  356. group([
  357. add.s(1, 2)
  358. ]) | add.s([3])
  359. ]) | add.s([4])
  360. ]) | add.s([5])
  361. ]) | add.s([6])
  362. res = c()
  363. assert [[[[3, 3], 4], 5], 6] == res.get(timeout=TIMEOUT)
  364. @flaky
  365. def test_parent_ids(self, manager):
  366. if not manager.app.conf.result_backend.startswith('redis'):
  367. raise pytest.skip('Requires redis result backend.')
  368. root = ids.si(i=1)
  369. expected_root_id = root.freeze().id
  370. g = chain(
  371. root, ids.si(i=2),
  372. chord(
  373. group(ids.si(i=i) for i in range(3, 50)),
  374. chain(collect_ids.s(i=50) | ids.si(i=51)),
  375. ),
  376. )
  377. self.assert_parentids_chord(g(), expected_root_id)
  378. @flaky
  379. def test_parent_ids__OR(self, manager):
  380. if not manager.app.conf.result_backend.startswith('redis'):
  381. raise pytest.skip('Requires redis result backend.')
  382. root = ids.si(i=1)
  383. expected_root_id = root.freeze().id
  384. g = (
  385. root |
  386. ids.si(i=2) |
  387. group(ids.si(i=i) for i in range(3, 50)) |
  388. collect_ids.s(i=50) |
  389. ids.si(i=51)
  390. )
  391. self.assert_parentids_chord(g(), expected_root_id)
  392. def assert_parentids_chord(self, res, expected_root_id):
  393. assert isinstance(res, AsyncResult)
  394. assert isinstance(res.parent, AsyncResult)
  395. assert isinstance(res.parent.parent, GroupResult)
  396. assert isinstance(res.parent.parent.parent, AsyncResult)
  397. assert isinstance(res.parent.parent.parent.parent, AsyncResult)
  398. # first we check the last task
  399. assert_ids(res, 51, expected_root_id, res.parent.id)
  400. # then the chord callback
  401. prev, (root_id, parent_id, value) = res.parent.get(timeout=30)
  402. assert value == 50
  403. assert root_id == expected_root_id
  404. # started by one of the chord header tasks.
  405. assert parent_id in res.parent.parent.results
  406. # check what the chord callback recorded
  407. for i, p in enumerate(prev):
  408. root_id, parent_id, value = p
  409. assert root_id == expected_root_id
  410. assert parent_id == res.parent.parent.parent.id
  411. # ids(i=2)
  412. root_id, parent_id, value = res.parent.parent.parent.get(timeout=30)
  413. assert value == 2
  414. assert parent_id == res.parent.parent.parent.parent.id
  415. assert root_id == expected_root_id
  416. # ids(i=1)
  417. root_id, parent_id, value = res.parent.parent.parent.parent.get(
  418. timeout=30)
  419. assert value == 1
  420. assert root_id == expected_root_id
  421. assert parent_id is None