Intro to Elixir processes

Alex Rozumii, Toptal

Who am I?

What is this talk about?

What is a process?

What is a process?

Process is an instance of a computer program that is being executed (wiki)

We're talking not about OS processes

BEAM processes

BEAM

BEAM

A virtual machine to run Erlang and Elixir

BEAM

Processes are lightweight and run across all CPUs.

BEAM

It’s not uncommon to have thousands of concurrent processes in an Elixir application.

BEAM

It's based on Actor model

Elixir process

Elixir process

iex> spawn(fn -> 
iex>   IO.puts "Hello, Elixir meetup!" 
iex> end)

Hello, Elixir meetup!
#PID<0.77.0>

Elixir process

defmodule Example do
  def add(a, b) do
    IO.puts(a + b)
  end
end

iex> spawn(Example, :add, [2, 3])
5
#PID<0.80.0>

Elixir process

iex> pid = spawn(fn -> IO.puts "Hi!" end)
Hi!
#PID<0.83.0>

iex> Process.alive? pid
false

He's dead, Jim

Recursion

Recursion

Recursion

defmodule Example do
  def printer do
    IO.puts "It's alive!"
    Process.sleep 500
    printer
  end
end

iex> pid = spawn(Example, :printer, [])
It's alive!
#PID<0.69.0>
It's alive!
It's alive!
It's alive!
It's alive!

Recursion

Recursion

But not really.

Tail call optimization is there.

Messaging

Messaging

defmodule Example do
  def listen do
    receive do
      {:ok, "hello"} -> IO.puts "World"
    end

    listen
  end
end

iex> pid = spawn(Example, :listen, [])
#PID<0.108.0>

iex> send pid, {:ok, "hello"}
World
{:ok, "hello"}

iex> send pid, :ok
:ok

Crashes

My code is ideal

- nobody

spawn

iex> self
#PID<0.56.0>

iex> spawn fn -> 5/0 end
#PID<0.62.0>

23:01:08.324 [error] Process #PID<0.62.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    :erlang./(5, 0)

iex> self
#PID<0.56.0>

spawn_link

iex> self
#PID<0.56.0>

iex> spawn_link fn -> 5/0 end
** (EXIT from #PID<0.56.0>) an exception was raised:
    ** (ArithmeticError) bad argument in arithmetic expression
        :erlang./(5, 0)

Interactive Elixir (1.3.0) - press Ctrl+C to exit (type h() ENTER for help)
23:02:34.029 [error] Process #PID<0.67.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    :erlang./(5, 0)

iex> self
#PID<0.68.0>

spawn_link

What's the point? It fails!

trap_exit

iex> self
#PID<0.68.0>

iex> Process.flag(:trap_exit, true)
false

iex> spawn_link fn -> 5/0 end
#PID<0.85.0>

23:38:21.950 [error] Process #PID<0.85.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    :erlang./(5, 0)

iex> self
#PID<0.68.0>

iex> flush
{:EXIT, #PID<0.85.0>, {:badarith, [{:erlang, :/, [5, 0], []}]}}
:ok

It's too complex?

You're right!

Supervisors

Supervisors

Supervisors are specialized processes with one purpose: monitoring other processes.

Supervisors

These supervisors enable us to create fault-tolerant applications by automatically restarting child processes when they fail.

Supervisors

import Supervisor.Spec

children = [
  worker(SimpleQueue, [], [name: SimpleQueue])
]

{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)

Supervisors

  • There are currently four different restart strategies available to supervisors.

Supervisors

  • There are currently four different restart strategies available to supervisors:

  • :one_for_one - Only restart the failed child process.

Supervisors

  • There are currently four different restart strategies available to supervisors:

  • :one_for_one - Only restart the failed child process.

  • :one_for_all - Restart all child processes in the event of a failure.

Supervisors

  • There are currently four different restart strategies available to supervisors:

  • :one_for_one - Only restart the failed child process.

  • :one_for_all - Restart all child processes in the event of a failure.

  • :rest_for_one - Restart the failed process and any process started after it.

Supervisors

  • There are currently four different restart strategies available to supervisors:

  • :one_for_one - Only restart the failed child process.

  • :one_for_all - Restart all child processes in the event of a failure.

  • :rest_for_one - Restart the failed process and any process started after it.

  • :simple_one_for_one - Best for dynamically attached children. Supervisor is required to contain only one child type.

Supervision tree

Every Mix app has a supervision tree

Every Mix app has a supervision tree

children = [
  supervisor(App.Endpoint, []),
  worker(App.Repo, []),
  supervisor(App.EventStore.Supervisor, []),
]

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

It’s so easy only if you use OTP behaviours

OTP Behaviours

GenServer

GenServer

A behavior is basically a message handling framework

GenServer

OTP behaviors essentially supply:

  • a message loop
  • integration with underlying OTP support for code upgrade, tracing, system messages, etc.

GenServer

StackOverflow has some nice explanation

GenServer

Let's go step by step

GenServer init

defmodule SimpleQueue do
  use GenServer

  @doc """
  Start our queue and link it.
  """
  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc """
  GenServer.init/1 callback
  """
  def init(state), do: {:ok, state}
end

GenServer handle_call

defmodule SimpleQueue do
  use GenServer

  ### GenServer API

  @doc """
  GenServer.init/1 callback
  """
  def init(state), do: {:ok, state}

  @doc """
  GenServer.handle_call/3 callback
  """
  def handle_call(:queue, _from, state), do: {:reply, state, state}

  ### Client API / Helper methods

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def queue, do: GenServer.call(__MODULE__, :queue)
end

GenServer handle_call

handle_call(request :: term, from, state :: term) ::
  {:reply, reply, new_state} |
  {:reply, reply, new_state, timeout | :hibernate} |
  {:noreply, new_state} |
  {:noreply, new_state, timeout | :hibernate} |
  {:stop, reason, reply, new_state} |
  {:stop, reason, new_state}

GenServer handle_cast

defmodule SimpleQueue do
  use GenServer

  ### GenServer API

  @doc """
  GenServer.init/1 callback
  """
  def init(state), do: {:ok, state}

  @doc """
  GenServer.handle_cast/2 callback
  """
  def handle_cast({:enqueue, value}, state) do
    {:noreply, state ++ [value]}
  end

  ### Client API / Helper methods

  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def enqueue(value), do: GenServer.cast(__MODULE__, {:enqueue, value})
end

GenServer handle_cast

handle_cast(request :: term, state :: term) ::
  {:noreply, new_state} |
  {:noreply, new_state, timeout | :hibernate} |
  {:stop, reason :: term, new_state} when new_state: term

GenServer as a real tool

defmodule MyApp.Worker do
  use GenServer

  def start_link() do
    GenServer.start_link(__MODULE__, [])
  end

  def init([]) do
    schedule_work()
    {:ok, []}
  end

  def handle_info(:work, state) do
    state = do_work(state)
    schedule_work()
    {:noreply, state}
  end

  defp do_work(state) do
    # Do your work here and return state
  end

  defp schedule_work do
    Process.send_after(self(), :work, 60_000)
  end
end

OTP tooling is very powerful

But it requires approach change

Thanks!

Ask me something!

Intro to Elixir processes

By Alex Rozumii

Intro to Elixir processes

This is presentation for the talk at the Kyiv Elixir Meetup

  • 331
Loading comments...

More from Alex Rozumii