Skip to content

Commit 382a563

Browse files
romaskuRoman Skurikhin
andauthored
bpo-40607: Reraise exception during task cancelation in asyncio.wait_for() (GH-20054)
Currently, if asyncio.wait_for() timeout expires, it cancels inner future and then always raises TimeoutError. In case those future is task, it can handle cancelation mannually, and those process can lead to some other exception. Current implementation silently loses thoses exception. To resolve this, wait_for will check was the cancelation successfull or not. In case there was exception, wait_for will reraise it. Co-authored-by: Roman Skurikhin <[email protected]>
1 parent c087a26 commit 382a563

File tree

5 files changed

+66
-6
lines changed

5 files changed

+66
-6
lines changed

Doc/library/asyncio-task.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,8 @@ Timeouts
453453
wrap it in :func:`shield`.
454454

455455
The function will wait until the future is actually cancelled,
456-
so the total wait time may exceed the *timeout*.
456+
so the total wait time may exceed the *timeout*. If an exception
457+
happens during cancellation, it is propagated.
457458

458459
If the wait is cancelled, the future *aw* is also cancelled.
459460

Lib/asyncio/tasks.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,15 @@ async def wait_for(fut, timeout, *, loop=None):
496496
# after wait_for() returns.
497497
# See https://bugs.python.org/issue32751
498498
await _cancel_and_wait(fut, loop=loop)
499-
raise exceptions.TimeoutError()
499+
# In case task cancellation failed with some
500+
# exception, we should re-raise it
501+
# See https://bugs.python.org/issue40607
502+
try:
503+
fut.result()
504+
except exceptions.CancelledError as exc:
505+
raise exceptions.TimeoutError() from exc
506+
else:
507+
raise exceptions.TimeoutError()
500508
finally:
501509
timeout_handle.cancel()
502510

Lib/test/test_asyncio/test_tasks.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ def __await__(self):
8080
return self
8181

8282

83+
# The following value can be used as a very small timeout:
84+
# it passes check "timeout > 0", but has almost
85+
# no effect on the test performance
86+
_EPSILON = 0.0001
87+
88+
8389
class BaseTaskTests:
8490

8591
Task = None
@@ -904,12 +910,53 @@ async def inner():
904910

905911
inner_task = self.new_task(loop, inner())
906912

907-
with self.assertRaises(asyncio.TimeoutError):
908-
await asyncio.wait_for(inner_task, timeout=0.1)
913+
await asyncio.wait_for(inner_task, timeout=_EPSILON)
909914

910-
self.assertTrue(task_done)
915+
with self.assertRaises(asyncio.TimeoutError) as cm:
916+
loop.run_until_complete(foo())
911917

912-
loop.run_until_complete(foo())
918+
self.assertTrue(task_done)
919+
chained = cm.exception.__context__
920+
self.assertEqual(type(chained), asyncio.CancelledError)
921+
922+
def test_wait_for_reraises_exception_during_cancellation(self):
923+
loop = asyncio.new_event_loop()
924+
self.addCleanup(loop.close)
925+
926+
class FooException(Exception):
927+
pass
928+
929+
async def foo():
930+
async def inner():
931+
try:
932+
await asyncio.sleep(0.2)
933+
finally:
934+
raise FooException
935+
936+
inner_task = self.new_task(loop, inner())
937+
938+
await asyncio.wait_for(inner_task, timeout=_EPSILON)
939+
940+
with self.assertRaises(FooException):
941+
loop.run_until_complete(foo())
942+
943+
def test_wait_for_raises_timeout_error_if_returned_during_cancellation(self):
944+
loop = asyncio.new_event_loop()
945+
self.addCleanup(loop.close)
946+
947+
async def foo():
948+
async def inner():
949+
try:
950+
await asyncio.sleep(0.2)
951+
except asyncio.CancelledError:
952+
return 42
953+
954+
inner_task = self.new_task(loop, inner())
955+
956+
await asyncio.wait_for(inner_task, timeout=_EPSILON)
957+
958+
with self.assertRaises(asyncio.TimeoutError):
959+
loop.run_until_complete(foo())
913960

914961
def test_wait_for_self_cancellation(self):
915962
loop = asyncio.new_event_loop()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,7 @@ J. Sipprell
15931593
Ngalim Siregar
15941594
Kragen Sitaker
15951595
Kaartic Sivaraam
1596+
Roman Skurikhin
15961597
Ville Skyttä
15971598
Michael Sloan
15981599
Nick Sloan
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When cancelling a task due to timeout, :meth:`asyncio.wait_for` will now
2+
propagate the exception if an error happens during cancellation.
3+
Patch by Roman Skurikhin.

0 commit comments

Comments
 (0)