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
