From 90695c5c4760836089e86b8c4b3e429cc3892a31 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 31 Mar 2024 08:48:53 -0700 Subject: [PATCH 01/19] Fix taskgroups handling of parent cancellation --- Lib/asyncio/taskgroups.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index 57f01230159319..d0c7d9dc85dfdf 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -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: From 6c4876d4b14e704a66a59e25b650dd2d2e57cc03 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 31 Mar 2024 09:00:01 -0700 Subject: [PATCH 02/19] When errors and parent cancellation are both pending, re-cancel parent --- Lib/asyncio/taskgroups.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index d0c7d9dc85dfdf..128d688fcbf489 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -140,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. + 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. From df6e8b7bf5640d3fc701533bb1b4efee67327d4b Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 31 Mar 2024 13:39:07 -0700 Subject: [PATCH 03/19] When uncancel() reaches zero, clear _must_cancel This fixes the one failing test in test_timeouts. Surprisingly, it doesn't break any other asyncio tests. --- Lib/asyncio/tasks.py | 2 ++ Modules/_asynciomodule.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 7fb697b9441c33..60c3f91bda1fbd 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -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 # EXPERIMENTAL return self._num_cancels_requested def __eager_start(self): diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 29246cfa6afd00..59d422dbb72456 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2393,6 +2393,10 @@ _asyncio_Task_uncancel_impl(TaskObj *self) { if (self->task_num_cancels_requested > 0) { self->task_num_cancels_requested -= 1; + if (self->task_num_cancels_requested == 0) { + self->task_must_cancel = 0; + Py_CLEAR(self->task_cancel_msg); + } } return PyLong_FromLong(self->task_num_cancels_requested); } From c762e363b5ed57bd6a209855ddffbe0f5879e283 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 1 Apr 2024 08:17:25 -0700 Subject: [PATCH 04/19] Remove 'experimental' comment --- Lib/asyncio/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 60c3f91bda1fbd..dadcb5b5f36bd7 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -256,7 +256,7 @@ def uncancel(self): if self._num_cancels_requested > 0: self._num_cancels_requested -= 1 if self._num_cancels_requested == 0: - self._must_cancel = False # EXPERIMENTAL + self._must_cancel = False return self._num_cancels_requested def __eager_start(self): From 49890427f38fa6363ff01e0386f290232e45d880 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 3 Apr 2024 20:27:53 -0700 Subject: [PATCH 05/19] Remove unneeded Py_CLEAR(self->task_cancel_msg) --- Modules/_asynciomodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 59d422dbb72456..b886051186de9c 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2395,7 +2395,6 @@ _asyncio_Task_uncancel_impl(TaskObj *self) self->task_num_cancels_requested -= 1; if (self->task_num_cancels_requested == 0) { self->task_must_cancel = 0; - Py_CLEAR(self->task_cancel_msg); } } return PyLong_FromLong(self->task_num_cancels_requested); From 3f91f2d6c11f2866069d6561038230161ba33290 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 09:39:06 -0700 Subject: [PATCH 06/19] Simple test for preserving cancelling() level Co-Authored-By: @Tinche --- Lib/test/test_asyncio/test_taskgroups.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 1ec8116953f811..d04e862e0299f7 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -833,6 +833,18 @@ async def run_coro_after_tg_closes(): loop = asyncio.get_event_loop() loop.run_until_complete(run_coro_after_tg_closes()) + async def test_cancelling_level_preserved(self): + async def raise_after(t, e): + await asyncio.sleep(t) + raise e() + + try: + async with asyncio.TaskGroup() as tg: + tg.create_task(raise_after(0.0, RuntimeError)) + except* RuntimeError: + pass + self.assertEqual(asyncio.current_task().cancelling(), 0) + if __name__ == "__main__": unittest.main() From 55ff73f45a7f2d5abfb65cb7221bd2e7191ebd56 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 09:53:46 -0700 Subject: [PATCH 07/19] Add test for nested task groups Co-Authored-By: @arthur-tacca --- Lib/test/test_asyncio/test_taskgroups.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index d04e862e0299f7..28c6385660eb91 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -845,6 +845,28 @@ async def raise_after(t, e): pass self.assertEqual(asyncio.current_task().cancelling(), 0) + async def test_nested_groups_both_cancelled(self): + async def raise_after(t, e): + await asyncio.sleep(t) + raise e() + + try: + async with asyncio.TaskGroup() as outer_tg: + try: + async with asyncio.TaskGroup() as inner_tg: + inner_tg.create_task(raise_after(0, RuntimeError)) + outer_tg.create_task(raise_after(0, ValueError)) + except* RuntimeError: + pass + else: + self.fail("RuntimeError not raised") + self.assertEqual(asyncio.current_task().cancelling(), 1) + except* ValueError: + pass + else: + self.fail("ValueError not raised") + self.assertEqual(asyncio.current_task().cancelling(), 0) + if __name__ == "__main__": unittest.main() From 50fd8d6b35581c459a8ec4ab22979d4bae5eca9e Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 12:53:28 -0700 Subject: [PATCH 08/19] Add test that is fixed by the re-cancel in __aexit__ Co-Authored-By: @arthur-tacca --- Lib/test/test_asyncio/test_taskgroups.py | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 28c6385660eb91..4852536defc93d 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -867,6 +867,38 @@ async def raise_after(t, e): self.fail("ValueError not raised") self.assertEqual(asyncio.current_task().cancelling(), 0) + async def test_error_and_cancel(self): + event = asyncio.Event() + + async def raise_error(): + event.set() + await asyncio.sleep(0) + raise RuntimeError() + + async def inner(): + try: + async with taskgroups.TaskGroup() as tg: + tg.create_task(raise_error()) + await asyncio.sleep(1) + self.fail("Sleep in group should have been cancelled") + except* RuntimeError: + self.assertEqual(asyncio.current_task().cancelling(), 1) + self.assertEqual(asyncio.current_task().cancelling(), 1) + await asyncio.sleep(1) + self.fail("Sleep after group should have been cancelled") + + async def outer(): + t = asyncio.create_task(inner()) + await event.wait() + self.assertEqual(t.cancelling(), 0) + t.cancel() + self.assertEqual(t.cancelling(), 1) + with self.assertRaises(asyncio.CancelledError): + await t + self.assertTrue(t.cancelled()) + + await outer() + if __name__ == "__main__": unittest.main() From 93ba76a74ac4b5e770efc4e1225b9fdfa84552f2 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 14:57:05 -0700 Subject: [PATCH 09/19] Test that uncancel() sets _must_cancel to False --- Lib/test/test_asyncio/test_tasks.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index bc6d88e65a4966..5b09c81faef62a 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -684,6 +684,30 @@ def on_timeout(): finally: loop.close() + def test_uncancel_resets_must_cancel(self): + + async def coro(): + await fut + return 42 + + loop = asyncio.new_event_loop() + fut = asyncio.Future(loop=loop) + task = self.new_task(loop, coro()) + loop.run_until_complete(asyncio.sleep(0)) # Get task waiting for fut + fut.set_result(None) # Make task runnable + try: + task.cancel() # Enter cancelled state + self.assertEqual(task.cancelling(), 1) + self.assertTrue(task._must_cancel) + + task.uncancel() # Undo cancellation + self.assertEqual(task.cancelling(), 0) + self.assertFalse(task._must_cancel) + finally: + res = loop.run_until_complete(task) + self.assertEqual(res, 42) + loop.close() + def test_cancel(self): def gen(): From f1f89c0ce3fb7b1647b05aebaae3cef23787c856 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 15:48:13 -0700 Subject: [PATCH 10/19] Move a stray asyncio news item into the asyncio section --- Doc/whatsnew/3.13.rst | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 7f6a86efc61bf7..0cb30865369006 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -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,12 @@ 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`.) + base64 ------ From 48c6dda6cd78cd52768e1cd23bb2e8509645efda Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 15:49:05 -0700 Subject: [PATCH 11/19] Add blurb --- ...-04-04-15-28-12.gh-issue-116720.aGhXns.rst | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst diff --git a/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst b/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst new file mode 100644 index 00000000000000..128fcf8eb3cd72 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst @@ -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* ` 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. From 1e2072837c847516d3fbb1d2bf0b24ea3cad4a5c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Thu, 4 Apr 2024 15:49:31 -0700 Subject: [PATCH 12/19] Add what's new entry --- Doc/whatsnew/3.13.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 0cb30865369006..28c1e9d07706a3 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -295,6 +295,28 @@ asyncio 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 + 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* ` 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 ------ From bc1522c8df7f83b36a13110385ad1a4e5181025a Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 5 Apr 2024 15:03:44 -0700 Subject: [PATCH 13/19] Update uncancel docs --- Doc/library/asyncio-task.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 3b10a0d628a86e..0bc013d73bc3a3 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -1369,6 +1369,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, + but this hadn't been done yet, that arrangement will be + 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., From 7f6e5ffe8fe046aa22e3c668d96c35340d850166 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 5 Apr 2024 15:03:58 -0700 Subject: [PATCH 14/19] Update task group docs --- Doc/library/asyncio-task.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 0bc013d73bc3a3..9ac59c3ea58b6c 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -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* ` 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`). + +.. versionchaged:: 3.13 + + Improved handling of simultaneous inside and outside cancellation. Sleeping ======== From c54fb22a1c9448b5629d33818039341ad3985cc4 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 5 Apr 2024 15:09:18 -0700 Subject: [PATCH 15/19] Fix markup error --- Doc/library/asyncio-task.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 9ac59c3ea58b6c..a49285bb0c634c 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -411,7 +411,7 @@ The :exc:`asyncio.CancelledError` will be raised at the next Task groups now preserve the cancellation count (as reported by :meth:`asyncio.Task.cancelling`). -.. versionchaged:: 3.13 +.. versionchanged:: 3.13 Improved handling of simultaneous inside and outside cancellation. From 6d4b3d7d57e501a623cf1a8ef5ee36acb794ce8d Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 5 Apr 2024 15:23:17 -0700 Subject: [PATCH 16/19] Add attribution to Arthur Tacca issue --- Doc/whatsnew/3.13.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 12412d25bcf6e3..144020f242d78a 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -317,6 +317,8 @@ asyncio reset the undocumented ``_must_cancel`` flag when the cancellation count reaches zero. + (Inspired by an issue reported by Arthur Tacca in :gh:`116720`.) + base64 ------ From b0866096df1a0064e97bfcd1b19d623b9b55a956 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 8 Apr 2024 13:41:34 -0700 Subject: [PATCH 17/19] Wording changes suggested by Carol Willing --- Doc/library/asyncio-task.rst | 6 +++--- Lib/asyncio/taskgroups.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index a49285bb0c634c..d6d0099b0898b6 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -1392,9 +1392,9 @@ Task Object 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, - but this hadn't been done yet, that arrangement will be + the method checks if a previous :meth:`cancel` call had arranged + for :exc:`CancelledError` to be thrown into the task. + If it hasn't been thrown yet, that arrangement will be rescinded (by resetting the internal ``_must_cancel`` flag). .. versionchanged:: 3.13 diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index 128d688fcbf489..f2ee9648c43876 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -140,8 +140,9 @@ 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. + # If the parent task is being cancelled from the outside + # of the taskgroup, un-cancel and re-cancel the parent task, + # which will keep the cancel count stable. if self._parent_task.cancelling(): self._parent_task.uncancel() self._parent_task.cancel() From 8ac42f64e0031aa013671002f1ed0054cbcfe889 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 8 Apr 2024 16:51:03 -0700 Subject: [PATCH 18/19] Changelog/whatsnew updates from Arthur Tacca Also switch to consistently using internal/external instead of inside/outside. --- Doc/whatsnew/3.13.rst | 13 +++++-------- ...24-04-04-15-28-12.gh-issue-116720.aGhXns.rst | 17 +++++++---------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 144020f242d78a..1414813d1acd06 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -295,20 +295,17 @@ asyncio never awaited). (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.) -* Improved behavior of :class:`asyncio.TaskGroup` when an outside cancellation +* Improved behavior of :class:`asyncio.TaskGroup` when an external 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 + In the case where a task group is cancelled externally 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* ` 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). + :meth:`~asyncio.Task.cancel` method. This ensures that a + :exc:`asyncio.CancelledError` will be raised at the next + :keyword:`await`, so the cancellation is not lost. An added benefit of these changes is that task groups now preserve the cancellation count (:meth:`asyncio.Task.cancelling`). diff --git a/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst b/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst index 128fcf8eb3cd72..39c7d6b8a1e978 100644 --- a/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst +++ b/Misc/NEWS.d/next/Library/2024-04-04-15-28-12.gh-issue-116720.aGhXns.rst @@ -1,17 +1,14 @@ -Improved behavior of :class:`asyncio.TaskGroup` when an outside cancellation +Improved behavior of :class:`asyncio.TaskGroup` when an external 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. +it was possible that the outer task group would misbehave, 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 +In the case where a task group is cancelled externally 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* ` 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). +:meth:`~asyncio.Task.cancel` method. This ensures that a +:exc:`asyncio.CancelledError` will be raised at the next +:keyword:`await`, so the cancellation is not lost. An added benefit of these changes is that task groups now preserve the cancellation count (:meth:`asyncio.Task.cancelling`). From ac87a8e62d1881c9b3731209a445ca1ea2d7bddb Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 8 Apr 2024 17:10:37 -0700 Subject: [PATCH 19/19] Attempted doc improvements inspired by Arthur Tacca's feedback --- Doc/library/asyncio-task.rst | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index d6d0099b0898b6..3d300c37419f13 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -392,28 +392,27 @@ 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. +Task groups are careful not to mix up the internal cancellation used to +"wake up" their :meth:`~object.__aexit__` with cancellation requests +for the task in which they are running made by other parties. 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. +will receive another cancellation and process its own exceptions. -In the case where a task group is cancelled from the outside and also must +In the case where a task group is cancelled externally 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* ` 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). +:meth:`~asyncio.Task.cancel` method. This ensures that a +:exc:`asyncio.CancelledError` will be raised at the next +:keyword:`await`, so the cancellation is not lost. -Task groups now preserve the cancellation count -(as reported by :meth:`asyncio.Task.cancelling`). +Task groups preserve the cancellation count +reported by :meth:`asyncio.Task.cancelling`. .. versionchanged:: 3.13 - Improved handling of simultaneous inside and outside cancellation. + Improved handling of simultaneous internal and external cancellations + and correct preservation of cancellation counts. Sleeping ========