Apache ActiveMQ Artemis transport
Last updated:
com.babelqueue:babelqueue-artemis is an Apache ActiveMQ Artemis transport on the Java core,
built on JMS (Jakarta Messaging 3.x). It sends the
canonical envelope as the message body with the
§7 projection, and consumes by routing
each message to a handler by URN — so a message it produces is consumed by any other BabelQueue
SDK, and vice-versa. Artemis speaks AMQP 1.0, so the other SDKs interoperate over the wire even
though Java uses JMS.
Install
Maven:
<dependency>
<groupId>com.babelqueue</groupId>
<artifactId>babelqueue-artemis</artifactId>
<version>1.0.3</version>
</dependency>
Requirements: Java 17+. It pulls babelqueue-core transitively; bring your Artemis JMS
client (org.apache.activemq:artemis-jakarta-client), which supplies both the jakarta.jms API
and the broker connection.
Produce
import com.babelqueue.artemis.ArtemisPublisher;
import jakarta.jms.*;
import org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory;
import java.util.Map;
ConnectionFactory factory = new ActiveMQJMSConnectionFactory("tcp://localhost:61616");
try (Connection connection = factory.createConnection("user", "pass")) {
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(session.createQueue("orders"));
String id = ArtemisPublisher.create(session, producer)
.publish("urn:babel:orders:created", Map.of("order_id", 1042L));
}
ArtemisPublisher takes a classic JMS Session (from Connection.createSession), not a
JMSContext. publish(...) throws the checked JMSException, so declare throws JMSException
on the enclosing method (or catch it).
publish(urn, data) returns the message meta.id; overloads add a traceId and a relative
Duration delay (native JMS 2.0 setDeliveryDelay).
Consume
import com.babelqueue.artemis.*;
import jakarta.jms.*;
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(session.createQueue("orders"));
MessageProducer dlqProducer = session.createProducer(null); // anonymous, for <queue>.dlq
ArtemisConsumer worker = ArtemisConsumer.builder(consumer, session)
.handler("urn:babel:orders:created", (envelope, message) -> {
// envelope.data(), envelope.traceId(), envelope.attempts() ...
})
.deadLetterQueue(dlqProducer, "orders.dlq")
.maxTries(3)
.build();
connection.start();
worker.run(() -> true); // receive → process → acknowledge
A successful handler acknowledges the message. A throwing handler leaves it unacknowledged and
recovers the session, so the broker redelivers it and bumps JMSXDeliveryCount; once maxTries
is reached the envelope goes to <queue>.dlq with a dead_letter block. The consumer routes on
JMSType. Unknown-URN strategy is one of fail / delete / release / dead_letter.
If a <queue>.dlq write fails on any of these paths (a handler that has run out of tries, an
unknown URN with the dead_letter strategy, a poison body, a version-gated message), the message is
not acknowledged: the consumer recovers the session, reports the error to onError, and the
broker redelivers it, so the handler may run again. Without this, the session-wide
acknowledge() of the next message would also have acknowledged the one whose DLQ write
failed. Keep handlers idempotent.
One message per poll() keeps the session-wide acknowledge() / recover() correct — a JMS
session is single-threaded, so run one ArtemisConsumer per thread.
Poison bodies and version-gated messages
Two kinds of delivery can never succeed, so the consumer settles them on their first delivery, never hands them to a handler and never retries them (ADR-0037, since 1.1.0):
- A poison body — it is not JSON, is not an envelope, has neither a valid
jobnor a validurn, or carries an unsupportedmeta.schema_version. With a dead-letter queue it is written to<queue>.dlqas the poison record: a JSON object body is spliced byte for byte with adead_letterblock (reason: "poison",original_queue= the consumed queue), anything else is wrapped underraw(base64 plusraw_encodingwhen it is not valid UTF-8). The message isacknowledged only after the write. Earlier releases forwarded the raw body instead, so a reader of<queue>.dlqmust not assume every record is a canonical envelope. - A version-gated message — the
bq_schema_versionmessage property is present, not blank and not1(§7.7). The body is never decoded; the raw body, byte for byte and with nodead_letterblock, is written to<queue>.dlqand the message is thenacknowledged.
A missing or blank bq_schema_version (the empty string, or only ASCII whitespace: space, tab,
LF, VT, FF, CR) changes nothing: the body is decoded as usual. A property present with a null
value counts as missing too. Any other value is compared with "1" exactly as sent, with no
trimming, so "2", "x", "01" and " 1" are all rejected without decoding. onError is
called with a null envelope. A "1" value does not skip the body check: the decoded envelope
still goes through the schema_version validation, and a body that fails it is a poison body.
What happens around the DLQ write:
| Configuration | Poison body | Version-gated message |
|---|---|---|
| dead-letter queue set | poison record to <queue>.dlq, then acknowledge |
raw body to <queue>.dlq, then acknowledge |
unknownUrn(DELETE) |
acknowledged, no record |
acknowledged, no record (dropped) |
| no dead-letter queue | acknowledged, dropped, logged |
acknowledged, dropped, logged |
| the DLQ write fails | recover(), reported, redelivered and written again |
same |
The consumer reads only the strategy you set explicitly with unknownUrn(...): with none set it
behaves as fail. Artemis’s own max-delivery-attempts is not involved in either case.
Contract mapping (§7)
| Envelope | Apache ActiveMQ Artemis (JMS) |
|---|---|
| body | message body (TextMessage, byte-identical across SDKs) |
job (URN) |
JMSType (consumer routes on this) |
trace_id |
JMSCorrelationID |
meta.id |
JMSMessageID (broker-set for JMS; body is authoritative) |
meta.schema_version |
property bq_schema_version (set on publish; the consumer checks it before decoding the body, and also validates the body’s meta.schema_version after decoding) |
meta.lang |
property bq_source_lang |
meta.created_at |
JMSTimestamp (Unix ms) |
attempts |
max(body, JMSXDeliveryCount − 1) (broker counter is 1-based) |
| reserve / ack | receive → process → acknowledge() (CLIENT_ACKNOWLEDGE) |
| retry / delay | recover() redelivery · native setDeliveryDelay |
| dead-letter | <queue>.dlq + dead_letter block (alongside the native DLA); a poison body is the ADR-0035 record, a version-gated message the raw body |
The JMS interfaces are mocked with Mockito — no Artemis, no network. The envelope is unchanged
(schema_version stays 1); Apache ActiveMQ Artemis is purely additive.