From 9201a2ed5307dedddf4fa13b0d27fb770aa20483 Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 2 Jul 2026 17:47:46 -0400 Subject: [PATCH] Drop the vestigial `._ria_nursery` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step B of the `._ria_nursery` removal (issue #477; see `ai/conc-anal/ria_nursery_removal_plan.md`). With step A having rerouted `.run_in_actor()` children onto the daemon nursery, the 2ndary "run-in-actor" nursery spawns nothing and its stored ref is never read — pure dead weight, - collapse the inner `async with trio.open_nursery() as ria_nursery` layer in `_open_and_supervise_one_cancels_all_nursery`; `da_nursery` is now the single nursery for ALL subactors. - `ActorNursery.__init__` loses the `ria_nursery` param + the `self._ria_nursery` attr; `start_actor()` loses its `nursery=` escape-hatch (spawns via `self._da_nursery` directly). - `._cancel_after_result_on_exit` stays — still the ria-child discriminator for `_reap_ria_portals()`. Behavior-preserving: a zero-task `trio.open_nursery()` only adds a checkpoint. The two error handlers are KEPT (now nested under the single nursery); merging them changes error/cancel propagation and is deferred to its own PR (TODO left at the outer `except`). Prompt-IO: ai/prompt-io/claude/20260702T172233Z_5cd190c5_prompt_io.md (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/runtime/_supervise.py | 278 +++++++++++++++------------------- 1 file changed, 126 insertions(+), 152 deletions(-) diff --git a/tractor/runtime/_supervise.py b/tractor/runtime/_supervise.py index 020e3f34..32f5b9b5 100644 --- a/tractor/runtime/_supervise.py +++ b/tractor/runtime/_supervise.py @@ -199,7 +199,6 @@ class ActorNursery: self, # TODO: maybe def these as fields of a struct looking type? actor: Actor, - ria_nursery: trio.Nursery, da_nursery: trio.Nursery, errors: dict[tuple[str, str], BaseException], @@ -233,14 +232,12 @@ class ActorNursery: # and syncing purposes to any actor opened nurseries. self._implicit_runtime_started: bool = False - # TODO: remove the `.run_in_actor()` API and thus this 2ndary - # nursery when that API get's moved outside this primitive! - self._ria_nursery = ria_nursery - # TODO, factor this into a .hilevel api! # # portals spawned with ``run_in_actor()`` are - # cancelled when their "main" result arrives + # cancelled when their "main" result arrives. Reaped by + # `_reap_ria_portals()` at nursery-block exit now that + # the 2ndary `._ria_nursery` is gone (see issue #477). self._cancel_after_result_on_exit: set = set() # trio.Nursery-like cancel (request) statuses @@ -298,11 +295,6 @@ class ActorNursery: debug_mode: bool|None = None, infect_asyncio: bool = False, inherit_parent_main: bool = True, - - # TODO: ideally we can rm this once we no longer have - # a `._ria_nursery` since the dependent APIs have been - # removed! - nursery: trio.Nursery|None = None, proc_kwargs: dict[str, typing.Any] | None = None, ) -> Portal: @@ -364,10 +356,8 @@ class ActorNursery: # start a task to spawn a process # blocks until process has been started and a portal setup - nursery: trio.Nursery = nursery or self._da_nursery - # XXX: the type ignore is actually due to a `mypy` bug - return await nursery.start( # type: ignore + return await self._da_nursery.start( # type: ignore partial( _spawn.new_proc, name, @@ -639,161 +629,145 @@ async def _open_and_supervise_one_cancels_all_nursery( # the collection of errors retreived from spawned sub-actors errors: dict[tuple[str, str], BaseException] = {} - # This is the outermost level "deamon actor" nursery. It is awaited - # **after** the below inner "run in actor nursery". This allows for - # handling errors that are generated by the inner nursery in - # a supervisor strategy **before** blocking indefinitely to wait for - # actors spawned in "daemon mode" (aka started using - # `ActorNursery.start_actor()`). - - # errors from this daemon actor nursery bubble up to caller + # The single "daemon actor" nursery into which ALL subactors + # are spawned — both `.start_actor()` daemons AND + # `.run_in_actor()` one-shots. The latter's result-reaping now + # runs via `_reap_ria_portals()` at block-exit rather than a + # 2ndary `._ria_nursery` (see the #477 removal); errors from + # this nursery bubble up to the caller. async with ( collapse_eg(), trio.open_nursery() as da_nursery, ): + an = ActorNursery( + actor, + da_nursery, + errors + ) try: - # This is the inner level "run in actor" nursery. It is - # awaited first since actors spawned in this way (using - # `ActorNusery.run_in_actor()`) are expected to only - # return a single result and then complete (i.e. be canclled - # gracefully). Errors collected from these actors are - # immediately raised for handling by a supervisor strategy. - # As such if the strategy propagates any error(s) upwards - # the above "daemon actor" nursery will be notified. - async with ( - collapse_eg(), - trio.open_nursery() as ria_nursery, - ): - an = ActorNursery( - actor, - ria_nursery, - da_nursery, - errors + try: + # spawning of actors happens in the caller's scope + # after we yield upwards + yield an + + # When we didn't error in the caller's scope, + # signal all process-monitor-tasks to conduct + # the "hard join phase". + log.runtime( + 'Waiting on subactors to complete:\n' + f'>}} {len(an._children)}\n' ) - try: - # spawning of actors happens in the caller's scope - # after we yield upwards - yield an + an._join_procs.set() - # When we didn't error in the caller's scope, - # signal all process-monitor-tasks to conduct - # the "hard join phase". - log.runtime( - 'Waiting on subactors to complete:\n' - f'>}} {len(an._children)}\n' - ) - 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) - # 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: + inner_err = _inner_err + errors[actor.aid.uid] = inner_err - except BaseException as _inner_err: - inner_err = _inner_err - errors[actor.aid.uid] = inner_err + # If we error in the root but the debugger is + # engaged we don't want to prematurely kill (and + # thus clobber access to) the local tty since it + # will make the pdb repl unusable. + # Instead try to wait for pdb to be released before + # tearing down. + await debug.maybe_wait_for_debugger( + child_in_debug=an._at_least_one_child_in_debug + ) - # If we error in the root but the debugger is - # engaged we don't want to prematurely kill (and - # thus clobber access to) the local tty since it - # will make the pdb repl unusable. - # Instead try to wait for pdb to be released before - # tearing down. - await debug.maybe_wait_for_debugger( - child_in_debug=an._at_least_one_child_in_debug - ) + # if the caller's scope errored then we activate our + # one-cancels-all supervisor strategy (don't + # worry more are coming). + an._join_procs.set() - # if the caller's scope errored then we activate our - # one-cancels-all supervisor strategy (don't - # worry more are coming). - an._join_procs.set() + # XXX NOTE XXX: hypothetically an error could + # be raised and then a cancel signal shows up + # slightly after in which case the `else:` + # block here might not complete? For now, + # shield both. + with trio.CancelScope(shield=True): + etype: type = type(inner_err) + if etype in ( + trio.Cancelled, + KeyboardInterrupt, + ) or ( + is_multi_cancelled(inner_err) + ): + log.cancel( + f'Actor-nursery cancelled by {etype}\n\n' - # XXX NOTE XXX: hypothetically an error could - # be raised and then a cancel signal shows up - # slightly after in which case the `else:` - # block here might not complete? For now, - # shield both. - with trio.CancelScope(shield=True): - etype: type = type(inner_err) - if etype in ( - trio.Cancelled, - KeyboardInterrupt, - ) or ( - is_multi_cancelled(inner_err) - ): - log.cancel( - f'Actor-nursery cancelled by {etype}\n\n' + f'{current_actor().aid.uid}\n' + f' |_{an}\n\n' - f'{current_actor().aid.uid}\n' - f' |_{an}\n\n' + # TODO: show tb str? + # f'{tb_str}' + ) + elif etype in { + ContextCancelled, + }: + log.cancel( + 'Actor-nursery caught remote cancellation\n' + '\n' + f'{inner_err.tb_str}' + ) + else: + log.exception( + 'Nursery errored with:\n' - # TODO: show tb str? - # f'{tb_str}' - ) - elif etype in { - ContextCancelled, - }: - log.cancel( - 'Actor-nursery caught remote cancellation\n' - '\n' - f'{inner_err.tb_str}' - ) - else: - log.exception( - 'Nursery errored with:\n' + # TODO: same thing as in + # `._invoke()` to compute how to + # place this div-line in the + # middle of the above msg + # content.. + # -[ ] prolly helper-func it too + # in our `.log` module.. + # '------ - ------' + ) - # TODO: same thing as in - # `._invoke()` to compute how to - # place this div-line in the - # middle of the above msg - # content.. - # -[ ] prolly helper-func it too - # in our `.log` module.. - # '------ - ------' - ) + # 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 + await an.cancel() - # 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 - 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, + ) - # 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 - - # TODO: this is the handler around the ``.run_in_actor()`` - # nursery. Ideally we can drop this entirely in the future as - # the whole ``.run_in_actor()`` API should be built "on top of" - # this lower level spawn-request-cancel "daemon actor" API where - # a local in-actor task nursery is used with one-to-one task - # + `await Portal.run()` calls and the results/errors are - # handled directly (inline) and errors by the local nursery. + # Safety-net handler for anything escaping the inner + # `except BaseException` above (e.g. a `trio.Cancelled` + # during its non-shielded debugger-wait, or a failure + # inside the shielded teardown itself). Post-#477 the + # 2ndary `._ria_nursery` (and its dedicated handler) is + # gone; this now guards the single daemon nursery scope. + # TODO: can this be merged into the inner handler now that + # there's only one nursery? (higher-risk, own PR) except ( Exception, BaseExceptionGroup,