Skip to content

Discovery ​

Discovery answers one question, once: which peers does a fresh node try to reach at boot? A node resolves a seed list, dials those seeds to join their cluster, and never calls discovery again. Every topology change after that, a node joining, leaving, failing, or changing metadata, is reported by membership, not by discovery.

Node bootsResolve seed listAny seedreachable?Join viaa seedyesno / emptyDeadline left?yes, retrynoAnchor a new clusterIn the cluster

An empty seed list is not an error: while an environment is still coming up (no pod of a headless service has registered yet), the provider returns empty and the boot sequence retries until a seed appears or the bootstrap deadline passes, at which point the node anchors a new cluster as its first member.

The discovery provider is the one part of clustering you plug in yourself. Two implementations ship in the zero-dependency core, and you can write your own.

DNS discovery ​

DnsDiscovery resolves a hostname to its addresses, ideal for a headless service in a container orchestrator, where one DNS name resolves to every pod's IP.

ts
import { DnsDiscovery, DnsRecordType } from "@tochemey/nodeakt";

new DnsDiscovery({
  hostname: "orders.default.svc.cluster.local",
  recordType: DnsRecordType.address, // A records; each IP + the given gossip port
  port: 7946,
});
OptionMeaning
hostnameThe name to resolve.
recordTypeDnsRecordType.srv (the default) reads SRV records and takes host and port from each; DnsRecordType.address (A) and DnsRecordType.address6 (AAAA) read IPs and attach port.
portThe gossip port to attach to each address in address/address6 mode; ignored for srv, which carries its own.
resolverAn optional resolver to query; defaults to the platform's node:dns.

With SRV records the service registers each node's gossip host and port, so no port is needed. With A records every node shares one gossip port, which is what the example uses: a Compose network alias resolves to all three containers. The Kubernetes example runs the same configuration against a headless service, whose name resolves to every pod behind it.

These options are the DnsDiscoveryOptions type, and recordType is a DnsRecordTypeValue. To point discovery at a non-default DNS source, pass a custom resolver implementing DnsResolver, whose SRV answers are DnsSrvRecords.

Static discovery ​

StaticDiscovery resolves a fixed list, for a known topology or a test.

ts
import { StaticDiscovery } from "@tochemey/nodeakt";

new StaticDiscovery(["10.0.0.1:7946", "10.0.0.2:7946", "10.0.0.3:7946"]);

The list may be empty to start a node that anchors its own cluster and waits for others to find it. Each entry is a host:port string; a blank entry is a TypeError at construction.

A custom provider ​

Discovery is deliberately pluggable, so the core takes on no cloud or orchestration dependency. Implement DiscoveryProvider to pull seeds from a Kubernetes API query, a Consul or NATS registry, or a cloud instance lookup:

ts
import type { DiscoveryProvider } from "@tochemey/nodeakt";

class ConsulDiscovery implements DiscoveryProvider {
  async resolve(): Promise<readonly string[]> {
    // Query your registry and return each peer's gossip endpoint as "host:port".
    // Return [] (not a rejection) while the source is simply not populated yet.
    return await lookupGossipEndpoints();
  }
}
  • resolve() is called once, at boot, and never again. Do not poll it for topology.
  • Return an empty array while the source is still coming up; the boot sequence treats empty as "not ready" and retries. Reject only on a genuine failure to query the source at all.

Released under the MIT License.