AceMQ for Ruby

A Ruby client for AceMQ messaging over AMQP, speaking the same wire contract as the Java, Go, .NET and Python libraries: the same reserved headers, the same defaults, the same retry arithmetic. A Ruby consumer reads what a Java producer writes.

That claim is pinned rather than asserted. The fixtures under spec/fixtures/ were generated by the Java implementation and are shared with Go, .NET and Python: envelope-fixtures.json pins the headers, and contract-fixtures.json pins the retry schedule, the queue names and the topology. Specs read both and hold this library to them. A port that hand-copies a contract out of documentation acquires a difference nobody notices until two languages disagree in production — see how the five libraries are kept in step, including the places they do not yet agree.

A message, end to end

require "acemq/amqp"

include AceMQ::AMQP

mq = Connection.open("amqp://guest:guest@localhost:5672",
                     "checkout@pod-7",
                     RetryPolicy.exponential(5, 1, 60))

Topology.new
        .exchange("orders-events", )
        .queue("orders.new", true)
        .binding("orders.new", "orders-events", "order.#")
        .apply(mq)

mq.publish({ "order_id" => "A-1" }, "order.placed",
           "orders-events", "order.placed.v2")

mq.consume("orders.new") do |message|
  message.payload          # => { "order_id" => "A-1" }
  message.envelope.attempt # => which delivery this is
  warehouse.reserve(message.payload) ? Ack.accept : Ack.retry("the warehouse said no")
end

Getting started takes this apart line by line, and the tutorials build a working service out of it in four steps.

What it does for you

An envelope on every message. Identity, type, version, correlation, causation, attempt count, origin and the time it was first published — the same fields Java, Go, .NET and Python write, so a message crossing between them keeps its history. Application headers are kept apart from those, and a reserved name in your own headers raises rather than being dropped. See the envelope.

Retries that stop, and wait in the right place. A handler returns Ack.retry, and a policy decides whether there is another attempt and how long to wait. A short wait happens in the consumer; a long one happens in the broker, in a rung queue whose time-to-live is the delay — because a consumer that sleeps through a five-minute backoff loses the whole wait when it restarts. See retries, dead letters and shutdown.

Nothing to install to read a message. The gem has no runtime dependencies. bunny is required lazily, at the moment a connection is opened, and named in the error if it is not there, so a process that only builds envelopes or checks a retry schedule needs no broker client at all. See testing without a broker.

Security you have to opt out of. An amqps:// URL is encrypted and verified, which is not what the underlying driver does on its own — see security, which explains the bunny behaviour this library exists to correct. Credentials are a separate object so the password never goes in the URL, and they render redacted through inspect, to_s and %p alike.

A seam for the things no library can guess. A tenant, a trace context, a log scope, a size limit. Interceptors wrap publishing and handling without anybody editing the engine.

Queues declared the way the other libraries declare them. A durable, named queue is a quorum queue, because x-queue-type is part of a queue's identity to the broker and the second service to declare orders differently cannot consume at all. See queue types.

Numbers, traces and an honest readiness answer. Metric names shared with the other four libraries, an in-memory registry with a Prometheus renderer, an OpenTelemetry adapter that joins a handler's span to the publish that caused it across processes and minutes, and a health report that calls a stopped consumer under a live connection degraded rather than down, and a broker applying back pressure up, with the reason rather than down. See metrics, tracing and health.

A body the broker cannot read. EncryptedCodec wraps whichever codec you already use and encrypts what it produced with AES-GCM, with a keyring so a key can be rotated while messages written with the old one are still queued. See encrypting the body.

Development certificates that cannot reach production. One command writes the authority and certificates a local TLS broker needs, and every one of them is stamped so that this library refuses it however trust is configured — unverified mode included. See development certificates.

The things everybody writes anyway, written once. Idempotency, the outbox, request and reply, replay, ordering, consumer groups, routing slips, pipelines, schemas and streams. Required separately, because none of it is protocol. See patterns.

