lib/mnesiac.ex

defmodule Mnesiac do
  @moduledoc """
  Mnesiac Manager
  """
  require Logger
  alias Mnesiac.StoreManager

  @doc """
  Start Mnesia with strict host checking
  """
  def init_mnesia(nodes) do
    nodes =
      Enum.filter(Node.list(), fn node ->
        node in List.flatten(nodes)
      end)

    case nodes do
      [head | _tail] ->
        join_cluster(head)

      [] ->
        start()
    end
  end

  @doc """
  Start Mnesia alone
  """
  def start do
    with :ok <- ensure_dir_exists(),
         :ok <- ensure_started(),
         :ok <- StoreManager.copy_schema(node()),
         :ok <- StoreManager.init_tables(),
         :ok <- StoreManager.ensure_tables_loaded() do
      :ok
    else
      {:error, reason} ->
        Logger.debug(fn -> "[mnesiac:#{node()}] #{inspect(reason)}" end)
        {:error, reason}
    end
  end

  @doc """
  Join to a Mnesia cluster
  """
  def join_cluster(cluster_node) do
    with :ok <- ensure_dir_exists(),
         :ok <- ensure_stopped(),
         :ok <- StoreManager.delete_schema(),
         :ok <- ensure_started(),
         :ok <- connect(cluster_node),
         :ok <- StoreManager.copy_schema(node()),
         :ok <- StoreManager.copy_tables(cluster_node),
         :ok <- StoreManager.ensure_tables_loaded() do
      :ok
    else
      {:error, reason} ->
        Logger.debug(fn -> "[mnesiac:#{node()}] #{inspect(reason)}" end)
        {:error, reason}
    end
  end

  @doc """
  Cluster status
  """
  def cluster_status do
    running = :mnesia.system_info(:running_db_nodes)
    stopped = :mnesia.system_info(:db_nodes) -- running

    if stopped == [] do
      [{:running_nodes, running}]
    else
      [{:running_nodes, running}, {:stopped_nodes, stopped}]
    end
  end

  @doc """
  Cluster with a node
  """
  def connect(cluster_node) do
    case :mnesia.change_config(:extra_db_nodes, [cluster_node]) do
      {:ok, [_cluster_node]} ->
        :ok

      {:ok, []} ->
        {:error, {:failed_to_connect_node, cluster_node}}

      {:error, reason} ->
        {:error, reason}
    end
  end

  @doc """
  Running Mnesia nodes
  """
  def running_nodes do
    :mnesia.system_info(:running_db_nodes)
  end

  @doc """
  Is node in Mnesia cluster?
  """
  def node_in_cluster?(cluster_node) do
    Enum.member?(:mnesia.system_info(:db_nodes), cluster_node)
  end

  @doc """
  Is running Mnesia node?
  """
  def running_db_node?(cluster_node) do
    Enum.member?(running_nodes(), cluster_node)
  end

  defp ensure_started do
    with :ok <- start_server(),
         :ok <- wait_for(:start) do
      :ok
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  defp ensure_stopped do
    with :stopped <- stop_server(),
         :ok <- wait_for(:stop) do
      :ok
    else
      {:error, reason} ->
        {:error, reason}
    end
  end

  defp ensure_dir_exists do
    mnesia_dir = :mnesia.system_info(:directory)

    with false <- File.exists?(mnesia_dir),
         :ok <- File.mkdir(mnesia_dir) do
      :ok
    else
      true ->
        :ok

      {:error, reason} ->
        {:error, reason}
    end
  end

  defp start_server do
    :mnesia.start()
  end

  defp stop_server do
    :mnesia.stop()
  end

  defp wait_for(:start) do
    case :mnesia.system_info(:is_running) do
      :yes ->
        :ok

      :no ->
        {:error, :mnesia_unexpectedly_stopped}

      :stopping ->
        {:error, :mnesia_unexpectedly_stopping}

      :starting ->
        Process.sleep(1_000)
        wait_for(:start)
    end
  end

  defp wait_for(:stop) do
    case :mnesia.system_info(:is_running) do
      :no ->
        :ok

      :yes ->
        {:error, :mnesia_unexpectedly_running}

      :starting ->
        {:error, :mnesia_unexpectedly_starting}

      :stopping ->
        Process.sleep(1_000)
        wait_for(:stop)
    end
  end
end