Clarify `to_actor.run()` ownership
The guides described every placement as spawn-run-reap and omitted the trampoline allowlist required when reusing an existing actor. - distinguish call-owned children from caller-owned portal actors - document stable module-global target addresses and allowlists - add the #477 feature news fragment (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))wkt/to_actor_subpkg
parent
086962ca36
commit
a849161fa5
|
|
@ -54,15 +54,16 @@ One-shot task actors
|
|||
|
||||
.. 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`, a linked
|
||||
:meth:`Portal.open_context` call and per-child cancellation/reaping,
|
||||
so don't design around it as the core model. It supersedes the
|
||||
legacy, non-blocking ``ActorNursery.run_in_actor()`` retained only
|
||||
for compatibility until its removal in PR #484.
|
||||
Without ``portal=``, :func:`tractor.to_actor.run` (parlance of
|
||||
``trio.to_thread.run_sync()`` and friends) is the convenience
|
||||
one-shot: spawn, run one task, block on its result and reap. It
|
||||
combines :meth:`ActorNursery.start_actor`, a linked
|
||||
:meth:`Portal.open_context` call and per-child reaping. With
|
||||
``portal=`` it owns only the linked task and leaves the existing
|
||||
actor's lifetime to the portal owner; that actor must expose both
|
||||
the target module and ``tractor.to_actor.MODULE``. It supersedes
|
||||
the legacy, non-blocking ``ActorNursery.run_in_actor()`` retained
|
||||
only for compatibility until its removal in PR #484.
|
||||
|
||||
.. deprecated:: 0.1.0a6
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,10 @@ one kwarg away,
|
|||
.. code:: python
|
||||
|
||||
async with tractor.open_actor_cluster(
|
||||
modules=['mylib.workers'],
|
||||
modules=[
|
||||
'mylib.workers',
|
||||
tractor.to_actor.MODULE,
|
||||
],
|
||||
count=4,
|
||||
names=['scout', 'miner', 'smelter', 'smith'],
|
||||
debug_mode=True, # whole-fleet crash-to-REPL
|
||||
|
|
@ -71,9 +74,11 @@ one kwarg away,
|
|||
|
||||
From here the composition patterns are the usual ``tractor`` fare:
|
||||
``portal.run()`` for bare one-shot RPCs (as in the demo),
|
||||
``tractor.to_actor.run(..., portal=portal)`` for linked one-shot calls,
|
||||
or — for a persistent bidirectional dialog per worker — concurrently
|
||||
enter N ``portal.open_context()`` blocks with
|
||||
``tractor.to_actor.run(..., portal=portal)`` for cancellation-linked
|
||||
one-shot tasks in an existing worker (include
|
||||
``tractor.to_actor.MODULE`` in ``modules``; the cluster still owns
|
||||
the worker's lifetime), or — for a persistent bidirectional dialog
|
||||
per worker — concurrently enter N ``portal.open_context()`` blocks with
|
||||
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
|
||||
for that whole layer.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ a single `structured concurrency`_ (SC) scope over IPC.
|
|||
:alt: sequence diagram of the context handshake msg flow
|
||||
|
||||
Pretty much everything else is (or is slated to be) built on this
|
||||
one primitive: ``tractor.to_actor.run()`` is a convenience for
|
||||
"spawn, run the lone task, await the result, tear down"; plain
|
||||
``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
|
||||
this page and the rest of the library reads as convenience
|
||||
wrappers B)
|
||||
one primitive: ``tractor.to_actor.run()`` uses it for a linked
|
||||
one-shot task, spawning and reaping an actor only when no ``portal=``
|
||||
is supplied; plain ``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 this page and the rest of the library reads
|
||||
as convenience wrappers B)
|
||||
|
||||
The endpoint contract
|
||||
---------------------
|
||||
|
|
|
|||
|
|
@ -82,10 +82,9 @@ don't build your app on it.
|
|||
|
||||
One-shot subactors: ``to_actor.run()``
|
||||
--------------------------------------
|
||||
When a subactor's *entire job* is a single function call, skip
|
||||
the portal plumbing with :func:`tractor.to_actor.run`: spawn,
|
||||
run the lone task, return its result and reap the process — all
|
||||
in one blocking call:
|
||||
When the call should own a fresh subactor whose entire job is one
|
||||
function call, :func:`tractor.to_actor.run` spawns it, runs the task,
|
||||
returns its result and reaps the process — all in one blocking call:
|
||||
|
||||
.. code:: python
|
||||
|
||||
|
|
@ -101,16 +100,37 @@ Semantics worth knowing:
|
|||
- it blocks until the remote task returns, re-raising any
|
||||
remote error in the usual boxed form right in the calling
|
||||
task.
|
||||
- "placement" is composable: ``an=`` spawns from an existing
|
||||
actor-nursery, ``portal=`` reuses an already-running actor
|
||||
(no spawn/reap, just a linked
|
||||
:meth:`~tractor.Portal.open_context` call; see the
|
||||
:doc:`context guide </guide/context>`), and passing neither
|
||||
opens a private call-scoped nursery (booting the runtime if needed).
|
||||
- placement also determines process ownership: ``an=`` spawns and
|
||||
reaps a fresh child in an existing actor nursery, while passing
|
||||
neither does the same in a private call-scoped nursery (booting
|
||||
the runtime if needed). ``portal=`` instead runs one linked task
|
||||
in an existing actor; it neither spawns nor reaps that actor, so
|
||||
the portal's owner remains responsible for its lifetime.
|
||||
- concurrency composes the plain ``trio`` way: schedule
|
||||
multiple ``run()`` calls into a local task nursery (see
|
||||
``examples/parallelism/to_actor_one_shots.py``).
|
||||
|
||||
A reused actor must expose both the target module and the
|
||||
``to_actor`` context trampoline:
|
||||
|
||||
.. code:: python
|
||||
|
||||
async with tractor.open_nursery() as an:
|
||||
portal = await an.start_actor(
|
||||
'worker',
|
||||
enable_modules=[
|
||||
__name__,
|
||||
tractor.to_actor.MODULE,
|
||||
],
|
||||
)
|
||||
try:
|
||||
final = await tractor.to_actor.run(
|
||||
partial(fib, n=10),
|
||||
portal=portal,
|
||||
)
|
||||
finally:
|
||||
await portal.cancel_actor()
|
||||
|
||||
Pure RPC daemons: ``run_daemon()``
|
||||
----------------------------------
|
||||
When a process's *only* job is to sit at the root of its own
|
||||
|
|
|
|||
|
|
@ -105,9 +105,9 @@ What's going on here?
|
|||
|
||||
``to_actor.run()``: quick one-shot parallelism
|
||||
----------------------------------------------
|
||||
:func:`tractor.to_actor.run` is the convenience wrapper: spawn
|
||||
an actor, run exactly one async function in it, block on the
|
||||
result, then reap the process — the distributed sibling of
|
||||
Without ``portal=``, :func:`tractor.to_actor.run` is the convenience
|
||||
wrapper: spawn an actor, run exactly one async function in it, block
|
||||
on the result, then reap the process — the distributed sibling of
|
||||
``trio.to_thread.run_sync()``.
|
||||
|
||||
.. code:: python
|
||||
|
|
@ -126,6 +126,10 @@ A few details worth knowing:
|
|||
``name='something_cuter'``.
|
||||
- the function's module is auto-added to the child's
|
||||
``enable_modules`` allowlist.
|
||||
- the target must be a module-global async function, or a
|
||||
``functools.partial`` thereof. Nested functions, methods and callable
|
||||
objects have no stable ``module:name`` RPC address and are rejected
|
||||
before actor startup.
|
||||
- target arguments are positional; use ``functools.partial()``
|
||||
to bind target keyword arguments. Keywords passed directly to
|
||||
``run()`` configure actor placement and spawning.
|
||||
|
|
@ -133,18 +137,23 @@ A few details worth knowing:
|
|||
child is *auto-cancelled* (reaped) right after — so remote
|
||||
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).
|
||||
- "placement" composes: ``an=`` spawns a call-owned child from an
|
||||
existing actor nursery, while passing neither opens a private
|
||||
call-scoped nursery. ``portal=`` instead reuses an existing actor:
|
||||
the call scopes only its linked remote task, neither spawns nor
|
||||
reaps the actor, and leaves its lifetime with the portal's owner.
|
||||
That actor must expose both the target module and
|
||||
``tractor.to_actor.MODULE``.
|
||||
|
||||
.. note::
|
||||
|
||||
:func:`tractor.to_actor.run` is a convenience, **not** the core
|
||||
model — it's built *entirely* on
|
||||
:meth:`~tractor.ActorNursery.start_actor` plus a linked
|
||||
:meth:`~tractor.Portal.open_context` call and per-child
|
||||
cancellation/reaping. Teach your fingers to use it for quick
|
||||
model. For actor-owning placements it combines
|
||||
:meth:`~tractor.ActorNursery.start_actor`, a linked
|
||||
:meth:`~tractor.Portal.open_context` call, and per-child
|
||||
cancellation/reaping. With ``portal=`` it uses only the linked
|
||||
context call and leaves the existing actor's lifetime untouched.
|
||||
Teach your fingers to use it for quick
|
||||
fire-and-collect parallelism — think a per-function trio-parallel_
|
||||
style one-shot — and reach for
|
||||
:meth:`~tractor.ActorNursery.start_actor` plus
|
||||
|
|
@ -153,25 +162,25 @@ A few details worth knowing:
|
|||
|
||||
Actor lifetimes and teardown order
|
||||
----------------------------------
|
||||
So we have two lifetime flavors:
|
||||
There are two actor-lifetime flavors:
|
||||
|
||||
- **one-shot** (``to_actor.run()``): lives exactly as long as
|
||||
its single task; reaped the moment its result (or error)
|
||||
arrives back in the (blocking) call.
|
||||
- **daemon** (:meth:`~tractor.ActorNursery.start_actor`): lives
|
||||
until *someone* cancels it — an explicit
|
||||
- **call-owned one-shot** (``to_actor.run()`` without ``portal=``):
|
||||
spawned for one task, then cancelled and joined before ``run()``
|
||||
returns its result or raises its error.
|
||||
- **caller-owned daemon** (:meth:`~tractor.ActorNursery.start_actor`),
|
||||
including an actor later reused through
|
||||
``to_actor.run(..., portal=portal)``: lives until *someone*
|
||||
cancels it via an explicit
|
||||
:meth:`~tractor.Portal.cancel_actor`, a bulk
|
||||
:meth:`~tractor.ActorNursery.cancel`, or the one-cancels-all
|
||||
strategy kicking in on error.
|
||||
|
||||
On a clean exit of the nursery block the teardown order is:
|
||||
|
||||
1. one-shot actors never make it to nursery exit: each is
|
||||
reaped inside its own ``to_actor.run()`` call, any error
|
||||
raising immediately in the calling task so your code
|
||||
(acting as supervisor) gets first crack at handling it.
|
||||
2. the nursery then waits on daemon actors — **indefinitely**.
|
||||
If you spawned a daemon, you own its lifetime.
|
||||
1. call-owned actors do not survive their own ``to_actor.run()``
|
||||
calls; each is reaped before its call returns.
|
||||
2. the nursery waits on caller-owned daemon actors
|
||||
**indefinitely**. If you spawned one, you own its lifetime.
|
||||
|
||||
When a child *is* cancelled, teardown is graceful-first per SC
|
||||
discipline: the runtime sends an IPC cancel request and gives
|
||||
|
|
|
|||
|
|
@ -43,15 +43,16 @@ Run it::
|
|||
What's going on here?
|
||||
|
||||
- ``trio.run(main)`` starts the **root actor**; the ``tractor``
|
||||
runtime boots *implicitly* inside ``tractor.to_actor.run()``
|
||||
whenever it isn't already up. No special entrypoint, no
|
||||
framework takeover - it's just a ``trio`` app,
|
||||
runtime boots *implicitly* inside this ``tractor.to_actor.run()``
|
||||
call because neither ``an=`` nor ``portal=`` was supplied. No
|
||||
special entrypoint, no framework takeover - it's just a ``trio``
|
||||
app,
|
||||
- inside ``main()`` a *subactor* is spawned via
|
||||
``tractor.to_actor.run()`` and told to run exactly one
|
||||
function: ``cellar_door()``,
|
||||
- the subactor, *some_linguist*, boots a fresh ``trio.run()`` in
|
||||
a **new process** and executes ``cellar_door()`` as its *main
|
||||
task* (note the child proving it is *not* the root with
|
||||
a **new process** and executes ``cellar_door()`` as its linked
|
||||
one-shot task (note the child proving it is *not* the root with
|
||||
``tractor.is_root_process()``), then ships the return value
|
||||
back over IPC,
|
||||
- the call *blocks* until that final result arrives, then
|
||||
|
|
@ -67,10 +68,10 @@ What's going on here?
|
|||
|
||||
.. note::
|
||||
|
||||
``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
|
||||
a single function call. The core primitives are
|
||||
Without ``portal=``, ``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 a single function call. The core primitives are
|
||||
:meth:`~tractor.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
|
||||
|
|
@ -79,10 +80,10 @@ What's going on here?
|
|||
|
||||
Daemon actors and RPC
|
||||
---------------------
|
||||
A ``to_actor.run()`` one-shot subactor terminates when its lone
|
||||
task returns. But often you want long-lived *daemon* actors
|
||||
instead: spawned once, then serving (allowlisted) RPC requests
|
||||
until told otherwise. That's ``start_actor()``:
|
||||
A subactor spawned by ``to_actor.run()`` terminates after its lone
|
||||
task returns. But often you want long-lived *daemon* actors instead:
|
||||
spawned once, then serving (allowlisted) RPC requests until told
|
||||
otherwise. That's ``start_actor()``:
|
||||
|
||||
.. literalinclude:: ../../examples/actor_spawning_and_causality_with_daemon.py
|
||||
:caption: examples/actor_spawning_and_causality_with_daemon.py
|
||||
|
|
@ -90,14 +91,17 @@ until told otherwise. That's ``start_actor()``:
|
|||
|
||||
Two lifetime rules to internalize:
|
||||
|
||||
- a ``to_actor.run()`` one-shot actor lives exactly as long as
|
||||
its lone task; the call blocks until that function (and thus
|
||||
the process) completes,
|
||||
- a subactor spawned and owned by ``to_actor.run()`` is cancelled
|
||||
and reaped before the call returns its result or raises its error,
|
||||
- a ``start_actor()`` actor *lives forever* - an RPC daemon the
|
||||
nursery will happily wait on **indefinitely** - until some
|
||||
task explicitly cancels it via ``Portal.cancel_actor()`` (as
|
||||
above), or its parent nursery is cancelled wholesale.
|
||||
|
||||
Passing ``portal=`` is different: the call owns only the linked
|
||||
remote task. It neither spawns nor reaps the existing actor; the
|
||||
portal's owner must end that actor's lifetime.
|
||||
|
||||
.. tip::
|
||||
|
||||
Want your *entire program* to just be a long-lived RPC
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
Add ``tractor.to_actor.run()`` for Trio-style one-shot async calls in
|
||||
new or existing actors, with caller-scoped result/error propagation,
|
||||
linked cancellation, and deterministic reaping of call-owned children.
|
||||
Loading…
Reference in New Issue