lib/node_registry.ex

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