From 55da0ede6929e4611dfd223a4ebf0191594c298d Mon Sep 17 00:00:00 2001 From: beardedeagle Date: Tue, 17 Jul 2018 23:03:46 -0500 Subject: [PATCH] Initial commit --- .formatter.exs | 4 + .gitignore | 22 +++-- README.md | 19 +++- config/.credo.exs | 78 ++++++++++++++++ config/config.exs | 1 + lib/mnesiac.ex | 207 ++++++++++++++++++++++++++++++++++++++++++ lib/store.ex | 87 ++++++++++++++++++ lib/supervisor.ex | 19 ++++ mix.exs | 54 +++++++++++ test/mnesiac_test.exs | 9 ++ test/test_helper.exs | 1 + 11 files changed, 491 insertions(+), 10 deletions(-) create mode 100644 .formatter.exs create mode 100644 config/.credo.exs create mode 100644 config/config.exs create mode 100644 lib/mnesiac.ex create mode 100644 lib/store.ex create mode 100644 lib/supervisor.ex create mode 100644 mix.exs create mode 100644 test/mnesiac_test.exs create mode 100644 test/test_helper.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..9cf23b2 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +[ + inputs: [".formatter.exs", "mix.exs", "config/.credo.exs", "{config,lib,test}/**/*.{ex,exs}"], + line_length: 98 +] diff --git a/.gitignore b/.gitignore index 86e4c3f..45e3818 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,15 @@ -/_build -/cover -/deps -/doc -/.fetch -erl_crash.dump -*.ez +_build +.DS_Store +.elixir_ls +.fetch +.idea +.sonarlint +.vscode *.beam -/config/*.secret.exs +*.ez +cover +deps +doc +erl_crash.dump +mnesiac-*.tar +mnesiac.iml diff --git a/README.md b/README.md index 012dbee..af81958 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# mnesiac -mnesia autoclustering made easy! +# Mnesiac + +Autoclustering for mnesia made easy! + +Docs can be found at [https://hexdocs.pm/mnesiac](https://hexdocs.pm/mnesiac). + +## Installation + +Simply add `mnesiac` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:mnesiac, "~> 0.1.0"} + ] +end +``` diff --git a/config/.credo.exs b/config/.credo.exs new file mode 100644 index 0000000..9ee35a9 --- /dev/null +++ b/config/.credo.exs @@ -0,0 +1,78 @@ +%{ + configs: [ + %{ + name: "default", + files: %{ + included: ["lib/", "src/", "test/", "web/", "apps/"], + excluded: [~r"/_build/", ~r"/deps/"] + }, + requires: [], + strict: true, + color: true, + checks: [ + {Credo.Check.Consistency.ExceptionNames}, + {Credo.Check.Consistency.LineEndings}, + {Credo.Check.Consistency.ParameterPatternMatching}, + {Credo.Check.Consistency.SpaceAroundOperators}, + {Credo.Check.Consistency.SpaceInParentheses}, + {Credo.Check.Consistency.TabsOrSpaces}, + {Credo.Check.Design.AliasUsage, priority: :low}, + {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, + {Credo.Check.Design.TagTODO, exit_status: 2}, + {Credo.Check.Design.TagFIXME}, + {Credo.Check.Readability.AliasOrder}, + {Credo.Check.Readability.FunctionNames}, + {Credo.Check.Readability.LargeNumbers}, + {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 98}, + {Credo.Check.Readability.ModuleAttributeNames}, + {Credo.Check.Readability.ModuleDoc}, + {Credo.Check.Readability.ModuleNames}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, + {Credo.Check.Readability.ParenthesesInCondition}, + {Credo.Check.Readability.PredicateFunctionNames}, + {Credo.Check.Readability.PreferImplicitTry}, + {Credo.Check.Readability.RedundantBlankLines}, + {Credo.Check.Readability.StringSigils}, + {Credo.Check.Readability.TrailingBlankLine}, + {Credo.Check.Readability.TrailingWhiteSpace}, + {Credo.Check.Readability.VariableNames}, + {Credo.Check.Readability.Semicolons}, + {Credo.Check.Readability.SpaceAfterCommas}, + {Credo.Check.Refactor.DoubleBooleanNegation}, + {Credo.Check.Refactor.CondStatements}, + {Credo.Check.Refactor.CyclomaticComplexity}, + {Credo.Check.Refactor.FunctionArity}, + {Credo.Check.Refactor.LongQuoteBlocks}, + {Credo.Check.Refactor.MatchInCondition}, + {Credo.Check.Refactor.NegatedConditionsInUnless}, + {Credo.Check.Refactor.NegatedConditionsWithElse}, + {Credo.Check.Refactor.Nesting}, + {Credo.Check.Refactor.PipeChainStart, + excluded_argument_types: [:atom, :binary, :fn, :keyword], excluded_functions: []}, + {Credo.Check.Refactor.UnlessWithElse}, + {Credo.Check.Warning.BoolOperationOnSameValues}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck}, + {Credo.Check.Warning.IExPry}, + {Credo.Check.Warning.IoInspect}, + {Credo.Check.Warning.LazyLogging}, + {Credo.Check.Warning.OperationOnSameValues}, + {Credo.Check.Warning.OperationWithConstantResult}, + {Credo.Check.Warning.UnusedEnumOperation}, + {Credo.Check.Warning.UnusedFileOperation}, + {Credo.Check.Warning.UnusedKeywordOperation}, + {Credo.Check.Warning.UnusedListOperation}, + {Credo.Check.Warning.UnusedPathOperation}, + {Credo.Check.Warning.UnusedRegexOperation}, + {Credo.Check.Warning.UnusedStringOperation}, + {Credo.Check.Warning.UnusedTupleOperation}, + {Credo.Check.Warning.RaiseInsideRescue}, + {Credo.Check.Refactor.ABCSize, false}, + {Credo.Check.Refactor.AppendSingleItem, false}, + {Credo.Check.Refactor.VariableRebinding, false}, + {Credo.Check.Warning.MapGetUnsafePass, false}, + {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, + {Credo.Check.Readability.Specs, false} + ] + } + ] +} diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..d2d855e --- /dev/null +++ b/config/config.exs @@ -0,0 +1 @@ +use Mix.Config diff --git a/lib/mnesiac.ex b/lib/mnesiac.ex new file mode 100644 index 0000000..1957df3 --- /dev/null +++ b/lib/mnesiac.ex @@ -0,0 +1,207 @@ +defmodule Mnesiac do + @moduledoc """ + Mnesiac Manager + """ + require Logger + + use GenServer + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + @impl true + def init(args) do + GenServer.cast(__MODULE__, {:init, args}) + + {:ok, []} + end + + @impl true + def handle_cast({:init, nodes}, _state) do + init_mnesia(nodes) + + {:noreply, []} + end + + @doc """ + Start Mnesia with/without a cluster + """ + def init_mnesia(nodes) do + nodes = + Enum.filter(List.delete(Node.list(), Node.self()), fn node -> + node in List.delete(List.flatten(nodes), Node.self()) + end) + + case nodes do + [h | _t] -> join_cluster(h) + [] -> start() + end + end + + @doc """ + Start Mnesia with/without a cluster. Test helper. + """ + def init_mnesia(nodes, :test) do + case List.delete(List.flatten(nodes), Node.self()) do + [h | _t] -> join_cluster(h) + [] -> start() + end + end + + @doc """ + Start Mnesia alone + """ + def start do + with :ok <- ensure_dir_exists(), + :ok <- start_server(), + :ok <- Store.copy_schema(Node.self()), + :ok <- Store.init_tables(), + :ok <- Store.ensure_tables_loaded() do + :ok + else + {:error, error} -> {:error, error} + end + end + + @doc """ + Join to a Mnesia cluster + """ + def join_cluster(cluster_node) do + with :ok <- ensure_stopped(), + :ok <- Store.delete_schema(), + :ok <- ensure_started(), + :ok <- connect(cluster_node), + :ok <- Store.copy_schema(Node.self()), + :ok <- Store.copy_tables(), + :ok <- Store.ensure_tables_loaded() do + :ok + else + {:error, reason} -> + Logger.log(:debug, fn -> 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}} + 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} -> + Logger.log(:debug, fn -> inspect(reason) end) + {: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 diff --git a/lib/store.ex b/lib/store.ex new file mode 100644 index 0000000..25b5a5f --- /dev/null +++ b/lib/store.ex @@ -0,0 +1,87 @@ +defmodule Store do + @moduledoc """ + Mnesia Store Manager + """ + @doc """ + Init tables + """ + def init_tables do + case :mnesia.system_info(:extra_db_nodes) do + [] -> create_tables() + [_ | _] -> copy_tables() + end + end + + @doc """ + Ensure tables loaded + """ + def ensure_tables_loaded do + tables = :mnesia.system_info(:local_tables) + + case :mnesia.wait_for_tables(tables, table_load_timeout()) do + :ok -> :ok + {:error, reason} -> {:error, reason} + {:timeout, bad_tables} -> {:error, {:timeout, bad_tables}} + end + end + + @doc """ + Create tables + """ + def create_tables do + Enum.each(stores(), fn data_mapper -> + apply(data_mapper, :init_store, []) + end) + + :ok + end + + @doc """ + Copy tables + """ + def copy_tables do + Enum.each(stores(), fn data_mapper -> + apply(data_mapper, :copy_store, []) + end) + + :ok + end + + @doc """ + Copy schema + """ + def copy_schema(cluster_node) do + copy_type = Application.get_env(:mnesiam, :schema_type, :ram_copies) + + case :mnesia.change_table_copy_type(:schema, cluster_node, copy_type) do + {:atomic, :ok} -> :ok + {:aborted, {:already_exists, :schema, _, _}} -> :ok + {:aborted, reason} -> {:error, reason} + end + end + + @doc """ + Delete schema + """ + def delete_schema do + :mnesia.delete_schema([Node.self()]) + end + + @doc """ + Delete schema copy + """ + def del_schema_copy(cluster_node) do + case :mnesia.del_table_copy(:schema, cluster_node) do + {:atomic, :ok} -> :ok + {:aborted, reason} -> {:error, reason} + end + end + + defp stores do + Application.get_env(:mnesiam, :stores) + end + + defp table_load_timeout do + Application.get_env(:mnesiam, :table_load_timeout, 600_000) + end +end diff --git a/lib/supervisor.ex b/lib/supervisor.ex new file mode 100644 index 0000000..0c1cca6 --- /dev/null +++ b/lib/supervisor.ex @@ -0,0 +1,19 @@ +defmodule Mnesiac.Supervisor do + @moduledoc false + use Supervisor + + def start_link([_config, opts] = args) do + Supervisor.start_link(__MODULE__, args, opts) + end + + def start_link([config]) do + start_link([config, []]) + end + + @impl true + def init([config, opts]) do + opts = Keyword.put(opts, :strategy, :one_for_one) + children = [{Mnesiam, [config]}] + Supervisor.init(children, opts) + end +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 0000000..27f5cbb --- /dev/null +++ b/mix.exs @@ -0,0 +1,54 @@ +defmodule Mnesiac.MixProject do + @moduledoc false + use Mix.Project + + def project do + [ + app: :mnesiac, + version: "0.1.0", + elixir: "~> 1.6", + elixirc_paths: elixirc_paths(Mix.env()), + test_coverage: [tool: ExCoveralls], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test + ], + dialyzer: [plt_add_deps: :transitive], + start_permanent: Mix.env() == :prod, + package: [ + description: "Autoclustering for mnesia made easy!", + files: ["lib", ".formatter.exs", "mix.exs", "README.md", "LICENSE", "CHANGELOG.md"], + maintainers: ["beardedeagle"], + licenses: ["MIT"], + links: %{"GitHub" => "https://github.com/beardedeagle/mnesiac"} + ], + aliases: [ + check: ["format", "compile --force", "credo --strict --all"], + test: "coveralls.html --trace --slowest 10" + ], + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger, :mnesia], + mod: {Mnesiac.Application, []} + ] + end + + defp elixirc_paths(env) when env in [:dev, :test], do: ["lib", "test/support"] + defp elixirc_paths(_env), do: ["lib"] + + defp deps do + [ + {:libcluster, "~> 3.0.2", optional: true}, + {:credo, "~> 0.9", only: [:dev], runtime: false}, + {:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false}, + {:ex_doc, "~> 0.18", only: [:dev], runtime: false}, + {:excoveralls, "~> 0.9", only: [:dev, :test], runtime: false} + ] + end +end diff --git a/test/mnesiac_test.exs b/test/mnesiac_test.exs new file mode 100644 index 0000000..e30f9b0 --- /dev/null +++ b/test/mnesiac_test.exs @@ -0,0 +1,9 @@ +defmodule MnesiacTest do + @moduledoc false + use ExUnit.Case + doctest Mnesiac + + test "greets the world" do + assert Mnesiac.hello() == :world + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()