Applications and Configuration
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:
tally, an application that counts hits per page name. Its supervision tree holds aRegistry, aDynamicSupervisorwith one counter per page, and aGenServerfor the grand total.- How Mix starts it, and its dependencies first.
- The application environment: configuration read when the app boots, and
Application.compile_env/3for values fixed when it's compiled. - 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.
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).
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, []}.
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.
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
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.
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
[
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:inapplication/0the application still loads and "starts", but nothing callsstart/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/3in 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, asstart/2does.