Getting started
From an empty Rails application to a message published in a request and consumed in a process of its own.
Install
Neither gem is on rubygems.org before 1.0. Both come from AceMQ's own static feed, which is a directory tree over HTTPS and needs no account and no credential.
# Gemfile
source "https://acemq.org/gems" do
gem "acemq-amqp", "~> 0.7.14"
gem "acemq-amqp-rails", "~> 0.2"
end
# The transport. `acemq-amqp` declares no runtime dependencies at all — reading
# an AceMQ envelope should not drag a broker client into a process that will
# never open a socket — so the application that does open sockets names it.
gem "bunny", "~> 2.23"bundle install~> 0.7.14 on the library is three components on
purpose. ~> 0.7 would be pessimistic on the
major — >= 0.7, < 1.0 — and would quietly
admit a 0.8 that changed something this gem is written against;
~> 0.7.14 is >= 0.7.14, < 0.8.0, so
patch releases arrive on their own and the next minor is looked at
before it ships here. bundle install resolves the newest
0.7.x the feed carries. The floor is 0.7.14: 0.7.11 is where stream
readers resume after a reconnect and unroutable outbox records are kept
rather than lost, 0.7.12 is where a retry hop the broker refuses
requeues the message instead of leaving it unacknowledged, 0.7.13 is
where a reply a requester cannot decode fails the call at once and a
requester stops leaving queues on the broker after it closes, and 0.7.14
is where a URL ending in a slash connects to the default vhost and every
publish is timed as acemq.publish.duration and tagged with
its routing key and type.
Ruby 3.1 and later; Rails 7.1, 7.2 and 8.x. Rails 8 needs Ruby 3.2, which is Rails' constraint rather than one this gem adds.
Configure
# config/acemq.yml
shared:
client_name: shop
consumer:
prefetch: 20
concurrency: 4
max_attempts: 5
initial_delay: 1
max_delay: 60
topology:
exchanges:
- { name: shop-events, type: topic }
queues:
- { name: shop.orders, dead_letter: true, retries: { max_attempts: 5, initial_delay: 1, max_delay: 60 } }
bindings:
- { queue: shop.orders, exchange: shop-events, routing_key: "order.#" }
development:
url: amqp://guest:guest@localhost:5672
test:
url: amqp://guest:guest@localhost:5672
production:
url: <%= ENV["ACEMQ_URL"] %>
shutdown_timeout: 20This is Rails' own config_for, so shared:,
per-environment sections and ERB all work exactly as they do in
database.yml. Anything that cannot be written in YAML — a
codec object, an interceptor, a telemetry reporter, an
AceMQ::AMQP::Security — goes in config.acemq.*
in an environment file, which is applied over the file. See configuration.md.
For production that usually means a TLS URL and credentials out of the environment; security.md is the page.
Declare the broker's shape
bin/rails acemq:topologyPrints the plan and applies it. Idempotent, so it belongs in a deploy
step beside db:migrate. The consumer process does it on
boot as well, which covers a development machine;
declare_topology_on_boot: false turns that off for an
estate where a deployment tool owns the broker.
Publish
class OrdersController < ApplicationController
def create
order = Order.create!(order_params)
AceMQ::Rails.publish(order.as_json, to: "order.placed",
exchange: "shop-events", type: "order.placed.v2")
head :created
end
endNo connection is built here and none is closed. The one the Railtie set up is reached by name, and it is opened on first use — so a broker that is down does not stop the pages that never touch it from serving. See publishing.md.
Consume
# app/consumers/orders_consumer.rb
class OrdersConsumer < AceMQ::Rails::Consumer
queue "shop.orders"
concurrency 4
def call(message)
Fulfilment.begin!(message.payload)
accept
rescue Warehouse::Unavailable => e
retry_later(e.message)
end
endapp/consumers needs no configuration: Rails autoloads
every direct subdirectory of app/.
Run the consumers
bundle exec acemq-consumerOr bin/rails acemq:consume. Not inside
Puma — consumers.md says why, and
the executable is the one that turns the development autoloader off,
which reloading.md explains is not
optional.
In a Procfile:
web: bin/rails server
mq: bundle exec acemq-consumer
In Kubernetes, a second Deployment with the same image and a different command, scaled by queue depth rather than by request rate — which is half the point of keeping them apart.
Check it is working
bin/rails acemq:health # what a readiness probe would see
bin/rails acemq:consumers # every consumer and the queue it readsAnd in the application, composed into whatever endpoint already exists:
report = AceMQ::AMQP::Health.aggregate(AceMQ::Rails::Health.check, DatabaseCheck.new)
render json: report.to_h, status: report.down? ? 503 : 200health.md has the rest, including why a blocked connection is reported healthy.
Then
Nothing above is instrumented, encrypted or idempotent, and that is where most applications go next:
| A TLS broker, credentials, encrypted payloads | security.md |
| Metrics, tracing and what is worth alerting on | observability.md |
| An event that must not be published without its row | outbox.md |
| A handler that will be called twice | idempotency.md |
| Everything else the library ships | patterns.md |