Hoist ria-reaping out of the spawn backends
Step A of the `._ria_nursery` removal (issue #477 follow-up, see `ai/conc-anal/ria_nursery_removal_plan.md`): `.run_in_actor()` children now spawn via the default daemon nursery and their result-reaping moves up into the `ActorNursery` machinery, - new `_supervise._reap_ria_portals()`: one `_spawn.cancel_on_completion()` task per ria child, run AFTER `._join_procs` is set — replacing the per-child reaper task the backends formerly spawned (keyed off `._cancel_after_result_on_exit` membership) which required routing such children into `._ria_nursery`. - happy path: reap awaited right after `._join_procs.set()`, preserving "collect ria results before daemon join" sequencing. - error path: snapshot ria `(portal, subactor)` pairs (backend `finally`s pop `._children` as procs reap), `await an.cancel()`, THEN a 0.5s-bounded reap over the snapshot; anything collectable is already queued in the local ctx and a parked reaper self-cleans (`trio.Cancelled` results are never stashed). NB: a concurrent reap+cancel variant deadlocked `test_multierror` and a 3s bound blew `test_cancel_while_childs_child_in_sync_sleep`'s deadline — deats in the plan doc's probe history. - `spawn/_trio.py` + `spawn/_mp.py`: drop the membership branch, per-child reaper nursery + now-unused `cancel_on_completion` imports; the join phase is a bare `soft_kill()`. `._ria_nursery` is now vestigial (zero spawn users): step B deletes it + `start_actor()`'s `nursery=` escape hatch and merges the supervisor's two error handlers. Prompt-IO: ai/prompt-io/claude/20260702T165806Z_a34aaf98_prompt_io.md (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codewkt/to_actor_subpkg
parent
993102695a
commit
5cd190c5c1
|
|
@ -438,8 +438,6 @@ class ActorNursery:
|
||||||
),
|
),
|
||||||
bind_addrs=bind_addrs,
|
bind_addrs=bind_addrs,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
# use the run_in_actor nursery
|
|
||||||
nursery=self._ria_nursery,
|
|
||||||
infect_asyncio=infect_asyncio,
|
infect_asyncio=infect_asyncio,
|
||||||
inherit_parent_main=inherit_parent_main,
|
inherit_parent_main=inherit_parent_main,
|
||||||
proc_kwargs=proc_kwargs
|
proc_kwargs=proc_kwargs
|
||||||
|
|
@ -580,6 +578,51 @@ class ActorNursery:
|
||||||
self._join_procs.set()
|
self._join_procs.set()
|
||||||
|
|
||||||
|
|
||||||
|
async def _reap_ria_portals(
|
||||||
|
an: ActorNursery,
|
||||||
|
errors: dict[tuple[str, str], BaseException],
|
||||||
|
ria_children: list[tuple[Portal, Actor]]|None = None,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Wait on and stash the final result/error from every
|
||||||
|
`.run_in_actor()`-spawned child then cancel its actor
|
||||||
|
runtime, one `_spawn.cancel_on_completion()` task per
|
||||||
|
child.
|
||||||
|
|
||||||
|
Replaces the per-child reaper task formerly spawned by
|
||||||
|
the spawn backends (keyed off
|
||||||
|
`._cancel_after_result_on_exit` membership) which
|
||||||
|
required routing such children into the (now removable)
|
||||||
|
`._ria_nursery`. Only call AFTER `._join_procs` is set
|
||||||
|
so user code inside the nursery block retains exclusive
|
||||||
|
result-await access; see the "manually await results"
|
||||||
|
note in `spawn._mp.mp_proc()`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if ria_children is None:
|
||||||
|
ria_children: list[tuple[Portal, Actor]] = [
|
||||||
|
(portal, subactor)
|
||||||
|
for subactor, _, portal in an._children.values()
|
||||||
|
if portal in an._cancel_after_result_on_exit
|
||||||
|
]
|
||||||
|
if not ria_children:
|
||||||
|
return
|
||||||
|
|
||||||
|
async with (
|
||||||
|
collapse_eg(),
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
):
|
||||||
|
portal: Portal
|
||||||
|
subactor: Actor
|
||||||
|
for portal, subactor in ria_children:
|
||||||
|
tn.start_soon(
|
||||||
|
_spawn.cancel_on_completion,
|
||||||
|
portal,
|
||||||
|
subactor,
|
||||||
|
errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def _open_and_supervise_one_cancels_all_nursery(
|
async def _open_and_supervise_one_cancels_all_nursery(
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
|
|
@ -641,6 +684,11 @@ async def _open_and_supervise_one_cancels_all_nursery(
|
||||||
)
|
)
|
||||||
an._join_procs.set()
|
an._join_procs.set()
|
||||||
|
|
||||||
|
# collect results (and errors) from all
|
||||||
|
# `.run_in_actor()` children then cancel
|
||||||
|
# each, one reaper task per child.
|
||||||
|
await _reap_ria_portals(an, errors)
|
||||||
|
|
||||||
except BaseException as _inner_err:
|
except BaseException as _inner_err:
|
||||||
inner_err = _inner_err
|
inner_err = _inner_err
|
||||||
errors[actor.aid.uid] = inner_err
|
errors[actor.aid.uid] = inner_err
|
||||||
|
|
@ -704,9 +752,39 @@ async def _open_and_supervise_one_cancels_all_nursery(
|
||||||
# '------ - ------'
|
# '------ - ------'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# snapshot `.run_in_actor()` children
|
||||||
|
# BEFORE cancelling: each backend
|
||||||
|
# spawn-task pops its `._children`
|
||||||
|
# entry as the proc gets reaped.
|
||||||
|
ria_children: list = [
|
||||||
|
(portal, subactor)
|
||||||
|
for subactor, _, portal
|
||||||
|
in an._children.values()
|
||||||
|
if portal in
|
||||||
|
an._cancel_after_result_on_exit
|
||||||
|
]
|
||||||
# cancel all subactors
|
# cancel all subactors
|
||||||
await an.cancel()
|
await an.cancel()
|
||||||
|
|
||||||
|
# then collect any already-relayed
|
||||||
|
# results/errors from ria children.
|
||||||
|
# Tightly bounded: anything
|
||||||
|
# collectable is already queued in
|
||||||
|
# the local ctx (relayed BEFORE the
|
||||||
|
# cancel above); a child hard-killed
|
||||||
|
# without relaying just parks its
|
||||||
|
# reaper which then self-cleans (a
|
||||||
|
# `trio.Cancelled` result is never
|
||||||
|
# stashed), mirroring the old
|
||||||
|
# backend-side reaper-vs-`soft_kill`
|
||||||
|
# cancel race.
|
||||||
|
with trio.move_on_after(0.5):
|
||||||
|
await _reap_ria_portals(
|
||||||
|
an,
|
||||||
|
errors,
|
||||||
|
ria_children=ria_children,
|
||||||
|
)
|
||||||
|
|
||||||
# ria_nursery scope end
|
# ria_nursery scope end
|
||||||
|
|
||||||
# TODO: this is the handler around the ``.run_in_actor()``
|
# TODO: this is the handler around the ``.run_in_actor()``
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ from ._entry import _mp_main
|
||||||
# by `try_set_start_method()` after module load time.
|
# by `try_set_start_method()` after module load time.
|
||||||
from . import _spawn
|
from . import _spawn
|
||||||
from ._spawn import (
|
from ._spawn import (
|
||||||
cancel_on_completion,
|
|
||||||
proc_waiter,
|
proc_waiter,
|
||||||
soft_kill,
|
soft_kill,
|
||||||
)
|
)
|
||||||
|
|
@ -187,30 +186,16 @@ async def mp_proc(
|
||||||
with trio.CancelScope(shield=True):
|
with trio.CancelScope(shield=True):
|
||||||
await actor_nursery._join_procs.wait()
|
await actor_nursery._join_procs.wait()
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
# This is a "soft" (cancellable) join/reap which
|
||||||
if portal in actor_nursery._cancel_after_result_on_exit:
|
# will remote cancel the actor on a ``trio.Cancelled``
|
||||||
nursery.start_soon(
|
# condition. Any `.run_in_actor()` result-reaping
|
||||||
cancel_on_completion,
|
# happens up in the `ActorNursery` machinery (see
|
||||||
portal,
|
# `_supervise._reap_ria_portals()`), NOT here.
|
||||||
subactor,
|
await soft_kill(
|
||||||
errors
|
proc,
|
||||||
)
|
proc_waiter,
|
||||||
|
portal
|
||||||
# This is a "soft" (cancellable) join/reap which
|
)
|
||||||
# will remote cancel the actor on a ``trio.Cancelled``
|
|
||||||
# condition.
|
|
||||||
await soft_kill(
|
|
||||||
proc,
|
|
||||||
proc_waiter,
|
|
||||||
portal
|
|
||||||
)
|
|
||||||
|
|
||||||
# cancel result waiter that may have been spawned in
|
|
||||||
# tandem if not done already
|
|
||||||
log.warning(
|
|
||||||
"Cancelling existing result waiter task for "
|
|
||||||
f"{subactor.aid.uid}")
|
|
||||||
nursery.cancel_scope.cancel()
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# hard reap sequence
|
# hard reap sequence
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ from tractor.msg import (
|
||||||
pretty_struct,
|
pretty_struct,
|
||||||
)
|
)
|
||||||
from ._spawn import (
|
from ._spawn import (
|
||||||
cancel_on_completion,
|
|
||||||
hard_kill,
|
hard_kill,
|
||||||
soft_kill,
|
soft_kill,
|
||||||
)
|
)
|
||||||
|
|
@ -195,31 +194,16 @@ async def trio_proc(
|
||||||
with trio.CancelScope(shield=True):
|
with trio.CancelScope(shield=True):
|
||||||
await actor_nursery._join_procs.wait()
|
await actor_nursery._join_procs.wait()
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
# This is a "soft" (cancellable) join/reap which
|
||||||
if portal in actor_nursery._cancel_after_result_on_exit:
|
# will remote cancel the actor on a ``trio.Cancelled``
|
||||||
nursery.start_soon(
|
# condition. Any `.run_in_actor()` result-reaping
|
||||||
cancel_on_completion,
|
# happens up in the `ActorNursery` machinery (see
|
||||||
portal,
|
# `_supervise._reap_ria_portals()`), NOT here.
|
||||||
subactor,
|
await soft_kill(
|
||||||
errors
|
proc,
|
||||||
)
|
trio.Process.wait, # XXX, uses `pidfd_open()` below.
|
||||||
|
portal
|
||||||
# This is a "soft" (cancellable) join/reap which
|
)
|
||||||
# will remote cancel the actor on a ``trio.Cancelled``
|
|
||||||
# condition.
|
|
||||||
await soft_kill(
|
|
||||||
proc,
|
|
||||||
trio.Process.wait, # XXX, uses `pidfd_open()` below.
|
|
||||||
portal
|
|
||||||
)
|
|
||||||
|
|
||||||
# cancel result waiter that may have been spawned in
|
|
||||||
# tandem if not done already
|
|
||||||
log.cancel(
|
|
||||||
'Cancelling portal result reaper task\n'
|
|
||||||
f'c)> {subactor.aid.reprol()!r}\n'
|
|
||||||
)
|
|
||||||
nursery.cancel_scope.cancel()
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# XXX NOTE XXX: The "hard" reap since no actor zombies are
|
# XXX NOTE XXX: The "hard" reap since no actor zombies are
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue