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
drop_ria_nursery
Gud Boi 2026-07-06 12:31:02 -04:00
parent a3057cb24f
commit d6bed7c4a0
8 changed files with 98 additions and 78 deletions

View File

@ -37,7 +37,6 @@ Spawning actors
.. autoclass:: ActorNursery .. autoclass:: ActorNursery
:members: start_actor, :members: start_actor,
run_in_actor,
cancel, cancel,
cancel_called, cancel_called,
cancelled_caught cancelled_caught
@ -47,10 +46,22 @@ Spawning actors
:meth:`ActorNursery.start_actor` (daemon actor + portal) is the :meth:`ActorNursery.start_actor` (daemon actor + portal) is the
blessed spawning primitive; pair it with blessed spawning primitive; pair it with
``Portal.open_context()`` for SC-linked remote tasks. ``Portal.open_context()`` for SC-linked remote tasks.
:meth:`ActorNursery.run_in_actor` is a *convenience* one-shot —
spawn, run a single task, auto-cancel after the result — slated One-shot task actors
to be rebuilt as a high-level wrapper, so don't design around --------------------
it as the core model.
.. autofunction:: tractor.to_actor.run
.. note::
:func:`tractor.to_actor.run` (parlance of
``trio.to_thread.run_sync()`` and friends) is the
*convenience* one-shot — spawn, run a single task, block on
its result, reap — built entirely on
:meth:`ActorNursery.start_actor` + :meth:`Portal.run` +
:meth:`Portal.cancel_actor`, so don't design around it as the
core model. It supersedes the removed (legacy, non-blocking)
``ActorNursery.run_in_actor()``.
.. deprecated:: 0.1.0a6 .. deprecated:: 0.1.0a6
@ -71,14 +82,12 @@ flowing back `exactly like trio`_.
:members: run, :members: run,
run_from_ns, run_from_ns,
open_stream_from, open_stream_from,
wait_for_result,
cancel_actor, cancel_actor,
chan chan
.. deprecated:: 0.1.0a6 .. deprecated:: 0.1.0a6
``Portal.result()`` warns; use :meth:`Portal.wait_for_result`. The str-form ``Portal.run('mod.path', 'fn_name')`` warns;
The str-form ``Portal.run('mod.path', 'fn_name')`` also warns;
pass a function *object* whose module is listed in the target's pass a function *object* whose module is listed in the target's
``enable_modules``. ``Portal.channel`` is the legacy spelling ``enable_modules``. ``Portal.channel`` is the legacy spelling
of :attr:`Portal.chan`. of :attr:`Portal.chan`.

View File

@ -76,8 +76,8 @@ Just flip the flag on :meth:`tractor.ActorNursery.start_actor`:
infect_asyncio=True, infect_asyncio=True,
) )
The one-shot convenience ``ActorNursery.run_in_actor()`` accepts The one-shot convenience ``tractor.to_actor.run()`` accepts the
the same flag. The ``to_asyncio`` APIs may **only** be called from same flag. The ``to_asyncio`` APIs may **only** be called from
tasks inside an infected actor; calling them anywhere else raises tasks inside an infected actor; calling them anywhere else raises
a loud ``RuntimeError``. You can introspect at runtime with a loud ``RuntimeError``. You can introspect at runtime with
``tractor.current_actor().is_infected_aio()``. ``tractor.current_actor().is_infected_aio()``.
@ -229,7 +229,7 @@ dialog, skip the channel ceremony and use
It schedules the fn as an ``asyncio.Task``, waits for completion It schedules the fn as an ``asyncio.Task``, waits for completion
and hands the return value back to ``trio``; think of it as the and hands the return value back to ``trio``; think of it as the
cross-loop sibling of ``ActorNursery.run_in_actor()``. Errors and cross-loop sibling of ``tractor.to_actor.run()``. Errors and
cancellation are translated exactly as for channels. cancellation are translated exactly as for channels.
Cross-loop errors and cancellation Cross-loop errors and cancellation

View File

@ -64,11 +64,13 @@ What's going on here?
- three healthy actors are spawned as daemons via - three healthy actors are spawned as daemons via
:meth:`tractor.ActorNursery.start_actor`; left alone they'd :meth:`tractor.ActorNursery.start_actor`; left alone they'd
happily idle forever, happily idle forever,
- a fourth actor runs ``assert_err()`` via ``.run_in_actor()`` and - a fourth actor runs ``assert_err()`` via a blocking
promptly trips its ``assert 0``, ``tractor.to_actor.run()`` one-shot and promptly trips its
``assert 0``,
- the resulting ``AssertionError`` ships back over IPC as a - the resulting ``AssertionError`` ships back over IPC as a
serialized error msg and re-raises *boxed* inside the nursery serialized error msg and re-raises *boxed* right at the call
block as a :class:`tractor.RemoteActorError`, inside the nursery block as a
:class:`tractor.RemoteActorError`,
- the nursery reacts like any ``trio`` nursery would: it cancels - the nursery reacts like any ``trio`` nursery would: it cancels
the three healthy siblings (graceful runtime-cancel requests, the three healthy siblings (graceful runtime-cancel requests,
acks awaited), reaps all four processes, then re-raises, acks awaited), reaps all four processes, then re-raises,

View File

@ -15,8 +15,8 @@ a single `structured concurrency`_ (SC) scope over IPC.
:alt: sequence diagram of the context handshake msg flow :alt: sequence diagram of the context handshake msg flow
Pretty much everything else is (or is slated to be) built on this Pretty much everything else is (or is slated to be) built on this
one primitive: ``ActorNursery.run_in_actor()`` is a convenience one primitive: ``tractor.to_actor.run()`` is a convenience for
for "spawn, open a context, await the result, tear down"; plain "spawn, run the lone task, await the result, tear down"; plain
``Portal.run()`` RPC is planned to be re-implemented on top of it; ``Portal.run()`` RPC is planned to be re-implemented on top of it;
the multi-process debugger's tree-wide REPL lock rides one. Grok the multi-process debugger's tree-wide REPL lock rides one. Grok
this page and the rest of the library reads as convenience this page and the rest of the library reads as convenience

View File

@ -119,15 +119,16 @@ Run a func in a process
Even a pool can be overkill; "run this one async func in a Even a pool can be overkill; "run this one async func in a
subprocess and give me the result" is a one-liner via subprocess and give me the result" is a one-liner via
:meth:`tractor.ActorNursery.run_in_actor`, :func:`tractor.to_actor.run`,
.. literalinclude:: ../../examples/parallelism/single_func.py .. literalinclude:: ../../examples/parallelism/single_func.py
:caption: examples/parallelism/single_func.py :caption: examples/parallelism/single_func.py
:language: python :language: python
``run_in_actor()`` is a *convenience wrapper* — spawn an actor, run ``to_actor.run()`` is a *convenience wrapper* — spawn an actor,
exactly one task in it, reap on result — not the core spawning run exactly one task in it, block on and return its result, reap
model (that's :meth:`tractor.ActorNursery.start_actor` plus — not the core spawning model (that's
:meth:`tractor.ActorNursery.start_actor` plus
:meth:`tractor.Portal.open_context`; see :doc:`/guide/context`). :meth:`tractor.Portal.open_context`; see :doc:`/guide/context`).
But for this fire-and-collect shape it's exactly the right amount But for this fire-and-collect shape it's exactly the right amount
of typing. of typing.

View File

@ -80,28 +80,30 @@ One special namespace exists: ``'self'`` resolves to the remote
how internal machinery (cancel requests, registry ops) travels; how internal machinery (cancel requests, registry ops) travels;
don't build your app on it. don't build your app on it.
One-shot results: ``wait_for_result()`` One-shot subactors: ``to_actor.run()``
--------------------------------------- --------------------------------------
A portal returned from When a subactor's *entire job* is a single function call, skip
:meth:`~tractor.ActorNursery.run_in_actor` has exactly one the portal plumbing with :func:`tractor.to_actor.run`: spawn,
"main" task running remotely; that task's ``return`` value is run the lone task, return its result and reap the process — all
delivered as the portal's *final result*: in one blocking call:
.. code:: python .. code:: python
portal = await an.run_in_actor(fib, n=10) final = await tractor.to_actor.run(fib, an=an, n=10)
final = await portal.wait_for_result()
Semantics worth knowing: Semantics worth knowing:
- it blocks until the remote task returns, re-raising any - it blocks until the remote task returns, re-raising any
remote error in the usual boxed form. remote error in the usual boxed form right in the calling
- once resolved it's idempotent: later calls return the same task.
cached value. - "placement" is composable: ``an=`` spawns from an existing
- a *daemon* portal (from ``start_actor()``) has no main task, actor-nursery, ``portal=`` reuses an already-running actor
so there's no final result to wait for: you'll get a warning (no spawn/reap, just a ``Portal.run()``), and passing
plus a ``NoResult`` sentinel. Results of individual daemon neither opens a private call-scoped nursery (booting the
calls come straight back from each ``await portal.run()``. runtime if needed).
- concurrency composes the plain ``trio`` way: schedule
multiple ``run()`` calls into a local task nursery (see
``examples/parallelism/to_actor_one_shots.py``).
Pure RPC daemons: ``run_daemon()`` Pure RPC daemons: ``run_daemon()``
---------------------------------- ----------------------------------

