-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
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
Conversation
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 usernameWe couldn't find a bugs.python.org (b.p.o) account corresponding to the following GitHub usernames: 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! |
Lib/asyncio/tasks.py
Outdated
if not fut.cancelled() and fut.exception() is not None: | ||
raise fut.exception() |
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.
Could it be
if not fut.cancelled():
return fut.result()
?
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.
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.
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.
OK, thanks, I find this even more appropriate way to handle such situations.
# In case task cancellation failed with some | ||
# exception, we should re-raise it |
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.
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`
I signed CLA yesterday, but for some reason checker returns 500 for my username. Is it OK and I should wait some more time? |
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.
A couple higher-level comments before a more detailed review:
-
In the
TimeoutError
case, it would be good to check that theCancelledError
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. -
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.
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 |
Seems to work fine when I entered |
try: | ||
return fut.result() | ||
except exceptions.CancelledError as exc: | ||
raise exceptions.TimeoutError() from exc |
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 put also an else: raise TimeoutError
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.
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.
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.
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.
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.
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()
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 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.
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.
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.
I entered "@romasku" (with '@' symbol) as my github username in b.p.o. account, and was checking against it. Now everything works, thank you! |
Thank you for your review! |
I have made the requested changes; please review again |
Thanks for making the requested changes! @cjerdonek: please review the changes made to this pull request. |
try: | ||
return fut.result() | ||
except exceptions.CancelledError as exc: | ||
raise exceptions.TimeoutError() from exc |
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.
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.
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 |
This looks great now. @cjerdonek please review again. |
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.
There are a couple more tiny things I noticed, but otherwise looks great. Thanks a lot for your good work on this, @romasku!
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 |
Thanks for making the requested changes! @cjerdonek, @1st1: please review the changes made to this pull request. |
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.
Thanks, @romasku.
@romasku, I forgot there are a couple more administrative things. Can you add your name to |
…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.
@cjerdonek, done. I am happy to contribute! |
Thanks so much, Roman! |
…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]>
https://bugs.python.org/issue40607