tmielke commented on code in PR #27576:
URL: https://github.com/apache/camel/pull/27576#discussion_r4220771974


##########
core/camel-management/src/test/java/org/apache/camel/management/ManagedRouteGroupExchangeCountTest.java:
##########
@@ -0,0 +1,188 @@
+/*
+ * 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.concurrent.TimeUnit;
+
+import org.apache.camel.api.management.ManagedCamelContext;
+import org.apache.camel.api.management.mbean.ManagedRouteGroupMBean;
+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.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests that a route group counts each exchange once per entry into the 
group: calls nested inside another route of the
+ * same group are not counted again (CAMEL-24887).
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedRouteGroupExchangeCountTest extends ManagementTestSupport {
+
+    @Test
+    public void testGroupCountsExchangeOnce() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message through x1 -> x2 (both in group "g")
+        template.sendBody("direct:x1", "Hello");
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesCompleted());
+        assertEquals(1, group.getExchangesTotal());
+        assertEquals(0, group.getExchangesInflight());
+
+        // send 2 more messages
+        template.sendBody("direct:x1", "World");
+        template.sendBody("direct:x1", "!");
+
+        assertEquals(3, group.getExchangesCompleted());
+        assertEquals(3, group.getExchangesTotal());
+    }
+
+    @Test
+    public void testGroupCountsOnceWhenRoutingThroughNonGroupRoute() throws 
Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message through x1 (group "g") -> y (no group) -> x2 (group 
"g")
+        template.sendBody("direct:x1-via-y", "Hello");
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesCompleted());
+        assertEquals(1, group.getExchangesTotal());
+    }
+
+    @Test
+    public void testGroupCountsWhenEntryIsOutsideGroup() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message to a route outside the group that calls x2 (in group 
"g") directly
+        template.sendBody("direct:outside", "Hello");
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesCompleted());
+    }
+
+    @Test
+    public void testGroupCountsFailure() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message that will fail in x2-fail (group "g")
+        try {
+            template.sendBody("direct:x1-fail", "Hello");
+        } catch (Exception e) {
+            // expected
+        }
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(1, group.getExchangesFailed());
+        assertEquals(0, group.getExchangesCompleted());
+        assertEquals(0, group.getExchangesInflight());
+    }
+
+    @Test
+    public void testTwoGroupsOnOnePath() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        // send 1 message through g1-route (group "g1") -> g2-route (group 
"g2")
+        template.sendBody("direct:g1-route", "Hello");
+
+        ManagedRouteGroupMBean group1 = mcc.getManagedRouteGroup("g1");
+        ManagedRouteGroupMBean group2 = mcc.getManagedRouteGroup("g2");
+        assertNotNull(group1);
+        assertNotNull(group2);
+        assertEquals(1, group1.getExchangesCompleted());
+        assertEquals(1, group2.getExchangesCompleted());
+    }
+
+    @Test
+    public void testWireTapGroupCounts() throws Exception {
+        ManagedCamelContext mcc = 
context.getCamelContextExtension().getContextPlugin(ManagedCamelContext.class);
+
+        getMockEndpoint("mock:wt-done").expectedMessageCount(1);
+
+        template.sendBody("direct:wt", "Hello");
+
+        assertMockEndpointsSatisfied();
+
+        // wait for the wireTapped exchange to complete in route "wt-target"
+        await().atMost(5, TimeUnit.SECONDS)
+                .untilAsserted(() -> assertEquals(1L, 
mcc.getManagedRoute("wt-target").getExchangesCompleted()));
+
+        // each route counts independently at the route level
+        assertEquals(1, mcc.getManagedRoute("wt").getExchangesCompleted());
+        assertEquals(1, 
mcc.getManagedRoute("wt-target").getExchangesCompleted());
+
+        // the group count is consistent with how the CamelContext MBean 
counts exchanges
+        long ctxCompleted = 
mcc.getManagedCamelContext().getExchangesCompleted();
+
+        ManagedRouteGroupMBean group = mcc.getManagedRouteGroup("g");
+        assertNotNull(group);
+        assertEquals(ctxCompleted, group.getExchangesCompleted());
+        assertEquals(ctxCompleted, group.getExchangesTotal());

Review Comment:
     Thanks for resolving the doc, upgrade-guide, and Javadoc threads.
   
     Regarding the wireTap test — I investigated this in detail by adding stack 
trace tracing to `DefaultUnitOfWork.pushRoute()`, and the copy's 
`routeStackLevel` is actually 2, not 1. The
     suggested test change would fail (the await times out because the group 
stays at 1, and MCC also reports 1). Here's what happens:
   
     1. `createCorrelatedCopy(exchange, false)` sets `copy.setUnitOfWork(null) 
`— the copy starts with no UoW. ✓
     2. `WireTapTask.run()` calls `asyncProcessor.process(copy)`. This 
`asyncProcessor` is a `CamelInternalProcessor` belonging to the source route 
(`"wt"`). Its `UnitOfWorkProcessorAdvice.before()` creates a fresh UoW and 
pushes `"wt"` onto the stack → `level = 1`.
     3. Inside that CIP's processor chain, a `SendProcessor` sends the copy to 
`direct:wt-target`. The `DirectProducer` enters a second 
`CamelInternalProcessor` for route `"wt-target"`. Its
        `UnitOfWorkProcessorAdvice.before()`sees the UoW already exists 
(created in step 2) and pushes "`wt-target"` → `level = 2`.
     4. When `completedExchange` fires for `"wt-target",` both 
`ManagedCamelContext (routeStackLevel(includeRT, includeK)<= 1 → false`) and 
`ManagedRouteGroup (routeStackLevel(filter) <= 1 → false)` skip the copy.
   
     The key is step 2: the WireTap wraps its child processor in a 
`CamelInternalProcessor` from the source route (for error handling and UoW 
lifecycle). This pushes the source route onto
     the copy's fresh UoW before the copy ever reaches the target route.
   
     Verified output:
   ```
     MCC.completedExchange: exchangeId=...0000 routeStackLevel=1 route=wt       
 counting=true
     MCC.completedExchange: exchangeId=...0001 routeStackLevel=2 
route=wt-target  counting=false
   
   ```
     So `ManagedCamelContext.getExchangesCompleted()` returns 1, not 2. The 
group also returns 1. Both are consistent.
   
     Regarding raciness: `CompositePerformanceCounter.completedExchange()` 
calls counter1 (route), counter2 (context), counter3 (group) sequentially in 
the same thread. By the time the
     route counter is updated, the context and group counters have already been 
updated in the same invocation — there is no async gap between them.
   
     If we want wireTap copies counted as separate exchanges (total = 2), the 
fix would need to change how `WireTapProcessor` wires its inner 
`CamelInternalProcessor`, or change the dedup
     logic in both `ManagedCamelContext` and `ManagedRouteGroup`. That seems 
like a separate concern from CAMEL-24887.
   
     I'll fix the stray blank line on line 183.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to