GenServer is a behavior module for implementing the server of a client-server relation. Itβs one of the core building blocks in Elixir/Erlang for creating stateful, concurrent processes that can handle messages reliably.
Hereβs a quick taste:
defmodule Counter do
use GenServer
# Client API
def start_link(initial_count) do
GenServer.start_link(__MODULE__, initial_count)
end
def increment(pid) do
GenServer.cast(pid, :increment)
end
def get_count(pid) do
GenServer.call(pid, :get_count)
end
# Server Callbacks
@impl true
def init(count), do: {:ok, count}
@impl true
def handle_cast(:increment, count), do: {:noreply, count + 1}
@impl true
def handle_call(:get_count, _from, count), do: {:reply, count, count}
end
# Usage:
{:ok, pid} = Counter.start_link(0)
Counter.increment(pid)
Counter.get_count(pid) # Returns 1
defmodule ShoppingCart do
use GenServer
defstruct items: %{}, total: 0
def start_link(_opts) do
GenServer.start_link(__MODULE__, %__MODULE__{})
end
def add_item(pid, item, price) do
GenServer.cast(pid, {:add_item, item, price})
end
@impl true
def handle_cast({:add_item, item, price}, state) do
new_items = Map.put(state.items, item, price)
new_total = state.total + price
{:noreply, %{state | items: new_items, total: new_total}}
end
end
call
for operations needing a responsecast
for fire-and-forget operationshandle_info
for unexpected or timer messagesdefmodule CacheServer do
use GenServer
# Synchronous - waits for response
def get(pid, key) do
GenServer.call(pid, {:get, key})
end
# Asynchronous - doesn't wait
def set(pid, key, value) do
GenServer.cast(pid, {:set, key, value})
end
# Timer message
def schedule_cleanup(pid) do
Process.send_after(pid, :cleanup, :timer.hours(1))
end
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_cast({:set, key, value}, state) do
{:noreply, Map.put(state, key, value)}
end
@impl true
def handle_info(:cleanup, state) do
schedule_cleanup(self())
{:noreply, %{}}
end
end
terminate/2
for cleanup:stop
tuple for controlled shutdownsdefmodule ResilientWorker do
use GenServer
@impl true
def init(state) do
Process.flag(:trap_exit, true)
{:ok, state}
end
@impl true
def handle_call(:risky_work, _from, state) do
case do_work() do
{:ok, result} -> {:reply, result, state}
{:error, reason} -> {:stop, reason, state}
end
end
@impl true
def terminate(reason, state) do
Logger.info("Terminating due to #{inspect(reason)}")
cleanup(state)
:ok
end
end
β Great for:
β Consider alternatives for:
defmodule PeriodicWorker do
use GenServer
@interval :timer.seconds(60)
def init(state) do
schedule_work()
{:ok, state}
end
def handle_info(:work, state) do
do_work(state)
schedule_work()
{:noreply, state}
end
defp schedule_work do
Process.send_after(self(), :work, @interval)
end
end
defmodule TrafficLight do
use GenServer
@states [:red, :yellow, :green]
def next_state(pid), do: GenServer.cast(pid, :next)
@impl true
def handle_cast(:next, state) do
next = Enum.at(@states, rem(Enum.find_index(@states, &(&1 == state)) + 1, 3))
{:noreply, next}
end
end
defmodule RateLimiter do
use GenServer
defstruct requests: %{}, limit: 0, window: 0
# Client API
def start_link(opts) do
limit = Keyword.get(opts, :limit, 10)
window = Keyword.get(opts, :window, :timer.seconds(1))
GenServer.start_link(__MODULE__, {limit, window})
end
def check_rate(pid, key) do
GenServer.call(pid, {:check_rate, key})
end
# Server Callbacks
@impl true
def init({limit, window}) do
{:ok, %__MODULE__{limit: limit, window: window}}
end
@impl true
def handle_call({:check_rate, key}, _from, state) do
now = System.system_time(:millisecond)
window_start = now - state.window
# Get existing requests and filter out old ones
requests = Map.get(state.requests, key, [])
valid_requests = Enum.filter(requests, &(&1 >= window_start))
cond do
length(valid_requests) < state.limit ->
# Under limit - allow request
new_requests = [now | valid_requests]
new_state = put_in(state.requests[key], new_requests)
{:reply, :ok, new_state}
true ->
# Over limit - deny request
{:reply, {:error, :rate_limited}, state}
end
end
end
# Usage Example:
{:ok, limiter} = RateLimiter.start_link(limit: 2, window: :timer.seconds(5))
# First two requests succeed
:ok = RateLimiter.check_rate(limiter, "user_1")
:ok = RateLimiter.check_rate(limiter, "user_1")
# Third request fails (within 5 second window)
{:error, :rate_limited} = RateLimiter.check_rate(limiter, "user_1")
# Different user has their own limit
:ok = RateLimiter.check_rate(limiter, "user_2")
This rate limiter:
limit
) within a time windowThe easiest way to experiment with GenServer is in iex
:
iex> defmodule Echo do
...> use GenServer
...> def init(state), do: {:ok, state}
...> def handle_call(:ping, _from, state) do
...> {:reply, :pong, state}
...> end
...> end
iex> {:ok, pid} = GenServer.start_link(Echo, [])
iex> GenServer.call(pid, :ping)
:pong
Remember: GenServer is powerful, but with great power comes great responsibility. Start simple and add complexity only when needed!