पाठ 11 / 25

GenServer: Stateful Servers

Implement a GenServer with a client API and server callbacks.

The standard server behaviour

Writing receive loops by hand is error-prone, so OTP provides GenServer, a behaviour for a process that holds state and handles requests. You implement callbacks: init/1 sets the initial state; handle_call/3 handles synchronous requests from GenServer.call, replying with {:reply, reply, new_state}; handle_cast/2 handles asynchronous GenServer.cast messages with {:noreply, new_state}; and handle_info/2 handles any other messages, such as timers from Process.send_after or monitor notifications. By convention, a module exposes a client API (Cart.add_item(pid, sku)) that wraps call and cast, keeping the message format private. GenServer.call has a default timeout of 5 seconds. A GenServer processes one message at a time, which serialises access to its state, but also means a slow callback blocks everyone; move heavy work into Tasks. Use GenServers to model stateful resources: caches, rate limiters, connection managers, game rooms or a shopping cart per user.

A shopping cart GenServer

Client API on top, callbacks below.

defmodule Shop.Cart do
  use GenServer

  # ---- client API ----
  def start_link(customer_id),
    do: GenServer.start_link(__MODULE__, customer_id, name: via(customer_id))

  def add_item(customer_id, sku, qty), do: GenServer.cast(via(customer_id), {:add, sku, qty})
  def items(customer_id), do: GenServer.call(via(customer_id), :items)

  defp via(customer_id), do: {:via, Registry, {Shop.CartRegistry, customer_id}}

  # ---- server callbacks ----
  @impl true
  def init(customer_id) do
    Process.send_after(self(), :expire, :timer.minutes(30))
    {:ok, %{customer_id: customer_id, items: %{}}}
  end

  @impl true
  def handle_cast({:add, sku, qty}, state) do
    items = Map.update(state.items, sku, qty, &(&1 + qty))
    {:noreply, %{state | items: items}}
  end

  @impl true
  def handle_call(:items, _from, state), do: {:reply, state.items, state}

  @impl true
  def handle_info(:expire, state), do: {:stop, :normal, state}   # abandoned cart
end

# usage (with Registry started under the supervisor, see next topic):
# {:ok, _} = Shop.Cart.start_link("c-1")
# Shop.Cart.add_item("c-1", "pen", 2)
# Shop.Cart.items("c-1")       # %{"pen" => 2}

A shop counter with one clerk

A GenServer is a counter with one clerk who keeps the ledger. Customers can wait for an answer (call) or drop a note in the box (cast). The clerk serves one customer at a time, so the ledger never gets mixed up, but a slow customer holds up the queue.

त्वरित जाँच: Which GenServer callback handles synchronous requests made with GenServer.call?

  • handle_cast/2
  • handle_call/3
  • handle_info/2
  • init/1
Answer

handle_call/3 — handle_call replies to the caller, which waits up to a timeout.