Packages

MQTT adapter for Conduit.

Current section

Files

Jump to
conduit_mqtt lib conduit_mqtt.ex
Raw

lib/conduit_mqtt.ex

defmodule ConduitMQTT do
@moduledoc """
MQTT adapter for Conduit.
* `connection_opts` - Connection opts for the adapter. For example: `[server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883}]`
For the full set of connection_opts, see the docs for underlying library [Tortiose](https://hexdocs.pm/tortoise/connecting_to_a_mqtt_broker.html#connection-handler).
`handler` and `client_id` are set by ConduitMQTT so don't set these yourself.
"""
use Conduit.Adapter
use Supervisor
require Logger
alias Conduit.Util
alias ConduitMQTT.Meta
@type broker :: module
@type client_id :: String.t()
@pool_size 5
defmodule NeedsWrappingError do
@moduledoc """
Exception raised when a message is published with attributes or headers that need wrapping.
"""
defexception [:message]
end
def child_spec([broker, _, _, _] = args) do
%{
id: name(broker),
start: {__MODULE__, :start_link, args},
type: :supervisor
}
end
def start_link(broker, topology, subscribers, opts) do
Meta.create(broker, @pool_size, Enum.count(Map.keys(subscribers)))
Supervisor.start_link(__MODULE__, [broker, topology, subscribers, opts], name: name(broker))
end
def init([broker, topology, subscribers, opts]) do
if !Enum.empty?(topology) do
Logger.warn("Topology should be empty list for MQTT Adapter")
end
Logger.info("MQTT Adapter started!")
children = [
{ConduitMQTT.SubPool, [broker, subscribers, opts]},
{ConduitMQTT.ConnPool, [broker, opts]}
]
Supervisor.init(children, strategy: :one_for_one)
end
def name(broker) do
Module.concat(broker, Adapter)
end
# TODO: Remove when conduit goes to 1.0
# Conduit will never call this if publish/4 is defined
def publish(message, _config, _opts) do
{:ok, message}
end
def publish(broker, message, config, opts) do
if !Keyword.get(config, :ignore_needs_wrapping, false) do
error_if_needs_wrap(message)
end
with_client_id(broker, fn client_id ->
Tortoise.publish_sync(client_id, message.destination, message.body, opts)
end)
end
@spec with_client_id(broker, (client_id -> term)) :: {:error, term} | {:ok, term} | term
def with_client_id(broker, fun) when is_function(fun, 1) do
with {:ok, client_id} <- get_client_id(broker, @pool_size) do
fun.(client_id)
end
end
@doc false
defp get_client_id(broker, retries) do
pool = ConduitMQTT.ConnPool.name(broker)
Util.retry([attempts: retries], fn ->
:poolboy.transaction(pool, &ConduitMQTT.Conn.get_client_id(&1))
end)
end
defp error_if_needs_wrap(message) do
if !Conduit.Message.get_private(message, :wrapped) && (has_attributes(message) || has_headers(message)) do
raise NeedsWrappingError, """
Message headers and attributes are not supported natively in MQTT. Conduit provides two plugs
for wrapping/unwrapping. See docs for more info:
https://hexdocs.pm/conduit/Conduit.Plug.Wrap.html
https://hexdocs.pm/conduit/Conduit.Plug.Unwrap.html
If you don't mind losing the headers and attributes in transit you can enable
`ignore_needs_wrapping: true` on the adapter opts in your config.
You can also stop using the message headers and attributes to stop receiving this error.
"""
end
end
@attributes ~w(content_encoding content_type correlation_id created_at created_by message_id user_id)a
defp has_attributes(message) do
Enum.any?(
@attributes,
fn attribute ->
Map.get(message, attribute) != nil
end
)
end
defp has_headers(message) do
message.headers != %{}
end
end