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#
import trio
import tractor
async def sleepy_jane() -> None:
uid: tuple = tractor.current_actor().uid
print(f'Yo i am actor {uid}')
await trio.sleep_forever()
async def main():
'''
Spawn a flat actor cluster, with one process per detected core.
'''
portal_map: dict[str, tractor.Portal]
# look at this hip new syntax!
async with (
tractor.open_actor_cluster(
modules=[__name__]
) as portal_map,
tractor.trionics.collapse_eg(),
trio.open_nursery() as tn,
):
for (name, portal) in portal_map.items():
tn.start_soon(
portal.run,
sleepy_jane,
)
await trio.sleep(0.5)
# kill the cluster with a cancel
raise KeyboardInterrupt
if __name__ == '__main__':
try:
trio.run(main)
except KeyboardInterrupt:
print('trio cancelled by KBI')
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 Cancellation and error propagation.
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 The Context: a cross-actor task pair
for that whole layer.
Clusters vs. nurseries#
The general shape: arbitrary nesting. A cluster is this, minus the nesting.#
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 Parallelism and worker pools for a
hand-rolled pool). Either way the supervision semantics are
identical: one-cancels-all error propagation and the no-zombies
guarantee from Cancellation and error propagation 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.
See also
Parallelism and worker pools — worker pools built “by hand” with plain actor nurseries (and why that’s easy peasy),
Cancellation and error propagation — the teardown machinery a cluster inherits for free.