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!