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 bb0a9b3c93
commit 551090d129
1 changed files with 22 additions and 12 deletions

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)
async with trio.open_nursery() as tn:
tn.start_soon(_direct, 'donny', 'gretchen') tn.start_soon(_direct, 'donny', 'gretchen')
tn.start_soon(_direct, 'gretchen', 'donny') 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...")