Search code examples
configurationelixirruntimegen-server

Correct way to pass runtime configuration to elixir processes


I'm trying to deploy an app to production and getting a little confused by environment and application variables and what is happening at compile time vs runtime.

In my app, I have a genserver process that requires a token to operate. So I use config/releases.exs to set the token variable at runtime:

# config/releases.exs

import Config

config :my_app, :my_token, System.fetch_env!("MY_TOKEN")

Then I have a bit of code that looks a bit like this:

defmodule MyApp.SomeService do

  use SomeBehaviour, token: Application.get_env(:my_app, :my_token),
                     other_config: :stuff

  ...

end

In production the genserver process (which does some http stuff) gives me 403 errors suggesting the token isn't there. So can I clarify, is the use keyword getting evaluated at compile time (in which case the application environment doest exist yet)?

If so, what is the correct way of getting runtime environment variables in to a service like this. Is it more correct to define the config in application.ex when starting the process? eg

children = [
  {MyApp.SomeService, [
    token: Application.get_env(:my_app, :my_token),
    other_config: :stuff
  ]}
  ...
]

Supervisor.start_link(children, opts)

I may have answered my own questions here, but would be helpful to get someone who knows what they're doing confirm and point me in the right way. Thanks


Solution

  • has two stages: compilation and runtime, both written in Elixir itself. To clearly understand what happens when one should figure out, that everything is macro and Elixir, during compilation stage, expands these macros until everything is expanded. That AST comes to runtime.

    In your example, use SomeBehaviour, foo: :bar is implicitly calling SomeBehaviour.__using__/1 macro. To expand the AST, it requires the argument (keyword list) to be expanded as well. Hence, Application.get_env(:my_app, :my_token) call happens in compile time.

    There are many possibilities to move it to runtime. If you are the owner of SomeBehaviour, make it accept the pair {:my_app, :my_token} and call Application.get_env/2 somewhere from inside it.

    Or, as you suggested, pass it as a parameter to children; this code belongs to function body, meaning it won’t be attempted to expand during compilation stage, but would rather be passed as AST to the resulting BEAM to be executed in runtime.