View File

@ -103,19 +103,22 @@ What's going on here?
on him **forever**. Daemon lifetimes are *yours* to end; that on him **forever**. Daemon lifetimes are *yours* to end; that
explicitness is the point. explicitness is the point.
``run_in_actor()``: quick one-shot parallelism ``to_actor.run()``: quick one-shot parallelism
---------------------------------------------- ----------------------------------------------
:meth:`~tractor.ActorNursery.run_in_actor` is the convenience :func:`tractor.to_actor.run` is the convenience wrapper: spawn
wrapper: spawn an actor, run exactly one async function in it, an actor, run exactly one async function in it, block on the
then reap the process as soon as the result arrives. result, then reap the process — the distributed sibling of
``trio.to_thread.run_sync()``.
.. code:: python .. code:: python
async with tractor.open_nursery() as an: async with (
portal = await an.run_in_actor(burn_cpu) tractor.open_nursery() as an,
trio.open_nursery() as tn,
):
# burn rubber in the parent too... # burn rubber in the parent too...
await burn_cpu() tn.start_soon(burn_cpu)
total = await portal.wait_for_result() total = await tractor.to_actor.run(burn_cpu, an=an)
A few details worth knowing: A few details worth knowing:
@ -124,18 +127,21 @@ A few details worth knowing:
- 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.
- extra ``**kwargs`` are forwarded to the function itself. - extra ``**kwargs`` are forwarded to the function itself.
- the child is *auto-cancelled* once its "main" result lands; - the call blocks until the result (or error) lands and the
at nursery exit these run-once children are always reaped child is *auto-cancelled* (reaped) right after — so remote
first (causality_ is paramount!). errors raise directly in your calling task (causality_ is
paramount!).
- "placement" composes: ``an=`` spawns from a caller-managed
actor-nursery, ``portal=`` reuses an already-running actor
(no spawn/reap), and passing neither opens a private
call-scoped nursery (booting the runtime if needed).
.. note:: .. note::
``run_in_actor()`` is a convenience, **not** the core model. ``to_actor.run()`` is a convenience, **not** the core model —
The source literally marks it for an eventual rebuild as it's built *entirely* on ``start_actor()`` + ``Portal.run()``
a thin "hilevel" wrapper on top of + ``Portal.cancel_actor()``. Teach your fingers to use it for
:meth:`~tractor.Portal.open_context` (the modern inter-actor quick fire-and-collect parallelism — think a per-function
task API). Teach your fingers to use it for quick
fire-and-collect parallelism — think a per-function
trio-parallel_ style one-shot — and reach for trio-parallel_ style one-shot — and reach for
``start_actor()`` + ``open_context()`` for anything ``start_actor()`` + ``open_context()`` for anything
long-lived, stateful or streaming long-lived, stateful or streaming
@ -145,9 +151,9 @@ Actor lifetimes and teardown order
---------------------------------- ----------------------------------
So we have two lifetime flavors: So we have two lifetime flavors:
- **run-once** (``run_in_actor()``): 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. arrives back in the (blocking) call.
- **daemon** (``start_actor()``): lives until *someone* cancels - **daemon** (``start_actor()``): lives until *someone* cancels
it — an explicit ``await portal.cancel_actor()``, a bulk it — an explicit ``await portal.cancel_actor()``, a bulk
``await an.cancel()``, or the one-cancels-all strategy kicking ``await an.cancel()``, or the one-cancels-all strategy kicking
@ -155,11 +161,12 @@ So we have two lifetime flavors:
On a clean exit of the nursery block the teardown order is: On a clean exit of the nursery block the teardown order is:
1. the nursery waits on every run-once actor's final result; 1. one-shot actors never make it to nursery exit: each is
any errors from these are raised immediately so your code reaped inside its own ``to_actor.run()`` call, any error
(acting as supervisor) gets first crack at handling them. raising immediately in the calling task so your code
2. then it waits on daemon actors — **indefinitely**. If you (acting as supervisor) gets first crack at handling it.
spawned a daemon, you own its lifetime. 2. the nursery then waits on daemon actors — **indefinitely**.
If you spawned a daemon, you own its lifetime.
When a child *is* cancelled, teardown is graceful-first per SC When a child *is* cancelled, teardown is graceful-first per SC
discipline: the runtime sends an IPC cancel request and gives discipline: the runtime sends an IPC cancel request and gives

