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`))wkt/to_actor_subpkg
parent
04123019b7
commit
40d2d9942e
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -37,7 +37,6 @@ Spawning actors
|
||||||
|
|
||||||
.. autoclass:: ActorNursery
|
.. autoclass:: ActorNursery
|
||||||
:members: start_actor,
|
:members: start_actor,
|
||||||
run_in_actor,
|
|
||||||
cancel,
|
cancel,
|
||||||
cancel_called,
|
cancel_called,
|
||||||
cancelled_caught
|
cancelled_caught
|
||||||
|
|
@ -46,11 +45,24 @@ Spawning actors
|
||||||
|
|
||||||
:meth:`ActorNursery.start_actor` (daemon actor + portal) is the
|
:meth:`ActorNursery.start_actor` (daemon actor + portal) is the
|
||||||
blessed spawning primitive; pair it with
|
blessed spawning primitive; pair it with
|
||||||
``Portal.open_context()`` for SC-linked remote tasks.
|
:meth:`Portal.open_context` for SC-linked remote tasks.
|
||||||
:meth:`ActorNursery.run_in_actor` is a *convenience* one-shot —
|
|
||||||
spawn, run a single task, auto-cancel after the result — slated
|
One-shot task actors
|
||||||
to be rebuilt as a high-level wrapper, so don't design around
|
--------------------
|
||||||
it as the core model.
|
|
||||||
|
.. autofunction:: tractor.to_actor.run
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
:func:`tractor.to_actor.run` (parlance of
|
||||||
|
``trio.to_thread.run_sync()`` and friends) is the
|
||||||
|
*convenience* one-shot — spawn, run a single task, block on
|
||||||
|
its result, reap — built entirely on
|
||||||
|
:meth:`ActorNursery.start_actor`, 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
|
.. deprecated:: 0.1.0a6
|
||||||
|
|
||||||
|
|
@ -71,14 +83,12 @@ flowing back `exactly like trio`_.
|
||||||
:members: run,
|
:members: run,
|
||||||
run_from_ns,
|
run_from_ns,
|
||||||
open_stream_from,
|
open_stream_from,
|
||||||
wait_for_result,
|
|
||||||
cancel_actor,
|
cancel_actor,
|
||||||
chan
|
chan
|
||||||
|
|
||||||
.. deprecated:: 0.1.0a6
|
.. deprecated:: 0.1.0a6
|
||||||
|
|
||||||
``Portal.result()`` warns; use :meth:`Portal.wait_for_result`.
|
The str-form ``Portal.run('mod.path', 'fn_name')`` warns;
|
||||||
The str-form ``Portal.run('mod.path', 'fn_name')`` also warns;
|
|
||||||
pass a function *object* whose module is listed in the target's
|
pass a function *object* whose module is listed in the target's
|
||||||
``enable_modules``. ``Portal.channel`` is the legacy spelling
|
``enable_modules``. ``Portal.channel`` is the legacy spelling
|
||||||
of :attr:`Portal.chan`.
|
of :attr:`Portal.chan`.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
names you can import and lean on without reading runtime internals.
|
||||||
Everything below is re-exported at the top level (``import
|
Everything below is re-exported at the top level (``import
|
||||||
tractor``) unless a page says otherwise; subsystems like
|
tractor``) unless a page says otherwise; subsystems like
|
||||||
``tractor.msg``, ``tractor.trionics``, ``tractor.to_asyncio``,
|
``tractor.msg``, ``tractor.trionics``, ``tractor.to_actor``,
|
||||||
``tractor.devx`` and ``tractor.log`` are importable as submodules.
|
``tractor.to_asyncio``, ``tractor.devx`` and ``tractor.log`` are
|
||||||
|
importable as submodules.
|
||||||
|
|
||||||
``tractor`` is "just trio_" extended across processes: every API
|
``tractor`` is "just trio_" extended across processes: every API
|
||||||
here is designed to keep the structured concurrency (SC) rules you
|
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_root_actor
|
||||||
open_nursery
|
open_nursery
|
||||||
|
to_actor.run
|
||||||
run_daemon
|
run_daemon
|
||||||
ActorNursery
|
ActorNursery
|
||||||
Portal
|
Portal
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,12 @@ Starting asyncio tasks from trio
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
:func:`open_channel_from` mirrors the
|
: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
|
``chan.started_nowait(value)`` and that value pops out as
|
||||||
``first`` on the trio side. :func:`run_task` is the one-shot
|
``first`` on the trio side. :func:`run_task` is the one-shot
|
||||||
form — run a single asyncio-compatible coroutine fn and return
|
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
|
The inter-loop channel
|
||||||
----------------------
|
----------------------
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ Just flip the flag on :meth:`tractor.ActorNursery.start_actor`:
|
||||||
infect_asyncio=True,
|
infect_asyncio=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
The one-shot convenience ``ActorNursery.run_in_actor()`` accepts
|
The one-shot convenience ``tractor.to_actor.run()`` accepts the
|
||||||
the same flag. The ``to_asyncio`` APIs may **only** be called from
|
same flag. The ``to_asyncio`` APIs may **only** be called from
|
||||||
tasks inside an infected actor; calling them anywhere else raises
|
tasks inside an infected actor; calling them anywhere else raises
|
||||||
a loud ``RuntimeError``. You can introspect at runtime with
|
a loud ``RuntimeError``. You can introspect at runtime with
|
||||||
``tractor.current_actor().is_infected_aio()``.
|
``tractor.current_actor().is_infected_aio()``.
|
||||||
|
|
@ -229,7 +229,7 @@ dialog, skip the channel ceremony and use
|
||||||
|
|
||||||
It schedules the fn as an ``asyncio.Task``, waits for completion
|
It schedules the fn as an ``asyncio.Task``, waits for completion
|
||||||
and hands the return value back to ``trio``; think of it as the
|
and hands the return value back to ``trio``; think of it as the
|
||||||
cross-loop sibling of ``ActorNursery.run_in_actor()``. Errors and
|
cross-loop sibling of ``tractor.to_actor.run()``. Errors and
|
||||||
cancellation are translated exactly as for channels.
|
cancellation are translated exactly as for channels.
|
||||||
|
|
||||||
Cross-loop errors and cancellation
|
Cross-loop errors and cancellation
|
||||||
|
|
|
||||||
|
|
@ -64,11 +64,13 @@ What's going on here?
|
||||||
- three healthy actors are spawned as daemons via
|
- three healthy actors are spawned as daemons via
|
||||||
:meth:`tractor.ActorNursery.start_actor`; left alone they'd
|
:meth:`tractor.ActorNursery.start_actor`; left alone they'd
|
||||||
happily idle forever,
|
happily idle forever,
|
||||||
- a fourth actor runs ``assert_err()`` via ``.run_in_actor()`` and
|
- a fourth actor runs ``assert_err()`` via a blocking
|
||||||
promptly trips its ``assert 0``,
|
``tractor.to_actor.run()`` one-shot and promptly trips its
|
||||||
|
``assert 0``,
|
||||||
- the resulting ``AssertionError`` ships back over IPC as a
|
- the resulting ``AssertionError`` ships back over IPC as a
|
||||||
serialized error msg and re-raises *boxed* inside the nursery
|
serialized error msg and re-raises *boxed* right at the call
|
||||||
block as a :class:`tractor.RemoteActorError`,
|
inside the nursery block as a
|
||||||
|
:class:`tractor.RemoteActorError`,
|
||||||
- the nursery reacts like any ``trio`` nursery would: it cancels
|
- the nursery reacts like any ``trio`` nursery would: it cancels
|
||||||
the three healthy siblings (graceful runtime-cancel requests,
|
the three healthy siblings (graceful runtime-cancel requests,
|
||||||
acks awaited), reaps all four processes, then re-raises,
|
acks awaited), reaps all four processes, then re-raises,
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,10 @@ one kwarg away,
|
||||||
...
|
...
|
||||||
|
|
||||||
From here the composition patterns are the usual ``tractor`` fare:
|
From here the composition patterns are the usual ``tractor`` fare:
|
||||||
``portal.run()`` for one-shot calls (as in the demo), or — for a
|
``portal.run()`` for bare one-shot RPCs (as in the demo),
|
||||||
persistent bidirectional dialog per worker — concurrently enter N
|
``tractor.to_actor.run(..., portal=portal)`` for linked one-shot calls,
|
||||||
``portal.open_context()`` blocks with
|
or — for a persistent bidirectional dialog per worker — concurrently
|
||||||
|
enter N ``portal.open_context()`` blocks with
|
||||||
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
|
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
|
||||||
for that whole layer.
|
for that whole layer.
|
||||||
|
|
||||||
|
|
@ -87,8 +88,8 @@ Clusters vs. nurseries
|
||||||
|
|
||||||
``open_actor_cluster()`` is sugar, not a new primitive: under the
|
``open_actor_cluster()`` is sugar, not a new primitive: under the
|
||||||
hood it's just :func:`tractor.open_nursery` plus N concurrent
|
hood it's just :func:`tractor.open_nursery` plus N concurrent
|
||||||
``start_actor()`` calls plus a ``.cancel()`` on the way out. Reach
|
:meth:`~tractor.ActorNursery.start_actor` calls plus a ``.cancel()``
|
||||||
for it when,
|
on the way out. Reach for it when,
|
||||||
|
|
||||||
- you want a *flat*, homogeneous fleet (classic worker-pool or
|
- you want a *flat*, homogeneous fleet (classic worker-pool or
|
||||||
map-style fan-out shapes),
|
map-style fan-out shapes),
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ a single `structured concurrency`_ (SC) scope over IPC.
|
||||||
:alt: sequence diagram of the context handshake msg flow
|
:alt: sequence diagram of the context handshake msg flow
|
||||||
|
|
||||||
Pretty much everything else is (or is slated to be) built on this
|
Pretty much everything else is (or is slated to be) built on this
|
||||||
one primitive: ``ActorNursery.run_in_actor()`` is a convenience
|
one primitive: ``tractor.to_actor.run()`` is a convenience for
|
||||||
for "spawn, open a context, await the result, tear down"; plain
|
"spawn, run the lone task, await the result, tear down"; plain
|
||||||
``Portal.run()`` RPC is planned to be re-implemented on top of it;
|
``Portal.run()`` RPC is planned to be re-implemented on top of it;
|
||||||
the multi-process debugger's tree-wide REPL lock rides one. Grok
|
the multi-process debugger's tree-wide REPL lock rides one. Grok
|
||||||
this page and the rest of the library reads as convenience
|
this page and the rest of the library reads as convenience
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ docs; what you read is what CI runs).
|
||||||
Roughly in "first date to long term relationship"
|
Roughly in "first date to long term relationship"
|
||||||
order,
|
order,
|
||||||
|
|
||||||
- :doc:`spawning` — actor nurseries, daemons +
|
- :doc:`spawning` — actor nurseries, daemons,
|
||||||
one-shot workers, process lifetimes.
|
``to_actor.run()`` one-shots and process lifetimes.
|
||||||
- :doc:`rpc` — portals: calling into another
|
- :doc:`rpc` — portals: calling into another
|
||||||
process like it's a local ``await``.
|
process like it's a local ``await``.
|
||||||
- :doc:`context` — the cross-actor task-pair
|
- :doc:`context` — the cross-actor task-pair
|
||||||
|
|
|
||||||
|
|
@ -119,15 +119,16 @@ Run a func in a process
|
||||||
|
|
||||||
Even a pool can be overkill; "run this one async func in a
|
Even a pool can be overkill; "run this one async func in a
|
||||||
subprocess and give me the result" is a one-liner via
|
subprocess and give me the result" is a one-liner via
|
||||||
:meth:`tractor.ActorNursery.run_in_actor`,
|
:func:`tractor.to_actor.run`,
|
||||||
|
|
||||||
.. literalinclude:: ../../examples/parallelism/single_func.py
|
.. literalinclude:: ../../examples/parallelism/single_func.py
|
||||||
:caption: examples/parallelism/single_func.py
|
:caption: examples/parallelism/single_func.py
|
||||||
:language: python
|
:language: python
|
||||||
|
|
||||||
``run_in_actor()`` is a *convenience wrapper* — spawn an actor, run
|
``to_actor.run()`` is a *convenience wrapper* — spawn an actor,
|
||||||
exactly one task in it, reap on result — not the core spawning
|
run exactly one task in it, block on and return its result, reap
|
||||||
model (that's :meth:`tractor.ActorNursery.start_actor` plus
|
— not the core spawning model (that's
|
||||||
|
:meth:`tractor.ActorNursery.start_actor` plus
|
||||||
:meth:`tractor.Portal.open_context`; see :doc:`/guide/context`).
|
:meth:`tractor.Portal.open_context`; see :doc:`/guide/context`).
|
||||||
But for this fire-and-collect shape it's exactly the right amount
|
But for this fire-and-collect shape it's exactly the right amount
|
||||||
of typing.
|
of typing.
|
||||||
|
|
|
||||||
|
|
@ -80,28 +80,36 @@ One special namespace exists: ``'self'`` resolves to the remote
|
||||||
how internal machinery (cancel requests, registry ops) travels;
|
how internal machinery (cancel requests, registry ops) travels;
|
||||||
don't build your app on it.
|
don't build your app on it.
|
||||||
|
|
||||||
One-shot results: ``wait_for_result()``
|
One-shot subactors: ``to_actor.run()``
|
||||||
---------------------------------------
|
--------------------------------------
|
||||||
A portal returned from
|
When a subactor's *entire job* is a single function call, skip
|
||||||
:meth:`~tractor.ActorNursery.run_in_actor` has exactly one
|
the portal plumbing with :func:`tractor.to_actor.run`: spawn,
|
||||||
"main" task running remotely; that task's ``return`` value is
|
run the lone task, return its result and reap the process — all
|
||||||
delivered as the portal's *final result*:
|
in one blocking call:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
portal = await an.run_in_actor(fib, n=10)
|
from functools import partial
|
||||||
final = await portal.wait_for_result()
|
|
||||||
|
final = await tractor.to_actor.run(
|
||||||
|
partial(fib, n=10),
|
||||||
|
an=an,
|
||||||
|
)
|
||||||
|
|
||||||
Semantics worth knowing:
|
Semantics worth knowing:
|
||||||
|
|
||||||
- it blocks until the remote task returns, re-raising any
|
- it blocks until the remote task returns, re-raising any
|
||||||
remote error in the usual boxed form.
|
remote error in the usual boxed form right in the calling
|
||||||
- once resolved it's idempotent: later calls return the same
|
task.
|
||||||
cached value.
|
- "placement" is composable: ``an=`` spawns from an existing
|
||||||
- a *daemon* portal (from ``start_actor()``) has no main task,
|
actor-nursery, ``portal=`` reuses an already-running actor
|
||||||
so there's no final result to wait for: you'll get a warning
|
(no spawn/reap, just a linked
|
||||||
plus a ``NoResult`` sentinel. Results of individual daemon
|
:meth:`~tractor.Portal.open_context` call; see the
|
||||||
calls come straight back from each ``await portal.run()``.
|
:doc:`context guide </guide/context>`), 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()``
|
Pure RPC daemons: ``run_daemon()``
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
@ -147,7 +155,8 @@ call tears down the entire sub-tree — SC, transitively.
|
||||||
|
|
||||||
When to graduate to ``Context``
|
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
|
Reach for :meth:`~tractor.Portal.open_context` with an
|
||||||
``@tractor.context`` endpoint as soon as you want:
|
``@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**
|
:meth:`~tractor.Portal.cancel_actor` nukes the **entire**
|
||||||
remote runtime and its process.
|
remote runtime and its process.
|
||||||
|
|
||||||
In fact the source plans for ``Portal.run()`` itself to be
|
:func:`tractor.to_actor.run` already enters the full
|
||||||
rebuilt on top of ``open_context()`` — contexts *are* the core
|
:meth:`~tractor.Portal.open_context` lifecycle. The older
|
||||||
inter-actor protocol. Take the full tour in
|
:meth:`~tractor.Portal.run` path instead uses the ``Context`` returned
|
||||||
:doc:`/guide/context`.
|
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 </guide/context>`.
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,31 +91,34 @@ somebody-ing:
|
||||||
|
|
||||||
What's going on here?
|
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
|
a new process, boots a ``tractor`` runtime inside it, and
|
||||||
allows it to serve functions from the current module (see the
|
allows it to serve functions from the current module (see the
|
||||||
allowlist section below).
|
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
|
frank's task tree and waits on its result — the full RPC story
|
||||||
lives in :doc:`/guide/rpc`.
|
lives in :doc:`/guide/rpc`.
|
||||||
- frank has no main task to complete, so without the final
|
- frank has no main task to complete, so without the final
|
||||||
``await portal.cancel_actor()`` the nursery block would wait
|
:meth:`~tractor.Portal.cancel_actor` call the nursery block would
|
||||||
on him **forever**. Daemon lifetimes are *yours* to end; that
|
wait on him **forever**. Daemon lifetimes are *yours* to end;
|
||||||
explicitness is the point.
|
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
|
:func:`tractor.to_actor.run` is the convenience wrapper: spawn
|
||||||
wrapper: spawn an actor, run exactly one async function in it,
|
an actor, run exactly one async function in it, block on the
|
||||||
then reap the process as soon as the result arrives.
|
result, then reap the process — the distributed sibling of
|
||||||
|
``trio.to_thread.run_sync()``.
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
async with tractor.open_nursery() as an:
|
async with (
|
||||||
portal = await an.run_in_actor(burn_cpu)
|
tractor.open_nursery() as an,
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
):
|
||||||
# burn rubber in the parent too...
|
# burn rubber in the parent too...
|
||||||
await burn_cpu()
|
tn.start_soon(burn_cpu)
|
||||||
total = await portal.wait_for_result()
|
total = await tractor.to_actor.run(burn_cpu, an=an)
|
||||||
|
|
||||||
A few details worth knowing:
|
A few details worth knowing:
|
||||||
|
|
||||||
|
|
@ -123,43 +126,52 @@ A few details worth knowing:
|
||||||
``name='something_cuter'``.
|
``name='something_cuter'``.
|
||||||
- the function's module is auto-added to the child's
|
- the function's module is auto-added to the child's
|
||||||
``enable_modules`` allowlist.
|
``enable_modules`` allowlist.
|
||||||
- extra ``**kwargs`` are forwarded to the function itself.
|
- target arguments are positional; use ``functools.partial()``
|
||||||
- the child is *auto-cancelled* once its "main" result lands;
|
to bind target keyword arguments. Keywords passed directly to
|
||||||
at nursery exit these run-once children are always reaped
|
``run()`` configure actor placement and spawning.
|
||||||
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::
|
.. note::
|
||||||
|
|
||||||
``run_in_actor()`` is a convenience, **not** the core model.
|
:func:`tractor.to_actor.run` is a convenience, **not** the core
|
||||||
The source literally marks it for an eventual rebuild as
|
model — it's built *entirely* on
|
||||||
a thin "hilevel" wrapper on top of
|
:meth:`~tractor.ActorNursery.start_actor` plus a linked
|
||||||
:meth:`~tractor.Portal.open_context` (the modern inter-actor
|
:meth:`~tractor.Portal.open_context` call and per-child
|
||||||
task API). Teach your fingers to use it for quick
|
cancellation/reaping. Teach your fingers to use it for quick
|
||||||
fire-and-collect parallelism — think a per-function
|
fire-and-collect parallelism — think a per-function trio-parallel_
|
||||||
trio-parallel_ style one-shot — and reach for
|
style one-shot — and reach for
|
||||||
``start_actor()`` + ``open_context()`` for anything
|
:meth:`~tractor.ActorNursery.start_actor` plus
|
||||||
long-lived, stateful or streaming
|
:meth:`~tractor.Portal.open_context` for anything long-lived,
|
||||||
(:doc:`/guide/context`).
|
stateful or streaming; see :doc:`/guide/context`.
|
||||||
|
|
||||||
Actor lifetimes and teardown order
|
Actor lifetimes and teardown order
|
||||||
----------------------------------
|
----------------------------------
|
||||||
So we have two lifetime flavors:
|
So we have two lifetime flavors:
|
||||||
|
|
||||||
- **run-once** (``run_in_actor()``): lives exactly as long as
|
- **one-shot** (``to_actor.run()``): lives exactly as long as
|
||||||
its single task; reaped the moment its result (or error)
|
its single task; reaped the moment its result (or error)
|
||||||
arrives.
|
arrives back in the (blocking) call.
|
||||||
- **daemon** (``start_actor()``): lives until *someone* cancels
|
- **daemon** (:meth:`~tractor.ActorNursery.start_actor`): lives
|
||||||
it — an explicit ``await portal.cancel_actor()``, a bulk
|
until *someone* cancels it — an explicit
|
||||||
``await an.cancel()``, or the one-cancels-all strategy kicking
|
:meth:`~tractor.Portal.cancel_actor`, a bulk
|
||||||
in on error.
|
: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:
|
On a clean exit of the nursery block the teardown order is:
|
||||||
|
|
||||||
1. the nursery waits on every run-once actor's final result;
|
1. one-shot actors never make it to nursery exit: each is
|
||||||
any errors from these are raised immediately so your code
|
reaped inside its own ``to_actor.run()`` call, any error
|
||||||
(acting as supervisor) gets first crack at handling them.
|
raising immediately in the calling task so your code
|
||||||
2. then it waits on daemon actors — **indefinitely**. If you
|
(acting as supervisor) gets first crack at handling it.
|
||||||
spawned a daemon, you own its lifetime.
|
2. the nursery then waits on daemon actors — **indefinitely**.
|
||||||
|
If you spawned a daemon, you own its lifetime.
|
||||||
|
|
||||||
When a child *is* cancelled, teardown is graceful-first per SC
|
When a child *is* cancelled, teardown is graceful-first per SC
|
||||||
discipline: the runtime sends an IPC cancel request and gives
|
discipline: the runtime sends an IPC cancel request and gives
|
||||||
|
|
|
||||||
|
|
@ -43,24 +43,20 @@ Run it::
|
||||||
What's going on here?
|
What's going on here?
|
||||||
|
|
||||||
- ``trio.run(main)`` starts the **root actor**; the ``tractor``
|
- ``trio.run(main)`` starts the **root actor**; the ``tractor``
|
||||||
runtime boots *implicitly* inside ``tractor.open_nursery()``
|
runtime boots *implicitly* inside ``tractor.to_actor.run()``
|
||||||
whenever it isn't already up. No special entrypoint, no
|
whenever it isn't already up. No special entrypoint, no
|
||||||
framework takeover - it's just a ``trio`` app,
|
framework takeover - it's just a ``trio`` app,
|
||||||
- inside ``main()`` a *subactor* is spawned via
|
- inside ``main()`` a *subactor* is spawned via
|
||||||
``ActorNursery.run_in_actor()`` and told to run exactly one
|
``tractor.to_actor.run()`` and told to run exactly one
|
||||||
function: ``cellar_door()``,
|
function: ``cellar_door()``,
|
||||||
- you get back a ``Portal``: your handle for invoking tasks in
|
|
||||||
the new process's (separate!) memory domain. We lean on it
|
|
||||||
much harder in the next section,
|
|
||||||
- the subactor, *some_linguist*, boots a fresh ``trio.run()`` in
|
- the subactor, *some_linguist*, boots a fresh ``trio.run()`` in
|
||||||
a **new process** and executes ``cellar_door()`` as its *main
|
a **new process** and executes ``cellar_door()`` as its *main
|
||||||
task* (note the child proving it is *not* the root with
|
task* (note the child proving it is *not* the root with
|
||||||
``tractor.is_root_process()``), then ships the return value
|
``tractor.is_root_process()``), then ships the return value
|
||||||
back over IPC,
|
back over IPC,
|
||||||
- the parent grabs that *final result* with
|
- the call *blocks* until that final result arrives, then
|
||||||
``await portal.wait_for_result()``, much like you'd expect
|
returns it - causality is preserved: your task only proceeds
|
||||||
from a "future" - except causality is preserved: the nursery
|
once the child is *done*, dead, and reaped.
|
||||||
block only exits once the child is *done*, dead, and reaped.
|
|
||||||
|
|
||||||
.. margin:: Just need a worker pool?
|
.. margin:: Just need a worker pool?
|
||||||
|
|
||||||
|
|
@ -71,19 +67,22 @@ What's going on here?
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
``run_in_actor()`` is the *convenience* wrapper: one-shot
|
``to_actor.run()`` (parlance of ``trio.to_thread`` and
|
||||||
|
friends) is the *convenience* wrapper: one-shot
|
||||||
spawn-run-reap semantics for when a subactor's entire job is
|
spawn-run-reap semantics for when a subactor's entire job is
|
||||||
a single function call. The core primitives are
|
a single function call. The core primitives are
|
||||||
``ActorNursery.start_actor()`` (next up) paired with
|
:meth:`~tractor.ActorNursery.start_actor` (next up) — which
|
||||||
``Portal.open_context()`` for full, SC-linked cross-actor
|
hands you a ``Portal``, your handle for invoking tasks in the
|
||||||
dialogs - see :doc:`/guide/context`.
|
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
|
Daemon actors and RPC
|
||||||
---------------------
|
---------------------
|
||||||
A ``run_in_actor()``-spawned actor terminates when its main task
|
A ``to_actor.run()`` one-shot subactor terminates when its lone
|
||||||
returns. But often you want long-lived *daemon* actors instead:
|
task returns. But often you want long-lived *daemon* actors
|
||||||
spawned once, then serving (allowlisted) RPC requests until told
|
instead: spawned once, then serving (allowlisted) RPC requests
|
||||||
otherwise. That's ``start_actor()``:
|
until told otherwise. That's ``start_actor()``:
|
||||||
|
|
||||||
.. literalinclude:: ../../examples/actor_spawning_and_causality_with_daemon.py
|
.. literalinclude:: ../../examples/actor_spawning_and_causality_with_daemon.py
|
||||||
:caption: examples/actor_spawning_and_causality_with_daemon.py
|
:caption: examples/actor_spawning_and_causality_with_daemon.py
|
||||||
|
|
@ -91,9 +90,9 @@ otherwise. That's ``start_actor()``:
|
||||||
|
|
||||||
Two lifetime rules to internalize:
|
Two lifetime rules to internalize:
|
||||||
|
|
||||||
- a ``run_in_actor()`` actor lives exactly as long as its main
|
- a ``to_actor.run()`` one-shot actor lives exactly as long as
|
||||||
task; the nursery waits for that function (and thus the
|
its lone task; the call blocks until that function (and thus
|
||||||
process) to complete before unblocking,
|
the process) completes,
|
||||||
- a ``start_actor()`` actor *lives forever* - an RPC daemon the
|
- a ``start_actor()`` actor *lives forever* - an RPC daemon the
|
||||||
nursery will happily wait on **indefinitely** - until some
|
nursery will happily wait on **indefinitely** - until some
|
||||||
task explicitly cancels it via ``Portal.cancel_actor()`` (as
|
task explicitly cancels it via ``Portal.cancel_actor()`` (as
|
||||||
|
|
@ -208,16 +207,20 @@ The script of the scene (runtime ``INFO`` log lines trimmed)::
|
||||||
|
|
||||||
The new tricks in play:
|
The new tricks in play:
|
||||||
|
|
||||||
- two subactors, *donny* and *gretchen*, are each told to run
|
- *donny* and *gretchen* start as daemon actors so each remains alive
|
||||||
``say_hello()`` targeting the *other* by name,
|
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
|
- ``tractor.wait_for_actor()`` blocks until the named peer has
|
||||||
registered with the tree's *registrar* (every actor announces
|
registered with the tree's *registrar* (every actor announces
|
||||||
itself at boot), then yields a ``Portal`` connected
|
itself at boot), then yields a ``Portal`` connected
|
||||||
**directly** to that peer,
|
**directly** to that peer,
|
||||||
- each actor invokes its partner's ``hi()`` over that portal:
|
- each actor invokes its partner's ``hi()`` over that portal:
|
||||||
actor-to-actor RPC with the root merely *directing* - and both
|
actor-to-actor RPC with the root merely *directing* - and each
|
||||||
final lines flow back to ``main()`` via
|
``Portal.run()`` returns its final line directly to ``main()``,
|
||||||
``await portal.wait_for_result()``,
|
- the actor nursery explicitly cancels both daemons only after both
|
||||||
|
dialogs complete,
|
||||||
- ``tractor.log.get_console_log("INFO")`` cranks up runtime
|
- ``tractor.log.get_console_log("INFO")`` cranks up runtime
|
||||||
logging so you can watch the spawn/register/cancel machinery
|
logging so you can watch the spawn/register/cancel machinery
|
||||||
narrate itself; remove it for a quiet set.
|
narrate itself; remove it for a quiet set.
|
||||||
|
|
|
||||||
|
|
@ -21,23 +21,35 @@ async def main():
|
||||||
"""Main tractor entry point, the "master" process (for now
|
"""Main tractor entry point, the "master" process (for now
|
||||||
acts as the "director").
|
acts as the "director").
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
print("Alright... Action!")
|
print("Alright... Action!")
|
||||||
|
|
||||||
donny = await n.run_in_actor(
|
# both actors wait on (then dial!) the *other*, so each
|
||||||
say_hello,
|
# must outlive both hellos: spawn as daemons, run the
|
||||||
name='donny',
|
# hellos concurrently, reap only once both complete.
|
||||||
# arguments are always named
|
portals: dict[str, tractor.Portal] = {
|
||||||
other_actor='gretchen',
|
name: await an.start_actor(
|
||||||
)
|
name,
|
||||||
gretchen = await n.run_in_actor(
|
enable_modules=[__name__],
|
||||||
say_hello,
|
)
|
||||||
name='gretchen',
|
for name in ('donny', 'gretchen')
|
||||||
other_actor='donny',
|
}
|
||||||
)
|
|
||||||
print(await gretchen.wait_for_result())
|
async def run_and_print(name: str, other_actor: str):
|
||||||
print(await donny.wait_for_result())
|
print(
|
||||||
print("CUTTTT CUUTT CUT!!! Donny!! You're supposed to say...")
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,14 @@ async def cellar_door():
|
||||||
async def main():
|
async def main():
|
||||||
"""The main ``tractor`` routine.
|
"""The main ``tractor`` routine.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
# spawn a subactor, run ``cellar_door()`` as its lone task,
|
||||||
|
# block until its result arrives and the subactor is reaped.
|
||||||
portal = await n.run_in_actor(
|
print(
|
||||||
|
await tractor.to_actor.run(
|
||||||
cellar_door,
|
cellar_door,
|
||||||
name='some_linguist',
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
|
@ -21,26 +23,39 @@ async def breakpoint_forever():
|
||||||
async def spawn_until(depth=0):
|
async def spawn_until(depth=0):
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""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:
|
if depth < 1:
|
||||||
|
|
||||||
await n.run_in_actor(breakpoint_forever)
|
tn.start_soon(
|
||||||
|
partial(
|
||||||
p = await n.run_in_actor(
|
tractor.to_actor.run,
|
||||||
name_error,
|
breakpoint_forever,
|
||||||
name='name_error'
|
an=an,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
await trio.sleep(0.5)
|
await trio.sleep(0.5)
|
||||||
# rx and propagate error from child
|
# rx and propagate error from child
|
||||||
await p.result()
|
await tractor.to_actor.run(
|
||||||
|
name_error,
|
||||||
|
an=an,
|
||||||
|
name='name_error',
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# recusrive call to spawn another process branching layer of
|
# 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
|
depth -= 1
|
||||||
await n.run_in_actor(
|
await tractor.to_actor.run(
|
||||||
spawn_until,
|
partial(
|
||||||
depth=depth,
|
spawn_until,
|
||||||
|
depth=depth,
|
||||||
|
),
|
||||||
|
an=an,
|
||||||
name=f'spawn_until_{depth}',
|
name=f'spawn_until_{depth}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -65,35 +80,38 @@ async def main():
|
||||||
└─ python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...)
|
└─ python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
debug_mode=True,
|
tractor.open_nursery(
|
||||||
loglevel='pdb',
|
debug_mode=True,
|
||||||
) as n:
|
loglevel='pdb',
|
||||||
|
) as an,
|
||||||
# spawn both actors
|
trio.open_nursery() as tn,
|
||||||
portal = await n.run_in_actor(
|
):
|
||||||
spawn_until,
|
# spawn both spawner trees as concurrent one-shots; the
|
||||||
depth=3,
|
# first tree's (relayed) error cancels the other.
|
||||||
name='spawner0',
|
tn.start_soon(
|
||||||
|
partial(
|
||||||
|
tractor.to_actor.run,
|
||||||
|
partial(
|
||||||
|
spawn_until,
|
||||||
|
depth=3,
|
||||||
|
),
|
||||||
|
an=an,
|
||||||
|
name='spawner0',
|
||||||
|
)
|
||||||
)
|
)
|
||||||
portal1 = await n.run_in_actor(
|
tn.start_soon(
|
||||||
spawn_until,
|
partial(
|
||||||
depth=4,
|
tractor.to_actor.run,
|
||||||
name='spawner1',
|
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__':
|
if __name__ == '__main__':
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ async def name_error():
|
||||||
async def spawn_error():
|
async def spawn_error():
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""A nested nursery that triggers another ``NameError``.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
portal = await n.run_in_actor(
|
return await tractor.to_actor.run(
|
||||||
name_error,
|
name_error,
|
||||||
|
an=an,
|
||||||
name='name_error_1',
|
name='name_error_1',
|
||||||
)
|
)
|
||||||
return await portal.result()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
@ -38,29 +38,36 @@ async def main():
|
||||||
- root actor should then fail on assert
|
- root actor should then fail on assert
|
||||||
- program termination
|
- program termination
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
debug_mode=True,
|
tractor.open_nursery(
|
||||||
loglevel='devx',
|
debug_mode=True,
|
||||||
) as n:
|
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
|
# ..and bg-schedule their erroring tasks.
|
||||||
portal = await n.run_in_actor(
|
tn.start_soon(portal.run, name_error)
|
||||||
name_error,
|
tn.start_soon(portal1.run, spawn_error)
|
||||||
name='name_error',
|
|
||||||
)
|
# yield to the bg tasks so both RPC requests are
|
||||||
portal1 = await n.run_in_actor(
|
# submitted (and start crashing) before the root's own
|
||||||
spawn_error,
|
# error below (the legacy `run_in_actor()` submitted
|
||||||
name='spawn_error',
|
# in-line with each spawn).
|
||||||
)
|
await trio.sleep(0.5)
|
||||||
|
|
||||||
# trigger a root actor error
|
# trigger a root actor error
|
||||||
assert 0
|
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__':
|
if __name__ == '__main__':
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@ async def name_error():
|
||||||
async def spawn_error():
|
async def spawn_error():
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""A nested nursery that triggers another ``NameError``.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
portal = await n.run_in_actor(
|
return await tractor.to_actor.run(
|
||||||
name_error,
|
name_error,
|
||||||
|
an=an,
|
||||||
name='name_error_1',
|
name='name_error_1',
|
||||||
)
|
)
|
||||||
return await portal.result()
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
@ -36,17 +36,39 @@ async def main():
|
||||||
`-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...)
|
`-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...)
|
||||||
`-python -m tractor._child --uid ('name_error', '3391222c ...)
|
`-python -m tractor._child --uid ('name_error', '3391222c ...)
|
||||||
"""
|
"""
|
||||||
|
errors: list[BaseException] = []
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
# loglevel='runtime',
|
# loglevel='runtime',
|
||||||
) as n:
|
) as an:
|
||||||
|
|
||||||
# Spawn both actors, don't bother with collecting results
|
async def run_and_collect(fn):
|
||||||
# (would result in a different debugger outcome due to parent's
|
'''
|
||||||
# cancellation).
|
One-shot whose (boxed) error is stashed instead of
|
||||||
await n.run_in_actor(breakpoint_forever)
|
raised so a sibling's crash never cancels the others
|
||||||
await n.run_in_actor(name_error)
|
before they've had their own debugger sessions (the
|
||||||
await n.run_in_actor(spawn_error)
|
"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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
|
|
@ -10,15 +12,17 @@ async def name_error():
|
||||||
async def spawn_until(depth=0):
|
async def spawn_until(depth=0):
|
||||||
""""A nested nursery that triggers another ``NameError``.
|
""""A nested nursery that triggers another ``NameError``.
|
||||||
"""
|
"""
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
if depth < 1:
|
if depth < 1:
|
||||||
# await n.run_in_actor('breakpoint_forever', breakpoint_forever)
|
await tractor.to_actor.run(name_error, an=an)
|
||||||
await n.run_in_actor(name_error)
|
|
||||||
else:
|
else:
|
||||||
depth -= 1
|
depth -= 1
|
||||||
await n.run_in_actor(
|
await tractor.to_actor.run(
|
||||||
spawn_until,
|
partial(
|
||||||
depth=depth,
|
spawn_until,
|
||||||
|
depth=depth,
|
||||||
|
),
|
||||||
|
an=an,
|
||||||
name=f'spawn_until_{depth}',
|
name=f'spawn_until_{depth}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -37,28 +41,37 @@ async def main():
|
||||||
└─ python -m tractor._child --uid ('name_error', '6c2733b8 ...)
|
└─ python -m tractor._child --uid ('name_error', '6c2733b8 ...)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
debug_mode=True,
|
tractor.open_nursery(
|
||||||
enable_transports=['uds'], # TODO, apss this via osenv?
|
debug_mode=True,
|
||||||
loglevel='devx', # XXX, required for test!
|
enable_transports=['uds'], # TODO, apss this via osenv?
|
||||||
) as n:
|
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
|
# ..while blocking on the shallow (faster to fail) tree
|
||||||
portal = await n.run_in_actor(
|
# whose propagated error triggers nursery cancellation.
|
||||||
spawn_until,
|
await tractor.to_actor.run(
|
||||||
depth=0,
|
partial(
|
||||||
|
spawn_until,
|
||||||
|
depth=0,
|
||||||
|
),
|
||||||
|
an=an,
|
||||||
name='spawner0',
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -13,17 +13,24 @@ async def main():
|
||||||
simultaneously.
|
simultaneously.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
debug_mode=True,
|
tractor.open_nursery(
|
||||||
# loglevel='debug' # ?XXX required?
|
debug_mode=True,
|
||||||
) as n:
|
# loglevel='debug' # ?XXX required?
|
||||||
|
) as an,
|
||||||
# spawn both actors
|
trio.open_nursery() as tn,
|
||||||
portal = await n.run_in_actor(key_error)
|
):
|
||||||
|
# spawn the actor..
|
||||||
|
portal = await an.start_actor(
|
||||||
|
'key_error',
|
||||||
|
enable_modules=[__name__],
|
||||||
|
)
|
||||||
print(
|
print(
|
||||||
f'Child is up @ {portal.chan.aid.reprol()}'
|
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
|
# XXX: originally a bug caused by this is where root would enter
|
||||||
# the debugger and clobber the tty used by the repl even though
|
# the debugger and clobber the tty used by the repl even though
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,11 @@ async def cancelled_before_pause(
|
||||||
async def main():
|
async def main():
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
) as n:
|
) as an:
|
||||||
portal: tractor.Portal = await n.run_in_actor(
|
await tractor.to_actor.run(
|
||||||
cancelled_before_pause,
|
cancelled_before_pause,
|
||||||
|
an=an,
|
||||||
)
|
)
|
||||||
await portal.wait_for_result()
|
|
||||||
|
|
||||||
# ensure the same works in the root actor!
|
# ensure the same works in the root actor!
|
||||||
await pm_on_cancelled()
|
await pm_on_cancelled()
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ async def main():
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
loglevel='cancel',
|
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,
|
breakpoint_forever,
|
||||||
|
an=an,
|
||||||
)
|
)
|
||||||
await portal.wait_for_result()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,12 @@ async def main():
|
||||||
) as an:
|
) as an:
|
||||||
|
|
||||||
# TODO: ideally the REPL arrives at this frame in the parent,
|
# TODO: ideally the REPL arrives at this frame in the parent,
|
||||||
# ABOVE the @api_frame of `Portal.run_in_actor()` (which
|
# ABOVE the @api_frame of `to_actor.run()` ..
|
||||||
# should eventually not even be a portal method ... XD)
|
|
||||||
# await tractor.pause()
|
# await tractor.pause()
|
||||||
p: tractor.Portal = await an.run_in_actor(name_error)
|
|
||||||
|
|
||||||
# with this style, should raise on this line
|
# the one-shot blocks on the subactor's result so the
|
||||||
await p.wait_for_result()
|
# boxed `NameError` raises right here.
|
||||||
|
await tractor.to_actor.run(name_error, an=an)
|
||||||
# with this alt style should raise at `open_nusery()`
|
|
||||||
# return await p.wait_for_result()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ async def main() -> None:
|
||||||
|
|
||||||
# TODO: 3 sub-actor usage cases:
|
# TODO: 3 sub-actor usage cases:
|
||||||
# -[x] via a `.open_context()`
|
# -[x] via a `.open_context()`
|
||||||
# -[ ] via a `.run_in_actor()` call
|
# -[ ] via a `to_actor.run()` call
|
||||||
# -[ ] via a `.run()`
|
# -[ ] via a `.run()`
|
||||||
# -[ ] via a `.to_thread.run_sync()` in subactor
|
# -[ ] via a `.to_thread.run_sync()` in subactor
|
||||||
async with p.open_context(
|
async with p.open_context(
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,20 @@ async def burn_cpu():
|
||||||
for _ in range(50000):
|
for _ in range(50000):
|
||||||
await trio.sleep(1/50000/50)
|
await trio.sleep(1/50000/50)
|
||||||
|
|
||||||
return os.getpid()
|
return pid
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
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
|
# run the same func as the lone task in a subactor,
|
||||||
await burn_cpu()
|
# 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}")
|
print(f"Collected subproc {pid}")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,20 @@ async def assert_err():
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as an:
|
||||||
real_actors = []
|
real_actors = []
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
real_actors.append(await n.start_actor(
|
real_actors.append(await an.start_actor(
|
||||||
f'actor_{i}',
|
f'actor_{i}',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
))
|
))
|
||||||
|
|
||||||
# start one actor that will fail immediately
|
# run one one-shot task actor that will fail immediately;
|
||||||
await n.run_in_actor(assert_err)
|
# 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
|
# ..as a ``RemoteActorError`` containing an ``AssertionError``
|
||||||
# an ``AssertionError`` and all the other actors have been cancelled
|
# and all the other actors have been cancelled
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue