Compare commits

..

28 Commits

Author SHA1 Message Date
Gud Boi 481ba00332 Doc the #477 migration outcome + one-shot-acm sketch
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..ad42871e`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-18 22:16:33 -04:00
Gud Boi 669989a961 Name every `ActorNursery` binding `an` in tests/examples
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
2026-08-18 22:16:33 -04:00
Gud Boi cdd4426451 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
2026-08-18 22:16:33 -04:00
Gud Boi 601b21deee Remove `run_in_actor()` + the ria reap cluster
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
2026-08-18 22:16:33 -04:00
Gud Boi fef19b23c6 Fix stale `@pub` docstring example in `experimental`
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
2026-08-18 22:15:49 -04:00
Gud Boi 5561ab297a Port docs off `run_in_actor` + `Portal.wait_for_result`
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
2026-08-18 22:15:49 -04:00
Gud Boi 07668a7e74 Port debugging examples off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi db0e893c85 Port non-debugging examples off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 74faf1fbfb Port `test_dynamic_pub_sub` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 7ffdc0ab97 Port SIGINT + sync-sleep cancel tests off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 20dfae0a16 Port `test_nested_multierrors` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 35cb49acc2 Fix unbound `timeout` under non-trio/MTF backends
`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
2026-08-18 22:15:49 -04:00
Gud Boi fe4ce730a8 Port `test_some_cancels_all` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 20a0d2c021 Doc ria-reap hang fix + paused reaper re-scope
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`))
2026-08-18 22:15:49 -04:00
Gud Boi aceecf2aab Port `test_cancellation` multierror cluster off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 1ec3f5c847 Port `test_registrar` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 08bea07766 Port `test_pubsub` off `run_in_actor`
`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
2026-08-18 22:15:49 -04:00
Gud Boi 0392566f3c Port `test_spawning` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi c8acae9eb8 Port `test_rpc` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 3f811b2b26 Port `test_runtime` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi 1390e68cb4 Port `test_infected_asyncio` off `run_in_actor`
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
2026-08-18 22:15:49 -04:00
Gud Boi c8ada801e6 Add anti-hang `fail_after` cap to aio-cancel test
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
2026-08-18 22:15:49 -04:00
Gud Boi 09bbe9701e Merge the supervise error handlers into one
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
2026-08-18 22:15:49 -04:00
Gud Boi 762deb1a13 Doc step-B2 handler-merge + prompt-io
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
2026-08-18 22:14:09 -04:00
Gud Boi 826bc2da51 Drop the vestigial `._ria_nursery`
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
2026-08-18 22:14:08 -04:00
Gud Boi 3a60fc04de Doc step-B outcome + prompt-io
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
2026-08-18 22:13:04 -04:00
Gud Boi 4a4076cef1 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-code
2026-08-18 22:13:04 -04:00
Gud Boi 14f200056f Add `_ria_nursery` removal plan + step-A prompt-io
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
2026-08-18 22:13:04 -04:00
14 changed files with 158 additions and 396 deletions

View File

@ -1,56 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
timestamp: 2026-08-19T18:46:40Z
git_ref: 481ba003
scope: code
substantive: true
raw_file: 20260819T184640Z_481ba003_prompt_io.raw.md
---
## Prompt
Rebase PR #484 onto final PR #481, migrate every affected one-shot call
to the new positional target API and continue through downstream tests,
examples and documentation review.
## Response summary
Converted stale target keyword calls to target partials so previously
named inputs remain explicit while placement/runtime controls stay
direct. Updated error expectations for local signature validation and
linked remote error propagation, then corrected docs which still
described the removed one-shot implementation. Linked spawning and
context lifecycle prose to the corresponding API methods and detailed
context guide.
## Files changed
- `docs/api/core.rst` - describe linked one-shot context execution.
- `docs/guide/rpc.rst` - update placement and target call semantics.
- `docs/guide/spawning.rst` - document positional target inputs.
- `examples/debugging/multi_nested_subactors_error_up_through_nurseries.py` - migrate nested actor target inputs.
- `examples/debugging/root_cancelled_but_child_is_in_tty_lock.py` - preserve named recursive target inputs with partials.
- `tests/test_advanced_streaming.py` - migrate streaming target inputs.
- `tests/test_cancellation.py` - migrate calls and tighten errors.
- `tests/test_infected_asyncio.py` - bind asyncio target options.
- `tests/test_rpc.py` - migrate RPC target argument binding.
- `tests/test_runtime.py` - preserve named runtime target inputs.
- `tests/test_spawning.py` - preserve named spawning target inputs.
## Human edits
The human selected the stack order and final PR #481 base, asked the
agent to continue after each diagnostic step and required a complete
commit plan after independently force-pushing the rebased history.
After reviewing the migration, the human required every formerly named
target input to remain visibly named through `functools.partial()`
rather than becoming positional. These were human-directed agent edits;
the human also required plain `start_actor()` and `open_context()`
references in the spawning and RPC guides to link to their API methods
and the detailed context guide, then clarified that `to_actor.run()`
already uses the full context API while `Portal.run()` should share
linked lifecycle machinery without necessarily delegating through
`Portal.open_context()` or adding a `Started` message. The human made
no direct source-line edits.