What is identical, and what is not

Identical, because a message crosses languages: the reserved header names and their types, the defaults applied when they are absent, the retry schedule arithmetic, the {queue}.dlq / {queue}.parked / {queue}.retry.{delay} naming, and the rules for giving up.

Not identical, deliberately: the API shape. Go gets ctx, .NET gets IAsyncEnumerable, Python gets dataclasses, and Ruby gets keyword arguments, frozen value objects, blocks and ? predicates. Forcing a Java shape onto Ruby produces a library nobody enjoys using. The contract is portable; the ergonomics are native.

Concretely, that means: Envelope and Credentials are frozen after construction and changed with with; a handler is a block that returns an Ack; every optional argument is a keyword argument; and a pattern that wraps a handler hands back a block you pass straight to consume.

The layout

Everything lives under AceMQ::AMQP, in one gem, with the patterns behind a second require:

acemq/amqp envelopes, headers, codecs, retry policy, topology, the connection and the transport
acemq/amqp/patterns idempotency, outbox, request-reply, replay, ordering, consumer groups, routing slips, sagas, scheduling, pipelines, schemas, streams
require "acemq/amqp"
require "acemq/amqp/patterns"   # only if you use them

include AceMQ::AMQP at the top of a file is how the examples here are written, because Connection, Topology, Ack and RetryPolicy read better than the fully qualified names at every call site. Nothing depends on it — qualify them if you would rather.

Installing

# Gemfile
source "https://acemq.org/gems" do
  gem "acemq-amqp", "~> 0.7", ">= 0.7.14"
end
gem "bunny", ">= 2.23", "< 4"   # for the transport

Ruby 3.1 or newer. RabbitMQ for the broker.

The gem itself declares no runtime dependencies, and three of the nine codecs are the only other things that ever ask for one. Each requires its gem lazily and names it when it is missing:

gem "rexml", "~> 3.3"             # XMLCodec. Ships with Ruby, but is a bundled
                                  # gem since 3.4, so Bundler needs it named.
gem "google-protobuf", "~> 4.29"  # ProtobufCodec
gem "avro", "~> 1.12"             # AvroCodec
gem "opentelemetry-api", "~> 1.8" # Telemetry::OpenTelemetry

The gem is not on rubygems.org. Releases go to AceMQ's own feed at https://acemq.org/gems, which is what the source block above names; the current one is 0.7.14. The releasing section of the README says why.

Where to go next

Start at one of two doors. Getting started explains the first message line by line; the tutorials are four sittings that end with a service that publishes, retries, deduplicates and says so.

Then the guide:

Getting started the first message, explained
Tutorials four, in order, each ending with something that runs
Publishing one message, a batch, and what a confirm does and does not promise
Consuming handlers, acks, prefetch and concurrency
Exchanges, queues and bindings where a message can actually go, and why a queue is quorum
The envelope what travels with a message
Codecs JSON, YAML, TOML, XML, Protobuf, Avro, text or raw bytes, and encrypted with AES-GCM
Interceptors the seam around publish and handle
Security TLS, trust, credentials and development certificates that cannot reach production
Retries, dead letters and shutdown the retry ladder, the dead-letter queue and the parking lot
Patterns idempotency, outbox, replay, ordering and nine more
Request and reply asking a question and blocking for the answer, and what that costs
Streams a queue that keeps what it delivered, and the machinery that does not apply to one
Metrics, tracing and health including the OpenTelemetry adapter
Testing without a broker the transport seam, and where it stops being enough
API reference every public class and method, from YARD

Status

Pre-1.0 and in development. The contract layer and the AMQP transport — connect, publish, consume, retry, dead-letter — are implemented and tested, against the shared fixtures and against a real broker, and so are the patterns above them. TLS and credentials are in, including a private certificate authority and mutual TLS. The API may change before 1.0. The current release is 0.7.14, from the gem feed at https://acemq.org/gems.