Compare commits

..

No commits in common. "9201a2ed5307dedddf4fa13b0d27fb770aa20483" and "5cd190c5c197cf935bbd32fc852ed5809a64a619" have entirely different histories.

4 changed files with 152 additions and 287 deletions

View File

@ -179,56 +179,6 @@ racing the 0.5s bound could drop an `errors` entry
`Portal._final_result_msg`/ctx queue state instead of `Portal._final_result_msg`/ctx queue state instead of
time-bounding. time-bounding.
## Step-B outcome (2026-07-02, done in tree)
Step A landed as `5cd190c5` (code) + `99310269` (docs).
Step B implemented on top (uncommitted):
- `._ria_nursery` is GONE — the inner
`async with (collapse_eg(), trio.open_nursery() as
ria_nursery)` layer in
`_open_and_supervise_one_cancels_all_nursery` is deleted;
`da_nursery` is now the single nursery for ALL subactors.
- `ActorNursery.__init__` drops the `ria_nursery` param +
the `self._ria_nursery` attr; `start_actor()` drops its
`nursery=` escape-hatch param (uses `self._da_nursery`
directly).
- `._cancel_after_result_on_exit` STAYS — it's the
ria-child discriminator for `_reap_ria_portals()`.
Deliberately NOT done (deferred to its own higher-risk PR,
flagged with a TODO at the outer `except`): merging the two
error handlers into one. Rationale — collapsing the empty
nursery is provably behavior-preserving (a zero-task
`trio.open_nursery()` only adds a checkpoint), whereas the
inner `except BaseException` (swallow-into-`errors`) and
outer `except (...)` (re-raise, safety-net for the inner
handler's own non-shielded awaits) have DIFFERENT
semantics; merging changes error/cancel propagation and
wants isolated review + its own gate. Both handlers are
kept, now nested directly under the single nursery.
Why the collapse is safe: post-step-A NOTHING spawns into
`ria_nursery` (its only reader, `run_in_actor`'s
`nursery=self._ria_nursery`, was removed in A; the stored
attr was never read again). So the layer was pure dead
weight.
Gate (trio backend, all 0-failure):
- targeted set (`test_cancellation test_spawning test_local
test_rpc test_to_actor`) = 49 passed, 1 xfailed.
- tail set (`test_reg_err_types remote_exc_relay
resource_cache ringbuf root_infect_asyncio root_runtime
runtime shm task_broadcasting trioisms trionics/`) = 63
passed, 1 skipped, 5 xfailed.
- full-suite head ~73% (subdirs + `test_2way`..`test_pubsub`)
= 303 passed before the known-flaky `test_dynamic_pub_sub`
TooSlowError stall (pre-existing; same hang in the step-A
full run). Suite ran slow this session (~13min vs 555s
cold, likely thermal from back-to-back runs), never
completing within an 800s bound — but split across the
above three runs EVERY module passed under step B.
## Verification gate ## Verification gate
- `tests/test_cancellation.py test_spawning.py test_local.py - `tests/test_cancellation.py test_spawning.py test_local.py

View File

@ -1,60 +0,0 @@
---
model: claude-opus-4-8
service: claude
session: c7860302-6a33-4d92-821c-0807d68be464
timestamp: 2026-07-02T17:22:33Z
git_ref: 5cd190c5
scope: code
substantive: true
raw_file: 20260702T172233Z_5cd190c5_prompt_io.raw.md
---
## Prompt
Branched session (`to_actor_subpkg_POST_WEEKLY_CUTOFF_2OPUS`,
now on Opus 4.8) continuing the `._ria_nursery` removal after
step A committed + pushed (`5cd190c5`/`99310269`):
> ok continue where your superior left off please
then, after the commit-split discussion:
> yup committed and pushed so take a look ma fren.
i.e. green-lighting step-B prep against the committed step-A
tree.
## Response summary
Step B of the `._ria_nursery` removal (issue #477): delete
the now-vestigial secondary nursery,
- collapse the inner `async with (collapse_eg(),
trio.open_nursery() as ria_nursery)` layer in
`_open_and_supervise_one_cancels_all_nursery``da_nursery`
becomes the single nursery for ALL subactors.
- `ActorNursery.__init__` drops the `ria_nursery` param + the
`self._ria_nursery` attr; `start_actor()` drops its
`nursery=` escape-hatch param.
- `._cancel_after_result_on_exit` kept (ria-child
discriminator for `_reap_ria_portals()`).
Verified behavior-preserving via a first-principles argument
(zero-task nursery = a bare checkpoint) + the targeted gate
(`test_cancellation test_spawning test_local test_rpc
test_to_actor` = 49 passed, 1 xfailed on trio). The two
error handlers were deliberately NOT merged — that changes
propagation semantics and is deferred to its own PR (TODO
left at the outer `except`).
## Files changed
- `tractor/runtime/_supervise.py` — collapse the ria nursery
layer + drop the ctor/`start_actor` params + refresh the
now-stale nursery comments
## Human edits
None yet — committed via the drafted
`.claude/git_commit_msg_ria_step_b.md` (user-driven
`git commit --edit`).

View File

@ -1,51 +0,0 @@
---
model: claude-opus-4-8
service: claude
timestamp: 2026-07-02T17:22:33Z
git_ref: 5cd190c5
diff_cmd: git diff 5cd190c5..wkt/to_actor_subpkg
---
# Raw AI output (diff-ref mode)
Step-B code lives on `wkt/to_actor_subpkg` after `5cd190c5`;
per diff-ref mode the verbatim content is reachable via the
pointer below.
## Generated files
> `git diff 5cd190c5..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
- `ActorNursery.__init__`: `ria_nursery` param removed;
`self._ria_nursery = ria_nursery` block deleted;
`_cancel_after_result_on_exit` comment refreshed.
- `start_actor()`: `nursery=` param removed; body uses
`self._da_nursery.start(...)` directly.
- `_open_and_supervise_one_cancels_all_nursery()`: the inner
`async with (collapse_eg(), trio.open_nursery() as
ria_nursery)` layer removed; `an = ActorNursery(actor,
da_nursery, errors)` constructed once under the single
`da_nursery`; the inner-try body de-indented one level;
both error handlers retained; the da-nursery lead comment
and the outer-`except` TODO refreshed to describe the
single-nursery reality + flag the (deferred) handler-merge.
> `git diff 5cd190c5..wkt/to_actor_subpkg -- ai/conc-anal/ria_nursery_removal_plan.md`
Added a "Step-B outcome" section (collapse rationale,
handler-merge deferral, safety argument, gate result).
## Test runs (verbatim)
```
targeted gate (trio):
tests/test_cancellation.py tests/test_spawning.py
tests/test_local.py tests/test_rpc.py tests/test_to_actor.py
-> 49 passed, 1 xfailed in 88.62s
signature checks:
ActorNursery.__init__ params: ['self', 'actor', 'da_nursery', 'errors']
start_actor has nursery param: False
full-suite gate: in progress (see stepB_full.log)
```

View File

@ -199,6 +199,7 @@ class ActorNursery:
self, self,
# TODO: maybe def these as fields of a struct looking type? # TODO: maybe def these as fields of a struct looking type?
actor: Actor, actor: Actor,
ria_nursery: trio.Nursery,
da_nursery: trio.Nursery, da_nursery: trio.Nursery,
errors: dict[tuple[str, str], BaseException], errors: dict[tuple[str, str], BaseException],
@ -232,12 +233,14 @@ class ActorNursery:
# and syncing purposes to any actor opened nurseries. # and syncing purposes to any actor opened nurseries.
self._implicit_runtime_started: bool = False 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! # TODO, factor this into a .hilevel api!
# #
# portals spawned with ``run_in_actor()`` are # portals spawned with ``run_in_actor()`` are
# cancelled when their "main" result arrives. Reaped by # cancelled when their "main" result arrives
# `_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() self._cancel_after_result_on_exit: set = set()
# trio.Nursery-like cancel (request) statuses # trio.Nursery-like cancel (request) statuses
@ -295,6 +298,11 @@ class ActorNursery:
debug_mode: bool|None = None, debug_mode: bool|None = None,
infect_asyncio: bool = False, infect_asyncio: bool = False,
inherit_parent_main: bool = True, 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, proc_kwargs: dict[str, typing.Any] | None = None,
) -> Portal: ) -> Portal:
@ -356,8 +364,10 @@ class ActorNursery:
# start a task to spawn a process # start a task to spawn a process
# blocks until process has been started and a portal setup # 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 # XXX: the type ignore is actually due to a `mypy` bug
return await self._da_nursery.start( # type: ignore return await nursery.start( # type: ignore
partial( partial(
_spawn.new_proc, _spawn.new_proc,
name, name,
@ -629,145 +639,161 @@ async def _open_and_supervise_one_cancels_all_nursery(
# the collection of errors retreived from spawned sub-actors # the collection of errors retreived from spawned sub-actors
errors: dict[tuple[str, str], BaseException] = {} errors: dict[tuple[str, str], BaseException] = {}
# The single "daemon actor" nursery into which ALL subactors # This is the outermost level "deamon actor" nursery. It is awaited
# are spawned — both `.start_actor()` daemons AND # **after** the below inner "run in actor nursery". This allows for
# `.run_in_actor()` one-shots. The latter's result-reaping now # handling errors that are generated by the inner nursery in
# runs via `_reap_ria_portals()` at block-exit rather than a # a supervisor strategy **before** blocking indefinitely to wait for
# 2ndary `._ria_nursery` (see the #477 removal); errors from # actors spawned in "daemon mode" (aka started using
# this nursery bubble up to the caller. # `ActorNursery.start_actor()`).
# errors from this daemon actor nursery bubble up to caller
async with ( async with (
collapse_eg(), collapse_eg(),
trio.open_nursery() as da_nursery, trio.open_nursery() as da_nursery,
): ):
an = ActorNursery(
actor,
da_nursery,
errors
)
try: try:
try: # This is the inner level "run in actor" nursery. It is
# spawning of actors happens in the caller's scope # awaited first since actors spawned in this way (using
# after we yield upwards # `ActorNusery.run_in_actor()`) are expected to only
yield an # return a single result and then complete (i.e. be canclled
# gracefully). Errors collected from these actors are
# When we didn't error in the caller's scope, # immediately raised for handling by a supervisor strategy.
# signal all process-monitor-tasks to conduct # As such if the strategy propagates any error(s) upwards
# the "hard join phase". # the above "daemon actor" nursery will be notified.
log.runtime( async with (
'Waiting on subactors to complete:\n' collapse_eg(),
f'>}} {len(an._children)}\n' trio.open_nursery() as ria_nursery,
):
an = ActorNursery(
actor,
ria_nursery,
da_nursery,
errors
) )
an._join_procs.set() try:
# spawning of actors happens in the caller's scope
# after we yield upwards
yield an
# collect results (and errors) from all # When we didn't error in the caller's scope,
# `.run_in_actor()` children then cancel # signal all process-monitor-tasks to conduct
# each, one reaper task per child. # the "hard join phase".
await _reap_ria_portals(an, errors) log.runtime(
'Waiting on subactors to complete:\n'
f'>}} {len(an._children)}\n'
)
an._join_procs.set()
except BaseException as _inner_err: # collect results (and errors) from all
inner_err = _inner_err # `.run_in_actor()` children then cancel
errors[actor.aid.uid] = inner_err # each, one reaper task per child.
await _reap_ria_portals(an, errors)
# If we error in the root but the debugger is except BaseException as _inner_err:
# engaged we don't want to prematurely kill (and inner_err = _inner_err
# thus clobber access to) the local tty since it errors[actor.aid.uid] = inner_err
# 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 # If we error in the root but the debugger is
# one-cancels-all supervisor strategy (don't # engaged we don't want to prematurely kill (and
# worry more are coming). # thus clobber access to) the local tty since it
an._join_procs.set() # 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
)
# XXX NOTE XXX: hypothetically an error could # if the caller's scope errored then we activate our
# be raised and then a cancel signal shows up # one-cancels-all supervisor strategy (don't
# slightly after in which case the `else:` # worry more are coming).
# block here might not complete? For now, an._join_procs.set()
# 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' # XXX NOTE XXX: hypothetically an error could
f' |_{an}\n\n' # 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'
# TODO: show tb str? f'{current_actor().aid.uid}\n'
# f'{tb_str}' f' |_{an}\n\n'
)
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 # TODO: show tb str?
# `._invoke()` to compute how to # f'{tb_str}'
# place this div-line in the )
# middle of the above msg elif etype in {
# content.. ContextCancelled,
# -[ ] prolly helper-func it too }:
# in our `.log` module.. log.cancel(
# '------ - ------' 'Actor-nursery caught remote cancellation\n'
) '\n'
f'{inner_err.tb_str}'
)
else:
log.exception(
'Nursery errored with:\n'
# snapshot `.run_in_actor()` children # TODO: same thing as in
# BEFORE cancelling: each backend # `._invoke()` to compute how to
# spawn-task pops its `._children` # place this div-line in the
# entry as the proc gets reaped. # middle of the above msg
ria_children: list = [ # content..
(portal, subactor) # -[ ] prolly helper-func it too
for subactor, _, portal # in our `.log` module..
in an._children.values() # '------ - ------'
if portal in )
an._cancel_after_result_on_exit
]
# cancel all subactors
await an.cancel()
# then collect any already-relayed # snapshot `.run_in_actor()` children
# results/errors from ria children. # BEFORE cancelling: each backend
# Tightly bounded: anything # spawn-task pops its `._children`
# collectable is already queued in # entry as the proc gets reaped.
# the local ctx (relayed BEFORE the ria_children: list = [
# cancel above); a child hard-killed (portal, subactor)
# without relaying just parks its for subactor, _, portal
# reaper which then self-cleans (a in an._children.values()
# `trio.Cancelled` result is never if portal in
# stashed), mirroring the old an._cancel_after_result_on_exit
# backend-side reaper-vs-`soft_kill` ]
# cancel race. # cancel all subactors
with trio.move_on_after(0.5): await an.cancel()
await _reap_ria_portals(
an,
errors,
ria_children=ria_children,
)
# Safety-net handler for anything escaping the inner # then collect any already-relayed
# `except BaseException` above (e.g. a `trio.Cancelled` # results/errors from ria children.
# during its non-shielded debugger-wait, or a failure # Tightly bounded: anything
# inside the shielded teardown itself). Post-#477 the # collectable is already queued in
# 2ndary `._ria_nursery` (and its dedicated handler) is # the local ctx (relayed BEFORE the
# gone; this now guards the single daemon nursery scope. # cancel above); a child hard-killed
# TODO: can this be merged into the inner handler now that # without relaying just parks its
# there's only one nursery? (higher-risk, own PR) # 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.
except ( except (
Exception, Exception,
BaseExceptionGroup, BaseExceptionGroup,