Skip to content

Drop run_in_actor() + the ria reap-cluster#484

Open
goodboy wants to merge 28 commits into
wkt/to_actor_subpkgfrom
drop_ria_nursery
Open

Drop run_in_actor() + the ria reap-cluster#484
goodboy wants to merge 28 commits into
wkt/to_actor_subpkgfrom
drop_ria_nursery

Conversation

@goodboy

@goodboy goodboy commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Drop run_in_actor() + the ria reap-cluster

Motivation

The legacy non-blocking ActorNursery.run_in_actor() predates our
streaming support and always sat awkwardly across multiple runtime
layers: it returned a Portal whose "main result" was owned by
invisible machinery (._expect_result_ctx + a teardown reap-task
raced against soft_kill() inside each spawn backend) and reaped
into the nursery's error-collection only at block exit. Issue #477
prescribed decoupling one-shot invocation from the nursery internals;
the sibling PR #481 (this PR's base) delivered the blocking successor
tractor.to_actor.run().

Attempting to keep the legacy API during the nursery-machinery
simplification exposed why it had to go: hoisting the backend-local
reapers up into _supervise dropped the reaper-vs-proc-death race
and produced an unbounded wait_for_result() hang whenever a
one-shot child was cancelled out-of-band. The correct scoping — a
result-wait living in the caller's task inside its own cancel-scope
— is exactly what to_actor.run() already is, so instead of bolting
a race back on we migrate every in-repo caller and remove the API
(and its whole reap cluster) outright.


Src of research


