Build fault-tolerant Calculator with OTP

Elixir/Erlang inbuilt provides us OTP (Open Telecom Platform) which is a set of libraries that ships with Erlang. OTP is not that much about telecom anymore (It's more about software that has the property of telecom applications, but yeah.)

Elixir/Erlang developers use OTP to build robust, fault-tolerant applications.

Let's build a Calculator with GenServer behavior with OTP. Later we will also use Supervisor behavior to restart the killed process. We will also see how to use ets i.e. Erlang Term Storage, an inbuilt memory store that comes with Elixir/Erlang. We will use ets to maintain the state, when the process is restarted by a Supervisor.

Create a mix project for Calculator

$ mix new calci --sup
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/calci.ex
* creating lib/calci/application.ex
* creating test
* creating test/test_helper.exs
* creating test/calci_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd calci
    mix test

Run "mix help" for more commands.

Run tests

$ mix test
Compiling 2 files (.ex)
Generated calci app
..

Finished in 0.02 seconds
1 doctest, 1 test, 0 failures

Randomized with seed 375143

Write Calculator Test

Create calculator_test.exs in test/calci folder

└── test
    ├── calci
    │   └── calculator_test.exs
    ├── calci_test.exs
    └── test_helper.exs

Make calculator_test.exs look as below

defmodule Calci.CalculatorTest do
  use ExUnit.Case
end

Let’s write our first test

defmodule Calci.CalculatorTest do
  use ExUnit.Case

  test "add 100" do
    calc = Calculator.new
    new_calc = Calculator.add(calc, 100)

    assert 100 == Calculator.value(new_calc)
  end
end


Run test, test should fail

$ mix test
Compiling 1 file (.ex)
Generated calci app
..

  1) test add 100 (Calci.CalculatorTest)
     test/calci/calculator_test.exs:12
     ** (UndefinedFunctionError) function Calci.Calculator.new/0 is undefined (module Calci.Calculator is not available)
     code: calc = Calculator.new()
     stacktrace:
       Calci.Calculator.new()
       test/calci/calculator_test.exs:13: (test)


Finished in 0.03 seconds
1 doctest, 2 tests, 1 failure

Randomized with seed 515834

Make test pass

Let’s write a Calculator module

Create a Calculator module at calci/lib/calci/calculator.ex

and add a new function to initialise calculator with 0

defmodule Calci.Calculator do
  alias Calci.Calculator

  defstruct value: 0

  def new do
    %Calculator{value: 0}
  end

  def add(%Calculator{value: old_value}, number) do
    %Calculator{value: old_value + number}
  end

  def get(%Calculator{value: value}) do
    value
  end
end

And here is test

defmodule Calci.CalculatorTest do
  use ExUnit.Case

  alias Calci.Calculator

  test "add 100" do
    calc = Calculator.new()
    new_calc = Calculator.add(calc, 100)

    assert 100 == Calculator.get(new_calc)
  end
end

Now when you run the test, it should pass

$ mix test
Compiling 1 file (.ex)
...

Finished in 0.03 seconds
1 doctest, 2 tests, 0 failures

Randomized with seed 576402

Introduce GenServer

Using GenServerhandle_call

Let’s introduce GenServer here to maintain state now.

defmodule Calci.Calculator do
  use GenServer

  alias Calci.Calculator

  defstruct value: 0

  ...

  def init(_initial_value) do
    {:ok, Calculator.new()}
  end

  def handle_call({:add, new_number}, _from, state) do
    {:reply, "add #{new_number}", Calculator.add(state, new_number)}
  end

  def handle_call(:get, _from, state) do
    :timer.sleep(1000)
    {:reply, Calculator.get(state), state}
  end
end

iex(1)> alias Calci.Calculator
Calci.Calculator

