|
| 1 | +# BabelQueue — Apache ActiveMQ Artemis (Java) |
| 2 | + |
| 3 | +`com.babelqueue:babelqueue-artemis` — an Apache ActiveMQ Artemis transport for |
| 4 | +[BabelQueue](https://babelqueue.com), built on **JMS** (Jakarta Messaging 3.x) and the |
| 5 | +framework-agnostic [`babelqueue-core`](https://github.com/BabelQueue/babelqueue-java). |
| 6 | + |
| 7 | +A canonical-envelope **publisher** and a URN-routed, `CLIENT_ACKNOWLEDGE` **consumer**, so an |
| 8 | +Artemis-based Java service speaks the same wire contract (envelope shape, URN identity, trace |
| 9 | +propagation) as the .NET, Python, Go and Node SDKs. Implements |
| 10 | +[§7 of the broker-bindings contract](https://babelqueue.com/docs/spec/1.x/broker-bindings#apache-activemq-artemis). |
| 11 | + |
| 12 | +Unlike Kafka, Artemis gives the binding **native** primitives — per-message acknowledgement, |
| 13 | +scheduled delivery, a delivery counter and a dead-letter address — so this transport maps onto |
| 14 | +them instead of re-implementing them (the envelope stays `schema_version: 1`): |
| 15 | + |
| 16 | +- the envelope JSON is the message **body** (`TextMessage`); the contract fields are mirrored |
| 17 | + onto JMS metadata — `JMSType` = URN, `JMSCorrelationID` = `trace_id`, `JMSTimestamp` = |
| 18 | + `created_at` — plus the `bq-` string properties (so a JMS **or** AMQP-1.0 consumer routes on |
| 19 | + `JMSType` without decoding the body); |
| 20 | +- consume is `CLIENT_ACKNOWLEDGE`: **acknowledge after success**; a throwing handler leaves the |
| 21 | + message unacknowledged and `recover()`s the session so the broker redelivers it (incrementing |
| 22 | + `JMSXDeliveryCount`); |
| 23 | +- **`attempts = max(body, JMSXDeliveryCount − 1)`** — `JMSXDeliveryCount` is the broker's |
| 24 | + 1-based authoritative redelivery counter, the body's `attempts` the floor; |
| 25 | +- delay uses **native** JMS 2.0 scheduled delivery (`setDeliveryDelay`); terminal failures go to |
| 26 | + an opt-in `<queue>.dlq` carrying the canonical envelope plus the additive `dead_letter` block, |
| 27 | + cross-language alongside Artemis's own dead-letter address. |
| 28 | + |
| 29 | +## Install (Maven) |
| 30 | + |
| 31 | +```xml |
| 32 | +<dependency> |
| 33 | + <groupId>com.babelqueue</groupId> |
| 34 | + <artifactId>babelqueue-artemis</artifactId> |
| 35 | + <version>1.0.0</version> |
| 36 | +</dependency> |
| 37 | +``` |
| 38 | + |
| 39 | +It pulls `babelqueue-core` transitively. The JMS API is `provided`-style — bring your Artemis |
| 40 | +JMS client (`org.apache.activemq:artemis-jakarta-client`), which supplies both the |
| 41 | +`jakarta.jms` API and the broker connection. |
| 42 | + |
| 43 | +## Produce |
| 44 | + |
| 45 | +```java |
| 46 | +ConnectionFactory factory = new org.apache.activemq.artemis.jms.client.ActiveMQJMSConnectionFactory( |
| 47 | + "tcp://localhost:61616"); |
| 48 | + |
| 49 | +try (JMSContext ctx = factory.createContext("user", "pass")) { |
| 50 | + Session session = ctx.createSession(Session.CLIENT_ACKNOWLEDGE); |
| 51 | + MessageProducer producer = session.createProducer(session.createQueue("orders")); |
| 52 | + |
| 53 | + String id = ArtemisPublisher.create(session, producer) |
| 54 | + .publish("urn:babel:orders:created", Map.of("order_id", 1042)); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +`publish(urn, data)` returns the message `meta.id`; overloads add a `traceId` and a relative |
| 59 | +`Duration delay` (native `setDeliveryDelay`). |
| 60 | + |
| 61 | +## Consume |
| 62 | + |
| 63 | +```java |
| 64 | +Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); |
| 65 | +MessageConsumer consumer = session.createConsumer(session.createQueue("orders")); |
| 66 | +MessageProducer dlqProducer = session.createProducer(null); // anonymous, for <queue>.dlq |
| 67 | + |
| 68 | +ArtemisConsumer worker = ArtemisConsumer.builder(consumer, session) |
| 69 | + .handler("urn:babel:orders:created", (env, message) -> { |
| 70 | + // env.data(), env.traceId(), env.attempts() ... |
| 71 | + }) |
| 72 | + .deadLetterQueue(dlqProducer, "orders.dlq") |
| 73 | + .maxTries(3) |
| 74 | + .onError((err, env, message) -> err.printStackTrace()) |
| 75 | + .build(); |
| 76 | + |
| 77 | +connection.start(); |
| 78 | +worker.run(() -> true); // receive → process → acknowledge, until you stop it |
| 79 | +``` |
| 80 | + |
| 81 | +A successful handler `acknowledge()`s the message. A throwing handler leaves it unacknowledged |
| 82 | +and `recover()`s the session, so the broker redelivers it and bumps `JMSXDeliveryCount`; once |
| 83 | +`maxTries` is reached the envelope goes to `<queue>.dlq` with a `dead_letter` block. The consumer |
| 84 | +routes on `JMSType`, so it never decodes a message it cannot handle. Unknown-URN strategy is one |
| 85 | +of `fail` / `delete` / `release` / `dead_letter`. |
| 86 | + |
| 87 | +> One message per `poll()` keeps the session-wide `acknowledge()` / `recover()` correct. A JMS |
| 88 | +> session is single-threaded — run one `ArtemisConsumer` per thread. |
| 89 | +
|
| 90 | +## Contract mapping (§7) |
| 91 | + |
| 92 | +| Envelope | Apache ActiveMQ Artemis (JMS) | |
| 93 | +| :--- | :--- | |
| 94 | +| body | message body (`TextMessage`, byte-identical across SDKs) | |
| 95 | +| `job` (URN) | `JMSType` (consumer routes on this) | |
| 96 | +| `trace_id` | `JMSCorrelationID` | |
| 97 | +| `meta.id` | `JMSMessageID` (broker-set for JMS; body is authoritative) | |
| 98 | +| `meta.schema_version` | property `bq-schema-version` (`"1"`) | |
| 99 | +| `meta.lang` | property `bq-source-lang` | |
| 100 | +| `meta.created_at` | `JMSTimestamp` (Unix ms) | |
| 101 | +| `attempts` | `max(body, JMSXDeliveryCount − 1)` (broker counter is 1-based) | |
| 102 | +| reserve / ack | `receive` → process → **`acknowledge()`** (CLIENT_ACKNOWLEDGE) | |
| 103 | +| retry / delay | `recover()` redelivery · native `setDeliveryDelay` | |
| 104 | +| dead-letter | `<queue>.dlq` + `dead_letter` block (alongside the native DLA) | |
| 105 | + |
| 106 | +The `bq-` property values are strings (integers as decimal, e.g. `"1"`); `bq-app-id` is |
| 107 | +`"babelqueue"`. The envelope is unchanged (`schema_version` stays `1`); Artemis is purely |
| 108 | +additive. |
| 109 | + |
| 110 | +## Build & test |
| 111 | + |
| 112 | +```bash |
| 113 | +mvn verify |
| 114 | +``` |
| 115 | + |
| 116 | +The JMS interfaces (`Session`, `MessageProducer`, `MessageConsumer`, `Message`) are mocked with |
| 117 | +Mockito — no Artemis, no network. JUnit 5, JaCoCo ≥90% line coverage. |
| 118 | + |
| 119 | +## License |
| 120 | + |
| 121 | +MIT |
0 commit comments