Skip to content

Commit

Permalink
Fixed level cancellation not resuming for all parent scopes after exi…
Browse files Browse the repository at this point in the history
…ting a shielded scope

Fixes #370.
  • Loading branch information
agronholm committed Sep 19, 2021
1 parent 9ad5e71 commit 737e911
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 4 deletions.
6 changes: 6 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Version history

This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.

**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 <https://github.com/agronholm/anyio/issues/370>`_)

**3.3.1**

- Added missing documentation for the ``ExceptionGroup.exceptions`` attribute
Expand Down
11 changes: 7 additions & 4 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -365,19 +365,22 @@ 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:
break

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
Expand Down
24 changes: 24 additions & 0 deletions tests/test_taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down

0 comments on commit 737e911

Please sign in to comment.