-
-
Notifications
You must be signed in to change notification settings - Fork 31.8k
bpo-32751: Wait for task cancellation in asyncio.wait_for() #7216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Currently, asyncio.wait_for(fut), upon reaching the timeout deadline, cancels the future and returns immediately. This is problematic for when *fut* is a Task, because it will be left running for an arbitrary amount of time. This behavior is iself surprising and may lead to related bugs such as the one described in bpo-33638: condition = asyncio.Condition() async with condition: await asyncio.wait_for(condition.wait(), timeout=0.5) Currently, instead of raising a TimeoutError, the above code will fail with `RuntimeError: cannot wait on un-acquired lock`, because `__aexit__` is reached _before_ `condition.wait()` finishes its cancellation and re-acquires the condition lock. To resolve this, make `wait_for` await for the task cancellation. The tradeoff here is that the `timeout` promise may be broken if the task decides to handle its cancellation in a slow way. This represents a behavior change and should probably not be back-patched to 3.6 and earlier.
Lib/asyncio/tasks.py
Outdated
# We cannot wait on *fut* directly to make | ||
# sure _cancel_and_wait itself is reliably cancellable. | ||
await waiter | ||
except futures.CancelledError: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this except
at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's an oversight from an earlier code iteration.
Lib/asyncio/tasks.py
Outdated
waiter = loop.create_future() | ||
cb = functools.partial(_release_waiter, waiter) | ||
fut.add_done_callback(cb) | ||
fut.cancel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd move 'fut.cancel()' into the 'try' block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect if a task has a slow cancellation routine -- many timeout related assumptions will be broken.
LGTM
|
||
try: | ||
fut.cancel() | ||
# We cannot wait on *fut* directly to make |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice trick!
Yeah. Broken timeouts is a small price to have the rest of asyncio code working correctly. A related bug where wait_for caused an incorrect |
…-7216) Currently, asyncio.wait_for(fut), upon reaching the timeout deadline, cancels the future and returns immediately. This is problematic for when *fut* is a Task, because it will be left running for an arbitrary amount of time. This behavior is iself surprising and may lead to related bugs such as the one described in bpo-33638: condition = asyncio.Condition() async with condition: await asyncio.wait_for(condition.wait(), timeout=0.5) Currently, instead of raising a TimeoutError, the above code will fail with `RuntimeError: cannot wait on un-acquired lock`, because `__aexit__` is reached _before_ `condition.wait()` finishes its cancellation and re-acquires the condition lock. To resolve this, make `wait_for` await for the task cancellation. The tradeoff here is that the `timeout` promise may be broken if the task decides to handle its cancellation in a slow way. This represents a behavior change and should probably not be back-patched to 3.6 and earlier. (cherry picked from commit e2b340a) Co-authored-by: Elvis Pranskevichus <elvis@magic.io>
GH-7223 is a backport of this pull request to the 3.7 branch. |
Currently, asyncio.wait_for(fut), upon reaching the timeout deadline, cancels the future and returns immediately. This is problematic for when *fut* is a Task, because it will be left running for an arbitrary amount of time. This behavior is iself surprising and may lead to related bugs such as the one described in bpo-33638: condition = asyncio.Condition() async with condition: await asyncio.wait_for(condition.wait(), timeout=0.5) Currently, instead of raising a TimeoutError, the above code will fail with `RuntimeError: cannot wait on un-acquired lock`, because `__aexit__` is reached _before_ `condition.wait()` finishes its cancellation and re-acquires the condition lock. To resolve this, make `wait_for` await for the task cancellation. The tradeoff here is that the `timeout` promise may be broken if the task decides to handle its cancellation in a slow way. This represents a behavior change and should probably not be back-patched to 3.6 and earlier. (cherry picked from commit e2b340a) Co-authored-by: Elvis Pranskevichus <elvis@magic.io>
When I was fixing bpo-32751 back in pythonGH-7216 I missed the case when *timeout* is zero or negative. This takes care of that. Props to @aaliddell for noticing the inconsistency.
… 0 (#21895) When I was fixing bpo-32751 back in GH-7216 I missed the case when *timeout* is zero or negative. This takes care of that. Props to @aaliddell for noticing the inconsistency.
… 0 (pythonGH-21895) When I was fixing bpo-32751 back in pythonGH-7216 I missed the case when *timeout* is zero or negative. This takes care of that. Props to @aaliddell for noticing the inconsistency. (cherry picked from commit c517fc7) Co-authored-by: Elvis Pranskevichus <elvis@magic.io>
…out <= 0 (pythonGH-21895) When I was fixing bpo-32751 back in pythonGH-7216 I missed the case when *timeout* is zero or negative. This takes care of that. Props to @aaliddell for noticing the inconsistency.. (cherry picked from commit c517fc7) Co-authored-by: Elvis Pranskevichus <elvis@magic.io>
…out <= 0 (pythonGH-21895) When I was fixing bpo-32751 back in pythonGH-7216 I missed the case when *timeout* is zero or negative. This takes care of that. Props to @aaliddell for noticing the inconsistency.. (cherry picked from commit c517fc7)
… 0 (python#21895) When I was fixing bpo-32751 back in pythonGH-7216 I missed the case when *timeout* is zero or negative. This takes care of that. Props to @aaliddell for noticing the inconsistency.
Currently,
asyncio.wait_for(fut)
, upon reaching the timeout deadline,cancels the future and returns immediately. This is problematic when
fut is a Task, because it will be left running for an arbitrary
amount of time. This behavior is iself surprising and may lead to
related bugs such as the one described in bpo-33638:
Currently, instead of raising a TimeoutError, the above code will fail
with
RuntimeError: cannot wait on un-acquired lock
, because__aexit__
is reached beforecondition.wait()
finishes itscancellation and re-acquires the condition lock.
To resolve this, make
wait_for
await for the task cancellation.The tradeoff here is that the
timeout
promise may be broken if thetask decides to handle its cancellation in a slow way. This represents
a behavior change and should probably not be back-patched to 3.6 and
earlier.
https://bugs.python.org/issue32751