In this topic, I have talk about Elixir process now I talk about GenServer
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 <–> Our GenServer callbacks
GenServer
can handle a lot of requests from other processes and ensure one event is processing at a time.
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
.
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__)
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)
Now we can easy work with GenServer
and save a lot of time for work with process/supervisor.
Source link
lol