This is an automated email from the ASF dual-hosted git repository.

apupier 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 0aedc4dfb810 CAMEL-25488: camel-jgroups-raft - tests use their own log 
and wait for the election to end before closing the channels
0aedc4dfb810 is described below

commit 0aedc4dfb810e9c69acb6d0c674346ccd3d5b10c
Author: AurĂ©lien Pupier <[email protected]>
AuthorDate: Tue Nov 3 11:30:52 2026 +0100

    CAMEL-25488: camel-jgroups-raft - tests use their own log and wait for the 
election to end before closing the channels
    
    A node with a single-member configuration (raft.xml, raftB.xml, raftC.xml) 
that is closed
    while its election is running leaves its voting thread alive: the thread is 
only told to stop,
    and as the node has a majority and no leader it keeps creating new terms 
forever.
    The in-memory log, thus the term, is shared by the nodes with the same Raft 
id in the JVM, so the
    orphan thread keeps increasing the term of the node C of the cluster tests. 
That node refuses the
    term of the leader elected by A and B and never gets a leader.
    
    - wait for the voting threads to end before the tests close their channels, 
and log the threads
      that still vote when they do not end
    - give each channel of the cluster tests its own log prefix
    - forget the in-memory logs after each other test, so that each test has 
logs of its own
    
    Co-authored-by: IBM Bob <[email protected]>
    Signed-off-by: AurĂ©lien Pupier <[email protected]>
---
 .../jgroups/raft/JGroupsRaftAbstractTest.java      | 20 +++++++++++
 .../raft/JGroupsRaftComponentConfiguredTest.java   |  3 +-
 .../raft/JGroupsRaftComponentDefaultTest.java      |  3 +-
 .../jgroups/raft/JGroupsRaftEndpointTest.java      |  3 +-
 .../component/jgroups/raft/RaftNodesState.java     | 40 ++++++++++++++++++++++
 .../cluster/JGroupsRaftClusterAbstractTest.java    |  6 ++++
 6 files changed, 69 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftAbstractTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftAbstractTest.java
index 27150ab6d8de..100e82d08b03 100644
--- 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftAbstractTest.java
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftAbstractTest.java
@@ -21,12 +21,32 @@ import java.util.concurrent.TimeUnit;
 import org.apache.camel.Exchange;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.awaitility.core.ConditionTimeoutException;
+import org.jgroups.protocols.raft.InMemoryLog;
 import org.jgroups.raft.RaftHandle;
