Skip to content

check for nursery misnesting on task exit #3307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2017,7 +2017,7 @@ def task_exited(self, task: Task, outcome: Outcome[object]) -> None:
task._cancel_status is not None
and task._cancel_status.abandoned_by_misnesting
and task._cancel_status.parent is None
):
) or any(not nursery._closed for nursery in task._child_nurseries):
# The cancel scope surrounding this task's nursery was closed
# before the task exited. Force the task to exit with an error,
# since the error might not have been caught elsewhere. See the
Expand Down
50 changes: 48 additions & 2 deletions src/trio/_core/_tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import time
import types
import weakref
from contextlib import ExitStack, contextmanager, suppress
from contextlib import (
AsyncExitStack,
ExitStack,
asynccontextmanager,
contextmanager,
suppress,
)
from math import inf, nan
from typing import TYPE_CHECKING, NoReturn, TypeVar
from unittest import mock
Expand Down Expand Up @@ -761,7 +767,7 @@ async def enter_scope() -> None:
assert scope.cancel_called # never become un-cancelled


async def test_cancel_scope_misnesting() -> None:
async def test_cancel_scope_misnesting_1() -> None:
outer = _core.CancelScope()
inner = _core.CancelScope()
with ExitStack() as stack:
Expand All @@ -771,6 +777,8 @@ async def test_cancel_scope_misnesting() -> None:
stack.close()
# No further error is raised when exiting the inner context


async def test_cancel_scope_misnesting_2() -> None:
# If there are other tasks inside the abandoned part of the cancel tree,
# they get cancelled when the misnesting is detected
async def task1() -> None:
Expand Down Expand Up @@ -828,6 +836,8 @@ def no_context(exc: RuntimeError) -> bool:
)
assert group.matches(exc_info.value.__context__)


async def test_cancel_scope_misnesting_3() -> None:
# Trying to exit a cancel scope from an unrelated task raises an error
# without affecting any state
async def task3(task_status: _core.TaskStatus[_core.CancelScope]) -> None:
Expand All @@ -844,6 +854,42 @@ async def task3(task_status: _core.TaskStatus[_core.CancelScope]) -> None:
scope.cancel()


async def test_nursery_misnest() -> None:
# See https://github.com/python-trio/trio/issues/3298
async def inner_func() -> None:
inner_nursery = await inner_cm.__aenter__()
inner_nursery.start_soon(sleep, 1)

with pytest.RaisesGroup(
pytest.RaisesExc(RuntimeError, match="Cancel scope stack corrupted")
):
async with _core.open_nursery() as outer_nursery:
inner_cm = _core.open_nursery()
outer_nursery.start_soon(inner_func)


async def test_asyncexitstack_nursery_misnest() -> None:
@asynccontextmanager
async def asynccontextmanager_that_creates_a_nursery_internally() -> (
AsyncGenerator[None]
):
async with _core.open_nursery() as nursery:
nursery.start_soon(
print_sleep_print, "task_in_asynccontextmanager_nursery", 2.0
)
yield

async def print_sleep_print(name: str, sleep_time: float) -> None:
await sleep(sleep_time)

async with AsyncExitStack() as stack, _core.open_nursery() as nursery:
# The asynccontextmanager is going to create a nursery that outlives this nursery!
nursery.start_soon(
stack.enter_async_context,
asynccontextmanager_that_creates_a_nursery_internally(),
)


@slow
async def test_timekeeping() -> None:
# probably a good idea to use a real clock for *one* test anyway...
Expand Down
2 changes: 1 addition & 1 deletion test-requirements.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# For tests
pytest >= 5.0 # for faulthandler in core
pytest >= 8.4 # for pytest.RaisesGroup
coverage >= 7.2.5
async_generator >= 1.9
pyright
Expand Down
Loading