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 e227737de332 CAMEL-24457: Fix ZooKeeper cluster split-brain on leader
isolation
e227737de332 is described below
commit e227737de3325f997d6f99f9f4c4b8bc5d7af7cc
Author: Mustafa Kamal ALmonayer
<[email protected]>
AuthorDate: Thu Aug 27 08:52:30 2026 +0300
CAMEL-24457: Fix ZooKeeper cluster split-brain on leader isolation
ZooKeeperClusterView.CamelLeaderElectionListener.takeLeadership() fired
its leadership-lost event while Curator's hasLeadership() still
returned true, since the event fired from inside takeLeadership()
before Curator's internals had flipped leadership state. This caused
ClusteredRoutePolicy to see no leadership change and keep the route
running on a node that had lost its ZooKeeper session, while a healthy
node elected a new leader and started the same route — a split-brain
with duplicate message processing.
Track leadership state in a view-owned volatile flag instead of
delegating to leaderSelector.hasLeadership(), set it before firing the
leadership-gained/lost events in takeLeadership()'s try/finally, and
close (not just interrupt) the LeaderSelector in doStop() so a
deliberate stop cannot race an auto-requeue. autoRequeue() is enabled
in doStart() so the node re-enters the election after ZooKeeper
reconnects.
Adds ZooKeeperClusterViewLeadershipLostIT to exercise the split-brain
scenario end-to-end: acquire leadership, lose it on ZK session loss,
and reclaim it after reconnect.
Closes #25611
---
.../zookeeper/cluster/ZooKeeperClusterView.java | 30 +++---
.../ZooKeeperClusterViewLeadershipLostIT.java | 117 +++++++++++++++++++++
2 files changed, 132 insertions(+), 15 deletions(-)
diff --git
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/cluster/ZooKeeperClusterView.java
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/cluster/ZooKeeperClusterView.java
index c276b152d26a..594134bb4207 100644
---
a/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/cluster/ZooKeeperClusterView.java
+++
b/components/camel-zookeeper/src/main/java/org/apache/camel/component/zookeeper/cluster/ZooKeeperClusterView.java
@@ -47,6 +47,7 @@ final class ZooKeeperClusterView extends
AbstractCamelClusterView {
private final CuratorFramework client;
private final CuratorLocalMember localMember;
private volatile LeaderSelector leaderSelector;
+ private volatile boolean leader;
public ZooKeeperClusterView(CamelClusterService cluster,
ZooKeeperCuratorConfiguration configuration,
CuratorFramework client, String namespace) {
@@ -110,6 +111,7 @@ final class ZooKeeperClusterView extends
AbstractCamelClusterView {
if (leaderSelector == null) {
leaderSelector = new LeaderSelector(client, getFullPath(), new
CamelLeaderElectionListener());
leaderSelector.setId(getClusterService().getId());
+ leaderSelector.autoRequeue();
leaderSelector.start();
} else {
leaderSelector.requeue();
@@ -118,16 +120,11 @@ final class ZooKeeperClusterView extends
AbstractCamelClusterView {
@Override
protected void doStop() throws Exception {
- if (leaderSelector != null) {
- leaderSelector.interruptLeadership();
- fireLeadershipChangedEvent(getLeader().orElse(null));
- }
- }
-
- @Override
- protected void doShutdown() throws Exception {
- if (leaderSelector != null) {
- leaderSelector.close();
+ LeaderSelector selector = leaderSelector;
+ leaderSelector = null;
+ if (selector != null) {
+ leader = false;
+ selector.close();
}
}
@@ -142,6 +139,7 @@ final class ZooKeeperClusterView extends
AbstractCamelClusterView {
private final class CamelLeaderElectionListener extends
LeaderSelectorListenerAdapter {
@Override
public void takeLeadership(CuratorFramework curatorFramework) throws
Exception {
+ leader = true;
fireLeadershipChangedEvent(localMember);
BlockingTask task =
Tasks.foregroundTask().withBudget(Budgets.iterationBudget()
@@ -149,17 +147,19 @@ final class ZooKeeperClusterView extends
AbstractCamelClusterView {
.withInterval(Duration.ofSeconds(5))
.build())
.build();
-
- task.run(getCamelContext(), () -> !isRunAllowed());
-
- fireLeadershipChangedEvent(getLeader().orElse(null));
+ try {
+ task.run(getCamelContext(), () -> !isRunAllowed());
+ } finally {
+ leader = false;
+ fireLeadershipChangedEvent((CamelClusterMember) null);
+ }
}
}
private final class CuratorLocalMember implements CamelClusterMember {
@Override
public boolean isLeader() {
- return leaderSelector != null && leaderSelector.hasLeadership();
+ return leader;
}
@Override
diff --git
a/components/camel-zookeeper/src/test/java/org/apache/camel/component/zookeeper/cluster/integration/ZooKeeperClusterViewLeadershipLostIT.java
b/components/camel-zookeeper/src/test/java/org/apache/camel/component/zookeeper/cluster/integration/ZooKeeperClusterViewLeadershipLostIT.java
new file mode 100644
index 000000000000..f23b136da743
--- /dev/null
+++
b/components/camel-zookeeper/src/test/java/org/apache/camel/component/zookeeper/cluster/integration/ZooKeeperClusterViewLeadershipLostIT.java
@@ -0,0 +1,117 @@
+/*
+ * 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.component.zookeeper.cluster.integration;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.cluster.CamelClusterView;
+import org.apache.camel.component.zookeeper.cluster.ZooKeeperClusterService;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.impl.cluster.ClusteredRoutePolicy;
+import org.apache.camel.test.infra.common.services.ContainerService;
+import org.apache.camel.test.infra.zookeeper.services.ZooKeeperService;
+import org.apache.camel.test.infra.zookeeper.services.ZooKeeperServiceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.containers.GenericContainer;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+class ZooKeeperClusterViewLeadershipLostIT {
+
+ private static final String NAMESPACE = "my-ns";
+ private static final String ROUTE_ID = "clustered-route";
+
+ @RegisterExtension
+ static ZooKeeperService service = ZooKeeperServiceFactory.createService();
+
+ @Test
+ void leadershipIsReleasedAndReacquiredAroundAZooKeeperOutage() throws
Exception {
+ GenericContainer<?> zooKeeper = zooKeeperContainer();
+
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ ZooKeeperClusterService clusterService = new
ZooKeeperClusterService();
+ clusterService.setId("node-1");
+ clusterService.setNodes(service.serverUrls());
+ clusterService.setBasePath("/camel");
+
+ clusterService.setSessionTimeout(5000);
+
+ context.disableJMX();
+ context.addService(clusterService);
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() throws Exception {
+ from("timer:zookeeper?period=1000")
+ .routeId(ROUTE_ID)
+
.routePolicy(ClusteredRoutePolicy.forNamespace(NAMESPACE))
+ .to("log:zookeeper?level=DEBUG");
+ }
+ });
+
+ context.start();
+
+ CamelClusterView view = clusterService.getView(NAMESPACE);
+
+ await().atMost(1, TimeUnit.MINUTES)
+ .untilAsserted(() -> {
+ assertEquals(true,
+ view.getLocalMember().isLeader(),
+ "the only node of the cluster must be the
leader");
+ assertEquals(true,
+
context.getRouteController().getRouteStatus(ROUTE_ID).isStarted(),
+ "the leader must have started the clustered
route");
+ });
+
+
zooKeeper.getDockerClient().pauseContainerCmd(zooKeeper.getContainerId()).exec();
+
+ try {
+ await().atMost(1, TimeUnit.MINUTES)
+ .untilAsserted(() -> {
+ assertEquals(false,
+ view.getLocalMember().isLeader(),
+ "the leadership must be given up once
ZooKeeper is no longer reachable");
+ assertEquals(false,
+
context.getRouteController().getRouteStatus(ROUTE_ID).isStarted(),
+ "the clustered route must be stopped once
the leadership is lost");
+ });
+ } finally {
+
zooKeeper.getDockerClient().unpauseContainerCmd(zooKeeper.getContainerId()).exec();
+ }
+
+ await().atMost(1, TimeUnit.MINUTES)
+ .untilAsserted(() -> {
+ assertEquals(true,
+ view.getLocalMember().isLeader(),
+ "the node must re-enter the election once
ZooKeeper is reachable again");
+ assertEquals(true,
+
context.getRouteController().getRouteStatus(ROUTE_ID).isStarted(),
+ "the clustered route must be restarted once
the leadership is taken back");
+ });
+ }
+ }
+
+ private static GenericContainer<?> zooKeeperContainer() {
+ assumeTrue(service instanceof ContainerService<?>,
+ "This test requires the local ZooKeeper container infra
service");
+
+ return ((ContainerService<?>) service).getContainer();
+ }
+}