| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 22b25d1 commit fa58e75
8 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -392,6 +392,27 @@ is also included in the exception group. | |||
| 392 | 392 | The same special case is made for | |
| 393 | 393 | :exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph. | |
| 394 | 394 | ||
| 395 | + Task groups are careful not to mix up the internal cancellation used to | ||
| 396 | + "wake up" their :meth:`~object.__aexit__` with cancellation requests | ||
| 397 | + for the task in which they are running made by other parties. | ||
| 398 | + In particular, when one task group is syntactically nested in another, | ||
| 399 | + and both experience an exception in one of their child tasks simultaneously, | ||
| 400 | + the inner task group will process its exceptions, and then the outer task group | ||
| 401 | + will receive another cancellation and process its own exceptions. | ||
| 402 | + | ||
| 403 | + In the case where a task group is cancelled externally and also must | ||
| 404 | + raise an :exc:`ExceptionGroup`, it will call the parent task's | ||
| 405 | + :meth:`~asyncio.Task.cancel` method. This ensures that a | ||
| 406 | + :exc:`asyncio.CancelledError` will be raised at the next | ||
| 407 | + :keyword:`await`, so the cancellation is not lost. | ||
| 408 | + | ||
| 409 | + Task groups preserve the cancellation count | ||
| 410 | + reported by :meth:`asyncio.Task.cancelling`. | ||
| 411 | + | ||
| 412 | + .. versionchanged:: 3.13 | ||
| 413 | + | ||
| 414 | + Improved handling of simultaneous internal and external cancellations | ||
| 415 | + and correct preservation of cancellation counts. | ||
| 395 | 416 | ||
| 396 | 417 | Sleeping | |
| 397 | 418 | ======== | |
@@ -1369,6 +1390,15 @@ Task Object | |||
| 1369 | 1390 | catching :exc:`CancelledError`, it needs to call this method to remove | |
| 1370 | 1391 | the cancellation state. | |
| 1371 | 1392 | ||
| 1393 | + When this method decrements the cancellation count to zero, | ||
| 1394 | + the method checks if a previous :meth:`cancel` call had arranged | ||
| 1395 | + for :exc:`CancelledError` to be thrown into the task. | ||
| 1396 | + If it hasn't been thrown yet, that arrangement will be | ||
| 1397 | + rescinded (by resetting the internal ``_must_cancel`` flag). | ||
| 1398 | + | ||
| 1399 | + .. versionchanged:: 3.13 | ||
| 1400 | + Changed to rescind pending cancellation requests upon reaching zero. | ||
| 1401 | + | ||
| 1372 | 1402 | .. method:: cancelling() | |
| 1373 | 1403 | ||
| 1374 | 1404 | Return the number of pending cancellation requests to this Task, i.e., | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -196,13 +196,6 @@ Other Language Changes | |||
| 196 | 196 | ||
| 197 | 197 | (Contributed by Sebastian Pipping in :gh:`115623`.) | |
| 198 | 198 | ||
| 199 | - * When :func:`asyncio.TaskGroup.create_task` is called on an inactive | ||
| 200 | - :class:`asyncio.TaskGroup`, the given coroutine will be closed (which | ||
| 201 | - prevents a :exc:`RuntimeWarning` about the given coroutine being | ||
| 202 | - never awaited). | ||
| 203 | - | ||
| 204 | - (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.) | ||
| 205 | - | ||
| 206 | 199 | * The :func:`ssl.create_default_context` API now includes | |
| 207 | 200 | :data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT` | |
| 208 | 201 | in its default flags. | |
@@ -300,6 +293,33 @@ asyncio | |||
| 300 | 293 | with the tasks being completed. | |
| 301 | 294 | (Contributed by Justin Arthur in :gh:`77714`.) | |
| 302 | 295 | ||
| 296 | + * When :func:`asyncio.TaskGroup.create_task` is called on an inactive | ||
| 297 | + :class:`asyncio.TaskGroup`, the given coroutine will be closed (which | ||
| 298 | + prevents a :exc:`RuntimeWarning` about the given coroutine being | ||
| 299 | + never awaited). | ||
| 300 | + (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.) | ||
| 301 | + | ||
| 302 | + * Improved behavior of :class:`asyncio.TaskGroup` when an external cancellation | ||
| 303 | + collides with an internal cancellation. For example, when two task groups | ||
| 304 | + are nested and both experience an exception in a child task simultaneously, | ||
| 305 | + it was possible that the outer task group would hang, because its internal | ||
| 306 | + cancellation was swallowed by the inner task group. | ||
| 307 | + | ||
| 308 | + In the case where a task group is cancelled externally and also must | ||
| 309 | + raise an :exc:`ExceptionGroup`, it will now call the parent task's | ||
| 310 | + :meth:`~asyncio.Task.cancel` method. This ensures that a | ||
| 311 | + :exc:`asyncio.CancelledError` will be raised at the next | ||
| 312 | + :keyword:`await`, so the cancellation is not lost. | ||
| 313 | + | ||
| 314 | + An added benefit of these changes is that task groups now preserve the | ||
| 315 | + cancellation count (:meth:`asyncio.Task.cancelling`). | ||
| 316 | + | ||
| 317 | + In order to handle some corner cases, :meth:`asyncio.Task.uncancel` may now | ||
| 318 | + reset the undocumented ``_must_cancel`` flag when the cancellation count | ||
| 319 | + reaches zero. | ||
| 320 | + | ||
| 321 | + (Inspired by an issue reported by Arthur Tacca in :gh:`116720`.) | ||
| 322 | + | ||
| 303 | 323 | * Add :meth:`asyncio.Queue.shutdown` (along with | |
| 304 | 324 | :exc:`asyncio.QueueShutDown`) for queue termination. | |
| 305 | 325 | (Contributed by Laurie Opperman and Yves Duprat in :gh:`104228`.) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -77,12 +77,6 @@ async def __aexit__(self, et, exc, tb): | |||
| 77 | 77 | propagate_cancellation_error = exc | |
| 78 | 78 | else: | |
| 79 | 79 | propagate_cancellation_error = None | |
| 80 | - if self._parent_cancel_requested: | ||
| 81 | - # If this flag is set we *must* call uncancel(). | ||
| 82 | - if self._parent_task.uncancel() == 0: | ||
| 83 | - # If there are no pending cancellations left, | ||
| 84 | - # don't propagate CancelledError. | ||
| 85 | - propagate_cancellation_error = None | ||
| 86 | 80 | ||
| 87 | 81 | if et is not None: | |
| 88 | 82 | if not self._aborting: | |
@@ -130,6 +124,13 @@ async def __aexit__(self, et, exc, tb): | |||
| 130 | 124 | if self._base_error is not None: | |
| 131 | 125 | raise self._base_error | |
| 132 | 126 | ||
| 127 | + if self._parent_cancel_requested: | ||
| 128 | + # If this flag is set we *must* call uncancel(). | ||
| 129 | + if self._parent_task.uncancel() == 0: | ||
| 130 | + # If there are no pending cancellations left, | ||
| 131 | + # don't propagate CancelledError. | ||
| 132 | + propagate_cancellation_error = None | ||
| 133 | + | ||
| 133 | 134 | # Propagate CancelledError if there is one, except if there | |
| 134 | 135 | # are other errors -- those have priority. | |
| 135 | 136 | if propagate_cancellation_error is not None and not self._errors: | |
@@ -139,6 +140,12 @@ async def __aexit__(self, et, exc, tb): | |||
| 139 | 140 | self._errors.append(exc) | |
| 140 | 141 | ||
| 141 | 142 | if self._errors: | |
| 143 | + # If the parent task is being cancelled from the outside | ||
| 144 | + # of the taskgroup, un-cancel and re-cancel the parent task, | ||
| 145 | + # which will keep the cancel count stable. | ||
| 146 | + if self._parent_task.cancelling(): | ||
| 147 | + self._parent_task.uncancel() | ||
| 148 | + self._parent_task.cancel() | ||
| 142 | 149 | # Exceptions are heavy objects that can have object | |
| 143 | 150 | # cycles (bad for GC); let's not keep a reference to | |
| 144 | 151 | # a bunch of them. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -255,6 +255,8 @@ def uncancel(self): | |||
| 255 | 255 | """ | |
| 256 | 256 | if self._num_cancels_requested > 0: | |
| 257 | 257 | self._num_cancels_requested -= 1 | |
| 258 | + if self._num_cancels_requested == 0: | ||
| 259 | + self._must_cancel = False | ||
| 258 | 260 | return self._num_cancels_requested | |
| 259 | 261 | ||
| 260 | 262 | def __eager_start(self): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -833,6 +833,72 @@ async def run_coro_after_tg_closes(): | |||
| 833 | 833 | loop = asyncio.get_event_loop() | |
| 834 | 834 | loop.run_until_complete(run_coro_after_tg_closes()) | |
| 835 | 835 | ||
| 836 | + async def test_cancelling_level_preserved(self): | ||
| 837 | + async def raise_after(t, e): | ||
| 838 | + await asyncio.sleep(t) | ||
| 839 | + raise e() | ||
| 840 | + | ||
| 841 | + try: | ||
| 842 | + async with asyncio.TaskGroup() as tg: | ||
| 843 | + tg.create_task(raise_after(0.0, RuntimeError)) | ||
| 844 | + except* RuntimeError: | ||
| 845 | + pass | ||
| 846 | + self.assertEqual(asyncio.current_task().cancelling(), 0) | ||
| 847 | + | ||
| 848 | + async def test_nested_groups_both_cancelled(self): | ||
| 849 | + async def raise_after(t, e): | ||
| 850 | + await asyncio.sleep(t) | ||
| 851 | + raise e() | ||
| 852 | + | ||
| 853 | + try: | ||
| 854 | + async with asyncio.TaskGroup() as outer_tg: | ||
| 855 | + try: | ||
| 856 | + async with asyncio.TaskGroup() as inner_tg: | ||
| 857 | + inner_tg.create_task(raise_after(0, RuntimeError)) | ||
| 858 | + outer_tg.create_task(raise_after(0, ValueError)) | ||
| 859 | + except* RuntimeError: | ||
| 860 | + pass | ||
| 861 | + else: | ||
| 862 | + self.fail("RuntimeError not raised") | ||
| 863 | + self.assertEqual(asyncio.current_task().cancelling(), 1) | ||
| 864 | + except* ValueError: | ||
| 865 | + pass | ||
| 866 | + else: | ||
| 867 | + self.fail("ValueError not raised") | ||
| 868 | + self.assertEqual(asyncio.current_task().cancelling(), 0) | ||
| 869 | + | ||
| 870 | + async def test_error_and_cancel(self): | ||
| 871 | + event = asyncio.Event() | ||
| 872 | + | ||
| 873 | + async def raise_error(): | ||
| 874 | + event.set() | ||
| 875 | + await asyncio.sleep(0) | ||
| 876 | + raise RuntimeError() | ||
| 877 | + | ||
| 878 | + async def inner(): | ||
| 879 | + try: | ||
| 880 | + async with taskgroups.TaskGroup() as tg: | ||
| 881 | + tg.create_task(raise_error()) | ||
| 882 | + await asyncio.sleep(1) | ||
| 883 | + self.fail("Sleep in group should have been cancelled") | ||
| 884 | + except* RuntimeError: | ||
| 885 | + self.assertEqual(asyncio.current_task().cancelling(), 1) | ||
| 886 | + self.assertEqual(asyncio.current_task().cancelling(), 1) | ||
| 887 | + await asyncio.sleep(1) | ||
| 888 | + self.fail("Sleep after group should have been cancelled") | ||
| 889 | + | ||
| 890 | + async def outer(): | ||
| 891 | + t = asyncio.create_task(inner()) | ||
| 892 | + await event.wait() | ||
| 893 | + self.assertEqual(t.cancelling(), 0) | ||
| 894 | + t.cancel() | ||
| 895 | + self.assertEqual(t.cancelling(), 1) | ||
| 896 | + with self.assertRaises(asyncio.CancelledError): | ||
| 897 | + await t | ||
| 898 | + self.assertTrue(t.cancelled()) | ||
| 899 | + | ||
| 900 | + await outer() | ||
| 901 | + | ||
| 836 | 902 | ||
| 837 | 903 | if __name__ == "__main__": | |
| 838 | 904 | unittest.main() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -684,6 +684,30 @@ def on_timeout(): | |||
| 684 | 684 | finally: | |
| 685 | 685 | loop.close() | |
| 686 | 686 | ||
| 687 | + def test_uncancel_resets_must_cancel(self): | ||
| 688 | + | ||
| 689 | + async def coro(): | ||
| 690 | + await fut | ||
| 691 | + return 42 | ||
| 692 | + | ||
| 693 | + loop = asyncio.new_event_loop() | ||
| 694 | + fut = asyncio.Future(loop=loop) | ||
| 695 | + task = self.new_task(loop, coro()) | ||
| 696 | + loop.run_until_complete(asyncio.sleep(0)) # Get task waiting for fut | ||
| 697 | + fut.set_result(None) # Make task runnable | ||
| 698 | + try: | ||
| 699 | + task.cancel() # Enter cancelled state | ||
| 700 | + self.assertEqual(task.cancelling(), 1) | ||
| 701 | + self.assertTrue(task._must_cancel) | ||
| 702 | + | ||
| 703 | + task.uncancel() # Undo cancellation | ||
| 704 | + self.assertEqual(task.cancelling(), 0) | ||
| 705 | + self.assertFalse(task._must_cancel) | ||
| 706 | + finally: | ||
| 707 | + res = loop.run_until_complete(task) | ||
| 708 | + self.assertEqual(res, 42) | ||
| 709 | + loop.close() | ||
| 710 | + | ||
| 687 | 711 | def test_cancel(self): | |
| 688 | 712 | ||
| 689 | 713 | def gen(): | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,18 @@ | |||
| 1 | + Improved behavior of :class:`asyncio.TaskGroup` when an external cancellation | ||
| 2 | + collides with an internal cancellation. For example, when two task groups | ||
| 3 | + are nested and both experience an exception in a child task simultaneously, | ||
| 4 | + it was possible that the outer task group would misbehave, because | ||
| 5 | + its internal cancellation was swallowed by the inner task group. | ||
| 6 | + | ||
| 7 | + In the case where a task group is cancelled externally and also must | ||
| 8 | + raise an :exc:`ExceptionGroup`, it will now call the parent task's | ||
| 9 | + :meth:`~asyncio.Task.cancel` method. This ensures that a | ||
| 10 | + :exc:`asyncio.CancelledError` will be raised at the next | ||
| 11 | + :keyword:`await`, so the cancellation is not lost. | ||
| 12 | + | ||
| 13 | + An added benefit of these changes is that task groups now preserve the | ||
| 14 | + cancellation count (:meth:`asyncio.Task.cancelling`). | ||
| 15 | + | ||
| 16 | + In order to handle some corner cases, :meth:`asyncio.Task.uncancel` may now | ||
| 17 | + reset the undocumented ``_must_cancel`` flag when the cancellation count | ||
| 18 | + reaches zero. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2393,6 +2393,9 @@ _asyncio_Task_uncancel_impl(TaskObj *self) | |||
| 2393 | 2393 | { | |
| 2394 | 2394 | if (self->task_num_cancels_requested > 0) { | |
| 2395 | 2395 | self->task_num_cancels_requested -= 1; | |
| 2396 | + if (self->task_num_cancels_requested == 0) { | ||
| 2397 | + self->task_must_cancel = 0; | ||
| 2398 | + } | ||
| 2396 | 2399 | } | |
| 2397 | 2400 | return PyLong_FromLong(self->task_num_cancels_requested); | |
| 2398 | 2401 | } | |
| Back | FazBrowse Home | New Git URL |
0 commit comments