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

Reply via email to