Current section
Files
Jump to
Current section
Files
lib/cluster_helper.ex
defmodule ClusterHelper do
@moduledoc """
A helper module for managing dynamic Elixir clusters.
`ClusterHelper` provides a simple, role-based API for tracking nodes in a
distributed cluster. It is built on top of the `:syn` library and is designed
for dynamic environments such as Kubernetes.
## Features
- **Role-based node management** – assign one or more roles to each node
- **Dynamic cluster support** – nodes are discovered and tracked automatically
as they join or leave the cluster
- **Fast lookups** – ETS-backed queries for both role→nodes and node→roles
- **Cluster-wide synchronisation** – changes propagate via pub/sub and a
configurable periodic pull
## Quick start
# Tag the current node
ClusterHelper.add_role(:web)
ClusterHelper.add_roles([:api, :cache])
# Query the cluster
ClusterHelper.get_nodes(:web) #=> [:"node1@host", :"node2@host"]
ClusterHelper.get_roles(:"node1@host") #=> [:web, :api]
ClusterHelper.all_nodes() #=> [:"node1@host", :"node2@host"]
# Check or remove roles
ClusterHelper.get_my_roles() #=> [:web, :api, :cache]
ClusterHelper.remove_role(:cache)
ClusterHelper.remove_roles([:api])
## Configuration (`config/config.exs`)
config :cluster_helper,
# Roles applied at startup (can also be changed at runtime)
roles: [:data, :web],
# Scope for the :syn library (default: ClusterHelper)
scope: :my_cluster,
# Timeout for remote role-pull RPC calls (default: 5_000 ms)
pull_timeout: 5_000,
# Interval between periodic background syncs (default: 7_000 ms)
pull_interval: 10_000
## Notes
Nodes must first be connected at the Erlang distribution level (e.g. via
[libcluster](https://hex.pm/packages/libcluster) or `Node.connect/1`).
`ClusterHelper` only tracks *roles* – it does not handle node discovery.
"""
alias ClusterHelper.NodeConfig
@type role :: atom()
@type node_name :: node()
# ── Role queries ─────────────────────────────────────────────────────────────
@doc """
Returns every node in the cluster that has been assigned `role`.
Returns `[]` when no node carries that role.
## Examples
ClusterHelper.add_role(:web)
ClusterHelper.get_nodes(:web)
#=> [:"node1@127.0.0.1"]
ClusterHelper.get_nodes(:unknown)
#=> []
"""
@spec get_nodes(role()) :: [node_name()]
def get_nodes(role), do: NodeConfig.get_nodes(role)
@doc """
Returns all roles assigned to `node`.
Returns `[]` when the node is unknown or has no roles.
## Examples
ClusterHelper.add_roles([:web, :api])
ClusterHelper.get_roles(Node.self())
#=> [:web, :api]
"""
@spec get_roles(node_name()) :: [role()]
def get_roles(node), do: NodeConfig.get_roles(node)
@doc """
Returns a deduplicated list of every node that has at least one role.
## Examples
ClusterHelper.all_nodes()
#=> [:"node1@127.0.0.1", :"node2@127.0.0.1"]
"""
@spec all_nodes() :: [node_name()]
def all_nodes(), do: NodeConfig.get_all_nodes()
# ── Local-node role management ────────────────────────────────────────────────
@doc """
Returns all roles currently assigned to this node.
## Examples
ClusterHelper.add_roles([:web, :api])
ClusterHelper.get_my_roles()
#=> [:web, :api]
"""
@spec get_my_roles() :: [role()]
def get_my_roles(), do: NodeConfig.get_my_roles()
@doc """
Adds `role` to the current node and propagates the change cluster-wide.
Duplicate roles are silently ignored.
## Examples
ClusterHelper.add_role(:web)
ClusterHelper.get_my_roles()
#=> [:web]
"""
@spec add_role(role()) :: :ok
def add_role(role), do: NodeConfig.add_role(role)
@doc """
Adds each role in `roles` to the current node and propagates cluster-wide.
All new roles are announced in a single pub/sub event. Duplicates are filtered.
## Examples
ClusterHelper.add_roles([:web, :api, :cache])
ClusterHelper.get_my_roles()
#=> [:web, :api, :cache]
"""
@spec add_roles([role()]) :: :ok
def add_roles(roles), do: NodeConfig.add_roles(roles)
@doc """
Removes `role` from the current node and propagates the change cluster-wide.
## Examples
ClusterHelper.add_roles([:web, :api])
ClusterHelper.remove_role(:api)
ClusterHelper.get_my_roles()
#=> [:web]
"""
@spec remove_role(role()) :: :ok
def remove_role(role), do: NodeConfig.remove_role(role)
@doc """
Removes each role in `roles` from the current node and propagates cluster-wide.
## Examples
ClusterHelper.add_roles([:web, :api, :cache])
ClusterHelper.remove_roles([:api, :cache])
ClusterHelper.get_my_roles()
#=> [:web]
"""
@spec remove_roles([role()]) :: :ok
def remove_roles(roles), do: NodeConfig.remove_roles(roles)
# ── Utility ───────────────────────────────────────────────────────────────────
@doc """
Returns `true` when `node` is the local node, `false` otherwise.
## Examples
ClusterHelper.local_node?(Node.self())
#=> true
ClusterHelper.local_node?(:"other@127.0.0.1")
#=> false
"""
@spec local_node?(node_name()) :: boolean()
def local_node?(node), do: NodeConfig.local_node?(node)
end