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 52c2942c0653 CAMEL-24091: camel-file - Snapshot idempotent key at poll 
time to prevent key mismatch (#24746)
52c2942c0653 is described below

commit 52c2942c06531fcde2048e68c171c012102783ef
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Jul 16 09:49:16 2026 +0200

    CAMEL-24091: camel-file - Snapshot idempotent key at poll time to prevent 
key mismatch (#24746)
    
    CAMEL-24091: camel-file - Snapshot idempotent key at poll time to prevent 
key mismatch
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/component/file/GenericFileConsumer.java  |  7 +++
 .../component/file/GenericFileOnCompletion.java    | 12 ++--
 .../org/apache/camel/ExchangeConstantProvider.java |  3 +-
 .../src/main/java/org/apache/camel/Exchange.java   |  1 +
 ...ileConsumerIdempotentKeyHeaderMutationTest.java | 66 ++++++++++++++++++++++
 5 files changed, 80 insertions(+), 9 deletions(-)

diff --git 
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
 
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
index e02c4a5091d8..76a775a91b74 100644
--- 
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
+++ 
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileConsumer.java
@@ -410,6 +410,13 @@ public abstract class GenericFileConsumer<T> extends 
ScheduledBatchPollingConsum
         // and then the file name would be changed
         String absoluteFileName = file.getAbsoluteFilePath();
 
+        // snapshot the idempotent key now while the exchange still has 
pristine file headers;
+        // after routing, headers like CamelFileName may be mutated, causing 
key mismatch at completion time
+        if (Boolean.TRUE.equals(endpoint.isIdempotent()) && 
endpoint.getIdempotentKey() != null) {
+            String key = endpoint.getIdempotentKey().evaluate(exchange, 
String.class);
+            exchange.setProperty(Exchange.FILE_IDEMPOTENT_KEY, key);
+        }
+
         // check if we can begin processing the file
         Exception beginCause = null;
         boolean begin = false;
diff --git 
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
 
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
index f731aff6faf7..9f6cfbad39e2 100644
--- 
a/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
+++ 
b/components/camel-file/src/main/java/org/apache/camel/component/file/GenericFileOnCompletion.java
@@ -112,12 +112,10 @@ public class GenericFileOnCompletion<T> implements 
Synchronization {
     protected void processStrategyCommit(
             GenericFileProcessStrategy<T> processStrategy, Exchange exchange, 
GenericFile<T> file) {
         if (Boolean.TRUE.equals(endpoint.isIdempotent())) {
-            // use absolute file path as default key, but evaluate if an
-            // expression key was configured
+            // use the poll-time snapshot so header mutations during routing 
don't cause key mismatch
             String key = absoluteFileName;
             if (endpoint.getIdempotentKey() != null) {
-                Exchange dummy = GenericFileHelper.createDummy(endpoint, 
exchange, () -> file);
-                key = endpoint.getIdempotentKey().evaluate(dummy, 
String.class);
+                key = exchange.getProperty(Exchange.FILE_IDEMPOTENT_KEY, 
absoluteFileName, String.class);
             }
             // only add to idempotent repository if we could process the file
             if (key != null) {
@@ -155,12 +153,10 @@ public class GenericFileOnCompletion<T> implements 
Synchronization {
         }
 
         if (Boolean.TRUE.equals(endpoint.isIdempotent())) {
-            // use absolute file path as default key, but evaluate if an
-            // expression key was configured
+            // use the poll-time snapshot so header mutations during routing 
don't cause key mismatch
             String key = absoluteFileName;
             if (endpoint.getIdempotentKey() != null) {
-                Exchange dummy = GenericFileHelper.createDummy(endpoint, 
exchange, () -> file);
-                key = endpoint.getIdempotentKey().evaluate(dummy, 
String.class);
+                key = exchange.getProperty(Exchange.FILE_IDEMPOTENT_KEY, 
absoluteFileName, String.class);
             }
             if (key != null) {
                 endpoint.getIdempotentRepository().remove(key);
diff --git 
a/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
 
b/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
index 6522152b19f2..ec7e00bb731b 100644
--- 
a/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
+++ 
b/core/camel-api/src/generated/java/org/apache/camel/ExchangeConstantProvider.java
@@ -17,7 +17,7 @@ public class ExchangeConstantProvider {
 
     private static final Map<String, String> MAP;
     static {
-        Map<String, String> map = new HashMap<>(154);
+        Map<String, String> map = new HashMap<>(155);
         map.put("ACTIVITY_SPAN_TAGS", "CamelActivitySpanTags");
         map.put("AGGREGATED_COLLECTION_GUARD", 
"CamelAggregatedCollectionGuard");
         map.put("AGGREGATED_COMPLETED_BY", "CamelAggregatedCompletedBy");
@@ -65,6 +65,7 @@ public class ExchangeConstantProvider {
         map.put("FATAL_FALLBACK_ERROR_HANDLER", 
"CamelFatalFallbackErrorHandler");
         map.put("FILE_CONTENT_TYPE", "CamelFileContentType");
         map.put("FILE_EXCHANGE_FILE", "CamelFileExchangeFile");
+        map.put("FILE_IDEMPOTENT_KEY", "CamelFileIdempotentKey");
         map.put("FILE_LAST_MODIFIED", "CamelFileLastModified");
         map.put("FILE_LENGTH", "CamelFileLength");
         map.put("FILE_LOCAL_WORK_PATH", "CamelFileLocalWorkPath");
diff --git a/core/camel-api/src/main/java/org/apache/camel/Exchange.java 
b/core/camel-api/src/main/java/org/apache/camel/Exchange.java
index c764291302a2..d2b64260536d 100644
--- a/core/camel-api/src/main/java/org/apache/camel/Exchange.java
+++ b/core/camel-api/src/main/java/org/apache/camel/Exchange.java
@@ -160,6 +160,7 @@ public interface Exchange extends VariableAware {
     String FILE_LOCK_RANDOM_ACCESS_FILE = "CamelFileLockRandomAccessFile";
     String FILE_LOCK_CHANNEL_FILE = "CamelFileLockChannelFile";
     String FILE_EXCHANGE_FILE = "CamelFileExchangeFile";
+    String FILE_IDEMPOTENT_KEY = "CamelFileIdempotentKey";
     String FILTER_NON_XML_CHARS = "CamelFilterNonXmlChars";
 
     String GROUPED_EXCHANGE = "CamelGroupedExchange";
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumerIdempotentKeyHeaderMutationTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumerIdempotentKeyHeaderMutationTest.java
new file mode 100644
index 000000000000..6365e849e695
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumerIdempotentKeyHeaderMutationTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.file;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test that mutating CamelFileName in a route does not break idempotent key 
evaluation at completion time. Before the
+ * fix, rollback would remove the wrong key (computed from mutated headers), 
leaving the eagerly-added poll-time key
+ * orphaned so the file was never retried.
+ */
+public class FileConsumerIdempotentKeyHeaderMutationTest extends 
ContextTestSupport {
+
+    private static final String TEST_FILE_NAME = "report-" + UUID.randomUUID() 
+ ".txt";
+    private final AtomicInteger attempts = new AtomicInteger();
+
+    @Test
+    public void testIdempotentKeyUnaffectedByHeaderMutation() throws Exception 
{
+        getMockEndpoint("mock:result").expectedBodiesReceived("Hello World");
+
+        template.sendBodyAndHeader(fileUri(), "Hello World", 
Exchange.FILE_NAME, TEST_FILE_NAME);
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                
from(fileUri("?idempotent=true&idempotentKey=${file:onlyname}-${file:size}"
+                             + "&initialDelay=0&delay=10"))
+                        .setHeader(Exchange.FILE_NAME, constant("output.txt"))
+                        .process(new Processor() {
+                            @Override
+                            public void process(Exchange exchange) throws 
Exception {
+                                if (attempts.incrementAndGet() == 1) {
+                                    throw new 
IllegalArgumentException("Simulated failure on first attempt");
+                                }
+                            }
+                        })
+                        .to("mock:result");
+            }
+        };
+    }
+}

Reply via email to