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 75bea5f705b1 CAMEL-26313: camel-core - ThrottlingExceptionRoutePolicy:
raising halfOpenAfter while the circuit is open no longer leaves the route
suspended (#28574)
75bea5f705b1 is described below
commit 75bea5f705b13df07a1efbad348cc98c21556552
Author: Vignesh Nayak Manel <[email protected]>
AuthorDate: Wed Oct 28 11:40:20 2026 +0530
CAMEL-26313: camel-core - ThrottlingExceptionRoutePolicy: raising
halfOpenAfter while the circuit is open no longer leaves the route suspended
(#28574)
* CAMEL-26313: camel-core - ThrottlingExceptionRoutePolicy: raising
halfOpenAfter while the circuit is open no longer leaves the route suspended
The half open timer is scheduled with the value of halfOpenAfter when the
circuit opens. If halfOpenAfter is raised while the circuit is open (it is
writable over JMX), the timer fires before the new value, and calculateState
took the "time not elapsed" branch, which scheduled nothing. The consumer is
suspended, so no exchange came to check again, and the route stayed
suspended
until it was restarted.
In that branch, schedule the timer again for the time that is left.
halfOpenAfter is read once, so a value lowered at the same time cannot give
a negative delay.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-26313: camel-core - ThrottlingExceptionRoutePolicy: only the half
open timer schedules the check again
An exchange still in flight when the circuit opened is done while the
circuit is open, and took the "time not elapsed" branch too: each one
created a new Timer (a new thread) and cancelled the previous one. Only
the check of the timer can leave the route suspended, so only that path
schedules the check again.
addHalfOpenTimer(Route, long) is now protected, so a subclass that
customises the scheduling also handles the timer scheduled again.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
...gExceptionRoutePolicyInflightWhileOpenTest.java | 102 +++++++++++++++++++++
...ExceptionRoutePolicyRaiseHalfOpenAfterTest.java | 72 +++++++++++++++
.../throttling/ThrottlingExceptionRoutePolicy.java | 19 +++-
3 files changed, 191 insertions(+), 2 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyInflightWhileOpenTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyInflightWhileOpenTest.java
new file mode 100644
index 000000000000..b716ea2e15a6
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyInflightWhileOpenTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.throttle;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.throttling.ThrottlingExceptionRoutePolicy;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * An exchange that is done while the circuit is open must not schedule the
half open check again: only the timer that
+ * was scheduled when the circuit opened (each timer has its own thread).
+ */
+class ThrottlingExceptionRoutePolicyInflightWhileOpenTest extends
ContextTestSupport {
+
+ private final AtomicInteger timers = new AtomicInteger();
+ private final AtomicInteger done = new AtomicInteger();
+ private final CountDownLatch slowStarted = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ private final ThrottlingExceptionRoutePolicy policy = new
ThrottlingExceptionRoutePolicy(1, 60000, 60000, null) {
+ @Override
+ protected void addHalfOpenTimer(Route route, long delay) {
+ timers.incrementAndGet();
+ super.addHalfOpenTimer(route, delay);
+ }
+
+ @Override
+ public void onExchangeDone(Route route, Exchange exchange) {
+ super.onExchangeDone(route, exchange);
+ done.incrementAndGet();
+ }
+ };
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+ context.getShutdownStrategy().setTimeout(1);
+ }
+
+ @Test
+ void testExchangeDoneWhileOpenDoesNotScheduleTheHalfOpenCheckAgain()
throws Exception {
+ template.sendBody("seda:start", "slow");
+ assertTrue(slowStarted.await(10, TimeUnit.SECONDS), "the slow exchange
should be in flight");
+
+ // opens the circuit, which schedules the half open check
+ template.sendBody("seda:start", "fail");
+ ServiceSupport consumer = (ServiceSupport)
context.getRoute("start").getConsumer();
+ await().atMost(10, TimeUnit.SECONDS).until(consumer::isSuspended);
+ assertEquals(1, timers.get(), "opening the circuit schedules the half
open check");
+
+ // the slow exchange is done while the circuit is open
+ release.countDown();
+ await().atMost(10, TimeUnit.SECONDS).until(() -> done.get() == 2);
+ assertEquals(1, timers.get(), "an exchange done while the circuit is
open must not schedule another check");
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:start?concurrentConsumers=2").routeId("start").routePolicy(policy)
+ .process(e -> {
+ if
("slow".equals(e.getMessage().getBody(String.class))) {
+ slowStarted.countDown();
+ release.await(10, TimeUnit.SECONDS);
+ } else {
+ throw new ThrottlingException("boom");
+ }
+ });
+ }
+ };
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyRaiseHalfOpenAfterTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyRaiseHalfOpenAfterTest.java
new file mode 100644
index 000000000000..cd275ae7b2a1
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyRaiseHalfOpenAfterTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.throttle;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.throttling.ThrottlingExceptionRoutePolicy;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Raising halfOpenAfter while the circuit is open delays the half open check,
it must not leave the route suspended.
+ */
+class ThrottlingExceptionRoutePolicyRaiseHalfOpenAfterTest extends
ContextTestSupport {
+
+ private final ThrottlingExceptionRoutePolicy policy = new
ThrottlingExceptionRoutePolicy(1, 60000, 2000, null);
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception {
+ super.setUp();
+ context.getShutdownStrategy().setTimeout(1);
+ }
+
+ @Test
+ void testRaisedHalfOpenAfterHalfOpensTheCircuit() throws Exception {
+ MockEndpoint result = getMockEndpoint("mock:result");
+ result.whenExchangeReceived(1, e -> e.setException(new
ThrottlingException("boom")));
+ result.expectedBodiesReceived("fail", "ok");
+
+ template.sendBody("seda:start", "fail");
+ ServiceSupport consumer = (ServiceSupport)
context.getRoute("start").getConsumer();
+ await().atMost(10, TimeUnit.SECONDS).until(consumer::isSuspended);
+
+ // as over JMX: the half open timer is due after the old value, before
the new one
+ policy.setHalfOpenAfter(3000);
+
+ await().atMost(10, TimeUnit.SECONDS).until(consumer::isStarted);
+ template.sendBody("seda:start", "ok");
+ MockEndpoint.assertIsSatisfied(context, 10, TimeUnit.SECONDS);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:start").routeId("start").routePolicy(policy).to("mock:result");
+ }
+ };
+ }
+}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
index e04c5755e568..255bcd7be538 100644
---
a/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
+++
b/core/camel-support/src/main/java/org/apache/camel/throttling/ThrottlingExceptionRoutePolicy.java
@@ -322,7 +322,9 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
} else if (state.get() == STATE_OPEN) {
if (!keepOpenBool.get()) {
long elapsedTimeSinceOpened = TimeSource.currentTimeMillis() -
openedAt;
- if (halfOpenAfter <= elapsedTimeSinceOpened) {
+ // read once, as halfOpenAfter can be changed at runtime (such
as over JMX)
+ long openFor = halfOpenAfter;
+ if (openFor <= elapsedTimeSinceOpened) {
LOG.debug("Checking an open circuit...");
if (halfOpenHandler != null) {
if (halfOpenHandler.isReadyToBeClosed()) {
@@ -337,7 +339,13 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
LOG.debug("Half opening circuit...");
halfOpenCircuit(route);
}
+ } else if (exchangeFailed == null) {
+ LOG.debug("Keeping circuit open (time not elapsed)...");
+ // the half open timer fired before halfOpenAfter, as it
was raised after the timer was scheduled:
+ // the consumer is suspended, so no exchange comes to
check again. Check when the time is up
+ addHalfOpenTimer(route, openFor - elapsedTimeSinceOpened);
} else {
+ // an exchange still in flight when the circuit opened:
the half open timer does the check
LOG.debug("Keeping circuit open (time not elapsed)...");
}
} else {
@@ -407,11 +415,18 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
}
protected void addHalfOpenTimer(Route route) {
+ addHalfOpenTimer(route, halfOpenAfter);
+ }
+
+ /**
+ * Schedules the half open check after the given delay, replacing the one
that is scheduled.
+ */
+ protected void addHalfOpenTimer(Route route, long delay) {
lock.lock();
try {
Timer previous = halfOpenTimer;
Timer timer = new Timer();
- timer.schedule(new HalfOpenTask(route, timer), halfOpenAfter);
+ timer.schedule(new HalfOpenTask(route, timer), delay);
halfOpenTimer = timer;
if (previous != null) {
// only one half open check at a time (each timer has its own
thread)