Skip to main content

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.

EventSignatureFires when
node.Cluster.MemberJoinedAction<ClusterNode>A node is admitted to active membership.
node.Cluster.MemberLeftAction<ClusterNode>A node leaves or is evicted.
node.Leadership.ChangedAction<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.

Subscribe before you rely on state

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:

EventMembers
NodeJoinedEventNode (NodeInfo), Incarnation (int)
NodeLeftEventNode (NodeInfo), Incarnation (int)
LeaderChangedEventNewLeader (NodeInfo), Epoch (int)
NodeRestartedEventNode (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.

Only members publish

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.

OptionDefaultChoose when
Policy = FireAndForgetYesLow-overhead announcements where an occasional miss is fine.
Policy = OrderedSubscribers must see events in publish order.
Policy = ParallelSubscribers can be dispatched concurrently.
Policy = RetryTransient delivery failures should be re-attempted.
Scope = ClusterWideYesThe whole cluster should hear it.
Scope = LocalOnlyThe 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.Publisher if 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 eventsCluster-event bus
Reachnode.Cluster, node.Leadershipnode.Events
Best forSimple local reactions to topologyEvent-driven pipelines and custom events
Custom payloadsNoYes (CustomClusterEvent<T>)
Delivery optionsNoYes (EventDispatchOptions)

Next steps