DEV Community

Cover image for Elixir OTP - Basics with project example
Luan Gomes
Luan Gomes

Posted on

Elixir OTP - Basics with project example

OTP

OTP (Open Telecom Platform) can be defined with three components: is based on Erlang, has a set of libraries from BEAM (Erlang VM) and follow a system design principles.

If you want to know more about the design principles, take a look at the book Designing for Scalability with Erlang/OTP from Francesco Cesarini and Steve Vinoski.

Processes

In the core of the OTP we have the processes, not those of the operating system, they are created directly in BEAM, lighter, totally isolated and communicate via messages asynchronously.

In addition to all these advantages, Elixir also provides some abstractions for developers to work faster and more productively with them, through tools that allow the creation of new processes, communication and finalization, besides, thanks to immutability we don't need to worry with saving state, then problems with race condition can be avoided more easily.

An example of process creation:

iex(1)> process = spawn(fn -> IO.puts("hey there!") end)
Hey there!
#PID<0.108.0>
Enter fullscreen mode Exit fullscreen mode

Supervisor

We can create several processes but so far we have no control over them, what happens if one dies? If we need to group by context? How to organize them? For this, the OTP provides the Supervisor, with him we can start and end a list of pre-defined processes, define a behavior so that when a process dies, it is executed, for example, restarted, in addition to leaving the structure according to the context through the Supervision Tree .

Supervision Tree

Creating a simple demo project

After understanding the basics, we will create a small project to demonstrate the ease of creating and killing processes, in addition to the communication between them, in this project we will manage the number of store employees.

Creating the project:

mix new otp_test --sup

cd otp_test
Enter fullscreen mode Exit fullscreen mode

We will create the store struct:

otp_test/lib/otp_test/store/struct.ex

defmodule Store.Struct do
  @enforce_keys [:name, :employees]
  defstruct [:name, :employees]
end
Enter fullscreen mode Exit fullscreen mode

Let's implement the GenServer behavior, with our customization:

otp_test/lib/otp_test/core/store.ex

defmodule Core.Store do
  use GenServer

  alias Store.Struct, as: Store

  def start_link(%Store{} = store) do
    GenServer.start_link(__MODULE__, store, name: String.to_atom(store.name))
  end

  @impl true
  def init(%Store{} = store) do
    {:ok, store}
  end

  @impl true
  def handle_call(:store, _from, %Store{} = store) do
    {:reply, store, store}
  end

  @impl true
  def handle_cast({:add_employees, amount}, %Store{} = store) do
    store =
      store
      |> Map.put(:employees, store.employees + amount)

    {:noreply, store}
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we implement the GenServer behavior, let's analyze the functions/callbacks:

  • start_link: Function to start the process, we define the name as an atom of the store name.
  • init: Function executed as soon as the process is started.
  • handle_call: Callback for synchronous executions, in this case we just want to return the store.
  • handle_cast: Callback for asynchronous executions, here we add more employees according to the sent parameter.

We can now test the implementation, checking if the init function is executed when generating the process:

iex -S mix

iex(1)> store = %Store.Struct{name: "test1", employees: 2}
%Store.Struct{employees: 2, name: "test1"}
iex(2)> Core.Store.start_link store
{:ok, #PID<0.164.0>}
Enter fullscreen mode Exit fullscreen mode

Let's add some employees:

iex(3)> GenServer.cast String.to_atom(store.name), {:add_employees, 4}     
:ok     
iex(4)> GenServer.call String.to_atom(store.name), :store             
%Store.Struct{employees: 6, name: "test1"}
Enter fullscreen mode Exit fullscreen mode

Now this store has 6 employees, we were able to change the value as we configured! 🎉

A simpler way to deal with GenServer is separating its implementation to a public api, let's do that:

otp_test/lib/otp_test/store/management.ex

defmodule Store.Management do
  alias Store.Struct, as: Store

  def open(%Store{} = store) do
    store
    |> Core.Store.start_link()
  end

  def get_store(%Store{} = store) do
    GenServer.call(String.to_atom(store.name), :store)
  end

  def add_employees(%Store{} = store, amount) do
    GenServer.cast(String.to_atom(store.name), {:add_employees, amount})
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we abstract the implementation for use in a public api that calls GenServer but doesn't care about implementing its callbacks.

Lets test:

iex(7)> store = %Store.Struct{name: "test2", employees: 3}
%Store.Struct{employees: 3, name: "test2"}
iex(8)> Store.Management.open store                       
{:ok, #PID<0.179.0>}
iex(9)> Store.Management.add_employees store, 4 
:ok     
iex(10)> Store.Management.get_store store       
%Store.Struct{employees: 7, name: "test2"}
Enter fullscreen mode Exit fullscreen mode

So far we've created processes, but we don't use Supervisors, let's put it into practice:

otp_test/lib/otp_test/store/supervisor.ex

defmodule Store.Supervisor do
  use Supervisor

  alias Store.Struct, as: Store

  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg,  name: __MODULE__)
  end

  def init(_init_arg) do
    children = [
      create_store(%Store{name: "Test1", employees: 2}),
      create_store(%Store{name: "Test2", employees: 2})
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp create_store(%Store{} = store) do
    %{
      id: String.to_atom(store.name),
      start: {Core.Store, :start_link, [store]}
    }
  end
end
Enter fullscreen mode Exit fullscreen mode

This way we create a unique supervisor for the store context, when we start, we create two children processes.

The strategy we define for this supervisor's processes is :one_for_one , which restarts each time one of them dies.

To test that our project will start with this supervisor, it is necessary to include it in the Supervisors list, in otp_test/lib/otp_test/application.ex

defmodule OtpTest.Application do
  # See https://hexdocs.pm/elixir/Application.html
  # for more information on OTP Applications
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Starts a worker by calling: OtpTest.Worker.start_link(arg)
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: OtpTest.Supervisor]
    Supervisor.start_link(children, opts)
    Store.Supervisor.start_link([])
  end
end
Enter fullscreen mode Exit fullscreen mode

To test our Supervisor started, let's restart the application and check how many processes are in it:

iex(1)> Supervisor.which_children Store.Supervisor
[
  {:Test2, #PID<0.143.0>, :worker, [Core.Store]},
  {:Test1, #PID<0.142.0>, :worker, [Core.Store]}
]
iex(2)> Supervisor.which_children OtpTest.Supervisor
[]
Enter fullscreen mode Exit fullscreen mode

We were able to confirm that when starting the application, Store.Supervisor created the two initial processes, let's test killing one to see if it will restart:

iex(3)> :sys.terminate :Test1, :kill
:ok
iex(4)> 
18:56:44.901 [error] GenServer :Test1 terminating
** (stop) :kill
Last message: []
State: %Store.Struct{employees: 2, name: "Test1"}

nil
iex(5)> Process.whereis :Test1
#PID<0.149.0>
iex(6)>
Enter fullscreen mode Exit fullscreen mode

It worked! 🎉

We killed the :Test1 process, which had the id 142, a new one was automatically generated, this time with the id 149.

With this example I hope that the importance and ease of using OTP in elixir applications is clear, its ease and extensibility allows for great productivity in projects.

Github code: https://github.com/Lgdev07/otp_test

I appreciate everyone who has read through here, if you guys have anything to add, please leave a comment.

Discussion (1)

Collapse
allyedge profile image
Allyedge

Awesome post mate. Really helped me with starting my new project. Thanks!