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 857dc649dd0e CAMEL-24630: Fix duplicate kamelet routes on supervised 
reload (#26173)
857dc649dd0e is described below

commit 857dc649dd0e4c4a32bd6b5caef889b5e5166ac2
Author: Omar Atie <[email protected]>
AuthorDate: Tue Sep 8 23:06:03 2026 -0700

    CAMEL-24630: Fix duplicate kamelet routes on supervised reload (#26173)
    
    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.
    
    Covered by a new KameletSupervisedReloadTest reproducing the reported
    reload sequence through the JMX path the dev console uses, plus
    
DefaultSupervisingRouteControllerTest.testSupervisedRemoveAllRoutesAndReload.
    
    Closes #26173
    
    Co-authored-by: Omar Atie <[email protected]>
    Co-authored-by: Cursor Agent <[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..b5ac30202e87
--- /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.junit6.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 f01cc75f748c..27f235af8b15 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
@@ -28,6 +28,7 @@ import org.apache.camel.ContextTestSupport;
 import org.apache.camel.Endpoint;
 import org.apache.camel.FailedToStartRouteException;
 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;
@@ -190,6 +191,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