ARTEMIS-4488 support 'literal' address setting match

This commit is contained in:
Justin Bertram 2023-11-02 16:38:41 -05:00 committed by Clebert Suconic
parent 36056a5bdd
commit 7c711c04c3
20 changed files with 256 additions and 27 deletions

View File

@ -682,6 +682,8 @@ public final class ActiveMQDefaultConfiguration {
public static final long DEFAULT_EMBEDDED_WEB_SERVER_RESTART_TIMEOUT = 5000;
public static final String DEFAULT_LITERAL_MATCH_MARKERS = null;
/**
* If true then the ActiveMQ Artemis Server will make use of any Protocol Managers that are in available on the classpath. If false then only the core protocol will be available, unless in Embedded mode where users can inject their own Protocol Managers.
*/
@ -1869,4 +1871,7 @@ public final class ActiveMQDefaultConfiguration {
return DEFAULT_EMBEDDED_WEB_SERVER_RESTART_TIMEOUT;
}
public static String getLiteralMatchMarkers() {
return DEFAULT_LITERAL_MATCH_MARKERS;
}
}

View File

@ -1457,4 +1457,8 @@ public interface Configuration {
Eventually with some coordination we can update it from various server components. */
// Inspired by https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#:~:text=The%20status%20describes%20the%20current,the%20desired%20state%20you%20supplied
void setStatus(String status);
String getLiteralMatchMarkers();
Configuration setLiteralMatchMarkers(String literalMatchMarkers);
}

View File

@ -423,6 +423,8 @@ public class ConfigurationImpl implements Configuration, Serializable {
private boolean suppressSessionNotifications = ActiveMQDefaultConfiguration.getDefaultSuppressSessionNotifications();
private String literalMatchMarkers = ActiveMQDefaultConfiguration.getLiteralMatchMarkers();
/**
* Parent folder for all data folders.
*/
@ -3194,6 +3196,17 @@ public class ConfigurationImpl implements Configuration, Serializable {
this.jsonStatus = JsonUtil.mergeAndUpdate(getJsonStatus(), update);
}
@Override
public String getLiteralMatchMarkers() {
return literalMatchMarkers;
}
@Override
public Configuration setLiteralMatchMarkers(String literalMatchMarkers) {
this.literalMatchMarkers = literalMatchMarkers;
return this;
}
// extend property utils with ability to auto-fill and locate from collections
// collection entries are identified by the name() property
private static class CollectionAutoFillPropertiesUtil extends PropertyUtilsBean {

View File

@ -297,4 +297,14 @@ public final class Validators {
}
}
};
public static final Validator NULL_OR_TWO_CHARACTERS = new Validator() {
@Override
public void validate(final String name, final Object value) {
String val = (String) value;
if (val != null && val.length() != 2) {
throw ActiveMQMessageBundle.BUNDLE.wrongLength(name, val, val.length(), 2);
}
}
};
}

View File

@ -810,6 +810,8 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
config.setSuppressSessionNotifications(getBoolean(e, "suppress-session-notifications", config.isSuppressSessionNotifications()));
config.setLiteralMatchMarkers(getString(e, "literal-match-markers", config.getLiteralMatchMarkers(), Validators.NULL_OR_TWO_CHARACTERS));
parseAddressSettings(e, config);
parseResourceLimits(e, config);

View File

@ -542,4 +542,7 @@ public interface ActiveMQMessageBundle {
@Message(id = 229250, value = "Connection has been marked as destroyed for remote connection {}.")
ActiveMQException connectionDestroyed(String remoteAddress);
@Message(id = 229251, value = "{} value '{}' is too long. It is {} characters but must be {} characters.")
IllegalArgumentException wrongLength(String name, String val, int actualLength, int requiredLength);
}

View File

@ -494,7 +494,7 @@ public class ActiveMQServerImpl implements ActiveMQServer {
public String modify(String input) {
return CompositeAddress.extractAddressName(input);
}
});
}, this.configuration.getLiteralMatchMarkers());
addressSettingsRepository.setDefault(new AddressSettings());

View File

