lib/wrapper.ex

defmodule Individual.Wrapper do
  @moduledoc """
  This module wraps underlying workers, that can not be registered in `:global` scope.

  ### The problem

  Worker can not register it's name by default, so we need wrapper, that would be
  registered inside global module for that purposes.

  ### Solution

  So, `Individual` will monitor `Individual.Wrapper`, which will be unique across cluster.
  Wrapper will monitor underlying module. If `Individual.Wrapper` will not be able to start
  because of name conflict, underlying module will even not try to start.

  When one of the cluster nodes will fall, `Individual` modules on all other
  nodes will try to start there own `Individual.Wrapper`'s. The first one, that will be started -
  will continue to work, and will start underlying module. All other will fail because
  of name conflicts.
  """
  require Logger

  @doc false
  def child_spec(son_child_spec) do
    Map.merge(son_child_spec, %{
      # We need to specify this worker as a supervisor to prevent timeout crashes
      type: :supervisor,
      # Shutdown should be specified as infinity to prevent timeout crashes
      shutdown: :infinity,
      start: {__MODULE__, :start_link, [son_child_spec]}
    })
  end

  def time_alive(pid) do
    GenServer.call(pid, :time_alive)
  end

  @doc """
  This function will be called by `Individual` module. No need to call it manually
  """
  def start_link(son_child_spec) do
    case GenServer.start_link(
           __MODULE__,
           son_child_spec,
           name: {:via, Individual.Registry, :"#Individual.Wrapper<#{son_child_spec.id}>"}
         ) do
      {:ok, pid} ->
        Process.register(pid, :"#Individual.Wrapper<#{son_child_spec.id}>")
        {:ok, pid}

      error ->
        error
    end
  end

  @doc false
  def init(son_child_spec) do
    # We are trying to register the wrapper with given name first. If the name
    # is taken, the worker's initialization process should not be started.
    # So we pend worker's initialization into main loop message handling.
    Process.send(self(), :init, [])
    {:ok, son_child_spec}
  end

  @doc false
  def handle_info(:init, son_child_spec) do
    Process.put(Individual.Wrapper.StartTime, :erlang.system_time())
    {:ok, son_child_spec} = start_worker(son_child_spec)
    {:noreply, son_child_spec}
  end

  def handle_call(:time_alive, _from, state) do
    {:reply, :erlang.system_time() - Process.get(Individual.Wrapper.StartTime, 0), state}
  end

  @doc false
  def start_worker(%{start: {module, function, args}, id: id} = son_child_spec) do
    # This function tries to start a worker.
    # If everything goes ok, this module starts to be supervised by wrapper.
    case Kernel.apply(module, function, args) do
      {:ok, pid} when is_pid(pid) ->
        Logger.debug("Individual: Starting worker #{son_child_spec.id}")
        Process.link(pid)
        {:ok, son_child_spec}

      {:error, {:already_started, pid}} ->
        raise RuntimeError,
              "Individual's supervised module #{id} try to take the name, that is already registered in current node's scope for process #{
                inspect pid
              }. Please, check your worker's starting functions - may be it's naming is in conflict with other names!"

      _err ->
        raise RuntimeError,
              "Individual's supervised module #{id} fails to start properly. It should return `{:ok, pid}` tuple to get supervised by Individual. Please, check you worker's starting functions - may be they don't return expected tuple!"
    end
  end
end