A Lib Abandoned
I have a web app built on Phoenix/Elixir that up until today used Coherence as its user authentication lib. Unfortunately the maintainer of this lib has straight ghosted, and so my options were fork Coherence and maintain it myself, build my own user auth system, or search out another lib. I did what I believed to be the smart and lazy choice and searched for something new. What I found was Pow.
New Lib on the Block
Pow is pretty dope. It has lots of great functionality out of the box, has controller callbacks with is a feature I felt Coherence was lacking, and is actively maintained. The documentation is lacking, but at the time of this writing, the project is very young, so I’m sure it will be added in due time.
One of Pow’s coolest upgrades over Coherence is that it has a built-in module for persistent sessions that uses Mnesia. I wasn’t familiar with Mnesia before this, and from my cursory reading, it seems pretty much like SQLite but built into Erlang (and therefore Elixir) so that’s pretty cool. I was definitely excited to try implementing this. However, it was not without its stumbling blocks. Here’s how I got it done.
The First Hurdle
The Pow readme shows you what your Application module should look like if you want to use Mnesia across the board (in dev, test, prod, etc), but I only wanted to use it in production, so here’s one way to accomplish that:
# lib/my_app/application.ex defmodule MyApp.Application do use Application def start(_type, _args) do import Supervisor.Spec pow_worker = @pow_session_cache_worker |> Enum.map(&worker(&1, [[nodes: [node()]]])) children = [ supervisor(MyApp.Repo, []), supervisor(MyAppWeb.Endpoint, []), ] ++ workers() opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end def config_change(changed, _new, removed) do MyAppWeb.Endpoint.config_change(changed, removed) :ok end defp workers() do import Supervisor.Spec case Application.get_env(:my_app, :pow)[:cache_store_backend] do Pow.Store.Backend.MnesiaCache -> [worker(Pow.Store.Backend.MnesiaCache, [[nodes: [node()]]])] _ -> [] end end end # config/prod.exs config :my_app, :pow, cache_store_backend: Pow.Store.Backend.MnesiaCache
I added the :cache_store_backend config value as per the Pow docs to my prod config, and the workers() method will check this value and return the proper worker in prod and an empty list in every other env.
I also added this config value in dev first just to double check and everything was fine so time to deploy. I use Distillery and Edeliver, so I built a new release, deployed it to production, and restarted the production app, but there was a problem:
Application my_app exited: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: Pow.Store.Backend.MnesiaCache ** (EXIT) an exception was raised: ** (UndefinedFunctionError) function :mnesia.create_schema/1 is undefined (module :mnesia is not available) :mnesia.create_schema([:"my_app@127.0.0.1"]) (pow) lib/pow/store/backend/mnesia_cache.ex:172: Pow.Store.Backend.MnesiaCache.table_init/1 (pow) lib/pow/store/backend/mnesia_cache.ex:66: Pow.Store.Backend.MnesiaCache.init/1 (stdlib) gen_server.erl:374: :gen_server.init_it/2 (stdlib) gen_server.erl:342: :gen_server.init_it/6 (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
The Second Hurdle
A quick Google led me to this GitHub page that said “This is because distillery doesn’t export mnesia by default. You need to tell distillery to export :mnesia by adding it to the extra_applications option in your mix application.”
I tried that, and this is what I got:
Application my_app exited: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: Pow.Store.Backend.MnesiaCache ** (EXIT) an exception was raised: ** (CaseClauseError) no case clause matching: {:aborted, {:bad_type, Pow.Store.Backend.MnesiaCache, :disc_copies, :"my_app@127.0.0.1"}} (pow) lib/pow/store/backend/mnesia_cache.ex:179: Pow.Store.Backend.MnesiaCache.table_init/1 (pow) lib/pow/store/backend/mnesia_cache.ex:66: Pow.Store.Backend.MnesiaCache.init/1 (stdlib) gen_server.erl:374: :gen_server.init_it/2 (stdlib) gen_server.erl:342: :gen_server.init_it/6 (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Ok, this seems like… idk progress? The GitHub page was definitely right about Distillery not exposing :mnesia, but as we’ll see in a bit, their fix is not quite what I needed. Anyway, googling this new error message was not terribly productive.
A Hurdlish Stumble
One thing I did find is that remote consoling into my production app and running :mnesia.system_info() yielded some information – most interesting to me was that the mnesia directory was something like /home/my_app/app_releases/Mnesia.my_app@127.0.0.1. This is not really an intuitive place to keep my disk-written copy of the data, so another quick search revealed I should add this line to my config/prod.exs file:
config :mnesia, dir: '/home/my_app/app_storage/Mnesia'
I also had to mkdir the app_storage and Mnesia dirs on my production server. One really important thing to note is that YOU HAVE TO USE SINGLE QUOTES. Why does it want a charlist instead of a string? If there are any Erlangelicals (™ pending) out there, I’d love to know the answer. I’ll post the error I was getting when using a double-quoted directory string in config for any lost googlers out there:
Application my_app exited: MyApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: Pow.Store.Backend.MnesiaCache ** (EXIT) an exception was raised: ** (CaseClauseError) no case clause matching: {:error, {:EXIT, :function_clause}} (pow) lib/pow/store/backend/mnesia_cache.ex:172: Pow.Store.Backend.MnesiaCache.table_init/1 (pow) lib/pow/store/backend/mnesia_cache.ex:66: Pow.Store.Backend.MnesiaCache.init/1 (stdlib) gen_server.erl:374: :gen_server.init_it/2 (stdlib) gen_server.erl:342: :gen_server.init_it/6 (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Still, this did not solve the 2nd error I got when I tried to start up the production app. Frustrated but unabated, I thought I’d try it out in dev again and see if I could piece together why one was working and the other was not.
The Finish Line
Lucky for me, I got the same error when I tried to start up the dev server. This meant it wasn’t some Distillery or production issue. After some experimentation, I discovered that having :mnesia in :extra_applications was the problem, because it was starting up Mnesia in memory, then when Pow tried to start it up with disk_copies enabled, it failed. But, I still needed Distillery to expose :mnesia to the rest of my app, so what to do?
The answer was with :included_applications. From the docs:
Any included application, defined in the :included_applications key of the .app file will also be loaded, but they won’t be started.
So I changed my application function in mix.exs to look like this:
def application do [ mod: {MyApp.Application, []}, included_applications: [:mnesia] ] end
And boom! It worked like a difficult-to-debug charm.