+import org.junit.jupiter.api.AfterEach;
 
 import static org.awaitility.Awaitility.await;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 public abstract class JGroupsRaftAbstractTest extends CamelTestSupport {
+    /**
+     * Runs before the Camel context is stopped (the methods of a class run 
before the ones of its superclass).
+     */
+    @AfterEach
+    void awaitElectionsFinished() {
+        RaftNodesState.awaitElectionsFinished();
+    }
+
+    /**
+     * The in-memory logs are in a map of the JVM, keyed by the Raft id of the 
node: forget them, so that each test has
+     * logs of its own and does not start with the term of a node of a former 
test with the same Raft id.
+     */
+    @Override
+    protected void cleanupResources() throws Exception {
+        InMemoryLog.logs.clear();
+        super.cleanupResources();
+    }
+
     protected void checkHeaders(Exchange exchange) {
         
assertNotNull(exchange.getIn().getHeader(JGroupsRaftConstants.HEADER_JGROUPSRAFT_COMMIT_INDEX,
 int.class));
         
assertNotNull(exchange.getIn().getHeader(JGroupsRaftConstants.HEADER_JGROUPSRAFT_CURRENT_TERM,
 int.class));
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentConfiguredTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentConfiguredTest.java
index a3a20b17e302..e1b39d5dcb26 100644
--- 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentConfiguredTest.java
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentConfiguredTest.java
@@ -18,7 +18,6 @@ package org.apache.camel.component.jgroups.raft;
 
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.jgroups.raft.utils.NopStateMachine;
-import org.apache.camel.test.junit6.CamelTestSupport;
 import org.jgroups.JChannel;
 import org.jgroups.raft.RaftHandle;
 import org.junit.jupiter.api.Test;
@@ -26,7 +25,7 @@ import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
-public class JGroupsRaftComponentConfiguredTest extends CamelTestSupport {
+public class JGroupsRaftComponentConfiguredTest extends 
JGroupsRaftAbstractTest {
 
     static final String CLUSTER_NAME = "JGroupsRaftComponentConfiguredTest";
     static final String CONFIGURED_ENDPOINT_URI
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentDefaultTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentDefaultTest.java
index 1f627a0f1c26..040c37b8d912 100644
--- 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentDefaultTest.java
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentDefaultTest.java
@@ -18,7 +18,6 @@ package org.apache.camel.component.jgroups.raft;
 
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.jgroups.raft.utils.NopStateMachine;
-import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,7 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public class JGroupsRaftComponentDefaultTest extends CamelTestSupport {
+public class JGroupsRaftComponentDefaultTest extends JGroupsRaftAbstractTest {
 
     static final String CLUSTER_NAME = "JGroupsRaftComponentDefaultTest";
     static final String CONFIGURED_ENDPOINT_URI = 
String.format("my-default-jgroupsraft:%s?raftId=A", CLUSTER_NAME);
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java
index 1c527efca231..6ff2ecc61a19 100644
--- 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpointTest.java
@@ -24,7 +24,6 @@ import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.jgroups.raft.utils.NopStateMachine;
 import org.apache.camel.spi.EndpointUriFactory;
 import org.apache.camel.spi.Registry;
-import org.apache.camel.test.junit6.CamelTestSupport;
 import org.jgroups.JChannel;
 import org.jgroups.raft.RaftHandle;
 import org.jgroups.raft.StateMachine;
@@ -34,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-public class JGroupsRaftEndpointTest extends CamelTestSupport {
+public class JGroupsRaftEndpointTest extends JGroupsRaftAbstractTest {
 
     static final String CLUSTER_NAME = "JGroupsRaftEndpointTest";
     static final String CONFIGURED_ENDPOINT_URI = "jgroups-raft:" + 
CLUSTER_NAME + "?raftId=A";
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/RaftNodesState.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/RaftNodesState.java
index 95305ba679fd..a3b50637125e 100644
--- 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/RaftNodesState.java
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/RaftNodesState.java
@@ -16,6 +16,10 @@
  */
 package org.apache.camel.component.jgroups.raft;
 
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.awaitility.core.ConditionTimeoutException;
 import org.jgroups.JChannel;
 import org.jgroups.protocols.MERGE3;
 import org.jgroups.protocols.pbcast.GMS;
@@ -24,9 +28,15 @@ import org.jgroups.protocols.raft.election.BaseElection;
 import org.jgroups.raft.RaftHandle;
 import org.jgroups.stack.Protocol;
 import org.jgroups.stack.ProtocolStack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.awaitility.Awaitility.await;
 
 public final class RaftNodesState {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(RaftNodesState.class);
+
     private RaftNodesState() {
     }
 
@@ -48,6 +58,36 @@ public final class RaftNodesState {
         }
     }
 
+    /**
+     * Wait until the election of every node of the JVM is over, that is until 
no thread votes anymore.
+     * <p>
+     * A node that closes its channel while it still votes keeps voting 
forever when it has a majority and no leader (a
+     * cluster of a single member): its voting thread is only told to stop. As 
the log (and thus the term) of a node is
+     * shared by the nodes with the same Raft id in the JVM, the thread of a 
former test keeps increasing the term of
+     * the node of the next tests, which then refuses the term of the leader 
and never accepts it. Wait before closing
+     * the channels, so that the thread ends by itself.
+     * <p>
+     * This is best effort: when an election does not end, the threads still 
voting are logged and the test goes on with
+     * its cleanup.
+     */
+    public static void awaitElectionsFinished() {
+        try {
+            await().atMost(5, TimeUnit.SECONDS)
+                    .pollInterval(50, TimeUnit.MILLISECONDS)
+                    .until(() -> votingThreads().isEmpty());
+        } catch (ConditionTimeoutException e) {
+            // the cleanup of the test goes on, and its result tells why
+            LOG.warn("The election does not end, these threads still vote: 
{}", votingThreads());
+        }
+    }
+
+    private static List<String> votingThreads() {
+        return Thread.getAllStackTraces().keySet().stream()
+                .filter(t -> t.isAlive() && 
t.getName().startsWith("voting-thread"))
+                .map(Thread::getName)
+                .toList();
+    }
+
     /**
      * Describe the state of the given nodes, to find out why no leader has 
been elected.
      */
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterAbstractTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterAbstractTest.java
index efee49589234..c0ae3f3b7070 100644
--- 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterAbstractTest.java
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/cluster/JGroupsRaftClusterAbstractTest.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.jgroups.raft.cluster;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 import org.apache.camel.CamelContext;
@@ -35,6 +36,8 @@ public abstract class JGroupsRaftClusterAbstractTest {
     protected final List<CamelContext> contexts = new ArrayList<>();
     protected final List<String> routeNames = new ArrayList<>();
     private final List<RaftHandle> openHandles = new ArrayList<>();
+    // the logs are in a map of the JVM, and are shared by the nodes of the 
JVM with the same Raft id
+    private final String logPrefix = getClass().getSimpleName() + "-" + 
UUID.randomUUID();
 
     /**
      * The channels are registered in the static routing table of 
SHARED_LOOPBACK, so a channel left open by a failed
@@ -43,6 +46,7 @@ public abstract class JGroupsRaftClusterAbstractTest {
      */
     @AfterEach
     void closeAll() {
+        RaftNodesState.awaitElectionsFinished();
         for (CamelContext context : contexts) {
             try {
                 context.stop();
@@ -68,6 +72,8 @@ public abstract class JGroupsRaftClusterAbstractTest {
         JChannel channel = new JChannel("raftABC.xml").name(id);
         RaftNodesState.enableElectionLogging(channel);
         RaftHandle handle = new RaftHandle(channel, stateMachine).raftId(id);
+        // a log of its own, so that the term of a node of another test with 
the same Raft id is not reused
+        handle.raft().logPrefix(logPrefix + "-" + id);
         openHandles.add(handle);
         return handle;
     }

Reply via email to