Skip to content

Commit 55db083

Browse files
Merge pull request #2696 from oremanj/guest-mode-waits-for-init
Ensure guest run is initialized when start_guest_run() returns
2 parents d3255a0 + cc2d74a commit 55db083

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed

newsfragments/2696.feature.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`trio.lowlevel.start_guest_run` now does a bit more setup of the guest run
2+
before it returns to its caller, so that the caller can immediately make calls to
3+
:func:`trio.current_time`, :func:`trio.lowlevel.spawn_system_task`,
4+
:func:`trio.lowlevel.current_trio_token`, etc.

trio/_core/_run.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,6 +2279,16 @@ def start_guest_run(
22792279
the host loop and then immediately starts the guest run, and then shuts
22802280
down the host when the guest run completes.
22812281
2282+
Once :func:`start_guest_run` returns successfully, the guest run
2283+
has been set up enough that you can invoke sync-colored Trio
2284+
functions such as :func:`~trio.current_time`, :func:`spawn_system_task`,
2285+
and :func:`current_trio_token`. If a `~trio.TrioInternalError` occurs
2286+
during this early setup of the guest run, it will be raised out of
2287+
:func:`start_guest_run`. All other errors, including all errors
2288+
raised by the *async_fn*, will be delivered to your
2289+
*done_callback* at some point after :func:`start_guest_run` returns
2290+
successfully.
2291+
22822292
Args:
22832293
22842294
run_sync_soon_threadsafe: An arbitrary callable, which will be passed a
@@ -2339,6 +2349,43 @@ def my_done_callback(run_outcome):
23392349
host_uses_signal_set_wakeup_fd=host_uses_signal_set_wakeup_fd,
23402350
),
23412351
)
2352+
2353+
# Run a few ticks of the guest run synchronously, so that by the
2354+
# time we return, the system nursery exists and callers can use
2355+
# spawn_system_task. We don't actually run any user code during
2356+
# this time, so it shouldn't be possible to get an exception here,
2357+
# except for a TrioInternalError.
2358+
next_send = None
2359+
for tick in range(5): # expected need is 2 iterations + leave some wiggle room
2360+
if runner.system_nursery is not None:
2361+
# We're initialized enough to switch to async guest ticks
2362+
break
2363+
try:
2364+
timeout = guest_state.unrolled_run_gen.send(next_send)
2365+
except StopIteration: # pragma: no cover
2366+
raise TrioInternalError(
2367+
"Guest runner exited before system nursery was initialized"
2368+
)
2369+
if timeout != 0: # pragma: no cover
2370+
guest_state.unrolled_run_gen.throw(
2371+
TrioInternalError(
2372+
"Guest runner blocked before system nursery was initialized"
2373+
)
2374+
)
2375+
# next_send should be the return value of
2376+
# IOManager.get_events() if no I/O was waiting, which is
2377+
# platform-dependent. We don't actually check for I/O during
2378+
# this init phase because no one should be expecting any yet.
2379+
next_send = 0 if sys.platform == "win32" else ()
2380+
else: # pragma: no cover
2381+
guest_state.unrolled_run_gen.throw(
2382+
TrioInternalError(
2383+
"Guest runner yielded too many times before "
2384+
"system nursery was initialized"
2385+
)
2386+
)
2387+
2388+
guest_state.unrolled_run_next_send = Value(next_send)
23422389
run_sync_soon_not_threadsafe(guest_state.guest_tick)
23432390

23442391

trio/_core/_tests/test_guest_mode.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# our main
2727
# - final result is returned
2828
# - any unhandled exceptions cause an immediate crash
29-
def trivial_guest_run(trio_fn, **start_guest_run_kwargs):
29+
def trivial_guest_run(trio_fn, *, in_host_after_start=None, **start_guest_run_kwargs):
3030
todo = queue.Queue()
3131

3232
host_thread = threading.current_thread()
@@ -58,6 +58,8 @@ def done_callback(outcome):
5858
done_callback=done_callback,
5959
**start_guest_run_kwargs,
6060
)
61+
if in_host_after_start is not None:
62+
in_host_after_start()
6163

6264
try:
6365
while True:
@@ -109,6 +111,49 @@ async def do_receive():
109111
trivial_guest_run(trio_main)
110112

111113

114+
def test_guest_is_initialized_when_start_returns():
115+
trio_token = None
116+
record = []
117+
118+
async def trio_main(in_host):
119+
record.append("main task ran")
120+
await trio.sleep(0)
121+
assert trio.lowlevel.current_trio_token() is trio_token
122+
return "ok"
123+
124+
def after_start():
125+
# We should get control back before the main task executes any code
126+
assert record == []
127+
128+
nonlocal trio_token
129+
trio_token = trio.lowlevel.current_trio_token()
130+
trio_token.run_sync_soon(record.append, "run_sync_soon cb ran")
131+
132+
@trio.lowlevel.spawn_system_task
133+
async def early_task():
134+
record.append("system task ran")
135+
await trio.sleep(0)
136+
137+
res = trivial_guest_run(trio_main, in_host_after_start=after_start)
138+
assert res == "ok"
139+
assert set(record) == {"system task ran", "main task ran", "run_sync_soon cb ran"}
140+
141+
# Errors during initialization (which can only be TrioInternalErrors)
142+
# are raised out of start_guest_run, not out of the done_callback
143+
with pytest.raises(trio.TrioInternalError):
144+
145+
class BadClock:
146+
def start_clock(self):
147+
raise ValueError("whoops")
148+
149+
def after_start_never_runs(): # pragma: no cover
150+
pytest.fail("shouldn't get here")
151+
152+
trivial_guest_run(
153+
trio_main, clock=BadClock(), in_host_after_start=after_start_never_runs
154+
)
155+
156+
112157
def test_host_can_directly_wake_trio_task():
113158
async def trio_main(in_host):
114159
ev = trio.Event()

0 commit comments

Comments
 (0)