Skip to content

Commit

Permalink
Ensure guest runner is initialized when start_guest_run() returns
Browse files Browse the repository at this point in the history
  • Loading branch information
oremanj committed Jul 11, 2023
1 parent 493c915 commit 88f604d
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 1 deletion.
4 changes: 4 additions & 0 deletions newsfragments/2696.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:func:`trio.lowlevel.start_guest_run` now does a bit more setup of the guest run
before it returns to its caller, so that the caller can immediately make calls to
:func:`trio.current_time`, :func:`trio.lowlevel.spawn_system_task`,
:func:`trio.lowlevel.current_trio_token`, etc.
43 changes: 43 additions & 0 deletions trio/_core/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,16 @@ def start_guest_run(
the host loop and then immediately starts the guest run, and then shuts
down the host when the guest run completes.
Once :func:`start_guest_run` returns successfully, the guest run
has been set up enough that you can invoke sync-colored Trio
functions such as :func:`current_time`, :func:`spawn_system_task`,
and :func:`current_trio_token`. If a `TrioInternalError` occurs
during this early setup of the guest run, it will be raised out of
:func:`start_guest_run`. All other errors, including all errors
raised by the *async_fn*, will be delivered to your
*done_callback* at some point after :func:`start_guest_run` returns
successfully.
Args:
run_sync_soon_threadsafe: An arbitrary callable, which will be passed a
Expand Down Expand Up @@ -2170,6 +2180,39 @@ def my_done_callback(run_outcome):
host_uses_signal_set_wakeup_fd=host_uses_signal_set_wakeup_fd,
),
)

# Run a few ticks of the guest run synchronously, so that by the
# time we return, the system nursery exists and callers can use
# spawn_system_task. We don't actually run any user code during
# this time, so it shouldn't be possible to get an exception here,
# except for a TrioInternalError.
next_send = None
for tick in range(5): # expected need is 2 iterations + leave some wiggle room
if runner.system_nursery is not None:
# We're initialized enough to switch to async guest ticks
break
try:
timeout = guest_state.unrolled_run_gen.send(next_send)
except StopIteration: # pragma: no cover
raise TrioInternalError(
"Guest runner exited before system nursery was initialized"
)
if timeout != 0: # pragma: no cover
guest_state.unrolled_run_gen.throw(
TrioInternalError(
"Guest runner blocked before system nursery was initialized"
)
)
next_send = ()
else: # pragma: no cover
guest_state.unrolled_run_gen.throw(
TrioInternalError(
"Guest runner yielded too many times before "
"system nursery was initialized"
)
)

guest_state.unrolled_run_next_send = Value(next_send)
run_sync_soon_not_threadsafe(guest_state.guest_tick)


Expand Down
46 changes: 45 additions & 1 deletion trio/_core/_tests/test_guest_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
# our main
# - final result is returned
# - any unhandled exceptions cause an immediate crash
def trivial_guest_run(trio_fn, **start_guest_run_kwargs):
def trivial_guest_run(trio_fn, *, in_host_after_start=None, **start_guest_run_kwargs):
todo = queue.Queue()

host_thread = threading.current_thread()
Expand Down Expand Up @@ -56,6 +56,8 @@ def done_callback(outcome):
done_callback=done_callback,
**start_guest_run_kwargs,
)
if in_host_after_start is not None:
in_host_after_start()

try:
while True:
Expand Down Expand Up @@ -107,6 +109,48 @@ async def do_receive():
trivial_guest_run(trio_main)


def test_guest_is_initialized_when_start_returns():
trio_token = None
record = []

async def trio_main(in_host):
record.append("main task ran")
await trio.sleep(0)
assert trio.lowlevel.current_trio_token() is trio_token
return "ok"

def after_start():
# We should get control back before the main task executes any code
assert record == []

nonlocal trio_token
trio_token = trio.lowlevel.current_trio_token()
trio_token.run_sync_soon(record.append, "run_sync_soon cb ran")

@trio.lowlevel.spawn_system_task
async def early_task():
record.append("system task ran")
await trio.sleep(0)

res = trivial_guest_run(trio_main, in_host_after_start=after_start)
assert res == "ok"
assert set(record) == {"system task ran", "main task ran", "run_sync_soon cb ran"}

# Errors during initialization (which can only be TrioInternalErrors)
# are raised out of start_guest_run, not out of the done_callback
with pytest.raises(trio.TrioInternalError):
class BadClock:
def start_clock(self):
raise ValueError("whoops")

def after_start_never_runs(): # pragma: no cover
pytest.fail("shouldn't get here")

trivial_guest_run(
trio_main, clock=BadClock(), in_host_after_start=after_start_never_runs
)


def test_host_can_directly_wake_trio_task():
async def trio_main(in_host):
ev = trio.Event()
Expand Down

0 comments on commit 88f604d

Please sign in to comment.