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

gh-116720: Fix corner cases of taskgroups #117407

Merged
merged 21 commits into from
Apr 9, 2024
Merged

Conversation

gvanrossum
Copy link
Member

@gvanrossum gvanrossum commented Mar 31, 2024

  • Make test_timeouts pass (test_timeout_taskgroup)
  • Add tests derives from examples in the issue
  • Add test for new uncancel() behavior (reset _must_cancel)
  • Add news entry
  • Add What's New entry
  • Update docs for uncancel()
  • Update docs for task groups
  • Add Co-Authored-By: @arthur-tacca

@gvanrossum
Copy link
Member Author

@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)?

@gvanrossum
Copy link
Member Author

@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.

@gvanrossum
Copy link
Member Author

@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.

@agronholm
Copy link
Contributor

@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.

Sure, I can take a look.

@agronholm
Copy link
Contributor

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 CancelScope._deliver_cancellation() where it will skip explicitly cancelling a task if this flag is already set.

@gvanrossum
Copy link
Member Author

Sorry, which flag is that? ‘cancelling()’ or ‘_must_cancel’?

@agronholm
Copy link
Contributor

Sorry, which flag is that? ‘cancelling()’ or ‘_must_cancel’?

_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
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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. :-)

@gvanrossum
Copy link
Member Author

Still to do are docs updates for uncancel and for task groups. Also, I'd love help with shortening the news blurb.

@gvanrossum gvanrossum marked this pull request as ready for review April 5, 2024 22:09
@gvanrossum
Copy link
Member Author

@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:

  • Changelog (Misc/NEWS)
  • What's new in 3.13
  • asyncio-task.rst: uncancel docs and task group docs

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 Co-Authored-By: Arthur Tacca to the final commit message, since I cribbed most of the unit tests from your original demonstrations. I will also add (Inspired by an issue reported by Arthur Tacca in :gh:`116720`.) to the "what's new" section.

@arthur-tacca
Copy link

@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 if self._parent_cancel_requested check. The check for self._parent_task.cancelling() also looks good. As for the change to Task (uncancel() will rest _must_cancel if cancelling reaches 0), I can't immediately think of a situation it would naturally arise when using task groups (as opposed to manually triggering it with an explicit uncancel() call) and I don't have time to try and figure out one, but it looks like a sensible thing to do if that situation does arise; perhaps it would come up with asyncio.Timeout objects, which I haven't experimented with at all.

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:

  • "... For example, when two task groups are nested and both experience an exception in a child task simultaneously, it was possible that the outer task group would hang, because ..." I'm not sure "would hang" is accurate. It would process the rest of the logic after the inner task group.
  • "This allows a try / except* surrounding the task group to handle the exceptions in the ExceptionGroup without losing the cancellation." This is a bit specific to the particular example I gave. For a start, it also applies to try / finally. It also could stop a except* or finally block running in full (if it has await ... in it), which is sort of the opposite of what the text says. Perhaps better to say something deliberately a bit less specific, e.g. more along the lines of "This ensures that a CancelledError will be raised at the next await, so the cancellation is not lost".

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.

@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# If the parent task is being cancelled from the outside,
# If the parent task is being cancelled from the outside of the taskgroup,

Copy link
Contributor

@willingc willingc left a 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!

@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# re-cancel it, while keeping the cancel count stable.
# un-cancel and re-cancel the parent task, which will keep the cancel count stable.

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

@willingc willingc Apr 7, 2024

Choose a reason for hiding this comment

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

Suggested change
if a previous :meth:`cancel` call had arranged for a
the method checks if a previous :meth:`cancel` call had arranged for

@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
:exc:`CancelledError` to be thrown into the task,
:exc:`CancelledError` to be thrown into the task.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

@gvanrossum gvanrossum requested a review from willingc April 9, 2024 00:17
Copy link
Contributor

@willingc willingc left a 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 gvanrossum merged commit fa58e75 into python:main Apr 9, 2024
36 checks passed
@gvanrossum gvanrossum deleted the fix-tg branch April 9, 2024 15:17
@Tinche
Copy link
Contributor

Tinche commented Apr 16, 2024

@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?

@gvanrossum
Copy link
Member Author

@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

Copyright © 2001-2024 Python Software Foundation; All Rights Reserved

and add a copy of the PSF license for 3.13 somewhere.

diegorusso pushed a commit to diegorusso/cpython that referenced this pull request Apr 17, 2024
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
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.

5 participants