Skip to content

bpo-40607: Reraise exception during task cancelation in asyncio.wait_for() #20054

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

Merged
merged 1 commit into from
May 15, 2020

Conversation

romasku
Copy link
Contributor

@romasku romasku commented May 12, 2020

@the-knights-who-say-ni
Copy link

Hello, and thanks for your contribution!

I'm a bot set up to make sure that the project can legally accept this contribution by verifying everyone involved has signed the PSF contributor agreement (CLA).

Recognized GitHub username

We couldn't find a bugs.python.org (b.p.o) account corresponding to the following GitHub usernames:

@romasku

This might be simply due to a missing "GitHub Name" entry in one's b.p.o account settings. This is necessary for legal reasons before we can look at this contribution. Please follow the steps outlined in the CPython devguide to rectify this issue.

You can check yourself to see if the CLA has been received.

Thanks again for the contribution, we look forward to reviewing it!

Comment on lines 502 to 503
if not fut.cancelled() and fut.exception() is not None:
raise fut.exception()
Copy link

Choose a reason for hiding this comment

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

Could it be

if not fut.cancelled():
    return fut.result()

?

Copy link
Member

Choose a reason for hiding this comment

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

Building on this, how about--

try:
    return fut.result()
except CancelledError as exc:
    raise TimeoutError() from exc

What's nice about this is that you'll see the CancelledError that caused the TimeoutError in the traceback. This will be even better when this PR is merged: #19951
In the traceback, you'll get to see exactly what line of code was interrupted by the cancellation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, thanks, I find this even more appropriate way to handle such situations.

Comment on lines +499 to +500
# In case task cancellation failed with some
# exception, we should re-raise it
Copy link

Choose a reason for hiding this comment

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

What about the case when task cancellation failed with some result (not exception)? Is it OK to ignore it? I mean something like

async def inner():
    try:
        await asyncio.sleep(0.2)
    except asyncio.CancellationError:
        return 123  # instead of `raise FooException`

@romasku romasku force-pushed the fix-issue-40607 branch from f31c497 to 7d4af0e Compare May 13, 2020 12:36
@romasku
Copy link
Contributor Author

romasku commented May 13, 2020

I signed CLA yesterday, but for some reason checker returns 500 for my username. Is it OK and I should wait some more time?

@cjerdonek
Copy link
Member

@Mariatta, are you able to answer @romasku's question above about the checker returning 500? Thanks a lot if you're able to help.

Copy link
Member

@cjerdonek cjerdonek left a comment

Choose a reason for hiding this comment

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

A couple higher-level comments before a more detailed review:

  1. In the TimeoutError case, it would be good to check that the CancelledError is getting chained. You can see an example of how to check for that in a test here: bpo-29587: Enable exception chaining for gen.throw() with "yield from" #19858
    Maybe this can be added to another existing test. I'm not sure as I haven't looked.

  2. It looks like your tests take 0.1 seconds to run, which adds up a lot given the number of tests. It would be better to design them so they run without having to sleep for non-negligible amounts of time but still be tests that are (1) simple and (2) not flaky. This may take a little more creativity, or it might be easy. I'm not sure, but it should be doable.

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@Mariatta
Copy link
Member

Seems to work fine when I entered romasku in the CLA checker app.
It says that there is no b.p.o user associated with the has romasku GitHub user. Basically you need to add romasku to your b.p.o account.

try:
return fut.result()
except exceptions.CancelledError as exc:
raise exceptions.TimeoutError() from exc
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 put also an else: raise TimeoutError

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, but I think that else branch is unreachable: if there isn't any exception in try block, then return works normally and we leave this method. Please explain if I am wrong.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, but your new code is simply not equivalent to the previous code that raised TimeoutError unconditionally. Now I see that there might be a condition for clear return and I don't want that.

Copy link
Member

Choose a reason for hiding this comment

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

The linked issue talks about the correct exception propagation. Basically, wait_for timeouts with an exception and we want that original exception to be propagated. That's fine and I'm +1 for that.

