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**