Skip to content

Commit 32302eb

Browse files
authored
Merge pull request #1743 from oremanj/from-thread-run-lastminute
Make trio.from_thread.run() raise RunFinishedError if the system nursery is closed
2 parents 20ee2b1 + 5d940e0 commit 32302eb

5 files changed

Lines changed: 56 additions & 2 deletions

File tree

newsfragments/1738.bugfix.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:func:`trio.from_thread.run` no longer crashes the Trio run if it is
2+
executed after the system nursery has been closed but before the run
3+
has finished. Calls made at this time will now raise
4+
`trio.RunFinishedError`. This fixes a regression introduced in
5+
Trio 0.17.0. The window in question is only one scheduler tick long in
6+
most cases, but may be longer if async generators need to be cleaned up.

trio/_core/_generated_run.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,15 @@ def spawn_system_task(async_fn, *args, name=None):
126126
127127
* System tasks do not inherit context variables from their creator.
128128
129+
Towards the end of a call to :meth:`trio.run`, after the main
130+
task and all system tasks have exited, the system nursery
131+
becomes closed. At this point, new calls to
132+
:func:`spawn_system_task` will raise ``RuntimeError("Nursery
133+
is closed to new arrivals")`` instead of creating a system
134+
task. It's possible to encounter this state either in
135+
a ``finally`` block in an async generator, or in a callback
136+
passed to :meth:`TrioToken.run_sync_soon` at the right moment.
137+
129138
Args:
130139
async_fn: An async callable.
131140
args: Positional arguments for ``async_fn``. If you want to pass

trio/_core/_run.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,15 @@ def spawn_system_task(self, async_fn, *args, name=None):
15461546
15471547
* System tasks do not inherit context variables from their creator.
15481548
1549+
Towards the end of a call to :meth:`trio.run`, after the main
1550+
task and all system tasks have exited, the system nursery
1551+
becomes closed. At this point, new calls to
1552+
:func:`spawn_system_task` will raise ``RuntimeError("Nursery
1553+
is closed to new arrivals")`` instead of creating a system
1554+
task. It's possible to encounter this state either in
1555+
a ``finally`` block in an async generator, or in a callback
1556+
passed to :meth:`TrioToken.run_sync_soon` at the right moment.
1557+
15491558
Args:
15501559
async_fn: An async callable.
15511560
args: Positional arguments for ``async_fn``. If you want to pass

trio/_threads.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# coding: utf-8
2+
13
import threading
24
import queue as stdlib_queue
35
from itertools import count
@@ -248,7 +250,8 @@ def from_thread_run(afn, *args, trio_token=None):
248250
249251
Raises:
250252
RunFinishedError: if the corresponding call to :func:`trio.run` has
251-
already completed.
253+
already completed, or if the run has started its final cleanup phase
254+
and can no longer spawn new system tasks.
252255
Cancelled: if the corresponding call to :func:`trio.run` completes
253256
while ``afn(*args)`` is running, then ``afn`` is likely to raise
254257
:exc:`trio.Cancelled`, and this will propagate out into
@@ -279,7 +282,12 @@ async def unprotected_afn():
279282
async def await_in_trio_thread_task():
280283
q.put_nowait(await outcome.acapture(unprotected_afn))
281284

282-
trio.lowlevel.spawn_system_task(await_in_trio_thread_task, name=afn)
285+
try:
286+
trio.lowlevel.spawn_system_task(await_in_trio_thread_task, name=afn)
287+
except RuntimeError: # system nursery is closed
288+
q.put_nowait(
289+
outcome.Error(trio.RunFinishedError("system nursery is closed"))
290+
)
283291

284292
return _run_fn_as_system_task(callback, afn, *args, trio_token=trio_token)
285293

trio/tests/test_threads.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .. import _core
88
from .. import Event, CapacityLimiter, sleep
99
from ..testing import wait_all_tasks_blocked
10+
from .._core.tests.tutil import buggy_pypy_asyncgens
1011
from .._threads import (
1112
to_thread_run_sync,
1213
current_default_thread_limiter,
@@ -554,3 +555,24 @@ def not_called(): # pragma: no cover
554555
trio_token = _core.current_trio_token()
555556
with pytest.raises(RuntimeError):
556557
from_thread_run_sync(not_called, trio_token=trio_token)
558+
559+
560+
@pytest.mark.skipif(buggy_pypy_asyncgens, reason="pypy 7.2.0 is buggy")
561+
def test_from_thread_run_during_shutdown():
562+
save = []
563+
record = []
564+
565+
async def agen():
566+
try:
567+
yield
568+
finally:
569+
with pytest.raises(_core.RunFinishedError), _core.CancelScope(shield=True):
570+
await to_thread_run_sync(from_thread_run, sleep, 0)
571+
record.append("ok")
572+
573+
async def main():
574+
save.append(agen())
575+
await save[-1].asend(None)
576+
577+
_core.run(main)
578+
assert record == ["ok"]

0 commit comments

Comments
 (0)