diff --git a/docs/api/core.rst b/docs/api/core.rst index 4ce5ccc2..54d999af 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -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 diff --git a/docs/guide/clustering.rst b/docs/guide/clustering.rst index 61af2cc0..be57b93c 100644 --- a/docs/guide/clustering.rst +++ b/docs/guide/clustering.rst @@ -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. diff --git a/docs/guide/context.rst b/docs/guide/context.rst index 23dce9aa..9da65b8e 100644 --- a/docs/guide/context.rst +++ b/docs/guide/context.rst @@ -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 --------------------- diff --git a/docs/guide/rpc.rst b/docs/guide/rpc.rst index 5b76d824..fbb4fd97 100644 --- a/docs/guide/rpc.rst +++ b/docs/guide/rpc.rst @@ -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 `), 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 diff --git a/docs/guide/spawning.rst b/docs/guide/spawning.rst index f56ad4ec..28842bf6 100644 --- a/docs/guide/spawning.rst +++ b/docs/guide/spawning.rst @@ -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 diff --git a/docs/start/quickstart.rst b/docs/start/quickstart.rst index 35d553fc..ccaf7ff9 100644 --- a/docs/start/quickstart.rst +++ b/docs/start/quickstart.rst @@ -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 diff --git a/nooz/477.feature.rst b/nooz/477.feature.rst new file mode 100644 index 00000000..5e8ff6e3 --- /dev/null +++ b/nooz/477.feature.rst @@ -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.