Summary of changes

  • simplify the nursery/spawn machinery in 3 gated steps: hoist
    ria-reaping out of the spawn backends, delete the vestigial 2ndary
    ._ria_nursery layer (a single da_nursery now hosts ALL
    subactors), and merge the two nested scope-error handlers into one
    (the outer's raise was provably dead).
  • add a red-first anti-hang guard: test_tractor_cancels_aio wrapped
    in trio.fail_after() so the (since dissolved) unbounded-reap hang
    class fails fast instead of wedging the suite.
  • migrate every in-repo run_in_actor() caller (one commit per
    suite/group for bisectability): 9 test modules, all 12 examples
    (incl. the 8 pexpect-driven debugging/ REPL scripts), 8 docs
    pages + the experimental/_pubsub docstring. Recurring conversion
    patterns,
    • blocking result -> to_actor.run(fn, an=an, ...).
    • fire-and-forget/forever -> bg to_actor.run() task in a local
      trio task-nursery (or start_actor() + bg Portal.run() when
      a portal handle is needed).
    • fan-out -> N bg one-shots or
      gather_contexts([p.open_context(..)]).
    • reap-all error collection -> the "collect don't cancel" pattern
      (see examples/debugging/multi_subactors.py).
  • loosen group-shape assertions where the teardown-reap-all
    BEG-of-N semantic died: local task-nurseries are cancel-on-first,
    raced siblings' trio.Cancelleds are absorbed, and the runtime's
    collapse_eg() unwraps every single-member group at each actor
    boundary — a fully-raced nested tree relays a bare (annotated)
    RemoteActorError chain. test_multierror_fast_nursery (pure
    reap-stress) is deleted; test_nested_multierrors is re-purposed
    as deep-tree cancel-cascade stress with a race-tolerant shape walk.
  • excise the API + cluster (2a59cefb, net -402 lines):
    run_in_actor(), ._cancel_after_result_on_exit,
    _reap_ria_portals(), Portal._submit_for_result()/
    ._expect_result_ctx/.wait_for_result()/deprecated .result(),
    exhaust_portal(), cancel_on_completion() and the NoResult
    sentinel. NB Context.wait_for_result() is a different (very
    alive) API and is untouched.
  • fix a mutual-rendezvous race the migration itself surfaced
    (a297a32a): paired one-shots that dial each other
    flaked since the winner is reaped while the loser still dials its
    registry-resolved (dead) sockaddr — such peers now pin lifetimes
    via start_actor() daemons + concurrent Portal.run()s + explicit
    an.cancel().
  • misc: fix an UnboundLocalError (timeout match lacked a
    non-trio/MTF default arm), fix the ancient @pub docstring
    example, and sweep all test/example ActorNursery bindings to the
    an naming convention.

Gates: full suite on trio = 413 passed, 0 failures, 0 hangs;
tests/devx/test_debugger.py byte-identical to the pre-migration
baseline (28 passed, 6 skipped); mp_spawn module-level gates green
throughout. (The full-session mp_spawn run mass-fails identically
at this PR's base — A/B-probed pre-existing and unexercised by CI's
trio-only matrix.)


TODOs before landing

  • CI matrix green on the final tip (all legs: ubuntu
    tcp+uds, macos tcp, sdist, sphinx).
  • recommend a -x-less full-suite pass on a normal-speed box if
    local runs looked thermally throttled (see the plan doc's B2
    notes).

Future follow up

Links

(this pr content was generated in some part by claude-code)

goodboy added 16 commits July 2, 2026 13:11
Split from the step-A code commit to keep the runtime diff
free of `ai/` meta noise,

- `ai/conc-anal/ria_nursery_removal_plan.md`: agent-verified
  machinery map + 3-step (A/B/C) design + probe history
  (reap-relocation deadlock -> sequencing fix -> bound
  tighten) + risk register for the `._ria_nursery` excision.
- `ai/prompt-io/claude/20260702T165806Z_a34aaf98_*`: NLNet
  provenance (log + unedited raw) for the step-A work.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
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-code
Split from the step-B code commit to keep the runtime diff
free of `ai/` meta noise,

- `ai/conc-anal/ria_nursery_removal_plan.md`: add a "Step-B
  outcome" section — the empty-nursery collapse, why it's
  behavior-preserving, the deliberate handler-merge deferral,
  and the targeted-gate result.
- `ai/prompt-io/claude/20260702T172233Z_5cd190c5_*`: NLNet
  provenance (log + unedited raw) for the step-B work.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
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
Split from the step-B2 code commit to keep the runtime diff
free of `ai/` meta noise,

- `ai/conc-anal/ria_nursery_removal_plan.md`: add a "Step-B2
  outcome" section — the dead-outer-`raise` trace, why the
  merge is behavior-preserving, and the gate results.
- `ai/prompt-io/claude/20260702T222544Z_9201a2ed_*`: NLNet
  provenance (log + unedited raw) for the step-B2 work.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Step B2 of the `._ria_nursery` removal (issue #477; see
`ai/conc-anal/ria_nursery_removal_plan.md`). With the 2ndary
nursery gone (step B), the two nested error handlers in
`_open_and_supervise_one_cancels_all_nursery` collapse to one,

- the outer `except (Exception, BaseExceptionGroup,
  trio.Cancelled)` existed to catch errors bubbling from the
  old `._ria_nursery.__aexit__` reaper-group; that nursery no
  longer exists.
- trace shows the outer handler's `raise` was already DEAD: the
  inner handler records `errors[uid]` as its first action, so
  `errors` is always non-empty by the time anything could reach
  the outer handler, and the `finally`'s raise-from-`errors`
  always superseded the outer `raise`.
- so fold both into a single `except BaseException as
  _scope_err` guarding the lone daemon nursery; the `finally`
  (unchanged) still raises the collected `errors` as a single
  exc or `BaseExceptionGroup`.
- drop the now-unused `outer_err`/`inner_err` locals.

Behaviour-preserving (net ~30 lines lighter); the big diff is
the one-level de-indent of the handler body. The two remaining
`maybe_wait_for_debugger()` guards collapse to the single
pre-teardown wait.

Prompt-IO: ai/prompt-io/claude/20260702T222544Z_9201a2ed_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
Wrap `test_tractor_cancels_aio`'s `main()` in a
`trio.fail_after(9 * cpu_perf_headroom())` so a wedged remote
runtime can't hang the test forever. This is the blessed
anti-hang guard here bc `pytest-timeout`'s global cap is
intentionally off (it breaks `trio` under the fork backends,
per the `pyproject` NOTE).

The cap is generous + CPU-headroom-scaled bc it's an anti-hang
guard, not a perf assertion. Motivated by the
`._ria_nursery`-removal regression where a wedged ria-reaper
once hung this exact test indefinitely.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
First test-file of the #477 `.run_in_actor()` removal (blocking
`to_actor.run()` is the successor; the legacy non-blocking one-shot
is dropped, not replaced). All 9 call-sites migrated,

- blocking result/error/streaming-result tests -> `to_actor.run(fn,
  an=an, ...)`; the "streaming" ones stream aio<->trio INSIDE the
  subactor so the caller only awaits the final result.
