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;
}