View File

@ -1,30 +0,0 @@
---
model: openai/gpt-5.6-sol
service: opencode
timestamp: 2026-08-19T18:46:40Z
git_ref: 481ba003
diff_cmd: git diff HEAD~1..HEAD
---
Migrate PR #484's downstream one-shot calls to PR #481's final
`tractor.to_actor.run()` contract after the stack rebase.
> `git diff HEAD~1..HEAD -- docs examples tests`
Pass target arguments positionally and bind target keyword-only inputs
with `functools.partial()`. Keep placement and runtime controls as
direct `to_actor.run()` keywords. Update the invalid-target-argument
test to expect local signature binding before actor startup and require
direct `RemoteActorError` propagation from linked one-shots.
Update API and guide prose to describe positional target inputs,
linked `Portal.open_context()` execution and per-child reaping instead
of the removed `Portal.run()` and target-`**kwargs` conventions.
Verification:
- core and migrated runtime batches: `97 passed`
- discovery and related lifecycle batch: `33 passed, 1 skipped`
- changed executable examples: `9 passed`
- mapped debugger cases: `12 passed, 6 skipped`
- Ruff, compilation and `git diff --check`: clean

View File

@ -58,10 +58,10 @@ One-shot task actors
``trio.to_thread.run_sync()`` and friends) is the ``trio.to_thread.run_sync()`` and friends) is the
*convenience* one-shot — spawn, run a single task, block on *convenience* one-shot — spawn, run a single task, block on
its result, reap — built entirely on its result, reap — built entirely on
:meth:`ActorNursery.start_actor`, a linked :meth:`ActorNursery.start_actor` + :meth:`Portal.run` +
:meth:`Portal.open_context` call and per-child cancellation/reaping, :meth:`Portal.cancel_actor`, so don't design around it as the
so don't design around it as the core model. It supersedes the core model. It supersedes the removed (legacy, non-blocking)
removed (legacy, non-blocking) ``ActorNursery.run_in_actor()``. ``ActorNursery.run_in_actor()``.
.. deprecated:: 0.1.0a6 .. deprecated:: 0.1.0a6

View File

@ -89,12 +89,7 @@ in one blocking call:
.. code:: python .. code:: python
from functools import partial final = await tractor.to_actor.run(fib, an=an, n=10)
final = await tractor.to_actor.run(
partial(fib, n=10),
an=an,
)
Semantics worth knowing: Semantics worth knowing:
@ -103,10 +98,9 @@ Semantics worth knowing:
task. task.
- "placement" is composable: ``an=`` spawns from an existing - "placement" is composable: ``an=`` spawns from an existing
actor-nursery, ``portal=`` reuses an already-running actor actor-nursery, ``portal=`` reuses an already-running actor
(no spawn/reap, just a linked (no spawn/reap, just a ``Portal.run()``), and passing
:meth:`~tractor.Portal.open_context` call; see the neither opens a private call-scoped nursery (booting the
:doc:`context guide </guide/context>`), and passing neither runtime if needed).
opens a private call-scoped nursery (booting the runtime if needed).
- concurrency composes the plain ``trio`` way: schedule - concurrency composes the plain ``trio`` way: schedule
multiple ``run()`` calls into a local task nursery (see multiple ``run()`` calls into a local task nursery (see
``examples/parallelism/to_actor_one_shots.py``). ``examples/parallelism/to_actor_one_shots.py``).
@ -155,8 +149,7 @@ call tears down the entire sub-tree — SC, transitively.
When to graduate to ``Context`` When to graduate to ``Context``
------------------------------- -------------------------------
The :meth:`~tractor.Portal.run` method is great for one-shot, ``portal.run()`` is great for one-shot, request-response calls.
request-response calls.
Reach for :meth:`~tractor.Portal.open_context` with an Reach for :meth:`~tractor.Portal.open_context` with an
``@tractor.context`` endpoint as soon as you want: ``@tractor.context`` endpoint as soon as you want:
@ -169,15 +162,10 @@ Reach for :meth:`~tractor.Portal.open_context` with an
:meth:`~tractor.Portal.cancel_actor` nukes the **entire** :meth:`~tractor.Portal.cancel_actor` nukes the **entire**
remote runtime and its process. remote runtime and its process.
:func:`tractor.to_actor.run` already enters the full In fact the source plans for ``Portal.run()`` itself to be
:meth:`~tractor.Portal.open_context` lifecycle. The older rebuilt on top of ``open_context()`` — contexts *are* the core
:meth:`~tractor.Portal.run` path instead uses the ``Context`` returned inter-actor protocol. Take the full tour in
by the lower-level ``Actor.start_remote_task()`` directly, avoiding a :doc:`/guide/context`.
``Started`` handshake but owning less lifecycle machinery. A follow-up
should factor their shared linked-task lifecycle without requiring
``Portal.run()`` to delegate through the public context API or add
another wire message. Take the full tour in
:doc:`the context guide </guide/context>`.
.. seealso:: .. seealso::