iex(2)> {:ok, pid} = GenServer.start_link(Calculator, 0, [])
{:ok, #PID<0.142.0>}

iex(3)> GenServer.call(pid, {:add, 400})
"add 400"
iex(4)> GenServer.call(pid, :get)
400

Add 400 again

iex(5)> GenServer.call(pid, {:add, 400})
"add 400"
iex(6)> GenServer.call(pid, :get)
800

This is working fine.

But when we run our test suite with

Tests are still failing

 $  mix test
# failing test

We should not pass state, but depend on GenServer to maintain our state.

Let’s change test for not passing state.

defmodule Calci.CalculatorTest do
  use ExUnit.Case

  alias Calci.Calculator

  test "add 100" do
    Calculator.new()
    Calculator.add(100)

    assert 100 == Calculator.get()
  end
end

 mix test
..

  1) test add 100 (Calci.CalculatorTest)
     test/calci/calculator_test.exs:12
     ** (UndefinedFunctionError) function Calci.Calculator.add/1 is undefined or private
     code: Calculator.add(100)
     stacktrace:
       (calci) Calci.Calculator.add(100)
       test/calci/calculator_test.exs:14: (test)



Finished in 0.04 seconds
1 doctest, 2 tests, 1 failure

Randomized with seed 588954

Naming the processes

iex(1)> alias Calci.Calculator
Calci.Calculator

