A simple Amqp library based on official elixir amqp client Written to prevent duplicated and boilerplate code to handle all the lifecycle of the amqp connection. Write your publisher or consumer and forget about the rest!
def deps do
[
{:amqpx, "~> 6.0.2"}
]
end
From 3.0.0 Amqpx is no longer an application. This is so the client can choose
in which environment or configuration to have consumers up and running. You
would then need to start your consumers and producer in the client's supervision
tree, instead of adding Amqpx to the extra_application
list as it was in the
past.
To start all consumers and producer inside your application, using the library helper function:
defmodule Application do
alias Amqpx.Helper
import Supervisor.Spec, warn: false
def start(_type, _args) do
children =
Enum.concat(
[
Helper.manager_supervisor_configuration(
Application.get_env(:myapp, :amqp_connection)
),
Helper.producer_supervisor_configuration(
Application.get_env(:myapp, :producer)
)
],
Helper.consumers_supervisor_configuration(
Application.get_env(:myapp, :consumers)
)
)
opts = [strategy: :one_for_one, name: Supervisor, max_restarts: 5] # set this accordingly with your consumers count, ex: max_restarts: n_consumer + 5
Supervisor.start_link(children, opts)
end
end
Start consumers and producer manually:
Amqpx.Gen.ConnectionManager.start_link(%{connection_params: Application.get_env(:myapp, :amqp_connection)})
Amqpx.Gen.Producer.start_link(Application.get_env(:myapp, :producer))
Enum.each(Application.get_env(:myapp, :consumers), &Amqpx.Gen.Consumer.start_link(&1))
config :myapp,
amqp_connection: [
username: "amqpx",
password: "amqpx",
host: "rabbit",
port: 5_000,
virtual_host: "amqpx",
heartbeat: 30,
connection_timeout: 10_000,
obfuscate_password: false, # default is true
]
Default parameters:
- prefetch_count: 50
- backoff: 5_000 (connection retry)
WARNING: headers exchange binding not supported by library helpers functions
config :myapp,
consumers: [
%{
handler_module: Myapp.Consumer,
prefetch_count: 100,
backoff: 10_000
}
]
config :myapp, Myapp.Consumer, %{
queue: "my_queue",
exchanges: [
%{name: "amq.topic", type: :topic, routing_keys: ["my.routing_key1","my.routing_key2"], opts: [durable: true]},
%{name: "my_exchange", type: :direct, routing_keys: ["my_queue"], opts: [durable: true]},
%{name: "my_exchange_fanout", type: :fanout, opts: [durable: true]}
],
opts: [
durable: true,
arguments: [
{"x-dead-letter-routing-key", :longstr, "my_queue_errored"},
{"x-dead-letter-exchange", :longstr, ""}
]
]
}
Default parameters:
- publish_timeout: 1_000
- backoff: 5_000 (connection retry)
- exchanges: []
- publish_retry_options: [ max_retries: 0, retry_policy: [], backoff: [ base_ms: 10, max_ms: 10_000 ] ]
You can also declare exchanges from the producer module, simply specify them in the configuration. There is an example below.
config :myapp, :producer, %{
publisher_confirms: false,
publish_timeout: 0,
exchanges: [
%{name: "my_exchange", type: :direct, opts: [durable: true]}
]
}
max_retries
: number of times apublish
will be retried. Apublish
can be executed at most (max_retries
+ 1) timesretry_policy
: collection of error conditions which will cause thepublish
to be retried. Can be a combination of the following atoms::on_publish_rejected
(when the broker itself rejects):on_confirm_timeout
(when the confirm from the broker times out):on_publish_error
(when there is an error returned at AMQP protocol level)
backoff
: sleep time betweenpublish
retries. Calculated asrandom_between(0, min(cap, base * 2 ** attempt))
base_ms
: time in millisecond that is used asbase
term in the formula abovemax_ms
: time in millisecond that is used ascap
term in the formula above
defmodule Myapp.Consumer do
@moduledoc nil
@behaviour Amqpx.Gen.Consumer
alias Amqpx.Basic
alias Amqpx.Helper
@config Application.get_env(:myapp, __MODULE__)
@queue Application.get_env(:myapp, __MODULE__)[:queue]
def setup(channel) do
# here you can declare your queues and exchanges
Helper.declare(channel, @config)
Basic.consume(channel, @queue, self()) # Don't forget to start consuming here!
{:ok, %{}}
end
def handle_message(payload, meta, state) do
IO.inspect("payload: #{inspect(payload)}, metadata: #{inspect(meta)}")
{:ok, state}
end
end
defmodule Myapp.Producer do
@moduledoc nil
alias Amqpx.Gen.Producer
def send_payload(payload) do
Producer.publish("my_exchange", "my_exchange_routing_key", payload)
end
end
You can define an implement an optional callback inside your Consumer
module
that will be called whenever an error is raised in the handle_message
callback
and the redelivered
flag is set to true
. This callback can be useful
whenever you want to define a standard rejection logic (e.g. datadog alarms and
such).
defmodule Myapp.HandleRejectionConsumer do
@moduledoc nil
@behaviour Amqpx.Gen.Consumer
alias Amqpx.Basic
alias Amqpx.Helper
@config Application.get_env(:myapp, __MODULE__)
@queue Application.get_env(:myapp, __MODULE__)[:queue]
def setup(channel) do
# here you can declare your queues and exchanges
Helper.declare(channel, @config)
Basic.consume(channel, @queue, self()) # Don't forget to start consuming here!
{:ok, %{}}
end
def handle_message(payload, meta, state) do
IO.inspect("payload: #{inspect(payload)}, metadata: #{inspect(meta)}")
# something that could fail
{:ok, state}
end
def handle_message_rejection(error) do
# will be invoked whenever handle_message fails
# do something like error logging
{:ok}
end
end
In order to run the test suite, you need to startup the docker compose and jump into it with:
docker compose run --service-ports console bash
and run the test suite with:
mix test