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
- 1,603