This is an automated email from the ASF dual-hosted git repository.
mattrpav pushed a commit to branch activemq-6.3.x
in repository https://gitbox.apache.org/repos/asf/activemq.git
The following commit(s) were added to refs/heads/activemq-6.3.x by this push:
new 3762e0898a Fix network bridge local-side close (#2475) (#2503)
3762e0898a is described below
commit 3762e0898ae4f60a5a4f1199a5cff35b9394ccbd
Author: Matt Pavlovich <[email protected]>
AuthorDate: Fri Aug 28 12:30:07 2026 -0500
Fix network bridge local-side close (#2475) (#2503)
* Fix: Perform full lifecycle network bridge shutdown on an administrative
stop
* Update transport connection stop to pass exception (#7)
* Update transport connection stop to pass exception
This updates the transport stop to pass any transport exception to the
transport during the stop() method call. The transport can optionally
decide to use it. In this case the VMTransport has been updated to
include the exception as part of the ShutdownInfo command so that
bridges know whether stop() was called normally or because of an error.
The use of this could be expanded to other transports in the future
Test fixes:
---------
(cherry picked from commit 9156668b2e4ce207d58cedcf64bce5b928ca7704)
Co-authored-by: Christopher L. Shannon <[email protected]>
---
.../activemq/broker/TransportConnection.java | 2 +-
.../network/DemandForwardingBridgeSupport.java | 11 +-
.../apache/activemq/transport/vm/VMTransport.java | 7 +-
.../org/apache/activemq/command/ShutdownInfo.java | 21 +-
.../org/apache/activemq/transport/Transport.java | 1 +
.../apache/activemq/transport/TransportFilter.java | 5 +
.../activemq/transport/TransportSupport.java | 5 +
.../transport/failover/FailoverTransport.java | 5 +
.../activemq/transport/fanout/FanoutTransport.java | 5 +
.../activemq/transport/mock/MockTransport.java | 5 +
...dgeDuplexLocalConnectionCloseReconnectTest.java | 230 ++++++++++++++
...orkBridgeLocalConnectionCloseReconnectTest.java | 331 +++++++++++++++++++++
...etworkBridgeSlowConsumerAbortReconnectTest.java | 164 ++++++++++
13 files changed, 786 insertions(+), 6 deletions(-)
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnection.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnection.java
index 26a14c8cfe..22f3d972f7 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnection.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/TransportConnection.java
@@ -1260,7 +1260,7 @@ public class TransportConnection implements Connection,
Task, CommandVisitor {
LOG.trace("Exception caught stopping. This exception is ignored.",
ignore);
}
try {
- transport.stop();
+ transport.stop(transportException.get());
LOG.debug("Stopped transport: {}", transport.getRemoteAddress());
} catch (Exception e) {
LOG.debug("Could not stop transport to {}. This exception is
ignored.", transport.getRemoteAddress(), e);
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/network/DemandForwardingBridgeSupport.java
b/activemq-broker/src/main/java/org/apache/activemq/network/DemandForwardingBridgeSupport.java
index 86dda092d4..9db3fc8b56 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/network/DemandForwardingBridgeSupport.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/network/DemandForwardingBridgeSupport.java
@@ -98,6 +98,7 @@ import
org.apache.activemq.transport.TransportDisposedIOException;
import org.apache.activemq.transport.TransportFilter;
import org.apache.activemq.transport.failover.FailoverTransport;
import org.apache.activemq.transport.tcp.TcpTransport;
+import org.apache.activemq.util.IOExceptionSupport;
import org.apache.activemq.util.IdGenerator;
import org.apache.activemq.util.IntrospectionSupport;
import org.apache.activemq.util.LongSequenceGenerator;
@@ -1328,8 +1329,14 @@ public abstract class DemandForwardingBridgeSupport
implements NetworkBridge, Br
} else if (command.isBrokerInfo()) {
futureLocalBrokerInfo.set((BrokerInfo) command);
} else if (command.isShutdownInfo()) {
- LOG.info("{} Shutting down {}",
configuration.getBrokerName(), configuration.getName());
- stop();
+ ShutdownInfo info = (ShutdownInfo) command;
+ if (brokerService.isStopping() ||
brokerService.isStopped() || info.getError() == null) {
+ LOG.info("{} Shutting down {}",
configuration.getBrokerName(), configuration.getName());
+ stop();
+ } else {
+ // Administrative shutdown via .stop() or
SlowConsumerStrategy needs lifecycle clean-up
+
serviceLocalException(IOExceptionSupport.create(info.getError()));
+ }
} else if (command.getClass() == ConnectionError.class) {
ConnectionError ce = (ConnectionError) command;
serviceLocalException(ce.getException());
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/transport/vm/VMTransport.java
b/activemq-broker/src/main/java/org/apache/activemq/transport/vm/VMTransport.java
index 9ef7860ecb..972c344898 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/transport/vm/VMTransport.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/transport/vm/VMTransport.java
@@ -196,6 +196,11 @@ public class VMTransport implements Transport, Task {
@Override
public void stop() throws Exception {
+ stop(null);
+ }
+
+ @Override
+ public void stop(Throwable exception) throws Exception {
// Only need to do this once, all future oneway calls will now
// fail as will any asnyc jobs in the task runner.
if (disposed.compareAndSet(false, true)) {
@@ -224,7 +229,7 @@ public class VMTransport implements Transport, Task {
// to cleanly shutdown the async tasks so that this is the last
// command it see's.
try {
- peer.transportListener.onCommand(new ShutdownInfo());
+ peer.transportListener.onCommand(new
ShutdownInfo(exception));
} catch (Exception ignore) {
}
diff --git
a/activemq-client/src/main/java/org/apache/activemq/command/ShutdownInfo.java
b/activemq-client/src/main/java/org/apache/activemq/command/ShutdownInfo.java
index 1a74317c23..dfc81141cd 100644
---
a/activemq-client/src/main/java/org/apache/activemq/command/ShutdownInfo.java
+++
b/activemq-client/src/main/java/org/apache/activemq/command/ShutdownInfo.java
@@ -19,14 +19,24 @@ package org.apache.activemq.command;
import org.apache.activemq.state.CommandVisitor;
/**
- *
+ *
* @openwire:marshaller code="11"
- *
+ *
*/
public class ShutdownInfo extends BaseCommand {
public static final byte DATA_STRUCTURE_TYPE = CommandTypes.SHUTDOWN_INFO;
+ private transient Throwable error;
+
+ public ShutdownInfo() {
+
+ }
+
+ public ShutdownInfo(Throwable error) {
+ this.error = error;
+ }
+
public byte getDataStructureType() {
return DATA_STRUCTURE_TYPE;
}
@@ -39,4 +49,11 @@ public class ShutdownInfo extends BaseCommand {
return true;
}
+ public Throwable getError() {
+ return error;
+ }
+
+ public void setError(Throwable error) {
+ this.error = error;
+ }
}
diff --git
a/activemq-client/src/main/java/org/apache/activemq/transport/Transport.java
b/activemq-client/src/main/java/org/apache/activemq/transport/Transport.java
index 2067c14412..ee4c0f379f 100644
--- a/activemq-client/src/main/java/org/apache/activemq/transport/Transport.java
+++ b/activemq-client/src/main/java/org/apache/activemq/transport/Transport.java
@@ -164,4 +164,5 @@ public interface Transport extends Service {
*/
WireFormat getWireFormat();
+ void stop(Throwable exception) throws Exception;
}
diff --git
a/activemq-client/src/main/java/org/apache/activemq/transport/TransportFilter.java
b/activemq-client/src/main/java/org/apache/activemq/transport/TransportFilter.java
index ce02a7ab1a..9034c68b17 100644
---
a/activemq-client/src/main/java/org/apache/activemq/transport/TransportFilter.java
+++
b/activemq-client/src/main/java/org/apache/activemq/transport/TransportFilter.java
@@ -72,6 +72,11 @@ public class TransportFilter implements TransportListener,
Transport {
next.stop();
}
+ @Override
+ public void stop(Throwable exception) throws Exception {
+ next.stop(exception);
+ }
+
@Override
public void onCommand(Object command) {
transportListener.onCommand(command);
diff --git
a/activemq-client/src/main/java/org/apache/activemq/transport/TransportSupport.java
b/activemq-client/src/main/java/org/apache/activemq/transport/TransportSupport.java
index cdf1174b56..583a5987b1 100644
---
a/activemq-client/src/main/java/org/apache/activemq/transport/TransportSupport.java
+++
b/activemq-client/src/main/java/org/apache/activemq/transport/TransportSupport.java
@@ -135,4 +135,9 @@ public abstract class TransportSupport extends
ServiceSupport implements Transpo
return isStarted();
}
+ @Override
+ public void stop(Throwable exception) throws Exception {
+ stop();
+ }
+
}
diff --git
a/activemq-client/src/main/java/org/apache/activemq/transport/failover/FailoverTransport.java
b/activemq-client/src/main/java/org/apache/activemq/transport/failover/FailoverTransport.java
index 7ab3b18818..379e8aa371 100644
---
a/activemq-client/src/main/java/org/apache/activemq/transport/failover/FailoverTransport.java
+++
b/activemq-client/src/main/java/org/apache/activemq/transport/failover/FailoverTransport.java
@@ -373,6 +373,11 @@ public class FailoverTransport implements
CompositeTransport {
}
}
+ @Override
+ public void stop(Throwable exception) throws Exception {
+ stop();
+ }
+
@Override
public void stop() throws Exception {
Transport transportToStop = null;
diff --git
a/activemq-client/src/main/java/org/apache/activemq/transport/fanout/FanoutTransport.java
b/activemq-client/src/main/java/org/apache/activemq/transport/fanout/FanoutTransport.java
index fa618e0889..d2e2f30cd4 100644
---
a/activemq-client/src/main/java/org/apache/activemq/transport/fanout/FanoutTransport.java
+++
b/activemq-client/src/main/java/org/apache/activemq/transport/fanout/FanoutTransport.java
@@ -295,6 +295,11 @@ public class FanoutTransport implements CompositeTransport
{
}
}
+ @Override
+ public void stop(Throwable exception) throws Exception {
+ stop();
+ }
+
@Override
public void stop() throws Exception {
try {
diff --git
a/activemq-client/src/main/java/org/apache/activemq/transport/mock/MockTransport.java
b/activemq-client/src/main/java/org/apache/activemq/transport/mock/MockTransport.java
index 8b00e27bc0..567f411f3f 100644
---
a/activemq-client/src/main/java/org/apache/activemq/transport/mock/MockTransport.java
+++
b/activemq-client/src/main/java/org/apache/activemq/transport/mock/MockTransport.java
@@ -71,6 +71,11 @@ public class MockTransport extends DefaultTransportListener
implements Transport
getNext().stop();
}
+ @Override
+ public void stop(Throwable exception) throws Exception {
+ getNext().stop(exception);
+ }
+
@Override
public void onCommand(Object command) {
getTransportListener().onCommand(command);
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeDuplexLocalConnectionCloseReconnectTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeDuplexLocalConnectionCloseReconnectTest.java
new file mode 100644
index 0000000000..f14b4bdd29
--- /dev/null
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeDuplexLocalConnectionCloseReconnectTest.java
@@ -0,0 +1,230 @@
+/**
+ * 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.activemq.network;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.jms.Connection;
+import jakarta.jms.Session;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.broker.TransportConnection;
+import org.apache.activemq.broker.region.RegionBroker;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.apache.activemq.test.annotations.ParallelTest;
+import org.apache.activemq.util.Wait;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * ShutdownInfo handling for a duplex network bridge, on both sides.
+ *
+ * A duplex bridge has two halves, each with its own local vm:// connection:
the initiator's
+ * bridge on the initiating broker, and the responder's bridge embedded in the
inbound
+ * TransportConnection on the accepting broker. Closing either vm://
connection server-side while
+ * its broker keeps running delivers a ShutdownInfo to that half, and both
halves should fail and
+ * recover so message flow resumes in both directions. An orderly broker
shutdown, which sends
+ * ShutdownInfo over the remote side of the shared connection, should still
stop the bridge without
+ * trying to reconnect to the stopped broker.
+ */
+@Category(ParallelTest.class)
+public class NetworkBridgeDuplexLocalConnectionCloseReconnectTest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(NetworkBridgeDuplexLocalConnectionCloseReconnectTest.class);
+
+ // one queue per direction, consumed only on the receiving side; a
consumer on the producing
+ // side would compete with the bridge's subscription for the same messages
+ private static final String QUEUE_TO_RESPONDER =
"BRIDGE.DUPLEX.TO.RESPONDER";
+ private static final String QUEUE_TO_INITIATOR =
"BRIDGE.DUPLEX.TO.INITIATOR";
+
+ private BrokerService initiatorBroker;
+ private BrokerService responderBroker;
+ private Connection initiatorConnection;
+ private Connection responderConnection;
+ private final AtomicInteger initiatorReceived = new AtomicInteger();
+ private final AtomicInteger responderReceived = new AtomicInteger();
+ private NetworkConnector nc;
+
+ @Before
+ public void setUp() throws Exception {
+ responderBroker = new BrokerService();
+ responderBroker.setBrokerName("responder");
+ responderBroker.setPersistent(false);
+ responderBroker.setUseJmx(false);
+ responderBroker.addConnector("tcp://127.0.0.1:0");
+ responderBroker.start();
+ responderBroker.waitUntilStarted();
+
+ initiatorBroker = new BrokerService();
+ initiatorBroker.setBrokerName("initiator");
+ initiatorBroker.setPersistent(false);
+ initiatorBroker.setUseJmx(false);
+ initiatorBroker.addConnector("tcp://127.0.0.1:0");
+ initiatorBroker.start();
+ initiatorBroker.waitUntilStarted();
+
+ var responderUri =
responderBroker.getTransportConnectors().get(0).getPublishableConnectURI();
+ nc = initiatorBroker.addNetworkConnector("static:(" + responderUri +
")");
+ nc.setName("duplex-to-responder");
+ nc.setDuplex(true);
+ // static inclusion is mirrored onto the responder half via the
BrokerInfo config, so
+ // forwarding works in both directions; this test covers lifecycle,
not demand propagation
+ nc.addStaticallyIncludedDestination(new
ActiveMQQueue(QUEUE_TO_RESPONDER));
+ nc.addStaticallyIncludedDestination(new
ActiveMQQueue(QUEUE_TO_INITIATOR));
+ nc.start();
+ assertTrue("duplex bridge should establish",
+ Wait.waitFor(() -> !nc.activeBridges().isEmpty(), 20_000, 10));
+
+ // consumers on both brokers create demand in both directions across
the one duplex bridge
+ responderConnection = new
ActiveMQConnectionFactory(responderUri).createConnection();
+ responderConnection.start();
+ responderConnection.createSession(false, Session.AUTO_ACKNOWLEDGE)
+ .createConsumer(new ActiveMQQueue(QUEUE_TO_RESPONDER))
+ .setMessageListener(m -> responderReceived.incrementAndGet());
+
+ var initiatorUri =
initiatorBroker.getTransportConnectors().get(0).getPublishableConnectURI();
+ initiatorConnection = new
ActiveMQConnectionFactory(initiatorUri).createConnection();
+ initiatorConnection.start();
+ initiatorConnection.createSession(false, Session.AUTO_ACKNOWLEDGE)
+ .createConsumer(new ActiveMQQueue(QUEUE_TO_INITIATOR))
+ .setMessageListener(m -> initiatorReceived.incrementAndGet());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ for (var c : new Connection[] { initiatorConnection,
responderConnection }) {
+ if (c != null) {
+ try { c.close(); } catch (Exception ignored) {}
+ }
+ }
+ if (initiatorBroker != null) {
+ initiatorBroker.stop();
+ initiatorBroker.waitUntilStopped();
+ }
+ if (responderBroker != null) {
+ responderBroker.stop();
+ responderBroker.waitUntilStopped();
+ }
+ }
+
+ /** Server-side close of the initiator half's local vm:// connection. */
+ @Test(timeout = 120_000)
+ public void testDuplexBridgeReconnectsAfterInitiatorLocalConnectionClose()
throws Exception {
+ assertFlowBothWays("before initiator-side close");
+
+ var oldBridge = nc.activeBridges().iterator().next();
+ var vmConnection = findVmConnection(initiatorBroker);
+ assertNotNull("expected the initiator bridge's local vm://
connection", vmConnection);
+ LOG.info("stopping initiator-side local connection server-side: {}",
vmConnection);
+ vmConnection.serviceException(new IOException("stopping initiator-side
local connection server-side"));
+
+ awaitNewBridge(oldBridge, "after initiator-side close");
+ assertFlowBothWays("after initiator-side close and reconnect");
+ }
+
+ /** Server-side close of the responder half's local vm:// connection. */
+ @Test(timeout = 120_000)
+ public void testDuplexBridgeReconnectsAfterResponderLocalConnectionClose()
throws Exception {
+ assertFlowBothWays("before responder-side close");
+
+ var oldBridge = nc.activeBridges().iterator().next();
+ var vmConnection = findVmConnection(responderBroker);
+ assertNotNull("expected the responder bridge half's local vm://
connection", vmConnection);
+ LOG.info("stopping responder-side local connection server-side: {}",
vmConnection);
+ vmConnection.stop();
+
+ awaitNewBridge(oldBridge, "after responder-side close");
+ assertFlowBothWays("after responder-side close and reconnect");
+ }
+
+ /**
+ * An orderly responder broker shutdown sends ShutdownInfo over the remote
side. The initiator
+ * bridge should stop and stay stopped, not linger in activeBridges
retrying against the
+ * stopped broker.
+ */
+ @Test(timeout = 120_000)
+ public void testDuplexBridgeStopsOnResponderBrokerShutdown() throws
Exception {
+ assertFlowBothWays("before responder broker shutdown");
+
+ responderBroker.stop();
+ responderBroker.waitUntilStopped();
+
+ assertTrue("initiator bridge should be gone after an orderly responder
broker shutdown",
+ Wait.waitFor(() -> nc.activeBridges().isEmpty(), 20_000, 10));
+ }
+
+ private void awaitNewBridge(NetworkBridge oldBridge, String label) throws
Exception {
+ assertTrue("duplex bridge must reconnect with a new instance " + label,
+ Wait.waitFor(() -> {
+ for (var bridge : nc.activeBridges()) {
+ if (bridge != oldBridge) {
+ return true;
+ }
+ }
+ return false;
+ }, 30_000, 10));
+ assertTrue("old duplex bridge instance must be deregistered " + label,
+ Wait.waitFor(() -> nc.activeBridges().size() == 1
+ && !nc.activeBridges().contains(oldBridge), 20_000,
10));
+ LOG.info("duplex bridge reconnected {}", label);
+ }
+
+ /** Messages must cross both ways over the duplex bridge. */
+ private void assertFlowBothWays(String label) throws Exception {
+ var responderBefore = responderReceived.get();
+ produce(initiatorBroker, QUEUE_TO_RESPONDER, 5);
+ assertTrue("initiator->responder flow " + label + " (responder had " +
responderBefore + ")",
+ Wait.waitFor(() -> responderReceived.get() >= responderBefore
+ 5, 30_000, 10));
+
+ var initiatorBefore = initiatorReceived.get();
+ produce(responderBroker, QUEUE_TO_INITIATOR, 5);
+ assertTrue("responder->initiator flow " + label + " (initiator had " +
initiatorBefore + ")",
+ Wait.waitFor(() -> initiatorReceived.get() >= initiatorBefore
+ 5, 30_000, 10));
+ }
+
+ private void produce(BrokerService broker, String queueName, int n) throws
Exception {
+ try (var c = new ActiveMQConnectionFactory(
+
broker.getTransportConnectors().get(0).getPublishableConnectURI()).createConnection();
+ var session = c.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(new
ActiveMQQueue(queueName))) {
+ c.start();
+
+ for (var i = 0; i < n; i++) {
+ producer.send(session.createTextMessage("m-" + i));
+ }
+ }
+ }
+
+ private TransportConnection findVmConnection(BrokerService broker) throws
Exception {
+ for (var connection : ((RegionBroker)
broker.getRegionBroker()).getClients()) {
+ var address = connection.getRemoteAddress();
+ if (address != null && address.startsWith("vm:") && connection
instanceof TransportConnection) {
+ return (TransportConnection) connection;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeLocalConnectionCloseReconnectTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeLocalConnectionCloseReconnectTest.java
new file mode 100644
index 0000000000..6c03f50b25
--- /dev/null
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeLocalConnectionCloseReconnectTest.java
@@ -0,0 +1,331 @@
+/**
+ * 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.activemq.network;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.jms.Connection;
+import jakarta.jms.Session;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.broker.TransportConnection;
+import org.apache.activemq.broker.region.RegionBroker;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.apache.activemq.command.ShutdownInfo;
+import org.apache.activemq.test.annotations.ParallelTest;
+import org.apache.activemq.transport.InactivityIOException;
+import org.apache.activemq.util.Wait;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * When a network bridge's local connection is closed server-side while the
broker keeps running,
+ * the bridge should fail and reconnect.
+ */
+@Category(ParallelTest.class)
+public class NetworkBridgeLocalConnectionCloseReconnectTest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(NetworkBridgeLocalConnectionCloseReconnectTest.class);
+
+ private static final String QUEUE_NAME = "BRIDGE.RECONNECT.TEST";
+
+ private BrokerService localBroker;
+ private BrokerService remoteBroker;
+ private Connection remoteConnection;
+ private final AtomicInteger remoteReceived = new AtomicInteger();
+
+ @Before
+ public void setUp() throws Exception {
+ remoteBroker = new BrokerService();
+ remoteBroker.setBrokerName("remote");
+ remoteBroker.setPersistent(false);
+ remoteBroker.setUseJmx(false);
+ remoteBroker.addConnector("tcp://127.0.0.1:0");
+ remoteBroker.start();
+ remoteBroker.waitUntilStarted();
+
+ localBroker = new BrokerService();
+ localBroker.setBrokerName("local");
+ localBroker.setPersistent(false);
+ localBroker.setUseJmx(false);
+ localBroker.start();
+ localBroker.waitUntilStarted();
+
+ var remoteUri =
remoteBroker.getTransportConnectors().get(0).getPublishableConnectURI();
+ remoteConnection = new
ActiveMQConnectionFactory(remoteUri).createConnection();
+ remoteConnection.start();
+ remoteConnection.createSession(false, Session.AUTO_ACKNOWLEDGE)
+ .createConsumer(new ActiveMQQueue(QUEUE_NAME))
+ .setMessageListener(m -> remoteReceived.incrementAndGet());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (remoteConnection != null) {
+ try { remoteConnection.close(); } catch (Exception ignored) {}
+ }
+ if (localBroker != null) {
+ localBroker.stop();
+ localBroker.waitUntilStopped();
+ }
+ if (remoteBroker != null) {
+ remoteBroker.stop();
+ remoteBroker.waitUntilStopped();
+ }
+ }
+
+ @Test(timeout = 120_000)
+ public void testBridgeReconnectsAfterLocalConnectionClosedServerSide()
throws Exception {
+ var nc = startBridge();
+
+ produce(5);
+ assertTrue("bridge should forward before the close",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 20_000, 10));
+
+ // The bridge's local side is a vm:// client of the local broker.
+ var oldBridge = nc.activeBridges().iterator().next();
+ var bridgeLocalConnection = findVmConnection(localBroker);
+ assertNotNull("expected the bridge's local vm:// connection on the
local broker", bridgeLocalConnection);
+ LOG.info("stopping the bridge's local connection server-side: {}",
bridgeLocalConnection);
+ bridgeLocalConnection.serviceException(new IOException("stopping the
bridge's local connection server-side"));
+
+ // a connection stop is not a broker stop
+ var reconnected = Wait.waitFor(() -> {
+ for (var bridge : nc.activeBridges()) {
+ if (bridge != oldBridge) {
+ return true;
+ }
+ }
+ return false;
+ }, 30_000, 10);
+ assertTrue("bridge must reconnect after local-side connection closed
and not a broker shutdown", reconnected);
+ assertTrue("stopped bridge instance should be removed from
activeBridges",
+ Wait.waitFor(() -> !nc.activeBridges().contains(oldBridge),
20_000, 10));
+
+ var before = remoteReceived.get();
+ produce(5);
+ assertTrue("messages produced after the reconnect must flow across the
new bridge "
+ + "(remote had " + before + ", waiting for " + (before
+ 5) + ")",
+ Wait.waitFor(() -> remoteReceived.get() >= before + 5, 20_000,
10));
+ LOG.info("bridge reconnected and resumed forwarding after server-side
local connection stop");
+ nc.stop();
+ }
+
+ /**
+ * A local broker shutdown should stop the bridge cleanly, with no failure
handling or
+ * reconnect attempt.
+ */
+ @Test(timeout = 120_000)
+ public void testBridgeStopsCleanlyOnLocalBrokerShutdown() throws Exception
{
+ var nc = startBridge();
+ produce(5);
+ assertTrue("bridge should forward before the shutdown",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 20_000, 10));
+
+ localBroker.stop();
+ localBroker.waitUntilStopped();
+
+ assertTrue("bridge should be gone after a local broker shutdown",
+ Wait.waitFor(() -> nc.activeBridges().isEmpty(), 20_000, 10));
+ // the remote broker should be left with only the test's consumer
connection - a
+ // reconnect attempt during shutdown would show up as an extra inbound
client
+ assertTrue("no bridge connection should linger or reconnect on the
remote broker",
+ Wait.waitFor(() -> ((RegionBroker)
remoteBroker.getRegionBroker()).getClients().length == 1, 20_000, 10));
+ Thread.sleep(2000);
+ assertTrue("and none may appear afterwards (no reconnect attempts
after broker stop)",
+ ((RegionBroker)
remoteBroker.getRegionBroker()).getClients().length == 1);
+ }
+
+ /**
+ * Stopping the connector delivers a ShutdownInfo to the bridge's own
local transport as part
+ * of its shutdown. That echo must not be treated as a failure and
reconnect, since the bridge
+ * is already disposed.
+ */
+ @Test(timeout = 120_000)
+ public void testConnectorStopStaysStopped() throws Exception {
+ var nc = startBridge();
+ produce(5);
+ assertTrue("bridge should forward before the stop",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 20_000, 10));
+
+ nc.stop();
+ assertTrue("bridge should be gone after connector stop",
+ Wait.waitFor(() -> nc.activeBridges().isEmpty(), 20_000, 10));
+
+ var receivedAtStop = remoteReceived.get();
+ produce(5);
+ Thread.sleep(3000);
+ assertTrue("bridge must not reconnect ", nc.activeBridges().isEmpty());
+ assertTrue("no messages move after the stop (remote had " +
receivedAtStop
+ + ", now " + remoteReceived.get() + ")", remoteReceived.get()
== receivedAtStop);
+ }
+
+ /**
+ * Every server-side close should be recovered from, not just the first.
After each close the
+ * bridge reconnects, only one bridge stays registered, and forwarding
still works at the end.
+ */
+ @Test(timeout = 120_000)
+ public void testBridgeReconnectsAfterRepeatedLocalConnectionCloses()
throws Exception {
+ var nc = startBridge();
+ produce(5);
+ assertTrue("bridge should forward before the closes",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 20_000, 10));
+
+ for (var round = 1; round <= 3; round++) {
+ var oldBridge = nc.activeBridges().iterator().next();
+ // after a reconnect the new bridge's vm connection may not be
registered yet
+ assertTrue("round " + round + ": bridge's local vm:// connection
should be present",
+ Wait.waitFor(() -> findVmConnection(localBroker) != null,
20_000, 10));
+ var bridgeLocalConnection = findVmConnection(localBroker);
+ bridgeLocalConnection.serviceException(new IOException("stopping
the bridge's local connection server-side"));
+
+ final var expectRound = round;
+ assertTrue("round " + expectRound + ": bridge must reconnect with
a new instance",
+ Wait.waitFor(() -> {
+ for (var bridge : nc.activeBridges()) {
+ if (bridge != oldBridge) {
+ return true;
+ }
+ }
+ return false;
+ }, 30_000, 10));
+ assertTrue("round " + expectRound + ": old bridge instance must be
deregistered, only one live bridge",
+ Wait.waitFor(() -> nc.activeBridges().size() == 1
+ && !nc.activeBridges().contains(oldBridge),
20_000, 10));
+ }
+
+ var before = remoteReceived.get();
+ produce(5);
+ assertTrue("forwarding must still work after repeated close/reconnect
cycles",
+ Wait.waitFor(() -> remoteReceived.get() >= before + 5, 20_000,
10));
+ nc.stop();
+ }
+
+ /**
+ * Once the bridge is stopped, a late ShutdownInfo must be a no-op:
serviceLocalException() on
+ * a disposed bridge must not fire bridgeFailed() or schedule a reconnect.
+ */
+ @Test(timeout = 120_000)
+ public void testStoppedBridgeIgnoresShutdownInfo() throws Exception {
+ var nc = startBridge();
+ produce(5);
+ assertTrue("bridge should forward before the stop",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 20_000, 10));
+
+ var bridge = (DemandForwardingBridgeSupport)
nc.activeBridges().iterator().next();
+ nc.stop();
+ assertTrue("bridge should be gone after connector stop",
+ Wait.waitFor(() -> nc.activeBridges().isEmpty(), 20_000, 10));
+
+ // deliver a late ShutdownInfo to the stopped bridge; the broker is
still running, so
+ // without the disposed guard this would take the failure path and
reconnect
+ bridge.serviceLocalCommand(new ShutdownInfo());
+
+ Thread.sleep(3000);
+ assertTrue("a ShutdownInfo delivered to a stopped bridge must not
resurrect it",
+ nc.activeBridges().isEmpty());
+ assertTrue("and no bridge connection may reappear on the remote
broker",
+ ((RegionBroker)
remoteBroker.getRegionBroker()).getClients().length == 1);
+ }
+
+ /**
+ * AbortSlowConsumerStrategy and AbortSlowAckConsumerStrategy with
abortConnection=true call
+ * connection.serviceException(InactivityIOException) on the bridge's
local connection. The
+ * bridge must restart and resume forwarding.
+ */
+ @Test(timeout = 120_000)
+ public void testBridgeRestartsAfterConnectionServiceException() throws
Exception {
+ var nc = startBridge();
+ produce(5);
+ assertTrue("bridge should forward before the abort",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 20_000, 10));
+
+ var oldBridge = nc.activeBridges().iterator().next();
+ var bridgeLocalConnection = findVmConnection(localBroker);
+ assertNotNull("expected the bridge's local vm:// connection",
bridgeLocalConnection);
+ LOG.info("servicing an InactivityIOException on the bridge's local
connection: {}", bridgeLocalConnection);
+ bridgeLocalConnection.serviceException(
+ new InactivityIOException("1 Consumers was slow too often or
too long"));
+
+ var restarted = Wait.waitFor(() -> {
+ for (var bridge : nc.activeBridges()) {
+ if (bridge != oldBridge) {
+ return true;
+ }
+ }
+ return false;
+ }, 30_000, 10);
+ assertTrue("bridge must restart after connection.serviceException(..)
on its local connection", restarted);
+ assertTrue("aborted bridge instance must be deregistered from
activeBridges",
+ Wait.waitFor(() -> !nc.activeBridges().contains(oldBridge),
20_000, 10));
+
+ var before = remoteReceived.get();
+ produce(5);
+ assertTrue("messages must flow across the restarted bridge (remote had
" + before + ")",
+ Wait.waitFor(() -> remoteReceived.get() >= before + 5, 20_000,
10));
+ LOG.info("bridge restarted and resumed forwarding after
serviceException abort");
+ nc.stop();
+ }
+
+ private NetworkConnector startBridge() throws Exception {
+ var remoteUri =
remoteBroker.getTransportConnectors().get(0).getPublishableConnectURI();
+ var nc = localBroker.addNetworkConnector("static:(" + remoteUri + ")");
+ nc.setName("to-remote");
+ nc.setDuplex(false);
+ nc.addStaticallyIncludedDestination(new ActiveMQQueue(QUEUE_NAME));
+ nc.start();
+ assertTrue("bridge should establish",
+ Wait.waitFor(() -> !nc.activeBridges().isEmpty(), 20_000, 10));
+ return nc;
+ }
+
+ private void produce(int n) throws Exception {
+ try (var c = new ActiveMQConnectionFactory(
+ localBroker.getTransportConnectors().isEmpty()
+ ? localBroker.getVmConnectorURI()
+ :
localBroker.getTransportConnectors().get(0).getPublishableConnectURI()).createConnection();
+ var session = c.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(new
ActiveMQQueue(QUEUE_NAME))) {
+ c.start();
+
+ for (var i = 0; i < n; i++) {
+ producer.send(session.createTextMessage("m-" + i));
+ }
+ }
+ }
+
+ // The bridge's local side is the only vm:// client connection on the
local broker
+ private TransportConnection findVmConnection(BrokerService broker) throws
Exception {
+ for (var connection :
+ ((RegionBroker) broker.getRegionBroker()).getClients()) {
+ var address = connection.getRemoteAddress();
+ if (address != null && address.startsWith("vm:") && connection
instanceof TransportConnection) {
+ return (TransportConnection) connection;
+ }
+ }
+ return null;
+ }
+}
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeSlowConsumerAbortReconnectTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeSlowConsumerAbortReconnectTest.java
new file mode 100644
index 0000000000..c3187d7fbf
--- /dev/null
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/network/NetworkBridgeSlowConsumerAbortReconnectTest.java
@@ -0,0 +1,164 @@
+/**
+ * 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.activemq.network;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import jakarta.jms.Connection;
+import jakarta.jms.Session;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.broker.region.policy.AbortSlowAckConsumerStrategy;
+import org.apache.activemq.broker.region.policy.PolicyEntry;
+import org.apache.activemq.broker.region.policy.PolicyMap;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.apache.activemq.test.annotations.ParallelTest;
+import org.apache.activemq.util.Wait;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AbortSlowAckConsumerStrategy with ignoreNetworkConsumers=false and
abortConnection=true aborts
+ * a network bridge's subscription. The abort closes the bridge's local
connection server-side, so
+ * the bridge should fail, reconnect, and resume forwarding.
+ *
+ * The strategy marks the bridge's subscription slow from timeSinceLastAck,
which starts at
+ * subscription creation, so an idle bridge is aborted after
maxTimeSinceLastAck without simulating
+ * a link fault.
+ */
+@Category(ParallelTest.class)
+public class NetworkBridgeSlowConsumerAbortReconnectTest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(NetworkBridgeSlowConsumerAbortReconnectTest.class);
+
+ private static final String QUEUE_NAME = "BRIDGE.ABORT.RECONNECT.TEST";
+ private static final long MAX_TIME_SINCE_LAST_ACK = 2000;
+ private static final long MAX_SLOW_DURATION = 2000;
+
+ private BrokerService localBroker;
+ private BrokerService remoteBroker;
+ private Connection remoteConnection;
+ private final AtomicInteger remoteReceived = new AtomicInteger();
+
+ @Before
+ public void setUp() throws Exception {
+ remoteBroker = new BrokerService();
+ remoteBroker.setBrokerName("remote");
+ remoteBroker.setPersistent(false);
+ remoteBroker.setUseJmx(false);
+ remoteBroker.addConnector("tcp://127.0.0.1:0");
+ remoteBroker.start();
+ remoteBroker.waitUntilStarted();
+
+ var strategy = new AbortSlowAckConsumerStrategy();
+ strategy.setIgnoreNetworkConsumers(false); // default true never
touches a bridge
+ strategy.setIgnoreIdleConsumers(false); // reach the bridge even
when it is idle
+ strategy.setMaxTimeSinceLastAck(MAX_TIME_SINCE_LAST_ACK);
+ strategy.setMaxSlowDuration(MAX_SLOW_DURATION);
+ strategy.setCheckPeriod(500);
+ strategy.setAbortConnection(true); // server-side stop of
the bridge's connection
+
+ var entry = new PolicyEntry();
+ entry.setQueue(">");
+ entry.setSlowConsumerStrategy(strategy);
+ var policyMap = new PolicyMap();
+ policyMap.setDefaultEntry(entry);
+
+ localBroker = new BrokerService();
+ localBroker.setBrokerName("local");
+ localBroker.setPersistent(false);
+ localBroker.setUseJmx(false);
+ localBroker.setDestinationPolicy(policyMap);
+ localBroker.start();
+ localBroker.waitUntilStarted();
+
+ var remoteUri =
remoteBroker.getTransportConnectors().get(0).getPublishableConnectURI();
+ remoteConnection = new
ActiveMQConnectionFactory(remoteUri).createConnection();
+ remoteConnection.start();
+ remoteConnection.createSession(false, Session.AUTO_ACKNOWLEDGE)
+ .createConsumer(new ActiveMQQueue(QUEUE_NAME))
+ .setMessageListener(m -> remoteReceived.incrementAndGet());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (remoteConnection != null) {
+ try { remoteConnection.close(); } catch (Exception ignored) {}
+ }
+ if (localBroker != null) {
+ localBroker.stop();
+ localBroker.waitUntilStopped();
+ }
+ if (remoteBroker != null) {
+ remoteBroker.stop();
+ remoteBroker.waitUntilStopped();
+ }
+ }
+
+ @Test(timeout = 120_000)
+ public void testBridgeReconnectsAfterSlowConsumerStrategyAbort() throws
Exception {
+ var remoteUri =
remoteBroker.getTransportConnectors().get(0).getPublishableConnectURI();
+ var nc = localBroker.addNetworkConnector("static:(" + remoteUri + ")");
+ nc.setName("to-remote");
+ nc.setDuplex(false);
+ nc.addStaticallyIncludedDestination(new ActiveMQQueue(QUEUE_NAME));
+ nc.start();
+ assertTrue("bridge should establish",
+ Wait.waitFor(() -> !nc.activeBridges().isEmpty(), 20_000, 10));
+ var firstBridge = nc.activeBridges().iterator().next();
+
+ // the strategy marks the idle bridge subscription slow after
maxTimeSinceLastAck and
+ // aborts its connection after maxSlowDuration; the bridge should then
come back
+ assertTrue("a new bridge instance must replace the aborted one (abort
expected ~"
+ + (MAX_TIME_SINCE_LAST_ACK + MAX_SLOW_DURATION) + "ms after
establish)",
+ Wait.waitFor(() -> {
+ for (var bridge : nc.activeBridges()) {
+ if (bridge != firstBridge) {
+ return true;
+ }
+ }
+ return false;
+ }, 60_000, 10));
+ assertTrue("the aborted bridge instance must be deregistered from
activeBridges",
+ Wait.waitFor(() -> !nc.activeBridges().contains(firstBridge),
20_000, 10));
+ LOG.info("bridge aborted by AbortSlowAckConsumerStrategy and
reconnected");
+
+ // an idle bridge keeps being recycled, but once messages flow and are
acked the active
+ // bridge is no longer slow, so forwarding converges
+ try (var c = new
ActiveMQConnectionFactory(localBroker.getVmConnectorURI()).createConnection();
+ var session = c.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(new
ActiveMQQueue(QUEUE_NAME));) {
+ c.start();
+
+
+ for (var i = 0; i < 5; i++) {
+ producer.send(session.createTextMessage("m-" + i));
+ }
+ }
+ assertTrue("messages must flow across a live bridge after the strategy
aborts",
+ Wait.waitFor(() -> remoteReceived.get() >= 5, 60_000, 10));
+ LOG.info("forwarding resumed after the strategy abort");
+ nc.stop();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact