diff --git a/docs/versionhistory.rst b/docs/versionhistory.rst
index 8614af49..4c861838 100644
--- a/docs/versionhistory.rst
+++ b/docs/versionhistory.rst
@@ -3,6 +3,12 @@ Version history
This library adheres to `Semantic Versioning 2.0 `_.
+**UNRELEASED**
+
+- Fixed cancellation problem on asyncio where level-triggered cancellation for **all** parent
+ cancel scopes would not resume after exiting a shielded nested scope
+ (`#370 `_)
+
**3.3.1**
- Added missing documentation for the ``ExceptionGroup.exceptions`` attribute
diff --git a/src/anyio/_backends/_asyncio.py b/src/anyio/_backends/_asyncio.py
index d5abdd3f..8e9fed0e 100644
--- a/src/anyio/_backends/_asyncio.py
+++ b/src/anyio/_backends/_asyncio.py
@@ -305,7 +305,7 @@ def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[Ba
host_task_state.cancel_scope = self._parent_scope
- # Restart the cancellation effort in the nearest directly cancelled parent scope if this
+ # Restart the cancellation effort in the farthest directly cancelled parent scope if this
# one was shielded
if self._shield:
self._deliver_cancellation_to_parent()
@@ -365,12 +365,12 @@ def _deliver_cancellation(self) -> None:
self._cancel_handle = None
def _deliver_cancellation_to_parent(self) -> None:
- """Start cancellation effort in the nearest directly cancelled parent scope"""
+ """Start cancellation effort in the farthest directly cancelled parent scope"""
scope = self._parent_scope
+ scope_to_cancel: Optional[CancelScope] = None
while scope is not None:
if scope._cancel_called and scope._cancel_handle is None:
- scope._deliver_cancellation()
- break
+ scope_to_cancel = scope
# No point in looking beyond any shielded scope
if scope._shield:
@@ -378,6 +378,9 @@ def _deliver_cancellation_to_parent(self) -> None:
scope = scope._parent_scope
+ if scope_to_cancel is not None:
+ scope_to_cancel._deliver_cancellation()
+
def _parent_cancelled(self) -> bool:
# Check whether any parent has been cancelled
cancel_scope = self._parent_scope
diff --git a/tests/test_taskgroups.py b/tests/test_taskgroups.py
index 62a2389c..82442dab 100644
--- a/tests/test_taskgroups.py
+++ b/tests/test_taskgroups.py
@@ -12,6 +12,7 @@
CancelScope, ExceptionGroup, create_task_group, current_effective_deadline, current_time,
fail_after, get_cancelled_exc_class, move_on_after, sleep, wait_all_tasks_blocked)
from anyio.abc import TaskGroup, TaskStatus
+from anyio.lowlevel import checkpoint
if sys.version_info < (3, 7):
current_task = asyncio.Task.current_task
@@ -723,6 +724,29 @@ async def killer(scope: CancelScope) -> None:
await sleep(2)
+async def test_triple_nested_shield() -> None:
+ """Regression test for #370."""
+
+ got_past_checkpoint = False
+
+ async def taskfunc() -> None:
+ nonlocal got_past_checkpoint
+
+ with CancelScope() as scope1:
+ with CancelScope() as scope2:
+ with CancelScope(shield=True):
+ scope1.cancel()
+ scope2.cancel()
+
+ await checkpoint()
+ got_past_checkpoint = True
+
+ async with create_task_group() as tg:
+ tg.start_soon(taskfunc)
+
+ assert not got_past_checkpoint
+
+
def test_task_group_in_generator(anyio_backend_name: str,
anyio_backend_options: Dict[str, Any]) -> None:
async def task_group_generator() -> AsyncGenerator[None, None]: