diff --git a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/config/FederationConfiguration.java b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/config/FederationConfiguration.java index b3865d795b..9c0916559d 100644 --- a/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/config/FederationConfiguration.java +++ b/artemis-core-client/src/main/java/org/apache/activemq/artemis/core/config/FederationConfiguration.java @@ -73,8 +73,7 @@ public class FederationConfiguration implements Serializable { return federationPolicyMap; } - // strange spelling!, it allows a type match for singular of correct plural Policies from properties - public FederationConfiguration addQueuePolicie(FederationQueuePolicyConfiguration federationPolicy) { + public FederationConfiguration addQueuePolicy(FederationQueuePolicyConfiguration federationPolicy) { federationPolicyMap.put(federationPolicy.getName(), federationPolicy); return this; } @@ -83,7 +82,7 @@ public class FederationConfiguration implements Serializable { return federationPolicyMap; } - public FederationConfiguration addAddressPolicie(FederationAddressPolicyConfiguration federationPolicy) { + public FederationConfiguration addAddressPolicy(FederationAddressPolicyConfiguration federationPolicy) { federationPolicyMap.put(federationPolicy.getName(), federationPolicy); return this; } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/AMQPSessionCallback.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/AMQPSessionCallback.java index 5a1b23ea6e..23bca6431c 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/AMQPSessionCallback.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/broker/AMQPSessionCallback.java @@ -136,6 +136,10 @@ public class AMQPSessionCallback implements SessionCallback { return coreMessageObjectPools; } + public AMQPSessionContext getAMQPSessionContext() { + return protonSession; + } + public ProtonProtocolManager getProtocolManager() { return manager; } @@ -154,10 +158,8 @@ public class AMQPSessionCallback implements SessionCallback { logger.warn(e.getMessage(), e); } }); - } - public void withinContext(Runnable run) throws Exception { OperationContext context = recoverContext(); try { @@ -305,33 +307,38 @@ public class AMQPSessionCallback implements SessionCallback { } } - public QueueQueryResult queueQuery(SimpleString queueName, RoutingType routingType, boolean autoCreate) throws Exception { - return queueQuery(queueName, routingType, autoCreate, null); - } - - public QueueQueryResult queueQuery(SimpleString queueName, RoutingType routingType, boolean autoCreate, SimpleString filter) throws Exception { - QueueQueryResult queueQueryResult = serverSession.executeQueueQuery(queueName); + public QueueQueryResult queueQuery(QueueConfiguration configuration, boolean autoCreate) throws Exception { + QueueQueryResult queueQueryResult = serverSession.executeQueueQuery(configuration.getName()); if (!queueQueryResult.isExists() && queueQueryResult.isAutoCreateQueues() && autoCreate) { try { - serverSession.createQueue(new QueueConfiguration(queueName).setRoutingType(routingType).setFilterString(filter).setAutoCreated(true)); + serverSession.createQueue(configuration.setAutoCreated(true)); } catch (ActiveMQQueueExistsException e) { // The queue may have been created by another thread in the mean time. Catch and do nothing. } - queueQueryResult = serverSession.executeQueueQuery(queueName); + queueQueryResult = serverSession.executeQueueQuery(configuration.getName()); } // if auto-create we will return whatever type was used before if (queueQueryResult.isExists() && !queueQueryResult.isAutoCreated()) { - //if routingType is null we bypass the check - if (routingType != null && queueQueryResult.getRoutingType() != routingType) { - throw new IllegalStateException("Incorrect Routing Type for queue " + queueName + ", expecting: " + routingType + " while it had " + queueQueryResult.getRoutingType()); + final RoutingType desiredRoutingType = configuration.getRoutingType(); + if (desiredRoutingType != null && queueQueryResult.getRoutingType() != desiredRoutingType) { + throw new IllegalStateException("Incorrect Routing Type for queried queue " + configuration.getName() + + ", expecting: " + desiredRoutingType + " while the actual type was: " + + queueQueryResult.getRoutingType()); } } return queueQueryResult; } + public QueueQueryResult queueQuery(SimpleString queueName, RoutingType routingType, boolean autoCreate) throws Exception { + return queueQuery(queueName, routingType, autoCreate, null); + } + + public QueueQueryResult queueQuery(SimpleString queueName, RoutingType routingType, boolean autoCreate, SimpleString filter) throws Exception { + return queueQuery(new QueueConfiguration(queueName).setRoutingType(routingType).setFilterString(filter), autoCreate); + } public boolean checkAddressAndAutocreateIfPossible(SimpleString address, RoutingType routingType) throws Exception { AutoCreateResult autoCreateResult = serverSession.checkAutoCreate(new QueueConfiguration(address).setRoutingType(routingType)); @@ -447,7 +454,7 @@ public class AMQPSessionCallback implements SessionCallback { Delivery delivery, SimpleString address, RoutingContext routingContext, - AMQPMessage message) throws Exception { + Message message) throws Exception { context.incrementSettle(); @@ -711,6 +718,24 @@ public class AMQPSessionCallback implements SessionCallback { return null; } + /** + * Adds key / value based metadata into the underlying server session implementation + * for use by the connection resources. + * + * @param key + * The key to add into the linked server session. + * @param value + * The value to add into the linked server session attached to the given key. + * + * @return this {@link AMQPSessionCallback} instance. + * + * @throws Exception if an error occurs while adding the metadata. + */ + public AMQPSessionCallback addMetaData(String key, String value) throws Exception { + serverSession.addMetaData(key, value); + return this; + } + public Transaction getTransaction(Binary txid, boolean remove) throws ActiveMQAMQPException { return protonSPI.getTransaction(txid, remove); } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java index 20771f26ca..8cff43c56a 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnection.java @@ -22,12 +22,15 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.activemq.artemis.api.core.ActiveMQException; @@ -38,6 +41,9 @@ import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; import org.apache.activemq.artemis.core.postoffice.Binding; import org.apache.activemq.artemis.core.postoffice.QueueBinding; @@ -57,6 +63,8 @@ import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerQueuePlugin; import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback; import org.apache.activemq.artemis.protocol.amqp.broker.ActiveMQProtonRemotingConnection; import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationSource; import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerAggregation; import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource; import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolLogger; @@ -86,6 +94,10 @@ import org.apache.qpid.proton.engine.Sender; import org.apache.qpid.proton.engine.Session; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.AMQP_LINK_INITIALIZER_KEY; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyOfferedCapabilities; + import java.lang.invoke.MethodHandles; public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, ActiveMQServerQueuePlugin, BrokerConnection { @@ -103,12 +115,14 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, private volatile boolean started = false; private final AMQPBrokerConnectionManager bridgeManager; private AMQPMirrorControllerSource mirrorControllerSource; + private AMQPFederationSource brokerFederation; private int retryCounter = 0; private int lastRetryCounter; private boolean connecting = false; - private volatile ScheduledFuture reconnectFuture; + private volatile ScheduledFuture reconnectFuture; private final Set senders = new HashSet<>(); private final Set receivers = new HashSet<>(); + private final Map> linkClosedInterceptors = new ConcurrentHashMap<>(); final Executor connectExecutor; final ScheduledExecutorService scheduledExecutorService; @@ -153,6 +167,10 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, return connecting; } + public int getConnectionTimeout() { + return bridgesConnector.getConnectTimeoutMillis(); + } + @Override public void stop() { if (!started) return; @@ -162,11 +180,17 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, protonRemotingConnection = null; connection = null; } - ScheduledFuture scheduledFuture = reconnectFuture; + ScheduledFuture scheduledFuture = reconnectFuture; reconnectFuture = null; if (scheduledFuture != null) { scheduledFuture.cancel(true); } + if (brokerFederation != null) { + try { + brokerFederation.stop(); + } catch (ActiveMQException e) { + } + } } @Override @@ -175,11 +199,14 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, started = true; server.getConfiguration().registerBrokerPlugin(this); try { - if (brokerConnectConfiguration != null && brokerConnectConfiguration.getConnectionElements() != null) { for (AMQPBrokerConnectionElement connectionElement : brokerConnectConfiguration.getConnectionElements()) { - if (connectionElement.getType() == AMQPBrokerConnectionAddressType.MIRROR) { + final AMQPBrokerConnectionAddressType elementType = connectionElement.getType(); + + if (elementType == AMQPBrokerConnectionAddressType.MIRROR) { installMirrorController((AMQPMirrorBrokerConnectionElement) connectionElement, server); + } else if (elementType == AMQPBrokerConnectionAddressType.FEDERATION) { + installFederation((AMQPFederatedBrokerConnectionElement) connectionElement, server); } } } @@ -190,6 +217,10 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, connectExecutor.execute(() -> doConnect()); } + public ActiveMQServer getServer() { + return server; + } + public NettyConnection getConnection() { return connection; } @@ -204,7 +235,8 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, } public void validateMatching(Queue queue, AMQPBrokerConnectionElement connectionElement) { - if (connectionElement.getType() != AMQPBrokerConnectionAddressType.MIRROR) { + if (connectionElement.getType() != AMQPBrokerConnectionAddressType.MIRROR && + connectionElement.getType() != AMQPBrokerConnectionAddressType.FEDERATION) { if (connectionElement.getQueueName() != null) { if (queue.getName().equals(connectionElement.getQueueName())) { createLink(queue, connectionElement); @@ -239,7 +271,44 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, return snf; } + /** + * Adds a remote link closed event interceptor that can intercept the closed event and if it + * returns true indicate that the close has been handled and that normal broker connection + * remote link closed handling should be ignored. + * + * @param id + * A unique Id value that identifies the intercepter for later removal. + * @param interceptor + * The predicate that will be called for any link close. + * + * @return this broker connection instance. + */ + public AMQPBrokerConnection addLinkClosedInterceptor(String id, Predicate interceptor) { + linkClosedInterceptors.put(id, interceptor); + return this; + } + + /** + * Remove a previously registered link close interceptor from the broker connection. + * + * @param id + * The id of the interceptor to remove + * + * @return this broker connection instance. + */ + public AMQPBrokerConnection removeLinkClosedInterceptor(String id) { + linkClosedInterceptors.remove(id); + return this; + } + private void linkClosed(Link link) { + for (Map.Entry> interceptor : linkClosedInterceptors.entrySet()) { + if (interceptor.getValue().test(link)) { + logger.trace("Remote link[{}] close intercepted and handled by interceptor: {}", link.getName(), interceptor.getKey()); + return; + } + } + if (link.getLocalState() == EndpointState.ACTIVE) { error(ActiveMQAMQPProtocolMessageBundle.BUNDLE.brokerConnectionRemoteLinkClosed(), lastRetryCounter); } @@ -280,7 +349,7 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, ConnectionEntry entry = protonProtocolManager.createOutgoingConnectionEntry(connection, saslFactory); server.getRemotingService().addConnectionEntry(connection, entry); protonRemotingConnection = (ActiveMQProtonRemotingConnection) entry.connection; - protonRemotingConnection.getAmqpConnection().setLinkCloseListener(this::linkClosed); + protonRemotingConnection.getAmqpConnection().addLinkRemoteCloseListener(getName(), this::linkClosed); connection.getChannel().pipeline().addLast(new AMQPBrokerConnectionChannelHandler(bridgesConnector.getChannelGroup(), protonRemotingConnection.getAmqpConnection().getHandler(), this, server.getExecutorFactory().getExecutor())); @@ -313,6 +382,10 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, connectSender(queue, queue.getName().toString(), mirrorControllerSource::setLink, (r) -> AMQPMirrorControllerSource.validateProtocolData(protonProtocolManager.getReferenceIDSupplier(), r, getMirrorSNF(replica)), server.getNodeID().toString(), new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY}, null); + } else if (connectionElement.getType() == AMQPBrokerConnectionAddressType.FEDERATION) { + // Starting the Federation triggers rebuild of federation links + // based on current broker state. + brokerFederation.handleConnectionRestored(protonRemotingConnection.getAmqpConnection(), sessionContext); } } } @@ -417,7 +490,6 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, server.scanAddresses(newPartition); - if (currentMirrorController == null) { server.installMirrorController(newPartition); } else { @@ -445,6 +517,47 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, return null; } + private void installFederation(AMQPFederatedBrokerConnectionElement connectionElement, ActiveMQServer server) throws Exception { + final AMQPFederationSource federation = new AMQPFederationSource(connectionElement.getName(), connectionElement.getProperties(), this); + + // Broker federation configuration for local resources that should be receiving from remote resources + // when there is local demand. + final Set localAddressPolicies = connectionElement.getLocalAddressPolicies(); + if (!localAddressPolicies.isEmpty()) { + for (AMQPFederationAddressPolicyElement policy : localAddressPolicies) { + federation.addAddressMatchPolicy( + AMQPFederationPolicySupport.create(policy, federation.getWildcardConfiguration())); + } + } + final Set localQueuePolicies = connectionElement.getLocalQueuePolicies(); + if (!localQueuePolicies.isEmpty()) { + for (AMQPFederationQueuePolicyElement policy : localQueuePolicies) { + federation.addQueueMatchPolicy( + AMQPFederationPolicySupport.create(policy, federation.getWildcardConfiguration())); + } + } + + // Broker federation configuration for remote resources that should be receiving from local resources + // when there is demand on the remote. + final Set remoteAddressPolicies = connectionElement.getRemoteAddressPolicies(); + if (!remoteAddressPolicies.isEmpty()) { + for (AMQPFederationAddressPolicyElement policy : remoteAddressPolicies) { + federation.addRemoteAddressMatchPolicy( + AMQPFederationPolicySupport.create(policy, federation.getWildcardConfiguration())); + } + } + final Set remoteQueuePolicies = connectionElement.getRemoteQueuePolicies(); + if (!remoteQueuePolicies.isEmpty()) { + for (AMQPFederationQueuePolicyElement policy : remoteQueuePolicies) { + federation.addRemoteQueueMatchPolicy( + AMQPFederationPolicySupport.create(policy, federation.getWildcardConfiguration())); + } + } + + this.brokerFederation = federation; + this.brokerFederation.start(); + } + private void connectReceiver(ActiveMQProtonRemotingConnection protonRemotingConnection, Session session, AMQPSessionContext sessionContext, @@ -537,7 +650,7 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, sender.open(); - final ScheduledFuture futureTimeout; + final ScheduledFuture futureTimeout; AtomicBoolean cancelled = new AtomicBoolean(false); @@ -551,7 +664,7 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, } // Using attachments to set up a Runnable that will be executed inside AMQPBrokerConnection::remoteLinkOpened - sender.attachments().set(Runnable.class, Runnable.class, () -> { + sender.attachments().set(AMQP_LINK_INITIALIZER_KEY, Runnable.class, () -> { ProtonServerSenderContext senderContext = new ProtonServerSenderContext(protonRemotingConnection.getAmqpConnection(), sender, sessionContext, sessionContext.getSessionSPI(), outgoingInitializer).setBeforeDelivery(beforeDeliver); try { if (!cancelled.get()) { @@ -596,30 +709,38 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, }); } - protected boolean verifyOfferedCapabilities(Sender sender, Symbol[] capabilities) { - - if (sender.getRemoteOfferedCapabilities() == null) { - return false; - } - - for (Symbol s : capabilities) { - boolean foundS = false; - for (Symbol b : sender.getRemoteOfferedCapabilities()) { - if (b.equals(s)) { - foundS = true; - break; - } - } - if (!foundS) { - return false; - } - } - - return true; + public void error(Throwable e) { + error(e, 0); } - protected void error(Throwable e) { - error(e, 0); + /** + * Provides an error API for resources of the broker connection that + * encounter errors during the normal operation of the resource that + * represent a terminal outcome for the connection. The connection + * retry counter will be reset to zero for these types of errors as + * these indicate a connection interruption that should initiate the + * start of a reconnect cycle if reconnection is configured. + * + * @param error + * The exception that describes the terminal connection error. + */ + public void runtimeError(Throwable error) { + error(error, 0); + } + + /** + * Provides an error API for resources of the broker connection that + * encounter errors during the connection / resource initialization + * phase that should constitute a terminal outcome for the connection. + * The connection retry counter will be incremented for these types of + * errors which can result in eventual termination of reconnect attempts + * when the limit is exceeded. + * + * @param error + * The exception that describes the terminal connection error. + */ + public void connectError(Throwable error) { + error(error, lastRetryCounter); } // the retryCounter is passed here @@ -678,9 +799,17 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, private void redoConnection() { - // avoiding retro-feeding an error call from the close after anyting else that happened. + // avoiding retro-feeding an error call from the close after anything else that happened. if (protonRemotingConnection != null) { - protonRemotingConnection.getAmqpConnection().setLinkCloseListener(null); + protonRemotingConnection.getAmqpConnection().clearLinkRemoteCloseListeners(); + } + + if (brokerFederation != null) { + try { + brokerFederation.handleConnectionDropped(); + } catch (ActiveMQException e) { + logger.debug("Broker Federation on connection {} threw an error on stop before connection attempt", getName()); + } } // we need to use the connectExecutor to initiate a redoConnection @@ -828,5 +957,4 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, return null; } } - } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnectionManager.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnectionManager.java index 8101c821f7..a593821814 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnectionManager.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/AMQPBrokerConnectionManager.java @@ -50,9 +50,9 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon private final ActiveMQServer server; private volatile boolean started = false; - List amqpConnectionsConfig; - List amqpBrokerConnectionList; - ProtonProtocolManager protonProtocolManager; + private List amqpConnectionsConfig; + private List amqpBrokerConnectionList; + private ProtonProtocolManager protonProtocolManager; public AMQPBrokerConnectionManager(ProtonProtocolManagerFactory factory, List amqpConnectionsConfig, ActiveMQServer server) { this.amqpConnectionsConfig = amqpConnectionsConfig; @@ -68,7 +68,6 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon amqpBrokerConnectionList = new ArrayList<>(); - for (AMQPBrokerConnectConfiguration config : amqpConnectionsConfig) { NettyConnectorFactory factory = new NettyConnectorFactory().setServerConnector(true); protonProtocolManager = (ProtonProtocolManager)protonProtocolManagerFactory.createProtocolManager(server, config.getTransportConfigurations().get(0).getExtraParams(), null, null); @@ -108,7 +107,6 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon public void connectionCreated(ActiveMQComponent component, Connection connection, ClientProtocolManager protocol) { } - @Override public void connectionDestroyed(Object connectionID) { for (AMQPBrokerConnection connection : amqpBrokerConnectionList) { @@ -125,7 +123,6 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon connection.connectionException(connectionID, me); } } - } @Override @@ -245,6 +242,4 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon return null; } } - - } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederation.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederation.java new file mode 100644 index 0000000000..4fe569c2ae --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederation.java @@ -0,0 +1,327 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.lang.invoke.MethodHandles; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Predicate; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationAddressPolicyManager; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationInternal; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationQueuePolicyManager; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.qpid.proton.engine.Link; +import org.apache.qpid.proton.engine.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A single AMQP Federation instance that can be tied to an AMQP broker connection or + * used on a remote peer to control the reverse case of when the remote configures the + * target side of the connection. + */ +public abstract class AMQPFederation implements FederationInternal { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + /** + * Value used to store the federation instance used by an AMQP connection that + * is performing remote command and control operations or is the target of said + * operations. Only one federation instance is allowed per connection and will + * be checked. + */ + public static final String FEDERATION_INSTANCE_RECORD = "FEDERATION_INSTANCE_RECORD"; + + private static final WildcardConfiguration DEFAULT_WILDCARD_CONFIGURATION = new WildcardConfiguration(); + + // Local policies that should be matched against demand on local addresses and queues. + protected final Map queueMatchPolicies = new ConcurrentHashMap<>(); + protected final Map addressMatchPolicies = new ConcurrentHashMap<>(); + protected final Map> linkClosedinterceptors = new ConcurrentHashMap<>(); + + protected final WildcardConfiguration wildcardConfiguration; + protected final ScheduledExecutorService scheduler; + + protected final String name; + protected final ActiveMQServer server; + + // Connection and Session are updated after each reconnect. + protected volatile AMQPConnectionContext connection; + protected volatile AMQPSessionContext session; + + protected boolean started; + protected volatile boolean connected; + + public AMQPFederation(String name, ActiveMQServer server) { + Objects.requireNonNull(name, "Federation name cannot be null"); + Objects.requireNonNull(server, "Provided server instance cannot be null"); + + this.name = name; + this.server = server; + this.scheduler = server.getScheduledPool(); + + if (server.getConfiguration().getWildcardConfiguration() != null) { + this.wildcardConfiguration = server.getConfiguration().getWildcardConfiguration(); + } else { + this.wildcardConfiguration = DEFAULT_WILDCARD_CONFIGURATION; + } + } + + /** + * @return the {@link WildcardConfiguration} that is in use by this server federation. + */ + public WildcardConfiguration getWildcardConfiguration() { + return wildcardConfiguration; + } + + public ScheduledExecutorService getScheduler() { + return scheduler; + } + + @Override + public ActiveMQServer getServer() { + return server; + } + + @Override + public String getName() { + return name; + } + + @Override + public synchronized boolean isStarted() { + return started; + } + + /** + * @return the session context assigned to this federation instance + */ + public abstract AMQPConnectionContext getConnectionContext(); + + /** + * @return the session context assigned to this federation instance + */ + public abstract AMQPSessionContext getSessionContext(); + + /** + * @return the timeout before signaling an error when creating remote link (0 mean disable). + */ + public abstract int getLinkAttachTimeout(); + + /** + * @return the configured {@link Receiver} link credit batch size. + */ + public abstract int getReceiverCredits(); + + /** + * @return the configured {@link Receiver} link credit low value. + */ + public abstract int getReceiverCreditsLow(); + + /** + * @return the size in bytes before a message is considered large. + */ + public abstract int getLargeMessageThreshold(); + + @Override + public final synchronized void start() throws ActiveMQException { + if (!started) { + handleFederationStarted(); + signalFederationStarted(); + started = true; + } + } + + @Override + public final synchronized void stop() throws ActiveMQException { + if (started) { + handleFederationStopped(); + signalFederationStopped(); + started = false; + } + } + + /** + * Adds a remote linked closed event interceptor that can intercept the closed event and + * if it returns true indicate that the close has been handled and that no further action + * need to be taken for this event. + * + * @param id + * A unique Id value that identifies the interceptor for later removal. + * @param interceptor + * The predicate that will be called for any link close. + * + * @return this {@link AMQPFederation} instance. + */ + public AMQPFederation addLinkClosedInterceptor(String id, Predicate interceptor) { + linkClosedinterceptors.put(id, interceptor); + return this; + } + + /** + * Remove a previously registered link close interceptor from the list of close interceptor bindings. + * + * @param id + * The id of the interceptor to remove + * + * @return this {@link AMQPFederation} instance. + */ + public AMQPFederation removeLinkClosedInterceptor(String id) { + linkClosedinterceptors.remove(id); + return this; + } + + /** + * Adds a new {@link FederationReceiveFromQueuePolicy} entry to the set of policies that this + * federation will use to create demand on the remote when local demand is present. + * + * @param queuePolicy + * The policy to add to the set of configured {@link FederationReceiveFromQueuePolicy} instance. + * + * @return this {@link AMQPFederation} instance. + * + * @throws ActiveMQException if an error occurs processing the added policy + */ + public synchronized AMQPFederation addQueueMatchPolicy(FederationReceiveFromQueuePolicy queuePolicy) throws ActiveMQException { + final FederationQueuePolicyManager manager = new AMQPFederationQueuePolicyManager(this, queuePolicy); + + queueMatchPolicies.put(queuePolicy.getPolicyName(), manager); + + logger.debug("AMQP Federation {} adding queue match policy: {}", getName(), queuePolicy.getPolicyName()); + + if (isStarted()) { + // This is a heavy operation in some cases so move off the IO thread + scheduler.execute(() -> manager.start()); + } + + return this; + } + + /** + * Adds a new {@link FederationReceiveFromAddressPolicy} entry to the set of policies that this + * federation will use to create demand on the remote when local demand is present. + * + * @param addressPolicy + * The policy to add to the set of configured {@link FederationReceiveFromAddressPolicy} instance. + * + * @return this {@link AMQPFederation} instance. + * + * @throws ActiveMQException if an error occurs processing the added policy + */ + public synchronized AMQPFederation addAddressMatchPolicy(FederationReceiveFromAddressPolicy addressPolicy) throws ActiveMQException { + final FederationAddressPolicyManager manager = new AMQPFederationAddressPolicyManager(this, addressPolicy); + + addressMatchPolicies.put(addressPolicy.getPolicyName(), manager); + + logger.debug("AMQP Federation {} adding address match policy: {}", getName(), addressPolicy.getPolicyName()); + + if (isStarted()) { + // This is a heavy operation in some cases so move off the IO thread + scheduler.execute(() -> manager.start()); + } + + return this; + } + + /** + * Error signaling API that must be implemented by the specific federation implementation + * to handle error when creating a federation resource such as an outgoing receiver link. + * + * @param cause + * The error that caused the resource creation to fail. + */ + protected abstract void signalResourceCreateError(Exception cause); + + /** + * Error signaling API that must be implemented by the specific federation implementation + * to handle errors encountered during normal operations. + * + * @param cause + * The error that caused the operation to fail. + */ + protected abstract void signalError(Exception cause); + + /** + * Provides an entry point for the concrete federation implementation to respond + * to being started. + * + * @throws ActiveMQException if an error is thrown during policy start. + */ + protected void handleFederationStarted() throws ActiveMQException { + if (connected) { + queueMatchPolicies.forEach((k, v) -> v.start()); + addressMatchPolicies.forEach((k, v) -> v.start()); + } + } + + /** + * Provides an entry point for the concrete federation implementation to respond + * to being stopped. + * + * @throws ActiveMQException if an error is thrown during policy stop. + */ + protected void handleFederationStopped() throws ActiveMQException { + queueMatchPolicies.forEach((k, v) -> v.stop()); + addressMatchPolicies.forEach((k, v) -> v.stop()); + } + + protected boolean invokeLinkClosedInterceptors(Link link) { + for (Map.Entry> interceptor : linkClosedinterceptors.entrySet()) { + if (interceptor.getValue().test(link)) { + logger.trace("Remote link[{}] close intercepted and handled by interceptor: {}", link.getName(), interceptor.getKey()); + return true; + } + } + + return false; + } + + protected void signalFederationStarted() { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).federationStarted(this); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("federationStarted", t); + } + } + + protected void signalFederationStopped() { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).federationStopped(this); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("federationStopped", t); + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressConsumer.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressConsumer.java new file mode 100644 index 0000000000..c0e05b8c60 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressConsumer.java @@ -0,0 +1,497 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_DELAY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_MSG_COUNT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_ADDRESS_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.FEDERATED_ADDRESS_SOURCE_PROPERTIES; + +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.AMQP_LINK_INITIALIZER_KEY; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.DETACH_FORCED; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.NOT_FOUND; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.RESOURCE_DELETED; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.Message; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.core.server.AddressQueryResult; +import org.apache.activemq.artemis.core.server.transformer.Transformer; +import org.apache.activemq.artemis.core.transaction.Transaction; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPNotFoundException; +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationConsumerInternal; +import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpJmsSelectorFilter; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerReceiverContext; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.Modified; +import org.apache.qpid.proton.amqp.messaging.Rejected; +import org.apache.qpid.proton.amqp.messaging.Released; +import org.apache.qpid.proton.amqp.messaging.Source; +import org.apache.qpid.proton.amqp.messaging.Target; +import org.apache.qpid.proton.amqp.messaging.TerminusDurability; +import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy; +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; +import org.apache.qpid.proton.amqp.transport.SenderSettleMode; +import org.apache.qpid.proton.engine.Delivery; +import org.apache.qpid.proton.engine.Link; +import org.apache.qpid.proton.engine.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Consumer implementation for Federated Addresses that receives from a remote + * AMQP peer and forwards those messages onto the internal broker Address for + * consumption by an attached consumers. + */ +public class AMQPFederationAddressConsumer implements FederationConsumerInternal { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // Redefined because AMQPMessage uses SimpleString in its annotations API for some reason. + private static final SimpleString MESSAGE_HOPS_ANNOTATION = + new SimpleString(AMQPFederationPolicySupport.MESSAGE_HOPS_ANNOTATION.toString()); + + // Desired capabilities that the federation receiver link needs the remote to offer in order + // for the federation receiver to be successfully opened. + private static final Symbol[] DESIRED_LINK_CAPABILITIES = new Symbol[] {FEDERATION_ADDRESS_RECEIVER}; + + private static final Symbol[] DEFAULT_OUTCOMES = new Symbol[]{Accepted.DESCRIPTOR_SYMBOL, Rejected.DESCRIPTOR_SYMBOL, + Released.DESCRIPTOR_SYMBOL, Modified.DESCRIPTOR_SYMBOL}; + + private final AMQPFederation federation; + private final AMQPFederationConsumerConfiguration configuration; + private final FederationConsumerInfo consumerInfo; + private final FederationReceiveFromAddressPolicy policy; + private final AMQPConnectionContext connection; + private final AMQPSessionContext session; + private final Predicate remoteCloseInterceptor = this::remoteLinkClosedInterceptor; + private final Transformer transformer; + + private AMQPFederatedAddressDeliveryReceiver receiver; + private Receiver protonReceiver; + private boolean started; + private volatile boolean closed; + private Consumer remoteCloseHandler; + + public AMQPFederationAddressConsumer(AMQPFederation federation, AMQPFederationConsumerConfiguration configuration, + AMQPSessionContext session, FederationConsumerInfo consumerInfo, FederationReceiveFromAddressPolicy policy) { + this.federation = federation; + this.consumerInfo = consumerInfo; + this.policy = policy; + this.connection = session.getAMQPConnectionContext(); + this.session = session; + this.configuration = configuration; + + final TransformerConfiguration transformerConfiguration = policy.getTransformerConfiguration(); + if (transformerConfiguration != null) { + this.transformer = federation.getServer().getServiceRegistry().getFederationTransformer(policy.getPolicyName(), transformerConfiguration); + } else { + this.transformer = (m) -> m; + } + } + + @Override + public Federation getFederation() { + return federation; + } + + @Override + public FederationConsumerInfo getConsumerInfo() { + return consumerInfo; + } + + /** + * @return the {@link FederationReceiveFromAddressPolicy} that initiated this consumer. + */ + public FederationReceiveFromAddressPolicy getPolicy() { + return policy; + } + + @Override + public synchronized void start() { + if (!started && !closed) { + started = true; + asyncCreateReceiver(); + } + } + + @Override + public synchronized void close() { + if (!closed) { + closed = true; + if (started) { + started = false; + connection.runLater(() -> { + federation.removeLinkClosedInterceptor(consumerInfo.getFqqn()); + + if (receiver != null) { + try { + receiver.close(false); + } catch (ActiveMQAMQPException e) { + } finally { + receiver = null; + } + } + + // Need to track the proton receiver and close it here as the default + // context implementation doesn't do that and could result in no detach + // being sent in some cases and possible resources leaks. + if (protonReceiver != null) { + try { + protonReceiver.close(); + } finally { + protonReceiver = null; + } + } + + connection.flush(); + }); + } + } + } + + @Override + public synchronized AMQPFederationAddressConsumer setRemoteClosedHandler(Consumer handler) { + if (started) { + throw new IllegalStateException("Cannot set a remote close handler after the consumer is started"); + } + + this.remoteCloseHandler = handler; + return this; + } + + protected boolean remoteLinkClosedInterceptor(Link link) { + if (link == protonReceiver && link.getRemoteCondition() != null && link.getRemoteCondition().getCondition() != null) { + final Symbol errorCondition = link.getRemoteCondition().getCondition(); + + // Cases where remote link close is not considered terminal, additional checks + // should be added as needed for cases where the remote has closed the link either + // during the attach or at some point later. + + if (RESOURCE_DELETED.equals(errorCondition)) { + // Remote side manually deleted this queue. + return true; + } else if (NOT_FOUND.equals(errorCondition)) { + // Remote did not have a queue that matched. + return true; + } else if (DETACH_FORCED.equals(errorCondition)) { + // Remote operator forced the link to detach. + return true; + } + } + + return false; + } + + private void signalBeforeFederationConsumerMessageHandled(Message message) throws ActiveMQException { + try { + federation.getServer().callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).beforeFederationConsumerMessageHandled(this, message); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("beforeFederationConsumerMessageHandled", t); + } + } + + private void signalAfterFederationConsumerMessageHandled(Message message) throws ActiveMQException { + try { + federation.getServer().callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).afterFederationConsumerMessageHandled(this, message); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("afterFederationConsumerMessageHandled", t); + } + } + + private String generateLinkName() { + return "federation-" + federation.getName() + + "-address-receiver-" + consumerInfo.getAddress() + + "-" + federation.getServer().getNodeID(); + } + + private void asyncCreateReceiver() { + connection.runLater(() -> { + if (closed) { + return; + } + + try { + final Receiver protonReceiver = session.getSession().receiver(generateLinkName()); + final Target target = new Target(); + final Source source = new Source(); + final String address = consumerInfo.getAddress(); + + if (RoutingType.ANYCAST.equals(consumerInfo.getRoutingType())) { + source.setCapabilities(AmqpSupport.QUEUE_CAPABILITY); + } else { + source.setCapabilities(AmqpSupport.TOPIC_CAPABILITY); + } + + source.setOutcomes(Arrays.copyOf(DEFAULT_OUTCOMES, DEFAULT_OUTCOMES.length)); + source.setDurable(TerminusDurability.NONE); + source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH); + source.setAddress(address); + + if (consumerInfo.getFilterString() != null && !consumerInfo.getFilterString().isEmpty()) { + final AmqpJmsSelectorFilter jmsFilter = new AmqpJmsSelectorFilter(consumerInfo.getFilterString()); + final Map filtersMap = new HashMap<>(); + filtersMap.put(AmqpSupport.JMS_SELECTOR_KEY, jmsFilter); + + source.setFilter(filtersMap); + } + + target.setAddress(address); + + final Map addressSourceProperties = new HashMap<>(); + // If the remote needs to create the address then it should apply these + // settings during the create. + addressSourceProperties.put(ADDRESS_AUTO_DELETE, policy.isAutoDelete()); + addressSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, policy.getAutoDeleteDelay()); + addressSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, policy.getAutoDeleteMessageCount()); + + final Map receiverProperties = new HashMap<>(); + receiverProperties.put(FEDERATED_ADDRESS_SOURCE_PROPERTIES, addressSourceProperties); + + protonReceiver.setSenderSettleMode(SenderSettleMode.UNSETTLED); + protonReceiver.setReceiverSettleMode(ReceiverSettleMode.FIRST); + protonReceiver.setDesiredCapabilities(DESIRED_LINK_CAPABILITIES); + protonReceiver.setProperties(receiverProperties); + protonReceiver.setTarget(target); + protonReceiver.setSource(source); + protonReceiver.open(); + + final ScheduledFuture openTimeoutTask; + final AtomicBoolean openTimedOut = new AtomicBoolean(false); + + if (federation.getLinkAttachTimeout() > 0) { + openTimeoutTask = federation.getServer().getScheduledPool().schedule(() -> { + openTimedOut.set(true); + federation.signalResourceCreateError(ActiveMQAMQPProtocolMessageBundle.BUNDLE.brokerConnectionTimeout()); + }, federation.getLinkAttachTimeout(), TimeUnit.SECONDS); + } else { + openTimeoutTask = null; + } + + this.protonReceiver = protonReceiver; + + protonReceiver.attachments().set(AMQP_LINK_INITIALIZER_KEY, Runnable.class, () -> { + try { + if (openTimeoutTask != null) { + openTimeoutTask.cancel(false); + } + + if (openTimedOut.get()) { + return; + } + + // Remote must support federation receivers otherwise we fail the connection unless the + // Attach indicates that a detach is incoming in which case we just allow the normal handling + // to occur. + if (protonReceiver.getRemoteSource() != null && !AmqpSupport.verifyOfferedCapabilities(protonReceiver)) { + federation.signalResourceCreateError( + ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(DESIRED_LINK_CAPABILITIES))); + return; + } + + // Intercept remote close and check for valid reasons for remote closure such as + // the remote peer not having a matching queue for this subscription or from an + // operator manually closing the link. + federation.addLinkClosedInterceptor(consumerInfo.getFqqn(), remoteCloseInterceptor); + + receiver = new AMQPFederatedAddressDeliveryReceiver(session, consumerInfo, protonReceiver); + + if (protonReceiver.getRemoteSource() != null) { + logger.debug("AMQP Federation {} address consumer {} completed open", federation.getName(), consumerInfo); + } else { + logger.debug("AMQP Federation {} address consumer {} rejected by remote", federation.getName(), consumerInfo); + } + + session.addReceiver(protonReceiver, (session, protonRcvr) -> { + return this.receiver; + }); + } catch (Exception e) { + federation.signalError(e); + } + }); + } catch (Exception e) { + federation.signalError(e); + } + + connection.flush(); + }); + } + + private static AMQPMessage incrementMessageHops(AMQPMessage message) { + Object hops = message.getAnnotation(MESSAGE_HOPS_ANNOTATION); + if (hops == null) { + message.setAnnotation(MESSAGE_HOPS_ANNOTATION, 1); + } else { + Number numHops = (Number) hops; + message.setAnnotation(MESSAGE_HOPS_ANNOTATION, numHops.intValue() + 1); + } + + return message; + } + + /** + * Wrapper around the standard receiver context that provides federation specific entry + * points and customizes inbound delivery handling for this Address receiver. + */ + private class AMQPFederatedAddressDeliveryReceiver extends ProtonServerReceiverContext { + + private final SimpleString cachedAddress; + + /** + * Creates the federation receiver instance. + * + * @param session + * The server session context bound to the receiver instance. + * @param receiver + * The proton receiver that will be wrapped in this server context instance. + */ + AMQPFederatedAddressDeliveryReceiver(AMQPSessionContext session, FederationConsumerInfo consumerInfo, Receiver receiver) { + super(session.getSessionSPI(), session.getAMQPConnectionContext(), session, receiver); + + this.cachedAddress = SimpleString.toSimpleString(consumerInfo.getAddress()); + } + + @Override + public void close(boolean remoteLinkClose) throws ActiveMQAMQPException { + super.close(remoteLinkClose); + + if (remoteLinkClose && remoteCloseHandler != null) { + try { + remoteCloseHandler.accept(AMQPFederationAddressConsumer.this); + } catch (Exception e) { + logger.debug("User remote closed handler threw error: ", e); + } finally { + remoteCloseHandler = null; + } + } + } + + @Override + protected Runnable createCreditRunnable(AMQPConnectionContext connection) { + // We defer to the configuration instance as opposed to the base class version that reads + // from the connection this allows us to defer to configured policy properties that specify + // credit. This also allows consumers created on the remote side of a federation connection + // to read from properties sent from the federation source that indicate the values that are + // configured on the local side. + return createCreditRunnable(configuration.getReceiverCredits(), configuration.getReceiverCreditsLow(), receiver, connection, this); + } + + @Override + protected int getConfiguredMinLargeMessageSize(AMQPConnectionContext connection) { + // Looks at policy properties first before looking at federation configuration and finally + // going to the base connection context to read the URI configuration. + return configuration.getLargeMessageThreshold(); + } + + @Override + public void initialize() throws Exception { + initialized = true; + + final Target target = (Target) receiver.getRemoteTarget(); + + // Match the settlement mode of the remote instead of relying on the default of MIXED. + receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode()); + + // We don't currently support SECOND so enforce that the answer is always FIRST + receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST); + + // the target will have an address and it will naturally have a Target otherwise + // the remote is misbehaving and we close it. + if (target == null || target.getAddress() == null || target.getAddress().isEmpty()) { + throw new ActiveMQAMQPInternalErrorException("Remote should have sent an valid Target but we got: " + target); + } + + address = SimpleString.toSimpleString(target.getAddress()); + defRoutingType = getRoutingType(target.getCapabilities(), address); + + try { + final AddressQueryResult result = sessionSPI.addressQuery(address, defRoutingType, false); + + // We initiated this link so the target should refer to an address that definitely exists + // however there is a chance the address was removed in the interim. + if (!result.isExists()) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.addressDoesntExist(address.toString()); + } + } catch (ActiveMQAMQPNotFoundException e) { + throw e; + } catch (Exception e) { + logger.debug(e.getMessage(), e); + throw new ActiveMQAMQPInternalErrorException(e.getMessage(), e); + } + + flow(); + } + + @Override + protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) { + try { + if (logger.isTraceEnabled()) { + logger.trace("AMQP Federation {} address consumer {} dispatching incoming message: {}", + federation.getName(), consumerInfo, message); + } + + final Message theMessage = transformer.transform(incrementMessageHops(message)); + + if (theMessage != message && logger.isTraceEnabled()) { + logger.trace("The transformer {} replaced the original message {} with a new instance {}", + transformer, message, theMessage); + } + + signalBeforeFederationConsumerMessageHandled(theMessage); + sessionSPI.serverSend(this, tx, receiver, delivery, cachedAddress, routingContext, theMessage); + signalAfterFederationConsumerMessageHandled(theMessage); + } catch (Exception e) { + logger.warn("Inbound delivery for {} encountered an error: {}", consumerInfo, e.getMessage(), e); + deliveryFailed(delivery, receiver, e); + } + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressPolicyManager.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressPolicyManager.java new file mode 100644 index 0000000000..9f82fbbca8 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressPolicyManager.java @@ -0,0 +1,217 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.lang.invoke.MethodHandles; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.core.server.Divert; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationAddressPolicyManager; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationConsumerInternal; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationGenericConsumerInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The AMQP Federation implementation of an federation address policy manager. + */ +public class AMQPFederationAddressPolicyManager extends FederationAddressPolicyManager { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected final AMQPFederation federation; + protected final AMQPFederationConsumerConfiguration configuration; + + protected final String remoteQueueFilter; + + public AMQPFederationAddressPolicyManager(AMQPFederation federation, FederationReceiveFromAddressPolicy addressPolicy) throws ActiveMQException { + super(federation, addressPolicy); + + this.federation = federation; + + if (policy.getMaxHops() > 0) { + this.remoteQueueFilter = "\"m." + AMQPFederationPolicySupport.MESSAGE_HOPS_ANNOTATION.toString() + + "\" IS NULL OR \"m." + AMQPFederationPolicySupport.MESSAGE_HOPS_ANNOTATION.toString() + + "\"<" + policy.getMaxHops(); + } else { + this.remoteQueueFilter = null; + } + + this.configuration = new AMQPFederationConsumerConfiguration(federation, policy.getProperties()); + } + + @Override + protected FederationGenericConsumerInfo createConsumerInfo(AddressInfo address) { + return FederationGenericConsumerInfo.build(address.getName().toString(), + generateQueueName(address), + address.getRoutingType(), + remoteQueueFilter, + federation, + policy); + } + + protected String generateQueueName(AddressInfo address) { + return "federation." + federation.getName() + ".address." + address.getName() + ".node." + server.getNodeID(); + } + + @Override + protected FederationConsumerInternal createFederationConsumer(FederationConsumerInfo consumerInfo) { + Objects.requireNonNull(consumerInfo, "Federation Address consumer information object was null"); + + if (logger.isTraceEnabled()) { + logger.trace("AMQP Federation {} creating address consumer: {} for policy: {}", federation.getName(), consumerInfo, policy.getPolicyName()); + } + + // Don't initiate anything yet as the caller might need to register error handlers etc + // before the attach is sent otherwise they could miss the failure case. + return new AMQPFederationAddressConsumer(federation, configuration, federation.getSessionContext(), consumerInfo, policy); + } + + @Override + protected boolean testIfAddressMatchesPolicy(AddressInfo addressInfo) { + if (!policy.test(addressInfo)) { + return false; + } + + // Address consumers can't pull as we have no real metric to indicate when / how much + // we should pull so instead we refuse to match if credit set to zero. + if (federation.getReceiverCredits() <= 0) { + logger.debug("Federation address policy rejecting match on {} because credit is set to zero:", addressInfo.getName()); + return false; + } else { + return true; + } + } + + @Override + protected void signalBeforeCreateFederationConsumer(FederationConsumerInfo info) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).beforeCreateFederationConsumer(info); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("beforeCreateFederationConsumer", t); + } + } + + @Override + protected void signalAfterCreateFederationConsumer(FederationConsumer consumer) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).afterCreateFederationConsumer(consumer); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("afterCreateFederationConsumer", t); + } + } + + @Override + protected void signalBeforeCloseFederationConsumer(FederationConsumer consumer) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).beforeCloseFederationConsumer(consumer); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("beforeCloseFederationConsumer", t); + } + } + + @Override + protected void signalAfterCloseFederationConsumer(FederationConsumer consumer) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).afterCloseFederationConsumer(consumer); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("afterCloseFederationConsumer", t); + } + } + + @Override + protected final boolean isPluginBlockingFederationConsumerCreate(AddressInfo address) { + final AtomicBoolean canCreate = new AtomicBoolean(true); + + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + if (canCreate.get()) { + canCreate.set(((ActiveMQServerAMQPFederationPlugin) plugin).shouldCreateFederationConsumerForAddress(address)); + } + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("shouldCreateFederationConsumerForAddress", t); + } + + return !canCreate.get(); + } + + @Override + protected final boolean isPluginBlockingFederationConsumerCreate(Divert divert, Queue queue) { + final AtomicBoolean canCreate = new AtomicBoolean(true); + + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + if (canCreate.get()) { + canCreate.set(((ActiveMQServerAMQPFederationPlugin) plugin).shouldCreateFederationConsumerForDivert(divert, queue)); + } + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("shouldCreateFederationConsumerForDivert", t); + } + + return !canCreate.get(); + } + + @Override + protected final boolean isPluginBlockingFederationConsumerCreate(Queue queue) { + final AtomicBoolean canCreate = new AtomicBoolean(true); + + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + if (canCreate.get()) { + canCreate.set(((ActiveMQServerAMQPFederationPlugin) plugin).shouldCreateFederationConsumerForQueue(queue)); + } + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("shouldCreateFederationConsumerForQueue", t); + } + + return !canCreate.get(); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressSenderController.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressSenderController.java new file mode 100644 index 0000000000..0b0cf957d9 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationAddressSenderController.java @@ -0,0 +1,213 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_DELAY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_MSG_COUNT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_ADDRESS_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.FEDERATED_ADDRESS_SOURCE_PROPERTIES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederation.FEDERATION_INSTANCE_RECORD; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.QUEUE_CAPABILITY; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.TOPIC_CAPABILITY; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; +import org.apache.activemq.artemis.api.core.ActiveMQExceptionType; +import org.apache.activemq.artemis.api.core.ActiveMQSecurityException; +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.server.AddressQueryResult; +import org.apache.activemq.artemis.core.server.Consumer; +import org.apache.activemq.artemis.core.server.QueueQueryResult; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPIllegalStateException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; +import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContext; +import org.apache.activemq.artemis.protocol.amqp.proton.SenderController; +import org.apache.activemq.artemis.selector.filter.FilterException; +import org.apache.activemq.artemis.selector.impl.SelectorParser; +import org.apache.qpid.proton.amqp.DescribedType; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.Source; +import org.apache.qpid.proton.amqp.transport.AmqpError; +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; +import org.apache.qpid.proton.engine.Connection; +import org.apache.qpid.proton.engine.Sender; + +/** + * {@link SenderController} used when an AMQP federation Address receiver is created + * and this side of the connection needs to create a matching sender. The address sender + * controller must check on initialization if the address exists and if not it should + * create it using the configuration values supplied in the link source properties that + * control the lifetime of the address once the link is closed. + */ +public final class AMQPFederationAddressSenderController implements SenderController { + + // Capabilities offered to the attaching federation receiver link that indicate this sender + // is a federation sender which allows the link open to complete. + private static final Symbol[] OFFERED_LINK_CAPABILITIES = new Symbol[] {FEDERATION_ADDRESS_RECEIVER}; + + private final AMQPSessionContext session; + private final AMQPSessionCallback sessionSPI; + + public AMQPFederationAddressSenderController(AMQPSessionContext session) { + this.session = session; + this.sessionSPI = session.getSessionSPI(); + } + + public AMQPSessionContext getSessionContext() { + return session; + } + + public AMQPSessionCallback getSessionCallback() { + return sessionSPI; + } + + @SuppressWarnings("unchecked") + @Override + public Consumer init(ProtonServerSenderContext senderContext) throws Exception { + final Sender sender = senderContext.getSender(); + final Source source = (Source) sender.getRemoteSource(); + final String selector; + final SimpleString queueName = SimpleString.toSimpleString(sender.getName()); + final Connection protonConnection = sender.getSession().getConnection(); + final org.apache.qpid.proton.engine.Record attachments = protonConnection.attachments(); + + if (attachments.get(FEDERATION_INSTANCE_RECORD, AMQPFederation.class) == null) { + throw new ActiveMQAMQPIllegalStateException("Cannot create a federation link from non-federation connection"); + } + + // Match the settlement mode of the remote instead of relying on the default of MIXED. + sender.setSenderSettleMode(sender.getRemoteSenderSettleMode()); + // We don't currently support SECOND so enforce that the answer is always FIRST + sender.setReceiverSettleMode(ReceiverSettleMode.FIRST); + // We need to offer back that we support federation for the remote to complete the attach. + sender.setOfferedCapabilities(OFFERED_LINK_CAPABILITIES); + + final Map addressSourceProperties; + + if (sender.getRemoteProperties() == null || !sender.getRemoteProperties().containsKey(FEDERATED_ADDRESS_SOURCE_PROPERTIES)) { + addressSourceProperties = Collections.EMPTY_MAP; + } else { + addressSourceProperties = (Map) sender.getRemoteProperties().get(FEDERATED_ADDRESS_SOURCE_PROPERTIES); + } + + final boolean autoDelete = (boolean) addressSourceProperties.getOrDefault(ADDRESS_AUTO_DELETE, false); + final long autoDeleteDelay = ((Number) addressSourceProperties.getOrDefault(ADDRESS_AUTO_DELETE_DELAY, 0)).longValue(); + final long autoDeleteMsgCount = ((Number) addressSourceProperties.getOrDefault(ADDRESS_AUTO_DELETE_MSG_COUNT, 0)).longValue(); + + // An address receiver may opt to filter on things like max message hops so we must + // check for a filter here and apply it if it exists. + final Map.Entry filter = AmqpSupport.findFilter(source.getFilter(), AmqpSupport.JMS_SELECTOR_FILTER_IDS); + + if (filter != null) { + selector = filter.getValue().getDescribed().toString(); + try { + SelectorParser.parse(selector); + } catch (FilterException e) { + throw new ActiveMQAMQPException(AmqpError.INVALID_FIELD, "Invalid filter", ActiveMQExceptionType.INVALID_FILTER_EXPRESSION); + } + } else { + selector = null; + } + + final SimpleString address = SimpleString.toSimpleString(source.getAddress()); + final AddressQueryResult addressQueryResult; + + try { + addressQueryResult = sessionSPI.addressQuery(address, RoutingType.MULTICAST, true); + } catch (ActiveMQSecurityException e) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.securityErrorCreatingConsumer(e.getMessage()); + } catch (ActiveMQAMQPException e) { + throw e; + } catch (Exception e) { + throw new ActiveMQAMQPInternalErrorException(e.getMessage(), e); + } + + if (!addressQueryResult.isExists()) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.sourceAddressDoesntExist(); + } + + final Set routingTypes = addressQueryResult.getRoutingTypes(); + + // Strictly enforce the MULTICAST nature of current address federation support. + if (!routingTypes.contains(RoutingType.MULTICAST)) { + throw new ActiveMQAMQPIllegalStateException("Address " + address + " is not configured for MULTICAST support"); + } + + final RoutingType routingType = getRoutingType(source); + + // Recover or create the queue we use to reflect the messages sent to the address to the remote + QueueQueryResult queueQuery = sessionSPI.queueQuery(queueName, routingType, false); + if (!queueQuery.isExists()) { + final QueueConfiguration configuration = new QueueConfiguration(queueName); + + configuration.setAddress(address); + configuration.setRoutingType(routingType); + configuration.setAutoCreateAddress(false); + configuration.setMaxConsumers(-1); + configuration.setPurgeOnNoConsumers(false); + configuration.setFilterString(selector); + configuration.setDurable(true); + configuration.setAutoCreated(false); + configuration.setAutoDelete(autoDelete); + configuration.setAutoDeleteDelay(autoDeleteDelay); + configuration.setAutoDeleteMessageCount(autoDeleteMsgCount); + + // Try and create it and then later we will validate fully that it matches our expectations + // since we could lose a race with some other resource creating its own resources. + queueQuery = sessionSPI.queueQuery(configuration, true); + } + + if (!queueQuery.getAddress().equals(address)) { + throw new ActiveMQAMQPIllegalStateException("Requested queue: " + queueName + " for federation of address: " + address + + ", but it is already mapped to a different address: " + queueQuery.getAddress()); + } + + return (Consumer) sessionSPI.createSender(senderContext, queueName, null, false); + } + + @Override + public void close() throws Exception { + // Currently there isn't anything needed on close of this controller. + } + + private static RoutingType getRoutingType(Source source) { + if (source != null) { + if (source.getCapabilities() != null) { + for (Symbol capability : source.getCapabilities()) { + if (TOPIC_CAPABILITY.equals(capability)) { + return RoutingType.MULTICAST; + } else if (QUEUE_CAPABILITY.equals(capability)) { + return RoutingType.ANYCAST; + } + } + } + } + + return ActiveMQDefaultConfiguration.getDefaultRoutingType(); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationCommandDispatcher.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationCommandDispatcher.java new file mode 100644 index 0000000000..33b977c3fd --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationCommandDispatcher.java @@ -0,0 +1,137 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.util.Objects; + +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.Consumer; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContext; +import org.apache.activemq.artemis.protocol.amqp.proton.SenderController; +import org.apache.qpid.proton.engine.Sender; + +/** + * A {@link SenderController} implementation used by the AMQP federation control link + * to encode and send federation policies or other commands to the remote side of the + * AMQP federation instance. + */ +public class AMQPFederationCommandDispatcher implements SenderController { + + private final Sender sender; + private final AMQPSessionCallback session; + private final ActiveMQServer server; + + AMQPFederationCommandDispatcher(Sender sender, ActiveMQServer server, AMQPSessionCallback session) { + this.session = session; + this.sender = sender; + this.server = server; + } + + /** + * Sends the given {@link FederationReceiveFromQueuePolicy} instance using the control + * link which should instruct the remote to begin federation operations back to this + * peer for matching remote queues with demand. + * + * @param policy + * The policy to encode and send over the federation control link. + * + * @throws Exception if an error occurs during the control and send operation. + */ + public void sendPolicy(FederationReceiveFromQueuePolicy policy) throws Exception { + Objects.requireNonNull(policy, "Cannot encode and send a null policy instance."); + + final AMQPMessage command = + AMQPFederationPolicySupport.encodeQueuePolicyControlMessage(policy); + + sendCommand(command); + } + + /** + * Sends the given {@link FederationReceiveFromAddressPolicy} instance using the control + * link which should instruct the remote to begin federation operations back to this + * peer for matching remote address. + * + * @param policy + * The policy to encode and send over the federation control link. + * + * @throws Exception if an error occurs during the control and send operation. + */ + public void sendPolicy(FederationReceiveFromAddressPolicy policy) throws Exception { + Objects.requireNonNull(policy, "Cannot encode and send a null policy instance."); + + final AMQPMessage command = + AMQPFederationPolicySupport.encodeAddressPolicyControlMessage(policy); + + sendCommand(command); + } + + /** + * Raw send command that accepts and {@link AMQPMessage} instance and routes it using the + * server post office instance. + * + * @param command + * The command message to send to the previously created control address. + * + * @throws Exception if an error occurs during the message send. + */ + public void sendCommand(AMQPMessage command) throws Exception { + Objects.requireNonNull(command, "Null command message is not expected and constitutes an error condition"); + + command.setAddress(getControlLinkAddress()); + + server.getPostOffice().route(command, true); + } + + @Override + public Consumer init(ProtonServerSenderContext senderContext) throws Exception { + // Get the dynamically generated name to use for local creation of a matching temporary + // queue that we will send control message to and the broker will dispatch as remote + // credit is made available. + final SimpleString queueName = SimpleString.toSimpleString(sender.getRemoteTarget().getAddress()); + + try { + session.createTemporaryQueue(queueName, RoutingType.ANYCAST); + } catch (Exception e) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.errorCreatingTemporaryQueue(e.getMessage()); + } + + return (Consumer) session.createSender(senderContext, queueName, null, false); + } + + @Override + public void close() throws Exception { + // Make a best effort to remove the temporary queue used for control commands on close. + final SimpleString queueName = SimpleString.toSimpleString(sender.getRemoteTarget().getAddress()); + + try { + session.removeTemporaryQueue(queueName); + } catch (Exception e) { + // Ignored as the temporary queue should be removed on connection termination. + } + } + + private String getControlLinkAddress() { + return sender.getRemoteTarget().getAddress(); + } +} \ No newline at end of file diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationCommandProcessor.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationCommandProcessor.java new file mode 100644 index 0000000000..b2c18ecd1b --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationCommandProcessor.java @@ -0,0 +1,162 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.lang.invoke.MethodHandles; + +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.transaction.Transaction; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessageBrokerAccessor; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonAbstractReceiver; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.Target; +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; +import org.apache.qpid.proton.engine.Delivery; +import org.apache.qpid.proton.engine.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.OPERATION_TYPE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_QUEUE_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_ADDRESS_POLICY; + +/** + * A specialized AMQP Receiver that handles commands from a remote Federation connection such + * as handling incoming policies that should be applied to local addresses and queues. + */ +public class AMQPFederationCommandProcessor extends ProtonAbstractReceiver { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // Capabilities that are offered to the remote sender that indicate this receiver supports the + // control link functions which allows the link open to complete. + private static final Symbol[] OFFERED_LINK_CAPABILITIES = new Symbol[] {FEDERATION_CONTROL_LINK}; + + private static final int PROCESSOR_RECEIVER_CREDITS = 10; + private static final int PROCESSOR_RECEIVER_CREDITS_LOW = 3; + + private final ActiveMQServer server; + private final AMQPFederationTarget federation; + + /** + * Create the new federation command receiver + * + * @param federation + * The AMQP Federation instance that this command consumer resides in. + * @param session + * The associated session for this federation command consumer. + * @param receiver + * The proton {@link Receiver} that this command consumer reads from. + */ + public AMQPFederationCommandProcessor(AMQPFederationTarget federation, AMQPSessionContext session, Receiver receiver) { + super(session.getSessionSPI(), session.getAMQPConnectionContext(), session, receiver); + + this.server = protonSession.getServer(); + this.federation = federation; + } + + @Override + public void initialize() throws Exception { + initialized = true; + + // For any incoming control link we should gate keep and allow configuration to + // prevent any user from creating federation connections. + + final Target target = (Target) receiver.getRemoteTarget(); + + // Match the settlement mode of the remote instead of relying on the default of MIXED. + receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode()); + + // We don't currently support SECOND so enforce that the answer is always FIRST + receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST); + + if (target == null || !target.getDynamic()) { + throw new ActiveMQAMQPInternalErrorException("Remote Target did not arrive as dynamic node: " + target); + } + + // The target needs a unique address for the remote to send commands to which will get + // deleted on connection close so no state is retained between connections, we know our + // link name is unique and carries the federation name that created it so we reuse that + // as the address for the dynamic node. + target.setAddress(receiver.getName()); + + // We need to offer back that we support control link instructions for the remote to succeed in + // opening its sender link. + receiver.setOfferedCapabilities(OFFERED_LINK_CAPABILITIES); + + flow(); + } + + @Override + protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) { + logger.trace("{}::actualdelivery called for {}", server, message); + + delivery.setContext(message); + + try { + final Object eventType = AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, OPERATION_TYPE); + + if (ADD_QUEUE_POLICY.equals(eventType)) { + final FederationReceiveFromQueuePolicy policy = + AMQPFederationPolicySupport.decodeReceiveFromQueuePolicy(message, federation.getWildcardConfiguration()); + + federation.addQueueMatchPolicy(policy); + } else if (ADD_ADDRESS_POLICY.equals(eventType)) { + final FederationReceiveFromAddressPolicy policy = + AMQPFederationPolicySupport.decodeReceiveFromAddressPolicy(message, federation.getWildcardConfiguration()); + + federation.addAddressMatchPolicy(policy); + } else { + federation.signalError(new ActiveMQAMQPInternalErrorException("Remote sent unknown command.")); + return; + } + + delivery.disposition(Accepted.getInstance()); + delivery.settle(); + + flow(); + + connection.flush(); + } catch (Throwable e) { + logger.warn(e.getMessage(), e); + federation.signalError( + new ActiveMQAMQPInternalErrorException("Error while processing incoming control message: " + e.getMessage() )); + } + } + + @Override + protected Runnable createCreditRunnable(AMQPConnectionContext connection) { + // The command processor is not bound to the configurable credit on the connection as it could be set + // to zero if trying to create pull federation consumers so we avoid any chance of that happening as + // otherwise there would be no credit granted for the remote to send us commands.. + return createCreditRunnable(PROCESSOR_RECEIVER_CREDITS, PROCESSOR_RECEIVER_CREDITS_LOW, receiver, connection, this); + } + + @Override + public void flow() { + creditRunnable.run(); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConfiguration.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConfiguration.java new file mode 100644 index 0000000000..2e6da7a093 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConfiguration.java @@ -0,0 +1,135 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LARGE_MESSAGE_THRESHOLD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LINK_ATTACH_TIMEOUT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS_LOW; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.qpid.proton.engine.Receiver; + +/** + * A configuration class that contains API for getting federation specific + * configuration either from a {@link Map} of configuration elements or from + * the connection associated with the federation instance, or possibly from a + * set default value. + */ +public final class AMQPFederationConfiguration { + + /** + * Default timeout value (in seconds) used to control when a link attach is considered to have + * failed due to not responding to an attach request. + */ + public static final int DEFAULT_LINK_ATTACH_TIMEOUT = 30; + + private final Map properties; + private final AMQPConnectionContext connection; + + @SuppressWarnings("unchecked") + public AMQPFederationConfiguration(AMQPConnectionContext connection, Map properties) { + Objects.requireNonNull(connection, "Connection provided cannot be null"); + + this.connection = connection; + + if (properties != null && !properties.isEmpty()) { + this.properties = new HashMap<>(properties); + } else { + this.properties = Collections.EMPTY_MAP; + } + } + + /** + * @return the credit batch size offered to a {@link Receiver} link. + */ + public int getReceiverCredits() { + final Object property = properties.get(RECEIVER_CREDITS); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return connection.getAmqpCredits(); + } + } + + /** + * @return the number of remaining credits on a {@link Receiver} before the batch is replenished. + */ + public int getReceiverCreditsLow() { + final Object property = properties.get(RECEIVER_CREDITS_LOW); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return connection.getAmqpLowCredits(); + } + } + + /** + * @return the size in bytes of an incoming message after which the {@link Receiver} treats it as large. + */ + public int getLargeMessageThreshold() { + final Object property = properties.get(LARGE_MESSAGE_THRESHOLD); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return connection.getProtocolManager().getAmqpMinLargeMessageSize(); + } + } + + /** + * @return the size in bytes of an incoming message after which the {@link Receiver} treats it as large. + */ + public int getLinkAttachTimeout() { + final Object property = properties.get(LINK_ATTACH_TIMEOUT); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return DEFAULT_LINK_ATTACH_TIMEOUT; + } + } + + /** + * Enumerate the configuration options in this configuration object and return a {@link Map} that + * contains the values which can be sent to a remote peer + * + * @return a Map that contains the values of each configuration option. + */ + public Map toConfigurationMap() { + final Map configMap = new HashMap<>(); + + configMap.put(RECEIVER_CREDITS, getReceiverCredits()); + configMap.put(RECEIVER_CREDITS_LOW, getReceiverCreditsLow()); + configMap.put(LARGE_MESSAGE_THRESHOLD, getLargeMessageThreshold()); + configMap.put(LINK_ATTACH_TIMEOUT, getLinkAttachTimeout()); + + return configMap; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConstants.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConstants.java new file mode 100644 index 0000000000..071d7305ad --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConstants.java @@ -0,0 +1,200 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.util.List; +import java.util.Map; + +import org.apache.activemq.artemis.core.server.transformer.Transformer; +import org.apache.qpid.proton.amqp.Symbol; + +/** + * Constants class for values used in the AMQP Federation implementation. + */ +public final class AMQPFederationConstants { + + /** + * Address used by a remote broker instance to validate that an incoming federation connection + * has access right to perform federation operations. The user that connects to the AMQP federation + * endpoint and attempt to create the control link must have write access to this address. + */ + public static final String FEDERATION_CONTROL_LINK_VALIDATION_ADDRESS = "$ACTIVEMQ_ARTEMIS_FEDERATION"; + + /** + * A desired capability added to the federation control link that must be offered + * in return for a federation connection to be successfully established. + */ + public static final Symbol FEDERATION_CONTROL_LINK = Symbol.getSymbol("AMQ_FEDERATION_CONTROL_LINK"); + + /** + * Property name used to embed a nested map of properties meant to be applied if the federation + * resources created on the remote end of the control link if configured to do so. These properties + * essentially carry local configuration to the remote side that would otherwise use broker defaults + * and not match behaviors of resources created on the local side of the connection. + */ + public static final Symbol FEDERATION_CONFIGURATION = Symbol.getSymbol("federation-configuration"); + + /** + * Property value that can be applied to federation configuration that controls the timeout value + * for a link attach to complete before the attach attempt is considered to have failed. The value + * is configured in seconds (default is 30 seconds). + */ + public static final String LINK_ATTACH_TIMEOUT = "attach-timeout"; + + /** + * Configuration property that defines the amount of credits to batch to an AMQP receiver link + * and the top up limit when sending more credit once the credits are determined to be running + * low. this can be sent to the peer so that dual federation configurations share the same + * configuration on both sides of the connection. + */ + public static final String RECEIVER_CREDITS = "amqpCredits"; + + /** + * A low water mark for receiver credits that indicates more should be sent to top it up to the + * original credit batch size. this can be sent to the peer so that dual federation configurations + * share the same configuration on both sides of the connection. + */ + public static final String RECEIVER_CREDITS_LOW = "amqpLowCredits"; + + /** + * Configuration property used to convey the local side value to use when considering if a message + * is a large message, this can be sent to the peer so that dual federation configurations share + * the same configuration on both sides of the connection. + */ + public static final String LARGE_MESSAGE_THRESHOLD = "minLargeMessageSize"; + + /** + * A desired capability added to the federation queue receiver link that must be offered + * in return for a federation queue receiver to be successfully opened. On the remote the + * presence of this capability indicates that the matching queue should be present on the + * remote and its absence constitutes a failure that should result in the attach request + * being failed. + */ + public static final Symbol FEDERATION_QUEUE_RECEIVER = Symbol.getSymbol("AMQ_FEDERATION_QUEUE_RECEIVER"); + + /** + * A desired capability added to the federation address receiver link that must be offered + * in return for a federation address receiver to be successfully opened. + */ + public static final Symbol FEDERATION_ADDRESS_RECEIVER = Symbol.getSymbol("AMQ_FEDERATION_ADDRESS_RECEIVER"); + + /** + * Property added to the receiver properties when opening an AMQP federation address or queue consumer + * that indicates the consumer priority that should be used when creating the remote consumer. The + * value assign to the properties {@link Map} is a signed integer value. + */ + public static final Symbol FEDERATION_RECEIVER_PRIORITY = Symbol.getSymbol("priority"); + + /** + * Commands sent across the control link will each carry an operation type to indicate + * the desired action the remote should take upon receipt of the command. The type of + * command infers the payload of the structure of the message payload. + */ + public static final Symbol OPERATION_TYPE = Symbol.getSymbol("x-opt-amq-federation-op-type"); + + /** + * Indicates that the message carries a federation queue match policy that should be + * added to the remote for reverse federation of matching queue from the remote peer. + */ + public static final String ADD_QUEUE_POLICY = "ADD_QUEUE_POLICY"; + + /** + * Indicates that the message carries a federation address match policy that should be + * added to the remote for reverse federation of matching queue from the remote peer. + */ + public static final String ADD_ADDRESS_POLICY = "ADD_ADDRESS_POLICY"; + + /** + * Both Queue and Address policies carry a unique name that will always be encoded. + */ + public static final String POLICY_NAME = "policy-name"; + + /** + * Queue policy includes are encoded as a {@link List} of flattened key / value pairs when configured. + */ + public static final String QUEUE_INCLUDES = "queue-includes"; + + /** + * Queue policy excludes are encoded as a {@link List} of flattened key / value pairs when configured. + */ + public static final String QUEUE_EXCLUDES = "queue-excludes"; + + /** + * Encodes a boolean value that indicates if the include federation option should be enabled. + */ + public static final String QUEUE_INCLUDE_FEDERATED = "include-federated"; + + /** + * Encodes a signed integer value that adjusts the priority of the any created queue receivers. + */ + public static final String QUEUE_PRIORITY_ADJUSTMENT = "priority-adjustment"; + + /** + * Address policy includes are encoded as a {@link List} of string entries when configured. + */ + public static final String ADDRESS_INCLUDES = "address-includes"; + + /** + * Address policy excludes are encoded as a {@link List} of string entries when configured. + */ + public static final String ADDRESS_EXCLUDES = "address-excludes"; + + /** + * Encodes a boolean value that indicates if queue auto delete option should be enabled. + */ + public static final String ADDRESS_AUTO_DELETE = "auto-delete"; + + /** + * Encodes a signed long value that controls the delay before auto deletion if auto delete is enabled. + */ + public static final String ADDRESS_AUTO_DELETE_DELAY = "auto-delete-delay"; + + /** + * Encodes a signed long value that controls the message count value that allows for address auto delete. + */ + public static final String ADDRESS_AUTO_DELETE_MSG_COUNT = "auto-delete-msg-count"; + + /** + * Encodes a signed integer value that controls the maximum number of hops allowed for federated messages. + */ + public static final String ADDRESS_MAX_HOPS = "max-hops"; + + /** + * Encodes boolean value that controls if the address federation should include divert bindings. + */ + public static final String ADDRESS_ENABLE_DIVERT_BINDINGS = "enable-divert-bindings"; + + /** + * Encodes a {@link Map} of String keys and values that are carried along in the federation + * policy (address or queue). These values can be used to add extended configuration to the + * policy object such as overriding settings from the connection URI. + */ + public static final String POLICY_PROPERTIES_MAP = "policy-properties-map"; + + /** + * Encodes a string value carrying the name of the {@link Transformer} class to use. + */ + public static final String TRANSFORMER_CLASS_NAME = "transformer-class-name"; + + /** + * Encodes a {@link Map} of String keys and values that are applied to the transformer + * configuration for the policy. + */ + public static final String TRANSFORMER_PROPERTIES_MAP = "transformer-properties-map"; + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConsumerConfiguration.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConsumerConfiguration.java new file mode 100644 index 0000000000..0842fedaa9 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationConsumerConfiguration.java @@ -0,0 +1,94 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LARGE_MESSAGE_THRESHOLD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LINK_ATTACH_TIMEOUT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS_LOW; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration options applied to a consumer created from federation policies + * for address or queue federation. The options first check the policy properties + * for matching configuration settings before looking at the federation's own + * configuration for the options managed here. + */ +public final class AMQPFederationConsumerConfiguration { + + private final Map properties; + private final AMQPFederation federation; + + @SuppressWarnings("unchecked") + public AMQPFederationConsumerConfiguration(AMQPFederation federation, Map properties) { + this.federation = federation; + + if (properties == null || properties.isEmpty()) { + this.properties = Collections.EMPTY_MAP; + } else { + this.properties = (Map) Collections.unmodifiableMap(new HashMap<>(properties)); + } + } + + public int getReceiverCredits() { + final Object property = properties.get(RECEIVER_CREDITS); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return federation.getReceiverCredits(); + } + } + + public int getReceiverCreditsLow() { + final Object property = properties.get(RECEIVER_CREDITS_LOW); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return federation.getReceiverCreditsLow(); + } + } + + public int getLargeMessageThreshold() { + final Object property = properties.get(LARGE_MESSAGE_THRESHOLD); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return federation.getLargeMessageThreshold(); + } + } + + public int getLinkAttachTimeout() { + final Object property = properties.get(LINK_ATTACH_TIMEOUT); + if (property instanceof Number) { + return ((Number) property).intValue(); + } else if (property instanceof String) { + return Integer.parseInt((String) property); + } else { + return federation.getLinkAttachTimeout(); + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationPolicySupport.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationPolicySupport.java new file mode 100644 index 0000000000..94f58a255e --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationPolicySupport.java @@ -0,0 +1,535 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.util.AbstractMap; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPStandardMessage; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; +import org.apache.activemq.artemis.protocol.amqp.util.NettyWritable; +import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.AmqpValue; +import org.apache.qpid.proton.amqp.messaging.MessageAnnotations; +import org.apache.qpid.proton.amqp.messaging.Section; +import org.apache.qpid.proton.codec.EncoderImpl; +import org.apache.qpid.proton.codec.WritableBuffer; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.OPERATION_TYPE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDE_FEDERATED; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_PRIORITY_ADJUSTMENT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.TRANSFORMER_CLASS_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.TRANSFORMER_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_QUEUE_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_DELAY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_MSG_COUNT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_ENABLE_DIVERT_BINDINGS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_MAX_HOPS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_ADDRESS_POLICY; + +/** + * Tools used when loading AMQP Broker connections configuration that includes Federation + * configuration. + */ +public final class AMQPFederationPolicySupport { + + /** + * Default priority adjustment used for a federation queue match policy if nothing + * was configured in the broker configuration file. + */ + public static final int DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT = -1; + + /** + * Annotation added to received messages from address consumers that indicates how many + * times the message has traversed a federation link. + */ + public static final Symbol MESSAGE_HOPS_ANNOTATION = Symbol.valueOf("x-opt-amq-fed-hops"); + + /** + * Property name used to embed a nested map of properties meant to be applied if the address + * indicated in an federation address receiver auto creates the federated address. + */ + public static final Symbol FEDERATED_ADDRESS_SOURCE_PROPERTIES = Symbol.valueOf("federated-address-source-properties"); + + /** + * Create an AMQP Message used to instruct the remote peer that it should perform + * Federation operations on the given {@link FederationReceiveFromQueuePolicy}. + * + * @param policy + * The policy to encode into an AMQP message. + * + * @return an AMQP Message with the encoded policy. + */ + public static AMQPMessage encodeQueuePolicyControlMessage(FederationReceiveFromQueuePolicy policy) { + final Map annotations = new LinkedHashMap<>(); + final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations); + final Map policyMap = new LinkedHashMap<>(); + final Section sectionBody = new AmqpValue(policyMap); + final ByteBuf buffer = PooledByteBufAllocator.DEFAULT.heapBuffer(1024); + + annotations.put(OPERATION_TYPE, ADD_QUEUE_POLICY); + + policyMap.put(POLICY_NAME, policy.getPolicyName()); + policyMap.put(QUEUE_INCLUDE_FEDERATED, policy.isIncludeFederated()); + policyMap.put(QUEUE_PRIORITY_ADJUSTMENT, policy.getPriorityAjustment()); + + if (!policy.getIncludes().isEmpty()) { + final List flattenedIncludes = new ArrayList<>(policy.getIncludes().size() * 2); + policy.getIncludes().forEach((entry) -> { + flattenedIncludes.add(entry.getKey()); + flattenedIncludes.add(entry.getValue()); + }); + + policyMap.put(QUEUE_INCLUDES, flattenedIncludes); + } + + if (!policy.getExcludes().isEmpty()) { + final List flatteneExcludes = new ArrayList<>(policy.getExcludes().size() * 2); + policy.getExcludes().forEach((entry) -> { + flatteneExcludes.add(entry.getKey()); + flatteneExcludes.add(entry.getValue()); + }); + + policyMap.put(QUEUE_EXCLUDES, flatteneExcludes); + } + + if (!policy.getProperties().isEmpty()) { + policyMap.put(POLICY_PROPERTIES_MAP, policy.getProperties()); + } + + if (policy.getTransformerConfiguration() != null) { + final TransformerConfiguration config = policy.getTransformerConfiguration(); + + policyMap.put(TRANSFORMER_CLASS_NAME, config.getClassName()); + if (!config.getProperties().isEmpty()) { + policyMap.put(TRANSFORMER_PROPERTIES_MAP, config.getProperties()); + } + } + + try { + final EncoderImpl encoder = TLSEncode.getEncoder(); + encoder.setByteBuffer(new NettyWritable(buffer)); + encoder.writeObject(messageAnnotations); + encoder.writeObject(sectionBody); + + final byte[] data = new byte[buffer.writerIndex()]; + buffer.readBytes(data); + + return new AMQPStandardMessage(0, data, null); + } finally { + TLSEncode.getEncoder().setByteBuffer((WritableBuffer) null); + buffer.release(); + } + } + + /** + * Create an AMQP Message used to instruct the remote peer that it should perform + * Federation operations on the given {@link FederationReceiveFromAddressPolicy}. + * + * @param policy + * The policy to encode into an AMQP message. + * + * @return an AMQP Message with the encoded policy. + */ + public static AMQPMessage encodeAddressPolicyControlMessage(FederationReceiveFromAddressPolicy policy) { + final Map annotations = new LinkedHashMap<>(); + final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations); + final Map policyMap = new LinkedHashMap<>(); + final Section sectionBody = new AmqpValue(policyMap); + final ByteBuf buffer = PooledByteBufAllocator.DEFAULT.heapBuffer(1024); + + annotations.put(OPERATION_TYPE, ADD_ADDRESS_POLICY); + + policyMap.put(POLICY_NAME, policy.getPolicyName()); + policyMap.put(ADDRESS_AUTO_DELETE, policy.isAutoDelete()); + policyMap.put(ADDRESS_AUTO_DELETE_DELAY, policy.getAutoDeleteDelay()); + policyMap.put(ADDRESS_AUTO_DELETE_MSG_COUNT, policy.getAutoDeleteMessageCount()); + policyMap.put(ADDRESS_MAX_HOPS, policy.getMaxHops()); + policyMap.put(ADDRESS_ENABLE_DIVERT_BINDINGS, policy.isEnableDivertBindings()); + if (!policy.getIncludes().isEmpty()) { + policyMap.put(ADDRESS_INCLUDES, new ArrayList<>(policy.getIncludes())); + } + if (!policy.getExcludes().isEmpty()) { + policyMap.put(ADDRESS_EXCLUDES, new ArrayList<>(policy.getExcludes())); + } + + if (!policy.getProperties().isEmpty()) { + policyMap.put(POLICY_PROPERTIES_MAP, policy.getProperties()); + } + + if (policy.getTransformerConfiguration() != null) { + final TransformerConfiguration config = policy.getTransformerConfiguration(); + + policyMap.put(TRANSFORMER_CLASS_NAME, config.getClassName()); + if (!config.getProperties().isEmpty()) { + policyMap.put(TRANSFORMER_PROPERTIES_MAP, config.getProperties()); + } + } + + try { + final EncoderImpl encoder = TLSEncode.getEncoder(); + encoder.setByteBuffer(new NettyWritable(buffer)); + encoder.writeObject(messageAnnotations); + encoder.writeObject(sectionBody); + + final byte[] data = new byte[buffer.writerIndex()]; + buffer.readBytes(data); + + return new AMQPStandardMessage(0, data, null); + } finally { + TLSEncode.getEncoder().setByteBuffer((WritableBuffer) null); + buffer.release(); + } + } + + /** + * Given an AMQP Message decode an {@link FederationReceiveFromQueuePolicy} from it and return + * the decoded value. The message should have already been inspected and determined to be an + * control message of the add to policy type. + * + * @param message + * The {@link AMQPMessage} that should carry an encoded {@link FederationReceiveFromQueuePolicy} + * @param wildcardConfig + * The {@link WildcardConfiguration} to use in the decoded policy. + * + * @return a decoded {@link FederationReceiveFromQueuePolicy} instance. + * + * @throws ActiveMQException if an error occurs while decoding the policy. + */ + @SuppressWarnings("unchecked") + public static FederationReceiveFromQueuePolicy decodeReceiveFromQueuePolicy(AMQPMessage message, WildcardConfiguration wildcardConfig) throws ActiveMQException { + final Section body = message.getBody(); + + if (!(body instanceof AmqpValue)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body was not an AmqpValue type"); + } + + final AmqpValue bodyValue = (AmqpValue) body; + + if (bodyValue.getValue() == null || !(bodyValue.getValue() instanceof Map)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body AmqpValue did not carry an encoded Map"); + } + + try { + final Map policyMap = (Map) bodyValue.getValue(); + + if (!policyMap.containsKey(POLICY_NAME)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body did not carry the required policy name"); + } + + final String policyName = (String) policyMap.get(POLICY_NAME); + final boolean includeFederated = (boolean) policyMap.getOrDefault(QUEUE_INCLUDE_FEDERATED, false); + final int priorityAdjustment = ((Number) policyMap.getOrDefault(QUEUE_PRIORITY_ADJUSTMENT, DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT)).intValue(); + final Set> includes = decodeFlattenedFilterSet(policyMap, QUEUE_INCLUDES); + final Set> excludes = decodeFlattenedFilterSet(policyMap, QUEUE_EXCLUDES); + final TransformerConfiguration transformerConfig; + + if (policyMap.containsKey(TRANSFORMER_CLASS_NAME)) { + transformerConfig = new TransformerConfiguration(); + transformerConfig.setClassName((String) policyMap.get(TRANSFORMER_CLASS_NAME)); + transformerConfig.setProperties((Map) policyMap.get(TRANSFORMER_PROPERTIES_MAP)); + } else { + transformerConfig = null; + } + + final Map properties; + + if (policyMap.containsKey(POLICY_PROPERTIES_MAP)) { + properties = (Map) policyMap.get(POLICY_PROPERTIES_MAP); + } else { + properties = null; + } + + return new FederationReceiveFromQueuePolicy(policyName, includeFederated, priorityAdjustment, + includes, excludes, properties, transformerConfig, + wildcardConfig); + } catch (ActiveMQException amqEx) { + throw amqEx; + } catch (Exception e) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Invalid encoded queue policy entry: " + e.getMessage()); + } + } + + @SuppressWarnings("unchecked") + private static Set> decodeFlattenedFilterSet(Map policyMap, String target) throws ActiveMQException { + final Object encodedObject = policyMap.get(target); + + if (encodedObject == null) { + return Collections.EMPTY_SET; + } + + if (!(encodedObject instanceof List)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Encoded queue policy entry was not the expected List type : " + target); + } + + final Set> policyEntrySet; + + try { + final List flattenedEntrySet = (List) encodedObject; + + if (flattenedEntrySet.isEmpty()) { + return Collections.EMPTY_SET; + } + + if ((flattenedEntrySet.size() & 1) != 0) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Encoded queue policy entry was must contain an even number of elements : " + target); + } + + policyEntrySet = new HashSet<>(Math.max(2, flattenedEntrySet.size() / 2)); + + for (int i = 0; i < flattenedEntrySet.size(); ) { + policyEntrySet.add(new SimpleEntry<>(flattenedEntrySet.get(i++), flattenedEntrySet.get(i++))); + } + + } catch (ActiveMQException amqEx) { + throw amqEx; + } catch (Exception e) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Invalid encoded queue policy entry: " + e.getMessage()); + } + + return policyEntrySet; + } + + /** + * Given an AMQP Message decode an {@link FederationReceiveFromAddressPolicy} from it and return + * the decoded value. The message should have already been inspected and determined to be an + * control message of the add to policy type. + * + * @param message + * The {@link AMQPMessage} that should carry an encoded {@link FederationReceiveFromQueuePolicy} + * @param wildcardConfig + * The {@link WildcardConfiguration} to use in the decoded policy. + * + * @return a decoded {@link FederationReceiveFromAddressPolicy} instance. + * + * @throws ActiveMQException if an error occurs during the policy decode. + */ + @SuppressWarnings("unchecked") + public static FederationReceiveFromAddressPolicy decodeReceiveFromAddressPolicy(AMQPMessage message, WildcardConfiguration wildcardConfig) throws ActiveMQException { + final Section body = message.getBody(); + + if (!(body instanceof AmqpValue)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body was not an AmqpValue type"); + } + + final AmqpValue bodyValue = (AmqpValue) body; + + if (bodyValue.getValue() == null || !(bodyValue.getValue() instanceof Map)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body AmqpValue did not carry an encoded Map"); + } + + try { + final Map policyMap = (Map) bodyValue.getValue(); + + if (!policyMap.containsKey(POLICY_NAME)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body did not carry the required policy name"); + } + + if (!policyMap.containsKey(ADDRESS_MAX_HOPS)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Message body did not carry the required max hops configuration"); + } + + final String policyName = (String) policyMap.get(POLICY_NAME); + final boolean autoDelete = (Boolean) policyMap.getOrDefault(ADDRESS_AUTO_DELETE, false); + final long autoDeleteDelay = ((Number) policyMap.getOrDefault(ADDRESS_AUTO_DELETE_DELAY, 0L)).longValue(); + final long autoDeleteMsgCount = ((Number) policyMap.getOrDefault(ADDRESS_AUTO_DELETE_MSG_COUNT, 0L)).longValue(); + final int maxHops = ((Number) policyMap.get(ADDRESS_MAX_HOPS)).intValue(); + final boolean enableDiverts = (Boolean) policyMap.getOrDefault(ADDRESS_ENABLE_DIVERT_BINDINGS, false); + + final Set includes; + final Set excludes; + + if (policyMap.containsKey(ADDRESS_INCLUDES)) { + includes = (Set) new HashSet<>((List)policyMap.get(ADDRESS_INCLUDES)); + } else { + includes = Collections.EMPTY_SET; + } + + if (policyMap.containsKey(ADDRESS_EXCLUDES)) { + excludes = (Set) new HashSet<>((List)policyMap.get(ADDRESS_EXCLUDES)); + } else { + excludes = Collections.EMPTY_SET; + } + + final TransformerConfiguration transformerConfig; + + if (policyMap.containsKey(TRANSFORMER_CLASS_NAME)) { + transformerConfig = new TransformerConfiguration(); + transformerConfig.setClassName((String) policyMap.get(TRANSFORMER_CLASS_NAME)); + transformerConfig.setProperties((Map) policyMap.get(TRANSFORMER_PROPERTIES_MAP)); + } else { + transformerConfig = null; + } + + final Map properties; + + if (policyMap.containsKey(POLICY_PROPERTIES_MAP)) { + properties = (Map) policyMap.get(POLICY_PROPERTIES_MAP); + } else { + properties = null; + } + + return new FederationReceiveFromAddressPolicy(policyName, autoDelete, autoDeleteDelay, + autoDeleteMsgCount, maxHops, enableDiverts, + includes, excludes, properties, transformerConfig, + wildcardConfig); + } catch (Exception e) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.malformedFederationControlMessage( + "Invalid encoded address policy entry: " + e.getMessage()); + } + } + + /** + * From the broker AMQP broker connection configuration element and the configured wild-card + * settings create an address match policy. + * + * @param element + * The broker connections element configuration that creates this policy. + * @param wildcards + * The configured wild-card settings for the broker or defaults. + * + * @return a new address match and handling policy for use in the broker connection. + */ + @SuppressWarnings("unchecked") + public static FederationReceiveFromAddressPolicy create(AMQPFederationAddressPolicyElement element, WildcardConfiguration wildcards) { + final Set includes; + final Set excludes; + + if (element.getIncludes() != null && !element.getIncludes().isEmpty()) { + includes = new HashSet<>(element.getIncludes().size()); + + element.getIncludes().forEach(addressMatch -> includes.add(addressMatch.getAddressMatch())); + } else { + includes = Collections.EMPTY_SET; + } + + if (element.getExcludes() != null && !element.getExcludes().isEmpty()) { + excludes = new HashSet<>(element.getExcludes().size()); + + element.getExcludes().forEach(addressMatch -> excludes.add(addressMatch.getAddressMatch())); + } else { + excludes = Collections.EMPTY_SET; + } + + // We translate from broker configuration to actual implementation to avoid any coupling here + // as broker configuration could change and or be updated. + + final FederationReceiveFromAddressPolicy policy = new FederationReceiveFromAddressPolicy( + element.getName(), + element.getAutoDelete() == null ? false : element.getAutoDelete(), + element.getAutoDeleteDelay() == null ? 0 : element.getAutoDeleteDelay(), + element.getAutoDeleteMessageCount() == null ? 0 : element.getAutoDeleteMessageCount(), + element.getMaxHops(), + element.isEnableDivertBindings() == null ? false : element.isEnableDivertBindings(), + includes, + excludes, + element.getProperties(), + element.getTransformerConfiguration(), + wildcards); + + return policy; + } + + /** + * From the broker AMQP broker connection configuration element and the configured wild-card + * settings create an queue match policy. If not configured otherwise the consumer priority value + * is always defaulted to a value of -1 in order to attempt to prevent federation + * consumers from consuming messages on the remote when a local consumer is present. + * + * @param element + * The broker connections element configuration that creates this policy. + * @param wildcards + * The configured wild-card settings for the broker or defaults. + * + * @return a new queue match and handling policy for use in the broker connection. + */ + @SuppressWarnings("unchecked") + public static FederationReceiveFromQueuePolicy create(AMQPFederationQueuePolicyElement element, WildcardConfiguration wildcards) { + final Set> includes; + final Set> excludes; + + if (element.getIncludes() != null && !element.getIncludes().isEmpty()) { + includes = new HashSet<>(element.getIncludes().size()); + + element.getIncludes().forEach(queueMatch -> + includes.add(new AbstractMap.SimpleImmutableEntry(queueMatch.getAddressMatch(), queueMatch.getQueueMatch()))); + } else { + includes = Collections.EMPTY_SET; + } + + if (element.getExcludes() != null && !element.getExcludes().isEmpty()) { + excludes = new HashSet<>(element.getExcludes().size()); + + element.getExcludes().forEach(queueMatch -> + excludes.add(new AbstractMap.SimpleImmutableEntry(queueMatch.getAddressMatch(), queueMatch.getQueueMatch()))); + } else { + excludes = Collections.EMPTY_SET; + } + + // We translate from broker configuration to actual implementation to avoid any coupling here + // as broker configuration could change and or be updated. + + final FederationReceiveFromQueuePolicy policy = new FederationReceiveFromQueuePolicy( + element.getName(), + element.isIncludeFederated(), + element.getPriorityAdjustment() == null ? DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT : element.getPriorityAdjustment(), + includes, + excludes, + element.getProperties(), + element.getTransformerConfiguration(), + wildcards); + + return policy; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueueConsumer.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueueConsumer.java new file mode 100644 index 0000000000..4b7a9993e7 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueueConsumer.java @@ -0,0 +1,556 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_QUEUE_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_RECEIVER_PRIORITY; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.AMQP_LINK_INITIALIZER_KEY; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.DETACH_FORCED; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.NOT_FOUND; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.RESOURCE_DELETED; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.Message; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.QueueQueryResult; +import org.apache.activemq.artemis.core.server.transformer.Transformer; +import org.apache.activemq.artemis.core.transaction.Transaction; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPNotFoundException; +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationConsumerInternal; +import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpJmsSelectorFilter; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerReceiverContext; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.Accepted; +import org.apache.qpid.proton.amqp.messaging.Modified; +import org.apache.qpid.proton.amqp.messaging.Rejected; +import org.apache.qpid.proton.amqp.messaging.Released; +import org.apache.qpid.proton.amqp.messaging.Source; +import org.apache.qpid.proton.amqp.messaging.Target; +import org.apache.qpid.proton.amqp.messaging.TerminusDurability; +import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy; +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; +import org.apache.qpid.proton.amqp.transport.SenderSettleMode; +import org.apache.qpid.proton.engine.Delivery; +import org.apache.qpid.proton.engine.EndpointState; +import org.apache.qpid.proton.engine.Link; +import org.apache.qpid.proton.engine.Receiver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Consumer implementation for Federated Queues that receives from a remote + * AMQP peer and forwards those messages onto the internal broker Queue for + * consumption by an attached resource. + */ +public class AMQPFederationQueueConsumer implements FederationConsumerInternal { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final int DEFAULT_PULL_CREDIT_BATCH_SIZE = 100; + + public static final int DEFAULT_PENDING_MSG_CHECK_BACKOFF_MULTIPLIER = 2; + public static final int DEFAULT_PENDING_MSG_CHECK_MAX_DELAY = 30; + + // Desired capabilities that the federation receiver link needs the remote to offer in order + // for the federation receiver to be successfully opened. + private static final Symbol[] DESIRED_LINK_CAPABILITIES = new Symbol[] {FEDERATION_QUEUE_RECEIVER}; + + private static final Symbol[] DEFAULT_OUTCOMES = new Symbol[]{Accepted.DESCRIPTOR_SYMBOL, Rejected.DESCRIPTOR_SYMBOL, + Released.DESCRIPTOR_SYMBOL, Modified.DESCRIPTOR_SYMBOL}; + + private final AMQPFederation federation; + private final AMQPFederationConsumerConfiguration configuration; + private final FederationConsumerInfo consumerInfo; + private final FederationReceiveFromQueuePolicy policy; + private final AMQPConnectionContext connection; + private final AMQPSessionContext session; + private final Predicate remoteCloseIntercepter = this::remoteLinkClosedIntercepter; + private final Transformer transformer; + + private AMQPFederatedQueueDeliveryReceiver receiver; + private Receiver protonReceiver; + private boolean started; + private volatile boolean closed; + private Consumer remoteCloseHandler; + + public AMQPFederationQueueConsumer(AMQPFederation federation, AMQPFederationConsumerConfiguration configuration, + AMQPSessionContext session, FederationConsumerInfo consumerInfo, FederationReceiveFromQueuePolicy policy) { + this.federation = federation; + this.consumerInfo = consumerInfo; + this.policy = policy; + this.connection = session.getAMQPConnectionContext(); + this.session = session; + this.configuration = configuration; + + final TransformerConfiguration transformerConfiguration = policy.getTransformerConfiguration(); + if (transformerConfiguration != null) { + this.transformer = federation.getServer().getServiceRegistry().getFederationTransformer(policy.getPolicyName(), transformerConfiguration); + } else { + this.transformer = (m) -> m; + } + } + + @Override + public Federation getFederation() { + return federation; + } + + @Override + public FederationConsumerInfo getConsumerInfo() { + return consumerInfo; + } + + /** + * @return the {@link FederationReceiveFromQueuePolicy} that initiated this consumer. + */ + public FederationReceiveFromQueuePolicy getPolicy() { + return policy; + } + + @Override + public synchronized void start() { + if (!started && !closed) { + started = true; + asyncCreateReceiver(); + } + } + + @Override + public synchronized void close() { + if (!closed) { + closed = true; + if (started) { + started = false; + connection.runLater(() -> { + federation.removeLinkClosedInterceptor(consumerInfo.getFqqn()); + + if (receiver != null) { + try { + receiver.close(false); + } catch (ActiveMQAMQPException e) { + } finally { + receiver = null; + } + } + + // Need to track the proton receiver and close it here as the default + // context implementation doesn't do that and could result in no detach + // being sent in some cases and possible resources leaks. + if (protonReceiver != null) { + try { + protonReceiver.close(); + } finally { + protonReceiver = null; + } + } + + connection.flush(); + }); + } + } + } + + @Override + public synchronized AMQPFederationQueueConsumer setRemoteClosedHandler(Consumer handler) { + if (started) { + throw new IllegalStateException("Cannot set a remote close handler after the consumer is started"); + } + + this.remoteCloseHandler = handler; + return this; + } + + protected boolean remoteLinkClosedIntercepter(Link link) { + if (link == protonReceiver && link.getRemoteCondition() != null && link.getRemoteCondition().getCondition() != null) { + final Symbol errorCondition = link.getRemoteCondition().getCondition(); + + // Cases where remote link close is not considered terminal, additional checks + // should be added as needed for cases where the remote has closed the link either + // during the attach or at some point later. + + if (RESOURCE_DELETED.equals(errorCondition)) { + // Remote side manually deleted this queue. + return true; + } else if (NOT_FOUND.equals(errorCondition)) { + // Remote did not have a queue that matched. + return true; + } else if (DETACH_FORCED.equals(errorCondition)) { + // Remote operator forced the link to detach. + return true; + } + } + + return false; + } + + private void signalBeforeFederationConsumerMessageHandled(Message message) throws ActiveMQException { + try { + federation.getServer().callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).beforeFederationConsumerMessageHandled(this, message); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("beforeFederationConsumerMessageHandled", t); + } + } + + private void signalAfterFederationConsumerMessageHandled(Message message) throws ActiveMQException { + try { + federation.getServer().callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).afterFederationConsumerMessageHandled(this, message); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("afterFederationConsumerMessageHandled", t); + } + } + + private String generateLinkName() { + return "federation-" + federation.getName() + + "-queue-receiver-" + consumerInfo.getFqqn() + + "-" + federation.getServer().getNodeID(); + } + + private void asyncCreateReceiver() { + connection.runLater(() -> { + if (closed) { + return; + } + + try { + final Receiver protonReceiver = session.getSession().receiver(generateLinkName()); + final Target target = new Target(); + final Source source = new Source(); + final String address = consumerInfo.getFqqn(); + final Queue localQueue = federation.getServer().locateQueue(consumerInfo.getQueueName()); + + if (RoutingType.ANYCAST.equals(consumerInfo.getRoutingType())) { + source.setCapabilities(AmqpSupport.QUEUE_CAPABILITY); + } else { + source.setCapabilities(AmqpSupport.TOPIC_CAPABILITY); + } + + source.setOutcomes(Arrays.copyOf(DEFAULT_OUTCOMES, DEFAULT_OUTCOMES.length)); + source.setDurable(TerminusDurability.NONE); + source.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH); + source.setAddress(address); + + if (consumerInfo.getFilterString() != null && !consumerInfo.getFilterString().isEmpty()) { + final AmqpJmsSelectorFilter jmsFilter = new AmqpJmsSelectorFilter(consumerInfo.getFilterString()); + final Map filtersMap = new HashMap<>(); + filtersMap.put(AmqpSupport.JMS_SELECTOR_KEY, jmsFilter); + + source.setFilter(filtersMap); + } + + target.setAddress(address); + + final Map receiverProperties = new HashMap<>(); + receiverProperties.put(FEDERATION_RECEIVER_PRIORITY, consumerInfo.getPriority()); + + protonReceiver.setSenderSettleMode(SenderSettleMode.UNSETTLED); + protonReceiver.setReceiverSettleMode(ReceiverSettleMode.FIRST); + protonReceiver.setDesiredCapabilities(DESIRED_LINK_CAPABILITIES); + protonReceiver.setProperties(receiverProperties); + protonReceiver.setTarget(target); + protonReceiver.setSource(source); + protonReceiver.open(); + + final ScheduledFuture openTimeoutTask; + final AtomicBoolean openTimedOut = new AtomicBoolean(false); + + if (federation.getLinkAttachTimeout() > 0) { + openTimeoutTask = federation.getServer().getScheduledPool().schedule(() -> { + openTimedOut.set(true); + federation.signalResourceCreateError(ActiveMQAMQPProtocolMessageBundle.BUNDLE.brokerConnectionTimeout()); + }, federation.getLinkAttachTimeout(), TimeUnit.SECONDS); + } else { + openTimeoutTask = null; + } + + this.protonReceiver = protonReceiver; + + protonReceiver.attachments().set(AMQP_LINK_INITIALIZER_KEY, Runnable.class, () -> { + try { + if (openTimeoutTask != null) { + openTimeoutTask.cancel(false); + } + + if (openTimedOut.get()) { + return; + } + + // Remote must support federation receivers otherwise we fail the connection unless the + // Attach indicates that a detach is incoming in which case we just allow the normal handling + // to occur. + if (protonReceiver.getRemoteSource() != null && !AmqpSupport.verifyOfferedCapabilities(protonReceiver)) { + federation.signalResourceCreateError( + ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(DESIRED_LINK_CAPABILITIES))); + return; + } + + // Intercept remote close and check for valid reasons for remote closure such as + // the remote peer not having a matching queue for this subscription or from an + // operator manually closing the link. + federation.addLinkClosedInterceptor(consumerInfo.getFqqn(), remoteCloseIntercepter); + + receiver = new AMQPFederatedQueueDeliveryReceiver(localQueue, protonReceiver); + + if (protonReceiver.getRemoteSource() != null) { + logger.debug("AMQP Federation {} queue consumer {} completed open", federation.getName(), consumerInfo); + } else { + logger.debug("AMQP Federation {} queue consumer {} rejected by remote", federation.getName(), consumerInfo); + } + + session.addReceiver(protonReceiver, (session, protonRcvr) -> { + return this.receiver; + }); + } catch (Exception e) { + federation.signalError(e); + } + }); + } catch (Exception e) { + federation.signalError(e); + } + + connection.flush(); + }); + } + + private static int caclulateNextDelay(int lastDelay, int backoffMultiplier, int maxDelay) { + final int nextDelay; + + if (lastDelay == 0) { + nextDelay = 1; + } else { + nextDelay = Math.min(lastDelay * backoffMultiplier, maxDelay); + } + + return nextDelay; + } + + /** + * Wrapper around the standard receiver context that provides federation specific entry + * points and customizes inbound delivery handling for this Queue receiver. + */ + private class AMQPFederatedQueueDeliveryReceiver extends ProtonServerReceiverContext { + + private final SimpleString cachedFqqn; + + private final Queue localQueue; + + /** + * Creates the federation receiver instance. + * + * @param session + * The server session context bound to the receiver instance. + * @param consumerInfo + * The {@link FederationConsumerInfo} that defines the consumer being created. + * @param receiver + * The proton receiver that will be wrapped in this server context instance. + * @param creditRunnable + * The {@link Runnable} to provide to the base class for managing link credit. + */ + AMQPFederatedQueueDeliveryReceiver(Queue localQueue, Receiver receiver) { + super(session.getSessionSPI(), session.getAMQPConnectionContext(), session, receiver); + + this.localQueue = localQueue; + this.cachedFqqn = SimpleString.toSimpleString(consumerInfo.getFqqn()); + } + + @Override + public void close(boolean remoteLinkClose) throws ActiveMQAMQPException { + super.close(remoteLinkClose); + + if (remoteLinkClose && remoteCloseHandler != null) { + try { + remoteCloseHandler.accept(AMQPFederationQueueConsumer.this); + } catch (Exception e) { + logger.debug("User remote closed handler threw error: ", e); + } finally { + remoteCloseHandler = null; + } + } + } + + @Override + public void initialize() throws Exception { + initialized = true; + + final Target target = (Target) receiver.getRemoteTarget(); + + // Match the settlement mode of the remote instead of relying on the default of MIXED. + receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode()); + + // We don't currently support SECOND so enforce that the answer is always FIRST + receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST); + + // the target will have an address and it will naturally have a Target otherwise + // the remote is misbehaving and we close it. + if (target == null || target.getAddress() == null || target.getAddress().isEmpty()) { + throw new ActiveMQAMQPInternalErrorException("Remote should have sent an valid Target but we got: " + target); + } + + address = SimpleString.toSimpleString(target.getAddress()); + defRoutingType = getRoutingType(target.getCapabilities(), address); + + try { + final QueueQueryResult result = sessionSPI.queueQuery(address, defRoutingType, false); + + // We initiated this link so the target should refer to an queue that definitely exists + // however there is a chance the queue was removed in the interim. + if (!result.isExists()) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.addressDoesntExist(address.toString()); + } + } catch (ActiveMQAMQPNotFoundException e) { + throw e; + } catch (Exception e) { + logger.debug(e.getMessage(), e); + throw new ActiveMQAMQPInternalErrorException(e.getMessage(), e); + } + + flow(); + } + + @Override + protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) { + try { + if (logger.isTraceEnabled()) { + logger.trace("AMQP Federation {} queue consumer {} dispatching incoming message: {}", + federation.getName(), consumerInfo, message); + } + + final Message theMessage = transformer.transform(message); + + if (theMessage != message && logger.isTraceEnabled()) { + logger.trace("The transformer {} replaced the original message {} with a new instance {}", + transformer, message, theMessage); + } + + signalBeforeFederationConsumerMessageHandled(theMessage); + sessionSPI.serverSend(this, tx, receiver, delivery, cachedFqqn, routingContext, theMessage); + signalAfterFederationConsumerMessageHandled(theMessage); + } catch (Exception e) { + logger.warn("Inbound delivery for {} encountered an error: {}", consumerInfo, e.getMessage(), e); + deliveryFailed(delivery, receiver, e); + } + } + + @Override + protected Runnable createCreditRunnable(AMQPConnectionContext connection) { + // We defer to the configuration instance as opposed to the base class version that reads + // from the connection this allows us to defer to configured policy properties that specify + // credit. This also allows consumers created on the remote side of a federation connection + // to read from properties sent from the federation source that indicate the values that are + // configured on the local side. + if (federation.getReceiverCredits() > 0) { + return createCreditRunnable(configuration.getReceiverCredits(), configuration.getReceiverCreditsLow(), receiver, connection, this); + } else { + return this::checkIfCreditTopUpNeeded; + } + } + + @Override + protected int getConfiguredMinLargeMessageSize(AMQPConnectionContext connection) { + // Looks at policy properties first before looking at federation configuration and finally + // going to the base connection context to read the URI configuration. + return configuration.getLargeMessageThreshold(); + } + + // Credit handling here kicks in when the connection is configured for zero link credit and + // we want to then batch credit to the remote only when there is no local pending messages + // which implies the local consumers are keeping up and we can pull more across. + + private final AtomicBoolean creditTopUpInProgress = new AtomicBoolean(); + + private final Runnable checkForNoBacklogRunnable = this::checkForNoBacklogOnQueue; + private final Runnable performCreditTopUpRunnable = this::performCreditTopUp; + + private int lastBacklogCheckDelay; + + private void checkIfCreditTopUpNeeded() { + if (!connection.isHandler()) { + connection.runLater(creditRunnable); + return; + } + + if (receiver.getCredit() + AMQPFederatedQueueDeliveryReceiver.this.pendingSettles <= 0 && !creditTopUpInProgress.get()) { + // We don't need more scheduled tasks stacking up trying to issue a new + // batch of credit so lets gate this now so they give up. + creditTopUpInProgress.set(true); + + // Move to the Queue executor to ensure we get a proper read on the state of pending messages. + localQueue.getExecutor().execute(checkForNoBacklogRunnable); + } + } + + private void checkForNoBacklogOnQueue() { + // Only when there is no backlog do we grant new credit, otherwise we must wait until + // the local backlog is zero. The top up must be run from the connection executor. + if (localQueue.getPendingMessageCount() == 0) { + lastBacklogCheckDelay = 0; + connection.runLater(performCreditTopUpRunnable); + } else { + lastBacklogCheckDelay = caclulateNextDelay(lastBacklogCheckDelay, DEFAULT_PENDING_MSG_CHECK_BACKOFF_MULTIPLIER, DEFAULT_PENDING_MSG_CHECK_MAX_DELAY); + + federation.getScheduler().schedule(() -> { + localQueue.getExecutor().execute(checkForNoBacklogRunnable); + }, lastBacklogCheckDelay, TimeUnit.SECONDS); + } + } + + private void performCreditTopUp() { + connection.requireInHandler(); + + if (receiver.getLocalState() != EndpointState.ACTIVE) { + return; // Closed before this was triggered. + } + + receiver.flow(DEFAULT_PULL_CREDIT_BATCH_SIZE); + connection.instantFlush(); + lastBacklogCheckDelay = 0; + creditTopUpInProgress.set(false); + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueuePolicyManager.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueuePolicyManager.java new file mode 100644 index 0000000000..3d5b9adae5 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueuePolicyManager.java @@ -0,0 +1,135 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.lang.invoke.MethodHandles; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationConsumerInternal; +import org.apache.activemq.artemis.protocol.amqp.federation.internal.FederationQueuePolicyManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The AMQP Federation implementation of an federation queue policy manager. + */ +public class AMQPFederationQueuePolicyManager extends FederationQueuePolicyManager { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected final AMQPFederation federation; + protected final AMQPFederationConsumerConfiguration configuration; + + public AMQPFederationQueuePolicyManager(AMQPFederation federation, FederationReceiveFromQueuePolicy queuePolicy) throws ActiveMQException { + super(federation, queuePolicy); + + this.federation = federation; + this.configuration = new AMQPFederationConsumerConfiguration(federation, policy.getProperties()); + } + + @Override + protected FederationConsumerInternal createFederationConsumer(FederationConsumerInfo consumerInfo) { + Objects.requireNonNull(consumerInfo, "Federation Queue consumer information object was null"); + + if (logger.isTraceEnabled()) { + logger.trace("AMQP Federation {} creating queue consumer: {} for policy: {}", federation.getName(), consumerInfo, policy.getPolicyName()); + } + + // Don't initiate anything yet as the caller might need to register error handlers etc + // before the attach is sent otherwise they could miss the failure case. + return new AMQPFederationQueueConsumer(federation, configuration, federation.getSessionContext(), consumerInfo, policy); + } + + @Override + protected void signalBeforeCreateFederationConsumer(FederationConsumerInfo info) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).beforeCreateFederationConsumer(info); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("beforeCreateFederationConsumer", t); + } + } + + @Override + protected void signalAfterCreateFederationConsumer(FederationConsumer consumer) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).afterCreateFederationConsumer(consumer); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("afterCreateFederationConsumer", t); + } + } + + @Override + protected void signalBeforeCloseFederationConsumer(FederationConsumer consumer) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).beforeCloseFederationConsumer(consumer); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("beforeCloseFederationConsumer", t); + } + } + + @Override + protected void signalAfterCloseFederationConsumer(FederationConsumer consumer) { + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + ((ActiveMQServerAMQPFederationPlugin) plugin).afterCloseFederationConsumer(consumer); + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("afterCloseFederationConsumer", t); + } + } + + @Override + protected final boolean isPluginBlockingFederationConsumerCreate(Queue queue) { + final AtomicBoolean canCreate = new AtomicBoolean(true); + + try { + server.callBrokerAMQPFederationPlugins((plugin) -> { + if (plugin instanceof ActiveMQServerAMQPFederationPlugin) { + if (canCreate.get()) { + canCreate.set(((ActiveMQServerAMQPFederationPlugin) plugin).shouldCreateFederationConsumerForQueue(queue)); + } + } + }); + } catch (ActiveMQException t) { + ActiveMQServerLogger.LOGGER.federationPluginExecutionError("shouldCreateFederationConsumerForQueue", t); + } + + return !canCreate.get(); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueueSenderController.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueueSenderController.java new file mode 100644 index 0000000000..b78fd789f7 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationQueueSenderController.java @@ -0,0 +1,166 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.QUEUE_CAPABILITY; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.TOPIC_CAPABILITY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederation.FEDERATION_INSTANCE_RECORD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_QUEUE_RECEIVER; + +import java.util.Map; + +import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; +import org.apache.activemq.artemis.api.core.ActiveMQExceptionType; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.server.Consumer; +import org.apache.activemq.artemis.core.server.QueueQueryResult; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPIllegalStateException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPNotFoundException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPNotImplementedException; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContext; +import org.apache.activemq.artemis.protocol.amqp.proton.SenderController; +import org.apache.activemq.artemis.selector.filter.FilterException; +import org.apache.activemq.artemis.selector.impl.SelectorParser; +import org.apache.activemq.artemis.utils.CompositeAddress; +import org.apache.qpid.proton.amqp.DescribedType; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.Source; +import org.apache.qpid.proton.amqp.transport.AmqpError; +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; +import org.apache.qpid.proton.engine.Connection; +import org.apache.qpid.proton.engine.Sender; + +/** + * {@link SenderController} used when an AMQP federation Queue receiver is created + * and this side of the connection needs to create a matching sender. The attach of + * the sender should only succeed if there is a local matching queue, otherwise the + * link should be closed with an error indicating that the matching resource is not + * present on this peer. + */ +public final class AMQPFederationQueueSenderController implements SenderController { + + // Capabilities offered to the attaching federation receiver link that indicate this sender + // is a federation sender which allows the link open to complete. + private static final Symbol[] OFFERED_LINK_CAPABILITIES = new Symbol[] {FEDERATION_QUEUE_RECEIVER}; + + private final AMQPSessionContext session; + private final AMQPSessionCallback sessionSPI; + + public AMQPFederationQueueSenderController(AMQPSessionContext session) { + this.session = session; + this.sessionSPI = session.getSessionSPI(); + } + + public AMQPSessionContext getSessionContext() { + return session; + } + + public AMQPSessionCallback getSessionCallback() { + return sessionSPI; + } + + @SuppressWarnings("unchecked") + @Override + public Consumer init(ProtonServerSenderContext senderContext) throws Exception { + final Sender sender = senderContext.getSender(); + final Source source = (Source) sender.getRemoteSource(); + final String selector; + final Connection protonConnection = sender.getSession().getConnection(); + final org.apache.qpid.proton.engine.Record attachments = protonConnection.attachments(); + + if (attachments.get(FEDERATION_INSTANCE_RECORD, AMQPFederation.class) == null) { + throw new ActiveMQAMQPIllegalStateException("Cannot create a federation link from non-federation connection"); + } + + if (source == null) { + throw new ActiveMQAMQPNotImplementedException("Null source lookup not supported on federation links."); + } + + // An queue receiver may supply a filter if the queue being federated had a filter attached + // to it at creation, this ensures that we only bring back message that match the original + // queue filter and not others that would simply increase traffic for no reason. + final Map.Entry filter = AmqpSupport.findFilter(source.getFilter(), AmqpSupport.JMS_SELECTOR_FILTER_IDS); + + if (filter != null) { + selector = filter.getValue().getDescribed().toString(); + try { + SelectorParser.parse(selector); + } catch (FilterException e) { + throw new ActiveMQAMQPException(AmqpError.INVALID_FIELD, "Invalid filter", ActiveMQExceptionType.INVALID_FILTER_EXPRESSION); + } + } else { + selector = null; + } + + final RoutingType routingType = getRoutingType(source); + final SimpleString targetAddress; + final SimpleString targetQueue; + + if (CompositeAddress.isFullyQualified(source.getAddress())) { + targetAddress = SimpleString.toSimpleString(CompositeAddress.extractAddressName(source.getAddress())); + targetQueue = SimpleString.toSimpleString(CompositeAddress.extractQueueName(source.getAddress())); + } else { + targetAddress = null; + targetQueue = SimpleString.toSimpleString(source.getAddress()); + } + + final QueueQueryResult result = sessionSPI.queueQuery(targetQueue, routingType, false, null); + if (!result.isExists()) { + throw new ActiveMQAMQPNotFoundException("Queue: '" + targetQueue + "' does not exist"); + } + + if (targetAddress != null && !result.getAddress().equals(targetAddress)) { + throw new ActiveMQAMQPNotFoundException("Queue: '" + targetQueue + "' is not mapped to specified address: " + targetAddress); + } + + // Match the settlement mode of the remote instead of relying on the default of MIXED. + sender.setSenderSettleMode(sender.getRemoteSenderSettleMode()); + // We don't currently support SECOND so enforce that the answer is always FIRST + sender.setReceiverSettleMode(ReceiverSettleMode.FIRST); + // We need to offer back that we support federation for the remote to complete the attach. + sender.setOfferedCapabilities(OFFERED_LINK_CAPABILITIES); + + return (Consumer) sessionSPI.createSender(senderContext, targetQueue, selector, false); + } + + private static RoutingType getRoutingType(Source source) { + if (source != null) { + if (source.getCapabilities() != null) { + for (Symbol capability : source.getCapabilities()) { + if (TOPIC_CAPABILITY.equals(capability)) { + return RoutingType.MULTICAST; + } else if (QUEUE_CAPABILITY.equals(capability)) { + return RoutingType.ANYCAST; + } + } + } + } + + return ActiveMQDefaultConfiguration.getDefaultRoutingType(); + } + + @Override + public void close() throws Exception { + // Currently there isn't anything needed on close of the controller + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationSource.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationSource.java new file mode 100644 index 0000000000..4d809f630b --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationSource.java @@ -0,0 +1,422 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONFIGURATION; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.AMQP_LINK_INITIALIZER_KEY; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.protocol.amqp.connect.AMQPBrokerConnection; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPIllegalStateException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConstants; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContext; +import org.apache.activemq.artemis.utils.UUIDGenerator; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.DeleteOnClose; +import org.apache.qpid.proton.amqp.messaging.Source; +import org.apache.qpid.proton.amqp.messaging.Target; +import org.apache.qpid.proton.amqp.messaging.TerminusDurability; +import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy; +import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode; +import org.apache.qpid.proton.amqp.transport.SenderSettleMode; +import org.apache.qpid.proton.engine.Connection; +import org.apache.qpid.proton.engine.Link; +import org.apache.qpid.proton.engine.Sender; +import org.apache.qpid.proton.engine.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This is the initiating side of a broker federation that occurs over an AMQP + * broker connection. + *

+ * This endpoint will create a control link to the remote peer that is a sender + * of federation commands which can be used to instruct the remote to initiate + * federation operations back to this peer over the same connection and without + * the need for local configuration. + */ +public class AMQPFederationSource extends AMQPFederation { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + // Capabilities set on the sender link used to send policies or other control messages to + // the remote federation target. + private static final Symbol[] CONTROL_LINK_CAPABILITIES = new Symbol[] {FEDERATION_CONTROL_LINK}; + + private final AMQPBrokerConnection brokerConnection; + + // Remote policies that should be conveyed to the remote server for reciprocal federation operations. + private final Map remoteQueueMatchPolicies = new HashMap<>(); + private final Map remoteAddressMatchPolicies = new HashMap<>(); + + private final Map properties; + + private volatile AMQPFederationConfiguration configuration; + + /** + * Creates a new AMQP Federation instance that will manage the state of a single AMQP + * broker federation instance using an AMQP broker connection as the IO channel. + * + * @param name + * The name of this federation instance. + * @param properties + * A set of optional properties that provide additional configuration. + * @param connection + * The broker connection over which this federation will occur. + */ + @SuppressWarnings("unchecked") + public AMQPFederationSource(String name, Map properties, AMQPBrokerConnection connection) { + super(name, connection.getServer()); + + if (properties == null || properties.isEmpty()) { + this.properties = Collections.EMPTY_MAP; + } else { + this.properties = (Map) Collections.unmodifiableMap(new HashMap<>(properties)); + } + + this.brokerConnection = connection; + this.brokerConnection.addLinkClosedInterceptor(getName(), this::invokeLinkClosedInterceptors); + } + + /** + * @return the {@link AMQPBrokerConnection} that this federation is attached to. + */ + public AMQPBrokerConnection getBrokerConnection() { + return brokerConnection; + } + + @Override + public int getLinkAttachTimeout() { + return configuration.getLinkAttachTimeout(); + } + + @Override + public synchronized AMQPSessionContext getSessionContext() { + if (!connected) { + throw new IllegalStateException("Cannot access session while federation is not connected"); + } + + return session; + } + + @Override + public synchronized AMQPConnectionContext getConnectionContext() { + if (!connected) { + throw new IllegalStateException("Cannot access connection while federation is not connected"); + } + + return connection; + } + + @Override + public synchronized int getReceiverCredits() { + if (!connected) { + throw new IllegalStateException("Cannot access connection configuration, federation is not connected"); + } + + return configuration.getReceiverCredits(); + } + + @Override + public synchronized int getReceiverCreditsLow() { + if (!connected) { + throw new IllegalStateException("Cannot access connection configuration, federation is not connected"); + } + + return configuration.getReceiverCreditsLow(); + } + + @Override + public synchronized int getLargeMessageThreshold() { + if (!connected) { + throw new IllegalStateException("Cannot access connection configuration, federation is not connected"); + } + + return configuration.getLargeMessageThreshold(); + } + + /** + * Adds a new {@link FederationReceiveFromQueuePolicy} entry to the set of policies that the + * remote end of this federation will use to create demand on the this server when local + * demand is present. + * + * @param queuePolicy + * The policy to add to the set of configured {@link FederationReceiveFromQueuePolicy} instance. + * + * @return this {@link AMQPFederationSource} instance. + */ + public synchronized AMQPFederationSource addRemoteQueueMatchPolicy(FederationReceiveFromQueuePolicy queuePolicy) { + remoteQueueMatchPolicies.putIfAbsent(queuePolicy.getPolicyName(), queuePolicy); + return this; + } + + /** + * Adds a new {@link FederationReceiveFromAddressPolicy} entry to the set of policies that the + * remote end of this federation will use to create demand on the this server when local + * demand is present. + * + * @param addressPolicy + * The policy to add to the set of configured {@link FederationReceiveFromAddressPolicy} instance. + * + * @return this {@link AMQPFederationSource} instance. + */ + public synchronized AMQPFederationSource addRemoteAddressMatchPolicy(FederationReceiveFromAddressPolicy addressPolicy) { + remoteAddressMatchPolicies.putIfAbsent(addressPolicy.getPolicyName(), addressPolicy); + return this; + } + + /** + * Called by the parent broker connection when the connection has failed and this federation + * should tear down any active resources and await a reconnect if one is allowed. + * + * @throws ActiveMQException if an error occurs processing the connection dropped event + */ + public synchronized void handleConnectionDropped() throws ActiveMQException { + connected = false; + + final AtomicReference errorCaught = new AtomicReference<>(); + + queueMatchPolicies.forEach((k, v) -> { + try { + v.stop(); + } catch (Exception ex) { + errorCaught.compareAndExchange(null, ex); + } + }); + + addressMatchPolicies.forEach((k, v) -> { + try { + v.stop(); + } catch (Exception ex) { + errorCaught.compareAndExchange(null, ex); + } + }); + + connection = null; + session = null; + + if (errorCaught.get() != null) { + final Exception error = errorCaught.get(); + if (error instanceof ActiveMQException) { + throw (ActiveMQException) error; + } else { + throw (ActiveMQException) new ActiveMQException(error.getMessage()).initCause(error); + } + } + } + + /** + * Called by the parent broker connection when the connection has been established and this + * federation should build up its active state based on the configuration. + * + * @param connection + * The new {@link Connection} that represents the currently active connection. + * @param session + * The new {@link Session} that was created for use by broker connection resources. + * + * @throws ActiveMQException if an error occurs processing the connection restored event + */ + public synchronized void handleConnectionRestored(AMQPConnectionContext connection, AMQPSessionContext session) throws ActiveMQException { + final Connection protonConnection = session.getSession().getConnection(); + final org.apache.qpid.proton.engine.Record attachments = protonConnection.attachments(); + + if (attachments.get(FEDERATION_INSTANCE_RECORD, AMQPFederation.class) != null) { + throw new ActiveMQAMQPIllegalStateException("An existing federation instance was found on the connection"); + } + + this.connection = connection; + this.session = session; + this.configuration = new AMQPFederationConfiguration(connection, properties); + + // Assign an federation instance to the connection which incoming federation links can look for + // to indicate this is a valid AMQP federation endpoint. + attachments.set(FEDERATION_INSTANCE_RECORD, AMQPFederationSource.class, this); + + // Create the control link and the outcome will then dictate if the configured + // policy managers are started or not. + asyncCreateControlLink(); + } + + @Override + protected void signalResourceCreateError(Exception cause) { + brokerConnection.connectError(cause); + } + + @Override + protected void signalError(Exception cause) { + brokerConnection.runtimeError(cause); + } + + protected boolean interceptLinkClosedEvent(Link link) { + return false; + } + + private void asyncCreateControlLink() { + // Schedule the control link creation on the connection event loop thread + // Eventual establishment of the control link indicates successful connection + // to a remote peer that can support AMQP federation requirements. + connection.runLater(() -> { + try { + final Sender sender = session.getSession().sender("Federation:" + getName() + ":" + UUIDGenerator.getInstance().generateStringUUID()); + final AMQPFederationCommandDispatcher commandLink = new AMQPFederationCommandDispatcher(sender, getServer(), session.getSessionSPI()); + final Target target = new Target(); + + // The control link should be dynamic and the node is destroyed if the connection drops + target.setDynamic(true); + target.setCapabilities(new Symbol[] {Symbol.valueOf("temporary-topic")}); + target.setDurable(TerminusDurability.NONE); + target.setExpiryPolicy(TerminusExpiryPolicy.LINK_DETACH); + // Set the dynamic node lifetime-policy to indicate this needs to be destroyed on close + // we don't want control links remaining once a federation connection is closed. + final Map dynamicNodeProperties = new HashMap<>(); + dynamicNodeProperties.put(AmqpSupport.LIFETIME_POLICY, DeleteOnClose.getInstance()); + target.setDynamicNodeProperties(dynamicNodeProperties); + + // Send our local configuration data to the remote side of the control link + // for use when creating remote federation resources. + final Map senderProperties = new HashMap<>(); + senderProperties.put(FEDERATION_CONFIGURATION, configuration.toConfigurationMap()); + + sender.setSenderSettleMode(SenderSettleMode.UNSETTLED); + sender.setReceiverSettleMode(ReceiverSettleMode.FIRST); + sender.setDesiredCapabilities(CONTROL_LINK_CAPABILITIES); + sender.setProperties(senderProperties); + sender.setTarget(target); + sender.setSource(new Source()); + sender.open(); + + final ScheduledFuture futureTimeout; + final AtomicBoolean cancelled = new AtomicBoolean(false); + + if (brokerConnection.getConnectionTimeout() > 0) { + futureTimeout = brokerConnection.getServer().getScheduledPool().schedule(() -> { + cancelled.set(true); + brokerConnection.connectError(ActiveMQAMQPProtocolMessageBundle.BUNDLE.brokerConnectionTimeout()); + }, brokerConnection.getConnectionTimeout(), TimeUnit.MILLISECONDS); + } else { + futureTimeout = null; + } + + // Using attachments to set up a Runnable that will be executed inside the remote link opened handler + sender.attachments().set(AMQP_LINK_INITIALIZER_KEY, Runnable.class, () -> { + try { + if (cancelled.get()) { + return; + } + + if (futureTimeout != null) { + futureTimeout.cancel(false); + } + + if (sender.getRemoteTarget() == null) { + brokerConnection.connectError( + ActiveMQAMQPProtocolMessageBundle.BUNDLE.federationControlLinkRefused(sender.getName())); + return; + } + + if (!AmqpSupport.verifyOfferedCapabilities(sender)) { + brokerConnection.connectError( + ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(CONTROL_LINK_CAPABILITIES))); + return; + } + + // We tag the session with the Federation marker as there could be incoming receivers created + // under it from a remote federation target if remote federation policies are configured. This + // allows the policy managers to then determine if local demand is from a federation target or + // not and based on configuration choose when to create remote receivers. + // + // This currently is a session global tag which means any consumer created from this session in + // response to remote attach of said receiver is going to get caught by the filtering but as of + // now we shouldn't be creating consumers other than federation consumers but if that were to + // change we'd either need single new session for this federation instance or a session per + // consumer at the extreme which then requires that the protocol handling code add the metadata + // during the receiver attach on the remote. + try { + session.getSessionSPI().addMetaData(FederationConstants.FEDERATION_NAME, getName()); + } catch (ActiveMQAMQPException e) { + throw e; + } catch (Exception e) { + logger.trace("Exception on add of federation Metadata: ", e); + throw new ActiveMQAMQPInternalErrorException("Error while configuring interal session metadata"); + } + + final ProtonServerSenderContext senderContext = + new ProtonServerSenderContext(connection, sender, session, session.getSessionSPI(), commandLink); + + session.addSender(sender, senderContext); + + connected = true; + + remoteQueueMatchPolicies.forEach((key, policy) -> { + try { + commandLink.sendPolicy(policy); + } catch (Exception e) { + brokerConnection.error(e); + } + }); + + remoteAddressMatchPolicies.forEach((key, policy) -> { + try { + commandLink.sendPolicy(policy); + } catch (Exception e) { + brokerConnection.error(e); + } + }); + + // Attempt to start the policy managers in another thread to avoid blocking the IO thread + scheduler.execute(() -> { + // Sync action with federation start / stop otherwise we could get out of sync + synchronized (AMQPFederationSource.this) { + if (isStarted()) { + queueMatchPolicies.forEach((k, v) -> v.start()); + addressMatchPolicies.forEach((k, v) -> v.start()); + } + } + }); + + } catch (Exception e) { + brokerConnection.error(e); + } + }); + } catch (Exception e) { + brokerConnection.error(e); + } + + connection.flush(); + }); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationTarget.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationTarget.java new file mode 100644 index 0000000000..ebfffbb15e --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationTarget.java @@ -0,0 +1,150 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import java.util.Objects; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConstants; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext; +import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.transport.AmqpError; +import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.engine.Connection; +import org.apache.qpid.proton.engine.EndpointState; +import org.apache.qpid.proton.engine.Link; + +/** + * This is the receiving side of an AMQP broker federation that occurs over an + * inbound connection from a remote peer. The federation target only comes into + * existence once a remote peer connects and successfully authenticates against + * a control link validation address. Only one federation target is allowed per + * connection. + */ +public class AMQPFederationTarget extends AMQPFederation { + + private final AMQPConnectionContext connection; + private final AMQPFederationConfiguration configuration; + + public AMQPFederationTarget(String name, AMQPFederationConfiguration configuration, AMQPSessionContext session, ActiveMQServer server) { + super(name, server); + + Objects.requireNonNull(session, "Provided session instance cannot be null"); + + this.session = session; + this.connection = session.getAMQPConnectionContext(); + this.connection.addLinkRemoteCloseListener(getName(), this::handleLinkRemoteClose); + this.configuration = configuration; + } + + @Override + public AMQPConnectionContext getConnectionContext() { + return connection; + } + + @Override + public AMQPSessionContext getSessionContext() { + return session; + } + + @Override + public int getReceiverCredits() { + return configuration.getReceiverCredits(); + } + + @Override + public int getReceiverCreditsLow() { + return configuration.getReceiverCreditsLow(); + } + + @Override + public int getLargeMessageThreshold() { + return configuration.getLargeMessageThreshold(); + } + + @Override + public int getLinkAttachTimeout() { + return configuration.getLinkAttachTimeout(); + } + + @Override + protected void handleFederationStarted() throws ActiveMQException { + // Tag the session with Federation metadata which will allow local federation policies sent by + // the remote to apply checks when seeing local demand to determine if a federation consumer + // should cause remote receivers to be created. + // + // This currently is a session global tag which means any consumer created from this session in + // response to remote attach of said receiver is going to get caught by the filtering but as of + // now we shouldn't be creating consumers other than federation consumers but if that were to + // change we'd either need single new session for this federation instance or a session per + // consumer at the extreme which then requires that the protocol handling code add the metadata + // during the receiver attach on the remote. + try { + session.getSessionSPI().addMetaData(FederationConstants.FEDERATION_NAME, getName()); + } catch (ActiveMQAMQPException e) { + throw e; + } catch (Exception e) { + throw new ActiveMQAMQPInternalErrorException("Error while configuring interal session metadata"); + } + + super.handleFederationStarted(); + } + + private void handleLinkRemoteClose(Link link) { + // If the connection has already closed then we can ignore this event. + final Connection protonConnection = link.getSession().getConnection(); + if (protonConnection.getLocalState() != EndpointState.ACTIVE) { + return; + } + + // If the link is locally closed then we closed it intentionally and + // we can continue as normal otherwise we need to check on why it closed. + if (link.getLocalState() != EndpointState.ACTIVE) { + return; + } + + // Did the federation links handle this so that we can ignore it? + // If not then we consider this a terminal outcome and close the connection. + if (!invokeLinkClosedInterceptors(link)) { + signalError(new ActiveMQAMQPInternalErrorException("Federation link closed unexpectedly: " + link.getName())); + } + } + + @Override + protected void signalResourceCreateError(Exception cause) { + signalError(cause); + } + + @Override + protected void signalError(Exception cause) { + final Symbol condition; + final String description = cause.getMessage(); + + if (cause instanceof ActiveMQAMQPException) { + condition = ((ActiveMQAMQPException) cause).getAmqpError(); + } else { + condition = AmqpError.INTERNAL_ERROR; + } + + connection.close(new ErrorCondition(condition, description)); + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/ActiveMQServerAMQPFederationPlugin.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/ActiveMQServerAMQPFederationPlugin.java new file mode 100644 index 0000000000..7980526fcd --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/ActiveMQServerAMQPFederationPlugin.java @@ -0,0 +1,181 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.Message; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationBrokerPlugin; +import org.apache.activemq.artemis.core.server.Divert; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; + +/** + * Broker plugin which allows users to intercept federation related events when AMQP + * federation is configured on the broker. + */ +public interface ActiveMQServerAMQPFederationPlugin extends AMQPFederationBrokerPlugin { + + /** + * After a federation instance has been started + * + * @param federation + * The {@link Federation} instance that is being started. + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void federationStarted(final Federation federation) throws ActiveMQException { + + } + + /** + * After a federation instance has been stopped + * + * @param federation + * The {@link Federation} instance that is being stopped. + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void federationStopped(final Federation federation) throws ActiveMQException { + + } + + /** + * Before a consumer for a federated resource is created + * + * @param consumerInfo + * The information that will be used when creating the federation consumer. + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void beforeCreateFederationConsumer(final FederationConsumerInfo consumerInfo) throws ActiveMQException { + + } + + /** + * After a consumer for a federated resource is created + * + * @param consumer + * The consumer that was created after a matching federated resource is detected. + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void afterCreateFederationConsumer(final FederationConsumer consumer) throws ActiveMQException { + + } + + /** + * Before a consumer for a federated resource is closed + * + * @param consumer + * The federation consumer that is going to be closed. + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void beforeCloseFederationConsumer(final FederationConsumer consumer) throws ActiveMQException { + + } + + /** + * After a consumer for a federated resource is closed + * + * @param consumer + * The federation consumer that has been closed. + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void afterCloseFederationConsumer(final FederationConsumer consumer) throws ActiveMQException { + + } + + /** + * Before a federation consumer handles a message + * + * @param consumer + * The {@link Federation} consumer that is handling a new incoming message. + * @param message + * The {@link Message} that is being handled + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void beforeFederationConsumerMessageHandled(final FederationConsumer consumer, Message message) throws ActiveMQException { + + } + + /** + * After a federation consumer handles a message + * + * @param consumer + * The {@link Federation} consumer that is handling a new incoming message. + * @param message + * The {@link Message} that is being handled + * + * @throws ActiveMQException if an error occurs during the call. + */ + default void afterFederationConsumerMessageHandled(final FederationConsumer consumer, Message message) throws ActiveMQException { + + } + + /** + * Conditionally create a federation consumer for an address that matches the configuration of this server + * federation. This allows custom logic to be inserted to decide when to create federation consumers + * + * @param address + * The address that matched the federation configuration + * + * @return if true, create the consumer, else if false don't create + * + * @throws ActiveMQException if an error occurs during the call. + */ + default boolean shouldCreateFederationConsumerForAddress(final AddressInfo address) throws ActiveMQException { + return true; + } + + /** + * Conditionally create a federation consumer for an address that matches the configuration of this server + * federation. This allows custom logic to be inserted to decide when to create federation consumers + * + * @param queue + * The queue that matched the federation configuration + * + * @return if true, create the consumer, else if false don't create + * + * @throws ActiveMQException if an error occurs during the call. + */ + default boolean shouldCreateFederationConsumerForQueue(final Queue queue) throws ActiveMQException { + return true; + } + + /** + * Conditionally create a federation consumer for an divert binding that matches the configuration of this + * server federation. This allows custom logic to be inserted to decide when to create federation consumers + * + * @param divert + * The {@link Divert} that matched the federation configuration + * @param queue + * The {@link Queue} that was attached for a divert forwarding address. + * + * @return if true, create the consumer, else if false don't create + * + * @throws ActiveMQException if an error occurs during the call. + */ + default boolean shouldCreateFederationConsumerForDivert(Divert divert, Queue queue) throws ActiveMQException { + return true; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/mirror/AMQPMirrorControllerTarget.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/mirror/AMQPMirrorControllerTarget.java index 47415cc049..771e38d269 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/mirror/AMQPMirrorControllerTarget.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/connect/mirror/AMQPMirrorControllerTarget.java @@ -151,7 +151,7 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement } // in a regular case we should not have more than amqpCredits on the pool, that's the max we would need - private final MpscPool ackMessageMpscPool = new MpscPool<>(amqpCredits, ACKMessageOperation::reset, ACKMessageOperation::new); + private final MpscPool ackMessageMpscPool = new MpscPool<>(connection.getAmqpCredits(), ACKMessageOperation::reset, ACKMessageOperation::new); final RoutingContextImpl routingContext = new RoutingContextImpl(null); @@ -260,7 +260,7 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement @Override public void initialize() throws Exception { - super.initialize(); + initialized = true; // Match the settlement mode of the remote instead of relying on the default of MIXED. receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode()); diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/Federation.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/Federation.java new file mode 100644 index 0000000000..1b1e55af02 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/Federation.java @@ -0,0 +1,42 @@ +/* + * 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.artemis.protocol.amqp.federation; + +import org.apache.activemq.artemis.core.server.ActiveMQServer; + +/** + * Base Federated server connection interface. + */ +public interface Federation { + + /** + * @return the unique name that was assigned to this server federation connector. + */ + String getName(); + + /** + * @return the {@link ActiveMQServer} instance assigned to this {@link Federation} + */ + ActiveMQServer getServer(); + + /** + * @return is this federation instance started (may not be connected yet). + */ + boolean isStarted(); + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConstants.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConstants.java new file mode 100644 index 0000000000..5837714888 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConstants.java @@ -0,0 +1,32 @@ +/* + * 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.artemis.protocol.amqp.federation; + +/** + * Some predefined constants used in various scenarios when building and managing a + * federation between peers. + */ +public abstract class FederationConstants { + + /** + * Constant value used in properties or other protocol constructs to indicate + * the name of the broker federation that an operation belongs to. + */ + public static final String FEDERATION_NAME = "federation-name"; + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConsumer.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConsumer.java new file mode 100644 index 0000000000..b3df6f5503 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConsumer.java @@ -0,0 +1,34 @@ +/* + * 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.artemis.protocol.amqp.federation; + +/** + * Basic API for a consumer instance that is tied to a given server federation instance. + */ +public interface FederationConsumer { + + /** + * @return the {@link Federation} that this consumer operates under. + */ + Federation getFederation(); + + /** + * @return an information object that defines the characteristics of the {@link FederationConsumer} + */ + FederationConsumerInfo getConsumerInfo(); + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConsumerInfo.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConsumerInfo.java new file mode 100644 index 0000000000..f370f7e4e6 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationConsumerInfo.java @@ -0,0 +1,105 @@ +/* + * 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.artemis.protocol.amqp.federation; + +import org.apache.activemq.artemis.api.core.RoutingType; + +/** + * Information and identification interface for Federation consumers that will be + * created on the remote broker as demand on the local broker is detected. The + * behavior and meaning of some APIs in this interface may vary slightly depending + * on the role of the consumer (Address or Queue). + */ +public interface FederationConsumerInfo { + + enum Role { + /** + * Consumer created from a match on a configured address federation policy. + */ + ADDRESS_CONSUMER, + + /** + * Consumer created from a match on a configured queue federation policy. + */ + QUEUE_CONSUMER + } + + /** + * @return the type of federation consumer being represented. + */ + Role getRole(); + + /** + * Gets the queue name that will be used for this federation consumer instance. + * + * For Queue federation this will be the name of the queue whose messages are + * being federated to this server instance. For an Address federation this will + * be an automatically generated name that should be unique to a given federation + * instance + * + * @return the queue name associated with the federation consumer + */ + String getQueueName(); + + /** + * Gets the address that will be used for this federation consumer instance. + * + * For Queue federation this is the address under which the matching queue must + * reside. For Address federation this is the actual address whose messages are + * being federated. + * + * @return the address associated with this federation consumer. + */ + String getAddress(); + + /** + * Gets the FQQN that comprises the address and queue where the remote consumer + * will be attached. + * + * @return provides the FQQN that can be used to address the consumer queue directly. + */ + String getFqqn(); + + /** + * Gets the routing type that will be requested when creating a consumer on the + * remote server. + * + * @return the routing type of the remote consumer. + */ + RoutingType getRoutingType(); + + /** + * Gets the filter string that will be used when creating the remote consumer. + * + * For Queue federation this will be the filter that exists on the local queue that + * is requesting federation of messages from the remote. For address federation this + * filter will be used to restrict some movement of messages amongst federated server + * addresses. + * + * @return the filter string in use for the federation consumer. + */ + String getFilterString(); + + /** + * Gets the priority value that will be requested for the remote consumer that is + * created. + * + * @return the assigned consumer priority for the federation consumer. + */ + int getPriority(); + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationReceiveFromAddressPolicy.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationReceiveFromAddressPolicy.java new file mode 100644 index 0000000000..dc367f4d69 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationReceiveFromAddressPolicy.java @@ -0,0 +1,182 @@ +/* + * 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.artemis.protocol.amqp.federation; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.core.settings.impl.Match; + +/** + * Policy used to provide federation of remote to local broker addresses, once created the policy + * configuration is immutable. + */ +public class FederationReceiveFromAddressPolicy implements BiPredicate { + + private final Set includesMatchers = new LinkedHashSet<>(); + private final Set excludesMatchers = new LinkedHashSet<>(); + + private final Collection includes; + private final Collection excludes; + + private final String policyName; + private final boolean autoDelete; + private final long autoDeleteDelay; + private final long autoDeleteMessageCount; + private final int maxHops; + private final boolean enableDivertBindings; + private final Map properties; + private final TransformerConfiguration transformerConfig; + + @SuppressWarnings("unchecked") + public FederationReceiveFromAddressPolicy(String name, boolean autoDelete, long autoDeleteDelay, + long autoDeleteMessageCount, int maxHops, boolean enableDivertBindings, + Collection includeAddresses, Collection excludeAddresses, + Map properties, TransformerConfiguration transformerConfig, + WildcardConfiguration wildcardConfig) { + Objects.requireNonNull(name, "The provided policy name cannot be null"); + Objects.requireNonNull(wildcardConfig, "The provided wild card configuration cannot be null"); + + this.policyName = name; + this.autoDelete = autoDelete; + this.autoDeleteDelay = autoDeleteDelay; + this.autoDeleteMessageCount = autoDeleteMessageCount; + this.maxHops = maxHops; + this.enableDivertBindings = enableDivertBindings; + this.transformerConfig = transformerConfig; + this.includes = Collections.unmodifiableCollection(includeAddresses == null ? Collections.EMPTY_LIST : includeAddresses); + this.excludes = Collections.unmodifiableCollection(excludeAddresses == null ? Collections.EMPTY_LIST : excludeAddresses); + + if (properties == null || properties.isEmpty()) { + this.properties = Collections.EMPTY_MAP; + } else { + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + // Create Matchers from configured includes and excludes for use when matching broker resources + includes.forEach((address) -> includesMatchers.add(new AddressMatcher(address, wildcardConfig))); + excludes.forEach((address) -> excludesMatchers.add(new AddressMatcher(address, wildcardConfig))); + } + + public String getPolicyName() { + return policyName; + } + + public boolean isAutoDelete() { + return autoDelete; + } + + public long getAutoDeleteDelay() { + return autoDeleteDelay; + } + + public long getAutoDeleteMessageCount() { + return autoDeleteMessageCount; + } + + public int getMaxHops() { + return maxHops; + } + + public boolean isEnableDivertBindings() { + return enableDivertBindings; + } + + public Collection getIncludes() { + return includes; + } + + public Collection getExcludes() { + return excludes; + } + + public Map getProperties() { + return properties; + } + + public TransformerConfiguration getTransformerConfiguration() { + return transformerConfig; + } + + /** + * Convenience test method for those who have an {@link AddressInfo} object + * but don't want to deal with the {@link SimpleString} object or any null + * checks. + * + * @param addressInfo + * The address info to check which if null will result in a negative result. + * + * @return true if the address value matches this configured policy. + */ + public boolean test(AddressInfo addressInfo) { + if (addressInfo != null) { + return test(addressInfo.getName().toString(), addressInfo.getRoutingType()); + } else { + return false; + } + } + + @Override + public boolean test(String address, RoutingType type) { + if (RoutingType.MULTICAST.equals(type)) { + for (AddressMatcher matcher : excludesMatchers) { + if (matcher.test(address)) { + return false; + } + } + + for (AddressMatcher matcher : includesMatchers) { + if (matcher.test(address)) { + return true; + } + } + } + + return false; + } + + private static class AddressMatcher implements Predicate { + + private final Predicate matcher; + + AddressMatcher(String address, WildcardConfiguration wildcardConfig) { + if (address == null || address.isEmpty()) { + matcher = (target) -> true; + } else { + matcher = new Match<>(address, null, wildcardConfig).getPattern().asPredicate(); + } + } + + @Override + public boolean test(String address) { + return matcher.test(address); + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationReceiveFromQueuePolicy.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationReceiveFromQueuePolicy.java new file mode 100644 index 0000000000..4cf6ef995e --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/FederationReceiveFromQueuePolicy.java @@ -0,0 +1,148 @@ +/* + * 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.artemis.protocol.amqp.federation; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; + +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.settings.impl.Match; + +/** + * Policy used to provide federation of remote to local broker queues, once created the policy + * configuration is immutable. + */ +public class FederationReceiveFromQueuePolicy implements BiPredicate { + + private final Set includeMatchers = new LinkedHashSet<>(); + private final Set excludeMatchers = new LinkedHashSet<>(); + + private final Collection> includes; + private final Collection> excludes; + + private final String policyName; + private final boolean includeFederated; + private final int priorityAdjustment; + private final Map properties; + private final TransformerConfiguration transformerConfig; + + @SuppressWarnings("unchecked") + public FederationReceiveFromQueuePolicy(String name, boolean includeFederated, int priorotyAdjustment, + Collection> includeQueues, + Collection> excludeQueues, + Map properties, TransformerConfiguration transformerConfig, + WildcardConfiguration wildcardConfig) { + Objects.requireNonNull(name, "The provided policy name cannot be null"); + Objects.requireNonNull(wildcardConfig, "The provided wild card configuration cannot be null"); + + this.policyName = name; + this.includeFederated = includeFederated; + this.priorityAdjustment = priorotyAdjustment; + this.transformerConfig = transformerConfig; + this.includes = Collections.unmodifiableCollection(includeQueues == null ? Collections.EMPTY_LIST : includeQueues); + this.excludes = Collections.unmodifiableCollection(excludeQueues == null ? Collections.EMPTY_LIST : excludeQueues); + + if (properties == null || properties.isEmpty()) { + this.properties = Collections.EMPTY_MAP; + } else { + this.properties = Collections.unmodifiableMap(new HashMap<>(properties)); + } + + // Create Matchers from configured includes and excludes for use when matching broker resources + includes.forEach((entry) -> includeMatchers.add(new QueueMatcher(entry.getKey(), entry.getValue(), wildcardConfig))); + excludes.forEach((entry) -> excludeMatchers.add(new QueueMatcher(entry.getKey(), entry.getValue(), wildcardConfig))); + } + + public String getPolicyName() { + return policyName; + } + + public boolean isIncludeFederated() { + return includeFederated; + } + + public int getPriorityAjustment() { + return priorityAdjustment; + } + + public Collection> getIncludes() { + return includes; + } + + public Collection> getExcludes() { + return excludes; + } + + public Map getProperties() { + return properties; + } + + public TransformerConfiguration getTransformerConfiguration() { + return transformerConfig; + } + + @Override + public boolean test(String address, String queue) { + for (QueueMatcher matcher : excludeMatchers) { + if (matcher.test(address, queue)) { + return false; + } + } + + for (QueueMatcher matcher : includeMatchers) { + if (matcher.test(address, queue)) { + return true; + } + } + + return false; + } + + private class QueueMatcher implements BiPredicate { + + private final Predicate addressMatch; + private final Predicate queueMatch; + + QueueMatcher(String address, String queue, WildcardConfiguration wildcardConfig) { + if (address == null || address.isEmpty()) { + addressMatch = (target) -> true; + } else { + addressMatch = new Match<>(address, null, wildcardConfig).getPattern().asPredicate(); + } + + if (queue == null || queue.isEmpty()) { + queueMatch = (target) -> true; + } else { + queueMatch = new Match<>(queue, null, wildcardConfig).getPattern().asPredicate(); + } + } + + @Override + public boolean test(String address, String queue) { + return addressMatch.test(address) && queueMatch.test(queue); + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationAddressPolicyManager.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationAddressPolicyManager.java new file mode 100644 index 0000000000..01a8a4866d --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationAddressPolicyManager.java @@ -0,0 +1,487 @@ +/* + * 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.artemis.protocol.amqp.federation.internal; + +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.postoffice.Binding; +import org.apache.activemq.artemis.core.postoffice.QueueBinding; +import org.apache.activemq.artemis.core.postoffice.impl.DivertBinding; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ActiveMQServerLogger; +import org.apache.activemq.artemis.core.server.Divert; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.federation.Federation; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerAddressPlugin; +import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerBindingPlugin; +import org.apache.activemq.artemis.core.transaction.Transaction; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manager for a federation which has address federation configuration which requires + * monitoring broker addresses and diverts for demand and creating a consumer on the + * remote side to federate messages back to this peer. + * + * Address federation replicates messages from the remote broker's address to an address + * on this broker but only when there is local demand on that address. If there is no + * local demand then federation if already established is halted. The manager creates + * a remote consumer on the federated address without any filtering other than that + * required for internal functionality in order to allow for a single remote consumer + * which can federate all messages to the local side where the existing queues can apply + * any filtering they have in place. + */ +public abstract class FederationAddressPolicyManager implements ActiveMQServerBindingPlugin, ActiveMQServerAddressPlugin { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected final ActiveMQServer server; + protected final FederationReceiveFromAddressPolicy policy; + protected final Map remoteConsumers = new HashMap<>(); + protected final FederationInternal federation; + protected final Map> matchingDiverts = new HashMap<>(); + + private volatile boolean started; + + public FederationAddressPolicyManager(FederationInternal federation, FederationReceiveFromAddressPolicy addressPolicy) throws ActiveMQException { + Objects.requireNonNull(federation, "The Federation instance cannot be null"); + Objects.requireNonNull(addressPolicy, "The Address match policy cannot be null"); + + this.federation = federation; + this.policy = addressPolicy; + this.server = federation.getServer(); + this.server.registerBrokerPlugin(this); + } + + /** + * Start the address policy manager which will initiate a scan of all broker divert + * bindings and create and matching remote receivers. Start on a policy manager + * should only be called after its parent {@link Federation} is started and the + * federation connection has been established. + */ + public synchronized void start() { + if (!started) { + started = true; + server.registerBrokerPlugin(this); + scanAllBindings(); // Create remote consumers for existing addresses with demand. + } + } + + /** + * Stops the address policy manager which will close any open remote receivers that are + * active for local queue demand. Stop should generally be called whenever the parent + * {@link Federation} loses its connection to the remote. + */ + public synchronized void stop() { + if (started) { + started = false; + server.unRegisterBrokerPlugin(this); + remoteConsumers.forEach((k, v) -> v.getConsumer().close()); // Cleanup and recreate if ever reconnected. + remoteConsumers.clear(); + matchingDiverts.clear(); + } + } + + @Override + public synchronized void afterAddAddress(AddressInfo addressInfo, boolean reload) { + if (started && policy.isEnableDivertBindings() && policy.test(addressInfo)) { + try { + // A Divert can exist in configuration prior to the address having been auto created + // etc so upon address add this check needs to be run to capture addresses that now + // match the divert. + server.getPostOffice() + .getDirectBindings(addressInfo.getName()) + .stream().filter(binding -> binding instanceof DivertBinding) + .forEach(this::afterAddBinding); + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.federationBindingsLookupError(addressInfo.getName(), e); + } + } + } + + @Override + public synchronized void afterAddBinding(Binding binding) { + if (started) { + checkBindingForMatch(binding); + } + } + + @Override + public synchronized void beforeRemoveBinding(SimpleString bindingName, Transaction tx, boolean deleteData) { + final Binding binding = server.getPostOffice().getBinding(bindingName); + final AddressInfo addressInfo = server.getPostOffice().getAddressInfo(binding.getAddress()); + + if (binding instanceof QueueBinding) { + tryRemoveDemandOnAddress(addressInfo); + + if (policy.isEnableDivertBindings()) { + // See if there is any matching diverts that match this queue binding and remove demand now that + // the queue is going away. Since a divert can be composite we need to check for a match of the + // queue address on each of the forwards if there are any. + matchingDiverts.entrySet().forEach(entry -> { + final SimpleString forwardAddress = entry.getKey().getDivert().getForwardAddress(); + + if (isAddressInDivertForwards(binding.getAddress(), forwardAddress)) { + final AddressInfo srcAddressInfo = server.getPostOffice().getAddressInfo(entry.getKey().getAddress()); + + if (entry.getValue().remove(((QueueBinding) binding).getQueue().getName())) { + tryRemoveDemandOnAddress(srcAddressInfo); + } + } + }); + } + } else if (policy.isEnableDivertBindings() || binding instanceof DivertBinding) { + final DivertBinding divertBinding = (DivertBinding) binding; + final Set matchingQueues = matchingDiverts.remove(binding); + + // Each entry in the matching queues set is one instance of demand that was + // registered on the source address which would have been federated from the + // remote so on remove we deduct each and if that removes all demand the remote + // consumer will be closed. + if (matchingQueues != null) { + try { + matchingQueues.forEach((queueName) -> tryRemoveDemandOnAddress(addressInfo)); + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.federationBindingsLookupError(divertBinding.getDivert().getForwardAddress(), e); + } + } + } + } + + protected final void tryRemoveDemandOnAddress(AddressInfo addressInfo) { + final FederationConsumerInfo consumerInfo = createConsumerInfo(addressInfo); + final FederationConsumerEntry entry = remoteConsumers.get(consumerInfo); + + if (entry != null && entry.reduceDemand()) { + final FederationConsumerInternal federationConsuner = entry.getConsumer(); + + try { + signalBeforeCloseFederationConsumer(federationConsuner); + federationConsuner.close(); + signalAfterCloseFederationConsumer(federationConsuner); + } finally { + remoteConsumers.remove(consumerInfo); + } + } + } + + /** + * Scans all bindings and push them through the normal bindings checks that + * would be done on an add. We filter here based on whether diverts are enabled + * just to reduce the result set but the check call should also filter as + * during normal operations divert bindings could be added. + */ + protected final void scanAllBindings() { + server.getPostOffice() + .getAllBindings() + .filter(bind -> bind instanceof QueueBinding || (policy.isEnableDivertBindings() && bind instanceof DivertBinding)) + .forEach(bind -> checkBindingForMatch(bind)); + } + + protected final void checkBindingForMatch(Binding binding) { + if (binding instanceof QueueBinding) { + final QueueBinding queueBinding = (QueueBinding) binding; + final AddressInfo addressInfo = server.getPostOffice().getAddressInfo(binding.getAddress()); + + reactIfBindingMatchesPolicy(addressInfo, queueBinding); + reactIfQueueBindingMatchesAnyDivertTarget(queueBinding); + } else if (binding instanceof DivertBinding) { + reactIfAnyQueueBindingMatchesDivertTarget((DivertBinding) binding); + } + } + + protected final void reactIfAnyQueueBindingMatchesDivertTarget(DivertBinding divertBinding) { + if (!policy.isEnableDivertBindings()) { + return; + } + + final AddressInfo addressInfo = server.getPostOffice().getAddressInfo(divertBinding.getAddress()); + + if (!testIfAddressMatchesPolicy(addressInfo)) { + return; + } + + // We only need to check if we've never seen the divert before, afterwards we will + // be checking it any time a new QueueBinding is added instead. + if (matchingDiverts.get(divertBinding) == null) { + final Set matchingQueues = new HashSet<>(); + matchingDiverts.put(divertBinding, matchingQueues); + + // We must account for the composite divert case by splitting the address and + // getting the bindings on each one. + final SimpleString forwardAddress = divertBinding.getDivert().getForwardAddress(); + final SimpleString[] forwardAddresses = forwardAddress.split(','); + + try { + for (SimpleString forward : forwardAddresses) { + server.getPostOffice().getBindingsForAddress(forward).getBindings() + .stream().filter(b -> b instanceof QueueBinding) + .map(b -> (QueueBinding) b) + .forEach(queueBinding -> { + if (isPluginBlockingFederationConsumerCreate(divertBinding.getDivert(), queueBinding.getQueue())) { + return; + } + + if (reactIfBindingMatchesPolicy(addressInfo, queueBinding)) { + matchingQueues.add(queueBinding.getQueue().getName()); + } + }); + } + } catch (Exception e) { + ActiveMQServerLogger.LOGGER.federationBindingsLookupError(forwardAddress, e); + } + } + } + + protected final void reactIfQueueBindingMatchesAnyDivertTarget(QueueBinding queueBinding) { + if (!policy.isEnableDivertBindings()) { + return; + } + + final SimpleString queueAddress = queueBinding.getAddress(); + final SimpleString queueName = queueBinding.getQueue().getName(); + + matchingDiverts.entrySet().forEach((e) -> { + final SimpleString forwardAddress = e.getKey().getDivert().getForwardAddress(); + final DivertBinding divertBinding = e.getKey(); + + // Check matched diverts to see if the QueueBinding address matches the address or + // addresses (composite diverts) of the Divert and if so then we can check if we need + // to create demand on the source address on the remote if we haven't done so already. + + if (!e.getValue().contains(queueName) && isAddressInDivertForwards(queueAddress, forwardAddress)) { + if (isPluginBlockingFederationConsumerCreate(divertBinding.getDivert(), queueBinding.getQueue())) { + return; + } + + final AddressInfo addressInfo = server.getPostOffice().getAddressInfo(divertBinding.getAddress()); + + // We know it matches address policy at this point but we don't yet know if any other + // remote demand exists and we want to check here if the react method did indeed add + // demand on the address and if so add this queue into the diverts matching queues set. + if (reactIfBindingMatchesPolicy(addressInfo, queueBinding)) { + e.getValue().add(queueName); + } + } + }); + } + + private static boolean isAddressInDivertForwards(final SimpleString queueAddress, final SimpleString forwardAddress) { + final SimpleString[] forwardAddresses = forwardAddress.split(','); + + for (SimpleString forward : forwardAddresses) { + if (queueAddress.equals(forward)) { + return true; + } + } + + return false; + } + + protected final boolean reactIfBindingMatchesPolicy(AddressInfo address, QueueBinding binding) { + if (testIfAddressMatchesPolicy(address)) { + logger.trace("Federation Address Policy matched on for demand on address: {} : binding: {}", address, binding); + + final FederationConsumerInfo consumerInfo = createConsumerInfo(address); + + // Check for existing consumer add demand from a additional local consumer + // to ensure the remote consumer remains active until all local demand is + // withdrawn. + if (remoteConsumers.containsKey(consumerInfo)) { + logger.trace("Federation Address Policy manager found existing demand for address: {}", address); + remoteConsumers.get(consumerInfo).addDemand(); + } else { + if (isPluginBlockingFederationConsumerCreate(address)) { + return false; + } + + if (isPluginBlockingFederationConsumerCreate(binding.getQueue())) { + return false; + } + + logger.trace("Federation Address Policy manager creating remote consumer for address: {}", address); + + signalBeforeCreateFederationConsumer(consumerInfo); + + final FederationConsumerInternal queueConsumer = createFederationConsumer(consumerInfo); + final FederationConsumerEntry entry = createConsumerEntry(queueConsumer); + + // Handle remote close with remove of consumer which means that future demand will + // attempt to create a new consumer for that demand. Ensure that thread safety is + // accounted for here as the notification can be asynchronous. + queueConsumer.setRemoteClosedHandler((closedConsumer) -> { + synchronized (this) { + try { + remoteConsumers.remove(closedConsumer.getConsumerInfo()); + } finally { + closedConsumer.close(); + } + } + }); + + // Called under lock so state should stay in sync + remoteConsumers.put(consumerInfo, entry); + + // Now that we are tracking it we can start it + queueConsumer.start(); + + signalAfterCreateFederationConsumer(queueConsumer); + } + + return true; + } + + return false; + } + + /** + * Performs the test against the configured address policy to check if the target + * address is a match or not. A subclass can override this method and provide its + * own match tests in combination with the configured matching policy. + * + * @param addressInfo + * The address that is being tested for a policy match. + * + * @return true if the address given is a match against the policy. + */ + protected boolean testIfAddressMatchesPolicy(AddressInfo addressInfo) { + return policy.test(addressInfo); + } + + /** + * Create a new {@link FederationConsumerInfo} based on the given {@link AddressInfo} + * and the configured {@link FederationReceiveFromAddressPolicy}. A subclass must override this + * method to return a consumer information object with the data used be that implementation. + * + * @param address + * The {@link AddressInfo} to use as a basis for the consumer information object. + * + * @return a new {@link FederationConsumerInfo} instance based on the given address. + */ + protected abstract FederationConsumerInfo createConsumerInfo(AddressInfo address); + + /** + * Creates a {@link FederationConsumerEntry} instance that will be used to store a {@link FederationConsumer} + * along with other state data needed to manage a federation consumer instance. A subclass can override + * this method to return a more customized entry type with additional state data. + * + * @param consumer + * The {@link FederationConsumerInternal} instance that will be housed in this entry. + * + * @return a new {@link FederationConsumerEntry} that holds the given federation consumer. + */ + protected FederationConsumerEntry createConsumerEntry(FederationConsumerInternal consumer) { + return new FederationConsumerEntry(consumer); + } + + /** + * Create a new {@link FederationConsumerInternal} instance using the consumer information + * given. This is called when local demand for a matched queue requires a new consumer to + * be created. This method by default will call the configured consumer factory function that + * was provided when the manager was created, a subclass can override this to perform additional + * actions for the create operation. + * + * @param consumerInfo + * The {@link FederationConsumerInfo} that defines the consumer to be created. + * + * @return a new {@link FederationConsumerInternal} instance that will reside in this manager. + */ + protected abstract FederationConsumerInternal createFederationConsumer(FederationConsumerInfo consumerInfo); + + /** + * Signal any registered plugins for this federation instance that a remote Address consumer + * is being created. + * + * @param info + * The {@link FederationConsumerInfo} that describes the remote Address consumer + */ + protected abstract void signalBeforeCreateFederationConsumer(FederationConsumerInfo info); + + /** + * Signal any registered plugins for this federation instance that a remote Address consumer + * has been created. + * + * @param consumer + * The {@link FederationConsumerInfo} that describes the remote Address consumer + */ + protected abstract void signalAfterCreateFederationConsumer(FederationConsumer consumer); + + /** + * Signal any registered plugins for this federation instance that a remote Address consumer + * is about to be closed. + * + * @param consumer + * The {@link FederationConsumer} that that is about to be closed. + */ + protected abstract void signalBeforeCloseFederationConsumer(FederationConsumer consumer); + + /** + * Signal any registered plugins for this federation instance that a remote Address consumer + * has now been closed. + * + * @param consumer + * The {@link FederationConsumer} that that has been closed. + */ + protected abstract void signalAfterCloseFederationConsumer(FederationConsumer consumer); + + /** + * Query all registered plugins for this federation instance to determine if any wish to + * prevent a federation consumer from being created for the given Queue. + * + * @param address + * The address on which the manager is intending to create a remote consumer for. + * + * @return true if any registered plugin signaled that creation should be suppressed. + */ + protected abstract boolean isPluginBlockingFederationConsumerCreate(AddressInfo address); + + /** + * Query all registered plugins for this federation instance to determine if any wish to + * prevent a federation consumer from being created for the given Queue. + * + * @param divert + * The {@link Divert} that triggered the manager to attempt to create a remote consumer. + * @param queue + * The {@link Queue} that triggered the manager to attempt to create a remote consumer. + * + * @return true if any registered plugin signaled that creation should be suppressed. + */ + protected abstract boolean isPluginBlockingFederationConsumerCreate(Divert divert, Queue queue); + + /** + * Query all registered plugins for this federation instance to determine if any wish to + * prevent a federation consumer from being created for the given Queue. + * + * @param queue + * The {@link Queue} that triggered the manager to attempt to create a remote consumer. + * + * @return true if any registered plugin signaled that creation should be suppressed. + */ + protected abstract boolean isPluginBlockingFederationConsumerCreate(Queue queue); + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationConsumerEntry.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationConsumerEntry.java new file mode 100644 index 0000000000..2abbdc2798 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationConsumerEntry.java @@ -0,0 +1,71 @@ +/* + * 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.artemis.protocol.amqp.federation.internal; + +/** + * Am entry type class used to hold a {@link FederationConsumerInternal} and + * any other state data needed by the manager that is creating them based on the + * policy configuration for the federation instance. The entry can be extended + * by federation implementation to hold additional state data for the federation + * consumer and the managing of its lifetime. + * + * This entry type provides a reference counter that can be used to register demand + * on a federation resource such that it is not torn down until all demand has been + * removed from the local resource. + */ +public class FederationConsumerEntry { + + private final FederationConsumerInternal consumer; + + private int references = 1; + + /** + * Creates a new consumer entry with a single reference + * + * @param consumer + * The federation consumer that will be carried in this entry. + */ + public FederationConsumerEntry(FederationConsumerInternal consumer) { + this.consumer = consumer; + } + + /** + * @return the consumer managed by this entry + */ + public FederationConsumerInternal getConsumer() { + return consumer; + } + + /** + * Add additional demand on the resource associated with this entries consumer. + */ + public void addDemand() { + references++; + } + + /** + * Reduce the known demand on the resource this entries consumer is associated with + * and returns true when demand reaches zero which indicates the consumer should be + * closed and the entry cleaned up. + * + * @return true if demand has fallen to zero on the resource associated with the consumer. + */ + public boolean reduceDemand() { + return --references == 0; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationConsumerInternal.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationConsumerInternal.java new file mode 100644 index 0000000000..b9240c62ea --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationConsumerInternal.java @@ -0,0 +1,56 @@ +/* + * 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.artemis.protocol.amqp.federation.internal; + +import java.util.function.Consumer; + +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; + +/** + * Internal federated consumer API that is subject to change without notice. + */ +public interface FederationConsumerInternal extends FederationConsumer { + + /** + * Starts the consumer instance which includes creating the remote resources + * and performing any internal initialization needed to fully establish the + * consumer instance. This call should not block and any errors encountered + * on creation of the backing consumer resources should utilize the error + * handling mechanisms of this {@link Federation} instance. + */ + void start(); + + /** + * Close the federation consumer instance and cleans up its resources. This method + * should not block and the actual resource shutdown work should occur asynchronously. + */ + void close(); + + /** + * Provides and event point for notification of the consumer having been closed by + * the remote. + * + * @param handler + * The handler that will be invoked when the remote closes this consumer. + * + * @return this consumer instance. + */ + FederationConsumerInternal setRemoteClosedHandler(Consumer handler); + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationGenericConsumerInfo.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationGenericConsumerInfo.java new file mode 100644 index 0000000000..5d533cb6c3 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationGenericConsumerInfo.java @@ -0,0 +1,184 @@ +/* + * 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.artemis.protocol.amqp.federation.internal; + +import java.util.Objects; + +import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.filter.Filter; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.ServerConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.utils.CompositeAddress; + +/** + * Information and identification class for Federation consumers created to + * federate queues. Instances of this class should be usable in Collections + * classes where equality and hashing support is needed. + */ +public class FederationGenericConsumerInfo implements FederationConsumerInfo { + + public static final String FEDERATED_QUEUE_PREFIX = "federated"; + public static final String QUEUE_NAME_FORMAT_STRING = "${address}::${routeType}"; + + private final Role role; + private final String address; + private final String queueName; + private final RoutingType routingType; + private final String filterString; + private final String fqqn; + private final int priority; + + protected FederationGenericConsumerInfo(Role role, String address, String queueName, RoutingType routingType, + String filterString, String fqqn, int priority) { + this.role = role; + this.address = address; + this.queueName = queueName; + this.routingType = routingType; + this.filterString = filterString; + this.fqqn = fqqn; + this.priority = priority; + } + + /** + * Factory for creating federation queue consumer information objects from server resources. + * + * @param consumer + * The {@link ServerConsumer} that this federation consumer is created for + * @param federation + * The parent {@link Federation} that this federation consumer is created for + * @param policy + * The {@link FederationReceiveFromQueuePolicy} that triggered this information object to be created. + * + * @return a newly created and configured {@link FederationConsumerInfo} instance. + */ + public static FederationGenericConsumerInfo build(ServerConsumer consumer, Federation federation, FederationReceiveFromQueuePolicy policy) { + final Queue queue = consumer.getQueue(); + final String queueName = queue.getName().toString(); + final String address = queue.getAddress().toString(); + final int priority = consumer.getPriority() + policy.getPriorityAjustment(); + final SimpleString filterString = Filter.toFilterString(queue.getFilter()); + + return new FederationGenericConsumerInfo(Role.QUEUE_CONSUMER, + address, + queueName, + queue.getRoutingType(), + filterString != null ? filterString.toString() : null, + CompositeAddress.toFullyQualified(address, queueName), + priority); + } + + /** + * Factory for creating federation address consumer information objects from server resources. + * + * @param address + * The address being federated, the remote consumer will be created under this address. + * @param queueName + * The name of the remote queue that will be created in order to route messages here. + * @param routingType + * The routing type to assign the remote consumer. + * @param filterString + * A filter string used by the federation instance to limit what enters the remote queue. + * @param federation + * The parent {@link Federation} that this federation consumer is created for + * @param policy + * The {@link FederationReceiveFromAddressPolicy} that triggered this information object to be created. + * + * @return a newly created and configured {@link FederationConsumerInfo} instance. + */ + public static FederationGenericConsumerInfo build(String address, String queueName, RoutingType routingType, String filterString, Federation federation, FederationReceiveFromAddressPolicy policy) { + return new FederationGenericConsumerInfo(Role.ADDRESS_CONSUMER, + address, + queueName, + routingType, + filterString, + CompositeAddress.toFullyQualified(address, queueName), + ActiveMQDefaultConfiguration.getDefaultConsumerPriority()); + } + + @Override + public Role getRole() { + return role; + } + + @Override + public String getQueueName() { + return queueName; + } + + @Override + public String getAddress() { + return address; + } + + @Override + public String getFqqn() { + return fqqn; + } + + @Override + public RoutingType getRoutingType() { + return routingType; + } + + @Override + public String getFilterString() { + return filterString; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof FederationGenericConsumerInfo)) { + return false; + } + + final FederationGenericConsumerInfo that = (FederationGenericConsumerInfo) o; + + return role == that.role && + priority == that.priority && + Objects.equals(address, that.address) && + Objects.equals(queueName, that.queueName) && + routingType == that.routingType && + Objects.equals(filterString, that.filterString) && + Objects.equals(fqqn, that.fqqn); + } + + @Override + public int hashCode() { + return Objects.hash(role, address, queueName, routingType, filterString, fqqn, priority); + } + + @Override + public String toString() { + return "FederationConsumerInfo: { " + getRole() + ", " + getFqqn() + "}"; + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationInternal.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationInternal.java new file mode 100644 index 0000000000..de52b8db3f --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationInternal.java @@ -0,0 +1,42 @@ +/* + * 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.artemis.protocol.amqp.federation.internal; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; + +/** + * Internal federated server API that is subject to change without notice. + */ +public interface FederationInternal extends Federation { + + /** + * Start the federation instance if not already started. + * + * @throws ActiveMQException if an error occurs during the start. + */ + void start() throws ActiveMQException; + + /** + * Stop the federation instance if not already stopped. + * + * @throws ActiveMQException if an error occurs during the stop. + */ + void stop() throws ActiveMQException; + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationQueuePolicyManager.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationQueuePolicyManager.java new file mode 100644 index 0000000000..c715ea0502 --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/federation/internal/FederationQueuePolicyManager.java @@ -0,0 +1,336 @@ +/* + * 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.artemis.protocol.amqp.federation.internal; + +import static org.apache.activemq.artemis.protocol.amqp.federation.FederationConstants.FEDERATION_NAME; + +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.core.filter.Filter; +import org.apache.activemq.artemis.core.filter.impl.FilterImpl; +import org.apache.activemq.artemis.core.postoffice.QueueBinding; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.ServerConsumer; +import org.apache.activemq.artemis.core.server.ServerSession; +import org.apache.activemq.artemis.core.server.federation.Federation; +import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerConsumerPlugin; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manager for a federation which has queue federation configuration which requires + * monitoring broker queues for demand and creating a consumer for on the remote side + * to federate messages back to this peer. + */ +public abstract class FederationQueuePolicyManager implements ActiveMQServerConsumerPlugin { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected final ActiveMQServer server; + protected final Predicate federationConsumerMatcher; + protected final FederationReceiveFromQueuePolicy policy; + protected final Map remoteConsumers = new HashMap<>(); + protected final FederationInternal federation; + + private volatile boolean started; + + public FederationQueuePolicyManager(FederationInternal federation, FederationReceiveFromQueuePolicy queuePolicy) throws ActiveMQException { + Objects.requireNonNull(federation, "The Federation instance cannot be null"); + Objects.requireNonNull(queuePolicy, "The Queue match policy cannot be null"); + + this.federation = federation; + this.policy = queuePolicy; + this.server = federation.getServer(); + this.federationConsumerMatcher = createFederationConsumerMatcher(server, queuePolicy); + } + + /** + * Start the queue policy manager which will initiate a scan of all broker queue + * bindings and create and matching remote receivers. Start on a policy manager + * should only be called after its parent {@link Federation} is started and the + * federation connection has been established. + */ + public synchronized void start() { + if (!started) { + started = true; + server.registerBrokerPlugin(this); + scanAllQueueBindings(); // Create consumers for existing queue with demand. + } + } + + /** + * Stops the queue policy manager which will close any open remote receivers that are + * active for local queue demand. Stop should generally be called whenever the parent + * {@link Federation} loses its connection to the remote. + */ + public synchronized void stop() { + if (started) { + // Ensures that on shutdown of a federation broker connection we don't leak + // broker plugin instances. + server.unRegisterBrokerPlugin(this); + started = false; + remoteConsumers.forEach((k, v) -> v.getConsumer().close()); // Cleanup and recreate if ever reconnected. + remoteConsumers.clear(); + } + } + + @Override + public synchronized void afterCreateConsumer(ServerConsumer consumer) { + if (started) { + reactIfConsumerMatchesPolicy(consumer); + } + } + + @Override + public synchronized void beforeCloseConsumer(ServerConsumer consumer, boolean failed) { + if (started) { + final FederationConsumerInfo consumerInfo = createConsumerInfo(consumer); + final FederationConsumerEntry entry = remoteConsumers.get(consumerInfo); + + if (entry != null && entry.reduceDemand()) { + final FederationConsumerInternal federationConsuner = entry.getConsumer(); + + try { + signalBeforeCloseFederationConsumer(federationConsuner); + federationConsuner.close(); + signalAfterCloseFederationConsumer(federationConsuner); + } finally { + remoteConsumers.remove(consumerInfo); + } + } + } + } + + protected final void scanAllQueueBindings() { + server.getPostOffice() + .getAllBindings() + .filter(b -> b instanceof QueueBinding) + .map(b -> (QueueBinding) b) + .forEach(b -> checkQueueForMatch(b.getQueue())); + } + + protected final void checkQueueForMatch(Queue queue) { + queue.getConsumers() + .stream() + .filter(consumer -> consumer instanceof ServerConsumer) + .map(c -> (ServerConsumer) c).forEach(this::reactIfConsumerMatchesPolicy); + } + + protected final void reactIfConsumerMatchesPolicy(ServerConsumer consumer) { + if (testIfQueueMatchesPolicy(consumer.getQueueAddress().toString(), consumer.getQueueName().toString())) { + // We should ignore federation consumers from remote peers but configuration does allow + // these to be federated again for some very specific use cases so we check before then + // moving onto any server plugin checks kick in. + if (federationConsumerMatcher.test(consumer)) { + return; + } + + if (isPluginBlockingFederationConsumerCreate(consumer.getQueue())) { + return; + } + + logger.trace("Federation Policy matched on consumer for binding: {}", consumer.getBinding()); + + final FederationConsumerInfo consumerInfo = createConsumerInfo(consumer); + + // Check for existing consumer add demand from a additional local consumer + // to ensure the remote consumer remains active until all local demand is + // withdrawn. + if (remoteConsumers.containsKey(consumerInfo)) { + remoteConsumers.get(consumerInfo).addDemand(); + } else { + signalBeforeCreateFederationConsumer(consumerInfo); + + final FederationConsumerInternal queueConsumer = createFederationConsumer(consumerInfo); + final FederationConsumerEntry entry = createConsumerEntry(queueConsumer); + + // Handle remote close with remove of consumer which means that future demand will + // attempt to create a new consumer for that demand. Ensure that thread safety is + // accounted for here as the notification can be asynchronous. + queueConsumer.setRemoteClosedHandler((closedConsumer) -> { + synchronized (this) { + try { + remoteConsumers.remove(closedConsumer.getConsumerInfo()); + } finally { + closedConsumer.close(); + } + } + }); + + // Called under lock so state should stay in sync + remoteConsumers.put(consumerInfo, entry); + + // Now that we are tracking it we can start it + queueConsumer.start(); + + signalAfterCreateFederationConsumer(queueConsumer); + } + } + } + + /** + * Performs the test against the configured queue policy to check if the target + * queue and its associated address is a match or not. A subclass can override + * this method and provide its own match tests in combination with the configured + * matching policy. + * + * @param address + * The address that is being tested for a policy match. + * @param queueName + * The name of the queue that is being tested for a policy match. + * + * @return true if the address given is a match against the policy. + */ + protected boolean testIfQueueMatchesPolicy(String address, String queueName) { + return policy.test(address, queueName); + } + + /** + * Create a new {@link FederationConsumerInfo} based on the given {@link ServerConsumer} + * and the configured {@link FederationReceiveFromQueuePolicy}. A subclass can override this + * method to return a consumer information object with additional data used be that implementation. + * + * @param consumer + * The {@link ServerConsumer} to use as a basis for the consumer information object. + * + * @return a new {@link FederationConsumerInfo} instance based on the server consumer + */ + protected FederationConsumerInfo createConsumerInfo(ServerConsumer consumer) { + return FederationGenericConsumerInfo.build(consumer, federation, policy); + } + + /** + * Creates a {@link FederationConsumerEntry} instance that will be used to store a {@link FederationConsumer} + * along with other state data needed to manage a federation consumer instance. A subclass can override + * this method to return a more customized entry type with additional state data. + * + * @param consumer + * The {@link FederationConsumerInternal} instance that will be housed in this entry. + * + * @return a new {@link FederationConsumerEntry} that holds the given federation consumer. + */ + protected FederationConsumerEntry createConsumerEntry(FederationConsumerInternal consumer) { + return new FederationConsumerEntry(consumer); + } + + /** + * Create a new {@link FederationConsumerInternal} instance using the consumer information + * given. This is called when local demand for a matched queue requires a new consumer to + * be created. A subclass must override this to perform the creation of the remote consumer. + * + * @param consumerInfo + * The {@link FederationConsumerInfo} that defines the consumer to be created. + * + * @return a new {@link FederationConsumerInternal} instance that will reside in this manager. + */ + protected abstract FederationConsumerInternal createFederationConsumer(FederationConsumerInfo consumerInfo); + + /** + * Creates a {@link Predicate} that should return true if the given consumer is a federation + * created consumer which should not be further federated. + * + * @param server + * The server instance for use in creating the filtering {@link Predicate}. + * @param policy + * The configured Queue matching policy that can provide additional match criteria. + * + * @return a {@link Predicate} that will return true if the consumer should be filtered. + * + * @throws ActiveMQException if an error occurs while creating the new consumer filter. + */ + protected Predicate createFederationConsumerMatcher(ActiveMQServer server, FederationReceiveFromQueuePolicy policy) throws ActiveMQException { + if (policy.isIncludeFederated()) { + return (consumer) -> false; // Configuration says to federate these + } else { + // This filter matches on the same criteria as the original Core client based + // Federation code which allows this implementation to see those consumers as + // well as its own which in this methods implementation must also use this same + // mechanism to mark federation resources. + + final Filter metaDataMatcher = + FilterImpl.createFilter("\"" + FEDERATION_NAME + "\" IS NOT NULL"); + + return (consumer) -> { + final ServerSession serverSession = server.getSessionByID(consumer.getSessionID()); + + if (serverSession != null && serverSession.getMetaData() != null) { + return metaDataMatcher.match(serverSession.getMetaData()); + } else { + return false; + } + }; + } + } + + /** + * Signal any registered plugins for this federation instance that a remote Queue consumer + * is being created. + * + * @param info + * The {@link FederationConsumerInfo} that describes the remote Queue consumer + */ + protected abstract void signalBeforeCreateFederationConsumer(FederationConsumerInfo info); + + /** + * Signal any registered plugins for this federation instance that a remote Queue consumer + * has been created. + * + * @param consumer + * The {@link FederationConsumerInfo} that describes the remote Queue consumer + */ + protected abstract void signalAfterCreateFederationConsumer(FederationConsumer consumer); + + /** + * Signal any registered plugins for this federation instance that a remote Queue consumer + * is about to be closed. + * + * @param consumer + * The {@link FederationConsumer} that that is about to be closed. + */ + protected abstract void signalBeforeCloseFederationConsumer(FederationConsumer consumer); + + /** + * Signal any registered plugins for this federation instance that a remote Queue consumer + * has now been closed. + * + * @param consumer + * The {@link FederationConsumer} that that has been closed. + */ + protected abstract void signalAfterCloseFederationConsumer(FederationConsumer consumer); + + /** + * Query all registered plugins for this federation instance to determine if any wish to + * prevent a federation consumer from being created for the given Queue. + * + * @param queue + * The {@link Queue} that the federation queue manager is attempting to create a remote consumer for. + * + * @return true if any registered plugin signaled that creation should be suppressed. + */ + protected abstract boolean isPluginBlockingFederationConsumerCreate(Queue queue); + +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/logger/ActiveMQAMQPProtocolMessageBundle.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/logger/ActiveMQAMQPProtocolMessageBundle.java index de5571370c..561122f02c 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/logger/ActiveMQAMQPProtocolMessageBundle.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/logger/ActiveMQAMQPProtocolMessageBundle.java @@ -18,6 +18,7 @@ package org.apache.activemq.artemis.protocol.amqp.logger; import org.apache.activemq.artemis.logs.annotation.LogBundle; import org.apache.activemq.artemis.logs.annotation.Message; +import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.logs.BundleFactory; import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPIllegalStateException; import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; @@ -101,4 +102,10 @@ public interface ActiveMQAMQPProtocolMessageBundle { @Message(id = 119024, value = "link is missing a desired capability declaration {}") ActiveMQAMQPIllegalStateException missingDesiredCapability(String capability); + + @Message(id = 119025, value = "Federation control link refused: address = {}") + ActiveMQAMQPIllegalStateException federationControlLinkRefused(String address); + + @Message(id = 119026, value = "Malformed Federation control message: {}") + ActiveMQException malformedFederationControlMessage(String address); } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java index 45ab664053..302d0218ee 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPConnectionContext.java @@ -42,8 +42,11 @@ import org.apache.activemq.artemis.core.security.SecurityAuth; import org.apache.activemq.artemis.protocol.amqp.broker.AMQPConnectionCallback; import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback; import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationAddressSenderController; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationQueueSenderController; import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource; import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPSecurityException; import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolLogger; import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle; import org.apache.activemq.artemis.protocol.amqp.proton.handler.EventHandler; @@ -76,11 +79,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.invoke.MethodHandles; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_ADDRESS_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK_VALIDATION_ADDRESS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_QUEUE_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.AMQP_LINK_INITIALIZER_KEY; import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.FAILOVER_SERVER_LIST; import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.HOSTNAME; import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.NETWORK_HOST; import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.PORT; import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.SCHEME; +import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyDesiredCapability;; public class AMQPConnectionContext extends ProtonInitializable implements EventHandler { @@ -98,8 +107,7 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH private final ClientSASLFactory saslClientFactory; private final Map connectionProperties = new HashMap<>(); private final ScheduledExecutorService scheduledPool; - - private LinkCloseListener linkCloseListener; + private final Map linkCloseListeners = new ConcurrentHashMap<>(); private final Map sessions = new ConcurrentHashMap<>(); @@ -111,7 +119,7 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH private final boolean bridgeConnection; private final ScheduleOperator scheduleOp = new ScheduleOperator(new ScheduleRunnable()); - private final AtomicReference> scheduledFutureRef = new AtomicReference(VOID_FUTURE); + private final AtomicReference> scheduledFutureRef = new AtomicReference<>(VOID_FUTURE); private String user; private String password; @@ -182,15 +190,46 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH } } - public LinkCloseListener getLinkCloseListener() { - return linkCloseListener; + @Override + public void initialize() throws Exception { + initialized = true; } - public AMQPConnectionContext setLinkCloseListener(LinkCloseListener linkCloseListener) { - this.linkCloseListener = linkCloseListener; + /** + * Adds a listener that will be invoked any time an AMQP link is remotely closed + * before having been closed on this end of the connection. + * + * @param id + * A unique ID assigned to the listener used to later remove it if needed. + * @param linkCloseListener + * The instance of a closed listener. + * + * @return this connection context instance. + */ + public AMQPConnectionContext addLinkRemoteCloseListener(String id, LinkCloseListener linkCloseListener) { + linkCloseListeners.put(id, linkCloseListener); return this; } + /** + * Remove the link remote close listener that is identified by the given ID. + * + * @param id + * The unique ID assigned to the listener when it was added. + */ + public void removeLinkRemoteCloseListener(String id) { + linkCloseListeners.remove(id); + } + + /** + * Clear all link remote close listeners, usually done before connection + * termination to avoid any remote close events triggering processing + * after the connection shutdown has already started. + */ + public void clearLinkRemoteCloseListeners() { + linkCloseListeners.clear(); + } + public boolean isBridgeConnection() { return bridgeConnection; } @@ -342,12 +381,11 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH } protected void remoteLinkOpened(Link link) throws Exception { + final AMQPSessionContext protonSession = getSessionExtension(link.getSession()); - AMQPSessionContext protonSession = getSessionExtension(link.getSession()); - - Runnable runnable = link.attachments().get(Runnable.class, Runnable.class); + final Runnable runnable = link.attachments().get(AMQP_LINK_INITIALIZER_KEY, Runnable.class); if (runnable != null) { - link.attachments().set(Runnable.class, Runnable.class, null); + link.attachments().set(AMQP_LINK_INITIALIZER_KEY, Runnable.class, null); runnable.run(); return; } @@ -358,71 +396,93 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH link.setSource(link.getRemoteSource()); link.setTarget(link.getRemoteTarget()); + if (link instanceof Receiver) { Receiver receiver = (Receiver) link; if (link.getRemoteTarget() instanceof Coordinator) { Coordinator coordinator = (Coordinator) link.getRemoteTarget(); protonSession.addTransactionHandler(coordinator, receiver); + } else if (isReplicaTarget(receiver)) { + handleReplicaTargetLinkOpened(protonSession, receiver); + } else if (isFederationControlLink(receiver)) { + handleFederationControlLinkOpened(protonSession, receiver); } else { - if (isReplicaTarget(receiver)) { - try { - try { - protonSession.getSessionSPI().check(SimpleString.toSimpleString(link.getTarget().getAddress()), CheckType.SEND, getSecurityAuth()); - } catch (ActiveMQSecurityException e) { - throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.securityErrorCreatingProducer(e.getMessage()); - } - - if (!verifyDesiredCapabilities(receiver, AMQPMirrorControllerSource.MIRROR_CAPABILITY)) { - throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingDesiredCapability(AMQPMirrorControllerSource.MIRROR_CAPABILITY.toString()); - } - } catch (ActiveMQAMQPException e) { - logger.warn(e.getMessage(), e); - - link.setTarget(null); - link.setCondition(new ErrorCondition(e.getAmqpError(), e.getMessage())); - link.close(); - - return; - } - - receiver.setOfferedCapabilities(new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY}); - protonSession.addReplicaTarget(receiver); - } else { - protonSession.addReceiver(receiver); - } + protonSession.addReceiver(receiver); } } else { - Sender sender = (Sender) link; - protonSession.addSender(sender); - } - } - - - protected boolean verifyDesiredCapabilities(Receiver reciever, Symbol s) { - - if (reciever.getRemoteDesiredCapabilities() == null) { - return false; - } - - boolean foundS = false; - for (Symbol b : reciever.getRemoteDesiredCapabilities()) { - if (b.equals(s)) { - foundS = true; - break; + final Sender sender = (Sender) link; + if (isFederationAddressReceiver(sender)) { + protonSession.addSender(sender, new AMQPFederationAddressSenderController(protonSession)); + } else if (isFederationQueueReceiver(sender)) { + protonSession.addSender(sender, new AMQPFederationQueueSenderController(protonSession)); + } else { + protonSession.addSender(sender); } } - if (!foundS) { - return false; - } - - return true; } + private void handleReplicaTargetLinkOpened(AMQPSessionContext protonSession, Receiver receiver) throws Exception { + try { + try { + protonSession.getSessionSPI().check(SimpleString.toSimpleString(receiver.getTarget().getAddress()), CheckType.SEND, getSecurityAuth()); + } catch (ActiveMQSecurityException e) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.securityErrorCreatingProducer(e.getMessage()); + } - private boolean isReplicaTarget(Link link) { + if (!verifyDesiredCapability(receiver, AMQPMirrorControllerSource.MIRROR_CAPABILITY)) { + throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingDesiredCapability(AMQPMirrorControllerSource.MIRROR_CAPABILITY.toString()); + } + } catch (ActiveMQAMQPException e) { + logger.warn(e.getMessage(), e); + + receiver.setTarget(null); + receiver.setCondition(new ErrorCondition(e.getAmqpError(), e.getMessage())); + receiver.close(); + + return; + } + + receiver.setOfferedCapabilities(new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY}); + protonSession.addReplicaTarget(receiver); + } + + private void handleFederationControlLinkOpened(AMQPSessionContext protonSession, Receiver receiver) throws Exception { + try { + try { + protonSession.getSessionSPI().check(SimpleString.toSimpleString(FEDERATION_CONTROL_LINK_VALIDATION_ADDRESS), CheckType.SEND, getSecurityAuth()); + } catch (ActiveMQSecurityException e) { + throw new ActiveMQAMQPSecurityException( + "User does not have permission to attach to the federation control address"); + } + + protonSession.addFederationCommandProcessor(receiver); + } catch (ActiveMQAMQPException e) { + logger.warn(e.getMessage(), e); + + receiver.setTarget(null); + receiver.setCondition(new ErrorCondition(e.getAmqpError(), e.getMessage())); + receiver.close(); + + return; + } + } + + private static boolean isReplicaTarget(Link link) { return link != null && link.getTarget() != null && link.getTarget().getAddress() != null && link.getTarget().getAddress().startsWith(ProtonProtocolManager.MIRROR_ADDRESS); } + private static boolean isFederationControlLink(Receiver receiver) { + return verifyDesiredCapability(receiver, FEDERATION_CONTROL_LINK); + } + + private static boolean isFederationQueueReceiver(Sender sender) { + return verifyDesiredCapability(sender, FEDERATION_QUEUE_RECEIVER); + } + + private static boolean isFederationAddressReceiver(Sender sender) { + return verifyDesiredCapability(sender, FEDERATION_ADDRESS_RECEIVER); + } + public Symbol[] getConnectionCapabilitiesOffered() { URI tc = connectionCallback.getFailoverList(); if (tc != null) { @@ -730,9 +790,15 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH public void onRemoteClose(Link link) throws Exception { handler.requireHandler(); - if (linkCloseListener != null) { - linkCloseListener.onClose(link); - } + final AtomicReference handlerThrew = new AtomicReference<>(); + + linkCloseListeners.forEach((k, v) -> { + try { + v.onClose(link); + } catch (Exception e) { + handlerThrew.compareAndSet(null, e); + } + }); ProtonDeliveryHandler linkContext = (ProtonDeliveryHandler) link.getContext(); if (linkContext != null) { @@ -746,6 +812,10 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH link.close(); link.free(); flush(); + + if (handlerThrew.get() != null) { + throw handlerThrew.get(); + } } @Override @@ -782,7 +852,6 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH } } - private class LocalSecurity implements SecurityAuth { @Override public String getUsername() { @@ -818,5 +887,4 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH return getProtocolManager().getSecurityDomain(); } } - } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPSessionContext.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPSessionContext.java index b64f41f3f8..f785dadf85 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPSessionContext.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AMQPSessionContext.java @@ -16,24 +16,35 @@ */ package org.apache.activemq.artemis.protocol.amqp.proton; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederation.FEDERATION_INSTANCE_RECORD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONFIGURATION; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import org.apache.activemq.artemis.api.core.ActiveMQException; import org.apache.activemq.artemis.api.core.ActiveMQSecurityException; import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederation; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationCommandProcessor; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConfiguration; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationTarget; import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource; import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerTarget; import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback; import org.apache.activemq.artemis.protocol.amqp.client.ProtonClientSenderContext; import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException; +import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPIllegalStateException; import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException; import org.apache.activemq.artemis.protocol.amqp.proton.transaction.ProtonTransactionHandler; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.transaction.Coordinator; import org.apache.qpid.proton.amqp.transport.ErrorCondition; +import org.apache.qpid.proton.engine.Connection; import org.apache.qpid.proton.engine.EndpointState; import org.apache.qpid.proton.engine.Receiver; import org.apache.qpid.proton.engine.Sender; @@ -74,10 +85,22 @@ public class AMQPSessionContext extends ProtonInitializable { return sessionSPI; } + public AMQPConnectionContext getAMQPConnectionContext() { + return connection; + } + + public Session getSession() { + return session; + } + + public ActiveMQServer getServer() { + return server; + } + @Override public void initialize() throws Exception { if (!isInitialized()) { - super.initialize(); + initialized = true; if (sessionSPI != null) { try { @@ -222,33 +245,85 @@ public class AMQPSessionContext extends ProtonInitializable { } public void addReplicaTarget(Receiver receiver) throws Exception { - try { - AMQPMirrorControllerTarget protonReceiver = new AMQPMirrorControllerTarget(sessionSPI, connection, this, receiver, server); - protonReceiver.initialize(); - receivers.put(receiver, protonReceiver); - sessionSPI.addProducer(receiver.getName(), receiver.getTarget().getAddress()); - receiver.setContext(protonReceiver); - HashMap brokerIDProperties = new HashMap<>(); + addReceiver(receiver, (r, s) -> { + final AMQPMirrorControllerTarget protonReceiver = + new AMQPMirrorControllerTarget(sessionSPI, connection, this, receiver, server); + + final HashMap brokerIDProperties = new HashMap<>(); brokerIDProperties.put(AMQPMirrorControllerSource.BROKER_ID, server.getNodeID().toString()); receiver.setProperties(brokerIDProperties); - connection.runNow(() -> { - receiver.open(); - connection.flush(); - }); - } catch (ActiveMQAMQPException e) { - receivers.remove(receiver); - receiver.setTarget(null); - receiver.setCondition(new ErrorCondition(e.getAmqpError(), e.getMessage())); - connection.runNow(() -> { - receiver.close(); - connection.flush(); - }); - } + + return protonReceiver; + }); + } + + @SuppressWarnings("unchecked") + public void addFederationCommandProcessor(Receiver receiver) throws Exception { + addReceiver(receiver, (r, s) -> { + final Connection protonConnection = receiver.getSession().getConnection(); + final org.apache.qpid.proton.engine.Record attachments = protonConnection.attachments(); + + try { + if (attachments.get(FEDERATION_INSTANCE_RECORD, AMQPFederation.class) != null) { + throw new ActiveMQAMQPIllegalStateException( + "Unexpected federation instance found on connection when creating control link processor"); + } + + final Map federationConfigurationMap; + + if (receiver.getRemoteProperties() == null || !receiver.getRemoteProperties().containsKey(FEDERATION_CONFIGURATION)) { + federationConfigurationMap = Collections.EMPTY_MAP; + } else { + federationConfigurationMap = (Map) receiver.getRemoteProperties().get(FEDERATION_CONFIGURATION); + } + + final AMQPFederationConfiguration configuration = new AMQPFederationConfiguration(connection, federationConfigurationMap); + final AMQPFederationTarget federation = new AMQPFederationTarget(receiver.getName(), configuration, this, server); + + federation.start(); + + final AMQPFederationCommandProcessor commandProcessor = + new AMQPFederationCommandProcessor(federation, sessionSPI.getAMQPSessionContext(), receiver); + + attachments.set(FEDERATION_INSTANCE_RECORD, AMQPFederationTarget.class, federation); + + return commandProcessor; + } catch (ActiveMQException e) { + final ActiveMQAMQPException cause; + + if (e instanceof ActiveMQAMQPException) { + cause = (ActiveMQAMQPException) e; + } else { + cause = new ActiveMQAMQPInternalErrorException(e.getMessage()); + } + + throw new RuntimeException(e.getMessage(), cause); + } + }); } public void addReceiver(Receiver receiver) throws Exception { + addReceiver(receiver, (r, s) -> { + return new ProtonServerReceiverContext(sessionSPI, connection, this, receiver); + }); + } + + @SuppressWarnings("unchecked") + public T addReceiver(Receiver receiver, BiFunction receiverBuilder) throws Exception { try { - ProtonServerReceiverContext protonReceiver = new ProtonServerReceiverContext(sessionSPI, connection, this, receiver); + final ProtonAbstractReceiver protonReceiver; + try { + protonReceiver = receiverBuilder.apply(this, receiver); + } catch (RuntimeException e) { + if (e.getCause() instanceof ActiveMQAMQPException) { + throw (ActiveMQAMQPException) e.getCause(); + } else if (e.getCause() != null) { + throw new ActiveMQAMQPInternalErrorException(e.getCause().getMessage(), e.getCause()); + } else { + throw new ActiveMQAMQPInternalErrorException(e.getMessage(), e); + } + } + protonReceiver.initialize(); receivers.put(receiver, protonReceiver); sessionSPI.addProducer(receiver.getName(), receiver.getTarget().getAddress()); @@ -257,6 +332,8 @@ public class AMQPSessionContext extends ProtonInitializable { receiver.open(); connection.flush(); }); + + return (T) protonReceiver; } catch (ActiveMQAMQPException e) { receivers.remove(receiver); receiver.setTarget(null); @@ -265,6 +342,8 @@ public class AMQPSessionContext extends ProtonInitializable { receiver.close(); connection.flush(); }); + + return null; } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupport.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupport.java index 4fdb3fbe1a..2729c87cdb 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupport.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupport.java @@ -18,16 +18,23 @@ package org.apache.activemq.artemis.protocol.amqp.proton; import java.util.AbstractMap; import java.util.Map; +import java.util.Objects; import org.apache.qpid.proton.amqp.DescribedType; import org.apache.qpid.proton.amqp.Symbol; import org.apache.qpid.proton.amqp.UnsignedLong; +import org.apache.qpid.proton.engine.Link; /** * Set of useful methods and definitions used in the AMQP protocol handling */ public class AmqpSupport { + // Key used to add a Runnable initializer to a link that the broker has opened + // which will be called when the remote responds to the broker outgoing Attach + // with its own Attach response. + public static final Object AMQP_LINK_INITIALIZER_KEY = Runnable.class; + // Default thresholds/values used for granting credit to producers public static final int AMQP_CREDITS_DEFAULT = 1000; public static final int AMQP_LOW_CREDITS_DEFAULT = 300; @@ -39,6 +46,7 @@ public class AmqpSupport { public static final boolean AMQP_USE_MODIFIED_FOR_TRANSIENT_DELIVERY_ERRORS = false; // Identification values used to locating JMS selector types. + public static final Symbol JMS_SELECTOR_KEY = Symbol.valueOf("jms-selector"); public static final UnsignedLong JMS_SELECTOR_CODE = UnsignedLong.valueOf(0x0000468C00000004L); public static final Symbol JMS_SELECTOR_NAME = Symbol.valueOf("apache.org:selector-filter:string"); public static final Object[] JMS_SELECTOR_FILTER_IDS = new Object[]{JMS_SELECTOR_CODE, JMS_SELECTOR_NAME}; @@ -67,6 +75,8 @@ public class AmqpSupport { public static final Symbol PLATFORM = Symbol.valueOf("platform"); public static final Symbol RESOURCE_DELETED = Symbol.valueOf("amqp:resource-deleted"); public static final Symbol CONNECTION_FORCED = Symbol.valueOf("amqp:connection:forced"); + public static final Symbol DETACH_FORCED = Symbol.valueOf("amqp:link:detach-forced"); + public static final Symbol NOT_FOUND = Symbol.valueOf("amqp:not-found"); public static final Symbol SHARED_SUBS = Symbol.valueOf("SHARED-SUBS"); public static final Symbol NETWORK_HOST = Symbol.valueOf("network-host"); public static final Symbol PORT = Symbol.valueOf("port"); @@ -145,4 +155,93 @@ public class AmqpSupport { return null; } + + private static final Symbol[] EMPTY_CAPABILITIES = new Symbol[0]; + + /** + * Verifies that the desired capabilities that were sent to the remote were indeed + * offered in return. If the remote has not offered a capability that was desired then + * the initiating resource should determine if the offered set is still acceptable or + * it should close the link and report the reason. + *

+ * The remote could have offered more capabilities than the requested desired capabilities, + * this method does not validate that or consider that a failure. + * + * @param link + * The link in question (Sender or Receiver). + * + * @return true if the remote offered all of the capabilities that were desired. + */ + public static boolean verifyOfferedCapabilities(final Link link) { + return verifyOfferedCapabilities(link, link.getDesiredCapabilities()); + } + + /** + * Verifies that the given set of desired capabilities (which should be the full set of + * desired capabilities configured on the link or a subset of those values) are indeed + * offered in return. If the remote has not offered a capability that was desired then + * the initiating resource should determine if the offered set is still acceptable or + * it should close the link and report the reason. + *

+ * The remote could have offered more capabilities than the requested desired capabilities, + * this method does not validate that or consider that a failure. + * + * @param link + * The link in question (Sender or Receiver). + * @param capabilities + * The capabilities that are required being checked for. + * + * @return true if the remote offered all of the capabilities that were desired. + */ + public static boolean verifyOfferedCapabilities(final Link link, final Symbol... capabilities) { + final Symbol[] desiredCapabilites = capabilities == null ? EMPTY_CAPABILITIES : capabilities; + final Symbol[] remoteOfferedCapabilites = + link.getRemoteOfferedCapabilities() == null ? EMPTY_CAPABILITIES : link.getRemoteOfferedCapabilities(); + + for (Symbol desired : desiredCapabilites) { + boolean foundCurrent = false; + + for (Symbol offered : remoteOfferedCapabilites) { + if (desired.equals(offered)) { + foundCurrent = true; + break; + } + } + + if (!foundCurrent) { + return false; + } + } + + return true; + } + + /** + * Verifies that the given remote desired capability is present in the remote link details. + *

+ * The remote could have desired more capabilities than the one given, this method does + * not validate that or consider that a failure. + * + * @param link + * The link in question (Sender or Receiver). + * @param desiredCapability + * The non-null capability that is being checked as being desired. + * + * @return true if the remote desired all of the capabilities that were given. + */ + public static boolean verifyDesiredCapability(final Link link, final Symbol desiredCapability) { + Objects.requireNonNull(desiredCapability, "Desired capability to verifiy cannot be null"); + + if (link.getRemoteDesiredCapabilities() == null) { + return false; + } + + for (Symbol capability : link.getRemoteDesiredCapabilities()) { + if (capability.equals(desiredCapability)) { + return true; + } + } + + return false; + } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonAbstractReceiver.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonAbstractReceiver.java index 4a5675cd5a..07c92d2ab9 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonAbstractReceiver.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonAbstractReceiver.java @@ -40,32 +40,20 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme protected final Receiver receiver; - /* - The maximum number of credits we will allocate to clients. - This number is also used by the broker when refresh client credits. - */ - protected final int amqpCredits; - - // Used by the broker to decide when to refresh clients credit. This is not used when client requests credit. - protected final int minCreditRefresh; - protected final int minLargeMessageSize; - final RoutingContext routingContext; + protected final RoutingContext routingContext; protected final AMQPSessionCallback sessionSPI; protected volatile AMQPLargeMessage currentLargeMessage; - /** - * We create this AtomicRunnable with setRan. - * This is because we always reuse the same instance. - * In case the creditRunnable was run, we reset and send it over. - * We set it as ran as the first one should always go through - */ + protected final Runnable creditRunnable; + protected final boolean useModified; protected int pendingSettles = 0; + public static boolean isBellowThreshold(int credit, int pending, int threshold) { return credit <= threshold - pending; } @@ -82,10 +70,8 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme this.connection = connection; this.protonSession = protonSession; this.receiver = receiver; - this.amqpCredits = connection.getAmqpCredits(); - this.minCreditRefresh = connection.getAmqpLowCredits(); - this.minLargeMessageSize = connection.getProtocolManager().getAmqpMinLargeMessageSize(); - this.creditRunnable = createCreditRunnable(amqpCredits, minCreditRefresh, receiver, connection, this); + this.minLargeMessageSize = getConfiguredMinLargeMessageSize(connection); + this.creditRunnable = createCreditRunnable(connection); useModified = this.connection.getProtocolManager().isUseModifiedForTransientDeliveryErrors(); this.routingContext = new RoutingContextImpl(null).setDuplicateDetection(connection.getProtocolManager().isAmqpDuplicateDetection()); } @@ -94,7 +80,6 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme sessionSPI.recoverContext(); } - protected void clearLargeMessage() { connection.runNow(() -> { if (currentLargeMessage != null) { @@ -109,10 +94,47 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme }); } - + /** + * Subclass can override this to provide a custom credit runnable that performs + * other checks or applies credit in a manner more fitting that implementation. + * + * @param connection + * The {@link AMQPConnectionContext} that this resource falls under. + * + * @return a {@link Runnable} that will perform the actual credit granting operation. + */ + protected Runnable createCreditRunnable(AMQPConnectionContext connection) { + return createCreditRunnable(connection.getAmqpCredits(), connection.getAmqpLowCredits(), receiver, connection, this); + } /** - * This Credit Runnable may be used in Mock tests to simulate the credit semantic here + * Subclass can override this to provide the minimum large message size that should + * be used when creating receiver instances. + * + * @param connection + * The {@link AMQPConnectionContext} that this resource falls under. + * + * @return the minimum large message size configuration value for this receiver. + */ + protected int getConfiguredMinLargeMessageSize(AMQPConnectionContext connection) { + return connection.getProtocolManager().getAmqpMinLargeMessageSize(); + } + + /** + * This Credit Runnable can be used to manage the credit replenishment of a target AMQP receiver. + * + * @param refill + * The number of credit to top off the receiver to + * @param threshold + * The low water mark for credit before refill is done + * @param receiver + * The proton receiver that will have its credit refilled + * @param connection + * The connection that own the receiver + * @param context + * The context that will be associated with the receiver + * + * @return A new Runnable that can be used to keep receiver credit replenished. */ public static Runnable createCreditRunnable(int refill, int threshold, @@ -122,9 +144,22 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme return new FlowControlRunner(refill, threshold, receiver, connection, context); } - /** - * This Credit Runnable may be used in Mock tests to simulate the credit semantic here + * This Credit Runnable can be used to manage the credit replenishment of a target AMQP receiver. + *

+ * This method is generally used for tests as it does not account for the receiver context that is + * assigned to the given receiver instance which does not allow for tracking pending settles. + * + * @param refill + * The number of credit to top off the receiver to + * @param threshold + * The low water mark for credit before refill is done + * @param receiver + * The proton receiver that will have its credit refilled + * @param connection + * The connection that own the receiver + * + * @return A new Runnable that can be used to keep receiver credit replenished. */ public static Runnable createCreditRunnable(int refill, int threshold, @@ -132,6 +167,7 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme AMQPConnectionContext connection) { return new FlowControlRunner(refill, threshold, receiver, connection, null); } + /** * The reason why we use the AtomicRunnable here * is because PagingManager will call Runnable in case it was blocked. @@ -139,8 +175,17 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme * and this serves as a control to avoid duplicated calls * */ static class FlowControlRunner implements Runnable { + + /* + * The number of credits sent to the remote when the runnable decides that a top off is needed. + */ final int refill; + + /* + * The low water mark before the runnable considers performing a credit top off. + */ final int threshold; + final Receiver receiver; final AMQPConnectionContext connection; final ProtonAbstractReceiver context; @@ -171,7 +216,6 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme connection.instantFlush(); } } - } } @@ -296,6 +340,10 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme close(false); } + public AMQPConnectionContext getConnection() { + return connection; + } + protected abstract void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx); // TODO: how to implement flow here? diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonInitializable.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonInitializable.java index 5c0a9a9663..7fc47ecdd5 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonInitializable.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonInitializable.java @@ -16,17 +16,15 @@ */ package org.apache.activemq.artemis.protocol.amqp.proton; -public class ProtonInitializable { +// TODO: This API is barely used and seems more trouble than its worth, consider removing +public abstract class ProtonInitializable { - private boolean initialized = false; + protected boolean initialized = false; public boolean isInitialized() { return initialized; } - public void initialize() throws Exception { - if (!initialized) { - initialized = true; - } - } + public abstract void initialize() throws Exception; + } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerReceiverContext.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerReceiverContext.java index 935450cc18..06565fdab7 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerReceiverContext.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerReceiverContext.java @@ -65,28 +65,27 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver { protected AddressFullMessagePolicy lastAddressPolicy; protected boolean addressAlreadyClashed = false; - protected final Runnable spiFlow = this::sessionSPIFlow; - private RoutingType defRoutingType; + protected RoutingType defRoutingType; public ProtonServerReceiverContext(AMQPSessionCallback sessionSPI, AMQPConnectionContext connection, AMQPSessionContext protonSession, Receiver receiver) { super(sessionSPI, connection, protonSession, receiver); - } @Override public void initialize() throws Exception { - super.initialize(); + initialized = true; + org.apache.qpid.proton.amqp.messaging.Target target = (org.apache.qpid.proton.amqp.messaging.Target) receiver.getRemoteTarget(); // Match the settlement mode of the remote instead of relying on the default of MIXED. receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode()); - // We don't currently support SECOND so enforce that the answer is anlways FIRST + // We don't currently support SECOND so enforce that the answer is always FIRST receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST); if (target != null) { @@ -156,7 +155,7 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver { return target != null ? getRoutingType(target.getCapabilities(), address) : getRoutingType((Symbol[]) null, address); } - private RoutingType getRoutingType(Symbol[] symbols, SimpleString address) { + protected RoutingType getRoutingType(Symbol[] symbols, SimpleString address) { RoutingType explicitRoutingType = getExplicitRoutingType(symbols); if (explicitRoutingType != null) { return explicitRoutingType; @@ -207,7 +206,6 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver { logger.warn(e.getMessage(), e); deliveryFailed(delivery, receiver, e); - } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerSenderContext.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerSenderContext.java index 8641fbc843..fe92213930 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerSenderContext.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/ProtonServerSenderContext.java @@ -107,9 +107,9 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - private static final Symbol COPY = Symbol.valueOf("copy"); - private static final Symbol TOPIC = Symbol.valueOf("topic"); - private static final Symbol QUEUE = Symbol.valueOf("queue"); + private static final Symbol COPY = AmqpSupport.COPY; + private static final Symbol TOPIC = AmqpSupport.TOPIC_CAPABILITY; + private static final Symbol QUEUE = AmqpSupport.QUEUE_CAPABILITY; private static final Symbol SHARED = Symbol.valueOf("shared"); private static final Symbol GLOBAL = Symbol.valueOf("global"); @@ -135,7 +135,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr volatile LargeMessageDeliveryContext pendingLargeMessage = null; volatile Runnable afterLargeMessage; - private int credits = 0; private AtomicInteger pending = new AtomicInteger(0); @@ -272,13 +271,12 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr } } - /** * create the actual underlying ActiveMQ Artemis Server Consumer */ @Override public void initialize() throws Exception { - super.initialize(); + initialized = true; if (controller == null) { controller = new DefaultController(sessionSPI); @@ -286,6 +284,7 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr try { brokerConsumer = controller.init(this); + preSettle = sender.getSenderSettleMode() == SenderSettleMode.SETTLED; onflowControlReady = brokerConsumer::promptDelivery; } catch (ActiveMQAMQPResourceLimitExceededException e1) { throw e1; @@ -357,7 +356,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr // any durable resources for say pub subs if (remoteLinkClose) { controller.close(); - } } catch (Exception e) { logger.warn(e.getMessage(), e); @@ -766,7 +764,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr position = proposedPosition; return (int)position; } finally { - TLSEncode.getEncoder().setByteBuffer((WritableBuffer)null); } } @@ -848,7 +845,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr message.usageUp(); pendingLargeMessage = new LargeMessageDeliveryContext(messageReference, message, delivery); pendingLargeMessage.deliver(); - } private void deliverStandard(MessageReference messageReference, AMQPMessage message) { @@ -966,7 +962,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr class DefaultController implements SenderController { - private boolean shared = false; boolean global = false; boolean multicast; @@ -981,7 +976,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr DefaultController(AMQPSessionCallback sessionSPI) { this.sessionSPI = sessionSPI; - } @Override @@ -992,7 +986,7 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr // Match the settlement mode of the remote instead of relying on the default of MIXED. sender.setSenderSettleMode(sender.getRemoteSenderSettleMode()); - // We don't currently support SECOND so enforce that the answer is anlways FIRST + // We don't currently support SECOND so enforce that the answer is always FIRST sender.setReceiverSettleMode(ReceiverSettleMode.FIRST); if (source != null) { @@ -1259,7 +1253,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr queue = addressToUse; } } - } if (queue == null) { @@ -1277,9 +1270,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr } } - // Detect if sender is in pre-settle mode. - preSettle = sender.getRemoteSenderSettleMode() == SenderSettleMode.SETTLED; - // We need to update the source with any filters we support otherwise the client // is free to consider the attach as having failed if we don't send back what we // do support or if we send something we don't support the client won't know we @@ -1291,7 +1281,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr return (Consumer) sessionSPI.createSender(senderContext, queue, multicast ? null : selector, browseOnly); } - private SimpleString getMatchingQueue(SimpleString queueName, SimpleString address, RoutingType routingType, SimpleString filter, boolean matchFilter) throws Exception { if (queueName != null) { QueueQueryResult result = sessionSPI.queueQuery(CompositeAddress.toFullyQualified(address, queueName), routingType, true, filter); @@ -1311,7 +1300,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr return null; } - @Override public void close() throws Exception { Source source = (Source) sender.getSource(); @@ -1345,6 +1333,5 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr } } } - } } diff --git a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/handler/ProtonHandler.java b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/handler/ProtonHandler.java index 7afc1b355c..5bd3537ef5 100644 --- a/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/handler/ProtonHandler.java +++ b/artemis-protocols/artemis-amqp-protocol/src/main/java/org/apache/activemq/artemis/protocol/amqp/proton/handler/ProtonHandler.java @@ -108,6 +108,11 @@ public class ProtonHandler extends ProtonInitializable implements SaslListener { private Runnable afterFlush; protected Set afterFlushSet; + @Override + public void initialize() throws Exception { + initialized = true; + } + public void afterFlush(Runnable runnable) { requireHandler(); if (afterFlush == null) { diff --git a/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationPolicySupportTest.java b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationPolicySupportTest.java new file mode 100644 index 0000000000..4966fce7dd --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/connect/federation/AMQPFederationPolicySupportTest.java @@ -0,0 +1,712 @@ +/* + * 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.artemis.protocol.amqp.connect.federation; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_DELAY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_MSG_COUNT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_ENABLE_DIVERT_BINDINGS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_MAX_HOPS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_ADDRESS_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_QUEUE_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.OPERATION_TYPE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDE_FEDERATED; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_PRIORITY_ADJUSTMENT; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement.AddressMatch; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement.QueueMatch; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPStandardMessage; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.util.NettyWritable; +import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode; +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.amqp.messaging.AmqpValue; +import org.apache.qpid.proton.amqp.messaging.MessageAnnotations; +import org.apache.qpid.proton.amqp.messaging.Properties; +import org.apache.qpid.proton.amqp.messaging.Section; +import org.apache.qpid.proton.codec.EncoderImpl; +import org.apache.qpid.proton.codec.WritableBuffer; +import org.jgroups.util.UUID; +import org.junit.Test; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; + +/** + * Tests for basic error checking and expected outcomes of the federation + * policy support class. + */ +public class AMQPFederationPolicySupportTest { + + private static final WildcardConfiguration DEFAULT_WILDCARD_CONFIGURATION = new WildcardConfiguration(); + + @Test + public void testEncodeReceiveFromQueuePolicy() { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>("a", "b")); + includes.add(new SimpleEntry<>("c", "d")); + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>("e", "f")); + excludes.add(new SimpleEntry<>("g", "h")); + final Map properties1 = new HashMap<>(); + properties1.put("amqpCredits", "10"); + properties1.put("amqpLowCredits", "3"); + final Map properties2 = new HashMap<>(); + properties2.put("amqpCredits", 10); + properties2.put("amqpLowCredits", 3); + + doTestEncodeReceiveFromQueuePolicy("test", false, 0, includes, excludes, properties1); + doTestEncodeReceiveFromQueuePolicy("test", true, 5, includes, excludes, properties2); + doTestEncodeReceiveFromQueuePolicy("test", false, -5, includes, excludes, null); + doTestEncodeReceiveFromQueuePolicy("test", true, 5, null, excludes, properties2); + doTestEncodeReceiveFromQueuePolicy("test", true, 5, includes, null, properties2); + doTestEncodeReceiveFromQueuePolicy("test", true, 5, Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()); + } + + @Test + public void testEncodeReceiveFromQueuePolicyNoExcludes() { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>("a", "b")); + includes.add(new SimpleEntry<>("c", "d")); + + doTestEncodeReceiveFromQueuePolicy("includes", false, 0, includes, null, null); + } + + @Test + public void testEncodeReceiveFromQueuePolicyNoIncludes() { + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>("e", "f")); + excludes.add(new SimpleEntry<>("g", "h")); + + doTestEncodeReceiveFromQueuePolicy("excludes", false, 0, null, excludes, null); + } + + @Test + public void testEncodeReceiveFromQueuePolicyNullsAndEmptyStrings() { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>(null, "b")); + includes.add(new SimpleEntry<>("", "d")); + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>("e", "")); + excludes.add(new SimpleEntry<>("g", null)); + + doTestEncodeReceiveFromQueuePolicy("excludes", false, 0, includes, excludes, null); + } + + @SuppressWarnings("unchecked") + private void doTestEncodeReceiveFromQueuePolicy(String name, + boolean includeFederated, int priorityAdjustment, + Collection> includes, + Collection> excludes, + Map policyProperties) { + final FederationReceiveFromQueuePolicy policy = new FederationReceiveFromQueuePolicy( + name, includeFederated, priorityAdjustment, includes, excludes, policyProperties, null, DEFAULT_WILDCARD_CONFIGURATION); + + final AMQPMessage message = AMQPFederationPolicySupport.encodeQueuePolicyControlMessage(policy); + + assertEquals(ADD_QUEUE_POLICY, message.getAnnotation(SimpleString.toSimpleString(OPERATION_TYPE.toString()))); + + assertNotNull(message.getBody()); + assertTrue(message.getBody() instanceof AmqpValue); + assertTrue(((AmqpValue) message.getBody()).getValue() instanceof Map); + + final Map policyMap = (Map) ((AmqpValue) message.getBody()).getValue(); + + assertEquals(name, policyMap.get(POLICY_NAME)); + assertEquals(includeFederated, policyMap.get(QUEUE_INCLUDE_FEDERATED)); + assertEquals(priorityAdjustment, policyMap.get(QUEUE_PRIORITY_ADJUSTMENT)); + + if (includes == null || includes.isEmpty()) { + assertFalse(policyMap.containsKey(QUEUE_INCLUDES)); + } else { + assertTrue(policyMap.containsKey(QUEUE_INCLUDES)); + assertTrue(policyMap.get(QUEUE_INCLUDES) instanceof List); + + final List flattenedIncludes = (List) policyMap.get(QUEUE_INCLUDES); + + assertEquals(includes.size() * 2, flattenedIncludes.size()); + + for (int i = 0; i < flattenedIncludes.size(); ) { + assertTrue(includes.contains(new SimpleEntry<>(flattenedIncludes.get(i++), flattenedIncludes.get(i++)))); + } + } + + if (excludes == null || excludes.isEmpty()) { + assertFalse(policyMap.containsKey(QUEUE_EXCLUDES)); + } else { + assertTrue(policyMap.containsKey(QUEUE_EXCLUDES)); + assertTrue(policyMap.get(QUEUE_EXCLUDES) instanceof List); + + final List flattenedExcludes = (List) policyMap.get(QUEUE_EXCLUDES); + + assertEquals(excludes.size() * 2, flattenedExcludes.size()); + + for (int i = 0; i < flattenedExcludes.size(); ) { + assertTrue(excludes.contains(new SimpleEntry<>(flattenedExcludes.get(i++), flattenedExcludes.get(i++)))); + } + } + + if (policyProperties == null || policyProperties.isEmpty()) { + assertFalse(policyMap.containsKey(POLICY_PROPERTIES_MAP)); + } else { + assertTrue(policyMap.containsKey(POLICY_PROPERTIES_MAP)); + assertTrue(policyMap.get(POLICY_PROPERTIES_MAP) instanceof Map); + + final Map encodedProperties = (Map) policyMap.get(POLICY_PROPERTIES_MAP); + + assertEquals(policyProperties.size(), encodedProperties.size()); + + policyProperties.forEach((k, v) -> { + assertTrue(encodedProperties.containsKey(k)); + assertEquals(v, encodedProperties.get(k)); + }); + } + } + + @Test + public void testEncodeReceiveFromAddressPolicy() { + final Set includes = new LinkedHashSet<>(); + includes.add("a"); + includes.add("b"); + includes.add("c"); + includes.add("d"); + final Set excludes = new LinkedHashSet<>(); + excludes.add("e"); + includes.add("f"); + includes.add("g"); + includes.add("h"); + includes.add("I"); + final Map properties1 = new HashMap<>(); + properties1.put("amqpCredits", "10"); + properties1.put("amqpLowCredits", "3"); + final Map properties2 = new HashMap<>(); + properties2.put("amqpCredits", 10); + properties2.put("amqpLowCredits", 3); + + doTestEncodeReceiveFromAddressPolicy("test", false, 0, 1, 2, true, includes, excludes, properties1); + doTestEncodeReceiveFromAddressPolicy("test", true, 1, 3, 2, false, includes, excludes, null); + doTestEncodeReceiveFromAddressPolicy("test", false, 2, 4, -1, false, includes, excludes, properties2); + doTestEncodeReceiveFromAddressPolicy("test", true, 7, -1, 255, true, includes, excludes, null); + doTestEncodeReceiveFromAddressPolicy("test", false, 2, 4, -1, false, null, excludes, properties2); + doTestEncodeReceiveFromAddressPolicy("test", true, 7, -1, 255, true, includes, null, null); + doTestEncodeReceiveFromAddressPolicy("test", true, 7, -1, 255, true, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap()); + } + + @SuppressWarnings("unchecked") + private void doTestEncodeReceiveFromAddressPolicy(String name, + boolean autoDelete, + long autoDeleteDelay, + long autoDeleteMessageCount, + int maxHops, + boolean enableDivertBindings, + Collection includes, + Collection excludes, + Map policyProperties) { + final FederationReceiveFromAddressPolicy policy = new FederationReceiveFromAddressPolicy( + name, autoDelete, autoDeleteDelay, autoDeleteMessageCount, maxHops, + enableDivertBindings, includes, excludes, policyProperties, null, DEFAULT_WILDCARD_CONFIGURATION); + + final AMQPMessage message = AMQPFederationPolicySupport.encodeAddressPolicyControlMessage(policy); + + assertEquals(ADD_ADDRESS_POLICY, message.getAnnotation(SimpleString.toSimpleString(OPERATION_TYPE.toString()))); + + assertNotNull(message.getBody()); + assertTrue(message.getBody() instanceof AmqpValue); + assertTrue(((AmqpValue) message.getBody()).getValue() instanceof Map); + + final Map policyMap = (Map) ((AmqpValue) message.getBody()).getValue(); + + assertEquals(name, policyMap.get(POLICY_NAME)); + assertEquals(autoDelete, policyMap.get(ADDRESS_AUTO_DELETE)); + assertEquals(autoDeleteDelay, policyMap.get(ADDRESS_AUTO_DELETE_DELAY)); + assertEquals(autoDeleteMessageCount, policyMap.get(ADDRESS_AUTO_DELETE_MSG_COUNT)); + assertEquals(maxHops, policyMap.get(ADDRESS_MAX_HOPS)); + assertEquals(enableDivertBindings, policyMap.get(ADDRESS_ENABLE_DIVERT_BINDINGS)); + + if (includes == null || includes.isEmpty()) { + assertFalse(policyMap.containsKey(ADDRESS_INCLUDES)); + } else { + assertTrue(policyMap.containsKey(ADDRESS_INCLUDES)); + assertTrue(policyMap.get(ADDRESS_INCLUDES) instanceof List); + + final List encodedIncludes = (List) policyMap.get(ADDRESS_INCLUDES); + + assertEquals(includes.size(), encodedIncludes.size()); + + for (int i = 0; i < encodedIncludes.size(); ++i) { + assertTrue(includes.contains(encodedIncludes.get(i))); + } + } + + if (excludes == null || excludes.isEmpty()) { + assertFalse(policyMap.containsKey(ADDRESS_EXCLUDES)); + } else { + assertTrue(policyMap.containsKey(ADDRESS_EXCLUDES)); + assertTrue(policyMap.get(ADDRESS_EXCLUDES) instanceof List); + + final List encodedExcludes = (List) policyMap.get(ADDRESS_EXCLUDES); + + assertEquals(excludes.size(), encodedExcludes.size()); + + for (int i = 0; i < encodedExcludes.size(); ++i) { + assertTrue(excludes.contains(encodedExcludes.get(i))); + } + } + + if (policyProperties == null || policyProperties.isEmpty()) { + assertFalse(policyMap.containsKey(POLICY_PROPERTIES_MAP)); + } else { + assertTrue(policyMap.containsKey(POLICY_PROPERTIES_MAP)); + assertTrue(policyMap.get(POLICY_PROPERTIES_MAP) instanceof Map); + + final Map encodedProperties = (Map) policyMap.get(POLICY_PROPERTIES_MAP); + + assertEquals(policyProperties.size(), encodedProperties.size()); + + policyProperties.forEach((k, v) -> { + assertTrue(encodedProperties.containsKey(k)); + assertEquals(v, encodedProperties.get(k)); + }); + } + } + + @Test + public void testDecodeReceiveFromQueuePolicyWithSingleIncludeAndExclude() throws ActiveMQException { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>("a", "b")); + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>("c", "d")); + + doTestDecodeReceiveFromQueuePolicy("address", "test", false, 0, includes, excludes, null); + } + + @Test + public void testDecodeReceiveFromQueuePolicyWithNullMatches() throws ActiveMQException { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>("a", null)); + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>(null, "b")); + + doTestDecodeReceiveFromQueuePolicy("address", "test", false, 0, includes, excludes, null); + } + + @Test + public void testDecodeReceiveFromQueuePolicy() throws ActiveMQException { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>("a", "b")); + includes.add(new SimpleEntry<>("c", "d")); + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>("e", "f")); + excludes.add(new SimpleEntry<>("g", "h")); + final Map properties = new HashMap<>(); + properties.put("amqpCredits", "10"); + properties.put("amqpLowCredits", "3"); + + doTestDecodeReceiveFromQueuePolicy("address", "test", false, 0, includes, excludes, null); + doTestDecodeReceiveFromQueuePolicy("address", "test", true, -5, includes, excludes, properties); + doTestDecodeReceiveFromQueuePolicy("address", "test", false, 5, includes, excludes, null); + doTestDecodeReceiveFromQueuePolicy("address", "test", true, -5, includes, null, properties); + doTestDecodeReceiveFromQueuePolicy("address", "test", true, -5, null, excludes, properties); + doTestDecodeReceiveFromQueuePolicy("address", "test", true, -5, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap()); + } + + private void doTestDecodeReceiveFromQueuePolicy(String address, String name, + boolean includeFederated, + int priorityAdjustment, + Collection> includes, + Collection> excludes, + Map policyProperties) throws ActiveMQException { + final Properties properties = new Properties(); + final Map annotations = new LinkedHashMap<>(); + final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations); + final Map policyMap = new LinkedHashMap<>(); + final Section sectionBody = new AmqpValue(policyMap); + + properties.setTo("address"); + + annotations.put(OPERATION_TYPE, ADD_QUEUE_POLICY); + + policyMap.put(POLICY_NAME, name); + policyMap.put(QUEUE_INCLUDE_FEDERATED, includeFederated); + policyMap.put(QUEUE_PRIORITY_ADJUSTMENT, priorityAdjustment); + + if (includes != null && !includes.isEmpty()) { + final List flattenedIncludes = new ArrayList<>(includes.size() * 2); + includes.forEach((entry) -> { + flattenedIncludes.add(entry.getKey()); + flattenedIncludes.add(entry.getValue()); + }); + + policyMap.put(QUEUE_INCLUDES, flattenedIncludes); + } + + if (excludes != null && !excludes.isEmpty()) { + final List flatteneExcludes = new ArrayList<>(excludes.size() * 2); + excludes.forEach((entry) -> { + flatteneExcludes.add(entry.getKey()); + flatteneExcludes.add(entry.getValue()); + }); + + policyMap.put(QUEUE_EXCLUDES, flatteneExcludes); + } + + if (policyProperties != null && !policyProperties.isEmpty()) { + policyMap.put(POLICY_PROPERTIES_MAP, policyProperties); + } + + final AMQPMessage amqpMessage = encodeFromAMQPTypes(properties, messageAnnotations, sectionBody); + + final FederationReceiveFromQueuePolicy policy = + AMQPFederationPolicySupport.decodeReceiveFromQueuePolicy(amqpMessage, DEFAULT_WILDCARD_CONFIGURATION); + + checkPolicyMatchesExpectations(policy, name, includeFederated, priorityAdjustment, includes, excludes, policyProperties); + } + + @Test + public void testDecodeReceiveFromAddressPolicy() throws ActiveMQException { + final Set includes = new LinkedHashSet<>(); + includes.add("a"); + includes.add("b"); + includes.add("c"); + includes.add("d"); + final Set excludes = new LinkedHashSet<>(); + excludes.add("e"); + includes.add("f"); + includes.add("g"); + includes.add("h"); + includes.add("I"); + final Map properties = new HashMap<>(); + properties.put("amqpCredits", "10"); + properties.put("amqpLowCredits", "3"); + + doTestDecodeReceiveFromAddressPolicy("address", "test", false, 0, 1, 2, true, includes, excludes, null); + doTestDecodeReceiveFromAddressPolicy("address", "test", false, 0, 1, 2, true, includes, excludes, properties); + doTestDecodeReceiveFromAddressPolicy("address", "test", false, 0, 1, 2, true, null, excludes, null); + doTestDecodeReceiveFromAddressPolicy("address", "test", false, 0, 1, 2, true, includes, null, properties); + doTestDecodeReceiveFromAddressPolicy("address", "test", false, 0, 1, 2, true, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap()); + } + + private void doTestDecodeReceiveFromAddressPolicy(String address, String name, + boolean autoDelete, + long autoDeleteDelay, + long autoDeleteMessageCount, + int maxHops, + boolean enableDivertBindings, + Collection includes, + Collection excludes, + Map policyProperties) throws ActiveMQException { + + final Properties properties = new Properties(); + final Map annotations = new LinkedHashMap<>(); + final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations); + final Map policyMap = new LinkedHashMap<>(); + final Section sectionBody = new AmqpValue(policyMap); + + properties.setTo("address"); + + annotations.put(OPERATION_TYPE, ADD_ADDRESS_POLICY); + + policyMap.put(POLICY_NAME, name); + policyMap.put(ADDRESS_AUTO_DELETE, autoDelete); + policyMap.put(ADDRESS_AUTO_DELETE_DELAY, autoDeleteDelay); + policyMap.put(ADDRESS_AUTO_DELETE_MSG_COUNT, autoDeleteMessageCount); + policyMap.put(ADDRESS_MAX_HOPS, maxHops); + policyMap.put(ADDRESS_ENABLE_DIVERT_BINDINGS, enableDivertBindings); + + if (includes != null && !includes.isEmpty()) { + policyMap.put(ADDRESS_INCLUDES, new ArrayList<>(includes)); + } + if (excludes != null && !excludes.isEmpty()) { + policyMap.put(ADDRESS_EXCLUDES, new ArrayList<>(excludes)); + } + if (policyProperties != null && !policyProperties.isEmpty()) { + policyMap.put(POLICY_PROPERTIES_MAP, policyProperties); + } + + final AMQPMessage amqpMessage = encodeFromAMQPTypes(properties, messageAnnotations, sectionBody); + + final FederationReceiveFromAddressPolicy policy = + AMQPFederationPolicySupport.decodeReceiveFromAddressPolicy(amqpMessage, DEFAULT_WILDCARD_CONFIGURATION); + + checkPolicyMatchesExpectations(policy, name, autoDelete, autoDeleteDelay, autoDeleteMessageCount, + maxHops, enableDivertBindings, includes, excludes, policyProperties); + } + + @Test + public void testDecodeOfQueuePolicyWithOddNumberOfIncludes() throws ActiveMQException { + final Properties properties = new Properties(); + final Map annotations = new LinkedHashMap<>(); + final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations); + final Map policyMap = new LinkedHashMap<>(); + final Section sectionBody = new AmqpValue(policyMap); + + properties.setTo("address"); + + annotations.put(OPERATION_TYPE, ADD_ADDRESS_POLICY); + + policyMap.put(POLICY_NAME, "test"); + policyMap.put(QUEUE_INCLUDE_FEDERATED, false); + policyMap.put(QUEUE_PRIORITY_ADJUSTMENT, 0); + + final List includes = new ArrayList<>(); + includes.add("a"); + includes.add("b"); + includes.add("c"); + + policyMap.put(QUEUE_INCLUDE_FEDERATED, includes); + + final AMQPMessage amqpMessage = encodeFromAMQPTypes(properties, messageAnnotations, sectionBody); + + assertThrows(ActiveMQException.class, () -> + AMQPFederationPolicySupport.decodeReceiveFromQueuePolicy(amqpMessage, DEFAULT_WILDCARD_CONFIGURATION)); + } + + @Test + public void testCreateQueuePolicyFromConfigurationElement() throws ActiveMQException { + final Set> includes = new LinkedHashSet<>(); + includes.add(new SimpleEntry<>("a", "b")); + includes.add(new SimpleEntry<>("c", "d")); + final Set> excludes = new LinkedHashSet<>(); + excludes.add(new SimpleEntry<>("e", "f")); + excludes.add(new SimpleEntry<>("g", "h")); + final Map properties1 = new HashMap<>(); + properties1.put("amqpCredits", "10"); + properties1.put("amqpLowCredits", "3"); + final Map properties2 = new HashMap<>(); + properties2.put("amqpCredits", 10); + properties2.put("amqpLowCredits", 3); + + doTestCreateQueuePolicyFromConfigurationElement("test", false, 0, includes, excludes, properties1); + doTestCreateQueuePolicyFromConfigurationElement("test", true, 5, includes, excludes, properties2); + doTestCreateQueuePolicyFromConfigurationElement("test", false, -5, includes, excludes, null); + doTestCreateQueuePolicyFromConfigurationElement("test", true, 5, null, excludes, properties1); + doTestCreateQueuePolicyFromConfigurationElement("test", true, 5, includes, null, properties2); + doTestCreateQueuePolicyFromConfigurationElement("test", false, 5, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap()); + } + + private void doTestCreateQueuePolicyFromConfigurationElement(String name, + boolean includeFederated, + int priorityAdjustment, + Collection> includes, + Collection> excludes, + Map policyProperties) throws ActiveMQException { + final AMQPFederationQueuePolicyElement element = new AMQPFederationQueuePolicyElement(); + + element.setName(name); + element.setPriorityAdjustment(priorityAdjustment); + element.setIncludeFederated(includeFederated); + element.setProperties(policyProperties); + + if (includes != null) { + includes.forEach(inc -> element.addInclude(new QueueMatch().setAddressMatch(inc.getKey()) + .setQueueMatch(inc.getValue()) + .setName(UUID.randomUUID().toString()))); + } + + if (excludes != null) { + excludes.forEach(ex -> element.addExclude(new QueueMatch().setAddressMatch(ex.getKey()) + .setQueueMatch(ex.getValue()) + .setName(UUID.randomUUID().toString()))); + } + + final FederationReceiveFromQueuePolicy policy = AMQPFederationPolicySupport.create(element, DEFAULT_WILDCARD_CONFIGURATION); + + checkPolicyMatchesExpectations(policy, name, includeFederated, priorityAdjustment, includes, excludes, policyProperties); + } + + @Test + public void testCreateAddressPolicyFromConfigurationElement() throws ActiveMQException { + final Set includes = new LinkedHashSet<>(); + includes.add("a"); + includes.add("b"); + includes.add("c"); + includes.add("d"); + final Set excludes = new LinkedHashSet<>(); + excludes.add("e"); + includes.add("f"); + includes.add("g"); + includes.add("h"); + includes.add("I"); + final Map properties = new HashMap<>(); + properties.put("amqpCredits", "10"); + properties.put("amqpLowCredits", "3"); + + doTestCreateAddressPolicyFromConfigurationElement("test", false, 0, 1, 2, true, includes, excludes, null); + doTestCreateAddressPolicyFromConfigurationElement("test", true, 1, 2, 3, true, includes, excludes, properties); + doTestCreateAddressPolicyFromConfigurationElement("test", false, 10, 9, 8, false, null, excludes, properties); + doTestCreateAddressPolicyFromConfigurationElement("test", true, 1, 1, 1, false, includes, null, null); + doTestCreateAddressPolicyFromConfigurationElement("test", false, 7, 1, 1, true, null, null, properties); + doTestCreateAddressPolicyFromConfigurationElement("test", false, 7, 1, 1, true, Collections.emptySet(), Collections.emptySet(), Collections.emptyMap()); + } + + private void doTestCreateAddressPolicyFromConfigurationElement(String name, + boolean autoDelete, + long autoDeleteDelay, + long autoDeleteMessageCount, + int maxHops, + boolean enableDivertBindings, + Collection includes, + Collection excludes, + Map policyProperties) throws ActiveMQException { + + final AMQPFederationAddressPolicyElement element = new AMQPFederationAddressPolicyElement(); + + element.setName(name); + element.setAutoDelete(autoDelete); + element.setAutoDeleteDelay(autoDeleteDelay); + element.setAutoDeleteMessageCount(autoDeleteMessageCount); + element.setMaxHops(maxHops); + element.setEnableDivertBindings(enableDivertBindings); + element.setProperties(policyProperties); + + if (includes != null) { + includes.forEach(inc -> element.addInclude(new AddressMatch().setAddressMatch(inc).setName(UUID.randomUUID().toString()))); + } + + if (excludes != null) { + excludes.forEach(ex -> element.addExclude(new AddressMatch().setAddressMatch(ex).setName(UUID.randomUUID().toString()))); + } + + final FederationReceiveFromAddressPolicy policy = AMQPFederationPolicySupport.create(element, DEFAULT_WILDCARD_CONFIGURATION); + + checkPolicyMatchesExpectations(policy, name, autoDelete, autoDeleteDelay, autoDeleteMessageCount, maxHops, + enableDivertBindings, includes, excludes, policyProperties); + } + + private void checkPolicyMatchesExpectations(FederationReceiveFromAddressPolicy policy, + String name, boolean autoDelete, long autoDeleteDelay, + long autoDeleteMessageCount, int maxHops, boolean enableDivertBindings, + Collection includes, Collection excludes, + Map policyProperties) { + assertEquals(name, policy.getPolicyName()); + assertEquals(autoDelete, policy.isAutoDelete()); + assertEquals(autoDeleteDelay, policy.getAutoDeleteDelay()); + assertEquals(autoDeleteMessageCount, policy.getAutoDeleteMessageCount()); + assertEquals(maxHops, policy.getMaxHops()); + assertEquals(enableDivertBindings, policy.isEnableDivertBindings()); + + if (includes == null || includes.isEmpty()) { + assertTrue(policy.getIncludes().isEmpty()); + } else { + assertEquals(includes.size(), policy.getIncludes().size()); + includes.forEach((include) -> assertTrue(policy.getIncludes().contains(include))); + } + + if (excludes == null || excludes.isEmpty()) { + assertTrue(policy.getExcludes().isEmpty()); + } else { + assertEquals(excludes.size(), policy.getExcludes().size()); + excludes.forEach((exclude) -> assertTrue(policy.getExcludes().contains(exclude))); + } + + if (policyProperties == null || policyProperties.isEmpty()) { + assertTrue(policy.getProperties().isEmpty()); + } else { + policyProperties.forEach((k, v) -> { + assertTrue(policy.getProperties().containsKey(k)); + assertEquals(v, policy.getProperties().get(k)); + }); + } + } + + private void checkPolicyMatchesExpectations(FederationReceiveFromQueuePolicy policy, + String name, boolean includeFederated, int priorityAdjustment, + Collection includes, Collection excludes, + Map policyProperties) { + + assertEquals(name, policy.getPolicyName()); + assertEquals(includeFederated, policy.isIncludeFederated()); + assertEquals(priorityAdjustment, policy.getPriorityAjustment()); + + if (includes == null || includes.isEmpty()) { + assertTrue(policy.getIncludes().isEmpty()); + } else { + assertEquals(includes.size(), policy.getIncludes().size()); + includes.forEach((include) -> assertTrue(policy.getIncludes().contains(include))); + } + + if (excludes == null || excludes.isEmpty()) { + assertTrue(policy.getExcludes().isEmpty()); + } else { + assertEquals(excludes.size(), policy.getExcludes().size()); + excludes.forEach((exclude) -> assertTrue(policy.getExcludes().contains(exclude))); + } + + if (policyProperties == null || policyProperties.isEmpty()) { + assertTrue(policy.getProperties().isEmpty()); + } else { + policyProperties.forEach((k, v) -> { + assertTrue(policy.getProperties().containsKey(k)); + assertEquals(v, policy.getProperties().get(k)); + }); + } + } + + private AMQPMessage encodeFromAMQPTypes(Properties properties, MessageAnnotations messageAnnotations, Section sectionBody) { + final ByteBuf buffer = PooledByteBufAllocator.DEFAULT.heapBuffer(1024); + + try { + final EncoderImpl encoder = TLSEncode.getEncoder(); + encoder.setByteBuffer(new NettyWritable(buffer)); + encoder.writeObject(messageAnnotations); + encoder.writeObject(properties); + encoder.writeObject(sectionBody); + + final byte[] data = new byte[buffer.writerIndex()]; + buffer.readBytes(data); + + final AMQPMessage amqpMessage = new AMQPStandardMessage(0, data, null); + amqpMessage.getProperties().setTo("test"); + amqpMessage.setAddress("test"); + + return amqpMessage; + } finally { + TLSEncode.getEncoder().setByteBuffer((WritableBuffer) null); + buffer.release(); + } + } +} diff --git a/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupportTest.java b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupportTest.java new file mode 100644 index 0000000000..80c0435cac --- /dev/null +++ b/artemis-protocols/artemis-amqp-protocol/src/test/java/org/apache/activemq/artemis/protocol/amqp/proton/AmqpSupportTest.java @@ -0,0 +1,111 @@ +/* + * 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.artemis.protocol.amqp.proton; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.apache.qpid.proton.amqp.Symbol; +import org.apache.qpid.proton.engine.Link; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * Test for utility APIs in the AMQP support class + */ +public class AmqpSupportTest { + + private static final Symbol A = Symbol.valueOf("A"); + private static final Symbol B = Symbol.valueOf("B"); + private static final Symbol C = Symbol.valueOf("C"); + private static final Symbol D = Symbol.valueOf("D"); + + private static final Symbol[] ALL = new Symbol[] {D, B, C, A}; + + @Test + public void testContains() { + assertFalse(AmqpSupport.contains(null, Symbol.valueOf("test"))); + assertFalse(AmqpSupport.contains(new Symbol[] {Symbol.valueOf("test")}, null)); + assertFalse(AmqpSupport.contains(new Symbol[] {Symbol.valueOf("a")}, Symbol.valueOf("test"))); + + assertTrue(AmqpSupport.contains(new Symbol[] {Symbol.valueOf("test")}, Symbol.valueOf("test"))); + assertTrue(AmqpSupport.contains(ALL, B)); + assertTrue(AmqpSupport.contains(ALL, D)); + assertTrue(AmqpSupport.contains(ALL, C)); + assertTrue(AmqpSupport.contains(ALL, A)); + } + + @Test + public void testVerifyOfferedCapabilitiesOfLink() { + final Link link = Mockito.mock(Link.class); + + Mockito.when(link.getDesiredCapabilities()).thenReturn(new Symbol[] {A}); + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(new Symbol[] {B, C}); + + assertFalse(AmqpSupport.verifyOfferedCapabilities(link)); + + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(null); + + assertFalse(AmqpSupport.verifyOfferedCapabilities(link)); + + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(new Symbol[] {B, C}); + Mockito.when(link.getDesiredCapabilities()).thenReturn(new Symbol[] {B, C}); + + assertTrue(AmqpSupport.verifyOfferedCapabilities(link)); + + Mockito.when(link.getDesiredCapabilities()).thenReturn(null); + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(null); + + assertTrue(AmqpSupport.verifyOfferedCapabilities(link)); + } + + @Test + public void testVerifyOfferedCapabilities() { + final Link link = Mockito.mock(Link.class); + + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(new Symbol[] {B, C}); + + assertFalse(AmqpSupport.verifyOfferedCapabilities(link, ALL)); + + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(ALL); + + assertTrue(AmqpSupport.verifyOfferedCapabilities(link, ALL)); + + Mockito.when(link.getRemoteOfferedCapabilities()).thenReturn(null); + + assertTrue(AmqpSupport.verifyOfferedCapabilities(link, (Symbol[]) null)); + } + + @Test + public void testVerifyDesiredCapability() { + final Link link = Mockito.mock(Link.class); + + Mockito.when(link.getRemoteDesiredCapabilities()).thenReturn(new Symbol[] {B, C}); + + assertFalse(AmqpSupport.verifyDesiredCapability(link, A)); + assertTrue(AmqpSupport.verifyDesiredCapability(link, C)); + assertTrue(AmqpSupport.verifyDesiredCapability(link, B)); + + assertThrows(NullPointerException.class, () -> AmqpSupport.verifyDesiredCapability(link, null)); + + Mockito.when(link.getRemoteDesiredCapabilities()).thenReturn((Symbol[]) null); + + assertFalse(AmqpSupport.verifyDesiredCapability(link, A)); + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java index 44cab79f97..cc045f2f0d 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/Configuration.java @@ -27,6 +27,7 @@ import java.util.concurrent.TimeUnit; import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; import org.apache.activemq.artemis.api.core.QueueConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationBrokerPlugin; import org.apache.activemq.artemis.core.config.routing.ConnectionRouterConfiguration; import org.apache.activemq.artemis.core.server.metrics.ActiveMQMetricsPlugin; import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerFederationPlugin; @@ -1402,6 +1403,11 @@ public interface Configuration { */ List getBrokerFederationPlugins(); + /** + * @return + */ + List getBrokerAMQPFederationPlugins(); + /** * @return */ diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectConfiguration.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectConfiguration.java index 0797d2dbeb..9f32d7af90 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectConfiguration.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectConfiguration.java @@ -19,7 +19,6 @@ package org.apache.activemq.artemis.core.config.amqpBrokerConnectivity; import java.net.URI; import java.util.ArrayList; import java.util.List; - import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.config.brokerConnectivity.BrokerConnectConfiguration; import org.apache.activemq.artemis.uri.ConnectorTransportConfigurationParser; @@ -44,7 +43,6 @@ public class AMQPBrokerConnectConfiguration extends BrokerConnectConfiguration { public AMQPBrokerConnectConfiguration addElement(AMQPBrokerConnectionElement amqpBrokerConnectionElement) { amqpBrokerConnectionElement.setParent(this); - if (amqpBrokerConnectionElement.getType() == AMQPBrokerConnectionAddressType.MIRROR && !(amqpBrokerConnectionElement instanceof AMQPMirrorBrokerConnectionElement)) { throw new IllegalArgumentException("must be an AMQPMirrorConnectionElement"); } @@ -62,6 +60,17 @@ public class AMQPBrokerConnectConfiguration extends BrokerConnectConfiguration { return connectionElements; } + public AMQPBrokerConnectConfiguration addFederation(AMQPFederatedBrokerConnectionElement amqpFederationElement) { + return addElement(amqpFederationElement); + } + + public List getFederations() { + // This returns all elements not just federation elements, broker properties relies on being able + // to modify the collection from the getter...it does not actually call the add method, it only + // uses the method to infer the type. + return connectionElements; + } + @Override public void parseURI() throws Exception { ConnectorTransportConfigurationParser parser = new ConnectorTransportConfigurationParser(false); @@ -117,5 +126,4 @@ public class AMQPBrokerConnectConfiguration extends BrokerConnectConfiguration { super.setAutostart(autostart); return this; } - } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectionAddressType.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectionAddressType.java index c1ac58ca2b..c9cdedd9f7 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectionAddressType.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPBrokerConnectionAddressType.java @@ -17,5 +17,5 @@ package org.apache.activemq.artemis.core.config.amqpBrokerConnectivity; public enum AMQPBrokerConnectionAddressType { - SENDER, RECEIVER, PEER, MIRROR + SENDER, RECEIVER, PEER, MIRROR, FEDERATION } diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederatedBrokerConnectionElement.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederatedBrokerConnectionElement.java new file mode 100644 index 0000000000..01c61c55b4 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederatedBrokerConnectionElement.java @@ -0,0 +1,166 @@ +/* + * 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.artemis.core.config.amqpBrokerConnectivity; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Configuration for broker federation that is managed over an AMQP broker connection. + */ +public class AMQPFederatedBrokerConnectionElement extends AMQPBrokerConnectionElement { + + private static final long serialVersionUID = -6701394020085679414L; + + private Set remoteAddressPolicies = new HashSet<>(); + private Set remoteQueuePolicies = new HashSet<>(); + + private Set localAddressPolicies = new HashSet<>(); + private Set localQueuePolicies = new HashSet<>(); + + private Map properties = new HashMap<>(); + + public AMQPFederatedBrokerConnectionElement() { + this.setType(AMQPBrokerConnectionAddressType.FEDERATION); + } + + public AMQPFederatedBrokerConnectionElement(String name) { + this.setType(AMQPBrokerConnectionAddressType.FEDERATION); + this.setName(name); + } + + @Override + public AMQPFederatedBrokerConnectionElement setType(AMQPBrokerConnectionAddressType type) { + if (!AMQPBrokerConnectionAddressType.FEDERATION.equals(type)) { + throw new IllegalArgumentException("Cannot change the type for this broker connection element"); + } else { + return (AMQPFederatedBrokerConnectionElement) super.setType(type); + } + } + + /** + * @return the configured remote address policy set. + */ + public Set getRemoteAddressPolicies() { + return remoteAddressPolicies; + } + + /** + * @param remoteAddressPolicy + * the policy to add to the set of remote address policies set + * + * @return this configuration element instance. + */ + public AMQPFederatedBrokerConnectionElement addRemoteAddressPolicy(AMQPFederationAddressPolicyElement remoteAddressPolicy) { + this.remoteAddressPolicies.add(remoteAddressPolicy); + return this; + } + + /** + * @return the configured remote queue policy set. + */ + public Set getRemoteQueuePolicies() { + return remoteQueuePolicies; + } + + /** + * @param remoteQueuePolicy + * the policy to add to the set of remote queue policies set + * + * @return this configuration element instance. + */ + public AMQPFederatedBrokerConnectionElement addRemoteQueuePolicy(AMQPFederationQueuePolicyElement remoteQueuePolicy) { + this.remoteQueuePolicies.add(remoteQueuePolicy); + return this; + } + + /** + * @return the configured local address policy set. + */ + public Set getLocalAddressPolicies() { + return localAddressPolicies; + } + + /** + * @param localAddressPolicy + * the policy to add to the set of local address policies set + * + * @return this configuration element instance. + */ + public AMQPFederatedBrokerConnectionElement addLocalAddressPolicy(AMQPFederationAddressPolicyElement localAddressPolicy) { + this.localAddressPolicies.add(localAddressPolicy); + return this; + } + + /** + * @return the configured local queue policy set. + */ + public Set getLocalQueuePolicies() { + return localQueuePolicies; + } + + /** + * @param localQueuePolicy + * the policy to add to the set of local queue policies set + * + * @return this configuration element instance. + */ + public AMQPFederatedBrokerConnectionElement addLocalQueuePolicy(AMQPFederationQueuePolicyElement localQueuePolicy) { + this.localQueuePolicies.add(localQueuePolicy); + return this; + } + + /** + * Adds the given property key and value to the federation configuration element. + * + * @param key + * The key that identifies the property + * @param value + * The value associated with the property key. + * + * @return this configuration element instance. + */ + public AMQPFederatedBrokerConnectionElement addProperty(String key, String value) { + properties.put(key, value); + return this; + } + + /** + * Adds the given property key and value to the federation configuration element. + * + * @param key + * The key that identifies the property + * @param value + * The value associated with the property key. + * + * @return this configuration element instance. + */ + public AMQPFederatedBrokerConnectionElement addProperty(String key, Number value) { + properties.put(key, value); + return this; + } + + /** + * @return the collection of configuration properties associated with this federation element. + */ + public Map getProperties() { + return properties; + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationAddressPolicyElement.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationAddressPolicyElement.java new file mode 100644 index 0000000000..55431d6796 --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationAddressPolicyElement.java @@ -0,0 +1,252 @@ +/* + * 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.artemis.core.config.amqpBrokerConnectivity; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.activemq.artemis.core.config.TransformerConfiguration; + +public final class AMQPFederationAddressPolicyElement implements Serializable { + + private static final long serialVersionUID = -5205164803216061323L; + + private final Set includes = new HashSet<>(); + private final Set excludes = new HashSet<>(); + private final Map properties = new HashMap<>(); + + private String name; + private Boolean autoDelete; + private Long autoDeleteDelay; + private Long autoDeleteMessageCount; + private int maxHops; + private Boolean enableDivertBindings; + private TransformerConfiguration transformerConfig; + + public String getName() { + return name; + } + + public AMQPFederationAddressPolicyElement setName(String name) { + this.name = name; + return this; + } + + public Set getIncludes() { + return includes; + } + + public AMQPFederationAddressPolicyElement addToIncludes(String include) { + includes.add(new AddressMatch().setAddressMatch(include)); + return this; + } + + public AMQPFederationAddressPolicyElement addInclude(AddressMatch include) { + includes.add(include); + return this; + } + + public AMQPFederationAddressPolicyElement setIncludes(Set includes) { + this.includes.clear(); + if (includes != null) { + this.includes.addAll(includes); + } + + return this; + } + + public Set getExcludes() { + return excludes; + } + + public AMQPFederationAddressPolicyElement addToExcludes(String exclude) { + excludes.add(new AddressMatch().setAddressMatch(exclude)); + return this; + } + + public AMQPFederationAddressPolicyElement addExclude(AddressMatch exclude) { + excludes.add(exclude); + return this; + } + + public AMQPFederationAddressPolicyElement setExcludes(Set excludes) { + this.excludes.clear(); + if (excludes != null) { + this.excludes.addAll(excludes); + } + + return this; + } + + public Map getProperties() { + return properties; + } + + public AMQPFederationAddressPolicyElement addProperty(String key, String value) { + properties.put(key, value); + return this; + } + + public AMQPFederationAddressPolicyElement addProperty(String key, Number value) { + properties.put(key, value); + return this; + } + + public AMQPFederationAddressPolicyElement setProperties(Map properties) { + this.properties.clear(); + if (properties != null) { + this.properties.putAll(properties); + } + + return this; + } + + public int getMaxHops() { + return maxHops; + } + + public AMQPFederationAddressPolicyElement setMaxHops(int maxHops) { + this.maxHops = maxHops; + return this; + } + + public Long getAutoDeleteMessageCount() { + return autoDeleteMessageCount; + } + + public AMQPFederationAddressPolicyElement setAutoDeleteMessageCount(Long autoDeleteMessageCount) { + this.autoDeleteMessageCount = autoDeleteMessageCount; + return this; + } + + public Long getAutoDeleteDelay() { + return autoDeleteDelay; + } + + public AMQPFederationAddressPolicyElement setAutoDeleteDelay(Long autoDeleteDelay) { + this.autoDeleteDelay = autoDeleteDelay; + return this; + } + + public Boolean getAutoDelete() { + return autoDelete; + } + + public AMQPFederationAddressPolicyElement setAutoDelete(Boolean autoDelete) { + this.autoDelete = autoDelete; + return this; + } + + public Boolean isEnableDivertBindings() { + return enableDivertBindings; + } + + public AMQPFederationAddressPolicyElement setEnableDivertBindings(Boolean enableDivertBindings) { + this.enableDivertBindings = enableDivertBindings; + return this; + } + + public AMQPFederationAddressPolicyElement setTransformerConfiguration(TransformerConfiguration transformerConfig) { + this.transformerConfig = transformerConfig; + return this; + } + + public TransformerConfiguration getTransformerConfiguration() { + return transformerConfig; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AMQPFederationAddressPolicyElement)) { + return false; + } + + final AMQPFederationAddressPolicyElement that = (AMQPFederationAddressPolicyElement) o; + + return maxHops == that.maxHops && + Objects.equals(name, that.name) && + Objects.equals(includes, that.includes) && + Objects.equals(excludes, that.excludes) && + Objects.equals(autoDelete, that.autoDelete) && + Objects.equals(autoDeleteDelay, that.autoDeleteDelay) && + Objects.equals(autoDeleteMessageCount, that.autoDeleteMessageCount); + } + + @Override + public int hashCode() { + return Objects.hash(name, includes, excludes, autoDelete, autoDeleteDelay, autoDeleteMessageCount, maxHops); + } + + // We are required to implement a named match type so that we can perform this configuration + // from the broker properties mechanism where there is no means of customizing the property + // set to parse address and queue names from some string encoded value. This could be simplified + // at some point if another configuration mechanism is created. The name value is not used + // internally in the AMQP federation implementation. + + public static class AddressMatch implements Serializable { + + private static final long serialVersionUID = 8517154638045698017L; + + private String name; + private String addressMatch; + + public String getName() { + return name; + } + + public AddressMatch setName(String name) { + this.name = name; + return this; + } + + public String getAddressMatch() { + return addressMatch; + } + + public AddressMatch setAddressMatch(String addressMatch) { + this.addressMatch = addressMatch; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof AddressMatch)) { + return false; + } + + final AddressMatch matcher = (AddressMatch) o; + + return Objects.equals(addressMatch, matcher.addressMatch); + } + + @Override + public int hashCode() { + return Objects.hash(addressMatch, addressMatch); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationBrokerPlugin.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationBrokerPlugin.java new file mode 100644 index 0000000000..711ca0ab9b --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationBrokerPlugin.java @@ -0,0 +1,29 @@ +/* + * 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.artemis.core.config.amqpBrokerConnectivity; + +import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerBasePlugin; + +/** + * Marker interface for AMQP Federation broker plugins which allows for the decoupling + * of the actual AMQP federation broker plugin API as the AMQP protocol module may not + * always be present on the classpath for a broker install. + */ +public interface AMQPFederationBrokerPlugin extends ActiveMQServerBasePlugin { + +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationQueuePolicyElement.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationQueuePolicyElement.java new file mode 100644 index 0000000000..ed03e86f5e --- /dev/null +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/amqpBrokerConnectivity/AMQPFederationQueuePolicyElement.java @@ -0,0 +1,231 @@ +/* + * 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.artemis.core.config.amqpBrokerConnectivity; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.activemq.artemis.core.config.TransformerConfiguration; + +public final class AMQPFederationQueuePolicyElement implements Serializable { + + private static final long serialVersionUID = 7519912064917015520L; + + private final Set includes = new HashSet<>(); + private final Set excludes = new HashSet<>(); + private final Map properties = new HashMap<>(); + + private String name; + private boolean includeFederated; + private Integer priorityAdjustment; + private TransformerConfiguration transformerConfig; + + public String getName() { + return name; + } + + public AMQPFederationQueuePolicyElement setName(String name) { + this.name = name; + return this; + } + + public Set getIncludes() { + return includes; + } + + public AMQPFederationQueuePolicyElement addToIncludes(String addressMatch, String queueMatch) { + includes.add(new QueueMatch().setAddressMatch(addressMatch).setQueueMatch(queueMatch)); + return this; + } + + public AMQPFederationQueuePolicyElement addInclude(QueueMatch match) { + includes.add(match); + return this; + } + + public AMQPFederationQueuePolicyElement setIncludes(Set includes) { + this.includes.clear(); + if (includes != null) { + this.includes.addAll(includes); + } + + return this; + } + + public Set getExcludes() { + return excludes; + } + + public AMQPFederationQueuePolicyElement addExclude(QueueMatch match) { + excludes.add(match); + return this; + } + + public AMQPFederationQueuePolicyElement addToExcludes(String addressMatch, String queueMatch) { + excludes.add(new QueueMatch().setAddressMatch(addressMatch).setQueueMatch(queueMatch)); + return this; + } + + public AMQPFederationQueuePolicyElement setExcludes(Set excludes) { + this.excludes.clear(); + if (excludes != null) { + this.excludes.addAll(excludes); + } + + return this; + } + + public AMQPFederationQueuePolicyElement addProperty(String key, String value) { + properties.put(key, value); + return this; + } + + public AMQPFederationQueuePolicyElement addProperty(String key, Number value) { + properties.put(key, value); + return this; + } + + public Map getProperties() { + return properties; + } + + public AMQPFederationQueuePolicyElement setProperties(Map properties) { + this.properties.clear(); + if (properties != null) { + this.properties.putAll(properties); + } + + return this; + } + + public boolean isIncludeFederated() { + return includeFederated; + } + + public AMQPFederationQueuePolicyElement setIncludeFederated(boolean includeFederated) { + this.includeFederated = includeFederated; + return this; + } + + public Integer getPriorityAdjustment() { + return priorityAdjustment; + } + + public AMQPFederationQueuePolicyElement setPriorityAdjustment(Integer priorityAdjustment) { + this.priorityAdjustment = priorityAdjustment; + return this; + } + + public AMQPFederationQueuePolicyElement setTransformerConfiguration(TransformerConfiguration transformerConfig) { + this.transformerConfig = transformerConfig; + return this; + } + + public TransformerConfiguration getTransformerConfiguration() { + return transformerConfig; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof AMQPFederationQueuePolicyElement)) { + return false; + } + + final AMQPFederationQueuePolicyElement that = (AMQPFederationQueuePolicyElement) o; + + return includeFederated == that.includeFederated && + Objects.equals(name, that.name) && + Objects.equals(includes, that.includes) && + Objects.equals(excludes, that.excludes) && + Objects.equals(priorityAdjustment, that.priorityAdjustment); + } + + @Override + public int hashCode() { + return Objects.hash(name, includeFederated, includes, excludes, priorityAdjustment); + } + + // We are required to implement a named match type so that we can perform this configuration + // from the broker properties mechanism where there is no means of customizing the property + // set to parse address and queue names from some string encoded value. This could be simplified + // at some point if another configuration mechanism is created. The name value is not used + // internally in the AMQP federation implementation. + + public static class QueueMatch implements Serializable { + + private static final long serialVersionUID = -1641189627591828008L; + + private String name; + private String addressMatch; + private String queueMatch; + + public String getName() { + return name; + } + + public QueueMatch setName(String name) { + this.name = name; + return this; + } + + public String getAddressMatch() { + return addressMatch; + } + + public QueueMatch setAddressMatch(String addressMatch) { + this.addressMatch = addressMatch; + return this; + } + + public String getQueueMatch() { + return queueMatch; + } + + public QueueMatch setQueueMatch(String queueMatch) { + this.queueMatch = queueMatch; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof QueueMatch)) { + return false; + } + + final QueueMatch matcher = (QueueMatch) o; + + return Objects.equals(queueMatch, matcher.queueMatch) && + Objects.equals(addressMatch, matcher.addressMatch); + } + + @Override + public int hashCode() { + return Objects.hash(queueMatch, addressMatch); + } + } +} diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java index dcf1e45da2..8e91149e02 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImpl.java @@ -63,6 +63,7 @@ import org.apache.activemq.artemis.api.core.TransportConfiguration; import org.apache.activemq.artemis.core.config.TransformerConfiguration; import org.apache.activemq.artemis.core.config.routing.ConnectionRouterConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationBrokerPlugin; import org.apache.activemq.artemis.core.config.BridgeConfiguration; import org.apache.activemq.artemis.core.config.ClusterConnectionConfiguration; import org.apache.activemq.artemis.core.config.Configuration; @@ -341,6 +342,7 @@ public class ConfigurationImpl implements Configuration, Serializable { private final List brokerBridgePlugins = new CopyOnWriteArrayList<>(); private final List brokerCriticalPlugins = new CopyOnWriteArrayList<>(); private final List brokerFederationPlugins = new CopyOnWriteArrayList<>(); + private final List brokerAMQPFederationPlugins = new CopyOnWriteArrayList<>(); private final List brokerResourcePlugins = new CopyOnWriteArrayList<>(); private Map> securityRoleNameMappings = new HashMap<>(); @@ -2155,6 +2157,9 @@ public class ConfigurationImpl implements Configuration, Serializable { if (plugin instanceof ActiveMQServerFederationPlugin) { brokerFederationPlugins.add((ActiveMQServerFederationPlugin) plugin); } + if (plugin instanceof AMQPFederationBrokerPlugin) { + brokerAMQPFederationPlugins.add((AMQPFederationBrokerPlugin) plugin); + } if (plugin instanceof ActiveMQServerResourcePlugin) { brokerResourcePlugins.add((ActiveMQServerResourcePlugin) plugin); } @@ -2193,6 +2198,9 @@ public class ConfigurationImpl implements Configuration, Serializable { if (plugin instanceof ActiveMQServerFederationPlugin) { brokerFederationPlugins.remove(plugin); } + if (plugin instanceof AMQPFederationBrokerPlugin) { + brokerAMQPFederationPlugins.remove(plugin); + } if (plugin instanceof ActiveMQServerResourcePlugin) { brokerResourcePlugins.remove(plugin); } @@ -2253,6 +2261,11 @@ public class ConfigurationImpl implements Configuration, Serializable { return brokerFederationPlugins; } + @Override + public List getBrokerAMQPFederationPlugins() { + return brokerAMQPFederationPlugins; + } + @Override public List getFederationConfigurations() { return federationConfigurations; @@ -3299,10 +3312,17 @@ public class ConfigurationImpl implements Configuration, Serializable { // find the add X and init an instance of the type with name=name StringBuilder addPropertyNameBuilder = new StringBuilder("add"); - // expect an add... without the plural for named accessors + // expect an add... without the plural for named access methods that add a single instance. if (collectionPropertyName != null && collectionPropertyName.length() > 0) { addPropertyNameBuilder.append(Character.toUpperCase(collectionPropertyName.charAt(0))); - addPropertyNameBuilder.append(collectionPropertyName, 1, collectionPropertyName.length() - 1); + if (collectionPropertyName.endsWith("ies")) { + // Plural form would convert to a singular ending in 'y' e.g. policies becomes policy + // or strategies becomes strategy etc. + addPropertyNameBuilder.append(collectionPropertyName, 1, collectionPropertyName.length() - 3); + addPropertyNameBuilder.append('y'); + } else { + addPropertyNameBuilder.append(collectionPropertyName, 1, collectionPropertyName.length() - 1); + } } // we don't know the type, infer from add method add(X x) or add(String key, X x) diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java index e9eb2874ce..bbbbb22955 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java @@ -60,6 +60,9 @@ import org.apache.activemq.artemis.core.config.ScaleDownConfiguration; import org.apache.activemq.artemis.core.config.TransformerConfiguration; import org.apache.activemq.artemis.core.config.WildcardConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; import org.apache.activemq.artemis.core.config.routing.PoolConfiguration; @@ -2152,7 +2155,6 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { mainConfig.addAMQPConnection(config); - NodeList senderList = e.getChildNodes(); for (int i = 0; i < senderList.getLength(); i++) { if (senderList.item(i).getNodeType() == Node.ELEMENT_NODE) { @@ -2172,6 +2174,28 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { amqpMirrorConnectionElement.setMessageAcknowledgements(messageAcks).setQueueCreation(queueCreation).setQueueRemoval(queueRemoval).setDurable(durable).setAddressFilter(addressFilter).setSync(sync); connectionElement = amqpMirrorConnectionElement; connectionElement.setType(AMQPBrokerConnectionAddressType.MIRROR); + } else if (nodeType == AMQPBrokerConnectionAddressType.FEDERATION) { + final AMQPFederatedBrokerConnectionElement amqpFederationConnectionElement = new AMQPFederatedBrokerConnectionElement(name); + final NodeList federationAttrs = e2.getChildNodes(); + + for (int j = 0; j < federationAttrs.getLength(); j++) { + final Node federationPolicy = federationAttrs.item(j); + + if (federationPolicy.getNodeName().equals("remote-queue-policy")) { + amqpFederationConnectionElement.addRemoteQueuePolicy(parseAMQPFederatedFromQueuePolicy((Element) federationPolicy, mainConfig)); + } else if (federationPolicy.getNodeName().equals("local-queue-policy")) { + amqpFederationConnectionElement.addLocalQueuePolicy(parseAMQPFederatedFromQueuePolicy((Element)federationPolicy, mainConfig)); + } else if (federationPolicy.getNodeName().equals("remote-address-policy")) { + amqpFederationConnectionElement.addRemoteAddressPolicy(parseAMQPFederatedFromAddressPolicy((Element)federationPolicy, mainConfig)); + } else if (federationPolicy.getNodeName().equals("local-address-policy")) { + amqpFederationConnectionElement.addLocalAddressPolicy(parseAMQPFederatedFromAddressPolicy((Element)federationPolicy, mainConfig)); + } else if (federationPolicy.getNodeName().equals("property")) { + amqpFederationConnectionElement.addProperty(getAttributeValue(federationPolicy, "key"), getAttributeValue(federationPolicy, "value")); + } + } + + connectionElement = amqpFederationConnectionElement; + connectionElement.setType(AMQPBrokerConnectionAddressType.FEDERATION); } else { String match = getAttributeValue(e2, "address-match"); String queue = getAttributeValue(e2, "queue-name"); @@ -2187,6 +2211,97 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { logger.debug("Adding AMQP connection :: {}", config); } + private AMQPFederationAddressPolicyElement parseAMQPFederatedFromAddressPolicy(Element policyNod, final Configuration mainConfig) throws Exception { + AMQPFederationAddressPolicyElement config = new AMQPFederationAddressPolicyElement(); + config.setName(policyNod.getAttribute("name")); + + NamedNodeMap attributes = policyNod.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node item = attributes.item(i); + if (item.getNodeName().equals("max-hops")) { + int maxConsumers = Integer.parseInt(item.getNodeValue()); + Validators.MINUS_ONE_OR_GE_ZERO.validate(item.getNodeName(), maxConsumers); + config.setMaxHops(maxConsumers); + } else if (item.getNodeName().equals("auto-delete")) { + boolean autoDelete = Boolean.parseBoolean(item.getNodeValue()); + config.setAutoDelete(autoDelete); + } else if (item.getNodeName().equals("auto-delete-delay")) { + long autoDeleteDelay = Long.parseLong(item.getNodeValue()); + Validators.GE_ZERO.validate("auto-delete-delay", autoDeleteDelay); + config.setAutoDeleteDelay(autoDeleteDelay); + } else if (item.getNodeName().equals("auto-delete-message-count")) { + long autoDeleteMessageCount = Long.parseLong(item.getNodeValue()); + Validators.MINUS_ONE_OR_GE_ZERO.validate("auto-delete-message-count", autoDeleteMessageCount); + config.setAutoDeleteMessageCount(autoDeleteMessageCount); + } else if (item.getNodeName().equals("enable-divert-bindings")) { + boolean enableDivertBindings = Boolean.parseBoolean(item.getNodeValue()); + config.setEnableDivertBindings(enableDivertBindings); + } + } + + final NodeList children = policyNod.getChildNodes(); + + final String transformerClassName = getString(policyNod, "transformer-class-name", null, Validators.NO_CHECK); + if (transformerClassName != null && !transformerClassName.isEmpty()) { + config.setTransformerConfiguration(getTransformerConfiguration(transformerClassName)); + } + + for (int j = 0; j < children.getLength(); j++) { + Node child = children.item(j); + + if (child.getNodeName().equals("include")) { + config.addToIncludes(((Element) child).getAttribute("address-match")); + } else if (child.getNodeName().equals("exclude")) { + config.addToExcludes(((Element) child).getAttribute("address-match")); + } else if (child.getNodeName().equals("transformer")) { + config.setTransformerConfiguration(getTransformerConfiguration(child)); + } else if (child.getNodeName().equals("property")) { + config.addProperty(getAttributeValue(child, "key"), getAttributeValue(child, "value")); + } + } + + return config; + } + + private AMQPFederationQueuePolicyElement parseAMQPFederatedFromQueuePolicy(Element policyNod, final Configuration mainConfig) throws Exception { + AMQPFederationQueuePolicyElement config = new AMQPFederationQueuePolicyElement(); + config.setName(policyNod.getAttribute("name")); + + NamedNodeMap attributes = policyNod.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node item = attributes.item(i); + if (item.getNodeName().equals("include-federated")) { + config.setIncludeFederated(Boolean.parseBoolean(item.getNodeValue())); + } else if (item.getNodeName().equals("priority-adjustment")) { + int priorityAdjustment = Integer.parseInt(item.getNodeValue()); + config.setPriorityAdjustment(priorityAdjustment); + } + } + + final NodeList children = policyNod.getChildNodes(); + + final String transformerClassName = getString(policyNod, "transformer-class-name", null, Validators.NO_CHECK); + if (transformerClassName != null && !transformerClassName.isEmpty()) { + config.setTransformerConfiguration(getTransformerConfiguration(transformerClassName)); + } + + for (int j = 0; j < children.getLength(); j++) { + Node child = children.item(j); + + if (child.getNodeName().equals("include")) { + config.addToIncludes(((Element) child).getAttribute("address-match"), ((Element) child).getAttribute("queue-match")); + } else if (child.getNodeName().equals("exclude")) { + config.addToExcludes(((Element) child).getAttribute("address-match"), ((Element) child).getAttribute("queue-match")); + } else if (child.getNodeName().equals("transformer")) { + config.setTransformerConfiguration(getTransformerConfiguration(child)); + } else if (child.getNodeName().equals("property")) { + config.addProperty(getAttributeValue(child, "key"), getAttributeValue(child, "value")); + } + } + + return config; + } + private void parseClusterConnectionConfiguration(final Element e, final Configuration mainConfig) throws Exception { String name = e.getAttribute("name"); @@ -2676,6 +2791,10 @@ public final class FileConfigurationParser extends XMLConfigurationUtil { return downstreamConfiguration; } + private FederationUpstreamConfiguration getBrokerConnections(final Element upstreamNode, final Configuration mainConfig) throws Exception { + return getFederationStream(new FederationUpstreamConfiguration(), upstreamNode, mainConfig); + } + private void getStaticConnectors(List staticConnectorNames, Node child) { NodeList children2 = ((Element) child).getElementsByTagName("connector-ref"); diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java index 97031c2df6..f5cb578a4b 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/ActiveMQServer.java @@ -35,6 +35,7 @@ import org.apache.activemq.artemis.core.config.BridgeConfiguration; import org.apache.activemq.artemis.core.config.Configuration; import org.apache.activemq.artemis.core.config.DivertConfiguration; import org.apache.activemq.artemis.core.config.FederationConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationBrokerPlugin; import org.apache.activemq.artemis.core.io.IOCriticalErrorListener; import org.apache.activemq.artemis.core.management.impl.ActiveMQServerControlImpl; import org.apache.activemq.artemis.core.paging.PagingManager; @@ -280,6 +281,8 @@ public interface ActiveMQServer extends ServiceComponent { List getBrokerFederationPlugins(); + List getBrokerAMQPFederationPlugins(); + List getBrokerResourcePlugins(); void callBrokerPlugins(ActiveMQPluginRunnable pluginRun) throws ActiveMQException; @@ -307,6 +310,8 @@ public interface ActiveMQServer extends ServiceComponent { void callBrokerFederationPlugins(ActiveMQPluginRunnable pluginRun) throws ActiveMQException; + void callBrokerAMQPFederationPlugins(ActiveMQPluginRunnable pluginRun) throws ActiveMQException; + void callBrokerResourcePlugins(ActiveMQPluginRunnable pluginRun) throws ActiveMQException; boolean hasBrokerPlugins(); @@ -331,6 +336,8 @@ public interface ActiveMQServer extends ServiceComponent { boolean hasBrokerFederationPlugins(); + boolean hasBrokerAMQPFederationPlugins(); + boolean hasBrokerResourcePlugins(); void checkQueueCreationLimit(String username) throws Exception; diff --git a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java index 3c3db20b98..e1b8ad6b86 100644 --- a/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java +++ b/artemis-server/src/main/java/org/apache/activemq/artemis/core/server/impl/ActiveMQServerImpl.java @@ -73,6 +73,7 @@ import org.apache.activemq.artemis.core.config.DivertConfiguration; import org.apache.activemq.artemis.core.config.FederationConfiguration; import org.apache.activemq.artemis.core.config.HAPolicyConfiguration; import org.apache.activemq.artemis.core.config.StoreConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationBrokerPlugin; import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl; import org.apache.activemq.artemis.core.config.impl.LegacyJMSConfiguration; import org.apache.activemq.artemis.core.config.storage.DatabaseStorageConfiguration; @@ -2650,6 +2651,11 @@ public class ActiveMQServerImpl implements ActiveMQServer { return configuration.getBrokerFederationPlugins(); } + @Override + public List getBrokerAMQPFederationPlugins() { + return configuration.getBrokerAMQPFederationPlugins(); + } + @Override public List getBrokerResourcePlugins() { return configuration.getBrokerResourcePlugins(); @@ -2731,6 +2737,11 @@ public class ActiveMQServerImpl implements ActiveMQServer { callBrokerPlugins(getBrokerFederationPlugins(), pluginRun); } + @Override + public void callBrokerAMQPFederationPlugins(final ActiveMQPluginRunnable pluginRun) throws ActiveMQException { + callBrokerPlugins(getBrokerAMQPFederationPlugins(), pluginRun); + } + @Override public void callBrokerResourcePlugins(final ActiveMQPluginRunnable pluginRun) throws ActiveMQException { callBrokerPlugins(getBrokerResourcePlugins(), pluginRun); @@ -2808,6 +2819,11 @@ public class ActiveMQServerImpl implements ActiveMQServer { return !getBrokerFederationPlugins().isEmpty(); } + @Override + public boolean hasBrokerAMQPFederationPlugins() { + return !getBrokerAMQPFederationPlugins().isEmpty(); + } + @Override public boolean hasBrokerResourcePlugins() { return !getBrokerResourcePlugins().isEmpty(); diff --git a/artemis-server/src/main/resources/schema/artemis-configuration.xsd b/artemis-server/src/main/resources/schema/artemis-configuration.xsd index 37144f8ba4..62ffa946a7 100644 --- a/artemis-server/src/main/resources/schema/artemis-configuration.xsd +++ b/artemis-server/src/main/resources/schema/artemis-configuration.xsd @@ -2047,6 +2047,7 @@ + @@ -2175,6 +2176,102 @@ + + + + This create a federation between this broker and the remote that is being + connected to, federation can occur in either direction over this connection + depending on the address and queue policy configurations. + + + + + + + + + + + + + Optional properties that can be applied to the federation configuration + + + + + + + + + + + + + + Optional properties that can be applied to the Queue federation policy + + + + + + + + an optional class name of a transformer + + + + + + + optional transformer configuration + + + + + + + + + + + + + + + + + + + Optional properties that can be applied to the Address federation policy + + + + + + + + an optional class name of a transformer + + + + + + + optional transformer configuration + + + + + + + + + + + + + + diff --git a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java index c7eb9d676d..e48e36e0dd 100644 --- a/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java +++ b/artemis-server/src/test/java/org/apache/activemq/artemis/core/config/impl/ConfigurationImplTest.java @@ -45,6 +45,9 @@ import org.apache.activemq.artemis.core.config.ConfigurationUtils; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; import org.apache.activemq.artemis.core.config.federation.FederationAddressPolicyConfiguration; import org.apache.activemq.artemis.core.config.federation.FederationPolicySet; @@ -77,6 +80,7 @@ import org.apache.commons.lang3.ClassUtils; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import java.lang.invoke.MethodHandles; import org.junit.Assert; import org.junit.Before; @@ -768,6 +772,256 @@ public class ConfigurationImplTest extends ActiveMQTestBase { Assert.assertEquals("foo", amqpMirrorBrokerConnectionElement.getAddressFilter()); } + @Test + public void testAMQPFederationLocalAddressPolicyConfiguration() throws Throwable { + doTestAMQPFederationAddressPolicyConfiguration(true); + } + + @Test + public void testAMQPFederationRemoteAddressPolicyConfiguration() throws Throwable { + doTestAMQPFederationAddressPolicyConfiguration(false); + } + + private void doTestAMQPFederationAddressPolicyConfiguration(boolean local) throws Throwable { + final ConfigurationImpl configuration = new ConfigurationImpl(); + + final String policyType = local ? "localAddressPolicies" : "remoteAddressPolicies"; + + final Properties insertionOrderedProperties = new ConfigurationImpl.InsertionOrderedProperties(); + insertionOrderedProperties.put("AMQPConnections.target.uri", "localhost:61617"); + insertionOrderedProperties.put("AMQPConnections.target.retryInterval", 55); + insertionOrderedProperties.put("AMQPConnections.target.reconnectAttempts", -2); + insertionOrderedProperties.put("AMQPConnections.target.user", "admin"); + insertionOrderedProperties.put("AMQPConnections.target.password", "password"); + insertionOrderedProperties.put("AMQPConnections.target.autostart", "false"); + // This line is unnecessary but serves as a match to what the mirror connectionElements style + // configuration does as a way of explicitly documenting what you are configuring here. + insertionOrderedProperties.put("AMQPConnections.target.federations.abc.type", "FEDERATION"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m1.addressMatch", "a"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m2.addressMatch", "b"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m3.addressMatch", "c"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.maxHops", "2"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.autoDelete", "true"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.autoDeleteMessageCount", "42"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.autoDeleteDelay", "10000"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.includes.m4.addressMatch", "y"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.excludes.m5.addressMatch", "z"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.enableDivertBindings", "true"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.properties.a", "b"); + + configuration.parsePrefixedProperties(insertionOrderedProperties, null); + + Assert.assertEquals(1, configuration.getAMQPConnections().size()); + AMQPBrokerConnectConfiguration connectConfiguration = configuration.getAMQPConnections().get(0); + Assert.assertEquals("target", connectConfiguration.getName()); + Assert.assertEquals("localhost:61617", connectConfiguration.getUri()); + Assert.assertEquals(55, connectConfiguration.getRetryInterval()); + Assert.assertEquals(-2, connectConfiguration.getReconnectAttempts()); + Assert.assertEquals("admin", connectConfiguration.getUser()); + Assert.assertEquals("password", connectConfiguration.getPassword()); + Assert.assertEquals(false, connectConfiguration.isAutostart()); + Assert.assertEquals(1,connectConfiguration.getFederations().size()); + AMQPBrokerConnectionElement amqpBrokerConnectionElement = connectConfiguration.getConnectionElements().get(0); + Assert.assertTrue(amqpBrokerConnectionElement instanceof AMQPFederatedBrokerConnectionElement); + AMQPFederatedBrokerConnectionElement amqpFederationBrokerConnectionElement = (AMQPFederatedBrokerConnectionElement) amqpBrokerConnectionElement; + Assert.assertEquals("abc", amqpFederationBrokerConnectionElement.getName()); + + if (local) { + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getRemoteAddressPolicies().size()); + Assert.assertEquals(2, amqpFederationBrokerConnectionElement.getLocalAddressPolicies().size()); + } else { + Assert.assertEquals(2, amqpFederationBrokerConnectionElement.getRemoteAddressPolicies().size()); + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getLocalAddressPolicies().size()); + } + + final Set addressPolicies = local ? amqpFederationBrokerConnectionElement.getLocalAddressPolicies() + : amqpFederationBrokerConnectionElement.getRemoteAddressPolicies(); + + AMQPFederationAddressPolicyElement addressPolicy1 = null; + AMQPFederationAddressPolicyElement addressPolicy2 = null; + + for (AMQPFederationAddressPolicyElement policy : addressPolicies) { + if (policy.getName().equals("policy1")) { + addressPolicy1 = policy; + } else if (policy.getName().equals("policy2")) { + addressPolicy2 = policy; + } else { + throw new AssertionError("Found federation queue policy with unexpected name: " + policy.getName()); + } + } + + Assert.assertNotNull(addressPolicy1); + Assert.assertEquals(2, addressPolicy1.getMaxHops()); + Assert.assertEquals(42L, addressPolicy1.getAutoDeleteMessageCount().longValue()); + Assert.assertEquals(10000L, addressPolicy1.getAutoDeleteDelay().longValue()); + Assert.assertNull(addressPolicy1.isEnableDivertBindings()); + Assert.assertTrue(addressPolicy1.getProperties().isEmpty()); + + addressPolicy1.getIncludes().forEach(match -> { + if (match.getName().equals("m1")) { + Assert.assertEquals("a", match.getAddressMatch()); + } else if (match.getName().equals("m2")) { + Assert.assertEquals("b", match.getAddressMatch()); + } else if (match.getName().equals("m3")) { + Assert.assertEquals("c", match.getAddressMatch()); + } else { + throw new AssertionError("Found address match that was not expected: " + match.getName()); + } + }); + + Assert.assertNotNull(addressPolicy2); + Assert.assertEquals(0, addressPolicy2.getMaxHops()); + Assert.assertNull(addressPolicy2.getAutoDeleteMessageCount()); + Assert.assertNull(addressPolicy2.getAutoDeleteDelay()); + Assert.assertTrue(addressPolicy2.isEnableDivertBindings()); + Assert.assertFalse(addressPolicy2.getProperties().isEmpty()); + Assert.assertEquals("b", addressPolicy2.getProperties().get("a")); + + addressPolicy2.getIncludes().forEach(match -> { + if (match.getName().equals("m4")) { + Assert.assertEquals("y", match.getAddressMatch()); + } else { + throw new AssertionError("Found address match that was not expected: " + match.getName()); + } + }); + + addressPolicy2.getExcludes().forEach(match -> { + if (match.getName().equals("m5")) { + Assert.assertEquals("z", match.getAddressMatch()); + } else { + throw new AssertionError("Found address match that was not expected: " + match.getName()); + } + }); + + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getLocalQueuePolicies().size()); + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getRemoteQueuePolicies().size()); + } + + @Test + public void testAMQPFederationLocalQueuePolicyConfiguration() throws Throwable { + doTestAMQPFederationQueuePolicyConfiguration(true); + } + + @Test + public void testAMQPFederationRemoteQueuePolicyConfiguration() throws Throwable { + doTestAMQPFederationQueuePolicyConfiguration(false); + } + + public void doTestAMQPFederationQueuePolicyConfiguration(boolean local) throws Throwable { + final ConfigurationImpl configuration = new ConfigurationImpl(); + + final String policyType = local ? "localQueuePolicies" : "remoteQueuePolicies"; + + final Properties insertionOrderedProperties = new ConfigurationImpl.InsertionOrderedProperties(); + insertionOrderedProperties.put("AMQPConnections.target.uri", "localhost:61617"); + insertionOrderedProperties.put("AMQPConnections.target.retryInterval", 55); + insertionOrderedProperties.put("AMQPConnections.target.reconnectAttempts", -2); + insertionOrderedProperties.put("AMQPConnections.target.user", "admin"); + insertionOrderedProperties.put("AMQPConnections.target.password", "password"); + insertionOrderedProperties.put("AMQPConnections.target.autostart", "false"); + // This line is unnecessary but serves as a match to what the mirror connectionElements style + // configuration does as a way of explicitly documenting what you are configuring here. + insertionOrderedProperties.put("AMQPConnections.target.federations.abc.type", "FEDERATION"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m1.addressMatch", "#"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m1.queueMatch", "b"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m2.addressMatch", "a"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy1.includes.m2.queueMatch", "c"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.includes.m3.queueMatch", "d"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.excludes.m3.addressMatch", "e"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.excludes.m3.queueMatch", "e"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.properties.p1", "value1"); + insertionOrderedProperties.put("AMQPConnections.target.federations.abc." + policyType + ".policy2.properties.p2", "value2"); + + configuration.parsePrefixedProperties(insertionOrderedProperties, null); + + Assert.assertEquals(1, configuration.getAMQPConnections().size()); + AMQPBrokerConnectConfiguration connectConfiguration = configuration.getAMQPConnections().get(0); + Assert.assertEquals("target", connectConfiguration.getName()); + Assert.assertEquals("localhost:61617", connectConfiguration.getUri()); + Assert.assertEquals(55, connectConfiguration.getRetryInterval()); + Assert.assertEquals(-2, connectConfiguration.getReconnectAttempts()); + Assert.assertEquals("admin", connectConfiguration.getUser()); + Assert.assertEquals("password", connectConfiguration.getPassword()); + Assert.assertEquals(false, connectConfiguration.isAutostart()); + Assert.assertEquals(1,connectConfiguration.getFederations().size()); + AMQPBrokerConnectionElement amqpBrokerConnectionElement = connectConfiguration.getConnectionElements().get(0); + Assert.assertTrue(amqpBrokerConnectionElement instanceof AMQPFederatedBrokerConnectionElement); + AMQPFederatedBrokerConnectionElement amqpFederationBrokerConnectionElement = (AMQPFederatedBrokerConnectionElement) amqpBrokerConnectionElement; + Assert.assertEquals("abc", amqpFederationBrokerConnectionElement.getName()); + + if (local) { + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getRemoteQueuePolicies().size()); + Assert.assertEquals(2, amqpFederationBrokerConnectionElement.getLocalQueuePolicies().size()); + } else { + Assert.assertEquals(2, amqpFederationBrokerConnectionElement.getRemoteQueuePolicies().size()); + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getLocalQueuePolicies().size()); + } + + final Set addressPolicies = local ? amqpFederationBrokerConnectionElement.getLocalQueuePolicies() : + amqpFederationBrokerConnectionElement.getRemoteQueuePolicies(); + + AMQPFederationQueuePolicyElement queuePolicy1 = null; + AMQPFederationQueuePolicyElement queuePolicy2 = null; + + for (AMQPFederationQueuePolicyElement policy : addressPolicies) { + if (policy.getName().equals("policy1")) { + queuePolicy1 = policy; + } else if (policy.getName().equals("policy2")) { + queuePolicy2 = policy; + } else { + throw new AssertionError("Found federation queue policy with unexpected name: " + policy.getName()); + } + } + + Assert.assertNotNull(queuePolicy1); + Assert.assertFalse(queuePolicy1.getIncludes().isEmpty()); + Assert.assertTrue(queuePolicy1.getExcludes().isEmpty()); + Assert.assertEquals(2, queuePolicy1.getIncludes().size()); + Assert.assertEquals(0, queuePolicy1.getProperties().size()); + + queuePolicy1.getIncludes().forEach(match -> { + if (match.getName().equals("m1")) { + Assert.assertEquals("#", match.getAddressMatch()); + Assert.assertEquals("b", match.getQueueMatch()); + } else if (match.getName().equals("m2")) { + Assert.assertEquals("a", match.getAddressMatch()); + Assert.assertEquals("c", match.getQueueMatch()); + } else { + throw new AssertionError("Found queue match that was not expected: " + match.getName()); + } + }); + + Assert.assertNotNull(queuePolicy2); + Assert.assertFalse(queuePolicy2.getIncludes().isEmpty()); + Assert.assertFalse(queuePolicy2.getExcludes().isEmpty()); + + queuePolicy2.getIncludes().forEach(match -> { + if (match.getName().equals("m3")) { + Assert.assertNull(match.getAddressMatch()); + Assert.assertEquals("d", match.getQueueMatch()); + } else { + throw new AssertionError("Found queue match that was not expected: " + match.getName()); + } + }); + + queuePolicy2.getExcludes().forEach(match -> { + if (match.getName().equals("m3")) { + Assert.assertEquals("e", match.getAddressMatch()); + Assert.assertEquals("e", match.getQueueMatch()); + } else { + throw new AssertionError("Found queue match that was not expected: " + match.getName()); + } + }); + + Assert.assertEquals(2, queuePolicy2.getProperties().size()); + Assert.assertTrue(queuePolicy2.getProperties().containsKey("p1")); + Assert.assertTrue(queuePolicy2.getProperties().containsKey("p2")); + Assert.assertEquals("value1", queuePolicy2.getProperties().get("p1")); + Assert.assertEquals("value2", queuePolicy2.getProperties().get("p2")); + + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getLocalAddressPolicies().size()); + Assert.assertEquals(0, amqpFederationBrokerConnectionElement.getRemoteAddressPolicies().size()); + } @Test public void testAMQPConnectionsConfigurationUriEnc() throws Throwable { diff --git a/artemis-server/src/test/resources/ConfigurationTest-full-config.xml b/artemis-server/src/test/resources/ConfigurationTest-full-config.xml index f57c05e128..3fc1e98c26 100644 --- a/artemis-server/src/test/resources/ConfigurationTest-full-config.xml +++ b/artemis-server/src/test/resources/ConfigurationTest-full-config.xml @@ -447,6 +447,11 @@ + + + + + @@ -454,6 +459,41 @@ + + + + + + class-another + + + + + + + + + + class-name + + + + class-another + + + + + + + something + + + + + + + + LOCAL diff --git a/docs/user-manual/amqp-broker-connections.adoc b/docs/user-manual/amqp-broker-connections.adoc index 38b07fb5d0..6e8984329e 100644 --- a/docs/user-manual/amqp-broker-connections.adoc +++ b/docs/user-manual/amqp-broker-connections.adoc @@ -402,3 +402,109 @@ When a queue with its distinct name (as in the following example) is used, sende In the above example the `broker connection` would create an AMQP sender towards "queues.A". IMPORTANT: To avoid confusion it is recommended that `address name` and `queue name` are kept the same. + +== Federation + +Broker federation allows the local broker to create remote receivers for addresses or queues that have local demand, conversely the broker connection can send federation configuration to the remote broker causing it to create receivers on the local broker based on remote demand on an address or queue over this same connection. + +Add a `` element within the `` element to configure federation to the broker instance, the `` contains all the configuration for authentication and reconnection handling, see the above sections to configure those values. + +The broker connection federation configuration consists of one or more policies that define either local or remote federation configurations for addresses or queues. + +[,xml] +---- + + + + + + + + + + + + + + + + + + + + +---- + +===== Local and remote address federation + +Local or Remote address federation configures the local or remote broker to watch for demand on addresses and when demand exists it will create a consumer on the matching address on the opposing broker. Because the consumers are created on addresses on the opposing broker the authentication credentials supplied to the broker connection must have sufficient access to read from (local address federation) or write to the matching address (remote address federation) on the opposing broker. + +An example of address federation configuration is shown below, the local and remote address policies have identical configuration parameters only the policy element names are different. + +[,xml] +---- + + + + + + + + + + +---- + +name:: +The name of the policy, these names should be unique within a broker connection's federation policy elements. +include:: +The address-match pattern to use to match included addresses, multiple of these can be set. If none are set all addresses are matched. +exclude:: +The address-match pattern to use to match excluded addresses, multiple of these can be set or it can be omitted if no excludes are needed. +max-hops:: +The number of hops that a message can have made for it to be federated, the default value is zero and will work for most simple federatio0n deployments however is certain topologies a higher value may be required to prevent looping. +auto-delete:: +For address federation, a durable queue is created on the opposing broker for the matching address. This option is used to mark if the queue should be deleted once the initiating broker disconnects, and the delay and message count parameters have been met. This is useful if you want to automate the clean up, though you may wish to disable this if you want messages to queued for the opposing broker when it disconnects. The default value is `false` and the federation consumer queue will not be auto deleted. +auto-delete-delay:: +The amount of time in milliseconds after the initiating broker has disconnected before the created queue can be eligible for `auto-delete`. The default value is zero if the option is omitted. +auto-delete-message-count:: +The amount number messages in the remote queue that the message count must be equal or below before the initiating broker has disconnected before the queue can be eligible for auto deletion. The default value is zero if the option is omitted. +enable-divert-bindings:: +Setting to true will enable divert bindings to be listened for demand. If there is a divert binding with an address that matches the included addresses for the federation, any queue bindings that match the forward address of the divert will create demand. The default value for this option is `false`. + +===== Local and remote queue federation + +Local or Remote queue federation configures the local or remote broker to watch for demand on queues and when demand exists it will create a consumer on the matching queue on the opposing broker. Because the consumers are created on queues on the opposing broker the authentication credentials supplied to the broker connection must have sufficient access to read from (local queue federation) or write to the matching queue (remote queue federation) on the opposing broker. + +An example of queue federation configuration is shown below, the local and remote queue policies have identical configuration parameters only the policy element names are different. + +[,xml] +---- + + + + + + + + + + +---- + +name:: +The name of the policy, these names should be unique within a broker connection's federation policy elements. +include:: +The queue-match pattern to use to match included queues, multiple of these can be set. If none are set all queues are matched. +exclude:: +The queue-match pattern to use to match excluded queues, multiple of these can be set or it can be omitted if no excludes are needed. +priority-adjustment:: +When federation consumers are created this value can be used to ensure that those federation consumers have a lower priority value than other local consumers on the same queue. The default value for this configuration option if not set is `-1`. +include-federated:: +Controls if consumers on a queue which come from federation instances should be counted when observing a queue for demand, by default this value is `false` and federation consumers are not counted. + diff --git a/examples/features/broker-connection/amqp-federation/pom.xml b/examples/features/broker-connection/amqp-federation/pom.xml new file mode 100644 index 0000000000..bad1da6695 --- /dev/null +++ b/examples/features/broker-connection/amqp-federation/pom.xml @@ -0,0 +1,164 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.broker-connection + broker-connections + 2.31.0-SNAPSHOT + + + amqp-federation + jar + amqp-federation + + + ${project.basedir}/../../../.. + + + + + org.apache.qpid + qpid-jms-client + + + + + + + org.apache.activemq + artemis-maven-plugin + + + create0 + + create + + + ${noServer} + ${basedir}/target/server0 + true + ${basedir}/target/classes/activemq/server0 + + -Djava.net.preferIPv4Stack=true + + + + create1 + + create + + + ${noServer} + ${basedir}/target/server1 + true + ${basedir}/target/classes/activemq/server1 + + -Djava.net.preferIPv4Stack=true + + + + + start1 + + cli + + + ${noServer} + true + ${basedir}/target/server1 + tcp://localhost:5771 + + run + + server1 + + + + start0 + + cli + + + true + ${noServer} + ${basedir}/target/server0 + tcp://localhost:5660 + + run + + server0 + + + + runClient + + runClient + + + + org.apache.activemq.artemis.jms.example.BrokerFederationExample + + + + stop0 + + cli + + + ${noServer} + ${basedir}/target/server0 + + stop + + + + + stop1 + + cli + + + ${noServer} + ${basedir}/target/server1 + + stop + + + + + + + org.apache.activemq.examples.broker-connection + amqp-federation + ${project.version} + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + diff --git a/examples/features/broker-connection/amqp-federation/readme.md b/examples/features/broker-connection/amqp-federation/readme.md new file mode 100644 index 0000000000..47322f3b99 --- /dev/null +++ b/examples/features/broker-connection/amqp-federation/readme.md @@ -0,0 +1,5 @@ +# AMQP Broker Connection with local and remote Federation + +To run the example, simply type **mvn verify** from this directory, or **mvn -PnoServer verify** if you want to create and start the broker manually. + +This example demonstrates how you can federate messages sent to an Address on a remote server back to the local server and also instruct the remote server to federate messages sent to a Queue on the local server back to itself over a single AMQP connection. diff --git a/examples/features/broker-connection/amqp-federation/src/main/java/org/apache/activemq/artemis/jms/example/BrokerFederationExample.java b/examples/features/broker-connection/amqp-federation/src/main/java/org/apache/activemq/artemis/jms/example/BrokerFederationExample.java new file mode 100644 index 0000000000..a590f02696 --- /dev/null +++ b/examples/features/broker-connection/amqp-federation/src/main/java/org/apache/activemq/artemis/jms/example/BrokerFederationExample.java @@ -0,0 +1,77 @@ +/* + * 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.artemis.jms.example; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.jms.Topic; + +import org.apache.qpid.jms.JmsConnectionFactory; + +/** + * This example is demonstrating how messages are federated between two brokers with the + * federation configuration located on only one broker (server0) and only a single outbound + * connection is configured from server0 to server1 + */ +public class BrokerFederationExample { + + public static void main(final String[] args) throws Exception { + final ConnectionFactory connectionFactoryServer0 = new JmsConnectionFactory("amqp://localhost:5660"); + final ConnectionFactory connectionFactoryServer1 = new JmsConnectionFactory("amqp://localhost:5771"); + + final Connection connectionOnServer0 = connectionFactoryServer0.createConnection(); + final Connection connectionOnServer1 = connectionFactoryServer1.createConnection(); + + connectionOnServer0.start(); + connectionOnServer1.start(); + + final Session sessionOnServer0 = connectionOnServer0.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionOnServer1 = connectionOnServer1.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic ordersTopic = sessionOnServer0.createTopic("orders"); + final Queue trackingQueue = sessionOnServer1.createQueue("tracking"); + + // Federation from server1 back to server0 on the orders address + final MessageProducer ordersProducerOn1 = sessionOnServer1.createProducer(ordersTopic); + final MessageConsumer ordersConsumerOn0 = sessionOnServer0.createConsumer(ordersTopic); + + final TextMessage orderMessageSent = sessionOnServer1.createTextMessage("new-order"); + + ordersProducerOn1.send(orderMessageSent); + + final TextMessage orderMessageReceived = (TextMessage) ordersConsumerOn0.receive(5_000); + + System.out.println("Consumer on server 0 received order message from producer on server 1 " + orderMessageReceived.getText()); + + // Federation from server0 to server1 on the tracking queue + final MessageProducer trackingProducerOn0 = sessionOnServer0.createProducer(trackingQueue); + final MessageConsumer trackingConsumerOn1 = sessionOnServer1.createConsumer(trackingQueue); + + final TextMessage trackingMessageSent = sessionOnServer0.createTextMessage("new-tracking-data"); + + trackingProducerOn0.send(trackingMessageSent); + + final TextMessage trackingMessageReceived = (TextMessage) trackingConsumerOn1.receive(5_000); + + System.out.println("Consumer on server 1 received tracking data from producer on server 0 " + trackingMessageReceived.getText()); + } +} diff --git a/examples/features/broker-connection/amqp-federation/src/main/resources/activemq/server0/broker.xml b/examples/features/broker-connection/amqp-federation/src/main/resources/activemq/server0/broker.xml new file mode 100644 index 0000000000..2e0e0b4a8f --- /dev/null +++ b/examples/features/broker-connection/amqp-federation/src/main/resources/activemq/server0/broker.xml @@ -0,0 +1,123 @@ + + + + + + + + 0.0.0.0 + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 44000 + + + + tcp://0.0.0.0:5660?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + +

+ + +
+
+ + + +
+ + + + diff --git a/examples/features/broker-connection/amqp-federation/src/main/resources/activemq/server1/broker.xml b/examples/features/broker-connection/amqp-federation/src/main/resources/activemq/server1/broker.xml new file mode 100644 index 0000000000..1ce9e95c53 --- /dev/null +++ b/examples/features/broker-connection/amqp-federation/src/main/resources/activemq/server1/broker.xml @@ -0,0 +1,106 @@ + + + + + + + + 0.0.0.0 + + false + + NIO + + + true + + 120000 + + 60000 + + HALT + + 44000 + + + + tcp://0.0.0.0:5771?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP,STOMP,HORNETQ,MQTT,OPENWIRE;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + +
+ + +
+
+ + + +
+
+ +
+
diff --git a/examples/features/broker-connection/pom.xml b/examples/features/broker-connection/pom.xml index 796f0a7446..dbf9a34207 100644 --- a/examples/features/broker-connection/pom.xml +++ b/examples/features/broker-connection/pom.xml @@ -51,6 +51,7 @@ under the License. amqp-sending-messages-multicast amqp-receiving-messages amqp-sending-overssl + amqp-federation disaster-recovery @@ -61,6 +62,7 @@ under the License. amqp-sending-messages-multicast amqp-receiving-messages amqp-sending-overssl + amqp-federation disaster-recovery diff --git a/pom.xml b/pom.xml index 4733f02a37..c34fe140e6 100644 --- a/pom.xml +++ b/pom.xml @@ -129,6 +129,7 @@ 2.0.61.Final 0.34.1 + 1.0.0-M17 1.7.36 2.20.0 1.10.0 @@ -235,6 +236,7 @@ --> + -Dorg.apache.activemq.artemis.utils.RetryRule.retry=${retryTests} -Dbrokerconfig.maxDiskUsage=100 -Dorg.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.DEFAULT_QUIET_PERIOD=0 -Dorg.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants.DEFAULT_SHUTDOWN_TIMEOUT=0 -Djava.library.path="${activemq.basedir}/target/bin/lib/linux-x86_64:${activemq.basedir}/target/bin/lib/linux-i686" -Djgroups.bind_addr=localhost -Djava.net.preferIPv4Stack=true -Dbasedir=${basedir} @@ -243,6 +245,7 @@ ${project.basedir} true + false 2.0.0.AM25 2.0.0-M1 @@ -728,6 +731,13 @@ ${proton.version} + + org.apache.qpid + protonj2-test-driver + ${protonj2.version} + test + + org.apache.qpid qpid-jms-client @@ -1671,6 +1681,9 @@ alphabetical ${maven.test.redirectTestOutputToFile} ${activemq-surefire-argline} + + ${proton.trace.frames} + diff --git a/tests/integration-tests/pom.xml b/tests/integration-tests/pom.xml index 2b0d0c427e..5327844f89 100644 --- a/tests/integration-tests/pom.xml +++ b/tests/integration-tests/pom.xml @@ -302,6 +302,11 @@ proton-j test + + org.apache.qpid + protonj2-test-driver + test + org.slf4j @@ -435,7 +440,6 @@ mockito-core test - diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationAddressPolicyTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationAddressPolicyTest.java new file mode 100644 index 0000000000..fc2d925069 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationAddressPolicyTest.java @@ -0,0 +1,2166 @@ +/* + * 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.artemis.tests.integration.amqp.connect; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_DELAY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_MSG_COUNT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_ENABLE_DIVERT_BINDINGS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_MAX_HOPS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_ADDRESS_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_ADDRESS_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONFIGURATION; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LARGE_MESSAGE_THRESHOLD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.OPERATION_TYPE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS_LOW; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.TRANSFORMER_CLASS_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.TRANSFORMER_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.FEDERATED_ADDRESS_SOURCE_PROPERTIES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.MESSAGE_HOPS_ANNOTATION; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; + +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.jms.Topic; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.DivertConfiguration; +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.core.server.transformer.Transformer; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromAddressPolicy; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport; +import org.apache.activemq.artemis.tests.util.CFUtil; +import org.apache.activemq.artemis.utils.Wait; +import org.apache.qpid.protonj2.test.driver.ProtonTestClient; +import org.apache.qpid.protonj2.test.driver.ProtonTestServer; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.HeaderMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.MessageAnnotationsMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.PropertiesMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher; +import org.jgroups.util.UUID; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests for AMQP Broker federation handling of the receive from and send to address policy + * configuration handling. + */ +public class AMQPFederationAddressPolicyTest extends AmqpClientTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final WildcardConfiguration DEFAULT_WILDCARD_CONFIGURATION = new WildcardConfiguration(); + + @Override + protected ActiveMQServer createServer() throws Exception { + // Creates the broker used to make the outgoing connection. The port passed is for + // that brokers acceptor. The test server connected to by the broker binds to a random port. + return createServer(AMQP_PORT, false); + } + + @Test(timeout = 20000) + public void testFederationCreatesAddressReceiverWhenLocalQueueIsStaticlyDefined() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.MULTICAST) + .setAddress("test") + .setAutoCreated(false)); + + Wait.assertTrue(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists()); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + // Should be no frames generated as we already federated the address and the staticly added + // queue should retain demand when this consumer leaves. + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("test")); + + connection.start(); + consumer.close(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + // This should trigger the federation consumer to be shutdown as the statically defined queue + // should be the only remaining demand on the address. + logger.info("Removing Queues from federated address to eliminate demand"); + server.destroyQueue(SimpleString.toSimpleString("test")); + Wait.assertFalse(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationCreatesAddressReceiverLinkForAddressMatch() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(true); + receiveFromAddress.setAutoDeleteDelay(10_000L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, true); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, 10_000L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationCreatesAddressReceiverLinkForAddressMatchUsingPolicyCredit() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(true); + receiveFromAddress.setAutoDeleteDelay(10_000L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.addProperty(RECEIVER_CREDITS, "25"); + receiveFromAddress.addProperty(RECEIVER_CREDITS_LOW, "5"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, true); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, 10_000L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(25); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationCreatesAddressReceiverLinkForAddressMatchWithMaxHopsFilter() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.setMaxHops(1); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(true); + receiveFromAddress.setAutoDeleteDelay(10_000L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final String expectedJMSFilter = "\"m." + MESSAGE_HOPS_ANNOTATION + + "\" IS NULL OR \"m." + MESSAGE_HOPS_ANNOTATION + + "\"<1"; + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, true); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, 10_000L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .withSource().withJMSSelector(expectedJMSFilter).and() + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationClosesAddressReceiverLinkWhenDemandRemoved() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + // Demand is removed so receiver should be detached. + consumer.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationRetainsAddressReceiverLinkWhenDurableSubscriberIsOffline() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + connection.setClientID("test-clientId"); + + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final Topic topic = session.createTopic("test"); + final MessageConsumer consumer = session.createSharedDurableConsumer(topic, "shared-subscription"); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + // Consumer goes offline but demand is retained for address + consumer.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + session.unsubscribe("shared-subscription"); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationClosesAddressReceiverLinkWaitsForAllDemandToRemoved() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer1 = session.createConsumer(session.createTopic("test")); + final MessageConsumer consumer2 = session.createConsumer(session.createTopic("test")); + + connection.start(); + consumer1.close(); // One is gone but another remains + + // Will fail if any frames arrive + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer2.close(); // Now demand is gone + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationHandlesAddressDeletedAndConsumerRecreates() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + server.removeAddressInfo(SimpleString.toSimpleString("test"), null, true); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + // Consumer recreates Address and adds demand back and federation should restart + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationConsumerCreatedWhenDemandAddedToDivertAddress() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("test") + .setForwardingAddress("forward") + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the forwarding address should create a remote consumer for the forwarded address. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("forward")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationConsumerCreatedWhenDemandAddedToCompositeDivertAddress() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("test") + .setForwardingAddress("forward1,forward2") + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the forwarding address should create a remote consumer for the forwarded address. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + + // Creating a consumer on each should result in only one attach for the source address + final MessageConsumer consumer1 = session.createConsumer(session.createTopic("forward1")); + final MessageConsumer consumer2 = session.createConsumer(session.createTopic("forward2")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + // Closing one should not remove all demand on the source address + consumer1.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer2.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationConsumerRemovesDemandFromDivertConsumersOnlyWhenAllDemandIsRemoved() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("test") + .setForwardingAddress("forward") + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the forwarding address should create a remote consumer for the forwarded address. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer1 = session.createConsumer(session.createTopic("forward")); + final MessageConsumer consumer2 = session.createConsumer(session.createTopic("forward")); + + connection.start(); + consumer1.close(); // One is gone but another remains + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer2.close(); // Now demand is gone + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationConsumerRetainsDemandForDivertBindingWithoutActiveAnycastSubscriptions() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("source"); // Divert matching works on the source address of the divert + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("myFederations"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + // Any demand on the forwarding address even if the forward is a Queue (ANYCAST) should create + // demand on the remote for the source address (If the source is MULTICAST) + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("source") + .setForwardingAddress("forward") + .setRoutingType(ComponentConfigurationRoutingType.ANYCAST) + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + // Current implementation requires the source address exist on the local broker before it + // will attempt to federate it from the remote. + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("source"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the forwarding address should create a remote consumer for the forwarded address. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("myFederations"), + containsString("source"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final Queue queue = session.createQueue("forward"); + final MessageConsumer consumer1 = session.createConsumer(queue); + final MessageConsumer consumer2 = session.createConsumer(queue); + + connection.start(); + consumer1.close(); // One is gone but another remains + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + consumer2.close(); // Demand remains as the Queue continues to exist + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationConsumerRemovesDemandForDivertBindingWithoutActiveMulticastSubscriptions() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("source"); // Divert matching works on the source address of the divert + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("myFederations"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + // Any demand on the forwarding address even if the forward is a Queue (ANYCAST) should create + // demand on the remote for the source address (If the source is MULTICAST) + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("source") + .setForwardingAddress("forward") + .setRoutingType(ComponentConfigurationRoutingType.MULTICAST) + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + // Current implementation requires the source address exist on the local broker before it + // will attempt to federate it from the remote. + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("source"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the forwarding address should create a remote consumer for the forwarded address. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("myFederations"), + containsString("source"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final Topic topic = session.createTopic("forward"); + final MessageConsumer consumer1 = session.createConsumer(topic); + final MessageConsumer consumer2 = session.createConsumer(topic); + + connection.start(); + consumer1.close(); // One is gone but another remains + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer2.close(); // Now demand is gone from the divert + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationRemovesRemoteDemandIfDivertIsRemoved() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("source"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("source") + .setForwardingAddress("forward") + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("source"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the forwarding address should create a remote consumer for the forwarding address. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("source"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createTopic("forward")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + server.destroyDivert(SimpleString.toSimpleString("test-divert")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testDivertBindingsDoNotCreateAdditionalDemandIfDemandOnForwardingAddressAlreadyExists() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(false); + receiveFromAddress.setAutoDeleteDelay(-1L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + receiveFromAddress.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divertConfig = new DivertConfiguration().setAddress("test") + .setForwardingAddress("forward") + .setName("test-divert"); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.deployDivert(divertConfig); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + final Map expectedSourceProperties = new HashMap<>(); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE, false); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, -1L); + expectedSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, -1L); + + // Demand on the main address creates demand on the same address remotely and then the diverts + // should just be tracked under that original demand. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), expectedSourceProperties) + .respond() + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + final MessageConsumer consumer1 = session.createConsumer(session.createTopic("forward")); + final MessageConsumer consumer2 = session.createConsumer(session.createTopic("forward")); + + consumer1.close(); + consumer2.close(); + + server.destroyDivert(SimpleString.toSimpleString("test-divert")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testInboundMessageRoutedToReceiverOnLocalAddress() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(0) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("test")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerAcceptsAddressPolicyFromControlLink() throws Exception { + server.start(); + + final ArrayList includes = new ArrayList<>(); + includes.add("address1"); + includes.add("address2"); + final ArrayList excludes = new ArrayList<>(); + includes.add("address3"); + + final FederationReceiveFromAddressPolicy policy = + new FederationReceiveFromAddressPolicy("test-address-policy", + true, 30_000L, 1000L, 1, true, + includes, excludes, null, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendAddresPolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerAcceptsAddressPolicyFromControlLinkWithTransformerConfiguration() throws Exception { + server.start(); + + final ArrayList includes = new ArrayList<>(); + includes.add("address1"); + includes.add("address2"); + final ArrayList excludes = new ArrayList<>(); + includes.add("address3"); + + final Map transformerProperties = new HashMap<>(); + transformerProperties.put("key1", "value1"); + transformerProperties.put("key2", "value2"); + + final TransformerConfiguration transformerConfiguration = new TransformerConfiguration(); + transformerConfiguration.setClassName(ApplicationPropertiesTransformer.class.getName()); + transformerConfiguration.setProperties(transformerProperties); + + final FederationReceiveFromAddressPolicy policy = + new FederationReceiveFromAddressPolicy("test-address-policy", + true, 30_000L, 1000L, 1, true, + includes, excludes, null, transformerConfiguration, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendAddresPolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesAddressWhenDemandIsApplied() throws Exception { + server.start(); + + final ArrayList includes = new ArrayList<>(); + includes.add("address1"); + + final FederationReceiveFromAddressPolicy policy = + new FederationReceiveFromAddressPolicy("test-address-policy", + true, 30_000L, 1000L, 1, true, + includes, null, null, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendAddresPolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withSource().withAddress("address1") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("address1")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesAddressWhenDemandIsAppliedUsingControllerDefinedLinkCredit() throws Exception { + server.start(); + + final ArrayList includes = new ArrayList<>(); + includes.add("address1"); + + final FederationReceiveFromAddressPolicy policy = + new FederationReceiveFromAddressPolicy("test-address-policy", + true, 30_000L, 1000L, 1, true, + includes, null, null, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test", 10, 9); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendAddresPolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withSource().withAddress("address1") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(10); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectFlow().withLinkCredit(10); // Should top up the credit as we set low to nine + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("address1")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesAddressWhenDemandIsAppliedUsingPolicyDefinedLinkCredit() throws Exception { + server.start(); + + final ArrayList includes = new ArrayList<>(); + includes.add("address1"); + + final Map properties = new HashMap<>(); + properties.put(RECEIVER_CREDITS, 40); + properties.put(RECEIVER_CREDITS_LOW, "39"); + properties.put(LARGE_MESSAGE_THRESHOLD, 1024); + + final FederationReceiveFromAddressPolicy policy = + new FederationReceiveFromAddressPolicy("test-address-policy", + true, 30_000L, 1000L, 1, true, + includes, null, properties, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test", 10, 9); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendAddresPolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withSource().withAddress("address1") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(40); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectFlow().withLinkCredit(40); // Should top up the credit as we set low to 39 + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("address1")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesAddressAndAppliesTransformerWhenDemandIsApplied() throws Exception { + server.start(); + + final ArrayList includes = new ArrayList<>(); + includes.add("address1"); + + final Map transformerProperties = new HashMap<>(); + transformerProperties.put("key1", "value1"); + transformerProperties.put("key2", "value2"); + + final TransformerConfiguration transformerConfiguration = new TransformerConfiguration(); + transformerConfiguration.setClassName(ApplicationPropertiesTransformer.class.getName()); + transformerConfiguration.setProperties(transformerProperties); + + final FederationReceiveFromAddressPolicy policy = + new FederationReceiveFromAddressPolicy("test-address-policy", + true, 30_000L, 1000L, 1, true, + includes, null, null, transformerConfiguration, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendAddresPolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withSource().withAddress("address1") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("address1")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + assertEquals("value1", message.getStringProperty("key1")); + assertEquals("value2", message.getStringProperty("key2")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerAnswersAttachOfFederationReceiverProperly() throws Exception { + server.start(); + + final Map remoteSourceProperties = new HashMap<>(); + remoteSourceProperties.put(ADDRESS_AUTO_DELETE, true); + remoteSourceProperties.put(ADDRESS_AUTO_DELETE_DELAY, 10_000L); + remoteSourceProperties.put(ADDRESS_AUTO_DELETE_MSG_COUNT, 1L); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("federation-address-receiver") + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()) + .withTarget().also() + .withSource().withAddress("test"); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName("federation-address-receiver") + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withProperty(FEDERATED_ADDRESS_SOURCE_PROPERTIES.toString(), remoteSourceProperties) + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test") + .withCapabilities("topic") + .and() + .withTarget().and() + .now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testReceiverWithMaxHopsFilterAppliesFilterCorrectly() throws Exception { + server.start(); + + final String maxHopsJMSFilter = "\"m." + MESSAGE_HOPS_ANNOTATION + + "\" IS NULL OR \"m." + MESSAGE_HOPS_ANNOTATION + + "\"<2"; + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("federation-address-receiver") + .withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()) + .withSource().withAddress("test") + .withJMSSelector(maxHopsJMSFilter); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName("federation-address-receiver") + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test") + .withCapabilities("topic") + .withJMSSelector(maxHopsJMSFilter) + .and() + .withTarget().and() + .now(); + peer.remoteFlow().withLinkCredit(10).now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + // Match typical generic Qpid JMS Text Message structure + final HeaderMatcher headerMatcher = new HeaderMatcher(true); + final MessageAnnotationsMatcher annotationsMatcher = new MessageAnnotationsMatcher(true); + final PropertiesMatcher properties = new PropertiesMatcher(true); + final EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World"); + final TransferPayloadCompositeMatcher matcher = new TransferPayloadCompositeMatcher(); + matcher.setHeadersMatcher(headerMatcher); + matcher.setMessageAnnotationsMatcher(annotationsMatcher); + matcher.setPropertiesMatcher(properties); + matcher.addMessageContentMatcher(bodyMatcher); + + peer.expectTransfer().withPayload(matcher).accept(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageProducer producer = session.createProducer(session.createTopic("test")); + + producer.send(session.createTextMessage("Hello World")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + peer.expectTransfer().withPayload(matcher).accept(); + + try (ProtonTestClient sendingPeer = new ProtonTestClient()) { + sendingPeer.queueClientSaslAnonymousConnect(); + sendingPeer.connect("localhost", AMQP_PORT); + sendingPeer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + sendingPeer.expectOpen(); + sendingPeer.expectBegin(); + sendingPeer.expectAttach(); + sendingPeer.expectFlow(); + sendingPeer.remoteOpen().withContainerId("test-sender").now(); + sendingPeer.remoteBegin().now(); + sendingPeer.remoteAttach().ofSender() + .withInitialDeliveryCount(0) + .withName("sending-peer") + .withTarget().withAddress("test") + .withCapabilities("topic").also() + .withSource().also() + .now(); + sendingPeer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + sendingPeer.expectDisposition().withSettled(true) + .withState().accepted(); + sendingPeer.expectDisposition().withSettled(true) + .withState().accepted(); + + sendingPeer.remoteTransfer().withDeliveryId(0) + .withHeader().withDurability(false).also() + .withProperties().withMessageId("ID:1").also() + .withMessageAnnotations().withAnnotation(MESSAGE_HOPS_ANNOTATION.toString(), 1).also() + .withBody().withString("Hello World") + .also() + .now(); + + // Should be accepted but not routed to the main test peer client acting as a federated receiver + sendingPeer.remoteTransfer().withDeliveryId(1) + .withHeader().withDurability(false).also() + .withProperties().withMessageId("ID:2").also() + .withMessageAnnotations().withAnnotation(MESSAGE_HOPS_ANNOTATION.toString(), 2).also() + .withBody().withString("Hello World") + .also() + .now(); + sendingPeer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteConnectionCannotAttachAddressFederationLinkWithoutControlLink() throws Exception { + server.start(); + + try (ProtonTestClient peer = new ProtonTestClient()) { + peer.queueClientSaslAnonymousConnect(); + peer.remoteOpen().queue(); + peer.expectOpen(); + peer.remoteBegin().queue(); + peer.expectBegin(); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + // Broker should reject the attach since there's no control link + peer.expectAttach().ofSender().withName("federation-address-receiver") + .withSource(nullValue()) + .withTarget(); + peer.expectDetach().respond(); + + // Attempt to create a federation address receiver link without existing control link + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName("federation-address-receiver") + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test") + .withCapabilities("topic") + .and() + .withTarget().and() + .now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testTransformInboundFederatedMessageBeforeDispatch() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final Map newApplicationProperties = new HashMap<>(); + newApplicationProperties.put("appProperty1", "one"); + newApplicationProperties.put("appProperty2", "two"); + + final TransformerConfiguration transformerConfiguration = new TransformerConfiguration(); + transformerConfiguration.setClassName(ApplicationPropertiesTransformer.class.getName()); + transformerConfiguration.setProperties(newApplicationProperties); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.setTransformerConfiguration(transformerConfiguration); + receiveFromAddress.addToIncludes("test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_ADDRESS_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("address-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(0) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("test")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + assertEquals("one", message.getStringProperty("appProperty1")); + assertEquals("two", message.getStringProperty("appProperty2")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationDoesNotCreateAddressReceiverLinkForAddressMatchWhenLinkCreditIsSetToZero() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement receiveFromAddress = new AMQPFederationAddressPolicyElement(); + receiveFromAddress.setName("address-policy"); + receiveFromAddress.addToIncludes("test"); + receiveFromAddress.setAutoDelete(true); + receiveFromAddress.setAutoDeleteDelay(10_000L); + receiveFromAddress.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalAddressPolicy(receiveFromAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration( + "test-address-federation", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort() + "?amqpCredits=0"); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createTopic("test")); + + connection.start(); + + assertNull(consumer.receiveNoWait()); + consumer.close(); + + // Should be no interactions with the peer as credit is zero and address policy + // will not apply to any match when credit cannot be offered to avoid stranding + // a receiver on a remote address with no credit. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + public static class ApplicationPropertiesTransformer implements Transformer { + + private final Map properties = new HashMap<>(); + + @Override + public void init(Map externalProperties) { + properties.putAll(externalProperties); + } + + @Override + public org.apache.activemq.artemis.api.core.Message transform(org.apache.activemq.artemis.api.core.Message message) { + if (!(message instanceof AMQPMessage)) { + return message; + } + + properties.forEach((k, v) -> { + message.putStringProperty(k, v); + }); + + // An AMQP message must be encoded again to carry along the modifications. + message.reencode(); + + return message; + } + } + + private void sendAddresPolicyToRemote(ProtonTestClient peer, FederationReceiveFromAddressPolicy policy) { + final Map policyMap = new LinkedHashMap<>(); + + policyMap.put(POLICY_NAME, policy.getPolicyName()); + policyMap.put(ADDRESS_AUTO_DELETE, policy.isAutoDelete()); + policyMap.put(ADDRESS_AUTO_DELETE_DELAY, policy.getAutoDeleteDelay()); + policyMap.put(ADDRESS_AUTO_DELETE_MSG_COUNT, policy.getAutoDeleteMessageCount()); + policyMap.put(ADDRESS_MAX_HOPS, policy.getMaxHops()); + policyMap.put(ADDRESS_ENABLE_DIVERT_BINDINGS, policy.isEnableDivertBindings()); + + if (!policy.getIncludes().isEmpty()) { + policyMap.put(ADDRESS_INCLUDES, new ArrayList<>(policy.getIncludes())); + } + if (!policy.getExcludes().isEmpty()) { + policyMap.put(ADDRESS_EXCLUDES, new ArrayList<>(policy.getExcludes())); + } + + final TransformerConfiguration transformerConfig = policy.getTransformerConfiguration(); + + if (transformerConfig != null) { + policyMap.put(TRANSFORMER_CLASS_NAME, transformerConfig.getClassName()); + if (transformerConfig.getProperties() != null && !transformerConfig.getProperties().isEmpty()) { + policyMap.put(TRANSFORMER_PROPERTIES_MAP, transformerConfig.getProperties()); + } + } + + if (!policy.getProperties().isEmpty()) { + policyMap.put(POLICY_PROPERTIES_MAP, policy.getProperties()); + } + + peer.remoteTransfer().withDeliveryId(0) + .withMessageAnnotations().withAnnotation(OPERATION_TYPE.toString(), ADD_ADDRESS_POLICY) + .also() + .withBody().withValue(policyMap) + .also() + .now(); + } + + // Use this method to script the initial handshake that a broker that is establishing + // a federation connection with a remote broker instance would perform. + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName) { + scriptFederationConnectToRemote(peer, federationName, AmqpSupport.AMQP_CREDITS_DEFAULT, AmqpSupport.AMQP_LOW_CREDITS_DEFAULT); + } + + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName, int amqpCredits, int amqpLowCredits) { + final String federationControlLinkName = "Federation:control:" + UUID.randomUUID().toString(); + + final Map federationConfiguration = new HashMap<>(); + federationConfiguration.put(RECEIVER_CREDITS, amqpCredits); + federationConfiguration.put(RECEIVER_CREDITS_LOW, amqpLowCredits); + + final Map senderProperties = new HashMap<>(); + senderProperties.put(FEDERATION_CONFIGURATION.toString(), federationConfiguration); + + peer.queueClientSaslAnonymousConnect(); + peer.remoteOpen().queue(); + peer.expectOpen(); + peer.remoteBegin().queue(); + peer.expectBegin(); + peer.remoteAttach().ofSender() + .withInitialDeliveryCount(0) + .withName(federationControlLinkName) + .withPropertiesMap(senderProperties) + .withDesiredCapabilities(FEDERATION_CONTROL_LINK.toString()) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().also() + .withTarget().withDynamic(true) + .withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withLifetimePolicyOfDeleteOnClose() + .withCapabilities("temporary-topic") + .also() + .queue(); + peer.expectAttach().ofReceiver() + .withTarget() + .withAddress(notNullValue()) + .also() + .withOfferedCapability(FEDERATION_CONTROL_LINK.toString()); + peer.expectFlow(); + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationBrokerPliuginTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationBrokerPliuginTest.java new file mode 100644 index 0000000000..75308a66f2 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationBrokerPliuginTest.java @@ -0,0 +1,565 @@ +/* + * 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.artemis.tests.integration.amqp.connect; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.jms.Topic; + +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.DivertConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; +import org.apache.activemq.artemis.core.server.Divert; +import org.apache.activemq.artemis.core.server.Queue; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.ActiveMQServerAMQPFederationPlugin; +import org.apache.activemq.artemis.protocol.amqp.federation.Federation; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumer; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationConsumerInfo; +import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport; +import org.apache.activemq.artemis.tests.util.CFUtil; +import org.apache.activemq.artemis.utils.Wait; +import org.junit.After; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test that covers the use of the AMQP Federation broker plugin + */ +public class AMQPFederationBrokerPliuginTest extends AmqpClientTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final int SERVER_PORT = AMQP_PORT; + private static final int SERVER_PORT_REMOTE = AMQP_PORT + 1; + + protected ActiveMQServer remoteServer; + + @Override + protected String getConfiguredProtocols() { + return "AMQP,CORE"; + } + + @Override + protected ActiveMQServer createServer() throws Exception { + remoteServer = createServer(SERVER_PORT_REMOTE, false); + + return createServer(SERVER_PORT, false); + } + + @After + @Override + public void tearDown() throws Exception { + super.tearDown(); + + try { + if (remoteServer != null) { + remoteServer.stop(); + } + } catch (Exception e) { + } + } + + @Test(timeout = 20000) + public void testFederationBrokerPluginWithAddressPolicyConfigured() throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement localAddressPolicy = new AMQPFederationAddressPolicyElement(); + localAddressPolicy.setName("test-policy"); + localAddressPolicy.addToIncludes("test"); + localAddressPolicy.setAutoDelete(false); + localAddressPolicy.setAutoDeleteDelay(-1L); + localAddressPolicy.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalAddressPolicy(localAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final AMQPTestFederationBrokerPlugin federationPlugin = new AMQPTestFederationBrokerPlugin(); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.registerBrokerPlugin(federationPlugin); + server.start(); + + Wait.assertTrue(() -> federationPlugin.started.get()); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic topic = sessionL.createTopic("test"); + + final MessageConsumer consumerL = sessionL.createConsumer(topic); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should trigger receiver on remote. + Wait.assertTrue(() -> server.addressQuery(SimpleString.toSimpleString("test")).isExists()); + Wait.assertTrue(() -> remoteServer.addressQuery(SimpleString.toSimpleString("test")).isExists()); + Wait.assertTrue(() -> federationPlugin.beforeCreateConsumerCapture.get() != null); + Wait.assertTrue(() -> federationPlugin.afterCreateConsumerCapture.get() != null); + + final MessageProducer producerR = sessionR.createProducer(topic); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + final AtomicReference messagePreHandled = new AtomicReference<>(); + final AtomicReference messagePostHandled = new AtomicReference<>(); + + federationPlugin.beforeMessageHandled = (c, m) -> { + messagePreHandled.set(m); + }; + federationPlugin.afterMessageHandled = (c, m) -> { + messagePostHandled.set(m); + }; + + producerR.send(message); + + Wait.assertTrue(() -> messagePreHandled.get() != null); + Wait.assertTrue(() -> messagePostHandled.get() != null); + + assertSame(messagePreHandled.get(), messagePostHandled.get()); + + final Message received = consumerL.receive(5_000); + + consumerL.close(); + + Wait.assertTrue(() -> federationPlugin.beforeCloseConsumerCapture.get() != null); + Wait.assertTrue(() -> federationPlugin.afterCloseConsumerCapture.get() != null); + + assertNotNull(received); + } + + server.stop(); + + Wait.assertTrue(() -> federationPlugin.stopped.get()); + } + + @Test(timeout = 20000) + public void testFederationBrokerPluginWithQueuePolicyConfigured() throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationQueuePolicyElement localQueuePolicy = new AMQPFederationQueuePolicyElement(); + localQueuePolicy.setName("test-policy"); + localQueuePolicy.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalQueuePolicy(localQueuePolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-queue-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final AMQPTestFederationBrokerPlugin federationPlugin = new AMQPTestFederationBrokerPlugin(); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + remoteServer.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + server.registerBrokerPlugin(federationPlugin); + server.start(); + + Wait.assertTrue(() -> federationPlugin.started.get()); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final javax.jms.Queue queue = sessionL.createQueue("test"); + + final MessageConsumer consumerL = sessionL.createConsumer(queue); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should trigger receiver on remote. + Wait.assertTrue(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists()); + Wait.assertTrue(() -> federationPlugin.beforeCreateConsumerCapture.get() != null); + Wait.assertTrue(() -> federationPlugin.afterCreateConsumerCapture.get() != null); + + final MessageProducer producerR = sessionR.createProducer(queue); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + final AtomicReference messagePreHandled = new AtomicReference<>(); + final AtomicReference messagePostHandled = new AtomicReference<>(); + + federationPlugin.beforeMessageHandled = (c, m) -> { + messagePreHandled.set(m); + }; + federationPlugin.afterMessageHandled = (c, m) -> { + messagePostHandled.set(m); + }; + + producerR.send(message); + + Wait.assertTrue(() -> messagePreHandled.get() != null); + Wait.assertTrue(() -> messagePostHandled.get() != null); + + assertSame(messagePreHandled.get(), messagePostHandled.get()); + + final Message received = consumerL.receive(5_000); + + consumerL.close(); + + Wait.assertTrue(() -> federationPlugin.beforeCloseConsumerCapture.get() != null); + Wait.assertTrue(() -> federationPlugin.afterCloseConsumerCapture.get() != null); + + assertNotNull(received); + } + + server.stop(); + + Wait.assertTrue(() -> federationPlugin.stopped.get()); + } + + @Test(timeout = 20000) + public void testPluginCanBlockAddressFederationConsumerCreate() throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement localAddressPolicy = new AMQPFederationAddressPolicyElement(); + localAddressPolicy.setName("test-policy"); + localAddressPolicy.addToIncludes("test"); + localAddressPolicy.setAutoDelete(false); + localAddressPolicy.setAutoDeleteDelay(-1L); + localAddressPolicy.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalAddressPolicy(localAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final AMQPTestFederationBrokerPlugin federationPlugin = new AMQPTestFederationBrokerPlugin(); + federationPlugin.shouldCreateConsumerForAddress = (a) -> false; + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.registerBrokerPlugin(federationPlugin); + server.start(); + + Wait.assertTrue(() -> federationPlugin.started.get()); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic topic = sessionL.createTopic("test"); + + final MessageConsumer consumerL = sessionL.createConsumer(topic); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should not trigger receiver on remote. + Wait.assertTrue(() -> server.addressQuery(SimpleString.toSimpleString("test")).isExists()); + + final MessageProducer producerR = sessionR.createProducer(topic); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + final AtomicReference messagePreHandled = new AtomicReference<>(); + + federationPlugin.beforeMessageHandled = (c, m) -> { + messagePreHandled.set(m); + }; + + producerR.send(message); + + assertNull(consumerL.receiveNoWait()); + assertNull(messagePreHandled.get()); + + consumerL.close(); + } + + server.stop(); + + Wait.assertTrue(() -> federationPlugin.stopped.get()); + } + + @Test(timeout = 20000) + public void testPluginCanBlockQueueFederationConsumerCreate() throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationQueuePolicyElement localQueuePolicy = new AMQPFederationQueuePolicyElement(); + localQueuePolicy.setName("test-policy"); + localQueuePolicy.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalQueuePolicy(localQueuePolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-queue-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final AMQPTestFederationBrokerPlugin federationPlugin = new AMQPTestFederationBrokerPlugin(); + federationPlugin.shouldCreateConsumerForQueue = (q) -> false; + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + remoteServer.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + server.registerBrokerPlugin(federationPlugin); + server.start(); + + Wait.assertTrue(() -> federationPlugin.started.get()); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final javax.jms.Queue queue = sessionL.createQueue("test"); + + final MessageConsumer consumerL = sessionL.createConsumer(queue); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should not trigger receiver on remote. + Wait.assertTrue(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists()); + + final MessageProducer producerR = sessionR.createProducer(queue); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + final AtomicReference messagePreHandled = new AtomicReference<>(); + + federationPlugin.beforeMessageHandled = (c, m) -> { + messagePreHandled.set(m); + }; + + producerR.send(message); + + assertNull(consumerL.receiveNoWait()); + assertNull(messagePreHandled.get()); + + consumerL.close(); + } + + server.stop(); + + Wait.assertTrue(() -> federationPlugin.stopped.get()); + } + + @Test(timeout = 20000) + public void testPluginCanBlockAddressFederationWhenDemandOnDivertIsAdded() throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement localAddressPolicy = new AMQPFederationAddressPolicyElement(); + localAddressPolicy.setName("test-policy"); + localAddressPolicy.addToIncludes("source"); + localAddressPolicy.setAutoDelete(false); + localAddressPolicy.setAutoDeleteDelay(-1L); + localAddressPolicy.setAutoDeleteMessageCount(-1L); + localAddressPolicy.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalAddressPolicy(localAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divert = new DivertConfiguration(); + divert.setName("test-divert"); + divert.setAddress("source"); + divert.setForwardingAddress("target"); + divert.setRoutingType(ComponentConfigurationRoutingType.MULTICAST); + + final AMQPTestFederationBrokerPlugin federationPlugin = new AMQPTestFederationBrokerPlugin(); + federationPlugin.shouldCreateConsumerForDivert = (d, q) -> false; + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.registerBrokerPlugin(federationPlugin); + server.start(); + server.deployDivert(divert); + // Currently the address must exist on the local before we will federate from the remote + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("source"), RoutingType.MULTICAST)); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic target = sessionL.createTopic("target"); + final Topic source = sessionL.createTopic("source"); + + final MessageConsumer consumerL = sessionL.createConsumer(target); + + connectionL.start(); + connectionR.start(); + + final MessageProducer producerR = sessionR.createProducer(source); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + final AtomicReference messagePreHandled = new AtomicReference<>(); + + federationPlugin.beforeMessageHandled = (c, m) -> { + messagePreHandled.set(m); + }; + + producerR.send(message); + + assertNull(consumerL.receiveNoWait()); + assertNull(messagePreHandled.get()); + } + + server.stop(); + + Wait.assertTrue(() -> federationPlugin.stopped.get()); + } + + private class AMQPTestFederationBrokerPlugin implements ActiveMQServerAMQPFederationPlugin { + + public final AtomicBoolean started = new AtomicBoolean(); + public final AtomicBoolean stopped = new AtomicBoolean(); + + public final AtomicReference beforeCreateConsumerCapture = new AtomicReference<>(); + public final AtomicReference afterCreateConsumerCapture = new AtomicReference<>(); + public final AtomicReference beforeCloseConsumerCapture = new AtomicReference<>(); + public final AtomicReference afterCloseConsumerCapture = new AtomicReference<>(); + + public Consumer beforeCreateConsumer = (c) -> beforeCreateConsumerCapture.set(c);; + public Consumer afterCreateConsumer = (c) -> afterCreateConsumerCapture.set(c); + public Consumer beforeCloseConsumer = (c) -> beforeCloseConsumerCapture.set(c); + public Consumer afterCloseConsumer = (c) -> afterCloseConsumerCapture.set(c); + + public BiConsumer beforeMessageHandled = (c, m) -> { }; + public BiConsumer afterMessageHandled = (c, m) -> { }; + + public Function shouldCreateConsumerForAddress = (a) -> true; + public Function shouldCreateConsumerForQueue = (q) -> true; + public BiFunction shouldCreateConsumerForDivert = (d, q) -> true; + + @Override + public void federationStarted(final Federation federation) throws ActiveMQException { + started.set(true); + } + + @Override + public void federationStopped(final Federation federation) throws ActiveMQException { + stopped.set(true); + } + + @Override + public void beforeCreateFederationConsumer(final FederationConsumerInfo consumerInfo) throws ActiveMQException { + beforeCreateConsumer.accept(consumerInfo); + } + + @Override + public void afterCreateFederationConsumer(final FederationConsumer consumer) throws ActiveMQException { + afterCreateConsumer.accept(consumer); + } + + @Override + public void beforeCloseFederationConsumer(final FederationConsumer consumer) throws ActiveMQException { + beforeCloseConsumer.accept(consumer); + } + + @Override + public void afterCloseFederationConsumer(final FederationConsumer consumer) throws ActiveMQException { + afterCloseConsumer.accept(consumer); + } + + @Override + public void beforeFederationConsumerMessageHandled(final FederationConsumer consumer, org.apache.activemq.artemis.api.core.Message message) throws ActiveMQException { + beforeMessageHandled.accept(consumer, message); + } + + @Override + public void afterFederationConsumerMessageHandled(final FederationConsumer consumer, org.apache.activemq.artemis.api.core.Message message) throws ActiveMQException { + afterMessageHandled.accept(consumer, message); + } + + @Override + public boolean shouldCreateFederationConsumerForAddress(final AddressInfo address) throws ActiveMQException { + return shouldCreateConsumerForAddress.apply(address); + } + + @Override + public boolean shouldCreateFederationConsumerForQueue(final Queue queue) throws ActiveMQException { + return shouldCreateConsumerForQueue.apply(queue); + } + + @Override + public boolean shouldCreateFederationConsumerForDivert(Divert divert, Queue queue) throws ActiveMQException { + return shouldCreateConsumerForDivert.apply(divert, queue); + } + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationConnectTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationConnectTest.java new file mode 100644 index 0000000000..7ab6126907 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationConnectTest.java @@ -0,0 +1,520 @@ +/* + * 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.artemis.tests.integration.amqp.connect; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_DELAY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_AUTO_DELETE_MSG_COUNT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_ENABLE_DIVERT_BINDINGS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADDRESS_MAX_HOPS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_ADDRESS_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_QUEUE_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONFIGURATION; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK_VALIDATION_ADDRESS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LARGE_MESSAGE_THRESHOLD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LINK_ATTACH_TIMEOUT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.OPERATION_TYPE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDE_FEDERATED; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_PRIORITY_ADJUSTMENT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS_LOW; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.allOf; + +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants; +import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport; +import org.apache.activemq.artemis.utils.Wait; +import org.apache.qpid.protonj2.test.driver.ProtonTestClient; +import org.apache.qpid.protonj2.test.driver.ProtonTestServer; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.MessageAnnotationsMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher; +import org.hamcrest.Matchers; +import org.jgroups.util.UUID; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests basic connect handling of the AMQP federation feature. + */ +public class AMQPFederationConnectTest extends AmqpClientTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + @Override + protected ActiveMQServer createServer() throws Exception { + // Creates the broker used to make the outgoing connection. The port passed is for + // that brokers acceptor. The test server connected to by the broker binds to a random port. + return createServer(AMQP_PORT, false); + } + + @Test(timeout = 20000) + public void testBrokerConnectsWithAnonymous() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + // No user or pass given, it will have to select ANONYMOUS even though PLAIN also offered + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testFederatedBrokerConnectsWithPlain() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLPlainConnect("user", "pass", "PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + server.getConfiguration().addAMQPConnection(amqpConnection); + amqpConnection.setUser("user"); + amqpConnection.setPassword("pass"); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testFederationConfiguredCreatesControlLink() throws Exception { + final int AMQP_MIN_LARGE_MESSAGE_SIZE = 10_000; + final int AMQP_CREDITS = 100; + final int AMQP_CREDITS_LOW = 50; + final int AMQP_LINK_ATTACH_TIMEOUT = 60; + + final Map federationConfiguration = new HashMap<>(); + federationConfiguration.put(RECEIVER_CREDITS, AMQP_CREDITS); + federationConfiguration.put(RECEIVER_CREDITS_LOW, AMQP_CREDITS_LOW); + federationConfiguration.put(LARGE_MESSAGE_THRESHOLD, AMQP_MIN_LARGE_MESSAGE_SIZE); + federationConfiguration.put(LINK_ATTACH_TIMEOUT, AMQP_LINK_ATTACH_TIMEOUT); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(AMQPFederationConstants.FEDERATION_CONTROL_LINK.toString()) + .withName(allOf(containsString("Federation"), containsString("myFederation"))) + .withProperty(FEDERATION_CONFIGURATION.toString(), federationConfiguration) + .withTarget().withDynamic(true) + .withCapabilities("temporary-topic") + .and() + .respond() + .withTarget().withAddress("test-control-address") + .and() + .withOfferedCapabilities(AMQPFederationConstants.FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration( + "testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort() + + "?amqpCredits=" + AMQP_CREDITS + "&amqpLowCredits=" + AMQP_CREDITS_LOW + + "&amqpMinLargeMessageSize=" + AMQP_MIN_LARGE_MESSAGE_SIZE); + amqpConnection.setReconnectAttempts(0);// No reconnects + final AMQPFederatedBrokerConnectionElement federation = new AMQPFederatedBrokerConnectionElement("myFederation"); + federation.addProperty(LINK_ATTACH_TIMEOUT, AMQP_LINK_ATTACH_TIMEOUT); + amqpConnection.addElement(federation); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + Wait.assertTrue(() -> server.locateQueue("test-control-address") != null); + } + } + + @Test(timeout = 20000) + public void testFederationCreatesControlLinkAndClosesConnectionIfCapabilityIsAbsent() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender().withDesiredCapability(AMQPFederationConstants.FEDERATION_CONTROL_LINK.toString()).respond(); + peer.expectConnectionToDrop(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(new AMQPFederatedBrokerConnectionElement("test")); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testFederationCreatesControlLinkAndClosesConnectionDetachIndicatesNotAuthorized() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender().withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()) + .withSource() + .also() + .withNullTarget(); + peer.remoteDetach().withErrorCondition("amqp:unauthorized-access", "Not authroized").queue(); + peer.expectDetach().optional(); + // Broker reconnect and allow it to attach this time. + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withTarget().withDynamic(true).and() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind() + .withTarget().withAddress("dynamic-name"); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(1);// One reconnects + amqpConnection.setRetryInterval(200); + amqpConnection.addElement(new AMQPFederatedBrokerConnectionElement("test")); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(10, TimeUnit.SECONDS); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testFederationSendsReceiveFromQueuePolicyToRemoteWhenSendToIsConfigured() throws Exception { + final MessageAnnotationsMatcher maMatcher = new MessageAnnotationsMatcher(true); + maMatcher.withEntry(OPERATION_TYPE.toString(), Matchers.is(ADD_QUEUE_POLICY)); + final Map policyMap = new LinkedHashMap<>(); + + final List includes = new ArrayList<>(); + includes.add("a"); + includes.add("b"); + includes.add("c"); + includes.add("d"); + final List excludes = new ArrayList<>(); + excludes.add("e"); + excludes.add("f"); + excludes.add("g"); + excludes.add("h"); + + policyMap.put(POLICY_NAME, "test-policy"); + policyMap.put(QUEUE_INCLUDE_FEDERATED, true); + policyMap.put(QUEUE_PRIORITY_ADJUSTMENT, 42); + policyMap.put(QUEUE_INCLUDES, includes); + policyMap.put(QUEUE_EXCLUDES, excludes); + + final EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher(policyMap); + final TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher(); + payloadMatcher.setMessageAnnotationsMatcher(maMatcher); + payloadMatcher.addMessageContentMatcher(bodyMatcher); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withTarget().withDynamic(true).and() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind().withTarget().withAddress("test-dynamic"); + peer.remoteFlow().withLinkCredit(10).queue(); + peer.expectTransfer().withPayload(payloadMatcher); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement sendToQueue = new AMQPFederationQueuePolicyElement(); + sendToQueue.setName("test-policy"); + sendToQueue.setIncludeFederated(true); + sendToQueue.setPriorityAdjustment(42); + sendToQueue.addToIncludes("a", "b"); + sendToQueue.addToIncludes("c", "d"); + sendToQueue.addToExcludes("e", "f"); + sendToQueue.addToExcludes("g", "h"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addRemoteQueuePolicy(sendToQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationSendsReceiveFromAddressPolicyToRemoteWhenSendToIsConfigured() throws Exception { + final MessageAnnotationsMatcher maMatcher = new MessageAnnotationsMatcher(true); + maMatcher.withEntry(OPERATION_TYPE.toString(), Matchers.is(ADD_ADDRESS_POLICY)); + final Map policyMap = new LinkedHashMap<>(); + + final List includes = new ArrayList<>(); + includes.add("include"); + final List excludes = new ArrayList<>(); + excludes.add("exclude"); + + policyMap.put(POLICY_NAME, "test-policy"); + policyMap.put(ADDRESS_AUTO_DELETE, true); + policyMap.put(ADDRESS_AUTO_DELETE_DELAY, 42L); + policyMap.put(ADDRESS_AUTO_DELETE_MSG_COUNT, 314L); + policyMap.put(ADDRESS_MAX_HOPS, 5); + policyMap.put(ADDRESS_ENABLE_DIVERT_BINDINGS, false); + policyMap.put(ADDRESS_INCLUDES, includes); + policyMap.put(ADDRESS_EXCLUDES, excludes); + + final EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher(policyMap); + final TransferPayloadCompositeMatcher payloadMatcher = new TransferPayloadCompositeMatcher(); + payloadMatcher.setMessageAnnotationsMatcher(maMatcher); + payloadMatcher.addMessageContentMatcher(bodyMatcher); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withTarget().withDynamic(true).and() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind().withTarget().withAddress("test-dynamic"); + peer.remoteFlow().withLinkCredit(10).queue(); + peer.expectTransfer().withPayload(payloadMatcher); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationAddressPolicyElement sendToAddress = new AMQPFederationAddressPolicyElement(); + sendToAddress.setName("test-policy"); + sendToAddress.setAutoDelete(true); + sendToAddress.setAutoDeleteDelay(42L); + sendToAddress.setAutoDeleteMessageCount(314L); + sendToAddress.setMaxHops(5); + sendToAddress.setEnableDivertBindings(false); + sendToAddress.addToIncludes("include"); + sendToAddress.addToExcludes("exclude"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addRemoteAddressPolicy(sendToAddress); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-send-policy", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testConnectToBrokerFromRemoteAsFederatedSourceAndCreateControlLink() throws Exception { + server.start(); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + + logger.info("Test stopped"); + } + } + + @Test(timeout = 20000) + public void testControlLinkPassesConnectAttemptWhenUserHasPrivledges() throws Exception { + enableSecurity(server, FEDERATION_CONTROL_LINK_VALIDATION_ADDRESS); + server.start(); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test", fullUser, fullPass); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + + logger.info("Test stopped"); + } + } + + @Test(timeout = 20000) + public void testControlLinkRefusesConnectAttemptWhenUseDoesNotHavePrivledgesForControlAddress() throws Exception { + enableSecurity(server, FEDERATION_CONTROL_LINK_VALIDATION_ADDRESS); + server.start(); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemoteNotAuthorizedForControlAddress(peer, "test", guestUser, guestPass); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + + logger.info("Test stopped"); + } + } + + // Use these methods to script the initial handshake that a broker that is establishing + // a federation connection with a remote broker instance would perform. + + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName) { + scriptFederationConnectToRemote(peer, federationName, false, null, null); + } + + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName, String username, String password) { + scriptFederationConnectToRemote(peer, federationName, true, username, password); + } + + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName, boolean auth, String username, String password) { + final String federationControlLinkName = "Federation:test:" + UUID.randomUUID().toString(); + + if (auth) { + peer.queueClientSaslPlainConnect(username, password); + } else { + peer.queueClientSaslAnonymousConnect(); + } + + peer.remoteOpen().queue(); + peer.expectOpen(); + peer.remoteBegin().queue(); + peer.expectBegin(); + peer.remoteAttach().ofSender() + .withName(federationControlLinkName) + .withDesiredCapabilities(FEDERATION_CONTROL_LINK.toString()) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().also() + .withTarget().withDynamic(true) + .withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withLifetimePolicyOfDeleteOnClose() + .withCapabilities("temporary-topic") + .also() + .queue(); + peer.expectAttach().ofReceiver() + .withTarget() + .withAddress(notNullValue()) + .also() + .withOfferedCapability(FEDERATION_CONTROL_LINK.toString()); + peer.expectFlow(); + } + + private void scriptFederationConnectToRemoteNotAuthorizedForControlAddress(ProtonTestClient peer, String federationName, String username, String password) { + final String federationControlLinkName = "Federation:test:" + UUID.randomUUID().toString(); + + peer.queueClientSaslPlainConnect(username, password); + peer.remoteOpen().queue(); + peer.expectOpen(); + peer.remoteBegin().queue(); + peer.expectBegin(); + peer.remoteAttach().ofSender() + .withName(federationControlLinkName) + .withDesiredCapabilities(FEDERATION_CONTROL_LINK.toString()) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().also() + .withTarget().withDynamic(true) + .withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withLifetimePolicyOfDeleteOnClose() + .withCapabilities("temporary-topic") + .also() + .queue(); + peer.expectAttach().ofReceiver() + .withTarget(nullValue()); + peer.expectDetach().withError("amqp:unauthorized-access", + "User does not have permission to attach to the federation control address").respond(); + peer.remoteClose().queue(); + peer.expectClose(); + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationQueuePolicyTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationQueuePolicyTest.java new file mode 100644 index 0000000000..32ccaec405 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationQueuePolicyTest.java @@ -0,0 +1,2544 @@ +/* + * 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.artemis.tests.integration.amqp.connect; + +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.ADD_QUEUE_POLICY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONFIGURATION; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_CONTROL_LINK; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_QUEUE_RECEIVER; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.FEDERATION_RECEIVER_PRIORITY; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.LARGE_MESSAGE_THRESHOLD; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.OPERATION_TYPE; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_EXCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDES; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_INCLUDE_FEDERATED; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.QUEUE_PRIORITY_ADJUSTMENT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.RECEIVER_CREDITS_LOW; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.TRANSFORMER_CLASS_NAME; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.TRANSFORMER_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_PROPERTIES_MAP; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT; +import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationQueueConsumer.DEFAULT_PULL_CREDIT_BATCH_SIZE; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; + +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.jms.Message; +import javax.jms.Queue; + +import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration; +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.TransformerConfiguration; +import org.apache.activemq.artemis.core.config.WildcardConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.transformer.Transformer; +import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage; +import org.apache.activemq.artemis.protocol.amqp.federation.FederationReceiveFromQueuePolicy; +import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport; +import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport; +import org.apache.activemq.artemis.tests.util.CFUtil; +import org.apache.qpid.protonj2.test.driver.ProtonTestClient; +import org.apache.qpid.protonj2.test.driver.ProtonTestServer; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.ApplicationPropertiesMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.HeaderMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.MessageAnnotationsMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.messaging.PropertiesMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.transport.TransferPayloadCompositeMatcher; +import org.apache.qpid.protonj2.test.driver.matchers.types.EncodedAmqpValueMatcher; +import org.jgroups.util.UUID; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tests for AMQP Broker federation handling of the receive from and send to queue policy + * configuration handling. + */ +public class AMQPFederationQueuePolicyTest extends AmqpClientTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final WildcardConfiguration DEFAULT_WILDCARD_CONFIGURATION = new WildcardConfiguration(); + + @Override + protected ActiveMQServer createServer() throws Exception { + // Creates the broker used to make the outgoing connection. The port passed is for + // that brokers acceptor. The test server connected to by the broker binds to a random port. + return createServer(AMQP_PORT, false); + } + + @Test(timeout = 20000) + public void testFederationCreatesQueueReceiverLinkForQueueMatchAnycast() throws Exception { + doTestFederationCreatesQueueReceiverLinkForQueueMatch(RoutingType.ANYCAST); + } + + @Test(timeout = 20000) + public void testFederationCreatesQueueReceiverLinkForQueueMatchMulticast() throws Exception { + doTestFederationCreatesQueueReceiverLinkForQueueMatch(RoutingType.MULTICAST); + } + + private void doTestFederationCreatesQueueReceiverLinkForQueueMatch(RoutingType routingType) throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(routingType) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .respond() + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + if (RoutingType.MULTICAST.equals(routingType)) { + // Requires FQQN to meet the test expectations + session.createConsumer(session.createTopic("test::test")); + } else { + session.createConsumer(session.createQueue("test")); + } + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationQueueReceiverCarriesConfiguredQueueFilter() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setFilterString("color='red'") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSource().withJMSSelector("color='red'").and() + .respond() + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationCreatesQueueReceiverLinkForQueueMatchUsingPolicyCredit() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + receiveFromQueue.addProperty(RECEIVER_CREDITS, "30"); + receiveFromQueue.addProperty(RECEIVER_CREDITS_LOW, "3"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSource().and() + .withTarget().and() + .respond() + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(30); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testFederationClosesQueueReceiverWhenDemandIsRemovedFromQueue() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationHandlesQueueDeletedAndConsumerRecreates() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + server.destroyQueue(SimpleString.toSimpleString("test"), null, false, true); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + // Consumer recreates Queue and adds demand back and federation should restart + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testSecondQueueConsumerDoesNotGenerateAdditionalFederationReceiver() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testLinkCreatedForEachDistinctQueueMatchInSameConfiguredPolicy() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("addr1", "test.1"); + receiveFromQueue.addToIncludes("addr2", "test.2"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.1").setRoutingType(RoutingType.ANYCAST) + .setAddress("addr1") + .setAutoCreated(false)); + server.createQueue(new QueueConfiguration("test.2").setRoutingType(RoutingType.ANYCAST) + .setAddress("addr2") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("addr1::test.1"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.1")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("addr2::test.2"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + session.createConsumer(session.createQueue("test.2")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationReceiverCreatedWhenWildcardPolicyMatchesConsumerQueue() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("", "test.#"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.queue").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.queue")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testRemoteCloseOfQueueReceiverRespondsToDetach() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("", "test.#"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.queue").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.queue")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); + peer.remoteDetach().withErrorCondition("amqp:resource-deleted", "the resource was deleted").afterDelay(10).now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testRejectedQueueReceiverAttachWhenLocalMatchingQueueNotFoundIsHandled() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("", "test.#"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.queue").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind() + .withNullSource(); + peer.expectFlow().withLinkCredit(1000); + peer.remoteDetach().withErrorCondition("amqp:not-found", "the requested queue was not found").queue().afterDelay(10); + peer.expectDetach(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.queue")); + + connection.start(); + + // Broker normally treats any remote link closure on the broker connection as terminal + // but shouldn't in this case as it indicates the requested federated queue wasn't present + // on the remote. New queue interest should result in a new attempt to federate the queue + // and this time we will let it succeed. + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test.queue"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + + session.createConsumer(session.createQueue("test.queue")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testRemoteCloseQueueReceiverWhenRemoteResourceIsDeletedIsHandled() throws Exception { + doTestRemoteCloseQueueReceiverForExpectedConditionsIsHandled("amqp:resource-deleted"); + } + + @Test(timeout = 20000) + public void testRemoteCloseQueueReceiverWhenRemoteReceiverIsForcedToDetachIsHandled() throws Exception { + doTestRemoteCloseQueueReceiverForExpectedConditionsIsHandled("amqp:link:detach-forced"); + } + + private void doTestRemoteCloseQueueReceiverForExpectedConditionsIsHandled(String condition) throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("", "test.#"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.queue").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test.queue"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + peer.remoteDetach().withErrorCondition(condition, "error message from remote....").queue().afterDelay(20); + peer.expectDetach(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.queue")); + + connection.start(); + + // Broker normally treats any remote link closure on the broker connection as terminal + // but shouldn't in this case as it indicates the requested federated queue receiver was + // forced closed. New queue interest should result in a new attempt to federate the queue + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test.queue"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + + session.createConsumer(session.createQueue("test.queue")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testUnhandledRemoteReceiverCloseConditionCausesConnectionRebuild() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("", "test.#"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(1); // One reconnect to meet test expectations and use a + amqpConnection.setRetryInterval(100); // Short reconnect interval. + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.queue").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test.queue"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.queue")); + + connection.start(); + + // Broker treats some remote link closures on the broker connection as terminal + // in this case we signal some internal error which should cause rebuild of the + // broker connection. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectDetach().optional(); // Broker is not consistent on sending the detach + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test.queue"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + + // Trigger the error that should cause the broker to drop and reconnect + peer.remoteDetach().withErrorCondition("amqp:internal-error", "the resource suffered an internal error").afterDelay(10).now(); + + peer.waitForScriptToComplete(50, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testInboundMessageRoutedToReceiverOnLocalQueue() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("", "test.#"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.queue").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test.queue"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withSource().withAddress("test::test.queue") + .and() + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(0) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createQueue("test.queue")); + + connection.start(); + + final Message message = consumer.receive(20_0000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testFederationCreatesQueueReceiverLinkWithDefaultPrioirty() throws Exception { + doTestFederationCreatesQueueReceiverLinkWithAdjustedPriority(0); + } + + @Test(timeout = 20000) + public void testFederationCreatesQueueReceiverLinkWithIncreasedPriority() throws Exception { + doTestFederationCreatesQueueReceiverLinkWithAdjustedPriority(5); + } + + @Test(timeout = 20000) + public void testFederationCreatesQueueReceiverLinkWithDecreasedPriority() throws Exception { + doTestFederationCreatesQueueReceiverLinkWithAdjustedPriority(-5); + } + + private void doTestFederationCreatesQueueReceiverLinkWithAdjustedPriority(int adjustment) throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.setPriorityAdjustment(adjustment); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), + ActiveMQDefaultConfiguration.getDefaultConsumerPriority() + adjustment) + .respond() + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()); + peer.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + } + + @Test(timeout = 20000) + public void testLinkCreatedForEachDistinctQueueMatchInSameConfiguredPolicyWithSameAddressMatch() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + // Ensures the code doesn't lose track when the address value is the same across includes. + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("addr", "test.1"); + receiveFromQueue.addToIncludes("addr", "test.2"); + receiveFromQueue.addToIncludes("addr", "test.3"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test.1").setRoutingType(RoutingType.ANYCAST) + .setAddress("addr") + .setAutoCreated(false)); + server.createQueue(new QueueConfiguration("test.2").setRoutingType(RoutingType.ANYCAST) + .setAddress("addr") + .setAutoCreated(false)); + server.createQueue(new QueueConfiguration("test.3").setRoutingType(RoutingType.ANYCAST) + .setAddress("addr") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("addr::test.1"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test.1")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("addr::test.2"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + session.createConsumer(session.createQueue("test.2")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("addr::test.3"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000).optional(); + + session.createConsumer(session.createQueue("test.3")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach().respond(); + peer.expectDetach().respond(); + peer.expectDetach().respond(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerAcceptsQueuePolicyFromControlLink() throws Exception { + server.start(); + + final Collection> includes = new ArrayList<>(); + includes.add(new AbstractMap.SimpleEntry<>("#", "testQueue")); + final Collection> excludes = new ArrayList<>(); + excludes.add(new AbstractMap.SimpleEntry<>("address1", "test.#")); + + final FederationReceiveFromQueuePolicy policy = + new FederationReceiveFromQueuePolicy("test-queue-policy", + true, -2, includes, excludes, null, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendQueuePolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerAcceptsQueuePolicyFromControlLinkWithTransformerConfiguration() throws Exception { + server.start(); + + final Collection> includes = new ArrayList<>(); + includes.add(new AbstractMap.SimpleEntry<>("#", "testQueue")); + final Collection> excludes = new ArrayList<>(); + excludes.add(new AbstractMap.SimpleEntry<>("address1", "test.#")); + + final Map transformerProperties = new HashMap<>(); + transformerProperties.put("key1", "value1"); + transformerProperties.put("key2", "value2"); + + final TransformerConfiguration transformerConfiguration = new TransformerConfiguration(); + transformerConfiguration.setClassName(ApplicationPropertiesTransformer.class.getName()); + transformerConfiguration.setProperties(transformerProperties); + + final FederationReceiveFromQueuePolicy policy = + new FederationReceiveFromQueuePolicy("test-queue-policy", + true, -2, includes, excludes, + null, transformerConfiguration, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendQueuePolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesQueueWhenDemandIsApplied() throws Exception { + server.start(); + + final Collection> includes = new ArrayList<>(); + includes.add(new AbstractMap.SimpleEntry<>("#", "testQueue")); + + final FederationReceiveFromQueuePolicy policy = + new FederationReceiveFromQueuePolicy("test-queue-policy", + true, -2, includes, null, null, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendQueuePolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("testQueue::testQueue") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createQueue("testQueue")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesQueueWhenDemandIsAppliedUsingControllerDefinedLinkCredit() throws Exception { + server.start(); + + final Collection> includes = new ArrayList<>(); + includes.add(new AbstractMap.SimpleEntry<>("#", "testQueue")); + + final FederationReceiveFromQueuePolicy policy = + new FederationReceiveFromQueuePolicy("test-queue-policy", + true, -2, includes, null, null, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test", 10, 9); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendQueuePolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("testQueue::testQueue") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(10); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectFlow().withLinkCredit(10); // Should top up the credit as we set low to nine + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createQueue("testQueue")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesQueueWhenDemandIsAppliedUsingPolicyDefinedLinkCredit() throws Exception { + server.start(); + + final Collection> includes = new ArrayList<>(); + includes.add(new AbstractMap.SimpleEntry<>("#", "testQueue")); + + final Map properties = new HashMap<>(); + properties.put(RECEIVER_CREDITS, "40"); + properties.put(RECEIVER_CREDITS_LOW, 39); + properties.put(LARGE_MESSAGE_THRESHOLD, 2048); + + final FederationReceiveFromQueuePolicy policy = + new FederationReceiveFromQueuePolicy("test-queue-policy", + true, -2, includes, null, properties, null, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test", 10, 9); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendQueuePolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("testQueue::testQueue") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(40); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectFlow().withLinkCredit(40); // Should top up the credit as we set low to 39 + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createQueue("testQueue")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteFederatesQueueAndAppliesTransformerWhenDemandIsApplied() throws Exception { + server.start(); + + final Collection> includes = new ArrayList<>(); + includes.add(new AbstractMap.SimpleEntry<>("#", "testQueue")); + + final Map transformerProperties = new HashMap<>(); + transformerProperties.put("key1", "value1"); + transformerProperties.put("key2", "value2"); + + final TransformerConfiguration transformerConfiguration = new TransformerConfiguration(); + transformerConfiguration.setClassName(ApplicationPropertiesTransformer.class.getName()); + transformerConfiguration.setProperties(transformerProperties); + + final FederationReceiveFromQueuePolicy policy = + new FederationReceiveFromQueuePolicy("test-queue-policy", + true, -2, includes, null, + null, transformerConfiguration, + DEFAULT_WILDCARD_CONFIGURATION); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDisposition().withSettled(true).withState().accepted(); + + sendQueuePolicyToRemote(peer, policy); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("testQueue::testQueue") + .and() + .respondInKind(); // Server detected demand + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(1) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createQueue("testQueue")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + assertEquals("value1", message.getStringProperty("key1")); + assertEquals("value2", message.getStringProperty("key2")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerAnswersAttachOfFederationReceiverProperly() throws Exception { + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerRoutesInboundMessageToFederatedReceiver() throws Exception { + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + peer.remoteFlow().withLinkCredit(1).now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + // Match typical generic Qpid JMS Text Message structure + final HeaderMatcher headerMatcher = new HeaderMatcher(true); + final MessageAnnotationsMatcher annotationsMatcher = new MessageAnnotationsMatcher(true); + final PropertiesMatcher properties = new PropertiesMatcher(true); + final EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("Hello World"); + final TransferPayloadCompositeMatcher matcher = new TransferPayloadCompositeMatcher(); + matcher.setHeadersMatcher(headerMatcher); + matcher.setMessageAnnotationsMatcher(annotationsMatcher); + matcher.setPropertiesMatcher(properties); + matcher.addMessageContentMatcher(bodyMatcher); + + peer.expectTransfer().withPayload(matcher).accept(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageProducer producer = session.createProducer(session.createQueue("test")); + + producer.send(session.createTextMessage("Hello World")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerCanFailLinkAttachIfQueueDoesNotExistWithoutClosingTheConnection() throws Exception { + server.start(); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test").withSource(nullValue()); + peer.expectDetach().withError("amqp:not-found", "Queue: 'test' does not exist").respond(); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Now create it and try again + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + // Connect to remote as if an queue had demand and matched our federation policy + // this is our second attempt and the remote queue exists now so it should succeed. + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerCanFailLinkAttachIfQueueDoesNotMatchFullExpectationWithoutClosingTheConnection() throws Exception { + server.start(); + + // Now create it and try again + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test").withSource(nullValue()); + peer.expectDetach().withError("amqp:not-found", "Queue: 'test' is not mapped to specified address: address").respond(); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("address::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Connect to remote as if an queue had demand and matched our federation policy + // this is our second attempt and the remote queue exists now so it should succeed. + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteConnectionCannotAttachQueueFederationLinkWithoutControlLink() throws Exception { + server.start(); + + try (ProtonTestClient peer = new ProtonTestClient()) { + peer.queueClientSaslAnonymousConnect(); + peer.remoteOpen().queue(); + peer.expectOpen(); + peer.remoteBegin().queue(); + peer.expectBegin(); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + // Broker should reject the attach since there's no control link + peer.expectAttach().ofSender().withName("federation-queue-receiver") + .withSource(nullValue()) + .withTarget(); + peer.expectDetach().respond(); + + // Attempt to create a federation queue receiver link without existing control link + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("federation-queue-receiver") + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testRemoteBrokerRoutesInboundMessageToFederatedReceiverWithFilterApplied() throws Exception { + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + try (ProtonTestClient peer = new ProtonTestClient()) { + scriptFederationConnectToRemote(peer, "test"); + peer.connect("localhost", AMQP_PORT); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofSender().withName("test::test") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Connect to remote as if an queue had demand and matched our federation policy + peer.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("test::test") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withJMSSelector("color = 'red'") + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + peer.remoteFlow().withLinkCredit(10).now(); + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + // Match typical generic Qpid JMS Text Message structure + final HeaderMatcher headerMatcher = new HeaderMatcher(true); + final MessageAnnotationsMatcher annotationsMatcher = new MessageAnnotationsMatcher(true); + final ApplicationPropertiesMatcher appPropertiesMatcher = new ApplicationPropertiesMatcher(true); + appPropertiesMatcher.withEntry("color", equalTo("red")); + final PropertiesMatcher properties = new PropertiesMatcher(true); + final EncodedAmqpValueMatcher bodyMatcher = new EncodedAmqpValueMatcher("red"); + final TransferPayloadCompositeMatcher matcher = new TransferPayloadCompositeMatcher(); + matcher.setHeadersMatcher(headerMatcher); + matcher.setMessageAnnotationsMatcher(annotationsMatcher); + matcher.setPropertiesMatcher(properties); + matcher.setApplicationPropertiesMatcher(appPropertiesMatcher); + matcher.addMessageContentMatcher(bodyMatcher); + + peer.expectTransfer().withPayload(matcher).accept(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageProducer producer = session.createProducer(session.createQueue("test")); + + // Federation receiver should have applied filter that causes this message not to federate + final Message blueMessage = session.createTextMessage("blue"); + blueMessage.setStringProperty("color", "blue"); + + final Message redMessage = session.createTextMessage("red"); + redMessage.setStringProperty("color", "red"); + + producer.send(blueMessage); + producer.send(redMessage); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + peer.expectClose(); + peer.remoteClose().now(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + + server.stop(); + } + } + + @Test(timeout = 20000) + public void testBrokerDoesNotFederateQueueIfOnlyDemandIsFromAnotherBrokerFederationSubscription() throws Exception { + try (ProtonTestServer target = new ProtonTestServer()) { + target.expectSASLAnonymousConnect(); + target.expectOpen().respond(); + target.expectBegin().respond(); + target.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + target.start(); + + final URI remoteURI = target.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + receiveFromQueue.setIncludeFederated(false); // No federation for federation subscriptions + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + + // Simulate another broker connecting as a federation instance that will create demand on + // queue "test::test" but should not generate demand to the server the broker connected + // to as a federation control because we configured it to ignore federation subscriptions + // when considering demand on the matching queues. + try (ProtonTestClient client = new ProtonTestClient()) { + scriptFederationConnectToRemote(client, "incoming-federation"); + client.connect("localhost", AMQP_PORT); + + client.waitForScriptToComplete(5, TimeUnit.SECONDS); + client.expectAttach().ofSender().withName("fake-federation-incoming-receiver-link") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Connect to remote as if a queue had demand and matched our federation policy + client.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("fake-federation-incoming-receiver-link") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + client.remoteFlow().withLinkCredit(10).now(); + client.waitForScriptToComplete(5, TimeUnit.SECONDS); + client.expectDetach(); + client.expectClose(); + + client.remoteDetach().withErrorCondition("amqp:resource-deleted", "The resource was deleted").now(); + client.remoteClose().now(); + client.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + // Would fail if any frames arrived that are not scripted to. + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + target.expectAttach().ofReceiver().withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT).respond() + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()); + target.expectFlow().withLinkCredit(1000); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + // Now create demand that isn't a federation consumer and the remote should see an incoming + // receiver attach for the federated queue. + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + target.expectClose(); + target.remoteClose().now(); + + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + target.close(); + } + } + } + + @Test(timeout = 20000) + public void testBrokerCanFederateQueueIfOnlyDemandIsFromAnotherBrokerFederationSubscription() throws Exception { + try (ProtonTestServer target = new ProtonTestServer()) { + target.expectSASLAnonymousConnect(); + target.expectOpen().respond(); + target.expectBegin().respond(); + target.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respond() + .withOfferedCapabilities(FEDERATION_CONTROL_LINK.toString()); + target.start(); + + final URI remoteURI = target.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + receiveFromQueue.setIncludeFederated(true); // do federate for federation subscriptions + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + // broker should create a new receiver that extends the federation receiver to this "broker" + // but because this is a federation of a federation the priority should drop by an additional + // increment as we apply the adjustment on each step + target.expectAttach().ofReceiver().withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test::test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT - 1).respond() + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()); + // Should get a flow but if the link goes away quick enough the broker won't get to this before detaching. + target.expectFlow().withLinkCredit(1000).optional(); + target.expectDetach().respond(); + + // Simulate another broker connecting as a federation instance that will create demand on + // queue "test::test" and should generate demand to the server the broker connected to + // as a federation control because we configured to not ignore federation subscriptions + // when considering demand on the matching queues. + try (ProtonTestClient client = new ProtonTestClient()) { + scriptFederationConnectToRemote(client, "incoming-federation"); + client.connect("localhost", AMQP_PORT); + + client.waitForScriptToComplete(5, TimeUnit.SECONDS); + client.expectAttach().ofSender().withName("fake-federation-incoming-receiver-link") + .withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withSource().withAddress("test::test"); + + // Connect to remote as if a queue had demand and matched our federation policy + client.remoteAttach().ofReceiver() + .withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString()) + .withName("fake-federation-incoming-receiver-link") + .withProperty(FEDERATION_RECEIVER_PRIORITY.toString(), DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withAddress("test::test") + .withCapabilities("queue") + .and() + .withTarget().and() + .now(); + client.remoteFlow().withLinkCredit(10).now(); + client.waitForScriptToComplete(5, TimeUnit.SECONDS); + + client.expectDetach(); + client.remoteDetach().withErrorCondition("amqp:resource-deleted", "Resource deleted").later(30); + client.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + target.expectClose(); + target.remoteClose().now(); + + target.waitForScriptToComplete(5, TimeUnit.SECONDS); + target.close(); + } + } + + @Test(timeout = 20000) + public void testTransformInboundFederatedMessageBeforeDispatch() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final Map newApplicationProperties = new HashMap<>(); + newApplicationProperties.put("appProperty1", "one"); + newApplicationProperties.put("appProperty2", "two"); + + final TransformerConfiguration transformerConfiguration = new TransformerConfiguration(); + transformerConfiguration.setClassName(ApplicationPropertiesTransformer.class.getName()); + transformerConfiguration.setProperties(newApplicationProperties); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.setTransformerConfiguration(transformerConfiguration); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(1000); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(0) + .queue(); + peer.expectDisposition().withSettled(true).withState().accepted(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final MessageConsumer consumer = session.createConsumer(session.createQueue("test")); + + connection.start(); + + final Message message = consumer.receive(5_000); + assertNotNull(message); + assertTrue(message instanceof TextMessage); + assertEquals("test-message", ((TextMessage) message).getText()); + assertEquals("one", message.getStringProperty("appProperty1")); + assertEquals("two", message.getStringProperty("appProperty2")); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectDetach(); // demand will be gone and receiver link should close. + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.close(); + } + } + + @Test(timeout = 20000) + public void testPullQueueConsumerGrantsCreditOnEmptyQueue() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration( + "testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort() + "?amqpCredits=0"); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + peer.expectFlow().withLinkCredit(DEFAULT_PULL_CREDIT_BATCH_SIZE); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + session.createConsumer(session.createQueue("test")); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + peer.close(); + } + } + } + + @Test(timeout = 30000) + public void testPullQueueConsumerGrantsCreditOnlyWhenPendingMessageIsConsumed() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration( + "testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort() + "?amqpCredits=0"); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + + // Must be pull consumer to ensure we can check that demand on remote doesn't + // offer credit until the pending message count on the Queue is zeroed + final ConnectionFactory factory = CFUtil.createConnectionFactory( + "AMQP", "tcp://localhost:" + AMQP_PORT + "?jms.prefetchPolicy.all=0"); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final Queue queue = session.createQueue("test"); + final MessageProducer producer = session.createProducer(queue); + + // Add to backlog + producer.send(session.createMessage()); + + // Now create demand on the queue + final MessageConsumer consumer = session.createConsumer(queue); + + connection.start(); + producer.close(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectFlow().withLinkCredit(DEFAULT_PULL_CREDIT_BATCH_SIZE); + + // Remove the backlog and credit should be offered to the remote + assertNotNull(consumer.receiveNoWait()); + + peer.waitForScriptToComplete(20, TimeUnit.SECONDS); + + peer.close(); + } + } + } + + @Test(timeout = 30000) + public void testPullQueueConsumerBatchCreditTopUpAfterEachBacklogDrain() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect(); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withDesiredCapability(FEDERATION_CONTROL_LINK.toString()) + .respondInKind(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Test started, peer listening on: {}", remoteURI); + + final AMQPFederationQueuePolicyElement receiveFromQueue = new AMQPFederationQueuePolicyElement(); + receiveFromQueue.setName("queue-policy"); + receiveFromQueue.addToIncludes("test", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("sample-federation"); + element.addLocalQueuePolicy(receiveFromQueue); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration( + "testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort() + "?amqpCredits=0"); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectAttach().ofReceiver() + .withDesiredCapability(FEDERATION_QUEUE_RECEIVER.toString()) + .withName(allOf(containsString("sample-federation"), + containsString("test"), + containsString("queue-receiver"), + containsString(server.getNodeID().toString()))) + .respondInKind(); + + // Must be pull consumer to ensure we can check that demand on remote doesn't + // offer credit until the pending message count on the Queue is zeroed + final ConnectionFactory factory = CFUtil.createConnectionFactory( + "AMQP", "tcp://localhost:" + AMQP_PORT + "?jms.prefetchPolicy.all=0"); + + try (Connection connection = factory.createConnection()) { + final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE); + final Queue queue = session.createQueue("test"); + final MessageProducer producer = session.createProducer(queue); + + // Add to backlog + producer.send(session.createMessage()); + + // Now create demand on the queue + final MessageConsumer consumer = session.createConsumer(queue); + + connection.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectFlow().withLinkCredit(DEFAULT_PULL_CREDIT_BATCH_SIZE); + + // Remove the backlog and credit should be offered to the remote + assertNotNull(consumer.receiveNoWait()); + + peer.waitForScriptToComplete(20, TimeUnit.SECONDS); + + // Consume all the credit that was presented in the batch + for (int i = 0; i < DEFAULT_PULL_CREDIT_BATCH_SIZE; ++i) { + peer.expectDisposition().withState().accepted(); + peer.remoteTransfer().withBody().withString("test-message") + .also() + .withDeliveryId(i) + .now(); + } + + // Consume all the newly received message from the remote except one + // which should leave the queue with a pending message so no credit + // should be offered. + for (int i = 0; i < DEFAULT_PULL_CREDIT_BATCH_SIZE - 1; ++i) { + assertNotNull(consumer.receiveNoWait()); + } + + // We should not get a new batch yet as there is still one pending + // message on the local queue we have not consumed. + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + peer.expectFlow().withLinkCredit(DEFAULT_PULL_CREDIT_BATCH_SIZE); + + // Remove the backlog and credit should be offered to the remote again + assertNotNull(consumer.receiveNoWait()); + + peer.waitForScriptToComplete(20, TimeUnit.SECONDS); + peer.expectDetach().respond(); + + consumer.close(); // Remove local demand and federation consumer is torn down. + + peer.waitForScriptToComplete(20, TimeUnit.SECONDS); + peer.close(); + } + } + } + + public static class ApplicationPropertiesTransformer implements Transformer { + + private final Map properties = new HashMap<>(); + + @Override + public void init(Map externalProperties) { + properties.putAll(externalProperties); + } + + @Override + public org.apache.activemq.artemis.api.core.Message transform(org.apache.activemq.artemis.api.core.Message message) { + if (!(message instanceof AMQPMessage)) { + return message; + } + + properties.forEach((k, v) -> { + message.putStringProperty(k, v); + }); + + // An AMQP message must be encoded again to carry along the modifications. + message.reencode(); + + return message; + } + } + + private void sendQueuePolicyToRemote(ProtonTestClient peer, FederationReceiveFromQueuePolicy policy) { + final Map policyMap = new LinkedHashMap<>(); + + policyMap.put(POLICY_NAME, policy.getPolicyName()); + policyMap.put(QUEUE_INCLUDE_FEDERATED, policy.isIncludeFederated()); + policyMap.put(QUEUE_PRIORITY_ADJUSTMENT, policy.getPriorityAjustment()); + + if (!policy.getIncludes().isEmpty()) { + final List flattenedIncludes = new ArrayList<>(policy.getIncludes().size() * 2); + policy.getIncludes().forEach((entry) -> { + flattenedIncludes.add(entry.getKey()); + flattenedIncludes.add(entry.getValue()); + }); + + policyMap.put(QUEUE_INCLUDES, flattenedIncludes); + } + + if (!policy.getExcludes().isEmpty()) { + final List flatteneExcludes = new ArrayList<>(policy.getExcludes().size() * 2); + policy.getExcludes().forEach((entry) -> { + flatteneExcludes.add(entry.getKey()); + flatteneExcludes.add(entry.getValue()); + }); + + policyMap.put(QUEUE_EXCLUDES, flatteneExcludes); + } + + final TransformerConfiguration transformerConfig = policy.getTransformerConfiguration(); + + if (transformerConfig != null) { + policyMap.put(TRANSFORMER_CLASS_NAME, transformerConfig.getClassName()); + if (transformerConfig.getProperties() != null && !transformerConfig.getProperties().isEmpty()) { + policyMap.put(TRANSFORMER_PROPERTIES_MAP, transformerConfig.getProperties()); + } + } + + if (!policy.getProperties().isEmpty()) { + policyMap.put(POLICY_PROPERTIES_MAP, policy.getProperties()); + } + + peer.remoteTransfer().withDeliveryId(0) + .withMessageAnnotations().withAnnotation(OPERATION_TYPE.toString(), ADD_QUEUE_POLICY) + .also() + .withBody().withValue(policyMap) + .also() + .now(); + } + + // Use this method to script the initial handshake that a broker that is establishing + // a federation connection with a remote broker instance would perform. + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName) { + scriptFederationConnectToRemote(peer, federationName, AmqpSupport.AMQP_CREDITS_DEFAULT, AmqpSupport.AMQP_LOW_CREDITS_DEFAULT); + } + + private void scriptFederationConnectToRemote(ProtonTestClient peer, String federationName, int amqpCredits, int amqpLowCredits) { + final String federationControlLinkName = "Federation:control:" + UUID.randomUUID().toString(); + + final Map federationConfiguration = new HashMap<>(); + federationConfiguration.put(RECEIVER_CREDITS, amqpCredits); + federationConfiguration.put(RECEIVER_CREDITS_LOW, amqpLowCredits); + + final Map senderProperties = new HashMap<>(); + senderProperties.put(FEDERATION_CONFIGURATION.toString(), federationConfiguration); + + peer.queueClientSaslAnonymousConnect(); + peer.remoteOpen().queue(); + peer.expectOpen(); + peer.remoteBegin().queue(); + peer.expectBegin(); + peer.remoteAttach().ofSender() + .withInitialDeliveryCount(0) + .withName(federationControlLinkName) + .withPropertiesMap(senderProperties) + .withDesiredCapabilities(FEDERATION_CONTROL_LINK.toString()) + .withSenderSettleModeUnsettled() + .withReceivervSettlesFirst() + .withSource().also() + .withTarget().withDynamic(true) + .withDurabilityOfNone() + .withExpiryPolicyOnLinkDetach() + .withLifetimePolicyOfDeleteOnClose() + .withCapabilities("temporary-topic") + .also() + .queue(); + peer.expectAttach().ofReceiver() + .withTarget() + .withAddress(notNullValue()) + .also() + .withOfferedCapability(FEDERATION_CONTROL_LINK.toString()); + peer.expectFlow(); + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationServerToServerTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationServerToServerTest.java new file mode 100644 index 0000000000..615c09d345 --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPFederationServerToServerTest.java @@ -0,0 +1,484 @@ +/* + * 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.artemis.tests.integration.amqp.connect; + +import java.lang.invoke.MethodHandles; +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.jms.Topic; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.api.core.RoutingType; +import org.apache.activemq.artemis.api.core.SimpleString; +import org.apache.activemq.artemis.core.config.DivertConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationAddressPolicyElement; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederationQueuePolicyElement; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.ComponentConfigurationRoutingType; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport; +import org.apache.activemq.artemis.tests.util.CFUtil; +import org.apache.activemq.artemis.utils.Wait; +import org.junit.After; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test AMQP federation between two servers. + */ +public class AMQPFederationServerToServerTest extends AmqpClientTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final int SERVER_PORT = AMQP_PORT; + private static final int SERVER_PORT_REMOTE = AMQP_PORT + 1; + + protected ActiveMQServer remoteServer; + + @Override + protected String getConfiguredProtocols() { + return "AMQP,CORE"; + } + + @Override + protected ActiveMQServer createServer() throws Exception { + remoteServer = createServer(SERVER_PORT_REMOTE, false); + + return createServer(SERVER_PORT, false); + } + + @After + @Override + public void tearDown() throws Exception { + super.tearDown(); + + try { + if (remoteServer != null) { + remoteServer.stop(); + } + } catch (Exception e) { + } + } + + @Test(timeout = 20000) + public void testAddresDemandOnLocalBrokerFederatesMessagesFromRemoteAMQP() throws Exception { + testAddresDemandOnLocalBrokerFederatesMessagesFromRemote("AMQP"); + } + + @Test(timeout = 20000) + public void testAddresDemandOnLocalBrokerFederatesMessagesFromRemoteCORE() throws Exception { + testAddresDemandOnLocalBrokerFederatesMessagesFromRemote("CORE"); + } + + private void testAddresDemandOnLocalBrokerFederatesMessagesFromRemote(String clientProtocol) throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement localAddressPolicy = new AMQPFederationAddressPolicyElement(); + localAddressPolicy.setName("test-policy"); + localAddressPolicy.addToIncludes("test"); + localAddressPolicy.setAutoDelete(false); + localAddressPolicy.setAutoDeleteDelay(-1L); + localAddressPolicy.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalAddressPolicy(localAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.start(); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic topic = sessionL.createTopic("test"); + + final MessageConsumer consumerL = sessionL.createConsumer(topic); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should trigger receiver on remote. + Wait.assertTrue(() -> server.addressQuery(SimpleString.toSimpleString("test")).isExists()); + Wait.assertTrue(() -> remoteServer.addressQuery(SimpleString.toSimpleString("test")).isExists()); + + final MessageProducer producerR = sessionR.createProducer(topic); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + producerR.send(message); + + final Message received = consumerL.receive(5_000); + assertNotNull(received); + } + } + + @Test(timeout = 20000) + public void testDivertAddressDemandOnLocalBrokerFederatesMessagesFromRemoteAMQP() throws Exception { + testDivertAddresDemandOnLocalBrokerFederatesMessagesFromRemote("AMQP"); + } + + @Test(timeout = 20000) + public void testDivertAddresDemandOnLocalBrokerFederatesMessagesFromRemoteCORE() throws Exception { + testDivertAddresDemandOnLocalBrokerFederatesMessagesFromRemote("CORE"); + } + + private void testDivertAddresDemandOnLocalBrokerFederatesMessagesFromRemote(String clientProtocol) throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement localAddressPolicy = new AMQPFederationAddressPolicyElement(); + localAddressPolicy.setName("test-policy"); + localAddressPolicy.addToIncludes("source"); + localAddressPolicy.setAutoDelete(false); + localAddressPolicy.setAutoDeleteDelay(-1L); + localAddressPolicy.setAutoDeleteMessageCount(-1L); + localAddressPolicy.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalAddressPolicy(localAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divert = new DivertConfiguration(); + divert.setName("test-divert"); + divert.setAddress("source"); + divert.setForwardingAddress("target"); + divert.setRoutingType(ComponentConfigurationRoutingType.MULTICAST); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.start(); + server.deployDivert(divert); + // Currently the address must exist on the local before we will federate from the remote + server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("source"), RoutingType.MULTICAST)); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic target = sessionL.createTopic("target"); + final Topic source = sessionL.createTopic("source"); + + final MessageConsumer consumerL = sessionL.createConsumer(target); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should trigger receiver on remote. + Wait.assertTrue(() -> remoteServer.addressQuery(SimpleString.toSimpleString("source")).isExists()); + + final MessageProducer producerR = sessionR.createProducer(source); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + producerR.send(message); + + final Message received = consumerL.receive(5_000); + assertNotNull(received); + } + } + + @Test(timeout = 20000) + public void testQueueDemandOnLocalBrokerFederatesMessagesFromRemoteAMQP() throws Exception { + testQueueDemandOnLocalBrokerFederatesMessagesFromRemote("AMQP"); + } + + @Test(timeout = 20000) + public void testQueueDemandOnLocalBrokerFederatesMessagesFromRemoteCORE() throws Exception { + testQueueDemandOnLocalBrokerFederatesMessagesFromRemote("CORE"); + } + + private void testQueueDemandOnLocalBrokerFederatesMessagesFromRemote(String clientProtocol) throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationQueuePolicyElement localQueuePolicy = new AMQPFederationQueuePolicyElement(); + localQueuePolicy.setName("test-policy"); + localQueuePolicy.addToIncludes("#", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addLocalQueuePolicy(localQueuePolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-queue-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + remoteServer.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + server.start(); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Queue queue = sessionL.createQueue("test"); + + final MessageConsumer consumerL = sessionL.createConsumer(queue); + + connectionL.start(); + connectionR.start(); + + // Demand on local queue should trigger receiver on remote. + Wait.assertTrue(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists()); + + final MessageProducer producerR = sessionR.createProducer(queue); + final TextMessage message = sessionR.createTextMessage("Hello World"); + + producerR.send(message); + + final Message received = consumerL.receive(5_000); + assertNotNull(received); + } + } + + @Test(timeout = 20000) + public void testAddresDemandOnRemoteBrokerFederatesMessagesFromLocalAMQP() throws Exception { + testAddresDemandOnRemoteBrokerFederatesMessagesFromLocal("AMQP"); + } + + @Test(timeout = 20000) + public void testAddresDemandOnRemoteBrokerFederatesMessagesFromLocalCORE() throws Exception { + testAddresDemandOnRemoteBrokerFederatesMessagesFromLocal("CORE"); + } + + private void testAddresDemandOnRemoteBrokerFederatesMessagesFromLocal(String clientProtocol) throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement remoteAddressPolicy = new AMQPFederationAddressPolicyElement(); + remoteAddressPolicy.setName("test-policy"); + remoteAddressPolicy.addToIncludes("test"); + remoteAddressPolicy.setAutoDelete(false); + remoteAddressPolicy.setAutoDeleteDelay(-1L); + remoteAddressPolicy.setAutoDeleteMessageCount(-1L); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addRemoteAddressPolicy(remoteAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.start(); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic topic = sessionL.createTopic("test"); + + final MessageConsumer consumerR = sessionR.createConsumer(topic); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should trigger receiver on remote. + Wait.assertTrue(() -> server.addressQuery(SimpleString.toSimpleString("test")).isExists()); + Wait.assertTrue(() -> remoteServer.addressQuery(SimpleString.toSimpleString("test")).isExists()); + + final MessageProducer producerL = sessionL.createProducer(topic); + final TextMessage message = sessionL.createTextMessage("Hello World"); + + producerL.send(message); + + final Message received = consumerR.receive(5_000); + assertNotNull(received); + } + } + + @Test(timeout = 20000) + public void testQueueDemandOnRemoteWithRemoteConfigrationLeadsToMessageBeingFederatedAMQP() throws Exception { + testQueueDemandOnRemoteWithRemoteConfigrationLeadsToMessageBeingFederated("AMQP"); + } + + @Test(timeout = 20000) + public void testQueueDemandOnRemoteWithRemoteConfigrationLeadsToMessageBeingFederatedCORE() throws Exception { + testQueueDemandOnRemoteWithRemoteConfigrationLeadsToMessageBeingFederated("CORE"); + } + + public void testQueueDemandOnRemoteWithRemoteConfigrationLeadsToMessageBeingFederated(String clientProtocol) throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationQueuePolicyElement remoteQueuePolicy = new AMQPFederationQueuePolicyElement(); + remoteQueuePolicy.setName("test-policy"); + remoteQueuePolicy.addToIncludes("#", "test"); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addRemoteQueuePolicy(remoteQueuePolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-queue-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + server.getConfiguration().addAMQPConnection(amqpConnection); + remoteServer.start(); + server.start(); + server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST) + .setAddress("test") + .setAutoCreated(false)); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Queue queue = sessionL.createQueue("test"); + + final MessageConsumer consumerR = sessionR.createConsumer(queue); + + connectionL.start(); + connectionR.start(); + + // Demand on remote queue should trigger receiver on remote. + Wait.assertTrue(() -> remoteServer.queueQuery(SimpleString.toSimpleString("test")).isExists()); + + final MessageProducer producerL = sessionL.createProducer(queue); + final TextMessage message = sessionL.createTextMessage("Hello World"); + + producerL.send(message); + + final Message received = consumerR.receive(5_000); + assertNotNull(received); + } + } + + @Test(timeout = 20000) + public void testDivertAddresDemandOnRemoteBrokerFederatesMessagesFromLocalAMQP() throws Exception { + testDivertAddresDemandOnRemoteBrokerFederatesMessagesFromLocal("AMQP"); + } + + @Test(timeout = 20000) + public void testDivertAddresDemandOnRemoteBrokerFederatesMessagesFromLocalCORE() throws Exception { + testDivertAddresDemandOnRemoteBrokerFederatesMessagesFromLocal("CORE"); + } + + private void testDivertAddresDemandOnRemoteBrokerFederatesMessagesFromLocal(String clientProtocol) throws Exception { + logger.info("Test started: {}", getTestName()); + + final AMQPFederationAddressPolicyElement remoteAddressPolicy = new AMQPFederationAddressPolicyElement(); + remoteAddressPolicy.setName("test-policy"); + remoteAddressPolicy.addToIncludes("source"); + remoteAddressPolicy.setAutoDelete(false); + remoteAddressPolicy.setAutoDeleteDelay(-1L); + remoteAddressPolicy.setAutoDeleteMessageCount(-1L); + remoteAddressPolicy.setEnableDivertBindings(true); + + final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement(); + element.setName("test"); + element.addRemoteAddressPolicy(remoteAddressPolicy); + + final AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE); + amqpConnection.setReconnectAttempts(10);// Limit reconnects + amqpConnection.addElement(element); + + final DivertConfiguration divert = new DivertConfiguration(); + divert.setName("test-divert"); + divert.setAddress("source"); + divert.setForwardingAddress("target"); + divert.setRoutingType(ComponentConfigurationRoutingType.MULTICAST); + + remoteServer.start(); + remoteServer.deployDivert(divert); + // Currently the address must exist on the local before we will federate from the remote + // and in this case since we are instructing the remote to federate from us the address must + // exist on the remote for that to happen. + remoteServer.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("source"), RoutingType.MULTICAST)); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + final ConnectionFactory factoryLocal = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT); + final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE); + + try (Connection connectionL = factoryLocal.createConnection(); + Connection connectionR = factoryRemote.createConnection()) { + + final Session sessionL = connectionL.createSession(Session.AUTO_ACKNOWLEDGE); + final Session sessionR = connectionR.createSession(Session.AUTO_ACKNOWLEDGE); + + final Topic target = sessionL.createTopic("target"); + final Topic source = sessionL.createTopic("source"); + + final MessageConsumer consumerR = sessionR.createConsumer(target); + + connectionL.start(); + connectionR.start(); + + // Demand on local address should trigger receiver on remote. + Wait.assertTrue(() -> server.addressQuery(SimpleString.toSimpleString("source")).isExists()); + + final MessageProducer producerL = sessionL.createProducer(source); + final TextMessage message = sessionL.createTextMessage("Hello World"); + + producerL.send(message); + + final Message received = consumerR.receive(5_000); + assertNotNull(received); + } + } +} diff --git a/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPMirrorConnectionTest.java b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPMirrorConnectionTest.java new file mode 100644 index 0000000000..770c40db2e --- /dev/null +++ b/tests/integration-tests/src/test/java/org/apache/activemq/artemis/tests/integration/amqp/connect/AMQPMirrorConnectionTest.java @@ -0,0 +1,266 @@ +/* + * 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.artemis.tests.integration.amqp.connect; + +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.MessageConsumer; +import javax.jms.Session; +import javax.jms.Topic; + +import org.apache.activemq.artemis.api.core.QueueConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; +import org.apache.activemq.artemis.core.server.ActiveMQServer; +import org.apache.activemq.artemis.core.server.impl.AddressInfo; +import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource; +import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport; +import org.apache.activemq.artemis.tests.util.CFUtil; +import org.apache.qpid.protonj2.test.driver.ProtonTestServer; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test some basic expected behaviors of the broker mirror connection. + */ +public class AMQPMirrorConnectionTest extends AmqpClientTestSupport { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private static final int BROKER_PORT_NUM = AMQP_PORT + 1; + + @Override + protected ActiveMQServer createServer() throws Exception { + // Creates the broker used to make the outgoing connection. The port passed is for + // that brokers acceptor. The test server connected to by the broker binds to a random port. + return createServer(BROKER_PORT_NUM, false); + } + + @Test(timeout = 20000) + public void testBrokerMirrorConnectsWithAnonymous() throws Exception { + final Map brokerProperties = new HashMap<>(); + brokerProperties.put(AMQPMirrorControllerSource.BROKER_ID.toString(), "Test-Broker"); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR")) + .withDesiredCapabilities("amq.mirror") + .respond() + .withOfferedCapabilities("amq.mirror") + .withPropertiesMap(brokerProperties); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + // No user or pass given, it will have to select ANONYMOUS even though PLAIN also offered + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(new AMQPMirrorBrokerConnectionElement()); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + server.stop(); + + // should be no more interactions + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testBrokerMirrorConnectsWithPlain() throws Exception { + final Map brokerProperties = new HashMap<>(); + brokerProperties.put(AMQPMirrorControllerSource.BROKER_ID.toString(), "Test-Broker"); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLPlainConnect("user", "pass", "PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR")) + .withDesiredCapabilities("amq.mirror") + .respond() + .withOfferedCapabilities("amq.mirror") + .withPropertiesMap(brokerProperties); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.setUser("user"); + amqpConnection.setPassword("pass"); + amqpConnection.addElement(new AMQPMirrorBrokerConnectionElement()); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + server.stop(); + + // should be no more interactions + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testBrokerHandlesSenderLinkOmitsMirrorCapability() throws Exception { + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR")) + .withDesiredCapabilities("amq.mirror") + .respond(); + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + // No user or pass given, it will have to select ANONYMOUS even though PLAIN also offered + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.addElement(new AMQPMirrorBrokerConnectionElement()); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + server.stop(); + + // should be no more interactions + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testBrokerAddsAddressAndQueue() throws Exception { + final Map brokerProperties = new HashMap<>(); + brokerProperties.put(AMQPMirrorControllerSource.BROKER_ID.toString(), "Test-Broker"); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLPlainConnect("user", "pass", "PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR")) + .withDesiredCapabilities("amq.mirror") + .respond() + .withOfferedCapabilities("amq.mirror") + .withPropertiesMap(brokerProperties); + peer.remoteFlow().withLinkCredit(10).queue(); + peer.expectTransfer().accept(); // Address create + peer.expectTransfer().accept(); // Queue create + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.setUser("user"); + amqpConnection.setPassword("pass"); + amqpConnection.addElement(new AMQPMirrorBrokerConnectionElement().setQueueCreation(true) + .setAddressFilter("sometest")); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + server.addAddressInfo(new AddressInfo("sometest").setAutoCreated(false)); + server.createQueue(new QueueConfiguration("sometest").setDurable(true)); + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + server.stop(); + + // should be no more interactions + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } + + @Test(timeout = 20000) + public void testCreateDurableConsumerReplicatesAddressAndQueue() throws Exception { + final Map brokerProperties = new HashMap<>(); + brokerProperties.put(AMQPMirrorControllerSource.BROKER_ID.toString(), "Test-Broker"); + + try (ProtonTestServer peer = new ProtonTestServer()) { + peer.expectSASLPlainConnect("user", "pass", "PLAIN", "ANONYMOUS"); + peer.expectOpen().respond(); + peer.expectBegin().respond(); + peer.expectAttach().ofSender() + .withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR")) + .withDesiredCapabilities("amq.mirror") + .respond() + .withOfferedCapabilities("amq.mirror") + .withPropertiesMap(brokerProperties); + peer.remoteFlow().withLinkCredit(10).queue(); + peer.expectTransfer().accept(); // Notification address create + peer.expectTransfer().accept(); // Address create + peer.expectTransfer().accept(); // Queue create + peer.start(); + + final URI remoteURI = peer.getServerURI(); + logger.info("Connect test started, peer listening on: {}", remoteURI); + + AMQPBrokerConnectConfiguration amqpConnection = + new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort()); + amqpConnection.setReconnectAttempts(0);// No reconnects + amqpConnection.setUser("user"); + amqpConnection.setPassword("pass"); + amqpConnection.addElement(new AMQPMirrorBrokerConnectionElement().setQueueCreation(true)); + server.getConfiguration().addAMQPConnection(amqpConnection); + server.start(); + + final ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + BROKER_PORT_NUM); + + try (Connection connection = factory.createConnection()) { + connection.setClientID("test-client-id"); + Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); + Topic topic = session.createTopic("test-topic"); + MessageConsumer consumer = session.createDurableConsumer(topic, "subscription"); + + consumer.close(); + } + + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + + server.stop(); + + // should be no more interactions + peer.waitForScriptToComplete(5, TimeUnit.SECONDS); + } + } +} diff --git a/tests/smoke-tests/pom.xml b/tests/smoke-tests/pom.xml index 5273038878..9a7c4e6887 100644 --- a/tests/smoke-tests/pom.xml +++ b/tests/smoke-tests/pom.xml @@ -1056,6 +1056,41 @@ + + + test-compile + createBrokerConnectFederationA + + create + + + amq + true + A + A + true + ${basedir}/target/brokerConnect/federationA + ${basedir}/target/classes/servers/brokerConnect/federationA + + + + test-compile + createBrokerConnectFederationB + + create + + + amq + true + B + B + true + 1 + ${basedir}/target/brokerConnect/federationB + ${basedir}/target/classes/servers/brokerConnect/federationB + + + test-compile diff --git a/tests/smoke-tests/src/main/resources/servers/brokerConnect/federationA/broker.xml b/tests/smoke-tests/src/main/resources/servers/brokerConnect/federationA/broker.xml new file mode 100644 index 0000000000..b829a0e74d --- /dev/null +++ b/tests/smoke-tests/src/main/resources/servers/brokerConnect/federationA/broker.xml @@ -0,0 +1,195 @@ + + + + + + + + 0.0.0.0 + + true + + + NIO + + data/paging + + data/bindings + + data/journal + + data/large-messages + + true + + 2 + + 10 + + 4096 + + 10M + + + 40000 + + + 1 + + + + + + + + + + + + + + + + + + 5000 + + + 90 + + + false + + 120000 + + 60000 + + HALT + + 40000 + + + tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
diff --git a/tests/smoke-tests/src/main/resources/servers/brokerConnect/federationB/broker.xml b/tests/smoke-tests/src/main/resources/servers/brokerConnect/federationB/broker.xml new file mode 100644 index 0000000000..2a9be9dcd2 --- /dev/null +++ b/tests/smoke-tests/src/main/resources/servers/brokerConnect/federationB/broker.xml @@ -0,0 +1,196 @@ + + + + + + + + 0.0.0.0 + + true + + + NIO + + data/paging + + data/bindings + + data/journal + + data/large-messages + + true + + 2 + + 10 + + 4096 + + 10M + + + 40000 + + + 1 + + + + + + + + + + + + + + + + + + 5000 + + + 90 + + + false + + 120000 + + 60000 + + HALT + + 40000 + + + + + + + + + + + + tcp://0.0.0.0:61617?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true + + + + + + + + + + + + + + + + + + + + + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + DLQ + ExpiryQueue + 0 + + -1 + 10 + PAGE + true + true + + + + +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
diff --git a/tests/smoke-tests/src/test/java/org/apache/activemq/artemis/tests/smoke/brokerConnection/DualFederationTest.java b/tests/smoke-tests/src/test/java/org/apache/activemq/artemis/tests/smoke/brokerConnection/DualFederationTest.java new file mode 100644 index 0000000000..f6d4b30a88 --- /dev/null +++ b/tests/smoke-tests/src/test/java/org/apache/activemq/artemis/tests/smoke/brokerConnection/DualFederationTest.java @@ -0,0 +1,117 @@ +/* + * 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.artemis.tests.smoke.brokerConnection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; + +import org.apache.activemq.artemis.tests.smoke.common.SmokeTestBase; +import org.apache.activemq.artemis.tests.util.CFUtil; +import org.apache.activemq.artemis.util.ServerUtil; +import org.junit.Before; +import org.junit.Test; + +public class DualFederationTest extends SmokeTestBase { + + public static final String SERVER_NAME_A = "brokerConnect/federationA"; + public static final String SERVER_NAME_B = "brokerConnect/federationB"; + + Process processB; + Process processA; + + @Before + public void beforeClass() throws Exception { + cleanupData(SERVER_NAME_A); + cleanupData(SERVER_NAME_B); + processB = startServer(SERVER_NAME_B, 0, 0); + processA = startServer(SERVER_NAME_A, 0, 0); + + ServerUtil.waitForServerToStart(1, "B", "B", 30000); + ServerUtil.waitForServerToStart(0, "A", "A", 30000); + } + + @Test + public void testFederatedBrokersAddressFederatedFromBtoA() throws Throwable { + ConnectionFactory cfA = CFUtil.createConnectionFactory("amqp", "tcp://localhost:61616"); + ConnectionFactory cfB = CFUtil.createConnectionFactory("amqp", "tcp://localhost:61617"); + + try (Connection connectionA = cfA.createConnection("A", "A"); + Connection connectionB = cfB.createConnection("B", "B")) { + + Session sessionA = connectionA.createSession(Session.AUTO_ACKNOWLEDGE); + Session sessionB = connectionB.createSession(Session.AUTO_ACKNOWLEDGE); + + Topic topic = sessionA.createTopic("toA"); + + connectionA.start(); + connectionB.start(); + + // Create local demand on A for address 'toA' which should result in a receiver + // being created to B on address 'toA' + MessageConsumer consumerA = sessionA.createConsumer(topic); + // Then a producer on broker B to address A should route to broker A + MessageProducer producerB = sessionB.createProducer(topic); + + Thread.sleep(5_000); // Time for federation resources to build + + final Message message = sessionB.createTextMessage("message from broker B"); + producerB.send(message); + + final Message received = consumerA.receive(1_000); + assertNotNull(received); + } + } + + @Test + public void testFederatedBrokersQueueFederatedFromAtoB() throws Throwable { + ConnectionFactory cfA = CFUtil.createConnectionFactory("amqp", "tcp://localhost:61616"); + ConnectionFactory cfB = CFUtil.createConnectionFactory("amqp", "tcp://localhost:61617"); + + try (Connection connectionA = cfA.createConnection("A", "A"); + Connection connectionB = cfB.createConnection("B", "B")) { + + Session sessionA = connectionA.createSession(Session.AUTO_ACKNOWLEDGE); + Session sessionB = connectionB.createSession(Session.AUTO_ACKNOWLEDGE); + + Queue queue = sessionA.createQueue("toB"); + + connectionA.start(); + connectionB.start(); + + // Create local demand on B for queue 'toB' which should result in a receiver + // being created to A on queue 'toB' + MessageConsumer consumerB = sessionB.createConsumer(queue); + // Then a producer on broker A to queue toB should route to broker B + MessageProducer producerA = sessionA.createProducer(queue); + + Thread.sleep(5_000); // Time for federation resources to build + + final Message message = sessionA.createTextMessage("message from broker A"); + producerA.send(message); + + final Message received = consumerB.receive(1_000); + assertNotNull(received); + } + } +} diff --git a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/config/impl/ConfigurationValidationTest.java b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/config/impl/ConfigurationValidationTest.java index 8b332fdf02..d22e9ccd0d 100644 --- a/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/config/impl/ConfigurationValidationTest.java +++ b/tests/unit-tests/src/test/java/org/apache/activemq/artemis/tests/unit/core/config/impl/ConfigurationValidationTest.java @@ -17,8 +17,10 @@ package org.apache.activemq.artemis.tests.unit.core.config.impl; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration; + import org.apache.activemq.artemis.core.config.FileDeploymentManager; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType; +import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFederatedBrokerConnectionElement; import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement; import org.apache.activemq.artemis.core.config.impl.FileConfiguration; import org.apache.activemq.artemis.tests.util.ActiveMQTestBase; @@ -36,8 +38,6 @@ public class ConfigurationValidationTest extends ActiveMQTestBase { System.setProperty("ninetyTwoProp", "92"); } - - /** * test does not pass in eclipse (because it can not find artemis-configuration.xsd). * It runs fine on the CLI with the proper env setting. @@ -67,7 +67,7 @@ public class ConfigurationValidationTest extends ActiveMQTestBase { deploymentManager.addDeployable(fc); deploymentManager.readConfiguration(); - Assert.assertEquals(3, fc.getAMQPConnection().size()); + Assert.assertEquals(4, fc.getAMQPConnection().size()); AMQPBrokerConnectConfiguration amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(0); Assert.assertEquals("testuser", amqpBrokerConnectConfiguration.getUser()); @@ -83,7 +83,6 @@ public class ConfigurationValidationTest extends ActiveMQTestBase { Assert.assertEquals(AMQPBrokerConnectionAddressType.RECEIVER, amqpBrokerConnectConfiguration.getConnectionElements().get(1).getType()); Assert.assertEquals("TEST-PEER", amqpBrokerConnectConfiguration.getConnectionElements().get(2).getMatchAddress().toString()); Assert.assertEquals(AMQPBrokerConnectionAddressType.PEER, amqpBrokerConnectConfiguration.getConnectionElements().get(2).getType()); - Assert.assertEquals("TEST-WITH-QUEUE-NAME", amqpBrokerConnectConfiguration.getConnectionElements().get(3).getQueueName().toString()); Assert.assertEquals(null, amqpBrokerConnectConfiguration.getConnectionElements().get(3).getMatchAddress()); Assert.assertEquals(AMQPBrokerConnectionAddressType.RECEIVER, amqpBrokerConnectConfiguration.getConnectionElements().get(3).getType()); @@ -96,10 +95,22 @@ public class ConfigurationValidationTest extends ActiveMQTestBase { Assert.assertFalse(mirrorConnectionElement.isDurable()); Assert.assertTrue(mirrorConnectionElement.isSync()); + Assert.assertEquals(AMQPBrokerConnectionAddressType.FEDERATION, amqpBrokerConnectConfiguration.getConnectionElements().get(5).getType()); + AMQPFederatedBrokerConnectionElement federationConnectionElement = (AMQPFederatedBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(5); + Assert.assertEquals("test1", federationConnectionElement.getName()); + Assert.assertEquals(1, federationConnectionElement.getLocalQueuePolicies().size()); + federationConnectionElement.getLocalQueuePolicies().forEach((p) -> { + Assert.assertEquals("composite", p.getName()); + Assert.assertEquals(1, p.getIncludes().size()); + Assert.assertEquals(0, p.getExcludes().size()); + Assert.assertNull(p.getTransformerConfiguration()); + }); amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(1); - Assert.assertEquals(null, amqpBrokerConnectConfiguration.getUser()); mirrorConnectionElement = (AMQPMirrorBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(0); - Assert.assertEquals(null, amqpBrokerConnectConfiguration.getPassword()); Assert.assertEquals("test2", amqpBrokerConnectConfiguration.getName()); + Assert.assertEquals(null, amqpBrokerConnectConfiguration.getUser()); + mirrorConnectionElement = (AMQPMirrorBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(0); + Assert.assertEquals(null, amqpBrokerConnectConfiguration.getPassword()); + Assert.assertEquals("test2", amqpBrokerConnectConfiguration.getName()); Assert.assertEquals("tcp://test2:222", amqpBrokerConnectConfiguration.getUri()); Assert.assertTrue(mirrorConnectionElement.isMessageAcknowledgements()); Assert.assertFalse(mirrorConnectionElement.isDurable()); @@ -109,6 +120,84 @@ public class ConfigurationValidationTest extends ActiveMQTestBase { amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(2); Assert.assertFalse(amqpBrokerConnectConfiguration.isAutostart()); + + amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(3); + Assert.assertFalse(amqpBrokerConnectConfiguration.isAutostart()); + AMQPFederatedBrokerConnectionElement federationElement = (AMQPFederatedBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(0); + Assert.assertEquals(1, federationElement.getLocalAddressPolicies().size()); + Assert.assertEquals(2, federationElement.getLocalQueuePolicies().size()); + Assert.assertEquals(1, federationElement.getRemoteAddressPolicies().size()); + Assert.assertEquals(1, federationElement.getRemoteQueuePolicies().size()); + Assert.assertTrue(federationElement.getProperties().containsKey("amqpCredits")); + Assert.assertEquals("7", federationElement.getProperties().get("amqpCredits")); + Assert.assertTrue(federationElement.getProperties().containsKey("amqpLowCredits")); + Assert.assertEquals("1", federationElement.getProperties().get("amqpLowCredits")); + + federationElement.getLocalAddressPolicies().forEach(p -> { + Assert.assertEquals("lap1", p.getName()); + Assert.assertEquals(1, p.getIncludes().size()); + p.getIncludes().forEach(match -> Assert.assertEquals("orders", match.getAddressMatch())); + Assert.assertEquals(1, p.getExcludes().size()); + p.getExcludes().forEach(match -> Assert.assertEquals("all.#", match.getAddressMatch())); + Assert.assertFalse(p.getAutoDelete()); + Assert.assertEquals(1L, (long) p.getAutoDeleteDelay()); + Assert.assertEquals(12L, (long) p.getAutoDeleteMessageCount()); + Assert.assertEquals(2, (int) p.getMaxHops()); + Assert.assertNotNull(p.getTransformerConfiguration()); + Assert.assertEquals("class-name", p.getTransformerConfiguration().getClassName()); + }); + federationElement.getLocalQueuePolicies().forEach((p) -> { + if (p.getName().endsWith("lqp1")) { + Assert.assertEquals(1, (int) p.getPriorityAdjustment()); + Assert.assertFalse(p.isIncludeFederated()); + Assert.assertEquals("class-another", p.getTransformerConfiguration().getClassName()); + Assert.assertNotNull(p.getProperties()); + Assert.assertFalse(p.getProperties().isEmpty()); + Assert.assertTrue(p.getProperties().containsKey("amqpCredits")); + Assert.assertEquals("1", p.getProperties().get("amqpCredits")); + } else if (p.getName().endsWith("lqp2")) { + Assert.assertNull(p.getPriorityAdjustment()); + Assert.assertFalse(p.isIncludeFederated()); + } else { + Assert.fail("Should only be two local queue policies"); + } + }); + federationElement.getRemoteAddressPolicies().forEach((p) -> { + Assert.assertEquals("rap1", p.getName()); + Assert.assertEquals(1, p.getIncludes().size()); + p.getIncludes().forEach(match -> Assert.assertEquals("support", match.getAddressMatch())); + Assert.assertEquals(0, p.getExcludes().size()); + Assert.assertTrue(p.getAutoDelete()); + Assert.assertEquals(2L, (long) p.getAutoDeleteDelay()); + Assert.assertEquals(42L, (long) p.getAutoDeleteMessageCount()); + Assert.assertEquals(1, (int) p.getMaxHops()); + Assert.assertNotNull(p.getTransformerConfiguration()); + Assert.assertEquals("something", p.getTransformerConfiguration().getClassName()); + Assert.assertEquals(2, p.getTransformerConfiguration().getProperties().size()); + Assert.assertEquals("value1", p.getTransformerConfiguration().getProperties().get("key1")); + Assert.assertEquals("value2", p.getTransformerConfiguration().getProperties().get("key2")); + Assert.assertNotNull(p.getProperties()); + Assert.assertFalse(p.getProperties().isEmpty()); + Assert.assertTrue(p.getProperties().containsKey("amqpCredits")); + Assert.assertEquals("2", p.getProperties().get("amqpCredits")); + Assert.assertTrue(p.getProperties().containsKey("amqpLowCredits")); + Assert.assertEquals("1", p.getProperties().get("amqpLowCredits")); + }); + federationElement.getRemoteQueuePolicies().forEach((p) -> { + Assert.assertEquals("rqp1", p.getName()); + Assert.assertEquals(-1, (int) p.getPriorityAdjustment()); + Assert.assertTrue(p.isIncludeFederated()); + p.getIncludes().forEach(match -> { + Assert.assertEquals("#", match.getAddressMatch()); + Assert.assertEquals("tracking", match.getQueueMatch()); + }); + Assert.assertNotNull(p.getProperties()); + Assert.assertFalse(p.getProperties().isEmpty()); + Assert.assertTrue(p.getProperties().containsKey("amqpCredits")); + Assert.assertEquals("2", p.getProperties().get("amqpCredits")); + Assert.assertTrue(p.getProperties().containsKey("amqpLowCredits")); + Assert.assertEquals("1", p.getProperties().get("amqpLowCredits")); + }); } @Test