From 40d2d9942e9f5f1dc40d5a6550b5ccc2bc63fb26 Mon Sep 17 00:00:00 2001 From: goodboy Date: Wed, 19 Aug 2026 23:29:02 -0400 Subject: [PATCH] Showcase `to_actor.run()` across docs The rendered guides and executable examples still taught the legacy `ActorNursery.run_in_actor()` result-portal model even though #481 adds its blocking, linked-context replacement. Deats, - migrate one-shots to direct results through `to_actor.run()` - preserve named target inputs with `functools.partial()` - use daemon actors where reciprocal dialogs need longer lifetimes - link the new API from core, asyncio and clustering references - retain only explicit legacy/removal notes Prompt-IO: ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.md (this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`)) --- .../20260820T023005Z_88a23449_prompt_io.md | 40 ++++++++ ...20260820T023005Z_88a23449_prompt_io.raw.md | 27 ++++++ docs/api/core.rst | 28 ++++-- docs/api/index.rst | 6 +- docs/api/to_asyncio.rst | 5 +- docs/guide/asyncio.rst | 6 +- docs/guide/cancellation.rst | 10 +- docs/guide/clustering.rst | 11 ++- docs/guide/context.rst | 4 +- docs/guide/index.rst | 4 +- docs/guide/parallelism.rst | 9 +- docs/guide/rpc.rst | 54 +++++++---- docs/guide/spawning.rst | 88 ++++++++++-------- docs/start/quickstart.rst | 53 ++++++----- examples/a_trynamic_first_scene.py | 42 ++++++--- examples/actor_spawning_and_causality.py | 13 +-- ...ed_subactors_error_up_through_nurseries.py | 92 +++++++++++-------- .../debugging/multi_subactor_root_errors.py | 51 +++++----- examples/debugging/multi_subactors.py | 42 +++++++-- ...root_cancelled_but_child_is_in_tty_lock.py | 63 ++++++++----- .../root_timeout_while_child_crashed.py | 23 +++-- examples/debugging/shielded_pause.py | 6 +- examples/debugging/subactor_breakpoint.py | 8 +- examples/debugging/subactor_error.py | 12 +-- examples/debugging/sync_bp.py | 2 +- examples/parallelism/single_func.py | 16 ++-- examples/remote_error_propagation.py | 13 +-- 27 files changed, 457 insertions(+), 271 deletions(-) create mode 100644 ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.md create mode 100644 ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.raw.md diff --git a/ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.md b/ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.md new file mode 100644 index 00000000..7ecdbba8 --- /dev/null +++ b/ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.md @@ -0,0 +1,40 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3 +timestamp: 2026-08-20T02:30:05Z +git_ref: 88a23449 +scope: docs +substantive: true +raw_file: 20260820T023005Z_88a23449_prompt_io.raw.md +--- + +## Prompt + +Audit all documentation and executable examples once more, replacing +prescriptive `run_in_actor()` usage with `to_actor.run()` or explicit +actor/context lifetime APIs before PR #481 lands. + +## Response summary + +Rewrote one-shot documentation around direct blocking result delivery, +linked context execution and per-call reaping. Migrated all runnable +examples, using daemon actors where reciprocal dialogs require longer +lifetimes. Added API/guide cross-links and retained only three explicit +legacy references. + +## Files changed + +- `docs/` - update API, quickstart and subsystem guides to showcase + `tractor.to_actor.run()` and link its underlying core APIs. +- `examples/` - migrate one-shot calls and preserve explicit daemon + lifetimes for reciprocal or long-lived actor dialogs. + +## Human edits + +The human requested a final docs pass covering every place that should +showcase `to_actor` over `.run_in_actor()`. Earlier review also required +named target arguments to remain visible through `functools.partial()` +and core API references to link to local guides/reference pages. These +were human-directed agent edits; the human made no direct source-line +edits. diff --git a/ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.raw.md b/ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.raw.md new file mode 100644 index 00000000..04cbd3e0 --- /dev/null +++ b/ai/prompt-io/opencode/20260820T023005Z_88a23449_prompt_io.raw.md @@ -0,0 +1,27 @@ +--- +model: openai/gpt-5.6-sol +service: opencode +timestamp: 2026-08-20T02:30:05Z +git_ref: 88a23449 +diff_cmd: git diff HEAD~1..HEAD +--- + +Perform a final rendered-documentation and executable-example pass so +PR #481 showcases `tractor.to_actor.run()` instead of the legacy +`ActorNursery.run_in_actor()` API. + +> `git diff HEAD~1..HEAD -- docs examples` + +Migrate one-shot guides and examples to direct result delivery through +`to_actor.run()`, preserving named target inputs with target partials. +Use daemon actors and concurrent portal calls where reciprocal actor +lifetimes require both peers to coexist. Add API and guide cross-links, +and retain only explicit legacy/removal notes. + +Verification: + +- executable docs examples: `23 passed` +- debugger/tooling TCP: `39 passed, 6 skipped` +- debugger/tooling UDS: `39 passed, 6 skipped` +- Ruff, compilation and `git diff --check`: clean +- local Sphinx build unavailable because Sphinx is not installed diff --git a/docs/api/core.rst b/docs/api/core.rst index 196da2d0..4ce5ccc2 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 @@ -46,11 +45,24 @@ 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. + :meth:`Portal.open_context` for SC-linked remote tasks. + +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`, 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. .. deprecated:: 0.1.0a6 @@ -71,14 +83,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/api/index.rst b/docs/api/index.rst index f2b90900..5782a8fe 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -5,8 +5,9 @@ This is the curated reference for ``tractor``'s public surface: the names you can import and lean on without reading runtime internals. Everything below is re-exported at the top level (``import tractor``) unless a page says otherwise; subsystems like -``tractor.msg``, ``tractor.trionics``, ``tractor.to_asyncio``, -``tractor.devx`` and ``tractor.log`` are importable as submodules. +``tractor.msg``, ``tractor.trionics``, ``tractor.to_actor``, +``tractor.to_asyncio``, ``tractor.devx`` and ``tractor.log`` are +importable as submodules. ``tractor`` is "just trio_" extended across processes: every API here is designed to keep the structured concurrency (SC) rules you @@ -23,6 +24,7 @@ Most-used names at a glance: open_root_actor open_nursery + to_actor.run run_daemon ActorNursery Portal diff --git a/docs/api/to_asyncio.rst b/docs/api/to_asyncio.rst index 05baad9a..3ccfd30e 100644 --- a/docs/api/to_asyncio.rst +++ b/docs/api/to_asyncio.rst @@ -30,11 +30,12 @@ Starting asyncio tasks from trio .. note:: :func:`open_channel_from` mirrors the - ``Portal.open_context()`` handshake: the asyncio side calls + :meth:`tractor.Portal.open_context` handshake: the asyncio side calls ``chan.started_nowait(value)`` and that value pops out as ``first`` on the trio side. :func:`run_task` is the one-shot form — run a single asyncio-compatible coroutine fn and return - its result to trio. + its result to trio; :func:`tractor.to_actor.run` is its + cross-process sibling. The inter-loop channel ---------------------- 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/clustering.rst b/docs/guide/clustering.rst index bc56b1af..61af2cc0 100644 --- a/docs/guide/clustering.rst +++ b/docs/guide/clustering.rst @@ -70,9 +70,10 @@ one kwarg away, ... From here the composition patterns are the usual ``tractor`` fare: -``portal.run()`` for one-shot calls (as in the demo), or — for a -persistent bidirectional dialog per worker — concurrently enter N -``portal.open_context()`` blocks with +``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.trionics.gather_contexts()``; see :doc:`/guide/context` for that whole layer. @@ -87,8 +88,8 @@ Clusters vs. nurseries ``open_actor_cluster()`` is sugar, not a new primitive: under the hood it's just :func:`tractor.open_nursery` plus N concurrent -``start_actor()`` calls plus a ``.cancel()`` on the way out. Reach -for it when, +:meth:`~tractor.ActorNursery.start_actor` calls plus a ``.cancel()`` +on the way out. Reach for it when, - you want a *flat*, homogeneous fleet (classic worker-pool or map-style fan-out shapes), 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/index.rst b/docs/guide/index.rst index cbd4fe7b..bea53ac5 100644 --- a/docs/guide/index.rst +++ b/docs/guide/index.rst @@ -9,8 +9,8 @@ docs; what you read is what CI runs). Roughly in "first date to long term relationship" order, -- :doc:`spawning` — actor nurseries, daemons + - one-shot workers, process lifetimes. +- :doc:`spawning` — actor nurseries, daemons, + ``to_actor.run()`` one-shots and process lifetimes. - :doc:`rpc` — portals: calling into another process like it's a local ``await``. - :doc:`context` — the cross-actor task-pair 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..5b76d824 100644 --- a/docs/guide/rpc.rst +++ b/docs/guide/rpc.rst @@ -80,28 +80,36 @@ 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() + from functools import partial + + final = await tractor.to_actor.run( + partial(fib, n=10), + an=an, + ) 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 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). +- 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()`` ---------------------------------- @@ -147,7 +155,8 @@ call tears down the entire sub-tree — SC, transitively. When to graduate to ``Context`` ------------------------------- -``portal.run()`` is great for one-shot, request-response calls. +The :meth:`~tractor.Portal.run` method is great for one-shot, +request-response calls. Reach for :meth:`~tractor.Portal.open_context` with an ``@tractor.context`` endpoint as soon as you want: @@ -160,10 +169,15 @@ Reach for :meth:`~tractor.Portal.open_context` with an :meth:`~tractor.Portal.cancel_actor` nukes the **entire** remote runtime and its process. -In fact the source plans for ``Portal.run()`` itself to be -rebuilt on top of ``open_context()`` — contexts *are* the core -inter-actor protocol. Take the full tour in -:doc:`/guide/context`. +:func:`tractor.to_actor.run` already enters the full +:meth:`~tractor.Portal.open_context` lifecycle. The older +:meth:`~tractor.Portal.run` path instead uses the ``Context`` returned +by the lower-level ``Actor.start_remote_task()`` directly, avoiding a +``Started`` handshake but owning less lifecycle machinery. A follow-up +should factor their shared linked-task lifecycle without requiring +``Portal.run()`` to delegate through the public context API or add +another wire message. Take the full tour in +:doc:`the context guide `. .. seealso:: diff --git a/docs/guide/spawning.rst b/docs/guide/spawning.rst index f1bbf26f..f56ad4ec 100644 --- a/docs/guide/spawning.rst +++ b/docs/guide/spawning.rst @@ -91,31 +91,34 @@ somebody-ing: What's going on here? -- ``start_actor('frank', enable_modules=[__name__])`` forks off +- :meth:`~tractor.ActorNursery.start_actor` forks off a new process, boots a ``tractor`` runtime inside it, and allows it to serve functions from the current module (see the allowlist section below). -- each ``await portal.run(...)`` schedules a *new* task in +- each :meth:`~tractor.Portal.run` call schedules a *new* task in frank's task tree and waits on its result — the full RPC story lives in :doc:`/guide/rpc`. - frank has no main task to complete, so without the final - ``await portal.cancel_actor()`` the nursery block would wait - on him **forever**. Daemon lifetimes are *yours* to end; that - explicitness is the point. + :meth:`~tractor.Portal.cancel_actor` call the nursery block would + wait 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: @@ -123,43 +126,52 @@ A few details worth knowing: ``name='something_cuter'``. - 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!). +- target arguments are positional; use ``functools.partial()`` + to bind target keyword arguments. Keywords passed directly to + ``run()`` configure actor placement and spawning. +- 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 - trio-parallel_ style one-shot — and reach for - ``start_actor()`` + ``open_context()`` for anything - long-lived, stateful or streaming - (:doc:`/guide/context`). + :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 + fire-and-collect parallelism — think a per-function trio-parallel_ + style one-shot — and reach for + :meth:`~tractor.ActorNursery.start_actor` plus + :meth:`~tractor.Portal.open_context` for anything long-lived, + stateful or streaming; see :doc:`/guide/context`. 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. -- **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 - in on error. + arrives back in the (blocking) call. +- **daemon** (:meth:`~tractor.ActorNursery.start_actor`): lives + until *someone* cancels it — 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. 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..35d553fc 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 - ``Portal.open_context()`` for full, SC-linked cross-actor - dialogs - see :doc:`/guide/context`. + :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 + :meth:`~tractor.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 @@ -208,16 +207,20 @@ The script of the scene (runtime ``INFO`` log lines trimmed):: The new tricks in play: -- two subactors, *donny* and *gretchen*, are each told to run - ``say_hello()`` targeting the *other* by name, +- *donny* and *gretchen* start as daemon actors so each remains alive + while the other discovers it and completes its line, +- a local ``trio`` nursery runs both ``Portal.run(say_hello)`` calls + concurrently; starting both actors first avoids either reciprocal + dialog racing one-shot process reaping, - ``tractor.wait_for_actor()`` blocks until the named peer has registered with the tree's *registrar* (every actor announces itself at boot), then yields a ``Portal`` connected **directly** to that peer, - each actor invokes its partner's ``hi()`` over that portal: - actor-to-actor RPC with the root merely *directing* - and both - final lines flow back to ``main()`` via - ``await portal.wait_for_result()``, + actor-to-actor RPC with the root merely *directing* - and each + ``Portal.run()`` returns its final line directly to ``main()``, +- the actor nursery explicitly cancels both daemons only after both + dialogs complete, - ``tractor.log.get_console_log("INFO")`` cranks up runtime logging so you can watch the spawn/register/cancel machinery narrate itself; remove it for a quiet set. diff --git a/examples/a_trynamic_first_scene.py b/examples/a_trynamic_first_scene.py index 05d61ba9..27d01ff0 100644 --- a/examples/a_trynamic_first_scene.py +++ b/examples/a_trynamic_first_scene.py @@ -21,23 +21,35 @@ async def main(): """Main tractor entry point, the "master" process (for now acts as the "director"). """ - async with tractor.open_nursery() as n: + async with tractor.open_nursery() as an: print("Alright... Action!") - donny = await n.run_in_actor( - say_hello, - name='donny', - # arguments are always named - other_actor='gretchen', - ) - gretchen = await n.run_in_actor( - say_hello, - name='gretchen', - other_actor='donny', - ) - print(await gretchen.wait_for_result()) - print(await donny.wait_for_result()) - print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...") + # both actors wait on (then dial!) the *other*, so each + # must outlive both hellos: spawn as daemons, run the + # hellos concurrently, reap only once both complete. + portals: dict[str, tractor.Portal] = { + name: await an.start_actor( + name, + enable_modules=[__name__], + ) + for name in ('donny', 'gretchen') + } + + async def run_and_print(name: str, other_actor: str): + print( + await portals[name].run( + say_hello, + other_actor=other_actor, + ) + ) + + async with trio.open_nursery() as tn: + tn.start_soon(run_and_print, 'donny', 'gretchen') + tn.start_soon(run_and_print, 'gretchen', 'donny') + + await an.cancel() + + print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...") if __name__ == '__main__': diff --git a/examples/actor_spawning_and_causality.py b/examples/actor_spawning_and_causality.py index 2232ae3e..00dc645c 100644 --- a/examples/actor_spawning_and_causality.py +++ b/examples/actor_spawning_and_causality.py @@ -10,17 +10,14 @@ async def cellar_door(): async def main(): """The main ``tractor`` routine. """ - async with tractor.open_nursery() as n: - - portal = await n.run_in_actor( + # spawn a subactor, run ``cellar_door()`` as its lone task, + # block until its result arrives and the subactor is reaped. + print( + await tractor.to_actor.run( cellar_door, name='some_linguist', ) - - # The ``async with`` will unblock here since the 'some_linguist' - # actor has completed its main task ``cellar_door``. - - print(await portal.wait_for_result()) + ) if __name__ == '__main__': diff --git a/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py b/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py index 6cfce50f..a18587dc 100644 --- a/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py +++ b/examples/debugging/multi_nested_subactors_error_up_through_nurseries.py @@ -1,3 +1,5 @@ +from functools import partial + import trio import tractor @@ -21,26 +23,39 @@ async def breakpoint_forever(): async def spawn_until(depth=0): """"A nested nursery that triggers another ``NameError``. """ - async with tractor.open_nursery() as n: + async with ( + tractor.open_nursery() as an, + trio.open_nursery() as tn, + ): if depth < 1: - await n.run_in_actor(breakpoint_forever) - - p = await n.run_in_actor( - name_error, - name='name_error' + tn.start_soon( + partial( + tractor.to_actor.run, + breakpoint_forever, + an=an, + ) ) + await trio.sleep(0.5) # rx and propagate error from child - await p.result() + await tractor.to_actor.run( + name_error, + an=an, + name='name_error', + ) else: # recusrive call to spawn another process branching layer of - # the tree + # the tree; blocks (up) each level until the leaf's + # `name_error` relays through. depth -= 1 - await n.run_in_actor( - spawn_until, - depth=depth, + await tractor.to_actor.run( + partial( + spawn_until, + depth=depth, + ), + an=an, name=f'spawn_until_{depth}', ) @@ -65,35 +80,38 @@ async def main(): └─ python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...) """ - async with tractor.open_nursery( - debug_mode=True, - loglevel='pdb', - ) as n: - - # spawn both actors - portal = await n.run_in_actor( - spawn_until, - depth=3, - name='spawner0', + async with ( + tractor.open_nursery( + debug_mode=True, + loglevel='pdb', + ) as an, + trio.open_nursery() as tn, + ): + # spawn both spawner trees as concurrent one-shots; the + # first tree's (relayed) error cancels the other. + tn.start_soon( + partial( + tractor.to_actor.run, + partial( + spawn_until, + depth=3, + ), + an=an, + name='spawner0', + ) ) - portal1 = await n.run_in_actor( - spawn_until, - depth=4, - name='spawner1', + tn.start_soon( + partial( + tractor.to_actor.run, + partial( + spawn_until, + depth=4, + ), + an=an, + name='spawner1', + ) ) - # TODO: test this case as well where the parent don't see - # the sub-actor errors by default and instead expect a user - # ctrl-c to kill the root. - with trio.move_on_after(3): - await trio.sleep_forever() - - # gah still an issue here. - await portal.result() - - # should never get here - await portal1.result() - if __name__ == '__main__': trio.run(main) diff --git a/examples/debugging/multi_subactor_root_errors.py b/examples/debugging/multi_subactor_root_errors.py index 31bb7dd1..5aa3a4ff 100644 --- a/examples/debugging/multi_subactor_root_errors.py +++ b/examples/debugging/multi_subactor_root_errors.py @@ -15,12 +15,12 @@ async def name_error(): async def spawn_error(): """"A nested nursery that triggers another ``NameError``. """ - async with tractor.open_nursery() as n: - portal = await n.run_in_actor( + async with tractor.open_nursery() as an: + return await tractor.to_actor.run( name_error, + an=an, name='name_error_1', - ) - return await portal.result() + ) async def main(): @@ -38,29 +38,36 @@ async def main(): - root actor should then fail on assert - program termination """ - async with tractor.open_nursery( - debug_mode=True, - loglevel='devx', - ) as n: + async with ( + tractor.open_nursery( + debug_mode=True, + loglevel='devx', + ) as an, + trio.open_nursery() as tn, + ): + # spawn both actors.. + portal = await an.start_actor( + 'name_error', + enable_modules=[__name__], + ) + portal1 = await an.start_actor( + 'spawn_error', + enable_modules=[__name__], + ) - # spawn both actors - portal = await n.run_in_actor( - name_error, - name='name_error', - ) - portal1 = await n.run_in_actor( - spawn_error, - name='spawn_error', - ) + # ..and bg-schedule their erroring tasks. + tn.start_soon(portal.run, name_error) + tn.start_soon(portal1.run, spawn_error) + + # yield to the bg tasks so both RPC requests are + # submitted (and start crashing) before the root's own + # error below (the legacy `run_in_actor()` submitted + # in-line with each spawn). + await trio.sleep(0.5) # trigger a root actor error assert 0 - # attempt to collect results (which raises error in parent) - # still has some issues where the parent seems to get stuck - await portal.result() - await portal1.result() - if __name__ == '__main__': trio.run(main) diff --git a/examples/debugging/multi_subactors.py b/examples/debugging/multi_subactors.py index 57634cc3..63ab5404 100644 --- a/examples/debugging/multi_subactors.py +++ b/examples/debugging/multi_subactors.py @@ -17,12 +17,12 @@ async def name_error(): async def spawn_error(): """"A nested nursery that triggers another ``NameError``. """ - async with tractor.open_nursery() as n: - portal = await n.run_in_actor( + async with tractor.open_nursery() as an: + return await tractor.to_actor.run( name_error, + an=an, name='name_error_1', ) - return await portal.result() async def main(): @@ -36,17 +36,39 @@ async def main(): `-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...) `-python -m tractor._child --uid ('name_error', '3391222c ...) """ + errors: list[BaseException] = [] + async with tractor.open_nursery( debug_mode=True, # loglevel='runtime', - ) as n: + ) as an: - # Spawn both actors, don't bother with collecting results - # (would result in a different debugger outcome due to parent's - # cancellation). - await n.run_in_actor(breakpoint_forever) - await n.run_in_actor(name_error) - await n.run_in_actor(spawn_error) + async def run_and_collect(fn): + ''' + One-shot whose (boxed) error is stashed instead of + raised so a sibling's crash never cancels the others + before they've had their own debugger sessions (the + "collect all errors" the legacy `run_in_actor()` API + did implicitly at nursery teardown). + + ''' + try: + await tractor.to_actor.run(fn, an=an) + except tractor.RemoteActorError as rae: + errors.append(rae) + + # Spawn all one-shot task actors, collecting (vs. + # raising) their errors. + async with trio.open_nursery() as tn: + tn.start_soon(run_and_collect, breakpoint_forever) + tn.start_soon(run_and_collect, name_error) + tn.start_soon(run_and_collect, spawn_error) + + if errors: + raise BaseExceptionGroup( + 'multi_subactors errored!', + errors, + ) if __name__ == '__main__': diff --git a/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py b/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py index 93daa33b..7b2e4d5a 100644 --- a/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py +++ b/examples/debugging/root_cancelled_but_child_is_in_tty_lock.py @@ -1,3 +1,5 @@ +from functools import partial + import trio import tractor @@ -10,15 +12,17 @@ async def name_error(): async def spawn_until(depth=0): """"A nested nursery that triggers another ``NameError``. """ - async with tractor.open_nursery() as n: + async with tractor.open_nursery() as an: if depth < 1: - # await n.run_in_actor('breakpoint_forever', breakpoint_forever) - await n.run_in_actor(name_error) + await tractor.to_actor.run(name_error, an=an) else: depth -= 1 - await n.run_in_actor( - spawn_until, - depth=depth, + await tractor.to_actor.run( + partial( + spawn_until, + depth=depth, + ), + an=an, name=f'spawn_until_{depth}', ) @@ -37,28 +41,37 @@ async def main(): └─ python -m tractor._child --uid ('name_error', '6c2733b8 ...) ''' - async with tractor.open_nursery( - debug_mode=True, - enable_transports=['uds'], # TODO, apss this via osenv? - loglevel='devx', # XXX, required for test! - ) as n: + async with ( + tractor.open_nursery( + debug_mode=True, + enable_transports=['uds'], # TODO, apss this via osenv? + loglevel='devx', # XXX, required for test! + ) as an, + trio.open_nursery() as tn, + ): + # spawn the deeper tree in the bg.. + tn.start_soon( + partial( + tractor.to_actor.run, + partial( + spawn_until, + depth=1, + ), + an=an, + name='spawner1', + ) + ) - # spawn both actors - portal = await n.run_in_actor( - spawn_until, - depth=0, + # ..while blocking on the shallow (faster to fail) tree + # whose propagated error triggers nursery cancellation. + await tractor.to_actor.run( + partial( + spawn_until, + depth=0, + ), + an=an, name='spawner0', ) - portal1 = await n.run_in_actor( - spawn_until, - depth=1, - name='spawner1', - ) - - # nursery cancellation should be triggered due to propagated - # error from child. - await portal.result() - await portal1.result() if __name__ == '__main__': diff --git a/examples/debugging/root_timeout_while_child_crashed.py b/examples/debugging/root_timeout_while_child_crashed.py index 4dfc699d..043cb5c7 100644 --- a/examples/debugging/root_timeout_while_child_crashed.py +++ b/examples/debugging/root_timeout_while_child_crashed.py @@ -13,17 +13,24 @@ async def main(): simultaneously. ''' - async with tractor.open_nursery( - debug_mode=True, - # loglevel='debug' # ?XXX required? - ) as n: - - # spawn both actors - portal = await n.run_in_actor(key_error) + async with ( + tractor.open_nursery( + debug_mode=True, + # loglevel='debug' # ?XXX required? + ) as an, + trio.open_nursery() as tn, + ): + # spawn the actor.. + portal = await an.start_actor( + 'key_error', + enable_modules=[__name__], + ) print( f'Child is up @ {portal.chan.aid.reprol()}' ) - + # ..then schedule its erroring task in the bg while the + # root blocks below. + tn.start_soon(portal.run, key_error) # XXX: originally a bug caused by this is where root would enter # the debugger and clobber the tty used by the repl even though diff --git a/examples/debugging/shielded_pause.py b/examples/debugging/shielded_pause.py index e6df907c..5a8c50e7 100644 --- a/examples/debugging/shielded_pause.py +++ b/examples/debugging/shielded_pause.py @@ -74,11 +74,11 @@ async def cancelled_before_pause( async def main(): async with tractor.open_nursery( debug_mode=True, - ) as n: - portal: tractor.Portal = await n.run_in_actor( + ) as an: + await tractor.to_actor.run( cancelled_before_pause, + an=an, ) - await portal.wait_for_result() # ensure the same works in the root actor! await pm_on_cancelled() diff --git a/examples/debugging/subactor_breakpoint.py b/examples/debugging/subactor_breakpoint.py index 67a5b7e0..e3a4f250 100644 --- a/examples/debugging/subactor_breakpoint.py +++ b/examples/debugging/subactor_breakpoint.py @@ -17,12 +17,14 @@ async def main(): async with tractor.open_nursery( debug_mode=True, loglevel='cancel', - ) as n: + ) as an: - portal = await n.run_in_actor( + # parks awaiting a result which only arrives once the + # user quits (`BdbQuit`s) the child's REPL loop. + await tractor.to_actor.run( breakpoint_forever, + an=an, ) - await portal.wait_for_result() if __name__ == '__main__': diff --git a/examples/debugging/subactor_error.py b/examples/debugging/subactor_error.py index fabdcedb..95c1fe12 100644 --- a/examples/debugging/subactor_error.py +++ b/examples/debugging/subactor_error.py @@ -12,16 +12,12 @@ async def main(): ) as an: # TODO: ideally the REPL arrives at this frame in the parent, - # ABOVE the @api_frame of `Portal.run_in_actor()` (which - # should eventually not even be a portal method ... XD) + # ABOVE the @api_frame of `to_actor.run()` .. # await tractor.pause() - p: tractor.Portal = await an.run_in_actor(name_error) - # with this style, should raise on this line - await p.wait_for_result() - - # with this alt style should raise at `open_nusery()` - # return await p.wait_for_result() + # the one-shot blocks on the subactor's result so the + # boxed `NameError` raises right here. + await tractor.to_actor.run(name_error, an=an) if __name__ == '__main__': diff --git a/examples/debugging/sync_bp.py b/examples/debugging/sync_bp.py index 8c4ba6e9..c22f0fff 100644 --- a/examples/debugging/sync_bp.py +++ b/examples/debugging/sync_bp.py @@ -90,7 +90,7 @@ async def main() -> None: # TODO: 3 sub-actor usage cases: # -[x] via a `.open_context()` - # -[ ] via a `.run_in_actor()` call + # -[ ] via a `to_actor.run()` call # -[ ] via a `.run()` # -[ ] via a `.to_thread.run_sync()` in subactor async with p.open_context( diff --git a/examples/parallelism/single_func.py b/examples/parallelism/single_func.py index c4ea29e3..3d8409bb 100644 --- a/examples/parallelism/single_func.py +++ b/examples/parallelism/single_func.py @@ -20,22 +20,20 @@ async def burn_cpu(): for _ in range(50000): await trio.sleep(1/50000/50) - return os.getpid() + return pid async def main(): - async with tractor.open_nursery() as n: + async with trio.open_nursery() as tn: - portal = await n.run_in_actor(burn_cpu) + # burn rubber in the parent too + tn.start_soon(burn_cpu) - # burn rubber in the parent too - await burn_cpu() + # run the same func as the lone task in a subactor, + # block on (and collect) its result + pid = await tractor.to_actor.run(burn_cpu) - # wait on result from target function - pid = await portal.wait_for_result() - - # end of nursery block print(f"Collected subproc {pid}") diff --git a/examples/remote_error_propagation.py b/examples/remote_error_propagation.py index aa2a73b4..db9eb9b5 100644 --- a/examples/remote_error_propagation.py +++ b/examples/remote_error_propagation.py @@ -7,19 +7,20 @@ async def assert_err(): async def main(): - async with tractor.open_nursery() as n: + async with tractor.open_nursery() as an: real_actors = [] for i in range(3): - real_actors.append(await n.start_actor( + real_actors.append(await an.start_actor( f'actor_{i}', enable_modules=[__name__], )) - # start one actor that will fail immediately - await n.run_in_actor(assert_err) + # run one one-shot task actor that will fail immediately; + # its error raises right here in the caller's task.. + await tractor.to_actor.run(assert_err, an=an) - # should error here with a ``RemoteActorError`` containing - # an ``AssertionError`` and all the other actors have been cancelled + # ..as a ``RemoteActorError`` containing an ``AssertionError`` + # and all the other actors have been cancelled if __name__ == '__main__':