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]: