From d6bed7c4a084e5d808f80d74069aa1ddbe4621ce Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 6 Jul 2026 12:31:02 -0400 Subject: [PATCH] Port docs off `run_in_actor` + `Portal.wait_for_result` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/api/core.rst | 25 +++++++++++------ docs/guide/asyncio.rst | 6 ++-- docs/guide/cancellation.rst | 10 ++++--- docs/guide/context.rst | 4 +-- docs/guide/parallelism.rst | 9 +++--- docs/guide/rpc.rst | 32 +++++++++++---------- docs/guide/spawning.rst | 55 +++++++++++++++++++++---------------- docs/start/quickstart.rst | 35 ++++++++++++----------- 8 files changed, 98 insertions(+), 78 deletions(-) diff --git a/docs/api/core.rst b/docs/api/core.rst index 196da2d0..e57ba5c7 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -37,7 +37,6 @@ Spawning actors .. autoclass:: ActorNursery :members: start_actor, - run_in_actor, cancel, cancel_called, cancelled_caught @@ -47,10 +46,22 @@ Spawning actors :meth:`ActorNursery.start_actor` (daemon actor + portal) is the blessed spawning primitive; pair it with ``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 - to be rebuilt as a high-level wrapper, so don't design around - it as the core model. + +One-shot task actors +-------------------- + +.. 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 @@ -71,14 +82,12 @@ flowing back `exactly like trio`_. :members: run, run_from_ns, open_stream_from, - wait_for_result, cancel_actor, chan .. deprecated:: 0.1.0a6 - ``Portal.result()`` warns; use :meth:`Portal.wait_for_result`. - The str-form ``Portal.run('mod.path', 'fn_name')`` also warns; + The str-form ``Portal.run('mod.path', 'fn_name')`` warns; pass a function *object* whose module is listed in the target's ``enable_modules``. ``Portal.channel`` is the legacy spelling of :attr:`Portal.chan`. diff --git a/docs/guide/asyncio.rst b/docs/guide/asyncio.rst index 27e1d05f..76798356 100644 --- a/docs/guide/asyncio.rst +++ b/docs/guide/asyncio.rst @@ -76,8 +76,8 @@ Just flip the flag on :meth:`tractor.ActorNursery.start_actor`: infect_asyncio=True, ) -The one-shot convenience ``ActorNursery.run_in_actor()`` accepts -the same flag. The ``to_asyncio`` APIs may **only** be called from +The one-shot convenience ``tractor.to_actor.run()`` accepts the +same flag. The ``to_asyncio`` APIs may **only** be called from tasks inside an infected actor; calling them anywhere else raises a loud ``RuntimeError``. You can introspect at runtime with ``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 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. Cross-loop errors and cancellation diff --git a/docs/guide/cancellation.rst b/docs/guide/cancellation.rst index 56f0d6d1..3b97b380 100644 --- a/docs/guide/cancellation.rst +++ b/docs/guide/cancellation.rst @@ -64,11 +64,13 @@ What's going on here? - three healthy actors are spawned as daemons via :meth:`tractor.ActorNursery.start_actor`; left alone they'd happily idle forever, -- a fourth actor runs ``assert_err()`` via ``.run_in_actor()`` and - promptly trips its ``assert 0``, +- a fourth actor runs ``assert_err()`` via a blocking + ``tractor.to_actor.run()`` one-shot and promptly trips its + ``assert 0``, - the resulting ``AssertionError`` ships back over IPC as a - serialized error msg and re-raises *boxed* inside the nursery - block as a :class:`tractor.RemoteActorError`, + serialized error msg and re-raises *boxed* right at the call + inside the nursery block as a + :class:`tractor.RemoteActorError`, - the nursery reacts like any ``trio`` nursery would: it cancels the three healthy siblings (graceful runtime-cancel requests, acks awaited), reaps all four processes, then re-raises, diff --git a/docs/guide/context.rst b/docs/guide/context.rst index 15ffc00d..23dce9aa 100644 --- a/docs/guide/context.rst +++ b/docs/guide/context.rst @@ -15,8 +15,8 @@ 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: ``ActorNursery.run_in_actor()`` is a convenience -for "spawn, open a context, await the result, tear down"; plain +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 diff --git a/docs/guide/parallelism.rst b/docs/guide/parallelism.rst index 7a6dc1c2..e688a26b 100644 --- a/docs/guide/parallelism.rst +++ b/docs/guide/parallelism.rst @@ -119,15 +119,16 @@ Run a func in a process Even a pool can be overkill; "run this one async func in a 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 :caption: examples/parallelism/single_func.py :language: python -``run_in_actor()`` is a *convenience wrapper* — spawn an actor, run -exactly one task in it, reap on result — not the core spawning -model (that's :meth:`tractor.ActorNursery.start_actor` plus +``to_actor.run()`` is a *convenience wrapper* — spawn an actor, +run exactly one task in it, block on and return its result, reap +— not the core spawning model (that's +:meth:`tractor.ActorNursery.start_actor` plus :meth:`tractor.Portal.open_context`; see :doc:`/guide/context`). But for this fire-and-collect shape it's exactly the right amount of typing. diff --git a/docs/guide/rpc.rst b/docs/guide/rpc.rst index 6c5d8fa0..29cef43c 100644 --- a/docs/guide/rpc.rst +++ b/docs/guide/rpc.rst @@ -80,28 +80,30 @@ One special namespace exists: ``'self'`` resolves to the remote how internal machinery (cancel requests, registry ops) travels; don't build your app on it. -One-shot results: ``wait_for_result()`` ---------------------------------------- -A portal returned from -:meth:`~tractor.ActorNursery.run_in_actor` has exactly one -"main" task running remotely; that task's ``return`` value is -delivered as the portal's *final result*: +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: .. code:: python - portal = await an.run_in_actor(fib, n=10) - final = await portal.wait_for_result() + final = await tractor.to_actor.run(fib, an=an, n=10) Semantics worth knowing: - it blocks until the remote task returns, re-raising any - remote error in the usual boxed form. -- once resolved it's idempotent: later calls return the same - cached value. -- a *daemon* portal (from ``start_actor()``) has no main task, - so there's no final result to wait for: you'll get a warning - plus a ``NoResult`` sentinel. Results of individual daemon - calls come straight back from each ``await portal.run()``. + 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 ``Portal.run()``), and passing + neither opens a private call-scoped nursery (booting the + 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()`` ---------------------------------- diff --git a/docs/guide/spawning.rst b/docs/guide/spawning.rst index f1bbf26f..8f01bc1a 100644 --- a/docs/guide/spawning.rst +++ b/docs/guide/spawning.rst @@ -103,19 +103,22 @@ What's going on here? on him **forever**. Daemon lifetimes are *yours* to end; that 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 -wrapper: spawn an actor, run exactly one async function in it, -then reap the process as soon as the result arrives. +: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 - async with tractor.open_nursery() as an: - portal = await an.run_in_actor(burn_cpu) + async with ( + tractor.open_nursery() as an, + trio.open_nursery() as tn, + ): # burn rubber in the parent too... - await burn_cpu() - total = await portal.wait_for_result() + tn.start_soon(burn_cpu) + total = await tractor.to_actor.run(burn_cpu, an=an) 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 ``enable_modules`` allowlist. - extra ``**kwargs`` are forwarded to the function itself. -- the child is *auto-cancelled* once its "main" result lands; - at nursery exit these run-once children are always reaped - first (causality_ is paramount!). +- the call blocks until the result (or error) lands and the + 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). .. note:: - ``run_in_actor()`` is a convenience, **not** the core model. - The source literally marks it for an eventual rebuild as - a thin "hilevel" wrapper on top of - :meth:`~tractor.Portal.open_context` (the modern inter-actor - task API). Teach your fingers to use it for quick - fire-and-collect parallelism — think a per-function + ``to_actor.run()`` is a convenience, **not** the core model — + it's built *entirely* on ``start_actor()`` + ``Portal.run()`` + + ``Portal.cancel_actor()``. Teach your fingers to use it for + quick fire-and-collect parallelism — think a per-function trio-parallel_ style one-shot — and reach for ``start_actor()`` + ``open_context()`` for anything long-lived, stateful or streaming @@ -145,9 +151,9 @@ Actor lifetimes and teardown order ---------------------------------- 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) - arrives. + arrives back in the (blocking) call. - **daemon** (``start_actor()``): lives until *someone* cancels it — an explicit ``await portal.cancel_actor()``, a bulk ``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: -1. the nursery waits on every run-once actor's final result; - any errors from these are raised immediately so your code - (acting as supervisor) gets first crack at handling them. -2. then it waits on daemon actors — **indefinitely**. If you - spawned a daemon, you own its lifetime. +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. 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 fabf4d8e..3f2e5710 100644 --- a/docs/start/quickstart.rst +++ b/docs/start/quickstart.rst @@ -43,24 +43,20 @@ Run it:: What's going on here? - ``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 framework takeover - it's just a ``trio`` app, - 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()``, -- 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 a **new process** and executes ``cellar_door()`` as its *main task* (note the child proving it is *not* the root with ``tractor.is_root_process()``), then ships the return value back over IPC, -- the parent grabs that *final result* with - ``await portal.wait_for_result()``, much like you'd expect - from a "future" - except causality is preserved: the nursery - block only exits once the child is *done*, dead, and reaped. +- the call *blocks* until that final result arrives, then + returns it - causality is preserved: your task only proceeds + once the child is *done*, dead, and reaped. .. margin:: Just need a worker pool? @@ -71,19 +67,22 @@ What's going on here? .. 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 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 dialogs - see :doc:`/guide/context`. Daemon actors and RPC --------------------- -A ``run_in_actor()``-spawned actor terminates when its main 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 ``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()``: .. literalinclude:: ../../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: -- a ``run_in_actor()`` actor lives exactly as long as its main - task; the nursery waits for that function (and thus the - process) to complete before unblocking, +- 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 ``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