This is an automated email from the ASF dual-hosted git repository.
mattrpav pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/activemq.git
The following commit(s) were added to refs/heads/main by this push:
new bbe46065d0 [AMQ-9692] Support destination gc sweep of destinations
with only wildcard consumers (#1484)
bbe46065d0 is described below
commit bbe46065d0253935d9c3629975372efe119246ec
Author: Matt Pavlovich <[email protected]>
AuthorDate: Wed Sep 2 11:13:08 2026 -0500
[AMQ-9692] Support destination gc sweep of destinations with only wildcard
consumers (#1484)
---
.../activemq/broker/region/AbstractRegion.java | 14 +-
.../activemq/broker/region/BaseDestination.java | 82 +++--
.../apache/activemq/broker/region/Destination.java | 1 +
.../activemq/broker/region/DestinationFilter.java | 5 +
.../apache/activemq/broker/region/TempQueue.java | 3 +-
.../activemq/broker/region/TempQueueRegion.java | 2 +-
.../activemq/broker/region/policy/PolicyEntry.java | 23 ++
.../apache/activemq/java/JavaPolicyEntryTest.java | 48 +--
.../region/DestinationCanGcConsumerTest.java | 208 ++++++++++++
.../activemq/broker/region/DestinationGCTest.java | 270 +++++++++++++--
.../DestinationGCWildcardDurableSubTest.java | 363 +++++++++++++++++++++
.../broker/region/DestinationIsActiveTest.java | 344 +++++++++++++++++++
12 files changed, 1274 insertions(+), 89 deletions(-)
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/AbstractRegion.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/AbstractRegion.java
index af77b1d449..2556c1510e 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/AbstractRegion.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/AbstractRegion.java
@@ -260,16 +260,16 @@ public abstract class AbstractRegion implements Region {
}
@Override
- public void removeDestination(ConnectionContext context,
ActiveMQDestination destination, long timeout)
- throws Exception {
-
+ public void removeDestination(ConnectionContext context,
ActiveMQDestination destination, long timeout) throws Exception {
// No timeout.. then try to shut down right way, fails if there are
// current subscribers.
if (timeout == 0) {
- for (Iterator<Subscription> iter =
subscriptions.values().iterator(); iter.hasNext();) {
- Subscription sub = iter.next();
- if (sub.matches(destination) ) {
- throw new JMSException("Destination: " + destination + "
still has an active subscription: " + sub);
+ final var dest = destinations.get(destination);
+ if (dest != null) {
+ for (var sub : subscriptions.values()) {
+ if (sub.matches(destination) && dest.isActive()) {
+ throw new JMSException("Destination: " + destination +
" still has an active subscription: " + sub);
+ }
}
}
}
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/BaseDestination.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/BaseDestination.java
index 3b461ed649..844533db3a 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/BaseDestination.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/BaseDestination.java
@@ -19,6 +19,7 @@ package org.apache.activemq.broker.region;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
import jakarta.jms.ResourceAllocationException;
@@ -105,7 +106,8 @@ public abstract class BaseDestination implements
Destination {
private long inactiveTimeoutBeforeGC = DEFAULT_INACTIVE_TIMEOUT_BEFORE_GC;
private boolean gcIfInactive;
private boolean gcWithNetworkConsumers;
- private long lastActiveTime=0l;
+ private boolean gcWithOnlyWildcardConsumers;
+ private long lastActiveTime = 0L;
private boolean reduceMemoryFootprint = false;
protected final Scheduler scheduler;
private boolean disposed = false;
@@ -311,12 +313,42 @@ public abstract class BaseDestination implements
Destination {
@Override
public boolean isActive() {
- boolean isActive = destinationStatistics.getConsumers().getCount() > 0
||
- destinationStatistics.getProducers().getCount() > 0;
- if (isActive && isGcWithNetworkConsumers() &&
destinationStatistics.getConsumers().getCount() > 0) {
- isActive = hasRegularConsumers(getConsumers());
+ // if we have producers then we are active
+ if (destinationStatistics.getProducers().getCount() > 0) {
+ return true;
}
- return isActive;
+
+ // Check if we have active consumers that should prevent GC
+ if (destinationStatistics.getConsumers().getCount() > 0) {
+ // if we have consumers and both gcWithNetwork and gcOnlyWildcard
consumers
+ // are false we can just return true, otherwise we need to check
each consumer
+ return (!isGcWithNetworkConsumers() &&
!isGcWithOnlyWildcardConsumers()) ||
+ hasActiveConsumers();
+ }
+
+ return false;
+ }
+
+ Predicate<Subscription> canGcConsumer = subscription -> {
+ // if isGcWithNetworkConsumers() is true and this is a network
subscription then we can GC
+ boolean canGcNetwork = isGcWithNetworkConsumers() &&
subscription.getConsumerInfo().isNetworkSubscription();
+ // if isGcWithOnlyWildcardConsumers() is true and this is a
non-durable wildcard then we can GC.
+ // An attached durable subscription never permits gc - its
registration and pending messages
+ // live in the destination's store, which gc destroys. Note offline
durable subscriptions
+ // stay attached only with keepDurableSubsActive=true (the default);
brokers running with
+ // keepDurableSubsActive=false forfeit this protection while the
subscriber is offline.
+ return canGcNetwork || (isGcWithOnlyWildcardConsumers() &&
subscription.isWildcard()
+ && !subscription.getConsumerInfo().isDurable());
+ };
+
+ protected boolean hasActiveConsumers() {
+ final List<Subscription> consumers = getConsumers();
+ for (Subscription subscription: consumers) {
+ if (!canGcConsumer.test(subscription)) {
+ return true;
+ }
+ }
+ return false;
}
@Override
@@ -796,19 +828,36 @@ public abstract class BaseDestination implements
Destination {
return gcWithNetworkConsumers;
}
+ /**
+ * Indicate if it is ok to gc destinations that have only wildcard
consumers
+ * @param gcWithOnlyWildcardConsumers
+ */
+ public void setGcWithOnlyWildcardConsumers(boolean
gcWithOnlyWildcardConsumers) {
+ this.gcWithOnlyWildcardConsumers = gcWithOnlyWildcardConsumers;
+ }
+
+ public boolean isGcWithOnlyWildcardConsumers() {
+ return gcWithOnlyWildcardConsumers;
+ }
+
@Override
public void markForGC(long timeStamp) {
- if (isGcIfInactive() && this.lastActiveTime == 0 && isActive() == false
- && destinationStatistics.getMessages().getCount() == 0 &&
getInactiveTimeoutBeforeGC() > 0l) {
+ if (isGcIfInactive()
+ && this.lastActiveTime == 0
+ && destinationStatistics.getMessages().getCount() == 0
+ && getInactiveTimeoutBeforeGC() > 0L
+ && !isActive()) {
this.lastActiveTime = timeStamp;
}
}
@Override
public boolean canGC() {
- boolean result = false;
- final long currentLastActiveTime = this.lastActiveTime;
- if (isGcIfInactive() && currentLastActiveTime != 0l &&
destinationStatistics.getMessages().getCount() == 0L ) {
+ var result = false;
+ final var currentLastActiveTime = this.lastActiveTime;
+ if (isGcIfInactive()
+ && currentLastActiveTime != 0L
+ && destinationStatistics.getMessages().getCount() == 0L) {
if ((System.currentTimeMillis() - currentLastActiveTime) >=
getInactiveTimeoutBeforeGC()) {
result = true;
}
@@ -865,17 +914,6 @@ public abstract class BaseDestination implements
Destination {
@Override
public abstract List<Subscription> getConsumers();
- protected boolean hasRegularConsumers(List<Subscription> consumers) {
- boolean hasRegularConsumers = false;
- for (Subscription subscription: consumers) {
- if (!subscription.getConsumerInfo().isNetworkSubscription()) {
- hasRegularConsumers = true;
- break;
- }
- }
- return hasRegularConsumers;
- }
-
public ConnectionContext createConnectionContext() {
ConnectionContext answer = new ConnectionContext();
answer.setBroker(this.broker);
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/Destination.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/Destination.java
index 22ba14894b..2901fd0e18 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/Destination.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/Destination.java
@@ -267,4 +267,5 @@ public interface Destination extends Service, Task,
Message.MessageDestination {
void setAdvancedMessageStatisticsEnabled(boolean
advancedMessageStatisticsEnabled);
+ boolean isGcWithOnlyWildcardConsumers();
}
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/DestinationFilter.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/DestinationFilter.java
index 154f013d6c..59bcc86877 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/DestinationFilter.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/DestinationFilter.java
@@ -429,6 +429,11 @@ public class DestinationFilter implements Destination {
next.setAdvancedMessageStatisticsEnabled(advancedMessageStatisticsEnabled);
}
+ @Override
+ public boolean isGcWithOnlyWildcardConsumers() {
+ return next.isGcWithOnlyWildcardConsumers();
+ }
+
public void deleteSubscription(ConnectionContext context, SubscriptionKey
key) throws Exception {
if (next instanceof DestinationFilter) {
DestinationFilter filter = (DestinationFilter) next;
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueue.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueue.java
index ebaf331e19..fd3884fca8 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueue.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueue.java
@@ -34,8 +34,7 @@ import org.slf4j.LoggerFactory;
*
*
*/
-public class TempQueue extends Queue implements TempDestination{
-
+public class TempQueue extends Queue implements TempDestination {
private static final Logger LOG = LoggerFactory.getLogger(TempQueue.class);
private final ActiveMQTempDestination tempDest;
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueueRegion.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueueRegion.java
index c6bf732041..97c54b35dd 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueueRegion.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/TempQueueRegion.java
@@ -58,7 +58,7 @@ public class TempQueueRegion extends AbstractTempRegion {
super.removeDestination(context, destination, timeout);
}
-
+
/*
* For a Queue, dispatch order is imperative to match acks, so the
dispatch is deferred till
* the notification to ensure that the subscription chosen by the master
is used.
diff --git
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/policy/PolicyEntry.java
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/policy/PolicyEntry.java
index 235ab7d5dc..373becfbb1 100644
---
a/activemq-broker/src/main/java/org/apache/activemq/broker/region/policy/PolicyEntry.java
+++
b/activemq-broker/src/main/java/org/apache/activemq/broker/region/policy/PolicyEntry.java
@@ -100,6 +100,7 @@ public class PolicyEntry extends DestinationMapEntry {
private boolean prioritizedMessages;
private boolean allConsumersExclusiveByDefault;
private boolean gcInactiveDestinations;
+ private boolean gcWithOnlyWildcardConsumers;
private boolean gcWithNetworkConsumers;
private long inactiveTimeoutBeforeGC =
BaseDestination.DEFAULT_INACTIVE_TIMEOUT_BEFORE_GC;
private boolean reduceMemoryFootprint;
@@ -265,6 +266,9 @@ public class PolicyEntry extends DestinationMapEntry {
if (isUpdate("gcInactiveDestinations", includedProperties)) {
destination.setGcIfInactive(isGcInactiveDestinations());
}
+ if (isUpdate("gcWithOnlyWildcardConsumers", includedProperties)) {
+
destination.setGcWithOnlyWildcardConsumers(isGcWithOnlyWildcardConsumers());
+ }
if (isUpdate("gcWithNetworkConsumers", includedProperties)) {
destination.setGcWithNetworkConsumers(isGcWithNetworkConsumers());
}
@@ -1090,6 +1094,25 @@ public class PolicyEntry extends DestinationMapEntry {
this.inactiveTimeoutBeforeGC = inactiveTimeoutBeforeGC;
}
+ /**
+ * Allow gc of inactive destinations whose only consumers are wildcard
subscriptions,
+ * supporting one-time-use destination patterns where a wildcard consumer
stays
+ * connected across destinations being created, drained and collected.
+ *
+ * An attached durable topic subscription always prevents gc - its
registration and
+ * pending messages live in the destination's store, which gc destroys.
Offline durable
+ * subscriptions remain attached only when the broker runs with
keepDurableSubsActive=true
+ * (the default); with keepDurableSubsActive=false this protection does
not apply while
+ * the subscriber is offline.
+ */
+ public void setGcWithOnlyWildcardConsumers(boolean
gcWithOnlyWildcardConsumers) {
+ this.gcWithOnlyWildcardConsumers = gcWithOnlyWildcardConsumers;
+ }
+
+ public boolean isGcWithOnlyWildcardConsumers() {
+ return gcWithOnlyWildcardConsumers;
+ }
+
public void setGcWithNetworkConsumers(boolean gcWithNetworkConsumers) {
this.gcWithNetworkConsumers = gcWithNetworkConsumers;
}
diff --git
a/activemq-runtime-config/src/test/java/org/apache/activemq/java/JavaPolicyEntryTest.java
b/activemq-runtime-config/src/test/java/org/apache/activemq/java/JavaPolicyEntryTest.java
index bc92505d7e..9c22ec9d98 100644
---
a/activemq-runtime-config/src/test/java/org/apache/activemq/java/JavaPolicyEntryTest.java
+++
b/activemq-runtime-config/src/test/java/org/apache/activemq/java/JavaPolicyEntryTest.java
@@ -662,7 +662,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
//initial config
setAllDestPolicyProperties(entry, true, true, 10,
- 100, 200, 1000, 400, 40, 30, true, true, 1000, true, true,
+ 100, 200, 1000, 400, 40, 30, true, true, true, 1000, true,
true,
30, true, true, true, true, true, true, true, true, true);
setAllQueuePolicyProperties(entry, 10000, true, true, true, true, 100,
100, true, true);
@@ -675,7 +675,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
//validate config
assertAllDestPolicyProperties(getQueue("Before"), true, true, 10,
- 100, 200, 1000, 400, 40, 30, true, true, 1000, true, true,
+ 100, 200, 1000, 400, 40, 30, true, true, true, 1000, true,
true,
30, true, true, true,true, true, true, true, true, true);
assertAllQueuePolicyProperties(getQueue("Before"), 10000, true, true,
true, true, 100,
100, true, true);
@@ -683,7 +683,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
//change config
setAllDestPolicyProperties(entry, false, false, 100,
- 1000, 2000, 10000, 4000, 400, 300, false, false, 1000, false,
false,
+ 1000, 2000, 10000, 4000, 400, 300, false, false, false, 1000,
false, false,
300, false, false, false,false, false, false, false, false,
false);
setAllQueuePolicyProperties(entry, 100000, false, false, false, false,
1000,
1000, false, false);
@@ -692,14 +692,14 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
TimeUnit.SECONDS.sleep(SLEEP);
assertAllDestPolicyProperties(getQueue("Before"), false, false, 100,
- 1000, 2000, 10000, 4000, 400, 300, false, false, 1000, false,
false,
+ 1000, 2000, 10000, 4000, 400, 300, false, false, false, 1000,
false, false,
300, false, false, false,false, false, false, false, false,
false);
assertAllQueuePolicyProperties(getQueue("Before"), 100000, false,
false, false, false, 1000,
1000, false, false);
//check new dest
assertAllDestPolicyProperties(getQueue("After"), false, false, 100,
- 1000, 2000, 10000, 4000, 400, 300, false, false, 1000, false,
false,
+ 1000, 2000, 10000, 4000, 400, 300, false, false, false, 1000,
false, false,
300, false, false, false, false, false, false, false, false,
false);
assertAllQueuePolicyProperties(getQueue("After"), 100000, false,
false, false, false, 1000,
1000, false, false);
@@ -713,7 +713,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
//initial config
setAllDestPolicyProperties(entry, true, true, 10,
- 100, 200, 1000, 400, 40, 30, true, true, 1000, true, true,
+ 100, 200, 1000, 400, 40, 30, true, true, true, 1000, true,
true,
30, true, true, true, true, true, true, true, true, true);
setAllTopicPolicyProperties(entry, 10000, true);
@@ -725,14 +725,14 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
//validate config
assertAllDestPolicyProperties(getTopic("Before"), true, true, 10,
- 100, 200, 1000, 400, 40, 30, true, true, 1000, true, true,
+ 100, 200, 1000, 400, 40, 30, true, true, true, 1000, true,
true,
30, true, true, true, true, true, true, true, true, true);
assertAllTopicPolicyProperties(getTopic("Before"), 10000, true);
//change config
setAllDestPolicyProperties(entry, false, false, 100,
- 1000, 2000, 10000, 4000, 400, 300, false, false, 1000, false,
false,
+ 1000, 2000, 10000, 4000, 400, 300, false, false, false, 1000,
false, false,
300, false, false, false, false, false, false, false, false,
false);
setAllTopicPolicyProperties(entry, 100000, false);
@@ -740,13 +740,13 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
TimeUnit.SECONDS.sleep(SLEEP);
assertAllDestPolicyProperties(getTopic("Before"), false, false, 100,
- 1000, 2000, 10000, 4000, 400, 300, false, false, 1000, false,
false,
+ 1000, 2000, 10000, 4000, 400, 300, false, false, false, 1000,
false, false,
300, false, false, false, false, false, false, false, false,
false);
assertAllTopicPolicyProperties(getTopic("Before"), 100000, false);
//check new dest
assertAllDestPolicyProperties(getTopic("After"), false, false, 100,
- 1000, 2000, 10000, 4000, 400, 300, false, false, 1000, false,
false,
+ 1000, 2000, 10000, 4000, 400, 300, false, false, false, 1000,
false, false,
300, false, false, false, false, false, false, false, false,
false);
assertAllTopicPolicyProperties(getTopic("After"), 100000, false);
}
@@ -820,6 +820,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
properties.add("cursorMemoryHighWaterMark");
properties.add("storeUsageHighWaterMark");
properties.add("gcInactiveDestinations");
+ properties.add("gcWithOnlyWildcardConsumers");
properties.add("gcWithNetworkConsumers");
properties.add("inactiveTimeoutBeforeGC");
properties.add("reduceMemoryFootprint");
@@ -862,12 +863,12 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
private void setAllDestPolicyProperties(PolicyEntry entry, boolean
producerFlowControl,
boolean alwaysRetroactive, long blockedProducerWarningInterval,
int maxPageSize,
int maxBrowsePageSize, long minimumMessageSize, int
maxExpirePageSize, int cursorMemoryHighWaterMark,
- int storeUsageHighWaterMark, boolean gcInactiveDestinations,
boolean gcWithNetworkConsumers,
- long inactiveTimeoutBeforeGC,boolean reduceMemoryFootprint,
boolean doOptimizeMessageStore,
- int optimizeMessageStoreInFlightLimit, boolean
advisoryForConsumed, boolean advisoryForDelivery,
- boolean advisoryForDispatched, boolean
advisoryForDiscardingMessages, boolean advisoryForSlowConsumers,
- boolean advisoryForFastProducers, boolean advisoryWhenFull,
boolean includeBodyForAdvisory,
- boolean sendAdvisoryIfNoConsumers) {
+ int storeUsageHighWaterMark, boolean gcInactiveDestinations,
boolean gcWithOnlyWildcardConsumers,
+ boolean gcWithNetworkConsumers, long inactiveTimeoutBeforeGC,
boolean reduceMemoryFootprint,
+ boolean doOptimizeMessageStore, int
optimizeMessageStoreInFlightLimit, boolean advisoryForConsumed,
+ boolean advisoryForDelivery, boolean advisoryForDispatched,
boolean advisoryForDiscardingMessages,
+ boolean advisoryForSlowConsumers, boolean
advisoryForFastProducers, boolean advisoryWhenFull,
+ boolean includeBodyForAdvisory, boolean sendAdvisoryIfNoConsumers)
{
entry.setProducerFlowControl(producerFlowControl);
entry.setAlwaysRetroactive(alwaysRetroactive);
@@ -879,6 +880,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
entry.setCursorMemoryHighWaterMark(cursorMemoryHighWaterMark);
entry.setStoreUsageHighWaterMark(storeUsageHighWaterMark);
entry.setGcInactiveDestinations(gcInactiveDestinations);
+ entry.setGcWithOnlyWildcardConsumers(gcWithOnlyWildcardConsumers);
entry.setGcWithNetworkConsumers(gcWithNetworkConsumers);
entry.setInactiveTimeoutBeforeGC(inactiveTimeoutBeforeGC);
entry.setReduceMemoryFootprint(reduceMemoryFootprint);
@@ -920,13 +922,12 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
private void assertAllDestPolicyProperties(BaseDestination dest, boolean
producerFlowControl,
boolean alwaysRetroactive, long blockedProducerWarningInterval,
int maxPageSize,
int maxBrowsePageSize, long minimumMessageSize, int
maxExpirePageSize, int cursorMemoryHighWaterMark,
- int storeUsageHighWaterMark, boolean gcInactiveDestinations,
boolean gcWithNetworkConsumers,
- long inactiveTimeoutBeforeGC,boolean reduceMemoryFootprint,
boolean doOptimizeMessageStore,
- int optimizeMessageStoreInFlightLimit, boolean
advisoryForConsumed, boolean advisoryForDelivery,
- boolean advisoryForDispatched, boolean
advisoryForDiscardingMessages, boolean advisoryForSlowConsumers,
- boolean advisoryForFastProducers, boolean advisoryWhenFull,
boolean includeBodyForAdvisory,
- boolean sendAdvisoryIfNoConsumers) {
-
+ int storeUsageHighWaterMark, boolean gcInactiveDestinations,
boolean gcWithOnlyWildcardConsumers,
+ boolean gcWithNetworkConsumers, long inactiveTimeoutBeforeGC,
boolean reduceMemoryFootprint,
+ boolean doOptimizeMessageStore, int
optimizeMessageStoreInFlightLimit, boolean advisoryForConsumed,
+ boolean advisoryForDelivery, boolean advisoryForDispatched,
boolean advisoryForDiscardingMessages,
+ boolean advisoryForSlowConsumers, boolean
advisoryForFastProducers, boolean advisoryWhenFull,
+ boolean includeBodyForAdvisory, boolean sendAdvisoryIfNoConsumers)
{
assertEquals(producerFlowControl, dest.isProducerFlowControl());
assertEquals(alwaysRetroactive, dest.isAlwaysRetroactive());
@@ -938,6 +939,7 @@ public class JavaPolicyEntryTest extends
RuntimeConfigTestSupport {
assertEquals(cursorMemoryHighWaterMark,
dest.getCursorMemoryHighWaterMark());
assertEquals(storeUsageHighWaterMark,
dest.getStoreUsageHighWaterMark());
assertEquals(gcInactiveDestinations, dest.isGcIfInactive());
+ assertEquals(gcWithOnlyWildcardConsumers,
dest.isGcWithOnlyWildcardConsumers());
assertEquals(gcWithNetworkConsumers, dest.isGcWithNetworkConsumers());
assertEquals(inactiveTimeoutBeforeGC,
dest.getInactiveTimeoutBeforeGC());
assertEquals(reduceMemoryFootprint, dest.isReduceMemoryFootprint());
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationCanGcConsumerTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationCanGcConsumerTest.java
new file mode 100644
index 0000000000..12349d0dee
--- /dev/null
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationCanGcConsumerTest.java
@@ -0,0 +1,208 @@
+/**
+ * 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.broker.region;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.command.ActiveMQDestination;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.apache.activemq.command.ConsumerInfo;
+import org.apache.activemq.test.annotations.ParallelTest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * [AMQ-9692] Direct coverage of the package-private
BaseDestination.canGcConsumer
+ * predicate, which decides whether a single subscription may be ignored by
+ * destination gc:
+ *
+ * (gcWithNetworkConsumers AND network) OR
+ * (gcWithOnlyWildcardConsumers AND wildcard AND NOT durable)
+ *
+ * The table covers every combination of the two policy flags and the three
+ * subscription traits. Notable rows it pins:
+ * - a durable subscription is never gc-eligible via the wildcard branch,
+ * regardless of flags
+ * - the network branch predates the durable exclusion and intentionally does
+ * not consult isDurable() - a durable network (bridge demand) subscription
+ * remains gc-eligible under gcWithNetworkConsumers, as before this feature
+ *
+ * gcNC = gcWithNetworkConsumers, gcWC = gcWithOnlyWildcardConsumers
+ * net = network subscription, wild = wildcard, dur = durable
+ */
+@Category(ParallelTest.class)
+@RunWith(Parameterized.class)
+public class DestinationCanGcConsumerTest {
+
+ private static BrokerService brokerService;
+ private static BaseDestination baseDestination;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ brokerService = new BrokerService();
+ brokerService.setPersistent(false);
+ brokerService.setUseJmx(false);
+ brokerService.start();
+ brokerService.waitUntilStarted();
+
+ var queue = new ActiveMQQueue("amq.gc.canGcConsumer");
+ brokerService.getBroker().addDestination(
+ brokerService.getAdminConnectionContext(), queue, false);
+ baseDestination = (BaseDestination)
brokerService.getDestination(queue);
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ if (brokerService != null) {
+ brokerService.stop();
+ brokerService.waitUntilStopped();
+ }
+ }
+
+ @Parameterized.Parameters(name = "gcNC={0} gcWC={1} net={2} wild={3}
dur={4} exp={5}")
+ public static Collection<Object[]> data() {
+ var truthTable = new Object[][] {
+ // Plain app subscription - never gc-eligible
+ { false, false, false, false, false, false },
+ { false, true, false, false, false, false },
+ { true, false, false, false, false, false },
+ { true, true, false, false, false, false },
+
+ // Network subscription - gc-eligible iff gcNC
+ { false, false, true, false, false, false },
+ { false, true, true, false, false, false },
+ { true, false, true, false, false, true },
+ { true, true, true, false, false, true },
+
+ // Wildcard subscription - gc-eligible iff gcWC
+ { false, false, false, true, false, false },
+ { false, true, false, true, false, true },
+ { true, false, false, true, false, false },
+ { true, true, false, true, false, true },
+
+ // Durable (non-wildcard) subscription - never gc-eligible
+ { false, false, false, false, true, false },
+ { false, true, false, false, true, false },
+ { true, false, false, false, true, false },
+ { true, true, false, false, true, false },
+
+ // Durable wildcard subscription - the durable exclusion blocks
+ // the wildcard branch under every flag combination
+ { false, false, false, true, true, false },
+ { false, true, false, true, true, false },
+ { true, false, false, true, true, false },
+ { true, true, false, true, true, false },
+
+ // Network + wildcard - eligible via either enabled branch
+ { false, false, true, true, false, false },
+ { false, true, true, true, false, true },
+ { true, false, true, true, false, true },
+ { true, true, true, true, false, true },
+
+ // Network + durable - the network branch does not consult
+ // isDurable() (pre-existing gcWithNetworkConsumers semantics)
+ { false, false, true, false, true, false },
+ { false, true, true, false, true, false },
+ { true, false, true, false, true, true },
+ { true, true, true, false, true, true },
+
+ // Network + durable wildcard - eligible only via the network
+ // branch; the wildcard branch stays blocked by the durable
+ { false, false, true, true, true, false },
+ { false, true, true, true, true, false },
+ { true, false, true, true, true, true },
+ { true, true, true, true, true, true }
+ };
+ var params = new ArrayList<Object[]>();
+ for (var row : truthTable) {
+ params.add(row);
+ }
+ return params;
+ }
+
+ private final boolean gcWithNetworkConsumersEnabled;
+ private final boolean gcWithOnlyWildcardConsumersEnabled;
+ private final boolean networkSubscription;
+ private final boolean wildcardSubscription;
+ private final boolean durableSubscription;
+ private final boolean canGcExpected;
+
+ public DestinationCanGcConsumerTest(boolean gcWithNetworkConsumersEnabled,
boolean gcWithOnlyWildcardConsumersEnabled, boolean networkSubscription,
boolean wildcardSubscription, boolean durableSubscription, boolean
canGcExpected) {
+ this.gcWithNetworkConsumersEnabled = gcWithNetworkConsumersEnabled;
+ this.gcWithOnlyWildcardConsumersEnabled =
gcWithOnlyWildcardConsumersEnabled;
+ this.networkSubscription = networkSubscription;
+ this.wildcardSubscription = wildcardSubscription;
+ this.durableSubscription = durableSubscription;
+ this.canGcExpected = canGcExpected;
+ }
+
+ @Test
+ public void testCanGcConsumer() throws Exception {
+
baseDestination.setGcWithNetworkConsumers(gcWithNetworkConsumersEnabled);
+
baseDestination.setGcWithOnlyWildcardConsumers(gcWithOnlyWildcardConsumersEnabled);
+
+ var subscription = new
MockSubscription(baseDestination.getActiveMQDestination(),
+ networkSubscription, wildcardSubscription,
durableSubscription);
+
+ assertEquals(canGcExpected,
baseDestination.canGcConsumer.test(subscription));
+ }
+
+ static class MockConsumerInfo extends ConsumerInfo {
+
+ private final boolean networkSubscription;
+ private final boolean durableSubscription;
+
+ public MockConsumerInfo(ActiveMQDestination activemqDestination,
boolean networkSubscription, boolean durableSubscription) {
+ setDestination(activemqDestination);
+ this.networkSubscription = networkSubscription;
+ this.durableSubscription = durableSubscription;
+ }
+
+ @Override
+ public boolean isNetworkSubscription() {
+ return this.networkSubscription;
+ }
+
+ @Override
+ public boolean isDurable() {
+ return this.durableSubscription;
+ }
+ }
+
+ static class MockSubscription extends QueueSubscription {
+
+ private final boolean wildcardSubscription;
+
+ public MockSubscription(ActiveMQDestination activemqDestination,
boolean networkSubscription, boolean wildcardSubscription, boolean
durableSubscription) throws Exception {
+ super(brokerService.getBroker(), null, null, new
MockConsumerInfo(activemqDestination, networkSubscription,
durableSubscription));
+ this.wildcardSubscription = wildcardSubscription;
+ }
+
+ @Override
+ public boolean isWildcard() {
+ return this.wildcardSubscription;
+ }
+ }
+}
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCTest.java
index 9e32578ccc..871cd0b39f 100644
---
a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCTest.java
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCTest.java
@@ -18,13 +18,11 @@ package org.apache.activemq.broker.region;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
-import jakarta.jms.Connection;
-import jakarta.jms.Message;
-import jakarta.jms.MessageConsumer;
-import jakarta.jms.MessageListener;
-import jakarta.jms.MessageProducer;
+import jakarta.jms.JMSException;
import jakarta.jms.Session;
import org.apache.activemq.ActiveMQConnectionFactory;
@@ -42,6 +40,8 @@ import org.slf4j.LoggerFactory;
import org.apache.activemq.test.annotations.ParallelTest;
import org.junit.experimental.categories.Category;
+import java.util.concurrent.atomic.AtomicInteger;
+
@Category(ParallelTest.class)
public class DestinationGCTest {
@@ -49,6 +49,8 @@ public class DestinationGCTest {
private final ActiveMQQueue queue = new ActiveMQQueue("TEST");
private final ActiveMQQueue otherQueue = new ActiveMQQueue("TEST-OTHER");
+ private final ActiveMQQueue wildcardQueueA = new
ActiveMQQueue("TEST.FOO.A");
+ private final ActiveMQQueue wildcardQueueB = new
ActiveMQQueue("TEST.FOO.B");
private BrokerService brokerService;
@@ -68,13 +70,30 @@ public class DestinationGCTest {
}
protected BrokerService createBroker() throws Exception {
- PolicyEntry entry = new PolicyEntry();
+ var entry = new PolicyEntry();
entry.setGcInactiveDestinations(true);
+ entry.setGcWithOnlyWildcardConsumers(true);
entry.setInactiveTimeoutBeforeGC(3000);
- PolicyMap map = new PolicyMap();
+ var map = new PolicyMap();
map.setDefaultEntry(entry);
- BrokerService broker = new BrokerService();
+ // GUARD.> queues allow wildcard-only removal but are excluded from the
+ // gc sweep, so the removeDestination guard tests are not raced by gc
+ var guardEntry = new PolicyEntry();
+ guardEntry.setQueue("GUARD.>");
+ guardEntry.setGcInactiveDestinations(false);
+ guardEntry.setGcWithOnlyWildcardConsumers(true);
+ map.put(new ActiveMQQueue("GUARD.>"), guardEntry);
+
+ // NOGC.> queues have the wildcard flag off - wildcard consumers keep
+ // these destinations active
+ var noGcEntry = new PolicyEntry();
+ noGcEntry.setQueue("NOGC.>");
+ noGcEntry.setGcInactiveDestinations(false);
+ noGcEntry.setGcWithOnlyWildcardConsumers(false);
+ map.put(new ActiveMQQueue("NOGC.>"), noGcEntry);
+
+ var broker = new BrokerService();
broker.setPersistent(false);
broker.setUseJmx(true);
broker.setDestinations(new ActiveMQDestination[] {queue});
@@ -89,19 +108,204 @@ public class DestinationGCTest {
public void testDestinationGCWithActiveConsumers() throws Exception {
assertEquals(1, brokerService.getAdminView().getQueues().length);
- ActiveMQConnectionFactory factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
- Connection connection = factory.createConnection();
- Session session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
- session.createProducer(otherQueue).close();
- MessageConsumer consumer = session.createConsumer(queue);
- consumer.setMessageListener(message -> {});
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var consumer = session.createConsumer(queue)) {
+
+ session.createProducer(otherQueue).close();
+ consumer.setMessageListener(message -> {});
+
+ connection.start();
+
+ assertTrue("After GC runs there should be one Queue.",
+ Wait.waitFor(() ->
brokerService.getAdminView().getQueues().length == 1));
+ }
+ }
+
+ @Test
+ public void testDestinationGCWithOnlyWildcardConsumers() throws Exception {
+ assertEquals(1, brokerService.getAdminView().getQueues().length);
+
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+
+ final var receivedCount = new AtomicInteger(0);
+
+ // Anonymous producer - does not register on the destinations, so the
gc
+ // assertions exercise only the wildcard consumer
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(null)) {
+
+ producer.send(wildcardQueueA, session.createTextMessage("Test
first step queueA"));
+ producer.send(wildcardQueueB, session.createTextMessage("Test
first step queueB"));
+
+ var consumer =
session.createConsumer(session.createQueue("TEST.FOO.*"));
+ consumer.setMessageListener(message ->
receivedCount.incrementAndGet());
+
+ connection.start();
- connection.start();
+ // Confirm queues are gc'd
+ assertTrue("After GC runs there should be one Queue (count=" +
brokerService.getAdminView().getQueues().length + ")",
+ Wait.waitFor(() ->
brokerService.getAdminView().getQueues().length == 1, 30000, 1000));
- assertTrue("After GC runs there should be one Queue.",
- Wait.waitFor(() -> brokerService.getAdminView().getQueues().length
== 1));
+ assertEquals(Integer.valueOf(2),
Integer.valueOf(receivedCount.get()));
- connection.close();
+ // Confirm wild-card consumer is able to stay active after zero
matching destinations
+ producer.send(wildcardQueueA, session.createTextMessage("Test
second step queueA"));
+
+ // Confirm queues are gc'd
+ assertTrue("After GC runs there should be one Queue (count=" +
brokerService.getAdminView().getQueues().length + ")",
+ Wait.waitFor(() ->
brokerService.getAdminView().getQueues().length == 1, 30000, 1000));
+ assertEquals(Integer.valueOf(3),
Integer.valueOf(receivedCount.get()));
+ }
+ }
+
+ private int countMatchingTopics(String prefix) throws Exception {
+ var count = 0;
+ for (var name : brokerService.getAdminView().getTopics()) {
+ var destinationName = name.getKeyProperty("destinationName");
+ if (destinationName != null && destinationName.startsWith(prefix))
{
+ count++;
+ }
+ }
+ return count;
+ }
+
+ // [AMQ-9692] non-durable topic flavor of the wildcard-consumer gc
lifecycle:
+ // topics are gc'd while the wildcard consumer stays connected, and
delivery
+ // continues when a producer recreates them
+ @Test(timeout = 60000)
+ public void testTopicDestinationGCWithOnlyWildcardConsumers() throws
Exception {
+ final var receivedCount = new AtomicInteger(0);
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+
+ // Subscribe before sending - non-durable topics do not retain
messages.
+ // Anonymous producer - does not register on the destinations
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var consumer =
session.createConsumer(session.createTopic("TEST.BAR.*"));
+ var producer = session.createProducer(null)) {
+
+ consumer.setMessageListener(message ->
receivedCount.incrementAndGet());
+ connection.start();
+
+ producer.send(session.createTopic("TEST.BAR.A"),
session.createTextMessage("first-a"));
+ producer.send(session.createTopic("TEST.BAR.B"),
session.createTextMessage("first-b"));
+
+ assertTrue("Wildcard topic consumer should receive both messages",
+ Wait.waitFor(() -> receivedCount.get() == 2));
+
+ // Both topics should gc while the wildcard consumer stays
connected
+ assertTrue("After GC runs there should be no TEST.BAR. topics",
+ Wait.waitFor(() -> countMatchingTopics("TEST.BAR.") == 0,
30000, 1000));
+
+ // A new send recreates the topic and the consumer must still
receive
+ producer.send(session.createTopic("TEST.BAR.A"),
session.createTextMessage("second-a"));
+ assertTrue("Wildcard topic consumer should receive after gc +
recreate",
+ Wait.waitFor(() -> receivedCount.get() == 3));
+
+ assertTrue("Recreated topic should gc again",
+ Wait.waitFor(() -> countMatchingTopics("TEST.BAR.") == 0,
30000, 1000));
+ }
+ }
+
+ // [AMQ-9692] removeDestination(timeout=0) guard: an app consumer must
block removal
+ @Test(timeout = 60000)
+ public void testRemoveDestinationWithAppConsumerThrows() throws Exception {
+ var guardQueue = new ActiveMQQueue("GUARD.APP");
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var consumer = session.createConsumer(guardQueue)) {
+
+ connection.start();
+
+ try {
+
brokerService.getBroker().removeDestination(brokerService.getAdminConnectionContext(),
guardQueue, 0);
+ fail("Expected JMSException removing a destination with an
active app consumer");
+ } catch (JMSException expected) {
+ assertTrue(expected.getMessage().contains("still has an active
subscription"));
+ }
+ }
+ }
+
+ // [AMQ-9692] removeDestination(timeout=0) guard: a wildcard-only consumer
permits
+ // removal when gcWithOnlyWildcardConsumers is enabled, and keeps working
afterwards
+ @Test(timeout = 60000)
+ public void testRemoveDestinationWithOnlyWildcardConsumerSucceeds() throws
Exception {
+ var guardQueue = new ActiveMQQueue("GUARD.WILD.A");
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ // Anonymous producer - does not register on the destination
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var consumer =
session.createConsumer(session.createQueue("GUARD.WILD.*"));
+ var producer = session.createProducer(null)) {
+
+ connection.start();
+
+ producer.send(guardQueue,
session.createTextMessage("before-remove"));
+ assertNotNull("Wildcard consumer should receive",
consumer.receive(5000));
+
+ // Only the wildcard consumer remains - removal is allowed
+
brokerService.getBroker().removeDestination(brokerService.getAdminConnectionContext(),
guardQueue, 0);
+
+ // The wildcard consumer stays connected - a new send recreates
the queue
+ producer.send(guardQueue,
session.createTextMessage("after-remove"));
+ assertNotNull("Wildcard consumer should receive after remove +
recreate", consumer.receive(5000));
+ }
+ }
+
+ // [AMQ-9692] removeDestination(timeout=0) guard: with
gcWithOnlyWildcardConsumers
+ // disabled a wildcard consumer blocks removal (default behavior unchanged)
+ @Test(timeout = 60000)
+ public void testRemoveDestinationWithWildcardConsumerFlagOffThrows()
throws Exception {
+ var noGcQueue = new ActiveMQQueue("NOGC.WILD.A");
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var consumer =
session.createConsumer(session.createQueue("NOGC.WILD.*"));
+ var producer = session.createProducer(null)) {
+
+ connection.start();
+
+ producer.send(noGcQueue, session.createTextMessage("test"));
+ assertNotNull("Wildcard consumer should receive",
consumer.receive(5000));
+
+ try {
+
brokerService.getBroker().removeDestination(brokerService.getAdminConnectionContext(),
noGcQueue, 0);
+ fail("Expected JMSException removing a wildcard-consumed
destination with the flag off");
+ } catch (JMSException expected) {
+ assertTrue(expected.getMessage().contains("still has an active
subscription"));
+ }
+ }
+ }
+
+ // [AMQ-9692] removeDestination(timeout=0) guard: removing a nonexistent
destination
+ // is a silent no-op even when a wildcard subscription matches the name.
RegionBroker
+ // filters nonexistent destinations before delegating, so this contract is
only
+ // reachable at the region level - call the queue region directly.
+ @Test(timeout = 60000)
+ public void testRemoveNonExistentDestinationMatchingWildcardIsNoOp()
throws Exception {
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var consumer =
session.createConsumer(session.createQueue("GUARD.MISSING.*"));
+ var producer = session.createProducer(null)) {
+
+ connection.start();
+
+ var queueCountBefore =
brokerService.getAdminView().getQueues().length;
+ var queueRegion = ((RegionBroker)
brokerService.getRegionBroker()).getQueueRegion();
+
queueRegion.removeDestination(brokerService.getAdminConnectionContext(),
+ new ActiveMQQueue("GUARD.MISSING.A"), 0);
+
+ assertEquals(queueCountBefore,
brokerService.getAdminView().getQueues().length);
+
+ // The wildcard subscription is unaffected by the no-op removal
+ producer.send(new ActiveMQQueue("GUARD.MISSING.A"),
session.createTextMessage("after-noop"));
+ assertNotNull("Wildcard consumer should still receive",
consumer.receive(5000));
+ }
}
@Test(timeout = 60000)
@@ -125,7 +329,7 @@ public class DestinationGCTest {
// but not all (verifying the sweep limit works)
assertTrue("GC should have removed some but not all queues",
Wait.waitFor(() -> {
- final int count =
brokerService.getAdminView().getQueues().length;
+ final var count =
brokerService.getAdminView().getQueues().length;
return count > 0 && count < 5;
}, 15000, 500));
@@ -137,27 +341,25 @@ public class DestinationGCTest {
@Test(timeout = 60000)
public void testDestinationGcAnonymousProducer() throws Exception {
- final ActiveMQQueue q = new ActiveMQQueue("Q.TEST.ANONYMOUS.PRODUCER");
+ final var q = new ActiveMQQueue("Q.TEST.ANONYMOUS.PRODUCER");
brokerService.getAdminView().addQueue(q.getPhysicalName());
assertEquals(2, brokerService.getAdminView().getQueues().length);
- final ActiveMQConnectionFactory factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
- final Connection connection = factory.createConnection();
- final Session session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
-
- // wait for the queue to be marked for GC
- logger.info("Waiting for '{}' to be marked for GC...", q);
- Wait.waitFor(() -> brokerService.getDestination(q).canGC(),
Wait.MAX_WAIT_MILLIS, 500L);
+ final var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ try (var connection = factory.createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(null)) {
- // create anonymous producer and send a message
- logger.info("Sending PERSISTENT message to QUEUE '{}'",
q.getPhysicalName());
- final MessageProducer producer = session.createProducer(null);
- producer.send(q, session.createTextMessage());
- producer.close();
+ // wait for the queue to be marked for GC
+ logger.info("Waiting for '{}' to be marked for GC...", q);
+ Wait.waitFor(() -> brokerService.getDestination(q).canGC(),
Wait.MAX_WAIT_MILLIS, 500L);
- assertFalse(brokerService.getDestination(q).canGC());
+ // send a message via the anonymous producer
+ logger.info("Sending PERSISTENT message to QUEUE '{}'",
q.getPhysicalName());
+ producer.send(q, session.createTextMessage());
- connection.close();
+ assertFalse(brokerService.getDestination(q).canGC());
+ }
}
}
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCWildcardDurableSubTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCWildcardDurableSubTest.java
new file mode 100644
index 0000000000..c89ece2537
--- /dev/null
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationGCWildcardDurableSubTest.java
@@ -0,0 +1,363 @@
+/**
+ * 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.broker.region;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import jakarta.jms.Connection;
+import jakarta.jms.Session;
+import jakarta.jms.TextMessage;
+
+import org.apache.activemq.ActiveMQConnectionFactory;
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.broker.region.policy.PolicyEntry;
+import org.apache.activemq.broker.region.policy.PolicyMap;
+import org.apache.activemq.command.ActiveMQTopic;
+import org.apache.activemq.store.TopicMessageStore;
+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;
+
+/**
+ * [AMQ-9692] Verify destination GC behavior for topics whose only
+ * subscription is a durable *wildcard* subscriber.
+ *
+ * Durable subscription state (registration + pending messages) is kept in the
+ * per-topic message store, and TopicRegion.addSubscriptionsForDestination()
+ * recovers durable subs from that store when a destination is (re)created.
+ * Destination GC destroys the store, so these tests probe whether the
+ * durable-subscription guarantees survive the gc + recreate cycle.
+ */
+@Category(ParallelTest.class)
+public class DestinationGCWildcardDurableSubTest {
+
+ private static final String CLIENT_ID = "durable-wildcard-client";
+ private static final String SUB_NAME = "durable-wildcard-sub";
+ private static final ActiveMQTopic WILDCARD_TOPIC = new
ActiveMQTopic("TEST.DUR.>");
+ private static final ActiveMQTopic TOPIC_A = new
ActiveMQTopic("TEST.DUR.A");
+
+ private BrokerService brokerService;
+
+ @Before
+ public void setUp() throws Exception {
+ brokerService = createBroker();
+ brokerService.start();
+ brokerService.waitUntilStarted();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (brokerService != null) {
+ brokerService.stop();
+ brokerService.waitUntilStopped();
+ }
+ }
+
+ protected BrokerService createBroker() throws Exception {
+ return createBroker(true);
+ }
+
+ protected BrokerService createBroker(boolean deleteAllMessagesOnStartup)
throws Exception {
+ return createBroker(deleteAllMessagesOnStartup, true);
+ }
+
+ protected BrokerService createBroker(boolean deleteAllMessagesOnStartup,
boolean gcWithOnlyWildcardConsumers) throws Exception {
+ var entry = new PolicyEntry();
+ entry.setGcInactiveDestinations(true);
+ entry.setGcWithOnlyWildcardConsumers(gcWithOnlyWildcardConsumers);
+ entry.setInactiveTimeoutBeforeGC(1000);
+ var map = new PolicyMap();
+ map.setDefaultEntry(entry);
+
+ var broker = new BrokerService();
+ // Persistent so durable subscription state goes through a real store
+ broker.setPersistent(true);
+
broker.setDataDirectory("target/activemq-data/DestinationGCWildcardDurableSubTest");
+ broker.setDeleteAllMessagesOnStartup(deleteAllMessagesOnStartup);
+ broker.setUseJmx(true);
+ broker.setSchedulePeriodForDestinationPurge(500);
+ broker.setDestinationPolicy(map);
+ return broker;
+ }
+
+ private Connection createConnection() throws Exception {
+ var factory = new
ActiveMQConnectionFactory("vm://localhost?create=false");
+ var connection = factory.createConnection();
+ connection.setClientID(CLIENT_ID);
+ connection.start();
+ return connection;
+ }
+
+ private int countMatchingTopics() throws Exception {
+ var count = 0;
+ for (var name : brokerService.getAdminView().getTopics()) {
+ var destinationName = name.getKeyProperty("destinationName");
+ if (destinationName != null &&
destinationName.startsWith("TEST.DUR.")) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Asserts the topic survives several gc sweep periods. The sweep runs
every
+ * 500ms with a 1000ms inactive timeout, so 4s covers multiple full
+ * mark-and-collect cycles.
+ */
+ private void assertTopicNotGcd() throws Exception {
+ Thread.sleep(4000);
+ assertEquals("Topic with a durable subscription must not be gc'd", 1,
countMatchingTopics());
+ }
+
+ /**
+ * A durable subscription's registration and pending messages live in the
+ * topic's store, which gc destroys. An active durable wildcard subscriber
+ * must therefore prevent the matched topic from being gc'd, and delivery
+ * must continue working.
+ */
+ @Test(timeout = 60000)
+ public void testActiveDurableWildcardSubPreventsTopicGc() throws Exception
{
+ // Anonymous producer - does not register on the destination, so the gc
+ // assertions exercise only the durable consumer
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME);
+ var producer = session.createProducer(null)) {
+
+ producer.send(TOPIC_A, session.createTextMessage("first"));
+
+ var received = durableSubscriber.receive(5000);
+ assertNotNull("Durable wildcard sub should receive first message",
received);
+ assertEquals("first", ((TextMessage) received).getText());
+
+ // Topic is drained but has a durable wildcard consumer - must not
gc
+ assertTopicNotGcd();
+
+ producer.send(TOPIC_A, session.createTextMessage("second"));
+
+ received = durableSubscriber.receive(5000);
+ assertNotNull("Durable wildcard sub should continue receiving
messages", received);
+ assertEquals("second", ((TextMessage) received).getText());
+
+ durableSubscriber.close();
+ session.unsubscribe(SUB_NAME);
+ }
+ }
+
+ /**
+ * A topic holding a message pending for an offline durable wildcard
+ * subscriber must NOT be gc'd - the pending message keeps the destination
+ * message count non-zero.
+ */
+ @Test(timeout = 60000)
+ public void testOfflineDurableWildcardSubWithPendingMessageNotGcd() throws
Exception {
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(TOPIC_A)) {
+
+ session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME).close();
+
+ // Subscriber offline (closed); send a message it should receive
later
+ producer.send(session.createTextMessage("pending"));
+ }
+
+ // Give the gc sweep several periods to (incorrectly) collect the topic
+ assertTopicNotGcd();
+
+ // Reconnect and confirm the pending message is delivered
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME)) {
+
+ var received = durableSubscriber.receive(5000);
+ assertNotNull("Offline durable sub should receive pending message
on reconnect", received);
+ assertEquals("pending", ((TextMessage) received).getText());
+
+ durableSubscriber.close();
+ session.unsubscribe(SUB_NAME);
+ }
+ }
+
+ /**
+ * Durability across broker restart: the durable wildcard sub's
registration is
+ * recovered from the topic's store at startup as an INACTIVE subscription
-
+ * counted on the destination, but not present in its consumers list. The
+ * recovered topic must not be gc'd, and a message sent before the
subscriber
+ * reconnects must be delivered.
+ */
+ @Test(timeout = 60000)
+ public void testOfflineDurableWildcardSubSurvivesBrokerRestart() throws
Exception {
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME);
+ var producer = session.createProducer(TOPIC_A)) {
+
+ producer.send(session.createTextMessage("before-restart"));
+ assertNotNull(durableSubscriber.receive(5000));
+ }
+
+ // Restart without wiping the store - the topic is recovered with an
+ // inactive durable subscription
+ brokerService.stop();
+ brokerService.waitUntilStopped();
+ brokerService = createBroker(false);
+ brokerService.start();
+ brokerService.waitUntilStarted();
+
+ // The recovered topic holds the durable registration - must not gc
+ assertTopicNotGcd();
+
+ // Send while the durable sub is still offline
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(TOPIC_A)) {
+
producer.send(session.createTextMessage("while-offline-after-restart"));
+ }
+
+ // Reconnect - durability requires the message survive the restart +
gc sweeps
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME)) {
+
+ var received = durableSubscriber.receive(5000);
+ assertNotNull("Message sent while durable sub was offline across a
restart must be delivered", received);
+ assertEquals("while-offline-after-restart", ((TextMessage)
received).getText());
+
+ durableSubscriber.close();
+ session.unsubscribe(SUB_NAME);
+ }
+ }
+
+ /**
+ * Pre-change behavior: with gcWithOnlyWildcardConsumers disabled (the
default),
+ * a durable wildcard subscriber keeps its matched topics from being gc'd -
+ * active or offline - and durability holds. Once the subscription is
removed
+ * entirely, gcInactiveDestinations collects the abandoned topic as before.
+ * Also documents the registration model: the durable sub is persisted in
each
+ * concrete matching topic's store, recorded under its wildcard
destination -
+ * no destination or store exists for the wildcard name itself.
+ */
+ @Test(timeout = 60000)
+ public void testDurableWildcardSubWithGcWildcardDisabled() throws
Exception {
+ // Swap in a broker with the wildcard gc flag OFF (pre-change /
default config)
+ brokerService.stop();
+ brokerService.waitUntilStopped();
+ brokerService = createBroker(true, false);
+ brokerService.start();
+ brokerService.waitUntilStarted();
+
+ // Anonymous producer - does not register on the destination, so the gc
+ // assertions exercise only the durable consumer
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME);
+ var producer = session.createProducer(null)) {
+
+ producer.send(TOPIC_A, session.createTextMessage("first"));
+ assertNotNull(durableSubscriber.receive(5000));
+
+ // The durable sub is registered in the concrete topic's store
under its
+ // wildcard subscribed destination
+ var store = (TopicMessageStore)
brokerService.getDestination(TOPIC_A).getMessageStore();
+ var subscriptions = store.getAllSubscriptions();
+ assertEquals(1, subscriptions.length);
+ assertEquals(WILDCARD_TOPIC,
subscriptions[0].getSubscribedDestination());
+
+ // Active durable wildcard sub - topic must not gc with the flag
off
+ assertTopicNotGcd();
+ }
+
+ // Offline durable wildcard sub - topic must still not gc
+ assertTopicNotGcd();
+
+ // Send while the durable sub is offline
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(TOPIC_A)) {
+ producer.send(session.createTextMessage("while-offline"));
+ }
+
+ // Reconnect - durability held
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME)) {
+
+ var received = durableSubscriber.receive(5000);
+ assertNotNull("Message sent while durable sub was offline must be
delivered", received);
+ assertEquals("while-offline", ((TextMessage) received).getText());
+
+ // Remove the subscription entirely - the topic is now truly
abandoned
+ durableSubscriber.close();
+ session.unsubscribe(SUB_NAME);
+ }
+
+ // With no durable registration left, gcInactiveDestinations collects
the topic
+ assertTrue("Abandoned topic should be gc'd once the durable
subscription is removed",
+ Wait.waitFor(new Wait.Condition() {
+ @Override
+ public boolean isSatisified() throws Exception {
+ return countMatchingTopics() == 0;
+ }
+ }, 15000, 500));
+ }
+
+ /**
+ * Durability guarantee: durable wildcard sub goes offline with the topic
+ * fully drained. The empty topic must NOT be gc'd (its store holds the
+ * durable registration), and a message sent while the subscriber is
offline
+ * MUST be delivered when it reconnects.
+ */
+ @Test(timeout = 60000)
+ public void
testOfflineDurableWildcardSubEmptyTopicNotGcdAndDeliversOnReconnect() throws
Exception {
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME);
+ var producer = session.createProducer(TOPIC_A)) {
+
+ producer.send(session.createTextMessage("before-offline"));
+ assertNotNull(durableSubscriber.receive(5000));
+ }
+
+ // Topic is drained and the durable wildcard sub is offline - must not
gc
+ assertTopicNotGcd();
+
+ // Send a message while the durable sub is offline
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var producer = session.createProducer(TOPIC_A)) {
+ producer.send(session.createTextMessage("while-offline"));
+ }
+
+ // Reconnect the durable subscriber - durability requires the message
arrive
+ try (var connection = createConnection();
+ var session = connection.createSession(false,
Session.AUTO_ACKNOWLEDGE);
+ var durableSubscriber =
session.createDurableSubscriber(WILDCARD_TOPIC, SUB_NAME)) {
+
+ var received = durableSubscriber.receive(5000);
+ assertNotNull("Message sent while durable sub was offline must be
delivered on reconnect", received);
+ assertEquals("while-offline", ((TextMessage) received).getText());
+
+ durableSubscriber.close();
+ session.unsubscribe(SUB_NAME);
+ }
+ }
+}
diff --git
a/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationIsActiveTest.java
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationIsActiveTest.java
new file mode 100644
index 0000000000..1904988f9b
--- /dev/null
+++
b/activemq-unit-tests/src/test/java/org/apache/activemq/broker/region/DestinationIsActiveTest.java
@@ -0,0 +1,344 @@
+/**
+ * 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.broker.region;
+
+import static org.junit.Assert.*;
+
+import org.apache.activemq.broker.BrokerService;
+import org.apache.activemq.broker.ConnectionContext;
+import org.apache.activemq.broker.region.policy.PolicyEntry;
+import org.apache.activemq.broker.region.policy.PolicyMap;
+import org.apache.activemq.command.ActiveMQDestination;
+import org.apache.activemq.command.ActiveMQQueue;
+import org.apache.activemq.command.ActiveMQTopic;
+import org.apache.activemq.command.ConnectionId;
+import org.apache.activemq.command.ConsumerId;
+import org.apache.activemq.command.ConsumerInfo;
+import org.apache.activemq.command.ProducerInfo;
+import org.apache.activemq.command.SessionId;
+import org.apache.activemq.test.annotations.ParallelTest;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * [AMQ-9692] Support garbage collecting destinations
+ * that have a wildcard-only subscription.
+ *
+ * This test suite confirms the logic in the
+ * BaseDestination.isActive() method to ensure
+ * destinations are not accidentally deleted due
+ * to incorrect logic combination of PolicyEntr
+ * config flag and status of current subscriptions.
+ *
+ * Every row runs against both a queue and a (non-durable) topic destination -
+ * the isActive() contract is destination-type agnostic. Note the durable rows
+ * exercise different paths per type: on a queue the durable sub appears in the
+ * consumers list (predicate path); on a topic an inactive durable sub is
+ * counted but not listed (count/list mismatch path).
+ *
+ * prod = attached producer
+ * appC = normal application consumer
+ * netC = network consumer
+ * wildC = wildcard consumer (non-durable)
+ * durWildC = durable wildcard consumer
+ */
+@Category(ParallelTest.class)
+@RunWith(Parameterized.class)
+public class DestinationIsActiveTest {
+
+ private static BrokerService brokerService;
+ private static final AtomicInteger counter = new AtomicInteger(0);
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ brokerService = createBroker();
+ brokerService.start();
+ brokerService.waitUntilStarted();
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ if (brokerService != null) {
+ brokerService.stop();
+ brokerService.waitUntilStopped();
+ }
+ }
+
+ @Parameterized.Parameters(name = "dest={0} gcNC={1} gcWC={2} prod={3}
appC={4} netC={5} wildC={6} durWildC={7} exp={8}") // Optional name attribute
for better test reporting
+ public static Collection<Object[]> data() {
+ // The truth table is destination-type agnostic - run every row against
+ // both a queue and a topic destination
+ var truthTable = new Object[][] {
+ // Simple app consumer
+ { false, false, false, false, false, false, false, false },
+ { false, true, false, false, false, false, false, false },
+ { true, false, false, false, false, false, false, false },
+ { true, true, false, false, false, false, false, false },
+ { false, false, false, true, false, false, false, true },
+ { false, true, false, true, false, false, false, true },
+ { true, false, false, true, false, false, false, true },
+ { true, true, false, true, false, false, false, true },
+
+ // Network consumer
+ { false, false, false, false, true, false, false, true },
+ { false, true, false, false, true, false, false, true },
+ { true, false, false, false, true, false, false, false },
+ { true, true, false, false, true, false, false, false },
+ { false, false, false, true, true, false, false, true },
+ { false, true, false, true, true, false, false, true },
+ { true, false, false, true, true, false, false, true },
+ { true, true, false, true, true, false, false, true },
+
+ // Wildcard consumer
+ { false, false, false, false, false, true, false, true },
+ { false, true, false, false, false, true, false, false },
+ { true, false, false, false, false, true, false, true },
+ { true, true, false, false, false, true, false, false },
+ { false, false, false, true, false, true, false, true },
+ { false, true, false, true, false, true, false, true },
+ { true, false, false, true, false, true, false, true },
+ { true, true, false, true, false, true, false, true },
+
+ // Mixed network + wildcard consumers - gc only allowed when
+ // BOTH flags permit ignoring their respective consumer type
+ { false, false, false, false, true, true, false, true },
+ { false, true, false, false, true, true, false, true },
+ { true, false, false, false, true, true, false, true },
+ { true, true, false, false, true, true, false, false },
+ { false, false, false, true, true, true, false, true },
+ { false, true, false, true, true, true, false, true },
+ { true, false, false, true, true, true, false, true },
+ { true, true, false, true, true, true, false, true },
+
+ // Durable wildcard consumer - never gc-eligible, its
registration
+ // and pending messages live in the destination's store
+ { false, false, false, false, false, false, true, true },
+ { false, true, false, false, false, false, true, true },
+ { true, false, false, false, false, false, true, true },
+ { true, true, false, false, false, false, true, true },
+ { false, true, false, false, false, true, true, true },
+ { true, true, false, false, true, true, true, true },
+
+ // Attached producer - always active, even when every consumer
+ // present is gc-eligible under the enabled flags
+ { false, false, true, false, false, false, false, true },
+ { true, true, true, false, false, false, false, true },
+ { true, false, true, false, true, false, false, true },
+ { false, true, true, false, false, true, false, true },
+ { true, true, true, false, true, true, false, true }
+ };
+
+ var params = new ArrayList<Object[]>();
+ for (var destinationType : List.of("queue", "topic")) {
+ for (var row : truthTable) {
+ var param = new Object[row.length + 1];
+ param[0] = destinationType;
+ System.arraycopy(row, 0, param, 1, row.length);
+ params.add(param);
+ }
+ }
+ return params;
+ }
+
+ private final String destinationType;
+ private final boolean gcWithNetworkConsumersEnabled;
+ private final boolean gcWithOnlyWildcardConsumersEnabled;
+ private final boolean producerActive;
+ private final boolean appConsumerActive;
+ private final boolean networkConsumerActive;
+ private final boolean wildcardConsumerActive;
+ private final boolean durableWildcardConsumerActive;
+ private final boolean activeExpected;
+
+ public DestinationIsActiveTest(String destinationType, boolean
gcWithNetworkConsumersEnabled, boolean gcWithOnlyWildcardConsumersEnabled,
boolean producerActive, boolean appConsumerActive, boolean
networkConsumerActive, boolean wildcardConsumerActive, boolean
durableWildcardConsumerActive, boolean activeExpected) {
+ this.destinationType = destinationType;
+ this.gcWithNetworkConsumersEnabled = gcWithNetworkConsumersEnabled;
+ this.gcWithOnlyWildcardConsumersEnabled =
gcWithOnlyWildcardConsumersEnabled;
+ this.producerActive = producerActive;
+ this.appConsumerActive = appConsumerActive;
+ this.networkConsumerActive = networkConsumerActive;
+ this.wildcardConsumerActive = wildcardConsumerActive;
+ this.durableWildcardConsumerActive = durableWildcardConsumerActive;
+ this.activeExpected = activeExpected;
+ }
+
+ @Test
+ public void testDestinationIsActive() throws Exception {
+ var destinationName = "amq.gc." + counter.incrementAndGet();
+ final var isTopic = "topic".equals(destinationType);
+
+ var policyEntry = new PolicyEntry();
+ policyEntry.setGcInactiveDestinations(true);
+
policyEntry.setGcWithOnlyWildcardConsumers(gcWithOnlyWildcardConsumersEnabled);
+ policyEntry.setGcWithNetworkConsumers(gcWithNetworkConsumersEnabled);
+ policyEntry.setInactiveTimeoutBeforeGC(3000L);
+ if (isTopic) {
+ policyEntry.setTopic(destinationName);
+ } else {
+ policyEntry.setQueue(destinationName);
+ }
+
brokerService.getDestinationPolicy().setPolicyEntries(List.of(policyEntry));
+
+ ActiveMQDestination activemqDestination;
+ if (isTopic) {
+ brokerService.getAdminView().addTopic(destinationName);
+ activemqDestination = new ActiveMQTopic(destinationName);
+ } else {
+ brokerService.getAdminView().addQueue(destinationName);
+ activemqDestination = new ActiveMQQueue(destinationName);
+ }
+ var destination = brokerService.getDestination(activemqDestination);
+
+ assertFalse(destination.isActive());
+
+ if(producerActive) {
+ destination.addProducer(null, new ProducerInfo());
+ }
+ if(appConsumerActive) {
+ destination.addSubscription(null, new
MockQueueSubscription(activemqDestination, false, false, false));
+ }
+ if(networkConsumerActive) {
+ destination.addSubscription(null, new
MockQueueSubscription(activemqDestination, true, false, false));
+ }
+ if(wildcardConsumerActive) {
+ destination.addSubscription(null, new
MockQueueSubscription(activemqDestination, false, true, false));
+ }
+ if(durableWildcardConsumerActive) {
+ // Topic.addSubscription casts durable subscriptions, so topics
need
+ // a DurableTopicSubscription-based mock (inactive, like one
recovered
+ // from the store at broker start)
+ if (isTopic) {
+ destination.addSubscription(null, new
MockDurableTopicSubscription(activemqDestination, true));
+ } else {
+ destination.addSubscription(null, new
MockQueueSubscription(activemqDestination, false, true, true));
+ }
+ }
+
+ assertEquals(activeExpected, destination.isActive());
+
+ // Test parameter config safety checks
+ // if an appConsumer is active, destination must *always* be active
+ if(appConsumerActive) {
+ assertTrue(destination.isActive());
+ }
+ // if a producer is attached, destination must *always* be active
+ if(producerActive) {
+ assertTrue(destination.isActive());
+ }
+ // a durable subscription must *always* keep the destination active
+ if(durableWildcardConsumerActive) {
+ assertTrue(destination.isActive());
+ }
+
+ if (isTopic) {
+ brokerService.getAdminView().removeTopic(destinationName);
+ } else {
+ brokerService.getAdminView().removeQueue(destinationName);
+ }
+ }
+
+ protected static BrokerService createBroker() throws Exception {
+ var map = new PolicyMap();
+ map.setDefaultEntry(new PolicyEntry());
+
+ var broker = new BrokerService();
+ broker.setPersistent(false);
+ broker.setUseJmx(true);
+ broker.setSchedulePeriodForDestinationPurge(100_000_000);
+ broker.setSchedulerSupport(true);
+ broker.setMaxPurgedDestinationsPerSweep(1);
+ broker.setDestinationPolicy(map);
+ return broker;
+ }
+
+ static class MockConsumerInfo extends ConsumerInfo {
+
+ private final boolean networkSubscription;
+ private final boolean durableSubscription;
+
+ public MockConsumerInfo(ActiveMQDestination activeMQDestination,
boolean networkSubscription, boolean durableSubscription) {
+ setDestination(activeMQDestination);
+ this.networkSubscription = networkSubscription;
+ this.durableSubscription = durableSubscription;
+ }
+
+ @Override
+ public boolean isNetworkSubscription() {
+ return this.networkSubscription;
+ }
+
+ @Override
+ public boolean isDurable() {
+ return this.durableSubscription;
+ }
+ }
+
+ static class MockQueueSubscription extends QueueSubscription {
+
+ private final boolean wildCardSubscription;
+
+ public MockQueueSubscription(ActiveMQDestination activemqDestination,
boolean networkSubscription, boolean wildCardSubscription, boolean
durableSubscription) throws Exception {
+ super(brokerService.getBroker(), null, null, new
MockConsumerInfo(activemqDestination, networkSubscription,
durableSubscription));
+ this.wildCardSubscription = wildCardSubscription;
+ }
+
+ @Override
+ public boolean isWildcard() {
+ return this.wildCardSubscription;
+ }
+ }
+
+ // An inactive durable subscription, as recovered from the store at broker
start
+ static class MockDurableTopicSubscription extends DurableTopicSubscription
{
+
+ private final boolean wildCardSubscription;
+
+ public MockDurableTopicSubscription(ActiveMQDestination
activemqDestination, boolean wildCardSubscription) throws Exception {
+ super(brokerService.getBroker(), brokerService.getSystemUsage(),
+ durableContext(),
durableConsumerInfo(activemqDestination), true);
+ this.wildCardSubscription = wildCardSubscription;
+ }
+
+ @Override
+ public boolean isWildcard() {
+ return this.wildCardSubscription;
+ }
+
+ private static ConnectionContext durableContext() throws Exception {
+ var context = new ConnectionContext();
+ context.setClientId("mock-durable-client-" + counter.get());
+ context.setBroker(brokerService.getBroker());
+ return context;
+ }
+
+ private static ConsumerInfo durableConsumerInfo(ActiveMQDestination
activemqDestination) {
+ var info = new MockConsumerInfo(activemqDestination, false, true);
+ info.setSubscriptionName("mock-durable-sub");
+ info.setConsumerId(new ConsumerId(new SessionId(new
ConnectionId("mock-durable-connection"), 1), counter.get()));
+ return info;
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]
For further information, visit: https://activemq.apache.org/contact