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

Alex Rozumii, Toptal

Part of the "Getting started with Elixir" series

Intro to history

In the previous part

  • Elixir processes
  • GenServer
  • Supervisor

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]

Alternatives to store state?

I have some!

Process dictionary

Process dictionary

iex> Process.get_keys
[:iex_history]

iex> Process.put(:app_custom_var, 321)
nil

iex> Process.get :app_custom_var
321

iex> Process.get_keys
[:app_custom_var, :iex_history]

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

Summary

Task

Storage options

  • Process dictionary
  • GenServer
  • Agent
  • ETS
  • Mnesia
  • External services

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!

Please ask some questions :)

Alex Rozumii

@brain-geek

Diving deeper into OTP

By Alex Rozumii

Diving deeper into OTP

  • 387
Loading comments...

More from Alex Rozumii