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

Reply via email to