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 d761ff850947 CAMEL-25852: camel-jgroups - an endpoint no longer
disconnects or closes the channel of the component while other endpoints use it
(#28325)
d761ff850947 is described below
commit d761ff850947da3d63c7549a6ba1fa9016ebd892
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 23 22:04:25 2026 +0530
CAMEL-25852: camel-jgroups - an endpoint no longer disconnects or closes
the channel of the component while other endpoints use it (#28325)
* CAMEL-25852: camel-jgroups - an endpoint no longer disconnects or closes
the channel of the component while other endpoints use it
The channel option of the JGroups component is given to every endpoint of
the
component, but each endpoint counted only its own producers and consumers:
when
those of one endpoint stopped, it disconnected the shared channel, and when
the
endpoint stopped (for example when its route was removed) it closed it. A
route
consuming from another endpoint of the same component (another URI of the
same
cluster, such as one with consumer options) then stayed started and received
nothing more; after the close, for good.
The component now counts the producers and consumers of a channel the
endpoints
did not create, and disconnects it when the last of them stops. An endpoint
only
closes a channel it created (from channelProperties or the default); the
channel
of the component, and a channel given to an endpoint (channel=#bean), are
closed
when the component stops.
A producer or consumer disconnects the channel only when it connected it:
Camel stops a service whose start failed (a consumer without
acceptAllObjects
or a filter refuses to start), and that stop took away a use it never added,
so the channel was disconnected under the other endpoints. A stopping
consumer
removes the receiver of the channel only when it is its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25852: camel-jgroups - count a connect of the endpoint's own
channel only when it succeeded
Review of #28325: with the connected flags of this PR, a producer or
consumer whose connect
failed no longer calls disconnect() on stop, so counting before
resolvedChannel.connect() left
the count of the endpoint's own channel one too high: the channel was not
disconnected when
the last producer or consumer stopped. JGroupsEndpoint.connect() now counts
after a successful
connect, as JGroupsComponent.connectSharedChannel does.
The own-channel path also connects and disconnects under a lock of the
endpoint (as the shared
path does under the component's lock), so that a producer that stops while
a consumer of the
same endpoint starts no longer leaves the consumer on a channel that the
producer disconnects.
resolveChannel() is package-private so that the tests can give the endpoint
a channel.
Tests: JGroupsEndpointOwnChannelTest
- aProducerWhoseConnectFailedDoesNotKeepTheChannelConnected fails when the
connect is counted
before it runs;
- aConsumerThatStartsWhileTheProducerStopsKeepsTheChannelConnected (a
channel whose disconnect
waits for the test) fails without the lock.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25852: camel-jgroups - make the count of the endpoint's own channel
a plain int
Review of #28325: connectCount of JGroupsEndpoint is only read and written
under connectLock
since the previous commit, so it no longer needs to be an AtomicInteger.
The Javadoc of
connect()/disconnect() said "shared channel"; it now says that a channel
the endpoint did not
create is counted by the component and a channel the endpoint created by
the endpoint.
No change in behaviour; the camel-jgroups unit tests (33) pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../camel/component/jgroups/JGroupsComponent.java | 71 +++++++++
.../camel/component/jgroups/JGroupsConsumer.java | 17 ++-
.../camel/component/jgroups/JGroupsEndpoint.java | 58 ++++++--
.../camel/component/jgroups/JGroupsProducer.java | 9 +-
.../JGroupsComponentChannelEndpointsTest.java | 151 +++++++++++++++++++
.../jgroups/JGroupsEndpointOwnChannelTest.java | 161 +++++++++++++++++++++
6 files changed, 453 insertions(+), 14 deletions(-)
diff --git
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsComponent.java
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsComponent.java
index 043dd32d7016..264a3738c861 100644
---
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsComponent.java
+++
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsComponent.java
@@ -16,7 +16,14 @@
*/
package org.apache.camel.component.jgroups;
+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.spi.Metadata;
@@ -56,6 +63,13 @@ public class JGroupsComponent extends DefaultComponent {
+ " insecure setting.")
private boolean acceptAllObjects;
+ // the connected producers and consumers of each channel that the
endpoints did not create (such as the channel of
+ // this component), as several endpoints use such a channel
+ private final Map<JChannel, Integer> sharedChannelUsers = new
IdentityHashMap<>();
+ // the channels that the endpoints did not create and connected, closed
when this component stops
+ private final Set<JChannel> sharedChannels = Collections.newSetFromMap(new
IdentityHashMap<>());
+ private final Lock sharedChannelLock = new ReentrantLock();
+
public JGroupsComponent() {
}
@@ -136,4 +150,61 @@ public class JGroupsComponent extends DefaultComponent {
this.acceptAllObjects = acceptAllObjects;
}
+ /**
+ * Connects a channel that several endpoints can use, for a producer or a
consumer of one of them.
+ */
+ void connectSharedChannel(JChannel sharedChannel, String clusterName)
throws Exception {
+ sharedChannelLock.lock();
+ try {
+ sharedChannel.connect(clusterName);
+ sharedChannelUsers.merge(sharedChannel, 1, Integer::sum);
+ sharedChannels.add(sharedChannel);
+ } finally {
+ sharedChannelLock.unlock();
+ }
+ }
+
+ /**
+ * Disconnects a channel that several endpoints can use, when its last
producer or consumer stops.
+ */
+ void disconnectSharedChannel(JChannel sharedChannel) {
+ sharedChannelLock.lock();
+ try {
+ Integer users = sharedChannelUsers.get(sharedChannel);
+ if (users == null) {
+ return;
+ }
+ if (users > 1) {
+ sharedChannelUsers.put(sharedChannel, users - 1);
+ } else {
+ sharedChannelUsers.remove(sharedChannel);
+ sharedChannel.disconnect();
+ }
+ } finally {
+ sharedChannelLock.unlock();
+ }
+ }
+
+ @Override
+ protected void doStop() throws Exception {
+ super.doStop();
+ List<JChannel> channels;
+ sharedChannelLock.lock();
+ try {
+ channels = new ArrayList<>(sharedChannels);
+ sharedChannelUsers.clear();
+ sharedChannels.clear();
+ } finally {
+ sharedChannelLock.unlock();
+ }
+ // the channel of the component is used by all its endpoints, so it is
closed with the component, as is a
+ // channel given to an endpoint
+ if (channel != null && !channels.contains(channel)) {
+ channels.add(channel);
+ }
+ for (JChannel sharedChannel : channels) {
+ sharedChannel.close();
+ }
+ }
+
}
diff --git
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsConsumer.java
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsConsumer.java
index c2b6237d079a..488c07e8ff61 100644
---
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsConsumer.java
+++
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsConsumer.java
@@ -18,6 +18,7 @@ package org.apache.camel.component.jgroups;
import org.apache.camel.Processor;
import org.apache.camel.support.DefaultConsumer;
+import org.jgroups.JChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +34,10 @@ public class JGroupsConsumer extends DefaultConsumer {
private final CamelJGroupsReceiver receiver;
private final JGroupsEndpoint endpoint;
+ // whether this consumer connected the channel, so that only a consumer
that connected it disconnects it (a consumer
+ // that failed to start is stopped as well, while the channel can be used
by the producers and consumers of other
+ // endpoints)
+ private volatile boolean connected;
public JGroupsConsumer(JGroupsEndpoint endpoint, Processor processor,
String clusterName) {
super(endpoint, processor);
@@ -50,13 +55,21 @@ public class JGroupsConsumer extends DefaultConsumer {
LOG.debug("Connecting receiver: {} to the cluster: {}.", receiver,
clusterName);
endpoint.getResolvedChannel().setReceiver(receiver);
endpoint.connect();
+ connected = true;
}
@Override
protected void doStop() throws Exception {
LOG.debug("Closing connection to cluster: {} from receiver: {}.",
clusterName, receiver);
- endpoint.getResolvedChannel().setReceiver(null);
- endpoint.disconnect();
+ JChannel channel = endpoint.getResolvedChannel();
+ if (channel != null && channel.getReceiver() == receiver) {
+ // only remove the receiver of this consumer, not the one of
another consumer of the channel
+ channel.setReceiver(null);
+ }
+ if (connected) {
+ connected = false;
+ endpoint.disconnect();
+ }
super.doStop();
}
}
diff --git
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
index b1f87d63ced8..9a88d432c277 100644
---
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
+++
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsEndpoint.java
@@ -17,7 +17,8 @@
package org.apache.camel.component.jgroups;
import java.io.ObjectInputFilter;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import org.apache.camel.Category;
import org.apache.camel.Component;
@@ -49,7 +50,11 @@ import org.slf4j.LoggerFactory;
public class JGroupsEndpoint extends DefaultEndpoint {
private static final Logger LOG =
LoggerFactory.getLogger(JGroupsEndpoint.class);
- private AtomicInteger connectCount = new AtomicInteger();
+ // the producers and consumers connected to the channel this endpoint
created, guarded by connectLock
+ private int connectCount;
+ // connects and disconnects the channel this endpoint created together
with its count, so that a producer that
+ // stops and a consumer that starts at the same time leave the channel
connected for the consumer
+ private final Lock connectLock = new ReentrantLock();
private JChannel channel;
private JChannel resolvedChannel;
@@ -155,13 +160,19 @@ public class JGroupsEndpoint extends DefaultEndpoint {
@Override
protected void doStop() throws Exception {
- if (resolvedChannel != null) {
+ // a channel this endpoint did not create (such as the channel of the
component) is used by other endpoints,
+ // and is closed by the component
+ if (resolvedChannel != null && isOwnChannel()) {
LOG.trace("Closing JGroups Channel {}", getEndpointUri());
resolvedChannel.close();
}
super.doStop();
}
+ private boolean isOwnChannel() {
+ return channel == null;
+ }
+
private ObjectInputFilter resolveDeserializationFilter() {
return DeserializationFilterHelper.resolveDeserializationFilter(
deserializationFilter,
DeserializationFilterHelper.DEFAULT_CLASS_DESERIALIZATION_FILTER);
@@ -213,7 +224,10 @@ public class JGroupsEndpoint extends DefaultEndpoint {
return stack != null && stack.findProtocol(AUTH.class,
SYM_ENCRYPT.class, ASYM_ENCRYPT.class) != null;
}
- private JChannel resolveChannel() throws Exception {
+ /**
+ * The channel of the endpoint: the channel option, or a channel it
creates. Package-private for tests.
+ */
+ JChannel resolveChannel() throws Exception {
if (channel != null) {
return channel;
}
@@ -224,21 +238,43 @@ public class JGroupsEndpoint extends DefaultEndpoint {
}
/**
- * Connect shared channel, called by producer and consumer.
+ * Connects the channel of this endpoint, called by producer and consumer.
A channel this endpoint did not create
+ * (such as the channel of the component) is counted by the component, as
other endpoints can use it; a channel this
+ * endpoint created is counted by the endpoint.
*/
public void connect() throws Exception {
- connectCount.incrementAndGet();
LOG.trace("Connecting JGroups Channel {}", getEndpointUri());
- resolvedChannel.connect(clusterName);
+ if (!isOwnChannel()) {
+ ((JGroupsComponent)
getComponent()).connectSharedChannel(resolvedChannel, clusterName);
+ return;
+ }
+ connectLock.lock();
+ try {
+ resolvedChannel.connect(clusterName);
+ // counted after a successful connect, as a producer or consumer
whose connect failed does not disconnect
+ connectCount++;
+ } finally {
+ connectLock.unlock();
+ }
}
/**
- * Disconnect shared channel, called by producer and consumer.
+ * Disconnects the channel of this endpoint when its last producer or
consumer disconnects, called by producer and
+ * consumer.
*/
public void disconnect() {
- if (connectCount.decrementAndGet() == 0) {
- LOG.trace("Disconnecting JGroups Channel {}", getEndpointUri());
- resolvedChannel.disconnect();
+ if (!isOwnChannel()) {
+ ((JGroupsComponent)
getComponent()).disconnectSharedChannel(resolvedChannel);
+ return;
+ }
+ connectLock.lock();
+ try {
+ if (--connectCount == 0) {
+ LOG.trace("Disconnecting JGroups Channel {}",
getEndpointUri());
+ resolvedChannel.disconnect();
+ }
+ } finally {
+ connectLock.unlock();
}
}
diff --git
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
index 1d041f6a10f6..e635c33d4efb 100644
---
a/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
+++
b/components/camel-jgroups/src/main/java/org/apache/camel/component/jgroups/JGroupsProducer.java
@@ -37,6 +37,9 @@ public class JGroupsProducer extends DefaultProducer {
private final String clusterName;
+ // whether this producer connected the channel, so that a producer that
failed to start does not disconnect it
+ private volatile boolean connected;
+
// Constructor
public JGroupsProducer(JGroupsEndpoint endpoint, String clusterName) {
@@ -52,11 +55,15 @@ public class JGroupsProducer 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/src/test/java/org/apache/camel/component/jgroups/JGroupsComponentChannelEndpointsTest.java
b/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsComponentChannelEndpointsTest.java
new file mode 100644
index 000000000000..74498c81132c
--- /dev/null
+++
b/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsComponentChannelEndpointsTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.jgroups.JChannel;
+import org.jgroups.ObjectMessage;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The channel 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 channel
while the other endpoints use it. The
+ * channel is closed when the CamelContext stops, as is a channel given to an
endpoint.
+ */
+class JGroupsComponentChannelEndpointsTest extends CamelTestSupport {
+
+ private static final String CLUSTER = "componentChannelEndpoints";
+
+ private JChannel componentChannel;
+ private JChannel endpointChannel;
+ private JChannel sender;
+
+ @Override
+ protected void doPreSetup() throws Exception {
+ super.doPreSetup();
+ componentChannel = new JChannel();
+ sender = new JChannel();
+ sender.connect(CLUSTER);
+ }
+
+ @Override
+ protected void bindToRegistry(Registry registry) throws Exception {
+ // a channel given to one endpoint (channel=#endpointChannel) is not
created by the endpoint either
+ endpointChannel = new JChannel();
+ registry.bind("endpointChannel", endpointChannel);
+ }
+
+ @Override
+ public void doPostTearDown() {
+ sender.close();
+ componentChannel.close();
+ endpointChannel.close();
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ JGroupsComponent component = new JGroupsComponent();
+ component.setChannel(componentChannel);
+ context.addComponent("jgroups", component);
+
+ // two endpoints of the same cluster: the consumer has an
option the producer has not
+ from("direct:send").routeId("producer").to("jgroups:" +
CLUSTER);
+ from("jgroups:" + CLUSTER +
"?acceptAllObjects=true").routeId("consumer").to("mock:received");
+
+ from("jgroups:" + CLUSTER +
"B?channel=#endpointChannel&acceptAllObjects=true").to("mock:endpointChannel");
+ }
+ };
+ }
+
+ @Test
+ void stoppingTheProducerKeepsTheConsumerReceiving() throws Exception {
+ context.getRouteController().stopRoute("producer");
+
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedBodiesReceived("after the producer stopped");
+
+ sender.send(new ObjectMessage(null, "after the producer stopped"));
+
+ mock.assertIsSatisfied();
+ assertTrue(componentChannel.isConnected());
+ }
+
+ @Test
+ void removingTheProducerRouteKeepsTheConsumerReceiving() throws Exception {
+ context.getRouteController().stopRoute("producer");
+ context.removeRoute("producer");
+
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedBodiesReceived("after the producer route was removed");
+
+ sender.send(new ObjectMessage(null, "after the producer route was
removed"));
+
+ mock.assertIsSatisfied();
+ assertTrue(componentChannel.isConnected());
+ }
+
+ @Test
+ void aConsumerThatFailsToStartKeepsTheChannelOfTheProducerConnected()
throws Exception {
+ context.getRouteController().stopRoute("consumer");
+ assertTrue(componentChannel.isConnected(), "connected for the
producer");
+
+ // no acceptAllObjects and no filter: the consumer refuses to start,
and is stopped
+ addRouteWithAConsumerThatFailsToStart();
+
+ assertTrue(componentChannel.isConnected(), "the channel of the
producer is still connected");
+ }
+
+ @Test
+ void aConsumerThatFailsToStartKeepsTheOtherConsumerReceiving() throws
Exception {
+ addRouteWithAConsumerThatFailsToStart();
+
+ MockEndpoint mock = getMockEndpoint("mock:received");
+ mock.expectedBodiesReceived("after a consumer failed to start");
+
+ sender.send(new ObjectMessage(null, "after a consumer failed to
start"));
+
+ mock.assertIsSatisfied();
+ }
+
+ private void addRouteWithAConsumerThatFailsToStart() {
+ assertThrows(Exception.class, () -> context.addRoutes(new
RouteBuilder() {
+ @Override
+ public void configure() {
+ from("jgroups:" + CLUSTER +
"?enableViewMessages=true").routeId("failing").to("mock:failing");
+ }
+ }));
+ }
+
+ @Test
+ void stoppingTheCamelContextClosesTheChannels() {
+ assertTrue(endpointChannel.isConnected());
+
+ context.stop();
+
+ assertTrue(componentChannel.isClosed());
+ assertTrue(endpointChannel.isClosed());
+ }
+}
diff --git
a/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsEndpointOwnChannelTest.java
b/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsEndpointOwnChannelTest.java
new file mode 100644
index 000000000000..e8b2f748a966
--- /dev/null
+++
b/components/camel-jgroups/src/test/java/org/apache/camel/component/jgroups/JGroupsEndpointOwnChannelTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Consumer;
+import org.apache.camel.Producer;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.jgroups.JChannel;
+import org.junit.jupiter.api.Test;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The producers and consumers of an endpoint share the channel the endpoint
created: it is disconnected when the last
+ * of them that connected it stops.
+ */
+class JGroupsEndpointOwnChannelTest {
+
+ private static final String CLUSTER = "endpointOwnChannel";
+
+ @Test
+ void aProducerWhoseConnectFailedDoesNotKeepTheChannelConnected() throws
Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ JChannel channel = new FailingFirstConnectChannel();
+ JGroupsEndpoint endpoint = endpointWithChannel(context, channel);
+
+ Producer producer = endpoint.createProducer();
+ assertThrows(Exception.class, producer::start);
+ // Camel stops a service whose start failed
+ producer.stop();
+
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+ consumer.start();
+ assertTrue(channel.isConnected());
+
+ // the only producer or consumer that connected the channel stops
+ consumer.stop();
+ assertFalse(channel.isConnected(), "the failed connect of the
producer is not counted");
+
+ endpoint.stop();
+ }
+ }
+
+ @Test
+ void aConsumerThatStartsWhileTheProducerStopsKeepsTheChannelConnected()
throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ BlockingDisconnectChannel channel = new
BlockingDisconnectChannel();
+ JGroupsEndpoint endpoint = endpointWithChannel(context, channel);
+
+ Producer producer = endpoint.createProducer();
+ producer.start();
+ Consumer consumer = endpoint.createConsumer(exchange -> {
+ });
+
+ // the producer stops: it is the last user of the channel, so it
disconnects it
+ Thread stopping = new Thread(producer::stop, "stopping-producer");
+ stopping.start();
+ assertTrue(channel.disconnecting.await(20, TimeUnit.SECONDS));
+
+ // the consumer starts while the channel is being disconnected
+ Thread starting = new Thread(consumer::start, "starting-consumer");
+ starting.start();
+ // it waits until the disconnect is done (or, without the lock,
finds the channel still connected and returns)
+ await().atMost(20, TimeUnit.SECONDS).until(
+ () -> starting.getState() == Thread.State.WAITING ||
starting.getState() == Thread.State.TERMINATED);
+ channel.release.countDown();
+ stopping.join(20000);
+ starting.join(20000);
+ assertFalse(stopping.isAlive());
+ assertFalse(starting.isAlive());
+
+ assertTrue(channel.isConnected(), "connected for the consumer");
+
+ consumer.stop();
+ assertFalse(channel.isConnected());
+ endpoint.stop();
+ }
+ }
+
+ private static JGroupsEndpoint endpointWithChannel(CamelContext context,
JChannel channel) throws Exception {
+ JGroupsComponent component = context.getComponent("jgroups",
JGroupsComponent.class);
+ JGroupsEndpoint endpoint = new JGroupsEndpoint("jgroups:" + CLUSTER,
component, null, CLUSTER, null, false) {
+ @Override
+ JChannel resolveChannel() {
+ // the channel that the endpoint creates (no channel option)
+ return channel;
+ }
+ };
+ endpoint.setAcceptAllObjects(true);
+ endpoint.setCamelContext(context);
+ endpoint.start();
+ return endpoint;
+ }
+
+ /**
+ * A channel whose first connect fails, as when its transport cannot bind.
+ */
+ private static final class FailingFirstConnectChannel extends JChannel {
+
+ private final AtomicInteger connects = new AtomicInteger();
+
+ FailingFirstConnectChannel() throws Exception {
+ }
+
+ @Override
+ public JChannel connect(String clusterName) throws Exception {
+ if (connects.incrementAndGet() == 1) {
+ throw new IllegalStateException("first connect fails");
+ }
+ return super.connect(clusterName);
+ }
+ }
+
+ /**
+ * A channel whose disconnect waits until the test releases it.
+ */
+ private static final class BlockingDisconnectChannel extends JChannel {
+
+ private final CountDownLatch disconnecting = new CountDownLatch(1);
+ private final CountDownLatch release = new CountDownLatch(1);
+
+ BlockingDisconnectChannel() throws Exception {
+ }
+
+ @Override
+ public JChannel disconnect() {
+ disconnecting.countDown();
+ try {
+ release.await(20, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ return super.disconnect();
+ }
+ }
+}