This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.18.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.18.x by this push:
new 9b5301f28dfe [backport camel-4.18.x] CAMEL-24457: Fix ZooKeeper
cluster split-brain on leader isolation (#25791)
9b5301f28dfe is described below
commit 9b5301f28dfeb4ef271033d9dce7399975eaa209
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Aug 27 08:46:24 2026 +0200
[backport camel-4.18.x] CAMEL-24457: Fix ZooKeeper cluster split-brain on
leader isolation (#25791)
Straight cherry-pick of #25611 onto camel-4.18.x.
Fixes a split-brain where a node that lost its ZooKeeper session kept
running a clustered route because the leadership-lost event fired
while Curator's hasLeadership() still returned true. Leadership state
is now tracked in a view-owned volatile flag, and the LeaderSelector
is closed (not just interrupted) on doStop() so a deliberate stop
can't race the ZK-reconnect auto-requeue.
See #25611 for full details, review discussion, and test plan.
Closes #25791
---
.../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();
+ }
+}