Cluster & Sharding¶
Three hosting shapes, each with a fixed naming convention.
| Shape | Suffix | Routing | Persistence |
|---|---|---|---|
| Cluster Singleton | *Manager |
one instance per cluster | literal PersistenceId |
| Shard Region | *Worker |
entity ID via message extractor | prefix-{entityId} |
| Local Entity Region | *Worker |
entity ID via EntityIdExtractor, node-local |
prefix-{entityId} |
Cluster Singletons¶
Use for coordinators that must exist exactly once: schedulers, registries, orchestrators. Name them *Manager. If persistent, use a literal PersistenceId.
public sealed class DownloadManager : ReceivePersistentActor
{
public override string PersistenceId => "download-manager";
}
Shard Regions¶
Use for entities that must be distributed across nodes. Name them *Worker. Routing is based on the entity ID that messages expose through IWithEntityId (see Messaging).
builder.WithShardRegion<DownloadWorker>(
typeName: "download",
entityPropsFactory: (_, _, resolver) => id => resolver.Props<DownloadWorker>(id),
messageExtractor: new ShardMessageExtractor(),
shardOptions: new ShardOptions
{
PassivateIdleEntityAfter = TimeSpan.FromMinutes(5)
});
Local Entity Regions¶
Same entity pattern as sharding (ID-based routing, passivation, message extractor) but local to the node. Use for request-scoped or short-lived entities that do not need cluster distribution. Entities self-passivate after finishing their work.
builder.WithEntityRegion<SearchWorker, SearchKey>(...); // EntityIdExtractor works with IWithEntityId
Because the protocol matches cluster sharding, an entity can migrate between the two without code changes.
Passivation¶
Choose the mechanism by region type
- Cluster sharding: configure
ShardOptions { PassivateIdleEntityAfter = ... }in the setup container. Never useContext.SetReceiveTimeoutplus a manualPassivateinside a sharded entity. - Local entity regions: the entity signals completion with a
Passivatemessage to its parent.EntityRegionOptions.PassivationTimeoutis only a safety net for stuck entities.
// Local region entity, done with its work
Receive<WorkFinished>(_ =>
{
Sender.Tell(new WorkCompleted());
Context.Parent.Tell(new Passivate(PoisonPill.Instance));
});
Routing interfaces¶
- One
IWith*Idper region. Each interface is a routing address, not a data type. - Two regions with different purposes get separate interfaces even if the ID value is the same.
- Domain-internal exception: one
IWith*Idmay span several regions of the same domain when it correlates a single multi-actor flow.
Cross-domain sharing is never allowed
Domains talk through messages. A routing interface shared between domains couples them at the type level and defeats domain isolation.
Choosing a shape¶
Decision guide
- Exactly one instance, coordinates others: Manager (singleton)
- Many independent entities, must survive node loss: Worker in a shard region
- Many short-lived entities tied to a single node's request: Worker in a local entity region
- Fixed-size parallel compute without identity:
*Actorin a pool (not an entity region)
Singleton caveats¶
- A singleton is a potential bottleneck. Keep it thin and delegate work to workers.
- During handover the singleton restarts on another node, so state must be recoverable (persistence or re-derivable).
- Child actors of a singleton are created at runtime through
Context.GetActor<T>()orResolveChildActor<T>(), not via constructor-injected refs.