- forever-task + cancel tests (`test_tractor_cancels_aio`,
  `test_trio_cancels_aio`) -> `start_actor()` +
  `Portal.open_context()` + cancel — can't block on a
  never-returning task. Adds a small `sleep_forever_aio_ctx`
  `@context` shim.
- greens the red `test_tractor_cancels_aio` anti-hang guard from
  the prior commit: under the correctly-scoped API the wait is
  bounded by the caller's cancel scope, so the hang is structurally
  gone — not patched.

Suite: 34 passed, 2 xfailed (trio backend).

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Sole call-site: an inlined `run_in_actor(...).result()` ->
blocking `tractor.to_actor.run(fn, an=an, ...)` (#477 removal).
Behaviour identical — the one-shot's result/error is awaited
in the caller's task rather than reaped at nursery teardown;
the enclosing `move_on_after` still cancels the sub in the
`error_in_child=False` case.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Sole call-site: `run_in_actor(sleep_back_actor, ...)` ->
blocking `tractor.to_actor.run(..., an=n, ...)` (#477 removal).
The RPC-callback subactor is awaited in-caller instead of
reaped at nursery teardown; `name=`/`enable_modules=` map to
`to_actor.run()`'s same-named params, the rest to `**fn_kwargs`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Migrate all 4 sites to blocking `tractor.to_actor.run()` (#477
removal),

- rename the two API-named tests to `test_to_actor_run_*`
  (`same_func_in_child`, `can_skip_parent_main_inheritance`) —
  they exercise the same spawn / `inherit_parent_main` path via
  the successor API.
- the recursive `spawn()` helper drops its white-box
  `an._children` / portal-`_peers` asserts (which probed
  `run_in_actor`'s portal + nursery-tracking internals);
  `to_actor.run()` returns the result and reaps internally, so
  keep the user-facing `result == 10` check.
- `test_most_beautiful_word` drops the 2nd `wait_for_result()`
  (the legacy result-cache re-fetch) — `to_actor.run()` delivers
  the value once, no cache.

Suite: 9 passed.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
`test_multi_actor_subs_arbiter_pub` used `run_in_actor()` to spawn
two forever-ish subscriber actors and hold their portals for a
later `cancel_actor()` (its `.result()` was commented out exactly
because `subs()` never cleanly returns). That deferred-spawn +
cancel shape isn't a blocking `to_actor.run()`, so convert to the
successor primitives (#477 removal),

- `start_actor()` per subscriber — keeps the portal for the
  existing `cancel_actor()` teardown,
- run `subs()` on each via a background `Portal.run()` task in a
  local `trio` nursery so both subscribe concurrently with the
  test's `wait_for_actor` / topic checks,
- each bg runner swallows the `RemoteActorError`/`ContextCancelled`
  that `cancel_actor()` relays; a trailing `tn.cancel_scope.cancel()`
  drops any lingering runner.

Suite: 8 passed.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Two sites migrated (#477 removal),

- `test_trynamic_trio`: donny + gretchen each wait on the *other*
  to register, so they must run CONCURRENTLY — was two
  non-blocking `run_in_actor()`s awaited after; now two
  `to_actor.run()` one-shots scheduled into a local `trio`
  task-nursery.
- the unregister-on-cancel cluster test: its non-streaming branch
  spawned `run_in_actor(trio.sleep_forever)` purely to keep each
  subactor alive + registered — a `start_actor()` daemon does that
  without a "main" task, so the spawn loop collapses to the same
  `start_actor()` the streaming branch already used.

Suite: 16 passed.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
First group of the `test_cancellation.py` `run_in_actor` removal
(#477),

- `test_remote_error` -> blocking `to_actor.run()` (single erroring
  one-shot; a bad-arg `TypeError` still relays as a
  `RemoteActorError`).
- `test_multierror` -> concurrent fan-out via
  `gather_contexts([p.open_context(assert_err_ctx) ...])` over
  `start_actor()` portals. NB `gather_contexts` is cancel-on-first
  so the 2nd errorer is usually cancelled before relaying its own
  exc and the pair collapses to a single `RemoteActorError` (vs the
  legacy reap-all-at-teardown `BEG`-of-N) — the assertion now
  accepts either shape.
- delete `test_multierror_fast_nursery` — a 25-actor stress test of
  `run_in_actor`'s teardown-reap; no analogous surface under the
  `to_actor` fan-out.
- add an `assert_err_ctx` `@context` shim for the `open_context`
  fan-out.

Remaining `test_cancellation` groups (some_cancels_all, nested,
SIGINT, sync-blocking) still on `run_in_actor` — ported next.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Append two sections to the ria-removal plan capturing the
2026-07-02 hang episode + the resulting design pivot.

Regression writeup: the full-suite hang on
`test_tractor_cancels_aio` root-caused to the step-A reaper
hoist (`5cd190c5`), not the B2 handler merge. The happy-path
`_reap_ria_portals()` parks unbounded on `wait_for_result()`
after a user `portal.cancel_actor()`; the old spawn-backend
reaper raced `soft_kill()`'s scope-cancel, the hoist dropped
it. Records the `proc.poll()` death-watch fix + why poll (not
the event `wait_func`) bc `soft_kill` already awaits
`proc.sentinel` (a 2nd `wait_readable` -> `BusyResourceError`).

Pause writeup: user's insight that the hoist landed in the
wrong scope — result-waiting belongs in the `to_actor`
one-shot scope (`_invoke_in_subactor()`), beside `an` + a
local task-nursery + cancel-scope, where bounding the wait is
trivial + the hang dissolves. So the poll fix is likely
SUPERSEDED (flagged do-not-land); the anti-hang guard commit
(`d1fb4a1a`) stays red-first per the failing-test convention.

(this patch was generated in some part by `claude-code` using
`claude-opus-4-8` (`anthropic`))
Second `test_cancellation.py` group of the `run_in_actor` removal
(#477),

- one-shot subactors now run as concurrent `to_actor.run(fn,
  an=an)` tasks in a local `trio` task-nursery, so their errors
  raise WHILE the actor-nursery block is open (vs the legacy
  teardown-reap) and the first error cancels sibling one-shots.
- wrap the task-nursery in `collapse_eg()` so the deterministic
  single-error cases still surface a bare `RemoteActorError`.
- loosen the group-shape assertion: the relay-vs-cancel race
  populates anywhere from 1 to `num_actors` `RemoteActorError`s
  (the exact-`num_actors` BEG was `run_in_actor`'s
  reap-all-at-teardown); group members are always
  `RemoteActorError` now since sibling `trio.Cancelled`s are
  absorbed by the task-nursery.
- move the daemon-portal call loop inside the task-nursery body
  so the sleep-forever one-shot case is cancelled by the daemon
  error raise.
- rename the `*run_in_actor*` param ids to `*one_shot*`.

Gate: 6 passed on both `trio` + `mp_spawn` backends.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
goodboy added 12 commits July 6, 2026 11:40
`test_nested_multierrors`'s backend/depth budget `match` only
carries arms for the `trio` + `main_thread_forkserver` spawn
backends, so running under any other (e.g. `mp_spawn`) leaves
`timeout` unbound and crashes with an `UnboundLocalError` at the
headroom-scaling below. Add default per-depth arms riding the MTF
budgets (same per-spawn round-trip cost class).

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Third `test_cancellation.py` group of the `run_in_actor` removal
(#477),

- `spawn_and_error` fans out each level's erroring one-shots as
  concurrent `to_actor.run(fn, an=an)` tasks in a local `trio`
  task-nursery (recursing per spawner subactor), as does the
  test-body's top-level spawner loop.
- the deterministic exact-breadth nested-BEG shape dies with the
  legacy teardown-reap: each level now groups whatever subset of
  sub-tree errors relay before the first one's cancel wins, and
  a single-member group gets unwrapped by the runtime's own
  `collapse_eg()` at every actor boundary — so a fully-raced
  tree relays a bare `RemoteActorError` chain.
- loosen the shape walk accordingly: accept a lone
  `RemoteActorError` or a 1..breadth group whose members box
  `ExceptionGroup` (multi-relay), `AssertionError` (collapsed
  leaf chain), `RemoteActorError` (re-boxed collapsed chain) or
  `BaseExceptionGroup` (runtime reap-deadline `Cancelled`
  upgrade); fold the windows-only tolerances into the same walk.
- raced sibling `trio.Cancelled`s are now ABSORBED by the
  task-nursery instead of landing in the group, so the MTF
  shape-mismatch xfail should consistently xpass — note added to
  drop the marker once CI confirms.
- add an `else: pytest.fail()` so a silently-clean tree can no
  longer pass.

Gate: both depths green on `trio` (10 consecutive runs) +
`mp_spawn`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Final `test_cancellation.py` group of the `run_in_actor` removal
(#477) — cancel-mechanics tests, so clean conversions,

- `test_cancel_via_SIGINT_other_task`: the 3 keep-alive
  `run_in_actor(sleep_forever)` one-shots become plain
  `start_actor()` daemons (an idle daemon needs no "main" task,
  and no longer shares a single dup'd `namesucka` name).
- `spawn_sub_with_sync_blocking_task`: the middle layer's spawn
  becomes a blocking `to_actor.run(spin_for, an=an)` which parks
  awaiting the sync-sleeping grandchild's result until cancelled
  from above.
- `test_cancel_while_childs_child_in_sync_sleep`: the
  fire-and-forget middle-actor spawn becomes a bg
  `to_actor.run()` task in a local task-nursery; the root's
  `assert 0` cancels it, driving the same
  graceful-cancel-then-zombie-reap cascade on the sync-blocked
  grandchild. The `man_cancel_outer` xfail param is unchanged.

Zero live `run_in_actor()` call-sites remain in this suite.

Gate: full `test_cancellation.py` module green on both `trio`
(18p/1xf) + `mp_spawn` (18p/1xf).

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The known-flaky dynamic pubsub test's 3 fire-and-forget spawn
sites (#477 removal),

- the forever-streaming `publisher` + N `consumer` one-shots now
  bg-schedule as `to_actor.run(fn, an=n)` tasks in a local `trio`
  task-nursery (`publisher`'s rendezvous name still derives from
  `fn.__name__`).
- the simulated user-cancel raise (`KeyboardInterrupt` /
  `TooSlowError` params) cancels the task-nursery, each one-shot
  reaping its subactor via `to_actor.run()`'s shielded
  `Portal.cancel_actor()`; `_run_and_match()`'s existing
  `BaseExceptionGroup.split()` walk covers the (possibly nested)
  relay shapes unchanged.
- spawns now issue concurrently rather than sequentially —
  comment on the fork-backend budget updated to match.

Gate: both params x4 runs green on `trio` + x1 on `mp_spawn`;
full module green.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
4 example scripts of the #477 removal sweep, each exercised by
`test_docs_examples.py`,

- `actor_spawning_and_causality.py`: the simplest possible
  `to_actor.run()` demo — private call-scoped nursery, block on
  and print the one-shot's result.
- `remote_error_propagation.py`: blocking `to_actor.run(an=n)`
  raises the boxed `AssertionError` in the caller's task,
  cancelling the sibling daemons.
- `parallelism/single_func.py`: bg-burn a core in the parent via
  a local task-nursery while the one-shot burns (and returns
  from) a subactor.
- `a_trynamic_first_scene.py`: donny + gretchen wait on each
  *other* so their one-shots run concurrently in a local
  task-nursery against a shared `an` (mirrors the migrated
  `test_trynamic_trio`).

Gate: all 4 green via the example-runner suite.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The 8 `examples/debugging/` scripts driven by the pexpect'd
`test_debugger.py` REPL-flows (#477 removal),

- blocking one-shots (`subactor_error`, `subactor_breakpoint`,
  `shielded_pause`): straight `to_actor.run(fn, an=an)` — the
  boxed error/`BdbQuit` raises in the root's task.
- `multi_subactors`: introduces the "collect all errors" pattern
  — each one-shot catches + stashes its `RemoteActorError` (vs
  raising) so no child's crash cancels its siblings before
  they've had their own REPL sessions, then a
  `BaseExceptionGroup` of the lot raises at the end; preserves
  the legacy teardown-reap REPL flow exactly (28/28 debugger
  suite unchanged).
- `multi_nested_subactors_error_up_through_nurseries` +
  `root_cancelled_but_child_is_in_tty_lock`: recursive
  `spawn_until` levels each block on their one-shot child; the
  parallel spawner-trees run as bg task-nursery one-shots where
  the first tree's error cancels the other.
- `multi_subactor_root_errors` +
  `root_timeout_while_child_crashed`: `start_actor()` + bg
  `Portal.run()` tasks so the root's own error/timeout races the
  already-crashed children, same as before.
- `sync_bp`: TODO-comment x-ref update only.

`test_debugger.py`: the nested-nurseries test's final-output
patterns update to the new relay shape — the LAST-released leaf
REPL's error chain wins each level's relay-vs-cancel race and
relays as a `collapse_eg()`-annotated collapsed chain, while the
sibling tree is cancelled + absorbed. (The legacy teardown-reap
grouped BOTH the `name_error` and bp-quit chains — explaining
the previously-mysterious "extra" `src_uid`/`relay_uid` patterns
noted in the old TODO.)

Gate: `tests/devx/test_debugger.py` = 28 passed, 6 skipped —
identical to the pre-migration baseline.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The 8-page docs sweep of the #477 removal, ahead of the API's
excision,

- `start/quickstart.rst`: the first-actor-tree walkthrough now
  narrates the (migrated) `to_actor.run()` example — no portal
  in hand until the daemon section introduces `start_actor()`.
- `guide/spawning.rst`: the one-shot section becomes
  `to_actor.run()` (blocking call, placement opts, "built on the
  primitives" note); lifetime/teardown rules update — one-shots
  never make it to nursery exit since each is reaped inside its
  own call.
- `guide/rpc.rst`: the `wait_for_result()` section (an API that
  dies with the reap cluster, incl. the `NoResult` sentinel)
  becomes a `to_actor.run()` one-shot section.
- `api/core.rst`: drop `run_in_actor`/`wait_for_result` from the
  autodoc member lists, drop the `Portal.result()` deprecation
  note, add a "One-shot task actors" `tractor.to_actor.run`
  autodoc section.
- `guide/{asyncio,context,cancellation,parallelism}.rst`:
  mention swaps to the successor API.

Gate: `make -C docs html` builds clean; `to_actor.run` autodoc
renders in `api/core.html`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The `_pubsub.pub` decorator's usage example predates several API
generations: ancient positional-arg-order `run_in_actor()` (a
missing `await` too) plus the deprecated `portal.result()` — and
`run_in_actor()` never allowed streaming funcs anyway. Show the
canonical `start_actor()` + `Portal.open_stream_from()`
consumption instead (#477 removal sweep).

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,

- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
  `._cancel_after_result_on_exit` portal-set and the
  `_reap_ria_portals()` teardown-reaper (both its happy-path
  block-exit call AND the error-path snapshot + 0.5s-bounded
  collection) are deleted — one-shot result-waiting now lives
  entirely in the caller's task via `to_actor.run()`, whose
  enclosing cancel-scope bounds the wait by construction (the
  correct-scoping fix for the unbounded-reap hang class; the
  `d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
  `._expect_result_ctx`, `._final_result_msg/_pld`,
  `.wait_for_result()` + the deprecated `.result()` alias are
  gone — a `Portal` no longer has any "main result" notion.
  NB `Context.wait_for_result()` is a different (very alive)
  API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
  `cancel_on_completion()` (the reaper tasks) deleted; backend
  comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
  reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
  call that was already a warn + `NoResult` no-op (the ctx-acm
  exit does the real result-wait); unshadow the 2nd `sctx` as
  `rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
  `_context.py`, `to_actor/`, `tests/test_to_actor.py`.

Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
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
Convention sweep (user req): all `tractor.open_nursery()`
bindings in test + example code use `an: ActorNursery` (`n`,
`nursery` + several tractor-nurseries confusingly named `tn`
are renamed); `trio.open_nursery()` bindings stay `tn` (incl.
`concurrent_actors_primes.py`'s inner trio nursery, renamed
`n` -> `tn` to match).

Purely mechanical, function-scoped renames — prose "nursery"/
"an" in docstrings/comments untouched; func-arg kwargs like
`portal.run(func, n=value)` untouched.

Gate: renamed test modules green on `trio`; full debugger suite
(28p/6s) + example-runner (21p) green.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Fold the endeavour's resolution into the plan doc + log the
session per prompt-io policy,

- `ria_nursery_removal_plan.md`: RESOLVED section — migrate
  everything, remove the API; the migration-pattern table
  (blocking / fire-and-forget / fan-out / collect-don't-cancel
  / mutual-rendezvous), the semantic deltas (cancel-on-first +
  `collapse_eg()` chain collapse vs the old teardown-reap BEG),
  the excision inventory and the structural dissolution of the
  reap-hang class.
- adds the `to_actor.open_one_shot()` follow-up sketch: an
  `@acm` + private task-nursery over the existing blocking
  `run()` — done-`trio.Event` as a result memo (NOT a
  cancel-relay), no `Portal` in the iface, errors always
  propagate at scope exit; zero `_supervise` coupling.
- prompt-io entry `20260706T172818Z_ad42871e` (+ raw diff-ref
  companion) covering commits `d01a2123..ad42871`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
@goodboy goodboy changed the title Drop ria nursery Drop run_in_actor() + the ria reap-cluster Jul 7, 2026
@goodboy goodboy marked this pull request as ready for review July 8, 2026 01:12
Copilot AI review requested due to automatic review settings July 8, 2026 01:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Removes the legacy non-blocking ActorNursery.run_in_actor() API and its associated “result reaping” machinery, migrating the codebase to the newer tractor.to_actor.run() one-shot API and simplifying the runtime/supervision + spawn backends accordingly.

Changes:

  • Excise run_in_actor() and the portal “main-result” cluster (Portal.wait_for_result() / deprecated Portal.result() / related sentinels & helpers).
  • Simplify actor supervision/spawn paths to use a single daemon nursery for all subactors; drop backend-local reaper logic.
  • Migrate in-repo tests/examples/docs to to_actor.run() or explicit start_actor() + Portal.run() patterns.

Reviewed changes

Copilot reviewed 63 out of 63 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tractor/to_actor/_api.py Refresh docs to reflect removal of legacy run_in_actor().
tractor/to_actor/init.py Update package docstring to describe to_actor as the replacement API.
tractor/spawn/_trio.py Remove run_in_actor()-specific reaper branch; rely on standard reap/kill flow.
tractor/spawn/_spawn.py Delete cancel_on_completion() / exhaust_portal() helpers and update backend import note.
tractor/spawn/_mp.py Mirror trio backend changes: remove run_in_actor()-specific reaper branch.
tractor/runtime/_supervise.py Remove run_in_actor() and the secondary nursery layer; unify supervision around one nursery.
tractor/runtime/_portal.py Remove portal “main result” machinery (wait_for_result, deprecated result, etc.).
tractor/msg/types.py Update message-type doc references to to_actor.run().
tractor/experimental/_pubsub.py Update pubsub docstring example to use start_actor() + streaming context manager.
tractor/_exceptions.py Remove NoResult sentinel (no longer needed without portal final-result API).
tractor/_context.py Update comments referencing legacy APIs to to_actor.run().
tests/test_to_actor.py Update test module docs to reflect legacy removal.
tests/test_spawning.py Migrate spawn recursion and one-shot invocations to to_actor.run().
tests/test_runtime.py Migrate one-shot usage to to_actor.run().
tests/test_rpc.py Migrate one-shot usage to to_actor.run() with an= placement.
tests/test_ringbuf.py Remove no-op portal .result() usage; rely on context manager teardown semantics.
tests/test_pubsub.py Replace legacy one-shot usage with daemon actors + background Portal.run() tasks.
tests/test_inter_peer_cancellation.py Naming sweep (nan) for ActorNursery bindings.
tests/test_infected_asyncio.py Migrate one-shot usage; add context shim for infected-asyncio “forever” cancellation patterns.
tests/test_child_manages_service_nursery.py Naming sweep (nan) for ActorNursery bindings.
tests/test_cancellation.py Migrate fan-out/error tests to to_actor.run() + local task nurseries / gather_contexts().
tests/test_advanced_streaming.py Migrate “forever streaming” patterns to bg one-shots via local task nurseries.
tests/discovery/test_registrar.py Naming sweep (nan) and migrate lingering patterns away from legacy APIs.
tests/devx/test_debugger.py Update expected patterns to match new error-group/collapse behavior.
examples/rpc_bidir_streaming.py Naming sweep (nan).
examples/remote_error_propagation.py Migrate one-shot example to to_actor.run().
examples/parallelism/single_func.py Demonstrate local+remote concurrency via trio.open_nursery() + to_actor.run().
examples/parallelism/concurrent_actors_primes.py Naming sweep (tnan) and minor nursery variable cleanup.
examples/integration/open_context_and_sleep.py Comment-only rename in illustrative snippet.
examples/infected_asyncio_echo_server.py Naming sweep (nan).
examples/debugging/sync_bp.py Update TODO comment to reference to_actor.run().
examples/debugging/subactor_error.py Migrate to to_actor.run() and remove portal-final-result calls.
examples/debugging/subactor_breakpoint.py Migrate to to_actor.run() and remove portal-final-result calls.
examples/debugging/subactor_bp_in_ctx.py Naming sweep (nan).
examples/debugging/shielded_pause.py Migrate to to_actor.run() and remove portal-final-result calls.
examples/debugging/root_timeout_while_child_crashed.py Use daemon + bg Portal.run() for erroring child while root blocks.
examples/debugging/root_cancelled_but_child_is_in_tty_lock.py Use bg to_actor.run() task to preserve legacy concurrent-tree behavior.
examples/debugging/per_actor_debug.py Naming sweep (tnan).
examples/debugging/open_ctx_modnofound.py Naming sweep (nan).
examples/debugging/multi_subactors.py Replace teardown “reap-all” behavior with an explicit “collect don’t cancel” pattern.
examples/debugging/multi_subactor_root_errors.py Use daemons + bg Portal.run() and align behavior with new scoping.
examples/debugging/multi_nested_subactors_error_up_through_nurseries.py Use concurrent to_actor.run() tasks for nested-tree error relays.
examples/asynchronous_generators.py Naming sweep (nan).
examples/actor_spawning_and_causality.py Replace run_in_actor() flow with a single blocking to_actor.run() call.
examples/actor_spawning_and_causality_with_daemon.py Naming sweep (nan).
examples/a_trynamic_first_scene.py Fix mutual-rendezvous lifetime by using daemons + concurrent Portal.run()s.
docs/start/quickstart.rst Update quickstart narrative to reference to_actor.run() and one-shot semantics.
docs/guide/spawning.rst Replace run_in_actor() docs with to_actor.run() and updated semantics.
docs/guide/rpc.rst Replace portal final-result docs with to_actor.run() one-shot explanation.
docs/guide/parallelism.rst Update one-shot parallelism docs to use to_actor.run().
docs/guide/context.rst Update references to the one-shot convenience API.
docs/guide/cancellation.rst Update cancellation narrative to reflect one-shot scoping via to_actor.run().
docs/guide/asyncio.rst Update infected-asyncio docs to reference to_actor.run() instead of legacy API.
docs/api/core.rst Remove run_in_actor from API docs; add to_actor.run as the one-shot function.
ai/prompt-io/claude/20260706T172818Z_ad42871e_prompt_io.raw.md Add prompt I/O artifact for provenance of generated changes.
ai/prompt-io/claude/20260706T172818Z_ad42871e_prompt_io.md Add summarized prompt I/O artifact.
ai/prompt-io/claude/20260702T222544Z_9201a2ed_prompt_io.raw.md Add earlier prompt I/O artifact.
ai/prompt-io/claude/20260702T222544Z_9201a2ed_prompt_io.md Add earlier summarized prompt I/O artifact.
ai/prompt-io/claude/20260702T172233Z_5cd190c5_prompt_io.raw.md Add earlier prompt I/O artifact.
ai/prompt-io/claude/20260702T172233Z_5cd190c5_prompt_io.md Add earlier summarized prompt I/O artifact.
ai/prompt-io/claude/20260702T165806Z_a34aaf98_prompt_io.raw.md Add earlier prompt I/O artifact.
ai/prompt-io/claude/20260702T165806Z_a34aaf98_prompt_io.md Add earlier summarized prompt I/O artifact.
ai/conc-anal/ria_nursery_removal_plan.md Add/extend internal analysis doc documenting the removal plan and outcomes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/test_pubsub.py
Comment on lines +282 to +283
# drop the bg `subs()` runners now the subs are cancelled
tn.cancel_scope.cancel()
registry_addrs=[reg_addr],
) as an:
portal = await an.run_in_actor(
# should raise RAE diectly
registry_addrs=[reg_addr],
) as an:
portal = await an.run_in_actor(
# should raise RAE diectly
# enable_stack_on_sig=True,
) as an:
portal = await an.run_in_actor(
# should raise RAE diectly

except tractor.RemoteActorError as err:
assert err.boxed_type == err_type
# we only expect this first error to propogate
else:
if expect_error:
pytest.fail(
"Deamon call should fail at checkpoint?")
async with (
tractor.open_nursery(
debug_mode=True,
enable_transports=['uds'], # TODO, apss this via osenv?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Factor ActorNursery.run_in_actor() (and possibly Portal.run()) into a new tractor.to_actor wrapper API

2 participants