View File

@ -91,17 +91,17 @@ somebody-ing:
What's going on here? What's going on here?
- :meth:`~tractor.ActorNursery.start_actor` forks off - ``start_actor('frank', enable_modules=[__name__])`` forks off
a new process, boots a ``tractor`` runtime inside it, and a new process, boots a ``tractor`` runtime inside it, and
allows it to serve functions from the current module (see the allows it to serve functions from the current module (see the
allowlist section below). allowlist section below).
- each :meth:`~tractor.Portal.run` call schedules a *new* task in - each ``await portal.run(...)`` schedules a *new* task in
frank's task tree and waits on its result — the full RPC story frank's task tree and waits on its result — the full RPC story
lives in :doc:`/guide/rpc`. lives in :doc:`/guide/rpc`.
- frank has no main task to complete, so without the final - frank has no main task to complete, so without the final
:meth:`~tractor.Portal.cancel_actor` call the nursery block would ``await portal.cancel_actor()`` the nursery block would wait
wait on him **forever**. Daemon lifetimes are *yours* to end; on him **forever**. Daemon lifetimes are *yours* to end; that
that explicitness is the point. explicitness is the point.
``to_actor.run()``: quick one-shot parallelism ``to_actor.run()``: quick one-shot parallelism
---------------------------------------------- ----------------------------------------------
@ -126,9 +126,7 @@ A few details worth knowing:
``name='something_cuter'``. ``name='something_cuter'``.
- the function's module is auto-added to the child's - the function's module is auto-added to the child's
``enable_modules`` allowlist. ``enable_modules`` allowlist.
- target arguments are positional; use ``functools.partial()`` - extra ``**kwargs`` are forwarded to the function itself.
to bind target keyword arguments. Keywords passed directly to
``run()`` configure actor placement and spawning.
- the call blocks until the result (or error) lands and the - the call blocks until the result (or error) lands and the
child is *auto-cancelled* (reaped) right after — so remote child is *auto-cancelled* (reaped) right after — so remote
errors raise directly in your calling task (causality_ is errors raise directly in your calling task (causality_ is
@ -140,16 +138,14 @@ A few details worth knowing:
.. note:: .. note::
:func:`tractor.to_actor.run` is a convenience, **not** the core ``to_actor.run()`` is a convenience, **not** the core model —
model — it's built *entirely* on it's built *entirely* on ``start_actor()`` + ``Portal.run()``
:meth:`~tractor.ActorNursery.start_actor` plus a linked + ``Portal.cancel_actor()``. Teach your fingers to use it for
:meth:`~tractor.Portal.open_context` call and per-child quick fire-and-collect parallelism — think a per-function
cancellation/reaping. Teach your fingers to use it for quick trio-parallel_ style one-shot — and reach for
fire-and-collect parallelism — think a per-function trio-parallel_ ``start_actor()`` + ``open_context()`` for anything
style one-shot — and reach for long-lived, stateful or streaming
:meth:`~tractor.ActorNursery.start_actor` plus (:doc:`/guide/context`).
:meth:`~tractor.Portal.open_context` for anything long-lived,
stateful or streaming; see :doc:`/guide/context`.
Actor lifetimes and teardown order Actor lifetimes and teardown order
---------------------------------- ----------------------------------
@ -158,11 +154,10 @@ So we have two lifetime flavors:
- **one-shot** (``to_actor.run()``): lives exactly as long as - **one-shot** (``to_actor.run()``): lives exactly as long as
its single task; reaped the moment its result (or error) its single task; reaped the moment its result (or error)
arrives back in the (blocking) call. arrives back in the (blocking) call.
- **daemon** (:meth:`~tractor.ActorNursery.start_actor`): lives - **daemon** (``start_actor()``): lives until *someone* cancels
until *someone* cancels it — an explicit it — an explicit ``await portal.cancel_actor()``, a bulk
:meth:`~tractor.Portal.cancel_actor`, a bulk ``await an.cancel()``, or the one-cancels-all strategy kicking
:meth:`~tractor.ActorNursery.cancel`, or the one-cancels-all in on error.
strategy kicking in on error.
On a clean exit of the nursery block the teardown order is: On a clean exit of the nursery block the teardown order is:

View File

@ -51,11 +51,9 @@ async def spawn_until(depth=0):
# `name_error` relays through. # `name_error` relays through.
depth -= 1 depth -= 1
await tractor.to_actor.run( await tractor.to_actor.run(
partial( spawn_until,
spawn_until,
depth=depth,
),
an=an, an=an,
depth=depth,
name=f'spawn_until_{depth}', name=f'spawn_until_{depth}',
) )
@ -92,22 +90,18 @@ async def main():
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial( spawn_until,
spawn_until,
depth=3,
),
an=an, an=an,
depth=3,
name='spawner0', name='spawner0',
) )
) )
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial( spawn_until,
spawn_until,
depth=4,
),
an=an, an=an,
depth=4,
name='spawner1', name='spawner1',
) )
) )

