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 869bfbc4cf6b CAMEL-24866: camel-support - route reload fails with 
duplicate route id when a new file is added next to a file with several routes
869bfbc4cf6b is described below

commit 869bfbc4cf6b56e249fa7bc99d99eb2f061d1aca
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 21 11:08:28 2026 +0200

    CAMEL-24866: camel-support - route reload fails with duplicate route id 
when a new file is added next to a file with several routes
    
    CAMEL-24866
    
    With a dev-mode app whose route file holds several routes, adding a second 
route file to the directory made the reload stop all routes and then fail:
    
    ```
    WARN  FileWatcherResourceReloadStrategy : Error reloading routes from file: 
.../hello.camel.yaml
    org.apache.camel.FailedToStartRouteException: Failed to start route: 
create-table because: duplicate route id detected create-table.
    ```
    
    `RouteWatcherReloadStrategy.onRouteReload` collected the sources to load 
again from every existing route, adding the route's source resource each time 
and only skipping the changed resource. A file with N routes was added N times, 
`DefaultRoutesLoader.updateRoutes` loaded it N times, and the second copy 
tripped the duplicate id check. A change to an existing file was fine because 
that file is the excluded one.
    
    The fix adds a source once per location, with the `equalResourceLocation` 
comparison the method already uses. Test: `RouteReloadNewFileTest` (fails with 
the duplicate id error without the change). Found with the camel-jbang-mcp 
server stepwise benchmark, where the model added a route file next to the 
example's three-route file and the app went down.
---
 .../camel/support/RouteWatcherReloadStrategy.java  |  8 +-
 .../camel/dsl/yaml/RouteReloadNewFileTest.groovy   | 97 ++++++++++++++++++++++
 2 files changed, 102 insertions(+), 3 deletions(-)

diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
index 316078843636..300bb65eab77 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
@@ -263,7 +263,7 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
             // to the last working set
             previousSources.forEach(rs -> {
                 // remember all the sources of the current routes (except the 
updated)
-                if (rs != null && !equalResourceLocation(resources, rs)) {
+                if (rs != null && !equalResourceLocation(resources, rs) && 
!equalResourceLocation(sources, rs)) {
                     sources.add(rs);
                 }
             });
@@ -273,9 +273,11 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
             // should all existing routes be stopped and removed first?
             if (removeAllRoutes) {
                 // remember all the sources of the current routes (except the 
updated)
+                // (a file with several routes is one source: adding it once 
per route would load it several
+                // times and fail on a duplicate route id, CAMEL-24866)
                 getCamelContext().getRoutes().forEach(r -> {
                     Resource rs = r.getSourceResource();
-                    if (rs != null && !equalResourceLocation(resources, rs)) {
+                    if (rs != null && !equalResourceLocation(resources, rs) && 
!equalResourceLocation(sources, rs)) {
                         sources.add(rs);
                     }
                 });
@@ -288,7 +290,7 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
 
             if (resources != null) {
                 for (Resource resource : resources) {
-                    if (Files.exists(Paths.get(resource.getURI()))) {
+                    if (Files.exists(Paths.get(resource.getURI())) && 
!equalResourceLocation(sources, resource)) {
                         sources.add(resource);
                     }
                 }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadNewFileTest.groovy
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadNewFileTest.groovy
new file mode 100644
index 000000000000..f0955596e36f
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/groovy/org/apache/camel/dsl/yaml/RouteReloadNewFileTest.groovy
@@ -0,0 +1,97 @@
+/*
+ * 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.dsl.yaml
+
+import org.apache.camel.ServiceStatus
+import org.apache.camel.dsl.yaml.support.YamlTestSupport
+import org.apache.camel.support.ResourceHelper
+import org.apache.camel.support.RouteWatcherReloadStrategy
+
+import java.nio.file.Files
+import java.nio.file.Path
+
+/**
+ * CAMEL-24866: a new route file next to a file with several routes reloads 
without a duplicate route id: the existing
+ * file is one source, whatever the number of routes it holds.
+ */
+class RouteReloadNewFileTest extends YamlTestSupport {
+
+    Path dir
+    Path shop
+
+    @Override
+    def doSetup() {
+        dir = Files.createTempDirectory("camel-reload")
+        shop = dir.resolve("shop.camel.yaml")
+        Files.writeString(shop, '''
+            - route:
+                id: orders
+                from:
+                  uri: direct:orders
+                  steps:
+                    - to:
+                        uri: mock:orders
+            - route:
+                id: report
+                from:
+                  uri: direct:report
+                  steps:
+                    - to:
+                        uri: mock:report
+            - route:
+                id: setup
+                from:
+                  uri: direct:setup
+                  steps:
+                    - to:
+                        uri: mock:setup
+            ''')
+        context.start()
+        loadRoutes(ResourceHelper.resolveResource(context, "file:" + shop))
+    }
+
+    def cleanup() {
+        dir.toFile().deleteDir()
+    }
+
+    def 'a new file reloads next to a file with several routes'() {
+        setup:
+            def strategy = new RouteWatcherReloadStrategy(dir.toString())
+            strategy.setCamelContext(context)
+            strategy.setPattern("*.yaml")
+            // the strategy is not started (no file watcher in the test): its 
reload callback is driven by hand
+            strategy.doStart()
+            assert context.routes.size() == 3
+        when: 'a second file is added'
+            def hello = dir.resolve("hello.camel.yaml")
+            Files.writeString(hello, '''
+                - route:
+                    id: hello
+                    from:
+                      uri: direct:hello
+                      steps:
+                        - to:
+                            uri: mock:hello
+                ''')
+            strategy.getResourceReload().onReload(hello.toString(), 
ResourceHelper.resolveResource(context, "file:" + hello))
+        then: 'the three routes of the first file and the new one run'
+            context.routes.size() == 4
+            ["orders", "report", "setup", "hello"].every { 
context.getRouteController().getRouteStatus(it) == ServiceStatus.Started }
+        cleanup:
+            strategy.doStop()
+    }
+}

Reply via email to