This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch CAMEL-26712-redelivered-statistics
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 7776649a3102757677954f2235c7950b5f991409
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 7 10:34:24 2026 +0100

    CAMEL-26712: camel-management - an exchange recovered by a redelivery is 
not counted when the route removes the CamelRedelivered header
    
    ExchangeHelper.isRedelivered, which the JMX statistics of routes, 
processors and the CamelContext use to count
    redeliveries, read only the CamelRedelivered header. A route that removes 
the Camel headers after the redelivery took it
    away, and the recovery was not counted. It now also reads the redelivery 
counter of the exchange, which the error
    handler keeps and a route cannot remove. A continued failure resets that 
counter too, as a handled one does.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../errorhandler/RedeliveryErrorHandler.java       |   3 +
 .../ManagedRedeliverRemoveHeadersTest.java         | 111 +++++++++++++++++++++
 .../org/apache/camel/support/ExchangeHelper.java   |   8 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   8 ++
 4 files changed, 129 insertions(+), 1 deletion(-)

diff --git 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
index 10fbcc955210..0eba055f267d 100644
--- 
a/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
+++ 
b/core/camel-core-processor/src/main/java/org/apache/camel/processor/errorhandler/RedeliveryErrorHandler.java
@@ -1676,6 +1676,9 @@ public abstract class RedeliveryErrorHandler extends 
ErrorHandlerSupport
             exchange.getIn().removeHeader(Exchange.REDELIVERED);
             exchange.getIn().removeHeader(Exchange.REDELIVERY_COUNTER);
             exchange.getIn().removeHeader(Exchange.REDELIVERY_MAX_COUNTER);
+            // also the counter of the exchange, as when the failure is 
handled: a continued exchange is not redelivered
+            exchange.getExchangeExtension().setRedeliveryCounter(-1);
+            exchange.getExchangeExtension().setRedeliveryMaxCounter(-1);
             exchange.getExchangeExtension().setFailureHandled(false);
             // keep the Exchange.EXCEPTION_CAUGHT as property so end user 
knows the caused exception
             // and mark it as handled (continued), so it is not regarded as a 
previous exception of a later failure
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/ManagedRedeliverRemoveHeadersTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRedeliverRemoveHeadersTest.java
new file mode 100644
index 000000000000..f6ca1944a002
--- /dev/null
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/ManagedRedeliverRemoveHeadersTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.atomic.AtomicInteger;
+
+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.apache.camel.management.DefaultManagementObjectNameStrategy.TYPE_ROUTE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * An exchange that completes after a redelivery counts as redelivered also 
when the route removes the Camel headers
+ * after the redelivery, which takes the CamelRedelivered header away 
(CAMEL-26712).
+ */
+@DisabledOnOs(OS.AIX)
+public class ManagedRedeliverRemoveHeadersTest extends ManagementTestSupport {
+
+    private final AtomicInteger calls = new AtomicInteger();
+
+    @Test
+    public void testRedeliveredAlsoWhenTheHeadersAreRemoved() throws Exception 
{
+        MBeanServer mbeanServer = getMBeanServer();
+
+        getMockEndpoint("mock:result").expectedMessageCount(2);
+        
getMockEndpoint("mock:result").message(0).header("CamelRedelivered").isNull();
+
+        template.sendBody("direct:removing", "Hello");
+        template.sendBody("direct:keeping", "World");
+
+        assertMockEndpointsSatisfied();
+
+        for (String route : new String[] { "removing", "keeping" }) {
+            ObjectName on = getCamelObjectName(TYPE_ROUTE, route);
+            assertEquals(1L, mbeanServer.getAttribute(on, 
"ExchangesCompleted"), route);
+            assertEquals(0L, mbeanServer.getAttribute(on, "FailuresHandled"), 
route);
+            assertEquals(1L, mbeanServer.getAttribute(on, "Redeliveries"), 
route);
+
+            on = getCamelObjectName(TYPE_PROCESSOR, route + "-flaky");
+            assertEquals(1L, mbeanServer.getAttribute(on, "Redeliveries"), 
route);
+        }
+    }
+
+    @Test
+    public void testContinuedIsNotRedelivered() throws Exception {
+        MBeanServer mbeanServer = getMBeanServer();
+
+        getMockEndpoint("mock:continued").expectedMessageCount(1);
+
+        template.sendBody("direct:continued", "Bye");
+
+        assertMockEndpointsSatisfied();
+
+        ObjectName on = getCamelObjectName(TYPE_ROUTE, "continued");
+        assertEquals(1L, mbeanServer.getAttribute(on, "ExchangesCompleted"));
+        assertEquals(0L, mbeanServer.getAttribute(on, "Redeliveries"));
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                
onException(IllegalStateException.class).maximumRedeliveries(2).redeliveryDelay(0);
+                
onException(IllegalArgumentException.class).maximumRedeliveries(2).redeliveryDelay(0).continued(true);
+
+                // the first call fails, the redelivery succeeds; then the 
Camel headers are removed
+                from("direct:removing").routeId("removing")
+                        .process(e -> flaky()).id("removing-flaky")
+                        .removeHeaders("Camel*")
+                        .to("mock:result");
+
+                from("direct:keeping").routeId("keeping")
+                        .process(e -> flaky()).id("keeping-flaky")
+                        .to("mock:result");
+
+                // every attempt fails, and the failure is continued
+                from("direct:continued").routeId("continued")
+                        .throwException(new IllegalArgumentException("Forced"))
+                        .to("mock:continued");
+            }
+        };
+    }
+
+    private void flaky() {
+        if (calls.incrementAndGet() % 2 == 1) {
+            throw new IllegalStateException("stock down");
+        }
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
index e8db04bf1570..49e1623ac3b1 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/ExchangeHelper.java
@@ -689,12 +689,18 @@ public final class ExchangeHelper {
     }
 
     /**
-     * Checks whether the exchange {@link UnitOfWork} is redelivered
+     * Checks whether the exchange {@link UnitOfWork} is redelivered: the 
error handler redelivered it (its redelivery
+     * counter, which a route cannot remove), or the {@link 
Exchange#REDELIVERED} header says so.
      *
      * @param  exchange the exchange
      * @return          <tt>true</tt> if redelivered, <tt>false</tt> otherwise
      */
     public static boolean isRedelivered(Exchange exchange) {
+        // the counter of the exchange first: a route that removes the Camel 
headers after the redelivery (such as
+        // removeHeaders("Camel*") before calling an endpoint) takes the 
header away (CAMEL-26712)
+        if (exchange.getExchangeExtension().getRedeliveryCounter() > 0) {
+            return true;
+        }
         return exchange.getIn().hasHeaders() && 
exchange.getIn().getHeader(Exchange.REDELIVERED, false, Boolean.class);
     }
 
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 f0a3e7e0ec09..ee7ae97e83b3 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
@@ -241,6 +241,14 @@ at most once per millisecond, so that the JMX statistics 
no longer generate an e
 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-management - an exchange recovered by a redelivery is counted when 
the route removes the Camel headers
+
+The `Redeliveries` statistics of the routes, processors and the `CamelContext` 
count an exchange that completed after a
+redelivery also when the route removed the `CamelRedelivered` header 
afterwards (such as `removeHeaders("Camel*")`
+before calling an endpoint). Before, such a recovery was not counted. 
`ExchangeHelper.isRedelivered` now also returns
+`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-core - a nested split no longer changes the processed items of the 
outer split
 
 With `errorThreshold` or `maxFailedRecords` on a split that contains another 
split, the inner split changed the number of

Reply via email to