128 lines
4.9 KiB
ReStructuredText
128 lines
4.9 KiB
ReStructuredText
|
|
Higher-level cluster APIs
|
||
|
|
=========================
|
||
|
|
|
||
|
|
Sometimes you don't want a hand-crafted supervision tree; you want
|
||
|
|
"a pile of workers, one per core, now please". For that there's
|
||
|
|
:func:`tractor.open_actor_cluster`: a convenience wrapper which
|
||
|
|
spawns a *flat* cluster of subactors and hands you back a portal to
|
||
|
|
each,
|
||
|
|
|
||
|
|
.. code:: python
|
||
|
|
|
||
|
|
@acm
|
||
|
|
async def open_actor_cluster(
|
||
|
|
modules: list[str], # RPC allowlist for workers
|
||
|
|
count: int = cpu_count(), # one per core by default
|
||
|
|
names: list[str]|None = None, # default: 'worker_{i}'
|
||
|
|
hard_kill: bool = False, # fwd to `an.cancel()`
|
||
|
|
**runtime_kwargs, # fwd to `open_root_actor()`
|
||
|
|
) -> AsyncGenerator[dict[str, tractor.Portal], None]:
|
||
|
|
|
||
|
|
A cluster in one block
|
||
|
|
----------------------
|
||
|
|
|
||
|
|
.. literalinclude:: ../../examples/quick_cluster.py
|
||
|
|
:caption: examples/quick_cluster.py
|
||
|
|
:language: python
|
||
|
|
|
||
|
|
Walkthrough,
|
||
|
|
|
||
|
|
- ``open_actor_cluster(modules=[__name__])`` concurrently spawns
|
||
|
|
one subactor per detected core (per
|
||
|
|
:func:`multiprocessing.cpu_count`); the ``modules`` list is the
|
||
|
|
usual ``enable_modules``-style capability allowlist so workers
|
||
|
|
may run functions defined in this module,
|
||
|
|
- it yields a ``dict[str, tractor.Portal]`` mapping worker name to
|
||
|
|
portal; note the keys get prefixed with the *spawning* actor's
|
||
|
|
name, so from the root you'll see ``'root.worker_0'``,
|
||
|
|
``'root.worker_1'``, etc.,
|
||
|
|
- a plain :class:`trio.Nursery` then fans out one
|
||
|
|
``portal.run(sleepy_jane)`` per worker; each prints its actor
|
||
|
|
``.uid`` from inside its own process then naps forever — what
|
||
|
|
runs *inside* each worker (and how many tasks you point at it)
|
||
|
|
is entirely yours to compose,
|
||
|
|
- ``tractor.trionics.collapse_eg()`` un-nests the strict
|
||
|
|
``ExceptionGroup`` wrapping so the demo's ``KeyboardInterrupt``
|
||
|
|
surfaces as itself instead of arriving eg-boxed,
|
||
|
|
- on block exit the whole fleet is torn down for you via
|
||
|
|
:meth:`tractor.ActorNursery.cancel`; pass ``hard_kill=True`` at
|
||
|
|
open time to skip straight to OS-level termination instead of
|
||
|
|
the graceful ladder described in :doc:`/guide/cancellation`.
|
||
|
|
|
||
|
|
Sizing, naming, fleet-wide options
|
||
|
|
----------------------------------
|
||
|
|
|
||
|
|
``count`` doesn't have to be core-count and the auto-generated
|
||
|
|
``'worker_{i}'`` names are just the default; pass your own (the
|
||
|
|
length must match ``count`` or you get a ``ValueError``). Any
|
||
|
|
extra ``**runtime_kwargs`` pass through verbatim to
|
||
|
|
:func:`tractor.open_root_actor`, so fleet-wide runtime options are
|
||
|
|
one kwarg away,
|
||
|
|
|
||
|
|
.. code:: python
|
||
|
|
|
||
|
|
async with tractor.open_actor_cluster(
|
||
|
|
modules=['mylib.workers'],
|
||
|
|
count=4,
|
||
|
|
names=['scout', 'miner', 'smelter', 'smith'],
|
||
|
|
debug_mode=True, # whole-fleet crash-to-REPL
|
||
|
|
) as portal_map:
|
||
|
|
...
|
||
|
|
|
||
|
|
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
|
||
|
|
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
|
||
|
|
for that whole layer.
|
||
|
|
|
||
|
|
Clusters vs. nurseries
|
||
|
|
----------------------
|
||
|
|
|
||
|
|
.. d2:: diagrams/actor_tree.d2
|
||
|
|
:margin:
|
||
|
|
:caption: The general shape: arbitrary nesting. A cluster is
|
||
|
|
this, minus the nesting.
|
||
|
|
:alt: a nested supervision tree of subactors
|
||
|
|
|
||
|
|
``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,
|
||
|
|
|
||
|
|
- you want a *flat*, homogeneous fleet (classic worker-pool or
|
||
|
|
map-style fan-out shapes),
|
||
|
|
- "one per core" — or a fixed ``count`` — is the right sizing
|
||
|
|
story,
|
||
|
|
- every child can share the same spawn options.
|
||
|
|
|
||
|
|
Drop down to a raw :class:`tractor.ActorNursery` when the topology
|
||
|
|
gets any fancier: nested trees, heterogeneous children, per-child
|
||
|
|
``debug_mode``/transport/module options, daemons mixed with
|
||
|
|
one-shot workers, and so on (see :doc:`/guide/parallelism` for a
|
||
|
|
hand-rolled pool). Either way the supervision semantics are
|
||
|
|
identical: one-cancels-all error propagation and the no-zombies
|
||
|
|
guarantee from :doc:`/guide/cancellation` apply to clusters too.
|
||
|
|
|
||
|
|
Provisional, by design
|
||
|
|
----------------------
|
||
|
|
|
||
|
|
.. note::
|
||
|
|
|
||
|
|
APIs in this section are considered **provisional**: the
|
||
|
|
signature and semantics of :func:`tractor.open_actor_cluster`
|
||
|
|
may shift as higher-level supervision machinery lands. We
|
||
|
|
encourage you to try it and provide feedback — the
|
||
|
|
`matrix channel`_ is the place to say hi, and `#22`_ tracks the
|
||
|
|
broader supervisor-strategy roadmap.
|
||
|
|
|
||
|
|
.. seealso::
|
||
|
|
|
||
|
|
- :doc:`/guide/parallelism` — worker pools built "by hand" with
|
||
|
|
plain actor nurseries (and why that's easy peasy),
|
||
|
|
- :doc:`/guide/cancellation` — the teardown machinery a cluster
|
||
|
|
inherits for free.
|
||
|
|
|
||
|
|
.. _matrix channel: https://matrix.to/#/!tractor:matrix.org
|
||
|
|
.. _#22: https://github.com/goodboy/tractor/issues/22
|