Skip to content

Commit 3b263e6

Browse files
authored
bpo-32684: Fix gather to propagate cancel of itself with return_exceptions (GH-7224)
1 parent 51bf38f commit 3b263e6

File tree

4 files changed

+43
-2
lines changed

4 files changed

+43
-2
lines changed

Doc/library/asyncio-task.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,10 @@ Task functions
568568
outer Future is *not* cancelled in this case. (This is to prevent the
569569
cancellation of one child to cause other children to be cancelled.)
570570

571+
.. versionchanged:: 3.6.6
572+
If the *gather* itself is cancelled, the cancellation is propagated
573+
regardless of *return_exceptions*.
574+
571575
.. function:: iscoroutine(obj)
572576

573577
Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`,

Lib/asyncio/tasks.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,7 @@ class _GatheringFuture(futures.Future):
548548
def __init__(self, children, *, loop=None):
549549
super().__init__(loop=loop)
550550
self._children = children
551+
self._cancel_requested = False
551552

552553
def cancel(self):
553554
if self.done():
@@ -556,6 +557,11 @@ def cancel(self):
556557
for child in self._children:
557558
if child.cancel():
558559
ret = True
560+
if ret:
561+
# If any child tasks were actually cancelled, we should
562+
# propagate the cancellation request regardless of
563+
# *return_exceptions* argument. See issue 32684.
564+
self._cancel_requested = True
559565
return ret
560566

561567

@@ -636,7 +642,10 @@ def _done_callback(i, fut):
636642
results[i] = res
637643
nfinished += 1
638644
if nfinished == nchildren:
639-
outer.set_result(results)
645+
if outer._cancel_requested:
646+
outer.set_exception(futures.CancelledError())
647+
else:
648+
outer.set_result(results)
640649

641650
for i, fut in enumerate(children):
642651
fut.add_done_callback(functools.partial(_done_callback, i))

Lib/test/test_asyncio/test_tasks.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1991,7 +1991,7 @@ def test_cancel_blocking_wait_for(self):
19911991
def test_cancel_wait_for(self):
19921992
self._test_cancel_wait_for(60.0)
19931993

1994-
def test_cancel_gather(self):
1994+
def test_cancel_gather_1(self):
19951995
"""Ensure that a gathering future refuses to be cancelled once all
19961996
children are done"""
19971997
loop = asyncio.new_event_loop()
@@ -2021,6 +2021,33 @@ def cancelling_callback(_):
20212021
self.assertFalse(gather_task.cancelled())
20222022
self.assertEqual(gather_task.result(), [42])
20232023

2024+
def test_cancel_gather_2(self):
2025+
loop = asyncio.new_event_loop()
2026+
self.addCleanup(loop.close)
2027+
2028+
async def test():
2029+
time = 0
2030+
while True:
2031+
time += 0.05
2032+
await asyncio.gather(asyncio.sleep(0.05, loop=loop),
2033+
return_exceptions=True,
2034+
loop=loop)
2035+
if time > 1:
2036+
return
2037+
2038+
async def main():
2039+
qwe = self.new_task(loop, test())
2040+
await asyncio.sleep(0.2, loop=loop)
2041+
qwe.cancel()
2042+
try:
2043+
await qwe
2044+
except asyncio.CancelledError:
2045+
pass
2046+
else:
2047+
self.fail('gather did not propagate the cancellation request')
2048+
2049+
loop.run_until_complete(main())
2050+
20242051
def test_exception_traceback(self):
20252052
# See http://bugs.python.org/issue28843
20262053

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix gather to propagate cancellation of itself even with return_exceptions.

0 commit comments

Comments
 (0)