View File

@ -43,24 +43,20 @@ Run it::
What's going on here? What's going on here?
- ``trio.run(main)`` starts the **root actor**; the ``tractor`` - ``trio.run(main)`` starts the **root actor**; the ``tractor``
runtime boots *implicitly* inside ``tractor.open_nursery()`` runtime boots *implicitly* inside ``tractor.to_actor.run()``
whenever it isn't already up. No special entrypoint, no whenever it isn't already up. No special entrypoint, no
framework takeover - it's just a ``trio`` app, framework takeover - it's just a ``trio`` app,
- inside ``main()`` a *subactor* is spawned via - inside ``main()`` a *subactor* is spawned via
``ActorNursery.run_in_actor()`` and told to run exactly one ``tractor.to_actor.run()`` and told to run exactly one
function: ``cellar_door()``, function: ``cellar_door()``,
- you get back a ``Portal``: your handle for invoking tasks in
the new process's (separate!) memory domain. We lean on it
much harder in the next section,
- the subactor, *some_linguist*, boots a fresh ``trio.run()`` in - the subactor, *some_linguist*, boots a fresh ``trio.run()`` in
a **new process** and executes ``cellar_door()`` as its *main a **new process** and executes ``cellar_door()`` as its *main
task* (note the child proving it is *not* the root with task* (note the child proving it is *not* the root with
``tractor.is_root_process()``), then ships the return value ``tractor.is_root_process()``), then ships the return value
back over IPC, back over IPC,
- the parent grabs that *final result* with - the call *blocks* until that final result arrives, then
``await portal.wait_for_result()``, much like you'd expect returns it - causality is preserved: your task only proceeds
from a "future" - except causality is preserved: the nursery once the child is *done*, dead, and reaped.
block only exits once the child is *done*, dead, and reaped.
.. margin:: Just need a worker pool? .. margin:: Just need a worker pool?
@ -71,19 +67,22 @@ What's going on here?
.. note:: .. note::
``run_in_actor()`` is the *convenience* wrapper: one-shot ``to_actor.run()`` (parlance of ``trio.to_thread`` and
friends) is the *convenience* wrapper: one-shot
spawn-run-reap semantics for when a subactor's entire job is spawn-run-reap semantics for when a subactor's entire job is
a single function call. The core primitives are a single function call. The core primitives are
``ActorNursery.start_actor()`` (next up) paired with ``ActorNursery.start_actor()`` (next up) — which hands you
a ``Portal``, your handle for invoking tasks in the new
process's (separate!) memory domain — paired with
``Portal.open_context()`` for full, SC-linked cross-actor ``Portal.open_context()`` for full, SC-linked cross-actor
dialogs - see :doc:`/guide/context`. dialogs - see :doc:`/guide/context`.
Daemon actors and RPC Daemon actors and RPC
--------------------- ---------------------
A ``run_in_actor()``-spawned actor terminates when its main task A ``to_actor.run()`` one-shot subactor terminates when its lone
returns. But often you want long-lived *daemon* actors instead: task returns. But often you want long-lived *daemon* actors
spawned once, then serving (allowlisted) RPC requests until told instead: spawned once, then serving (allowlisted) RPC requests
otherwise. That's ``start_actor()``: until told otherwise. That's ``start_actor()``:
.. literalinclude:: ../../examples/actor_spawning_and_causality_with_daemon.py .. literalinclude:: ../../examples/actor_spawning_and_causality_with_daemon.py
:caption: examples/actor_spawning_and_causality_with_daemon.py :caption: examples/actor_spawning_and_causality_with_daemon.py
@ -91,9 +90,9 @@ otherwise. That's ``start_actor()``:
Two lifetime rules to internalize: Two lifetime rules to internalize:
- a ``run_in_actor()`` actor lives exactly as long as its main - a ``to_actor.run()`` one-shot actor lives exactly as long as
task; the nursery waits for that function (and thus the its lone task; the call blocks until that function (and thus
process) to complete before unblocking, the process) completes,
- a ``start_actor()`` actor *lives forever* - an RPC daemon the - a ``start_actor()`` actor *lives forever* - an RPC daemon the
nursery will happily wait on **indefinitely** - until some nursery will happily wait on **indefinitely** - until some
task explicitly cancels it via ``Portal.cancel_actor()`` (as task explicitly cancels it via ``Portal.cancel_actor()`` (as