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 7315c70a249b CAMEL-26393: camel-management - JMX performance counters: 
reduce contention on the per-exchange path (#28760)
7315c70a249b is described below

commit 7315c70a249b3de311ee6a0a1771a265a4ee3298
Author: Adriano Machado <[email protected]>
AuthorDate: Sat Oct 31 05:29:16 2026 -0400

    CAMEL-26393: camel-management - JMX performance counters: reduce contention 
on the per-exchange path (#28760)
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../management/mbean/ManagedCamelContext.java      |   2 +-
 .../mbean/ManagedPerformanceCounter.java           |  27 ++-
 .../camel/management/mbean/ManagedRoute.java       |   2 +-
 .../camel/management/mbean/StatisticCounter.java   |  16 +-
 .../camel/management/mbean/StatisticDelta.java     |  10 +-
 .../camel/management/mbean/StatisticValue.java     |   5 +-
 .../camel/management/ManagedStatisticsTest.java    |   4 +-
 .../mbean/ManagedPerformanceCounterTest.java       | 202 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   7 +
 9 files changed, 247 insertions(+), 28 deletions(-)

diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
index 342212324621..c4ca9819ae53 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedCamelContext.java
@@ -408,7 +408,7 @@ public class ManagedCamelContext extends 
ManagedPerformanceCounter implements Ma
 
     @Override
     public long getRemoteExchangesInflight() {
-        return remoteExchangesInflight.getValue();
+        return Math.max(0, remoteExchangesInflight.getValue());
     }
 
     @Override
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
index e0bf112a7c0f..1e26083c5930 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedPerformanceCounter.java
@@ -57,7 +57,6 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
     private Statistic totalProcessingTime;
     private Statistic lastProcessingTime;
     private Statistic deltaProcessingTime;
-    private Statistic meanProcessingTime;
     private Statistic firstExchangeCompletedTimestamp;
     private String firstExchangeCompletedExchangeId;
     private Statistic firstExchangeFailureTimestamp;
@@ -92,7 +91,6 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
         this.totalProcessingTime = new StatisticCounter();
         this.lastProcessingTime = new StatisticValue();
         this.deltaProcessingTime = new StatisticDelta();
-        this.meanProcessingTime = new StatisticValue();
 
         this.firstExchangeCompletedTimestamp = new StatisticValue();
         this.firstExchangeFailureTimestamp = new StatisticValue();
@@ -122,7 +120,6 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
         totalProcessingTime.reset();
         lastProcessingTime.reset();
         deltaProcessingTime.reset();
-        meanProcessingTime.reset();
         firstExchangeCompletedTimestamp.reset();
         firstExchangeCompletedExchangeId = null;
         firstExchangeFailureTimestamp.reset();
@@ -152,7 +149,8 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
 
     @Override
     public long getExchangesInflight() {
-        return exchangesInflight.getValue();
+        // the sum of the counter can see a decrement before its increment
+        return Math.max(0, exchangesInflight.getValue());
     }
 
     @Override
@@ -177,7 +175,9 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
 
     @Override
     public long getMeanProcessingTime() {
-        return meanProcessingTime.getValue();
+        // computed here as summing the counters is too costly to do for every 
exchange
+        long completed = exchangesCompleted.getValue();
+        return completed > 0 ? totalProcessingTime.getValue() / completed : -1;
     }
 
     @Override
@@ -374,19 +374,14 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
             firstExchangeCompletedTimestamp.updateValue(now);
         }
 
-        lastExchangeCompletedTimestamp.updateValue(now);
         if (firstExchangeCompletedExchangeId == null) {
             firstExchangeCompletedExchangeId = exchange.getExchangeId();
         }
-        lastExchangeCompletedExchangeId = exchange.getExchangeId();
-
-        // update mean
-        long mean = 0;
-        long completed = exchangesCompleted.getValue();
-        if (completed > 0) {
-            mean = totalProcessingTime.getValue() / completed;
+        // at most once per millisecond, as getting the id generates it for an 
exchange that has none yet
+        if (lastExchangeCompletedTimestamp.getValue() != now) {
+            lastExchangeCompletedExchangeId = exchange.getExchangeId();
+            lastExchangeCompletedTimestamp.updateValue(now);
         }
-        meanProcessingTime.updateValue(mean);
     }
 
     @Override
@@ -445,7 +440,7 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
         sb.append(String.format(" totalProcessingTime=\"%s\"", 
totalProcessingTime.getValue()));
         sb.append(String.format(" lastProcessingTime=\"%s\"", 
lastProcessingTime.getValue()));
         sb.append(String.format(" deltaProcessingTime=\"%s\"", 
deltaProcessingTime.getValue()));
-        sb.append(String.format(" meanProcessingTime=\"%s\"", 
meanProcessingTime.getValue()));
+        sb.append(String.format(" meanProcessingTime=\"%s\"", 
getMeanProcessingTime()));
         sb.append(String.format(" exchangesThroughput=\"%s\"", 
getThroughput()));
         if (percentileWindow != null) {
             sb.append(String.format(" p50ProcessingTime=\"%s\"", 
getProcessingTimeP50()));
@@ -498,7 +493,7 @@ public abstract class ManagedPerformanceCounter extends 
ManagedCounter
         jo.put("totalProcessingTime", totalProcessingTime.getValue());
         jo.put("lastProcessingTime", lastProcessingTime.getValue());
         jo.put("deltaProcessingTime", deltaProcessingTime.getValue());
-        jo.put("meanProcessingTime", meanProcessingTime.getValue());
+        jo.put("meanProcessingTime", getMeanProcessingTime());
         jo.put("exchangesThroughput", getThroughput());
         if (percentileWindow != null) {
             jo.put("p50ProcessingTime", getProcessingTimeP50());
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
index cff094fc7858..be08000140f2 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/ManagedRoute.java
@@ -895,7 +895,7 @@ public class ManagedRoute extends ManagedPerformanceCounter 
implements ManagedRo
 
     @Override
     public long getRemoteExchangesInflight() {
-        return remoteConsumer ? getExchangesInflight() : 
remoteExchangesInflight.getValue();
+        return remoteConsumer ? getExchangesInflight() : Math.max(0, 
remoteExchangesInflight.getValue());
     }
 
     @Override
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticCounter.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticCounter.java
index cf0b1a6914dd..c96ed77fd542 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticCounter.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticCounter.java
@@ -16,30 +16,34 @@
  */
 package org.apache.camel.management.mbean;
 
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.LongAdder;
 
+/**
+ * A counter updated for every exchange by many threads, so it is striped: 
updates scale, and the value is summed when
+ * it is read. A value read during concurrent updates can miss an update that 
is in progress.
+ */
 public class StatisticCounter extends Statistic {
 
-    private final AtomicLong value = new AtomicLong();
+    private final LongAdder value = new LongAdder();
 
     @Override
     public void updateValue(long newValue) {
-        value.getAndAdd(newValue);
+        value.add(newValue);
     }
 
     @Override
     public long getValue() {
-        return value.get();
+        return value.sum();
     }
 
     @Override
     public String toString() {
-        return Long.toString(value.get());
+        return Long.toString(value.sum());
     }
 
     @Override
     public void reset() {
-        value.set(0);
+        value.reset();
     }
 
     @Override
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticDelta.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticDelta.java
index edbd18a18e5a..bb79656116eb 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticDelta.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticDelta.java
@@ -25,8 +25,14 @@ public class StatisticDelta extends Statistic {
 
     @Override
     public void updateValue(long newValue) {
-        lastValue.set(value.longValue());
-        value.set(newValue);
+        // a read leaves the cache line shared between threads, which a write 
for an unchanged value does not
+        long current = value.get();
+        if (lastValue.get() != current) {
+            lastValue.set(current);
+        }
+        if (current != newValue) {
+            value.set(newValue);
+        }
     }
 
     @Override
diff --git 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticValue.java
 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticValue.java
index 1d4686b6f80f..4b42848458dc 100644
--- 
a/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticValue.java
+++ 
b/core/camel-management/src/main/java/org/apache/camel/management/mbean/StatisticValue.java
@@ -24,7 +24,10 @@ public class StatisticValue extends Statistic {
 
     @Override
     public void updateValue(long newValue) {
-        value.set(newValue);
+        // a read leaves the cache line shared between threads, which a write 
for an unchanged value does not
+        if (value.get() != newValue) {
+            value.set(newValue);
+        }
     }
 
     @Override
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedStatisticsTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedStatisticsTest.java
index 968e5454c569..62a21a726657 100644
--- 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedStatisticsTest.java
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedStatisticsTest.java
@@ -30,6 +30,7 @@ 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.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @DisabledOnOs(OS.AIX)
 public class ManagedStatisticsTest extends ManagementTestSupport {
@@ -71,7 +72,8 @@ public class ManagedStatisticsTest extends 
ManagementTestSupport {
         assertNull(firstFail);
 
         String last = (String) mbeanServer.getAttribute(on, 
"LastExchangeCompletedExchangeId");
-        assertEquals(result.getReceivedExchanges().get(4).getExchangeId(), 
last);
+        // updated at most once per millisecond, and the exchanges can be sent 
within the same millisecond
+        assertTrue(result.getReceivedExchanges().stream().anyMatch(e -> 
e.getExchangeId().equals(last)));
 
         String lastFail = (String) mbeanServer.getAttribute(on, 
"LastExchangeFailureExchangeId");
         assertNull(lastFail);
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/mbean/ManagedPerformanceCounterTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/mbean/ManagedPerformanceCounterTest.java
new file mode 100644
index 000000000000..956fca9195f2
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/mbean/ManagedPerformanceCounterTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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 java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ManagedPerformanceCounterTest {
+
+    private static final int THREADS = 8;
+    private static final int UPDATES = 20000;
+
+    private CamelContext context;
+    private ManagedPerformanceCounter counter;
+
+    @BeforeEach
+    public void setUp() {
+        context = new DefaultCamelContext();
+        counter = new ManagedPerformanceCounter() {
+        };
+        counter.init(context.getManagementStrategy());
+    }
+
+    @AfterEach
+    public void tearDown() throws Exception {
+        context.close();
+    }
+
+    @Test
+    public void testMeanIsUnsetUntilAnExchangeCompletes() {
+        // monitoring tools rely on -1 meaning no data, which is not the same 
as a mean of 0 ms
+        assertEquals(-1, counter.getMeanProcessingTime());
+
+        counter.failedExchange(new DefaultExchange(context));
+        assertEquals(-1, counter.getMeanProcessingTime());
+
+        counter.completedExchange(new DefaultExchange(context), 10);
+        counter.completedExchange(new DefaultExchange(context), 20);
+        assertEquals(15, counter.getMeanProcessingTime());
+
+        counter.reset();
+        assertEquals(-1, counter.getMeanProcessingTime());
+    }
+
+    @Test
+    public void testDeltaIsZeroForEqualProcessingTimes() {
+        // repeated values must not leave the delta of the previous change 
behind
+        counter.completedExchange(new DefaultExchange(context), 5);
+        assertEquals(5, counter.getDeltaProcessingTime());
+        counter.completedExchange(new DefaultExchange(context), 8);
+        assertEquals(3, counter.getDeltaProcessingTime());
+        counter.completedExchange(new DefaultExchange(context), 8);
+        assertEquals(0, counter.getDeltaProcessingTime());
+        counter.completedExchange(new DefaultExchange(context), 2);
+        assertEquals(-6, counter.getDeltaProcessingTime());
+    }
+
+    @Test
+    public void testInflightSurvivesReset() {
+        // exchanges that are inflight during a reset complete afterwards, and 
must still be counted down
+        counter.processExchange(new DefaultExchange(context), "processor");
+        counter.reset();
+        assertEquals(1, counter.getExchangesInflight());
+
+        counter.completedExchange(new DefaultExchange(context), 1);
+        assertEquals(0, counter.getExchangesInflight());
+    }
+
+    @Test
+    public void testInflightIsNeverNegative() {
+        // the sum read over the striped cells of the counter can see a 
decrement before its increment
+        counter.completedExchange(new DefaultExchange(context), 1);
+        assertEquals(0, counter.getExchangesInflight());
+    }
+
+    @Test
+    public void testLastExchangeIdDoesNotGenerateAnIdPerExchange() {
+        // exchange ids are lazy and a random id is costly, so the counter 
must not ask each exchange for its id
+        AtomicLong generated = new AtomicLong();
+        context.setUuidGenerator(() -> "ID-" + generated.incrementAndGet());
+
+        int exchanges = 50000;
+        long start = System.currentTimeMillis();
+        for (int i = 0; i < exchanges; i++) {
+            counter.completedExchange(new DefaultExchange(context), 0);
+        }
+        long elapsed = System.currentTimeMillis() - start;
+
+        assertEquals(exchanges, counter.getExchangesCompleted());
+        // at most one id for every millisecond the loop was seen in
+        long max = elapsed + 1;
+        assertTrue(generated.get() <= max,
+                "Generated " + generated.get() + " ids in " + elapsed + " ms, 
expected at most " + max);
+    }
+
+    @Test
+    public void testLastExchangeIdIsUpdatedInALaterMillisecond() {
+        Exchange first = new DefaultExchange(context);
+        counter.completedExchange(first, 1);
+        assertEquals(first.getExchangeId(), 
counter.getFirstExchangeCompletedExchangeId());
+        assertEquals(first.getExchangeId(), 
counter.getLastExchangeCompletedExchangeId());
+
+        long time = counter.getLastExchangeCompletedTimestamp().getTime();
+        while (System.currentTimeMillis() <= time) {
+            Thread.onSpinWait();
+        }
+
+        Exchange second = new DefaultExchange(context);
+        counter.completedExchange(second, 1);
+        assertEquals(first.getExchangeId(), 
counter.getFirstExchangeCompletedExchangeId());
+        assertEquals(second.getExchangeId(), 
counter.getLastExchangeCompletedExchangeId());
+    }
+
+    @Test
+    public void testResetClearsLastExchangeId() {
+        counter.completedExchange(new DefaultExchange(context), 1);
+        counter.reset();
+        assertNull(counter.getLastExchangeCompletedExchangeId());
+
+        // also within the same millisecond as the exchange before the reset
+        Exchange exchange = new DefaultExchange(context);
+        counter.completedExchange(exchange, 1);
+        assertEquals(exchange.getExchangeId(), 
counter.getLastExchangeCompletedExchangeId());
+    }
+
+    @Test
+    public void testLastFailedExchangeIdIsExact() {
+        // failures are rare and worth finding, so they are not throttled as 
completed exchanges are
+        counter.failedExchange(new DefaultExchange(context));
+        Exchange last = new DefaultExchange(context);
+        counter.failedExchange(last);
+        assertEquals(last.getExchangeId(), 
counter.getLastExchangeFailureExchangeId());
+    }
+
+    @Test
+    public void testConcurrentExchangesAreAllCounted() throws Exception {
+        ExecutorService executor = Executors.newFixedThreadPool(THREADS);
+        try {
+            CountDownLatch start = new CountDownLatch(1);
+            List<Future<?>> futures = new ArrayList<>();
+            for (int t = 0; t < THREADS; t++) {
+                final long time = t + 1L;
+                futures.add(executor.submit(() -> {
+                    start.await();
+                    Exchange exchange = new DefaultExchange(context);
+                    for (int i = 0; i < UPDATES; i++) {
+                        counter.processExchange(exchange, "processor");
+                        counter.completedExchange(exchange, time);
+                    }
+                    return null;
+                }));
+            }
+            start.countDown();
+            for (Future<?> future : futures) {
+                future.get(30, TimeUnit.SECONDS);
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+
+        assertEquals(160000, counter.getExchangesTotal());
+        assertEquals(160000, counter.getExchangesCompleted());
+        assertEquals(0, counter.getExchangesInflight());
+        // 20000 * (1 + 2 + ... + 8)
+        assertEquals(720000, counter.getTotalProcessingTime());
+        assertEquals(4, counter.getMeanProcessingTime());
+        assertEquals(1, counter.getMinProcessingTime());
+        assertEquals(8, counter.getMaxProcessingTime());
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d298864d2e54..5e1a0e08c1cc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -100,6 +100,13 @@ in its model, so clearing them on the route saved little 
memory at the cost of m
 Applications that called `Route.clearRouteModel()` directly can remove the 
call. Implementations of the `Route` interface
 that override `clearRouteModel()` can remove the override.
 
+=== camel-management - LastExchangeCompletedExchangeId is updated at most once 
per millisecond
+
+The JMX attribute `LastExchangeCompletedExchangeId`, of the `CamelContext`, 
route and processor MBeans, is now updated
+at most once per millisecond, so that the JMX statistics no longer generate an 
exchange id for every exchange. When
+several exchanges complete within the same millisecond, the attribute has the 
id of the first one, not of the last one.
+Failed exchanges are not affected: `LastExchangeFailureExchangeId` is still 
updated for every failed exchange.
+
 === camel-core - RedeliveryErrorHandler.defensiveCopyExchangeIfNeeded is 
deprecated
 
 When redelivery is enabled, the error handler (`DefaultErrorHandler`, 
`DeadLetterChannel` and the transaction error

Reply via email to