defmodule Liaison.Strategy.Epmd do @moduledoc """ Strategy for node connections via EPMD ```elixir config :liaison, strategy: [ [ strategy: #{__MODULE__}, reconnect_period: 10, nodes: [] ] ] ``` ## Staying Connected Staying conencted via the Epmd strategy is now easier than ever, just specify the nodes you want to connect to, and how often you want to attempt re-connect """ @type strategy_config :: Keyword.t() @default_config [ nodes: [], reconnect_period: 10 ] alias Liaison.Logger alias Liaison.NodeHelper use GenServer @doc false def child_spec(opts) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [opts]} } end @spec start_link(keyword) :: :ignored | {:error, any()} | {:ok, pid} def start_link(strategy_config) do config = merge_config(strategy_config) |> expand_nodes() GenServer.start_link(__MODULE__, config, name: __MODULE__) end defp merge_config(config1 \\ @default_config, config2) do Keyword.merge(config1, config2) end defp expand_nodes(config) do nodes = Enum.map(config[:nodes], &NodeHelper.to_nodename/1) Keyword.put(config, :nodes, nodes) end @doc false @spec init(any) :: {:ok, any} def init(state) do log(:info, "Monitoring started") Process.flag(:trap_exit, true) :net_kernel.monitor_nodes(true) send(self(), :ensure_connected) {:ok, state} end # ---------------- node up and down management -------------- def handle_info(:ensure_connected, state) do {_already_con, connected, not_connected} = connect_all(state[:nodes]) case connected do [] -> nil con -> log(:info, "Connected: #{inspect(con)}") end case not_connected do [] -> nil not_connected -> log(:warn, "Couldnt connect: #{inspect(not_connected)}") schedule(:ensure_connected, state[:reconnect_period]) end {:noreply, state} end def handle_info({:nodedown, node}, state) do log(:debug, "node #{node} down") send(self(), :ensure_connected) display_nodes() {:noreply, state} end def handle_info({:nodeup, node}, state) do log(:debug, "node #{node} up") display_nodes() {:noreply, state} end # --------------------- server calls -------------- def handle_call(:get_config, _from, state) do {:reply, state, state} end def handle_call({:update_strategy, strategy}, _from, old_state) do new_state = merge_config(old_state, strategy) |> expand_nodes() {:reply, {old_state, new_state}, new_state} end # ---------------------- helper functions ------------ defp log(level, msg) do Logger.log(level, "Epmd", msg) end defp schedule(msg, seconds) do Process.send_after(self(), msg, seconds * 1000) end defp display_nodes() do log(:debug, "node list: #{inspect(get_nodes())}") end # returns {already_connected, connected, not_connected} defp connect_all(nodes) do default = %{already_connected: [], connected: [], not_connected: []} Enum.reduce(nodes, default, fn node, acc -> case Enum.member?(get_nodes(), node) do true -> Map.update!(acc, :already_connected, &[node | &1]) _ -> case connect(node) do true -> Map.update!(acc, :connected, &[node | &1]) _ -> Map.update!(acc, :not_connected, &[node | &1]) end end end) |> Map.values() |> List.to_tuple() end defp connect(node), do: Node.connect(node) defp get_nodes(), do: Node.list() # -------------------- client calls ---------------- @doc """ Returns the current configuration of the strategy """ @spec get_config :: keyword def get_config() do GenServer.call(__MODULE__, :get_config) end @doc """ Updates the given module to adopt a new strategy. Returns {old config, new config} """ @spec update_strategy(strategy_config) :: {:ok, {strategy_config, strategy_config}} def update_strategy(strat_config) do change = GenServer.call(__MODULE__, {:update_strategy, strat_config}) {:ok, change} end end