From b750257d5f3b01ce2c5d8763b11bd8235862f101 Mon Sep 17 00:00:00 2001 From: subotac <73706465+subotac@users.noreply.github.com> Date: Fri, 31 Jul 2026 21:30:08 +0300 Subject: [PATCH 1/2] Fixed cancellation retry for tasks with native cancellation --- docs/versionhistory.rst | 3 +++ src/anyio/_backends/_asyncio.py | 3 ++- tests/test_taskgroups.py | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index b9846a30c..2b2553657 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -8,6 +8,9 @@ This library adheres to `Semantic Versioning 2.0 `_. - Added the ``anyio.Future`` synchronization primitive which behaves similar to ``asyncio.Future``, allowing tasks to wait for a value (or exception) from another task (`#1146 `_; PR by @Vizonex) +- Fixed cancellation delivery on asyncio scheduling an unnecessary retry for tasks + that already had native cancellation pending + (`#1258 `_; PR by @subotac) - Added guidance for managing multiple memory object stream producers and consumers with cloned streams (`#330 `_; PR by @nightcityblade) diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py index d0f4b0126..c547c319d 100644 --- a/src/anyio/_backends/_asyncio.py +++ b/src/anyio/_backends/_asyncio.py @@ -595,10 +595,11 @@ def _deliver_cancellation(self, origin: CancelScope) -> bool: if task.done(): continue - should_retry = True if task._must_cancel: # type: ignore[attr-defined] continue + should_retry = True + # The task is eligible for cancellation if it has started if task is not current and (task is self._host_task or _task_started(task)): waiter = task._fut_waiter # type: ignore[attr-defined] diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py index cd935f796..3ead143ff 100644 --- a/tests/test_taskgroups.py +++ b/tests/test_taskgroups.py @@ -400,6 +400,45 @@ async def owner() -> EditableCancelScope: spy.assert_called_once() +@pytest.mark.parametrize("anyio_backend", asyncio_params) +async def test_no_retry_for_task_with_pending_cancellation( + mocker: MockerFixture, +) -> None: + """Regression test for #1258.""" + from anyio._backends import _asyncio + + # To allow the mocker to override a @final class + class EditableCancelScope(_asyncio.CancelScope): + pass + + async def owner( + started: asyncio.Future[EditableCancelScope], blocker: asyncio.Future[None] + ) -> None: + scope = EditableCancelScope().__enter__() + started.set_result(scope) + await blocker + + loop = asyncio.get_running_loop() + started: asyncio.Future[EditableCancelScope] = loop.create_future() + blocker: asyncio.Future[None] = loop.create_future() + task = asyncio.create_task(owner(started, blocker)) + scope = await started + spy = mocker.spy(scope, "_deliver_cancellation") + + # Make the waiter done before cancelling the task so asyncio marks the task + # itself for cancellation instead of forwarding cancellation to the waiter. + blocker.set_result(None) + task.cancel() + assert task._must_cancel # type: ignore[attr-defined] + + scope.cancel() + assert scope._cancel_handle is None + spy.assert_called_once() + + with pytest.raises(asyncio.CancelledError): + await task + + @pytest.mark.parametrize("return_handle", [False, True]) async def test_start_exception_delivery(return_handle: bool) -> None: def task_fn(*, task_status: TaskStatus[str] = TASK_STATUS_IGNORED) -> None: From 9bb250dc4b75eefbf06f6dce3f7858cb79760443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Sat, 8 Aug 2026 00:37:21 +0300 Subject: [PATCH 2/2] Relocated the changelog entry --- docs/versionhistory.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst index 2b2553657..f273b095d 100644 --- a/docs/versionhistory.rst +++ b/docs/versionhistory.rst @@ -8,9 +8,6 @@ This library adheres to `Semantic Versioning 2.0 `_. - Added the ``anyio.Future`` synchronization primitive which behaves similar to ``asyncio.Future``, allowing tasks to wait for a value (or exception) from another task (`#1146 `_; PR by @Vizonex) -- Fixed cancellation delivery on asyncio scheduling an unnecessary retry for tasks - that already had native cancellation pending - (`#1258 `_; PR by @subotac) - Added guidance for managing multiple memory object stream producers and consumers with cloned streams (`#330 `_; PR by @nightcityblade) @@ -48,6 +45,9 @@ This library adheres to `Semantic Versioning 2.0 `_. ``Path(".txt")``) instead of raising ``ValueError`` when given an empty stem on a path with a non-empty suffix, unlike :meth:`pathlib.PurePath.with_stem` (`#1200 `_; PR by @Sanjays2402) +- Fixed cancellation delivery on asyncio scheduling an unnecessary retry for tasks + that already had native cancellation pending + (`#1258 `_; PR by @subotac) **4.14.2**