Envío as a reincarnation of GenEvent²
· 3 min read
Envío
is kinda
GenEvent²
, the modern
idiomatic pub-sub implementation of event passing.
In version
1.4
, Elixir core team deprecated
GenEvent
and introduced
Registry
. The reason behind
that is
GenEvent
is unable to exploit concurrency out of the box and
processes multiple handlers serially. This was done on purpose because
GenEvent
was mainly used for logging stuff and one probably wants the
log entries to appear in order as they were emitted. This is not the case for
most event applications, though.
While
José Valim suggests
using a simple approach involving a
Supervisor
with many instances of
GenServer
acting as subscribers (this is how it was done in
ExUnit
,)
this way requires a load of redundant boilerplate and it is not as transparent
for the developer as it could be.
That all forced me to introduce
Envío
package.
It is a drop-in replacement for
GenEvent
'like functionality, built on top
of
Registry
and supporting
both synchronous and asychronous subscriptions (called
:dispatch
and
pub_sub
respectively, with names gracefully stolen from
Registry
vocabulary.)
With this in mind, the main goal was to drastically simplify creation of both publishers and subscribers, hide the boilerplate and make things work out of the box with a minimal amount of additional code required.
Envío
also manages it’s own registry,
Envio.Registry
to deliberately
relieve the amount of code you need to create as a boilerplate.
Application example
One of most typical examples of how
Envío
is supposed to be used, would be
asynchronous publishing notifications to
Slack
. Here is the
whole
code
needed to provide this functionality in your application:
# lib/slack_publisher.ex
defmodule SlackPublisher do
# use SlackPublisher.broadcast(term()) anywhere to publish
use Envio.Publisher, channel: :main
end
# config/prod.exs
config :envio, :backends, %{
Envio.Slack => %{
{SlackPublisher, :main} => [
hook_url: {:system, "SLACK_ENVIO_HOOK_URL"}
]
}
}
That’s it. Once environment variable
SLACK_ENVIO_HOOK_URL
is setup to point
to any
Slack application
enabled for your
team, you are all set. Do anywhere in your code:
SlackPublisher.broadcast(%{
title: "Foo changed",
level: :info,
foo: %{bar: [baz: [42, 3.1415]]}})
And receive a notification in your slack channel.
Complicated workflows
Envío
simplifies the creation of both publishers and subscribers
hiding all the boilerplate behind
use Envio.Publisher
and
use Envio.Subscriber
. The syntax of
Envio.Publisher
is shown above in the
Slack
example; for subscriber it’s nearly the same:
defmodule MySubscriber do
use Envio.Subscriber, channels: [{MyPublisher, :featured}]
def handle_envio(message, state) do
{:noreply, state} = super(message, state)
IO.inspect({message, state}, label: "Received")
{:noreply, state}
end
end
handle_envio/2
is the only callback defined for this behaviour. Implement it to whatever
you need and all the messages sent to
:featured
channel by
MyPublisher
will now be handled.
For
:dispatch
type of the subscription, it’s even easier: just put
Envio.register(
{MySubscriber, :on_envio}, # the function of arity 2 must exist
dispatch: %Envio.Channel{source: Publisher, name: :featured}
)
anywhere in your code and start receiving messages synchronously.
Backends
Envío
introduces a concept of backends.
Slack
mentioned above would be
one of them, shipped with a package itself. Backends are supposed to implement
Envio.Backend
behaviour
currently consisting of the only function
on_envio/2
. The backend, once
specified in the configuration file, will be automatically plugged into the
internally managed
Registry
and subscribed to the channel set in config.
config :envio, :backends, %{
Envio.MyBackend => %{
{MyPublisher, :featured} => [
callback_url: {:system, "SLACK_ENVIO_HOOK_URL"},
callback_headers: %{"X-Envio-From" => "MyPublisher"}
]
}
}
Envio.MyBackend
is expected to exist and impelement
Envio.Backend
.
The options from the config file will be passed alongside message/payload
as a second parameter. For instance, the
Slack
implementation does literally
the following:
@impl true
def on_envio(message, meta) do
case meta do
%{hook_url: hook_url} ->
HTTPoison.post(
hook_url,
format(message),
[{"Content-Type", "application/json"}]
)
_ -> {:error, :no_hook_url_in_envio}
end
end
That’s it. ¡Envíos feliz!