lib/coder_ring/gen_ring.ex

defmodule CoderRing.GenRing do
  @moduledoc """
  GenServer wrapper for a CoderRing.

  Keeping memo state in memory with a process means some database reads can
  be skipped. State is, however, always synced to the database so it can be
  restored properly on app restart.

  Take care to only have one GenRing proc running for a ring at any given
  time. For instance, if running on a multi-server deployment, use Erlang's
  clustered mode and a global process registry like
  [Horde](https://github.com/derekkraan/horde) to guarantee no more than one
  proc for a ring across the cluster.

  When running on a single server, it should be sufficient to use GenRing.

  ## Usage

  Create a module in your application:

  ```elixir
  defmodule MyApp.CoderRing do
    use CoderRing.GenRing, otp_app: :my_app
  end
  ```

  Then, add it to your application supervisor:

  ```elixir
  def start(_type, _args) do
    children =
      [
        ...
      ] ++
        MyApp.CoderRing.child_specs()

    opts = [...]
    {:ok, pid} = Supervisor.start_link(children, opts)
  end
  ```
  """

  defmacro __using__(otp_app: otp_app) do
    quote do
      use CoderRing, otp_app: unquote(otp_app), module: __MODULE__
      use GenServer

      @doc false
      @spec child_spec(atom) :: Supervisor.child_spec()
      def child_spec(name) do
        %{
          id: __MODULE__,
          start: {__MODULE__, :start_link, [name]}
        }
      end

      @doc "Get a list of child specs for all configured rings."
      @spec child_specs :: [Supervisor.child_spec()]
      def child_specs do
        Enum.map(rings(), &child_spec(&1.name))
      end

      @doc """
      Start a GenServer and quietly ignore it when Horde has already started this
      proc somewhere.
      """
      @spec start_link(atom) :: GenServer.on_start()
      def start_link(name) do
        GenServer.start_link(__MODULE__, name, name: :"#{__MODULE__}_#{name}")
      end

      @impl CoderRing
      def call(name, message) do
        GenServer.call(:"#{__MODULE__}_#{name}", message)
      end

      @impl GenServer
      def init(name) do
        {:ok, name |> ring() |> load_memo() |> populate_if_empty()}
      end

      @impl GenServer
      def handle_call(:stop, _from, state) do
        {:stop, :normal, :ok, state}
      end

      def handle_call(message, _from, state) do
        {reply, state} = invoke(state, message)
        {:reply, reply, state}
      end
    end
  end
end