Repository navigation
AsyncIO's wait_for can hide cancellation in a rare race condition #86296
Description
Activity
tvoinarovskyi commented
on Oct 23, 2020 tvoinarovskyimannequinMannequinAuthorMore actionsHi, during migration to Python 3.8.6 we encountered a behavior change from previous versions: wait_for ignored the request to cancellation and returned instead. After investigation, it seems to be related to the update in bpo-32751 and is only reproduced if the waited task is finished when cancellation of wait_for happens (code mistakes external CancelledError for a timeout).
The following example can reproduce the behavior on both 3.8.6 and 3.9.0 for me:
import asyncio async def inner(): return async def with_for_coro(): await asyncio.wait_for(inner(), timeout=100) await asyncio.sleep(1) print('End of with_for_coro. Should not be reached!') async def main(): task = asyncio.create_task(with_for_coro()) await asyncio.sleep(0) assert not task.done() task.cancel() print('Called task.cancel()') await task # -> You would expect a CancelledError to be raised. asyncio.run(main())Changing the wait time before cancellation slightly will return the correct behavior and CancelledError will be raised.
- added3.8 (EOL)end of lifeend of life3.9 (EOL)end of lifeend of life3.10 (EOL)end of lifeend of lifetype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Oct 23, 2020 tvoinarovskyi commented
on Oct 25, 2020 tvoinarovskyimannequinMannequinAuthorMore actionsHi Chris,
Yes, I do believe that is the respectful change, if we look the CancelledError is not checked to be external or originate from the timer.
Best regards,
Taras VoinarovskyiI've also found this deficiency of asyncio.wait_for by debugging an obscure hang in an application. Back then I've quickly made an issue about it: https://bugs.python.org/issue43389
I've just closed it as duplicate, since this issue covers the same bug and has been around longer.
I'm surprised this issue has not got more attention. This definitely needs a fix. Ignoring a CancellationError is something that is heavily discouraged by the documentation in general. Silently ignoring/eating a cancellation makes this construct unreliable without good workarounds, aside from not using it.
For a specific application, this was so broken, that I had to come up with a fix in the short term. I made an asyncio.wait_for variant available as a library that fixes the problem: https://github.com/Traktormaster/wait-for2
The repository has a detailed description of the issue and the way it fixes it. It also has test cases to assert the behaviour of the builtin and the fixed wait_for construct from the library.
Echoing nmatravolgyi's comments - I got here after hitting this bug and I too am amazed it's been around so long and hasn't been addressed.
It makes cancellation in my application very unreliable, and the reason I need well-controlled cancellation semantics is to allow emergency stop of physical movement.
It has been addressed, PR should be merged this week: #28149
Like most open source projects, it just needed someone to actually propose a fix.
can this be fixed by implementing wait_for with
asyncio.timeout?:from . import timeouts async def wait_for(aw, timeout): async with timeouts.timeout(timeout): return await aw
Reacted by ghost4318 remaining items
I believe using
asyncio.shield()can offer a work around to this in at least Python 3.8.6 (I've only tested it in this version). Modifying the test script that reproduces the issue given here as follows (wrapping the coroutine in thewait_for()call inasyncio.shield()):import asyncio from typing import List async def left(tasks: List[asyncio.Task], event: asyncio.Event): me = asyncio.current_task() assert tasks[0] is me await asyncio.sleep(0.1) event.set() tasks[1].cancel() print('cancelled right') async def right(tasks: List[asyncio.Task], event: asyncio.Event, use_wait_for: bool): try: me = asyncio.current_task() assert tasks[1] is me if use_wait_for: await asyncio.wait_for(asyncio.shield(event.wait()), 1) else: await event.wait() print('right done, was not cancelled') except asyncio.CancelledError: print('right got cancelled') raise async def left_right(use_wait_for: bool): tasks = [] event = asyncio.Event() tasks.append(asyncio.create_task(left(tasks, event))) tasks.append(asyncio.create_task(right(tasks, event, use_wait_for))) await asyncio.wait(tasks) if __name__ == '__main__': print('Not using wait_for():') asyncio.run(left_right(False)) print('\nUsing wait_for():') asyncio.run(left_right(True))Gives the desired output:
Not using wait_for(): cancelled right right got cancelled Using wait_for(): cancelled right right got cancelledThis suggests the issue is to do with the exception being swallowed somewhere deep in the call stack.
- added a commit that references this issue
on Nov 17, 2022 And fixed in 3.12 by gh-96764. (We're not 100.00% sure that the fix doesn't disturb some workaround, so we're not backporting the fix to 3.11 even though that has timeout(), and we would have to devise a totally different fix for 3.10, which we're not inclined to do, sorry. But going forward it should be fixed.
- added 2 commits that reference this issue
on Aug 4, 2023 - added a commit that references this issue
on Apr 2, 2024 for feedback I just wasted several partial days tracking a production only issue that only happens once a day almost certainly due to this (add logging, narrow down, add more logging..etc...etc). I'd reconsider the decision as it can waste many development hours trying to find what the issue is when the code at hand seems reasonable, until you come to the conclusion that the library must be swallowing the exception. Either that or at a minimum update the docs to say don't use it in python<3.12 or there be dragons.
Reacted by PaulRosenfield and Nikit0xic- added a commit that references this issue
on Jun 13, 2026 - added a commit that references this issue
on Aug 10, 2026 - added a commit that references this issue
on Aug 31, 2026 - added a commit that references this issue
on Sep 24, 2026 - added a commit that references this issue
on Oct 6, 2026
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs