This is an automated email from the ASF dual-hosted git repository. apupier pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit fd4c5603c6cae715fed3777c9fffbbdcf087f515 Author: smjain <[email protected]> AuthorDate: Wed Sep 23 16:34:09 2026 +0530 CAMEL-24922: camel-core - Sticky failover without round robin should also try the endpoints before the last good one With sticky mode and no round robin, the failover load balancer started from the last known good endpoint and gave up at the end of the list, so the endpoints before it were never tried again and every exchange failed once that endpoint was down, even with maximumFailoverAttempts -1. Continue from the first endpoint until each endpoint has been tried once. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../loadbalancer/FailOverLoadBalancer.java | 15 +++ .../processor/FailoverStickyWrapAroundTest.java | 104 +++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/loadbalancer/FailOverLoadBalancer.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/loadbalancer/FailOverLoadBalancer.java index 1ac119f11259..a3ffb283509c 100644 --- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/loadbalancer/FailOverLoadBalancer.java +++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/loadbalancer/FailOverLoadBalancer.java @@ -181,6 +181,7 @@ public class FailOverLoadBalancer extends LoadBalancerSupport implements Traceab final AsyncCallback callback; final AsyncProcessor[] processors; int index; + int start; int attempts; // use a copy of the original exchange before failover to avoid populating side effects // directly into the original exchange @@ -198,6 +199,7 @@ public class FailOverLoadBalancer extends LoadBalancerSupport implements Traceab } else if (isRoundRobin()) { index = counter.updateAndGet(x -> ++x < processors.length ? x : 0); } + start = index; LOG.trace("Failover starting with endpoint index {}", index); } @@ -245,6 +247,11 @@ public class FailOverLoadBalancer extends LoadBalancerSupport implements Traceab LOG.trace("Failover is round robin enabled and therefore starting from the first endpoint"); index = 0; counter.set(0); + } else if (isSticky() && start > 0) { + // sticky mode started from the last known good endpoint, so the endpoints + // before it have not been tried yet + LOG.trace("Failover is sticky enabled and therefore continuing from the first endpoint"); + index = 0; } else { // no more processors to try LOG.trace("Breaking out of failover as we reached the end of endpoints to use for failover"); @@ -254,6 +261,14 @@ public class FailOverLoadBalancer extends LoadBalancerSupport implements Traceab } } + if (copy != null && isSticky() && !isRoundRobin() && index == start) { + // sticky mode (without round robin) has tried all endpoints once + LOG.trace("Breaking out of failover as all endpoints have been tried"); + ExchangeHelper.copyResults(exchange, copy); + callback.done(false); + return; + } + // try again but copy original exchange before we failover copy = prepareExchangeForFailover(exchange); AsyncProcessor processor = processors[index]; diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/FailoverStickyWrapAroundTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/FailoverStickyWrapAroundTest.java new file mode 100644 index 000000000000..6bd3909596dc --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/processor/FailoverStickyWrapAroundTest.java @@ -0,0 +1,104 @@ +/* + * 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.processor; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.camel.CamelExecutionException; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Sticky failover (without round robin) must also try the endpoints before the last known good endpoint, once each. + */ +public class FailoverStickyWrapAroundTest extends ContextTestSupport { + + private final Set<String> down = ConcurrentHashMap.newKeySet(); + + @Test + public void testFailoverStickyContinuesFromFirstEndpoint() throws Exception { + // a and b are down, so c becomes the last known good endpoint + down.add("a"); + down.add("b"); + getMockEndpoint("mock:a").expectedMessageCount(1); + getMockEndpoint("mock:b").expectedMessageCount(1); + getMockEndpoint("mock:c").expectedBodiesReceived("Hello World"); + template.sendBody("direct:start", "Hello World"); + assertMockEndpointsSatisfied(); + + // now c is down and a and b are up: sticky starts from c, and must then continue from a + resetMocks(); + down.clear(); + down.add("c"); + getMockEndpoint("mock:a").expectedBodiesReceived("Bye World"); + getMockEndpoint("mock:b").expectedMessageCount(0); + getMockEndpoint("mock:c").expectedBodiesReceived("Bye World"); + template.sendBody("direct:start", "Bye World"); + assertMockEndpointsSatisfied(); + + // and a is now the last known good endpoint + resetMocks(); + getMockEndpoint("mock:a").expectedBodiesReceived("Hi World"); + getMockEndpoint("mock:b").expectedMessageCount(0); + getMockEndpoint("mock:c").expectedMessageCount(0); + template.sendBody("direct:start", "Hi World"); + assertMockEndpointsSatisfied(); + } + + @Test + public void testFailoverStickyTriesEachEndpointOnceWhenAllDown() throws Exception { + down.add("a"); + down.add("b"); + getMockEndpoint("mock:c").expectedMessageCount(1); + template.sendBody("direct:start", "Hello World"); + assertMockEndpointsSatisfied(); + + resetMocks(); + down.add("c"); + getMockEndpoint("mock:a").expectedMessageCount(1); + getMockEndpoint("mock:b").expectedMessageCount(1); + getMockEndpoint("mock:c").expectedMessageCount(1); + assertThrows(CamelExecutionException.class, () -> template.sendBody("direct:start", "Bye World")); + assertMockEndpointsSatisfied(); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .loadBalance().failover(-1, false, false, true) + .to("direct:a", "direct:b", "direct:c"); + + from("direct:a").to("mock:a").process(e -> failIfDown("a")); + from("direct:b").to("mock:b").process(e -> failIfDown("b")); + from("direct:c").to("mock:c").process(e -> failIfDown("c")); + } + }; + } + + private void failIfDown(String name) { + if (down.contains(name)) { + throw new IllegalArgumentException(name + " is down"); + } + } +}
