콘텐츠로 이동
한국어

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.

clusterseed nodesjoining nodeclusterseed nodesjoining nodejoining → weakly-up? → upover a few gossip roundsJoin announcementgossip JoinGossip — current view

This page covers the mechanics of that handshake, plus the seed-discovery layer on top.

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.

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:

SettingDefaultWhat
seeds[]List of "host:port" strings. Empty = “I’m the first.”
seedRetryIntervalMs3000If 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.
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:

n3n2n1n3n2n1All three fresh, all given seed list [n1, n2, n3]filters itself out → seeds = [n2, n3] → not emptysame on both — everyone waits to be let inno member is `up`, so there is no leader,so nobody is ever promotedjoin announcementjoin announcement

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.

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:

ProviderWhen
ConfigStatic list (the case above).
DNSResolves _actor-ts._tcp.example.com SRV records.
Kubernetes APILists pods matching a label selector.
AggregateFalls 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.

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:

  • SelfUp fires once when this node transitions to up. Useful gate for starting work that requires cluster membership.
  • MemberUp fires every time any member reaches up.

For startup logic that needs other members (“wait until at least 3 nodes are up before serving traffic”), count MemberUps after SelfUp.