View File

@ -18,11 +18,9 @@ async def spawn_until(depth=0):
else: else:
depth -= 1 depth -= 1
await tractor.to_actor.run( await tractor.to_actor.run(
partial( spawn_until,
spawn_until,
depth=depth,
),
an=an, an=an,
depth=depth,
name=f'spawn_until_{depth}', name=f'spawn_until_{depth}',
) )
@ -53,11 +51,9 @@ async def main():
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial( spawn_until,
spawn_until,
depth=1,
),
an=an, an=an,
depth=1,
name='spawner1', name='spawner1',
) )
) )
@ -65,11 +61,9 @@ async def main():
# ..while blocking on the shallow (faster to fail) tree # ..while blocking on the shallow (faster to fail) tree
# whose propagated error triggers nursery cancellation. # whose propagated error triggers nursery cancellation.
await tractor.to_actor.run( await tractor.to_actor.run(
partial( spawn_until,
spawn_until,
depth=0,
),
an=an, an=an,
depth=0,
name='spawner0', name='spawner0',
) )

View File

@ -248,12 +248,10 @@ def test_dynamic_pub_sub(
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial( consumer,
consumer,
subs=[sub],
),
an=an, an=an,
name=f'consumer_{sub}', name=f'consumer_{sub}',
subs=[sub],
) )
) )
@ -261,12 +259,10 @@ def test_dynamic_pub_sub(
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial( consumer,
consumer,
subs=list(_registry.keys()),
),
an=an, an=an,
name='consumer_dynamic', name='consumer_dynamic',
subs=list(_registry.keys()),
) )
) )

View File

