-
-
Notifications
You must be signed in to change notification settings - Fork 30.8k
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
gh-116720: Fix corner cases of taskgroups #117407
Conversation
e97f22a
to
5912d70
Compare
@arthur-tacca, what do you think of this PR? Does it pass all your tests? Do you want to contribute in any way (e.g. by converting your example programs into unittests)? |
@agronholm I wonder if you could review this PR? (It's in draft mode because there are no tests or doc yet, but the changes to taskgroups.py and tasks.py are as I plan to keep them.) It might affect anyio because of two things: (a) Better support for nested taskgroups; (b) change to uncancel() to reset the internal "must cancel" flag when the pending cancellation count reaches zero. |
@Tinche Same question for you -- you might have an opinion on the changes that I'm proposing to make to asyncio task and task group behavior here. |
Sure, I can take a look. |
It's hard to definitively say if this will affect AnyIO, but I did run the task group tests against this PR and they passed. The only place where AnyIO looks at this flag is in |
Sorry, which flag is that? ‘cancelling()’ or ‘_must_cancel’? |
|
@@ -255,6 +255,8 @@ def uncancel(self): | |||
""" | |||
if self._num_cancels_requested > 0: | |||
self._num_cancels_requested -= 1 | |||
if self._num_cancels_requested == 0: | |||
self._must_cancel = False |
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.
Shouldn't this also clear the cancel message?
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 doubt it -- self._cancel_message
is never cleared, and it's never used unless self._must_cancel
is set.
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.
Okay – I was just confused by that Py_CLEAR()
in the C version, but this explains 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.
Oh, you're right, that CLEAR is unnecessary there too. :-)
This fixes the one failing test in test_timeouts. Surprisingly, it doesn't break any other asyncio tests.
Co-Authored-By: @Tinche
Co-Authored-By: @arthur-tacca
Co-Authored-By: @arthur-tacca
Still to do are docs updates for uncancel and for task groups. Also, I'd love help with shortening the news blurb. |
@willingc Would you be my reviewer for this PR? I feel particularly challenged by the various documentation updates -- this is a pretty subtle improvement (given how long it's taken before someone reported the issue :-) and I'm struggling describing the changes and the end state appropriately in various places:
I'd say that currently the Changelog news item is probably too long, and there's too much duplication between all three forms of documentation. @arthur-tacca: I haven't heard from you on the issue in a while. Your feedback on any part of this PR would be much appreciated. I plan to give you credit by adding |
@gvanrossum Sorry for ghosting you. I had some limited time for open source between jobs (which I actually planned to spend mainly on something else), which has run out and it's now hard for me to post even short comments like this one. Super-brief review: Your code changes look good to me. As I said in an earlier comment, I think it's good that you've just moved (rather than duplicated) the Credit: Thanks for crediting me, that's much appreciated. I don't think my contributions are significant enough to need a CLA but in any case I have signed one (for a small change to typing extensions). Changelog:
Docs changes: "Task groups are careful not to drop outside cancellations when they collide with a cancellation internal to the task group." I must admit, I would struggle to understand what this means if I hadn't been involved in this issue. But that isn't very helpful since I don't have a better suggestion to give. |
Lib/asyncio/taskgroups.py
Outdated
@@ -139,6 +140,11 @@ async def __aexit__(self, et, exc, tb): | |||
self._errors.append(exc) | |||
|
|||
if self._errors: | |||
# If the parent task is being cancelled from the outside, |
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.
# If the parent task is being cancelled from the outside, | |
# If the parent task is being cancelled from the outside of the taskgroup, |
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.
@gvanrossum Overall, this looks good. I made a few suggestions on wording, but the existing text is also fine. Thanks!
Lib/asyncio/taskgroups.py
Outdated
@@ -139,6 +140,11 @@ async def __aexit__(self, et, exc, tb): | |||
self._errors.append(exc) | |||
|
|||
if self._errors: | |||
# If the parent task is being cancelled from the outside, | |||
# re-cancel it, while keeping the cancel count stable. |
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.
# re-cancel it, while keeping the cancel count stable. | |
# un-cancel and re-cancel the parent task, which will keep the cancel count stable. |
Doc/library/asyncio-task.rst
Outdated
@@ -1369,6 +1391,15 @@ Task Object | |||
catching :exc:`CancelledError`, it needs to call this method to remove | |||
the cancellation state. | |||
|
|||
When this method decrements the cancellation count to zero, | |||
if a previous :meth:`cancel` call had arranged for a |
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.
if a previous :meth:`cancel` call had arranged for a | |
the method checks if a previous :meth:`cancel` call had arranged for |
Doc/library/asyncio-task.rst
Outdated
@@ -1369,6 +1391,15 @@ Task Object | |||
catching :exc:`CancelledError`, it needs to call this method to remove | |||
the cancellation state. | |||
|
|||
When this method decrements the cancellation count to zero, | |||
if a previous :meth:`cancel` call had arranged for a | |||
:exc:`CancelledError` to be thrown into the task, |
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.
:exc:`CancelledError` to be thrown into the task, | |
:exc:`CancelledError` to be thrown into the task. |
Doc/library/asyncio-task.rst
Outdated
When this method decrements the cancellation count to zero, | ||
if a previous :meth:`cancel` call had arranged for a | ||
:exc:`CancelledError` to be thrown into the task, | ||
but this hadn't been done yet, that arrangement will be |
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.
but this hadn't been done yet, that arrangement will be | |
If this hadn't been done yet, that arrangement will be |
(Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.) | ||
|
||
* Improved behavior of :class:`asyncio.TaskGroup` when an outside cancellation | ||
collides with an internal cancellation. For example, when two task groups |
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!
Also switch to consistently using internal/external instead of inside/outside.
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.
Looks good. Thanks everyone.
@gvanrossum would you mind if I copied your solution into quattro to serve as a backport of sorts, for 3.11 and 3.12? Quattro is Apache 2 licensed, that's compatible right? |
That should be absolutely fine -- if I understand the PSF license correctly, all you need to do is add this text somewhere
and add a copy of the PSF license for 3.13 somewhere. |
This prevents external cancellations of a task group's parent task to be dropped when an internal cancellation happens at the same time. Also strengthen the semantics of uncancel() to clear self._must_cancel when the cancellation count reaches zero. Co-Authored-By: Tin Tvrtković <tinchester@gmail.com> Co-Authored-By: Arthur Tacca
uncancel()
behavior (reset_must_cancel
)uncancel()