Diving deeper into OTP: Agents, Tasks, GenEvent, ETS

Alex Rozumii
Elixir Club Kharkiv, 2017

Intro to history

Plan for today

  • Task
  • GenEvent
  • Agent
  • ETS

Let's start

What if...

I need to make bunch of external requests

What if...

I need to make a long computation

Task

Task

defmodule Example do
  def double(x) do
    :timer.sleep(2000)
    x * 2
  end
end

iex> task = Task.async(Example, :double, [2000])
%Task{pid: #PID<0.111.0>, ref: #Reference<0.0.8.200>}

iex> Task.await(task)
4000

Alternatives?

spawn

spawn

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>

What if...

I want dynamically added handlers

GenEvent / GenStage

GenEvent

GenEvent

# Start a new event manager.
{:ok, pid} = GenEvent.start_link([])

GenEvent

# Define an Event Handler
defmodule LoggerHandler do
  use GenEvent

  # Callbacks

  def handle_event({:log, x}, messages) do
    {:ok, [x | messages]}
  end

  def handle_call(:messages, messages) do
    {:ok, Enum.reverse(messages), []}
  end
end

# Start a new event manager.
{:ok, pid} = GenEvent.start_link([])

# Attach an event handler to the event manager.
GenEvent.add_handler(pid, LoggerHandler, [])
#=> :ok

GenEvent

# Attach an event handler to the event manager.
GenEvent.add_handler(pid, LoggerHandler, [])
#=> :ok

# Send some events to the event manager.
GenEvent.notify(pid, {:log, 1})
#=> :ok

GenEvent.notify(pid, {:log, 2})
#=> :ok

# Call functions on specific handlers in the manager.
GenEvent.call(pid, LoggerHandler, :messages)
#=> [1, 2]

GenEvent.call(pid, LoggerHandler, :messages)
#=> []

Gotcha - supervision

defmodule LoggerHandlerWatcher do
  @doc """
    inits the GenServer by starting a new handler
  """
  def init(logger_pid) do
    start_handler(logger_pid)
  end

  @doc """
    handles EXIT messages from the GenEvent handler and restarts it
  """
  def handle_info({:gen_event_EXIT, _handler, _reason}, logger_pid) do
    {:ok, logger_pid} = start_handler(logger_pid)
    {:noreply, logger_pid}
  end

  @doc """
    starts a new handler listening for events on `logger_pid`
  """
  defp start_handler(logger_pid) do
    case GenEvent.add_mon_handler(logger_pid, LoggerHandler, []) do
     :ok ->
       {:ok, logger_pid}
     {:error, reason}  ->
       {:stop, reason}
    end
  end
end

GenStage? See next talk ;)

What if...

I have a state which needs to be persisted

What if...

I need a global storage

What if...

I need to store mutable data

GenServer

With some access methods!

GenServer

Why bother? We already have such functionality

Agent

Agent

iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
{:ok, #PID<0.65.0>}

iex> Agent.update(agent, fn (state) -> state ++ [4, 5] end)
:ok

iex> Agent.get(agent, &(&1))
[1, 2, 3, 4, 5]

Agent

iex> self
#PID<0.115.0>

iex> {:ok, agent} = Agent.start_link(fn -> [] end)
{:ok, #PID<0.123.0>}

iex> Agent.get(agent, fn(_) -> IO.inspect self end)
#PID<0.123.0>

iex> self
#PID<0.115.0>

Agent

Operations supported:

  • get
  • get_and_update
  • update
  • cast

Agent

# They can be named!

iex> Agent.start_link(fn -> [1, 2, 3] end, name: Numbers)
{:ok, #PID<0.74.0>}

iex> Agent.get(Numbers, &(&1))
[1, 2, 3]

What if...

you want to do complex queries?

What if...

you want to do store even more data (50 mb, 1 gb)?

What if...

you want to persist data?

What if...

you want to have concurrent access?

What if...

Agent is not enough?

ETS / DETS

ETS

O(log n) or O(1) access times!

ETS

iex> table = :ets.new(:meetups, [:set, :protected])
8212

iex> :ets.new(:meetups, [:set, :protected, :named_table])
:user_lookup

iex> :ets.insert(:meetups, {"Kyiv Elixir", "awesome", ["Elixir"]})
true

iex> :ets.lookup(:meetups, "Kyiv Elixir")
{"Kyiv Elixir", "awesome", ["Elixir"]}

iex> :ets.match(:meetups, {:"$1", "awesome", :"_"})
[["Kyiv Elixir"]]

iex> :ets.delete(:meetups, "Kyiv Elixir")
true

iex> :ets.delete(:meetups)
true

ETS

  • set — (default) One value per key. Keys are unique.
  • ordered_set — Similar to set but ordered by Erlang/Elixir term.
  • bag — Many objects per key but only one instance of each object per key.
  • duplicate_bag — Many objects per key, with duplicates allowed.

Even ETS is not enough?

Mnesia

real-time distributed database management system

Now you have even more tools

Now you have even more tools to make your app needlessly complex

Now you have even more tools to make your app needlessly complex

Thanks!

Ask some questions

Alex Rozumii

@brain-geek
FB: arozumiy

Diving deeper into OTP

By Alex Rozumii

Diving deeper into OTP

  • 311
Loading comments...

More from Alex Rozumii