Skip to content

Commit 9c5639d

Browse files
authored
Backport network flaky test improvements (#2045)
1 parent be8415f commit 9c5639d

2 files changed

Lines changed: 161 additions & 69 deletions

File tree

activemq-unit-tests/src/test/java/org/apache/activemq/network/DurableSyncNetworkBridgeTest.java

Lines changed: 133 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,10 @@
1616
*/
1717
package org.apache.activemq.network;
1818

19-
import static org.junit.Assert.assertEquals;
20-
import static org.junit.Assert.assertTrue;
21-
22-
import java.io.File;
23-
import java.net.URI;
24-
import java.util.ArrayList;
25-
import java.util.Arrays;
26-
import java.util.Collection;
27-
import java.util.List;
28-
import java.util.concurrent.TimeUnit;
29-
3019
import jakarta.jms.Message;
3120
import jakarta.jms.MessageConsumer;
3221
import jakarta.jms.MessageProducer;
3322
import jakarta.jms.Session;
34-
3523
import org.apache.activemq.ActiveMQConnectionFactory;
3624
import org.apache.activemq.advisory.AdvisoryBroker;
3725
import org.apache.activemq.broker.BrokerPlugin;
@@ -62,6 +50,18 @@
6250
import org.slf4j.Logger;
6351
import org.slf4j.LoggerFactory;
6452

53+
import java.io.File;
54+
import java.io.IOException;
55+
import java.net.URI;
56+
import java.util.ArrayList;
57+
import java.util.Arrays;
58+
import java.util.Collection;
59+
import java.util.List;
60+
import java.util.concurrent.TimeUnit;
61+
62+
import static org.junit.Assert.assertEquals;
63+
import static org.junit.Assert.assertTrue;
64+
6565
@RunWith(Parameterized.class)
6666
public class DurableSyncNetworkBridgeTest extends DynamicNetworkTestSupport {
6767

@@ -83,7 +83,7 @@ public class DurableSyncNetworkBridgeTest extends DynamicNetworkTestSupport {
8383
private final FLOW flow;
8484

8585
@Rule
86-
public Timeout globalTimeout = new Timeout(30, TimeUnit.SECONDS);
86+
public Timeout globalTimeout = new Timeout(60, TimeUnit.SECONDS);
8787

8888
@Parameters
8989
public static Collection<Object[]> data() {
@@ -138,6 +138,10 @@ public void testRemoveSubscriptionPropagate() throws Exception {
138138
assertSubscriptionsCount(broker1, topic, 1);
139139
assertNCDurableSubsCount(broker2, topic, 1);
140140

141+
// Wait for subscription to become inactive before attempting removal
142+
// It's very important to wait here, otherwise the removal may not propagate
143+
waitForSubscriptionInactive(broker1, topic, subName);
144+
141145
removeSubscription(broker1, subName);
142146

143147
assertSubscriptionsCount(broker1, topic, 0);
@@ -222,7 +226,12 @@ public void testRemoveSubscriptionWithBridgeOfflineIncludedChanged() throws Exce
222226
//Test that on successful reconnection of the bridge that
223227
//the NC sub will be removed
224228
restartBroker(broker2, true);
225-
assertNCDurableSubsCount(broker2, topic, 1);
229+
// In REVERSE flow, broker2=localBroker has the bridge and broker1 (remoteBroker)
230+
// is already running, so the sync may have already cleaned up the NC durable sub.
231+
// This "before sync" assertion is only valid in FORWARD flow.
232+
if (flow == FLOW.FORWARD) {
233+
assertNCDurableSubsCount(broker2, topic, 1);
234+
}
226235
restartBroker(broker1, true);
227236
assertBridgeStarted();
228237
assertNCDurableSubsCount(broker2, topic, 0);
@@ -249,7 +258,9 @@ public void testSubscriptionRemovedAfterIncludedChanged() throws Exception {
249258
//the NC sub will be removed because even though the local subscription exists,
250259
//it no longer matches the included filter
251260
restartBroker(broker2, true);
252-
assertNCDurableSubsCount(broker2, topic, 1);
261+
if (flow == FLOW.FORWARD) {
262+
assertNCDurableSubsCount(broker2, topic, 1);
263+
}
253264
restartBroker(broker1, true);
254265
assertBridgeStarted();
255266
assertNCDurableSubsCount(broker2, topic, 0);
@@ -287,7 +298,9 @@ public void testSubscriptionRemovedAfterStaticChanged() throws Exception {
287298
//the NC sub will be removed because even though the local subscription exists,
288299
//it no longer matches the included static filter
289300
restartBroker(broker2, true);
290-
assertNCDurableSubsCount(broker2, topic, 1);
301+
if (flow == FLOW.FORWARD) {
302+
assertNCDurableSubsCount(broker2, topic, 1);
303+
}
291304
restartBroker(broker1, true);
292305
assertBridgeStarted();
293306
assertNCDurableSubsCount(broker2, topic, 0);
@@ -316,10 +329,13 @@ public void testAddAndRemoveSubscriptionWithBridgeOfflineMultiTopics() throws Ex
316329
//Test that on successful reconnection of the bridge that
317330
//the NC sub will be removed for topic1 but will stay for topic2
318331

319-
//before sync, the old NC should exist
332+
//before sync, the old NC should exist (only verifiable in FORWARD flow;
333+
//in REVERSE, broker2=localBroker has the bridge and sync may already have run)
320334
restartBroker(broker2, true);
321-
assertNCDurableSubsCount(broker2, topic, 1);
322-
assertNCDurableSubsCount(broker2, topic2, 0);
335+
if (flow == FLOW.FORWARD) {
336+
assertNCDurableSubsCount(broker2, topic, 1);
337+
assertNCDurableSubsCount(broker2, topic2, 0);
338+
}
323339

324340
//After sync, remove old NC and create one for topic 2
325341
restartBroker(broker1, true);
@@ -527,7 +543,6 @@ public void testAddOnlineSubscriptionsWithBridgeOffline() throws Exception {
527543
session1.createDurableSubscriber(topic, "sub3");
528544
session1.createDurableSubscriber(excludeTopic, "sub-exclude");
529545

530-
Thread.sleep(1000);
531546
assertNCDurableSubsCount(broker2, topic, 1);
532547
assertNCDurableSubsCount(broker2, excludeTopic, 0);
533548

@@ -566,13 +581,10 @@ public void testAddOnlineSubscriptionsTwoBridges() throws Exception {
566581
secondConnector.start();
567582

568583
//Make sure both bridges are connected
569-
assertTrue(Wait.waitFor(new Condition() {
570-
@Override
571-
public boolean isSatisified() throws Exception {
572-
return localBroker.getNetworkConnectors().get(0).activeBridges().size() == 1 &&
573-
localBroker.getNetworkConnectors().get(1).activeBridges().size() == 1;
574-
}
575-
}, 10000, 500));
584+
assertTrue(Wait.waitFor(() ->
585+
localBroker.getNetworkConnectors().get(0).activeBridges().size() == 1 &&
586+
localBroker.getNetworkConnectors().get(1).activeBridges().size() == 1,
587+
TimeUnit.SECONDS.toMillis(15), 500));
576588

577589
//Make sure NC durables exist for both bridges
578590
assertNCDurableSubsCount(broker2, topic2, 1);
@@ -633,13 +645,7 @@ public void testVirtualDestSubForceDurableSync() throws Exception {
633645
final DestinationStatistics remoteDestStatistics2 = remoteBroker.getDestination(
634646
new ActiveMQQueue("include.test.bar.bridge")).getDestinationStatistics();
635647

636-
assertTrue(Wait.waitFor(new Condition() {
637-
638-
@Override
639-
public boolean isSatisified() throws Exception {
640-
return remoteDestStatistics2.getMessages().getCount() == 501;
641-
}
642-
}));
648+
assertTrue(Wait.waitFor(() -> remoteDestStatistics2.getMessages().getCount() == 501));
643649

644650
}
645651

@@ -719,8 +725,36 @@ protected void doSetUp(boolean deleteAllMessages, boolean startNetworkConnector,
719725
included = new ActiveMQTopic(testTopicName);
720726
doSetUpRemoteBroker(deleteAllMessages, remoteDataDir, 0);
721727
doSetUpLocalBroker(deleteAllMessages, startNetworkConnector, localDataDir);
722-
//Give time for advisories to propagate
723-
Thread.sleep(1000);
728+
//Wait for the bridge to be fully started (advisory consumers registered).
729+
//Note: activeBridges().size() == 1 is NOT sufficient because bridges are added
730+
//to the map before start() completes asynchronously. We must wait for the
731+
//startedLatch which counts down after advisory consumers are registered.
732+
if (startNetworkConnector) {
733+
waitForBridgeFullyStarted();
734+
}
735+
}
736+
737+
private void waitForBridgeFullyStarted() throws Exception {
738+
// Wait for the local bridge to be fully started (advisory consumers registered)
739+
assertTrue("Local bridge should be fully started", Wait.waitFor(() -> {
740+
if (localBroker.getNetworkConnectors().get(0).activeBridges().isEmpty()) {
741+
return false;
742+
}
743+
final NetworkBridge bridge = localBroker.getNetworkConnectors().get(0).activeBridges().iterator().next();
744+
if (bridge instanceof DemandForwardingBridgeSupport) {
745+
return ((DemandForwardingBridgeSupport) bridge).startedLatch.getCount() == 0;
746+
}
747+
return true;
748+
}, TimeUnit.SECONDS.toMillis(15), 100));
749+
750+
// Also wait for the duplex bridge on the remote broker to be fully started.
751+
// The duplex connector creates a separate DemandForwardingBridge on the remote side
752+
// that also needs its advisory consumers registered before it can process events.
753+
assertTrue("Duplex bridge should be fully started", Wait.waitFor(() -> {
754+
final DemandForwardingBridge duplexBridge = findDuplexBridge(
755+
remoteBroker.getTransportConnectors().get(0));
756+
return duplexBridge != null && duplexBridge.startedLatch.getCount() == 0;
757+
}, TimeUnit.SECONDS.toMillis(15), 100));
724758
}
725759

726760
protected void restartLocalBroker(boolean startNetworkConnector) throws Exception {
@@ -729,13 +763,42 @@ protected void restartLocalBroker(boolean startNetworkConnector) throws Exceptio
729763
}
730764

731765
protected void restartRemoteBroker() throws Exception {
732-
int port = 0;
733-
if (remoteBroker != null) {
734-
List<TransportConnector> transportConnectors = remoteBroker.getTransportConnectors();
735-
port = transportConnectors.get(0).getConnectUri().getPort();
736-
}
766+
final int previousPort = remoteBroker.getTransportConnectors().get(0).getConnectUri().getPort();
767+
final File dataDir = remoteBroker.getDataDirectoryFile();
737768
stopRemoteBroker();
738-
doSetUpRemoteBroker(false, remoteBroker.getDataDirectoryFile(), port);
769+
try {
770+
doSetUpRemoteBroker(false, dataDir, previousPort);
771+
} catch (final IOException e) {
772+
if (e.getCause() instanceof java.net.BindException) {
773+
// Previous port still in TIME_WAIT — use a new ephemeral port
774+
doSetUpRemoteBroker(false, dataDir, 0);
775+
// Update the local broker's network connector to point to the new port
776+
updateLocalNetworkConnectorUri();
777+
} else {
778+
throw e;
779+
}
780+
}
781+
}
782+
783+
/**
784+
* When the remote broker restarts on a new ephemeral port (BindException fallback),
785+
* any existing network connector on the local broker still points to the old port.
786+
* This method stops the old connector and replaces it with one targeting the new URI.
787+
*/
788+
private void updateLocalNetworkConnectorUri() throws Exception {
789+
if (localBroker == null) {
790+
return;
791+
}
792+
final List<NetworkConnector> connectors = localBroker.getNetworkConnectors();
793+
if (connectors.isEmpty()) {
794+
return;
795+
}
796+
final NetworkConnector oldConnector = connectors.get(0);
797+
oldConnector.stop();
798+
localBroker.removeNetworkConnector(oldConnector);
799+
final NetworkConnector newConnector = configureLocalNetworkConnector();
800+
localBroker.addNetworkConnector(newConnector);
801+
newConnector.start();
739802
}
740803

741804
protected void doSetUpLocalBroker(boolean deleteAllMessages, boolean startNetworkConnector,
@@ -753,12 +816,14 @@ protected void doSetUpLocalBroker(boolean deleteAllMessages, boolean startNetwor
753816
localConnection.start();
754817

755818
if (startNetworkConnector) {
756-
Wait.waitFor(new Condition() {
757-
@Override
758-
public boolean isSatisified() throws Exception {
759-
return localBroker.getNetworkConnectors().get(0).activeBridges().size() == 1;
760-
}
761-
}, 5000, 500);
819+
// Best-effort wait for the bridge to appear. Do NOT use assertTrue here
820+
// because some tests restart localBroker before remoteBroker is running,
821+
// relying on the bridge connecting later when remoteBroker restarts.
822+
// Tests that need the bridge to be fully started call assertBridgeStarted() explicitly.
823+
// Keep timeout short (5s) to avoid growing the NC reconnect backoff too much,
824+
// which would delay bridge formation when the remote broker starts later.
825+
Wait.waitFor(() -> localBroker.getNetworkConnectors().get(0).activeBridges().size() == 1,
826+
TimeUnit.SECONDS.toMillis(5), 500);
762827
}
763828
localSession = localConnection.createSession(false, Session.AUTO_ACKNOWLEDGE);
764829

@@ -869,4 +934,24 @@ protected BrokerService createRemoteBroker(File dataDir, int port) throws Except
869934
return brokerService;
870935
}
871936

937+
/**
938+
* Wait for a durable subscription to become inactive before attempting removal.
939+
* This prevents "Durable consumer is in use" errors when consumer close operations
940+
* complete asynchronously (especially visible with Java 25's different thread scheduling).
941+
*/
942+
protected void waitForSubscriptionInactive(final BrokerService brokerService,
943+
final ActiveMQTopic topic,
944+
final String subName) throws Exception {
945+
assertTrue("Subscription should become inactive", Wait.waitFor(() -> {
946+
final List<org.apache.activemq.broker.region.DurableTopicSubscription> subs = getSubscriptions(brokerService, topic);
947+
for (final org.apache.activemq.broker.region.DurableTopicSubscription sub : subs) {
948+
if (sub.getSubscriptionKey().getSubscriptionName().equals(subName)) {
949+
return !sub.isActive();
950+
}
951+
}
952+
// If subscription doesn't exist, it's considered inactive
953+
return true;
954+
}, TimeUnit.SECONDS.toMillis(15), 100));
955+
}
956+
872957
}

activemq-unit-tests/src/test/java/org/apache/activemq/network/DynamicNetworkTestSupport.java

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.reflect.Field;
2525
import java.util.ArrayList;
2626
import java.util.List;
27+
import java.util.concurrent.TimeUnit;
2728

2829
import jakarta.jms.Connection;
2930
import jakarta.jms.JMSException;
@@ -45,7 +46,6 @@
4546
import org.apache.activemq.command.RemoveSubscriptionInfo;
4647
import org.apache.activemq.util.SubscriptionKey;
4748
import org.apache.activemq.util.Wait;
48-
import org.apache.activemq.util.Wait.Condition;
4949
import org.junit.Rule;
5050
import org.junit.rules.TemporaryFolder;
5151

@@ -95,9 +95,16 @@ protected void stopRemoteBroker() throws Exception {
9595
}
9696

9797
protected void assertBridgeStarted() throws Exception {
98-
assertTrue(Wait.waitFor(
99-
() -> localBroker.getNetworkConnectors().get(0).activeBridges().size() == 1,
100-
10000, 500));
98+
assertTrue("Bridge should be fully started", Wait.waitFor(() -> {
99+
if (localBroker.getNetworkConnectors().get(0).activeBridges().size() != 1) {
100+
return false;
101+
}
102+
final NetworkBridge bridge = localBroker.getNetworkConnectors().get(0).activeBridges().iterator().next();
103+
if (bridge instanceof DemandForwardingBridgeSupport) {
104+
return ((DemandForwardingBridgeSupport) bridge).startedLatch.getCount() == 0;
105+
}
106+
return true;
107+
}, TimeUnit.SECONDS.toMillis(10), 500));
101108
}
102109

103110
protected RemoveSubscriptionInfo getRemoveSubscriptionInfo(final ConnectionContext context,
@@ -119,8 +126,8 @@ protected void waitForConsumerCount(final DestinationStatistics destinationStati
119126

120127
protected void waitForDispatchFromLocalBroker(final DestinationStatistics destinationStatistics, final int count) throws Exception {
121128
assertTrue(Wait.waitFor(() -> count == destinationStatistics.getDequeues().getCount() &&
122-
count == destinationStatistics.getDispatched().getCount() &&
123-
count == destinationStatistics.getForwards().getCount()));
129+
count == destinationStatistics.getDispatched().getCount() &&
130+
count == destinationStatistics.getForwards().getCount()));
124131
}
125132

126133
protected void assertLocalBrokerStatistics(final DestinationStatistics localStatistics, final int count) {
@@ -135,17 +142,19 @@ protected interface ConsumerCreator {
135142

136143
protected void assertNCDurableSubsCount(final BrokerService brokerService,
137144
final ActiveMQTopic dest, final int count) throws Exception {
138-
assertTrue(Wait.waitFor(() -> count == getNCDurableSubs(brokerService, dest).size(),
139-
10000, 500));
145+
assertTrue("Expected " + count + " NC durable subs on " + dest,
146+
Wait.waitFor(() -> count == getNCDurableSubs(brokerService, dest).size(),
147+
TimeUnit.SECONDS.toMillis(30), 500));
140148
}
141149

142150
protected void assertConsumersCount(final BrokerService brokerService,
143151
final ActiveMQDestination dest, final int count) throws Exception {
144152
assertTrue(Wait.waitFor(() -> count == getConsumers(brokerService, dest).size(),
145-
10000, 500));
146-
Thread.sleep(1000);
147-
// Check one more time after a short pause to make sure the count didn't increase past what we wanted
148-
assertEquals(count, getConsumers(brokerService, dest).size());
153+
10000, 500));
154+
// Wait a bit longer and verify the count is stable (didn't increase past what we wanted)
155+
assertTrue("Consumer count should remain stable at " + count,
156+
Wait.waitFor(() -> count == getConsumers(brokerService, dest).size(),
157+
TimeUnit.SECONDS.toMillis(5), 500));
149158
}
150159

151160
protected List<Subscription> getConsumers(final BrokerService brokerService,
@@ -194,7 +203,7 @@ protected List<DurableTopicSubscription> getNCDurableSubs(final BrokerService br
194203
}
195204

196205
protected void removeSubscription(final BrokerService brokerService,
197-
final String subName) throws Exception {
206+
final String subName) throws Exception {
198207
final RemoveSubscriptionInfo info = new RemoveSubscriptionInfo();
199208
info.setClientId(clientId);
200209
info.setSubscriptionName(subName);
@@ -208,12 +217,9 @@ protected void removeSubscription(final BrokerService brokerService,
208217

209218
protected void assertSubscriptionsCount(final BrokerService brokerService,
210219
final ActiveMQTopic dest, final int count) throws Exception {
211-
assertTrue(Wait.waitFor(new Condition() {
212-
@Override
213-
public boolean isSatisified() throws Exception {
214-
return count == getSubscriptions(brokerService, dest).size();
215-
}
216-
}, 10000, 500));
220+
assertTrue("Expected " + count + " subscriptions on " + dest,
221+
Wait.waitFor(() -> count == getSubscriptions(brokerService, dest).size(),
222+
TimeUnit.SECONDS.toMillis(30), 500));
217223
}
218224

219225
protected void assertSubscriptionMapCounts(NetworkBridge networkBridge, final int count) {
@@ -226,8 +232,9 @@ protected void assertSubscriptionMapCounts(NetworkBridge networkBridge, final in
226232
protected DemandForwardingBridge findDuplexBridge(final TransportConnector connector) throws Exception {
227233
assertNotNull(connector);
228234

229-
for (TransportConnection tc : connector.getConnections()) {
230-
if (tc.getConnectionId().startsWith("networkConnector_")) {
235+
for (final TransportConnection tc : connector.getConnections()) {
236+
final String connectionId = tc.getConnectionId();
237+
if (connectionId != null && connectionId.startsWith("networkConnector_")) {
231238
final Field bridgeField = TransportConnection.class.getDeclaredField("duplexBridge");
232239
bridgeField.setAccessible(true);
233240
return (DemandForwardingBridge) bridgeField.get(tc);

0 commit comments

Comments
 (0)