iex(2)> {:ok, pid} = GenServer.start_link(Calculator, Calculator.new, [])
{:ok, #PID<0.143.0>}

iex(3)> GenServer.call(pid, {:add, 400})
"add 400"

iex(4)> Process.register pid, Calculator
true

iex(5)> Process.whereis(Calculator)
#PID<0.143.0>

iex(6)> GenServer.call(Calculator, {:add, 900})
"add 900"

iex(7)> GenServer.call(Calculator, {:add, 10})
"add 10"

iex(8)> GenServer.call(Calculator, :get)
1310

Instead of doing manually we can pass following option

name: __MODULE__ in GenServer.start_link to register current module name to refer to GenServer PID.

def start_link(args // []) do
  GenServer.start_link(__MODULE__, 0, name: __MODULE__)
end	

Let’s fix tests by adding changes in Calculator

defmodule Calci.Calculator do
  use GenServer

  alias Calci.Calculator

  defstruct value: 0

  # API
  def start_link(_arg \\ []) do
    GenServer.start_link(__MODULE__, Calculator.new(), name: __MODULE__)
  end

  def new do
    %Calculator{value: 0}
  end

  def add(number) do
    GenServer.call(Calculator, {:add, number})
  end

  def get() do
    GenServer.call(Calculator, :get)
  end

  # Callbacks

  def init(init_value) do
    {:ok, init_value}
  end

  def handle_call({:add, new_number}, _from, state) do
    {:reply, "add #{new_number}", _add(state, new_number)}
  end

  def handle_call(:get, _from, state) do
    :timer.sleep(1000)
    {:reply, _get(state), state}
  end

  # Private functions

  defp _add(%Calculator{value: old_value}, number) do
    %Calculator{value: old_value + number}
  end

  defp _get(%Calculator{value: value}) do
    value
  end
end

And here is how our CalculatorTest looks like

defmodule Calci.CalculatorTest do
  use ExUnit.Case

  alias Calci.Calculator

  # Runs before each test
  setup do
    Calculator.start_link()
    {:ok, %{}}
  end

  test "add 100" do
    Calculator.add(100)

    assert 100 == Calculator.get()
  end

  test "add 100 twice" do
    Calculator.add(100)
    Calculator.add(100)
    result = Calculator.get()

    assert 200 == result
  end
end

Now run the tests

$ mix test
....

Finished in 2.0 seconds
1 doctest, 3 tests, 0 failures

Randomized with seed 786247

Use of callbacks handle_cast

Let’s understand and make use of handle_cast callback available in GenServer behaviour

We can add a reset function in our Calculator which will allow us to reset the number in Calculator to 0

Let’s write a test

  test "reset to 0" do
    Calculator.add(100)
    Calculator.reset()

    result = Calculator.get()

    assert 0 == result
  end


defmodule Calci.Calculator do
  ...
  def reset() do
    GenServer.cast(Calculator, :reset)
  end
  ...
  def handle_cast(:reset, _state) do
    {:noreply, Calculator.new()}
  end
end

Remaining tests (subtract, multiply, divide)

Let’s add more tests and get Calculator working fine.

  test "subtract 100" do
    Calculator.add(100)
    Calculator.subtract(100)

    result = Calculator.get()

    assert 0 == result
  end

  test "multiple by 5" do
    Calculator.add(100)
    Calculator.multiply(5)

    result = Calculator.get()

    assert 500 == result
  end

  test "divide by 100" do
    Calculator.add(300)
    Calculator.divide(100)

    result = Calculator.get()

    assert 3 == result
  end

Our Calculator would have following new functions now

  def subtract(number) do
    GenServer.call(Calculator, {:subtract, number})
  end

  def multiply(number) do
    GenServer.call(Calculator, {:multiply, number})
  end

  def divide(number) do
    GenServer.call(Calculator, {:divide, number})
  end

...

  def handle_call({:subtract, number}, _from, state) do
    {:reply, "subtract #{number}", _subtract(state, number)}
  end

  def handle_call({:multiply, number}, _from, state) do
    {:reply, "multiply #{number}", _multiply(state, number)}
  end

  def handle_call({:divide, number}, _from, state) do
    {:reply, "divide #{number}", _divide(state, number)}
  end

...

  defp _subtract(%Calculator{value: old_value}, number) do
    %Calculator{value: old_value - number}
  end

  defp _multiply(%Calculator{value: old_value}, number) do
    %Calculator{value: old_value * number}
  end

  defp _divide(%Calculator{value: old_value}, number) do
    %Calculator{value: old_value / number}
  end

Now run the tests

$ mix test
........

Finished in 6.0 seconds
1 doctest, 7 tests, 0 failures

Randomized with seed 276060

Let it crash

Let’s try to divide by 0

iex(1)> alias Calci.Calculator
Calci.Calculator

iex(2)> Calculator.start_link([])
{:ok, #PID<0.142.0>}

iex(3)> Calculator.add(100)
"add 100"

iex(4)> Calculator.get
100

iex(5)> Process.whereis(Calculator) # To check process present
#PID<0.151.0>. 

iex(6)> Calculator.divide(0)

16:29:42.885 [error] GenServer Calci.Calculator terminating
** (ArithmeticError) bad argument in arithmetic expression
    (calci) lib/calci/calculator.ex:47: Calci.Calculator.handle_call/3
    (stdlib) gen_server.erl:661: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:690: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.139.0>): {:divide, 0}
State: 100
Client #PID<0.139.0> is alive

    (stdlib) gen.erl:167: :gen.do_call/4
    (elixir) lib/gen_server.ex:1007: GenServer.call/3
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir) src/elixir.erl:275: :elixir.eval_forms/4
    (iex) lib/iex/evaluator.ex:257: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:237: IEx.Evaluator.do_eval/3
    (iex) lib/iex/evaluator.ex:215: IEx.Evaluator.eval/3
    (iex) lib/iex/evaluator.ex:103: IEx.Evaluator.loop/1
** (EXIT from #PID<0.139.0>) shell process exited with reason: an exception was raised:
    ** (ArithmeticError) bad argument in arithmetic expression
        (calci) lib/calci/calculator.ex:47: Calci.Calculator.handle_call/3
        (stdlib) gen_server.erl:661: :gen_server.try_handle_call/4
        (stdlib) gen_server.erl:690: :gen_server.handle_msg/6
        (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

Interactive Elixir (1.9.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(7)> Calculator.get
** (UndefinedFunctionError) function Calculator.get/0 is undefined (module Calculator is not available)
    Calculator.get()

We can see that Calculator process is no longer available. That’s the issue.

iex(1)> Process.whereis(Calculator)
nil

Adding supervisor

In your mix project, you must have this file

calci/`lib/calci/application.ex

Just add {Calci.Calculator, []} in the children array

defmodule Calci.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Calci.Calculator, []}
    ]

    opts = [strategy: :one_for_one, name: Calci.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Now when we try

iex -S mix

iex(1)> alias Calci.Calculator
Calci.Calculator

iex(2)> Calculator.start_link([])
{:error, {:already_started, #PID<0.139.0>}}
iex(3)>

That’s the difference.

iex(3)> Calculator.add(100)
"add 100"

iex(4)> Calculator.add(100)
"add 100"

iex(5)> Calculator.get
200

iex(6)> Calculator.divide(0)

16:34:19.663 [error] GenServer Calci.Calculator terminating
** (ArithmeticError) bad argument in arithmetic expression
    (calci) lib/calci/calculator.ex:47: Calci.Calculator.handle_call/3
    (stdlib) gen_server.erl:661: :gen_server.try_handle_call/4
    (stdlib) gen_server.erl:690: :gen_server.handle_msg/6
    (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.140.0>): {:divide, 0}
State: 200
Client #PID<0.140.0> is alive

    (stdlib) gen.erl:167: :gen.do_call/4
    (elixir) lib/gen_server.ex:1007: GenServer.call/3
    (stdlib) erl_eval.erl:680: :erl_eval.do_apply/6
    (elixir) src/elixir.erl:275: :elixir.eval_forms/4
    (iex) lib/iex/evaluator.ex:257: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:237: IEx.Evaluator.do_eval/3
    (iex) lib/iex/evaluator.ex:215: IEx.Evaluator.eval/3
    (iex) lib/iex/evaluator.ex:103: IEx.Evaluator.loop/1
** (exit) exited in: GenServer.call(Calci.Calculator, {:divide, 0}, 5000)
    ** (EXIT) an exception was raised:
        ** (ArithmeticError) bad argument in arithmetic expression
            (calci) lib/calci/calculator.ex:47: Calci.Calculator.handle_call/3
            (stdlib) gen_server.erl:661: :gen_server.try_handle_call/4
            (stdlib) gen_server.erl:690: :gen_server.handle_msg/6
            (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
    (elixir) lib/gen_server.ex:1010: GenServer.call/3

But now when you do

iex(6)> Calculator.get
0

Calculator PID is still alive, we can check

iex(7)> Process.alive?(Process.whereis(Calculator))
true

iex(2)> Calculator.add(100)
"add 100"
iex(3)> Calculator.add(100)
"add 100"

iex(4)> Calculator.get
200

That's good. We are now able to restart our Calculator process.

But after crash our Calculator state is resetting to 0.

We need to maintain state when process is restarted. For this Elixir (Erlang) offers an inbuilt memory store named ets (Erlang Term Storage)

Let's add ets support

Final version

As it's inbuilt in Elixir language, we don't need to add any library depdencency. WE can start with following steps.

Create ets table in application.ex

defmodule Calci.Application do
  use Application

  defp init_ets() do
    :ets.new(:calculator, [:set, :public, :named_table])
  end

  def start(_type, _args) do
    children = [
      {Calci.Calculator, []}
    ]
    opts = [strategy: :one_for_one, name: Calci.Supervisor]
    init_ets()
    Supervisor.start_link(children, opts)
  end
end


defmodule Calci.Calculator do
  use GenServer

  alias Calci.Calculator

  defstruct value: 0
  
  ...  
  def init(init_value) do
    lookup()
  end
  ...

  defp _add(%Calculator{value: old_value}, number) do
    result = old_value + number
    store(result)
    %Calculator{value: result}
  end

  ...

  defp store(result) do
    :ets.insert(:calculator, {:result, %Calculator{value: result}})
  end

  defp lookup() do
    case :ets.lookup(:calculator, :result) do
      [{:result, result}] -> {:ok, result}
      [] -> {:ok, %Calculator{value: 0}}
    end
  end
end

After adding ets support, when we run

iex(1)> Calculator.add(100)
Add 100
iex(2)> Calculator.add(100)
Add 100
iex(1)> Calculator.get
200
iex(5)> Calculator.divide(0)

Calculator process would crash again with exception of ArithmeticError

But we when we run, we should get our value in Calculator process back.

iex(1)> Calculator.get
200

This is because, before Calculator process was crashed, we had stored it's state in ets After the Calculator process is restarted, we can still get the value back as state is properly reinitialized for the new process.

That's how we restarted the process and recovered the state.