1616 */
1717package 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-
3019import jakarta .jms .Message ;
3120import jakarta .jms .MessageConsumer ;
3221import jakarta .jms .MessageProducer ;
3322import jakarta .jms .Session ;
34-
3523import org .apache .activemq .ActiveMQConnectionFactory ;
3624import org .apache .activemq .advisory .AdvisoryBroker ;
3725import org .apache .activemq .broker .BrokerPlugin ;
6250import org .slf4j .Logger ;
6351import 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 )
6666public 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}
0 commit comments