Runtime vs. compile time
Elixir is a compiled language, but when you work with it - it behaves like interpreted language. The whole development workflow of typical Elixir project is very similar to the workflow employed in Ruby projects. You write some code and you run it.
The key difference between Ruby or Elixir is, however, that Elixir does
compile the files and persist compiled modules (in *.beam
) files on
disk. Check out your _build
folder to find them.
When you, as a programmer, write Elixir code, you rarely have to
explicitly run mix compile
. Adding or modifying module will trigger
recompilation behind the scenes, when you - for example - start the
project with mix phx.server
.
Moreover, if you work with Phoenix framework it will re-compile your project between web requests if you change the code, and even push the changes to both - server-side templates and JavaScript code via a WebSocket protocol to client browser - triggering instant update.
It feels very interactive, it feels like interpreted - it feels like Ruby. Or better.
But the compilation phase is still there, and different settings affect different phases. Let’s have a closer look on what is what in typical Elixir & Phoenix project.
Our example app
Let’s generate an example Phoenix 1.3 app for the reference:
$ mix phx.new compile_and_runtime
* creating compile_and_runtime/config/config.exs
* creating compile_and_runtime/config/dev.exs
* creating compile_and_runtime/config/prod.exs
...
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
$
The first thing that Phoenix generator does is in fact creating Mix configuration files.
If you open config/config.exs
you should see something like this:
use Mix.Config
config :compile_and_runtime,
ecto_repos: [CompileAndRuntime.Repo]
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "hhtXFfzljiIyKk9fo9/g1R8dJGb/7ZIxEdSfILaxuCB8mwT1g74BWdXru2AAnk8e",
render_errors: [view: CompileAndRuntimeWeb.ErrorView, accepts: ~w(html json)],
pubsub: [name: CompileAndRuntime.PubSub,
adapter: Phoenix.PubSub.PG2]
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:user_id]
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env}.exs"
and there will be additional important configuration in your env-specific files imported by the last line. For example, they would configure Ecto / database connection like:
use Mix.Config
...
# Configure your database
config :compile_and_runtime, CompileAndRuntime.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "compile_and_runtime_dev",
hostname: "localhost",
pool_size: 10
Configuration in dev & test mode
When you work on the application on your local computer, you are usually
using two Mix environments: :dev
and :test
. The default one is
:dev
, so your config/config.exs
will load config/dev.exs
.
Does this happen at runtime or compile time? Does it matter?
The answer to the first question is: “compile time”. The answer to second is: “probably does not matter”.
During local development, Elixir will load your config/config.exs
file, whenever you run a mix task. So, speaking roughly, when you run
mix phx.server
, the following events will happen:
Phase 1: Initialization
Elixir supports writing Elixir in your configuration files, it also
supports executing Elixir code during compilation but it is whole
different story to discuss at some other point in time. So the
config/config.exs
and then config/dev.exs
are loaded & executed, and
the result is that appropriate Application
environment
keys will be set.
In this phase you can safely use primitive Elixir types, such as numbers, strings, integers etc. in your configuration files. But you can also write snippets of Elixir.
The most common use case for the later is to provide dynamic configuration, often loaded from shell environment variables.
Let’s say you have this snippet that configures Phoenix Endpoint in
config/dev.exs
:
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
http: [port: 4000],
...
You can make it depend on environment variable in two ways: either use piece of custom Elixir code that will be executed:
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
http: [port: System.get_env("PORT")],
...
or depend on custom functionality provided by library itself to support dynamic configuration using environment variables. Historically, many Elixir libraries supported this with:
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
http: [port: {:system, "PORT"}],
...
You should not use the above, as not all libraries support this scheme, and it might stop being supported any time in future as well, since there are now better way of doing this. In fact, Phoenix developers already have deprecation of this feature on their to-do list.
For dev and test environments, this way of providing configuration
using System.get_env
is usually fine. The only gotcha you need to
watch out for is that the values in shell environment are always
strings. We are relying above again on Phoenix being smart enough to
convert strings to integer for us, but we most likely want to write it
explicitly anyway:
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
http: [port: System.get_env("PORT") |> String.to_integer()],
...
When this phase finishes, the Application environment is already filled
in fully, and the libraries can use
Application.get_env/3
to read these values.
Phase 2: Compilation
In this phase, Mix project is being compiled behind the scenes for us. Elixir code we write, or library authors wrote for us, can - and often does - rely on the result of Phase 1. Since we can execute Elixir during compilation of Elixir code, this feature is often used in conjunction with reading Application environment and making some decisions about how and which code to compile.
A good example is Elixir’s Logger, which can be configured to purge all calls with certain level from code on compilation, so they get never executed.
config :logger,
backends: [:console],
compile_time_purge_matching: [
[level_lower_than: :info]
]
Read more about it in Logger documentation.
More examples can be found libraries that you generally use, one popular example is Ecto, that relies on environment to choose JSON library to use when generating some code common among adapters with bits of meta-programming.
Another example of compile-time configuration, that results in different code being compiled can be found in Poison library:
if Application.get_env(:poison, :native) do
@compile [:native, {:hipe, [:o3]}]
end
The snippet above executes at compile-time, and sets @compile
module
variable that later is used by Erlang compiler
directly.
Phase 3: Application start up
After our project’s environment has been populated in Phase 1, and it
has been compiled in Phase 2, Mix would execute the compiled code. Here
is where the most calls to Application.get_env/3
happen. Phoenix would
use it to decide if it should start endpoint or
not,
Ecto to load it’s
configuration
etc.
It is often that application environment is only used once, when the supervision tree starts to make decisions on initial configuration.
Phase 4: Execution
It is possible, however, that applications rely on environment configuration during the actual execution, post their start-up phases.
You can use Application.get_env
within the functions you write. If you
do so, the code will be executed at runtime, Application environment
fetched and read at runtime and you can do update the configuration at
runtime as well. This comes with slight penalty in terms of performance,
but it is used by some libraries out there, for example
Crawler
uses it to provide default option values.
If you modify application environment during execution with
Application.put_env/3
, the changes will be visible immediately during
next call to the function reading from environment.
Phoenix-specific dev mode note
If you are developing application using Phoenix, it does one more nice thing for you: detects configuration changes in dev mode. If you modify your config files, on the next request you will be presented with an error, saying that you need to re-start the application in order to changes you made to take effect. It’s a neat feature that does not exist if you are writing applications not using Phoenix, so something to watch out for.
Production: Here Be Dragons
For these coming from Ruby, Python or PHP backgrounds, the compiled nature of Elixir starts to show in full form only when they start to do production deployments.
Contrary to interpreted languages, you generally don’t upload the source files to the server. You do not have to have Elixir and Erlang installed on the server either.
Elixir builds upon the concept of Erlang releases, which bundle together compiled application, assets and configuration into single executable, which can be uploaded and executed on the server.
Programmers who were being put off by that procedure, and still can
deploy Phoenix applications can use Heroku with custom
buildpack
and run in production in similar way that you run in dev mode. This
means, their application configuration will be loaded, executed before
compilation, and they can safely use System.get_env/1
and similar
techniques to populate the Application environment on start up.
The releases can be created with several different tools, but distillery has been steadily gaining popularity. It is our default tool to create releases in AmberBit for a while now, and we were delighted by the changes v2.0 brought us.
Traditionally, the problem with releases was that the configuration has been loaded before compilation. And compilation happens somewhere on the build server (or dev’s local machine), but the release itself is being executed on different server.
Since the release has configuration already evaluated and bundled in, one option was to set all of the configuration options on build server. When you upload release to server and run it - all configuration options are then already available in Application environment.
This is not flexible enough, however, in many many cases. You often want to change some configuration option without the need to re-compile and re-upload the whole project to server. This takes time and is tedious task, and surely there are better way.
Distillery prior to version 2.0, provided the mechanism of setting
compile-time environment variable (sic!) called REPLACE_OS_VARS
to
true, and then you had to fiddle around with rel/vm.args
file and
rel/sys.config
file and use special environment variables
interpolation syntax in your config/*
files to provide runtime
configuration behavior, where application would read variables from
shell environment.
The syntax in config files looked like:
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
http: [port: "${PORT}"],
...
Good? Not quite.
While the above did work for the most part, the REPLACE_OS_VARS
mechanism did only work with strings, and disallowed any arbitrary
Elixir code to be executed at runtime. Libraries that accepted strings
and converted to integers on their own worked fine, but others did not
support it. The following code would not work and crash with error
during compilation:
config :compile_and_runtime, CompileAndRuntimeWeb.Endpoint,
http: [port: "${PORT}" |> String.to_integer()],
...
That’s not great, is it?
To work around this issue, many libraries started to provide runtime execution hooks. For example, you could explicitly initialize Phoenix endpoints by adding a hook to endpoint itself.
Problem solved? Not quite. This again has been supported by only some of
the libraries, not all. Moreover, it means you have to write a piece of
code specifically for a particular environment, and initialization
procedures for :dev
and :prod
would differ. I personally don’t like
solutions like that as I find them often causing bugs that are spotted
only on production.
Is there a chance for us to have consistent runtime configuration
between :dev
and :prod
or is it a Holly Grail we will never find?
Distillery 2.0
Luckily, Distillery 2.0 solved the problem in rather elegant way.
In order to provide a dynamic, runtime configuration, you no longer
have to mess around with REPLACE_OS_VARS
or Erlang configuration
files.
Distillery’s own runtime configuration mechanism allows you to populate parts of Application environment, during release start-up, before any of specified OTP applications are started.
There are several different adapters, which allow you to store this dynamic configuration in different file formats (such as Toml, YAML or JSON). The most convenient one for me in general is - however, Elixir config provider.
Using this provider is straightforward. One has to modify only the
rel/config.exs
file, to point it to *.exs
file containing runtime
configuration. This file can be included in the release itself, but it
does not have to. You can place your runtime_config.exs
file
somewhere on the server, and point to it in your release file using
absolute path:
set config_providers: [
{Mix.Releases.Config.Providers.Elixir, ["/etc/app_runtime_config.exs"]}
]
When you upload a release compiled with the above to the server and
execute it, runtime config will be read from
/etc/app_runtime_config.exs
file, and Application environment
populated accordingly before application start up.
In such scenario you might not care about storing or reading environment variables from shell at all, and write values directly to your configuration file.
More common (I think) will be, however, use where you keep your release
runtime configuration in repository, and upload it to server along with
release itself. In such case crate the runtime configuration in
rel/runtime_configuration.exs
, and reference from release build
config:
release :compile_and_runtime do
...
set config_providers: [
{Mix.Releases.Config.Providers.Elixir, ["${RELEASE_ROOT_DIR}/etc/runtime_config.exs"]}
]
set overlays: [
{:copy, "rel/runtime_config.exs", "etc/runtime_config.exs"}
]
set applications: [
:runtime_tools,
compile_and_runtime: :permanent
]
end
This ensures that the rel/runtime_config.exs
file committed to
repository and present during compilation of release will be copied over
to etc/runtime_config.exs
relative to where the release is going to be
unpacked on the server. Then it loads the file before applications start
up.
The runtime_config.exs
file syntax itself? It mimics any other
Mix.Config
file such as config/config.exs
or config/dev.exs
.
In order to provide dynamic configuration for your :prod
environment,
move some lines from config/prod.exs
to this new
rel/runtime_config.exs
file and adjust accordingly. The result could
be something like:
use Mix.Config
...
config :compile_and_runtime, CompileAndRuntime.Endpoint,
http: [port: System.get_env("PORT") |> String.to_integer()],
url: [host: System.get_env("DOMAIN"), scheme: "https"],
secret_key_base: System.get_env("SECRET_KEY_BASE")
This way you clearly separate compilation time configuration (and leave
it in config/prod.exs
) from runtime configuration (and move it to
rel/runtime_config.exs
.
It is the Holly Grail, and you should update to Distillery 2.0 now :D.
Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.