Skip to content

Commit

Permalink
Fixed task group getting cancelled if start() gets cancelled (#717)
Browse files Browse the repository at this point in the history
This change fixes the problem by special casing the situation where the Future backing `task_status` was cancelled which only happens when the host task is cancelled.
  • Loading branch information
agronholm committed May 26, 2024
1 parent 8b648bc commit 9f5f14b
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 8 deletions.
13 changes: 9 additions & 4 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
portals
- Added ``__slots__`` to ``AsyncResource`` so that child classes can use ``__slots__``
(`#733 <https://github.com/agronholm/anyio/pull/733>`_; PR by Justin Su)
- Fixed two bugs with ``TaskGroup.start()`` on asyncio:

* Fixed erroneous ``RuntimeError: called 'started' twice on the same task status``
when cancelling a task in a TaskGroup created with the ``start()`` method before
the first checkpoint is reached after calling ``task_status.started()``
(`#706 <https://github.com/agronholm/anyio/issues/706>`_; PR by Dominik Schwabe)
* Fixed the entire task group being cancelled if a ``TaskGroup.start()`` call gets
cancelled (`#685 <https://github.com/agronholm/anyio/issues/685>`_,
`#710 <https://github.com/agronholm/anyio/issues/710>`_)
- Fixed a race condition that caused crashes when multiple event loops of the same
backend were running in separate threads and simultaneously attempted to use AnyIO for
their first time (`#425 <https://github.com/agronholm/anyio/issues/425>`_; PR by David
Jiricek and Ganden Schaffner)
- Fixed erroneous ``RuntimeError: called 'started' twice on the same task status``
when cancelling a task in a TaskGroup created with the ``start()`` method before
the first checkpoint is reached after calling ``task_status.started()``
(`#706 <https://github.com/agronholm/anyio/issues/706>`_; PR by Dominik Schwabe)
- Fixed cancellation delivery on asyncio incrementing the wrong cancel scope's
cancellation counter when cascading a cancel operation to a child scope, thus failing
to uncancel the host task (`#716 <https://github.com/agronholm/anyio/issues/716>`_)
Expand Down
6 changes: 6 additions & 0 deletions src/anyio/_backends/_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,12 @@ def task_done(_task: asyncio.Task) -> None:
exc = e

if exc is not None:
# The future can only be in the cancelled state if the host task was
# cancelled, so return immediately instead of adding one more
# CancelledError to the exceptions list
if task_status_future is not None and task_status_future.cancelled():
return

if task_status_future is None or task_status_future.done():
if not isinstance(exc, CancelledError):
self._exceptions.append(exc)
Expand Down
23 changes: 19 additions & 4 deletions tests/test_taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
get_current_task,
move_on_after,
sleep,
sleep_forever,
wait_all_tasks_blocked,
)
from anyio.abc import TaskGroup, TaskStatus
Expand Down Expand Up @@ -127,7 +128,6 @@ async def test_no_called_started_twice() -> None:
async def taskfunc(*, task_status: TaskStatus) -> None:
task_status.started()

# anyio>4.3.0 should not raise "RuntimeError: called 'started' twice on the same task status"
async with create_task_group() as tg:
coro = tg.start(taskfunc)
tg.cancel_scope.cancel()
Expand Down Expand Up @@ -196,9 +196,6 @@ async def taskfunc(*, task_status: TaskStatus) -> None:
assert not finished


@pytest.mark.xfail(
sys.version_info < (3, 9), reason="Requires a way to detect cancellation source"
)
@pytest.mark.parametrize("anyio_backend", ["asyncio"])
async def test_start_native_host_cancelled() -> None:
started = finished = False
Expand Down Expand Up @@ -1360,6 +1357,24 @@ async def wait_cancel() -> None:
await cancelled.wait()


async def test_start_cancels_parent_scope() -> None:
"""Regression test for #685 / #710."""
started: bool = False

async def in_task_group(task_status: TaskStatus[None]) -> None:
nonlocal started
started = True
await sleep_forever()

async with create_task_group() as tg:
with CancelScope() as inner_scope:
inner_scope.cancel()
await tg.start(in_task_group)

assert started
assert not tg.cancel_scope.cancel_called


class TestTaskStatusTyping:
"""
These tests do not do anything at run time, but since the test suite is also checked
Expand Down

0 comments on commit 9f5f14b

Please sign in to comment.