Installation
Add cluster_helper to your mix.exs dependencies:
def deps do
[
{:cluster_helper, "~> 0.7"}
]
endQuick Start
Nodes must be connected at the Erlang distribution level (e.g. via
libcluster or Node.connect/1).
Tag a node with roles
# Add roles in the default scope
ClusterHelper.add_role(:web)
ClusterHelper.add_roles([:api, "cache", {:shard, 1}])
# Add roles in a specific scope
ClusterHelper.add_role(:worker, :data_processing)
ClusterHelper.add_roles([:ingester, :aggregator], :analytics)Query the cluster
# Find all nodes with a given role
ClusterHelper.get_nodes(:web)
#=> [:"node1@host", :"node2@host"]
# Find all roles for a node
ClusterHelper.get_roles(:"node1@host")
#=> [:web, :api]
# List every known node
ClusterHelper.all_nodes()
#=> [:"node1@host", :"node2@host"]
# Query within a specific scope
ClusterHelper.get_nodes(:worker, :data_processing)
#=> [:"node1@host"]Remove roles
ClusterHelper.remove_role("cache")
ClusterHelper.remove_roles([{:shard, 1}])Manage scopes at runtime
ClusterHelper.join_scope(:monitoring)
ClusterHelper.add_role(:prometheus_exporter, :monitoring)
ClusterHelper.list_scopes()
#=> [:cluster_helper, :monitoring]
ClusterHelper.leave_scope(:monitoring)Core Concepts
Roles
A role is any Elixir term — atoms, strings, tuples, integers, or nested data:
ClusterHelper.add_roles([:web, "cache", {:shard, 1}, 42])Roles are the unit of query. You ask "which nodes have role X?" and the answer is returned from a lock-free ETS read in microseconds.
Scopes
A scope is an isolated namespace. Each scope maintains its own:
- Role-to-node and node-to-role mappings
:pgprocess group for cluster messaging- Generation counter for efficient sync
A node can have :web in scope :frontend and :worker in scope :backend
simultaneously with zero cross-talk.
Propagation
Changes on one node propagate cluster-wide via :pg broadcast and a periodic
pull with generation-based change detection. This ensures convergence even when
broadcasts are missed.