Skip to content

Commit a7bd173

Browse files
committed
Initial release: Apache ActiveMQ Artemis JMS transport on babelqueue-core
0 parents  commit a7bd173

15 files changed

Lines changed: 1327 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
java: [ "17", "21" ]
18+
steps:
19+
- uses: actions/checkout@v5
20+
21+
- uses: actions/setup-java@v4
22+
with:
23+
distribution: temurin
24+
java-version: ${{ matrix.java }}
25+
cache: maven
26+
27+
# `verify` runs the JUnit 5 suite and the JaCoCo >=90% line-coverage gate. The JMS
28+
# interfaces (Session/Producer/Consumer/Message) are mocked with Mockito — no Artemis,
29+
# no network.
30+
- name: Test
31+
run: mvn -B --no-transfer-progress verify
32+
33+
ci-green:
34+
name: CI green
35+
runs-on: ubuntu-latest
36+
needs: [test]
37+
if: ${{ always() }}
38+
steps:
39+
- name: Fail if any required job did not pass
40+
run: |
41+
if ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}; then
42+
echo "A required job failed or was cancelled."
43+
exit 1
44+
fi

.github/workflows/release.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: [ "v*" ]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
release:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v5
15+
16+
- uses: actions/setup-java@v4
17+
with:
18+
distribution: temurin
19+
java-version: "17"
20+
cache: maven
21+
# Writes a settings.xml server entry `central` using the env vars below,
22+
# and imports the GPG key for signing.
23+
server-id: central
24+
server-username: CENTRAL_TOKEN_USERNAME
25+
server-password: CENTRAL_TOKEN_PASSWORD
26+
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
27+
gpg-passphrase: MAVEN_GPG_PASSPHRASE
28+
29+
- name: Run tests
30+
run: mvn -B --no-transfer-progress verify
31+
32+
- name: Publish to Maven Central
33+
run: mvn -B --no-transfer-progress -Prelease -DskipTests deploy
34+
env:
35+
CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
36+
CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
37+
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
38+
39+
- name: Create GitHub Release
40+
uses: softprops/action-gh-release@v2
41+
with:
42+
generate_release_notes: true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target/
2+
*.class

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Changelog
2+
3+
All notable changes to `com.babelqueue:babelqueue-artemis` are documented here.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6+
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
The envelope wire format is versioned separately by `meta.schema_version`
8+
(currently **1**) — see the contract at [babelqueue.com](https://babelqueue.com).
9+
10+
## [1.0.0] - 2026-06-13
11+
12+
### Added
13+
- Initial release. An Apache ActiveMQ Artemis transport on `babelqueue-core` over **JMS**
14+
(Jakarta Messaging 3.x), implementing §7 of the broker-bindings contract. Artemis offers
15+
native ack/scheduled-delivery/delivery-counter/dead-letter-address, so the binding maps onto
16+
them rather than re-implementing: `ArtemisPublisher` (body = canonical envelope `TextMessage`,
17+
`JMSType` = URN, `JMSCorrelationID` = `trace_id`, the `bq-` string-property projection — so a
18+
JMS or AMQP-1.0 consumer routes on `JMSType`; a delay uses native JMS 2.0 `setDeliveryDelay`)
19+
and `ArtemisConsumer` (`CLIENT_ACKNOWLEDGE` consume — acknowledge after success; a throwing
20+
handler `recover()`s the session for broker redelivery; **`attempts = max(body,
21+
JMSXDeliveryCount − 1)`** with the broker's 1-based counter authoritative; terminal failures
22+
go to `<queue>.dlq` with the additive `dead_letter` block; `fail`/`delete`/`release`/
23+
`dead_letter` unknown-URN strategies; poison bodies forwarded raw to the DLQ). Java 17, JUnit
24+
5, JaCoCo ≥90% line coverage; the JMS interfaces are mocked with Mockito (no Artemis, no
25+
network). The envelope is unchanged (`schema_version: 1`); Apache ActiveMQ Artemis is purely
26+
additive.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Muhammet Şafak
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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

Comments
 (0)