ARTEMIS-4419 Add federation support to AMQP broker connections
Allows federation of addresses and queues over an outbound AMQP broker connection and provide configuration via XML or broker propeties.
This commit is contained in:
parent
f860be432e
commit
d830f04de8
|
@ -73,8 +73,7 @@ public class FederationConfiguration implements Serializable {
|
||||||
return federationPolicyMap;
|
return federationPolicyMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
// strange spelling!, it allows a type match for singular of correct plural Policies from properties
|
public FederationConfiguration addQueuePolicy(FederationQueuePolicyConfiguration federationPolicy) {
|
||||||
public FederationConfiguration addQueuePolicie(FederationQueuePolicyConfiguration federationPolicy) {
|
|
||||||
federationPolicyMap.put(federationPolicy.getName(), federationPolicy);
|
federationPolicyMap.put(federationPolicy.getName(), federationPolicy);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -83,7 +82,7 @@ public class FederationConfiguration implements Serializable {
|
||||||
return federationPolicyMap;
|
return federationPolicyMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FederationConfiguration addAddressPolicie(FederationAddressPolicyConfiguration federationPolicy) {
|
public FederationConfiguration addAddressPolicy(FederationAddressPolicyConfiguration federationPolicy) {
|
||||||
federationPolicyMap.put(federationPolicy.getName(), federationPolicy);
|
federationPolicyMap.put(federationPolicy.getName(), federationPolicy);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,10 @@ public class AMQPSessionCallback implements SessionCallback {
|
||||||
return coreMessageObjectPools;
|
return coreMessageObjectPools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AMQPSessionContext getAMQPSessionContext() {
|
||||||
|
return protonSession;
|
||||||
|
}
|
||||||
|
|
||||||
public ProtonProtocolManager getProtocolManager() {
|
public ProtonProtocolManager getProtocolManager() {
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
|
@ -154,10 +158,8 @@ public class AMQPSessionCallback implements SessionCallback {
|
||||||
logger.warn(e.getMessage(), e);
|
logger.warn(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void withinContext(Runnable run) throws Exception {
|
public void withinContext(Runnable run) throws Exception {
|
||||||
OperationContext context = recoverContext();
|
OperationContext context = recoverContext();
|
||||||
try {
|
try {
|
||||||
|
@ -305,33 +307,38 @@ public class AMQPSessionCallback implements SessionCallback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueueQueryResult queueQuery(SimpleString queueName, RoutingType routingType, boolean autoCreate) throws Exception {
|
public QueueQueryResult queueQuery(QueueConfiguration configuration, boolean autoCreate) throws Exception {
|
||||||
return queueQuery(queueName, routingType, autoCreate, null);
|
QueueQueryResult queueQueryResult = serverSession.executeQueueQuery(configuration.getName());
|
||||||
}
|
|
||||||
|
|
||||||
public QueueQueryResult queueQuery(SimpleString queueName, RoutingType routingType, boolean autoCreate, SimpleString filter) throws Exception {
|
|
||||||
QueueQueryResult queueQueryResult = serverSession.executeQueueQuery(queueName);
|
|
||||||
|
|
||||||
if (!queueQueryResult.isExists() && queueQueryResult.isAutoCreateQueues() && autoCreate) {
|
if (!queueQueryResult.isExists() && queueQueryResult.isAutoCreateQueues() && autoCreate) {
|
||||||
try {
|
try {
|
||||||
serverSession.createQueue(new QueueConfiguration(queueName).setRoutingType(routingType).setFilterString(filter).setAutoCreated(true));
|
serverSession.createQueue(configuration.setAutoCreated(true));
|
||||||
} catch (ActiveMQQueueExistsException e) {
|
} catch (ActiveMQQueueExistsException e) {
|
||||||
// The queue may have been created by another thread in the mean time. Catch and do nothing.
|
// 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 auto-create we will return whatever type was used before
|
||||||
if (queueQueryResult.isExists() && !queueQueryResult.isAutoCreated()) {
|
if (queueQueryResult.isExists() && !queueQueryResult.isAutoCreated()) {
|
||||||
//if routingType is null we bypass the check
|
final RoutingType desiredRoutingType = configuration.getRoutingType();
|
||||||
if (routingType != null && queueQueryResult.getRoutingType() != routingType) {
|
if (desiredRoutingType != null && queueQueryResult.getRoutingType() != desiredRoutingType) {
|
||||||
throw new IllegalStateException("Incorrect Routing Type for queue " + queueName + ", expecting: " + routingType + " while it had " + queueQueryResult.getRoutingType());
|
throw new IllegalStateException("Incorrect Routing Type for queried queue " + configuration.getName() +
|
||||||
|
", expecting: " + desiredRoutingType + " while the actual type was: " +
|
||||||
|
queueQueryResult.getRoutingType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return queueQueryResult;
|
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 {
|
public boolean checkAddressAndAutocreateIfPossible(SimpleString address, RoutingType routingType) throws Exception {
|
||||||
AutoCreateResult autoCreateResult = serverSession.checkAutoCreate(new QueueConfiguration(address).setRoutingType(routingType));
|
AutoCreateResult autoCreateResult = serverSession.checkAutoCreate(new QueueConfiguration(address).setRoutingType(routingType));
|
||||||
|
@ -447,7 +454,7 @@ public class AMQPSessionCallback implements SessionCallback {
|
||||||
Delivery delivery,
|
Delivery delivery,
|
||||||
SimpleString address,
|
SimpleString address,
|
||||||
RoutingContext routingContext,
|
RoutingContext routingContext,
|
||||||
AMQPMessage message) throws Exception {
|
Message message) throws Exception {
|
||||||
|
|
||||||
context.incrementSettle();
|
context.incrementSettle();
|
||||||
|
|
||||||
|
@ -711,6 +718,24 @@ public class AMQPSessionCallback implements SessionCallback {
|
||||||
return null;
|
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 {
|
public Transaction getTransaction(Binary txid, boolean remove) throws ActiveMQAMQPException {
|
||||||
return protonSPI.getTransaction(txid, remove);
|
return protonSPI.getTransaction(txid, remove);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,15 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.apache.activemq.artemis.api.core.ActiveMQException;
|
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.AMQPBrokerConnectConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType;
|
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.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.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement;
|
||||||
import org.apache.activemq.artemis.core.postoffice.Binding;
|
import org.apache.activemq.artemis.core.postoffice.Binding;
|
||||||
import org.apache.activemq.artemis.core.postoffice.QueueBinding;
|
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.AMQPSessionCallback;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.broker.ActiveMQProtonRemotingConnection;
|
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.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.AMQPMirrorControllerAggregation;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource;
|
import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolLogger;
|
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.apache.qpid.proton.engine.Session;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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;
|
import java.lang.invoke.MethodHandles;
|
||||||
|
|
||||||
public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, ActiveMQServerQueuePlugin, BrokerConnection {
|
public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener, ActiveMQServerQueuePlugin, BrokerConnection {
|
||||||
|
@ -103,12 +115,14 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
private volatile boolean started = false;
|
private volatile boolean started = false;
|
||||||
private final AMQPBrokerConnectionManager bridgeManager;
|
private final AMQPBrokerConnectionManager bridgeManager;
|
||||||
private AMQPMirrorControllerSource mirrorControllerSource;
|
private AMQPMirrorControllerSource mirrorControllerSource;
|
||||||
|
private AMQPFederationSource brokerFederation;
|
||||||
private int retryCounter = 0;
|
private int retryCounter = 0;
|
||||||
private int lastRetryCounter;
|
private int lastRetryCounter;
|
||||||
private boolean connecting = false;
|
private boolean connecting = false;
|
||||||
private volatile ScheduledFuture reconnectFuture;
|
private volatile ScheduledFuture<?> reconnectFuture;
|
||||||
private final Set<Queue> senders = new HashSet<>();
|
private final Set<Queue> senders = new HashSet<>();
|
||||||
private final Set<Queue> receivers = new HashSet<>();
|
private final Set<Queue> receivers = new HashSet<>();
|
||||||
|
private final Map<String, Predicate<Link>> linkClosedInterceptors = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
final Executor connectExecutor;
|
final Executor connectExecutor;
|
||||||
final ScheduledExecutorService scheduledExecutorService;
|
final ScheduledExecutorService scheduledExecutorService;
|
||||||
|
@ -153,6 +167,10 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
return connecting;
|
return connecting;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getConnectionTimeout() {
|
||||||
|
return bridgesConnector.getConnectTimeoutMillis();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
if (!started) return;
|
if (!started) return;
|
||||||
|
@ -162,11 +180,17 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
protonRemotingConnection = null;
|
protonRemotingConnection = null;
|
||||||
connection = null;
|
connection = null;
|
||||||
}
|
}
|
||||||
ScheduledFuture scheduledFuture = reconnectFuture;
|
ScheduledFuture<?> scheduledFuture = reconnectFuture;
|
||||||
reconnectFuture = null;
|
reconnectFuture = null;
|
||||||
if (scheduledFuture != null) {
|
if (scheduledFuture != null) {
|
||||||
scheduledFuture.cancel(true);
|
scheduledFuture.cancel(true);
|
||||||
}
|
}
|
||||||
|
if (brokerFederation != null) {
|
||||||
|
try {
|
||||||
|
brokerFederation.stop();
|
||||||
|
} catch (ActiveMQException e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -175,11 +199,14 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
started = true;
|
started = true;
|
||||||
server.getConfiguration().registerBrokerPlugin(this);
|
server.getConfiguration().registerBrokerPlugin(this);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (brokerConnectConfiguration != null && brokerConnectConfiguration.getConnectionElements() != null) {
|
if (brokerConnectConfiguration != null && brokerConnectConfiguration.getConnectionElements() != null) {
|
||||||
for (AMQPBrokerConnectionElement connectionElement : brokerConnectConfiguration.getConnectionElements()) {
|
for (AMQPBrokerConnectionElement connectionElement : brokerConnectConfiguration.getConnectionElements()) {
|
||||||
if (connectionElement.getType() == AMQPBrokerConnectionAddressType.MIRROR) {
|
final AMQPBrokerConnectionAddressType elementType = connectionElement.getType();
|
||||||
|
|
||||||
|
if (elementType == AMQPBrokerConnectionAddressType.MIRROR) {
|
||||||
installMirrorController((AMQPMirrorBrokerConnectionElement) connectionElement, server);
|
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());
|
connectExecutor.execute(() -> doConnect());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ActiveMQServer getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
public NettyConnection getConnection() {
|
public NettyConnection getConnection() {
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
@ -204,7 +235,8 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateMatching(Queue queue, AMQPBrokerConnectionElement connectionElement) {
|
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 (connectionElement.getQueueName() != null) {
|
||||||
if (queue.getName().equals(connectionElement.getQueueName())) {
|
if (queue.getName().equals(connectionElement.getQueueName())) {
|
||||||
createLink(queue, connectionElement);
|
createLink(queue, connectionElement);
|
||||||
|
@ -239,7 +271,44 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
return snf;
|
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<Link> 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) {
|
private void linkClosed(Link link) {
|
||||||
|
for (Map.Entry<String, Predicate<Link>> 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) {
|
if (link.getLocalState() == EndpointState.ACTIVE) {
|
||||||
error(ActiveMQAMQPProtocolMessageBundle.BUNDLE.brokerConnectionRemoteLinkClosed(), lastRetryCounter);
|
error(ActiveMQAMQPProtocolMessageBundle.BUNDLE.brokerConnectionRemoteLinkClosed(), lastRetryCounter);
|
||||||
}
|
}
|
||||||
|
@ -280,7 +349,7 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
ConnectionEntry entry = protonProtocolManager.createOutgoingConnectionEntry(connection, saslFactory);
|
ConnectionEntry entry = protonProtocolManager.createOutgoingConnectionEntry(connection, saslFactory);
|
||||||
server.getRemotingService().addConnectionEntry(connection, entry);
|
server.getRemotingService().addConnectionEntry(connection, entry);
|
||||||
protonRemotingConnection = (ActiveMQProtonRemotingConnection) entry.connection;
|
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()));
|
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(),
|
connectSender(queue, queue.getName().toString(), mirrorControllerSource::setLink, (r) -> AMQPMirrorControllerSource.validateProtocolData(protonProtocolManager.getReferenceIDSupplier(), r, getMirrorSNF(replica)), server.getNodeID().toString(),
|
||||||
new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY}, null);
|
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);
|
server.scanAddresses(newPartition);
|
||||||
|
|
||||||
|
|
||||||
if (currentMirrorController == null) {
|
if (currentMirrorController == null) {
|
||||||
server.installMirrorController(newPartition);
|
server.installMirrorController(newPartition);
|
||||||
} else {
|
} else {
|
||||||
|
@ -445,6 +517,47 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
return null;
|
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<AMQPFederationAddressPolicyElement> localAddressPolicies = connectionElement.getLocalAddressPolicies();
|
||||||
|
if (!localAddressPolicies.isEmpty()) {
|
||||||
|
for (AMQPFederationAddressPolicyElement policy : localAddressPolicies) {
|
||||||
|
federation.addAddressMatchPolicy(
|
||||||
|
AMQPFederationPolicySupport.create(policy, federation.getWildcardConfiguration()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Set<AMQPFederationQueuePolicyElement> 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<AMQPFederationAddressPolicyElement> remoteAddressPolicies = connectionElement.getRemoteAddressPolicies();
|
||||||
|
if (!remoteAddressPolicies.isEmpty()) {
|
||||||
|
for (AMQPFederationAddressPolicyElement policy : remoteAddressPolicies) {
|
||||||
|
federation.addRemoteAddressMatchPolicy(
|
||||||
|
AMQPFederationPolicySupport.create(policy, federation.getWildcardConfiguration()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Set<AMQPFederationQueuePolicyElement> 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,
|
private void connectReceiver(ActiveMQProtonRemotingConnection protonRemotingConnection,
|
||||||
Session session,
|
Session session,
|
||||||
AMQPSessionContext sessionContext,
|
AMQPSessionContext sessionContext,
|
||||||
|
@ -537,7 +650,7 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
|
|
||||||
sender.open();
|
sender.open();
|
||||||
|
|
||||||
final ScheduledFuture futureTimeout;
|
final ScheduledFuture<?> futureTimeout;
|
||||||
|
|
||||||
AtomicBoolean cancelled = new AtomicBoolean(false);
|
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
|
// 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);
|
ProtonServerSenderContext senderContext = new ProtonServerSenderContext(protonRemotingConnection.getAmqpConnection(), sender, sessionContext, sessionContext.getSessionSPI(), outgoingInitializer).setBeforeDelivery(beforeDeliver);
|
||||||
try {
|
try {
|
||||||
if (!cancelled.get()) {
|
if (!cancelled.get()) {
|
||||||
|
@ -596,30 +709,38 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean verifyOfferedCapabilities(Sender sender, Symbol[] capabilities) {
|
public void error(Throwable e) {
|
||||||
|
error(e, 0);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// the retryCounter is passed here
|
||||||
|
@ -678,9 +799,17 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
|
|
||||||
private void redoConnection() {
|
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) {
|
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
|
// we need to use the connectExecutor to initiate a redoConnection
|
||||||
|
@ -828,5 +957,4 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,9 +50,9 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon
|
||||||
private final ActiveMQServer server;
|
private final ActiveMQServer server;
|
||||||
private volatile boolean started = false;
|
private volatile boolean started = false;
|
||||||
|
|
||||||
List<AMQPBrokerConnectConfiguration> amqpConnectionsConfig;
|
private List<AMQPBrokerConnectConfiguration> amqpConnectionsConfig;
|
||||||
List<AMQPBrokerConnection> amqpBrokerConnectionList;
|
private List<AMQPBrokerConnection> amqpBrokerConnectionList;
|
||||||
ProtonProtocolManager protonProtocolManager;
|
private ProtonProtocolManager protonProtocolManager;
|
||||||
|
|
||||||
public AMQPBrokerConnectionManager(ProtonProtocolManagerFactory factory, List<AMQPBrokerConnectConfiguration> amqpConnectionsConfig, ActiveMQServer server) {
|
public AMQPBrokerConnectionManager(ProtonProtocolManagerFactory factory, List<AMQPBrokerConnectConfiguration> amqpConnectionsConfig, ActiveMQServer server) {
|
||||||
this.amqpConnectionsConfig = amqpConnectionsConfig;
|
this.amqpConnectionsConfig = amqpConnectionsConfig;
|
||||||
|
@ -68,7 +68,6 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon
|
||||||
|
|
||||||
amqpBrokerConnectionList = new ArrayList<>();
|
amqpBrokerConnectionList = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
for (AMQPBrokerConnectConfiguration config : amqpConnectionsConfig) {
|
for (AMQPBrokerConnectConfiguration config : amqpConnectionsConfig) {
|
||||||
NettyConnectorFactory factory = new NettyConnectorFactory().setServerConnector(true);
|
NettyConnectorFactory factory = new NettyConnectorFactory().setServerConnector(true);
|
||||||
protonProtocolManager = (ProtonProtocolManager)protonProtocolManagerFactory.createProtocolManager(server, config.getTransportConfigurations().get(0).getExtraParams(), null, null);
|
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) {
|
public void connectionCreated(ActiveMQComponent component, Connection connection, ClientProtocolManager protocol) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void connectionDestroyed(Object connectionID) {
|
public void connectionDestroyed(Object connectionID) {
|
||||||
for (AMQPBrokerConnection connection : amqpBrokerConnectionList) {
|
for (AMQPBrokerConnection connection : amqpBrokerConnectionList) {
|
||||||
|
@ -125,7 +123,6 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon
|
||||||
connection.connectionException(connectionID, me);
|
connection.connectionException(connectionID, me);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -245,6 +242,4 @@ public class AMQPBrokerConnectionManager implements ActiveMQComponent, ClientCon
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String, FederationQueuePolicyManager> queueMatchPolicies = new ConcurrentHashMap<>();
|
||||||
|
protected final Map<String, FederationAddressPolicyManager> addressMatchPolicies = new ConcurrentHashMap<>();
|
||||||
|
protected final Map<String, Predicate<Link>> 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<Link> 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<String, Predicate<Link>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Link> remoteCloseInterceptor = this::remoteLinkClosedInterceptor;
|
||||||
|
private final Transformer transformer;
|
||||||
|
|
||||||
|
private AMQPFederatedAddressDeliveryReceiver receiver;
|
||||||
|
private Receiver protonReceiver;
|
||||||
|
private boolean started;
|
||||||
|
private volatile boolean closed;
|
||||||
|
private Consumer<FederationConsumerInternal> 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<FederationConsumerInternal> 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<Symbol, Object> filtersMap = new HashMap<>();
|
||||||
|
filtersMap.put(AmqpSupport.JMS_SELECTOR_KEY, jmsFilter);
|
||||||
|
|
||||||
|
source.setFilter(filtersMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.setAddress(address);
|
||||||
|
|
||||||
|
final Map<String, Object> 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<Symbol, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> addressSourceProperties;
|
||||||
|
|
||||||
|
if (sender.getRemoteProperties() == null || !sender.getRemoteProperties().containsKey(FEDERATED_ADDRESS_SOURCE_PROPERTIES)) {
|
||||||
|
addressSourceProperties = Collections.EMPTY_MAP;
|
||||||
|
} else {
|
||||||
|
addressSourceProperties = (Map<String, Object>) 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<Symbol, DescribedType> 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<RoutingType> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> properties;
|
||||||
|
private final AMQPConnectionContext connection;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public AMQPFederationConfiguration(AMQPConnectionContext connection, Map<String, Object> 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<String, Object> toConfigurationMap() {
|
||||||
|
final Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
||||||
|
}
|
|
@ -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<String, Object> properties;
|
||||||
|
private final AMQPFederation federation;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public AMQPFederationConsumerConfiguration(AMQPFederation federation, Map<String, ?> properties) {
|
||||||
|
this.federation = federation;
|
||||||
|
|
||||||
|
if (properties == null || properties.isEmpty()) {
|
||||||
|
this.properties = Collections.EMPTY_MAP;
|
||||||
|
} else {
|
||||||
|
this.properties = (Map<String, Object>) 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Symbol, Object> annotations = new LinkedHashMap<>();
|
||||||
|
final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations);
|
||||||
|
final Map<String, Object> 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<String> 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<String> 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<Symbol, Object> annotations = new LinkedHashMap<>();
|
||||||
|
final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations);
|
||||||
|
final Map<String, Object> 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<String, Object> policyMap = (Map<String, Object>) 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<Map.Entry<String, String>> includes = decodeFlattenedFilterSet(policyMap, QUEUE_INCLUDES);
|
||||||
|
final Set<Map.Entry<String, String>> 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<String, String>) policyMap.get(TRANSFORMER_PROPERTIES_MAP));
|
||||||
|
} else {
|
||||||
|
transformerConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> properties;
|
||||||
|
|
||||||
|
if (policyMap.containsKey(POLICY_PROPERTIES_MAP)) {
|
||||||
|
properties = (Map<String, Object>) 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<Map.Entry<String, String>> decodeFlattenedFilterSet(Map<String, Object> 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<Map.Entry<String, String>> policyEntrySet;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final List<String> flattenedEntrySet = (List<String>) 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<String, Object> policyMap = (Map<String, Object>) 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<String> includes;
|
||||||
|
final Set<String> excludes;
|
||||||
|
|
||||||
|
if (policyMap.containsKey(ADDRESS_INCLUDES)) {
|
||||||
|
includes = (Set<String>) new HashSet<>((List<String>)policyMap.get(ADDRESS_INCLUDES));
|
||||||
|
} else {
|
||||||
|
includes = Collections.EMPTY_SET;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policyMap.containsKey(ADDRESS_EXCLUDES)) {
|
||||||
|
excludes = (Set<String>) new HashSet<>((List<String>)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<String, String>) policyMap.get(TRANSFORMER_PROPERTIES_MAP));
|
||||||
|
} else {
|
||||||
|
transformerConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Object> properties;
|
||||||
|
|
||||||
|
if (policyMap.containsKey(POLICY_PROPERTIES_MAP)) {
|
||||||
|
properties = (Map<String, Object>) 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<String> includes;
|
||||||
|
final Set<String> 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 <code>-1</code> 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<Map.Entry<String, String>> includes;
|
||||||
|
final Set<Map.Entry<String, String>> excludes;
|
||||||
|
|
||||||
|
if (element.getIncludes() != null && !element.getIncludes().isEmpty()) {
|
||||||
|
includes = new HashSet<>(element.getIncludes().size());
|
||||||
|
|
||||||
|
element.getIncludes().forEach(queueMatch ->
|
||||||
|
includes.add(new AbstractMap.SimpleImmutableEntry<String, String>(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<String, String>(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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Link> remoteCloseIntercepter = this::remoteLinkClosedIntercepter;
|
||||||
|
private final Transformer transformer;
|
||||||
|
|
||||||
|
private AMQPFederatedQueueDeliveryReceiver receiver;
|
||||||
|
private Receiver protonReceiver;
|
||||||
|
private boolean started;
|
||||||
|
private volatile boolean closed;
|
||||||
|
private Consumer<FederationConsumerInternal> 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<FederationConsumerInternal> 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<Symbol, Object> filtersMap = new HashMap<>();
|
||||||
|
filtersMap.put(AmqpSupport.JMS_SELECTOR_KEY, jmsFilter);
|
||||||
|
|
||||||
|
source.setFilter(filtersMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
target.setAddress(address);
|
||||||
|
|
||||||
|
final Map<Symbol, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Symbol, DescribedType> 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* 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<String, FederationReceiveFromQueuePolicy> remoteQueueMatchPolicies = new HashMap<>();
|
||||||
|
private final Map<String, FederationReceiveFromAddressPolicy> remoteAddressMatchPolicies = new HashMap<>();
|
||||||
|
|
||||||
|
private final Map<String, Object> 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<String, Object> properties, AMQPBrokerConnection connection) {
|
||||||
|
super(name, connection.getServer());
|
||||||
|
|
||||||
|
if (properties == null || properties.isEmpty()) {
|
||||||
|
this.properties = Collections.EMPTY_MAP;
|
||||||
|
} else {
|
||||||
|
this.properties = (Map<String, Object>) 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<Exception> 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<Symbol, Object> 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<Symbol, Object> 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
// in a regular case we should not have more than amqpCredits on the pool, that's the max we would need
|
||||||
private final MpscPool<ACKMessageOperation> ackMessageMpscPool = new MpscPool<>(amqpCredits, ACKMessageOperation::reset, ACKMessageOperation::new);
|
private final MpscPool<ACKMessageOperation> ackMessageMpscPool = new MpscPool<>(connection.getAmqpCredits(), ACKMessageOperation::reset, ACKMessageOperation::new);
|
||||||
|
|
||||||
final RoutingContextImpl routingContext = new RoutingContextImpl(null);
|
final RoutingContextImpl routingContext = new RoutingContextImpl(null);
|
||||||
|
|
||||||
|
@ -260,7 +260,7 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() throws Exception {
|
public void initialize() throws Exception {
|
||||||
super.initialize();
|
initialized = true;
|
||||||
|
|
||||||
// Match the settlement mode of the remote instead of relying on the default of MIXED.
|
// Match the settlement mode of the remote instead of relying on the default of MIXED.
|
||||||
receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode());
|
receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode());
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -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<String, RoutingType> {
|
||||||
|
|
||||||
|
private final Set<AddressMatcher> includesMatchers = new LinkedHashSet<>();
|
||||||
|
private final Set<AddressMatcher> excludesMatchers = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
private final Collection<String> includes;
|
||||||
|
private final Collection<String> 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<String, Object> properties;
|
||||||
|
private final TransformerConfiguration transformerConfig;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public FederationReceiveFromAddressPolicy(String name, boolean autoDelete, long autoDeleteDelay,
|
||||||
|
long autoDeleteMessageCount, int maxHops, boolean enableDivertBindings,
|
||||||
|
Collection<String> includeAddresses, Collection<String> excludeAddresses,
|
||||||
|
Map<String, Object> 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<String> getIncludes() {
|
||||||
|
return includes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<String> getExcludes() {
|
||||||
|
return excludes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> 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 <code>true</code> 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<String> {
|
||||||
|
|
||||||
|
private final Predicate<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String> {
|
||||||
|
|
||||||
|
private final Set<QueueMatcher> includeMatchers = new LinkedHashSet<>();
|
||||||
|
private final Set<QueueMatcher> excludeMatchers = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
private final Collection<Map.Entry<String, String>> includes;
|
||||||
|
private final Collection<Map.Entry<String, String>> excludes;
|
||||||
|
|
||||||
|
private final String policyName;
|
||||||
|
private final boolean includeFederated;
|
||||||
|
private final int priorityAdjustment;
|
||||||
|
private final Map<String, Object> properties;
|
||||||
|
private final TransformerConfiguration transformerConfig;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public FederationReceiveFromQueuePolicy(String name, boolean includeFederated, int priorotyAdjustment,
|
||||||
|
Collection<Map.Entry<String, String>> includeQueues,
|
||||||
|
Collection<Map.Entry<String, String>> excludeQueues,
|
||||||
|
Map<String, Object> 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<Map.Entry<String, String>> getIncludes() {
|
||||||
|
return includes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Map.Entry<String, String>> getExcludes() {
|
||||||
|
return excludes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> 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<String, String> {
|
||||||
|
|
||||||
|
private final Predicate<String> addressMatch;
|
||||||
|
private final Predicate<String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FederationConsumerInfo, FederationConsumerEntry> remoteConsumers = new HashMap<>();
|
||||||
|
protected final FederationInternal federation;
|
||||||
|
protected final Map<DivertBinding, Set<SimpleString>> 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<SimpleString> 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<SimpleString> 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 <code>true</code> 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);
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<FederationConsumerInternal> handler);
|
||||||
|
|
||||||
|
}
|
|
@ -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() + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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<ServerConsumer> federationConsumerMatcher;
|
||||||
|
protected final FederationReceiveFromQueuePolicy policy;
|
||||||
|
protected final Map<FederationConsumerInfo, FederationConsumerEntry> 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 <code>true</code> 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<ServerConsumer> 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);
|
||||||
|
|
||||||
|
}
|
|
@ -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.LogBundle;
|
||||||
import org.apache.activemq.artemis.logs.annotation.Message;
|
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.logs.BundleFactory;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPIllegalStateException;
|
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.exceptions.ActiveMQAMQPInternalErrorException;
|
||||||
|
@ -101,4 +102,10 @@ public interface ActiveMQAMQPProtocolMessageBundle {
|
||||||
|
|
||||||
@Message(id = 119024, value = "link is missing a desired capability declaration {}")
|
@Message(id = 119024, value = "link is missing a desired capability declaration {}")
|
||||||
ActiveMQAMQPIllegalStateException missingDesiredCapability(String capability);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.AMQPConnectionCallback;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
|
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.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.connect.mirror.AMQPMirrorControllerSource;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException;
|
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.ActiveMQAMQPProtocolLogger;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle;
|
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.proton.handler.EventHandler;
|
import org.apache.activemq.artemis.protocol.amqp.proton.handler.EventHandler;
|
||||||
|
@ -76,11 +79,17 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import java.lang.invoke.MethodHandles;
|
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.FAILOVER_SERVER_LIST;
|
||||||
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.HOSTNAME;
|
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.NETWORK_HOST;
|
||||||
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.PORT;
|
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.SCHEME;
|
||||||
|
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyDesiredCapability;;
|
||||||
|
|
||||||
public class AMQPConnectionContext extends ProtonInitializable implements EventHandler {
|
public class AMQPConnectionContext extends ProtonInitializable implements EventHandler {
|
||||||
|
|
||||||
|
@ -98,8 +107,7 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
private final ClientSASLFactory saslClientFactory;
|
private final ClientSASLFactory saslClientFactory;
|
||||||
private final Map<Symbol, Object> connectionProperties = new HashMap<>();
|
private final Map<Symbol, Object> connectionProperties = new HashMap<>();
|
||||||
private final ScheduledExecutorService scheduledPool;
|
private final ScheduledExecutorService scheduledPool;
|
||||||
|
private final Map<String, LinkCloseListener> linkCloseListeners = new ConcurrentHashMap<>();
|
||||||
private LinkCloseListener linkCloseListener;
|
|
||||||
|
|
||||||
private final Map<Session, AMQPSessionContext> sessions = new ConcurrentHashMap<>();
|
private final Map<Session, AMQPSessionContext> sessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@ -111,7 +119,7 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
private final boolean bridgeConnection;
|
private final boolean bridgeConnection;
|
||||||
|
|
||||||
private final ScheduleOperator scheduleOp = new ScheduleOperator(new ScheduleRunnable());
|
private final ScheduleOperator scheduleOp = new ScheduleOperator(new ScheduleRunnable());
|
||||||
private final AtomicReference<Future<?>> scheduledFutureRef = new AtomicReference(VOID_FUTURE);
|
private final AtomicReference<Future<?>> scheduledFutureRef = new AtomicReference<>(VOID_FUTURE);
|
||||||
|
|
||||||
private String user;
|
private String user;
|
||||||
private String password;
|
private String password;
|
||||||
|
@ -182,15 +190,46 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LinkCloseListener getLinkCloseListener() {
|
@Override
|
||||||
return linkCloseListener;
|
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;
|
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() {
|
public boolean isBridgeConnection() {
|
||||||
return bridgeConnection;
|
return bridgeConnection;
|
||||||
}
|
}
|
||||||
|
@ -342,12 +381,11 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void remoteLinkOpened(Link link) throws Exception {
|
protected void remoteLinkOpened(Link link) throws Exception {
|
||||||
|
final AMQPSessionContext protonSession = getSessionExtension(link.getSession());
|
||||||
|
|
||||||
AMQPSessionContext protonSession = getSessionExtension(link.getSession());
|
final Runnable runnable = link.attachments().get(AMQP_LINK_INITIALIZER_KEY, Runnable.class);
|
||||||
|
|
||||||
Runnable runnable = link.attachments().get(Runnable.class, Runnable.class);
|
|
||||||
if (runnable != null) {
|
if (runnable != null) {
|
||||||
link.attachments().set(Runnable.class, Runnable.class, null);
|
link.attachments().set(AMQP_LINK_INITIALIZER_KEY, Runnable.class, null);
|
||||||
runnable.run();
|
runnable.run();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -358,71 +396,93 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
|
|
||||||
link.setSource(link.getRemoteSource());
|
link.setSource(link.getRemoteSource());
|
||||||
link.setTarget(link.getRemoteTarget());
|
link.setTarget(link.getRemoteTarget());
|
||||||
|
|
||||||
if (link instanceof Receiver) {
|
if (link instanceof Receiver) {
|
||||||
Receiver receiver = (Receiver) link;
|
Receiver receiver = (Receiver) link;
|
||||||
if (link.getRemoteTarget() instanceof Coordinator) {
|
if (link.getRemoteTarget() instanceof Coordinator) {
|
||||||
Coordinator coordinator = (Coordinator) link.getRemoteTarget();
|
Coordinator coordinator = (Coordinator) link.getRemoteTarget();
|
||||||
protonSession.addTransactionHandler(coordinator, receiver);
|
protonSession.addTransactionHandler(coordinator, receiver);
|
||||||
|
} else if (isReplicaTarget(receiver)) {
|
||||||
|
handleReplicaTargetLinkOpened(protonSession, receiver);
|
||||||
|
} else if (isFederationControlLink(receiver)) {
|
||||||
|
handleFederationControlLinkOpened(protonSession, receiver);
|
||||||
} else {
|
} else {
|
||||||
if (isReplicaTarget(receiver)) {
|
protonSession.addReceiver(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Sender sender = (Sender) link;
|
final Sender sender = (Sender) link;
|
||||||
protonSession.addSender(sender);
|
if (isFederationAddressReceiver(sender)) {
|
||||||
}
|
protonSession.addSender(sender, new AMQPFederationAddressSenderController(protonSession));
|
||||||
}
|
} else if (isFederationQueueReceiver(sender)) {
|
||||||
|
protonSession.addSender(sender, new AMQPFederationQueueSenderController(protonSession));
|
||||||
|
} else {
|
||||||
protected boolean verifyDesiredCapabilities(Receiver reciever, Symbol s) {
|
protonSession.addSender(sender);
|
||||||
|
|
||||||
if (reciever.getRemoteDesiredCapabilities() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean foundS = false;
|
|
||||||
for (Symbol b : reciever.getRemoteDesiredCapabilities()) {
|
|
||||||
if (b.equals(s)) {
|
|
||||||
foundS = true;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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);
|
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() {
|
public Symbol[] getConnectionCapabilitiesOffered() {
|
||||||
URI tc = connectionCallback.getFailoverList();
|
URI tc = connectionCallback.getFailoverList();
|
||||||
if (tc != null) {
|
if (tc != null) {
|
||||||
|
@ -730,9 +790,15 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
public void onRemoteClose(Link link) throws Exception {
|
public void onRemoteClose(Link link) throws Exception {
|
||||||
handler.requireHandler();
|
handler.requireHandler();
|
||||||
|
|
||||||
if (linkCloseListener != null) {
|
final AtomicReference<Exception> handlerThrew = new AtomicReference<>();
|
||||||
linkCloseListener.onClose(link);
|
|
||||||
}
|
linkCloseListeners.forEach((k, v) -> {
|
||||||
|
try {
|
||||||
|
v.onClose(link);
|
||||||
|
} catch (Exception e) {
|
||||||
|
handlerThrew.compareAndSet(null, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ProtonDeliveryHandler linkContext = (ProtonDeliveryHandler) link.getContext();
|
ProtonDeliveryHandler linkContext = (ProtonDeliveryHandler) link.getContext();
|
||||||
if (linkContext != null) {
|
if (linkContext != null) {
|
||||||
|
@ -746,6 +812,10 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
link.close();
|
link.close();
|
||||||
link.free();
|
link.free();
|
||||||
flush();
|
flush();
|
||||||
|
|
||||||
|
if (handlerThrew.get() != null) {
|
||||||
|
throw handlerThrew.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -782,7 +852,6 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class LocalSecurity implements SecurityAuth {
|
private class LocalSecurity implements SecurityAuth {
|
||||||
@Override
|
@Override
|
||||||
public String getUsername() {
|
public String getUsername() {
|
||||||
|
@ -818,5 +887,4 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
|
||||||
return getProtocolManager().getSecurityDomain();
|
return getProtocolManager().getSecurityDomain();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,24 +16,35 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.activemq.artemis.protocol.amqp.proton;
|
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.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
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.api.core.ActiveMQSecurityException;
|
||||||
import org.apache.activemq.artemis.core.server.ActiveMQServer;
|
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.AMQPMirrorControllerSource;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerTarget;
|
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.broker.AMQPSessionCallback;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.client.ProtonClientSenderContext;
|
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.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.exceptions.ActiveMQAMQPInternalErrorException;
|
||||||
import org.apache.activemq.artemis.protocol.amqp.proton.transaction.ProtonTransactionHandler;
|
import org.apache.activemq.artemis.protocol.amqp.proton.transaction.ProtonTransactionHandler;
|
||||||
import org.apache.qpid.proton.amqp.Symbol;
|
import org.apache.qpid.proton.amqp.Symbol;
|
||||||
import org.apache.qpid.proton.amqp.transaction.Coordinator;
|
import org.apache.qpid.proton.amqp.transaction.Coordinator;
|
||||||
import org.apache.qpid.proton.amqp.transport.ErrorCondition;
|
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.EndpointState;
|
||||||
import org.apache.qpid.proton.engine.Receiver;
|
import org.apache.qpid.proton.engine.Receiver;
|
||||||
import org.apache.qpid.proton.engine.Sender;
|
import org.apache.qpid.proton.engine.Sender;
|
||||||
|
@ -74,10 +85,22 @@ public class AMQPSessionContext extends ProtonInitializable {
|
||||||
return sessionSPI;
|
return sessionSPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AMQPConnectionContext getAMQPConnectionContext() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session getSession() {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActiveMQServer getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() throws Exception {
|
public void initialize() throws Exception {
|
||||||
if (!isInitialized()) {
|
if (!isInitialized()) {
|
||||||
super.initialize();
|
initialized = true;
|
||||||
|
|
||||||
if (sessionSPI != null) {
|
if (sessionSPI != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -222,33 +245,85 @@ public class AMQPSessionContext extends ProtonInitializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addReplicaTarget(Receiver receiver) throws Exception {
|
public void addReplicaTarget(Receiver receiver) throws Exception {
|
||||||
try {
|
addReceiver(receiver, (r, s) -> {
|
||||||
AMQPMirrorControllerTarget protonReceiver = new AMQPMirrorControllerTarget(sessionSPI, connection, this, receiver, server);
|
final AMQPMirrorControllerTarget protonReceiver =
|
||||||
protonReceiver.initialize();
|
new AMQPMirrorControllerTarget(sessionSPI, connection, this, receiver, server);
|
||||||
receivers.put(receiver, protonReceiver);
|
|
||||||
sessionSPI.addProducer(receiver.getName(), receiver.getTarget().getAddress());
|
final HashMap<Symbol, Object> brokerIDProperties = new HashMap<>();
|
||||||
receiver.setContext(protonReceiver);
|
|
||||||
HashMap<Symbol, Object> brokerIDProperties = new HashMap<>();
|
|
||||||
brokerIDProperties.put(AMQPMirrorControllerSource.BROKER_ID, server.getNodeID().toString());
|
brokerIDProperties.put(AMQPMirrorControllerSource.BROKER_ID, server.getNodeID().toString());
|
||||||
receiver.setProperties(brokerIDProperties);
|
receiver.setProperties(brokerIDProperties);
|
||||||
connection.runNow(() -> {
|
|
||||||
receiver.open();
|
return protonReceiver;
|
||||||
connection.flush();
|
});
|
||||||
});
|
}
|
||||||
} catch (ActiveMQAMQPException e) {
|
|
||||||
receivers.remove(receiver);
|
@SuppressWarnings("unchecked")
|
||||||
receiver.setTarget(null);
|
public void addFederationCommandProcessor(Receiver receiver) throws Exception {
|
||||||
receiver.setCondition(new ErrorCondition(e.getAmqpError(), e.getMessage()));
|
addReceiver(receiver, (r, s) -> {
|
||||||
connection.runNow(() -> {
|
final Connection protonConnection = receiver.getSession().getConnection();
|
||||||
receiver.close();
|
final org.apache.qpid.proton.engine.Record attachments = protonConnection.attachments();
|
||||||
connection.flush();
|
|
||||||
});
|
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<String, Object> federationConfigurationMap;
|
||||||
|
|
||||||
|
if (receiver.getRemoteProperties() == null || !receiver.getRemoteProperties().containsKey(FEDERATION_CONFIGURATION)) {
|
||||||
|
federationConfigurationMap = Collections.EMPTY_MAP;
|
||||||
|
} else {
|
||||||
|
federationConfigurationMap = (Map<String, Object>) 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 {
|
public void addReceiver(Receiver receiver) throws Exception {
|
||||||
|
addReceiver(receiver, (r, s) -> {
|
||||||
|
return new ProtonServerReceiverContext(sessionSPI, connection, this, receiver);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T extends ProtonAbstractReceiver> T addReceiver(Receiver receiver, BiFunction<AMQPSessionContext, Receiver, T> receiverBuilder) throws Exception {
|
||||||
try {
|
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();
|
protonReceiver.initialize();
|
||||||
receivers.put(receiver, protonReceiver);
|
receivers.put(receiver, protonReceiver);
|
||||||
sessionSPI.addProducer(receiver.getName(), receiver.getTarget().getAddress());
|
sessionSPI.addProducer(receiver.getName(), receiver.getTarget().getAddress());
|
||||||
|
@ -257,6 +332,8 @@ public class AMQPSessionContext extends ProtonInitializable {
|
||||||
receiver.open();
|
receiver.open();
|
||||||
connection.flush();
|
connection.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return (T) protonReceiver;
|
||||||
} catch (ActiveMQAMQPException e) {
|
} catch (ActiveMQAMQPException e) {
|
||||||
receivers.remove(receiver);
|
receivers.remove(receiver);
|
||||||
receiver.setTarget(null);
|
receiver.setTarget(null);
|
||||||
|
@ -265,6 +342,8 @@ public class AMQPSessionContext extends ProtonInitializable {
|
||||||
receiver.close();
|
receiver.close();
|
||||||
connection.flush();
|
connection.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,16 +18,23 @@ package org.apache.activemq.artemis.protocol.amqp.proton;
|
||||||
|
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.apache.qpid.proton.amqp.DescribedType;
|
import org.apache.qpid.proton.amqp.DescribedType;
|
||||||
import org.apache.qpid.proton.amqp.Symbol;
|
import org.apache.qpid.proton.amqp.Symbol;
|
||||||
import org.apache.qpid.proton.amqp.UnsignedLong;
|
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
|
* Set of useful methods and definitions used in the AMQP protocol handling
|
||||||
*/
|
*/
|
||||||
public class AmqpSupport {
|
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
|
// Default thresholds/values used for granting credit to producers
|
||||||
public static final int AMQP_CREDITS_DEFAULT = 1000;
|
public static final int AMQP_CREDITS_DEFAULT = 1000;
|
||||||
public static final int AMQP_LOW_CREDITS_DEFAULT = 300;
|
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;
|
public static final boolean AMQP_USE_MODIFIED_FOR_TRANSIENT_DELIVERY_ERRORS = false;
|
||||||
|
|
||||||
// Identification values used to locating JMS selector types.
|
// 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 UnsignedLong JMS_SELECTOR_CODE = UnsignedLong.valueOf(0x0000468C00000004L);
|
||||||
public static final Symbol JMS_SELECTOR_NAME = Symbol.valueOf("apache.org:selector-filter:string");
|
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};
|
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 PLATFORM = Symbol.valueOf("platform");
|
||||||
public static final Symbol RESOURCE_DELETED = Symbol.valueOf("amqp:resource-deleted");
|
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 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 SHARED_SUBS = Symbol.valueOf("SHARED-SUBS");
|
||||||
public static final Symbol NETWORK_HOST = Symbol.valueOf("network-host");
|
public static final Symbol NETWORK_HOST = Symbol.valueOf("network-host");
|
||||||
public static final Symbol PORT = Symbol.valueOf("port");
|
public static final Symbol PORT = Symbol.valueOf("port");
|
||||||
|
@ -145,4 +155,93 @@ public class AmqpSupport {
|
||||||
|
|
||||||
return null;
|
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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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.
|
||||||
|
* <p>
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,32 +40,20 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
|
|
||||||
protected final Receiver receiver;
|
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;
|
protected final int minLargeMessageSize;
|
||||||
|
|
||||||
final RoutingContext routingContext;
|
protected final RoutingContext routingContext;
|
||||||
|
|
||||||
protected final AMQPSessionCallback sessionSPI;
|
protected final AMQPSessionCallback sessionSPI;
|
||||||
|
|
||||||
protected volatile AMQPLargeMessage currentLargeMessage;
|
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 Runnable creditRunnable;
|
||||||
|
|
||||||
protected final boolean useModified;
|
protected final boolean useModified;
|
||||||
|
|
||||||
protected int pendingSettles = 0;
|
protected int pendingSettles = 0;
|
||||||
|
|
||||||
public static boolean isBellowThreshold(int credit, int pending, int threshold) {
|
public static boolean isBellowThreshold(int credit, int pending, int threshold) {
|
||||||
return credit <= threshold - pending;
|
return credit <= threshold - pending;
|
||||||
}
|
}
|
||||||
|
@ -82,10 +70,8 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.protonSession = protonSession;
|
this.protonSession = protonSession;
|
||||||
this.receiver = receiver;
|
this.receiver = receiver;
|
||||||
this.amqpCredits = connection.getAmqpCredits();
|
this.minLargeMessageSize = getConfiguredMinLargeMessageSize(connection);
|
||||||
this.minCreditRefresh = connection.getAmqpLowCredits();
|
this.creditRunnable = createCreditRunnable(connection);
|
||||||
this.minLargeMessageSize = connection.getProtocolManager().getAmqpMinLargeMessageSize();
|
|
||||||
this.creditRunnable = createCreditRunnable(amqpCredits, minCreditRefresh, receiver, connection, this);
|
|
||||||
useModified = this.connection.getProtocolManager().isUseModifiedForTransientDeliveryErrors();
|
useModified = this.connection.getProtocolManager().isUseModifiedForTransientDeliveryErrors();
|
||||||
this.routingContext = new RoutingContextImpl(null).setDuplicateDetection(connection.getProtocolManager().isAmqpDuplicateDetection());
|
this.routingContext = new RoutingContextImpl(null).setDuplicateDetection(connection.getProtocolManager().isAmqpDuplicateDetection());
|
||||||
}
|
}
|
||||||
|
@ -94,7 +80,6 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
sessionSPI.recoverContext();
|
sessionSPI.recoverContext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected void clearLargeMessage() {
|
protected void clearLargeMessage() {
|
||||||
connection.runNow(() -> {
|
connection.runNow(() -> {
|
||||||
if (currentLargeMessage != null) {
|
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,
|
public static Runnable createCreditRunnable(int refill,
|
||||||
int threshold,
|
int threshold,
|
||||||
|
@ -122,9 +144,22 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
return new FlowControlRunner(refill, threshold, receiver, connection, context);
|
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.
|
||||||
|
* <p>
|
||||||
|
* 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,
|
public static Runnable createCreditRunnable(int refill,
|
||||||
int threshold,
|
int threshold,
|
||||||
|
@ -132,6 +167,7 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
AMQPConnectionContext connection) {
|
AMQPConnectionContext connection) {
|
||||||
return new FlowControlRunner(refill, threshold, receiver, connection, null);
|
return new FlowControlRunner(refill, threshold, receiver, connection, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The reason why we use the AtomicRunnable here
|
* The reason why we use the AtomicRunnable here
|
||||||
* is because PagingManager will call Runnable in case it was blocked.
|
* 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
|
* and this serves as a control to avoid duplicated calls
|
||||||
* */
|
* */
|
||||||
static class FlowControlRunner implements Runnable {
|
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;
|
final int refill;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The low water mark before the runnable considers performing a credit top off.
|
||||||
|
*/
|
||||||
final int threshold;
|
final int threshold;
|
||||||
|
|
||||||
final Receiver receiver;
|
final Receiver receiver;
|
||||||
final AMQPConnectionContext connection;
|
final AMQPConnectionContext connection;
|
||||||
final ProtonAbstractReceiver context;
|
final ProtonAbstractReceiver context;
|
||||||
|
@ -171,7 +216,6 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
connection.instantFlush();
|
connection.instantFlush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,6 +340,10 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
|
||||||
close(false);
|
close(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AMQPConnectionContext getConnection() {
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx);
|
protected abstract void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx);
|
||||||
|
|
||||||
// TODO: how to implement flow here?
|
// TODO: how to implement flow here?
|
||||||
|
|
|
@ -16,17 +16,15 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.activemq.artemis.protocol.amqp.proton;
|
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() {
|
public boolean isInitialized() {
|
||||||
return initialized;
|
return initialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() throws Exception {
|
public abstract void initialize() throws Exception;
|
||||||
if (!initialized) {
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,28 +65,27 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver {
|
||||||
protected AddressFullMessagePolicy lastAddressPolicy;
|
protected AddressFullMessagePolicy lastAddressPolicy;
|
||||||
protected boolean addressAlreadyClashed = false;
|
protected boolean addressAlreadyClashed = false;
|
||||||
|
|
||||||
|
|
||||||
protected final Runnable spiFlow = this::sessionSPIFlow;
|
protected final Runnable spiFlow = this::sessionSPIFlow;
|
||||||
|
|
||||||
private RoutingType defRoutingType;
|
protected RoutingType defRoutingType;
|
||||||
|
|
||||||
public ProtonServerReceiverContext(AMQPSessionCallback sessionSPI,
|
public ProtonServerReceiverContext(AMQPSessionCallback sessionSPI,
|
||||||
AMQPConnectionContext connection,
|
AMQPConnectionContext connection,
|
||||||
AMQPSessionContext protonSession,
|
AMQPSessionContext protonSession,
|
||||||
Receiver receiver) {
|
Receiver receiver) {
|
||||||
super(sessionSPI, connection, protonSession, receiver);
|
super(sessionSPI, connection, protonSession, receiver);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() throws Exception {
|
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();
|
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.
|
// Match the settlement mode of the remote instead of relying on the default of MIXED.
|
||||||
receiver.setSenderSettleMode(receiver.getRemoteSenderSettleMode());
|
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);
|
receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
|
||||||
|
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
|
@ -156,7 +155,7 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver {
|
||||||
return target != null ? getRoutingType(target.getCapabilities(), address) : getRoutingType((Symbol[]) null, address);
|
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);
|
RoutingType explicitRoutingType = getExplicitRoutingType(symbols);
|
||||||
if (explicitRoutingType != null) {
|
if (explicitRoutingType != null) {
|
||||||
return explicitRoutingType;
|
return explicitRoutingType;
|
||||||
|
@ -207,7 +206,6 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver {
|
||||||
logger.warn(e.getMessage(), e);
|
logger.warn(e.getMessage(), e);
|
||||||
|
|
||||||
deliveryFailed(delivery, receiver, e);
|
deliveryFailed(delivery, receiver, e);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -107,9 +107,9 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
|
||||||
|
|
||||||
private static final Symbol COPY = Symbol.valueOf("copy");
|
private static final Symbol COPY = AmqpSupport.COPY;
|
||||||
private static final Symbol TOPIC = Symbol.valueOf("topic");
|
private static final Symbol TOPIC = AmqpSupport.TOPIC_CAPABILITY;
|
||||||
private static final Symbol QUEUE = Symbol.valueOf("queue");
|
private static final Symbol QUEUE = AmqpSupport.QUEUE_CAPABILITY;
|
||||||
private static final Symbol SHARED = Symbol.valueOf("shared");
|
private static final Symbol SHARED = Symbol.valueOf("shared");
|
||||||
private static final Symbol GLOBAL = Symbol.valueOf("global");
|
private static final Symbol GLOBAL = Symbol.valueOf("global");
|
||||||
|
|
||||||
|
@ -135,7 +135,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
volatile LargeMessageDeliveryContext pendingLargeMessage = null;
|
volatile LargeMessageDeliveryContext pendingLargeMessage = null;
|
||||||
volatile Runnable afterLargeMessage;
|
volatile Runnable afterLargeMessage;
|
||||||
|
|
||||||
|
|
||||||
private int credits = 0;
|
private int credits = 0;
|
||||||
|
|
||||||
private AtomicInteger pending = new AtomicInteger(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
|
* create the actual underlying ActiveMQ Artemis Server Consumer
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void initialize() throws Exception {
|
public void initialize() throws Exception {
|
||||||
super.initialize();
|
initialized = true;
|
||||||
|
|
||||||
if (controller == null) {
|
if (controller == null) {
|
||||||
controller = new DefaultController(sessionSPI);
|
controller = new DefaultController(sessionSPI);
|
||||||
|
@ -286,6 +284,7 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
|
|
||||||
try {
|
try {
|
||||||
brokerConsumer = controller.init(this);
|
brokerConsumer = controller.init(this);
|
||||||
|
preSettle = sender.getSenderSettleMode() == SenderSettleMode.SETTLED;
|
||||||
onflowControlReady = brokerConsumer::promptDelivery;
|
onflowControlReady = brokerConsumer::promptDelivery;
|
||||||
} catch (ActiveMQAMQPResourceLimitExceededException e1) {
|
} catch (ActiveMQAMQPResourceLimitExceededException e1) {
|
||||||
throw e1;
|
throw e1;
|
||||||
|
@ -357,7 +356,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
// any durable resources for say pub subs
|
// any durable resources for say pub subs
|
||||||
if (remoteLinkClose) {
|
if (remoteLinkClose) {
|
||||||
controller.close();
|
controller.close();
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn(e.getMessage(), e);
|
logger.warn(e.getMessage(), e);
|
||||||
|
@ -766,7 +764,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
position = proposedPosition;
|
position = proposedPosition;
|
||||||
return (int)position;
|
return (int)position;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
TLSEncode.getEncoder().setByteBuffer((WritableBuffer)null);
|
TLSEncode.getEncoder().setByteBuffer((WritableBuffer)null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -848,7 +845,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
message.usageUp();
|
message.usageUp();
|
||||||
pendingLargeMessage = new LargeMessageDeliveryContext(messageReference, message, delivery);
|
pendingLargeMessage = new LargeMessageDeliveryContext(messageReference, message, delivery);
|
||||||
pendingLargeMessage.deliver();
|
pendingLargeMessage.deliver();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deliverStandard(MessageReference messageReference, AMQPMessage message) {
|
private void deliverStandard(MessageReference messageReference, AMQPMessage message) {
|
||||||
|
@ -966,7 +962,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
|
|
||||||
class DefaultController implements SenderController {
|
class DefaultController implements SenderController {
|
||||||
|
|
||||||
|
|
||||||
private boolean shared = false;
|
private boolean shared = false;
|
||||||
boolean global = false;
|
boolean global = false;
|
||||||
boolean multicast;
|
boolean multicast;
|
||||||
|
@ -981,7 +976,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
|
|
||||||
DefaultController(AMQPSessionCallback sessionSPI) {
|
DefaultController(AMQPSessionCallback sessionSPI) {
|
||||||
this.sessionSPI = sessionSPI;
|
this.sessionSPI = sessionSPI;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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.
|
// Match the settlement mode of the remote instead of relying on the default of MIXED.
|
||||||
sender.setSenderSettleMode(sender.getRemoteSenderSettleMode());
|
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);
|
sender.setReceiverSettleMode(ReceiverSettleMode.FIRST);
|
||||||
|
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
|
@ -1259,7 +1253,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
queue = addressToUse;
|
queue = addressToUse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queue == null) {
|
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
|
// 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
|
// 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
|
// 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);
|
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 {
|
private SimpleString getMatchingQueue(SimpleString queueName, SimpleString address, RoutingType routingType, SimpleString filter, boolean matchFilter) throws Exception {
|
||||||
if (queueName != null) {
|
if (queueName != null) {
|
||||||
QueueQueryResult result = sessionSPI.queueQuery(CompositeAddress.toFullyQualified(address, queueName), routingType, true, filter);
|
QueueQueryResult result = sessionSPI.queueQuery(CompositeAddress.toFullyQualified(address, queueName), routingType, true, filter);
|
||||||
|
@ -1311,7 +1300,6 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws Exception {
|
public void close() throws Exception {
|
||||||
Source source = (Source) sender.getSource();
|
Source source = (Source) sender.getSource();
|
||||||
|
@ -1345,6 +1333,5 @@ public class ProtonServerSenderContext extends ProtonInitializable implements Pr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,11 @@ public class ProtonHandler extends ProtonInitializable implements SaslListener {
|
||||||
private Runnable afterFlush;
|
private Runnable afterFlush;
|
||||||
protected Set<Runnable> afterFlushSet;
|
protected Set<Runnable> afterFlushSet;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() throws Exception {
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
public void afterFlush(Runnable runnable) {
|
public void afterFlush(Runnable runnable) {
|
||||||
requireHandler();
|
requireHandler();
|
||||||
if (afterFlush == null) {
|
if (afterFlush == null) {
|
||||||
|
|
|
@ -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<Map.Entry<String, String>> includes = new LinkedHashSet<>();
|
||||||
|
includes.add(new SimpleEntry<>("a", "b"));
|
||||||
|
includes.add(new SimpleEntry<>("c", "d"));
|
||||||
|
final Set<Map.Entry<String, String>> excludes = new LinkedHashSet<>();
|
||||||
|
excludes.add(new SimpleEntry<>("e", "f"));
|
||||||
|
excludes.add(new SimpleEntry<>("g", "h"));
|
||||||
|
final Map<String, Object> properties1 = new HashMap<>();
|
||||||
|
properties1.put("amqpCredits", "10");
|
||||||
|
properties1.put("amqpLowCredits", "3");
|
||||||
|
final Map<String, Object> 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<Map.Entry<String, String>> 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<Map.Entry<String, String>> 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<Map.Entry<String, String>> includes = new LinkedHashSet<>();
|
||||||
|
includes.add(new SimpleEntry<>(null, "b"));
|
||||||
|
includes.add(new SimpleEntry<>("", "d"));
|
||||||
|
final Set<Map.Entry<String, String>> 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<Map.Entry<String, String>> includes,
|
||||||
|
Collection<Map.Entry<String, String>> excludes,
|
||||||
|
Map<String, Object> 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<String, Object> policyMap = (Map<String, Object>) ((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<String> flattenedIncludes = (List<String>) 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<String> flattenedExcludes = (List<String>) 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<String, String> encodedProperties = (Map<String, String>) 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<String> includes = new LinkedHashSet<>();
|
||||||
|
includes.add("a");
|
||||||
|
includes.add("b");
|
||||||
|
includes.add("c");
|
||||||
|
includes.add("d");
|
||||||
|
final Set<String> excludes = new LinkedHashSet<>();
|
||||||
|
excludes.add("e");
|
||||||
|
includes.add("f");
|
||||||
|
includes.add("g");
|
||||||
|
includes.add("h");
|
||||||
|
includes.add("I");
|
||||||
|
final Map<String, Object> properties1 = new HashMap<>();
|
||||||
|
properties1.put("amqpCredits", "10");
|
||||||
|
properties1.put("amqpLowCredits", "3");
|
||||||
|
final Map<String, Object> 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<String> includes,
|
||||||
|
Collection<String> excludes,
|
||||||
|
Map<String, Object> 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<String, Object> policyMap = (Map<String, Object>) ((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<String> encodedIncludes = (List<String>) 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<String> encodedExcludes = (List<String>) 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<String, String> encodedProperties = (Map<String, String>) 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<Map.Entry<String, String>> includes = new LinkedHashSet<>();
|
||||||
|
includes.add(new SimpleEntry<>("a", "b"));
|
||||||
|
final Set<Map.Entry<String, String>> 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<Map.Entry<String, String>> includes = new LinkedHashSet<>();
|
||||||
|
includes.add(new SimpleEntry<>("a", null));
|
||||||
|
final Set<Map.Entry<String, String>> 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<Map.Entry<String, String>> includes = new LinkedHashSet<>();
|
||||||
|
includes.add(new SimpleEntry<>("a", "b"));
|
||||||
|
includes.add(new SimpleEntry<>("c", "d"));
|
||||||
|
final Set<Map.Entry<String, String>> excludes = new LinkedHashSet<>();
|
||||||
|
excludes.add(new SimpleEntry<>("e", "f"));
|
||||||
|
excludes.add(new SimpleEntry<>("g", "h"));
|
||||||
|
final Map<String, String> 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<Map.Entry<String, String>> includes,
|
||||||
|
Collection<Map.Entry<String, String>> excludes,
|
||||||
|
Map<String, String> policyProperties) throws ActiveMQException {
|
||||||
|
final Properties properties = new Properties();
|
||||||
|
final Map<Symbol, Object> annotations = new LinkedHashMap<>();
|
||||||
|
final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations);
|
||||||
|
final Map<String, Object> 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<String> 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<String> 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<String> includes = new LinkedHashSet<>();
|
||||||
|
includes.add("a");
|
||||||
|
includes.add("b");
|
||||||
|
includes.add("c");
|
||||||
|
includes.add("d");
|
||||||
|
final Set<String> excludes = new LinkedHashSet<>();
|
||||||
|
excludes.add("e");
|
||||||
|
includes.add("f");
|
||||||
|
includes.add("g");
|
||||||
|
includes.add("h");
|
||||||
|
includes.add("I");
|
||||||
|
final Map<String, String> 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<String> includes,
|
||||||
|
Collection<String> excludes,
|
||||||
|
Map<String, String> policyProperties) throws ActiveMQException {
|
||||||
|
|
||||||
|
final Properties properties = new Properties();
|
||||||
|
final Map<Symbol, Object> annotations = new LinkedHashMap<>();
|
||||||
|
final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations);
|
||||||
|
final Map<String, Object> 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<Symbol, Object> annotations = new LinkedHashMap<>();
|
||||||
|
final MessageAnnotations messageAnnotations = new MessageAnnotations(annotations);
|
||||||
|
final Map<String, Object> 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<String> 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<Map.Entry<String, String>> includes = new LinkedHashSet<>();
|
||||||
|
includes.add(new SimpleEntry<>("a", "b"));
|
||||||
|
includes.add(new SimpleEntry<>("c", "d"));
|
||||||
|
final Set<Map.Entry<String, String>> excludes = new LinkedHashSet<>();
|
||||||
|
excludes.add(new SimpleEntry<>("e", "f"));
|
||||||
|
excludes.add(new SimpleEntry<>("g", "h"));
|
||||||
|
final Map<String, Object> properties1 = new HashMap<>();
|
||||||
|
properties1.put("amqpCredits", "10");
|
||||||
|
properties1.put("amqpLowCredits", "3");
|
||||||
|
final Map<String, Object> 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<Map.Entry<String, String>> includes,
|
||||||
|
Collection<Map.Entry<String, String>> excludes,
|
||||||
|
Map<String, Object> 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<String> includes = new LinkedHashSet<>();
|
||||||
|
includes.add("a");
|
||||||
|
includes.add("b");
|
||||||
|
includes.add("c");
|
||||||
|
includes.add("d");
|
||||||
|
final Set<String> excludes = new LinkedHashSet<>();
|
||||||
|
excludes.add("e");
|
||||||
|
includes.add("f");
|
||||||
|
includes.add("g");
|
||||||
|
includes.add("h");
|
||||||
|
includes.add("I");
|
||||||
|
final Map<String, Object> 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<String> includes,
|
||||||
|
Collection<String> excludes,
|
||||||
|
Map<String, Object> 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<String, ?> 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<String, ?> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
|
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
|
||||||
import org.apache.activemq.artemis.api.core.QueueConfiguration;
|
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.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.config.routing.ConnectionRouterConfiguration;
|
||||||
import org.apache.activemq.artemis.core.server.metrics.ActiveMQMetricsPlugin;
|
import org.apache.activemq.artemis.core.server.metrics.ActiveMQMetricsPlugin;
|
||||||
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerFederationPlugin;
|
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerFederationPlugin;
|
||||||
|
@ -1402,6 +1403,11 @@ public interface Configuration {
|
||||||
*/
|
*/
|
||||||
List<ActiveMQServerFederationPlugin> getBrokerFederationPlugins();
|
List<ActiveMQServerFederationPlugin> getBrokerFederationPlugins();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
List<AMQPFederationBrokerPlugin> getBrokerAMQPFederationPlugins();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -19,7 +19,6 @@ package org.apache.activemq.artemis.core.config.amqpBrokerConnectivity;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.activemq.artemis.api.core.TransportConfiguration;
|
import org.apache.activemq.artemis.api.core.TransportConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.brokerConnectivity.BrokerConnectConfiguration;
|
import org.apache.activemq.artemis.core.config.brokerConnectivity.BrokerConnectConfiguration;
|
||||||
import org.apache.activemq.artemis.uri.ConnectorTransportConfigurationParser;
|
import org.apache.activemq.artemis.uri.ConnectorTransportConfigurationParser;
|
||||||
|
@ -44,7 +43,6 @@ public class AMQPBrokerConnectConfiguration extends BrokerConnectConfiguration {
|
||||||
public AMQPBrokerConnectConfiguration addElement(AMQPBrokerConnectionElement amqpBrokerConnectionElement) {
|
public AMQPBrokerConnectConfiguration addElement(AMQPBrokerConnectionElement amqpBrokerConnectionElement) {
|
||||||
amqpBrokerConnectionElement.setParent(this);
|
amqpBrokerConnectionElement.setParent(this);
|
||||||
|
|
||||||
|
|
||||||
if (amqpBrokerConnectionElement.getType() == AMQPBrokerConnectionAddressType.MIRROR && !(amqpBrokerConnectionElement instanceof AMQPMirrorBrokerConnectionElement)) {
|
if (amqpBrokerConnectionElement.getType() == AMQPBrokerConnectionAddressType.MIRROR && !(amqpBrokerConnectionElement instanceof AMQPMirrorBrokerConnectionElement)) {
|
||||||
throw new IllegalArgumentException("must be an AMQPMirrorConnectionElement");
|
throw new IllegalArgumentException("must be an AMQPMirrorConnectionElement");
|
||||||
}
|
}
|
||||||
|
@ -62,6 +60,17 @@ public class AMQPBrokerConnectConfiguration extends BrokerConnectConfiguration {
|
||||||
return connectionElements;
|
return connectionElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AMQPBrokerConnectConfiguration addFederation(AMQPFederatedBrokerConnectionElement amqpFederationElement) {
|
||||||
|
return addElement(amqpFederationElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AMQPBrokerConnectionElement> 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
|
@Override
|
||||||
public void parseURI() throws Exception {
|
public void parseURI() throws Exception {
|
||||||
ConnectorTransportConfigurationParser parser = new ConnectorTransportConfigurationParser(false);
|
ConnectorTransportConfigurationParser parser = new ConnectorTransportConfigurationParser(false);
|
||||||
|
@ -117,5 +126,4 @@ public class AMQPBrokerConnectConfiguration extends BrokerConnectConfiguration {
|
||||||
super.setAutostart(autostart);
|
super.setAutostart(autostart);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,5 +17,5 @@
|
||||||
package org.apache.activemq.artemis.core.config.amqpBrokerConnectivity;
|
package org.apache.activemq.artemis.core.config.amqpBrokerConnectivity;
|
||||||
|
|
||||||
public enum AMQPBrokerConnectionAddressType {
|
public enum AMQPBrokerConnectionAddressType {
|
||||||
SENDER, RECEIVER, PEER, MIRROR
|
SENDER, RECEIVER, PEER, MIRROR, FEDERATION
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<AMQPFederationAddressPolicyElement> remoteAddressPolicies = new HashSet<>();
|
||||||
|
private Set<AMQPFederationQueuePolicyElement> remoteQueuePolicies = new HashSet<>();
|
||||||
|
|
||||||
|
private Set<AMQPFederationAddressPolicyElement> localAddressPolicies = new HashSet<>();
|
||||||
|
private Set<AMQPFederationQueuePolicyElement> localQueuePolicies = new HashSet<>();
|
||||||
|
|
||||||
|
private Map<String, Object> 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<AMQPFederationAddressPolicyElement> 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<AMQPFederationQueuePolicyElement> 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<AMQPFederationAddressPolicyElement> 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<AMQPFederationQueuePolicyElement> 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<String, Object> getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AddressMatch> includes = new HashSet<>();
|
||||||
|
private final Set<AddressMatch> excludes = new HashSet<>();
|
||||||
|
private final Map<String, Object> 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<AddressMatch> 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<AddressMatch> includes) {
|
||||||
|
this.includes.clear();
|
||||||
|
if (includes != null) {
|
||||||
|
this.includes.addAll(includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<AddressMatch> 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<AddressMatch> excludes) {
|
||||||
|
this.excludes.clear();
|
||||||
|
if (excludes != null) {
|
||||||
|
this.excludes.addAll(excludes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
|
@ -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<QueueMatch> includes = new HashSet<>();
|
||||||
|
private final Set<QueueMatch> excludes = new HashSet<>();
|
||||||
|
private final Map<String, Object> 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<QueueMatch> 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<QueueMatch> includes) {
|
||||||
|
this.includes.clear();
|
||||||
|
if (includes != null) {
|
||||||
|
this.includes.addAll(includes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<QueueMatch> 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<QueueMatch> 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<String, Object> getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AMQPFederationQueuePolicyElement setProperties(Map<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.TransformerConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.routing.ConnectionRouterConfiguration;
|
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.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.BridgeConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.ClusterConnectionConfiguration;
|
import org.apache.activemq.artemis.core.config.ClusterConnectionConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.Configuration;
|
import org.apache.activemq.artemis.core.config.Configuration;
|
||||||
|
@ -341,6 +342,7 @@ public class ConfigurationImpl implements Configuration, Serializable {
|
||||||
private final List<ActiveMQServerBridgePlugin> brokerBridgePlugins = new CopyOnWriteArrayList<>();
|
private final List<ActiveMQServerBridgePlugin> brokerBridgePlugins = new CopyOnWriteArrayList<>();
|
||||||
private final List<ActiveMQServerCriticalPlugin> brokerCriticalPlugins = new CopyOnWriteArrayList<>();
|
private final List<ActiveMQServerCriticalPlugin> brokerCriticalPlugins = new CopyOnWriteArrayList<>();
|
||||||
private final List<ActiveMQServerFederationPlugin> brokerFederationPlugins = new CopyOnWriteArrayList<>();
|
private final List<ActiveMQServerFederationPlugin> brokerFederationPlugins = new CopyOnWriteArrayList<>();
|
||||||
|
private final List<AMQPFederationBrokerPlugin> brokerAMQPFederationPlugins = new CopyOnWriteArrayList<>();
|
||||||
private final List<ActiveMQServerResourcePlugin> brokerResourcePlugins = new CopyOnWriteArrayList<>();
|
private final List<ActiveMQServerResourcePlugin> brokerResourcePlugins = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
private Map<String, Set<String>> securityRoleNameMappings = new HashMap<>();
|
private Map<String, Set<String>> securityRoleNameMappings = new HashMap<>();
|
||||||
|
@ -2155,6 +2157,9 @@ public class ConfigurationImpl implements Configuration, Serializable {
|
||||||
if (plugin instanceof ActiveMQServerFederationPlugin) {
|
if (plugin instanceof ActiveMQServerFederationPlugin) {
|
||||||
brokerFederationPlugins.add((ActiveMQServerFederationPlugin) plugin);
|
brokerFederationPlugins.add((ActiveMQServerFederationPlugin) plugin);
|
||||||
}
|
}
|
||||||
|
if (plugin instanceof AMQPFederationBrokerPlugin) {
|
||||||
|
brokerAMQPFederationPlugins.add((AMQPFederationBrokerPlugin) plugin);
|
||||||
|
}
|
||||||
if (plugin instanceof ActiveMQServerResourcePlugin) {
|
if (plugin instanceof ActiveMQServerResourcePlugin) {
|
||||||
brokerResourcePlugins.add((ActiveMQServerResourcePlugin) plugin);
|
brokerResourcePlugins.add((ActiveMQServerResourcePlugin) plugin);
|
||||||
}
|
}
|
||||||
|
@ -2193,6 +2198,9 @@ public class ConfigurationImpl implements Configuration, Serializable {
|
||||||
if (plugin instanceof ActiveMQServerFederationPlugin) {
|
if (plugin instanceof ActiveMQServerFederationPlugin) {
|
||||||
brokerFederationPlugins.remove(plugin);
|
brokerFederationPlugins.remove(plugin);
|
||||||
}
|
}
|
||||||
|
if (plugin instanceof AMQPFederationBrokerPlugin) {
|
||||||
|
brokerAMQPFederationPlugins.remove(plugin);
|
||||||
|
}
|
||||||
if (plugin instanceof ActiveMQServerResourcePlugin) {
|
if (plugin instanceof ActiveMQServerResourcePlugin) {
|
||||||
brokerResourcePlugins.remove(plugin);
|
brokerResourcePlugins.remove(plugin);
|
||||||
}
|
}
|
||||||
|
@ -2253,6 +2261,11 @@ public class ConfigurationImpl implements Configuration, Serializable {
|
||||||
return brokerFederationPlugins;
|
return brokerFederationPlugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AMQPFederationBrokerPlugin> getBrokerAMQPFederationPlugins() {
|
||||||
|
return brokerAMQPFederationPlugins;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<FederationConfiguration> getFederationConfigurations() {
|
public List<FederationConfiguration> getFederationConfigurations() {
|
||||||
return federationConfigurations;
|
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
|
// find the add X and init an instance of the type with name=name
|
||||||
|
|
||||||
StringBuilder addPropertyNameBuilder = new StringBuilder("add");
|
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) {
|
if (collectionPropertyName != null && collectionPropertyName.length() > 0) {
|
||||||
addPropertyNameBuilder.append(Character.toUpperCase(collectionPropertyName.charAt(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)
|
// we don't know the type, infer from add method add(X x) or add(String key, X x)
|
||||||
|
|
|
@ -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.TransformerConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.WildcardConfiguration;
|
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.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.AMQPBrokerConnectionAddressType;
|
||||||
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement;
|
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement;
|
||||||
import org.apache.activemq.artemis.core.config.routing.PoolConfiguration;
|
import org.apache.activemq.artemis.core.config.routing.PoolConfiguration;
|
||||||
|
@ -2152,7 +2155,6 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
|
||||||
|
|
||||||
mainConfig.addAMQPConnection(config);
|
mainConfig.addAMQPConnection(config);
|
||||||
|
|
||||||
|
|
||||||
NodeList senderList = e.getChildNodes();
|
NodeList senderList = e.getChildNodes();
|
||||||
for (int i = 0; i < senderList.getLength(); i++) {
|
for (int i = 0; i < senderList.getLength(); i++) {
|
||||||
if (senderList.item(i).getNodeType() == Node.ELEMENT_NODE) {
|
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);
|
amqpMirrorConnectionElement.setMessageAcknowledgements(messageAcks).setQueueCreation(queueCreation).setQueueRemoval(queueRemoval).setDurable(durable).setAddressFilter(addressFilter).setSync(sync);
|
||||||
connectionElement = amqpMirrorConnectionElement;
|
connectionElement = amqpMirrorConnectionElement;
|
||||||
connectionElement.setType(AMQPBrokerConnectionAddressType.MIRROR);
|
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 {
|
} else {
|
||||||
String match = getAttributeValue(e2, "address-match");
|
String match = getAttributeValue(e2, "address-match");
|
||||||
String queue = getAttributeValue(e2, "queue-name");
|
String queue = getAttributeValue(e2, "queue-name");
|
||||||
|
@ -2187,6 +2211,97 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
|
||||||
logger.debug("Adding AMQP connection :: {}", config);
|
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 {
|
private void parseClusterConnectionConfiguration(final Element e, final Configuration mainConfig) throws Exception {
|
||||||
String name = e.getAttribute("name");
|
String name = e.getAttribute("name");
|
||||||
|
|
||||||
|
@ -2676,6 +2791,10 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
|
||||||
return downstreamConfiguration;
|
return downstreamConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private FederationUpstreamConfiguration getBrokerConnections(final Element upstreamNode, final Configuration mainConfig) throws Exception {
|
||||||
|
return getFederationStream(new FederationUpstreamConfiguration(), upstreamNode, mainConfig);
|
||||||
|
}
|
||||||
|
|
||||||
private void getStaticConnectors(List<String> staticConnectorNames, Node child) {
|
private void getStaticConnectors(List<String> staticConnectorNames, Node child) {
|
||||||
NodeList children2 = ((Element) child).getElementsByTagName("connector-ref");
|
NodeList children2 = ((Element) child).getElementsByTagName("connector-ref");
|
||||||
|
|
||||||
|
|
|
@ -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.Configuration;
|
||||||
import org.apache.activemq.artemis.core.config.DivertConfiguration;
|
import org.apache.activemq.artemis.core.config.DivertConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.FederationConfiguration;
|
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.io.IOCriticalErrorListener;
|
||||||
import org.apache.activemq.artemis.core.management.impl.ActiveMQServerControlImpl;
|
import org.apache.activemq.artemis.core.management.impl.ActiveMQServerControlImpl;
|
||||||
import org.apache.activemq.artemis.core.paging.PagingManager;
|
import org.apache.activemq.artemis.core.paging.PagingManager;
|
||||||
|
@ -280,6 +281,8 @@ public interface ActiveMQServer extends ServiceComponent {
|
||||||
|
|
||||||
List<ActiveMQServerFederationPlugin> getBrokerFederationPlugins();
|
List<ActiveMQServerFederationPlugin> getBrokerFederationPlugins();
|
||||||
|
|
||||||
|
List<AMQPFederationBrokerPlugin> getBrokerAMQPFederationPlugins();
|
||||||
|
|
||||||
List<ActiveMQServerResourcePlugin> getBrokerResourcePlugins();
|
List<ActiveMQServerResourcePlugin> getBrokerResourcePlugins();
|
||||||
|
|
||||||
void callBrokerPlugins(ActiveMQPluginRunnable pluginRun) throws ActiveMQException;
|
void callBrokerPlugins(ActiveMQPluginRunnable pluginRun) throws ActiveMQException;
|
||||||
|
@ -307,6 +310,8 @@ public interface ActiveMQServer extends ServiceComponent {
|
||||||
|
|
||||||
void callBrokerFederationPlugins(ActiveMQPluginRunnable<ActiveMQServerFederationPlugin> pluginRun) throws ActiveMQException;
|
void callBrokerFederationPlugins(ActiveMQPluginRunnable<ActiveMQServerFederationPlugin> pluginRun) throws ActiveMQException;
|
||||||
|
|
||||||
|
void callBrokerAMQPFederationPlugins(ActiveMQPluginRunnable<AMQPFederationBrokerPlugin> pluginRun) throws ActiveMQException;
|
||||||
|
|
||||||
void callBrokerResourcePlugins(ActiveMQPluginRunnable<ActiveMQServerResourcePlugin> pluginRun) throws ActiveMQException;
|
void callBrokerResourcePlugins(ActiveMQPluginRunnable<ActiveMQServerResourcePlugin> pluginRun) throws ActiveMQException;
|
||||||
|
|
||||||
boolean hasBrokerPlugins();
|
boolean hasBrokerPlugins();
|
||||||
|
@ -331,6 +336,8 @@ public interface ActiveMQServer extends ServiceComponent {
|
||||||
|
|
||||||
boolean hasBrokerFederationPlugins();
|
boolean hasBrokerFederationPlugins();
|
||||||
|
|
||||||
|
boolean hasBrokerAMQPFederationPlugins();
|
||||||
|
|
||||||
boolean hasBrokerResourcePlugins();
|
boolean hasBrokerResourcePlugins();
|
||||||
|
|
||||||
void checkQueueCreationLimit(String username) throws Exception;
|
void checkQueueCreationLimit(String username) throws Exception;
|
||||||
|
|
|
@ -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.FederationConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.HAPolicyConfiguration;
|
import org.apache.activemq.artemis.core.config.HAPolicyConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.StoreConfiguration;
|
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.ConfigurationImpl;
|
||||||
import org.apache.activemq.artemis.core.config.impl.LegacyJMSConfiguration;
|
import org.apache.activemq.artemis.core.config.impl.LegacyJMSConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.storage.DatabaseStorageConfiguration;
|
import org.apache.activemq.artemis.core.config.storage.DatabaseStorageConfiguration;
|
||||||
|
@ -2650,6 +2651,11 @@ public class ActiveMQServerImpl implements ActiveMQServer {
|
||||||
return configuration.getBrokerFederationPlugins();
|
return configuration.getBrokerFederationPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AMQPFederationBrokerPlugin> getBrokerAMQPFederationPlugins() {
|
||||||
|
return configuration.getBrokerAMQPFederationPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ActiveMQServerResourcePlugin> getBrokerResourcePlugins() {
|
public List<ActiveMQServerResourcePlugin> getBrokerResourcePlugins() {
|
||||||
return configuration.getBrokerResourcePlugins();
|
return configuration.getBrokerResourcePlugins();
|
||||||
|
@ -2731,6 +2737,11 @@ public class ActiveMQServerImpl implements ActiveMQServer {
|
||||||
callBrokerPlugins(getBrokerFederationPlugins(), pluginRun);
|
callBrokerPlugins(getBrokerFederationPlugins(), pluginRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void callBrokerAMQPFederationPlugins(final ActiveMQPluginRunnable<AMQPFederationBrokerPlugin> pluginRun) throws ActiveMQException {
|
||||||
|
callBrokerPlugins(getBrokerAMQPFederationPlugins(), pluginRun);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void callBrokerResourcePlugins(final ActiveMQPluginRunnable<ActiveMQServerResourcePlugin> pluginRun) throws ActiveMQException {
|
public void callBrokerResourcePlugins(final ActiveMQPluginRunnable<ActiveMQServerResourcePlugin> pluginRun) throws ActiveMQException {
|
||||||
callBrokerPlugins(getBrokerResourcePlugins(), pluginRun);
|
callBrokerPlugins(getBrokerResourcePlugins(), pluginRun);
|
||||||
|
@ -2808,6 +2819,11 @@ public class ActiveMQServerImpl implements ActiveMQServer {
|
||||||
return !getBrokerFederationPlugins().isEmpty();
|
return !getBrokerFederationPlugins().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasBrokerAMQPFederationPlugins() {
|
||||||
|
return !getBrokerAMQPFederationPlugins().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasBrokerResourcePlugins() {
|
public boolean hasBrokerResourcePlugins() {
|
||||||
return !getBrokerResourcePlugins().isEmpty();
|
return !getBrokerResourcePlugins().isEmpty();
|
||||||
|
|
|
@ -2047,6 +2047,7 @@
|
||||||
<xsd:element name="receiver" type="amqp-address-match-type"/>
|
<xsd:element name="receiver" type="amqp-address-match-type"/>
|
||||||
<xsd:element name="peer" type="amqp-address-match-type"/>
|
<xsd:element name="peer" type="amqp-address-match-type"/>
|
||||||
<xsd:element name="mirror" type="amqp-mirror-type"/>
|
<xsd:element name="mirror" type="amqp-mirror-type"/>
|
||||||
|
<xsd:element name="federation" type="amqp-federation-type"/>
|
||||||
</xsd:choice>
|
</xsd:choice>
|
||||||
</xsd:sequence>
|
</xsd:sequence>
|
||||||
<xsd:attribute name="uri" type="xsd:string" use="required">
|
<xsd:attribute name="uri" type="xsd:string" use="required">
|
||||||
|
@ -2175,6 +2176,102 @@
|
||||||
|
|
||||||
</xsd:complexType>
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="amqp-federation-type">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
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.
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
<xsd:sequence minOccurs="1" maxOccurs="unbounded">
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="local-queue-policy" type="amqpFederationQueuePolicyType" />
|
||||||
|
<xsd:element name="local-address-policy" type="amqpFederationAddressPolicyType" />
|
||||||
|
<xsd:element name="remote-queue-policy" type="amqpFederationQueuePolicyType" />
|
||||||
|
<xsd:element name="remote-address-policy" type="amqpFederationAddressPolicyType" />
|
||||||
|
</xsd:choice>
|
||||||
|
<xsd:element ref="property" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
Optional properties that can be applied to the federation configuration
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:sequence>
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="amqpFederationQueuePolicyType">
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="include" type="queueMatchType" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xsd:element name="exclude" type="queueMatchType" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xsd:element ref="property" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
Optional properties that can be applied to the Queue federation policy
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||||
|
<xsd:element name="transformer-class-name" type="xsd:string" minOccurs="0" maxOccurs="1">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
an optional class name of a transformer
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="transformer" type="transformerType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
optional transformer configuration
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="priority-adjustment" type="xsd:int" use="optional" />
|
||||||
|
<xsd:attribute name="include-federated" type="xsd:boolean" use="optional" />
|
||||||
|
<xsd:attribute name="name" type="xsd:ID" use="required" />
|
||||||
|
<xsd:attributeGroup ref="xml:specialAttrs"/>
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
|
<xsd:complexType name="amqpFederationAddressPolicyType">
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="include" type="addressMatchType" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xsd:element name="exclude" type="addressMatchType" minOccurs="0" maxOccurs="unbounded"/>
|
||||||
|
<xsd:element ref="property" minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
Optional properties that can be applied to the Address federation policy
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||||
|
<xsd:element name="transformer-class-name" type="xsd:string" minOccurs="0" maxOccurs="1">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
an optional class name of a transformer
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="transformer" type="transformerType" minOccurs="0" maxOccurs="1">
|
||||||
|
<xsd:annotation>
|
||||||
|
<xsd:documentation>
|
||||||
|
optional transformer configuration
|
||||||
|
</xsd:documentation>
|
||||||
|
</xsd:annotation>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="auto-delete" type="xsd:boolean" use="optional" />
|
||||||
|
<xsd:attribute name="auto-delete-delay" type="xsd:long" use="optional" />
|
||||||
|
<xsd:attribute name="auto-delete-message-count" type="xsd:long" use="optional" />
|
||||||
|
<xsd:attribute name="max-hops" type="xsd:int" use="optional" />
|
||||||
|
<xsd:attribute name="name" type="xsd:ID" use="required" />
|
||||||
|
<xsd:attribute name="enable-divert-bindings" type="xsd:boolean" use="optional" />
|
||||||
|
<xsd:attributeGroup ref="xml:specialAttrs"/>
|
||||||
|
</xsd:complexType>
|
||||||
|
|
||||||
<xsd:complexType name="cluster-connectionUriType">
|
<xsd:complexType name="cluster-connectionUriType">
|
||||||
<xsd:attribute name="address" type="xsd:string" use="required">
|
<xsd:attribute name="address" type="xsd:string" use="required">
|
||||||
<xsd:annotation>
|
<xsd:annotation>
|
||||||
|
|
|
@ -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.AMQPBrokerConnectConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectionAddressType;
|
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.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.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement;
|
||||||
import org.apache.activemq.artemis.core.config.federation.FederationAddressPolicyConfiguration;
|
import org.apache.activemq.artemis.core.config.federation.FederationAddressPolicyConfiguration;
|
||||||
import org.apache.activemq.artemis.core.config.federation.FederationPolicySet;
|
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.mockito.Mockito;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
@ -768,6 +772,256 @@ public class ConfigurationImplTest extends ActiveMQTestBase {
|
||||||
Assert.assertEquals("foo", amqpMirrorBrokerConnectionElement.getAddressFilter());
|
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<AMQPFederationAddressPolicyElement> 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<AMQPFederationQueuePolicyElement> 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
|
@Test
|
||||||
public void testAMQPConnectionsConfigurationUriEnc() throws Throwable {
|
public void testAMQPConnectionsConfigurationUriEnc() throws Throwable {
|
||||||
|
|
|
@ -447,6 +447,11 @@
|
||||||
<peer address-match="TEST-PEER"/>
|
<peer address-match="TEST-PEER"/>
|
||||||
<receiver queue-name="TEST-WITH-QUEUE-NAME"/>
|
<receiver queue-name="TEST-WITH-QUEUE-NAME"/>
|
||||||
<mirror message-acknowledgements="false" queue-creation="false" durable="false" queue-removal="false" address-filter="TEST-QUEUE,!IGNORE-QUEUE" sync="true"/>
|
<mirror message-acknowledgements="false" queue-creation="false" durable="false" queue-removal="false" address-filter="TEST-QUEUE,!IGNORE-QUEUE" sync="true"/>
|
||||||
|
<federation>
|
||||||
|
<local-queue-policy name="composite" priority-adjustment="1" include-federated="false">
|
||||||
|
<include address-match="test" queue-match="tracking" />
|
||||||
|
</local-queue-policy>
|
||||||
|
</federation>
|
||||||
</amqp-connection>
|
</amqp-connection>
|
||||||
<amqp-connection uri="tcp://test2:222" name="test2">
|
<amqp-connection uri="tcp://test2:222" name="test2">
|
||||||
<mirror durable="false"/>
|
<mirror durable="false"/>
|
||||||
|
@ -454,6 +459,41 @@
|
||||||
<amqp-connection uri="tcp://false" name="auto-start-false" auto-start="false">
|
<amqp-connection uri="tcp://false" name="auto-start-false" auto-start="false">
|
||||||
<mirror/>
|
<mirror/>
|
||||||
</amqp-connection>
|
</amqp-connection>
|
||||||
|
<amqp-connection uri="tcp://federation" name="federation-test" auto-start="false">
|
||||||
|
<federation>
|
||||||
|
<local-queue-policy name="lqp1" priority-adjustment="1" include-federated="false">
|
||||||
|
<include address-match="test" queue-match="tracking" />
|
||||||
|
<property key="amqpCredits" value="1"/>
|
||||||
|
<transformer-class-name>class-another</transformer-class-name>
|
||||||
|
</local-queue-policy>
|
||||||
|
<remote-queue-policy name="rqp1" priority-adjustment="-1" include-federated="true">
|
||||||
|
<include address-match="#" queue-match="tracking" />
|
||||||
|
<property key="amqpCredits" value="2"/>
|
||||||
|
<property key="amqpLowCredits" value="1"/>
|
||||||
|
</remote-queue-policy>
|
||||||
|
<local-address-policy name="lap1" auto-delete="false" auto-delete-delay="1" auto-delete-message-count="12" max-hops="2" enable-divert-bindings="true">
|
||||||
|
<include address-match="orders" />
|
||||||
|
<exclude address-match="all.#" />
|
||||||
|
<transformer-class-name>class-name</transformer-class-name>
|
||||||
|
</local-address-policy>
|
||||||
|
<local-queue-policy name="lqp2">
|
||||||
|
<include address-match="address" queue-match="theQueue" />
|
||||||
|
<transformer-class-name>class-another</transformer-class-name>
|
||||||
|
</local-queue-policy>
|
||||||
|
<remote-address-policy name="rap1" auto-delete="true" auto-delete-delay="2" auto-delete-message-count="42" max-hops="1" enable-divert-bindings="false">
|
||||||
|
<include address-match="support" />
|
||||||
|
<property key="amqpCredits" value="2"/>
|
||||||
|
<property key="amqpLowCredits" value="1"/>
|
||||||
|
<transformer>
|
||||||
|
<class-name>something</class-name>
|
||||||
|
<property key="key1" value="value1"/>
|
||||||
|
<property key="key2" value="value2"/>
|
||||||
|
</transformer>
|
||||||
|
</remote-address-policy>
|
||||||
|
<property key="amqpCredits" value="7"/>
|
||||||
|
<property key="amqpLowCredits" value="1"/>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
</broker-connections>
|
</broker-connections>
|
||||||
<grouping-handler name="gh1">
|
<grouping-handler name="gh1">
|
||||||
<type>LOCAL</type>
|
<type>LOCAL</type>
|
||||||
|
|
|
@ -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".
|
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.
|
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 `<federation>` element within the `<amqp-connection>` element to configure federation to the broker instance, the `<amqp-connection>` 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]
|
||||||
|
----
|
||||||
|
<broker-connections>
|
||||||
|
<amqp-connection uri="tcp://HOST:PORT" name="federation-example">
|
||||||
|
<federation>
|
||||||
|
<local-address-policy name="example-local-address-policy">
|
||||||
|
<include address-match="local-address.#" />
|
||||||
|
<exclude address-match="local-address.excluded" />
|
||||||
|
</local-address-policy>
|
||||||
|
<local-queue-policy name="example-local-queue-policy">
|
||||||
|
<include address-match="address" queue-match="local-queue" />
|
||||||
|
</local-queue-policy>
|
||||||
|
<remote-address-policy name="example-remote-address-policy">
|
||||||
|
<include address-match="remote-address" />
|
||||||
|
</remote-address-policy>
|
||||||
|
<remote-queue-policy name="example-remote-queue-policy">
|
||||||
|
<include address-match="#" queue-match="remote-queue" />
|
||||||
|
<exclude address-match="excluded.#" queue-match="remote-queue-excluded" />
|
||||||
|
</remote-queue-policy>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
|
</broker-connections>
|
||||||
|
----
|
||||||
|
|
||||||
|
===== 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]
|
||||||
|
----
|
||||||
|
<broker-connections>
|
||||||
|
<amqp-connection uri="tcp://HOST:PORT" name="federation-example">
|
||||||
|
<federation>
|
||||||
|
<local-address-policy name="example-local-address-policy"
|
||||||
|
auto-delete="false"
|
||||||
|
auto-delete-delay="1"
|
||||||
|
auto-delete-message-count="12"
|
||||||
|
max-hops="1"
|
||||||
|
enable-divert-bindings="true">
|
||||||
|
<include address-match="local-address.#" />
|
||||||
|
<exclude address-match="local-address.excluded" />
|
||||||
|
</local-address-policy>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
|
</broker-connections>
|
||||||
|
----
|
||||||
|
|
||||||
|
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]
|
||||||
|
----
|
||||||
|
<broker-connections>
|
||||||
|
<amqp-connection uri="tcp://HOST:PORT" name="federation-example">
|
||||||
|
<federation>
|
||||||
|
<local-queue-policy name="example-local-queue-policy">
|
||||||
|
<include address-match="#" queue-match="remote-queue" />
|
||||||
|
<exclude address-match="excluded.#" queue-match="remote-queue-excluded" />
|
||||||
|
</local-queue-policy>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
|
</broker-connections>
|
||||||
|
----
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
<?xml version='1.0'?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.apache.activemq.examples.broker-connection</groupId>
|
||||||
|
<artifactId>broker-connections</artifactId>
|
||||||
|
<version>2.31.0-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>amqp-federation</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<name>amqp-federation</name>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.qpid</groupId>
|
||||||
|
<artifactId>qpid-jms-client</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.activemq</groupId>
|
||||||
|
<artifactId>artemis-maven-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>create0</id>
|
||||||
|
<goals>
|
||||||
|
<goal>create</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<ignore>${noServer}</ignore>
|
||||||
|
<instance>${basedir}/target/server0</instance>
|
||||||
|
<allowAnonymous>true</allowAnonymous>
|
||||||
|
<configuration>${basedir}/target/classes/activemq/server0</configuration>
|
||||||
|
<!-- this makes it easier in certain envs -->
|
||||||
|
<javaOptions>-Djava.net.preferIPv4Stack=true</javaOptions>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>create1</id>
|
||||||
|
<goals>
|
||||||
|
<goal>create</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<ignore>${noServer}</ignore>
|
||||||
|
<instance>${basedir}/target/server1</instance>
|
||||||
|
<allowAnonymous>true</allowAnonymous>
|
||||||
|
<configuration>${basedir}/target/classes/activemq/server1</configuration>
|
||||||
|
<!-- this makes it easier in certain envs -->
|
||||||
|
<javaOptions>-Djava.net.preferIPv4Stack=true</javaOptions>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<!-- we first start broker 1, to avoid reconnecting statements -->
|
||||||
|
<execution>
|
||||||
|
<id>start1</id>
|
||||||
|
<goals>
|
||||||
|
<goal>cli</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<ignore>${noServer}</ignore>
|
||||||
|
<spawn>true</spawn>
|
||||||
|
<location>${basedir}/target/server1</location>
|
||||||
|
<testURI>tcp://localhost:5771</testURI>
|
||||||
|
<args>
|
||||||
|
<param>run</param>
|
||||||
|
</args>
|
||||||
|
<name>server1</name>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>start0</id>
|
||||||
|
<goals>
|
||||||
|
<goal>cli</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<spawn>true</spawn>
|
||||||
|
<ignore>${noServer}</ignore>
|
||||||
|
<location>${basedir}/target/server0</location>
|
||||||
|
<testURI>tcp://localhost:5660</testURI>
|
||||||
|
<args>
|
||||||
|
<param>run</param>
|
||||||
|
</args>
|
||||||
|
<name>server0</name>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>runClient</id>
|
||||||
|
<goals>
|
||||||
|
<goal>runClient</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<!-- you may have to set export MAVEN_OPTS="-Djava.net.preferIPv4Stack=true"
|
||||||
|
if you are on MacOS for instance -->
|
||||||
|
<clientClass>org.apache.activemq.artemis.jms.example.BrokerFederationExample</clientClass>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>stop0</id>
|
||||||
|
<goals>
|
||||||
|
<goal>cli</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<ignore>${noServer}</ignore>
|
||||||
|
<location>${basedir}/target/server0</location>
|
||||||
|
<args>
|
||||||
|
<param>stop</param>
|
||||||
|
</args>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>stop1</id>
|
||||||
|
<goals>
|
||||||
|
<goal>cli</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<ignore>${noServer}</ignore>
|
||||||
|
<location>${basedir}/target/server1</location>
|
||||||
|
<args>
|
||||||
|
<param>stop</param>
|
||||||
|
</args>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.activemq.examples.broker-connection</groupId>
|
||||||
|
<artifactId>amqp-federation</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-clean-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
|
@ -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.
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?xml version='1.0'?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration xmlns="urn:activemq"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xsi:schemaLocation="urn:activemq /schema/artemis-configuration.xsd">
|
||||||
|
|
||||||
|
<core xmlns="urn:activemq:core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="urn:activemq:core ">
|
||||||
|
|
||||||
|
<name>0.0.0.0</name>
|
||||||
|
|
||||||
|
<persistence-enabled>false</persistence-enabled>
|
||||||
|
|
||||||
|
<journal-type>NIO</journal-type>
|
||||||
|
|
||||||
|
<!-- should the broker detect dead locks and other issues -->
|
||||||
|
<critical-analyzer>true</critical-analyzer>
|
||||||
|
|
||||||
|
<critical-analyzer-timeout>120000</critical-analyzer-timeout>
|
||||||
|
|
||||||
|
<critical-analyzer-check-period>60000</critical-analyzer-check-period>
|
||||||
|
|
||||||
|
<critical-analyzer-policy>HALT</critical-analyzer-policy>
|
||||||
|
|
||||||
|
<page-sync-timeout>44000</page-sync-timeout>
|
||||||
|
|
||||||
|
<acceptors>
|
||||||
|
<!-- Acceptor for every supported protocol -->
|
||||||
|
<acceptor name="artemis">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</acceptor>
|
||||||
|
</acceptors>
|
||||||
|
|
||||||
|
<broker-connections>
|
||||||
|
<amqp-connection uri="tcp://localhost:5771" name="federation-example" retry-interval="100">
|
||||||
|
<!-- This will create a federation connection between servers, the local
|
||||||
|
server will federate messages sent to the address 'orders' from the
|
||||||
|
remote and the remote will federate message sent to the tracking queue
|
||||||
|
on the local broker to itself. -->
|
||||||
|
<federation>
|
||||||
|
<local-address-policy name="address-federation-from-remote">
|
||||||
|
<include address-match="orders" />
|
||||||
|
</local-address-policy>
|
||||||
|
<remote-queue-policy name="queue-federation-to-remote">
|
||||||
|
<include address-match="#" queue-match="tracking" />
|
||||||
|
</remote-queue-policy>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
|
</broker-connections>
|
||||||
|
|
||||||
|
<security-settings>
|
||||||
|
<security-setting match="#">
|
||||||
|
<permission type="createNonDurableQueue" roles="guest"/>
|
||||||
|
<permission type="deleteNonDurableQueue" roles="guest"/>
|
||||||
|
<permission type="createDurableQueue" roles="guest"/>
|
||||||
|
<permission type="deleteDurableQueue" roles="guest"/>
|
||||||
|
<permission type="createAddress" roles="guest"/>
|
||||||
|
<permission type="deleteAddress" roles="guest"/>
|
||||||
|
<permission type="consume" roles="guest"/>
|
||||||
|
<permission type="browse" roles="guest"/>
|
||||||
|
<permission type="send" roles="guest"/>
|
||||||
|
<permission type="manage" roles="guest"/>
|
||||||
|
</security-setting>
|
||||||
|
</security-settings>
|
||||||
|
|
||||||
|
<address-settings>
|
||||||
|
<!-- if you define auto-create on certain queues, management has to be auto-create -->
|
||||||
|
<address-setting match="activemq.management#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
<!--default for catch all-->
|
||||||
|
<address-setting match="#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
</address-settings>
|
||||||
|
|
||||||
|
<addresses>
|
||||||
|
<address name="orders">
|
||||||
|
<multicast>
|
||||||
|
</multicast>
|
||||||
|
</address>
|
||||||
|
<address name="tracking">
|
||||||
|
<anycast>
|
||||||
|
<queue name="tracking" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
</addresses>
|
||||||
|
|
||||||
|
</core>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version='1.0'?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration xmlns="urn:activemq"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xsi:schemaLocation="urn:activemq /schema/artemis-configuration.xsd">
|
||||||
|
|
||||||
|
<core xmlns="urn:activemq:core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="urn:activemq:core ">
|
||||||
|
|
||||||
|
<name>0.0.0.0</name>
|
||||||
|
|
||||||
|
<persistence-enabled>false</persistence-enabled>
|
||||||
|
|
||||||
|
<journal-type>NIO</journal-type>
|
||||||
|
|
||||||
|
<!-- should the broker detect dead locks and other issues -->
|
||||||
|
<critical-analyzer>true</critical-analyzer>
|
||||||
|
|
||||||
|
<critical-analyzer-timeout>120000</critical-analyzer-timeout>
|
||||||
|
|
||||||
|
<critical-analyzer-check-period>60000</critical-analyzer-check-period>
|
||||||
|
|
||||||
|
<critical-analyzer-policy>HALT</critical-analyzer-policy>
|
||||||
|
|
||||||
|
<page-sync-timeout>44000</page-sync-timeout>
|
||||||
|
|
||||||
|
<acceptors>
|
||||||
|
<!-- Acceptor for every supported protocol -->
|
||||||
|
<acceptor name="artemis">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</acceptor>
|
||||||
|
</acceptors>
|
||||||
|
|
||||||
|
<security-settings>
|
||||||
|
<security-setting match="#">
|
||||||
|
<permission type="createNonDurableQueue" roles="guest"/>
|
||||||
|
<permission type="deleteNonDurableQueue" roles="guest"/>
|
||||||
|
<permission type="createDurableQueue" roles="guest"/>
|
||||||
|
<permission type="deleteDurableQueue" roles="guest"/>
|
||||||
|
<permission type="createAddress" roles="guest"/>
|
||||||
|
<permission type="deleteAddress" roles="guest"/>
|
||||||
|
<permission type="consume" roles="guest"/>
|
||||||
|
<permission type="browse" roles="guest"/>
|
||||||
|
<permission type="send" roles="guest"/>
|
||||||
|
<permission type="manage" roles="guest"/>
|
||||||
|
</security-setting>
|
||||||
|
</security-settings>
|
||||||
|
|
||||||
|
<address-settings>
|
||||||
|
<!-- if you define auto-create on certain queues, management has to be auto-create -->
|
||||||
|
<address-setting match="activemq.management#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
<!--default for catch all-->
|
||||||
|
<address-setting match="#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
</address-settings>
|
||||||
|
|
||||||
|
<addresses>
|
||||||
|
<address name="orders">
|
||||||
|
<multicast>
|
||||||
|
</multicast>
|
||||||
|
</address>
|
||||||
|
<address name="tracking">
|
||||||
|
<anycast>
|
||||||
|
<queue name="tracking" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
</addresses>
|
||||||
|
|
||||||
|
</core>
|
||||||
|
</configuration>
|
|
@ -51,6 +51,7 @@ under the License.
|
||||||
<module>amqp-sending-messages-multicast</module>
|
<module>amqp-sending-messages-multicast</module>
|
||||||
<module>amqp-receiving-messages</module>
|
<module>amqp-receiving-messages</module>
|
||||||
<module>amqp-sending-overssl</module>
|
<module>amqp-sending-overssl</module>
|
||||||
|
<module>amqp-federation</module>
|
||||||
<module>disaster-recovery</module>
|
<module>disaster-recovery</module>
|
||||||
</modules>
|
</modules>
|
||||||
</profile>
|
</profile>
|
||||||
|
@ -61,6 +62,7 @@ under the License.
|
||||||
<module>amqp-sending-messages-multicast</module>
|
<module>amqp-sending-messages-multicast</module>
|
||||||
<module>amqp-receiving-messages</module>
|
<module>amqp-receiving-messages</module>
|
||||||
<module>amqp-sending-overssl</module>
|
<module>amqp-sending-overssl</module>
|
||||||
|
<module>amqp-federation</module>
|
||||||
<module>disaster-recovery</module>
|
<module>disaster-recovery</module>
|
||||||
</modules>
|
</modules>
|
||||||
</profile>
|
</profile>
|
||||||
|
|
13
pom.xml
13
pom.xml
|
@ -129,6 +129,7 @@
|
||||||
<!-- this is basically for tests -->
|
<!-- this is basically for tests -->
|
||||||
<netty-tcnative-version>2.0.61.Final</netty-tcnative-version>
|
<netty-tcnative-version>2.0.61.Final</netty-tcnative-version>
|
||||||
<proton.version>0.34.1</proton.version>
|
<proton.version>0.34.1</proton.version>
|
||||||
|
<protonj2.version>1.0.0-M17</protonj2.version>
|
||||||
<slf4j.version>1.7.36</slf4j.version>
|
<slf4j.version>1.7.36</slf4j.version>
|
||||||
<log4j.version>2.20.0</log4j.version>
|
<log4j.version>2.20.0</log4j.version>
|
||||||
<qpid.jms.version>1.10.0</qpid.jms.version>
|
<qpid.jms.version>1.10.0</qpid.jms.version>
|
||||||
|
@ -235,6 +236,7 @@
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
<activemq-surefire-argline>-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
|
<activemq-surefire-argline>-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.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}
|
-Djava.net.preferIPv4Stack=true -Dbasedir=${basedir}
|
||||||
|
@ -243,6 +245,7 @@
|
||||||
</activemq-surefire-argline>
|
</activemq-surefire-argline>
|
||||||
<activemq.basedir>${project.basedir}</activemq.basedir>
|
<activemq.basedir>${project.basedir}</activemq.basedir>
|
||||||
<skipOWASP>true</skipOWASP>
|
<skipOWASP>true</skipOWASP>
|
||||||
|
<proton.trace.frames>false</proton.trace.frames>
|
||||||
|
|
||||||
<directory-version>2.0.0.AM25</directory-version>
|
<directory-version>2.0.0.AM25</directory-version>
|
||||||
<directory-jdbm2-version>2.0.0-M1</directory-jdbm2-version>
|
<directory-jdbm2-version>2.0.0-M1</directory-jdbm2-version>
|
||||||
|
@ -728,6 +731,13 @@
|
||||||
<version>${proton.version}</version>
|
<version>${proton.version}</version>
|
||||||
<!-- License: Apache 2.0 -->
|
<!-- License: Apache 2.0 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.qpid</groupId>
|
||||||
|
<artifactId>protonj2-test-driver</artifactId>
|
||||||
|
<version>${protonj2.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
<!-- License: Apache 2.0 -->
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.qpid</groupId>
|
<groupId>org.apache.qpid</groupId>
|
||||||
<artifactId>qpid-jms-client</artifactId>
|
<artifactId>qpid-jms-client</artifactId>
|
||||||
|
@ -1671,6 +1681,9 @@
|
||||||
<runOrder>alphabetical</runOrder>
|
<runOrder>alphabetical</runOrder>
|
||||||
<redirectTestOutputToFile>${maven.test.redirectTestOutputToFile}</redirectTestOutputToFile>
|
<redirectTestOutputToFile>${maven.test.redirectTestOutputToFile}</redirectTestOutputToFile>
|
||||||
<argLine>${activemq-surefire-argline}</argLine>
|
<argLine>${activemq-surefire-argline}</argLine>
|
||||||
|
<environmentVariables>
|
||||||
|
<PN_TRACE_FRM>${proton.trace.frames}</PN_TRACE_FRM>
|
||||||
|
</environmentVariables>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|
|
@ -302,6 +302,11 @@
|
||||||
<artifactId>proton-j</artifactId>
|
<artifactId>proton-j</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.qpid</groupId>
|
||||||
|
<artifactId>protonj2-test-driver</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<!-- logging -->
|
<!-- logging -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
|
@ -435,7 +440,6 @@
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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<org.apache.activemq.artemis.api.core.Message> messagePreHandled = new AtomicReference<>();
|
||||||
|
final AtomicReference<org.apache.activemq.artemis.api.core.Message> 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<org.apache.activemq.artemis.api.core.Message> messagePreHandled = new AtomicReference<>();
|
||||||
|
final AtomicReference<org.apache.activemq.artemis.api.core.Message> 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<org.apache.activemq.artemis.api.core.Message> 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<org.apache.activemq.artemis.api.core.Message> 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<org.apache.activemq.artemis.api.core.Message> 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<FederationConsumerInfo> beforeCreateConsumerCapture = new AtomicReference<>();
|
||||||
|
public final AtomicReference<FederationConsumer> afterCreateConsumerCapture = new AtomicReference<>();
|
||||||
|
public final AtomicReference<FederationConsumer> beforeCloseConsumerCapture = new AtomicReference<>();
|
||||||
|
public final AtomicReference<FederationConsumer> afterCloseConsumerCapture = new AtomicReference<>();
|
||||||
|
|
||||||
|
public Consumer<FederationConsumerInfo> beforeCreateConsumer = (c) -> beforeCreateConsumerCapture.set(c);;
|
||||||
|
public Consumer<FederationConsumer> afterCreateConsumer = (c) -> afterCreateConsumerCapture.set(c);
|
||||||
|
public Consumer<FederationConsumer> beforeCloseConsumer = (c) -> beforeCloseConsumerCapture.set(c);
|
||||||
|
public Consumer<FederationConsumer> afterCloseConsumer = (c) -> afterCloseConsumerCapture.set(c);
|
||||||
|
|
||||||
|
public BiConsumer<FederationConsumer, org.apache.activemq.artemis.api.core.Message> beforeMessageHandled = (c, m) -> { };
|
||||||
|
public BiConsumer<FederationConsumer, org.apache.activemq.artemis.api.core.Message> afterMessageHandled = (c, m) -> { };
|
||||||
|
|
||||||
|
public Function<AddressInfo, Boolean> shouldCreateConsumerForAddress = (a) -> true;
|
||||||
|
public Function<Queue, Boolean> shouldCreateConsumerForQueue = (q) -> true;
|
||||||
|
public BiFunction<Divert, Queue, Boolean> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> 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<String, Object> policyMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
final List<String> includes = new ArrayList<>();
|
||||||
|
includes.add("a");
|
||||||
|
includes.add("b");
|
||||||
|
includes.add("c");
|
||||||
|
includes.add("d");
|
||||||
|
final List<String> 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<String, Object> policyMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
final List<String> includes = new ArrayList<>();
|
||||||
|
includes.add("include");
|
||||||
|
final List<String> 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();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1056,6 +1056,41 @@
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
|
||||||
|
<!-- used on DualFederationTest -->
|
||||||
|
<execution>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<id>createBrokerConnectFederationA</id>
|
||||||
|
<goals>
|
||||||
|
<goal>create</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<role>amq</role>
|
||||||
|
<allowAnonymous>true</allowAnonymous>
|
||||||
|
<user>A</user>
|
||||||
|
<password>A</password>
|
||||||
|
<noWeb>true</noWeb>
|
||||||
|
<instance>${basedir}/target/brokerConnect/federationA</instance>
|
||||||
|
<configuration>${basedir}/target/classes/servers/brokerConnect/federationA</configuration>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<id>createBrokerConnectFederationB</id>
|
||||||
|
<goals>
|
||||||
|
<goal>create</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<role>amq</role>
|
||||||
|
<allowAnonymous>true</allowAnonymous>
|
||||||
|
<user>B</user>
|
||||||
|
<password>B</password>
|
||||||
|
<noWeb>true</noWeb>
|
||||||
|
<portOffset>1</portOffset>
|
||||||
|
<instance>${basedir}/target/brokerConnect/federationB</instance>
|
||||||
|
<configuration>${basedir}/target/classes/servers/brokerConnect/federationB</configuration>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
|
||||||
<!-- used on MaxQueueResourceTest -->
|
<!-- used on MaxQueueResourceTest -->
|
||||||
<execution>
|
<execution>
|
||||||
<phase>test-compile</phase>
|
<phase>test-compile</phase>
|
||||||
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
<?xml version='1.0'?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration xmlns="urn:activemq"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xsi:schemaLocation="urn:activemq /schema/artemis-configuration.xsd">
|
||||||
|
|
||||||
|
<core xmlns="urn:activemq:core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="urn:activemq:core ">
|
||||||
|
|
||||||
|
<name>0.0.0.0</name>
|
||||||
|
|
||||||
|
<persistence-enabled>true</persistence-enabled>
|
||||||
|
|
||||||
|
<!-- this could be ASYNCIO, MAPPED, NIO
|
||||||
|
ASYNCIO: Linux Libaio
|
||||||
|
MAPPED: mmap files
|
||||||
|
NIO: Plain Java Files
|
||||||
|
-->
|
||||||
|
<journal-type>NIO</journal-type>
|
||||||
|
|
||||||
|
<paging-directory>data/paging</paging-directory>
|
||||||
|
|
||||||
|
<bindings-directory>data/bindings</bindings-directory>
|
||||||
|
|
||||||
|
<journal-directory>data/journal</journal-directory>
|
||||||
|
|
||||||
|
<large-messages-directory>data/large-messages</large-messages-directory>
|
||||||
|
|
||||||
|
<journal-datasync>true</journal-datasync>
|
||||||
|
|
||||||
|
<journal-min-files>2</journal-min-files>
|
||||||
|
|
||||||
|
<journal-pool-files>10</journal-pool-files>
|
||||||
|
|
||||||
|
<journal-device-block-size>4096</journal-device-block-size>
|
||||||
|
|
||||||
|
<journal-file-size>10M</journal-file-size>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This value was determined through a calculation.
|
||||||
|
Your system could perform 25 writes per millisecond
|
||||||
|
on the current journal configuration.
|
||||||
|
That translates as a sync write every 40000 nanoseconds.
|
||||||
|
|
||||||
|
Note: If you specify 0 the system will perform writes directly to the disk.
|
||||||
|
We recommend this to be 0 if you are using journalType=MAPPED and journal-datasync=false.
|
||||||
|
-->
|
||||||
|
<journal-buffer-timeout>40000</journal-buffer-timeout>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
When using ASYNCIO, this will determine the writing queue depth for libaio.
|
||||||
|
-->
|
||||||
|
<journal-max-io>1</journal-max-io>
|
||||||
|
<!--
|
||||||
|
You can verify the network health of a particular NIC by specifying the <network-check-NIC> element.
|
||||||
|
<network-check-NIC>theNicName</network-check-NIC>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Use this to use an HTTP server to validate the network
|
||||||
|
<network-check-URL-list>http://www.apache.org</network-check-URL-list> -->
|
||||||
|
|
||||||
|
<!-- <network-check-period>10000</network-check-period> -->
|
||||||
|
<!-- <network-check-timeout>1000</network-check-timeout> -->
|
||||||
|
|
||||||
|
<!-- this is a comma separated list, no spaces, just DNS or IPs
|
||||||
|
it should accept IPV6
|
||||||
|
|
||||||
|
Warning: Make sure you understand your network topology as this is meant to validate if your network is valid.
|
||||||
|
Using IPs that could eventually disappear or be partially visible may defeat the purpose.
|
||||||
|
You can use a list of multiple IPs, and if any successful ping will make the server OK to continue running -->
|
||||||
|
<!-- <network-check-list>10.0.0.1</network-check-list> -->
|
||||||
|
|
||||||
|
<!-- use this to customize the ping used for ipv4 addresses -->
|
||||||
|
<!-- <network-check-ping-command>ping -c 1 -t %d %s</network-check-ping-command> -->
|
||||||
|
|
||||||
|
<!-- use this to customize the ping used for ipv6 addresses -->
|
||||||
|
<!-- <network-check-ping6-command>ping6 -c 1 %2$s</network-check-ping6-command> -->
|
||||||
|
|
||||||
|
<!-- how often we are looking for how many bytes are being used on the disk in ms -->
|
||||||
|
<disk-scan-period>5000</disk-scan-period>
|
||||||
|
|
||||||
|
<!-- once the disk hits this limit the system will block, or close the connection in certain protocols
|
||||||
|
that won't support flow control. -->
|
||||||
|
<max-disk-usage>90</max-disk-usage>
|
||||||
|
|
||||||
|
<!-- should the broker detect dead locks and other issues -->
|
||||||
|
<critical-analyzer>false</critical-analyzer>
|
||||||
|
|
||||||
|
<critical-analyzer-timeout>120000</critical-analyzer-timeout>
|
||||||
|
|
||||||
|
<critical-analyzer-check-period>60000</critical-analyzer-check-period>
|
||||||
|
|
||||||
|
<critical-analyzer-policy>HALT</critical-analyzer-policy>
|
||||||
|
|
||||||
|
<page-sync-timeout>40000</page-sync-timeout>
|
||||||
|
|
||||||
|
<acceptors>
|
||||||
|
<acceptor name="artemis">tcp://0.0.0.0:61616?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true</acceptor>
|
||||||
|
</acceptors>
|
||||||
|
|
||||||
|
<broker-connections>
|
||||||
|
<amqp-connection uri="tcp://localhost:61617" name="federation-fromB" reconnect-attempts="-1" retry-interval="100" user="B" password="B">
|
||||||
|
<federation>
|
||||||
|
<local-address-policy name="address-toA-from-B">
|
||||||
|
<include address-match="toA" />
|
||||||
|
</local-address-policy>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
|
</broker-connections>
|
||||||
|
|
||||||
|
<security-settings>
|
||||||
|
<security-setting match="#">
|
||||||
|
<permission type="createNonDurableQueue" roles="amq"/>
|
||||||
|
<permission type="deleteNonDurableQueue" roles="amq"/>
|
||||||
|
<permission type="createDurableQueue" roles="amq"/>
|
||||||
|
<permission type="deleteDurableQueue" roles="amq"/>
|
||||||
|
<permission type="createAddress" roles="amq"/>
|
||||||
|
<permission type="deleteAddress" roles="amq"/>
|
||||||
|
<permission type="consume" roles="amq"/>
|
||||||
|
<permission type="browse" roles="amq"/>
|
||||||
|
<permission type="send" roles="amq"/>
|
||||||
|
<!-- we need this otherwise ./artemis data imp wouldn't work -->
|
||||||
|
<permission type="manage" roles="amq"/>
|
||||||
|
</security-setting>
|
||||||
|
</security-settings>
|
||||||
|
|
||||||
|
<address-settings>
|
||||||
|
<!-- if you define auto-create on certain queues, management has to be auto-create -->
|
||||||
|
<address-setting match="activemq.management#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
<!--default for catch all-->
|
||||||
|
<address-setting match="#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
</address-settings>
|
||||||
|
|
||||||
|
<addresses>
|
||||||
|
<address name="DLQ">
|
||||||
|
<anycast>
|
||||||
|
<queue name="DLQ" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
<address name="ExpiryQueue">
|
||||||
|
<anycast>
|
||||||
|
<queue name="ExpiryQueue" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
<address name="toB">
|
||||||
|
<anycast>
|
||||||
|
<queue name="toB" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
<address name="toA">
|
||||||
|
<multicast/>
|
||||||
|
</address>
|
||||||
|
</addresses>
|
||||||
|
</core>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,196 @@
|
||||||
|
<?xml version='1.0'?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration xmlns="urn:activemq"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||||
|
xsi:schemaLocation="urn:activemq /schema/artemis-configuration.xsd">
|
||||||
|
|
||||||
|
<core xmlns="urn:activemq:core" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="urn:activemq:core ">
|
||||||
|
|
||||||
|
<name>0.0.0.0</name>
|
||||||
|
|
||||||
|
<persistence-enabled>true</persistence-enabled>
|
||||||
|
|
||||||
|
<!-- this could be ASYNCIO, MAPPED, NIO
|
||||||
|
ASYNCIO: Linux Libaio
|
||||||
|
MAPPED: mmap files
|
||||||
|
NIO: Plain Java Files
|
||||||
|
-->
|
||||||
|
<journal-type>NIO</journal-type>
|
||||||
|
|
||||||
|
<paging-directory>data/paging</paging-directory>
|
||||||
|
|
||||||
|
<bindings-directory>data/bindings</bindings-directory>
|
||||||
|
|
||||||
|
<journal-directory>data/journal</journal-directory>
|
||||||
|
|
||||||
|
<large-messages-directory>data/large-messages</large-messages-directory>
|
||||||
|
|
||||||
|
<journal-datasync>true</journal-datasync>
|
||||||
|
|
||||||
|
<journal-min-files>2</journal-min-files>
|
||||||
|
|
||||||
|
<journal-pool-files>10</journal-pool-files>
|
||||||
|
|
||||||
|
<journal-device-block-size>4096</journal-device-block-size>
|
||||||
|
|
||||||
|
<journal-file-size>10M</journal-file-size>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This value was determined through a calculation.
|
||||||
|
Your system could perform 25 writes per millisecond
|
||||||
|
on the current journal configuration.
|
||||||
|
That translates as a sync write every 40000 nanoseconds.
|
||||||
|
|
||||||
|
Note: If you specify 0 the system will perform writes directly to the disk.
|
||||||
|
We recommend this to be 0 if you are using journalType=MAPPED and journal-datasync=false.
|
||||||
|
-->
|
||||||
|
<journal-buffer-timeout>40000</journal-buffer-timeout>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
When using ASYNCIO, this will determine the writing queue depth for libaio.
|
||||||
|
-->
|
||||||
|
<journal-max-io>1</journal-max-io>
|
||||||
|
<!--
|
||||||
|
You can verify the network health of a particular NIC by specifying the <network-check-NIC> element.
|
||||||
|
<network-check-NIC>theNicName</network-check-NIC>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Use this to use an HTTP server to validate the network
|
||||||
|
<network-check-URL-list>http://www.apache.org</network-check-URL-list> -->
|
||||||
|
|
||||||
|
<!-- <network-check-period>10000</network-check-period> -->
|
||||||
|
<!-- <network-check-timeout>1000</network-check-timeout> -->
|
||||||
|
|
||||||
|
<!-- this is a comma separated list, no spaces, just DNS or IPs
|
||||||
|
it should accept IPV6
|
||||||
|
|
||||||
|
Warning: Make sure you understand your network topology as this is meant to validate if your network is valid.
|
||||||
|
Using IPs that could eventually disappear or be partially visible may defeat the purpose.
|
||||||
|
You can use a list of multiple IPs, and if any successful ping will make the server OK to continue running -->
|
||||||
|
<!-- <network-check-list>10.0.0.1</network-check-list> -->
|
||||||
|
|
||||||
|
<!-- use this to customize the ping used for ipv4 addresses -->
|
||||||
|
<!-- <network-check-ping-command>ping -c 1 -t %d %s</network-check-ping-command> -->
|
||||||
|
|
||||||
|
<!-- use this to customize the ping used for ipv6 addresses -->
|
||||||
|
<!-- <network-check-ping6-command>ping6 -c 1 %2$s</network-check-ping6-command> -->
|
||||||
|
|
||||||
|
<!-- how often we are looking for how many bytes are being used on the disk in ms -->
|
||||||
|
<disk-scan-period>5000</disk-scan-period>
|
||||||
|
|
||||||
|
<!-- once the disk hits this limit the system will block, or close the connection in certain protocols
|
||||||
|
that won't support flow control. -->
|
||||||
|
<max-disk-usage>90</max-disk-usage>
|
||||||
|
|
||||||
|
<!-- should the broker detect dead locks and other issues -->
|
||||||
|
<critical-analyzer>false</critical-analyzer>
|
||||||
|
|
||||||
|
<critical-analyzer-timeout>120000</critical-analyzer-timeout>
|
||||||
|
|
||||||
|
<critical-analyzer-check-period>60000</critical-analyzer-check-period>
|
||||||
|
|
||||||
|
<critical-analyzer-policy>HALT</critical-analyzer-policy>
|
||||||
|
|
||||||
|
<page-sync-timeout>40000</page-sync-timeout>
|
||||||
|
|
||||||
|
<broker-connections>
|
||||||
|
<amqp-connection uri="tcp://localhost:61616" name="federation-fromA" reconnect-attempts="-1" retry-interval="100" user="A" password="A">
|
||||||
|
<federation>
|
||||||
|
<local-queue-policy name="queue-toB-from-A">
|
||||||
|
<include queue-match="toB" address-match="#" />
|
||||||
|
</local-queue-policy>
|
||||||
|
</federation>
|
||||||
|
</amqp-connection>
|
||||||
|
</broker-connections>
|
||||||
|
<acceptors>
|
||||||
|
<acceptor name="artemis">tcp://0.0.0.0:61617?tcpSendBufferSize=1048576;tcpReceiveBufferSize=1048576;amqpMinLargeMessageSize=102400;protocols=CORE,AMQP;useEpoll=true;amqpCredits=1000;amqpLowCredits=300;amqpDuplicateDetection=true</acceptor>
|
||||||
|
</acceptors>
|
||||||
|
|
||||||
|
<security-settings>
|
||||||
|
|
||||||
|
<security-setting match="#">
|
||||||
|
<permission type="createNonDurableQueue" roles="amq"/>
|
||||||
|
<permission type="deleteNonDurableQueue" roles="amq"/>
|
||||||
|
<permission type="createDurableQueue" roles="amq"/>
|
||||||
|
<permission type="deleteDurableQueue" roles="amq"/>
|
||||||
|
<permission type="createAddress" roles="amq"/>
|
||||||
|
<permission type="deleteAddress" roles="amq"/>
|
||||||
|
<permission type="consume" roles="amq"/>
|
||||||
|
<permission type="browse" roles="amq"/>
|
||||||
|
<permission type="send" roles="amq"/>
|
||||||
|
<!-- we need this otherwise ./artemis data imp wouldn't work -->
|
||||||
|
<permission type="manage" roles="amq"/>
|
||||||
|
</security-setting>
|
||||||
|
|
||||||
|
</security-settings>
|
||||||
|
|
||||||
|
<address-settings>
|
||||||
|
<!-- if you define auto-create on certain queues, management has to be auto-create -->
|
||||||
|
<address-setting match="activemq.management#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
<!--default for catch all-->
|
||||||
|
<address-setting match="#">
|
||||||
|
<dead-letter-address>DLQ</dead-letter-address>
|
||||||
|
<expiry-address>ExpiryQueue</expiry-address>
|
||||||
|
<redelivery-delay>0</redelivery-delay>
|
||||||
|
<!-- with -1 only the global-max-size is in use for limiting -->
|
||||||
|
<max-size-bytes>-1</max-size-bytes>
|
||||||
|
<message-counter-history-day-limit>10</message-counter-history-day-limit>
|
||||||
|
<address-full-policy>PAGE</address-full-policy>
|
||||||
|
<auto-create-queues>true</auto-create-queues>
|
||||||
|
<auto-create-addresses>true</auto-create-addresses>
|
||||||
|
</address-setting>
|
||||||
|
</address-settings>
|
||||||
|
|
||||||
|
<addresses>
|
||||||
|
<address name="DLQ">
|
||||||
|
<anycast>
|
||||||
|
<queue name="DLQ" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
<address name="ExpiryQueue">
|
||||||
|
<anycast>
|
||||||
|
<queue name="ExpiryQueue" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
<address name="toB">
|
||||||
|
<anycast>
|
||||||
|
<queue name="toB" />
|
||||||
|
</anycast>
|
||||||
|
</address>
|
||||||
|
<address name="toA">
|
||||||
|
<multicast/>
|
||||||
|
</address>
|
||||||
|
</addresses>
|
||||||
|
</core>
|
||||||
|
</configuration>
|
|
@ -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
|
||||||
|
* <br>
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* <br>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,10 @@
|
||||||
package org.apache.activemq.artemis.tests.unit.core.config.impl;
|
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.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration;
|
||||||
|
|
||||||
import org.apache.activemq.artemis.core.config.FileDeploymentManager;
|
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.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.amqpBrokerConnectivity.AMQPMirrorBrokerConnectionElement;
|
||||||
import org.apache.activemq.artemis.core.config.impl.FileConfiguration;
|
import org.apache.activemq.artemis.core.config.impl.FileConfiguration;
|
||||||
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
|
import org.apache.activemq.artemis.tests.util.ActiveMQTestBase;
|
||||||
|
@ -36,8 +38,6 @@ public class ConfigurationValidationTest extends ActiveMQTestBase {
|
||||||
System.setProperty("ninetyTwoProp", "92");
|
System.setProperty("ninetyTwoProp", "92");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* test does not pass in eclipse (because it can not find artemis-configuration.xsd).
|
* 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.
|
* It runs fine on the CLI with the proper env setting.
|
||||||
|
@ -67,7 +67,7 @@ public class ConfigurationValidationTest extends ActiveMQTestBase {
|
||||||
deploymentManager.addDeployable(fc);
|
deploymentManager.addDeployable(fc);
|
||||||
deploymentManager.readConfiguration();
|
deploymentManager.readConfiguration();
|
||||||
|
|
||||||
Assert.assertEquals(3, fc.getAMQPConnection().size());
|
Assert.assertEquals(4, fc.getAMQPConnection().size());
|
||||||
|
|
||||||
AMQPBrokerConnectConfiguration amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(0);
|
AMQPBrokerConnectConfiguration amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(0);
|
||||||
Assert.assertEquals("testuser", amqpBrokerConnectConfiguration.getUser());
|
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(AMQPBrokerConnectionAddressType.RECEIVER, amqpBrokerConnectConfiguration.getConnectionElements().get(1).getType());
|
||||||
Assert.assertEquals("TEST-PEER", amqpBrokerConnectConfiguration.getConnectionElements().get(2).getMatchAddress().toString());
|
Assert.assertEquals("TEST-PEER", amqpBrokerConnectConfiguration.getConnectionElements().get(2).getMatchAddress().toString());
|
||||||
Assert.assertEquals(AMQPBrokerConnectionAddressType.PEER, amqpBrokerConnectConfiguration.getConnectionElements().get(2).getType());
|
Assert.assertEquals(AMQPBrokerConnectionAddressType.PEER, amqpBrokerConnectConfiguration.getConnectionElements().get(2).getType());
|
||||||
|
|
||||||
Assert.assertEquals("TEST-WITH-QUEUE-NAME", amqpBrokerConnectConfiguration.getConnectionElements().get(3).getQueueName().toString());
|
Assert.assertEquals("TEST-WITH-QUEUE-NAME", amqpBrokerConnectConfiguration.getConnectionElements().get(3).getQueueName().toString());
|
||||||
Assert.assertEquals(null, amqpBrokerConnectConfiguration.getConnectionElements().get(3).getMatchAddress());
|
Assert.assertEquals(null, amqpBrokerConnectConfiguration.getConnectionElements().get(3).getMatchAddress());
|
||||||
Assert.assertEquals(AMQPBrokerConnectionAddressType.RECEIVER, amqpBrokerConnectConfiguration.getConnectionElements().get(3).getType());
|
Assert.assertEquals(AMQPBrokerConnectionAddressType.RECEIVER, amqpBrokerConnectConfiguration.getConnectionElements().get(3).getType());
|
||||||
|
@ -96,10 +95,22 @@ public class ConfigurationValidationTest extends ActiveMQTestBase {
|
||||||
Assert.assertFalse(mirrorConnectionElement.isDurable());
|
Assert.assertFalse(mirrorConnectionElement.isDurable());
|
||||||
Assert.assertTrue(mirrorConnectionElement.isSync());
|
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);
|
amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(1);
|
||||||
Assert.assertEquals(null, amqpBrokerConnectConfiguration.getUser()); mirrorConnectionElement = (AMQPMirrorBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(0);
|
Assert.assertEquals(null, amqpBrokerConnectConfiguration.getUser());
|
||||||
Assert.assertEquals(null, amqpBrokerConnectConfiguration.getPassword()); Assert.assertEquals("test2", amqpBrokerConnectConfiguration.getName());
|
mirrorConnectionElement = (AMQPMirrorBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(0);
|
||||||
|
Assert.assertEquals(null, amqpBrokerConnectConfiguration.getPassword());
|
||||||
|
Assert.assertEquals("test2", amqpBrokerConnectConfiguration.getName());
|
||||||
Assert.assertEquals("tcp://test2:222", amqpBrokerConnectConfiguration.getUri());
|
Assert.assertEquals("tcp://test2:222", amqpBrokerConnectConfiguration.getUri());
|
||||||
Assert.assertTrue(mirrorConnectionElement.isMessageAcknowledgements());
|
Assert.assertTrue(mirrorConnectionElement.isMessageAcknowledgements());
|
||||||
Assert.assertFalse(mirrorConnectionElement.isDurable());
|
Assert.assertFalse(mirrorConnectionElement.isDurable());
|
||||||
|
@ -109,6 +120,84 @@ public class ConfigurationValidationTest extends ActiveMQTestBase {
|
||||||
|
|
||||||
amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(2);
|
amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(2);
|
||||||
Assert.assertFalse(amqpBrokerConnectConfiguration.isAutostart());
|
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
|
@Test
|
||||||
|
|
Loading…
Reference in New Issue