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)
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():
"""Main tractor entry point, the "master" process (for now
acts as the "director").
"""
async with (
tractor.open_nursery() as an,
trio.open_nursery() as tn,
):
async with tractor.open_nursery() as an:
print("Alright... Action!")
# both actors wait on the *other* to register so their
# one-shots must run concurrently.
tn.start_soon(run_and_print, an, 'donny', 'gretchen')
tn.start_soon(run_and_print, an, 'gretchen', 'donny')
# both actors wait on (then dial!) the *other*, so each
# must outlive both hellos: spawn as daemons, run the
# hellos concurrently, reap only once both complete.
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...")

View File

@ -152,27 +152,37 @@ async def test_trynamic_trio(
for the directed subs.
'''
async with (
tractor.open_nursery() as n,
trio.open_nursery() as tn,
):
async with tractor.open_nursery() as an:
print("Alright... Action!")
# donny + gretchen each wait on the *other* to register, so
# they must run CONCURRENTLY — schedule both one-shots into a
# local task-nursery (was two non-blocking `run_in_actor()`s).
# donny + gretchen each wait on (then dial!) the *other*, so
# both actors must OUTLIVE both hellos: spawn as daemons and
# 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):
res = await tractor.to_actor.run(
res = await portals[this_name].run(
ria_fn,
an=n,
other_actor=other_actor,
reg_addr=reg_addr,
name=this_name,
)
print(res)
tn.start_soon(_direct, 'donny', 'gretchen')
tn.start_soon(_direct, 'gretchen', 'donny')
async with trio.open_nursery() as tn:
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...")