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() {