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-codedrop_ria_nursery
parent
a3057cb24f
commit
d6bed7c4a0
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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()``
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue