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 5a84225089f9 CAMEL-26799: camel-cluster - ClusteredRoutePolicy starts
a route when the leadership was taken before the route could be started (#29589)
5a84225089f9 is described below
commit 5a84225089f915316b7a0c64e8635d4f3083f1c8
Author: allthingssecurity <[email protected]>
AuthorDate: Wed Nov 11 12:21:40 2026 +0530
CAMEL-26799: camel-cluster - ClusteredRoutePolicy starts a route when the
leadership was taken before the route could be started (#29589)
* CAMEL-26799: camel-cluster - ClusteredRoutePolicy starts a route when the
leadership was taken before the route could be started
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/impl/cluster/ClusteredRoutePolicy.java | 51 +++++++-
.../camel/cluster/ClusteredEdgeCasesTest.java | 134 +++++++++++++++++++++
2 files changed, 181 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 db96dd245bfd..46d9db812017 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,11 +43,13 @@ import org.apache.camel.cluster.CamelClusterService;
import org.apache.camel.cluster.CamelClusterView;
import org.apache.camel.spi.CamelEvent;
import org.apache.camel.spi.CamelEvent.CamelContextStartedEvent;
+import org.apache.camel.spi.CamelEvent.RouteAddedEvent;
import org.apache.camel.spi.ThreadPoolProfile;
import org.apache.camel.support.RoutePolicySupport;
import org.apache.camel.support.SimpleEventNotifierSupport;
import org.apache.camel.support.cluster.ClusterServiceHelper;
import org.apache.camel.support.cluster.ClusterServiceSelectors;
+import org.apache.camel.support.service.BaseService;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ReferenceCount;
import org.apache.camel.util.concurrent.ThreadPoolRejectedPolicy;
@@ -77,6 +79,8 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
private final Lock retainLock;
private final AtomicReference<CamelClusterView> clusterView;
private final AtomicBoolean leadershipChangePending;
+ // a route of this policy was added to the CamelContext, so the leader may
now start it
+ private final AtomicBoolean routeAdded;
private CamelClusterService clusterService;
private volatile boolean startManagedRoutesEarly;
private volatile boolean initialDelayElapsed;
@@ -100,6 +104,7 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
this.lock = new ReentrantLock();
this.retainLock = new ReentrantLock();
this.leadershipChangePending = new AtomicBoolean();
+ this.routeAdded = new AtomicBoolean();
// the routes are started and stopped on the policy thread, while
routes are added and removed by the caller
this.stoppedRoutes = ConcurrentHashMap.newKeySet();
this.startedRoutes = ConcurrentHashMap.newKeySet();
@@ -337,6 +342,8 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
return;
}
+ // a route of this policy was added to the CamelContext since the
last change
+ boolean added = routeAdded.getAndSet(false);
boolean isLeader = view.getLocalMember().isLeader();
if (isLeader && leader.compareAndSet(false, isLeader)) {
LOG.debug("Leadership taken");
@@ -344,6 +351,10 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
} else if (!isLeader && leader.getAndSet(isLeader)) {
LOG.debug("Leadership lost");
stopManagedRoutes();
+ } else if (isLeader && added) {
+ // a route that could not be started when the leadership was
taken, as it was not added to the
+ // CamelContext yet (doStartManagedRoutes only starts the
routes that are still stopped)
+ startManagedRoutes();
}
} finally {
lock.unlock();
@@ -375,19 +386,40 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
}
if (!isRunAllowed()) {
+ if (isNew() || isBuild() || isInit()) {
+ // the leadership was taken before this policy started (a
cluster service started on its own, with a
+ // view that is already the leader): start the routes once the
CamelContext has started. A policy that
+ // is stopping or stopped does not start them
+ LOG.debug("Will defer starting managed routes until the policy
and the camel context are started");
+ startManagedRoutesEarly = true;
+ }
return;
}
try {
for (Route route : stoppedRoutes) {
- ServiceStatus status = getStatus(route);
+ ServiceStatus status =
camelContext.getRouteController().getRouteStatus(route.getId());
+ if (status == null) {
+ // the route is not added to the CamelContext yet:
starting it now would do nothing, so it is
+ // started when it is added (RouteAddedEvent)
+ LOG.debug("Will defer starting route '{}' until it is
added to the CamelContext", route.getId());
+ continue;
+ }
boolean autostart = autoStartupRoutes.contains(route);
- if (status != null && status.isStartable() && autostart) {
+ if (status.isStartable() && autostart) {
LOG.debug("Starting route '{}'", route.getId());
camelContext.getRouteController().startRoute(route.getId());
startedRoutes.add(route);
+ if (camelContext.getRoute(route.getId()) != route
+ || route instanceof BaseService service &&
service.isShutdown()) {
+ // the route was removed while it was started
(startRoute waited for the removal, and did
+ // nothing): a route added later with the same id must
not be stopped by this policy. Checked
+ // after the add, as a removed route is shut down
before onRemove forgets it
+ LOG.debug("Route '{}' was removed while it was being
started", route.getId());
+ startedRoutes.remove(route);
+ }
}
}
@@ -525,12 +557,23 @@ public final class ClusteredRoutePolicy extends
RoutePolicySupport implements Ca
implements ExtendedStartupListener, NonManagedService {
@Override
public void notify(CamelEvent event) throws Exception {
- onCamelContextStarted();
+ 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)
+ routeAdded.set(true);
+ handOverLeadershipChange();
+ } else {
+ onCamelContextStarted();
+ }
}
@Override
public boolean isEnabled(CamelEvent event) {
- return event instanceof CamelContextStartedEvent;
+ // a route added while the CamelContext starts is started (or
deferred) with the others. The event also comes
+ // when a stopped route is warmed up again; the extra pass then
has nothing to do
+ return event instanceof CamelContextStartedEvent
+ || event instanceof RouteAddedEvent rae &&
camelContext.isStarted()
+ && stoppedRoutes.contains(rae.getRoute());
}
@Override
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 20907a4bc322..a7121e12016c 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
@@ -20,14 +20,18 @@ import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.camel.CamelContext;
+import org.apache.camel.Route;
import org.apache.camel.ServiceStatus;
import org.apache.camel.builder.RouteBuilder;
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.support.RoutePolicySupport;
import org.apache.camel.support.cluster.AbstractCamelClusterService;
import org.apache.camel.support.cluster.AbstractCamelClusterView;
import org.apache.camel.support.cluster.ClusterServiceSelectors;
@@ -237,6 +241,136 @@ public class ClusteredEdgeCasesTest {
}
}
+ @Test
+ public void testLeadershipTakenBeforeTheCamelContextStarted() throws
Exception {
+ // a cluster service started on its own (not by the CamelContext),
whose view is already the leader
+ TestClusterService cs = new TestClusterService("cs");
+ cs.start();
+ // creates the view
+ CamelClusterView view = cs.getView("my-ns");
+ CamelContext context = new DefaultCamelContext();
+ try {
+ cs.getView().setLeader(true);
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace(cs, "my-ns");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+ }
+ });
+ context.start();
+
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ cs.releaseView(view);
+ cs.stop();
+ }
+ }
+
+ @Test
+ public void testLeadershipTakenWhileARouteIsAdded() throws Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ // tells when the policy thread has tried to start the route
+ CountDownLatch startTried = new CountDownLatch(1);
+ context.setRouteController(new DefaultRouteController(context) {
+ @Override
+ public void startRoute(String routeId) throws Exception {
+ super.startRoute(routeId);
+ startTried.countDown();
+ }
+ });
+ context.start();
+ try {
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ // listed after the clustered policy: this node becomes the leader
while the route is created, before the
+ // route is added to the CamelContext
+ RoutePolicySupport leaderWhileAdding = new RoutePolicySupport() {
+ @Override
+ public void onInit(Route route) {
+ cs.getView().setLeader(true);
+ try {
+ // the policy thread tries to start the route now (the
old code did); the fixed policy does
+ // not, as the route is not added yet, so this wait
ends after its timeout of one second
+ startTried.await(1, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ };
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("seda:foo").routeId("foo").routePolicy(policy,
leaderWhileAdding).to("mock:foo");
+ }
+ });
+
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
+ @Test
+ public void testRouteRemovedWhileThePolicyStartsIt() throws Exception {
+ TestClusterService cs = new TestClusterService("cs");
+ CamelContext context = new DefaultCamelContext();
+ context.addService(cs);
+ CountDownLatch starting = new CountDownLatch(1);
+ CountDownLatch removed = new CountDownLatch(1);
+ // the start of the route by the policy thread waits until the route
was removed
+ context.setRouteController(new DefaultRouteController(context) {
+ @Override
+ public void startRoute(String routeId) throws Exception {
+ if ("foo".equals(routeId) && starting.getCount() > 0) {
+ starting.countDown();
+ // on the policy thread: the test checks the outcome
(bounded wait, the test fails anyway)
+ removed.await(20, TimeUnit.SECONDS);
+ }
+ super.startRoute(routeId);
+ }
+ });
+ ClusteredRoutePolicy policy =
ClusteredRoutePolicy.forNamespace("my-ns");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("seda:foo").routeId("foo").routePolicy(policy).to("mock:foo");
+
from("seda:bar").routeId("bar").routePolicy(policy).to("mock:bar");
+ }
+ });
+ context.start();
+ try {
+ cs.getView().setLeader(true);
+ assertTrue(starting.await(10, TimeUnit.SECONDS), "The policy did
not start the route");
+ context.removeRoute("foo");
+ removed.countDown();
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("bar")));
+
+ // a route that is not clustered with the same id
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("seda:foo2").routeId("foo").to("mock:foo2");
+ }
+ });
+ assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo"));
+
+ // the policy must not stop it when this node loses the leadership
+ cs.getView().setLeader(false);
+ await().atMost(10, TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Stopped,
context.getRouteController().getRouteStatus("bar")));
+ await().during(1, TimeUnit.SECONDS).atMost(2,
TimeUnit.SECONDS).untilAsserted(
+ () -> assertEquals(ServiceStatus.Started,
context.getRouteController().getRouteStatus("foo")));
+ } finally {
+ context.stop();
+ }
+ }
+
// *********************************
// Helpers
// *********************************