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 a7169d7c6ab6 CAMEL-25488/CAMEL-24598: fix flaky jgroups-raft tests -
take care of Learner role change (#27613)
a7169d7c6ab6 is described below
commit a7169d7c6ab64d6f440980a7b49dfb55210e8ff4
Author: Aurélien Pupier <[email protected]>
AuthorDate: Mon Oct 12 22:50:36 2026 +0200
CAMEL-25488/CAMEL-24598: fix flaky jgroups-raft tests - take care of
Learner role change (#27613)
Co-authored-by: IBM Bob 2.2.1
---
.../raft/cluster/ClusterRoleChangeListener.java | 7 ++-
.../raft/cluster/JGroupsRaftClusterView.java | 22 ++++++-
.../cluster/ClusterRoleChangeListenerTest.java | 73 ++++++++++++++++++++++
.../raft/cluster/JGroupsRaftClusterViewTest.java | 39 ++++++++++++
.../src/test/resources/raftABC.xml | 7 +--
5 files changed, 139 insertions(+), 9 deletions(-)
diff --git
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListener.java
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListener.java
index 90eaf6249049..88bf38dd9895 100644
---
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListener.java
+++
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListener.java
@@ -46,14 +46,17 @@ public class ClusterRoleChangeListener implements
RAFT.RoleChange {
}
break;
case Follower:
+ case Learner:
if (jgroupsRaftClusterView.isMaster()) {
jgroupsRaftClusterView.setMaster(false);
jgroupsRaftClusterView.fireLeadershipChangedEvent((CamelClusterMember) null);
}
break;
default:
- LOG.error("Role {} unknown.", role);
- throw new UnsupportedOperationException("Role " + role + "
unknown.");
+ // Role enum is exhaustively handled above. This branch guards
against
+ // new Role values being added in future jgroups-raft releases.
+ LOG.warn("Unexpected role {}; ignoring.", role);
+ break;
}
}
}
diff --git
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterView.java
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterView.java
index b983a10491ae..9abca349bf48 100644
---
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterView.java
+++
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterView.java
@@ -39,6 +39,7 @@ public class JGroupsRaftClusterView extends
AbstractCamelClusterView {
private RaftHandle raftHandle;
private String raftId;
private volatile boolean isMaster;
+ private ClusterRoleChangeListener roleListener;
protected JGroupsRaftClusterView(CamelClusterService cluster, String
namespace, String jgroupsConfig,
String jgroupsClusterName, RaftHandle
raftHandle, String raftId) {
@@ -83,12 +84,16 @@ public class JGroupsRaftClusterView extends
AbstractCamelClusterView {
}
fireLeadershipChangedEvent((CamelClusterMember) null);
+ // Register the listener once before attempting to connect;
registering inside
+ // the retry loop would accumulate duplicate listeners across retries.
+ roleListener = new ClusterRoleChangeListener(this);
+ raftHandle.addRoleListener(roleListener);
+
// it may take a while for event to trigger and allow us to join so
retry a while
Exception cause = null;
for (int i = 1; i < 11; i++) {
LOG.debug("Attempt #{} for raft {} to join {}", i, raftId,
jgroupsClusterName);
try {
- raftHandle.addRoleListener(new
ClusterRoleChangeListener(this));
raftHandle.channel().connect(jgroupsClusterName);
LOG.debug("Joined and connected to {} with raft id: {}",
jgroupsClusterName, raftId);
cause = null;
@@ -108,8 +113,14 @@ public class JGroupsRaftClusterView extends
AbstractCamelClusterView {
protected void doStop() throws Exception {
isMaster = false;
fireLeadershipChangedEvent((CamelClusterMember) null);
- LOG.info("Disconnecting JGroupsraft Channel for JGroupsRaftClusterView
with Id {}", raftId);
- raftHandle.channel().disconnect();
+ if (raftHandle != null && raftHandle.channel() != null) {
+ LOG.info("Disconnecting JGroupsraft Channel for
JGroupsRaftClusterView with Id {}", raftId);
+ raftHandle.channel().disconnect();
+ }
+ if (roleListener != null) {
+ raftHandle.removeRoleListener(roleListener);
+ roleListener = null;
+ }
if (raftHandle != null && raftHandle.log() != null) {
raftHandle.log().close();
LOG.info("Closed Log for JGroupsRaftClusterView with Id {}",
raftId);
@@ -128,6 +139,11 @@ public class JGroupsRaftClusterView extends
AbstractCamelClusterView {
LOG.info("Closed JGroupsraft Channel Channel for
JGroupsRaftClusterView with Id {}", raftId);
}
+ if (roleListener != null) {
+ raftHandle.removeRoleListener(roleListener);
+ roleListener = null;
+ }
+
if (raftHandle.log() != null) {
LOG.info("Closing Log for JGroupsRaftClusterView with Id {}",
raftId);
raftHandle.log().close();
diff --git
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListenerTest.java
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListenerTest.java
new file mode 100644
index 000000000000..f8bc3ea44d78
--- /dev/null
+++
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/ClusterRoleChangeListenerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.jgroups.raft.cluster;
+
+import org.jgroups.protocols.raft.Role;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link ClusterRoleChangeListener} that verify role
transitions without requiring a real JGroups
+ * cluster.
+ */
+public class ClusterRoleChangeListenerTest {
+
+ private JGroupsRaftClusterView view;
+ private ClusterRoleChangeListener listener;
+
+ @BeforeEach
+ void setUp() {
+ // Minimal in-memory view: no cluster service needed, no listeners
registered,
+ // so fireLeadershipChangedEvent() is a safe no-op.
+ view = new JGroupsRaftClusterView(null, "test-ns", null, null, null,
"A");
+ listener = new ClusterRoleChangeListener(view);
+ }
+
+ @Test
+ void leaderRoleSetsViewAsMaster() {
+ assertFalse(view.isMaster());
+ listener.roleChanged(Role.Leader);
+ assertTrue(view.isMaster());
+ }
+
+ @Test
+ void followerRoleClearsViewMasterStatus() {
+ view.setMaster(true);
+ listener.roleChanged(Role.Follower);
+ assertFalse(view.isMaster());
+ }
+
+ @Test
+ void learnerRoleClearsViewMasterStatus() {
+ view.setMaster(true);
+ // Role.Learner previously fell through to default: throw (swallowed
by RAFT), leaving the view as master.
+ // It must now de-master the view without throwing.
+ assertDoesNotThrow(() -> listener.roleChanged(Role.Learner));
+ assertFalse(view.isMaster());
+ }
+
+ @Test
+ void learnerRoleWhenNotMasterIsNoOp() {
+ assertFalse(view.isMaster());
+ assertDoesNotThrow(() -> listener.roleChanged(Role.Learner));
+ assertFalse(view.isMaster());
+ }
+}
diff --git
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterViewTest.java
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterViewTest.java
new file mode 100644
index 000000000000..6e578cd02ecf
--- /dev/null
+++
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterViewTest.java
@@ -0,0 +1,39 @@
+/*
+ * 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.jgroups.raft.cluster;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+/**
+ * Unit tests for {@link JGroupsRaftClusterView} lifecycle methods that do not
require a real JGroups cluster.
+ */
+public class JGroupsRaftClusterViewTest {
+
+ /**
+ * doStop() must not throw NullPointerException when raftHandle is null
(e.g. when the view failed to start before a
+ * handle was initialised).
+ */
+ @Test
+ void doStopWithNullRaftHandleDoesNotThrow() {
+ JGroupsRaftClusterView view = new JGroupsRaftClusterView(null,
"test-ns", null, null, null, "A");
+
+ // raftHandle is null (never set / doStart never ran) — doStop must be
safe.
+ assertDoesNotThrow(view::doStop);
+ }
+}
diff --git a/components/camel-jgroups-raft/src/test/resources/raftABC.xml
b/components/camel-jgroups-raft/src/test/resources/raftABC.xml
index aede037dc220..afe88caec51e 100644
--- a/components/camel-jgroups-raft/src/test/resources/raftABC.xml
+++ b/components/camel-jgroups-raft/src/test/resources/raftABC.xml
@@ -31,10 +31,9 @@
thread_pool.keep_alive_time="5000"/>
<SHARED_LOOPBACK_PING />
- <MERGE3 />
- <FD_SOCK/>
+ <MERGE3 min_interval="1000" max_interval="3000"/>
<FD_ALL/>
- <VERIFY_SUSPECT timeout="1500" />
+ <VERIFY_SUSPECT timeout="1500"/>
<pbcast.NAKACK2 xmit_interval="500"/>
<UNICAST3 xmit_interval="500"/>
<pbcast.STABLE desired_avg_gossip="50000" max_bytes="4M"/>
@@ -43,7 +42,7 @@
<UFC max_credits="2M" min_threshold="0.4"/>
<MFC max_credits="2M" min_threshold="0.4"/>
<FRAG2 frag_size="60K" />
- <raft.ELECTION />
+ <raft.ELECTION vote_timeout="1000"/>
<raft.RAFT members="A,B,C" raft_id="${raft_id:undefined}"
log_class="org.jgroups.protocols.raft.InMemoryLog"/>
<raft.REDIRECT/>
</config>
\ No newline at end of file