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 867b5d053e96 CAMEL-26880: camel-cluster - ClusteredRoutePolicy applies
the leadership again when the CamelContext resumes (#29784)
867b5d053e96 is described below
commit 867b5d053e968f188205b6dd210363fdc3f796f0
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 13 15:02:02 2026 +0530
CAMEL-26880: camel-cluster - ClusteredRoutePolicy applies the leadership
again when the CamelContext resumes (#29784)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/impl/cluster/ClusteredRoutePolicy.java | 32 +++-
.../camel/cluster/ClusteredEdgeCasesTest.java | 175 +++++++++++++++++++++
2 files changed, 203 insertions(+), 4 deletions(-)
diff --git
a/core/camel-cluster/src/main/java/org/apache/camel/impl/cluster/ClusteredRoutePolicy.java
b/core/camel-cluster/src/main/java/org/apache/camel/impl/cluster/ClusteredRoutePolicy.java
index 1c0347d5ebd7..862bc7e607e4 100644
---
a/core/camel-cluster/src/main/java/org/apache/camel/impl/cluster/ClusteredRoutePolicy.java
+++
b/core/camel-cluster/src/main/java/org/apache/camel/impl/cluster/ClusteredRoutePolicy.java
@@ -43,6 +43,7 @@ import org.apache.camel.cluster.CamelClusterMember;
import org.apache.camel.cluster.CamelClusterService;
import org.apache.camel.cluster.CamelClusterView;
import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.spi.CamelEvent.CamelContextResumedEvent;
import org.apache.camel.spi.CamelEvent.CamelContextStartedEvent;
import org.apache.camel.spi.CamelEvent.RouteAddedEvent;
import org.apache.camel.spi.ThreadPoolProfile;
@@ -83,12 +84,14 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
private final AtomicReference<CamelClusterView> clusterView;
private final AtomicBoolean leadershipChangePending;
// the leadership is applied again to the routes (and not only on a
change): a route of this policy was added to
- // the CamelContext, or a route failed to start or stop
+ // the CamelContext, a route failed to start or stop, or the CamelContext
resumed
private final AtomicBoolean reapply;
private final AtomicBoolean retryScheduled;
private CamelClusterService clusterService;
private volatile boolean startManagedRoutesEarly;
private volatile boolean initialDelayElapsed;
+ // the CamelContext was resumed once (a policy attached after the start
does not get the started event)
+ private volatile boolean contextResumed;
private Duration initialDelay;
private volatile ExecutorService leadershipExecutor;
@@ -413,10 +416,23 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
private void doStartManagedRoutes() {
// if we are currently starting up Camel context then defer starting
routes till its fully started
if (camelContext.isStarting()) {
+ if (contextStarted.get() || contextResumed) {
+ // the CamelContext is resuming (its status is starting until
the resume completed, and the resumed
+ // event comes before that): the started event does not come
again, so try again later
+ LOG.debug("Will start managed routes once camel context has
resumed");
+ scheduleRetry();
+ return;
+ }
LOG.debug("Will defer starting managed routes until camel context
is fully started");
startManagedRoutesEarly = true;
return;
}
+ // a route cannot be started while the CamelContext is suspended
(startRoute does nothing then): the leadership is
+ // applied again when the CamelContext resumes
(CamelContextResumedEvent)
+ if (camelContext.isSuspending() || camelContext.isSuspended()) {
+ LOG.debug("Will defer starting managed routes until camel context
is resumed");
+ return;
+ }
// and if the initial delay has not elapsed yet, then defer starting
routes till it has
if (!initialDelayElapsed && initialDelay != null &&
initialDelay.toMillis() > 0) {
LOG.debug("Will defer starting managed routes until the initial
delay {} has elapsed", initialDelay);
@@ -656,7 +672,14 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
implements ExtendedStartupListener, NonManagedService {
@Override
public void notify(CamelEvent event) throws Exception {
- if (event instanceof RouteAddedEvent) {
+ if (event instanceof CamelContextResumedEvent) {
+ contextResumed = true;
+ // a leadership taken while the CamelContext was suspended
could not start the routes, and the resume
+ // starts (or resumes) the routes that were running when it
was suspended, also on a node that lost the
+ // leadership meanwhile: the policy thread applies the
leadership again
+ reapply.set(true);
+ handOverLeadershipChange();
+ } else if (event instanceof RouteAddedEvent) {
// on the thread that adds the route, which holds the
CamelContext route lock: the policy thread starts
// the route if this node is the leader (the route is known to
the CamelContext from now on)
reapply.set(true);
@@ -668,9 +691,10 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
@Override
public boolean isEnabled(CamelEvent event) {
- // a route added while the CamelContext starts is started (or
deferred) with the others. The event also comes
+ // the resumed event: see notify. A route added while the
CamelContext starts is started (or deferred) with
+ // the others. The added event also comes
// when a stopped route is warmed up again; the extra pass then
has nothing to do
- return event instanceof CamelContextStartedEvent
+ return event instanceof CamelContextStartedEvent || event
instanceof CamelContextResumedEvent
|| event instanceof RouteAddedEvent rae &&
camelContext.isStarted()
&& stoppedRoutes.contains(rae.getRoute());
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/cluster/ClusteredEdgeCasesTest.java
b/core/camel-core/src/test/java/org/apache/camel/cluster/ClusteredEdgeCasesTest.java
index 6af19850ad56..54935235cc4a 100644
---
a/core/camel-core/src/test/java/org/apache/camel/cluster/ClusteredEdgeCasesTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/cluster/ClusteredEdgeCasesTest.java
@@ -37,9 +37,11 @@ import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.impl.cluster.ClusteredRouteController;
import org.apache.camel.impl.cluster.ClusteredRoutePolicy;
import org.apache.camel.impl.engine.DefaultRouteController;
+import org.apache.camel.spi.CamelEvent;
import org.apache.camel.support.DefaultComponent;
import org.apache.camel.support.DefaultConsumer;
import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.support.EventNotifierSupport;
import org.apache.camel.support.RoutePolicySupport;
import org.apache.camel.support.cluster.AbstractCamelClusterService;
import org.apache.camel.support.cluster.AbstractCamelClusterView;
@@ -446,6 +448,179 @@ public class ClusteredEdgeCasesTest {
}
}
+ @Test
+ public void testLeadershipTakenWhileTheCamelContextIsSuspended() throws
Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+ }
+ });
+ context.start();
+ try {
+ context.suspend();
+ // the node becomes the leader while the CamelContext is suspended
(from JMX, for example)
+ cs.getView().setLeader(true);
+ await().atMost(10, TimeUnit.SECONDS).until(policy::isLeader);
+
+ context.resume();
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ public void
testLeadershipTakenWhileTheCamelContextIsSuspendedAndASlowResume() throws
Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ policy.setRetryDelay(Duration.ofMillis(100));
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+ }
+ });
+ context.start();
+ // an event notifier after the one of the policy holds the resume, so
the CamelContext is still starting when
+ // the policy thread applies the leadership after the resumed event
+ CountDownLatch neverReleased = new CountDownLatch(1);
+ context.getManagementStrategy().addEventNotifier(new
EventNotifierSupport() {
+ @Override
+ public void notify(CamelEvent event) throws Exception {
+ neverReleased.await(500, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public boolean isEnabled(CamelEvent event) {
+ return event instanceof CamelEvent.CamelContextResumedEvent;
+ }
+ });
+ try {
+ context.suspend();
+ cs.getView().setLeader(true);
+ await().atMost(10, TimeUnit.SECONDS).until(policy::isLeader);
+
+ context.resume();
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ public void testRouteAddedAfterStartAndASlowResume() throws Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ policy.setRetryDelay(Duration.ofMillis(100));
+ context.start();
+ // the policy is attached after the CamelContext started, so it never
gets the started event
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+ }
+ });
+ CountDownLatch neverReleased = new CountDownLatch(1);
+ context.getManagementStrategy().addEventNotifier(new
EventNotifierSupport() {
+ @Override
+ public void notify(CamelEvent event) throws Exception {
+ neverReleased.await(500, TimeUnit.MILLISECONDS);
+ }
+
+ @Override
+ public boolean isEnabled(CamelEvent event) {
+ return event instanceof CamelEvent.CamelContextResumedEvent;
+ }
+ });
+ try {
+ context.suspend();
+ cs.getView().setLeader(true);
+ await().atMost(10, TimeUnit.SECONDS).until(policy::isLeader);
+
+ context.resume();
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ public void
testLeadershipLostWhileTheCamelContextIsSuspendedWithASuspendableConsumer()
throws Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+ }
+ });
+ context.start();
+ try {
+ cs.getView().setLeader(true);
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+
+ context.suspend();
+ cs.getView().setLeader(false);
+ await().atMost(10, TimeUnit.SECONDS).until(() ->
!policy.isLeader());
+
+ context.resume();
+ await().during(1, TimeUnit.SECONDS).atMost(10,
TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Stopped,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ public void testLeadershipLostWhileTheCamelContextIsSuspended() throws
Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ // a consumer that does not support suspension (the failing component
with no failure): the suspend of the
+ // CamelContext stops its route, and the resume starts it again
+ context.addComponent("failing", new FailingStartComponent(new
AtomicInteger()));
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("failing:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+ }
+ });
+ context.start();
+ try {
+ cs.getView().setLeader(true);
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+
+ context.suspend();
+ // the node loses the leadership while the CamelContext is
suspended
+ cs.getView().setLeader(false);
+ await().atMost(10, TimeUnit.SECONDS).until(() ->
!policy.isLeader());
+
+ // the resume starts the routes it stopped: this node must not
keep running the route
+ context.resume();
+ await().during(1, TimeUnit.SECONDS).atMost(10,
TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Stopped,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
@Test
public void testRouteThatFailsToStopIsStartedAgainOnTheNextLeadership()
throws Exception {
TestClusterService cs = new TestClusterService("cs");