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) | |
|---|---|---|
| Addressing | You name the recipient(s). | You name the event type; subscribers self-select. |
| Coupling | Sender knows the receiver. | Publisher and subscriber are decoupled. |
| Shape | One-to-one or one-to-all, request/reply. | One-to-many fan-out by subscription. |
| Analogy | A phone call. | A broadcast on a topic. |
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:
| Event | Fires when |
|---|---|
NodeJoinedEvent | A node is admitted into active membership. |
NodeLeftEvent | A node leaves or is evicted. |
LeaderChangedEvent | Leadership changes — carries NewLeader and Epoch. |
NodeRestartedEvent | A 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;
});
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,
});
| Option | Values | Meaning |
|---|---|---|
Policy | FireAndForget (default), Ordered, Parallel, Retry | How subscribers are dispatched and whether delivery is retried. |
Scope | ClusterWide (default), LocalOnly | Whether the event crosses the cluster or stays on the publishing node. |
MaxRetryAttempts | int (default 3) | Retry count when Policy = Retry. |
RetryDelayMilliseconds | int (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.
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
- Messaging — the directed alternative when you know the recipient.
- Subscribing to events — the task-focused how-to for both lifecycle and custom events.
- Health and gossip — sharing continuous state rather than discrete events.