Elixir by Example

Applications and Configuration

Every command ran when the site was built; the output is real Download project

On the earlier pages a script started its supervisors and processes by hand, line by line. A real system wants that done for it: when it boots, its supervision tree should start, after everything it depends on, and when it shuts down, the tree should stop in order. OTP's unit for this is the application: a component with a name, a version, a list of applications it needs, configuration, and optionally a supervision tree that starts and stops with it. Elixir itself, Logger and every Hex package are applications, and so is every Mix project (Mix Projects built one with no processes).

On this page:

  1. tally, an application that counts hits per page name. Its supervision tree holds a Registry, a DynamicSupervisor with one counter per page, and a GenServer for the grand total.
  2. How Mix starts it, and its dependencies first.
  3. The application environment: configuration read when the app boots, and Application.compile_env/3 for values fixed when it's compiled.
  4. Child specifications: a bare module name, and a map for modules without child_spec/1.

The project

application/0 in mix.exs describes the application. mod: names the callback module that starts it, and the list after it is passed to that module's start/2. extra_applications lists applications that ship with Elixir or Erlang and must be started first; applications from deps are added automatically.

mix.exs
defmodule Tally.MixProject do
  use Mix.Project

  def project do
    [app: :tally, version: "0.1.0", elixir: "~> 1.20", deps: []]
  end

  def application do
    [
      mod: {Tally.Application, []},
      extra_applications: [:logger]
    ]
  end
end

Both settings go into the application environment (see Mix Projects for config files).

config/config.exs
import Config

config :tally,
  max_counters: 2,
  max_name_length: 10

use Application makes this a callback module. start/2 must start the application's top supervisor and return {:ok, pid}; when that supervisor exits, the application has stopped. The first argument is the start type (:normal except in distributed takeovers) and the second the list from mod:. An optional stop/1 runs after the tree has shut down, for cleanup outside it.

start/2 runs when the app boots, so it's the place to read the configuration it needs: here the most counters the DynamicSupervisor may hold (max_children:). Tally.Total stands alone in the list: a bare module name means {Tally.Total, []}.

lib/tally/application.ex
defmodule Tally.Application do
  use Application

  @impl true
  def start(_type, _args) do
    max = Application.fetch_env!(:tally, :max_counters)

    children = [
      {Registry, keys: :unique, name: Tally.Names},
      {DynamicSupervisor, name: Tally.Counters, max_children: max},
      Tally.Total
    ]

    Supervisor.start_link(children, strategy: :one_for_one, name: Tally.Supervisor)
  end
end

One counter per page name, registered in Tally.Names under that name.

lib/tally/counter.ex
defmodule Tally.Counter do
  use GenServer

  def start_link(name) do
    GenServer.start_link(__MODULE__, 0, name: {:via, Registry, {Tally.Names, name}})
  end

  def hit(pid), do: GenServer.call(pid, :hit)

  @impl true
  def init(count), do: {:ok, count}

  @impl true
  def handle_call(:hit, _from, count), do: {:reply, count + 1, count + 1}
end
lib/tally/total.ex
defmodule Tally.Total do
  use GenServer

  def start_link(_opts), do: GenServer.start_link(__MODULE__, 0, name: __MODULE__)

  def add, do: GenServer.cast(__MODULE__, :add)
  def get, do: GenServer.call(__MODULE__, :get)

  @impl true
  def init(total), do: {:ok, total}

  @impl true
  def handle_cast(:add, total), do: {:noreply, total + 1}

  @impl true
  def handle_call(:get, _from, total), do: {:reply, total, total}
end

Tally.hit/1 finds the page's counter, starting it under the DynamicSupervisor the first time. The guard needs the longest allowed name as a constant, so @max_name_length reads it with Application.compile_env/3: once, while the module is compiled.

lib/tally.ex
defmodule Tally do
  @moduledoc "Counts hits per page name."

  # Read once, when this module is compiled: a guard needs a constant.
  @max_name_length Application.compile_env(:tally, :max_name_length)

  def hit(name) when is_binary(name) and byte_size(name) <= @max_name_length do
    with {:ok, pid} <- counter(name) do
      Tally.Total.add()
      {:ok, Tally.Counter.hit(pid)}
    end
  end

  def hit(name) when is_binary(name), do: {:error, :name_too_long}

  defp counter(name) do
    case Registry.lookup(Tally.Names, name) do
      [{pid, _}] -> {:ok, pid}
      [] -> start_counter(name)
    end
  end

  defp start_counter(name) do
    case DynamicSupervisor.start_child(Tally.Counters, {Tally.Counter, name}) do
      {:ok, pid} -> {:ok, pid}
      {:error, {:already_started, pid}} -> {:ok, pid}
      {:error, :max_children} -> {:error, :too_many_counters}
    end
  end
end
.formatter.exs
[
  inputs: ["{mix,.formatter}.exs", "{config,lib}/**/*.{ex,exs}"]
]

The application file

mix compile writes tally.app next to the compiled modules. It's the application's description in Erlang terms, generated from mix.exs, and it's what OTP reads to load and start the app. :file.consult/1 reads it back as Elixir data:

$ mix compile
$ mix run -e '{:ok, [app]} = :file.consult("_build/dev/lib/tally/ebin/tally.app"); IO.inspect(app)'
Compiling 4 files (.ex)
Generated tally app
{:application, :tally,
 [
   modules: [Tally, Tally.Application, Tally.Counter, Tally.Total],
   compile_env: [{:tally, [:max_name_length], {:ok, 10}}],
   optional_applications: [],
   applications: [:kernel, :stdlib, :elixir, :logger],
   description: ~c"tally",
   registered: [],
   vsn: ~c"0.1.0",
   mod: {Tally.Application, []}
 ]}

