This is an automated email from the ASF dual-hosted git repository.

Croway 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 6449c0ee3de2 CAMEL-24046: camel-sql - JdbcCachedMessageIdRepository 
must not cache a key whose insert failed, make cache thread-safe
6449c0ee3de2 is described below

commit 6449c0ee3de2ccd38f8f583191fef59ab79eef73
Author: croway <[email protected]>
AuthorDate: Mon Jul 13 11:42:33 2026 +0200

    CAMEL-24046: camel-sql - JdbcCachedMessageIdRepository must not cache a key 
whose insert failed, make cache thread-safe
---
 .../jdbc/JdbcCachedMessageIdRepository.java        | 35 +++++----
 ...cCachedMessageIdRepositoryFailedInsertTest.java | 85 ++++++++++++++++++++++
 2 files changed, 105 insertions(+), 15 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 eac9de9c1b16..dbb4f010adbd 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
@@ -16,8 +16,9 @@
  */
 package org.apache.camel.processor.idempotent.jdbc;
 
-import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.sql.DataSource;
 
@@ -29,9 +30,9 @@ import 
org.springframework.transaction.support.TransactionTemplate;
  * Caching version of {@link JdbcMessageIdRepository}
  */
 public class JdbcCachedMessageIdRepository extends JdbcMessageIdRepository {
-    private Map<String, Integer> cache = new HashMap<>();
-    private int hitCount;
-    private int missCount;
+    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";
 
@@ -67,23 +68,27 @@ public class JdbcCachedMessageIdRepository extends 
JdbcMessageIdRepository {
     @Override
     public boolean add(final String key) {
         Integer previousValue = cache.getOrDefault(key, 0);
-        cache.put(key, previousValue + 1);
         if (previousValue != 0) {
-            hitCount++;
+            cache.merge(key, 1, Integer::sum);
+            hitCount.incrementAndGet();
             return false;
         }
-        missCount++;
-        return super.add(key);
+        missCount.incrementAndGet();
+        boolean added = super.add(key);
+        // only remember the key after the database insert succeeded, 
otherwise a failed insert
+        // would leave the key cached and every redelivery would be rejected 
as a duplicate
+        cache.merge(key, 1, Integer::sum);
+        return added;
     }
 
     @Override
     public boolean contains(final String key) {
         Integer previousValue = cache.getOrDefault(key, 0);
         if (previousValue != 0) {
-            hitCount++;
+            hitCount.incrementAndGet();
             return true;
         }
-        missCount++;
+        missCount.incrementAndGet();
         return super.contains(key);
     }
 
@@ -96,8 +101,8 @@ public class JdbcCachedMessageIdRepository extends 
JdbcMessageIdRepository {
     @Override
     public void clear() {
         cache.clear();
-        hitCount = 0;
-        missCount = 0;
+        hitCount.set(0);
+        missCount.set(0);
         super.clear();
     }
 
@@ -110,18 +115,18 @@ public class JdbcCachedMessageIdRepository extends 
JdbcMessageIdRepository {
     }
 
     public int getHitCount() {
-        return hitCount;
+        return hitCount.get();
     }
 
     public int getMissCount() {
-        return missCount;
+        return missCount.get();
     }
 
     public void reload() {
         transactionTemplate.execute(status -> {
             try {
                 cache = jdbcTemplate.query(getQueryAllString(), resultSet -> {
-                    Map<String, Integer> messageIdCount = new HashMap<>();
+                    Map<String, Integer> messageIdCount = new 
ConcurrentHashMap<>();
                     while (resultSet.next()) {
                         messageIdCount.put(resultSet.getString(1), 
resultSet.getInt(2));
                     }
diff --git 
a/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepositoryFailedInsertTest.java
 
b/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepositoryFailedInsertTest.java
new file mode 100644
index 000000000000..2e3c63e4d087
--- /dev/null
+++ 
b/components/camel-sql/src/test/java/org/apache/camel/processor/idempotent/jdbc/JdbcCachedMessageIdRepositoryFailedInsertTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * When the database insert fails, {@link 
JdbcCachedMessageIdRepository#add(String)} propagates the exception and the
+ * idempotent consumer EIP never processes the message. The failed key must 
therefore not be remembered as processed: a
+ * redelivery of the same message must be processable once the database is 
available again.
+ */
+public class JdbcCachedMessageIdRepositoryFailedInsertTest {
+
+    private static final String PROCESSOR_NAME = "myProcessorName";
+
+    private EmbeddedDatabase dataSource;
+    private JdbcTemplate jdbcTemplate;
+    private JdbcCachedMessageIdRepository repository;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        dataSource = new EmbeddedDatabaseBuilder()
+                .setType(EmbeddedDatabaseType.H2)
+                .generateUniqueName(true)
+                .build();
+        jdbcTemplate = new JdbcTemplate(dataSource);
+        repository = new JdbcCachedMessageIdRepository(dataSource, 
PROCESSOR_NAME);
+        repository.start();
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        if (repository != null) {
+            repository.stop();
+        }
+        if (dataSource != null) {
+            dataSource.shutdown();
+        }
+    }
+
+    @Test
+    public void failedInsertMustNotMarkMessageAsProcessed() {
+        // sanity: normal add works and duplicates are detected
+        assertTrue(repository.add("first"));
+        assertFalse(repository.add("first"));
+
+        // simulate a database outage while adding a new key
+        jdbcTemplate.execute("DROP TABLE CAMEL_MESSAGEPROCESSED");
+        assertThrows(Exception.class, () -> repository.add("second"));
+
+        // database is back
+        jdbcTemplate.execute(repository.getCreateString());
+
+        // the failed add must not have marked "second" as processed: the 
message was
+        // never processed and nothing was stored in the database
+        assertFalse(repository.contains("second"),
+                "a key whose insert failed must not be reported as already 
processed");
+        assertTrue(repository.add("second"),
+                "redelivery of a message whose insert failed must be 
processable");
+    }
+}

Reply via email to