This article is all about how to achieve the fault tolerance concept using the Supervisor
in Elixir. This gonna be the lengthy reading as I tried to explain each line of code briefly.
This article is not about How the Supervisor or GenServer works.
What we gonna do here?
Here, we are going to implement Mini Banking System.
Assumption
We assume only one account holder is present in this Mini Banking System to make this simple and more understandable.
Note or Warning:
This is not a production grade application. So, consider this as just another example project to demonstrate certain real world problems.
Create the Supervisor application
$ mix new --sup bank
You can name it as you like. As our application resembles like a mini bank application with single account, I named it as Bank.
So, after creating the new application, change the directory to the application
$ cd Bank
This is how the directory hierarchy of application looks. Here, I am using elixir v1.6.3
.
I hope that you already have some idea about what these individual files do based on their file_name convention.
Now, open the file bank.ex
and remove the code inside the file. We’ll code that file from the scratch to have full control of the code.
At first, we make our module Bank to behave like GenServer
by adding the line use GenServer
. This line implements the default server callbacks.
# bank.ex
defmodule Bank do
use GenServer
end
We will provide client and Server callbacks here.
Starting and Initiating Bank GenServer
Add the following definition under the line use GenServer
in the file bank.ex
.
def start_link(initial_balance) do
GenServer.start_link(Bank, initial_balance, name: Bank)
end
In the above code, we are doing nothing more fancy just starting the server using GenServer.start_link
definition by just passing the initial_balance
.
The GenServer.start_link
call will trigger the init
definition so add the following init
definition also.
def init balance do
{:ok, balance}
end
The init
definition is used to initiate the state of the server as we like in the way. Here, our state is simply the balance of the account. It gets updated based on the message requests.
Client API. Here, we will provide some functions to interact with the server. You can guess its functionality from the name of the function.
#client api
def get_current_balance() do
GenServer.call(Bank, :get)
end
def show_balance() do
IO.inspect(get_current_balance(), label: "Available Balance")
end
def deposit amount do
GenServer.cast(Bank, {:credit, amount})
end
def with_draw amount do
transaction = GenServer.call(Bank, {:with_draw, amount})
case transaction do
:ok ->
IO.inspect amount, label: "Amount debitted"
show_balance()
{:ok, error}->
IO.inspect error.reason, label: "ERROR:"
end
end
When get_current_balance
function is called, it makes synchronous request to the Bank
GenServer
by sending message :get
. Don’t worry soon we are going to handle those requests in the server api section.
The show_balance
is for showing the current balance in account.
The deposit/1
function is used to credit the given amount in the account. It makes an asynchronous request to the server with message {:credit, amount}
.
The with_draw/1
function triggers the GenServer handle_call
with a message {:with_draw, amount}
. As we know, the handle_call
deals with all synchronous requests. So, we are expecting the return value from the server call and the returned value is assigned to transaction
.
Server callbacks API
# server api
#synchronous requests
def handle_call(:get, _from, current_balance) do
{:reply, current_balance, current_balance}
end
def handle_call({:with_draw, with_draw_amount}, _from, current_balance)
when current_balance > with_draw_amount do
GenServer.cast(Bank, {:debit, with_draw_amount})
{:reply, :ok, current_balance}
end
#asynchronous requests
def handle_cast({:credit,amount}, current_balance) do
{:noreply, current_balance + amount }
end
def handle_cast({:debit,amount}, current_balance) when amount < current_balance do
{:noreply, current_balance - amount }
end
If you observe lines of code, we have two synchronous and two asynchronous call backs.
Basically, the banking system deals with databases. But, here we keep this simple to understand the Supervisor and GenServer concepts.
The handle_call(:get, _from, current_balance)
will just reply with current state as state is considered as a balance here.
The handle_call({:with_draw, with_draw_amount}, _from, current_balance)
deals with the withdraw function calls. We have when condition to be satisfied before entering into the function. Once the condition is satisfied, it makes another asynchronous request GenServer.cast(Bank, {:debit, with_draw_amount})
to debit the amount from the account and it gives reply with an atom :ok to specify that transaction is completed.
The handle_cast({:credit,amount}, current_balance)
it just updates the state by adding given amount to the current balance {:noreply, current_balance + amount }
In the similar fashion, we implemented the handle_cast({:debit,amount}, current_balance) when amount < current_balance
Here, we are checking the condition again to make sure the balance not to be in negative.
🔥 What happens when the client tries to withdraw amount more than the current_balance he has ?
We, purposely did not handle here. Soon you will come to know what will happen if you do that.
So far, we have created the GenServer
for transaction system. But, we haven’t dealt with how this server gets started with the intial_balance
. For that, we are adding this server as child to the application.
Now, open the file application.ex
inside the lib/bank
folder. Under the start
function, you will see an empty children
list.
Change the function some thing like in the following code snipped by adding a child to children list.
# lib/application.ex
...
def start(_type, _orgs) do
children: [
{Bank, 5000}
]
..
end
So, when the application is started, it starts the Bank
GenServer
also with an argument 5000
as initial_balance. That is the most simplest way of specifying the child spec. You can also add the child in different ways. This calls the start_link/1
function with a parameter 5000
as initial_balance
.
Running Application
Now, run the project in the interactive mode as iex -S mix
and call the client functions we have defined.
iex> Bank.show_balance
You can see the code execution in the above screenshot. Here, everything is worked fine except the case when you are trying to withdraw amount more than the current balance. It raised a run-time error and restarted the server.
So far well and good. But, the actual problem is, it restarted the server again with initial_balance not with the state before the server collapsed. What I am trying to say is
intial_balance 5000 current_balance = 5000 deposit 5000 current_balance 5000+5000=10000 with_draw 3000 current_balance=100000–3000=7000 with_draw 10000 current_balance = 70000–10000=fail to process Exception
Before server failure, the current_balance is 7000 but after server failure and is restarted. So, we can call show_balance
function to check the state after server failure.
iex> Bank.show_balance
Available Balance: 5000 # actually it should be 7000
5000
So, whenever the server is restarted, it restarts based on the child specification. Our child specification is {Bank, 5000}
. That is why the current_balance is changed to 5000 instead of 7000.
Hope you understood the problem. Now, we are going to update the system to work fine as best as possible.
Things to do
- Backup the previous state of the server before failing.
- Changing the child_spec to restart the server with previous state.
To achieve this, we take the help of one extra Supervisor and GenServer
🔥 You can do this in your own different style of coding. If you find better code for this kinda problem feel free to share .
Let’s code it.
Here, we’ll change the files completely. Please have a look at the following flow diagram.
Modifying application.ex
Open the file lib/application.ex
and change the file content to
defmodule Bank.Application do
@moduledoc false
use Application
def start(_type, _args) do
Bank.Supervisor.start_link(5000)
end
end
The start function is triggered when we run the application. Instead of adding the child Bank GenServer like we did in the above, we are calling the start_link
function from Bank.Supervisor
module with an argument.
Bank.Supervisor.start_link(5000)
Now, we have to code the lib/bank/supervisor.ex
Creating supervisor.ex
Create a file lib/bank/supervisor.ex
. This will start our children process Cache
and Bank
. We haven’t implement the Cache
server. Soon we will do that.
defmodule Bank.Supervisor do
use Supervisor
def start_link(initial_balance) do
bank_supervisor = {:ok, sup} = Supervisor.start_link(__MODULE__, [initial_balance])
start_children(sup, initial_balance)
bank_supervisor
end
def start_children(sup, initial_balance) do
{:ok, cache_pid} =
Supervisor.start_child(sup, worker(Bank.Cache, [initial_balance]))
Supervisor.start_child(sup, worker(Bank, [cache_pid]))
end
def init(_) do
supervise([], strategy: :one_for_one)
end
end
Once it is triggered from the application start the function like Bank.Supervisor.start_link(5000)
, at very first it initiates the supervisor with empty children. We need a supervisor pid to add children to the supervisor wisely. The sup variable is the supervisor pid here.
After that, it is calling its own function start_children
passing sup
and initial_balance
. We initiate two children Cache
and Bank
in this function.
The {:ok, cache_pid} = Supervisor.start_child(sup, worker(Bank.Cache, [initial_balance]))
starts the Cache child. We used worker(Bank.Cache, [initial_balance])
as child spec cause Cache is going to be the GenServer worker. Soon we will code it.
We are taking the help of Cache
to initiate the Bank
server unlike we directly passed the value to start_link
function of Bank
server, we call a function from the Cache
server. So, we need the pid of Cache
to pass this to start_link
function in Bank server. So, the order of creating the child should be like above.
The Supervisor.start_child(sup, worker(Bank, [cache_pid]))
initiates the Bank
server passing the cache_pid
as an argument.
I know that it looks little confusing here, but you need little focus to get the point. Nothing is complex here. If you can’t understand now, you will once you will see the complete code. Don’t worry it will be simple once every file is done with coding.
Coding Cache server
This cache server provides two functions get_balance
and save_balance
. The Bank server calls one of these functions based on the requirement. The save_function
is called whenever the Bank
server is collapsed to save its previous state.
Have a look at the code
defmodule Bank.Cache do
use GenServer
def start_link initial_balance do
GenServer.start_link(__MODULE__, initial_balance)
end
def init(initial_balance) do
{:ok, initial_balance}
end
def get_balance(pid) do
GenServer.call pid, :get
end
def save_balance(new_balance, pid) do
GenServer.cast(pid,{:save, new_balance})
end
def handle_call(:get, _from, balance) do
{:reply, balance, balance}
end
def handle_cast({:save, new_balance}, _balance) do
{:noreply, new_balance}
end
end
The get_balance
function always returns the state of the Cache and save_balance
updates the state whenever the Bank
server is collapsed. We haven’t updated the Bank
GenServer
to use the Cache
server. Soon we will update our Bank
server as well.
I hope you got the idea of what the cache does.
Editing Bank GenServer
Things to be changed.
- Server initiation and state structure
- handling terminate calls when the server collapse.
Before Editing
According to our previous code, the server is initiated with the passed value and we haven’t implemented the terminate
call back which is triggered before the server gets collapsed.
Code after changing things in bank.ex
defmodule Bank do
use GenServer
alias Bank.Cache
@moduledoc """
Documentation for Bank.
"""
# client API
def start_link(cache_pid) do
GenServer.start_link(Bank, cache_pid, name: Bank)
end
def init cache_pid do
balance = Cache.get_balance(cache_pid)
{:ok, %{current_balance: balance, cache_pid: cache_pid}}
end
def get_current_balance() do
GenServer.call(Bank, :get)
end
def show_balance() do
IO.inspect(get_current_balance(), label: "Available Balance")
end
def deposit amount do
GenServer.cast(Bank, {:credit, amount})
end
def with_draw amount do
transaction = GenServer.call(Bank, {:with_draw, amount})
case transaction do
:ok ->
IO.inspect amount, label: "Amount debitted"
show_balance()
{:ok, error}->
IO.inspect error.reason, label: "ERROR:"
end
end
# Server API
def handle_call(:get, _from, state) do
{:reply, state.current_balance, state}
end
def handle_call({:with_draw, with_draw_amount}, _from, %{current_balance: current_balance } = state)
when current_balance > with_draw_amount do
GenServer.cast(Bank, {:debit, with_draw_amount})
{:reply, :ok, state}
end
def handle_cast({:credit,amount}, %{current_balance: current_balance}=state) do
new_balance = current_balance + amount
{:noreply, %{state | current_balance: new_balance } }
end
def handle_cast({:debit,amount}, %{current_balance: current_balance} = state) when amount < current_balance do
new_balance = current_balance - amount
{:noreply, %{state | current_balance: new_balance } }
end
def terminate(_reason, state) do
Cache.save_balance(state.current_balance, state.cache_pid)
end
end
The following code snippet will initiate the server.
def init cache_pid do
balance = Cache.get_balance(cache_pid)
{:ok, %{current_balance: balance, cache_pid: cache_pid}}
end
If you observer, before setting the state, we are calling
balance = Cache.get_balance(cache_pid)
which makes a synchronous call to the Cache
to get the current_balance
. As, this is the beginning, it simply returns the initial_balance
.
We also changed the structure of the state to map {:ok, %{current_balance: balance, cache_pid: cache_pid}}
. We need pid of the Cache
server when it is terminated to save its previous state.
handling server termination
When we write a line use GenServer
in the module it abstract the GenServer implementation. So, we no need to handle all the things needed. The GenServer module Behaviour module will take care of that.
In Erlang you need to implement 6 server callback functions to write the GenServer
. In elixir, it is much flexible to write the GenServer
by just overriding the things we needed.
We need to override the terminate
callback to deal with server termination.
def terminate(_reason, state) do
Cache.save_balance(state.current_balance, state.cache_pid)
end
The terminate callback receives reason and its previous state as parameters. So, we will make use of this to preserve our state by calling the save_balance
function from the Cache
module.
I hope you got the point. We will make some slight modifications of state as we updated in the rest of the functions and callbacks. You can understand them easily.
Now, we have updated all things needed to work smoothly.
Running Application
Run the application in interactive mode as iex -S mix
Execute the functions in Bank server as we did in earlier.
iex> Bank.show_balance
Available Balance: 5000
5000
iex> Bank.deposit 5000
:ok
iex> Bank.show_balance
Available Balance: 10000
10000
iex> Bank.with_draw 3000
Amount debitted: 3000
Available Balance: 7000
iex> Bank.with_draw 10000 # raises an error#after error
iex> Bank.show_balance
Available Balance: 7000 # state is preserved after server collapse
Resources: Download source code zip
Project link : https://gitlab.com/ahamtech/articles/bank
Hope you like this.
Check out the GitHub repository on Killer Elixir Tips
Glad if you can contribute with a ★