Configuration
Two ways in, and they are not alternatives.
config/acemq.yml, read through Rails'
own config_for. That is where a URL, a virtual host, a
topology and the consumer defaults belong: they differ per environment
and they are data. shared:, per-environment sections and
ERB all work as they do in database.yml, because it is the
same reader.
config.acemq.* in
config/application.rb or an environment file, for the
things that are decisions rather than data — a codec object, a telemetry
reporter, an AceMQ::AMQP::Security. None of those can be
written in YAML without inventing a second configuration language.
The file is read first and the Ruby is applied over it. A file
checked in for every environment is the general statement; a line in
config/environments/test.rb is the specific one, and the
specific one wins.
A key neither half recognises raises on boot rather than being
ignored. A typo in a configuration file that is silently dropped is how
a production broker ends up with the default retry policy and nobody
knows why. Dashes are accepted alongside underscores, because
max-outstanding-publishes is how the same setting is
spelled in the Spring Boot starter's application.yaml and
somebody will copy it across.
Connection
| Setting | Default | |
|---|---|---|
url |
amqp://guest:guest@localhost:5672 |
amqps:// is verified against the system trust store on
its own |
username, password |
— | Kept out of the URL, so they stay out of the log line that reports a failed connection |
token |
— | Instead of a username and password, for a broker taking one |
virtual_host |
— | The name, taken literally and escaped whole: / becomes
/%2F, which is the vhost really called / |
client_name |
the process default | Stamped on every published message as x-acemq-origin.
The difference between a dead-lettered message you can trace to a pod
and one you cannot |
connect_timeout |
10 |
Seconds for the AMQP handshake |
heartbeat |
:server |
Or a number of seconds. Taking the broker's suggestion is right almost always |
connect_on_boot |
false |
See below |
transport_options |
{} |
Passed to Bunny.new, for the handful of things this
does not name |
connect_on_boot is false by default and that is
a decision. A broker that is down should not stop an
application from serving the pages that never touch it. The connection
is opened on first use instead. Set it to true for a service that would
rather fail fast — a consumer-only deployment, say — and note that it is
deliberately not rescued: an application that asked to connect on boot
asked to fail, and swallowing the failure would give it the lazy
behaviour it turned off.
Publishing
| Setting | Default | |
|---|---|---|
format |
json |
A name AceMQ::AMQP::Codecs.build knows:
bytes, json, string,
toml, xml, yaml. Anything else
raises and names the list |
codec |
— | A codec object, for anything that cannot be named — a
composite, protobuf, Avro, an encrypted one, a claim-check wrapper. Wins
over format |
max_outstanding_publishes |
the library's | How many publishes may be waiting for a confirm at once. Added to the library in 0.6.0 |
publisher_confirms |
true |
Cannot be turned off; false raises. See below |
Protobuf and Avro are not names, and that is not an
omission: each needs something a string cannot carry — a generated
message class, a schema, a registry — so each is built and handed over
in codec. serialization.md
is the whole of this.
max_outstanding_publishes is the back pressure that
stops a runaway loop from holding a million messages in this process's
memory, and it is what bounds a publish_all larger than the
ceiling: such a batch is written in waves rather than held whole.
Reaching it raises PublishError rather than waiting — observability.md says
why, and what to do about it.
publisher_confirms cannot be turned off, and
setting it to false raises. The library opens its publishing
channel with confirm_select and has no keyword for a
publish without confirms, so there is nothing for false to
do. It refuses rather than being ignored, for the same reason an unknown
key does: a line in a configuration file that reads as though durability
had been traded for speed, and changed nothing at all, is worse than no
line.
Interceptors and telemetry
Neither can be written in YAML, because both are objects.
| Setting | Default | |
|---|---|---|
interceptors |
[] |
Applied to the process connection as it is opened. A bare object is accepted as well as an array |
telemetry |
— | A reporter answering count, observe and
gauge. Nil is the library's own no-op, which costs
nothing |
# config/application.rb
config.acemq.interceptors = [TenantStamp.new, AceMQ::AMQP::Telemetry::OpenTelemetry.new]
config.acemq.telemetry = AceMQStatsdReporter.new(STATSD)interceptors exists so that registering one does
not open the broker connection. intercept_publish
and intercept_consume are instance methods, so an
initializer that reaches for AceMQ::Rails.connection to
call one dials the broker during boot — the one thing
connect_on_boot: false is for, and it happens quietly. This
list is applied on the way out of Connection.open
instead.
Each entry is offered to both sides; the library keeps only the hooks
the object answers to, which is how one setting serves publishing and
consuming both. A connection handed in with
AceMQ::Rails.connection= — a test's own — is left exactly
as it was given.
interceptors.md and observability.md are the pages.
TLS
config.acemq.tls = AceMQ::AMQP::Security.verified(
certificate_authority: Rails.root.join("config/certs/ca.pem").to_s
)A Security object rather than a set of YAML paths,
because the library's Security is where the checks live — a
development certificate refused unless asked for, a minimum TLS version,
a client certificate that has to be a pair — and re-expressing them here
would be re-implementing them. amqps:// on its own needs
none of this: it verifies against the system trust store.
Mutual TLS, credentials from a mounted file, a rotating secret, development certificates and payload encryption are all security.md.
Do not set credentials twice. A
Securitybuilt withcredentials:in it, plususername/passwordhere, raisesConfigurationError— "put them in one place so there is no question which login is used".
Consumer defaults
Under consumer:, and every one of them overridable per
consumer class.
| Setting | Default | |
|---|---|---|
prefetch |
20 |
Unacknowledged messages one consumer holds |
concurrency |
1 |
Messages worked on at once. One keeps a queue's messages in the order the broker offers them |
max_attempts |
1 |
One means no retries |
initial_delay, multiplier,
max_delay |
0, 2.0, 0 |
The ladder |
jitter |
— | A factor, to stop a fleet retrying in lockstep |
give_up_after |
— | Seconds of message age past which it is dead-lettered whatever the attempt count |
broker_wait_threshold |
30 |
Where a retry waits |
broker_wait_threshold is a shutdown setting as
much as a reliability one. A delay shorter than it is waited
out in this process, holding a prefetch slot and holding up a drain; a
delay at or above it is waited out on a rung queue in the broker, where
a restart does not have to survive it and a drain does not have to sit
through it. lifecycle.md has the
arithmetic.
Topology
| Setting | Default | |
|---|---|---|
topology |
{} |
The exchanges, queues and bindings. Empty applies nothing, silently and on purpose |
declare_topology_on_boot |
true |
Whether the consumer process applies it as it starts |
declare_topology_on_boot is true because a consumer that
starts before its queue exists reads nothing and says nothing about why.
Turn it off for an estate where a deployment tool owns the broker —
bin/rails acemq:topology is then the only thing that
declares anything.
See topology.md, and streams.md for type: stream.
Shutdown
| Setting | Default | |
|---|---|---|
shutdown_timeout |
20 |
Seconds the consumer process spends draining before it gives up |
Twenty is chosen to sit inside Kubernetes' thirty-second default
terminationGracePeriodSeconds with room for the process to
exit. It must be comfortably shorter than whatever will kill the process
— TimeoutStopSec under systemd, docker stop -t
under Docker, ten seconds by default there.
Consumers
| Setting | Default | |
|---|---|---|
consumer_paths |
[] |
Extra directories to autoload consumers from |
Empty, and that is not an omission: Rails autoloads every direct
subdirectory of app/, so app/consumers needs
nothing. This is for lib/consumers and the like.
The whole thing, once
shared:
client_name: shop
format: json
max_outstanding_publishes: 1000
shutdown_timeout: 20
consumer:
prefetch: 20
concurrency: 4
max_attempts: 5
initial_delay: 1
max_delay: 300
broker_wait_threshold: 10
topology:
exchanges:
- { name: shop-events, type: topic }
queues:
- name: shop.orders
dead_letter: true
retries: { max_attempts: 5, initial_delay: 1, max_delay: 300 }
bindings:
- { queue: shop.orders, exchange: shop-events, routing_key: "order.#" }
production:
url: <%= ENV["ACEMQ_URL"] %>
username: <%= ENV["ACEMQ_USERNAME"] %>
password: <%= ENV["ACEMQ_PASSWORD"] %>
virtual_host: shop
connect_on_boot: true