Elixir client for FireMQ, a RabbitMQ-compatible message broker. It speaks the native FireMQ protocol over TCP or TLS and is written in pure Elixir.
Defaults favour safety: queues and exchanges are durable, messages are persistent, and
publish returns only after the broker confirms the message.
Install
Add :firemq to your dependencies in mix.exs. It needs Elixir 1.15 or later.
def deps do
[
{:firemq, "~> 0.1"}
]
endQuickstart
Start a broker, for example with firemq --dev, then:
{:ok, conn} = FireMQ.connect("fire://guest:guest@localhost:5673")
{:ok, _info} = FireMQ.declare_queue(conn, "jobs")
# Returns once the broker has taken responsibility for the message.
:ok = FireMQ.publish(conn, "", "jobs", "hello")
{:ok, sub} = FireMQ.subscribe(conn, "jobs")
receive do
{:firemq, ^sub, message} ->
IO.puts(message.body)
:ok = FireMQ.ack(message)
end
:ok = FireMQ.close(conn)The examples/ directory has the classic tutorials: hello_world, work_queues,
publish_subscribe, routing, topics and rpc. Run one with
FIREMQ_URL=fire://localhost mix run examples/topics.exs.
Connect
{:ok, conn} = FireMQ.connect("fire://user:password@host:5673/vhost", heartbeat: 30)The URL schemes are fire:// (TCP, port 5673 by default) and fires:// (TLS, port
5674). User and password default to guest. The vhost is the URL path without its
leading /, percent-decoded; an empty path means vhost /.
Options:
| Option | Default | Meaning |
|---|---|---|
:heartbeat | 30 | Heartbeat interval in seconds. 0 disables heartbeats. |
:connection_name | none | Name shown in the management UI. |
:connect_timeout | 10_000 | Milliseconds to wait for the TCP connection and the handshake. |
:ssl | [] | Options for :ssl.connect/3, merged over the defaults. |
For fires://, the client verifies the broker's certificate against the system CA store.
Pass your own CA with ssl: [cacertfile: "ca.pem"]. For local development only, you can
skip verification with ssl: [verify: :verify_none].
A connection opened with FireMQ.connect/2 belongs to the calling process: it closes
when that process exits. To keep a connection running independently, put it under a
supervisor (see Supervision).
API
Every function takes the connection first and returns :ok, {:ok, value} or
{:error, %FireMQ.Error{}}. Each one also has a bang variant, such as publish!/5, that
returns the value or raises FireMQ.Error. Functions that wait for the broker accept a
:timeout option in milliseconds, 30_000 by default.
Topology
{:ok, %FireMQ.QueueInfo{name: "jobs", messages: 0, consumers: 0}} =
FireMQ.declare_queue(conn, "jobs", args: %{"x-max-length" => 10_000})
# An empty name lets the broker pick one. A temporary queue is exclusive to this
# connection and deleted when it closes.
{:ok, %FireMQ.QueueInfo{name: reply_queue}} = FireMQ.declare_queue(conn, "", temporary: true)
:ok = FireMQ.declare_exchange(conn, "orders", :topic)
:ok = FireMQ.bind_queue(conn, "jobs", "orders", "order.*")
:ok = FireMQ.unbind_queue(conn, "jobs", "orders", "order.*")
:ok = FireMQ.bind_exchange(conn, "audit", "orders", "#")
{:ok, purged} = FireMQ.purge_queue(conn, "jobs")
{:ok, deleted} = FireMQ.delete_queue(conn, "jobs", if_empty: true)
:ok = FireMQ.delete_exchange(conn, "orders")declare_queue/3 options: :durable (default true), :exclusive, :auto_delete,
:passive, :temporary and :args. declare_exchange/4 options: :durable (default
true), :auto_delete, :internal, :passive and :args. Arguments use RabbitMQ's
names, such as x-message-ttl, x-dead-letter-exchange or x-overflow.
Publish
:ok =
FireMQ.publish(conn, "orders", "order.created", ~s({"id":42}),
mandatory: true,
properties: [content_type: "application/json", message_id: "o-42"],
headers: %{"trace" => "abc", "attempt" => 1}
)Use exchange "" to send straight to the queue named by the routing key.
| Option | Default | Meaning |
|---|---|---|
:persistent | true | Write the message to disk. |
:mandatory | false | Fail with kind :unroutable when no queue receives the message. |
:confirm | true | Wait for the broker's confirm. With false, return once the message is written to the socket. |
:properties | [] | content_type, content_encoding, correlation_id, reply_to, expiration (ms), message_id, timestamp (Unix seconds or a DateTime), type, user_id, app_id, priority. |
:headers | %{} | Application headers: string keys, any MessagePack value. |
Consume
subscribe/3 delivers messages to the calling process as
{:firemq, sub_ref, %FireMQ.Message{}}:
{:ok, sub} = FireMQ.subscribe(conn, "jobs", prefetch: 10)
receive do
{:firemq, ^sub, %FireMQ.Message{body: body} = message} ->
case process(body) do
:ok -> FireMQ.ack(message)
:retry -> FireMQ.nack(message, true)
:fail -> FireMQ.nack(message, false)
end
{:firemq_cancelled, ^sub, %FireMQ.Error{} = reason} ->
# The broker ended the subscription (for example, the queue was deleted)
# or the connection closed.
{:stopped, reason}
end
:ok = FireMQ.unsubscribe(conn, sub)A message has body, exchange, routing_key, properties (a map with atom keys),
headers (a map with string keys), redelivered and delivery_tag.
The broker sends at most :prefetch unacknowledged messages (100 by default). The client
grants more credit as you settle messages, half the window at a time. With
no_ack: true, messages count as acknowledged when sent and the client grants credit as
they arrive.
nack(message, true) requeues the message, which comes back with redelivered: true.
nack(message, false) dead-letters it when the queue has a dead-letter exchange, and
drops it otherwise.
When the subscribing process exits, the client cancels the subscription and returns the messages that process had not settled to the queue.
To take a single message, use get/3. It returns {:ok, nil} when the queue is empty:
{:ok, message} = FireMQ.get(conn, "jobs")
if message, do: FireMQ.ack(message)Supervision
FireMQ.Connection is a GenServer with a child_spec/1, so you can start it under a
supervisor and reach it by name:
children = [
{FireMQ.Connection, url: "fire://guest:guest@localhost:5673", name: MyApp.FireMQ}
]
Supervisor.start_link(children, strategy: :one_for_one)
FireMQ.publish(MyApp.FireMQ, "", "jobs", "hello")FireMQ.Connection.start_link/1 takes the same options as FireMQ.connect/2, plus
:url and :name. If the broker cannot be reached or refuses the login, start_link/1
returns {:error, %FireMQ.Error{}}.
When the broker closes the connection or the socket fails, the connection process exits
with {:shutdown, %FireMQ.Error{}} and the supervisor starts a new one. The client does
not reconnect on its own, and subscriptions do not survive a restart: subscribers receive
{:firemq_cancelled, sub_ref, error} and must subscribe again.
Errors
Failures return {:error, %FireMQ.Error{kind: kind, code: code, message: message}}.
kind | When | code |
|---|---|---|
:server | The broker refused a request. | AMQP reply code, such as 403, 404, 405 or 406. |
:unroutable | A mandatory publish reached no queue. | 312 |
:nacked | The broker rejected a publish, for example because a reject-publish queue is full. | The broker's code, usually 406. |
:closed | The connection is closed or could not be opened. | The broker's close code, or nil. |
:timeout | The broker did not answer in time. | nil |
:protocol | The broker sent something the client cannot read, or a frame would exceed the broker's size limit. | nil |
:url | The connection URL is invalid or uses an unsupported scheme. | nil |
case FireMQ.publish(conn, "orders", "order.unknown", "x", mandatory: true) do
:ok -> :ok
{:error, %FireMQ.Error{kind: :unroutable}} -> Logger.warning("nobody is listening")
{:error, %FireMQ.Error{} = error} -> raise error
endA server error on one request, such as a 406 when you redeclare a queue with other
properties, does not close the connection.
Development
The unit tests need no broker:
mix test
The integration tests and examples need a running broker:
firemq --ephemeral &
FIREMQ_URL=fire://guest:guest@localhost:5673 mix test --only integration
The conformance driver in conformance/driver.exs runs the FireMQ SDK conformance suite.
Compile first, so that Mix prints nothing on the driver's standard output:
mix compile
firemq-conformance run --driver "mix run conformance/driver.exs"
License
Apache-2.0. See LICENSE.