Joining and seeds
Это содержимое пока не доступно на вашем языке.
A node enters a cluster by contacting a seed node. The seed
gossips back its current membership view; the joiner is added as
joining, propagates through gossip, and once the leader sees it
(plus convergence), transitions to up.
This page covers the mechanics of that handshake, plus the seed-discovery layer on top.
The simplest case — explicit seeds
Section titled “The simplest case — explicit seeds”import { ActorSystem, Cluster, ClusterOptions } from 'actor-ts';
const system = ActorSystem.create('my-app');
const clusterOptions = ClusterOptions.create() .withHost('10.0.0.5') .withPort(2552) .withSeeds(['10.0.0.5:2552', '10.0.0.6:2552', '10.0.0.7:2552']);const cluster = await Cluster.join( system, clusterOptions,);Three seeds. The joiner contacts each in order until one
responds. Once any seed accepts, the cluster’s gossip propagates
the new member; convergence to up happens within a few seconds
on a healthy network.
The seed list is just a bootstrap hint — once joined, the node learns about every other peer via gossip. Seeds don’t have to be special after the join.
Configuration
Section titled “Configuration”type ClusterOptionsType = { host: string; // this node's address port: number; // this node's TCP port seeds?: string[]; // peer addresses for bootstrap roles?: string[]; // role tags failureDetector?: Partial<...>; transport?: Transport; gossipIntervalMs?: number; seedRetryIntervalMs?: number; // retry interval if no seed responds // ...};The seed-related knobs:
| Setting | Default | What |
|---|---|---|
seeds | [] | List of "host:port" strings. Empty = “I’m the first.” |
seedRetryIntervalMs | 3000 | If no seed responds, retry the list this often until one does. |
selfElection | 'immediate' | When this node may form a cluster alone: 'immediate' (only on an empty seed list), 'never', or a millisecond grace. Set by cluster bootstrap. |
The first node
Section titled “The first node”const clusterOptions = ClusterOptions.create() .withHost('0.0.0.0') .withPort(2552) .withSeeds([]);const cluster = await Cluster.join( system, clusterOptions,);An empty seeds list (or one that’s all-unreachable) means this
node bootstraps the cluster by itself. It auto-promotes to
leader; future joiners contact it.
This makes single-node development trivial — no seed list to maintain. Add a second node later by giving it the first’s address as a seed.
For production, designate one node with an empty seed list and give the rest that node’s address — or, better, use cluster bootstrap, which removes the need for a designated first node entirely.
The symmetric seed list does not cold-start
Section titled “The symmetric seed list does not cold-start”It is tempting to hand every node the same list containing every node. That configuration never forms a cluster:
Cluster removes this node’s own address from its seed list, so a
symmetric list leaves no node with the empty list that
selfElection: 'immediate' requires. Every node stays joining
forever; there is no leader, and the leader is what promotes
joining → up.
Two ways out:
- One designated first node with
seeds: [], the rest pointing at it. Simple, but that node is special at start-up, which containers and autoscalers make awkward. - Cluster bootstrap — every node gets the same configuration and the framework elects exactly one to break the tie. This is the option to reach for whenever nodes start simultaneously.
The seedRetryIntervalMs retry loop still matters, but it solves a
different problem: a seed that is reachable eventually. It cannot
manufacture a first member.
Seed discovery — beyond a static list
Section titled “Seed discovery — beyond a static list”A hard-coded seed list works for tests and small clusters. For production where nodes have dynamic IPs (containers, K8s pods), use a seed provider:
| Provider | When |
|---|---|
| Config | Static list (the case above). |
| DNS | Resolves _actor-ts._tcp.example.com SRV records. |
| Kubernetes API | Lists pods matching a label selector. |
| Aggregate | Falls through multiple providers (e.g. K8s, then DNS). |
import { KubernetesApiSeedProvider, KubernetesApiSeedProviderOptions } from 'actor-ts/discovery';
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create() .withNamespace('default') .withServiceName('actor-ts') .withPort(2552);const seedProvider = new KubernetesApiSeedProvider( kubernetesApiSeedProviderOptions,);
const seeds = await seedProvider.discover();
const clusterOptions = ClusterOptions.create() .withHost(process.env.POD_IP!) .withPort(2552) .withSeeds(seeds);const cluster = await Cluster.join( system, clusterOptions,);The provider returns a snapshot of seed addresses; the framework uses them to bootstrap the join. See Discovery overview for the seed provider model.
Watching the join progress
Section titled “Watching the join progress”import { SelfUp, MemberUp } from 'actor-ts';
cluster.subscribe((evt) => { if (evt instanceof SelfUp) { console.log(`this node is now Up`); } else if (evt instanceof MemberUp) { console.log(`peer ${evt.member.address} reached Up`); }});Two key events:
SelfUpfires once when this node transitions toup. Useful gate for starting work that requires cluster membership.MemberUpfires every time any member reachesup.
For startup logic that needs other members (“wait until at least 3
nodes are up before serving traffic”), count MemberUps after
SelfUp.
What can go wrong
Section titled “What can go wrong”Where to next
Section titled “Where to next”- Cluster overview — the bigger picture.
- Cluster bootstrap — stable observation and initial-seed election for simultaneous starts.
- Weakly-up — gradual-join semantics for slow convergence.
- Failure detector — how heartbeats keep the membership view fresh after join.
- Discovery overview — seed providers for dynamic environments.
- Downing strategies — split-brain resolution after the cluster forms.
