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:
(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__)
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)