You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I've spotted a little bug in how asyncio.CancelledError() exception is propagated inside an asyncio.Task.
Since python 3.9 the asyncio.Task.cancel() method has a new 'msg' parameter, that will create an asyncio.CancelledError(msg) exception incorporating that message.
The exception is successfully propagated to the coroutine the asyncio.Task is running, so the coroutine successfully gets raised an asyncio.CancelledError(msg) with the specified message in asyncio.Task.cancel(msg) method.
But, once the asyncio.Task is cancelled, is impossible to retrieve that original asyncio.CancelledError(msg) exception with the message, because it seems that *a new* asyncio.CancelledError() [without the message] is raised when asyncio.Task.result() or asyncio.Task.exception() methods are called.
I have the feeling that this is just wrong, and that the original message specified in asyncio.Task.cancel(msg) should be propagated even also asyncio.Task.result() is called.
I'm including a little snippet of code that clearly shows this bug.
I'm using python 3.9.6, in particular:
Python 3.9.6 (default, Aug 21 2021, 09:02:49)
[GCC 10.2.1 20210110] on linux
But, once the asyncio.Task is cancelled, is impossible to retrieve that original asyncio.CancelledError(msg) exception with the message, because it seems that *a new* asyncio.CancelledError() [without the message] is raised when asyncio.Task.result() or asyncio.Task.exception() methods are called.
You say it's "impossible", but isn't the message accessible via the exception chain (and visible in the traceback)? One benefit of not duplicating the message on the internal call to cancel() is that it makes it easier to pinpoint which CancelledError object is associated with the user's call to cancel(), and which is associated with the call done by asyncio internals, which is a different cancellation. Another benefit is that it prevents info from being duplicated in the traceback.
OK, I see your point.
But I still can't understand one thing and I think it's very confusing:
if you see my example, inside the job() coroutine, I get correctly
cancelled with an `asyncio.CancelledError` exception containing my message.
Now: if I re-raise the asyncio.CancelledError as-is, I lose the message,
if I call the `asyncio.Task.exception()` function.
If I raise a *new* asyncio.CancelledError with a new message, inside the
job() coroutine's `except asyncio.CancelledError:` block, I still lose the
message if I call `asyncio.Task.exception()`.
But if I raise another exception, say `raise ValueError("TEST")`, always
from the `except asyncio.CancelledError:` block of the job() coroutine, I
*get* the message!
I get `ValueError("TEST")` by calling `asyncio.Task.exception()`, while I
don't with the `asyncio.CancelledError()` one.
Is this really wanted? Sorry, but I still find this a lot confusing.
Shouldn't it be better to return from the asyncio.Task.exception() the
old one (containing the message) ?
Or, otherwise, create a new instance of the exception for ALL the
exception classes?
Now: if I re-raise the asyncio.CancelledError as-is, I lose the message,
if I call the asyncio.Task.exception() function.
Re-raise asyncio.CancelledError where? (And what do you mean by "re-raise"?) Call asyncio.Task.exception() where? This isn't part of your example, so it's not clear what you mean exactly.
pagliariccim commented on Oct 9, 2021
pagliariccimmannequin
MannequinAuthor
More actions
Chris,
I'm attaching to this e-mail the code I'm referring to.
As you can see, in line 10, I re-raise the asyncio.CancelledError exception
with a message "TEST".
That message is lost, due to the reasons we've talked about.
My point is that, if we substitute that line 10, with the commented line
11, and we comment the line 10, so we raise a ValueError("TEST") exception,
as you can see, the message "TEST" is NOT LOST.
I just find this counter-intuitive, and error-prone.
AT LEAST should be very well specified in the docs.
Now: if I re-raise the asyncio.CancelledError as-is, I lose the
message,
if I call the asyncio.Task.exception() function.
Re-raise asyncio.CancelledError where? (And what do you mean by
"re-raise"?) Call asyncio.Task.exception() where? This isn't part of your
example, so it's not clear what you mean exactly.
I still don't see you calling asyncio.Task.exception() in your new attachment...
pagliariccim commented on Oct 9, 2021
pagliariccimmannequin
MannequinAuthor
More actions
Chris,
ok, I have modified the snippet of code to better show what I mean.
Still here, the message of the CancelledError exception is lost, but if I
comment line 10, and uncomment line 11, so I throw a ValueError("TEST"),
that "TEST" string will be printed, so the message is not lost.
Again, I just find this behavior very counter-intuitive, and should be VERY
WELL documented in the docs.
I have a pull request for the issue.
It doesn't use Future.set_exception() but creates a new CancelledError() with propagated message.
The result is the same, except raised exceptions are not comparable by is check.
As a benefit, _cancelled_exc works with the patch, exc.context is correctly set.
The patch is not backported because it changes existing behavior a little. I'd like to avoid a situation when third-party code works with Python 3.11+, 3.10.3+, and 3.9.11+ only.
pagliariccim commented on Feb 17, 2022
pagliariccimmannequin
MannequinAuthor
More actions
Andrew,
many thanks for your time, solving this issue.
I think your solution is the best to fix this little problem and I agree
with you on backporting.
I have a pull request for the issue.
It doesn't use Future.set_exception() but creates a new CancelledError()
with propagated message.
The result is the same, except raised exceptions are not comparable by is check.
As a benefit, _cancelled_exc works with the patch, exc.context is
correctly set.
The patch is not backported because it changes existing behavior a little.
I'd like to avoid a situation when third-party code works with Python
3.11+, 3.10.3+, and 3.9.11+ only.
Andrew, the approach I described would I feel be much better. It would result in more concise, less verbose tracebacks, as opposed to more verbose -- not just because the message won't be repeated, but also because it eliminates the unneeded creation of intermediate exceptions. It would also cause is checks to work, which is what we want since behaviorally it should be the same exception.
New changeset 4140bcb by Andrew Svetlov in branch 'main': bpo-45390: Propagate CancelledError's message from cancelled task to its awaiter (GH-31383) 4140bcb
For future reference, with Andrew's change merged above, the traceback for the example snippet in my message above: https://bugs.python.org/issue45390#msg403570
is now the following. Observe that (1) the call to sleep() continues to be present, but (2) without introducing two new intermediate CancelledErrors, which increase the verbosity of the traceback:
Traceback (most recent call last):
File "/home/andrew/projects/cpython/exc_traceback.py", line 14, in <module>
asyncio.run(main())
^^^^^^^^^^^^^^^^^^^
File "/home/andrew/projects/cpython/Lib/asyncio/runners.py", line 44, in runreturn loop.run_until_complete(main)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/andrew/projects/cpython/Lib/asyncio/base_events.py", line 640, in run_until_completereturn future.result()
^^^^^^^^^^^^^^^
File "/home/andrew/projects/cpython/exc_traceback.py", line 11, in mainawait task
^^^^^^^^^^
File "/home/andrew/projects/cpython/exc_traceback.py", line 5, in jobawait asyncio.sleep(5)
^^^^^^^^^^^^^^^^^^^^^^
File "/home/andrew/projects/cpython/Lib/asyncio/tasks.py", line 619, in sleepreturnawait future
^^^^^^^^^^^^asyncio.exceptions.CancelledError: cancel job
(This is copied from Andrew's comment in the PR here: #31383 (comment) )
Serhiy, can you provide a sample snippet for your case with output, like I did in my message linked above?
Serhiy is right, Condition.wait() has the following code:
finally:
# Must reacquire lock even if wait is cancelled
cancelled = False
while True:
try:
await self.acquire()
break
except exceptions.CancelledError:
cancelled = True
ifcancelled:
raiseexceptions.CancelledError
It swallows CancelledError exceptions from waiters and re-raises CancelledError without the cancellation message.
Also Future.result() and Future.exception() can raise a CancelledError. So a CancelledError raised in a task may not contain a message passed to Task.cancel().
You need to catch a CancelledError raised in a coroutine and re-raise a new CancelledError with the specified cancel message if the task was cancelled.
graingert commented on Feb 23, 2022
graingertmannequin
Mannequin
More actions
there could be multiple messages here
perhaps it could be:
finally:
# Must reacquire lock even if wait is cancelled
cancelled = []
while True:
try:
await self.acquire()
break
except exceptions.CancelledError as e:
cancelled.append(e)
if len(cancelled) > 1:
raise ExceptionGroup("Cancelled", cancelled)
if cancelled:
raise cancelled[0]
Activity
pagliariccim commentedon Oct 6, 2021
I've spotted a little bug in how asyncio.CancelledError() exception is propagated inside an asyncio.Task.
Since python 3.9 the asyncio.Task.cancel() method has a new 'msg' parameter, that will create an asyncio.CancelledError(msg) exception incorporating that message.
The exception is successfully propagated to the coroutine the asyncio.Task is running, so the coroutine successfully gets raised an asyncio.CancelledError(msg) with the specified message in asyncio.Task.cancel(msg) method.
But, once the asyncio.Task is cancelled, is impossible to retrieve that original asyncio.CancelledError(msg) exception with the message, because it seems that *a new* asyncio.CancelledError() [without the message] is raised when asyncio.Task.result() or asyncio.Task.exception() methods are called.
I have the feeling that this is just wrong, and that the original message specified in asyncio.Task.cancel(msg) should be propagated even also asyncio.Task.result() is called.
I'm including a little snippet of code that clearly shows this bug.
I'm using python 3.9.6, in particular:
Python 3.9.6 (default, Aug 21 2021, 09:02:49)
[GCC 10.2.1 20210110] on linux
bensimner commentedon Oct 6, 2021
This seems to be present in both the Python implementation as well as the accelerated C _asyncio module.
It looks like that when a Task awaits a cancelled future,
the task itself is cancelled but the cancellation message is not propagated to the task.
https://github.com/python/cpython/blob/main/Lib/asyncio/tasks.py#L242
cjerdonek commentedon Oct 7, 2021
You say it's "impossible", but isn't the message accessible via the exception chain (and visible in the traceback)? One benefit of not duplicating the message on the internal call to cancel() is that it makes it easier to pinpoint which CancelledError object is associated with the user's call to cancel(), and which is associated with the call done by asyncio internals, which is a different cancellation. Another benefit is that it prevents info from being duplicated in the traceback.
graingert commentedon Oct 7, 2021
afaik this is intentional https://bugs.python.org/issue31033
pagliariccim commentedon Oct 9, 2021
OK, I see your point.
But I still can't understand one thing and I think it's very confusing:
cancelled with an `asyncio.CancelledError` exception containing my message.
if I call the `asyncio.Task.exception()` function.
job() coroutine's `except asyncio.CancelledError:` block, I still lose the
message if I call `asyncio.Task.exception()`.
from the `except asyncio.CancelledError:` block of the job() coroutine, I
*get* the message!
I get `ValueError("TEST")` by calling `asyncio.Task.exception()`, while I
don't with the `asyncio.CancelledError()` one.
Is this really wanted? Sorry, but I still find this a lot confusing.
Shouldn't it be better to return from the
asyncio.Task.exception()theold one (containing the message) ?
Or, otherwise, create a new instance of the exception for ALL the
exception classes?
Thank you for your time,
My Best Regards,
M.
On Thu, Oct 7, 2021 at 10:25 AM Thomas Grainger <report@bugs.python.org>
wrote:
cjerdonek commentedon Oct 9, 2021
Re-raise asyncio.CancelledError where? (And what do you mean by "re-raise"?) Call asyncio.Task.exception() where? This isn't part of your example, so it's not clear what you mean exactly.
pagliariccim commentedon Oct 9, 2021
Chris,
I'm attaching to this e-mail the code I'm referring to.
As you can see, in line 10, I re-raise the asyncio.CancelledError exception
with a message "TEST".
That message is lost, due to the reasons we've talked about.
My point is that, if we substitute that line 10, with the commented line
11, and we comment the line 10, so we raise a ValueError("TEST") exception,
as you can see, the message "TEST" is NOT LOST.
I just find this counter-intuitive, and error-prone.
AT LEAST should be very well specified in the docs.
Regards,
M.
On Sat, Oct 9, 2021 at 2:51 PM Chris Jerdonek <report@bugs.python.org>
wrote:
cjerdonek commentedon Oct 9, 2021
I still don't see you calling asyncio.Task.exception() in your new attachment...
pagliariccim commentedon Oct 9, 2021
Chris,
ok, I have modified the snippet of code to better show what I mean.
Still here, the message of the CancelledError exception is lost, but if I
comment line 10, and uncomment line 11, so I throw a ValueError("TEST"),
that "TEST" string will be printed, so the message is not lost.
Again, I just find this behavior very counter-intuitive, and should be VERY
WELL documented in the docs.
Thanks,
M.
On Sat, Oct 9, 2021 at 3:06 PM Chris Jerdonek <report@bugs.python.org>
wrote:
4 remaining items
asvetlov commentedon Feb 17, 2022
I have a pull request for the issue.
It doesn't use
Future.set_exception()but creates a new CancelledError() with propagated message.The result is the same, except raised exceptions are not comparable by
ischeck.As a benefit,
_cancelled_excworks with the patch, exc.context is correctly set.The patch is not backported because it changes existing behavior a little. I'd like to avoid a situation when third-party code works with Python 3.11+, 3.10.3+, and 3.9.11+ only.
pagliariccim commentedon Feb 17, 2022
Andrew,
many thanks for your time, solving this issue.
I think your solution is the best to fix this little problem and I agree
with you on backporting.
My Best Regards,
and thanks again.
Marco
On Thu, Feb 17, 2022 at 10:29 AM Andrew Svetlov <report@bugs.python.org>
wrote:
cjerdonek commentedon Feb 17, 2022
Andrew, the approach I described would I feel be much better. It would result in more concise, less verbose tracebacks, as opposed to more verbose -- not just because the message won't be repeated, but also because it eliminates the unneeded creation of intermediate exceptions. It would also cause is checks to work, which is what we want since behaviorally it should be the same exception.
asvetlov commentedon Feb 21, 2022
New changeset 4140bcb by Andrew Svetlov in branch 'main':
bpo-45390: Propagate CancelledError's message from cancelled task to its awaiter (GH-31383)
4140bcb
serhiy-storchaka commentedon Feb 23, 2022
Seems a CancelledError message can be lost also in Condition.wait().
cjerdonek commentedon Feb 23, 2022
For future reference, with Andrew's change merged above, the traceback for the example snippet in my message above:
https://bugs.python.org/issue45390#msg403570
is now the following. Observe that (1) the call to sleep() continues to be present, but (2) without introducing two new intermediate CancelledErrors, which increase the verbosity of the traceback:
(This is copied from Andrew's comment in the PR here:
#31383 (comment) )
Serhiy, can you provide a sample snippet for your case with output, like I did in my message linked above?
asvetlov commentedon Feb 23, 2022
Serhiy is right, Condition.wait() has the following code:
It swallows CancelledError exceptions from waiters and re-raises CancelledError without the cancellation message.
serhiy-storchaka commentedon Feb 23, 2022
Also Future.result() and Future.exception() can raise a CancelledError. So a CancelledError raised in a task may not contain a message passed to Task.cancel().
You need to catch a CancelledError raised in a coroutine and re-raise a new CancelledError with the specified cancel message if the task was cancelled.
graingert commentedon Feb 23, 2022
there could be multiple messages here
perhaps it could be:
gvanrossum commentedon Feb 23, 2022
We should really stop appending to a closed issue.
Anyway, raising ExceptionGroup is backwards incompatible, since "except CancelledError" wouldn't cancel the group.
Add workaround for <python/cpython#89553>.