-
-
Notifications
You must be signed in to change notification settings - Fork 31.1k
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
Changes from 16 commits
90695c5
6c4876d
df6e8b7
c762e36
4989042
3f91f2d
55ff73f
50fd8d6
93ba76a
f1f89c0
48c6dda
1e20728
bc1522c
7f6e5ff
56251ff
c54fb22
6d4b3d7
b086609
8ac42f6
ac87a8e
be9365c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -392,6 +392,28 @@ is also included in the exception group. | |||||
The same special case is made for | ||||||
:exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph. | ||||||
|
||||||
Task groups are careful not to drop outside cancellations | ||||||
when they collide with a cancellation internal to the task group. | ||||||
In particular, when one task group is syntactically nested in another, | ||||||
and both experience an exception in one of their child tasks simultaneously, | ||||||
the inner task group will process its exceptions, and then the outer task group | ||||||
will experience another cancellation and process its own exceptions. | ||||||
|
||||||
In the case where a task group is cancelled from the outside and also must | ||||||
raise an :exc:`ExceptionGroup`, it will call the parent task's | ||||||
:meth:`~asyncio.Task.cancel` method. This allows a :keyword:`try` / | ||||||
:keyword:`except* <except_star>` surrounding the task group to handle | ||||||
the exceptions in the ``ExceptionGroup`` without losing the cancellation. | ||||||
The :exc:`asyncio.CancelledError` will be raised at the next | ||||||
:keyword:`await` (which may be implied at the end of a surrounding | ||||||
:keyword:`async with` block). | ||||||
|
||||||
Task groups now preserve the cancellation count | ||||||
(as reported by :meth:`asyncio.Task.cancelling`). | ||||||
|
||||||
.. versionchanged:: 3.13 | ||||||
|
||||||
Improved handling of simultaneous inside and outside cancellation. | ||||||
|
||||||
Sleeping | ||||||
======== | ||||||
|
@@ -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 commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
rescinded (by resetting the internal ``_must_cancel`` flag). | ||||||
|
||||||
.. versionchanged:: 3.13 | ||||||
Changed to rescind pending cancellation requests upon reaching zero. | ||||||
|
||||||
.. method:: cancelling() | ||||||
|
||||||
Return the number of pending cancellation requests to this Task, i.e., | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -192,13 +192,6 @@ Other Language Changes | |
|
||
(Contributed by Sebastian Pipping in :gh:`115623`.) | ||
|
||
* When :func:`asyncio.TaskGroup.create_task` is called on an inactive | ||
:class:`asyncio.TaskGroup`, the given coroutine will be closed (which | ||
prevents a :exc:`RuntimeWarning` about the given coroutine being | ||
never awaited). | ||
|
||
(Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.) | ||
|
||
* The :func:`ssl.create_default_context` API now includes | ||
:data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT` | ||
in its default flags. | ||
|
@@ -296,6 +289,34 @@ asyncio | |
with the tasks being completed. | ||
(Contributed by Justin Arthur in :gh:`77714`.) | ||
|
||
* When :func:`asyncio.TaskGroup.create_task` is called on an inactive | ||
:class:`asyncio.TaskGroup`, the given coroutine will be closed (which | ||
prevents a :exc:`RuntimeWarning` about the given coroutine being | ||
never awaited). | ||
(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 commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
are nested and both experience an exception in a child task simultaneously, | ||
it was possible that the outer task group would hang, because its internal | ||
cancellation was swallowed by the inner task group. | ||
|
||
In the case where a task group is cancelled from the outside and also must | ||
raise an :exc:`ExceptionGroup`, it will now call the parent task's | ||
:meth:`~asyncio.Task.cancel` method. This allows a :keyword:`try` / | ||
:keyword:`except* <except_star>` surrounding the task group to handle | ||
the exceptions in the ``ExceptionGroup`` without losing the cancellation. | ||
The :exc:`asyncio.CancelledError` will be raised at the next | ||
:keyword:`await` (which may be implied at the end of a surrounding | ||
:keyword:`async with` block). | ||
|
||
An added benefit of these changes is that task groups now preserve the | ||
cancellation count (:meth:`asyncio.Task.cancelling`). | ||
|
||
In order to handle some corner cases, :meth:`asyncio.Task.uncancel` may now | ||
reset the undocumented ``_must_cancel`` flag when the cancellation count | ||
reaches zero. | ||
|
||
base64 | ||
------ | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -77,12 +77,6 @@ async def __aexit__(self, et, exc, tb): | |||||
propagate_cancellation_error = exc | ||||||
else: | ||||||
propagate_cancellation_error = None | ||||||
if self._parent_cancel_requested: | ||||||
# If this flag is set we *must* call uncancel(). | ||||||
if self._parent_task.uncancel() == 0: | ||||||
# If there are no pending cancellations left, | ||||||
# don't propagate CancelledError. | ||||||
propagate_cancellation_error = None | ||||||
|
||||||
if et is not None: | ||||||
if not self._aborting: | ||||||
|
@@ -130,6 +124,13 @@ async def __aexit__(self, et, exc, tb): | |||||
if self._base_error is not None: | ||||||
raise self._base_error | ||||||
|
||||||
if self._parent_cancel_requested: | ||||||
# If this flag is set we *must* call uncancel(). | ||||||
if self._parent_task.uncancel() == 0: | ||||||
# If there are no pending cancellations left, | ||||||
# don't propagate CancelledError. | ||||||
propagate_cancellation_error = None | ||||||
|
||||||
# Propagate CancelledError if there is one, except if there | ||||||
# are other errors -- those have priority. | ||||||
if propagate_cancellation_error is not None and not self._errors: | ||||||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# re-cancel it, while keeping the cancel count stable. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
if self._parent_task.cancelling(): | ||||||
self._parent_task.uncancel() | ||||||
self._parent_task.cancel() | ||||||
# Exceptions are heavy objects that can have object | ||||||
# cycles (bad for GC); let's not keep a reference to | ||||||
# a bunch of them. | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. I doubt it -- There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay – I was just confused by that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, you're right, that CLEAR is unnecessary there too. :-) |
||
return self._num_cancels_requested | ||
|
||
def __eager_start(self): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
Improved behavior of :class:`asyncio.TaskGroup` when an outside cancellation | ||
collides with an internal cancellation. 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 its internal | ||
cancellation was swallowed by the inner task group. | ||
|
||
In the case where a task group is cancelled from the outside and also must | ||
raise an :exc:`ExceptionGroup`, it will now call the parent task's | ||
:meth:`~asyncio.Task.cancel` method. This allows a :keyword:`try` / | ||
:keyword:`except* <except_star>` surrounding the task group to handle | ||
the exceptions in the ``ExceptionGroup`` without losing the cancellation. | ||
The :exc:`asyncio.CancelledError` will be raised at the next | ||
:keyword:`await` (which may be implied at the end of a surrounding | ||
:keyword:`async with` block). | ||
|
||
An added benefit of these changes is that task groups now preserve the | ||
cancellation count (:meth:`asyncio.Task.cancelling`). | ||
|
||
In order to handle some corner cases, :meth:`asyncio.Task.uncancel` may now | ||
reset the undocumented ``_must_cancel`` flag when the cancellation count | ||
reaches zero. |
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.