Subscribing to events
A cluster-aware service spends most of its life reacting: a peer joined, the leader changed, someone announced a cache invalidation. Nodus gives you two ways to hear about these — the convenience events on node.Cluster and node.Leadership, and the typed cluster-event bus on node.Events. This page shows both and when to use each.
For the design behind the bus, see Cluster events.
Membership and leadership: the convenience events
The simplest way to react to topology changes is the events directly on the membership and leadership areas. Subscribe once, right after startup.
node.Cluster.MemberJoined += m =>
Console.WriteLine($"joined: {m.NodeId} (members now: {node.Cluster.Members.Count + 1})");
node.Cluster.MemberLeft += m =>
Console.WriteLine($"left: {m.NodeId}");
node.Leadership.Changed += (leader, epoch) =>
{
Console.WriteLine($"leader is now {leader.NodeId} at epoch {epoch}");
if (node.Leadership.IsSelf)
StartLeaderOnlyWork();
else
StopLeaderOnlyWork();
};
Each event carries just what you need to react. MemberJoined and MemberLeft carry the ClusterNode; Changed carries the new leader and the new epoch.
| Event | Signature | Fires when |
|---|---|---|
node.Cluster.MemberJoined | Action<ClusterNode> | A node is admitted to active membership. |
node.Cluster.MemberLeft | Action<ClusterNode> | A node leaves or is evicted. |
node.Leadership.Changed | Action<ClusterNode, int> | Leadership changes; carries the new leader and new epoch. |
This is the most direct way to gate leader-only work: react to Changed, then check IsSelf.
Wire these handlers immediately after StartAsync returns. Membership and leadership can settle within moments of joining, and a handler attached late may miss the first join or the initial election.
The cluster-event bus
node.Events (IClusterEvents) is the typed publish/subscribe bus. It carries both Nodus's own lifecycle events and your custom events, cluster-wide.
Subscribing to built-in lifecycle events
The same lifecycle changes are available as bus events, useful when you already run an event-driven pipeline.
node.Events.Subscribe<LeaderChangedEvent>(evt =>
{
Console.WriteLine($"leader → {evt.NewLeader.NodeId} (epoch {evt.Epoch})");
return Task.CompletedTask;
});
node.Events.Subscribe<NodeJoinedEvent>(evt =>
{
Console.WriteLine($"node joined: {evt.Node.NodeId}");
return Task.CompletedTask;
});
node.Events.Subscribe<NodeLeftEvent>(evt =>
{
Console.WriteLine($"node left: {evt.Node.NodeId}");
return Task.CompletedTask;
});
Each built-in event exposes the members you react to:
| Event | Members |
|---|---|
NodeJoinedEvent | Node (NodeInfo), Incarnation (int) |
NodeLeftEvent | Node (NodeInfo), Incarnation (int) |
LeaderChangedEvent | NewLeader (NodeInfo), Epoch (int) |
NodeRestartedEvent | Node (NodeInfo), Generation (long) |
Custom events
To announce your own domain events, wrap a payload in CustomClusterEvent<T>.
Publishing
Construct the event with your payload, optionally set Publisher, and publish it.
public sealed class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
var evt = new CustomClusterEvent<Customer>(new Customer { Id = 1, Name = "Ada" })
{
Publisher = node.NodeId,
};
await node.Events.PublishAsync(evt);
Subscribing
Custom subscriptions take two type arguments — the event type and its payload type.
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 Metadata dictionary you can use to tag events with routing hints or tracing ids.
PublishAsync requires the member role and throws NotSupportedException from a non-member node. Subscribing is always allowed.
Controlling delivery
PublishAsync accepts an optional EventDispatchOptions to control how the event is delivered.
await node.Events.PublishAsync(evt, new EventDispatchOptions
{
Policy = DispatchPolicy.Ordered, // FireAndForget | Ordered | Parallel | Retry
Scope = DeliveryScope.ClusterWide, // ClusterWide (default) | LocalOnly
MaxRetryAttempts = 5, // used by Retry (default 3)
RetryDelayMilliseconds = 250, // default 200
});
The two enums determine reach and ordering. Policy defaults to FireAndForget; Scope defaults to ClusterWide.
| Option | Default | Choose when |
|---|---|---|
Policy = FireAndForget | Yes | Low-overhead announcements where an occasional miss is fine. |
Policy = Ordered | Subscribers must see events in publish order. | |
Policy = Parallel | Subscribers can be dispatched concurrently. | |
Policy = Retry | Transient delivery failures should be re-attempted. | |
Scope = ClusterWide | Yes | The whole cluster should hear it. |
Scope = LocalOnly | The event is a purely in-process signal. |
Writing robust handlers
Handlers run on the dispatch path, so keep them fast and self-contained.
- Keep handlers fast and non-throwing. An exception in a handler is contained, but a slow handler delays the pipeline. Offload real work to a background task or queue.
- Handlers may run for events from any node, including your own published events. Filter on
evt.Publisherif you must ignore your own. - Events are live, not historical. A node that was down when an event was published does not receive it later. To reconstruct missed state after (re)joining, use a state transfer over direct messaging or the gossip digest — the bus is for current announcements, not replay.
node.Events.Subscribe<CustomClusterEvent<Customer>, Customer>(async evt =>
{
if (evt.Publisher == node.NodeId) return; // ignore my own
try
{
await ProjectAsync(evt.Payload); // real work off the dispatch path if heavy
}
catch (Exception ex)
{
_logger.LogWarning(ex, "failed to project customer {Id}", evt.Payload.Id);
}
});
Convenience events versus the bus
Use the convenience events for straightforward "when the leader changes, do X." Use the bus when you want custom events, delivery control, or a uniform event-handling pipeline.
| Convenience events | Cluster-event bus | |
|---|---|---|
| Reach | node.Cluster, node.Leadership | node.Events |
| Best for | Simple local reactions to topology | Event-driven pipelines and custom events |
| Custom payloads | No | Yes (CustomClusterEvent<T>) |
| Delivery options | No | Yes (EventDispatchOptions) |
Next steps
- Cluster events — the design and delivery semantics.
- Health and membership — querying state, not just reacting to changes.
- Leader election — what drives
LeaderChanged.