Skip to content
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

bpo-32751: Wait for task cancellation in asyncio.wait_for() #7216

Merged
merged 3 commits into from
May 29, 2018

Conversation

elprans
Copy link
Contributor

@elprans elprans commented May 29, 2018

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:

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.

https://bugs.python.org/issue32751

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.
# We cannot wait on *fut* directly to make
# sure _cancel_and_wait itself is reliably cancellable.
await waiter
except futures.CancelledError:
Copy link
Member

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?

Copy link
Contributor Author

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.

waiter = loop.create_future()
cb = functools.partial(_release_waiter, waiter)
fut.add_done_callback(cb)
fut.cancel()
Copy link
Member

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

Copy link
Contributor

@asvetlov asvetlov left a 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice trick!

@1st1
Copy link
Member

1st1 commented May 29, 2018

I suspect if a task has a slow cancellation routine -- many timeout related assumptions will be broken.

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 asyncio.Condition behaviour scared me.

@1st1 1st1 merged commit e2b340a into python:master May 29, 2018
@miss-islington
Copy link
Contributor

Thanks @elprans for the PR, and @1st1 for merging it 🌮🎉.. I'm working now to backport this PR to: 3.7.
🐍🍒⛏🤖 I'm not a witch! I'm not a witch!

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request May 29, 2018
…-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>
@bedevere-bot
Copy link

GH-7223 is a backport of this pull request to the 3.7 branch.

miss-islington added a commit that referenced this pull request May 29, 2018
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>
elprans added a commit to elprans/cpython that referenced this pull request Aug 15, 2020
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.
1st1 pushed a commit that referenced this pull request Aug 26, 2020
… 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.
miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Aug 26, 2020
… 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>
ambv pushed a commit that referenced this pull request Aug 26, 2020
… 0 (GH-21895) (GH-21963)

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.
(cherry picked from commit c517fc7)

Co-authored-by: Elvis Pranskevichus <elvis@magic.io>
elprans added a commit to elprans/cpython that referenced this pull request Aug 26, 2020
…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>
elprans added a commit to elprans/cpython that referenced this pull request Aug 26, 2020
…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)
1st1 pushed a commit that referenced this pull request Aug 26, 2020
…out <= 0 (GH-21895) (#21967)

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..
(cherry picked from commit c517fc7)
xzy3 pushed a commit to xzy3/cpython that referenced this pull request Oct 18, 2020
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants