Skip to main content

Cluster events

Some things a node wants to say are not addressed to anyone in particular — the leader changed, a key was invalidated, a new epoch of work has begun. The publisher does not know or care who is listening; it announces, and whoever subscribed reacts.

That is what the cluster event bus provides, reached through node.Events (IClusterEvents). It is a typed publish/subscribe system that propagates events across the whole cluster, and it is deliberately distinct from direct messaging.

Events vs. direct messaging

Both ride the same transport, but they model different intents. Use events when the publisher should not have to know who consumes the announcement, which keeps producers and consumers independent as the cluster evolves.

Direct messaging (node.Messaging)Cluster events (node.Events)
AddressingYou name the recipient(s).You name the event type; subscribers self-select.
CouplingSender knows the receiver.Publisher and subscriber are decoupled.
ShapeOne-to-one or one-to-all, request/reply.One-to-many fan-out by subscription.
AnalogyA phone call.A broadcast on a topic.

Cluster event fan-out: a publisher calls PublishAsync and the cluster-wide event bus delivers the event to self-selected subscribers on node-2, node-3, and the publishing node's own local subscribers.

The event contract

Every event implements IClusterEvent, which carries two things: a Scope (LocalOnly or ClusterWide) and an EventType string. You rarely implement the interface by hand — you derive from ClusterEventBase<T> (which fixes Scope to ClusterWide and gives you a typed Payload and a Timestamp) or, for arbitrary payloads, use CustomClusterEvent<T>.

Built-in lifecycle events

Nodus publishes its own lifecycle events on the same bus, so you can subscribe to them just like your own:

EventFires when
NodeJoinedEventA node is admitted into active membership.
NodeLeftEventA node leaves or is evicted.
LeaderChangedEventLeadership changes — carries NewLeader and Epoch.
NodeRestartedEventA node is observed to have restarted — carries the observed Generation.
node.Events.Subscribe<LeaderChangedEvent>(evt =>
{
Console.WriteLine($"leader is now {evt.NewLeader.NodeId} at epoch {evt.Epoch}");
return Task.CompletedTask;
});
Two ways to hear about leadership and membership

Lifecycle changes are available both as convenience events on node.Cluster / node.Leadership (MemberJoined, MemberLeft, Changed) and as cluster events on node.Events (NodeJoinedEvent, and so on). The convenience events are simplest for local reactions; the cluster-event forms are useful when you already run an event-driven subscription pipeline.

Custom events

For your own announcements, wrap a payload in CustomClusterEvent<T>:

public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}

// publish
var evt = new CustomClusterEvent<Customer>(new Customer { Id = 1, Name = "Ada" })
{
Publisher = node.NodeId,
};
await node.Events.PublishAsync(evt);

// subscribe — note the two type args: the event and its payload
node.Events.Subscribe<CustomClusterEvent<Customer>, Customer>(evt =>
{
Console.WriteLine($"{evt.Publisher} published customer {evt.Payload.Name}");
return Task.CompletedTask;
});

CustomClusterEvent<T> also carries an optional Publisher id and a Metadata dictionary you can use to tag events.

Publishing and dispatch options

PublishAsync<T> accepts an optional EventDispatchOptions that controls how the event is delivered:

await node.Events.PublishAsync(evt, new EventDispatchOptions
{
Policy = DispatchPolicy.Retry, // FireAndForget | Ordered | Parallel | Retry
Scope = DeliveryScope.ClusterWide, // ClusterWide (default) | LocalOnly
MaxRetryAttempts = 3,
RetryDelayMilliseconds = 200,
});
OptionValuesMeaning
PolicyFireAndForget (default), Ordered, Parallel, RetryHow subscribers are dispatched and whether delivery is retried.
ScopeClusterWide (default), LocalOnlyWhether the event crosses the cluster or stays on the publishing node.
MaxRetryAttemptsint (default 3)Retry count when Policy = Retry.
RetryDelayMillisecondsint (default 200)Delay between retries.

The default — fire-and-forget, cluster-wide — is the right choice for most announcements. Choose Ordered when subscribers must see events in publish order, Retry when a transient delivery failure should be re-attempted, and LocalOnly when the event is purely an in-process signal.

Only members publish

Like broadcast messaging, PublishAsync requires the member role and throws NotSupportedException from a non-member node.

Delivery semantics

Cluster events are a best-effort fan-out by default (FireAndForget), tuned toward low overhead. When you need stronger delivery, opt into Retry (re-attempts on transient failure) or Ordered (preserves publish order per subscriber).

They are not a durable message queue: events are not persisted, and a node that was down when an event was published does not receive it retroactively. If you need a node to reconstruct missed state after it (re)joins, model that as a state transfer over direct messaging or ride a summary on the gossip digest — the event bus is for live announcements, not history.

Next steps