Skip to content

Commit cb1aafc

Browse files
committed
forcefully abort children of unclosed nurseries to avoid internalerror
1 parent 6e4735a commit cb1aafc

File tree

2 files changed

+48
-9
lines changed

2 files changed

+48
-9
lines changed

src/trio/_core/_run.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,11 +2013,33 @@ def task_exited(self, task: Task, outcome: Outcome[object]) -> None:
20132013
lot.break_lot(task)
20142014
del GLOBAL_PARKING_LOT_BREAKER[task]
20152015

2016-
if (
2016+
if task._child_nurseries:
2017+
# Forcefully abort any tasks spawned by the misnested nursery to
2018+
# avoid internal errors.
2019+
runner = GLOBAL_RUN_CONTEXT.runner
2020+
for nursery in task._child_nurseries:
2021+
nursery.cancel_scope.cancel()
2022+
for child in nursery._children:
2023+
if child in runner.runq:
2024+
runner.runq.remove(child)
2025+
runner.tasks.remove(child)
2026+
nursery._children.clear()
2027+
try:
2028+
# Raise this, rather than just constructing it, to get a
2029+
# traceback frame included
2030+
raise RuntimeError(
2031+
"Nursery stack corrupted: nurseries spawned by "
2032+
f"{task!r} was still live when the task exited\n{MISNESTING_ADVICE}",
2033+
)
2034+
except RuntimeError as new_exc:
2035+
if isinstance(outcome, Error):
2036+
new_exc.__context__ = outcome.error
2037+
outcome = Error(new_exc)
2038+
elif (
20172039
task._cancel_status is not None
20182040
and task._cancel_status.abandoned_by_misnesting
20192041
and task._cancel_status.parent is None
2020-
) or any(not nursery._closed for nursery in task._child_nurseries):
2042+
):
20212043
# The cancel scope surrounding this task's nursery was closed
20222044
# before the task exited. Force the task to exit with an error,
20232045
# since the error might not have been caught elsewhere. See the

src/trio/_core/_tests/test_run.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -861,7 +861,7 @@ async def inner_func() -> None:
861861
inner_nursery.start_soon(sleep, 1)
862862

863863
with pytest.RaisesGroup(
864-
pytest.RaisesExc(RuntimeError, match="Cancel scope stack corrupted")
864+
pytest.RaisesExc(RuntimeError, match="Nursery stack corrupted")
865865
):
866866
async with _core.open_nursery() as outer_nursery:
867867
inner_cm = _core.open_nursery()
@@ -874,20 +874,37 @@ async def asynccontextmanager_that_creates_a_nursery_internally() -> (
874874
AsyncGenerator[None]
875875
):
876876
async with _core.open_nursery() as nursery:
877-
nursery.start_soon(
878-
print_sleep_print, "task_in_asynccontextmanager_nursery", 2.0
879-
)
877+
await nursery.start(started_sleeper)
878+
nursery.start_soon(unstarted_task)
880879
yield
881880

882-
async def print_sleep_print(name: str, sleep_time: float) -> None:
883-
await sleep(sleep_time)
881+
async def started_sleeper(task_status: _core.TaskStatus[None]) -> None:
882+
task_status.started()
883+
await sleep_forever()
884884

885-
async with AsyncExitStack() as stack, _core.open_nursery() as nursery:
885+
async def unstarted_task() -> None:
886+
raise AssertionError("this should not even get a chance to run")
887+
888+
async with AsyncExitStack() as stack:
889+
manager = _core.open_nursery()
890+
nursery = await manager.__aenter__()
886891
# The asynccontextmanager is going to create a nursery that outlives this nursery!
887892
nursery.start_soon(
888893
stack.enter_async_context,
889894
asynccontextmanager_that_creates_a_nursery_internally(),
890895
)
896+
with pytest.RaisesGroup(
897+
pytest.RaisesExc(RuntimeError, match="Nursery stack corrupted")
898+
):
899+
await manager.__aexit__(None, None, None)
900+
901+
# The outer nursery forcefully aborts the inner nursery and stops `unstarted_task`
902+
# from ever being started.
903+
with pytest.warns(
904+
RuntimeWarning,
905+
match="^coroutine 'test_asyncexitstack_nursery_misnest.<locals>.unstarted_task' was never awaited$",
906+
):
907+
gc_collect_harder()
891908

892909

893910
@slow

0 commit comments

Comments
 (0)