GenServer, a simple way to work with Elixir process
Author: Manh Vu
Published: 2024-06-29

In the topic about “Elixir Process, what is? how works?” I have talk about Elixir process now I talk about GenServer & use cases in Elixir.

GenServer is a template/skeleton for work with process like a server/client model. It’s easy to add to Supervisor and easy to build a robust system (a super weapon in Elixir/Erlang world).

GenServer included 2 parts, one is server side it’s included in language, an other part is client side.

Of course, we can self make our GenServer but with existed GenServer we have a lot of benefits like: handle errors, support Supervisor, don’t need effort to wrap/handle message for communicating between two processes.

Flow of GenServer:

Other processes <–> Our public APIs <–> GenServer client <-send request(msg) - [wait result]-> GenServer server loop <–> Our GenServer callbacks

Function call flow of GenServer:

function call flow (result can be missed for handle_cast or return :noreply from our callback functions)

GenServer can handle a lot of requests from other processes and ensure only one event is processing at a time. We can use GenServer for sharing state/global data and avoid race condition (by atomic execution - a request is complete action/transaction).

GenServer handle events by pass messages between our client code and callback api of GenServer, downside of this is reduce performance & message queue can raise an OMM error, you need to take care this if you develop a large scale system.

Server part

This part have included code for handle errors, support Supervisor, hot reload code, terminating and our implemented callback APIs.

Server side (a process) maintains state (a term - map, tuple, list,…) for us. We get/update state by implement callbacks of GenServer.

To start a GenServer we need to call public api start_link (common & official way) (by add to a supervisor, directly call in runtime & Elixir shell) then GenServer will call our init callback to init state.

A thing to remember in start phase is name (option: :name) of GenServer it uses to call from other processes (or we use returned pid, less convenience). Name of GenServer can set to local (call only in local node), global (register in all nodes in cluster, can call remotely from other node) or a mechanism like Registry for managing.

After init, GenServer go to loop function for handle events (wait messages that are wrapped in tuple with special formats for detecting kind of event) from client side (other process).

For implement server events, GenServer provided some groups of callback functions:

  • handle_call, for client send request & get result.
  • handle_cast, for client send request & doesn’t need result.
  • handle_info, for direct message was sent to server by send function.
  • other functions like: init (for init state), terminate (shutdown GenServer, code_change (for hot reload code, I will go to this in other post).

Notice: Our state (data) on GenServer can rescue when GenServer is crashed (by storage in other GenServer or :ets table in other process). That is a one of technics using to build a robust system.

We have group of functions need to implement for server side:

  • Callback functions, for code in server calling.
  • start_link (common way, for easy integrate to a supervisor) function call to GenServer.start_link.

Client part

This part is our implement code for public APIs for other process (client) can send request to GenServer & start_link (for easy integrate to a supervisor) function (if needed).

Normally, we have group of functions:

  • Public functions, for our code (process) can call to get/update state on server and return result (if needed).

Step init & handle event from server code

Almost case, GenServer started from Supervisor by call start_link (a common name) or other public function with params (or not) then call to GenServer.start_link like:

GenServer.start_link(__MODULE__, nil, name: __MODULE__)

After that GenServer will call init callback function for init our state then GenServer will help us maintain our state.

init function example:

  @impl true
  def init(_) do
    # init state.
    state = %{}

    {:ok, state}
  end

(code from my team, create an empty map and return it as state to GenServer)

Now in our GenServer has state(data) for get/update we need implement hanlde_call or handle_cast callback and add a simple public function to call our request

example:

  # Public function
  def add_stock({_stock, _price} = data) do
    GenServer.cast(__MODULE__, {:add_stock, data})
  end
  
  # callback function
  @impl true
  def handle_cast( {:add_stock, {stock, price}, state) do
    {:noreply, Map.put(state, stock, price)}
  end

(this code implement public api and callback to add a stock and price to state (a map))

  # Public function
  def get_stock(stock) do
    GenServer.call(__MODULE__, {:get_stock, stock})
  end
  
  # callback function
  @impl true
  def handle_call({:get_stock, stock}, _from, state) do
    {:reply, Map.get(state, stock), state}
  end

(this couple of function is wrap a public api for easy call from outside, a callback function for get data from state)

We can use pattern matching in the header of callback function or move it to a private function if needed.

call/cast event is main way to communicate with GenServer but we can an other way is handle_info for send request to GenServer.

Example:

  # callback function
  @impl true
  def handle_info({:get_stock, from, stock}, state) do
    send(from, Map.get(state, stock))

    {:noreply, state}
  end

(this code handle directly request came from other process (or itself) by send(server_pid, {:get_stock, self(), "Stock_A")))

For every event (call, cast, handle_info) we can send other message to GenServer to tell stop, error. Please check it more on hexdocs

hot_reload code, for case we need update state (example: change data format in our state) we can implement code_change to do that.

Example:

  # callback
  @impl true
  def code_change(_old_vsn, state, _extra) do
    ets = create_outside_ets()
    put_old_state_to_ets(state)
    {:ok, state}
  end

(this code will handle case we update our GenServer - convert data from map to :ets table).

After all, if need to clean up state when GenServer we can implement terminate callback.

Example:

  # callback
  @impl true
  def terminate(reason, state) do
    clean_up_outside_ets()

    :normal
  end

(this code help us clean up state (data) if it use outside resource).

Using GenServer with Supervisor

Very convenience for use if use GenSever with Supervisor. We can add to application supervisor or our DynamicSupervisor.

Example we have GenServer declare a meta & init + start_link like:

defmodule Demo.StockHolder do
  use GenServer, restart: :transient

  def start_link(_) do
    GenServer.start_link(__MODULE__, :start_time, name: __MODULE__)
  end

  # ...

(See a keyword use, we can add meta data for init a child for Supervisor, in here i add :restart strategy only, other can check on docs)

Now we can add directy to application supervisor like:

defmodule Demo.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      Demo.StockHolder
    ]

    opts = [strategy: :one_for_one, name: Demo.Supervisor]
    Supervisor.start_link(children, opts)
  end

Now we have a GenServer start follow our application.

For case using with DynamicSupervisor we add a Supervisor to our application like:

defmodule Demo.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
       {DynamicSupervisor, name: Demo.DynamicSupervisor, strategy: :one_for_one}
    ]

    opts = [strategy: :one_for_one, name: Demo.Supervisor]
    Supervisor.start_link(children, opts)
  end

and in our code we can add our GenServer like:

DynamicSupervisor.start_child(Demo.DynamicSupervisor, Demo.StockHolder)

Use cases for GenServer

  1. Sharing data between process.
  2. Handle errors & make a robust system.
  3. Easy make a worker process for Supervisor.
  4. Easy to add code support for hot reload code(very interesting feature).

Now we can easy work with GenServer and save a lot of time for work with process/supervisor.