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 295b8d648cf2 CAMEL-26291: camel-core - ThrottlingExceptionRoutePolicy
must count the failures within failureWindow, and close the circuit on a
success in the half open state (#28467)
295b8d648cf2 is described below
commit 295b8d648cf2014ee671f264634a31e60a027386
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Oct 26 13:32:36 2026 +0530
CAMEL-26291: camel-core - ThrottlingExceptionRoutePolicy must count the
failures within failureWindow, and close the circuit on a success in the half
open state (#28467)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
...tlingExceptionRoutePolicyFailureWindowTest.java | 195 +++++++++++++++++++++
...xceptionRoutePolicyHalfOpenHandlerSedaTest.java | 2 +-
...ingExceptionRoutePolicyHalfOpenHandlerTest.java | 2 +-
...ThrottlingExceptionRoutePolicyHalfOpenTest.java | 2 +-
.../ThrottlingExceptionRoutePolicyTest.java | 2 +-
.../throttling/ThrottlingExceptionRoutePolicy.java | 88 ++++++++--
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 ++
7 files changed, 286 insertions(+), 18 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyFailureWindowTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyFailureWindowTest.java
new file mode 100644
index 000000000000..d14b861661f5
--- /dev/null
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyFailureWindowTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+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.apache.camel.util.TimeSource;
+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.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The {@link ThrottlingExceptionRoutePolicy} opens the circuit when
failureThreshold failures occur within the
+ * failureWindow, and an exchange done in the half open state decides: a
success closes the circuit, also when the
+ * failures that opened it are still within the failure window.
+ */
+class ThrottlingExceptionRoutePolicyFailureWindowTest extends
ContextTestSupport {
+
+ // long enough for two sends in a row to be within it also on a slow
machine
+ private static final long FAILURE_WINDOW = 1000;
+
+ // the half open check comes before the failures that opened the circuit
leave the window (as with the defaults: a
+ // window of 60 seconds and a half open check after 30 seconds)
+ private final LatchPolicy halfOpenPolicy = new LatchPolicy(1, 60000, 100);
+ private final ThrottlingExceptionRoutePolicy windowPolicy
+ = new ThrottlingExceptionRoutePolicy(2, FAILURE_WINDOW, 60000,
null);
+ private final LatchPolicy racePolicy = new LatchPolicy(1, 60000, 100);
+
+ private final CountDownLatch slowEntered = new CountDownLatch(1);
+ private final CountDownLatch slowRelease = new CountDownLatch(1);
+
+ @Test
+ void testSuccessInHalfOpenStateClosesTheCircuit() throws Exception {
+ ServiceSupport consumer = (ServiceSupport)
context.getRoute("halfOpen").getConsumer();
+
+ assertThrows(CamelExecutionException.class, () ->
template.sendBody("direct:halfOpen", "fail"));
+ // the half open check of the timer waits until it may go, so the
circuit is still open
+ assertEquals("opened", halfOpenPolicy.getStateAsString());
+ assertTrue(consumer.isSuspended());
+
+ // the half open timer resumes the consumer
+ halfOpenPolicy.timerMayGo.countDown();
+ assertTrue(halfOpenPolicy.halfOpened.await(10, TimeUnit.SECONDS));
+ assertEquals("half opened", halfOpenPolicy.getStateAsString());
+ // no further half open check while the test runs
+ halfOpenPolicy.setHalfOpenAfter(60000);
+
+ template.sendBody("direct:halfOpen", "ok");
+ assertEquals("closed", halfOpenPolicy.getStateAsString(), "A success
in the half open state should close the circuit");
+ assertFalse(consumer.isSuspended());
+ }
+
+ @Test
+ void testFailuresOutsideTheWindowDoNotOpenTheCircuit() {
+ ServiceSupport consumer = (ServiceSupport)
context.getRoute("window").getConsumer();
+
+ assertThrows(CamelExecutionException.class, () ->
template.sendBody("direct:window", "fail"));
+ long first = windowPolicy.getLastFailure();
+ // the first failure leaves the window (a condition on the clock,
there is no event to wait for)
+ await().atMost(10, TimeUnit.SECONDS).until(() ->
TimeSource.currentTimeMillis() > first + FAILURE_WINDOW);
+
+ // only this failure is within the window
+ assertThrows(CamelExecutionException.class, () ->
template.sendBody("direct:window", "fail"));
+ assertEquals("closed", windowPolicy.getStateAsString(),
+ "One failure within the window should not open the circuit
with a threshold of 2");
+ assertFalse(consumer.isSuspended());
+
+ // and a second failure within the window opens it
+ assertThrows(CamelExecutionException.class, () ->
template.sendBody("direct:window", "fail"));
+ assertEquals("opened", windowPolicy.getStateAsString());
+ assertTrue(consumer.isSuspended());
+ }
+
+ /**
+ * An exchange that was in flight when the circuit opened is done
successfully when the half open check of the timer
+ * is due, and half opens the circuit itself; the timer's check, which
runs at the same time, must then leave the
+ * half open circuit to the next exchange, and not open it again because
of the failure that opened it.
+ */
+ @Test
+ void testHalfOpenTimerDoesNotReopenACircuitHalfOpenedByAnExchange() throws
Exception {
+ ServiceSupport consumer = (ServiceSupport)
context.getRoute("race").getConsumer();
+
+ Future<Object> slow = template.asyncRequestBody("direct:race", "slow");
+ assertTrue(slowEntered.await(10, TimeUnit.SECONDS));
+ assertThrows(CamelExecutionException.class, () ->
template.sendBody("direct:race", "fail"));
+ assertEquals("opened", racePolicy.getStateAsString());
+ assertTrue(consumer.isSuspended());
+
+ // the timer's half open check has started (and waits); the straggler
is done with a success now
+ assertTrue(racePolicy.timerChecking.await(10, TimeUnit.SECONDS));
+ slowRelease.countDown();
+ assertEquals("slow", slow.get(10, TimeUnit.SECONDS));
+ assertEquals("half opened", racePolicy.getStateAsString());
+ assertFalse(consumer.isSuspended());
+
+ // the timer's check goes on and ends
+ racePolicy.timerMayGo.countDown();
+ racePolicy.timerThread.join(10000);
+ assertFalse(racePolicy.timerThread.isAlive());
+ assertEquals("half opened", racePolicy.getStateAsString(),
+ "The half open timer should not open the circuit again after
an exchange half opened it");
+ assertFalse(consumer.isSuspended());
+
+ // the next exchange decides
+ template.sendBody("direct:race", "ok");
+ assertEquals("closed", racePolicy.getStateAsString());
+ assertFalse(consumer.isSuspended());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:halfOpen").routeId("halfOpen").routePolicy(halfOpenPolicy)
+ .filter(body().isEqualTo("fail")).throwException(new
IllegalArgumentException("Forced"));
+
+
from("direct:window").routeId("window").routePolicy(windowPolicy)
+ .throwException(new
IllegalArgumentException("Forced"));
+
+ from("direct:race").routeId("race").routePolicy(racePolicy)
+ .process(exchange -> {
+ if
("slow".equals(exchange.getMessage().getBody())) {
+ slowEntered.countDown();
+ assertTrue(slowRelease.await(10,
TimeUnit.SECONDS));
+ }
+ })
+ .filter(body().isEqualTo("fail")).throwException(new
IllegalArgumentException("Forced"));
+ }
+ };
+ }
+
+ /**
+ * The policy, with latches: halfOpened when the circuit has been half
opened, and a gate that stops the half open
+ * check of the timer thread (the first one) before it reads the state,
until timerMayGo.
+ */
+ private static final class LatchPolicy extends
ThrottlingExceptionRoutePolicy {
+ private final CountDownLatch halfOpened = new CountDownLatch(1);
+ private final CountDownLatch timerChecking = new CountDownLatch(1);
+ private final CountDownLatch timerMayGo = new CountDownLatch(1);
+ private volatile Thread timerThread;
+
+ LatchPolicy(int threshold, long failureWindow, long halfOpenAfter) {
+ super(threshold, failureWindow, halfOpenAfter, List.of());
+ }
+
+ @Override
+ protected boolean isThresholdExceeded() {
+ // the half open check of the timer calls this first, before it
reads the state
+ if (timerThread == null &&
Thread.currentThread().getName().startsWith("Timer-")) {
+ timerThread = Thread.currentThread();
+ timerChecking.countDown();
+ try {
+ timerMayGo.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ return super.isThresholdExceeded();
+ }
+
+ @Override
+ protected void halfOpenCircuit(Route route) {
+ super.halfOpenCircuit(route);
+ halfOpened.countDown();
+ }
+ }
+}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerSedaTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerSedaTest.java
index 352b4537f801..bdf0bb8b031e 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerSedaTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerSedaTest.java
@@ -110,7 +110,7 @@ public class
ThrottlingExceptionRoutePolicyHalfOpenHandlerSedaTest extends Conte
@Override
public void configure() {
int threshold = 2;
- long failureWindow = 30;
+ long failureWindow = 60000;
long halfOpenAfter = 1000;
ThrottlingExceptionRoutePolicy policy
= new ThrottlingExceptionRoutePolicy(threshold,
failureWindow, halfOpenAfter, null);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerTest.java
index d1af59abe46a..091682adcb1e 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenHandlerTest.java
@@ -106,7 +106,7 @@ public class
ThrottlingExceptionRoutePolicyHalfOpenHandlerTest extends ContextTe
@Override
public void configure() {
int threshold = 2;
- long failureWindow = 30;
+ long failureWindow = 60000;
long halfOpenAfter = 250;
ThrottlingExceptionRoutePolicy policy
= new ThrottlingExceptionRoutePolicy(threshold,
failureWindow, halfOpenAfter, null);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenTest.java
index 500d789e8f24..4dfd0004475b 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyHalfOpenTest.java
@@ -106,7 +106,7 @@ public class ThrottlingExceptionRoutePolicyHalfOpenTest
extends ContextTestSuppo
@Override
public void configure() {
int threshold = 2;
- long failureWindow = 30;
+ long failureWindow = 60000;
long halfOpenAfter = 250;
ThrottlingExceptionRoutePolicy policy
= new ThrottlingExceptionRoutePolicy(threshold,
failureWindow, halfOpenAfter, null);
diff --git
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyTest.java
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyTest.java
index 3630d9db281d..dbc752ad994f 100644
---
a/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/processor/throttle/ThrottlingExceptionRoutePolicyTest.java
@@ -105,7 +105,7 @@ public class ThrottlingExceptionRoutePolicyTest extends
ContextTestSupport {
@Override
public void configure() {
int threshold = 2;
- long failureWindow = 30;
+ long failureWindow = 60000;
long halfOpenAfter = 1000;
ThrottlingExceptionRoutePolicy policy
= new ThrottlingExceptionRoutePolicy(threshold,
failureWindow, halfOpenAfter, null);
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 ab9e81f85e1f..e04c5755e568 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
@@ -16,7 +16,9 @@
*/
package org.apache.camel.throttling;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Deque;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
@@ -48,10 +50,11 @@ import org.slf4j.LoggerFactory;
* database down) then it will stop consuming new messages from the endpoint
by stopping the consumer. The
* implementation is comparable to the Circuit Breaker pattern. After a set
amount of time, it will move to a half open
* state and attempt to determine if the consumer can be started. There are
two ways to determine if a route can be
- * closed after being opened (1) start the consumer and check the failure
threshold (2) call the
- * {@link ThrottlingExceptionHalfOpenHandler} The second option allows a
custom check to be performed without having to
- * take on the possibility of multiple messages from the endpoint. The idea is
that a handler could run a simple test
- * (ie select 1 from dual) to determine if the processes that cause the route
to be open are now available
+ * closed after being opened (1) start the consumer and let the next exchange
decide: a success closes the circuit and a
+ * failure opens it again (2) call the {@link
ThrottlingExceptionHalfOpenHandler} The second option allows a custom
+ * check to be performed without having to take on the possibility of multiple
messages from the endpoint. The idea is
+ * that a handler could run a simple test (ie select 1 from dual) to determine
if the processes that cause the route to
+ * be open are now available
*/
@Metadata(label = "bean",
description = "A throttle based RoutePolicy which is modelled after
the circuit breaker and will stop consuming"
@@ -102,6 +105,10 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
private volatile Timer halfOpenTimer;
private volatile long lastFailure;
private volatile long openedAt;
+ // the times of the most recent failures (at most failureThreshold of
them), oldest first, guarded by failureLock
+ // (not by lock, which is held while the consumer is suspended or resumed)
+ private final Lock failureLock = new ReentrantLock();
+ private final Deque<Long> failureTimes = new ArrayDeque<>();
public ThrottlingExceptionRoutePolicy() {
}
@@ -201,16 +208,19 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
openCircuit(route);
}
} else {
- if (hasFailed(exchange)) {
+ boolean failed = hasFailed(exchange);
+ if (failed) {
// record the failure
failures.incrementAndGet();
- lastFailure = TimeSource.currentTimeMillis();
+ long now = TimeSource.currentTimeMillis();
+ lastFailure = now;
+ recordFailureTime(now);
} else {
success.incrementAndGet();
}
// check for state change
- calculateState(route);
+ calculateState(route, failed);
}
}
@@ -250,7 +260,41 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
return answer;
}
- private void calculateState(Route route) {
+ private void recordFailureTime(long time) {
+ failureLock.lock();
+ try {
+ failureTimes.addLast(time);
+ trimFailureTimes();
+ } finally {
+ failureLock.unlock();
+ }
+ }
+
+ private void clearFailureTimes() {
+ failureLock.lock();
+ try {
+ failureTimes.clear();
+ } finally {
+ failureLock.unlock();
+ }
+ }
+
+ private void trimFailureTimes() {
+ // only the most recent failureThreshold failures decide whether the
threshold is exceeded
+ int max = Math.max(failureThreshold, 1);
+ while (failureTimes.size() > max) {
+ failureTimes.removeFirst();
+ }
+ }
+
+ /**
+ * Calculates the state of the circuit.
+ *
+ * @param route the route
+ * @param exchangeFailed whether the exchange that is done failed, or null
when there is no exchange (the check of
+ * an open circuit after halfOpenAfter)
+ */
+ private void calculateState(Route route, Boolean exchangeFailed) {
// have we reached the failure limit?
boolean failureLimitReached = isThresholdExceeded();
@@ -261,7 +305,14 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
openCircuit(route);
}
} else if (state.get() == STATE_HALF_OPEN) {
- if (failureLimitReached) {
+ // the exchange done in the half open state decides: a failure
opens the circuit again, and a success
+ // closes it. The failures that opened the circuit must not count
against it again: they may still be within
+ // the failure window (with the defaults the circuit is half
opened after 30 seconds, and the window is 60)
+ if (exchangeFailed == null) {
+ // the half open check of the timer, after an exchange that
was done at the same time has already half
+ // opened the circuit: the next exchange decides
+ LOG.debug("Keeping circuit half open (waiting for an
exchange)...");
+ } else if (exchangeFailed) {
LOG.debug("Opening circuit...");
openCircuit(route);
} else {
@@ -300,10 +351,18 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
protected boolean isThresholdExceeded() {
boolean output = false;
logState();
- // failures exceed the threshold
- // AND the last of those failures occurred within window
- if (failures.get() >= failureThreshold && lastFailure >=
TimeSource.currentTimeMillis() - failureWindow) {
- output = true;
+ // at least failureThreshold failures occurred within the window, that
is the oldest of the most recent
+ // failureThreshold failures occurred within the window
+ long since = TimeSource.currentTimeMillis() - failureWindow;
+ failureLock.lock();
+ try {
+ trimFailureTimes();
+ Long oldest = failureTimes.peekFirst();
+ if (failureTimes.size() >= failureThreshold && (oldest != null ?
oldest : lastFailure) >= since) {
+ output = true;
+ }
+ } finally {
+ failureLock.unlock();
}
return output;
@@ -390,6 +449,7 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
}
failures.set(0);
success.set(0);
+ clearFailureTimes();
lastFailure = 0;
openedAt = 0;
state.set(STATE_CLOSED);
@@ -444,7 +504,7 @@ public class ThrottlingExceptionRoutePolicy extends
RoutePolicySupport implement
public void run() {
// cancel the timer of this task only, a newer timer may already
have been scheduled
timer.cancel();
- calculateState(route);
+ calculateState(route, null);
}
}
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 7dcb1fd4ef7f..c91326786302 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
@@ -69,6 +69,19 @@ or payload from the body after a timed out `pollEnrich` must
keep it before the
with the same message id, headers and attachments, so after a timeout it is no
longer of the message type of the
consumer (such as `GenericFileMessage` or `JmsMessage`).
+=== camel-core - ThrottlingExceptionRoutePolicy counts the failures within the
failure window
+
+The circuit of `ThrottlingExceptionRoutePolicy` now opens when
`failureThreshold` failures occurred within the last
+`failureWindow` milliseconds, as documented. Before, it counted every failure
since the circuit last closed (or since
+the route started), and only the last failure had to be within the window, so
failures spread over hours opened the
+circuit on a single new failure. Routes whose failures are spread out
therefore open their circuit less often now; to
+open it sooner, lower `failureThreshold` or raise `failureWindow`.
+
+In the half open state, a successful exchange now closes the circuit, and a
failed exchange opens it again. Before, a
+successful exchange opened the circuit again as long as the failures that
opened it were within `failureWindow`; with
+the defaults (a window of 60 seconds, a half open check after 30 seconds) a
success in the first 30 seconds after the
+half open check therefore kept the circuit open. A `halfOpenHandler` decides
as before.
+
=== camel-main - a failure to start is logged
A Camel Main application that fails to start now logs why, at ERROR: `Error
starting Camel: ...` with the stack trace,