tractor/docs/guide/clustering.rst

128 lines
4.9 KiB
ReStructuredText
Raw Normal View History

2026-06-11 19:17:14 +00:00
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