@ -33,13 +33,30 @@ public interface HierarchicalRepository<T> {
/**
* Add a new match to the repository
*
* @param match The regex to use to match against
* @param match the pattern to use to match against
* @param value the value to hold against the match
*/
void addMatch(String match, T value);
/**
* Add a new match to the repository
*
* @param match the pattern to use to match against
* @param value the value to hold against the match
* @param immutableMatch
*/
void addMatch(String match, T value, boolean immutableMatch);
/**
* Add a new match to the repository
*
* @param match the pattern to use to match against
* @param value the value to hold against the match
* @param immutableMatch whether this match can be removed
* @param notifyListeners whether to notify any listeners that the match has been added
*/
void addMatch(String match, T value, boolean immutableMatch, boolean notifyListeners);
/**
* return the value held against the nearest match
*

View File

@ -17,6 +17,7 @@
package org.apache.activemq.artemis.core.settings.impl;
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -38,7 +39,6 @@ import org.apache.activemq.artemis.core.settings.HierarchicalRepositoryChangeLis
import org.apache.activemq.artemis.core.settings.Mergeable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;
/**
* allows objects to be mapped against a regex pattern and held in order in a list
@ -60,6 +60,7 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
*/
private final Map<String, Match<T>> wildcardMatches = new HashMap<>();
private final Map<String, Match<T>> exactMatches = new HashMap<>();
private final Map<String, Match<T>> literalMatches = new HashMap<>();
/**
* Certain values cannot be removed after installed.
@ -79,6 +80,12 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
private final WildcardConfiguration wildcardConfiguration;
private final boolean checkLiteral;
private final char literalMatchMarkerStart;
private final char literalMatchMarkerEnd;
/**
* a cache
*/
@ -111,13 +118,22 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
}
public HierarchicalObjectRepository(final WildcardConfiguration wildcardConfiguration) {
this(wildcardConfiguration, new MatchModifier() { });
this(wildcardConfiguration, new MatchModifier() { }, null);
}
public HierarchicalObjectRepository(final WildcardConfiguration wildcardConfiguration, final MatchModifier matchModifier) {
public HierarchicalObjectRepository(final WildcardConfiguration wildcardConfiguration, final MatchModifier matchModifier, final String literalMatchMarkers) {
this.wildcardConfiguration = wildcardConfiguration == null ? DEFAULT_WILDCARD_CONFIGURATION : wildcardConfiguration;
this.matchComparator = new MatchComparator(this.wildcardConfiguration);
this.matchModifier = matchModifier;
if (literalMatchMarkers != null) {
this.checkLiteral = true;
this.literalMatchMarkerStart = literalMatchMarkers.charAt(0);
this.literalMatchMarkerEnd = literalMatchMarkers.charAt(1);
} else {
this.checkLiteral = false;
this.literalMatchMarkerStart = 0;
this.literalMatchMarkerEnd = 0;
}
}
@Override
@ -141,16 +157,11 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
onChange();
}
@Override
public void addMatch(final String match, final T value) {
addMatch(match, value, false);
}
@Override
public List<T> values() {
lock.readLock().lock();
try {
ArrayList<T> values = new ArrayList<>(wildcardMatches.size() + exactMatches.size());
ArrayList<T> values = new ArrayList<>(wildcardMatches.size() + exactMatches.size() + literalMatches.size());
for (Match<T> matchValue : wildcardMatches.values()) {
values.add(matchValue.getValue());
@ -160,27 +171,39 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
values.add(matchValue.getValue());
}
for (Match<T> matchValue : literalMatches.values()) {
values.add(matchValue.getValue());
}
return values;
} finally {
lock.readLock().unlock();
}
}
/**
* Add a new match to the repository
*
* @param match The regex to use to match against
* @param value the value to hold against the match
*/
@Override
public void addMatch(final String match, final T value) {
addMatch(match, value, false);
}
@Override
public void addMatch(final String match, final T value, final boolean immutableMatch) {
addMatch(match, value, immutableMatch, true);
}
private void addMatch(final String match, final T value, final boolean immutableMatch, boolean notifyListeners) {
@Override
public void addMatch(final String match, final T value, final boolean immutableMatch, final boolean notifyListeners) {
String modifiedMatch = match;
boolean literal = false;
if (checkLiteral) {
literal = match.charAt(0) == literalMatchMarkerStart && match.charAt(match.length() - 1) == literalMatchMarkerEnd;
if (literal) {
modifiedMatch = match.substring(1, match.length() - 1);
}
}
modifiedMatch = matchModifier.modify(modifiedMatch);
lock.writeLock().lock();
try {
String modifiedMatch = matchModifier.modify(match);
// an exact match (i.e. one without wildcards) won't impact any other matches so no need to clear the cache
if (usesWildcards(modifiedMatch)) {
clearCache();
@ -192,8 +215,10 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
immutables.add(modifiedMatch);
}
Match.verify(modifiedMatch, wildcardConfiguration);
Match<T> match1 = new Match<>(modifiedMatch, value, wildcardConfiguration);
if (usesWildcards(modifiedMatch)) {
Match<T> match1 = new Match<>(modifiedMatch, value, wildcardConfiguration, literal);
if (literal) {
literalMatches.put(modifiedMatch, match1);
} else if (usesWildcards(modifiedMatch)) {
wildcardMatches.put(modifiedMatch, match1);
} else {
exactMatches.put(modifiedMatch, match1);
@ -272,6 +297,9 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
}
} else {
((Mergeable) actualMatch).merge(match.getValue());
if (match.isLiteral()) {
break;
}
}
}
return actualMatch;
@ -304,8 +332,7 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
lock.writeLock().lock();
try {
String modMatch = matchModifier.modify(match);
boolean isImmutable = immutables.contains(modMatch);
if (isImmutable) {
if (immutables.contains(modMatch)) {
logger.debug("Cannot remove match {} since it came from a main config", modMatch);
} else {
/**
@ -318,6 +345,7 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
} else {
clearCache();
exactMatches.remove(modMatch);
literalMatches.remove(modMatch);
}
onChange();
}
@ -411,6 +439,7 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
private void clearMatches() {
wildcardMatches.clear();
exactMatches.clear();
literalMatches.clear();
}
private void onChange() {
@ -449,6 +478,11 @@ public class HierarchicalObjectRepository<T> implements HierarchicalRepository<T
possibleMatches.put(entry.getKey(), entryMatch);
}
}
if (literalMatches.containsKey(match)) {
possibleMatches.put(match, literalMatches.get(match));
}
return possibleMatches;
}

View File

@ -42,10 +42,17 @@ public class Match<T> {
private final T value;
private final boolean literal;
public Match(final String match, final T value, final WildcardConfiguration wildcardConfiguration) {
this(match, value, wildcardConfiguration, false);
}
public Match(final String match, final T value, final WildcardConfiguration wildcardConfiguration, final boolean literal) {
this.match = match;
this.value = value;
pattern = createPattern(match, wildcardConfiguration, false);
this.literal = literal;
}
/**
@ -93,6 +100,10 @@ public class Match<T> {
return value;
}
public final boolean isLiteral() {
return literal;
}
@Override
public boolean equals(final Object o) {
if (this == o) {

View File

@ -909,6 +909,16 @@
</xsd:annotation>
</xsd:element>
<xsd:element name="literal-match-markers" type="xsd:string" default="" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
The characters that mark a "literal" match. A literal match means the setting(s) will only apply to
the exact match regardless of wildcards. If this setting is not omitted then it must be two
characters - the start marker and the end marker.
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element ref="security-settings" maxOccurs="1" minOccurs="0"/>
<xsd:element ref="broker-plugins" maxOccurs="1" minOccurs="0"/>

View File

@ -443,6 +443,17 @@ public class FileConfigurationParserTest extends ActiveMQTestBase {
Assert.assertEquals(-1, settings.getMaxReadPageMessages());
}
@Test
public void testLiteralMatchMarkers() throws Exception {
String configStr = "<configuration><literal-match-markers>()</literal-match-markers><address-settings>\n<address-setting match=\"(foo)\">\n<max-read-page-bytes>-1</max-read-page-bytes></address-setting>\n</address-settings></configuration>\n";
FileConfigurationParser parser = new FileConfigurationParser();
ByteArrayInputStream input = new ByteArrayInputStream(configStr.getBytes(StandardCharsets.UTF_8));
Configuration configuration = parser.parseMainConfig(input);
Assert.assertEquals("()", configuration.getLiteralMatchMarkers());
}
// you should not use K, M notations on address settings max-size-messages
@Test
public void testExpectedErrorOverMaxMessageNotation() throws Exception {

View File

@ -152,6 +152,7 @@ public class FileConfigurationTest extends ConfigurationImplTest {
Assert.assertEquals(false, conf.isCreateBindingsDir());
Assert.assertEquals(true, conf.isAmqpUseCoreSubscriptionNaming());
Assert.assertEquals(false, conf.isSuppressSessionNotifications());
Assert.assertEquals("()", conf.getLiteralMatchMarkers());
Assert.assertEquals("max concurrent io", 17, conf.getPageMaxConcurrentIO());
Assert.assertEquals(true, conf.isReadWholePage());

View File

@ -30,7 +30,7 @@ public class ValidatorsTest extends Assert {
private static void failure(final Validators.Validator validator, final Object value) {
try {
validator.validate(RandomUtil.randomString(), value);
Assert.fail(validator + " must not validate " + value);
Assert.fail(validator + " must not validate '" + value + "'");
} catch (IllegalArgumentException e) {
}
@ -141,4 +141,14 @@ public class ValidatorsTest extends Assert {
ValidatorsTest.failure(Validators.MINUS_ONE_OR_POSITIVE_INT, Integer.MAX_VALUE + 1);
}
@Test
public void testTWO_CHARACTERS() {
ValidatorsTest.failure(Validators.NULL_OR_TWO_CHARACTERS, "1234");
ValidatorsTest.failure(Validators.NULL_OR_TWO_CHARACTERS, "123");
ValidatorsTest.failure(Validators.NULL_OR_TWO_CHARACTERS, "1");
ValidatorsTest.success(Validators.NULL_OR_TWO_CHARACTERS, "12");
ValidatorsTest.success(Validators.NULL_OR_TWO_CHARACTERS, null);
}
}

View File

@ -24,6 +24,7 @@ import org.apache.activemq.artemis.core.config.WildcardConfiguration;
import org.apache.activemq.artemis.core.security.Role;
import org.apache.activemq.artemis.core.settings.impl.HierarchicalObjectRepository;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@ -40,6 +41,14 @@ public class RepositoryTest extends ActiveMQTestBase {
securityRepository = new HierarchicalObjectRepository<>();
}
@Override
@After
public void tearDown() throws Exception {
super.tearDown();
DummyMergeable.reset();
}
@Test
public void testDefault() {
securityRepository.setDefault(new HashSet<Role>());
@ -65,6 +74,32 @@ public class RepositoryTest extends ActiveMQTestBase {
Assert.assertEquals("abd#", repo.getMatch("a.b.d"));
}
/*
* A "literal" match is one which uses wild-cards but should not be applied to other matches "below" it in the hierarchy.
*/
@Test
public void testLiteral() {
HierarchicalObjectRepository<DummyMergeable> repo = new HierarchicalObjectRepository<>(null, new HierarchicalObjectRepository.MatchModifier() { }, "()");
repo.addMatch("#", new DummyMergeable(0));
repo.addMatch("(a.#)", new DummyMergeable(1));
repo.addMatch("a.#", new DummyMergeable(2));
repo.addMatch("a.b", new DummyMergeable(3));
repo.getMatch("a.b");
assertTrue(DummyMergeable.contains(0));
assertFalse(DummyMergeable.contains(1));
assertTrue(DummyMergeable.contains(2));
assertTrue(DummyMergeable.contains(3));
DummyMergeable.reset();
repo.getMatch("a.#");
assertTrue(DummyMergeable.contains(0));
assertTrue(DummyMergeable.contains(1));
assertFalse(DummyMergeable.contains(2));
assertFalse(DummyMergeable.contains(3));
}
@Test
public void testCacheWithWildcards() throws Throwable {
HierarchicalObjectRepository<String> repo = new HierarchicalObjectRepository<>();

View File

@ -69,6 +69,7 @@
<critical-analyzer-timeout>777</critical-analyzer-timeout>
<critical-analyzer>false</critical-analyzer>
<suppress-session-notifications>false</suppress-session-notifications>
<literal-match-markers>()</literal-match-markers>
<remoting-incoming-interceptors>
<class-name>org.apache.activemq.artemis.tests.unit.core.config.impl.TestInterceptor1</class-name>
<class-name>org.apache.activemq.artemis.tests.unit.core.config.impl.TestInterceptor2</class-name>

View File

@ -68,6 +68,7 @@
<critical-analyzer-check-period>333</critical-analyzer-check-period>
<critical-analyzer-timeout>777</critical-analyzer-timeout>
<critical-analyzer>false</critical-analyzer>
<literal-match-markers>()</literal-match-markers>
<remoting-incoming-interceptors>
<class-name>org.apache.activemq.artemis.tests.unit.core.config.impl.TestInterceptor1</class-name>
<class-name>org.apache.activemq.artemis.tests.unit.core.config.impl.TestInterceptor2</class-name>

View File

@ -68,6 +68,7 @@
<critical-analyzer-check-period>333</critical-analyzer-check-period>
<critical-analyzer-timeout>777</critical-analyzer-timeout>
<critical-analyzer>false</critical-analyzer>
<literal-match-markers>()</literal-match-markers>
<xi:include href="${xincludePath}/ConfigurationTest-xinclude-schema-config-remoting-incoming-interceptors.xml"/>
<xi:include href="${xincludePath}/ConfigurationTest-xinclude-schema-config-remoting-outgoing-interceptors.xml"/>
<persist-delivery-count-before-delivery>true</persist-delivery-count-before-delivery>

View File

@ -5,14 +5,16 @@
With address settings you can provide a block of settings which will be applied to any addresses that match the string in the `match` attribute.
In the below example the settings would only be applied to the address `order.foo` address, but it is also possible to use xref:wildcard-syntax.adoc#wildcard-syntax[wildcards] to apply settings.
For example, if you used the `match` string `queue.#` the settings would be applied to all addresses which start with `queue.`
For example, if you used the `match` string `queue.#` the settings would be applied to _all_ addresses which start with `queue.`.
Address settings are *hierarchical*.
Therefore, if more than one `address-setting` would match then the settings are applied in order of their specificity with the more specific match taking priority.
A match on the any-words delimiter (`#`) is considered less specific than a match without it.
A match with a single word delimiter `*` is considered less specific than a match on an exact queue name.
A match on the any-words delimiter (`#` by default) is considered less specific than a match without it.
A match with a single word delimiter (`*` by default) is considered less specific than a match on an exact queue name.
In this way settings can be "layered" so that configuration details don't need to be repeated.
Address setting matches can also be "literal" which can be used to interrupt the hierarchy in useful ways.
The meaning of the specific settings are explained fully throughout the user manual, however here is a brief description with a link to the appropriate chapter if available.
Here an example of an `address-setting` entry that might be found in the `broker.xml` file.
@ -389,3 +391,39 @@ defines the maximum size of the duplicate ID cache for an address, as each addre
that helps to detect and prevent the processing of duplicate messages based on their unique identification.
By default, the `id-cache-size` setting inherits from the global `id-cache-size`, with a default of `20000`
elements if not explicitly configured. Read more about xref:duplicate-detection.adoc#configuring-the-duplicate-id-cache[duplicate id cache sizes].
## Literal Matches
A _literal_ match is a match that contains wildcards but should be applied _without regard_ to those wildcards. In other words, the wildcards should be ignored and the address settings should only be applied to the literal (i.e. exact) match.
This can be useful when an application uses a xref:wildcard-routing.adoc[wildcard address]. For example, if an application creates a multicast queue on the address `orders.#` and that queue needs a different configuration than other matching addresses like `orders.retail` and `orders.wholesale`. Generally speaking this kind of use-case is rare, but wildcard addresses are often used by MQTT clients, and this kind of configuration flexiblity is useful.
### Configuring a Literal Match
If you want to configure a literal match the first thing to do is to configure the `literal-match-markers` parameter in `broker.xml`. This defines the beginning and ending characters used to mark the literal match, e.g.:
[,xml]
----
<core>
...
<literal-match-markers>()</literal-match-markers>
...
</core>
----
By default, no value is defined for `literal-match-markers` which means that literal matches are disabled by default. The value must be only 2 characters.
Once `literal-match-markers` is defined you can then use those markers in the `match` of the address setting, e.g.
[,xml]
----
<address-settings>
<address-setting match="(orders.#)">
<enable-metrics>true</enable-metrics>
</address-setting>
<address-setting match="orders.#">
<enable-metrics>false</enable-metrics>
</address-setting>
</address-settings>
----
Using these settings metrics will be enabled on the address `orders.#` and any queues bound directly on that address, but metrics will _not_ be enabled for other matching addresses like `orders.retail` or `orders.wholesale` and any queues bound to those addresses.

View File

@ -24,10 +24,12 @@ import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.api.core.client.ServerLocator;
import org.apache.activemq.artemis.core.config.Configuration;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.settings.HierarchicalRepository;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
import org.apache.activemq.artemis.tests.util.RandomUtil;
import org.junit.Assert;
import org.junit.Test;
@ -113,6 +115,26 @@ public class AddressSettingsTest extends ActiveMQTestBase {
}
@Test
public void testLiteralMatch() throws Exception {
final SimpleString literal = RandomUtil.randomSimpleString();
final SimpleString nonLiteral = RandomUtil.randomSimpleString();
Configuration configuration = createDefaultConfig(false);
configuration.setLiteralMatchMarkers("()");
ActiveMQServer server = createServer(false, configuration);
server.start();
HierarchicalRepository<AddressSettings> repo = server.getAddressSettingsRepository();
repo.addMatch("(foo.#)", new AddressSettings().setDeadLetterAddress(literal));
repo.addMatch("foo.#", new AddressSettings().setDeadLetterAddress(nonLiteral));
// should be the DLA from foo.# - the literal match
Assert.assertEquals(literal, repo.getMatch("foo.#").getDeadLetterAddress());
Assert.assertEquals(nonLiteral, repo.getMatch("foo.bar").getDeadLetterAddress());
}
@Test
public void test2LevelHierarchyWithDLA() throws Exception {
ActiveMQServer server = createServer(false);