@ -104,7 +104,7 @@ async def do_nuthin():
[ [
# expected to be thrown in assert_err # expected to be thrown in assert_err
({}, AssertionError), ({}, AssertionError),
# argument mismatch rejected locally before spawn # argument mismatch raised in _invoke()
({'unexpected': 10}, TypeError) ({'unexpected': 10}, TypeError)
], ],
ids=['no_args', 'unexpected_args'], ids=['no_args', 'unexpected_args'],
@ -130,34 +130,48 @@ def test_remote_error(
# `to_actor.run()` blocks on the one-shot's result and # `to_actor.run()` blocks on the one-shot's result and
# raises the remote error directly here in the caller's # raises the remote error directly here in the caller's
# task. Invalid target args fail local signature binding # task (a bad-arg `TypeError` likewise relays as a
# before any one-shot actor is spawned. # `RemoteActorError`).
try: try:
await tractor.to_actor.run( await tractor.to_actor.run(
partial(assert_err, **args), assert_err,
an=an, an=an,
name='errorer', name='errorer',
**args
) )
except tractor.RemoteActorError as err: except tractor.RemoteActorError as err:
assert err.boxed_type == errtype assert err.boxed_type == errtype
print("Look Maa that actor failed hard, hehh") print("Look Maa that actor failed hard, hehh")
raise raise
# Invalid args never cross the process boundary. # ensure boxed errors
if args: if args:
with pytest.raises(errtype): with pytest.raises(tractor.RemoteActorError) as excinfo:
trio.run(main)
else:
# The linked one-shot raises the child's boxed error
# directly in this caller task.
with pytest.raises(
tractor.RemoteActorError,
) as excinfo:
trio.run(main) trio.run(main)
assert excinfo.value.boxed_type == errtype assert excinfo.value.boxed_type == errtype
else:
# the root task will also error on the `Portal.result()`
# call so we expect an error from there AND the child.
# |_ tho seems like on new `trio` this doesn't always
# happen?
with pytest.raises((
BaseExceptionGroup,
tractor.RemoteActorError,
)) as excinfo:
trio.run(main)
# ensure boxed errors are `errtype`
err: BaseException = excinfo.value
if isinstance(err, BaseExceptionGroup):
suberrs: list[BaseException] = err.exceptions
else:
suberrs: list[BaseException] = [err]
for exc in suberrs:
assert exc.boxed_type == errtype
def test_multierror( def test_multierror(
reg_addr: tuple[str, int], reg_addr: tuple[str, int],
@ -377,9 +391,10 @@ async def test_some_cancels_all(
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial(func, **kwargs), func,
an=an, an=an,
name=f'actor_{i}', name=f'actor_{i}',
**kwargs,
) )
) )
@ -454,14 +469,12 @@ async def spawn_and_error(
if depth > 0: if depth > 0:
args = ( args = (
partial( spawn_and_error,
spawn_and_error,
breadth=breadth,
depth=depth - 1,
),
) )
kwargs = { kwargs = {
'name': f'spawner_{i}_depth_{depth}', 'name': f'spawner_{i}_depth_{depth}',
'breadth': breadth,
'depth': depth - 1,
} }
else: else:
args = ( args = (
@ -692,13 +705,11 @@ async def test_nested_multierrors(
tn.start_soon( tn.start_soon(
partial( partial(
tractor.to_actor.run, tractor.to_actor.run,
partial( spawn_and_error,
spawn_and_error,
breadth=subactor_breadth,
depth=depth,
),
an=an, an=an,
name=f'spawner_{i}', name=f'spawner_{i}',
breadth=subactor_breadth,
depth=depth,
) )
) )
except ( except (

View File

@ -5,7 +5,7 @@ The hipster way to force SC onto the stdlib's "async": 'infection mode'.
import asyncio import asyncio
import builtins import builtins
from contextlib import ExitStack from contextlib import ExitStack
from functools import partial # from functools import partial
import itertools import itertools
import importlib import importlib
import os import os
@ -209,12 +209,10 @@ def test_aio_simple_error(
debug_mode=debug_mode, debug_mode=debug_mode,
) as an: ) as an:
await to_actor.run( await to_actor.run(
partial( asyncio_actor,
asyncio_actor,
target='sleep_and_err',
expect_err='AssertionError',
),
an=an, an=an,
target='sleep_and_err',
expect_err='AssertionError',
infect_asyncio=True, infect_asyncio=True,
) )
@ -457,14 +455,10 @@ def test_aio_cancelled_from_aio_causes_trio_cancelled(
# relays the remote error here in the caller's task. # relays the remote error here in the caller's task.
with trio.fail_after(1 + delay): with trio.fail_after(1 + delay):
await to_actor.run( await to_actor.run(
partial( asyncio_actor,
asyncio_actor,
target='aio_cancel',
expect_err=(
'tractor.to_asyncio.AsyncioCancelled'
),
),
an=an, an=an,
target='aio_cancel',
expect_err='tractor.to_asyncio.AsyncioCancelled',
infect_asyncio=True, infect_asyncio=True,
) )
@ -669,12 +663,10 @@ def test_basic_interloop_channel_stream(
) as an: ) as an:
# should raise RAE diectly # should raise RAE diectly
await to_actor.run( await to_actor.run(
partial( stream_from_aio,
stream_from_aio,
fan_out=fan_out,
),
an=an, an=an,
infect_asyncio=True, infect_asyncio=True,
fan_out=fan_out,
) )
trio.run(main) trio.run(main)
@ -690,11 +682,9 @@ def test_trio_error_cancels_intertask_chan(
) as an: ) as an:
# should trigger remote actor error # should trigger remote actor error
await to_actor.run( await to_actor.run(
partial( stream_from_aio,
stream_from_aio,
trio_raise_err=True,
),
an=an, an=an,
trio_raise_err=True,
infect_asyncio=True, infect_asyncio=True,
) )
@ -729,11 +719,9 @@ def test_trio_closes_early_causes_aio_checkpoint_raise(
# should raise RAE diectly # should raise RAE diectly
print('waiting on final infected subactor result..') print('waiting on final infected subactor result..')
res: None = await to_actor.run( res: None = await to_actor.run(
partial( stream_from_aio,
stream_from_aio,
trio_exit_early=True,
),
an=an, an=an,
trio_exit_early=True,
infect_asyncio=True, infect_asyncio=True,
) )
assert res is None assert res is None
@ -782,13 +770,11 @@ def test_aio_exits_early_relays_AsyncioTaskExited(
# should raise RAE diectly # should raise RAE diectly
print('waiting on final infected subactor result..') print('waiting on final infected subactor result..')
res: None = await to_actor.run( res: None = await to_actor.run(
partial( stream_from_aio,
stream_from_aio,
trio_exit_early=False,
aio_exit_early=True,
),
an=an, an=an,
infect_asyncio=True, infect_asyncio=True,
trio_exit_early=False,
aio_exit_early=True,
) )
assert res is None assert res is None
print(f'infected subactor returned result: {res!r}\n') print(f'infected subactor returned result: {res!r}\n')
@ -823,11 +809,9 @@ def test_aio_errors_and_channel_propagates_and_closes(
) as an: ) as an:
# should trigger RAE directly, not an eg. # should trigger RAE directly, not an eg.
await to_actor.run( await to_actor.run(
partial( stream_from_aio,
stream_from_aio,
aio_raise_err=True,
),
an=an, an=an,
aio_raise_err=True,
infect_asyncio=True, infect_asyncio=True,
) )

View File

@ -4,7 +4,6 @@ related API and error checks.
''' '''
import itertools import itertools
from functools import partial
from unittest.mock import ( from unittest.mock import (
AsyncMock, AsyncMock,
Mock, Mock,
@ -240,21 +239,19 @@ def test_rpc_errors(
actor = tractor.current_actor() actor = tractor.current_actor()
assert actor.is_registrar assert actor.is_registrar
await tractor.to_actor.run( await tractor.to_actor.run(
partial( sleep_back_actor,
sleep_back_actor,
actor_name=subactor_requests_to,
func_name=funcname,
func_defined=bool(func_defined),
exposed_mods=exposed_mods,
reg_addr=reg_addr,
),
an=an, an=an,
actor_name=subactor_requests_to,
name='subactor', name='subactor',
# Function from the local exposed module space the # function from the local exposed module space
# subactor invokes when it RPCs back to this actor. # the subactor will invoke when it RPCs back to this actor
func_name=funcname,
exposed_mods=exposed_mods,
func_defined=True if func_defined else False,
enable_modules=subactor_exposed_mods, enable_modules=subactor_exposed_mods,
reg_addr=reg_addr,
) )
def run(): def run():

View File

@ -2,7 +2,6 @@
Verifying internal runtime state and undocumented extras. Verifying internal runtime state and undocumented extras.
""" """
from functools import partial
import os import os
import pytest import pytest
@ -85,12 +84,10 @@ async def test_lifetime_stack_wipes_tmpfile(
loglevel=loglevel, loglevel=loglevel,
) as an: ) as an:
await tractor.to_actor.run( await tractor.to_actor.run(
partial( crash_and_clean_tmpdir,
crash_and_clean_tmpdir,
tmp_file_path=path,
error=error_in_child,
),
an=an, an=an,
tmp_file_path=path,
error=error_in_child,
) )
except ( except (
tractor.RemoteActorError, tractor.RemoteActorError,

View File

@ -51,18 +51,18 @@ async def spawn(
# recursively spawn this same `spawn()` fn as the lone # recursively spawn this same `spawn()` fn as the lone
# task of a one-shot child subactor and get its result. # task of a one-shot child subactor and get its result.
result = await tractor.to_actor.run( result = await tractor.to_actor.run(
partial( spawn,
spawn,
should_be_root=False,
data=data_to_pass_down,
reg_addr=reg_addr,
),
an=an, an=an,
# spawning args # spawning args
name='sub-actor', name='sub-actor',
enable_modules=[__name__], enable_modules=[__name__],
# passed to a subactor-recursive RPC invoke
# of this same `spawn()` fn.
should_be_root=False,
data=data_to_pass_down,
reg_addr=reg_addr,
) )
assert result == 10 assert result == 10
return result return result
@ -152,11 +152,9 @@ async def test_most_beautiful_word(
debug_mode=debug_mode, debug_mode=debug_mode,
) as an: ) as an:
res: Any = await tractor.to_actor.run( res: Any = await tractor.to_actor.run(
partial( cellar_door,
cellar_door,
return_value=return_value,
),
an=an, an=an,
return_value=return_value,
name='some_linguist', name='some_linguist',
) )
assert res == return_value assert res == return_value
@ -206,12 +204,10 @@ def test_loglevel_propagated_to_subactor(
) as an: ) as an:
await tractor.to_actor.run( await tractor.to_actor.run(
partial( check_loglevel,
check_loglevel,
level=level,
),
an=an, an=an,
loglevel=level, loglevel=level,
level=level,
) )
trio.run(main) trio.run(main)
@ -277,23 +273,19 @@ def test_to_actor_run_can_skip_parent_main_inheritance(
# Default: child receives parent __main__ bootstrap data # Default: child receives parent __main__ bootstrap data
await tractor.to_actor.run( await tractor.to_actor.run(
partial( check_parent_main_inheritance,
check_parent_main_inheritance,
expect_inherited=True,
),
an=an, an=an,
name='replaying-parent-main', name='replaying-parent-main',
expect_inherited=True,
) )
# Opt-out: child gets no parent __main__ data # Opt-out: child gets no parent __main__ data
await tractor.to_actor.run( await tractor.to_actor.run(
partial( check_parent_main_inheritance,
check_parent_main_inheritance,
expect_inherited=False,
),
an=an, an=an,
name='isolated-parent-main', name='isolated-parent-main',
inherit_parent_main=False, inherit_parent_main=False,
expect_inherited=False,
) )
trio.run(main) trio.run(main)

View File

@ -89,7 +89,7 @@ async def _try_cancel_then_kill(
Sends a graceful actor-runtime cancel-RPC via Sends a graceful actor-runtime cancel-RPC via
`Portal.cancel_actor(raise_on_timeout=True)`. If the bounded-wait `Portal.cancel_actor(raise_on_timeout=True)`. If the bounded-wait
expires before the peer ack's, `ActorTooSlowError` is raised and expires before the peer ack's, `ActorTooSlowError` is raised and
we escalate via `proc.kill()` per SC-discipline: we escalate via `proc.terminate()` (SIGTERM) per SC-discipline:
graceful cancel-req -> bounded wait -> hard-kill graceful cancel-req -> bounded wait -> hard-kill
@ -101,9 +101,11 @@ async def _try_cancel_then_kill(
the wider write-up. the wider write-up.
''' '''
# XXX, delay hard-kill escalation while any debugger guard # XXX, do NOT escalate to `proc.terminate()` while ANY of
# below is active. Killing the sub immediately would tear down # the following are true — SIGTERM-ing a sub would tear
# its tree and clobber an actor proxying a REPL session: # down its sub-tree including any descendant proxying
# stdio to/from a REPL-locked actor, clobbering the user's
# debug session:
# #
# - `Lock.ctx_in_debug is not None`: most precise — some # - `Lock.ctx_in_debug is not None`: most precise — some
# actor in the tree is currently REPL-locked. Set in the # actor in the tree is currently REPL-locked. Set in the
@ -119,7 +121,7 @@ async def _try_cancel_then_kill(
# child. # child.
# #
# - `debug_mode_active`: this nursery has at least one # - `debug_mode_active`: this nursery has at least one
# child started with an explicit `debug_mode=True` arg # child started with an explicit `debug_mode=` arg
# (`ActorNursery._at_least_one_child_in_debug`). Catches # (`ActorNursery._at_least_one_child_in_debug`). Catches
# the case where root is NOT in debug-mode but a # the case where root is NOT in debug-mode but a
# nursery-direct child opted in. # nursery-direct child opted in.
@ -129,57 +131,36 @@ async def _try_cancel_then_kill(
# mutated by per-child `debug_mode=True`). ORing covers # mutated by per-child `debug_mode=True`). ORing covers
# every flavor without false-positively skipping # every flavor without false-positively skipping
# legitimate hard-kill paths in non-debug trees. # legitimate hard-kill paths in non-debug trees.
debug_protected: bool = ( if (
debug.Lock.ctx_in_debug is not None debug.Lock.ctx_in_debug is not None
or or
_state._runtime_vars.get('_debug_mode', False) _state._runtime_vars.get('_debug_mode', False)
or or
debug_mode_active debug_mode_active
) ):
await portal.cancel_actor()
return
try: try:
cancelled: bool = await portal.cancel_actor( await portal.cancel_actor(raise_on_timeout=True)
raise_on_timeout=not debug_protected,
)
if not cancelled:
if debug_protected:
await debug.maybe_wait_for_debugger(
child_in_debug=(
debug_mode_active
or
debug.Lock.ctx_in_debug is not None
),
header_msg=(
'Delaying subproc hard-reap while '
'debugger locked..\n'
),
)
peer_id: str = portal.channel.aid.reprol()
raise ActorTooSlowError(
f'Peer {peer_id} disconnected before '
f'acknowledging its `Actor.cancel()` RPC'
)
except ActorTooSlowError as too_slow: except ActorTooSlowError as too_slow:
log.error( log.error(
f'Cancel-ack TIMED OUT for sub-actor\n' f'Cancel-ack TIMED OUT for sub-actor\n'
f' uid: {subactor.aid.reprol()!r}\n' f' uid: {subactor.aid.reprol()!r}\n'
f' reason: {too_slow}\n' f' reason: {too_slow}\n'
f'-> escalating to `proc.kill()` (hard-reap)\n' f'-> escalating to `proc.terminate()` (hard-kill)\n'
) )
# XXX, the `subint` backend stores an `int` interp-id in the # XXX, the `subint` backend stores an `int` interp-id in the
# `proc` slot (not a `Process`), so it has no `.kill()`. # `proc` slot (not a `Process`), so it has no `.terminate()`.
# Guard here so a cancel-ack timeout doesn't `AttributeError` # Guard here so a cancel-ack timeout doesn't `AttributeError`
# once that backend lands; its hard-kill path is a TODO. # once that backend lands; its hard-kill path is a TODO.
if hasattr(proc, 'kill'): if hasattr(proc, 'terminate'):
if proc.poll() is None: proc.terminate()
proc.kill()
else: else:
log.error( log.error(
f'Cannot hard-kill sub-actor — backend proc-handle ' f'Cannot hard-kill sub-actor — backend proc-handle '
f'{proc!r} ({type(proc).__name__!r}) has no ' f'{proc!r} ({type(proc).__name__!r}) has no '
f'`.kill()`!\n' f'`.terminate()`!\n'
f' uid: {subactor.aid.reprol()!r}\n' f' uid: {subactor.aid.reprol()!r}\n'
f'TODO: per-backend cancel-escalation.\n' f'TODO: per-backend cancel-escalation.\n'
) )
@ -237,14 +218,6 @@ class ActorNursery:
] = {} ] = {}
self._join_procs = trio.Event() self._join_procs = trio.Event()
self._child_reap_requests: dict[
tuple[str, str],
trio.Event,
] = {}
self._child_reaped: dict[
tuple[str, str],
trio.Event,
] = {}
self._at_least_one_child_in_debug: bool = False self._at_least_one_child_in_debug: bool = False
self.errors = errors self.errors = errors
self._scope_error: BaseException|None = None self._scope_error: BaseException|None = None
@ -299,79 +272,6 @@ class ActorNursery:
# self._cancelled_caught # self._cancelled_caught
) )
def _register_child_reap(
self,
uid: tuple[str, str],
) -> tuple[trio.Event, trio.Event]:
'''
Register a child monitor's process-reap events.
'''
reap_request = trio.Event()
reaped = trio.Event()
self._child_reap_requests[uid] = reap_request
self._child_reaped[uid] = reaped
if self._join_procs.is_set():
reap_request.set()
return reap_request, reaped
def _request_reap_all(self) -> None:
'''
Release every child monitor into its process-join phase.
'''
self._join_procs.set()
for reap_request in tuple(
self._child_reap_requests.values()
):
reap_request.set()
def _mark_child_reaped(
self,
uid: tuple[str, str],
) -> None:
'''
Publish completed child-process teardown to its waiter.
'''
self._children.pop(uid, None)
self._child_reap_requests.pop(uid, None)
reaped: trio.Event|None = self._child_reaped.pop(
uid,
None,
)
if reaped is not None:
reaped.set()
async def _cancel_and_reap_child(
self,
portal: Portal,
) -> None:
'''
Cancel, join and unregister one nursery-owned child.
'''
uid: tuple[str, str] = portal.channel.aid.uid
child_entry = self._children.get(uid)
if child_entry is None:
return
subactor, proc, _ = child_entry
reap_request: trio.Event = self._child_reap_requests[uid]
reaped: trio.Event = self._child_reaped[uid]
with trio.CancelScope(shield=True):
try:
await _try_cancel_then_kill(
portal,
proc,
subactor,
self._at_least_one_child_in_debug,
)
finally:
reap_request.set()
await reaped.wait()
async def start_actor( async def start_actor(
self, self,
name: str, name: str,
@ -416,7 +316,7 @@ class ActorNursery:
# allow setting debug policy per actor # allow setting debug policy per actor
if debug_mode is not None: if debug_mode is not None:
_rtv['_debug_mode'] = debug_mode _rtv['_debug_mode'] = debug_mode
self._at_least_one_child_in_debug |= debug_mode self._at_least_one_child_in_debug = True
enable_modules = list(enable_modules or []) enable_modules = list(enable_modules or [])
proc_kwargs = dict(proc_kwargs or {}) proc_kwargs = dict(proc_kwargs or {})
@ -485,7 +385,7 @@ class ActorNursery:
# TODO: impl a repr for spawn more compact # TODO: impl a repr for spawn more compact
# then `._children`.. # then `._children`..
children: tuple = tuple(self._children.values()) children: dict = self._children
child_count: int = len(children) child_count: int = len(children)
msg: str = f'Cancelling actor nursery with {child_count} children\n' msg: str = f'Cancelling actor nursery with {child_count} children\n'
@ -504,7 +404,7 @@ class ActorNursery:
subactor, subactor,
proc, proc,
portal, portal,
) in children: ) in children.values():
# TODO: are we ever even going to use this or # TODO: are we ever even going to use this or
# is the spawning backend responsible for such # is the spawning backend responsible for such
@ -524,9 +424,7 @@ class ActorNursery:
await event.wait() await event.wait()
# channel/portal should now be up # channel/portal should now be up
_, _, portal = self._children[ _, _, portal = children[subactor.aid.uid]
subactor.aid.uid
]
# XXX should be impossible to get here # XXX should be impossible to get here
# unless method was called from within # unless method was called from within
@ -573,14 +471,14 @@ class ActorNursery:
subactor, subactor,
proc, proc,
portal, portal,
) in children: ) in children.values():
log.warning(f"Hard killing process {proc}") log.warning(f"Hard killing process {proc}")
proc.terminate() proc.terminate()
else: else:
self._cancelled_caught self._cancelled_caught
# mark ourselves as having (tried to have) cancelled all subactors # mark ourselves as having (tried to have) cancelled all subactors
self._request_reap_all() self._join_procs.set()
@acm @acm
@ -748,13 +646,15 @@ async def open_nursery(
Create and yield a new ``ActorNursery`` to be used for spawning Create and yield a new ``ActorNursery`` to be used for spawning
structured concurrent subactors. structured concurrent subactors.
When an actor is spawned a new trio task invokes one of the When an actor is spawned a new trio task is started which
process spawning backends to create and start a new subprocess. invokes one of the process spawning backends to create and start
These tasks are started in the supervisor's process nursery. a new subprocess. These tasks are started by one of two nurseries
Spawning from a task is required because ``trio_run_in_process`` detailed below. The reason for spawning processes from within
creates an internal nursery which the opening task **must** close; a new task is because ``trio_run_in_process`` itself creates a new
this also makes each task's cancellation scope correspond to its internal nursery and the same task that opens a nursery **must**
spawned subactor. close it. It turns out this approach is probably more correct
anyway since it is more clear from the following nested nurseries
which cancellation scopes correspond to each spawned subactor set.
''' '''
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb