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 06f69193642a CAMEL-25527: camel-mybatis, camel-sql - do not run
onConsume for a failed or rollback only exchange (#27677)
06f69193642a is described below
commit 06f69193642a88ab7dab16d635620e435369efee
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 11 12:31:36 2026 +0530
CAMEL-25527: camel-mybatis, camel-sql - do not run onConsume for a failed
or rollback only exchange (#27677)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/catalog/docs/mybatis-component.adoc | 5 +
.../src/main/docs/mybatis-component.adoc | 5 +
.../camel/component/mybatis/MyBatisConsumer.java | 10 +-
.../MyBatisOnConsumeFailedPooledExchangeTest.java | 34 ++++++
.../mybatis/MyBatisOnConsumeFailedTest.java | 129 +++++++++++++++++++++
.../apache/camel/component/sql/SqlConsumer.java | 20 ++--
.../SqlConsumerRollbackOnlyPooledExchangeTest.java | 34 ++++++
.../component/sql/SqlConsumerRollbackOnlyTest.java | 124 ++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 9 ++
9 files changed, 359 insertions(+), 11 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc
index e457330f334c..34c9939ba20b 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/mybatis-component.adoc
@@ -607,6 +607,11 @@ And the statements in the sqlmap file:
</update>
----
+The statements only run when the exchange completed successfully. If the
exchange failed (or was marked rollback
+only), they are not run and the row is consumed again by the next poll, so a
row that always fails is consumed again at
+every poll. Handle the exception in the route (for example with
`onException(...).handled(true)`) to run the statements
+for a failed exchange anyway.
+
=== Participating in transactions
Setting up a transaction manager under camel-mybatis can be a little bit
diff --git a/components/camel-mybatis/src/main/docs/mybatis-component.adoc
b/components/camel-mybatis/src/main/docs/mybatis-component.adoc
index e457330f334c..34c9939ba20b 100644
--- a/components/camel-mybatis/src/main/docs/mybatis-component.adoc
+++ b/components/camel-mybatis/src/main/docs/mybatis-component.adoc
@@ -607,6 +607,11 @@ And the statements in the sqlmap file:
</update>
----
+The statements only run when the exchange completed successfully. If the
exchange failed (or was marked rollback
+only), they are not run and the row is consumed again by the next poll, so a
row that always fails is consumed again at
+every poll. Handle the exception in the route (for example with
`onException(...).handled(true)`) to run the statements
+for a failed exchange anyway.
+
=== Participating in transactions
Setting up a transaction manager under camel-mybatis can be a little bit
diff --git
a/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisConsumer.java
b/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisConsumer.java
index 6bf0e4e35a32..6622f86c846a 100644
---
a/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisConsumer.java
+++
b/components/camel-mybatis/src/main/java/org/apache/camel/component/mybatis/MyBatisConsumer.java
@@ -136,18 +136,22 @@ public class MyBatisConsumer extends
ScheduledBatchPollingConsumer {
try {
getProcessor().process(exchange);
- if (onConsume != null) {
+ // only consume the data when the exchange was processed
successfully: a failed or rollback only
+ // exchange leaves the data as it is, so the next poll
consumes it again
+ if (onConsume != null && !exchange.isFailed() &&
!exchange.isRollbackOnly()) {
endpoint.getProcessingStrategy().commit(endpoint,
exchange, data, onConsume);
}
} catch (Exception e) {
handleException(e);
}
- if (getEndpoint().isTransacted() && exchange.isFailed()) {
+ if (getEndpoint().isTransacted() && (exchange.isFailed() ||
exchange.isRollbackOnly())) {
// break out as we are transacted and should rollback
cause = exchange.getException();
if (cause == null) {
- cause = new RollbackExchangeException("Rollback
transaction due error processing exchange", null);
+ // rollback only, there was no error (the exception must
be created before the exchange is released)
+ cause = new RollbackExchangeException(
+ "Rollback transaction as the exchange was marked
as rollback only", exchange);
}
}
releaseExchange(exchange, false);
diff --git
a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisOnConsumeFailedPooledExchangeTest.java
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisOnConsumeFailedPooledExchangeTest.java
new file mode 100644
index 000000000000..3da28b205295
--- /dev/null
+++
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisOnConsumeFailedPooledExchangeTest.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.mybatis;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+
+/**
+ * The same cases with the pooled exchange factory, which resets an exchange
when it is released: the consumer must read
+ * the outcome of the exchange before it releases it.
+ */
+class MyBatisOnConsumeFailedPooledExchangeTest extends
MyBatisOnConsumeFailedTest {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext camelContext = super.createCamelContext();
+ camelContext.getCamelContextExtension().setExchangeFactory(new
PooledExchangeFactory());
+ return camelContext;
+ }
+}
diff --git
a/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisOnConsumeFailedTest.java
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisOnConsumeFailedTest.java
new file mode 100644
index 000000000000..0731a9656bb7
--- /dev/null
+++
b/components/camel-mybatis/src/test/java/org/apache/camel/component/mybatis/MyBatisOnConsumeFailedTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.mybatis;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.RollbackExchangeException;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The onConsume statements must only run for a row whose exchange was
processed successfully: a failed or rollback only
+ * exchange must leave the row unprocessed, so the next poll consumes it again.
+ */
+class MyBatisOnConsumeFailedTest extends MyBatisTestSupport {
+
+ @Override
+ protected boolean createTestData() {
+ return false;
+ }
+
+ @Override
+ protected String getCreateStatement() {
+ return "create table ACCOUNT (ACC_ID INTEGER, ACC_FIRST_NAME
VARCHAR(255), ACC_LAST_NAME VARCHAR(255), ACC_EMAIL VARCHAR(255), PROCESSED
BOOLEAN DEFAULT false)";
+ }
+
+ @Test
+ void testFailedExchangeIsNotConsumed() throws Exception {
+ insertAccounts(account(1, "ok"), account(2, "fail"));
+
+ MyBatisConsumer consumer = (MyBatisConsumer)
context.getRoute("on-consume").getConsumer();
+ // one poll in the test thread (the scheduler is not started)
+ assertEquals(2, consumer.poll());
+
+ assertEquals(List.of(2), unprocessedAccountIds(), "only the
successfully processed account must be consumed");
+ }
+
+ @Test
+ void testRollbackOnlyExchangeIsNotConsumed() throws Exception {
+ insertAccounts(account(1, "ok"), account(2, "rollback"));
+
+ MyBatisConsumer consumer = (MyBatisConsumer)
context.getRoute("on-consume").getConsumer();
+ assertEquals(2, consumer.poll());
+
+ assertEquals(List.of(2), unprocessedAccountIds(),
+ "an account whose exchange was marked rollback only must not
be consumed");
+ }
+
+ @Test
+ void testTransactedRollbackOnlyExchangeBreaksOutOfBatch() throws Exception
{
+ insertAccounts(account(1, "ok"), account(2, "rollback"), account(3,
"ok"));
+
+ MyBatisConsumer consumer = (MyBatisConsumer)
context.getRoute("transacted").getConsumer();
+ RollbackExchangeException e =
assertThrows(RollbackExchangeException.class, consumer::poll,
+ "a rollback only exchange must break out of a transacted
batch, as a failed exchange does");
+ // there was no error, and the message (logged by the consumer at
every such poll) must not say so
+ assertTrue(e.getMessage().startsWith("Rollback transaction as the
exchange was marked as rollback only"),
+ e.getMessage());
+
+ assertEquals(List.of(2, 3), unprocessedAccountIds(),
+ "neither the rollback only account nor the accounts after it
must be consumed");
+ assertEquals(1,
getMockEndpoint("mock:processed").getReceivedCounter());
+ }
+
+ private void insertAccounts(Account... accounts) {
+ template.sendBody("mybatis:insertAccount?statementType=Insert",
accounts);
+ }
+
+ private static Account account(int id, String lastName) {
+ Account account = new Account();
+ account.setId(id);
+ account.setFirstName("Account" + id);
+ account.setLastName(lastName);
+ account.setEmailAddress("account" + id + "@example.com");
+ return account;
+ }
+
+ private List<Integer> unprocessedAccountIds() {
+ List<?> accounts =
template.requestBody("mybatis:selectUnprocessedAccounts?statementType=SelectList",
null,
+ List.class);
+ List<Integer> ids = new ArrayList<>();
+ for (Object account : accounts) {
+ ids.add(((Account) account).getId());
+ }
+ return ids;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+
consume("mybatis:selectUnprocessedAccounts?onConsume=consumeAccount&startScheduler=false",
"on-consume");
+
consume("mybatis:selectUnprocessedAccounts?onConsume=consumeAccount&transacted=true&startScheduler=false",
+ "transacted");
+ }
+
+ private void consume(String uri, String routeId) {
+ from(uri).routeId(routeId)
+ .choice().when(simple("${body.lastName} ==
'rollback'")).markRollbackOnly().end()
+ .process(exchange -> {
+ Account account =
exchange.getIn().getBody(Account.class);
+ if ("fail".equals(account.getLastName())) {
+ throw new IllegalStateException("Simulated
failure for account " + account.getId());
+ }
+ })
+ .to("mock:processed");
+ }
+ };
+ }
+}
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConsumer.java
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConsumer.java
index 2a5c732dc4f3..dc03eb13005b 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConsumer.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/component/sql/SqlConsumer.java
@@ -278,20 +278,24 @@ public class SqlConsumer extends
ScheduledBatchPollingConsumer {
exchange.setException(e);
}
- if (getEndpoint().isTransacted() && exchange.isFailed()) {
+ // a rollback only exchange (without an exception) did not
complete successfully either
+ boolean failed = exchange.isFailed() || exchange.isRollbackOnly();
+
+ if (getEndpoint().isTransacted() && failed) {
// break out as we are transacted and should rollback
Exception cause = exchange.getException();
+ if (cause == null) {
+ // rollback only, there was no error (the exception must
be created before the exchange is released)
+ cause = new RollbackExchangeException(
+ "Rollback transaction as the exchange was marked
as rollback only", exchange);
+ }
// must release exchange
releaseExchange(exchange, false);
- if (cause != null) {
- throw cause;
- } else {
- throw new RollbackExchangeException("Rollback transaction
due error processing exchange", null);
- }
+ throw cause;
}
// pick the on consume to use
- String sql = exchange.isFailed() ? onConsumeFailed : onConsume;
+ String sql = failed ? onConsumeFailed : onConsume;
try {
// we can only run on consume if there was data
if (data != null && sql != null) {
@@ -360,7 +364,7 @@ public class SqlConsumer extends
ScheduledBatchPollingConsumer {
}
/**
- * Sets a SQL to execute when the row failed being processed.
+ * Sets a SQL to execute when the row failed being processed (the exchange
failed or was marked rollback only).
*/
public void setOnConsumeFailed(String onConsumeFailed) {
this.onConsumeFailed = onConsumeFailed;
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlConsumerRollbackOnlyPooledExchangeTest.java
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlConsumerRollbackOnlyPooledExchangeTest.java
new file mode 100644
index 000000000000..3164de8b51b4
--- /dev/null
+++
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlConsumerRollbackOnlyPooledExchangeTest.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.sql;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.PooledExchangeFactory;
+
+/**
+ * The same cases with the pooled exchange factory, which resets an exchange
when it is released: the consumer must read
+ * the outcome of the exchange before it releases it.
+ */
+class SqlConsumerRollbackOnlyPooledExchangeTest extends
SqlConsumerRollbackOnlyTest {
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext camelContext = super.createCamelContext();
+ camelContext.getCamelContextExtension().setExchangeFactory(new
PooledExchangeFactory());
+ return camelContext;
+ }
+}
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlConsumerRollbackOnlyTest.java
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlConsumerRollbackOnlyTest.java
new file mode 100644
index 000000000000..e125ab09c4ca
--- /dev/null
+++
b/components/camel-sql/src/test/java/org/apache/camel/component/sql/SqlConsumerRollbackOnlyTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.sql;
+
+import java.util.List;
+
+import org.apache.camel.RollbackExchangeException;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A row whose exchange was marked rollback only (without an exception) did
not complete successfully: it must get
+ * onConsumeFailed instead of onConsume (no statement when onConsumeFailed is
not set), and it must break out of a
+ * transacted batch as a failed exchange does.
+ */
+class SqlConsumerRollbackOnlyTest extends CamelTestSupport {
+
+ private static final String ON_CONSUME = "&onConsume=update projects set
license = 'DONE' where id = :#id";
+ private static final String ON_CONSUME_FAILED = "&onConsumeFailed=update
projects set license = 'BAD' where id = :#id";
+
+ private EmbeddedDatabase db;
+ private JdbcTemplate jdbcTemplate;
+
+ @Override
+ public void doPreSetup() throws Exception {
+ db = new EmbeddedDatabaseBuilder()
+ .setName(getClass().getSimpleName())
+ .setType(EmbeddedDatabaseType.H2)
+ .addScript("sql/createAndPopulateDatabase.sql").build();
+
+ jdbcTemplate = new JdbcTemplate(db);
+ }
+
+ @Override
+ public void doPostTearDown() throws Exception {
+ if (db != null) {
+ db.shutdown();
+ }
+ }
+
+ @Test
+ void testRollbackOnlyExchangeRunsOnConsumeFailed() throws Exception {
+ SqlConsumer consumer = (SqlConsumer)
context.getRoute("on-consume").getConsumer();
+ // one poll in the test thread (the scheduler is not started)
+ assertEquals(3, consumer.poll());
+
+ assertEquals(List.of("DONE", "BAD", "DONE"), licenses(),
+ "the AMQ row, whose exchange was marked rollback only, must
get onConsumeFailed");
+ }
+
+ @Test
+ void testRollbackOnlyExchangeWithoutOnConsumeFailedIsNotConsumed() throws
Exception {
+ SqlConsumer consumer = (SqlConsumer)
context.getRoute("on-consume-only").getConsumer();
+ assertEquals(3, consumer.poll());
+
+ assertEquals(List.of("DONE", "ASF", "DONE"), licenses(),
+ "without onConsumeFailed, the AMQ row, whose exchange was
marked rollback only, must not get onConsume");
+ }
+
+ @Test
+ void testTransactedRollbackOnlyExchangeBreaksOutOfBatch() throws Exception
{
+ SqlConsumer consumer = (SqlConsumer)
context.getRoute("transacted").getConsumer();
+ // the consumer wraps the exception of the batch
+ RuntimeCamelException e = assertThrows(RuntimeCamelException.class,
consumer::poll,
+ "a rollback only exchange must break out of a transacted
batch, as a failed exchange does");
+ assertInstanceOf(RollbackExchangeException.class, e.getCause());
+ // there was no error, and the message (logged by the consumer at
every such poll) must not say so
+ assertTrue(e.getCause().getMessage().startsWith("Rollback transaction
as the exchange was marked as rollback only"),
+ e.getCause().getMessage());
+
+ assertEquals(List.of("DONE", "ASF", "XXX"), licenses(),
+ "neither the rollback only row nor the rows after it must be
consumed");
+ }
+
+ private List<String> licenses() {
+ return jdbcTemplate.queryForList("select license from projects order
by id", String.class);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ getContext().getComponent("sql",
SqlComponent.class).setDataSource(db);
+
+ consume("on-consume", ON_CONSUME + ON_CONSUME_FAILED);
+ consume("on-consume-only", ON_CONSUME);
+ consume("transacted", ON_CONSUME + ON_CONSUME_FAILED +
"&transacted=true");
+ }
+
+ private void consume(String routeId, String options) {
+ from("sql:select * from projects order by
id?startScheduler=false" + options)
+ .routeId(routeId)
+ .choice().when(simple("${body[PROJECT]} ==
'AMQ'")).markRollbackOnly().end()
+ .to("mock:processed");
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index c2568bcd695c..ef55a568c050 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -863,6 +863,15 @@ comma and the statement ran once for each part (once for
each byte of a `byte[]`
Collections, iterators and other arrays (including `int[]`, `long[]` and other
primitive arrays) still run the statement
once per element; send a `List` to run it once per value.
+=== camel-mybatis, camel-sql - onConsume does not run for a failed or rollback
only exchange (Breaking change)
+
+The camel-mybatis consumer no longer runs the `onConsume` statements for a row
whose exchange failed or was marked
+rollback only: the row is consumed again by the next poll, so a row that
always fails is now processed at every poll.
+Handle the exception in the route (for example with
`onException(...).handled(true)`) to consume the row anyway. The
+camel-sql consumer now runs `onConsumeFailed` instead of `onConsume` for a
rollback only exchange (no statement when
+`onConsumeFailed` is not set, so the row is consumed again by the next poll).
With `transacted=true` both consumers now
+break out of the batch at a rollback only exchange, as they do at a failed one.
+
=== camel-groovy
A `GroovyShellFactory` is now looked up in the registry once per
`CamelContext`, when the first groovy expression is