4.9 KiB
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 tractor.open_actor_cluster: a convenience wrapper which spawns a flat cluster of subactors and hands you back a portal to each,
@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
../../examples/quick_cluster.py
Walkthrough,
open_actor_cluster(modules=[__name__])concurrently spawns one subactor per detected core (permultiprocessing.cpu_count); themoduleslist is the usualenable_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
trio.Nurserythen fans out oneportal.run(sleepy_jane)per worker; each prints its actor.uidfrom 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 strictExceptionGroupwrapping so the demo'sKeyboardInterruptsurfaces as itself instead of arriving eg-boxed,- on block exit the whole fleet is torn down for you via
tractor.ActorNursery.cancel; passhard_kill=Trueat open time to skip straight to OS-level termination instead of the graceful ladder described in/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 tractor.open_root_actor, so fleet-wide runtime options are one kwarg away,
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 /guide/context for that whole layer.
Clusters vs. nurseries
open_actor_cluster() is sugar, not a new primitive: under the hood it's just 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 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 /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 /guide/cancellation apply to clusters too.
Provisional, by design
Note
APIs in this section are considered provisional: the signature and semantics of 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.
/guide/parallelism— worker pools built "by hand" with plain actor nurseries (and why that's easy peasy),/guide/cancellation— the teardown machinery a cluster inherits for free.