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 d4688eb0bb38 CAMEL-25816: camel-hazelcast - the transacted SEDA 
consumer ends its transaction on its own thread (#28205)
d4688eb0bb38 is described below

commit d4688eb0bb38a776f428b464d86c2b613fd4d29c
Author: allthingssecurity <[email protected]>
AuthorDate: Thu Oct 22 00:36:27 2026 +0530

    CAMEL-25816: camel-hazelcast - the transacted SEDA consumer ends its 
transaction on its own thread (#28205)
    
    * CAMEL-25816: camel-hazelcast - the transacted SEDA consumer ends its 
transaction on its own thread
    
    A Hazelcast transaction can only be committed or rolled back by the thread
    that began it. When the route of a transacted hazelcast-seda consumer
    continued on another thread (an asynchronous producer such as camel-kafka,
    or threads()), the done callback tried to commit or roll back there, which
    failed with "Transaction cannot span multiple threads!" (a WARN that the
    reactive executor ignores). The message stayed reserved by a transaction
    that never ended: a failed message was not taken again, and when the
    application left the cluster the other members rolled back its open
    transactions, so every message processed since the start was put back
    into the queue and consumed again.
    
    With transacted=true the consumer thread now waits for the exchange to
    complete and commits or rolls back itself. Upgrade note in the 4.24 guide.
    
    * CAMEL-25816: camel-hazelcast - address review: release the transacted 
exchange after the commit or rollback
    
    With the pooled exchange factory an auto released exchange is reset when
    its unit of work is done, which clears its exception before the
    transacted consumer decides to commit or roll back: a failed exchange was
    committed and its message lost. The transacted consumer now creates the
    exchange without auto release and releases it after the commit or
    rollback, as camel-jpa and camel-sql do. The transacted path is chosen
    on the transacted option, so an exchange it releases is never routed
    asynchronously.
    
    * CAMEL-25816: camel-hazelcast - address review: document the transaction 
timeout, stop the context before the members in the tests
    
    The consumer begins its transaction with newTransactionContext(), which
    uses TransactionOptions.getDefault() (a 2 minute timeout), and
    TransactionImpl.commit() checks the timeout first. Now that a route that
    continues on another thread is waited for, an exchange can outlast it:
    the commit then fails with TransactionTimedOutException, the
    transaction is rolled back and the message is consumed again. The
    transacted option description (with the generated JSON and endpoint
    DSL) and the upgrade guide note say so.
    
    The tests stop the Camel context before they shut the Hazelcast members
    down (JUnit runs the @AfterEach of the test before the extension of
    CamelTestSupport stops the context), so the consumers no longer poll
    members that are gone, and the new test classes and methods are
    package-private.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/components/hazelcast-seda.json   |   2 +-
 .../component/hazelcast/seda/hazelcast-seda.json   |   2 +-
 .../hazelcast/seda/HazelcastSedaConfiguration.java |   6 +-
 .../hazelcast/seda/HazelcastSedaConsumer.java      |  66 ++++++----
 .../HazelcastSedaTransactedAsyncRouteTest.java     | 136 +++++++++++++++++++++
 .../HazelcastSedaTransactedPooledExchangeTest.java |  34 ++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  14 +++
 .../dsl/HazelcastSedaEndpointBuilderFactory.java   |  48 +++++++-
 8 files changed, 277 insertions(+), 31 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hazelcast-seda.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hazelcast-seda.json
index fc879033caaa..430f9cce531e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hazelcast-seda.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/hazelcast-seda.json
@@ -43,7 +43,7 @@
     "concurrentConsumers": { "index": 9, "kind": "parameter", "displayName": 
"Concurrent Consumers", "group": "seda", "label": "seda", "required": false, 
"type": "integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 1, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "To use concurrent 
consumers polling from the SEDA queue." },
     "onErrorDelay": { "index": 10, "kind": "parameter", "displayName": "On 
Error Delay", "group": "seda", "label": "seda", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 1000, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "Milliseconds before 
consumer continues polling after an error has occurred." },
     "pollTimeout": { "index": 11, "kind": "parameter", "displayName": "Poll 
Timeout", "group": "seda", "label": "seda", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 1000, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "The timeout used when 
consuming from the SEDA queue. When a timeout occurs, the consum [...]
-    "transacted": { "index": 12, "kind": "parameter", "displayName": 
"Transacted", "group": "seda", "label": "seda", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "If set to true then the 
consumer runs in transaction mode, where the messages in the [...]
+    "transacted": { "index": 12, "kind": "parameter", "displayName": 
"Transacted", "group": "seda", "label": "seda", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "If set to true then the 
consumer runs in transaction mode, where the messages in the [...]
     "transferExchange": { "index": 13, "kind": "parameter", "displayName": 
"Transfer Exchange", "group": "seda", "label": "seda", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "security": "insecure:serialization", "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "If set to true the whole 
Exchange [...]
   }
 }
diff --git 
a/components/camel-hazelcast/src/generated/resources/META-INF/org/apache/camel/component/hazelcast/seda/hazelcast-seda.json
 
b/components/camel-hazelcast/src/generated/resources/META-INF/org/apache/camel/component/hazelcast/seda/hazelcast-seda.json
index fc879033caaa..430f9cce531e 100644
--- 
a/components/camel-hazelcast/src/generated/resources/META-INF/org/apache/camel/component/hazelcast/seda/hazelcast-seda.json
+++ 
b/components/camel-hazelcast/src/generated/resources/META-INF/org/apache/camel/component/hazelcast/seda/hazelcast-seda.json
@@ -43,7 +43,7 @@
     "concurrentConsumers": { "index": 9, "kind": "parameter", "displayName": 
"Concurrent Consumers", "group": "seda", "label": "seda", "required": false, 
"type": "integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 1, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "To use concurrent 
consumers polling from the SEDA queue." },
     "onErrorDelay": { "index": 10, "kind": "parameter", "displayName": "On 
Error Delay", "group": "seda", "label": "seda", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 1000, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "Milliseconds before 
consumer continues polling after an error has occurred." },
     "pollTimeout": { "index": 11, "kind": "parameter", "displayName": "Poll 
Timeout", "group": "seda", "label": "seda", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": 1000, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "The timeout used when 
consuming from the SEDA queue. When a timeout occurs, the consum [...]
-    "transacted": { "index": 12, "kind": "parameter", "displayName": 
"Transacted", "group": "seda", "label": "seda", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "If set to true then the 
consumer runs in transaction mode, where the messages in the [...]
+    "transacted": { "index": 12, "kind": "parameter", "displayName": 
"Transacted", "group": "seda", "label": "seda", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "If set to true then the 
consumer runs in transaction mode, where the messages in the [...]
     "transferExchange": { "index": 13, "kind": "parameter", "displayName": 
"Transfer Exchange", "group": "seda", "label": "seda", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "security": "insecure:serialization", "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.hazelcast.seda.HazelcastSedaConfiguration", 
"configurationField": "configuration", "description": "If set to true the whole 
Exchange [...]
   }
 }
diff --git 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConfiguration.java
 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConfiguration.java
index 4ad12767cc17..9c5be6dcd05d 100644
--- 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConfiguration.java
+++ 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConfiguration.java
@@ -121,7 +121,11 @@ public class HazelcastSedaConfiguration {
 
     /**
      * If set to true then the consumer runs in transaction mode, where the 
messages in the seda queue will only be
-     * removed if the transaction commits, which happens when the processing 
is complete.
+     * removed if the transaction commits, which happens when the processing 
is complete. A Hazelcast transaction
+     * belongs to the thread that began it, so each consumer thread waits for 
its exchange to complete, also when the
+     * route continues on another thread, before it takes the next message. 
The exchange must complete within the
+     * Hazelcast transaction timeout (2 minutes by default): when it takes 
longer, the commit fails, the transaction is
+     * rolled back and the message is consumed again.
      */
     public void setTransacted(boolean transacted) {
         this.transacted = transacted;
diff --git 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConsumer.java
 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConsumer.java
index a444a6c19d42..361ae8759d04 100644
--- 
a/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConsumer.java
+++ 
b/components/camel-hazelcast/src/main/java/org/apache/camel/component/hazelcast/seda/HazelcastSedaConsumer.java
@@ -87,11 +87,14 @@ public class HazelcastSedaConsumer extends DefaultConsumer 
implements Runnable {
         BaseQueue<?> queue = 
endpoint.getHazelcastInstance().getQueue(endpoint.getConfiguration().getQueueName());
 
         while (queue != null && isRunAllowed()) {
-            final Exchange exchange = createExchange(true);
+            final boolean transacted = 
endpoint.getConfiguration().isTransacted();
+            // the transacted consumer reads the outcome of the exchange after 
the routing is done, so it releases the
+            // exchange itself: a pooled exchange is otherwise reset (and its 
exception cleared) when it is done
+            final Exchange exchange = createExchange(!transacted);
 
             TransactionContext transactionCtx = null;
             try {
-                if (endpoint.getConfiguration().isTransacted()) {
+                if (transacted) {
                     // Get and begin transaction if exist
                     transactionCtx = 
endpoint.getHazelcastInstance().newTransactionContext();
 
@@ -112,27 +115,24 @@ public class HazelcastSedaConsumer extends 
DefaultConsumer implements Runnable {
                     }
                     try {
                         final TransactionContext txc = transactionCtx;
-                        // process using the asynchronous routing engine
-                        processor.process(exchange, new AsyncCallback() {
-                            public void done(boolean asyncDone) {
-                                // a rollback only exchange (without an 
exception) did not complete successfully either
-                                boolean rollback = exchange.getException() != 
null || exchange.isRollbackOnly()
-                                        || exchange.isRollbackOnlyLast();
-                                if (rollback && txc != null) {
-                                    LOG.trace("Rollback transaction: {}", 
txc.getTxnId());
-                                    txc.rollbackTransaction();
-                                }
-                                if (exchange.getException() != null) {
-                                    
getExceptionHandler().handleException("Error processing exchange", exchange,
-                                            exchange.getException());
-                                }
-                                // It's OK, I commit
-                                if (!rollback && txc != null) {
-                                    LOG.trace("Commit transaction: {}", 
txc.getTxnId());
-                                    txc.commitTransaction();
-                                }
+                        if (transacted) {
+                            // a Hazelcast transaction belongs to the thread 
that began it, and cannot be committed
+                            // or rolled back from another thread: wait for 
the exchange to complete, also when the
+                            // route continues on another thread, and end the 
transaction on this thread
+                            try {
+                                processor.process(exchange);
+                            } catch (Exception e) {
+                                exchange.setException(e);
                             }
-                        });
+                            onExchangeDone(exchange, txc);
+                        } else {
+                            // process using the asynchronous routing engine
+                            processor.process(exchange, new AsyncCallback() {
+                                public void done(boolean asyncDone) {
+                                    onExchangeDone(exchange, null);
+                                }
+                            });
+                        }
                     } catch (Exception e) {
                         LOG.error("Hzlq Exception caught: {}", e, e);
                         // Rollback
@@ -175,8 +175,30 @@ public class HazelcastSedaConsumer extends DefaultConsumer 
implements Runnable {
                         .withName("HazelcastSedaErrorRecoveryDelay")
                         .build()
                         .run(getEndpoint().getCamelContext(), () -> true);
+            } finally {
+                if (transacted) {
+                    releaseExchange(exchange, false);
+                }
             }
         }
     }
 
+    private void onExchangeDone(Exchange exchange, TransactionContext txc) {
+        // a rollback only exchange (without an exception) did not complete 
successfully either
+        boolean rollback = exchange.getException() != null || 
exchange.isRollbackOnly()
+                || exchange.isRollbackOnlyLast();
+        if (rollback && txc != null) {
+            LOG.trace("Rollback transaction: {}", txc.getTxnId());
+            txc.rollbackTransaction();
+        }
+        if (exchange.getException() != null) {
+            getExceptionHandler().handleException("Error processing exchange", 
exchange, exchange.getException());
+        }
+        // It's OK, I commit
+        if (!rollback && txc != null) {
+            LOG.trace("Commit transaction: {}", txc.getTxnId());
+            txc.commitTransaction();
+        }
+    }
+
 }
diff --git 
a/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaTransactedAsyncRouteTest.java
 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaTransactedAsyncRouteTest.java
new file mode 100644
index 000000000000..5ba93dfaae51
--- /dev/null
+++ 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaTransactedAsyncRouteTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.component.hazelcast;
+
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.hazelcast.config.Config;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A transacted Hazelcast SEDA consumer whose route continues on another 
thread (here {@code threads()}, as with any
+ * asynchronous producer). A Hazelcast transaction can only be committed or 
rolled back by the thread that began it, so
+ * the consumer must end the transaction on its own thread once the exchange 
is complete.
+ */
+class HazelcastSedaTransactedAsyncRouteTest extends CamelTestSupport {
+
+    private final AtomicInteger attempts = new AtomicInteger();
+    private HazelcastInstance member;
+    private HazelcastInstance otherMember;
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        String clusterName = "seda-transacted-async-" + UUID.randomUUID();
+        otherMember = 
Hazelcast.newHazelcastInstance(memberConfig(clusterName));
+        member = Hazelcast.newHazelcastInstance(memberConfig(clusterName));
+        await().atMost(30, TimeUnit.SECONDS).until(() -> 
member.getCluster().getMembers().size() == 2);
+
+        CamelContext context = super.createCamelContext();
+        context.getRegistry().bind("hazelcastInstance", member);
+        return context;
+    }
+
+    private static Config memberConfig(String clusterName) {
+        Config config = new Config();
+        config.setClusterName(clusterName);
+        config.getMetricsConfig().setEnabled(false);
+        
config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false);
+        
config.getNetworkConfig().getJoin().getAutoDetectionConfig().setEnabled(false);
+        
config.getNetworkConfig().getJoin().getTcpIpConfig().setEnabled(true).addMember("127.0.0.1");
+        return config;
+    }
+
+    @AfterEach
+    void shutdownMembers() {
+        // JUnit runs this before the extension of CamelTestSupport stops the 
context: stop it first, so that the
+        // consumers do not keep polling members that are shut down
+        context.stop();
+        if (member != null) {
+            member.shutdown();
+        }
+        if (otherMember != null) {
+            otherMember.shutdown();
+        }
+    }
+
+    @Test
+    void testCompletedExchangesAreCommitted() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:done");
+        mock.expectedBodiesReceivedInAnyOrder("a", "b", "c");
+
+        member.getQueue("done").offer("a");
+        member.getQueue("done").offer("b");
+        member.getQueue("done").offer("c");
+
+        mock.assertIsSatisfied();
+
+        // the application leaves the cluster (a restart or a redeploy): the 
other member rolls back the transactions
+        // the application left open, which puts their messages back into the 
queue
+        context.stop();
+        member.shutdown();
+        member = null;
+
+        await().during(3, TimeUnit.SECONDS).atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertEquals(0, 
otherMember.getQueue("done").size(),
+                        "The processed messages were not removed from the 
queue"));
+    }
+
+    @Test
+    void testFailedExchangeIsRolledBack() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:failed");
+        mock.expectedBodiesReceived("x");
+
+        member.getQueue("failed").offer("x");
+
+        // the first attempt fails and must be rolled back, so the message is 
taken again
+        mock.assertIsSatisfied();
+        assertEquals(2, attempts.get());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("hazelcast-seda:done?transacted=true&hazelcastInstance=#hazelcastInstance").routeId("done")
+                        .threads(1)
+                        .to("mock:done");
+
+                
from("hazelcast-seda:failed?transacted=true&hazelcastInstance=#hazelcastInstance").routeId("failed")
+                        .threads(1)
+                        .process(exchange -> {
+                            if (attempts.incrementAndGet() == 1) {
+                                throw new IllegalStateException("Forced 
failure of the first attempt");
+                            }
+                        })
+                        .to("mock:failed");
+            }
+        };
+    }
+}
diff --git 
a/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaTransactedPooledExchangeTest.java
 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaTransactedPooledExchangeTest.java
new file mode 100644
index 000000000000..d938bb5b8955
--- /dev/null
+++ 
b/components/camel-hazelcast/src/test/java/org/apache/camel/component/hazelcast/HazelcastSedaTransactedPooledExchangeTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.component.hazelcast;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+
+/**
+ * The same transacted routes with pooled exchanges: a pooled exchange is 
reset when it is done, so the consumer must
+ * read the outcome of the exchange before it is released, or a failed 
exchange would be committed.
+ */
+class HazelcastSedaTransactedPooledExchangeTest extends 
HazelcastSedaTransactedAsyncRouteTest {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.getCamelContextExtension().setExchangeFactory(new 
PooledExchangeFactory());
+        return context;
+    }
+}
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 5587787d422e..94e1f7e5073f 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
@@ -945,6 +945,20 @@ kept apart. A valid token that has none of these claims is 
now rejected with HTT
 * The `CamelOAuthTokenValidationResult` exchange property is now set on the 
exchange that the route processes, as
 documented. Before, it was only set on the incoming HTTP exchange.
 
+=== camel-hazelcast - the transacted SEDA consumer waits for an exchange that 
continues on another thread
+
+A Hazelcast transaction can only be committed or rolled back by the thread 
that began it. When the route of a
+transacted (`transacted=true`) Hazelcast SEDA consumer continued on another 
thread (an asynchronous producer, or
+`threads()`), the consumer tried to commit or roll back the transaction on 
that thread, which failed with
+`Transaction cannot span multiple threads!` (logged as a WARN and ignored). 
The message was neither removed nor
+put back: a failed message was not taken again, and the messages, also the 
processed ones, were put back into the
+queue when the application left the Hazelcast cluster, so they were consumed 
again. Now the consumer thread waits for
+the exchange to complete and ends the transaction itself, so it takes the next 
message only then; use
+`concurrentConsumers` to process several messages at the same time. The 
exchange must complete within the Hazelcast
+transaction timeout (2 minutes by default, `TransactionOptions`): when it 
takes longer, the commit fails with
+`TransactionTimedOutException`, which is logged at ERROR, the transaction is 
rolled back and the message is consumed
+again, also when the route succeeded.
+
 === camel-sql, camel-mybatis, camel-jooq - an exchange marked rollback only 
last is not consumed either
 
 The SQL, MyBatis and jOOQ consumers already left a message unconsumed when the 
route marked the exchange rollback
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HazelcastSedaEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HazelcastSedaEndpointBuilderFactory.java
index dad4ad4a5003..75ae9dc56bd9 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HazelcastSedaEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/HazelcastSedaEndpointBuilderFactory.java
@@ -244,7 +244,13 @@ public interface HazelcastSedaEndpointBuilderFactory {
         /**
          * If set to true then the consumer runs in transaction mode, where the
          * messages in the seda queue will only be removed if the transaction
-         * commits, which happens when the processing is complete.
+         * commits, which happens when the processing is complete. A Hazelcast
+         * transaction belongs to the thread that began it, so each consumer
+         * thread waits for its exchange to complete, also when the route
+         * continues on another thread, before it takes the next message. The
+         * exchange must complete within the Hazelcast transaction timeout (2
+         * minutes by default): when it takes longer, the commit fails, the
+         * transaction is rolled back and the message is consumed again.
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -261,7 +267,13 @@ public interface HazelcastSedaEndpointBuilderFactory {
         /**
          * If set to true then the consumer runs in transaction mode, where the
          * messages in the seda queue will only be removed if the transaction
-         * commits, which happens when the processing is complete.
+         * commits, which happens when the processing is complete. A Hazelcast
+         * transaction belongs to the thread that began it, so each consumer
+         * thread waits for its exchange to complete, also when the route
+         * continues on another thread, before it takes the next message. The
+         * exchange must complete within the Hazelcast transaction timeout (2
+         * minutes by default): when it takes longer, the commit fails, the
+         * transaction is rolled back and the message is consumed again.
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 
@@ -647,7 +659,13 @@ public interface HazelcastSedaEndpointBuilderFactory {
         /**
          * If set to true then the consumer runs in transaction mode, where the
          * messages in the seda queue will only be removed if the transaction
-         * commits, which happens when the processing is complete.
+         * commits, which happens when the processing is complete. A Hazelcast
+         * transaction belongs to the thread that began it, so each consumer
+         * thread waits for its exchange to complete, also when the route
+         * continues on another thread, before it takes the next message. The
+         * exchange must complete within the Hazelcast transaction timeout (2
+         * minutes by default): when it takes longer, the commit fails, the
+         * transaction is rolled back and the message is consumed again.
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -664,7 +682,13 @@ public interface HazelcastSedaEndpointBuilderFactory {
         /**
          * If set to true then the consumer runs in transaction mode, where the
          * messages in the seda queue will only be removed if the transaction
-         * commits, which happens when the processing is complete.
+         * commits, which happens when the processing is complete. A Hazelcast
+         * transaction belongs to the thread that began it, so each consumer
+         * thread waits for its exchange to complete, also when the route
+         * continues on another thread, before it takes the next message. The
+         * exchange must complete within the Hazelcast transaction timeout (2
+         * minutes by default): when it takes longer, the commit fails, the
+         * transaction is rolled back and the message is consumed again.
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 
@@ -979,7 +1003,13 @@ public interface HazelcastSedaEndpointBuilderFactory {
         /**
          * If set to true then the consumer runs in transaction mode, where the
          * messages in the seda queue will only be removed if the transaction
-         * commits, which happens when the processing is complete.
+         * commits, which happens when the processing is complete. A Hazelcast
+         * transaction belongs to the thread that began it, so each consumer
+         * thread waits for its exchange to complete, also when the route
+         * continues on another thread, before it takes the next message. The
+         * exchange must complete within the Hazelcast transaction timeout (2
+         * minutes by default): when it takes longer, the commit fails, the
+         * transaction is rolled back and the message is consumed again.
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -996,7 +1026,13 @@ public interface HazelcastSedaEndpointBuilderFactory {
         /**
          * If set to true then the consumer runs in transaction mode, where the
          * messages in the seda queue will only be removed if the transaction
-         * commits, which happens when the processing is complete.
+         * commits, which happens when the processing is complete. A Hazelcast
+         * transaction belongs to the thread that began it, so each consumer
+         * thread waits for its exchange to complete, also when the route
+         * continues on another thread, before it takes the next message. The
+         * exchange must complete within the Hazelcast transaction timeout (2
+         * minutes by default): when it takes longer, the commit fails, the
+         * transaction is rolled back and the message is consumed again.
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 

Reply via email to