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!