Can't configure ssl to load custom session cache module

I’m attempting to use a custom session cache module for the ssl application. I’ve got the following in config/config.exs:

config :ssl,
  session_cb: TlsServer.SessionCache

The module itself looks like this:

defmodule TlsServer.SessionCache do
  @behaviour :ssl_session_cache_api

  defdelegate init(options), to: :ssl_session_cache
  defdelegate terminate(ref), to: :ssl_session_cache
  defdelegate lookup(ref, key), to: :ssl_session_cache
  defdelegate update(ref, key, session), to: :ssl_session_cache
  defdelegate delete(ref, key), to: :ssl_session_cache
  defdelegate foldl(fun, acc, ref), to: :ssl_session_cache
  defdelegate select_session(ref, port), to: :ssl_session_cache
  defdelegate size(ref), to: :ssl_session_cache

…yes, it just delegates everything at the moment.

But when I run it with PORT=15350 iex -S mix run --no-halt, I get the following error:

17:19:01.821 [info]  Application ssl exited: :ssl_app.start(:normal, []) returned an error: shutdown: failed to start child: :ssl_admin_sup
    ** (EXIT) shutdown: failed to start child: :ssl_manager
        ** (EXIT) an exception was raised:
            ** (UndefinedFunctionError) function TlsServer.SessionCache.init/1 is undefined (module TlsServer.SessionCache is not available)
                TlsServer.SessionCache.init([role: :client])
                (ssl 9.5.3) ssl_manager.erl:237: :ssl_manager.init/1
                (stdlib 3.11.2) gen_server.erl:374: :gen_server.init_it/2
                (stdlib 3.11.2) gen_server.erl:342: :gen_server.init_it/6
                (stdlib 3.11.2) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

Is there something I need to do differently to configure an Elixir module as a callback to an Erlang module? Or is there some problem with my module not being available at the correct time, somehow?

Do I have a problem that my application depends on ranch, which depends on ssl, which depends on a module from my application? That wouldn’t matter in Erlang, but does Elixir lazily-load my module or something?

If I run my application with the following:

PORT=15350 iex -pa _build/dev/lib/tls_server/ebin/ \
    -pa _build/dev/lib/ranch/ebin/ \
    --erl "-ssl session_cb 'Elixir.TlsServer.SessionCache'"

…and then use Application.ensure_all_started(:tls_server), then it all works.

But that’s awkward.

Update: It looks like it’s the mix part that’s breaking. I can’t even run mix release with that configuration item present. Which implies that mix is doing something lazy w.r.t. updating the code path, and that it’s also starting ssl (too early).

If I move the offending configuration to config/releases.exs and then use mix release, etc., then it works correctly.

This is slower than just using iex -S mix run. Can I get it to load extra config, but only immediately before starting the application?

Yes, your module works just fine as long as it is in the load path when :ssl starts. But outside of a release (including at compile time and release build time) it may be a catch 22.

If you don’t mind a hack, you can add…

  Application.put_env(:ssl, :session_cb, TlsServer.SessionCache)

…to the top of your Application.start/2

I got it to work in two different ways:

  1. Using mix release and putting the setting in config/releases.ex, where it’s only used by the release.
  2. Not using mix run to run the app. Instead, I used iex --app thusly:
ERL_LIBS=_build/dev/lib PORT=15350 iex --erl-config sys.config --app tls_server

…where sys.config looks like this:

    [{session_cb, 'Elixir.TlsServer.SessionCache'}]

Note that this doesn’t load config/config.exs, so you’ll need to merge that into sys.config. I did it with a custom compiler.

The root of the issue is that, in any project that has any Hex dependencies, Mix starts Hex. This happens even when passing --no-compile --no-deps-check. Hex requires :ssl, which therefore gets started before the load paths have been set up for the application.

1 Like

Thanks for the clarification. I suspected it was something like that.