defmodule NodeRegistry do
@moduledoc """
A simple registry for nodes.
It uses the Erlang :global module. https://www.erlang.org/doc/man/global.html
One must start the registry on each node:
Add {NodeRegistry, :my_service} to the supervisor's children list.
"""
use GenServer
defstruct [:node, :name, :pid]
@type t :: %__MODULE__{node: node, name: atom, pid: pid}
@lookup_prefix :node_registry
@spec start_link(atom()) :: GenServer.on_start()
def start_link(name) when is_atom(name) do
name = :"#{name}_#{Node.self()}"
GenServer.start_link(__MODULE__, name, name: __MODULE__)
end
@impl true
def init(name) do
case :global.register_name({@lookup_prefix, name}, self()) do
:yes ->
:global.sync()
{:ok, %__MODULE__{node: Node.self(), name: name, pid: self()}}
:no ->
{:stop, :cannot_register}
end
end
@impl true
def handle_call(:state, _from, state), do: {:reply, state, state}
@spec list() :: list(NodeRegistry.t())
def list, do: all()
@spec all() :: list(NodeRegistry.t())
def all do
:global_names
|> :ets.match({{@lookup_prefix, :"$1"}, :"$2", :_, :_})
|> Enum.reduce(%{}, fn [name, pid], acc ->
Map.put(acc, name, :erlang.node(pid))
end)
end
@spec info(node | atom) :: NodeRegistry.t()
def info(:self) do
GenServer.call(__MODULE__, :state)
end
def info(:all) do
Enum.map(all(), fn {_name, node} -> info(node) end)
end
def info(node) do
:rpc.call(node, NodeRegistry, :info, [:self])
end
@spec node_with_name(atom) :: node | nil
def node_with_name(name) when is_atom(name) do
case :global.whereis_name({@lookup_prefix, name}) do
:undefined ->
nil
pid when is_pid(pid) ->
:erlang.node(pid)
end
end
@spec nodes_with_prefix(atom) :: list(node)
def nodes_with_prefix(prefix) when is_atom(prefix) do
all()
|> Enum.filter(fn {name, _node} -> String.starts_with?("#{name}", "#{prefix}") end)
|> Enum.map(fn {_name, node} -> node end)
end
@spec random_node_with_prefix(atom) :: node
def random_node_with_prefix(prefix) when is_atom(prefix) do
case nodes_with_prefix(prefix) do
[] -> nil
nodes -> Enum.random(nodes)
end
end
end