This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new d201af225932 CAMEL-25698: camel-jta - roll back the transaction of an
exchange marked rollback only last (#27983)
d201af225932 is described below
commit d201af225932ea6e8c7c8e71fc9516f3eb27ec9b
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 17 14:43:42 2026 +0530
CAMEL-25698: camel-jta - roll back the transaction of an exchange marked
rollback only last (#27983)
The JTA TransactionErrorHandler rolled the transaction back only when the
exchange had an exception or was marked rollback only.
markRollbackOnlyLast()
sets only the rollback only last flag, so the transaction was committed, and
the flag and exception were then cleared, so nothing showed that the
rollback
was skipped.
Roll back when the exchange is marked rollback only last too, as the
camel-spring TransactionErrorHandler does since CAMEL-17105. The flag is
still
cleared after the transaction, so an outer transacted() block does not see
it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/jta/TransactionErrorHandler.java | 4 +-
...ransactionErrorHandlerRollbackOnlyLastTest.java | 125 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 +++
3 files changed, 140 insertions(+), 2 deletions(-)
diff --git
a/components/camel-jta/src/main/java/org/apache/camel/jta/TransactionErrorHandler.java
b/components/camel-jta/src/main/java/org/apache/camel/jta/TransactionErrorHandler.java
index 30a928a6e58d..4246d1060f8d 100644
---
a/components/camel-jta/src/main/java/org/apache/camel/jta/TransactionErrorHandler.java
+++
b/components/camel-jta/src/main/java/org/apache/camel/jta/TransactionErrorHandler.java
@@ -211,8 +211,8 @@ public class TransactionErrorHandler extends
ErrorHandlerSupport
}
// after handling and still an exception or marked as rollback
- // only then rollback
- if (exchange.getException() != null ||
exchange.isRollbackOnly()) {
+ // only (or rollback only last, this transaction) then rollback
+ if (exchange.getException() != null ||
exchange.isRollbackOnly() || exchange.isRollbackOnlyLast()) {
// wrap exception in transacted exception
if (exchange.getException() != null) {
diff --git
a/components/camel-jta/src/test/java/org/apache/camel/jta/TransactionErrorHandlerRollbackOnlyLastTest.java
b/components/camel-jta/src/test/java/org/apache/camel/jta/TransactionErrorHandlerRollbackOnlyLastTest.java
new file mode 100644
index 000000000000..3d217dda6627
--- /dev/null
+++
b/components/camel-jta/src/test/java/org/apache/camel/jta/TransactionErrorHandlerRollbackOnlyLastTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.jta;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.Processor;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.engine.DefaultUnitOfWork;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * An exchange marked with markRollbackOnlyLast() (no exception) must roll
back the transaction of the
+ * TransactionErrorHandler, as the camel-spring TransactionErrorHandler does
(CAMEL-17105), and the marker must then be
+ * cleared so that an outer transaction is not affected.
+ */
+class TransactionErrorHandlerRollbackOnlyLastTest {
+
+ private CamelContext camelContext;
+ private final AtomicInteger commits = new AtomicInteger();
+ private final AtomicInteger rollbacks = new AtomicInteger();
+
+ @BeforeEach
+ void setUp() throws Exception {
+ camelContext = new DefaultCamelContext();
+ camelContext.start();
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (camelContext != null) {
+ camelContext.stop();
+ }
+ }
+
+ private JtaTransactionPolicy createPolicy() {
+ // a policy that records whether the transaction would be committed or
rolled back
+ return new JtaTransactionPolicy() {
+ @Override
+ public void run(Runnable runnable) throws Throwable {
+ try {
+ runnable.run();
+ } catch (Throwable t) {
+ rollbacks.incrementAndGet();
+ throw t;
+ }
+ commits.incrementAndGet();
+ }
+ };
+ }
+
+ private TransactionErrorHandler createErrorHandler(Processor processor) {
+ TransactionErrorHandler answer
+ = new TransactionErrorHandler(camelContext, processor,
createPolicy(), LoggingLevel.WARN);
+ answer.start();
+ return answer;
+ }
+
+ private Exchange createExchange() {
+ Exchange exchange =
camelContext.getEndpoint("direct:test").createExchange();
+ exchange.getExchangeExtension().setUnitOfWork(new
DefaultUnitOfWork(exchange));
+ return exchange;
+ }
+
+ @Test
+ void markRollbackOnlyLastRollsBackTheTransaction() throws Exception {
+ TransactionErrorHandler handler = createErrorHandler(exchange ->
exchange.setRollbackOnlyLast(true));
+ try {
+ Exchange exchange = createExchange();
+ handler.process(exchange);
+
+ assertEquals(1, rollbacks.get(), "The transaction should be rolled
back");
+ assertEquals(0, commits.get(), "The transaction should not be
committed");
+ // rollback only last affects only this transaction: an outer
transaction does not see the marker
+ assertFalse(exchange.isRollbackOnlyLast());
+ assertNull(exchange.getException());
+ } finally {
+ handler.stop();
+ }
+ }
+
+ @Test
+ void markRollbackOnlyLastInTransactedRouteRollsBackTheTransaction() throws
Exception {
+ camelContext.getRegistry().bind("recordingPolicy", createPolicy());
+ camelContext.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:start")
+ .transacted("recordingPolicy")
+ .setBody(constant("done"))
+ .markRollbackOnlyLast();
+ }
+ });
+
+ Exchange result =
camelContext.createProducerTemplate().send("direct:start", e ->
e.getIn().setBody("Hello"));
+
+ assertEquals(1, rollbacks.get(), "The transaction should be rolled
back");
+ assertEquals(0, commits.get(), "The transaction should not be
committed");
+ assertNull(result.getException());
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index ff8694f11a3f..890e3d79c5fb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -713,6 +713,19 @@ again. With `transacted=true` the SQL and MyBatis
consumers break out of the bat
`markRollbackOnlyLast()` reaches the consumer only when no `transacted()` EIP
has consumed the flag, so the
consumer's own transaction is the last one.
+=== camel-jta - an exchange marked rollback only last rolls back its
transaction
+
+In a `transacted()` route that uses a JTA transaction manager,
`markRollbackOnlyLast()` now rolls back the
+transaction of that `transacted()` block, as it already did with the
camel-spring transaction error handler. Before,
+the transaction was committed, and the marker was then cleared, so nothing
showed that the rollback was skipped.
+The marker is still cleared afterwards, so an outer `transacted()` block does
not see it.
+
+If the `transacted()` block in which the exchange is marked rollback only last
joins the transaction of an outer block
+(for example a `PROPAGATION_MANDATORY` block inside a `PROPAGATION_REQUIRED`
block), that shared JTA transaction is now
+marked rollback only, so the commit of the outer block fails (in Camel Quarkus
with `CamelException: Unable to commit transaction`) where it used
+to commit, as with camel-spring (`UnexpectedRollbackException`). To roll back
only the inner block, use
+`PROPAGATION_REQUIRES_NEW` for it.
+
=== camel-telegram - webhook requests are verified with a secret token
In webhook mode, Camel now registers the webhook with a secret token, which
Telegram sends in the