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

davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/camel-4.18.x by this push:
     new 0b39241fca86 [backport camel-4.18.x] CAMEL-24630: Fix duplicate 
kamelet routes on supervised reload (#26324)
0b39241fca86 is described below

commit 0b39241fca86557401cf4614b4cb6bbf3829e25c
Author: Guillaume Nodet - AI Bot <[email protected]>
AuthorDate: Sat Sep 12 09:18:32 2026 +0200

    [backport camel-4.18.x] CAMEL-24630: Fix duplicate kamelet routes on 
supervised reload (#26324)
    
    Reloading kamelet routes with DefaultSupervisingRouteController enabled
    left duplicate internal route entries, and ManagedCamelContext
    .getStartedRoutes() subsequently threw a NullPointerException, breaking
    the Camel JBang dev console.
    
    Kamelet child routes were being materialized twice on reload:
    addRouteFromKamelet() already creates the route (Stopped, under
    supervision), but KameletComponent then called startRouteDefinitions()
    again because the status was not Started. That registered a second Route
    instance under the same route id, leaving orphan instances whose
    getRouteStatus() returned null.
    
    KameletComponent now calls startRouteDefinitions() only when the route
    does not yet exist in the controller (getRouteStatus(id) == null). A
    supervising controller owns the lifecycle of the routes it manages, so
    Camel must not re-materialize a route that is already registered and
    Stopped.
    
    Backport of #26173 to camel-4.18.x, with the CamelTestSupport import
    adjusted from camel-test-junit6 to camel-test-junit5, which is what this
    branch provides.
    
    Co-authored-by: Omar Atie <[email protected]>
    Co-authored-by: Cursor Agent <[email protected]>
    Co-authored-by: Guillaume Nodet <[email protected]>
---
 .../camel/component/kamelet/KameletComponent.java  |   5 +-
 .../kamelet/KameletSupervisedReloadTest.java       | 144 +++++++++++++++++++++
 .../DefaultSupervisingRouteControllerTest.java     |  43 ++++++
 3 files changed, 189 insertions(+), 3 deletions(-)

diff --git 
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
 
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
index 241daa024827..36d1efa71e38 100644
--- 
a/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
+++ 
b/components/camel-kamelet/src/main/java/org/apache/camel/component/kamelet/KameletComponent.java
@@ -493,10 +493,9 @@ public class KameletComponent extends DefaultComponent {
                         endpoint.getKameletProperties());
                 RouteDefinition def = context.getRouteDefinition(id);
 
-                // start the route if not already started
+                // start the route if it was not already materialized (avoid 
duplicate Route instances on reload)
                 ServiceStatus status = 
context.getRouteController().getRouteStatus(id);
-                boolean started = status != null && status.isStarted();
-                if (!started) {
+                if (status == null) {
                     
context.startRouteDefinitions(Collections.singletonList(def));
                 }
 
diff --git 
a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletSupervisedReloadTest.java
 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletSupervisedReloadTest.java
new file mode 100644
index 000000000000..4871ef54b2e0
--- /dev/null
+++ 
b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletSupervisedReloadTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.kamelet;
+
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ServiceStatus;
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedCamelContextMBean;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.SupervisingRouteController;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * CAMEL-24630: reloading kamelet routes under a supervising route controller 
must not leave duplicate route entries or
+ * break {@code ManagedCamelContext.getStartedRoutes()}.
+ */
+public class KameletSupervisedReloadTest extends CamelTestSupport {
+
+    private static final int INITIAL_DELAY = 200;
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    @Override
+    protected boolean useJmx() {
+        return true;
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        
context.getManagementStrategy().getManagementAgent().setRegisterRoutesCreateByKamelet(true);
+        return context;
+    }
+
+    @Test
+    void supervisedKameletReloadDoesNotDuplicateRoutesOrBreakManagement() 
throws Exception {
+        SupervisingRouteController supervising = 
context.getRouteController().supervising();
+        supervising.setInitialDelay(INITIAL_DELAY);
+
+        context.addRoutes(routes());
+        context.start();
+
+        ManagedCamelContextMBean managed = resolveManagedCamelContextMBean();
+
+        for (int i = 0; i <= 2; i++) {
+            final int reload = i;
+            awaitReloadStable(supervising);
+            assertReloadState(reload, supervising, managed);
+            if (reload == 2) {
+                break;
+            }
+            reloadRoutes();
+        }
+    }
+
+    private ManagedCamelContextMBean resolveManagedCamelContextMBean() {
+        // Same lookup path as ContextDevConsole / JBang dev console (not 
direct MBean construction)
+        ManagedCamelContext plugin = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+        assertThat(plugin).isNotNull();
+        ManagedCamelContextMBean managed = plugin.getManagedCamelContext();
+        assertThat(managed).isNotNull();
+        return managed;
+    }
+
+    private void awaitReloadStable(SupervisingRouteController supervising) {
+        await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> {
+            assertThat(context.getRoutesSize()).isEqualTo(2);
+            for (String routeId : context.getRouteIds()) {
+                ServiceStatus status = supervising.getRouteStatus(routeId);
+                assertThat(status).isNotNull();
+                assertThat(status.isStarted()).isTrue();
+            }
+        });
+    }
+
+    private void assertReloadState(int reload, SupervisingRouteController 
supervising, ManagedCamelContextMBean managed) {
+        assertThat(context.getRoutesSize()).as("route count after reload %s", 
reload).isEqualTo(2);
+        assertThat(context.getRouteIds()).as("unique route ids after reload 
%s", reload).hasSize(2);
+
+        Set<String> routeIdsFromInstances = new HashSet<>();
+        for (var route : context.getRoutes()) {
+            assertThat(routeIdsFromInstances.add(route.getId()))
+                    .as("duplicate route instance for id %s after reload %s", 
route.getId(), reload)
+                    .isTrue();
+            assertThat(supervising.getRouteStatus(route.getId()))
+                    .as("status for route %s after reload %s", route.getId(), 
reload)
+                    .isNotNull();
+        }
+
+        assertThat(supervising.getControlledRoutes()).as("controlled routes 
after reload %s", reload).hasSize(2);
+
+        Integer started = managed.getStartedRoutes();
+        assertThat(started).as("started routes after reload %s", 
reload).isEqualTo(2);
+    }
+
+    private void reloadRoutes() throws Exception {
+        SupervisingRouteController supervising = 
context.getRouteController().supervising();
+        supervising.removeAllRoutes();
+        context.removeRouteTemplates("*");
+        context.getEndpointRegistry().clear();
+        context.addRoutes(routes());
+    }
+
+    private static RouteBuilder routes() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("probe-source")
+                        .from("timer:probe?repeatCount=1&delay=10")
+                        .setBody(constant("hello"))
+                        .to("kamelet:sink");
+
+                from("kamelet:probe-source").routeId("probe-parent")
+                        .process(exchange -> {
+                        });
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
index 54f9c8c5c39e..b42a0ca628ec 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/engine/DefaultSupervisingRouteControllerTest.java
@@ -27,6 +27,7 @@ import org.apache.camel.Consumer;
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Processor;
+import org.apache.camel.Route;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
 import org.apache.camel.component.seda.SedaComponent;
@@ -189,6 +190,48 @@ public class DefaultSupervisingRouteControllerTest extends 
ContextTestSupport {
         assertEquals(10, events.size());
     }
 
+    @Test
+    public void testSupervisedRemoveAllRoutesAndReload() throws Exception {
+        SupervisingRouteController src = 
context.getRouteController().supervising();
+        src.setInitialDelay(100);
+
+        context.addRoutes(reloadRoutes());
+        context.start();
+
+        await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+            assertEquals("Started", 
context.getRouteController().getRouteStatus("reload-a").toString());
+            assertEquals("Started", 
context.getRouteController().getRouteStatus("reload-b").toString());
+        });
+
+        for (int i = 0; i < 2; i++) {
+            final int reload = i;
+            src.removeAllRoutes();
+            context.getEndpointRegistry().clear();
+            context.addRoutes(reloadRoutes());
+            src.startRoutes(true);
+
+            await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
+                assertEquals(2, context.getRoutesSize(), "route count after 
reload " + reload);
+                assertEquals(2, context.getRouteIds().size(), "unique route 
ids after reload " + reload);
+                assertEquals(2, src.getControlledRoutes().size(), "controlled 
routes after reload " + reload);
+                for (Route route : context.getRoutes()) {
+                    assertNotNull(src.getRouteStatus(route.getId()),
+                            "route status for " + route.getId() + " after 
reload " + reload);
+                }
+            });
+        }
+    }
+
+    private static RouteBuilder reloadRoutes() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("timer:reloadA?repeatCount=1&delay=10").routeId("reload-a").to("mock:a");
+                
from("timer:reloadB?repeatCount=1&delay=10").routeId("reload-b").to("mock:b");
+            }
+        };
+    }
+
     private static class MyRoute extends RouteBuilder {
         @Override
         public void configure() {

Reply via email to