applications is what must be running first: Erlang's kernel and stdlib are always there, then elixir, then logger from extra_applications. mod is the callback module. compile_env records every value the code read at compile time, so it can be checked later (below).

Starting it

mix run (and iex -S mix) compiles the project, then starts its application and, before it, every application it needs, in order. Application.spec/2 reads a key of the description; Application.started_applications/0 lists what's running, most recently started first. mix and compiler are there because Mix itself is running.

$ mix run -e 'IO.inspect(Application.spec(:tally, :applications))'
$ mix run -e 'IO.inspect(for {app, _description, _vsn} <- Application.started_applications(), do: app)'
[:kernel, :stdlib, :elixir, :logger]
[:tally, :logger, :mix, :elixir, :compiler, :stdlib, :kernel]

The supervision tree is up before the expression runs. which_children/1 lists the top supervisor's children, most recently started first; the pids are left out because they differ on every run. A Registry is itself a small supervisor, hence its type.

$ mix run -e 'for {id, _pid, type, modules} <- Supervisor.which_children(Tally.Supervisor), do: IO.inspect({id, type, modules})'
{Tally.Total, :worker, [Tally.Total]}
{Tally.Counters, :supervisor, [DynamicSupervisor]}
{Tally.Names, :supervisor, [Registry]}

Every counter is a child of Tally.Counters. With max_counters: 2 a third page is refused, and the guard rejects names longer than 10 bytes. (Each mix run boots a fresh system, so nothing carries over between commands. A list of two-element tuples that start with atoms prints as a keyword list.)

$ mix run -e 'IO.inspect(for page <- ["home", "home", "about", "blog"], do: Tally.hit(page)); IO.inspect(Tally.Total.get(), label: "total")'
$ mix run -e 'IO.inspect(Tally.hit("a-very-long-name"))'
[ok: 1, ok: 2, ok: 1, error: :too_many_counters]
total: 3
{:error, :name_too_long}

mix run --no-start loads the code and the configuration but doesn't start the project's application, which is useful for one-off tasks that must not boot it. Application.ensure_all_started/1 then starts an application and everything it needs, and returns the ones it had to start: only tally, since Mix had already started logger.

$ mix run --no-start -e 'IO.inspect(Process.whereis(Tally.Supervisor))'
$ mix run --no-start -e 'IO.inspect(Application.ensure_all_started(:tally)); IO.inspect(Tally.hit("home"))'
nil
{:ok, [:tally]}
{:ok, 1}

The application environment

Each application has its own environment, a keyword store filled from its .app file and then from the config files. Application.fetch_env!/2 raises if a key is missing; Application.get_env/3 returns a default instead.

$ mix run -e 'IO.inspect(Application.fetch_env!(:tally, :max_counters))'
$ mix run -e 'IO.inspect(Application.get_env(:tally, :missing, :default))'
2
:default

Application.put_env/3 changes a value while the system runs. Only code that reads the value afterwards sees the change: start/2 read max_counters when the app booted. So before booting, with --no-start, the new limit takes effect:

$ mix run --no-start -e 'Application.put_env(:tally, :max_counters, 3); Application.ensure_all_started(:tally); IO.inspect(for page <- ["a", "b", "c", "d"], do: Tally.hit(page))'
[ok: 1, ok: 1, ok: 1, error: :too_many_counters]

A value read with Application.compile_env/3 is part of the compiled code, so changing it at runtime changes nothing:

$ mix run -e 'Application.put_env(:tally, :max_name_length, 50); IO.inspect(Tally.hit("a-very-long-name"))'
{:error, :name_too_long}

That's why the .app file records it. When a config file changes, Mix recompiles the project, so the new value gets compiled in. A release can't recompile, so it checks the recorded values when it boots and refuses to start if one changed. Runtime Configuration shows that, and config/runtime.exs, which is read on the server each time the system starts.

Use Application.fetch_env!/2 inside functions for almost everything, and compile_env only where the compiler needs the value, as in a guard or a module attribute.

Child specifications

A supervisor needs a child specification for each child: a map that says at least how to start it (start: {module, function, args}) and under which id. {Registry, keys: :unique, name: Tally.Names} asks the module for its spec by calling Registry.child_spec(keys: :unique, name: Tally.Names), and use GenServer defines a child_spec/1 for Tally.Total. A bare module name passes [], so both of these are the same spec:

$ mix run -e 'IO.inspect(Tally.Total.child_spec([]))'
$ mix run -e 'IO.inspect(Supervisor.child_spec(Tally.Total, []))'
%{id: Tally.Total, start: {Tally.Total, :start_link, [[]]}}
%{id: Tally.Total, start: {Tally.Total, :start_link, [[]]}}

Erlang modules usually have no child_spec/1, so they go in the children list as a map. Erlang's process groups, :pg, are started like this in later chapters:

children = [
  %{id: :pg, start: {:pg, :start_link, []}}
]

Pitfall: listing such a module bare, as children = [:pg], fails when the supervisor starts: "The module :pg was given as a child to a supervisor but it does not implement child_spec/1". Write the map instead.

Pitfall: without mod: in application/0 the application still loads and "starts", but nothing calls start/2, so none of its processes run. That's right for a library like Decimal, and a puzzling bug when you forgot it.

Pitfall: the application environment is global to the whole system. Application.put_env/3 in one test changes it for every other test running at the same time. Pass settings as arguments where you can, and read the environment at the edges, as start/2 does.