The Power of Mnesia

Erlang/Elixir

Mnesia is the master-less distributed storage built-in Erlang and by default it inherits all the goodies of Erlang cluster distribution. Lets give it a try.

1. Node 1 (n1@drakarys)

1.1 Start first node

  iex --sname n1

Mind the IEX prompt for (n1@drakarys) node name; drakarys is my laptop.

  Node.self()
iex(n1@drakarys)3>
:n1@drakarys
iex(n1@drakarys)4>

1.2 Install deps

We are going to use a Mnesia wrapper written in Elixir called Memento.

  Mix.install([
    {:memento, "~> 0.4"}
  ])
iex(n1@drakarys)5>
:ok
iex(n1@drakarys)6>

1.3 Define and compile author module

  cat author.ex
defmodule Blog.Author do
  use Memento.Table, attributes: [:username, :fullname]
end
  c("author.ex")
iex(n1@drakarys)7>
[Blog.Author]
iex(n1@drakarys)8>

1.4 Create author table

  Memento.Table.create(Blog.Author)
iex(n1@drakarys)9>
:ok
iex(n1@drakarys)10>

1.5 Create an author

  Memento.transaction! fn ->
    Memento.Query.write(%Blog.Author{username: :sarah, fullname: "Sarah Molton"})
  end
iex(n1@drakarys)11>
%Blog.Author{
  __meta__: Memento.Table,
  username: :sarah,
  fullname: "Sarah Molton"
}
iex(n1@drakarys)12>

1.6 Read authors

  Memento.transaction! fn ->
    Memento.Query.all(Blog.Author)
  end
iex(n1@drakarys)15>
[
  %Blog.Author{
    __meta__: Memento.Table,
    username: :sarah,
    fullname: "Sarah Molton"
  }
]
iex(n1@drakarys)16>

So far so good, nothing new under the sun, just basic storage Create, Read stuff.

2. Node 2 (n2@drakarys)

The same start/install/create things for the second node as well.

2.1 Init the second node

  iex --sname n2
  Node.self()
iex(n2@drakarys)3>
:n2@drakarys
iex(n2@drakarys)4>
  Mix.install([
    {:memento, "~> 0.4"}
  ])
iex(n2@drakarys)5>
:ok
iex(n2@drakarys)6>
  c("author.ex")
iex(n2@drakarys)7>
[Blog.Author]
iex(n2@drakarys)8>

2.4 Read authors

Trying to fetch authors will end up with error because we did not create the table on this node.

  Memento.transaction! fn ->
    Memento.Query.all(Blog.Author)
  end
iex(n2@drakarys)9>
** (Memento.Error) Transaction Failed with: {:no_exists, Blog.Author}
    (memento 0.4.1) lib/memento/transaction.ex:178: Memento.Transaction.handle_result/1
    iex:1: (file)
iex(n2@drakarys)9>

and we only have a single Mnesia node running.

  Memento.system |> Keyword.get(:running_db_nodes)
iex(n2@drakarys)10>
[:n2@drakarys]
iex(n2@drakarys)11>

2.4 Add node in Erlang cluster

Let's connect the two Erlang nodes.

  Node.connect(:n1@drakarys)
  Node.list
iex(n2@drakarys)12>
[:n1@drakarys]
iex(n2@drakarys)13>

2.5 Add node in Mnesia

also connect the nodes in Mnesia.

  Memento.add_nodes(:n1@drakarys)
  Memento.system |> Keyword.get(:running_db_nodes)
iex(n2@drakarys)14>
[:n1@drakarys, :n2@drakarys]
iex(n2@drakarys)15>

2.6 Read authors

  Memento.transaction! fn ->
    Memento.Query.all(Blog.Author)
  end
iex(n2@drakarys)16>
[
  %Blog.Author{
    __meta__: Memento.Table,
    username: :sarah,
    fullname: "Sarah Molton"
  }
]
iex(n2@drakarys)17>

BAM! MAGIC! Mnesia automatically synchronize the two nodes in cluster and returns the author table.

3. Migration

Unfortunately the migration is not implemented in Memento yet (maybe I will give it a try…) and we have to get down to underlying Mnesia Erlang library, is Elixir it is referenced via :mnesia.

3.1 Migrate Author table

To keep things simple we will just add new property (column) called age with default value of 21.

  case :mnesia.create_table(Blog.Author, [attributes: [:username, :fullname, :age]]) do
    {:aborted, {:already_exists, Blog.Author}} ->
      case :mnesia.table_info(Blog.Author, :attributes) do
        [:username, :fullname] ->
          :mnesia.wait_for_tables([Blog.Author], 5000)
          :mnesia.transform_table(
            Blog.Author,
            fn ({Blog.Author, username, fullname}) ->
              {Blog.Author, username, fullname, 21}
            end,
            [:username, :fullname, :age]
            )
        [:username, :fullname, :age] ->
          :ok
        other ->
          {:error, other}
      end
    other ->
      {:ok, other}
  end
iex(n2@drakarys)18>
{:atomic, :ok}
iex(n2@drakarys)19>

3.2 Read author with Mnesia

Now, let's read migrated author using plain :mnesia.

  :mnesia.transaction(fn ->
    :mnesia.read({Blog.Author, :sarah})
  end)
iex(n2@drakarys)20>
{:atomic, [{Blog.Author, :sarah, "Sarah Molton", 21}]}
iex(n2@drakarys)21>

3.3 Read author with Memento

In Memento we need to recompile the new author module with age property first.

  c("author.ex")
iex(n2@drakarys)26>
    warning: redefining module Blog.Author (current version defined in memory)
    │
  1 │ defmodule Blog.Author do
    │ ~~~~~~~~~~~~~~~~~~~~~~~~
    │
    └─ author.ex:1: Blog.Author (module)

[Blog.Author]
iex(n2@drakarys)27>
  Memento.transaction! fn ->
    Memento.Query.all(Blog.Author)
  end
iex(n2@drakarys)28>
[
  %Blog.Author{
    __meta__: Memento.Table,
    username: :sarah,
    fullname: "Sarah Molton",
    age: 21
  }
]
iex(n2@drakarys)29>

BAM! MAGIC! again, we can see the newly added default value for age property.

4. Persisting to disk

And last thing, persistence, by default Mnesia works in memory but we can easily persist specific tables to disk.

4.1 Change table storage type

  # List of nodes where you want to persist
  nodes = [ node() ]

  # Create the schema
  Memento.stop
  Memento.Schema.create(nodes)
  Memento.start

  # Create your tables with disc_copies_o (only the ones you want persisted on disk)
  Memento.Table.create(Blog.Author, disc_copies: nodes)
iex(n2@drakarys)30>
08:05:44.043 [notice] Application mnesia exited: :stopped
:ok
iex(n2@drakarys)31>

4.2 List storage

  ls -l Mnesia.n2@drakarys
total 20
-rw-r--r-- 1 icostan users  145 Nov 22 08:08 DECISION_TAB.LOG
-rw-r--r-- 1 icostan users    8 Nov 22 08:05 Elixir.Blog.Author.DCD
-rw-r--r-- 1 icostan users   87 Nov 22 08:08 LATEST.LOG
-rw-r--r-- 1 icostan users 6775 Nov 22 08:05 schema.DAT

This is it for this post, in later installment I will talk about performance and how everything fits together in a cloud-agnostic cluster environment distributed across multiple continents.

Happy distributed storage with Erlang and Mnesia!