The code here is slightly different though and a bit hard to trace so it would be easier to read if it was replaced with:

           try:
                fut.result()
            except exceptions.CancelledError as exc:
                raise exceptions.TimeoutError() from exc
            else:
                raise exceptions.TimeoutError()

Copy link
Member

Choose a reason for hiding this comment

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

I think the issue here is that there is a race condition where the task can complete after the timeout has elapsed. So the question is whether the caller should get to learn that the task completed in this case.

Copy link
Member

@1st1 1st1 May 14, 2020

Choose a reason for hiding this comment

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

Yes, the current implementation discards the result, as the probability of actually having it is low. I'd like to keep it that way. If we want to change the current behavior that should be done in a separate issue, with a new PR and new discussion.

@romasku
Copy link
Contributor Author

romasku commented May 13, 2020

I entered "@romasku" (with '@' symbol) as my github username in b.p.o. account, and was checking against it. Now everything works, thank you!

@romasku romasku force-pushed the fix-issue-40607 branch from 7d4af0e to c4bfb96 Compare May 13, 2020 20:06
@romasku
Copy link
Contributor Author

romasku commented May 13, 2020

Thank you for your review!
To overcome the second issue (about sleeping in tests) I've added PositiveZero class: it passes greater than zero check, so asyncio.wait_for main case logic runs, but it makes sleep time effectively zero.
To be honest, I am not sure about this solution: just using really small sleep time (for 0.000001 seconds) works the same way.
Also, I noticed that there are many other tests in test_tasks.py that sleep for 0.1-0.3 seconds - maybe it's better to make another PR to optimize those tests?

@romasku romasku force-pushed the fix-issue-40607 branch from c4bfb96 to a46c582 Compare May 13, 2020 21:55
@romasku
Copy link
Contributor Author

romasku commented May 13, 2020

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@cjerdonek: please review the changes made to this pull request.

@bedevere-bot bedevere-bot requested a review from cjerdonek May 13, 2020 21:56
try:
return fut.result()
except exceptions.CancelledError as exc:
raise exceptions.TimeoutError() from exc
Copy link
Member

Choose a reason for hiding this comment

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

Yes, but your new code is simply not equivalent to the previous code that raised TimeoutError unconditionally. Now I see that there might be a condition for clear return and I don't want that.

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@1st1
Copy link
Member

1st1 commented May 14, 2020

This looks great now. @cjerdonek please review again.

Copy link
Member

@cjerdonek cjerdonek left a comment

Choose a reason for hiding this comment

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

There are a couple more tiny things I noticed, but otherwise looks great. Thanks a lot for your good work on this, @romasku!

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@romasku
Copy link
Contributor Author

romasku commented May 15, 2020

I have made the requested changes; please review again

@bedevere-bot
Copy link

Thanks for making the requested changes!

@cjerdonek, @1st1: please review the changes made to this pull request.

@bedevere-bot bedevere-bot requested review from 1st1 and cjerdonek May 15, 2020 10:22
Copy link
Member

@cjerdonek cjerdonek left a comment

Choose a reason for hiding this comment

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

Thanks, @romasku.

@cjerdonek
Copy link
Member

cjerdonek commented May 15, 2020

@romasku, I forgot there are a couple more administrative things. Can you add your name to Misc/ACKS and also add "Patch by <your name>." at the end of your NEWS entry? More details can be found here: https://devguide.python.org/committing/#handling-others-code

…or()

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.
@romasku romasku force-pushed the fix-issue-40607 branch from 7393a28 to c2f4f3c Compare May 15, 2020 13:46
@romasku
Copy link
Contributor Author

romasku commented May 15, 2020

@cjerdonek, done. I am happy to contribute!

@1st1 1st1 merged commit 382a563 into python:master May 15, 2020
@1st1
Copy link
Member

1st1 commented May 15, 2020

Thanks so much, Roman!

arturoescaip pushed a commit to arturoescaip/cpython that referenced this pull request May 24, 2020
…for() (pythonGH-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]>
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.

7 participants