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.
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
:
(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 toGenServer.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
- Sharing data between process.
- Handle errors & make a robust system.
- Easy make a worker process for
Supervisor
. - 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)