DEV Community

Mạnh Vũ
Mạnh Vũ

Posted on

GenServer, a simple way to work with Elixir process.

In this topic 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 (our code, for implement a GenServer).

Of course, we can self made an our GenServer but with exist GenServer we have a lot of benefits like: handel errors, support Supervisor, don't need effort to wrap/handle message between two processes.

Flow of GenServer:

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

Call flow:

Image description
(result can be missed for handle_cast or return without reply from our callback implement functions)

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

GenServer handle events by pass messages between our code and GenServer then downside of this is reduce performance & message queue can OMM need to take care this if you do large scale system.

Server part

This part have included code for handle error, support Supervisor, hot reload code.

Server side maintain state (a term - map, tupble, list,..) of us and we get/update state by implement callbacks of GenServer.

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

For our code can handle server event, server provided 3 group of callback functions:

  • handle_call, for client send request & get result.
  • handle_cast, for client send request & doesn't need result.
  • utils functions like: init (for init state), terminate (shutdown GenServer, code_change (for hot reload code, I will go to this in other post).

Sever code already in language.

Notice: Our state (data) on GenServer can rescue when GenServer is crashed!

Client part

This part is our implement code for GenServer included callbacks, public APIs (for convenience), meta data & start_link function for Supervisor (if needed).

Normally, we have three group of function:

  • Callback functions, for code in server calling.
  • Public functions, for our code (process) can call to get/update state on server.
  • Private functions, simple for work with state, prepare before call server.

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__)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

(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

Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

(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

  # ...
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and in our code we can add our GenServer like:

DynamicSupervisor.start_child(Demo.DynamicSupervisor, Demo.StockHolder)
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)