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 eefa74fb8a2f CAMEL-25072: camel-management - EIP and service MBeans: 
fix the remaining follow-ups from the deep review (#27074)
eefa74fb8a2f is described below

commit eefa74fb8a2f75fda0d6c4eaf89823a4ea053880
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 29 14:00:34 2026 +0200

    CAMEL-25072: camel-management - EIP and service MBeans: fix the remaining 
follow-ups from the deep review (#27074)
    
    * CAMEL-25072: camel-management - EIP and service MBeans: fix the remaining 
follow-ups from the deep review
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../engine/DefaultEndpointServiceRegistry.java     |  10 +-
 .../api/management/mbean/CamelOpenMBeanTypes.java  |   2 +-
 .../mbean/ManagedEndpointServiceRegistry.java      |   4 +
 .../management/mbean/ManagedEventNotifier.java     |  34 +++---
 .../camel/management/mbean/StatisticMaximum.java   |  12 +-
 .../camel/management/mbean/StatisticMinimum.java   |  12 +-
 .../JmxNotificationEventNotifierTest.java          |  25 +++++
 .../ManagedEndpointServiceRegistryRuntimeTest.java |  37 ++++++
 .../ManagedEndpointServiceRegistryTest.java        | 125 +++++++++++++++++++++
 .../mbean/StatisticMinimumMaximumTest.java         |  95 ++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   7 ++
 11 files changed, 330 insertions(+), 33 deletions(-)

diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultEndpointServiceRegistry.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultEndpointServiceRegistry.java
index c949d02ed401..5a7fe24c1444 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultEndpointServiceRegistry.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultEndpointServiceRegistry.java
@@ -83,7 +83,8 @@ public class DefaultEndpointServiceRegistry extends 
ServiceSupport implements En
                 hosted = dc.isHostedService();
                 routeId = dc.getRouteId();
             }
-            var stat = findStats(endpoint.getEndpointUri(), dir);
+            // the stats of the route of the consumer, as several routes can 
consume from the same endpoint
+            var stat = findStats(endpoint.getEndpointUri(), dir, routeId);
             long hits = 0;
             if (stat.isPresent()) {
                 var s = stat.get();
@@ -92,7 +93,7 @@ public class DefaultEndpointServiceRegistry extends 
ServiceSupport implements En
             }
             if ("out".equals(dir) && stat.isEmpty()) {
                 // no OUT stat, then the endpoint may be used only for IN
-                stat = findStats(endpoint.getEndpointUri(), "in");
+                stat = findStats(endpoint.getEndpointUri(), "in", null);
                 if (stat.isPresent()) {
                     return null;
                 }
@@ -115,12 +116,13 @@ public class DefaultEndpointServiceRegistry extends 
ServiceSupport implements En
         return size;
     }
 
-    private Optional<RuntimeEndpointRegistry.Statistic> findStats(String uri, 
String direction) {
+    private Optional<RuntimeEndpointRegistry.Statistic> findStats(String uri, 
String direction, String routeId) {
         if (camelContext.getRuntimeEndpointRegistry() == null) {
             return Optional.empty();
         }
         return 
camelContext.getRuntimeEndpointRegistry().getEndpointStatistics().stream()
-                .filter(s -> uri.equals(s.getUri()) && (direction == null || 
s.getDirection().equals(direction)))
+                .filter(s -> uri.equals(s.getUri()) && (direction == null || 
s.getDirection().equals(direction))
+                        && (routeId == null || routeId.equals(s.getRouteId())))
                 .findFirst();
     }
 
diff --git 
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
 
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
index c1a686c01abf..5d5a65249025 100644
--- 
a/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
+++ 
b/core/camel-management-api/src/main/java/org/apache/camel/api/management/mbean/CamelOpenMBeanTypes.java
@@ -34,7 +34,7 @@ public final class CamelOpenMBeanTypes {
         CompositeType ct = listEndpointServicesCompositeType();
         return new TabularType(
                 "listEndpointServices", "Lists all the endpoint services in 
the registry", ct,
-                new String[] { "component", "dir", "serviceUrl", "endpointUri" 
});
+                new String[] { "component", "dir", "serviceUrl", 
"endpointUri", "routeId" });
     }
 
     public static CompositeType listEndpointServicesCompositeType() throws 
OpenDataException {
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEndpointServiceRegistry.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEndpointServiceRegistry.java
index 56428490709f..b933dfe8aeab 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEndpointServiceRegistry.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEndpointServiceRegistry.java
@@ -88,6 +88,10 @@ public class ManagedEndpointServiceRegistry extends 
ManagedService implements Ma
                     m.forEach((k, v) -> sj.add(k + "=" + v));
                     metadata = sj.toString();
                 }
+                if (answer.containsKey(new Object[] { component, dir, 
serviceUrl, endpointUri, routeId })) {
+                    // endpoints of the same route that only differ in a 
secret are the same uri when sanitized
+                    continue;
+                }
 
                 CompositeData data = new CompositeDataSupport(
                         ct,
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEventNotifier.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEventNotifier.java
index 1ccfe2c12dfc..e8383d672990 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEventNotifier.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedEventNotifier.java
@@ -20,10 +20,12 @@ import java.util.ArrayList;
 import java.util.List;
 
 import javax.management.MBeanNotificationInfo;
+import javax.management.Notification;
 import javax.management.NotificationBroadcasterSupport;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.api.management.JmxNotificationBroadcasterAware;
+import org.apache.camel.spi.CamelEvent;
 import org.apache.camel.spi.EventNotifier;
 import org.apache.camel.spi.ManagementStrategy;
 
@@ -163,26 +165,22 @@ public class ManagedEventNotifier extends 
NotificationBroadcasterSupport impleme
 
     @Override
     public MBeanNotificationInfo[] getNotificationInfo() {
-        // all the class names in the event package
-        String[] names = {
-                "CamelContextStartedEvent", "CamelContextStartingEvent", 
"CamelContextStartupFailureEvent",
-                "CamelContextStopFailureEvent", "CamelContextStoppedEvent", 
"CamelContextStoppingEvent",
-                "CamelContextSuspendingEvent", "CamelContextSuspendedEvent", 
"CamelContextResumingEvent",
-                "CamelContextResumedEvent",
-                "CamelContextResumeFailureEvent", "ExchangeCompletedEvent", 
"ExchangeCreatedEvent", "ExchangeFailedEvent",
-                "ExchangeFailureHandledEvent", "ExchangeRedeliveryEvents", 
"ExchangeSendingEvent", "ExchangeSentEvent",
-                "RouteStartedEvent",
-                "RouteStoppedEvent", "ServiceStartupFailureEvent", 
"ServiceStopFailureEvent",
-                "StepStartedEvent", "StepCompletedEvent", "StepFailedEvent" };
-
+        // JmxNotificationEventNotifier uses the simple class name of the 
event as the notification type
         List<MBeanNotificationInfo> infos = new ArrayList<>();
-        for (String name : names) {
-            MBeanNotificationInfo info = new MBeanNotificationInfo(
-                    new String[] { "org.apache.camel.management.event" },
-                    "org.apache.camel.management.event." + name, "The event " 
+ name + " occurred");
-            infos.add(info);
+        for (CamelEvent.Type type : CamelEvent.Type.values()) {
+            if (type == CamelEvent.Type.Custom) {
+                // custom events have their own class names
+                continue;
+            }
+            String name = type.name();
+            if (name.startsWith("Routes")) {
+                // the routes events of the CamelContext
+                name = "CamelContext" + name;
+            }
+            name = name + "Event";
+            infos.add(new MBeanNotificationInfo(
+                    new String[] { name }, Notification.class.getName(), "The 
event " + name + " occurred"));
         }
-
         return infos.toArray(new MBeanNotificationInfo[0]);
     }
 
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMaximum.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMaximum.java
index 01aef94189f0..39e752c048d7 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMaximum.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMaximum.java
@@ -24,12 +24,14 @@ public class StatisticMaximum extends Statistic {
 
     @Override
     public void updateValue(long newValue) {
-        // its okay its not 100% thread safe (these jmx counters are not 
guaranteed to be accurate for min/max values)
-        // if we use the atomic operation updateAndGet then the JVM creates a 
new lambda per call which creates a new object
-        // in the JVM and causes higher memory footprint
+        // compare and set in a loop (and not updateAndGet, which creates a 
new lambda per call), so a concurrent
+        // update cannot be lost
         long current = value.get();
-        if (current == -1 || current < newValue) {
-            value.set(newValue);
+        while (current == -1 || current < newValue) {
+            if (value.compareAndSet(current, newValue)) {
+                return;
+            }
+            current = value.get();
         }
     }
 
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMinimum.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMinimum.java
index 3e7872a07c9b..abacb4cbcd38 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMinimum.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticMinimum.java
@@ -24,12 +24,14 @@ public class StatisticMinimum extends Statistic {
 
     @Override
     public void updateValue(long newValue) {
-        // its okay its not 100% thread safe (these jmx counters are not 
guaranteed to be accurate for min/max values)
-        // if we use the atomic operation updateAndGet then the JVM creates a 
new lambda per call which creates a new object
-        // in the JVM and causes higher memory footprint
+        // compare and set in a loop (and not updateAndGet, which creates a 
new lambda per call), so a concurrent
+        // update cannot be lost
         long current = value.get();
-        if (current == -1 || current > newValue) {
-            value.set(newValue);
+        while (current == -1 || current > newValue) {
+            if (value.compareAndSet(current, newValue)) {
+                return;
+            }
+            current = value.get();
         }
     }
 
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/JmxNotificationEventNotifierTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/JmxNotificationEventNotifierTest.java
index 1785833dc61b..278a5ec7f86d 100644
--- 
a/core/camel-management/src/test/java/org/apache/camel/management/JmxNotificationEventNotifierTest.java
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/JmxNotificationEventNotifierTest.java
@@ -16,6 +16,11 @@
  */
 package org.apache.camel.management;
 
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.management.MBeanNotificationInfo;
 import javax.management.Notification;
 import javax.management.NotificationFilter;
 import javax.management.NotificationListener;
@@ -30,7 +35,9 @@ import org.junit.jupiter.api.condition.OS;
 
 import static 
org.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_EVENT_NOTIFIER;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @DisabledOnOs(OS.AIX)
 public class JmxNotificationEventNotifierTest extends ManagementTestSupport {
@@ -77,6 +84,18 @@ public class JmxNotificationEventNotifierTest extends 
ManagementTestSupport {
 
         assertEquals(8, listener.getEventCounter(), "Get a wrong number of 
events");
 
+        // the notification types that were sent are advertised by the MBean
+        Set<String> advertised = new HashSet<>();
+        for (MBeanNotificationInfo info : 
context.getManagementStrategy().getManagementAgent().getMBeanServer()
+                .getMBeanInfo(on).getNotifications()) {
+            assertEquals(Notification.class.getName(), info.getName());
+            advertised.addAll(Arrays.asList(info.getNotifTypes()));
+        }
+        assertFalse(listener.getTypes().isEmpty());
+        for (String type : listener.getTypes()) {
+            assertTrue(advertised.contains(type), "Notification type " + type 
+ " should be advertised");
+        }
+
         context.stop();
     }
 
@@ -117,11 +136,17 @@ public class JmxNotificationEventNotifierTest extends 
ManagementTestSupport {
     private class MyNotificationListener implements NotificationListener {
 
         private int eventCounter;
+        private final Set<String> types = new HashSet<>();
 
         @Override
         public void handleNotification(Notification notification, Object 
handback) {
             log.debug("Get the notification : {}", notification);
             eventCounter++;
+            types.add(notification.getType());
+        }
+
+        public Set<String> getTypes() {
+            return types;
         }
 
         public int getEventCounter() {
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointServiceRegistryRuntimeTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointServiceRegistryRuntimeTest.java
new file mode 100644
index 000000000000..0d4d545af397
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointServiceRegistryRuntimeTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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 org.apache.camel.CamelContext;
+import org.apache.camel.impl.engine.DefaultRuntimeEndpointRegistry;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+/**
+ * The same as {@link ManagedEndpointServiceRegistryTest} with the runtime 
endpoint registry enabled, which gives the
+ * hits and route of each endpoint service.
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedEndpointServiceRegistryRuntimeTest extends 
ManagedEndpointServiceRegistryTest {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.setRuntimeEndpointRegistry(new 
DefaultRuntimeEndpointRegistry());
+        return context;
+    }
+}
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointServiceRegistryTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointServiceRegistryTest.java
new file mode 100644
index 000000000000..836644a4d50c
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointServiceRegistryTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import javax.management.openmbean.CompositeData;
+import javax.management.openmbean.TabularData;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.Endpoint;
+import org.apache.camel.MultipleConsumersSupport;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.EndpointServiceLocation;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.DefaultEndpoint;
+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_SERVICE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisabledOnOs(OS.AIX)
+public class ManagedEndpointServiceRegistryTest extends ManagementTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        context.addComponent("service", new DefaultComponent() {
+            @Override
+            protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+                return new ServiceEndpoint(uri, this);
+            }
+        });
+        // the registry is created on first use, so create it before the 
context is started to have it managed
+        context.getCamelContextExtension().getEndpointServiceRegistry();
+        return context;
+    }
+
+    @Test
+    public void testTwoRoutesConsumeSameService() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+        ObjectName on = getCamelObjectName(TYPE_SERVICE, 
"DefaultEndpointServiceRegistry");
+
+        // both routes consume the same service, which failed with 
KeyAlreadyExistsException
+        TabularData data = (TabularData) mbeanServer.invoke(on, 
"listEndpointServices", null, null);
+
+        List<String> routeIds = new ArrayList<>();
+        for (Object row : data.values()) {
+            CompositeData cd = (CompositeData) row;
+            assertEquals("localhost:8080", cd.get("serviceUrl"));
+            if ("in".equals(cd.get("dir"))) {
+                routeIds.add((String) cd.get("routeId"));
+            }
+        }
+        routeIds.sort(null);
+        assertEquals(List.of("a", "b"), routeIds);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("service:foo").routeId("a").to("mock:a");
+                from("service:foo").routeId("b").to("mock:b");
+            }
+        };
+    }
+
+    private static class ServiceEndpoint extends DefaultEndpoint implements 
EndpointServiceLocation, MultipleConsumersSupport {
+
+        ServiceEndpoint(String uri, DefaultComponent component) {
+            super(uri, component);
+        }
+
+        @Override
+        public String getServiceUrl() {
+            return "localhost:8080";
+        }
+
+        @Override
+        public String getServiceProtocol() {
+            return "tcp";
+        }
+
+        @Override
+        public boolean isMultipleConsumersSupported() {
+            return true;
+        }
+
+        @Override
+        public Producer createProducer() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Consumer createConsumer(Processor processor) {
+            return new DefaultConsumer(this, processor);
+        }
+    }
+}
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/mbean/StatisticMinimumMaximumTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/mbean/StatisticMinimumMaximumTest.java
new file mode 100644
index 000000000000..f9994aeecc4a
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/mbean/StatisticMinimumMaximumTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.mbean;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class StatisticMinimumMaximumTest {
+
+    private static final int THREADS = 8;
+    private static final int UPDATES = 20000;
+
+    @Test
+    public void testMinimumMaximum() {
+        StatisticMinimum min = new StatisticMinimum();
+        StatisticMaximum max = new StatisticMaximum();
+        assertFalse(min.isUpdated());
+        assertFalse(max.isUpdated());
+        assertEquals(0, min.getValue());
+        assertEquals(0, max.getValue());
+
+        for (long v : new long[] { 5, 3, 8, 3, 6 }) {
+            min.updateValue(v);
+            max.updateValue(v);
+        }
+        assertTrue(min.isUpdated());
+        assertEquals(3, min.getValue());
+        assertEquals(8, max.getValue());
+
+        min.reset();
+        max.reset();
+        assertFalse(min.isUpdated());
+        assertFalse(max.isUpdated());
+    }
+
+    @RepeatedTest(5)
+    public void testConcurrentUpdates() throws Exception {
+        StatisticMinimum min = new StatisticMinimum();
+        StatisticMaximum max = new StatisticMaximum();
+
+        // each thread updates with its own range of values, in the order that 
makes each update a new min and max
+        ExecutorService executor = Executors.newFixedThreadPool(THREADS);
+        try {
+            CountDownLatch start = new CountDownLatch(1);
+            List<Future<?>> futures = new ArrayList<>();
+            for (int t = 0; t < THREADS; t++) {
+                final int thread = t;
+                futures.add(executor.submit(() -> {
+                    start.await();
+                    for (int i = 0; i < UPDATES; i++) {
+                        max.updateValue((long) i * THREADS + thread + 1);
+                        min.updateValue((long) (UPDATES - i) * THREADS - 
thread);
+                    }
+                    return null;
+                }));
+            }
+            start.countDown();
+            for (Future<?> future : futures) {
+                future.get(30, TimeUnit.SECONDS);
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+
+        // a lost update would leave a value from another thread that is not 
the real min or max
+        assertEquals((long) UPDATES * THREADS, max.getValue());
+        assertEquals(1L, min.getValue());
+    }
+}
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 bbe263fc2f08..370e1e134d0f 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
@@ -564,6 +564,13 @@ Some tabular data returned by the JMX MBeans had a key 
that was not unique, so t
 `KeyAlreadyExistsException`. The tabular data of the Choice EIP and doTry EIP 
`extendedInformation` and of
 `listTasks` of the task manager registry now have an `index` item as their 
key, the exchange factories of `listStatistics`
 are keyed by `url` and `routeId`, and endpoints that only differ in a secret 
(which is masked) are shown once.
+The `listEndpointServices` operation of the endpoint service registry is keyed 
by `routeId` as well, so two routes
+that consume from the same service are both listed.
+
+The notification types advertised by the event notifier MBean 
(`getNotificationInfo`) are now the types that
+`JmxNotificationEventNotifier` sends, which is the simple class name of the 
event (such as `ExchangeCompletedEvent`),
+and the notification class is `javax.management.Notification`. Before, the 
advertised types used a
+`org.apache.camel.management.event.` prefix and several events were missing.
 
 === camel-exec
 

Reply via email to