Building a node
Every task with Nodus starts from one entry point that builds the container, joins the cluster, and returns a running handle. This page covers that lifecycle: the two ways to start, what the builder does, the startup contract, and how to shut down cleanly.
The entry point
NodusNode is the single entry point. There are two ways in.
The simplest start
When a default container is enough, StartAsync takes the cluster config and this node's identity and returns a running node.
using Clustron.Nodus.Abstractions;
using Clustron.Nodus.Client;
await using INodusNode node = await NodusNode.StartAsync(config, self);
This is shorthand for NodusNode.Configure(config).WithNodeIdentity(self).StartAsync().
The fluent builder
When you supply your own container, drive config overrides, or compose Nodus into an existing host, use Configure. It returns a NodusNodeBuilder.
await using INodusNode node = await NodusNode
.Configure(config)
.WithNodeIdentity(self) // required: which node this process is
.WithServices(services) // optional: compose into your container
.WithConfiguration(fullConfig) // optional: IConfiguration for env/CLI overrides
.WithOverrides(c => c.Election.Strategy = ElectionStrategyKind.OldestNode) // optional
.StartAsync(cancellationToken);
The builder methods:
| Builder method | Signature | Required |
|---|---|---|
WithNodeIdentity | NodusNodeBuilder WithNodeIdentity(NodeInfo self) | Yes |
WithServices | NodusNodeBuilder WithServices(IServiceCollection services) | No |
WithConfiguration | NodusNodeBuilder WithConfiguration(IConfiguration fullConfig) | No |
WithOverrides | NodusNodeBuilder WithOverrides(Action<NodusConfig> overrides) | No |
StartAsync | Task<INodusNode> StartAsync(CancellationToken ct = default) | Yes |
Node identity is mandatory. A product resolves "which node am I" from its own launch inputs — a command-line argument, the CLUSTRON_NODE_ID environment variable, or an orchestrator-assigned ordinal — and passes the resulting NodeInfo. Calling StartAsync without WithNodeIdentity throws InvalidOperationException.
The two inputs
A node is defined by a NodusConfig (the whole cluster) and a NodeInfo (this process's slice of it). Every process in a cluster shares the same NodusConfig and selects its own NodeInfo from the peer list.
const string clusterId = "orders";
var config = new NodusConfig
{
ClusterId = clusterId,
Cluster = new ClusterInfo
{
Nodes =
{
new NodeInfo { NodeId = "node-1", Host = "localhost", Port = 7601, ClusterId = clusterId, Roles = { NodusRoles.Member } },
new NodeInfo { NodeId = "node-2", Host = "localhost", Port = 7602, ClusterId = clusterId, Roles = { NodusRoles.Member } },
}
},
Election = new ElectionOptions { Strategy = ElectionStrategyKind.OldestNode },
};
// resolve "which node am I" from a launch input
var myId = Environment.GetEnvironmentVariable("CLUSTRON_NODE_ID") ?? "node-1";
var self = config.Cluster.Nodes.Single(n => n.NodeId == myId);
NodusConfig also exposes cluster-wide options. The values below are the code defaults; see Configuration and transport for the full table.
| Property | Type | Default |
|---|---|---|
ClusterId | string | "default-cluster" |
Cluster | ClusterInfo | empty peer set |
Roles | List<string> | empty |
Election | ElectionOptions | OldestNode |
Metrics | MetricsOptions | 60 s window |
RetryOptions | RetryOptions | 3 attempts / 500 ms |
UseDuplexConnections | bool | true |
The container Nodus needs
Nodus core services log through ILogger<T> and resolve an IHttpClientFactory. Register both in the container you hand it.
var services = new ServiceCollection();
services.AddLogging(b => b.AddConsole());
services.AddHttpClient();
If logging or HTTP is missing from the container, startup fails resolving core dependencies. This is the most common first-run mistake.
When you call WithServices(services), Nodus registers its own services into your collection and builds the provider for you. You can register your own product services alongside it and resolve them from node.Services.
Startup order
StartAsync runs a fixed, ordered sequence and returns only once the node is serving.
When the returned Task completes, the node is started and joining. You get back an INodusNode with IsRunning == true, and you can immediately subscribe to events and send messages. StartAsync on the node is idempotent: calling it again on an already-running node is a no-op, so a product that builds the node in its own container and drives the process lifecycle can call it itself.
The running handle
The returned INodusNode is the object a product holds. It carries the node's identity and lifecycle plus the six capability areas.
string id = node.NodeId; // this node's stable id
bool running = node.IsRunning; // true once started, false once stopped
// the six capability areas
node.Cluster; // membership and topology
node.Leadership; // who leads this epoch
node.Messaging; // point-to-point, broadcast, request/reply
node.Events; // typed cluster-wide pub/sub
node.Health; // liveness and metrics
node.Gossip; // state digest on heartbeats
node.Services; // the underlying container, for advanced composition
node.Services exposes the DI container for advanced cases, such as resolving your own registered services. Prefer the named areas — they are the supported surface.
Graceful shutdown and disposal
INodusNode is IAsyncDisposable. There are two ways to leave the cluster cleanly, and they do the same thing.
// explicit
await node.StopAsync();
// or via disposal (await using, or an explicit call)
await node.DisposeAsync();
StopAsync leaves the cluster gracefully: it stops heartbeating, so peers stop seeing this node as alive; it broadcasts its departure so peers evict it promptly rather than waiting out the suspicion window; and it tears down the transport. Disposing an already-stopped node is a no-op. Disposing a running one stops it first.
The idiomatic pattern is await using, which guarantees a clean leave even on an exception.
await using INodusNode node = await NodusNode.StartAsync(config, self);
// ... run until a shutdown signal ...
var done = new TaskCompletionSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; done.TrySetResult(); };
await done.Task;
// leaving the 'await using' scope disposes the node → graceful leave
Because a graceful leave broadcasts the departure, survivors run their re-election immediately instead of waiting out the suspicion window (about 20 seconds). Prefer StopAsync or disposal over killing the process when you can.
Next steps
- Sending messages — put the messaging plane to work.
- Subscribing to events — react to membership and leadership changes.
- Configuration and transport — the
NodusConfigandNodeInfoyou passed here, in full. - Leader election — the strategy you selected in
ElectionOptions.