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 6c100bb4d822 CAMEL-26705: camel-sql -
JdbcOrphanLockAwareIdempotentRepository and JdbcCachedMessageIdRepository are
not restart safe (#29402)
6c100bb4d822 is described below
commit 6c100bb4d82234b093c1d07eb7ec85861e64a5a6
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Nov 8 13:32:32 2026 +0530
CAMEL-26705: camel-sql - JdbcOrphanLockAwareIdempotentRepository and
JdbcCachedMessageIdRepository are not restart safe (#29402)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../jdbc/JdbcCachedMessageIdRepository.java | 10 +-
.../JdbcOrphanLockAwareIdempotentRepository.java | 19 ++-
.../JdbcCachedMessageIdRepositoryRestartTest.java | 82 +++++++++++++
...anLockAwareIdempotentRepositoryRestartTest.java | 127 +++++++++++++++++++++
4 files changed, 231 insertions(+), 7 deletions(-)
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepository.java
b/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepository.java
index dbb4f010adbd..07c2bbb91f42 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepository.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepository.java
@@ -30,11 +30,14 @@ import
org.springframework.transaction.support.TransactionTemplate;
* Caching version of {@link JdbcMessageIdRepository}
*/
public class JdbcCachedMessageIdRepository extends JdbcMessageIdRepository {
+
+ protected static final String DEFAULT_QUERY_ALL_STRING
+ = "SELECT messageId, COUNT(*) FROM CAMEL_MESSAGEPROCESSED WHERE
processorName = ? GROUP BY messageId";
+
private volatile Map<String, Integer> cache = new ConcurrentHashMap<>();
private final AtomicInteger hitCount = new AtomicInteger();
private final AtomicInteger missCount = new AtomicInteger();
- private String queryAllString
- = "SELECT messageId, COUNT(*) FROM CAMEL_MESSAGEPROCESSED WHERE
processorName = ? GROUP BY messageId";
+ private String queryAllString = DEFAULT_QUERY_ALL_STRING;
public JdbcCachedMessageIdRepository() {
}
@@ -55,7 +58,8 @@ public class JdbcCachedMessageIdRepository extends
JdbcMessageIdRepository {
protected void doInit() throws Exception {
super.doInit();
if (getTableName() != null) {
- queryAllString = queryAllString.replaceFirst(DEFAULT_TABLENAME,
getTableName());
+ // update query string from default table name to the new table
name
+ queryAllString =
DEFAULT_QUERY_ALL_STRING.replace(DEFAULT_TABLENAME, getTableName());
}
}
diff --git
a/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepository.java
b/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepository.java
index 0cb53e1e4388..10bcaee14429 100644
---
a/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepository.java
+++
b/components/camel-sql/src/main/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepository.java
@@ -46,6 +46,9 @@ import
org.springframework.transaction.support.TransactionTemplate;
*/
public class JdbcOrphanLockAwareIdempotentRepository extends
JdbcMessageIdRepository {
+ protected static final String DEFAULT_UPDATE_TIMESTAMP_QUERY
+ = "UPDATE CAMEL_MESSAGEPROCESSED SET createdAt =? WHERE
processorName =? AND messageId = ?";
+
private final StampedLock sl = new StampedLock();
private final Set<ProcessorNameAndMessageId> processorNameMessageIdSet =
new HashSet<>();
@@ -62,8 +65,7 @@ public class JdbcOrphanLockAwareIdempotentRepository extends
JdbcMessageIdReposi
/** intervals after which keep alive is sent for the locks held by an
instance **/
private long lockKeepAliveIntervalMillis;
- private String updateTimestampQuery
- = "UPDATE CAMEL_MESSAGEPROCESSED SET createdAt =? WHERE
processorName =? AND messageId = ?";
+ private String updateTimestampQuery = DEFAULT_UPDATE_TIMESTAMP_QUERY;
public JdbcOrphanLockAwareIdempotentRepository(CamelContext camelContext) {
super();
@@ -150,8 +152,15 @@ public class JdbcOrphanLockAwareIdempotentRepository
extends JdbcMessageIdReposi
super.doInit();
if (getTableName() != null) {
- updateTimestampQuery =
updateTimestampQuery.replaceFirst(DEFAULT_TABLENAME, getTableName());
+ // update query string from default table name to the new table
name
+ updateTimestampQuery =
DEFAULT_UPDATE_TIMESTAMP_QUERY.replace(DEFAULT_TABLENAME, getTableName());
}
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+
executorServiceManager = context.getExecutorServiceManager();
executorService =
executorServiceManager.newSingleThreadScheduledExecutor(this,
this.getClass().getSimpleName());
@@ -162,10 +171,12 @@ public class JdbcOrphanLockAwareIdempotentRepository
extends JdbcMessageIdReposi
}
@Override
- protected void doShutdown() throws Exception {
+ protected void doStop() throws Exception {
if (executorServiceManager != null && executorService != null) {
executorServiceManager.shutdownGraceful(executorService);
+ executorService = null;
}
+ super.doStop();
}
@Override
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepositoryRestartTest.java
b/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepositoryRestartTest.java
new file mode 100644
index 000000000000..3f6a4404ea50
--- /dev/null
+++
b/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepositoryRestartTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.processor.idempotent.jdbc;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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.assertTrue;
+
+/**
+ * Stopping and starting the repository again keeps the query that reloads the
cache for a custom table name.
+ */
+public class JdbcCachedMessageIdRepositoryRestartTest {
+
+ private static final String TABLE_NAME = "CAMEL_MESSAGEPROCESSED_ORDERS";
+ private static final String PROCESSOR_NAME = "APP_1";
+ private static final int RESTARTS = 3;
+
+ private EmbeddedDatabase dataSource;
+ private JdbcCachedMessageIdRepository repository;
+
+ @BeforeEach
+ public void setup() {
+ dataSource = new EmbeddedDatabaseBuilder()
+ .setType(EmbeddedDatabaseType.H2)
+ .generateUniqueName(true)
+ .build();
+ repository = new JdbcCachedMessageIdRepository(dataSource,
PROCESSOR_NAME);
+ repository.setTableName(TABLE_NAME);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ repository.shutdown();
+ dataSource.shutdown();
+ }
+
+ @Test
+ public void testRestartKeepsQueryAllString() {
+ String expected = "SELECT messageId, COUNT(*) FROM " + TABLE_NAME + "
WHERE processorName = ? GROUP BY messageId";
+
+ repository.start();
+ assertEquals(expected, repository.getQueryAllString());
+
+ for (int i = 0; i < RESTARTS; i++) {
+ repository.stop();
+ repository.start();
+ }
+ assertEquals(expected, repository.getQueryAllString());
+
+ // a key added by another node while this one is stopped is in the
cache after the next start
+ repository.stop();
+ new JdbcTemplate(dataSource).update(
+ "INSERT INTO " + TABLE_NAME + " (processorName, messageId,
createdAt) VALUES (?, ?, CURRENT_TIMESTAMP)",
+ PROCESSOR_NAME, "FILE_1");
+ repository.start();
+
+ int hits = repository.getHitCount();
+ assertTrue(repository.contains("FILE_1"));
+ assertEquals(hits + 1, repository.getHitCount());
+ }
+}
diff --git
a/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepositoryRestartTest.java
b/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepositoryRestartTest.java
new file mode 100644
index 000000000000..dbb40986998e
--- /dev/null
+++
b/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcOrphanLockAwareIdempotentRepositoryRestartTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.processor.idempotent.jdbc;
+
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ScheduledExecutorService;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.engine.DefaultExecutorServiceManager;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Stopping and starting the repository again keeps the SQL for a custom table
name and runs one keep-alive task.
+ */
+public class JdbcOrphanLockAwareIdempotentRepositoryRestartTest {
+
+ private static final String TABLE_NAME = "CAMEL_MESSAGEPROCESSED_ORDERS";
+ private static final String PROCESSOR_NAME = "APP_1";
+ private static final long LOCK_MAX_AGE_MILLIS = 300_000L;
+ private static final int RESTARTS = 3;
+
+ private final List<ScheduledExecutorService> keepAliveExecutors = new
CopyOnWriteArrayList<>();
+ private EmbeddedDatabase dataSource;
+ private DefaultCamelContext context;
+ private JdbcOrphanLockAwareIdempotentRepository repository;
+
+ @BeforeEach
+ public void setup() {
+ dataSource = new EmbeddedDatabaseBuilder()
+ .setType(EmbeddedDatabaseType.H2)
+ .generateUniqueName(true)
+ .build();
+
+ context = new DefaultCamelContext();
+ context.setExecutorServiceManager(new
DefaultExecutorServiceManager(context) {
+ @Override
+ public ScheduledExecutorService
newSingleThreadScheduledExecutor(Object source, String name) {
+ ScheduledExecutorService answer =
super.newSingleThreadScheduledExecutor(source, name);
+ keepAliveExecutors.add(answer);
+ return answer;
+ }
+ });
+
+ repository = new JdbcOrphanLockAwareIdempotentRepository(dataSource,
PROCESSOR_NAME, context);
+ repository.setTableName(TABLE_NAME);
+ repository.setLockMaxAgeMillis(LOCK_MAX_AGE_MILLIS);
+ // the test calls keepAlive() itself
+ repository.setLockKeepAliveIntervalMillis(LOCK_MAX_AGE_MILLIS / 2);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ repository.shutdown();
+ context.stop();
+ dataSource.shutdown();
+ }
+
+ @Test
+ public void testRestartKeepsUpdateTimestampQuery() {
+ String expected = "UPDATE " + TABLE_NAME + " SET createdAt =? WHERE
processorName =? AND messageId = ?";
+
+ repository.start();
+ assertEquals(expected, repository.getUpdateTimestampQuery());
+
+ for (int i = 0; i < RESTARTS; i++) {
+ repository.stop();
+ repository.start();
+ }
+ assertEquals(expected, repository.getUpdateTimestampQuery());
+
+ // the keep-alive still refreshes a lease after the restarts
+ assertTrue(repository.add("FILE_1"));
+ Timestamp expired = new Timestamp(System.currentTimeMillis() -
LOCK_MAX_AGE_MILLIS - 60_000L);
+ new JdbcTemplate(dataSource).update("UPDATE " + TABLE_NAME + " SET
createdAt = ? WHERE messageId = ?", expired,
+ "FILE_1");
+ assertFalse(repository.contains("FILE_1"));
+
+ repository.keepAlive();
+ assertTrue(repository.contains("FILE_1"));
+ }
+
+ @Test
+ public void testRestartUsesOneKeepAliveExecutor() {
+ repository.start();
+ assertEquals(1, keepAliveExecutors.size());
+
+ for (int i = 0; i < RESTARTS; i++) {
+ repository.stop();
+
assertTrue(keepAliveExecutors.stream().allMatch(ExecutorService::isShutdown),
+ "The keep-alive executor should be shut down when the
repository is stopped");
+ repository.start();
+ }
+
+ assertEquals(RESTARTS + 1, keepAliveExecutors.size());
+ assertEquals(1, keepAliveExecutors.stream().filter(e ->
!e.isShutdown()).count());
+
+ repository.shutdown();
+
assertTrue(keepAliveExecutors.stream().allMatch(ExecutorService::isShutdown));
+ }
+}