bpo-32684: Fix gather to propagate cancel of itself with return_exceptions (GH-7209)

This commit is contained in:
Yury Selivanov 2018-05-29 17:20:02 -04:00 committed by GitHub
parent 1cee216cf3
commit 863b674909
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 46 additions and 2 deletions

View file

@ -640,6 +640,10 @@ Task functions
outer Future is *not* cancelled in this case. (This is to prevent the outer Future is *not* cancelled in this case. (This is to prevent the
cancellation of one child to cause other children to be cancelled.) cancellation of one child to cause other children to be cancelled.)
.. versionchanged:: 3.7.0
If the *gather* itself is cancelled, the cancellation is propagated
regardless of *return_exceptions*.
.. function:: iscoroutine(obj) .. function:: iscoroutine(obj)
Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`, Return ``True`` if *obj* is a :ref:`coroutine object <coroutine>`,

View file

@ -591,6 +591,7 @@ class _GatheringFuture(futures.Future):
def __init__(self, children, *, loop=None): def __init__(self, children, *, loop=None):
super().__init__(loop=loop) super().__init__(loop=loop)
self._children = children self._children = children
self._cancel_requested = False
def cancel(self): def cancel(self):
if self.done(): if self.done():
@ -599,6 +600,11 @@ class _GatheringFuture(futures.Future):
for child in self._children: for child in self._children:
if child.cancel(): if child.cancel():
ret = True ret = True
if ret:
# If any child tasks were actually cancelled, we should
# propagate the cancellation request regardless of
# *return_exceptions* argument. See issue 32684.
self._cancel_requested = True
return ret return ret
@ -673,7 +679,13 @@ def gather(*coros_or_futures, loop=None, return_exceptions=False):
res = fut.result() res = fut.result()
results.append(res) results.append(res)
outer.set_result(results) if outer._cancel_requested:
# If gather is being cancelled we must propagate the
# cancellation regardless of *return_exceptions* argument.
# See issue 32684.
outer.set_exception(futures.CancelledError())
else:
outer.set_result(results)
arg_to_fut = {} arg_to_fut = {}
children = [] children = []

View file

@ -2037,7 +2037,7 @@ class BaseTaskTests:
def test_cancel_wait_for(self): def test_cancel_wait_for(self):
self._test_cancel_wait_for(60.0) self._test_cancel_wait_for(60.0)
def test_cancel_gather(self): def test_cancel_gather_1(self):
"""Ensure that a gathering future refuses to be cancelled once all """Ensure that a gathering future refuses to be cancelled once all
children are done""" children are done"""
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
@ -2067,6 +2067,33 @@ class BaseTaskTests:
self.assertFalse(gather_task.cancelled()) self.assertFalse(gather_task.cancelled())
self.assertEqual(gather_task.result(), [42]) self.assertEqual(gather_task.result(), [42])
def test_cancel_gather_2(self):
loop = asyncio.new_event_loop()
self.addCleanup(loop.close)
async def test():
time = 0
while True:
time += 0.05
await asyncio.gather(asyncio.sleep(0.05),
return_exceptions=True,
loop=loop)
if time > 1:
return
async def main():
qwe = asyncio.Task(test())
await asyncio.sleep(0.2)
qwe.cancel()
try:
await qwe
except asyncio.CancelledError:
pass
else:
self.fail('gather did not propagate the cancellation request')
loop.run_until_complete(main())
def test_exception_traceback(self): def test_exception_traceback(self):
# See http://bugs.python.org/issue28843 # See http://bugs.python.org/issue28843

View file

@ -0,0 +1 @@
Fix gather to propagate cancellation of itself even with return_exceptions.