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 7cce4ff2f34a CAMEL-26781: camel-core - the runtime endpoint registry 
counts the reply of an InOut consumer once per message (#29478)
7cce4ff2f34a is described below

commit 7cce4ff2f34ac17410bc65cc95083aecafe37242
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 22:47:02 2026 +0100

    CAMEL-26781: camel-core - the runtime endpoint registry counts the reply of 
an InOut consumer once per message (#29478)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../resilience4j/ResilienceInputHitsTest.java      |  13 ++-
 .../engine/DefaultRuntimeEndpointRegistry.java     |  99 +++++++++++---------
 .../management/ManagedEndpointReplyHitsTest.java   | 100 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   6 +-
 4 files changed, 176 insertions(+), 42 deletions(-)

diff --git 
a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
 
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
index 2f47bd2129ac..823851a3dd3d 100644
--- 
a/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
+++ 
b/components/camel-resilience4j/src/test/java/org/apache/camel/component/resilience4j/ResilienceInputHitsTest.java
@@ -27,7 +27,8 @@ import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 /**
- * The circuit breaker runs a copy of the exchange with its own unit of work: 
the copy is no second input of the route.
+ * The circuit breaker runs a copy of the exchange with its own unit of work: 
the copy is no second input of the route,
+ * and no second reply of an InOut consumer.
  */
 public class ResilienceInputHitsTest extends CamelTestSupport {
 
@@ -55,6 +56,16 @@ public class ResilienceInputHitsTest extends 
CamelTestSupport {
         assertEquals(3, hits("supplier", "direct://supplier", "in"));
     }
 
+    @Test
+    public void testOneReplyPerRequest() {
+        for (int i = 0; i < 3; i++) {
+            assertEquals("in stock", template.requestBody("seda:check", 
"CAMEL-MUG"));
+        }
+
+        assertEquals(3, hits("check", "seda://check", "in"));
+        assertEquals(3, hits("check", "seda://check", "out"));
+    }
+
     private long hits(String routeId, String uri, String direction) {
         return 
context.getRuntimeEndpointRegistry().getEndpointStatistics().stream()
                 .filter(s -> routeId.equals(s.getRouteId()) && 
uri.equals(s.getUri()) && direction.equals(s.getDirection()))
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
index 83e840dc5c91..1d4c6af21ae4 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRuntimeEndpointRegistry.java
@@ -66,7 +66,7 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
     private EndpointUtilizationStatistics outputUtilization;
     private EndpointSizeStatistics inputSizeStats;
     private EndpointSizeStatistics outputSizeStats;
-    private final RoutePolicySupport inputHits = new InputHits();
+    private final RoutePolicySupport consumerHits = new ConsumerHits();
 
     @Override
     public boolean isEnabled() {
@@ -215,12 +215,13 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
         }
         if (extended) {
             // the input hits of a route are counted when an exchange enters 
it through its consumer, as the route
-            // counts its exchanges: the copies EIPs make inside a route 
(circuit breaker, split, multicast, recipient
-            // list) have their own unit of work and ExchangeCreatedEvent but 
are no input, and an exchange that comes
-            // from another route over direct is one without an 
ExchangeCreatedEvent.
-            // One shared instance for all routes: InputHits keeps no 
per-route state, the counts live in the
-            // thread-safe inputUtilization
-            getCamelContext().addRoutePolicyFactory((context, routeId, route) 
-> inputHits);
+            // counts its exchanges, and the replies of an InOut consumer when 
the exchange is done there: the copies
+            // EIPs make inside a route (circuit breaker, split, multicast, 
recipient list) have their own unit of work,
+            // created and completed events, but are no input and no reply, 
and an exchange that comes from another
+            // route over direct is one without an ExchangeCreatedEvent.
+            // One shared instance for all routes: ConsumerHits keeps no 
per-route state, the counts live in the
+            // thread-safe inputUtilization and outputUtilization
+            getCamelContext().addRoutePolicyFactory((context, routeId, route) 
-> consumerHits);
             LOG.debug(
                     "Runtime endpoint registry is in extended mode gathering 
usage statistics of all incoming and outgoing endpoints (cache limit: {})",
                     limit);
@@ -353,36 +354,14 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
                 exchange.removeProperty("CamelMessageHeadersSizeSending");
             }
         } else if (event instanceof ExchangeCompletedEvent || event instanceof 
ExchangeFailedEvent) {
-            // InOut consumers send a reply back when the exchange completes;
-            // record this as an "out" hit on the consumer's fromEndpoint
-            CamelEvent.ExchangeEvent ee = (CamelEvent.ExchangeEvent) event;
-            Exchange exchange = ee.getExchange();
-            if (exchange.getPattern() != null && 
exchange.getPattern().isOutCapable()) {
-                Endpoint endpoint = exchange.getFromEndpoint();
-                if (endpoint != null) {
-                    String routeId = exchange.getFromRouteId();
-                    String uri = endpoint.getEndpointUri();
-                    Map<String, String> uris = outputs.get(routeId);
-                    if (uris != null) {
-                        uris.putIfAbsent(uri, uri);
-                    }
-                    if (extended) {
-                        String key = asUtilizationKey(routeId, uri);
-                        if (key != null) {
-                            outputUtilization.onHit(key);
-                            if (messageSizeEnabled) {
-                                Message message = exchange.getMessage();
-                                MessageSizeStrategy strategy = 
getCamelContext().getMessageSizeStrategy();
-                                long bodySize = 
strategy.computeBodySize(message);
-                                long headersSize = 
strategy.computeHeadersSize(message);
-                                outputSizeStats.onHit(key, bodySize, 
headersSize);
-                                // reset stream cache so the body is 
re-readable
-                                if (message.getBody() instanceof StreamCache 
sc) {
-                                    sc.reset();
-                                }
-                            }
-                        }
-                    }
+            // an InOut consumer sends a reply back when the exchange 
completes: its endpoint is an output of the route
+            // (the hits are counted by the route policy, as the copies EIPs 
make complete as well)
+            Exchange exchange = ((CamelEvent.ExchangeEvent) 
event).getExchange();
+            Endpoint endpoint = exchange.getFromEndpoint();
+            if (endpoint != null && exchange.getPattern() != null && 
exchange.getPattern().isOutCapable()) {
+                Map<String, String> uris = 
outputs.get(exchange.getFromRouteId());
+                if (uris != null) {
+                    uris.putIfAbsent(endpoint.getEndpointUri(), 
endpoint.getEndpointUri());
                 }
             }
         }
@@ -439,8 +418,41 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
         }
     }
 
-    /** The route policy of every route in extended mode: an exchange that 
begins in a route is an input hit. */
-    private final class InputHits extends RoutePolicySupport {
+    /**
+     * An InOut consumer sends a reply when the exchange it created is done in 
its route: an out hit of the consumer's
+     * endpoint. Not for a route the exchange came to over direct (its 
consumer sends no reply of its own), nor for the
+     * copies EIPs make inside a route, which are done in their own unit of 
work but never in the route.
+     */
+    private void onRouteReply(Route route, Exchange exchange) {
+        Endpoint endpoint = exchange.getFromEndpoint();
+        String routeId = route.getRouteId();
+        if (endpoint == null || exchange.getPattern() == null || 
!exchange.getPattern().isOutCapable()
+                || !routeId.equals(exchange.getFromRouteId())) {
+            return;
+        }
+        String key = asUtilizationKey(routeId, endpoint.getEndpointUri());
+        if (key == null || outputUtilization == null) {
+            return;
+        }
+        outputUtilization.onHit(key);
+        if (messageSizeEnabled) {
+            Message message = exchange.getMessage();
+            MessageSizeStrategy strategy = 
getCamelContext().getMessageSizeStrategy();
+            long bodySize = strategy.computeBodySize(message);
+            long headersSize = strategy.computeHeadersSize(message);
+            outputSizeStats.onHit(key, bodySize, headersSize);
+            // reset stream cache so the body is re-readable
+            if (message.getBody() instanceof StreamCache sc) {
+                sc.reset();
+            }
+        }
+    }
+
+    /**
+     * The route policy of every route in extended mode: an exchange that 
begins in a route is an input hit, and the
+     * reply of an InOut consumer when it is done there an out hit.
+     */
+    private final class ConsumerHits extends RoutePolicySupport {
 
         @Override
         public void onExchangeBegin(Route route, Exchange exchange) {
@@ -449,9 +461,16 @@ public class DefaultRuntimeEndpointRegistry extends 
EventNotifierSupport impleme
             }
         }
 
+        @Override
+        public void onExchangeDone(Route route, Exchange exchange) {
+            if (extended && enabled) {
+                onRouteReply(route, exchange);
+            }
+        }
+
         @Override
         public String toString() {
-            return "RuntimeEndpointRegistryInputHits";
+            return "RuntimeEndpointRegistryConsumerHits";
         }
     }
 
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointReplyHitsTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointReplyHitsTest.java
new file mode 100644
index 000000000000..b1f2369fc33f
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedEndpointReplyHitsTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ManagementStatisticsLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.RuntimeEndpointRegistry;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * An InOut consumer sends a reply per message it received, as an out hit of 
its endpoint: the sub-exchanges of a split,
+ * multicast or recipient list are no reply, and a route called over direct 
sends none.
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedEndpointReplyHitsTest extends ManagementTestSupport {
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext camelContext = super.createCamelContext();
+        
camelContext.getManagementStrategy().getManagementAgent().setStatisticsLevel(ManagementStatisticsLevel.Extended);
+        return camelContext;
+    }
+
+    @Test
+    public void testSplitIsOneReply() {
+        assertEquals("done", template.requestBody("seda:split", "a,b,c"));
+        assertEquals("done", template.requestBody("seda:split", "d,e,f"));
+
+        assertEquals(2, hits("split", "seda://split", "out"));
+    }
+
+    @Test
+    public void testMulticastIsOneReply() {
+        assertEquals("done", template.requestBody("seda:multicast", "Hello 
World"));
+
+        assertEquals(1, hits("multicast", "seda://multicast", "out"));
+        assertEquals(0, hits("sub", "direct://sub", "out"));
+    }
+
+    @Test
+    public void testRecipientListIsOneReply() {
+        assertEquals("done", template.requestBodyAndHeader("seda:list", "Hello 
World", "to", "mock:b,direct:sub"));
+        assertEquals("done", template.requestBodyAndHeader("seda:list", "Bye 
World", "to", "mock:b,direct:sub"));
+
+        assertEquals(2, hits("list", "seda://list", "out"));
+        assertEquals(0, hits("sub", "direct://sub", "out"));
+    }
+
+    @Test
+    public void testARouteCalledOverDirectSendsNoReply() {
+        for (int i = 0; i < 3; i++) {
+            template.requestBody("seda:call", "Hello " + i);
+        }
+
+        assertEquals(3, hits("call", "seda://call", "out"));
+        assertEquals(3, hits("call", "direct://sub", "out"));
+        assertEquals(0, hits("sub", "direct://sub", "out"));
+    }
+
+    private long hits(String routeId, String uri, String direction) {
+        return 
context.getRuntimeEndpointRegistry().getEndpointStatistics().stream()
+                .filter(s -> routeId.equals(s.getRouteId()) && 
uri.equals(s.getUri()) && direction.equals(s.getDirection()))
+                .mapToLong(RuntimeEndpointRegistry.Statistic::getHits).sum();
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
from("seda:split").routeId("split").split(body().tokenize(",")).to("mock:split").end()
+                        .transform(constant("done"));
+                
from("seda:multicast").routeId("multicast").multicast().to("mock:a", 
"direct:sub").end()
+                        .transform(constant("done"));
+                
from("seda:list").routeId("list").recipientList(header("to")).end().transform(constant("done"));
+                from("seda:call").routeId("call").to("direct:sub");
+                from("direct:sub").routeId("sub").to("mock:sub");
+            }
+        };
+    }
+}
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 b53a6ee48f2b..58fafe5f1b8f 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
@@ -285,7 +285,7 @@ before calling an endpoint). Before, such a recovery was 
not counted. `ExchangeH
 `true` when the error handler redelivered the exchange, without looking at the 
header only. An exchange whose failure
 is continued (`continued(true)`) no longer keeps its redelivery counter, as 
with a handled failure.
 
-=== camel-management - the input hits of the runtime endpoint registry are the 
messages a route received
+=== camel-management - the input and reply hits of the runtime endpoint 
registry count each message once
 
 The runtime endpoint registry (in extended statistics mode, as the 
`RuntimeEndpointRegistry` MBean, the endpoints dev
 console and the Camel CLI show it) now counts an input hit of a route when an 
exchange enters the route through its
@@ -294,6 +294,10 @@ and the sub-exchanges of a Split, Multicast or Recipient 
List were counted as in
 route that consumes from `direct` (or got a message sent by a 
`ProducerTemplate` to its `direct` endpoint) had no input
 hits at all.
 
+Likewise, the reply of an InOut consumer is counted as an out hit of its 
endpoint when the exchange is done in the route,
+once per message. Before, the completed copy of a Circuit Breaker and the 
sub-exchanges of a Split, Multicast or
+Recipient List were counted as replies as well.
+
 === camel-core - a step after a redelivered step gets all its redeliveries
 
 When a step of a route was redelivered and then succeeded, a later failure (of 
another step of the route, or of a

Reply via email to