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 86f1e5f46277 CAMEL-25883: camel-jgroups-raft - an endpoint no longer 
disconnects or closes the RaftHandle of the component while other endpoints use 
it (#28425)
86f1e5f46277 is described below

commit 86f1e5f46277b70c9ab7723a87aede9be2f8ed03
Author: allthingssecurity <[email protected]>
AuthorDate: Sun Oct 25 13:07:01 2026 +0530

    CAMEL-25883: camel-jgroups-raft - an endpoint no longer disconnects or 
closes the RaftHandle of the component while other endpoints use it (#28425)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../jgroups/raft/JGroupsRaftComponent.java         |  90 ++++++++++++++++
 .../jgroups/raft/JGroupsRaftConsumer.java          |   9 +-
 .../jgroups/raft/JGroupsRaftEndpoint.java          |  40 ++++---
 .../jgroups/raft/JGroupsRaftProducer.java          |   8 +-
 .../JGroupsRaftComponentHandleEndpointsTest.java   | 118 +++++++++++++++++++++
 .../raft/JGroupsRaftNeverStartedConsumerTest.java  |  62 +++++++++++
 6 files changed, 313 insertions(+), 14 deletions(-)

diff --git 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponent.java
 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponent.java
index b83cdb53cf52..4e7e256d2c17 100644
--- 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponent.java
+++ 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponent.java
@@ -16,7 +16,14 @@
  */
 package org.apache.camel.component.jgroups.raft;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 import org.apache.camel.Endpoint;
 import org.apache.camel.component.jgroups.raft.utils.NopStateMachine;
@@ -26,6 +33,8 @@ import org.apache.camel.spi.annotations.Component;
 import org.apache.camel.support.DefaultComponent;
 import org.jgroups.raft.RaftHandle;
 import org.jgroups.raft.StateMachine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Component providing support for JGroups-raft leader election and shared 
state machine implementation
@@ -33,6 +42,8 @@ import org.jgroups.raft.StateMachine;
  */
 @Component("jgroups-raft")
 public class JGroupsRaftComponent extends DefaultComponent {
+    private static final Logger LOG = 
LoggerFactory.getLogger(JGroupsRaftComponent.class);
+
     @UriParam
     @Metadata(defaultValue = "null")
     private RaftHandle raftHandle;
@@ -46,6 +57,13 @@ public class JGroupsRaftComponent extends DefaultComponent {
     @Metadata(defaultValue = "raft.xml")
     private String channelProperties;
 
+    // the connected producers and consumers of each RaftHandle that the 
endpoints did not create (such as the
+    // raftHandle of this component), as several endpoints use such a handle
+    private final Map<RaftHandle, Integer> sharedHandleUsers = new 
IdentityHashMap<>();
+    // the handles that the endpoints did not create and connected, closed 
when this component stops
+    private final Set<RaftHandle> sharedHandles = 
Collections.newSetFromMap(new IdentityHashMap<>());
+    private final Lock sharedHandleLock = new ReentrantLock();
+
     @Override
     protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
         return new JGroupsRaftEndpoint(
@@ -99,5 +117,77 @@ public class JGroupsRaftComponent extends DefaultComponent {
         this.channelProperties = channelProperties;
     }
 
+    /**
+     * Connects a RaftHandle that several endpoints can use, for a producer or 
a consumer of one of them.
+     */
+    void connectSharedHandle(RaftHandle sharedHandle, String clusterName) 
throws Exception {
+        sharedHandleLock.lock();
+        try {
+            sharedHandle.channel().connect(clusterName);
+            sharedHandleUsers.merge(sharedHandle, 1, Integer::sum);
+            sharedHandles.add(sharedHandle);
+        } finally {
+            sharedHandleLock.unlock();
+        }
+    }
+
+    /**
+     * Disconnects a RaftHandle that several endpoints can use, when its last 
producer or consumer stops.
+     */
+    void disconnectSharedHandle(RaftHandle sharedHandle) {
+        sharedHandleLock.lock();
+        try {
+            Integer users = sharedHandleUsers.get(sharedHandle);
+            if (users == null) {
+                return;
+            }
+            if (users > 1) {
+                sharedHandleUsers.put(sharedHandle, users - 1);
+            } else {
+                sharedHandleUsers.remove(sharedHandle);
+                LOG.trace("Disconnecting JGroupsraft Channel {}", 
sharedHandle.channel());
+                sharedHandle.channel().disconnect();
+            }
+        } finally {
+            sharedHandleLock.unlock();
+        }
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+        List<RaftHandle> handles;
+        sharedHandleLock.lock();
+        try {
+            handles = new ArrayList<>(sharedHandles);
+            sharedHandleUsers.clear();
+            sharedHandles.clear();
+        } finally {
+            sharedHandleLock.unlock();
+        }
+        // the raftHandle of the component is used by all its endpoints, so it 
is closed with the component, as is a
+        // handle given to an endpoint
+        if (raftHandle != null && !handles.contains(raftHandle)) {
+            handles.add(raftHandle);
+        }
+        for (RaftHandle handle : handles) {
+            // each handle is closed, also when closing another one fails
+            try {
+                if (handle.channel() != null) {
+                    handle.channel().close();
+                }
+            } catch (Exception e) {
+                LOG.warn("Error closing the channel of the RaftHandle {}: {}", 
handle, e.getMessage(), e);
+            }
+            try {
+                if (handle.log() != null) {
+                    handle.log().close();
+                }
+            } catch (Exception e) {
+                LOG.warn("Error closing the log of the RaftHandle {}: {}", 
handle, e.getMessage(), e);
+            }
+        }
+    }
+
     //TODO: implement a org.jgroups.protocols.raft.StateMachine as a Camel 
Consumer.
 }
diff --git 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftConsumer.java
 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftConsumer.java
index 623b4d94bbdc..0587dc1e356f 100644
--- 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftConsumer.java
+++ 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftConsumer.java
@@ -33,6 +33,9 @@ public class JGroupsRaftConsumer extends DefaultConsumer {
 
     private final CamelRoleChangeListener roleListener;
     private final JGroupsRaftEndpoint endpoint;
+    // whether this consumer connected the handle, so that only a consumer 
that connected it disconnects it (a consumer
+    // that never started, or failed to start, is stopped as well, while the 
handle can be used by other endpoints)
+    private volatile boolean connected;
 
     public JGroupsRaftConsumer(JGroupsRaftEndpoint endpoint, Processor 
processor, String clusterName,
                                boolean enableRoleChangeEvents) {
@@ -53,6 +56,7 @@ public class JGroupsRaftConsumer extends DefaultConsumer {
             endpoint.getResolvedRaftHandle().addRoleListener(roleListener);
         }
         endpoint.connect();
+        connected = true;
     }
 
     @Override
@@ -61,7 +65,10 @@ public class JGroupsRaftConsumer extends DefaultConsumer {
             LOG.debug("Closing connection to cluster: {} from roleListener: 
{}.", clusterName, roleListener);
             endpoint.getResolvedRaftHandle().removeRoleListener(roleListener);
         }
-        endpoint.disconnect();
+        if (connected) {
+            connected = false;
+            endpoint.disconnect();
+        }
         super.doStop();
     }
 }
diff --git 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java
 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java
index b0d16e839aae..6885d8f913ca 100644
--- 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java
+++ 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftEndpoint.java
@@ -114,19 +114,27 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint {
 
     @Override
     protected void doStop() throws Exception {
-        LOG.info("Closing JGroupsraft Channel {}", getEndpointUri());
-        if (resolvedRaftHandle != null && resolvedRaftHandle.channel() != 
null) {
-            resolvedRaftHandle.channel().close();
-            LOG.info("Closed JGroupsraft Channel {}", getEndpointUri());
-        }
-        LOG.info("Closing Log {}", getEndpointUri());
-        if (resolvedRaftHandle != null && resolvedRaftHandle.log() != null) {
-            resolvedRaftHandle.log().close();
-            LOG.info("Closed Log Channel {}", getEndpointUri());
+        // a handle this endpoint did not create (such as the raftHandle of 
the component) is used by other endpoints,
+        // and is closed by the component
+        if (resolvedRaftHandle != null && isOwnHandle()) {
+            LOG.info("Closing JGroupsraft Channel {}", getEndpointUri());
+            if (resolvedRaftHandle.channel() != null) {
+                resolvedRaftHandle.channel().close();
+                LOG.info("Closed JGroupsraft Channel {}", getEndpointUri());
+            }
+            LOG.info("Closing Log {}", getEndpointUri());
+            if (resolvedRaftHandle.log() != null) {
+                resolvedRaftHandle.log().close();
+                LOG.info("Closed Log Channel {}", getEndpointUri());
+            }
         }
         super.doStop();
     }
 
+    private boolean isOwnHandle() {
+        return raftHandle == null;
+    }
+
     private RaftHandle resolveHandle() throws Exception {
         if (raftHandle != null) {
             LOG.trace("Raft Handle resolved as passed by Component: {}", 
raftHandle);
@@ -136,14 +144,14 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint {
             LOG.trace("Raft Handle created with configured channelProperties: 
{} and state machine: {}", channelProperties,
                     stateMachine);
             @SuppressWarnings("resource")
-            // NOTE: channel will be closed by the component during
+            // NOTE: channel will be closed by this endpoint during
             // stop() lifecycle step.
             JChannel channel = new JChannel(channelProperties).name(raftId); 
// NOSONAR
             return new RaftHandle(channel, stateMachine).raftId(raftId);
         }
         LOG.trace("Raft Handle created with defaults: {}, {},", 
JGroupsRaftConstants.DEFAULT_JGROUPSRAFT_CONFIG, stateMachine);
         @SuppressWarnings("resource")
-        // NOTE: channel will be closed by the component during
+        // NOTE: channel will be closed by this endpoint during
         // stop() lifecycle step.
         JChannel channel = new 
JChannel(JGroupsRaftConstants.DEFAULT_JGROUPSRAFT_CONFIG).name(raftId); // 
NOSONAR
         return new RaftHandle(channel, stateMachine).raftId(raftId);
@@ -155,9 +163,13 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint {
      * @throws Exception
      */
     public void connect() throws Exception {
-        connectCount.incrementAndGet();
         LOG.trace("Connecting JGroups-raft Channel {} with cluster name: {}, 
raftHandle: {} and using config: {}",
                 getEndpointUri(), clusterName, resolvedRaftHandle, 
channelProperties == null ? "default" : channelProperties);
+        if (!isOwnHandle()) {
+            ((JGroupsRaftComponent) 
getComponent()).connectSharedHandle(resolvedRaftHandle, clusterName);
+            return;
+        }
+        connectCount.incrementAndGet();
         resolvedRaftHandle.channel().connect(clusterName);
     }
 
@@ -165,6 +177,10 @@ public class JGroupsRaftEndpoint extends DefaultEndpoint {
      * Disconnect shared RaftHandle channel, called by producer and consumer.
      */
     public void disconnect() {
+        if (!isOwnHandle()) {
+            ((JGroupsRaftComponent) 
getComponent()).disconnectSharedHandle(resolvedRaftHandle);
+            return;
+        }
         if (connectCount.decrementAndGet() == 0) {
             LOG.trace("Disconnecting JGroupsraft Channel {}", 
getEndpointUri());
             resolvedRaftHandle.channel().disconnect();
diff --git 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java
 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java
index dca07dfc7b92..5ee0e999d49b 100644
--- 
a/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java
+++ 
b/components/camel-jgroups-raft/src/main/java/org/apache/camel/component/jgroups/raft/JGroupsRaftProducer.java
@@ -32,6 +32,8 @@ public class JGroupsRaftProducer extends DefaultProducer {
 
     // Producer settings
     private final JGroupsRaftEndpoint endpoint;
+    // whether this producer connected the handle, so that a producer that 
never started does not disconnect it
+    private volatile boolean connected;
 
     // Constructor
     public JGroupsRaftProducer(JGroupsRaftEndpoint endpoint) {
@@ -45,11 +47,15 @@ public class JGroupsRaftProducer extends DefaultProducer {
     protected void doStart() throws Exception {
         super.doStart();
         endpoint.connect();
+        connected = true;
     }
 
     @Override
     protected void doStop() throws Exception {
-        endpoint.disconnect();
+        if (connected) {
+            connected = false;
+            endpoint.disconnect();
+        }
         super.doStop();
     }
 
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentHandleEndpointsTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentHandleEndpointsTest.java
new file mode 100644
index 000000000000..1c53c4667678
--- /dev/null
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftComponentHandleEndpointsTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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;
+
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.jgroups.raft.utils.NopStateMachine;
+import org.apache.camel.spi.Registry;
+import org.jgroups.JChannel;
+import org.jgroups.raft.RaftHandle;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The raftHandle of the component is used by every endpoint of the component: 
an endpoint whose producer or consumer
+ * stops, or that is stopped itself, must not disconnect or close the handle 
while the other endpoints use it.
+ */
+class JGroupsRaftComponentHandleEndpointsTest extends JGroupsRaftAbstractTest {
+
+    private static final String CLUSTER_NAME = 
"JGroupsRaftComponentHandleEndpointsTest";
+    private static final String PRODUCER_URI = "jgroups-raft:" + CLUSTER_NAME;
+    private static final String CONSUMER_URI = "jgroups-raft:" + CLUSTER_NAME 
+ "?enableRoleChangeEvents=true";
+
+    private RaftHandle componentHandle;
+    private RaftHandle endpointHandle;
+
+    @Override
+    protected void bindToRegistry(Registry registry) throws Exception {
+        // a handle given to one endpoint (raftHandle=#endpointHandle) is not 
created by the endpoint either
+        endpointHandle = new RaftHandle(new JChannel("raftB.xml").name("B"), 
new NopStateMachine()).raftId("B");
+        registry.bind("endpointHandle", endpointHandle);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                // a single member cluster (raft.xml), so the handle becomes 
leader on its own and can commit
+                componentHandle = new RaftHandle(new 
JChannel("raft.xml").name("A"), new NopStateMachine()).raftId("A");
+                JGroupsRaftComponent component = new JGroupsRaftComponent();
+                component.setRaftHandle(componentHandle);
+                context.addComponent("jgroups-raft", component);
+
+                // two endpoints of the same cluster: the consumer has an 
option the producer has not
+                from("direct:send").routeId("producer").to(PRODUCER_URI);
+                from(CONSUMER_URI).routeId("consumer").to("mock:events");
+
+                from("jgroups-raft:" + CLUSTER_NAME + 
"B?raftHandle=#endpointHandle").to("mock:endpointHandle");
+            }
+        };
+    }
+
+    @BeforeEach
+    void endpointsShareTheHandleOfTheComponent() {
+        JGroupsRaftEndpoint producerEndpoint = 
getMandatoryEndpoint(PRODUCER_URI, JGroupsRaftEndpoint.class);
+        JGroupsRaftEndpoint consumerEndpoint = 
getMandatoryEndpoint(CONSUMER_URI, JGroupsRaftEndpoint.class);
+        assertNotSame(producerEndpoint, consumerEndpoint);
+        assertSame(componentHandle, producerEndpoint.getResolvedRaftHandle());
+        assertSame(componentHandle, consumerEndpoint.getResolvedRaftHandle());
+        waitForLeader(30, componentHandle);
+    }
+
+    @Test
+    void stoppingTheProducerKeepsTheHandleConnected() throws Exception {
+        context.getRouteController().stopRoute("producer");
+
+        assertTrue(componentHandle.channel().isConnected(), "the consumer 
route still uses the handle");
+        assertHandleCommits();
+    }
+
+    @Test
+    void removingTheProducerRouteKeepsTheHandleOpen() throws Exception {
+        context.getRouteController().stopRoute("producer");
+        context.removeRoute("producer");
+
+        assertFalse(componentHandle.channel().isClosed(), "the consumer route 
still uses the handle");
+        assertTrue(componentHandle.channel().isConnected(), "the consumer 
route still uses the handle");
+        assertHandleCommits();
+    }
+
+    @Test
+    void stoppingTheCamelContextClosesTheHandles() {
+        assertTrue(endpointHandle.channel().isConnected());
+
+        context.stop();
+
+        assertTrue(componentHandle.channel().isClosed());
+        assertTrue(endpointHandle.channel().isClosed());
+    }
+
+    private void assertHandleCommits() throws Exception {
+        waitForLeader(30, componentHandle);
+        long commitIndex = componentHandle.commitIndex();
+        componentHandle.set(new byte[] { 1 }, 0, 1, 10, TimeUnit.SECONDS);
+        assertTrue(componentHandle.commitIndex() > commitIndex);
+    }
+}
diff --git 
a/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftNeverStartedConsumerTest.java
 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftNeverStartedConsumerTest.java
new file mode 100644
index 000000000000..7c778539b558
--- /dev/null
+++ 
b/components/camel-jgroups-raft/src/test/java/org/apache/camel/component/jgroups/raft/JGroupsRaftNeverStartedConsumerTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.jgroups.raft.utils.NopStateMachine;
+import org.jgroups.JChannel;
+import org.jgroups.raft.RaftHandle;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A route whose consumer never started (autoStartup false) is stopped or 
removed: it must not disconnect the raftHandle
+ * of the component, which the producer of another endpoint uses.
+ */
+class JGroupsRaftNeverStartedConsumerTest extends JGroupsRaftAbstractTest {
+
+    private static final String CLUSTER_NAME = 
"JGroupsRaftNeverStartedConsumerTest";
+
+    private RaftHandle componentHandle;
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                componentHandle = new RaftHandle(new 
JChannel("raft.xml").name("A"), new NopStateMachine()).raftId("A");
+                JGroupsRaftComponent component = new JGroupsRaftComponent();
+                component.setRaftHandle(componentHandle);
+                context.addComponent("jgroups-raft", component);
+
+                from("direct:send").routeId("producer").to("jgroups-raft:" + 
CLUSTER_NAME);
+                from("jgroups-raft:" + CLUSTER_NAME + 
"?enableRoleChangeEvents=true").routeId("lazy").autoStartup(false)
+                        .to("mock:events");
+            }
+        };
+    }
+
+    @Test
+    void stoppingANeverStartedConsumerRouteKeepsTheHandleConnected() throws 
Exception {
+        assertTrue(componentHandle.channel().isConnected(), "the producer 
connected the handle");
+        context.getRouteController().stopRoute("lazy");
+        assertTrue(componentHandle.channel().isConnected(), "after stopRoute 
of the never started route");
+        context.removeRoute("lazy");
+        assertTrue(componentHandle.channel().isConnected(), "after removeRoute 
of the never started route");
+    }
+}

Reply via email to