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