Queues, exchanges and bindings
The question AMQP cannot answer
AMQP gives you two ways to ask about a queue, and neither returns its configuration.
A passive declare tells you whether the queue exists. A real declare tells you whether the arguments you passed match the ones the broker holds — by succeeding, or by killing your channel with a 406. Neither reports what the arguments are.
That is not a gap in AceMQ for Java; it is the protocol. The consequence is that a topology mismatch can be detected but not explained:
QueueInfo queue = admin.queue("orders.new").orElseThrow();
queue.arguments(); // {x-message-ttl=60000, x-dead-letter-exchange=dlx, x-queue-type=classic}
queue.argument("x-message-ttl"); // 60000Comparing against a topology
RabbitMQ 4 records x-queue-type on every
queue, including queues declared with no arguments at all. So
this is wrong:
// Reports drift on every classic queue in the estate.
boolean matches = queue.arguments().equals(expected);Compare the arguments you care about, not the whole map:
boolean matches = expected.entrySet().stream()
.allMatch(e -> Objects.equals(queue.argument(e.getKey()), e.getValue()));That also has the property you want when RabbitMQ adds another broker-maintained argument in a future version: a queue you did not change does not suddenly start reporting drift.
Counters
| Method | What it is |
|---|---|
messagesReady() |
Waiting, not yet delivered. The depth number. |
messagesUnacknowledged() |
Delivered to a consumer, not yet acked |
messages() |
The sum of the two |
consumers() |
Consumers currently attached |
state() |
"running", "idle",
"flow" |
messages() stays high while a healthy consumer works
through a batch, so a depth check built on it fires during normal
operation. Use messagesReady().
For depth across many queues at once, the management API is the wrong tool — one HTTP request per queue does not scale to an estate. Use the metrics endpoint, which returns every queue in one scrape.
Listing
for (QueueInfo q : admin.queues()) {
System.out.printf("%-30s %8d %s%n", q.name(), q.messagesReady(), q.type());
}queues() is scoped to the current vhost. On a large
broker this is a big response; the metrics endpoint is cheaper if you
only want the numbers.
Exchanges and bindings
ExchangeInfo dlx = admin.exchange("dlx").orElseThrow();
dlx.type(); // "topic"
dlx.internal(); // false
for (BindingInfo b : admin.bindingsForQueue("orders.new")) {
System.out.println(b.source() + " --[" + b.routingKey() + "]--> " + b.destination());
}The default exchange has an empty name. Every queue
is implicitly bound to it by its own name, and those bindings come back
from bindings() with an empty source().
isDefaultExchangeBinding() identifies them, and they are
almost never what you want in a topology export — you did not declare
them and you cannot delete them.
The empty name is also why exchange("") is found by
scanning the listing rather than by requesting a path: the URL would
collapse to the listing endpoint and return an array where an object was
expected.
Two encoding details that cost a test failure each
The default vhost is literally / and
must reach the URL as %2F. Unencoded it produces
/api/queues///orders.new, which 404s — and a 404 is
indistinguishable from a queue that is not there. The library encodes
it; the detail is here because it will bite you if you build URLs
yourself.
URLEncoder turns a space into
+. Correct for a form body, wrong for a path: a
queue named dead letters would be looked up as
dead+letters and reported missing. Path segments are
percent-encoded here, not form-encoded.
Declaring and deleting
AMQP can declare a queue, and usually should: a queue an application depends on is best declared by that application at startup, so it exists before the first publish. These exist for the cases AMQP cannot serve — provisioning for a team that has not deployed yet, or restoring from a backup.
admin.declareQueue("orders.new", true, Map.of("x-message-ttl", 60000));
admin.declareExchange("dlx", "topic", true, Map.of());
admin.bindQueue("dlx", "orders.new", "orders.#", Map.of());Both declares are idempotent in the same way PUT is:
running the code twice succeeds twice.
admin.deleteQueue("orders.new"); // with everything in it
admin.deleteExchange("dlx"); // and its bindingsPurging
admin.purgeQueue("orders.dlq");Discards every message and keeps the queue. Irreversible, with no confirmation. It is genuinely right for a dead-letter queue whose contents have been analysed and cannot be replayed, and it is exactly as fast on a queue holding something important.
Unbinding takes the binding, not a routing key
BindingInfo binding = admin.bindingsForQueue("orders.new").stream()
.filter(b -> !b.isDefaultExchangeBinding())
.findFirst()
.orElseThrow();
admin.unbind(binding);A binding has no name. Several may join the same pair of objects with
different routing keys, so the broker identifies one by a
properties_key — and that key arrives already
percent-encoded. A routing key of a.# has a
properties key of a.%23, which belongs in the URL exactly
as given; encoding it again produces a.%2523 and matches
nothing.
That is why unbind takes the whole object.
Reconstructing the key correctly is not something a caller should have
to know about, and getting it wrong produces a 404 that looks like a
binding which was already gone.
The default-exchange binding is refused rather than attempted — the broker maintains it, and it exists for as long as the queue does.
Errors
| Situation | What you get |
|---|---|
| Queue absent | Optional.empty() |
| Bad credentials | AdminException, saying the user needs
monitoring or administrator |
| Plugin disabled | AdminException — the API answers 406,
not 404 |
| Broker unreachable | AdminException wrapping the
IOException |
The 406 is worth knowing. A disabled
rabbitmq_shovel_management plugin does not look like a
missing endpoint; it looks like a refusal to negotiate, and treating it
as "not found" would report an empty shovel list on a broker that has
shovels.