--- name: otp-essentials description: Use when writing GenServer, Supervisor, Task, Agent, or Registry code — init/handle_continue, call vs cast, supervision strategies, process naming. file_patterns: - "**/application.ex" - "**/*_server.ex" - "**/*_supervisor.ex" - "**/workers/**/*.ex" auto_suggest: true --- # OTP Essentials ## RULES — Follow these with no exceptions 1. **Always use `@impl true`** before GenServer/Agent callbacks (init, handle_call, handle_cast, handle_info, terminate) 2. **Keep `init/1` fast** — no blocking calls, no DB queries; use `handle_continue` for expensive setup 3. **Use `GenServer.call` for request/response, `GenServer.cast` for fire-and-forget** — never cast when you need a result 4. **Always define a public API wrapping GenServer calls** — callers should never use `GenServer.call(pid, ...)` directly 5. **Use `Task.async`/`Task.await` with bounded timeouts** — never `Task.async` without a corresponding `Task.await` or `Task.yield` 6. **Name processes via Registry, not atoms** — atom table is finite and never garbage collected 7. **Supervisors own process lifecycle** — never start unsupervised long-running processes --- ## GenServer ### Public API Pattern Always wrap GenServer calls behind a public module API. Callers should not know they're talking to a GenServer. ```elixir # Bad — leaks GenServer implementation to callers GenServer.call(MyApp.Cache, {:get, key}) # Good — public API hides the GenServer defmodule MyApp.Cache do use GenServer # --- Public API --- def start_link(opts) do name = Keyword.get(opts, :name, __MODULE__) GenServer.start_link(__MODULE__, opts, name: name) end def get(key, server \\ __MODULE__) do GenServer.call(server, {:get, key}) end def put(key, value, server \\ __MODULE__) do GenServer.cast(server, {:put, key, value}) end # --- Callbacks --- @impl true def init(_opts) do {:ok, %{}} end @impl true def handle_call({:get, key}, _from, state) do {:reply, Map.get(state, key), state} end @impl true def handle_cast({:put, key, value}, state) do {:noreply, Map.put(state, key, value)} end end ``` ### Fast Init with handle_continue Never block in `init/1`. Use `handle_continue` for expensive setup. ```elixir # Bad — blocks the supervisor while loading data @impl true def init(opts) do data = MyApp.Repo.all(MyApp.Item) # Blocks! {:ok, %{items: data}} end # Good — returns immediately, loads data asynchronously @impl true def init(opts) do {:ok, %{items: []}, {:continue, :load_data}} end @impl true def handle_continue(:load_data, state) do data = MyApp.Repo.all(MyApp.Item) {:noreply, %{state | items: data}} end ``` ### call vs cast ```elixir # call — synchronous, caller waits for reply (use for reads, queries) def get_count(server \\ __MODULE__) do GenServer.call(server, :get_count) end @impl true def handle_call(:get_count, _from, state) do {:reply, state.count, state} end # cast — asynchronous, fire-and-forget (use for writes, side effects) def increment(server \\ __MODULE__) do GenServer.cast(server, :increment) end @impl true def handle_cast(:increment, state) do {:noreply, %{state | count: state.count + 1}} end ``` ### handle_info for External Messages Use `handle_info` for messages not sent via `call`/`cast` — timers, monitors, PubSub, etc. ```elixir @impl true def init(_opts) do Process.send_after(self(), :tick, 1_000) {:ok, %{count: 0}} end @impl true def handle_info(:tick, state) do Process.send_after(self(), :tick, 1_000) {:noreply, %{state | count: state.count + 1}} end ``` ### Error Handling in Callbacks Handle *expected* errors gracefully — reply with `{:error, reason}` and keep the state. Let *unexpected* errors crash so the supervisor restarts the process. ```elixir # Expected failure — reply with an error tuple, log it, keep serving @impl true def handle_call(:risky_operation, _from, state) do case perform_operation() do {:ok, result} -> {:reply, {:ok, result}, update_state(state, result)} {:error, reason} -> Logger.error("Operation failed: #{inspect(reason)}") {:reply, {:error, reason}, state} end end # Unexpected failure — let it crash @impl true def handle_cast(:dangerous_work, state) do # If this raises, the supervisor restarts the process with clean state result = dangerous_function!() {:noreply, Map.put(state, :result, result)} end ``` --- ## Supervisors ### Supervision Strategies ```elixir # one_for_one — restart only the failed child (most common) children = [ {MyApp.Cache, []}, {MyApp.Worker, []} ] Supervisor.start_link(children, strategy: :one_for_one) # one_for_all — restart ALL children when one fails # Use when children depend on each other's state Supervisor.start_link(children, strategy: :one_for_all) # rest_for_one — restart failed child and all children started AFTER it # Use when later children depend on earlier ones Supervisor.start_link(children, strategy: :rest_for_one) ``` ### Application Supervision Tree ```elixir defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.Repo, {Phoenix.PubSub, name: MyApp.PubSub}, MyApp.Cache, MyAppWeb.Endpoint ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end ``` ### DynamicSupervisor for Runtime Children Use when you need to start processes on demand, not at boot. ```elixir defmodule MyApp.RoomSupervisor do use DynamicSupervisor def start_link(init_arg) do DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__) end @impl true def init(_init_arg) do DynamicSupervisor.init(strategy: :one_for_one) end def start_room(room_id) do spec = {MyApp.Room, room_id: room_id} DynamicSupervisor.start_child(__MODULE__, spec) end def stop_room(pid) do DynamicSupervisor.terminate_child(__MODULE__, pid) end end ``` --- ## Tasks ### async/await for Concurrent Work ```elixir # Parallel fetch with bounded timeout task1 = Task.async(fn -> fetch_user_profile(user_id) end) task2 = Task.async(fn -> fetch_user_posts(user_id) end) profile = Task.await(task1, 5_000) posts = Task.await(task2, 5_000) ``` ### async_stream for Batch Processing ```elixir # Process items concurrently with bounded concurrency user_ids |> Task.async_stream(&fetch_user/1, max_concurrency: 4, timeout: 10_000) |> Enum.map(fn {:ok, result} -> result end) ``` ### Supervised Tasks (fire-and-forget) Start tasks under a `Task.Supervisor` for clean shutdown and observability. Note: `Task.Supervisor.start_child/2` defaults to `restart: :temporary` — crashed tasks are **not** restarted. Pass `restart: :transient` if you want restarts on abnormal exit. ```elixir # Add to your supervision tree {Task.Supervisor, name: MyApp.TaskSupervisor} # Start a supervised task — not restarted by default (restart: :temporary) Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> send_welcome_email(user) end) # Restart on crash — pass restart: :transient Task.Supervisor.start_child( MyApp.TaskSupervisor, fn -> send_welcome_email(user) end, restart: :transient ) ``` --- ## Agent Use Agent for simple state when GenServer is overkill. If you need `handle_info`, timeouts, or complex logic, use GenServer instead. ```elixir defmodule MyApp.Counter do use Agent def start_link(initial_value) do Agent.start_link(fn -> initial_value end, name: __MODULE__) end def value do Agent.get(__MODULE__, & &1) end def increment do Agent.update(__MODULE__, &(&1 + 1)) end end ``` --- ## Process Naming ### Registry (preferred) ```elixir # In application supervision tree {Registry, keys: :unique, name: MyApp.Registry} # In GenServer start_link def start_link(room_id) do GenServer.start_link(__MODULE__, room_id, name: {:via, Registry, {MyApp.Registry, {:room, room_id}}} ) end # Lookup def get_room(room_id) do case Registry.lookup(MyApp.Registry, {:room, room_id}) do [{pid, _}] -> {:ok, pid} [] -> {:error, :not_found} end end ``` ### Atoms (only for singletons) ```elixir # OK — single global process GenServer.start_link(__MODULE__, opts, name: __MODULE__) # Bad — dynamic atom creation from user input GenServer.start_link(__MODULE__, opts, name: String.to_atom("room_#{room_id}")) ``` --- ## Process Linking vs Monitoring ```elixir # Link — bidirectional, crash propagates (use in supervisors) Process.link(pid) # Monitor — unidirectional, receive :DOWN message (use for observation) ref = Process.monitor(pid) @impl true def handle_info({:DOWN, _ref, :process, pid, reason}, state) do # Handle monitored process dying {:noreply, cleanup(state, pid)} end ``` --- ## ETS for Shared Read-Heavy State When many processes need to read the same data and writes are infrequent: ```elixir # Create table in a GenServer (owner process) @impl true def init(_opts) do table = :ets.new(:my_cache, [:named_table, :set, :public, read_concurrency: true]) {:ok, %{table: table}} end # Any process can read :ets.lookup(:my_cache, key) # Only owner should write (or use :public carefully) :ets.insert(:my_cache, {key, value}) ``` --- ## Common Anti-Patterns ```elixir # Bad — bottleneck GenServer (all requests go through one process) def get_user(id), do: GenServer.call(UserServer, {:get, id}) # Fix: Use ETS, a database, or partition work across multiple processes # Bad — god process (one GenServer doing everything) # Fix: Split into focused processes, each with one responsibility # Bad — unmonitored Task.async Task.async(fn -> do_work() end) # no await or yield — caller loses track of work # Fix: Always await, or use Task.Supervisor.start_child for fire-and-forget # Bad — blocking the caller unnecessarily def send_email(user) do GenServer.call(EmailServer, {:send, user}) # Waits for email to send end # Fix: Use cast if caller doesn't need the result def send_email(user) do GenServer.cast(EmailServer, {:send, user}) end ``` --- ## Testing ```elixir # Start GenServer in test test "get and put values" do start_supervised!({MyApp.Cache, name: :test_cache}) assert MyApp.Cache.get(:key, :test_cache) == nil MyApp.Cache.put(:key, "value", :test_cache) assert MyApp.Cache.get(:key, :test_cache) == "value" end # Test with Task test "concurrent fetch" do task = Task.async(fn -> MyApp.fetch_data() end) assert {:ok, data} = Task.await(task, 5_000) end ``` See `testing-essentials` skill for comprehensive testing patterns.