OTP — GenServer, Supervisor, and Agent
Elixir OTP behaviours — GenServer, Supervisor, Agent, and Task for building reliable concurrent systems
OTP (Open Telecom Platform) is Erlang’s framework for building fault-tolerant systems. Elixir provides GenServer, Supervisor, Agent, and Task as the primary building blocks.
GenServer
A GenServer is a process that holds state and handles synchronous (call) and asynchronous (cast) requests.
Defining a GenServer
defmodule Counter do
use GenServer
# --- Client API ---
def start_link(initial \\ 0) do
GenServer.start_link(__MODULE__, initial, name: __MODULE__)
end
def inc(pid \\ __MODULE__) do
GenServer.cast(pid, :inc)
end
def dec(pid \\ __MODULE__) do
GenServer.cast(pid, :dec)
end
def get(pid \\ __MODULE__) do
GenServer.call(pid, :get)
end
# --- Server Callbacks ---
@impl true
def init(initial), do: {:ok, initial}
@impl true
def handle_call(:get, _from, count) do
{:reply, count, count}
end
@impl true
def handle_cast(:inc, count) do
{:noreply, count + 1}
end
@impl true
def handle_cast(:dec, count) do
{:noreply, count - 1}
end
endUsing it
Counter.start_link(0) # {:ok, pid}
Counter.inc() # :ok (async)
Counter.inc() # :ok (async)
Counter.get() # 2 (sync)handle_call return values
| Return | Meaning |
|---|---|
{:reply, data, new_state} |
Reply to caller, update state |
{:reply, data, new_state, timeout} |
Same + set inactivity timeout |
{:noreply, new_state} |
Don’t reply yet (use GenServer.reply/2 later) |
{:stop, reason, reply, new_state} |
Stop gracefully, send reply |
{:stop, reason, new_state} |
Stop gracefully, no reply |
handle_cast return values
| Return | Meaning |
|---|---|
{:noreply, new_state} |
Update state, continue |
{:noreply, new_state, timeout} |
Same + set inactivity timeout |
{:stop, reason, new_state} |
Stop gracefully |
Supervisor
Supervisors monitor child processes and restart them when they crash.
Static supervisor
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{Counter, 0},
{MyApp.Worker, []}
]
Supervisor.init(children, strategy: :one_for_one)
end
endDynamic supervisor
For processes that are started/stopped at runtime:
# In your application supervision tree:
children = [
{DynamicSupervisor, strategy: :one_for_one, name: MyApp.DynamicSupervisor}
]
# Starting a child dynamically
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {MyWorker, arg})
# Stopping a child
DynamicSupervisor.terminate_child(MyApp.DynamicSupervisor, pid)Restart strategies
| Strategy | Description |
|---|---|
:one_for_one |
Only the crashed process is restarted |
:one_for_all |
All children are terminated and restarted |
:rest_for_one |
Crashed process + all started after it are restarted |
Restart options
| Option | Meaning |
|---|---|
:permanent |
Always restart (default) |
:temporary |
Never restart |
:transient |
Restart only on abnormal exit |
Agent
Simpler than GenServer — just holds state with get/update functions.
# Start an agent
{:ok, pid} = Agent.start_link(fn -> 0 end)
# Or with a name
Agent.start_link(fn -> %{} end, name: :cache)
# Get state
Agent.get(:cache, & &1) # => %{}
Agent.get(:cache, fn state -> state[:key] end)
# Update state
Agent.update(:cache, fn state -> Map.put(state, :key, "value") end)
# Get and update
Agent.get_and_update(:cache, fn state ->
{state[:count], Map.update(state, :count, 1, &(&1 + 1))}
end)Task
Run a computation in a separate process. Good for one-off work.
# Fire and forget
Task.start(fn -> do_something_slow() end)
# Await result (blocks up to timeout)
task = Task.async(fn -> fetch_from_api() end)
result = Task.await(task, 5000) # 5s timeout
# Multiple tasks
tasks = Enum.map(urls, &Task.async(fn -> HTTP.get(&1) end))
results = Task.await_many(tasks, 10_000)
# Parallel stream
Task.async_stream(collection, &process/1, max_concurrency: 4)
|> Enum.to_list()Application supervision tree
In application.ex:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
MyApp.PubSub,
{DynamicSupervisor, strategy: :one_for_one, name: MyApp.DynamicSupervisor},
MyApp.Web.Endpoint
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end