Fix mutual-rendezvous premature-reap race (#477)

The `test_trynamic_trio` + `a_trynamic_first_scene.py` migration
to paired `to_actor.run()` one-shots carries a race the legacy
`run_in_actor()` shape never had: donny + gretchen each
`wait_for_actor()` (then DIAL) the *other*, but a one-shot is
reaped the instant its own hello returns — so the slower peer
can resolve the winner's registry entry and connect to an
already-dead sockaddr -> `ConnectionRefusedError` boxed as a
`RemoteActorError` (or a reg-wait `TooSlowError`), flaking
~1-in-3 standalone runs.

Mutual-rendezvous peers must OUTLIVE both dialogs, so pin the
lifetimes explicitly: `start_actor()` both as daemons, run both
hellos concurrently via bg `Portal.run()` tasks, then reap with
`an.cancel()` only after the task-nursery joins. (The legacy
teardown-reap provided this pinning implicitly — one of the
few places its semantics were ever actually relied upon.)

Gate: `-k trynamic` standalone x8 green (was flaking); full
`test_registrar` module + the example-runner green.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
drop_ria_nursery
Gud Boi 2026-07-06 13:21:31 -04:00
parent 2a59cefbe8
commit a297a32af6
2 changed files with 47 additions and 36 deletions

View File

@ -17,36 +17,37 @@ async def say_hello(other_actor):
return await portal.run(hi) return await portal.run(hi)
async def run_and_print(
an: tractor.ActorNursery,
name: str,
other_actor: str,
):
print(
await tractor.to_actor.run(
say_hello,
an=an,
name=name,
# arguments are always named
other_actor=other_actor,
)
)
async def main(): async def main():
"""Main tractor entry point, the "master" process (for now """Main tractor entry point, the "master" process (for now
acts as the "director"). acts as the "director").
""" """
async with ( async with tractor.open_nursery() as an:
tractor.open_nursery() as an,
trio.open_nursery() as tn,
):
print("Alright... Action!") print("Alright... Action!")
# both actors wait on the *other* to register so their # both actors wait on (then dial!) the *other*, so each
# one-shots must run concurrently. # must outlive both hellos: spawn as daemons, run the
tn.start_soon(run_and_print, an, 'donny', 'gretchen') # hellos concurrently, reap only once both complete.
tn.start_soon(run_and_print, an, 'gretchen', 'donny') portals: dict[str, tractor.Portal] = {
name: await an.start_actor(
name,
enable_modules=[__name__],
)
for name in ('donny', 'gretchen')
}
async def run_and_print(name: str, other_actor: str):
print(
await portals[name].run(
say_hello,
other_actor=other_actor,
)
)
async with trio.open_nursery() as tn:
tn.start_soon(run_and_print, 'donny', 'gretchen')
tn.start_soon(run_and_print, 'gretchen', 'donny')
await an.cancel()
print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...") print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...")

View File

@ -152,27 +152,37 @@ async def test_trynamic_trio(
for the directed subs. for the directed subs.
''' '''
async with ( async with tractor.open_nursery() as an:
tractor.open_nursery() as n,
trio.open_nursery() as tn,
):
print("Alright... Action!") print("Alright... Action!")
# donny + gretchen each wait on the *other* to register, so # donny + gretchen each wait on (then dial!) the *other*, so
# they must run CONCURRENTLY — schedule both one-shots into a # both actors must OUTLIVE both hellos: spawn as daemons and
# local task-nursery (was two non-blocking `run_in_actor()`s). # only reap after both tasks complete. NB a pair of eagerly
# reaped `to_actor.run()` one-shots races: the first to
# finish dies while the other may still be dialing its
# registry-resolved (now dead) sockaddr -> conn-refused.
portals: dict[str, tractor.Portal] = {
name: await an.start_actor(
name,
enable_modules=[__name__],
)
for name in ('donny', 'gretchen')
}
async def _direct(this_name: str, other_actor: str): async def _direct(this_name: str, other_actor: str):
res = await tractor.to_actor.run( res = await portals[this_name].run(
ria_fn, ria_fn,
an=n,
other_actor=other_actor, other_actor=other_actor,
reg_addr=reg_addr, reg_addr=reg_addr,
name=this_name,
) )
print(res) print(res)
tn.start_soon(_direct, 'donny', 'gretchen') async with trio.open_nursery() as tn:
tn.start_soon(_direct, 'gretchen', 'donny') tn.start_soon(_direct, 'donny', 'gretchen')
tn.start_soon(_direct, 'gretchen', 'donny')
# both hellos have completed; reap the thespians.
await an.cancel()
print("CUTTTT CUUTT CUT!!?! Donny!! You're supposed to say...") print("CUTTTT CUUTT CUT!!?! Donny!! You're supposed to say...")