Rodney Norris

Pub/sub with gproc vs GenEvent in elixir

The library gproc was recently recommended to me for registering processes in my elixir app. While I was researching the library I came across this in the readme:

An interesting application of gproc is building publish/subscribe patterns.

Example:

subscribe(EventType) ->
     %% Gproc notation: {p, l, Name} means {(p)roperty, (l)ocal, Name}
     gproc:reg({p, l, {?MODULE, EventType}}).

 notify(EventType, Msg) ->
     Key = {?MODULE, EventType},
     gproc:send({p, l, Key}, {self(), Key, Msg}).>

While doing some further research I came across a blog post with an elixir example. These peaked my interest and got me thinking about using gproc for pub/sub in a project, instead of GenEvent.

Below is an example of a bare-bones implementation using GenEvent similar to how I’ve been using it in projects. Then a comparable example using gproc.

Example GenEvent usage

# supervisor.ex
defmodule EventSup do
  use Supervisor

  def start_link, do: Supervisor.start_link(__MODULE__, [], [])

  def init(_) do
    supervise(
      [
        worker(GenEvent, [[name: :event_manager]], [id: :event_manager]),
        worker(EventMonitor, [:event_manager])
      ],
      [strategy: :one_for_one]
    )
  end
end
# event_monitor.ex
defmodule EventMonitor do
  use GenServer

  def start_link(mgr), do: GenServer.start(__MODULE__, [mgr], [])

  def init(mgr) do
    :ok = add_handler(mgr)
    {:ok, mgr}
  end

  def handle_info({:gen_event_EXIT, _handler, _reason}, mgr) do
    :ok = add_handler(mgr)
    {:noreply, mgr}
  end

  def add_handler(mgr) do
    GenEvent.add_mon_handler(mgr, EventHandler, [])
  end
end

# event_handler.ex
defmodule EventHandler do
  use GenEvent
  require Logger

  def init(_), do: {:ok, {}}

  def handle_event(event, state) do
    Logger.info "received event #{inspect event}"

    {:ok, state}
  end
end

# send event
iex>GenEvent.notify(:event_manager, {:event, "stuff"})

Example gproc pub/sub equivalent

# supervisor.ex
defmodule EventSup do
  use Supervisor

  def start_link, do: Supervisor.start_link(__MODULE__, [], [])

  def init(_) do
    supervise(
      [
        worker(EventHandler, [:event_manager])
      ],
      [strategy: :one_for_one]
    )
  end
end

# event_handler.ex
defmodule EventHandler do
  use GenServer
  require Logger

  def start_link(topic), do: GenServer.start_link(__MODULE__, topic, [])

  def init(topic) do
    :gproc.reg({:p, :l, topic})

    {:ok, topic}
  end

  def handle_info(msg, topic) do
    Logger.info "received message #{inspect msg}"

    {:noreply, topic}
  end
end

# send message
iex>:gproc.send({:p, :l, :event_manager}, {:message, "stuff"})

Between these two I’m favoring gproc. Only needing to add one GenServer to my supervision tree to receive events from another module or OTP application is really nice. The ability to register more than just atoms as property names is good too. I’m not crazy about the api, but I could easily wrap that if I’m going to be using throughout a large project.

I’m not totally sold on gproc just yet though. I still have some other pub/sub solutions to research. I found elixir_pubsub and erlbus. Which could both fit the same use case.

There is also the Phoenix.PubSub now that its being broken out into a separate module. This is the option I’m most interested in as it matures with the next version of Phoenix.