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 56215ac16a8c CAMEL-25073: camel-management - Keep the MBean of a 
processor shared by routes while another route uses it (item 2) (#27464)
56215ac16a8c is described below

commit 56215ac16a8cdcb9189a58530e51fd9a951624a6
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Oct 7 15:57:00 2026 +0530

    CAMEL-25073: camel-management - Keep the MBean of a processor shared by 
routes while another route uses it (item 2) (#27464)
    
    Outputs of a context scoped onException/onCompletion and route 
configuration onException share one processor MBean across routes. Removing one 
route unregistered it while other routes still used it. The MBean is now kept 
while another route uses it and re-attached as needed (CAMEL-25073 item 2).
    
    Closes #27464
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../InstrumentationInterceptStrategy.java          |  24 +++-
 .../management/JmxManagementLifecycleStrategy.java | 105 ++++++++++++++---
 .../InstrumentationInterceptStrategyTest.java      |  34 ++++++
 ...gedRouteRemoveContextScopedOnExceptionTest.java | 124 +++++++++++++++++++++
 .../ManagedRouteRemoveSharedProcessorTest.java     |  85 ++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   4 +
 6 files changed, 354 insertions(+), 22 deletions(-)

diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/InstrumentationInterceptStrategy.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/InstrumentationInterceptStrategy.java
index bae6d03b0139..681d7d4a14ca 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/InstrumentationInterceptStrategy.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/InstrumentationInterceptStrategy.java
@@ -20,9 +20,10 @@ import java.util.Map;
 
 import org.apache.camel.NamedNode;
 import org.apache.camel.Processor;
+import org.apache.camel.Route;
 import org.apache.camel.management.mbean.ManagedPerformanceCounter;
 import org.apache.camel.spi.ManagementInterceptStrategy;
-import org.apache.camel.util.KeyValueHolder;
+import org.apache.camel.util.ObjectHelper;
 
 /**
  * This strategy class wraps targeted processors with a {@link 
InstrumentationProcessor}. Each InstrumentationProcessor
@@ -33,13 +34,26 @@ import org.apache.camel.util.KeyValueHolder;
  */
 public class InstrumentationInterceptStrategy implements 
ManagementInterceptStrategy {
 
+    /**
+     * A processor that has been wrapped for performance counters.
+     *
+     * @param definition               the definition the processor was 
created from, which can be shared by several
+     *                                 routes (such as the outputs of a 
context scoped onException)
+     * @param instrumentationProcessor the processor that wraps it
+     * @param route                    the route the processor belongs to
+     */
+    public record WrappedProcessor(NamedNode definition, 
InstrumentationProcessor<?> instrumentationProcessor, Route route) {
+    }
+
     private final Map<NamedNode, PerformanceCounter> registeredCounters;
-    private final Map<Processor, KeyValueHolder<NamedNode, 
InstrumentationProcessor<?>>> wrappedProcessors;
+    private final Map<Processor, WrappedProcessor> wrappedProcessors;
+    private final Route route;
 
     public InstrumentationInterceptStrategy(Map<NamedNode, PerformanceCounter> 
registeredCounters,
-                                            Map<Processor, 
KeyValueHolder<NamedNode, InstrumentationProcessor<?>>> wrappedProcessors) {
+                                            Map<Processor, WrappedProcessor> 
wrappedProcessors, Route route) {
         this.registeredCounters = registeredCounters;
         this.wrappedProcessors = wrappedProcessors;
+        this.route = ObjectHelper.notNull(route, "route");
     }
 
     @Override
@@ -55,9 +69,7 @@ public class InstrumentationInterceptStrategy implements 
ManagementInterceptStra
         if (counter != null) {
             // add it to the mapping of wrappers so we can later change it to a
             // decorated counter when we register the processor
-            KeyValueHolder<NamedNode, InstrumentationProcessor<?>> holder
-                    = new KeyValueHolder<>(definition, 
instrumentationProcessor);
-            wrappedProcessors.put(target, holder);
+            wrappedProcessors.put(target, new WrappedProcessor(definition, 
instrumentationProcessor, route));
         }
         return instrumentationProcessor;
     }
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
index 2b0ecc33282b..6cea6d06718a 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/JmxManagementLifecycleStrategy.java
@@ -57,6 +57,7 @@ import org.apache.camel.cluster.CamelClusterService;
 import org.apache.camel.health.HealthCheckRegistry;
 import org.apache.camel.impl.debugger.BacklogTracer;
 import org.apache.camel.impl.debugger.DefaultBacklogDebugger;
+import 
org.apache.camel.management.InstrumentationInterceptStrategy.WrappedProcessor;
 import org.apache.camel.management.mbean.ManagedAsyncProcessorAwaitManager;
 import org.apache.camel.management.mbean.ManagedBacklogDebugger;
 import org.apache.camel.management.mbean.ManagedBacklogTracer;
@@ -70,6 +71,7 @@ import 
org.apache.camel.management.mbean.ManagedEndpointServiceRegistry;
 import org.apache.camel.management.mbean.ManagedErrorRegistry;
 import org.apache.camel.management.mbean.ManagedExchangeFactoryManager;
 import org.apache.camel.management.mbean.ManagedInflightRepository;
+import org.apache.camel.management.mbean.ManagedProcessor;
 import org.apache.camel.management.mbean.ManagedProducerCache;
 import org.apache.camel.management.mbean.ManagedRestRegistry;
 import org.apache.camel.management.mbean.ManagedRoute;
@@ -131,7 +133,6 @@ import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.support.task.TaskManagerRegistry;
 import org.apache.camel.throttling.ThrottlingExceptionRoutePolicy;
 import org.apache.camel.throttling.ThrottlingInflightRoutePolicy;
-import org.apache.camel.util.KeyValueHolder;
 import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -148,8 +149,9 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
 
     // the wrapped processors is for performance counters, which are in use 
for the created routes
     // when a route is removed, we should remove the associated processors 
from this map
-    private final Map<Processor, KeyValueHolder<NamedNode, 
InstrumentationProcessor<?>>> wrappedProcessors = new HashMap<>();
-    // the managed object of a processor (by its definition), which is reused 
when its route is started again
+    private final Map<Processor, WrappedProcessor> wrappedProcessors = new 
HashMap<>();
+    // the managed object of a processor (by its definition), which is reused 
when its route is started again,
+    // and shared by the routes that use the same definition (such as the 
outputs of a context scoped onException)
     private final Map<NamedNode, Object> managedProcessors = new HashMap<>();
     private final 
List<java.util.function.Consumer<JmxManagementLifecycleStrategy>> preServices = 
new ArrayList<>();
     private final TimerListenerManager loadTimer = new ManagedLoadTimer();
@@ -623,6 +625,13 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
 
         Object managedObject = getManagedObjectForService(context, service, 
route);
         if (managedObject != null) {
+            Map.Entry<Processor, WrappedProcessor> other
+                    = service instanceof Processor processor ? 
getProcessorOfOtherRoute(processor) : null;
+            if (other != null) {
+                // the definition is shared with another route that still uses 
the mbean, so keep it
+                reattachProcessor(managedObject, other);
+                return;
+            }
             try {
                 unmanageObject(managedObject);
             } catch (Exception e) {
@@ -630,9 +639,58 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
             }
         }
         if (service instanceof Processor processor) {
-            KeyValueHolder<NamedNode, InstrumentationProcessor<?>> holder = 
wrappedProcessors.get(processor);
+            WrappedProcessor holder = wrappedProcessors.get(processor);
             if (holder != null) {
-                managedProcessors.remove(holder.getKey());
+                managedProcessors.remove(holder.definition());
+            }
+        }
+    }
+
+    /**
+     * Gets a processor of another route that is created from the same 
definition as the given processor, such as the
+     * outputs of a context scoped onException, which is the same definition 
in every route.
+     */
+    private Map.Entry<Processor, WrappedProcessor> 
getProcessorOfOtherRoute(Processor processor) {
+        WrappedProcessor holder = wrappedProcessors.get(processor);
+        if (holder == null || isRouteDefinition(holder)) {
+            return null;
+        }
+        String routeId = holder.route().getId();
+        for (Map.Entry<Processor, WrappedProcessor> entry : 
wrappedProcessors.entrySet()) {
+            WrappedProcessor other = entry.getValue();
+            if (other.definition() == holder.definition() && 
!routeId.equals(other.route().getId())) {
+                return entry;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Whether the definition of the wrapped processor belongs to the route of 
the processor (and not to several routes)
+     */
+    private static boolean isRouteDefinition(WrappedProcessor holder) {
+        RouteDefinition def = 
ProcessorDefinitionHelper.getRoute(holder.definition());
+        return def != null && holder.route().getId().equals(def.getId());
+    }
+
+    /**
+     * Attaches the managed object of a processor, whose route is being 
removed, to the processor of another route that
+     * uses the same definition.
+     */
+    private void reattachProcessor(Object managedObject, Map.Entry<Processor, 
WrappedProcessor> other) {
+        if (managedObject instanceof ManagedProcessor mp) {
+            mp.setProcessor(other.getKey());
+            Route route = other.getValue().route();
+            if (mp.getRoute() != route) {
+                // register the mbean again so it shows (and is listed under) 
the route that still uses it
+                try {
+                    unmanageObject(mp);
+                    mp.setRoute(route);
+                    manageObject(mp);
+                } catch (Exception e) {
+                    LOG.warn("Could not register processor: {} as Processor 
MBean of route: {}", mp.getProcessorId(),
+                            route.getId(), e);
+                }
             }
         }
     }
@@ -748,7 +806,7 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
         // a bit of magic here as the processors we want to manage have 
already been registered
         // in the wrapped processors map when Camel have instrumented the 
route on route initialization
         // so the idea is now to only manage the processors from the map
-        KeyValueHolder<NamedNode, InstrumentationProcessor<?>> holder = 
wrappedProcessors.get(processor);
+        WrappedProcessor holder = wrappedProcessors.get(processor);
         if (holder == null) {
             // skip as it's not a well known processor we want to manage 
anyway, such as Channel/UnitOfWork/Pipeline etc.
             return null;
@@ -756,20 +814,21 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
 
         // reuse the managed object of the processor, as it is already 
registered when its route is started again
         // (otherwise the statistics would be counted on a new object that is 
not the registered MBean)
-        Object managedObject = managedProcessors.get(holder.getKey());
+        Object managedObject = managedProcessors.get(holder.definition());
         if (managedObject == null) {
             // get the managed object as it can be a specialized type such as 
a Delayer/Throttler etc.
             managedObject
-                    = 
getManagementObjectStrategy().getManagedObjectForProcessor(context, processor, 
holder.getKey(), route);
+                    = 
getManagementObjectStrategy().getManagedObjectForProcessor(context, processor, 
holder.definition(),
+                            route);
             if (managedObject != null) {
-                managedProcessors.put(holder.getKey(), managedObject);
+                managedProcessors.put(holder.definition(), managedObject);
             }
         }
         // only manage if we have a name for it as otherwise we do not want to 
manage it anyway
         if (managedObject != null) {
             // is it a performance counter then we need to set our counter
             if (managedObject instanceof PerformanceCounter) {
-                InstrumentationProcessor<?> counter = holder.getValue();
+                InstrumentationProcessor<?> counter = 
holder.instrumentationProcessor();
                 if (counter != null) {
                     // change counter to us
                     counter.setCounter(managedObject);
@@ -1040,7 +1099,8 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
 
         // set this managed intercept strategy that executes the JMX 
instrumentation for performance metrics
         // so our registered counters can be used for fine-grained performance 
instrumentation
-        route.setManagementInterceptStrategy(new 
InstrumentationInterceptStrategy(registeredCounters, wrappedProcessors));
+        route.setManagementInterceptStrategy(
+                new InstrumentationInterceptStrategy(registeredCounters, 
wrappedProcessors, route));
     }
 
     /**
@@ -1052,18 +1112,31 @@ public class JmxManagementLifecycleStrategy extends 
ServiceSupport implements Li
      */
     private void removeWrappedProcessorsForRoutes(Collection<Route> routes) {
         // loop the routes, and remove the route associated wrapped 
processors, as they are no longer in use
+        // (by the route they were created for, as a definition can be shared 
by several routes, such as the outputs
+        // of a context scoped onException, whose parent is not a route)
         for (Route route : routes) {
             String id = route.getId();
 
-            Iterator<KeyValueHolder<NamedNode, InstrumentationProcessor<?>>> 
it = wrappedProcessors.values().iterator();
+            Set<NamedNode> shared = new HashSet<>();
+            Iterator<WrappedProcessor> it = 
wrappedProcessors.values().iterator();
             while (it.hasNext()) {
-                KeyValueHolder<NamedNode, InstrumentationProcessor<?>> holder 
= it.next();
-                RouteDefinition def = 
ProcessorDefinitionHelper.getRoute(holder.getKey());
-                if (def != null && id.equals(def.getId())) {
-                    managedProcessors.remove(holder.getKey());
+                WrappedProcessor holder = it.next();
+                if (id.equals(holder.route().getId())) {
+                    if (isRouteDefinition(holder)) {
+                        managedProcessors.remove(holder.definition());
+                    } else {
+                        shared.add(holder.definition());
+                    }
                     it.remove();
                 }
             }
+            // keep the managed object of a shared definition that is still in 
use by another route
+            if (!shared.isEmpty()) {
+                for (WrappedProcessor holder : wrappedProcessors.values()) {
+                    shared.remove(holder.definition());
+                }
+                shared.forEach(managedProcessors::remove);
+            }
         }
     }
 
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/InstrumentationInterceptStrategyTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/InstrumentationInterceptStrategyTest.java
new file mode 100644
index 000000000000..41aac4079b28
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/InstrumentationInterceptStrategyTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.management;
+
+import java.util.HashMap;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class InstrumentationInterceptStrategyTest {
+
+    @Test
+    public void testRouteMustNotBeNull() {
+        IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class,
+                () -> new InstrumentationInterceptStrategy(new HashMap<>(), 
new HashMap<>(), null));
+        assertEquals("route must be specified", e.getMessage());
+    }
+}
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoveContextScopedOnExceptionTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoveContextScopedOnExceptionTest.java
new file mode 100644
index 000000000000..4f19c41b5bbb
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoveContextScopedOnExceptionTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.management;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static 
org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_PROCESSOR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The outputs of a context scoped onException are the same definition in 
every route, and share one processor mbean.
+ */
+@DisabledOnOs(OS.AIX)
+class ManagedRouteRemoveContextScopedOnExceptionTest extends 
ManagementTestSupport {
+
+    @Test
+    void testRemoveRouteKeepsMBeanOfOtherRoute() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on = getCamelObjectName(TYPE_PROCESSOR, "myError");
+
+        template.sendBody("direct:a", "A");
+        template.sendBody("direct:b", "B");
+        assertEquals(2L, mbeanServer.getAttribute(on, "ExchangesTotal"));
+        assertEquals("a", mbeanServer.getAttribute(on, "RouteId"));
+
+        context.getRouteController().stopRoute("a");
+        context.removeRoute("a");
+
+        assertTrue(mbeanServer.isRegistered(on), "The onException processor 
mbean should still be registered for route b");
+        assertEquals("b", mbeanServer.getAttribute(on, "RouteId"));
+        assertEquals("Started", mbeanServer.getAttribute(on, "State"));
+        DefaultManagementAgent agent = (DefaultManagementAgent) 
context.getManagementStrategy().getManagementAgent();
+        assertTrue(agent.getRouteProcessorMBeanNames("b", false).contains(on), 
"Should be a processor of route b");
+        assertFalse(agent.getRouteProcessorMBeanNames("a", 
false).contains(on), "Should not be a processor of route a");
+
+        template.sendBody("direct:b", "B");
+        assertEquals(3L, mbeanServer.getAttribute(on, "ExchangesTotal"));
+    }
+
+    @Test
+    void testRemoveOtherRoute() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on = getCamelObjectName(TYPE_PROCESSOR, "myError");
+
+        template.sendBody("direct:a", "A");
+        template.sendBody("direct:b", "B");
+
+        context.getRouteController().stopRoute("b");
+        context.removeRoute("b");
+
+        assertTrue(mbeanServer.isRegistered(on), "The onException processor 
mbean should still be registered for route a");
+        assertEquals("a", mbeanServer.getAttribute(on, "RouteId"));
+        assertEquals("Started", mbeanServer.getAttribute(on, "State"));
+
+        template.sendBody("direct:a", "A");
+        assertEquals(3L, mbeanServer.getAttribute(on, "ExchangesTotal"));
+    }
+
+    @Test
+    void testRemoveAllRoutes() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on = getCamelObjectName(TYPE_PROCESSOR, "myError");
+
+        template.sendBody("direct:a", "A");
+        template.sendBody("direct:b", "B");
+
+        context.getRouteController().stopRoute("a");
+        context.removeRoute("a");
+        context.getRouteController().stopRoute("b");
+        context.removeRoute("b");
+
+        assertFalse(mbeanServer.isRegistered(on), "The onException processor 
mbean should be unregistered");
+        assertEquals(0, wrappedProcessors().size(), "No wrapped processors 
should be left after removing all routes");
+    }
+
+    private Map<?, ?> wrappedProcessors() throws Exception {
+        JmxManagementLifecycleStrategy strategy = 
context.getLifecycleStrategies().stream()
+                .filter(JmxManagementLifecycleStrategy.class::isInstance)
+                .map(JmxManagementLifecycleStrategy.class::cast)
+                .findFirst().orElseThrow();
+        Field field = 
JmxManagementLifecycleStrategy.class.getDeclaredField("wrappedProcessors");
+        field.setAccessible(true);
+        return (Map<?, ?>) field.get(strategy);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // context scoped
+                
onException(Exception.class).handled(true).to("mock:error").id("myError");
+
+                from("direct:a").routeId("a").throwException(new 
IllegalArgumentException("Forced a"));
+                from("direct:b").routeId("b").throwException(new 
IllegalArgumentException("Forced b"));
+            }
+        };
+    }
+}
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoveSharedProcessorTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoveSharedProcessorTest.java
new file mode 100644
index 000000000000..7a8cac41873a
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteRemoveSharedProcessorTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.management;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.builder.RouteConfigurationBuilder;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static 
org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_PROCESSOR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The outputs of a context scoped onCompletion and of the onException of a 
route configuration are also the same
+ * definition in every route that uses them, and share one processor mbean.
+ */
+@DisabledOnOs(OS.AIX)
+class ManagedRouteRemoveSharedProcessorTest extends ManagementTestSupport {
+
+    @Test
+    void testRemoveRouteKeepsMBeansOfOtherRoute() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName done = getCamelObjectName(TYPE_PROCESSOR, "myDone");
+        ObjectName error = getCamelObjectName(TYPE_PROCESSOR, "myError");
+
+        template.sendBody("direct:a", "A");
+        template.sendBody("direct:b", "B");
+        assertEquals(2L, mbeanServer.getAttribute(done, "ExchangesTotal"));
+        assertEquals(2L, mbeanServer.getAttribute(error, "ExchangesTotal"));
+
+        context.getRouteController().stopRoute("a");
+        context.removeRoute("a");
+
+        for (ObjectName on : new ObjectName[] { done, error }) {
+            assertTrue(mbeanServer.isRegistered(on), on + " should still be 
registered for route b");
+            assertEquals("b", mbeanServer.getAttribute(on, "RouteId"));
+        }
+        template.sendBody("direct:b", "B");
+        assertEquals(3L, mbeanServer.getAttribute(done, "ExchangesTotal"));
+        assertEquals(3L, mbeanServer.getAttribute(error, "ExchangesTotal"));
+    }
+
+    @Override
+    protected RoutesBuilder[] createRouteBuilders() {
+        RoutesBuilder configuration = new RouteConfigurationBuilder() {
+            @Override
+            public void configuration() {
+                
routeConfiguration("myConfig").onException(Exception.class).handled(true).to("mock:error").id("myError");
+            }
+        };
+        RoutesBuilder routes = new RouteBuilder() {
+            @Override
+            public void configure() {
+                // context scoped
+                onCompletion().to("mock:done").id("myDone");
+
+                from("direct:a").routeId("a").routeConfigurationId("myConfig")
+                        .throwException(new IllegalArgumentException("Forced 
a"));
+                from("direct:b").routeId("b").routeConfigurationId("myConfig")
+                        .throwException(new IllegalArgumentException("Forced 
b"));
+            }
+        };
+        return new RoutesBuilder[] { configuration, routes };
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 2efde4c330fa..3164ab7f11d3 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -742,6 +742,10 @@ succeeded on its second redelivery reported 1 instead of 
2, and a route whose ex
 completed reported 0), and a processor or route that the exchange went through 
after a redelivery could count that
 redelivery as well.
 
+The constructor of 
`org.apache.camel.management.InstrumentationInterceptStrategy` (used internally 
by
+`JmxManagementLifecycleStrategy`) now takes a map of 
`InstrumentationInterceptStrategy.WrappedProcessor` values and the
+`Route` whose processors it instruments, which must not be `null`.
+
 ==== mbeansLevel=ContextOnly
 
 With `mbeansLevel=ContextOnly` the components are no longer registered as 
MBeans (like the endpoints, thread pools

Reply via email to