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.


Join Our Telegram Channel

Blackoders
  Telegram
  Channel

Check out the GitHub repository on Killer Elixir Tips

Blackoders
  Telegram
  Channel Glad if you can contribute with a

🎉 Happy Coding :)


author image
WRITTEN BY
Blackode