This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch camel-quarkus-main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git
commit 9d4649a731d5bc17144bb7ed90fda5b7977e658a
Author: jomin mathew <>
AuthorDate: Tue Mar 24 12:10:55 2026 +0000
Fixes #6195: Add integration tests for Saga example
---
saga/README.adoc | 25 +++-
saga/pom.xml | 19 ++-
saga/saga-app/pom.xml | 17 +++
.../org/apache/camel/example/saga/SagaRoute.java | 9 +-
saga/saga-flight-service/pom.xml | 17 +++
.../org/apache/camel/example/saga/FlightRoute.java | 10 +-
saga/saga-integration-tests/README.md | 126 ++++++++++++++++++
saga/saga-integration-tests/pom.xml | 148 +++++++++++++++++++++
.../org/apache/camel/example/saga/SagaBasicIT.java | 28 ++++
.../apache/camel/example/saga/SagaBasicTest.java | 106 +++++++++++++++
.../camel/example/saga/SagaTestResource.java | 115 ++++++++++++++++
.../src/test/resources/application.yml | 48 +++++++
saga/saga-payment-service/pom.xml | 17 +++
.../apache/camel/example/saga/PaymentRoute.java | 4 +-
saga/saga-train-service/pom.xml | 17 +++
.../org/apache/camel/example/saga/TrainRoute.java | 10 +-
16 files changed, 697 insertions(+), 19 deletions(-)
diff --git a/saga/README.adoc b/saga/README.adoc
index d6dabf57..8a7ade08 100644
--- a/saga/README.adoc
+++ b/saga/README.adoc
@@ -8,12 +8,16 @@ and other general information.
=== How it works
-There are 4 services as participants of the Saga:
+There are 5 modules in this example:
-- payment-service: it emulates a real payment transaction, and it will be used
by both flight-service and train-service
+**Service Modules:**
+- app: is the starting point, and it emulates a user that starts the
transaction to buy both flight and train tickets
- flight-service: it emulates the booking of a flight ticket, and it uses the
payment-service to execute a payment transaction
- train-service: it emulates the reservation of a train seat, and it uses the
payment-service to execute a payment transaction
-- app: is the starting point, and it emulates a user that starts the
transaction to buy both flight and train tickets
+- payment-service: it emulates a real payment transaction, and it will be used
by both flight-service and train-service
+
+**Test Module:**
+- integration-tests: contains automated integration tests using Testcontainers
for Docker-based testing
The starting point is a REST endpoint that creates a request for a new
reservation
and there is 15% probability that the payment service fails.
@@ -101,6 +105,21 @@ Transaction
http://localhost:8080/lra-coordinator/0_ffff7f000001_8aad_62d16f11_7
----
+=== Running Tests
+
+The integration tests use Testcontainers to automatically start Artemis and
LRA Coordinator in Docker.
+
+[source,shell]
+----
+# Ensure Docker is running
+docker ps
+
+# Run integration tests
+mvn clean test -pl saga-integration-tests
+----
+
+See link:saga-integration-tests/README.md[saga-integration-tests/README.md]
for more details.
+
=== Package and run the application
Once you are done with developing you may want to package and run the
application.
diff --git a/saga/pom.xml b/saga/pom.xml
index 932b4961..75d70b56 100644
--- a/saga/pom.xml
+++ b/saga/pom.xml
@@ -44,6 +44,7 @@
<formatter-maven-plugin.version>2.29.0</formatter-maven-plugin.version>
<impsort-maven-plugin.version>1.13.0</impsort-maven-plugin.version>
+ <jandex-maven-plugin.version>3.0.1</jandex-maven-plugin.version>
<license-maven-plugin.version>5.0.0</license-maven-plugin.version>
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<maven-jar-plugin.version>3.5.0</maven-jar-plugin.version>
@@ -56,6 +57,7 @@
<module>saga-flight-service</module>
<module>saga-payment-service</module>
<module>saga-train-service</module>
+ <module>saga-integration-tests</module>
</modules>
<dependencyManagement>
@@ -87,6 +89,10 @@
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-core</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus</groupId>
+ <artifactId>camel-quarkus-bean</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.camel.quarkus</groupId>
<artifactId>camel-quarkus-direct</artifactId>
@@ -104,13 +110,6 @@
<artifactId>quarkus-artemis-jms</artifactId>
<version>${quarkiverse-artemis.version}</version>
</dependency>
-
- <!-- Test -->
- <dependency>
- <groupId>io.quarkus</groupId>
- <artifactId>quarkus-junit</artifactId>
- <scope>test</scope>
- </dependency>
</dependencies>
<build>
@@ -182,6 +181,12 @@
<version>${maven-jar-plugin.version}</version>
</plugin>
+ <plugin>
+ <groupId>io.smallrye</groupId>
+ <artifactId>jandex-maven-plugin</artifactId>
+ <version>${jandex-maven-plugin.version}</version>
+ </plugin>
+
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
diff --git a/saga/saga-app/pom.xml b/saga/saga-app/pom.xml
index b7fd20c3..ad10c7b7 100644
--- a/saga/saga-app/pom.xml
+++ b/saga/saga-app/pom.xml
@@ -31,4 +31,21 @@
<name>Camel Quarkus :: Examples :: Saga :: App</name>
<description>Main application starting SAGA</description>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>io.smallrye</groupId>
+ <artifactId>jandex-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>make-index</id>
+ <goals>
+ <goal>jandex</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
</project>
diff --git
a/saga/saga-app/src/main/java/org/apache/camel/example/saga/SagaRoute.java
b/saga/saga-app/src/main/java/org/apache/camel/example/saga/SagaRoute.java
index d1985326..6f31b1df 100644
--- a/saga/saga-app/src/main/java/org/apache/camel/example/saga/SagaRoute.java
+++ b/saga/saga-app/src/main/java/org/apache/camel/example/saga/SagaRoute.java
@@ -16,9 +16,11 @@
*/
package org.apache.camel.example.saga;
+import jakarta.enterprise.context.ApplicationScoped;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.rest.RestParamType;
+@ApplicationScoped
public class SagaRoute extends RouteBuilder {
@Override
@@ -33,12 +35,15 @@ public class SagaRoute extends RouteBuilder {
.compensation("direct:cancelOrder")
.log("Executing saga #${header.id} with LRA
${header.Long-Running-Action}")
.setHeader("payFor", constant("train"))
+ // Request timeout prevents indefinite waits during service
failures or slow responses
.to("jms:queue:{{example.services.train}}?exchangePattern=InOut" +
- "&replyTo={{example.services.train}}.reply")
+ "&replyTo={{example.services.train}}.reply" +
+ "&requestTimeout=30000")
.log("train seat reserved for saga #${header.id} with payment
transaction: ${body}")
.setHeader("payFor", constant("flight"))
.to("jms:queue:{{example.services.flight}}?exchangePattern=InOut" +
- "&replyTo={{example.services.flight}}.reply")
+ "&replyTo={{example.services.flight}}.reply" +
+ "&requestTimeout=30000")
.log("flight booked for saga #${header.id} with payment
transaction: ${body}")
.setBody(header("Long-Running-Action"))
.end();
diff --git a/saga/saga-flight-service/pom.xml b/saga/saga-flight-service/pom.xml
index 40cf31d0..62c36c58 100644
--- a/saga/saga-flight-service/pom.xml
+++ b/saga/saga-flight-service/pom.xml
@@ -31,4 +31,21 @@
<name>Camel Quarkus :: Examples :: Saga :: Flight Service</name>
<description>Flight Service</description>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>io.smallrye</groupId>
+ <artifactId>jandex-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>make-index</id>
+ <goals>
+ <goal>jandex</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
</project>
diff --git
a/saga/saga-flight-service/src/main/java/org/apache/camel/example/saga/FlightRoute.java
b/saga/saga-flight-service/src/main/java/org/apache/camel/example/saga/FlightRoute.java
index 8e0bd7e0..04694f18 100644
---
a/saga/saga-flight-service/src/main/java/org/apache/camel/example/saga/FlightRoute.java
+++
b/saga/saga-flight-service/src/main/java/org/apache/camel/example/saga/FlightRoute.java
@@ -16,9 +16,11 @@
*/
package org.apache.camel.example.saga;
+import jakarta.enterprise.context.ApplicationScoped;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.SagaPropagation;
+@ApplicationScoped
public class FlightRoute extends RouteBuilder {
@Override
@@ -27,14 +29,16 @@ public class FlightRoute extends RouteBuilder {
.saga()
.propagation(SagaPropagation.MANDATORY)
.option("id", header("id"))
- .compensation("direct:cancelPurchase")
+ .compensation("direct:cancelFlightPurchase")
.log("Buying flight #${header.id}")
+ // Request timeout prevents indefinite waits during payment
service failures
.to("jms:queue:{{example.services.payment}}?exchangePattern=InOut" +
- "&replyTo={{example.services.payment}}.flight.reply")
+ "&replyTo={{example.services.payment}}.flight.reply" +
+ "&requestTimeout=30000")
.log("Payment for flight #${header.id} done with transaction
${body}")
.end();
- from("direct:cancelPurchase")
+ from("direct:cancelFlightPurchase")
.log("Flight purchase #${header.id} has been cancelled due to
payment failure");
}
diff --git a/saga/saga-integration-tests/README.md
b/saga/saga-integration-tests/README.md
new file mode 100644
index 00000000..86a3f139
--- /dev/null
+++ b/saga/saga-integration-tests/README.md
@@ -0,0 +1,126 @@
+# Saga Integration Tests
+
+Integration tests for the Camel Quarkus Saga example demonstrating distributed
transaction coordination using the LRA (Long Running Action) pattern with JMS
messaging.
+
+## Overview
+
+This module tests the Saga example by running all services (train, flight,
payment) in a single JVM with Testcontainers managing external dependencies
(LRA Coordinator and Artemis broker).
+
+## Test Coverage
+
+The test suite verifies:
+
+- **LRA Integration:** Saga coordination with LRA coordinator
+- **JMS Messaging:** Request-reply pattern over Artemis queues
+- **Service Participation:** Train, flight, and payment service coordination
+- **Compensation Flow:** Rollback when failures occur (15% random failure rate)
+- **End-to-End Flow:** Complete saga orchestration
+
+### Test Case
+
+`testSagaWithLRAAndRandomOutcomes()` - Comprehensive end-to-end test that
verifies the complete saga flow including LRA coordination, all service
participation (train, flight, payment), and validates both success and
compensation scenarios. Since the payment service has a 15% random failure
rate, the test accepts either outcome as valid.
+
+## Running Tests
+
+### Prerequisites
+
+- Java 17+
+- Maven 3.9+
+- Docker (for Testcontainers)
+
+### Execute Tests
+
+```bash
+# Run all tests
+mvn clean test -pl saga-integration-tests
+
+# Run specific test
+mvn test -pl saga-integration-tests -Dtest=SagaBasicTest#testCompleteSagaFlow
+
+# Native mode
+mvn clean verify -Pnative -pl saga-integration-tests
+```
+
+## Infrastructure
+
+**Testcontainers manages:**
+
+- **LRA Coordinator** (`quay.io/jbosstm/lra-coordinator:latest`) - Distributed
saga coordination
+- **Artemis Broker** (`quay.io/artemiscloud/activemq-artemis-broker:latest`) -
JMS messaging
+
+Both containers run on a shared Docker network with proper wait strategies.
+
+## Configuration
+
+### Test Settings (`src/test/resources/application.yml`)
+
+Key configuration settings:
+
+```yaml
+quarkus:
+ http:
+ port: 8084
+ log:
+ file:
+ enable: true
+ path: target/quarkus.log
+ category:
+ "org.apache.camel": DEBUG
+ "org.apache.camel.saga": DEBUG
+ "org.apache.camel.component.lra": DEBUG
+
+camel:
+ lra:
+ enabled: true
+ # coordinator-url and local-participant-url are set by SagaTestResource
+ component:
+ jms:
+ test-connection-on-startup: true
+ concurrent-consumers: 5
+```
+
+Dynamic configuration (Artemis URL, LRA coordinator URL) is injected by
`SagaTestResource` at test runtime.
+
+## Saga Flow
+
+```
+POST /api/saga?id=1
+ → SagaRoute creates LRA transaction
+ → Sends to jms:queue:saga-train-service
+ → TrainRoute processes and sends to jms:queue:saga-payment-service
+ → PaymentRoute completes payment
+ → Sends to jms:queue:saga-flight-service
+ → FlightRoute processes and sends to jms:queue:saga-payment-service
+ → PaymentRoute completes payment
+ → LRA Coordinator commits saga
+ → Returns LRA ID
+```
+
+## Troubleshooting
+
+### Tests Fail with "Connection Refused"
+
+Docker not running. Start Docker and verify:
+```bash
+docker ps
+```
+
+### Tests Timeout
+
+Increase timeout in tests:
+```java
+await().atMost(30, TimeUnit.SECONDS) // Instead of 10-15
+```
+
+### View Container Logs
+
+```bash
+docker ps # Find container ID
+docker logs <container-id>
+```
+
+## Related Links
+
+- [Camel Saga
EIP](https://camel.apache.org/components/latest/eips/saga-eip.html)
+- [Camel LRA
Component](https://camel.apache.org/components/latest/lra-component.html)
+- [Issue #6195](https://github.com/apache/camel-quarkus/issues/6195)
diff --git a/saga/saga-integration-tests/pom.xml
b/saga/saga-integration-tests/pom.xml
new file mode 100644
index 00000000..e74983de
--- /dev/null
+++ b/saga/saga-integration-tests/pom.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <artifactId>camel-quarkus-examples-saga</artifactId>
+ <groupId>org.apache.camel.quarkus.examples</groupId>
+ <version>3.35.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>camel-quarkus-example-saga-integration-tests</artifactId>
+ <name>Camel Quarkus :: Examples :: Saga :: Integration Tests</name>
+ <description>Integration tests for Saga example</description>
+
+ <dependencies>
+ <!-- Saga app dependencies - includes routes and configurations -->
+ <dependency>
+ <groupId>org.apache.camel.quarkus.examples</groupId>
+ <artifactId>camel-quarkus-example-saga-app</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus.examples</groupId>
+ <artifactId>camel-quarkus-example-saga-flight</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus.examples</groupId>
+ <artifactId>camel-quarkus-example-saga-train</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel.quarkus.examples</groupId>
+ <artifactId>camel-quarkus-example-saga-payment</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <!-- Test dependencies -->
+ <dependency>
+ <groupId>io.quarkus</groupId>
+ <artifactId>quarkus-junit5</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>io.rest-assured</groupId>
+ <artifactId>rest-assured</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>${quarkus.platform.group-id}</groupId>
+ <artifactId>quarkus-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>build</id>
+ <goals>
+ <goal>build</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <trimStackTrace>true</trimStackTrace>
+ <systemPropertyVariables>
+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+ </systemPropertyVariables>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>native</id>
+ <activation>
+ <property>
+ <name>native</name>
+ </property>
+ </activation>
+ <properties>
+ <quarkus.native.enabled>true</quarkus.native.enabled>
+ </properties>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-failsafe-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>integration-test</goal>
+ <goal>verify</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>skip-testcontainers-tests</id>
+ <activation>
+ <property>
+ <name>skip-testcontainers-tests</name>
+ </property>
+ </activation>
+ <properties>
+ <skipTests>true</skipTests>
+ </properties>
+ </profile>
+ </profiles>
+
+</project>
diff --git
a/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaBasicIT.java
b/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaBasicIT.java
new file mode 100644
index 00000000..c27b6790
--- /dev/null
+++
b/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaBasicIT.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.example.saga;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+/**
+ * Integration test for native mode compilation.
+ * Extends SagaBasicTest to run the same tests against native executable.
+ */
+@QuarkusIntegrationTest
+public class SagaBasicIT extends SagaBasicTest {
+ // Runs all tests from SagaBasicTest in native mode
+}
diff --git
a/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaBasicTest.java
b/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaBasicTest.java
new file mode 100644
index 00000000..64700c51
--- /dev/null
+++
b/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaBasicTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.example.saga;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.RestAssured;
+import org.awaitility.Awaitility;
+import org.jboss.logmanager.Logger;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Basic integration tests for Saga example.
+ * Tests verify saga orchestration, service participation, routing, and
compensation.
+ * Note: Payment service has 15% random failure rate to test compensation
scenarios.
+ */
+@QuarkusTest
+@QuarkusTestResource(SagaTestResource.class)
+public class SagaBasicTest {
+ private static final String LOG_FILE = "target/quarkus.log";
+ private static final Logger LOG =
Logger.getLogger(SagaBasicTest.class.getName());
+
+ /**
+ * Test saga orchestration with LRA - accepts both success and
compensation outcomes.
+ * Payment service has 15% random failure rate, so either scenario is
valid.
+ */
+ @Test
+ public void testSagaWithLRAAndRandomOutcomes() throws Exception {
+ // Trigger saga asynchronously (may timeout on payment failure, which
is expected)
+ CompletableFuture.runAsync(() -> {
+ try {
+ RestAssured.given()
+ .queryParam("id", 1)
+ .post("/api/saga");
+ } catch (Exception e) {
+ // Expected - request may timeout on payment failure
+ }
+ });
+
+ // Wait for saga to start and process fully
+ // In native mode, we need to wait longer for all messages to be logged
+ await().atMost(60, TimeUnit.SECONDS).untilAsserted(() -> {
+ String log = Files.readString(Paths.get(LOG_FILE));
+ assertTrue(log.contains("Executing saga #1"), "Saga should start
with LRA");
+
+ // Wait until we see evidence of completion (success or failure)
+ boolean completed = log.contains("done for order #1")
+ || log.contains("fails!")
+ || log.contains("cancelled");
+ assertTrue(completed, "Saga should complete with either success or
compensation");
+ });
+
+ Awaitility.await()
+ .pollDelay(Duration.ofSeconds(1))
+ .pollInterval(Duration.ofMillis(250))
+ .atMost(Duration.ofMinutes(1))
+ .untilAsserted(() -> {
+ String log = Files.readString(Paths.get(LOG_FILE));
+
+ // Verify LRA coordinator is used
+ assertTrue(log.contains("lra-coordinator"), "Should use
LRA coordinator");
+
+ // Verify services participated
+ assertTrue(log.contains("Buying train") ||
log.contains("Buying flight"),
+ "Services should participate");
+
+ // Check outcome - either success or compensation is valid
+ boolean hasSuccess = log.contains("done for order #1");
+ boolean hasFailure = log.contains("fails!");
+ boolean hasCompensation = log.contains("cancelled");
+
+ if (hasFailure || hasCompensation) {
+ LOG.info("Saga #1: Compensation scenario tested
(payment failed, saga rolled back)");
+ } else if (hasSuccess) {
+ LOG.info("Saga #1: Success scenario tested (all
payments completed)");
+ }
+
+ // Either outcome is valid because the trigger for saga
compensation is triggered at random
+ assertTrue(hasSuccess || hasFailure,
+ "Saga should complete with either success or
compensation");
+ });
+ }
+}
diff --git
a/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaTestResource.java
b/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaTestResource.java
new file mode 100644
index 00000000..b2fa0000
--- /dev/null
+++
b/saga/saga-integration-tests/src/test/java/org/apache/camel/example/saga/SagaTestResource.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.example.saga;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
+import org.apache.commons.lang3.SystemUtils;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+
+/**
+ * Testcontainers resource for Saga integration tests.
+ * Manages the lifecycle of LRA Coordinator and Artemis broker.
+ */
+public class SagaTestResource implements QuarkusTestResourceLifecycleManager {
+
+ private static final String LRA_IMAGE =
"quay.io/jbosstm/lra-coordinator:7.0.1.Final-3.8.3";
+ private static final String ARTEMIS_IMAGE =
"quay.io/arkmq-org/activemq-artemis-broker:artemis.2.51.0";
+ private static final int LRA_PORT = 8080;
+ private static final int ARTEMIS_PORT = 61616;
+
+ private GenericContainer<?> lraContainer;
+ private GenericContainer<?> artemisContainer;
+ private Network network;
+
+ @Override
+ public Map<String, String> start() {
+ network = Network.newNetwork();
+
+ // Start Artemis broker
+ artemisContainer = new GenericContainer<>(ARTEMIS_IMAGE)
+ .withNetwork(network)
+ .withNetworkAliases("artemis")
+ .withEnv("AMQ_USER", "admin")
+ .withEnv("AMQ_PASSWORD", "admin")
+ .withExposedPorts(ARTEMIS_PORT)
+ .waitingFor(Wait.forListeningPort()
+ .withStartupTimeout(Duration.ofSeconds(60)));
+
+ artemisContainer.start();
+
+ // Start LRA Coordinator
+ lraContainer = new GenericContainer<>(LRA_IMAGE)
+ .withNetwork(network)
+ .withNetworkAliases("lra-coordinator")
+ .withEnv("QUARKUS_HTTP_PORT", String.valueOf(LRA_PORT))
+ .withExposedPorts(LRA_PORT)
+ .withExtraHost("host.testcontainers.internal", "host-gateway")
+ .waitingFor(Wait.forHttp("/lra-coordinator")
+ .forPort(LRA_PORT)
+ .forStatusCode(200)
+ .withStartupTimeout(Duration.ofSeconds(90)));
+
+ if (!SystemUtils.IS_OS_LINUX) {
+ lraContainer.withNetworkMode("bridge");
+ }
+
+ lraContainer.start();
+
+ Map<String, String> config = new HashMap<>();
+
+ // Artemis configuration
+ String artemisUrl = String.format("tcp://%s:%d",
+ artemisContainer.getHost(),
+ artemisContainer.getMappedPort(ARTEMIS_PORT));
+ config.put("quarkus.artemis.url", artemisUrl);
+ config.put("quarkus.artemis.username", "admin");
+ config.put("quarkus.artemis.password", "admin");
+
+ // LRA configuration
+ String lraCoordinatorUrl = String.format("http://%s:%d",
+ lraContainer.getHost(),
+ lraContainer.getMappedPort(LRA_PORT));
+ config.put("camel.lra.coordinator-url", lraCoordinatorUrl);
+
+ // Set local participant URL - use host.testcontainers.internal for
coordinator callbacks
+ config.put("camel.lra.local-participant-url",
"http://host.testcontainers.internal:8084/api");
+
+ // Allow external connections
+ config.put("quarkus.http.host", "0.0.0.0");
+
+ return config;
+ }
+
+ @Override
+ public void stop() {
+ if (artemisContainer != null) {
+ artemisContainer.stop();
+ }
+ if (lraContainer != null) {
+ lraContainer.stop();
+ }
+ if (network != null) {
+ network.close();
+ }
+ }
+}
diff --git a/saga/saga-integration-tests/src/test/resources/application.yml
b/saga/saga-integration-tests/src/test/resources/application.yml
new file mode 100644
index 00000000..da44b5a2
--- /dev/null
+++ b/saga/saga-integration-tests/src/test/resources/application.yml
@@ -0,0 +1,48 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# Test configuration
+# External dependencies (Artemis, LRA) are managed by SagaTestResource
+
+quarkus:
+ http:
+ port: 8084
+ log:
+ console:
+ format: "%d{HH:mm:ss} %-5p [%c{2.}] %s%e%n"
+ level: INFO
+ min-level: DEBUG
+ file:
+ enable: true
+ path: target/quarkus.log
+
+camel:
+ rest:
+ context-path: /api
+ component:
+ jms:
+ test-connection-on-startup: true
+ concurrent-consumers: 5
+ lra:
+ enabled: true
+ # coordinator-url and local-participant-url are set by SagaTestResource
+
+example:
+ services:
+ train: saga-train-service
+ flight: saga-flight-service
+ payment: saga-payment-service
diff --git a/saga/saga-payment-service/pom.xml
b/saga/saga-payment-service/pom.xml
index 83d7b951..0661d46d 100644
--- a/saga/saga-payment-service/pom.xml
+++ b/saga/saga-payment-service/pom.xml
@@ -31,4 +31,21 @@
<name>Camel Quarkus :: Examples :: Saga :: Payment Service</name>
<description>Payment Service</description>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>io.smallrye</groupId>
+ <artifactId>jandex-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>make-index</id>
+ <goals>
+ <goal>jandex</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
</project>
diff --git
a/saga/saga-payment-service/src/main/java/org/apache/camel/example/saga/PaymentRoute.java
b/saga/saga-payment-service/src/main/java/org/apache/camel/example/saga/PaymentRoute.java
index 342a3d87..3b2fd20b 100644
---
a/saga/saga-payment-service/src/main/java/org/apache/camel/example/saga/PaymentRoute.java
+++
b/saga/saga-payment-service/src/main/java/org/apache/camel/example/saga/PaymentRoute.java
@@ -16,9 +16,11 @@
*/
package org.apache.camel.example.saga;
+import jakarta.enterprise.context.ApplicationScoped;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.SagaPropagation;
+@ApplicationScoped
public class PaymentRoute extends RouteBuilder {
@Override
@@ -33,7 +35,7 @@ public class PaymentRoute extends RouteBuilder {
.log("Paying ${header.payFor} for order #${header.id}")
.setBody(header("JMSCorrelationID"))
.choice()
- .when(x -> Math.random() >= 0.85)
+ .when(simple("${random(0,100)} >= 85"))
.log("Payment ${header.payFor} for saga #${header.id} fails!")
.throwException(new RuntimeException("Random failure during
payment"))
.endChoice()
diff --git a/saga/saga-train-service/pom.xml b/saga/saga-train-service/pom.xml
index 5af427e8..ef7da71b 100644
--- a/saga/saga-train-service/pom.xml
+++ b/saga/saga-train-service/pom.xml
@@ -31,4 +31,21 @@
<name>Camel Quarkus :: Examples :: Saga :: Train Service</name>
<description>Train Service</description>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>io.smallrye</groupId>
+ <artifactId>jandex-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>make-index</id>
+ <goals>
+ <goal>jandex</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
</project>
diff --git
a/saga/saga-train-service/src/main/java/org/apache/camel/example/saga/TrainRoute.java
b/saga/saga-train-service/src/main/java/org/apache/camel/example/saga/TrainRoute.java
index bf5b05a6..ef339ef4 100644
---
a/saga/saga-train-service/src/main/java/org/apache/camel/example/saga/TrainRoute.java
+++
b/saga/saga-train-service/src/main/java/org/apache/camel/example/saga/TrainRoute.java
@@ -16,9 +16,11 @@
*/
package org.apache.camel.example.saga;
+import jakarta.enterprise.context.ApplicationScoped;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.SagaPropagation;
+@ApplicationScoped
public class TrainRoute extends RouteBuilder {
@Override
@@ -27,14 +29,16 @@ public class TrainRoute extends RouteBuilder {
.saga()
.propagation(SagaPropagation.MANDATORY)
.option("id", header("id"))
- .compensation("direct:cancelPurchase")
+ .compensation("direct:cancelTrainPurchase")
.log("Buying train #${header.id}")
+ // Request timeout prevents indefinite waits during payment
service failures
.to("jms:queue:{{example.services.payment}}?exchangePattern=InOut" +
- "&replyTo={{example.services.payment}}.train.reply")
+ "&replyTo={{example.services.payment}}.train.reply" +
+ "&requestTimeout=30000")
.log("Payment for train #${header.id} done with transaction
${body}")
.end();
- from("direct:cancelPurchase")
+ from("direct:cancelTrainPurchase")
.log("Train purchase #${header.id} has been cancelled due to
payment failure");
}