Client + Cluster¶
In this guide you'll connect to a running cluster from outside using ClusterClient. Along the way you'll learn how the client discovers topology, how entity_ref creates a local proxy for routing, and how ask() enables request-reply from external code.
When to Use ClusterClient¶
ClusteredActorSystem joins the cluster as a full member — it runs actors, owns shards, participates in gossip. ClusterClient is for code that uses the cluster without joining it: API servers, CLI tools, monitoring dashboards. It connects via TCP, subscribes to topology updates, and routes messages directly to the node owning each shard.
Messages and Entity Factory¶
A loyalty points system. Each user is a sharded entity:
@dataclass(frozen=True)
class AddPoints:
amount: int
@dataclass(frozen=True)
class GetPoints:
reply_to: ActorRef[int]
type LoyaltyMsg = AddPoints | GetPoints
NUM_SHARDS = 10
# ── Entity factory ───────────────────────────────────────────────────
def loyalty_entity(entity_id: str) -> Behavior[LoyaltyMsg]:
def active(points: int = 0) -> Behavior[LoyaltyMsg]:
async def receive(_ctx: Any, msg: LoyaltyMsg) -> Behavior[LoyaltyMsg]:
match msg:
case AddPoints(amount):
print(f" [{entity_id}] +{amount} points (total: {points + amount})")
return active(points + amount)
case GetPoints(reply_to):
reply_to.tell(points)
return Behaviors.same()
return Behaviors.receive(receive)
return active()
Starting the Cluster¶
A single-node cluster with a sharded "loyalty" entity type:
async def main() -> None:
# 1. Start a single-node cluster
cluster = ClusteredActorSystem(
name="loyalty-cluster",
host="127.0.0.1",
port=25530,
node_id="node-1",
)
async with cluster:
cluster.spawn(
Behaviors.sharded(loyalty_entity, num_shards=NUM_SHARDS),
"loyalty",
)
await cluster.wait_for(1)
print("── Cluster running ──\n")
Connecting from Outside¶
ClusterClient takes a list of contact points and the cluster's system name:
# 2. Connect from outside with ClusterClient
async with ClusterClient(
contact_points=[("127.0.0.1", 25530)],
system_name="loyalty-cluster",
) as client:
await asyncio.sleep(1.0)
The client connects to the contact point, subscribes to TopologySnapshot updates, and caches shard allocations locally. entity_ref("loyalty", num_shards=NUM_SHARDS) creates a local proxy actor that routes ShardEnvelope messages to the cluster.
Fire and Forget¶
tell() works the same as inside the cluster — wrap the message in ShardEnvelope:
async def send_points(loyalty: ActorRef[ShardEnvelope[LoyaltyMsg]]) -> None:
print("── Sending points ──")
loyalty.tell(ShardEnvelope("user-1", AddPoints(100)))
loyalty.tell(ShardEnvelope("user-1", AddPoints(50)))
loyalty.tell(ShardEnvelope("user-2", AddPoints(200)))
await asyncio.sleep(0.5)
Request-Reply¶
client.ask() creates a temporary ref, sends the message, and waits for the response:
async def query_points(
client: ClusterClient, loyalty: ActorRef[ShardEnvelope[LoyaltyMsg]]
) -> None:
print("\n── Querying points ──")
for user in ("user-1", "user-2"):
points: int = await client.ask(
loyalty,
lambda r, uid=user: ShardEnvelope(uid, GetPoints(reply_to=r)),
timeout=5.0,
)
print(f" {user}: {points} points")
The reply travels back through the existing TCP connection — no extra server socket or reverse tunnel needed.
Running It¶
async def main() -> None:
# 1. Start a single-node cluster
cluster = ClusteredActorSystem(
name="loyalty-cluster",
host="127.0.0.1",
port=25530,
node_id="node-1",
)
async with cluster:
cluster.spawn(
Behaviors.sharded(loyalty_entity, num_shards=NUM_SHARDS),
"loyalty",
)
await cluster.wait_for(1)
print("── Cluster running ──\n")
# 2. Connect from outside with ClusterClient
async with ClusterClient(
contact_points=[("127.0.0.1", 25530)],
system_name="loyalty-cluster",
) as client:
await asyncio.sleep(1.0)
loyalty = client.entity_ref("loyalty", num_shards=NUM_SHARDS)
await send_points(loyalty)
await query_points(client, loyalty)
asyncio.run(main())
Output:
── Cluster running ──
── Sending points ──
[user-1] +100 points (total: 100)
[user-1] +50 points (total: 150)
[user-2] +200 points (total: 200)
── Querying points ──
user-1: 150 points
user-2: 200 points
Run the Full Example¶
git clone https://github.com/gabfssilva/casty.git
cd casty
uv run python examples/guides/07_cluster_client.py
What you learned:
ClusterClientconnects to a cluster without joining it — topology-aware routing with zero membership.entity_ref(shard_type, num_shards=N)returns a cached proxy that routesShardEnvelopeto the correct node.client.ask()enables request-reply from outside the cluster using temporary refs.- Fault tolerance is built in — the client rotates contact points on timeout and caches topology locally.