ARTEMIS-4502 Support core messages crossing broker connection links

Allow for core messages to be tunneled over broker connection links used
for AMQP Federation and for broker mirroring. This eliminates the need to
convert from Core to AMQP and from loading core large messages fully into
memory for that conversion.
This commit is contained in:
Timothy Bish 2023-11-13 16:21:09 -05:00 committed by Robbie Gemmell
parent 54c1ae630c
commit 93a74dc00c
56 changed files with 6123 additions and 1270 deletions

View File

@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.buffers.impl.ChannelBufferWrapper;
import org.apache.activemq.artemis.core.client.impl.TopologyMemberImpl;
import org.apache.activemq.artemis.core.remoting.CloseListener;
@ -268,11 +269,11 @@ public class AMQPConnectionCallback implements FailureListener, CloseListener {
return null;
}
public String invokeIncomingInterceptors(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
public String invokeIncomingInterceptors(Message message, ActiveMQProtonRemotingConnection connection) {
return manager.invokeIncoming(message, connection);
}
public String invokeOutgoingInterceptors(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
public String invokeOutgoingInterceptors(Message message, ActiveMQProtonRemotingConnection connection) {
return manager.invokeOutgoing(message, connection);
}
}

View File

@ -547,7 +547,7 @@ public class AMQPSessionCallback implements SessionCallback {
final RoutingContext routingContext) {
OperationContext oldContext = recoverContext();
try {
if (invokeIncoming((AMQPMessage) message, (ActiveMQProtonRemotingConnection) transportConnection.getProtocolConnection()) == null) {
if (invokeIncoming(message, (ActiveMQProtonRemotingConnection) transportConnection.getProtocolConnection()) == null) {
serverSession.send(transaction, message, directDeliver, receiver.getName(), false, routingContext);
afterIO(new IOCallback() {
@ -770,11 +770,11 @@ public class AMQPSessionCallback implements SessionCallback {
manager.getServer().getSecurityStore().check(address, checkType, session);
}
public String invokeIncoming(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
public String invokeIncoming(Message message, ActiveMQProtonRemotingConnection connection) {
return protonSPI.invokeIncomingInterceptors(message, connection);
}
public String invokeOutgoing(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
public String invokeOutgoing(Message message, ActiveMQProtonRemotingConnection connection) {
return protonSPI.invokeOutgoingInterceptors(message, connection);
}

View File

@ -25,6 +25,7 @@ import java.util.concurrent.Executor;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.BaseInterceptor;
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.api.core.client.ActiveMQClient;
@ -352,12 +353,32 @@ public class ProtonProtocolManager extends AbstractProtocolManager<AMQPMessage,
return routingHandler;
}
public String invokeIncoming(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
return super.invokeInterceptors(this.incomingInterceptors, message, connection);
public String invokeIncoming(Message message, ActiveMQProtonRemotingConnection connection) {
// For tunneled messages we need to check the type as our interceptor only cares about
// AMQP message right now so there's not notification point for other types that cross
if (incomingInterceptors != null && !incomingInterceptors.isEmpty()) {
if (message instanceof AMQPMessage) {
return super.invokeInterceptors(this.incomingInterceptors, (AMQPMessage) message, connection);
} else {
return null;
}
} else {
return null;
}
}
public String invokeOutgoing(AMQPMessage message, ActiveMQProtonRemotingConnection connection) {
return super.invokeInterceptors(this.outgoingInterceptors, message, connection);
public String invokeOutgoing(Message message, ActiveMQProtonRemotingConnection connection) {
// For tunneled messages we need to check the type as our interceptor only cares about
// AMQP message right now so there's not notification point for other types that cross
if (outgoingInterceptors != null && !outgoingInterceptors.isEmpty()) {
if (message instanceof AMQPMessage) {
return super.invokeInterceptors(this.outgoingInterceptors, (AMQPMessage) message, connection);
} else {
return null;
}
} else {
return null;
}
}
public int getInitialRemoteMaxFrameSize() {

View File

@ -34,6 +34,7 @@ import java.util.function.Predicate;
import java.util.stream.Stream;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.QueueConfiguration;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString;
@ -60,6 +61,7 @@ import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.server.impl.AddressInfo;
import org.apache.activemq.artemis.core.server.mirror.MirrorController;
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerQueuePlugin;
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.broker.ActiveMQProtonRemotingConnection;
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager;
@ -69,7 +71,13 @@ import org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorContro
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.ActiveMQAMQPProtocolMessageBundle;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPLargeMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreLargeMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport;
import org.apache.activemq.artemis.protocol.amqp.proton.MessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContext;
import org.apache.activemq.artemis.protocol.amqp.proton.SenderController;
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
@ -96,6 +104,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.AMQP_LINK_INITIALIZER_KEY;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyCapabilities;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyOfferedCapabilities;
import java.lang.invoke.MethodHandles;
@ -104,6 +113,13 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
/**
* Default value for the core message tunneling feature that indicates if core protocol messages
* should be streamed as binary blobs as the payload of an custom AMQP message which avoids any
* conversions of the messages to / from AMQP.
*/
public static final boolean DEFAULT_CORE_MESSAGE_TUNNELING_ENABLED = true;
private final AMQPBrokerConnectConfiguration brokerConnectConfiguration;
private final ProtonProtocolManager protonProtocolManager;
private final ActiveMQServer server;
@ -250,11 +266,11 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
public void createLink(Queue queue, AMQPBrokerConnectionElement connectionElement) {
if (connectionElement.getType() == AMQPBrokerConnectionAddressType.PEER) {
Symbol[] dispatchCapability = new Symbol[]{AMQPMirrorControllerSource.QPID_DISPATCH_WAYPOINT_CAPABILITY};
connectSender(queue, queue.getAddress().toString(), null, null, null, null, dispatchCapability);
connectSender(queue, queue.getAddress().toString(), null, null, null, null, dispatchCapability, null);
connectReceiver(protonRemotingConnection, session, sessionContext, queue, dispatchCapability);
} else {
if (connectionElement.getType() == AMQPBrokerConnectionAddressType.SENDER) {
connectSender(queue, queue.getAddress().toString(), null, null, null, null, null);
connectSender(queue, queue.getAddress().toString(), null, null, null, null, null, null);
}
if (connectionElement.getType() == AMQPBrokerConnectionAddressType.RECEIVER) {
connectReceiver(protonRemotingConnection, session, sessionContext, queue);
@ -378,10 +394,28 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
if (connectionElement.getType() == AMQPBrokerConnectionAddressType.MIRROR) {
AMQPMirrorBrokerConnectionElement replica = (AMQPMirrorBrokerConnectionElement)connectionElement;
Queue queue = server.locateQueue(getMirrorSNF(replica));
final Queue queue = server.locateQueue(getMirrorSNF(replica));
connectSender(queue, queue.getName().toString(), mirrorControllerSource::setLink, (r) -> AMQPMirrorControllerSource.validateProtocolData(protonProtocolManager.getReferenceIDSupplier(), r, getMirrorSNF(replica)), server.getNodeID().toString(),
new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY}, null);
final boolean coreTunnelingEnabled = isCoreMessageTunnelingEnabled(replica);
final Symbol[] desiredCapabilities;
if (coreTunnelingEnabled) {
desiredCapabilities = new Symbol[] {AMQPMirrorControllerSource.MIRROR_CAPABILITY,
AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT};
} else {
desiredCapabilities = new Symbol[] {AMQPMirrorControllerSource.MIRROR_CAPABILITY};
}
final Symbol[] requiredOfferedCapabilities = new Symbol[] {AMQPMirrorControllerSource.MIRROR_CAPABILITY};
connectSender(queue,
queue.getName().toString(),
mirrorControllerSource::setLink,
(r) -> AMQPMirrorControllerSource.validateProtocolData(protonProtocolManager.getReferenceIDSupplier(), r, getMirrorSNF(replica)),
server.getNodeID().toString(),
desiredCapabilities,
null,
requiredOfferedCapabilities);
} else if (connectionElement.getType() == AMQPBrokerConnectionAddressType.FEDERATION) {
// Starting the Federation triggers rebuild of federation links
// based on current broker state.
@ -608,7 +642,8 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
java.util.function.Consumer<? super MessageReference> beforeDeliver,
String brokerID,
Symbol[] desiredCapabilities,
Symbol[] targetCapabilities) {
Symbol[] targetCapabilities,
Symbol[] requiredOfferedCapabilities) {
logger.debug("Connecting outbound for {}", queue);
if (session == null) {
@ -675,9 +710,9 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
error(ActiveMQAMQPProtocolMessageBundle.BUNDLE.senderLinkRefused(sender.getTarget().getAddress()), lastRetryCounter);
return;
}
if (desiredCapabilities != null) {
if (!verifyOfferedCapabilities(sender, desiredCapabilities)) {
error(ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(desiredCapabilities)), lastRetryCounter);
if (requiredOfferedCapabilities != null) {
if (!verifyOfferedCapabilities(sender, requiredOfferedCapabilities)) {
error(ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(requiredOfferedCapabilities)), lastRetryCounter);
return;
}
}
@ -761,6 +796,14 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
final Sender sender;
final AMQPSessionCallback sessionSPI;
protected boolean tunnelCoreMessages;
protected AMQPMessageWriter standardMessageWriter;
protected AMQPLargeMessageWriter largeMessageWriter;
protected AMQPTunneledCoreMessageWriter coreMessageWriter;
protected AMQPTunneledCoreLargeMessageWriter coreLargeMessageWriter;
AMQPOutgoingController(Queue queue, Sender sender, AMQPSessionCallback sessionSPI) {
this.queue = queue;
this.sessionSPI = sessionSPI;
@ -770,12 +813,46 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
@Override
public Consumer init(ProtonServerSenderContext senderContext) throws Exception {
SimpleString queueName = queue.getName();
// Did we ask for core tunneling? If so did the remote offer it in return
tunnelCoreMessages = verifyCapabilities(sender.getDesiredCapabilities(), AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT) &&
verifyOfferedCapabilities(sender, AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT);
return (Consumer) sessionSPI.createSender(senderContext, queueName, null, false);
}
@Override
public void close() throws Exception {
}
@Override
public MessageWriter selectOutgoingMessageWriter(ProtonServerSenderContext sender, MessageReference reference) {
final MessageWriter selected;
final Message message = reference.getMessage();
if (message instanceof AMQPMessage) {
if (message.isLargeMessage()) {
selected = largeMessageWriter != null ? largeMessageWriter :
(largeMessageWriter = new AMQPLargeMessageWriter(sender));
} else {
selected = standardMessageWriter != null ? standardMessageWriter :
(standardMessageWriter = new AMQPMessageWriter(sender));
}
} else if (tunnelCoreMessages) {
if (message.isLargeMessage()) {
selected = coreLargeMessageWriter != null ? coreLargeMessageWriter :
(coreLargeMessageWriter = new AMQPTunneledCoreLargeMessageWriter(sender));
} else {
selected = coreMessageWriter != null ? coreMessageWriter :
(coreMessageWriter = new AMQPTunneledCoreMessageWriter(sender));
}
} else {
selected = standardMessageWriter != null ? standardMessageWriter :
(standardMessageWriter = new AMQPMessageWriter(sender));
}
return selected;
}
}
public void disconnect() throws Exception {
@ -957,4 +1034,16 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
return null;
}
}
public static boolean isCoreMessageTunnelingEnabled(AMQPMirrorBrokerConnectionElement configuration) {
final Object property = configuration.getProperties().get(AmqpSupport.TUNNEL_CORE_MESSAGES);
if (property instanceof Boolean) {
return (Boolean) property;
} else if (property instanceof String) {
return Boolean.parseBoolean((String) property);
} else {
return DEFAULT_CORE_MESSAGE_TUNNELING_ENABLED;
}
}
}

View File

@ -148,6 +148,11 @@ public abstract class AMQPFederation implements FederationInternal {
*/
public abstract int getLargeMessageThreshold();
/**
* @return the true if the federation should support core message tunneling.
*/
public abstract boolean isCoreMessageTunnelingEnabled();
@Override
public final synchronized void start() throws ActiveMQException {
if (!started) {

View File

@ -22,7 +22,9 @@ import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPF
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.AMQPFederationPolicySupport.MESSAGE_HOPS_PROPERTY;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
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;
@ -39,6 +41,7 @@ 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.ICoreMessage;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.api.core.SimpleString;
@ -60,9 +63,13 @@ 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.AMQPTunneledCoreLargeMessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreMessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.MessageReader;
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.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Modified;
import org.apache.qpid.proton.amqp.messaging.Rejected;
import org.apache.qpid.proton.amqp.messaging.Released;
@ -91,10 +98,6 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
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};
@ -298,7 +301,14 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
protonReceiver.setSenderSettleMode(SenderSettleMode.UNSETTLED);
protonReceiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
protonReceiver.setDesiredCapabilities(DESIRED_LINK_CAPABILITIES);
protonReceiver.setDesiredCapabilities(new Symbol[] {FEDERATION_ADDRESS_RECEIVER});
// If enabled offer core tunneling which we prefer to AMQP conversions of core as
// the large ones will be converted to standard AMQP messages in memory. When not
// offered the remote must not use core tunneling and AMQP conversion will be the
// fallback.
if (configuration.isCoreMessageTunnelingEnabled()) {
protonReceiver.setOfferedCapabilities(new Symbol[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT});
}
protonReceiver.setProperties(receiverProperties);
protonReceiver.setTarget(target);
protonReceiver.setSource(source);
@ -331,9 +341,9 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
// 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)) {
if (protonReceiver.getRemoteSource() != null && !AmqpSupport.verifyOfferedCapabilities(protonReceiver, FEDERATION_ADDRESS_RECEIVER)) {
federation.signalResourceCreateError(
ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(DESIRED_LINK_CAPABILITIES)));
ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(FEDERATION_ADDRESS_RECEIVER.toString()));
return;
}
@ -365,8 +375,9 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
});
}
private static AMQPMessage incrementMessageHops(AMQPMessage message) {
private static AMQPMessage incrementAMQPMessageHops(AMQPMessage message) {
Object hops = message.getAnnotation(MESSAGE_HOPS_ANNOTATION);
if (hops == null) {
message.setAnnotation(MESSAGE_HOPS_ANNOTATION, 1);
} else {
@ -380,6 +391,19 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
return message;
}
private static ICoreMessage incrementCoreMessageHops(ICoreMessage message) {
Object hops = message.getObjectProperty(MESSAGE_HOPS_PROPERTY);
if (hops == null) {
message.putObjectProperty(MESSAGE_HOPS_PROPERTY, 1);
} else {
Number numHops = (Number) hops;
message.putObjectProperty(MESSAGE_HOPS_PROPERTY, 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.
@ -388,6 +412,10 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
private final SimpleString cachedAddress;
private MessageReader coreMessageReader;
private MessageReader coreLargeMessageReader;
/**
* Creates the federation receiver instance.
*
@ -474,18 +502,39 @@ public class AMQPFederationAddressConsumer implements FederationConsumerInternal
}
@Override
protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) {
protected MessageReader trySelectMessageReader(Receiver receiver, Delivery delivery) {
if (delivery.getMessageFormat() == AMQP_TUNNELED_CORE_MESSAGE_FORMAT) {
return coreMessageReader != null ?
coreMessageReader : (coreMessageReader = new AMQPTunneledCoreMessageReader(this));
} else if (delivery.getMessageFormat() == AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT) {
return coreLargeMessageReader != null ?
coreLargeMessageReader : (coreLargeMessageReader = new AMQPTunneledCoreLargeMessageReader(this));
} else {
return super.trySelectMessageReader(receiver, delivery);
}
}
@Override
protected void actualDelivery(Message message, Delivery delivery, DeliveryAnnotations deliveryAnnotations, 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));
final Message baseMessage;
if (theMessage != message && logger.isTraceEnabled()) {
if (message instanceof ICoreMessage) {
baseMessage = incrementCoreMessageHops((ICoreMessage) message);
} else {
baseMessage = incrementAMQPMessageHops((AMQPMessage) message);
}
final Message theMessage = transformer.transform(baseMessage);
if (theMessage != baseMessage && logger.isTraceEnabled()) {
logger.trace("The transformer {} replaced the original message {} with a new instance {}",
transformer, message, theMessage);
transformer, baseMessage, theMessage);
}
signalBeforeFederationConsumerMessageHandled(theMessage);

View File

@ -17,6 +17,8 @@
package org.apache.activemq.artemis.protocol.amqp.connect.federation;
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.generateAddressFilter;
import java.lang.invoke.MethodHandles;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
@ -51,15 +53,7 @@ public class AMQPFederationAddressPolicyManager extends FederationAddressPolicyM
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.remoteQueueFilter = generateAddressFilter(policy.getMaxHops());
this.configuration = new AMQPFederationConsumerConfiguration(federation, policy.getProperties());
}

View File

@ -25,6 +25,7 @@ import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPF
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 static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyOfferedCapabilities;
import java.util.Collections;
import java.util.Map;
@ -38,7 +39,6 @@ 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;
@ -64,26 +64,10 @@ import org.apache.qpid.proton.engine.Sender;
* 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 final class AMQPFederationAddressSenderController extends AMQPFederationBaseSenderController {
public AMQPFederationAddressSenderController(AMQPSessionContext session) {
this.session = session;
this.sessionSPI = session.getSessionSPI();
}
public AMQPSessionContext getSessionContext() {
return session;
}
public AMQPSessionCallback getSessionCallback() {
return sessionSPI;
super(session);
}
@SuppressWarnings("unchecked")
@ -104,8 +88,15 @@ public final class AMQPFederationAddressSenderController implements SenderContro
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);
// We need to offer back that we support federation for the remote to complete the attach
sender.setOfferedCapabilities(new Symbol[] {FEDERATION_ADDRESS_RECEIVER});
// We indicate desired to meet specification that we cannot use a capability unless we
// indicated it was desired, however unless offered by the remote we cannot use it.
sender.setDesiredCapabilities(new Symbol[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT});
// We need to check that the remote offers its ability to read tunneled core messages and
// if not we must not send them but instead convert all messages to AMQP messages first.
tunnelCoreMessages = verifyOfferedCapabilities(sender, AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT);
final Map<String, Object> addressSourceProperties;
@ -190,11 +181,6 @@ public final class AMQPFederationAddressSenderController implements SenderContro
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) {

View File

@ -0,0 +1,95 @@
/*
* 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.Message;
import org.apache.activemq.artemis.core.server.MessageReference;
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.proton.AMQPLargeMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreLargeMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreMessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.MessageWriter;
import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContext;
import org.apache.activemq.artemis.protocol.amqp.proton.SenderController;
/**
* A base class abstract {@link SenderController} implementation for use by federation address and
* queue senders that provides some common functionality used between both.
*/
public abstract class AMQPFederationBaseSenderController implements SenderController {
protected final AMQPSessionContext session;
protected final AMQPSessionCallback sessionSPI;
protected AMQPMessageWriter standardMessageWriter;
protected AMQPLargeMessageWriter largeMessageWriter;
protected AMQPTunneledCoreMessageWriter coreMessageWriter;
protected AMQPTunneledCoreLargeMessageWriter coreLargeMessageWriter;
protected boolean tunnelCoreMessages; // only enabled if remote offers support.
public AMQPFederationBaseSenderController(AMQPSessionContext session) {
this.session = session;
this.sessionSPI = session.getSessionSPI();
}
public AMQPSessionContext getSessionContext() {
return session;
}
public AMQPSessionCallback getSessionCallback() {
return sessionSPI;
}
@Override
public void close() throws Exception {
// Currently there isn't anything needed on close of this controller.
}
@Override
public MessageWriter selectOutgoingMessageWriter(ProtonServerSenderContext sender, MessageReference reference) {
final MessageWriter selected;
final Message message = reference.getMessage();
if (message instanceof AMQPMessage) {
if (message.isLargeMessage()) {
selected = largeMessageWriter != null ? largeMessageWriter :
(largeMessageWriter = new AMQPLargeMessageWriter(sender));
} else {
selected = standardMessageWriter != null ? standardMessageWriter :
(standardMessageWriter = new AMQPMessageWriter(sender));
}
} else if (tunnelCoreMessages) {
if (message.isLargeMessage()) {
selected = coreLargeMessageWriter != null ? coreLargeMessageWriter :
(coreLargeMessageWriter = new AMQPTunneledCoreLargeMessageWriter(sender));
} else {
selected = coreMessageWriter != null ? coreMessageWriter :
(coreMessageWriter = new AMQPTunneledCoreMessageWriter(sender));
}
} else {
selected = standardMessageWriter != null ? standardMessageWriter :
(standardMessageWriter = new AMQPMessageWriter(sender));
}
return selected;
}
}

View File

@ -19,6 +19,7 @@ package org.apache.activemq.artemis.protocol.amqp.connect.federation;
import java.lang.invoke.MethodHandles;
import org.apache.activemq.artemis.api.core.Message;
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;
@ -31,6 +32,7 @@ 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.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Target;
import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
import org.apache.qpid.proton.engine.Delivery;
@ -111,22 +113,24 @@ public class AMQPFederationCommandProcessor extends ProtonAbstractReceiver {
}
@Override
protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) {
protected void actualDelivery(Message message, Delivery delivery, DeliveryAnnotations deliveryAnnotations, Receiver receiver, Transaction tx) {
logger.trace("{}::actualdelivery called for {}", server, message);
final AMQPMessage controlMessage = (AMQPMessage) message;
delivery.setContext(message);
try {
final Object eventType = AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, OPERATION_TYPE);
final Object eventType = AMQPMessageBrokerAccessor.getMessageAnnotationProperty(controlMessage, OPERATION_TYPE);
if (ADD_QUEUE_POLICY.equals(eventType)) {
final FederationReceiveFromQueuePolicy policy =
AMQPFederationPolicySupport.decodeReceiveFromQueuePolicy(message, federation.getWildcardConfiguration());
AMQPFederationPolicySupport.decodeReceiveFromQueuePolicy(controlMessage, federation.getWildcardConfiguration());
federation.addQueueMatchPolicy(policy);
} else if (ADD_ADDRESS_POLICY.equals(eventType)) {
final FederationReceiveFromAddressPolicy policy =
AMQPFederationPolicySupport.decodeReceiveFromAddressPolicy(message, federation.getWildcardConfiguration());
AMQPFederationPolicySupport.decodeReceiveFromAddressPolicy(controlMessage, federation.getWildcardConfiguration());
federation.addAddressMatchPolicy(policy);
} else {

View File

@ -28,6 +28,7 @@ import java.util.Map;
import java.util.Objects;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPConnectionContext;
import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport;
import org.apache.qpid.proton.engine.Receiver;
/**
@ -44,6 +45,13 @@ public final class AMQPFederationConfiguration {
*/
public static final int DEFAULT_LINK_ATTACH_TIMEOUT = 30;
/**
* Default value for the core message tunneling feature that indicates if core protocol messages
* should be streamed as binary blobs as the payload of an custom AMQP message which avoids any
* conversions of the messages to / from AMQP.
*/
public static final boolean DEFAULT_CORE_MESSAGE_TUNNELING_ENABLED = true;
private final Map<String, Object> properties;
private final AMQPConnectionContext connection;
@ -116,6 +124,20 @@ public final class AMQPFederationConfiguration {
}
}
/**
* @return true if the federation is configured to tunnel core messages as AMQP custom messages.
*/
public boolean isCoreMessageTunnelingEnabled() {
final Object property = properties.get(AmqpSupport.TUNNEL_CORE_MESSAGES);
if (property instanceof Boolean) {
return (Boolean) property;
} else if (property instanceof String) {
return Boolean.parseBoolean((String) property);
} else {
return DEFAULT_CORE_MESSAGE_TUNNELING_ENABLED;
}
}
/**
* 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
@ -129,6 +151,7 @@ public final class AMQPFederationConfiguration {
configMap.put(RECEIVER_CREDITS_LOW, getReceiverCreditsLow());
configMap.put(LARGE_MESSAGE_THRESHOLD, getLargeMessageThreshold());
configMap.put(LINK_ATTACH_TIMEOUT, getLinkAttachTimeout());
configMap.put(AmqpSupport.TUNNEL_CORE_MESSAGES, isCoreMessageTunnelingEnabled());
return configMap;
}

View File

@ -26,6 +26,8 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport;
/**
* Configuration options applied to a consumer created from federation policies
* for address or queue federation. The options first check the policy properties
@ -91,4 +93,15 @@ public final class AMQPFederationConsumerConfiguration {
return federation.getLinkAttachTimeout();
}
}
public boolean isCoreMessageTunnelingEnabled() {
final Object property = properties.get(AmqpSupport.TUNNEL_CORE_MESSAGES);
if (property instanceof Boolean) {
return (Boolean) property;
} else if (property instanceof String) {
return Boolean.parseBoolean((String) property);
} else {
return federation.isCoreMessageTunnelingEnabled();
}
}
}

View File

@ -85,12 +85,43 @@ public final class AMQPFederationPolicySupport {
*/
public static final Symbol MESSAGE_HOPS_ANNOTATION = Symbol.valueOf("x-opt-amq-fed-hops");
/**
* Property value placed on Core messages to indicate number of hops that a message has
* made when crossing Federation links. This value is used when Core messages are tunneled
* via an AMQP custom message and then recreated again on the other side.
*/
public static final String MESSAGE_HOPS_PROPERTY = "_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");
/**
* Constructs an address filter for a federated address receiver link that deals with
* both AMQP messages and unwrapped Core messages which can carry different hops markers.
* If the max is less than or equal to zero no filter is created as these values are used
* to indicate no max hops for federated messages on an address.
*
* @param maxHops
* The max allowed number of hops before a message should stop cross federation links.
*
* @return the address filter string or null if not needed.
*/
public static String generateAddressFilter(int maxHops) {
if (maxHops <= 0) {
return null;
}
return "(\"m." + MESSAGE_HOPS_ANNOTATION.toString() +
"\" IS NULL OR \"m." + MESSAGE_HOPS_ANNOTATION.toString() +
"\"<" + maxHops + ")" +
" AND " +
"(" + MESSAGE_HOPS_PROPERTY + " IS NULL OR " +
MESSAGE_HOPS_PROPERTY + "<" + maxHops + ")";
}
/**
* Create an AMQP Message used to instruct the remote peer that it should perform
* Federation operations on the given {@link FederationReceiveFromQueuePolicy}.

View File

@ -19,6 +19,8 @@ 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.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
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;
@ -44,7 +46,6 @@ 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;
@ -57,9 +58,13 @@ 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.AMQPTunneledCoreLargeMessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreMessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.MessageReader;
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.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Modified;
import org.apache.qpid.proton.amqp.messaging.Rejected;
import org.apache.qpid.proton.amqp.messaging.Released;
@ -90,10 +95,6 @@ public class AMQPFederationQueueConsumer implements FederationConsumerInternal {
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};
@ -291,7 +292,14 @@ public class AMQPFederationQueueConsumer implements FederationConsumerInternal {
protonReceiver.setSenderSettleMode(SenderSettleMode.UNSETTLED);
protonReceiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
protonReceiver.setDesiredCapabilities(DESIRED_LINK_CAPABILITIES);
protonReceiver.setDesiredCapabilities(new Symbol[] {FEDERATION_QUEUE_RECEIVER});
// If enabled offer core tunneling which we prefer to AMQP conversions of core as
// the large ones will be converted to standard AMQP messages in memory. When not
// offered the remote must not use core tunneling and AMQP conversion will be the
// fallback.
if (configuration.isCoreMessageTunnelingEnabled()) {
protonReceiver.setOfferedCapabilities(new Symbol[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT});
}
protonReceiver.setProperties(receiverProperties);
protonReceiver.setTarget(target);
protonReceiver.setSource(source);
@ -324,9 +332,9 @@ public class AMQPFederationQueueConsumer implements FederationConsumerInternal {
// 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)) {
if (protonReceiver.getRemoteSource() != null && !AmqpSupport.verifyOfferedCapabilities(protonReceiver, FEDERATION_QUEUE_RECEIVER)) {
federation.signalResourceCreateError(
ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(Arrays.toString(DESIRED_LINK_CAPABILITIES)));
ActiveMQAMQPProtocolMessageBundle.BUNDLE.missingOfferedCapability(FEDERATION_QUEUE_RECEIVER.toString()));
return;
}
@ -380,6 +388,10 @@ public class AMQPFederationQueueConsumer implements FederationConsumerInternal {
private final Queue localQueue;
private MessageReader coreMessageReader;
private MessageReader coreLargeMessageReader;
/**
* Creates the federation receiver instance.
*
@ -454,7 +466,20 @@ public class AMQPFederationQueueConsumer implements FederationConsumerInternal {
}
@Override
protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) {
protected MessageReader trySelectMessageReader(Receiver receiver, Delivery delivery) {
if (delivery.getMessageFormat() == AMQP_TUNNELED_CORE_MESSAGE_FORMAT) {
return coreMessageReader != null ?
coreMessageReader : (coreMessageReader = new AMQPTunneledCoreMessageReader(this));
} else if (delivery.getMessageFormat() == AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT) {
return coreLargeMessageReader != null ?
coreLargeMessageReader : (coreLargeMessageReader = new AMQPTunneledCoreLargeMessageReader(this));
} else {
return super.trySelectMessageReader(receiver, delivery);
}
}
@Override
protected void actualDelivery(Message message, Delivery delivery, DeliveryAnnotations deliveryAnnotations, Receiver receiver, Transaction tx) {
try {
if (logger.isTraceEnabled()) {
logger.trace("AMQP Federation {} queue consumer {} dispatching incoming message: {}",

View File

@ -19,6 +19,7 @@ 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.proton.AmqpSupport.verifyOfferedCapabilities;
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;
@ -30,7 +31,6 @@ 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;
@ -57,26 +57,10 @@ import org.apache.qpid.proton.engine.Sender;
* 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 final class AMQPFederationQueueSenderController extends AMQPFederationBaseSenderController {
public AMQPFederationQueueSenderController(AMQPSessionContext session) {
this.session = session;
this.sessionSPI = session.getSessionSPI();
}
public AMQPSessionContext getSessionContext() {
return session;
}
public AMQPSessionCallback getSessionCallback() {
return sessionSPI;
super(session);
}
@SuppressWarnings("unchecked")
@ -137,8 +121,15 @@ public final class AMQPFederationQueueSenderController implements SenderControll
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);
// We need to offer back that we support federation for the remote to complete the attach
sender.setOfferedCapabilities(new Symbol[] {FEDERATION_QUEUE_RECEIVER});
// We indicate desired to meet specification that we cannot use a capability unless we
// indicated it was desired, however unless offered by the remote we cannot use it.
sender.setDesiredCapabilities(new Symbol[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT});
// We need to check that the remote offers its ability to read tunneled core messages and
// if not we must not send them but instead convert all messages to AMQP messages first.
tunnelCoreMessages = verifyOfferedCapabilities(sender, AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT);
return (Consumer) sessionSPI.createSender(senderContext, targetQueue, selector, false);
}
@ -158,9 +149,4 @@ public final class AMQPFederationQueueSenderController implements SenderControll
return ActiveMQDefaultConfiguration.getDefaultRoutingType();
}
@Override
public void close() throws Exception {
// Currently there isn't anything needed on close of the controller
}
}

View File

@ -169,6 +169,15 @@ public class AMQPFederationSource extends AMQPFederation {
return configuration.getLargeMessageThreshold();
}
@Override
public boolean isCoreMessageTunnelingEnabled() {
if (!connected) {
throw new IllegalStateException("Cannot access connection configuration, federation is not connected");
}
return configuration.isCoreMessageTunnelingEnabled();
}
/**
* 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

View File

@ -86,6 +86,11 @@ public class AMQPFederationTarget extends AMQPFederation {
return configuration.getLinkAttachTimeout();
}
@Override
public boolean isCoreMessageTunnelingEnabled() {
return configuration.isCoreMessageTunnelingEnabled();
}
@Override
protected void handleFederationStarted() throws ActiveMQException {
// Tag the session with Federation metadata which will allow local federation policies sent by

View File

@ -51,11 +51,15 @@ 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.proton.AMQPConnectionContext;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPSessionContext;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreLargeMessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledCoreMessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.MessageReader;
import org.apache.activemq.artemis.protocol.amqp.proton.ProtonAbstractReceiver;
import org.apache.activemq.artemis.utils.ByteUtil;
import org.apache.activemq.artemis.utils.pools.MpscPool;
import org.apache.qpid.proton.amqp.messaging.Accepted;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
@ -77,6 +81,8 @@ import static org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirro
import static org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource.QUEUE;
import static org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource.INTERNAL_ID_EXTRA_PROPERTY;
import static org.apache.activemq.artemis.protocol.amqp.connect.mirror.AMQPMirrorControllerSource.TARGET_QUEUES;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implements MirrorController {
@ -166,6 +172,10 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
OperationContext mirrorContext;
private MessageReader coreMessageReader;
private MessageReader coreLargeMessageReader;
public AMQPMirrorControllerTarget(AMQPSessionCallback sessionSPI,
AMQPConnectionContext connection,
AMQPSessionContext protonSession,
@ -190,7 +200,7 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
}
@Override
protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) {
protected void actualDelivery(Message message, Delivery delivery, DeliveryAnnotations deliveryAnnotations, Receiver receiver, Transaction tx) {
recoverContext();
incrementSettle();
@ -202,47 +212,58 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
ACKMessageOperation messageAckOperation = this.ackMessageMpscPool.borrow().setDelivery(delivery);
try {
/** We use message annotations, because on the same link we will receive control messages
* coming from mirror events,
* and the actual messages that need to be replicated.
* Using anything from the body would force us to parse the body on regular messages.
* The body of the message may still be used on control messages, on cases where a JSON string is sent. */
Object eventType = AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, EVENT_TYPE);
if (eventType != null) {
if (eventType.equals(ADD_ADDRESS)) {
AddressInfo addressInfo = parseAddress(message);
addAddress(addressInfo);
} else if (eventType.equals(DELETE_ADDRESS)) {
AddressInfo addressInfo = parseAddress(message);
if (message instanceof AMQPMessage) {
final AMQPMessage amqpMessage = (AMQPMessage) message;
deleteAddress(addressInfo);
} else if (eventType.equals(CREATE_QUEUE)) {
QueueConfiguration queueConfiguration = parseQueue(message);
/** We use message annotations, because on the same link we will receive control messages
* coming from mirror events,
* and the actual messages that need to be replicated.
* Using anything from the body would force us to parse the body on regular messages.
* The body of the message may still be used on control messages, on cases where a JSON string is sent. */
Object eventType = AMQPMessageBrokerAccessor.getMessageAnnotationProperty(amqpMessage, EVENT_TYPE);
if (eventType != null) {
if (eventType.equals(ADD_ADDRESS)) {
AddressInfo addressInfo = parseAddress(amqpMessage);
createQueue(queueConfiguration);
} else if (eventType.equals(DELETE_QUEUE)) {
String address = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, ADDRESS);
String queueName = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, QUEUE);
addAddress(addressInfo);
} else if (eventType.equals(DELETE_ADDRESS)) {
AddressInfo addressInfo = parseAddress(amqpMessage);
deleteQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName));
} else if (eventType.equals(POST_ACK)) {
String nodeID = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, BROKER_ID);
deleteAddress(addressInfo);
} else if (eventType.equals(CREATE_QUEUE)) {
QueueConfiguration queueConfiguration = parseQueue(amqpMessage);
AckReason ackReason = AMQPMessageBrokerAccessor.getMessageAnnotationAckReason(message);
createQueue(queueConfiguration);
} else if (eventType.equals(DELETE_QUEUE)) {
String address = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(amqpMessage, ADDRESS);
String queueName = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(amqpMessage, QUEUE);
if (nodeID == null) {
nodeID = getRemoteMirrorId(); // not sending the nodeID means it's data generated on that broker
deleteQueue(SimpleString.toSimpleString(address), SimpleString.toSimpleString(queueName));
} else if (eventType.equals(POST_ACK)) {
String nodeID = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(amqpMessage, BROKER_ID);
AckReason ackReason = AMQPMessageBrokerAccessor.getMessageAnnotationAckReason(amqpMessage);
if (nodeID == null) {
nodeID = getRemoteMirrorId(); // not sending the nodeID means it's data generated on that broker
}
String queueName = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(amqpMessage, QUEUE);
AmqpValue value = (AmqpValue) amqpMessage.getBody();
Long messageID = (Long) value.getValue();
if (postAcknowledge(queueName, nodeID, messageID, messageAckOperation, ackReason)) {
messageAckOperation = null;
}
}
String queueName = (String) AMQPMessageBrokerAccessor.getMessageAnnotationProperty(message, QUEUE);
AmqpValue value = (AmqpValue) message.getBody();
Long messageID = (Long) value.getValue();
if (postAcknowledge(queueName, nodeID, messageID, messageAckOperation, ackReason)) {
} else {
if (sendMessage(amqpMessage, deliveryAnnotations, messageAckOperation)) {
// since the send was successful, we give up the reference here,
// so there won't be any call on afterCompleteOperations
messageAckOperation = null;
}
}
} else {
if (sendMessage(message, messageAckOperation)) {
if (sendMessage(message, deliveryAnnotations, messageAckOperation)) {
// since the send was successful, we give up the reference here,
// so there won't be any call on afterCompleteOperations
messageAckOperation = null;
@ -267,9 +288,23 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
// We don't currently support SECOND so enforce that the answer is anlways FIRST
receiver.setReceiverSettleMode(ReceiverSettleMode.FIRST);
flow();
}
@Override
protected MessageReader trySelectMessageReader(Receiver receiver, Delivery delivery) {
if (delivery.getMessageFormat() == AMQP_TUNNELED_CORE_MESSAGE_FORMAT) {
return coreMessageReader != null ?
coreMessageReader : (coreMessageReader = new AMQPTunneledCoreMessageReader(this));
} else if (delivery.getMessageFormat() == AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT) {
return coreLargeMessageReader != null ?
coreLargeMessageReader : (coreLargeMessageReader = new AMQPTunneledCoreLargeMessageReader(this));
} else {
return super.trySelectMessageReader(receiver, delivery);
}
}
private QueueConfiguration parseQueue(AMQPMessage message) {
AmqpValue bodyValue = (AmqpValue) message.getBody();
String body = (String) bodyValue.getValue();
@ -428,20 +463,19 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
* as the sendMessage was successful the OperationContext of the transaction will take care of the completion.
* The caller of this method should give up any reference to messageCompletionAck when this method returns true.
* */
private boolean sendMessage(AMQPMessage message, ACKMessageOperation messageCompletionAck) throws Exception {
private boolean sendMessage(Message message, DeliveryAnnotations deliveryAnnotations, ACKMessageOperation messageCompletionAck) throws Exception {
if (message.getMessageID() <= 0) {
message.setMessageID(server.getStorageManager().generateID());
}
String internalMirrorID = (String)AMQPMessageBrokerAccessor.getDeliveryAnnotationProperty(message, BROKER_ID);
String internalMirrorID = (String) deliveryAnnotations.getValue().get(BROKER_ID);
if (internalMirrorID == null) {
internalMirrorID = getRemoteMirrorId(); // not pasisng the ID means the data was generated on the remote broker
}
Long internalIDLong = (Long) AMQPMessageBrokerAccessor.getDeliveryAnnotationProperty(message, INTERNAL_ID);
String internalAddress = (String) AMQPMessageBrokerAccessor.getDeliveryAnnotationProperty(message, INTERNAL_DESTINATION);
Long internalIDLong = (Long) deliveryAnnotations.getValue().get(INTERNAL_ID);
String internalAddress = (String) deliveryAnnotations.getValue().get(INTERNAL_DESTINATION);
Collection<String> targetQueues = (Collection) AMQPMessageBrokerAccessor.getDeliveryAnnotationProperty(message, TARGET_QUEUES);
Collection<String> targetQueues = (Collection<String>) deliveryAnnotations.getValue().get(TARGET_QUEUES);
long internalID = 0;
@ -502,9 +536,9 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
}
/** When the source mirror receives messages from a cluster member of his own, it should then fill targetQueues so we could play the same semantic the source applied on its routing */
private void targetQueuesRouting( final Message message,
final RoutingContext context,
final Collection<String> queueNames) throws Exception {
private void targetQueuesRouting(final Message message,
final RoutingContext context,
final Collection<String> queueNames) throws Exception {
Bindings bindings = server.getPostOffice().getBindingsForAddress(message.getAddressSimpleString());
queueNames.forEach(name -> {
Binding binding = bindings.getBinding(name);
@ -518,7 +552,6 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
});
}
@Override
public void postAcknowledge(MessageReference ref, AckReason reason) {
// Do nothing
@ -590,7 +623,5 @@ public class AMQPMirrorControllerTarget extends ProtonAbstractReceiver implement
public void run() {
operation.done();
}
}
}

View File

@ -435,7 +435,15 @@ public class AMQPConnectionContext extends ProtonInitializable implements EventH
return;
}
receiver.setOfferedCapabilities(new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY});
// We need to check if the remote desires to send us tunneled core messages or not, and if
// we support that we need to offer that back so it knows it can actually do core tunneling.
if (verifyDesiredCapability(receiver, AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT)) {
receiver.setOfferedCapabilities(new Symbol[] {AMQPMirrorControllerSource.MIRROR_CAPABILITY,
AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT});
} else {
receiver.setOfferedCapabilities(new Symbol[]{AMQPMirrorControllerSource.MIRROR_CAPABILITY});
}
protonSession.addReplicaTarget(receiver);
}

View File

@ -0,0 +1,115 @@
/*
* 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 org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPLargeMessage;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
/**
* Reader of {@link AMQPLargeMessage} content which reads all bytes and completes once a
* non-partial delivery is read.
*/
public class AMQPLargeMessageReader implements MessageReader {
private final ProtonAbstractReceiver serverReceiver;
private AMQPLargeMessage currentMessage;
private DeliveryAnnotations deliveryAnnotations;
private boolean closed = true;
public AMQPLargeMessageReader(ProtonAbstractReceiver serverReceiver) {
this.serverReceiver = serverReceiver;
}
@Override
public DeliveryAnnotations getDeliveryAnnotations() {
return deliveryAnnotations;
}
@Override
public void close() {
if (!closed) {
if (currentMessage != null) {
try {
currentMessage.deleteFile();
} catch (Throwable error) {
ActiveMQServerLogger.LOGGER.errorDeletingLargeMessageFile(error);
} finally {
currentMessage = null;
}
}
deliveryAnnotations = null;
closed = true;
}
}
@Override
public AMQPLargeMessageReader open() {
if (!closed) {
throw new IllegalStateException("Reader was not closed before call to open.");
}
closed = false;
return this;
}
@Override
public Message readBytes(Delivery delivery) throws Exception {
if (closed) {
throw new IllegalStateException("AMQP Large Message Reader is closed and read cannot proceed");
}
final Receiver receiver = ((Receiver) delivery.getLink());
final ReadableBuffer dataBuffer = receiver.recv();
if (currentMessage == null) {
final AMQPSessionCallback sessionSPI = serverReceiver.getSessionContext().getSessionSPI();
final long id = sessionSPI.getStorageManager().generateID();
currentMessage = new AMQPLargeMessage(id, delivery.getMessageFormat(), null,
sessionSPI.getCoreMessageObjectPools(),
sessionSPI.getStorageManager());
currentMessage.parseHeader(dataBuffer);
sessionSPI.getStorageManager().largeMessageCreated(id, currentMessage);
}
currentMessage.addBytes(dataBuffer);
final AMQPLargeMessage result;
if (!delivery.isPartial()) {
currentMessage.releaseResources(true, true);
result = currentMessage;
// We don't want a close to delete the file now, we've released the resources.
currentMessage = null;
deliveryAnnotations = result.getDeliveryAnnotations();
} else {
result = null;
}
return result;
}
}

View File

@ -0,0 +1,316 @@
/*
* 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 java.lang.invoke.MethodHandles;
import org.apache.activemq.artemis.core.message.LargeBodyReader;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPLargeMessage;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessageBrokerAccessor;
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.util.NettyReadable;
import org.apache.activemq.artemis.protocol.amqp.util.NettyWritable;
import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode;
import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Header;
import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
import org.apache.qpid.proton.amqp.messaging.Properties;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
/**
* A writer of {@link AMQPLargeMessage} content that handles the read from
* large message file and write into the AMQP sender with some respect for
* the AMQP frame size in use by this connection.
*/
public class AMQPLargeMessageWriter implements MessageWriter {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final ProtonServerSenderContext serverSender;
private final AMQPConnectionContext connection;
private final AMQPSessionCallback sessionSPI;
private final Sender protonSender;
private MessageReference reference;
private AMQPLargeMessage message;
private Delivery delivery;
private long position;
private boolean initialPacketHandled;
private volatile boolean closed = true;
public AMQPLargeMessageWriter(ProtonServerSenderContext serverSender) {
this.serverSender = serverSender;
this.connection = serverSender.getSessionContext().getAMQPConnectionContext();
this.sessionSPI = serverSender.getSessionContext().getSessionSPI();
this.protonSender = serverSender.getSender();
}
@Override
public boolean isWriting() {
return !closed;
}
@Override
public void close() {
if (!closed) {
try {
if (message != null) {
message.usageDown();
}
} finally {
reset(true);
}
}
}
@Override
public AMQPLargeMessageWriter open() {
if (!closed) {
throw new IllegalStateException("Trying to open an AMQP Large Message writer that was not closed");
}
reset(false);
return this;
}
private void reset(boolean closedState) {
message = null;
reference = null;
delivery = null;
position = 0;
initialPacketHandled = false;
closed = closedState;
}
@Override
public void writeBytes(MessageReference messageReference) {
if (protonSender.getLocalState() == EndpointState.CLOSED) {
logger.debug("Not delivering message {} as the sender is closed and credits were available, if you see too many of these it means clients are issuing credits and closing the connection with pending credits a lot of times", messageReference);
return;
}
if (closed) {
throw new IllegalStateException("Cannot write to an AMQP Large Message Writer that has been closed");
}
this.reference = messageReference;
this.message = (AMQPLargeMessage) messageReference.getMessage();
if (sessionSPI.invokeOutgoing(message, (ActiveMQProtonRemotingConnection) sessionSPI.getTransportConnection().getProtocolConnection()) != null) {
return;
}
this.delivery = serverSender.createDelivery(messageReference, (int) this.message.getMessageFormat());
message.usageUp();
tryDelivering();
}
/**
* Used to provide re-entry from the flow control executor when IO back-pressure has eased
*/
private void resume() {
connection.runNow(this::tryDelivering);
}
private void tryDelivering() {
// This is discounting some bytes due to Transfer payload
final int frameSize = protonSender.getSession().getConnection().getTransport().getOutboundFrameSizeLimit() - 50 - (delivery.getTag() != null ? delivery.getTag().length : 0);
try {
final ByteBuf frameBuffer = PooledByteBufAllocator.DEFAULT.directBuffer(frameSize, frameSize);
final NettyReadable frameView = new NettyReadable(frameBuffer);
try (LargeBodyReader context = message.getLargeBodyReader()) {
context.open();
context.position(position);
long bodySize = context.getSize();
// materialize it so we can use its internal NIO buffer
frameBuffer.ensureWritable(frameSize);
if (!initialPacketHandled && protonSender.getLocalState() != EndpointState.CLOSED) {
if (!deliverInitialPacket(context, frameBuffer)) {
return;
}
initialPacketHandled = true;
}
for (; protonSender.getLocalState() != EndpointState.CLOSED && position < bodySize; ) {
if (!connection.flowControl(this::resume)) {
return;
}
frameBuffer.clear();
final int readSize = context.readInto(frameBuffer.internalNioBuffer(0, frameSize));
frameBuffer.writerIndex(readSize);
protonSender.send(frameView);
position += readSize;
if (readSize > 0) {
if (position < bodySize) {
connection.instantFlush();
}
}
}
} finally {
frameBuffer.release();
}
serverSender.reportDeliveryComplete(this, reference, delivery, true);
} catch (Exception deliveryError) {
serverSender.reportDeliveryError(this, reference, deliveryError);
}
}
private boolean deliverInitialPacket(final LargeBodyReader context, final ByteBuf frameBuffer) throws Exception {
assert position == 0 && context.position() == 0 && !initialPacketHandled;
if (!connection.flowControl(this::resume)) {
return false;
}
frameBuffer.clear();
message.checkReference(reference);
DeliveryAnnotations deliveryAnnotationsToEncode = reference.getProtocolData(DeliveryAnnotations.class);
try {
replaceInitialHeader(deliveryAnnotationsToEncode, context, new NettyWritable(frameBuffer));
} catch (IndexOutOfBoundsException indexOutOfBoundsException) {
assert position == 0 : "this shouldn't happen unless replaceInitialHeader is updating position before modifying frameBuffer";
logger.debug("Delivery of message failed with an overFlowException, retrying again with expandable buffer");
// on the very first packet, if the initial header was replaced with a much bigger header
// (re-encoding) we could recover the situation with a retry using an expandable buffer.
// this is tested on org.apache.activemq.artemis.tests.integration.amqp.AmqpMessageDivertsTest
sendAndFlushInitialPacket(deliveryAnnotationsToEncode, context);
return true;
}
int readSize = 0;
final int writableBytes = frameBuffer.writableBytes();
if (writableBytes != 0) {
final int writtenBytes = frameBuffer.writerIndex();
readSize = context.readInto(frameBuffer.internalNioBuffer(writtenBytes, writableBytes));
if (readSize > 0) {
frameBuffer.writerIndex(writtenBytes + readSize);
}
}
protonSender.send(new NettyReadable(frameBuffer));
if (readSize > 0) {
position += readSize;
}
connection.instantFlush();
return true;
}
/**
* This must be used when either the delivery annotations or re-encoded buffer is bigger than the frame size.
* <br>
* This will create one expandable buffer, send and flush it.
*/
private void sendAndFlushInitialPacket(DeliveryAnnotations deliveryAnnotationsToEncode, LargeBodyReader context) throws Exception {
// if the buffer overflow happened during the initial position
// this means the replaced headers are bigger then the frame size
// on this case we do with an expandable netty buffer
final ByteBuf nettyBuffer = PooledByteBufAllocator.DEFAULT.directBuffer(AMQPMessageBrokerAccessor.getRemainingBodyPosition(message) * 2);
try {
replaceInitialHeader(deliveryAnnotationsToEncode, context, new NettyWritable(nettyBuffer));
protonSender.send(new NettyReadable(nettyBuffer));
} finally {
nettyBuffer.release();
connection.instantFlush();
}
}
private int replaceInitialHeader(DeliveryAnnotations deliveryAnnotationsToEncode, LargeBodyReader context, WritableBuffer buf) throws Exception {
TLSEncode.getEncoder().setByteBuffer(buf);
try {
int proposedPosition = writeHeaderAndAnnotations(deliveryAnnotationsToEncode);
if (message.isReencoded()) {
proposedPosition = writeMessageAnnotationsPropertiesAndApplicationProperties(context, message);
}
context.position(proposedPosition);
position = proposedPosition;
return (int) position;
} finally {
TLSEncode.getEncoder().setByteBuffer((WritableBuffer) null);
}
}
/**
* Write properties and application properties when the message is flagged as re-encoded.
*/
private int writeMessageAnnotationsPropertiesAndApplicationProperties(LargeBodyReader context, AMQPLargeMessage message) throws Exception {
int bodyPosition = AMQPMessageBrokerAccessor.getRemainingBodyPosition(message);
assert bodyPosition > 0;
writeMessageAnnotationsPropertiesAndApplicationPropertiesInternal(message);
return bodyPosition;
}
private void writeMessageAnnotationsPropertiesAndApplicationPropertiesInternal(AMQPLargeMessage message) {
MessageAnnotations messageAnnotations = AMQPMessageBrokerAccessor.getDecodedMessageAnnotations(message);
if (messageAnnotations != null) {
TLSEncode.getEncoder().writeObject(messageAnnotations);
}
Properties amqpProperties = AMQPMessageBrokerAccessor.getCurrentProperties(message);
if (amqpProperties != null) {
TLSEncode.getEncoder().writeObject(amqpProperties);
}
ApplicationProperties applicationProperties = AMQPMessageBrokerAccessor.getDecodedApplicationProperties(message);
if (applicationProperties != null) {
TLSEncode.getEncoder().writeObject(applicationProperties);
}
}
private int writeHeaderAndAnnotations(DeliveryAnnotations deliveryAnnotationsToEncode) {
Header header = AMQPMessageBrokerAccessor.getCurrentHeader(message);
if (header != null) {
TLSEncode.getEncoder().writeObject(header);
}
if (deliveryAnnotationsToEncode != null) {
TLSEncode.getEncoder().writeObject(deliveryAnnotationsToEncode);
}
return message.getPositionAfterDeliveryAnnotations();
}
}

View File

@ -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.protocol.amqp.proton;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPMessage;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
/**
* Reader of AMQP (non-large) messages which reads all bytes and decodes once a non-partial
* delivery is read.
*/
public class AMQPMessageReader implements MessageReader {
private final ProtonAbstractReceiver serverReceiver;
private DeliveryAnnotations deliveryAnnotations;
private boolean closed = true;
public AMQPMessageReader(ProtonAbstractReceiver serverReceiver) {
this.serverReceiver = serverReceiver;
}
@Override
public DeliveryAnnotations getDeliveryAnnotations() {
return deliveryAnnotations;
}
@Override
public void close() {
closed = true;
deliveryAnnotations = null;
}
@Override
public MessageReader open() {
if (!closed) {
throw new IllegalStateException("Message reader must be properly closed before open call");
}
return this;
}
@Override
public Message readBytes(Delivery delivery) {
if (delivery.isPartial()) {
return null; // Only receive payload when complete
}
final Receiver receiver = ((Receiver) delivery.getLink());
final ReadableBuffer payload = receiver.recv();
final AMQPMessage message = serverReceiver.getSessionContext().getSessionSPI().createStandardMessage(delivery, payload);
deliveryAnnotations = message.getDeliveryAnnotations();
return message;
}
}

View File

@ -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.proton;
import java.lang.invoke.MethodHandles;
import org.apache.activemq.artemis.core.server.MessageReference;
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.broker.ActiveMQProtonRemotingConnection;
import org.apache.activemq.artemis.protocol.amqp.converter.CoreAmqpConverter;
import org.apache.activemq.artemis.protocol.amqp.util.NettyReadable;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An writer of AMQP (non-large) messages or messages which will convert any
* non-AMQP message to AMQP before writing the encoded bytes into the AMQP
* sender.
*/
public class AMQPMessageWriter implements MessageWriter {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final ProtonServerSenderContext serverSender;
private final AMQPSessionCallback sessionSPI;
private final Sender protonSender;
public AMQPMessageWriter(ProtonServerSenderContext serverSender) {
this.serverSender = serverSender;
this.sessionSPI = serverSender.getSessionContext().getSessionSPI();
this.protonSender = serverSender.getSender();
}
@Override
public void writeBytes(MessageReference messageReference) {
if (protonSender.getLocalState() == EndpointState.CLOSED) {
logger.debug("Not delivering message {} as the sender is closed and credits were available, if you see too many of these it means clients are issuing credits and closing the connection with pending credits a lot of times", messageReference);
return;
}
try {
final AMQPMessage amqpMessage = CoreAmqpConverter.checkAMQP(messageReference.getMessage(), null);
if (sessionSPI.invokeOutgoing(amqpMessage, (ActiveMQProtonRemotingConnection) sessionSPI.getTransportConnection().getProtocolConnection()) != null) {
return;
}
final Delivery delivery = serverSender.createDelivery(messageReference, (int) amqpMessage.getMessageFormat());
final ReadableBuffer sendBuffer = amqpMessage.getSendBuffer(messageReference.getDeliveryCount(), messageReference);
boolean releaseRequired = sendBuffer instanceof NettyReadable;
try {
if (releaseRequired) {
protonSender.send(sendBuffer);
// Above send copied, so release now if needed
releaseRequired = false;
((NettyReadable) sendBuffer).getByteBuf().release();
} else {
// Don't have pooled content, no need to release or copy.
protonSender.sendNoCopy(sendBuffer);
}
serverSender.reportDeliveryComplete(this, messageReference, delivery, false);
} finally {
if (releaseRequired) {
((NettyReadable) sendBuffer).getByteBuf().release();
}
}
} catch (Exception deliveryError) {
serverSender.reportDeliveryError(this, messageReference, deliveryError);
}
}
}

View File

@ -0,0 +1,333 @@
/*
* 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 org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.LargeServerMessage;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException;
import org.apache.activemq.artemis.protocol.amqp.util.NettyReadable;
import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode;
import org.apache.qpid.proton.amqp.messaging.AmqpSequence;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.Data;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Footer;
import org.apache.qpid.proton.amqp.messaging.Header;
import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
import org.apache.qpid.proton.amqp.messaging.Properties;
import org.apache.qpid.proton.codec.DecoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.TypeConstructor;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
/**
* Reader of tunneled large Core message that have been written as the body of an
* AMQP delivery with a custom message format that indicates this payload. The reader
* will extract bytes from the delivery and write them into a Core large message file
* which is then routed into the broker as if received from a Core connection.
*/
public class AMQPTunneledCoreLargeMessageReader implements MessageReader {
private final ProtonAbstractReceiver serverReceiver;
private enum State {
/**
* Awaiting initial decode of first section in delivery which could be delivery
* annotations or could be the first data section which will be the core message
* headers and properties.
*/
INITIALIZING,
/**
* Accumulating the bytes from the remote that comprise the message headers and
* properties that must be decoded before creating the large message instance.
*/
CORE_HEADER_BUFFERING,
/**
* Awaiting a Data section that contains some or all of the bytes of the
* Core large message body, there can be multiple Data sections if the size
* is greater than 2GB or the peer encodes them in smaller chunks
*/
BODY_SECTION_PENDING,
/**
* Accumulating the actual large message payload into the large message file
*/
BODY_BUFFERING,
/**
* The full message has been read and no more incoming bytes are accepted.
*/
DONE,
/**
* Indicates the reader is closed and cannot be used until opened.
*/
CLOSED
}
private final CompositeByteBuf pendingRecvBuffer = Unpooled.compositeBuffer();
private final NettyReadable pendingReadable = new NettyReadable(pendingRecvBuffer);
private DeliveryAnnotations deliveryAnnotations;
private ByteBuf coreHeadersBuffer;
private LargeServerMessage coreLargeMessage;
private int largeMessageSectionRemaining;
private State state = State.CLOSED;
public AMQPTunneledCoreLargeMessageReader(ProtonAbstractReceiver serverReceiver) {
this.serverReceiver = serverReceiver;
}
@Override
public DeliveryAnnotations getDeliveryAnnotations() {
return deliveryAnnotations;
}
@Override
public void close() {
if (state != State.CLOSED) {
if (coreLargeMessage != null) {
try {
coreLargeMessage.deleteFile();
} catch (Throwable error) {
ActiveMQServerLogger.LOGGER.errorDeletingLargeMessageFile(error);
} finally {
coreLargeMessage = null;
}
}
pendingRecvBuffer.clear();
deliveryAnnotations = null;
coreHeadersBuffer = null;
largeMessageSectionRemaining = 0;
state = State.CLOSED;
}
}
@Override
public AMQPTunneledCoreLargeMessageReader open() {
if (state != State.CLOSED) {
throw new IllegalStateException("Reader the reader was not closed before call to open.");
}
state = State.INITIALIZING;
return this;
}
@Override
public Message readBytes(Delivery delivery) throws Exception {
if (state == State.CLOSED) {
throw new IllegalStateException("Core over AMQP Large Message Reader is closed and read cannot proceed");
}
if (state == State.DONE) {
throw new IllegalStateException("The reader already read a message and was not reset");
}
final Receiver receiver = ((Receiver) delivery.getLink());
final ReadableBuffer recieved = receiver.recv();
// Store what we read into a composite as we may need to hold onto some or all of
// the received data until a complete type is available.
pendingRecvBuffer.addComponent(true, Unpooled.wrappedBuffer(recieved.byteBuffer()));
final DecoderImpl decoder = TLSEncode.getDecoder();
decoder.setBuffer(pendingReadable);
try {
while (pendingRecvBuffer.isReadable()) {
pendingRecvBuffer.markReaderIndex();
try {
if (state == State.CORE_HEADER_BUFFERING) {
tryReadHeadersAndProperties(pendingRecvBuffer);
} else if (state == State.BODY_BUFFERING) {
tryReadMessageBody(delivery, pendingRecvBuffer);
} else {
scanForNextMessageSection(decoder);
}
// Advance mark so read bytes can be discarded and we can start from this
// location next time.
pendingRecvBuffer.markReaderIndex();
} catch (ActiveMQException ex ) {
throw ex;
} catch (Exception e) {
// We expect exceptions from proton when only partial section are received within
// a frame so we will continue trying to decode until either we read a complete
// section or we consume everything to the point of completing the delivery.
if (delivery.isPartial()) {
pendingRecvBuffer.resetReaderIndex();
break; // Not enough data to decode yet.
} else {
throw new ActiveMQAMQPInternalErrorException(
"Decoding error encounted in tunneled core large message.", e);
}
} finally {
pendingRecvBuffer.discardReadComponents();
}
}
if (!delivery.isPartial()) {
if (coreLargeMessage == null) {
throw new ActiveMQAMQPInternalErrorException(
"Tunneled Core large message delivery contained no large message body.");
}
final Message result = coreLargeMessage.toMessage();
// We don't want a close to delete the file now, so we release these resources.
coreLargeMessage.releaseResources(true, true);
coreLargeMessage = null;
state = State.DONE;
return result;
}
return null;
} finally {
decoder.setBuffer(null);
}
}
private void scanForNextMessageSection(DecoderImpl decoder) throws ActiveMQException {
final TypeConstructor<?> constructor = decoder.readConstructor();
if (Header.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (DeliveryAnnotations.class.equals(constructor.getTypeClass())) {
deliveryAnnotations = (DeliveryAnnotations) constructor.readValue();
} else if (MessageAnnotations.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (Properties.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (ApplicationProperties.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (Data.class.equals(constructor.getTypeClass())) {
// Store how much we need to read before the Data section payload is consumed.
final int dataSectionRemaining = readNextDataSectionSize(pendingReadable);
if (state.ordinal() < State.CORE_HEADER_BUFFERING.ordinal()) {
coreHeadersBuffer = Unpooled.buffer(dataSectionRemaining, dataSectionRemaining);
state = State.CORE_HEADER_BUFFERING;
} else if (state.ordinal() < State.BODY_BUFFERING.ordinal()) {
largeMessageSectionRemaining = dataSectionRemaining;
state = State.BODY_BUFFERING;
} else {
throw new IllegalStateException("Data section found when not expecting any more input.");
}
} else if (AmqpValue.class.equals(constructor.getTypeClass())) {
throw new IllegalArgumentException("Received an AmqpValue payload in core tunneled AMQP message");
} else if (AmqpSequence.class.equals(constructor.getTypeClass())) {
throw new IllegalArgumentException("Received an AmqpSequence payload in core tunneled AMQP message");
} else if (Footer.class.equals(constructor.getTypeClass())) {
if (coreLargeMessage == null) {
throw new IllegalArgumentException("Received an Footer but no actual message paylod in core tunneled AMQP message");
}
constructor.skipValue(); // Ignore for forward compatibility
}
}
// This reads the size for the encoded Binary that should follow a detected Data section and
// leaves the buffer at the head of the payload of the Binary, readers from here should just
// use the returned size to determine when all the binary payload is consumed.
private static int readNextDataSectionSize(ReadableBuffer buffer) throws ActiveMQException {
final byte encodingCode = buffer.get();
switch (encodingCode) {
case EncodingCodes.VBIN8:
return buffer.get() & 0xFF;
case EncodingCodes.VBIN32:
return buffer.getInt();
case EncodingCodes.NULL:
return 0;
default:
throw new ActiveMQException("Expected Binary type but found encoding: " + encodingCode);
}
}
private boolean tryReadHeadersAndProperties(ByteBuf buffer) throws Exception {
final int writeSize = Math.min(coreHeadersBuffer.writableBytes(), buffer.readableBytes());
if (writeSize > 0) {
coreHeadersBuffer.writeBytes(buffer, writeSize);
}
// Have we read all the message headers and properties or is there more to come
// we can't create the actual Core message until we get everything.
if (coreHeadersBuffer.isWritable()) {
return false;
}
try {
final AMQPSessionCallback sessionSPI = serverReceiver.getSessionContext().getSessionSPI();
final long id = sessionSPI.getStorageManager().generateID();
final CoreMessage coreMessage = new CoreMessage();
coreMessage.decodeHeadersAndProperties(coreHeadersBuffer);
coreLargeMessage = sessionSPI.getStorageManager().createLargeMessage(id, coreMessage);
coreHeadersBuffer = null; // Buffer can be discarded once the decode is done
state = State.BODY_SECTION_PENDING;
} catch (ActiveMQException ex) {
throw ex;
} catch (Exception ex) {
throw new ActiveMQAMQPInternalErrorException(
"Encountered error while attempting to create a Core Large message instance", ex);
}
return true;
}
private void tryReadMessageBody(Delivery delivery, ByteBuf buffer) throws Exception {
// Account for multiple Data section in the body, don't read full contents unless
// the current readable is the full section contents or what's left of it.
final int writeSize = Math.min(largeMessageSectionRemaining, buffer.readableBytes());
try {
final ActiveMQBuffer bodyBuffer = ActiveMQBuffers.wrappedBuffer(buffer.slice(buffer.readerIndex(), writeSize));
coreLargeMessage.addBytes(bodyBuffer);
} catch (ActiveMQException ex) {
throw ex;
} catch (Exception ex) {
throw new ActiveMQAMQPInternalErrorException("Error while adding body bytes to Core Large message", ex);
}
largeMessageSectionRemaining -= writeSize;
buffer.readerIndex(buffer.readerIndex() + writeSize);
if (largeMessageSectionRemaining == 0) {
state = State.BODY_SECTION_PENDING;
}
}
}

View File

@ -0,0 +1,382 @@
/*
* 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.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT;
import java.lang.invoke.MethodHandles;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.message.LargeBodyReader;
import org.apache.activemq.artemis.core.persistence.impl.journal.LargeServerMessageImpl;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.protocol.amqp.util.NettyReadable;
import org.apache.activemq.artemis.protocol.amqp.util.NettyWritable;
import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
/**
* Writer of tunneled large Core messages that will be written as the body of an
* AMQP delivery with a custom message format that indicates this payload. The writer
* will read bytes from the Core large message file and write them into an AMQP
* Delivery that will be sent across to the remote peer where it can be processed
* and a Core message recreated for dispatch as if it had been sent from a Core
* connection.
*/
public class AMQPTunneledCoreLargeMessageWriter implements MessageWriter {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final byte DATA_DESCRIPTOR = 0x75;
private static final int DATA_SECTION_ENCODING_BYTES = Long.BYTES;
private enum State {
/**
* Writing the optional AMQP delivery annotations which can provide additional context.
*/
STREAMING_DELIVERY_ANNOTATIONS,
/**
* Writing the core message headers and properties that describe the message.
*/
STREAMING_CORE_HEADERS,
/**
* Writing the actual message payload from the large message file.
*/
STREAMING_BODY,
/**
* Done writing, no more bytes will be written.
*/
DONE,
/**
* The writer is closed and cannot be used again until open is called.
*/
CLOSED
}
private final ProtonServerSenderContext serverSender;
private final AMQPConnectionContext connection;
private final Sender protonSender;
private DeliveryAnnotations annotations;
private MessageReference reference;
private LargeServerMessageImpl message;
private Delivery delivery;
private int frameSize;
// Used for storing the encoded delivery annotations or the core header and properties
// while writing into the frame buffer which could be smaller.
private ByteBuf encodingBuffer;
// For writing any of the sections this stores where we are in the total writes expected
// for that section so that on IO flow control we can resume later where we left off.
private long position;
// When writing the Data section(s) for the core large message body each Data section is
// limited to 2GB to ensure receiver Codec can handle the encoding.
private int dataSectionRemaining;
private volatile State state = State.CLOSED;
public AMQPTunneledCoreLargeMessageWriter(ProtonServerSenderContext serverSender) {
this.serverSender = serverSender;
this.connection = serverSender.getSessionContext().getAMQPConnectionContext();
this.protonSender = serverSender.getSender();
}
@Override
public boolean isWriting() {
return state != State.CLOSED;
}
@Override
public void close() {
if (state != State.CLOSED) {
try {
if (message != null) {
message.usageDown();
}
} finally {
reset(State.CLOSED);
}
}
}
@Override
public AMQPTunneledCoreLargeMessageWriter open() {
if (state != State.CLOSED) {
throw new IllegalStateException("Trying to open an AMQP Large Message writer that was not closed");
}
reset(State.STREAMING_DELIVERY_ANNOTATIONS);
return this;
}
private void reset(State newState) {
message = null;
reference = null;
delivery = null;
position = 0;
dataSectionRemaining = 0;
state = newState;
encodingBuffer = null;
}
@Override
public void writeBytes(MessageReference messageReference) {
if (protonSender.getLocalState() == EndpointState.CLOSED) {
logger.debug("Not delivering message {} as the sender is closed and credits were available, if you see too many of these it means clients are issuing credits and closing the connection with pending credits a lot of times", messageReference);
return;
}
if (state == State.CLOSED) {
throw new IllegalStateException("Cannot write to an AMQP Large Message Writer that has been closed");
}
if (state == State.DONE) {
throw new IllegalStateException(
"Cannot write to an AMQP Large Message Writer that was already used to write a message and was not reset");
}
reference = messageReference;
message = (LargeServerMessageImpl) messageReference.getMessage();
annotations = reference.getProtocolData(DeliveryAnnotations.class);
delivery = serverSender.createDelivery(messageReference, AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT);
// We will deduct some bytes from the frame for encoding the Transfer payload which could exclude
// the delivery tag on successive transfers but we aren't sure if that will happen so we assume not.
frameSize = protonSender.getSession().getConnection().getTransport().getOutboundFrameSizeLimit() - 50 - (delivery.getTag() != null ? delivery.getTag().length : 0);
message.usageUp();
tryDelivering();
}
/**
* Used to provide re-entry from the flow control executor when IO back-pressure has eased
*/
private void resume() {
connection.runNow(this::tryDelivering);
}
private ByteBuf getOrCreateDeliveryAnnotationsBuffer() {
if (encodingBuffer == null) {
encodingBuffer = Unpooled.buffer();
final EncoderImpl encoder = TLSEncode.getEncoder();
try {
encoder.setByteBuffer(new NettyWritable(encodingBuffer));
encoder.writeObject(annotations);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
}
return encodingBuffer;
}
private ByteBuf getOrCreateMessageHeaderBuffer() {
if (encodingBuffer == null) {
final int headersSize = message.getHeadersAndPropertiesEncodeSize();
final int bufferSize = headersSize + DATA_SECTION_ENCODING_BYTES;
encodingBuffer = Unpooled.buffer(bufferSize, bufferSize);
writeDataSectionTypeInfo(encodingBuffer, headersSize);
message.encodeHeadersAndProperties(encodingBuffer);
}
return encodingBuffer;
}
// Will return true when the optional delivery annotations are fully sent or are not present, and false
// if not able to send due to a flow control event.
private boolean trySendDeliveryAnnotations(ByteBuf frameBuffer, NettyReadable frameView) {
for (; protonSender.getLocalState() != EndpointState.CLOSED && state == State.STREAMING_DELIVERY_ANNOTATIONS; ) {
if (annotations != null && annotations.getValue() != null && !annotations.getValue().isEmpty()) {
if (!connection.flowControl(this::resume)) {
break; // Resume will restart writing the headers section from where we left off.
}
final ByteBuf annotationsBuffer = getOrCreateDeliveryAnnotationsBuffer();
final int readSize = (int) Math.min(frameBuffer.writableBytes(), annotationsBuffer.readableBytes() - position);
position += readSize;
annotationsBuffer.readBytes(frameBuffer, readSize);
// In case the Delivery Annotations encoding exceed the AMQP frame size we
// flush and keep sending until done or until flow controlled.
if (!frameBuffer.isWritable()) {
protonSender.send(frameView);
frameBuffer.clear();
connection.instantFlush();
}
if (!annotationsBuffer.isReadable()) {
encodingBuffer = null;
position = 0;
state = State.STREAMING_CORE_HEADERS;
}
} else {
state = State.STREAMING_CORE_HEADERS;
}
}
return state == State.STREAMING_CORE_HEADERS;
}
// Will return true when the header was fully sent false if not all the header
// data could be sent due to a flow control event.
private boolean trySendHeadersAndProperties(ByteBuf frameBuffer, NettyReadable frameView) {
for (; protonSender.getLocalState() != EndpointState.CLOSED && state == State.STREAMING_CORE_HEADERS; ) {
if (!connection.flowControl(this::resume)) {
break; // Resume will restart writing the headers section from where we left off.
}
final ByteBuf headerBuffer = getOrCreateMessageHeaderBuffer();
final int readSize = (int) Math.min(frameBuffer.writableBytes(), headerBuffer.readableBytes() - position);
position += readSize;
headerBuffer.readBytes(frameBuffer, readSize);
// In case the Core message header and properties exceed the AMQP frame size we
// flush and keep sending until done or until flow controlled.
if (!frameBuffer.isWritable()) {
protonSender.send(frameView);
frameBuffer.clear();
connection.instantFlush();
}
if (!headerBuffer.isReadable()) {
encodingBuffer = null;
position = 0;
state = State.STREAMING_BODY;
}
}
return state == State.STREAMING_BODY;
}
// Should return true whenever the message contents have been fully written and false otherwise
// so that more writes can be attempted after flow control allows it.
private boolean tryDeliveryMessageBody(ByteBuf frameBuffer, NettyReadable frameView) throws ActiveMQException {
try (LargeBodyReader context = message.getLargeBodyReader()) {
context.open();
context.position(position);
final long bodySize = context.getSize();
for (; protonSender.getLocalState() != EndpointState.CLOSED && state == State.STREAMING_BODY; ) {
if (!connection.flowControl(this::resume)) {
break;
}
if (dataSectionRemaining == 0) {
// Cap section at 2GB or the remaining contents of the file if smaller
dataSectionRemaining = (int) Math.min(Integer.MAX_VALUE, bodySize - position);
// Ensure the frame buffer has room for the Data section encoding.
if (frameBuffer.writableBytes() < DATA_SECTION_ENCODING_BYTES) {
protonSender.send(frameView);
frameBuffer.clear();
}
writeDataSectionTypeInfo(frameBuffer, dataSectionRemaining);
}
final int readSize = context.readInto(
frameBuffer.internalNioBuffer(frameBuffer.writerIndex(), frameBuffer.writableBytes()));
frameBuffer.writerIndex(frameBuffer.writerIndex() + readSize);
position += readSize;
dataSectionRemaining -= readSize;
if (!frameBuffer.isWritable() || position == bodySize) {
protonSender.send(frameView);
frameBuffer.clear();
// Only flush on partial writes, the sender will flush on completion so
// we avoid excessive flushing in that case.
if (position < bodySize) {
connection.instantFlush();
} else {
state = State.DONE;
}
}
}
return state == State.DONE;
}
}
private void tryDelivering() {
final ByteBuf frameBuffer = PooledByteBufAllocator.DEFAULT.directBuffer(frameSize, frameSize);
try {
final NettyReadable frameView = new NettyReadable(frameBuffer);
// materialize it so we can use its internal NIO buffer
frameBuffer.ensureWritable(frameSize);
switch (state) {
case STREAMING_DELIVERY_ANNOTATIONS:
if (!trySendDeliveryAnnotations(frameBuffer, frameView)) {
return;
}
case STREAMING_CORE_HEADERS:
if (!trySendHeadersAndProperties(frameBuffer, frameView)) {
return;
}
case STREAMING_BODY:
if (!tryDeliveryMessageBody(frameBuffer, frameView)) {
return;
}
serverSender.reportDeliveryComplete(this, reference, delivery, true);
break;
default:
throw new IllegalStateException("The writer already wrote a message and was not reset");
}
} catch (Exception deliveryError) {
serverSender.reportDeliveryError(this, reference, deliveryError);
} finally {
frameBuffer.release();
}
}
private void writeDataSectionTypeInfo(ByteBuf buffer, int encodedSize) {
buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
buffer.writeByte(EncodingCodes.SMALLULONG);
buffer.writeByte(DATA_DESCRIPTOR);
buffer.writeByte(EncodingCodes.VBIN32);
buffer.writeInt(encodedSize); // Core message will encode into this size.
}
}

View File

@ -0,0 +1,160 @@
/*
* 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 java.nio.ByteBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode;
import org.apache.qpid.proton.amqp.Binary;
import org.apache.qpid.proton.amqp.messaging.AmqpSequence;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.Data;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Footer;
import org.apache.qpid.proton.amqp.messaging.Header;
import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
import org.apache.qpid.proton.amqp.messaging.Properties;
import org.apache.qpid.proton.codec.DecoderImpl;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.TypeConstructor;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
/**
* Reader of tunneled Core message that have been written as the body of an AMQP
* delivery with a custom message format that indicates this payload. The reader
* will extract bytes from the delivery and decode from them a standard Core message
* which is then routed into the broker as if received from a Core connection.
*/
public class AMQPTunneledCoreMessageReader implements MessageReader {
private final ProtonAbstractReceiver serverReceiver;
private boolean closed = true;
private DeliveryAnnotations deliveryAnnotations;
public AMQPTunneledCoreMessageReader(ProtonAbstractReceiver serverReceiver) {
this.serverReceiver = serverReceiver;
}
@Override
public DeliveryAnnotations getDeliveryAnnotations() {
return deliveryAnnotations;
}
@Override
public void close() {
closed = true;
deliveryAnnotations = null;
}
@Override
public MessageReader open() {
if (!closed) {
throw new IllegalStateException("Message reader must be properly closed before open call");
}
return this;
}
@Override
public Message readBytes(Delivery delivery) {
if (delivery.isPartial()) {
return null; // Only receive payload when complete
}
final AMQPSessionCallback sessionSPI = serverReceiver.getSessionContext().getSessionSPI();
final Receiver receiver = ((Receiver) delivery.getLink());
final ReadableBuffer recievedBuffer = receiver.recv();
if (recievedBuffer.remaining() == 0) {
throw new IllegalArgumentException("Received empty delivery when expecting a core message encoding");
}
final DecoderImpl decoder = TLSEncode.getDecoder();
decoder.setBuffer(recievedBuffer);
Data payloadData = null;
try {
while (recievedBuffer.hasRemaining()) {
final TypeConstructor<?> constructor = decoder.readConstructor();
if (Header.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (DeliveryAnnotations.class.equals(constructor.getTypeClass())) {
deliveryAnnotations = (DeliveryAnnotations) constructor.readValue();
} else if (MessageAnnotations.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (Properties.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (ApplicationProperties.class.equals(constructor.getTypeClass())) {
constructor.skipValue(); // Ignore for forward compatibility
} else if (Data.class.equals(constructor.getTypeClass())) {
if (payloadData != null) {
throw new IllegalArgumentException("Received an unexpected additional Data section in core tunneled AMQP message");
}
payloadData = (Data) constructor.readValue();
} else if (AmqpValue.class.equals(constructor.getTypeClass())) {
throw new IllegalArgumentException("Received an AmqpValue payload in core tunneled AMQP message");
} else if (AmqpSequence.class.equals(constructor.getTypeClass())) {
throw new IllegalArgumentException("Received an AmqpSequence payload in core tunneled AMQP message");
} else if (Footer.class.equals(constructor.getTypeClass())) {
if (payloadData == null) {
throw new IllegalArgumentException("Received an Footer but no actual message payload in core tunneled AMQP message");
}
constructor.skipValue(); // Ignore for forward compatibility
}
}
} finally {
decoder.setBuffer(null);
}
if (payloadData == null) {
throw new IllegalArgumentException("Did not receive a Data section payload in core tunneled AMQP message");
}
final Binary payloadBinary = payloadData.getValue();
if (payloadBinary == null || payloadBinary.getLength() <= 0) {
throw new IllegalArgumentException("Received an unexpected empty message payload in core tunneled AMQP message");
}
final ByteBuffer payload = payloadBinary.asByteBuffer();
final ActiveMQBuffer buffer = ActiveMQBuffers.wrappedBuffer(payload);
// Ensure the wrapped buffer readable bytes reflects the data section payload read.
buffer.writerIndex(payload.remaining());
final CoreMessage coreMessage = new CoreMessage(sessionSPI.getCoreMessageObjectPools());
coreMessage.reloadPersistence(buffer, sessionSPI.getCoreMessageObjectPools());
coreMessage.setMessageID(sessionSPI.getStorageManager().generateID());
return coreMessage;
}
}

View File

@ -0,0 +1,117 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.activemq.artemis.protocol.amqp.proton;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
import java.lang.invoke.MethodHandles;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.ICoreMessage;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.protocol.amqp.util.NettyWritable;
import org.apache.activemq.artemis.protocol.amqp.util.TLSEncode;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Writer of tunneled Core messages that will be written as the body of an AMQP
* delivery with a custom message format that indicates this payload. The writer
* will encode the bytes from the Core large message file and write them into an
* AMQP Delivery that will be sent across to the remote peer where it can be
* processed and a Core message recreated for dispatch as if it had been sent from
* a Core connection.
*/
public class AMQPTunneledCoreMessageWriter implements MessageWriter {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final byte DATA_DESCRIPTOR = 0x75;
private static final int DATA_SECTION_ENCODING_BYTES = Long.BYTES;
private final ProtonServerSenderContext serverSender;
private final Sender protonSender;
public AMQPTunneledCoreMessageWriter(ProtonServerSenderContext serverSender) {
this.serverSender = serverSender;
this.protonSender = serverSender.getSender();
}
@Override
public void writeBytes(MessageReference messageReference) {
if (protonSender.getLocalState() == EndpointState.CLOSED) {
logger.debug("Not delivering message {} as the sender is closed and credits were available, if you see too many of these it means clients are issuing credits and closing the connection with pending credits a lot of times", messageReference);
return;
}
try {
final ICoreMessage message = (ICoreMessage) messageReference.getMessage();
final int encodedSize = message.getPersistSize();
final ByteBuf buffer = Unpooled.buffer(encodedSize + DATA_SECTION_ENCODING_BYTES); // Account for the data section
final Delivery delivery = serverSender.createDelivery(messageReference, AMQP_TUNNELED_CORE_MESSAGE_FORMAT);
final DeliveryAnnotations annotations = messageReference.getProtocolData(DeliveryAnnotations.class);
if (annotations != null && annotations.getValue() != null && annotations.getValue().size() > 0) {
final EncoderImpl encoder = TLSEncode.getEncoder();
try {
encoder.setByteBuffer(new NettyWritable(buffer));
encoder.writeObject(annotations);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
}
// This encoding would work up to a Core message that encodes to but does not exceed
// 2 GB in which case we'd need to send multiple data sections but this would be unlikely
// to succeed and Large message handling should have been in place for such messages.
buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
buffer.writeByte(EncodingCodes.SMALLULONG);
buffer.writeByte(DATA_DESCRIPTOR);
buffer.writeByte(EncodingCodes.VBIN32);
buffer.writeInt(encodedSize); // Core message will encode into this size.
final ActiveMQBuffer bufferWrapper = ActiveMQBuffers.wrappedBuffer(buffer);
message.persist(bufferWrapper);
// Update the buffer that was allocated with the bytes that were written using the wrapper
// since the wrapper doesn't update the wrapper buffer.
buffer.writerIndex(buffer.writerIndex() + encodedSize);
// Don't have pooled content, no need to release or copy.
protonSender.sendNoCopy(new ReadableBuffer.ByteBufferReader(buffer.nioBuffer()));
serverSender.reportDeliveryComplete(this, messageReference, delivery, false);
} catch (Exception deliveryError) {
serverSender.reportDeliveryError(this, messageReference, deliveryError);
}
}
}

View File

@ -0,0 +1,69 @@
/*
* 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;
/**
* Message constants used for handling the "tunneling" of other protocol messages
* in an AMQP delivery sent from one broker to another without conversion.
*
* A tunneled Core message is sent with a custom message format indicating either
* a standard or large core message is carried within. The message is encoded using
* the standard (message format zero) AMQP message structure. The core message is
* encoded in the body section as two or more Data sections. The first being the
* message headers and properties encoding. Any remaining Data sections comprise
* the body of the Core message.
*/
public class AMQPTunneledMessageConstants {
/*
* Prefix value used on all custom message formats that is the ASF IANA number.
*
* https://www.iana.org/assignments/enterprise-numbers/enterprise-numbers
*/
private static final int ARTEMIS_TUNNELED_MESSAGE_FORMAT_PREFIX = 0x468C0000;
/*
* Used to indicate that the format contains a Core message (non-large).
*/
private static final int ARTEMIS_CORE_MESSAGE_TYPE = 0x00000100;
/*
* Used to indicate that the format contains a Core large message.
*/
private static final int ARTEMIS_CORE_LARGE_MESSAGE_TYPE = 0x00000200;
/*
* Indicate version one of the message format
*/
private static final int ARTEMIS_MESSAGE_FORMAT_V1 = 0x00;
/**
* Core message format value used when sending from one broker to another
*/
public static final int AMQP_TUNNELED_CORE_MESSAGE_FORMAT = ARTEMIS_TUNNELED_MESSAGE_FORMAT_PREFIX |
ARTEMIS_CORE_MESSAGE_TYPE |
ARTEMIS_MESSAGE_FORMAT_V1;
/**
* Core large message format value used when sending from one broker to another
*/
public static final int AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT = ARTEMIS_TUNNELED_MESSAGE_FORMAT_PREFIX |
ARTEMIS_CORE_LARGE_MESSAGE_TYPE |
ARTEMIS_MESSAGE_FORMAT_V1;
}

View File

@ -20,9 +20,13 @@ import java.util.AbstractMap;
import java.util.Map;
import java.util.Objects;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.utils.DestinationUtil;
import org.apache.qpid.proton.amqp.DescribedType;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.UnsignedLong;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.messaging.Target;
import org.apache.qpid.proton.engine.Link;
/**
@ -69,6 +73,8 @@ public class AmqpSupport {
public static final Symbol DELAYED_DELIVERY = Symbol.valueOf("DELAYED_DELIVERY");
public static final Symbol QUEUE_PREFIX = Symbol.valueOf("queue-prefix");
public static final Symbol TOPIC_PREFIX = Symbol.valueOf("topic-prefix");
public static final Symbol SHARED = Symbol.valueOf("shared");
public static final Symbol GLOBAL = Symbol.valueOf("global");
public static final Symbol CONNECTION_OPEN_FAILED = Symbol.valueOf("amqp:connection-establishment-failed");
public static final Symbol PRODUCT = Symbol.valueOf("product");
public static final Symbol VERSION = Symbol.valueOf("version");
@ -95,6 +101,20 @@ public class AmqpSupport {
public static final Symbol SOLE_CONNECTION_CAPABILITY = Symbol.valueOf("sole-connection-for-container");
/**
* A capability added to the sender or receiver links that indicate that the link either wants
* support for or offers support for tunneling Core messages as custom formatted AMQP messages.
*/
public static final Symbol CORE_MESSAGE_TUNNELING_SUPPORT = Symbol.getSymbol("AMQ_CORE_MESSAGE_TUNNELING");
/**
* Property value that can be applied to federation configuration that controls if the federation
* receivers will request that the sender peer tunnel core messages inside an AMQP message as a binary
* blob to be unwrapped on the other side. The sending peer would still need to support this feature
* for message tunneling to occur.
*/
public static final String TUNNEL_CORE_MESSAGES = "tunnel-core-messages";
/**
* Search for a given Symbol in a given array of Symbol object.
*
@ -173,7 +193,7 @@ public class AmqpSupport {
* @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());
return verifyCapabilities(link.getRemoteOfferedCapabilities(), link.getDesiredCapabilities());
}
/**
@ -194,15 +214,76 @@ public class AmqpSupport {
* @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();
return verifyCapabilities(link.getRemoteOfferedCapabilities(), capabilities);
}
for (Symbol desired : desiredCapabilites) {
/**
* Verifies that the given 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) {
return verifyCapabilities(link.getRemoteDesiredCapabilities(), desiredCapability);
}
/**
* Verifies that the desired capability is present in the Source capabilities.
*
* @param source
* The Source instance whose capabilities are being searched.
* @param capability
* 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 verifySourceCapability(final Source source, final Symbol capability) {
return verifyCapabilities(source.getCapabilities(), capability);
}
/**
* Verifies that the desired capability is present in the Source capabilities.
*
* @param target
* The Target instance whose capabilities are being searched.
* @param capability
* 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 verifyTargetCapability(final Target target, final Symbol capability) {
return verifyCapabilities(target.getCapabilities(), capability);
}
/**
* Verifies that the given set of capabilities contains each of the desired capabilities.
* <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 offered
* The capabilities that were offered from the remote or were set by the local side
* @param desired
* The desired capabilities to search for in the offered set.
*
* @return <code>true</code> if the desired capabilities were found in the offered set.
*/
public static boolean verifyCapabilities(final Symbol[] offered, final Symbol... desired) {
final Symbol[] desiredCapabilites = desired == null ? EMPTY_CAPABILITIES : desired;
final Symbol[] offeredCapabilites = offered == null ? EMPTY_CAPABILITIES : offered;
for (Symbol desiredCapability : desiredCapabilites) {
boolean foundCurrent = false;
for (Symbol offered : remoteOfferedCapabilites) {
if (desired.equals(offered)) {
for (Symbol offeredCapability : offeredCapabilites) {
if (desiredCapability.equals(offeredCapability)) {
foundCurrent = true;
break;
}
@ -217,31 +298,53 @@ public class AmqpSupport {
}
/**
* 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.
* Given the input values construct a Queue name for use in messaging handlers.
*
* @param link
* The link in question (Sender or Receiver).
* @param desiredCapability
* The non-null capability that is being checked as being desired.
* @param useCoreSubscriptionNaming
* Should the name match core client subscription naming.
* @param clientId
* The client ID of the remote peer.
* @param senderId
* The ID assigned to the sender asking for a generated Queue name.
* @param shared
* Is this Queue used for shared subscriptions
* @param global
* Should the shared subscription Queue indicate globally shared.
* @param isVolatile
* Is the Queue meant to be volatile or not.
*
* @return true if the remote desired all of the capabilities that were given.
* @return a queue name based on the provided inputs.
*/
public static boolean verifyDesiredCapability(final Link link, final Symbol desiredCapability) {
Objects.requireNonNull(desiredCapability, "Desired capability to verifiy cannot be null");
public static SimpleString createQueueName(boolean useCoreSubscriptionNaming,
String clientId,
String senderId,
boolean shared,
boolean global,
boolean isVolatile) {
if (link.getRemoteDesiredCapabilities() == null) {
return false;
}
Objects.requireNonNull(senderId, "The sender Id cannot be null");
for (Symbol capability : link.getRemoteDesiredCapabilities()) {
if (capability.equals(desiredCapability)) {
return true;
if (useCoreSubscriptionNaming) {
final boolean durable = !isVolatile;
final String subscriptionName = senderId.contains("|") ? senderId.split("\\|")[0] : senderId;
final String clientID = clientId == null || clientId.isEmpty() || global ? null : clientId;
return DestinationUtil.createQueueNameForSubscription(durable, clientID, subscriptionName);
} else {
String queue = clientId == null || clientId.isEmpty() || global ? senderId : clientId + "." + senderId;
if (shared) {
if (queue.contains("|")) {
queue = queue.split("\\|")[0];
}
if (isVolatile) {
queue += ":shared-volatile";
}
if (global) {
queue += ":global";
}
}
}
return false;
return SimpleString.toSimpleString(queue);
}
}
}

View File

@ -0,0 +1,501 @@
/*
* 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.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.COPY;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.GLOBAL;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.QUEUE_CAPABILITY;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.SHARED;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.TOPIC_CAPABILITY;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.createQueueName;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifyDesiredCapability;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.verifySourceCapability;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.api.core.ActiveMQIllegalStateException;
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException;
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.MessageReference;
import org.apache.activemq.artemis.core.server.QueueQueryResult;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPLargeMessage;
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.exceptions.ActiveMQAMQPNotFoundException;
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolLogger;
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle;
import org.apache.activemq.artemis.protocol.amqp.proton.handler.ProtonHandler;
import org.apache.activemq.artemis.reader.MessageUtil;
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.messaging.TerminusDurability;
import org.apache.qpid.proton.amqp.messaging.TerminusExpiryPolicy;
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.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The default {@link SenderController} instance used by and sender context that is not
* assigned a custom controller. This controller is extensible so that specialized sender
* controllers can be created from it.
* <p>
* The default controller works best with incoming AMQP clients and JMS over AMQP clients.
* For intra-broker connections it is likely that a custom sender controller would be a more
* flexible option that using the default controller.
*/
public class DefaultSenderController implements SenderController {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final AMQPConnectionContext connection;
private final AMQPSessionCallback sessionSPI;
private final Sender protonSender;
private final String clientId;
// A cached AMQP standard message writers married to the server sender instance on initialization
private AMQPMessageWriter standardMessageWriter;
private AMQPLargeMessageWriter largeMessageWriter;
private boolean shared;
private boolean global;
private boolean multicast;
private SimpleString queue;
private SimpleString tempQueueName;
private String selector;
private RoutingType routingTypeToUse = RoutingType.ANYCAST;
private boolean isVolatile;
public DefaultSenderController(AMQPSessionContext session, Sender protonSender, String clientId) {
this.connection = session.getAMQPConnectionContext();
this.sessionSPI = session.getSessionSPI();
this.protonSender = protonSender;
this.clientId = clientId;
}
@SuppressWarnings("unchecked")
@Override
public Consumer init(ProtonServerSenderContext senderContext) throws Exception {
validateConnectionState();
this.standardMessageWriter = new AMQPMessageWriter(senderContext);
this.largeMessageWriter = new AMQPLargeMessageWriter(senderContext);
Source source = (Source) protonSender.getRemoteSource();
final Map<Symbol, Object> supportedFilters = new HashMap<>();
// Match the settlement mode of the remote instead of relying on the default of MIXED.
protonSender.setSenderSettleMode(protonSender.getRemoteSenderSettleMode());
// We don't currently support SECOND so enforce that the answer is always FIRST
protonSender.setReceiverSettleMode(ReceiverSettleMode.FIRST);
if (source != null) {
// We look for message selectors on every receiver, while in other cases we might only
// consume the filter depending on the subscription type.
Map.Entry<Symbol, DescribedType> filter = AmqpSupport.findFilter(source.getFilter(), AmqpSupport.JMS_SELECTOR_FILTER_IDS);
if (filter != null) {
selector = filter.getValue().getDescribed().toString();
// Validate the Selector.
try {
SelectorParser.parse(selector);
} catch (FilterException e) {
throw new ActiveMQAMQPException(AmqpError.INVALID_FIELD, "Invalid filter", ActiveMQExceptionType.INVALID_FILTER_EXPRESSION);
}
supportedFilters.put(filter.getKey(), filter.getValue());
}
}
if (source == null) {
// Attempt to recover a previous subscription happens when a link reattach happens on a
// subscription queue
String pubId = protonSender.getName();
global = verifyDesiredCapability(protonSender, GLOBAL);
shared = verifyDesiredCapability(protonSender, SHARED);
queue = createQueueName(connection.isUseCoreSubscriptionNaming(), clientId, pubId, true, global, false);
QueueQueryResult result = sessionSPI.queueQuery(queue, RoutingType.MULTICAST, false);
multicast = true;
routingTypeToUse = RoutingType.MULTICAST;
// Once confirmed that the address exists we need to return a Source that reflects
// the lifetime policy and capabilities of the new subscription.
if (result.isExists()) {
source = new org.apache.qpid.proton.amqp.messaging.Source();
source.setAddress(queue.toString());
source.setDurable(TerminusDurability.UNSETTLED_STATE);
source.setExpiryPolicy(TerminusExpiryPolicy.NEVER);
source.setDistributionMode(COPY);
source.setCapabilities(TOPIC_CAPABILITY);
SimpleString filterString = result.getFilterString();
if (filterString != null) {
selector = filterString.toString();
boolean noLocal = false;
String remoteContainerId = protonSender.getSession().getConnection().getRemoteContainer();
String noLocalFilter = MessageUtil.CONNECTION_ID_PROPERTY_NAME.toString() + "<>'" + remoteContainerId + "'";
if (selector.endsWith(noLocalFilter)) {
if (selector.length() > noLocalFilter.length()) {
noLocalFilter = " AND " + noLocalFilter;
selector = selector.substring(0, selector.length() - noLocalFilter.length());
} else {
selector = null;
}
noLocal = true;
}
if (noLocal) {
supportedFilters.put(AmqpSupport.NO_LOCAL_NAME, AmqpNoLocalFilter.NO_LOCAL);
}
if (selector != null && !selector.trim().isEmpty()) {
supportedFilters.put(AmqpSupport.JMS_SELECTOR_NAME, new AmqpJmsSelectorFilter(selector));
}
}
protonSender.setSource(source);
} else {
throw new ActiveMQAMQPNotFoundException("Unknown subscription link: " + protonSender.getName());
}
} else if (source.getDynamic()) {
// if dynamic we have to create the node (queue) and set the address on the target, the
// node is temporary and will be deleted on closing of the session
queue = SimpleString.toSimpleString(java.util.UUID.randomUUID().toString());
tempQueueName = queue;
try {
sessionSPI.createTemporaryQueue(queue, RoutingType.ANYCAST);
// protonSession.getServerSession().createQueue(queue, queue, null, true, false);
} catch (Exception e) {
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.errorCreatingTemporaryQueue(e.getMessage());
}
source.setAddress(queue.toString());
} else {
SimpleString addressToUse;
SimpleString queueNameToUse = null;
shared = verifySourceCapability(source, SHARED);
global = verifySourceCapability(source, GLOBAL);
final boolean isFQQN;
//find out if we have an address made up of the address and queue name, if yes then set queue name
if (CompositeAddress.isFullyQualified(source.getAddress())) {
isFQQN = true;
addressToUse = SimpleString.toSimpleString(CompositeAddress.extractAddressName(source.getAddress()));
queueNameToUse = SimpleString.toSimpleString(CompositeAddress.extractQueueName(source.getAddress()));
} else {
isFQQN = false;
addressToUse = SimpleString.toSimpleString(source.getAddress());
}
//check to see if the client has defined how we act
boolean clientDefined = verifySourceCapability(source, TOPIC_CAPABILITY) || verifySourceCapability(source, QUEUE_CAPABILITY);
if (clientDefined) {
multicast = verifySourceCapability(source, TOPIC_CAPABILITY);
AddressQueryResult addressQueryResult = null;
try {
addressQueryResult = sessionSPI.addressQuery(addressToUse, multicast ? RoutingType.MULTICAST : RoutingType.ANYCAST, 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();
}
Set<RoutingType> routingTypes = addressQueryResult.getRoutingTypes();
//if the client defines 1 routing type and the broker another then throw an exception
if (multicast && !routingTypes.contains(RoutingType.MULTICAST)) {
throw new ActiveMQAMQPIllegalStateException("Address " + addressToUse + " is not configured for topic support");
} else if (!multicast && !routingTypes.contains(RoutingType.ANYCAST)) {
//if client specifies fully qualified name that's allowed, don't throw exception.
if (queueNameToUse == null) {
throw new ActiveMQAMQPIllegalStateException("Address " + addressToUse + " is not configured for queue support");
}
}
} else {
// if not we look up the address
AddressQueryResult addressQueryResult = null;
// Set this to the broker configured default for the address prior to the lookup so that
// an auto create will actually use the configured defaults. The actual query result will
// contain the true answer on what routing type the address actually has though.
final RoutingType routingType = sessionSPI.getDefaultRoutingType(addressToUse);
routingTypeToUse = routingType == null ? ActiveMQDefaultConfiguration.getDefaultRoutingType() : routingType;
try {
addressQueryResult = sessionSPI.addressQuery(addressToUse, routingTypeToUse, 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();
}
Set<RoutingType> routingTypes = addressQueryResult.getRoutingTypes();
if (routingTypes.contains(RoutingType.MULTICAST) && routingTypes.size() == 1) {
multicast = true;
} else {
//todo add some checks if both routing types are supported
multicast = false;
}
}
routingTypeToUse = multicast ? RoutingType.MULTICAST : RoutingType.ANYCAST;
// if not dynamic then we use the target's address as the address to forward the
// messages to, however there has to be a queue bound to it so we need to check this.
if (multicast) {
Map.Entry<Symbol, DescribedType> filter = AmqpSupport.findFilter(source.getFilter(), AmqpSupport.NO_LOCAL_FILTER_IDS);
if (filter != null) {
String remoteContainerId = protonSender.getSession().getConnection().getRemoteContainer();
String noLocalFilter = MessageUtil.CONNECTION_ID_PROPERTY_NAME.toString() + "<>'" + remoteContainerId + "'";
if (selector != null) {
selector += " AND " + noLocalFilter;
} else {
selector = noLocalFilter;
}
supportedFilters.put(filter.getKey(), filter.getValue());
}
SimpleString simpleStringSelector = SimpleString.toSimpleString(selector);
queue = getMatchingQueue(queueNameToUse, addressToUse, RoutingType.MULTICAST, simpleStringSelector, isFQQN);
//if the address specifies a broker configured queue then we always use this, treat it as a queue
if (queue != null) {
multicast = false;
} else if (TerminusDurability.UNSETTLED_STATE.equals(source.getDurable()) || TerminusDurability.CONFIGURATION.equals(source.getDurable())) {
// if we are a subscription and durable create a durable queue using the container
// id and link name
String pubId = protonSender.getName();
queue = createQueueName(connection.isUseCoreSubscriptionNaming(), clientId, pubId, shared, global, false);
QueueQueryResult result = sessionSPI.queueQuery(queue, routingTypeToUse, false);
if (result.isExists()) {
/*
* If a client attaches to an existing durable subscription with a different filter or address then
* we must recreate the queue (JMS semantics). However, if the corresponding queue is managed via the
* configuration then we don't want to change it. We must account for optional address prefixes that
* are not carried over into the actual created address by stripping any prefix value that matches
* those configured on the acceptor.
*/
if (!result.isConfigurationManaged() &&
(!Objects.equals(result.getAddress(), sessionSPI.removePrefix(addressToUse)) ||
!Objects.equals(result.getFilterString(), simpleStringSelector))) {
if (result.getConsumerCount() == 0) {
sessionSPI.deleteQueue(queue);
if (shared) {
sessionSPI.createSharedDurableQueue(addressToUse, RoutingType.MULTICAST, queue, simpleStringSelector);
} else {
sessionSPI.createUnsharedDurableQueue(addressToUse, RoutingType.MULTICAST, queue, simpleStringSelector);
}
} else {
throw new ActiveMQAMQPIllegalStateException("Unable to recreate subscription, consumers already exist");
}
}
} else {
if (shared) {
sessionSPI.createSharedDurableQueue(addressToUse, RoutingType.MULTICAST, queue, simpleStringSelector);
} else {
sessionSPI.createUnsharedDurableQueue(addressToUse, RoutingType.MULTICAST, queue, simpleStringSelector);
}
}
} else {
// otherwise we are a volatile subscription
isVolatile = true;
if (shared && protonSender.getName() != null) {
queue = createQueueName(connection.isUseCoreSubscriptionNaming(), clientId, protonSender.getName(), shared, global, isVolatile);
QueueQueryResult result = sessionSPI.queueQuery(queue, routingTypeToUse, false);
if ((!result.isExists() || !Objects.equals(result.getAddress(), addressToUse) || !Objects.equals(result.getFilterString(), simpleStringSelector)) && !result.isConfigurationManaged()) {
sessionSPI.createSharedVolatileQueue(addressToUse, RoutingType.MULTICAST, queue, simpleStringSelector);
}
} else {
queue = SimpleString.toSimpleString(java.util.UUID.randomUUID().toString());
tempQueueName = queue;
try {
sessionSPI.createTemporaryQueue(addressToUse, queue, RoutingType.MULTICAST, simpleStringSelector);
} catch (Exception e) {
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.errorCreatingTemporaryQueue(e.getMessage());
}
}
}
} else {
if (queueNameToUse != null) {
SimpleString matchingAnycastQueue;
QueueQueryResult result = sessionSPI.queueQuery(CompositeAddress.toFullyQualified(addressToUse, queueNameToUse), null, false, null);
if (result.isExists()) {
// if the queue exists and we're using FQQN then just ignore the routing-type
routingTypeToUse = null;
}
matchingAnycastQueue = getMatchingQueue(queueNameToUse, addressToUse, routingTypeToUse, null, false);
if (matchingAnycastQueue != null) {
queue = matchingAnycastQueue;
} else {
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.sourceAddressDoesntExist();
}
} else {
SimpleString matchingAnycastQueue = sessionSPI.getMatchingQueue(addressToUse, RoutingType.ANYCAST);
if (matchingAnycastQueue != null) {
queue = matchingAnycastQueue;
} else {
queue = addressToUse;
}
}
}
if (queue == null) {
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.sourceAddressNotSet();
}
try {
if (!sessionSPI.queueQuery(queue, routingTypeToUse, !multicast).isExists()) {
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.sourceAddressDoesntExist();
}
} catch (ActiveMQAMQPNotFoundException e) {
throw e;
} catch (Exception e) {
throw new ActiveMQAMQPInternalErrorException(e.getMessage(), e);
}
}
// We need to update the source with any filters we support otherwise the client
// is free to consider the attach as having failed if we don't send back what we
// do support or if we send something we don't support the client won't know we
// have not honored what it asked for.
source.setFilter(supportedFilters.isEmpty() ? null : supportedFilters);
boolean browseOnly = !multicast && source.getDistributionMode() != null && source.getDistributionMode().equals(COPY);
return (Consumer) sessionSPI.createSender(senderContext, queue, multicast ? null : selector, browseOnly);
}
@Override
public void close() throws Exception {
Source source = (Source) protonSender.getSource();
if (source != null && source.getAddress() != null && multicast) {
SimpleString queueName = SimpleString.toSimpleString(source.getAddress());
QueueQueryResult result = sessionSPI.queueQuery(queueName, routingTypeToUse, false);
if (result.isExists() && source.getDynamic()) {
sessionSPI.deleteQueue(queueName);
} else {
if (source.getDurable() == TerminusDurability.NONE && tempQueueName != null && (source.getExpiryPolicy() == TerminusExpiryPolicy.LINK_DETACH || source.getExpiryPolicy() == TerminusExpiryPolicy.SESSION_END)) {
sessionSPI.removeTemporaryQueue(tempQueueName);
} else {
String pubId = protonSender.getName();
if (pubId.contains("|")) {
pubId = pubId.split("\\|")[0];
}
SimpleString queue = createQueueName(connection.isUseCoreSubscriptionNaming(), clientId, pubId, shared, global, isVolatile);
result = sessionSPI.queueQuery(queue, multicast ? RoutingType.MULTICAST : RoutingType.ANYCAST, false);
//only delete if it isn't volatile and has no consumers
if (result.isExists() && !isVolatile && result.getConsumerCount() == 0) {
sessionSPI.deleteQueue(queue);
}
}
}
} else if (source != null && source.getDynamic() && (source.getExpiryPolicy() == TerminusExpiryPolicy.LINK_DETACH || source.getExpiryPolicy() == TerminusExpiryPolicy.SESSION_END)) {
try {
sessionSPI.removeTemporaryQueue(SimpleString.toSimpleString(source.getAddress()));
} catch (Exception e) {
// Ignore on close, its temporary anyway and will be removed later
}
}
}
@Override
public MessageWriter selectOutgoingMessageWriter(ProtonServerSenderContext sender, MessageReference reference) {
final MessageWriter selected;
if (reference.getMessage() instanceof AMQPLargeMessage) {
selected = largeMessageWriter;
} else {
selected = standardMessageWriter;
}
return selected;
}
protected SimpleString getMatchingQueue(SimpleString queueName, SimpleString address, RoutingType routingType, SimpleString filter, boolean matchFilter) throws Exception {
if (queueName != null) {
QueueQueryResult result = sessionSPI.queueQuery(CompositeAddress.toFullyQualified(address, queueName), routingType, true, filter);
if (!result.isExists()) {
throw new ActiveMQAMQPNotFoundException("Queue: '" + queueName + "' does not exist");
} else {
if (!result.getAddress().equals(address)) {
throw new ActiveMQAMQPNotFoundException("Queue: '" + queueName + "' does not exist for address '" + address + "'");
}
if (matchFilter && filter != null && result.getFilterString() != null && !filter.equals(result.getFilterString())) {
throw new ActiveMQIllegalStateException("Queue: " + queueName + " filter mismatch [" + filter + "] is different than existing filter [" + result.getFilterString() + "]");
}
return sessionSPI.getMatchingQueue(address, queueName, routingType);
}
}
return null;
}
private void validateConnectionState() throws ActiveMQException {
final ProtonHandler handler = connection == null ? null : connection.getHandler();
final Connection qpidConnection = handler == null ? null : handler.getConnection();
if (qpidConnection == null) {
if (logger.isDebugEnabled()) {
logger.debug("validateConnectionState:: connection={}, handler={}, qpidConnection={}", connection, handler, qpidConnection);
}
ActiveMQAMQPProtocolLogger.LOGGER.invalidAMQPConnectionState("null", "null");
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.invalidAMQPConnectionState("null");
}
if (qpidConnection.getRemoteState() == EndpointState.CLOSED) {
ActiveMQAMQPProtocolLogger.LOGGER.invalidAMQPConnectionState(qpidConnection.getRemoteState(), connection.getRemoteAddress());
throw ActiveMQAMQPProtocolMessageBundle.BUNDLE.invalidAMQPConnectionState(qpidConnection.getRemoteState());
}
}
}

View File

@ -0,0 +1,66 @@
/*
* 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 org.apache.activemq.artemis.api.core.Message;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.engine.Delivery;
/**
* Message reader for incoming messages from and AMQP receiver context which will
* handle the read and decode of message payload into am AMQP message.
*/
public interface MessageReader {
/**
* Closes the reader and releases any in use resources. If the reader was not
* finished processing an incoming message when closed the reader should release
* any resources that might be held such as large message files etc.
*/
void close();
/**
* Reset any internal state of this reader and prepares it to begin processing a
* new delivery. A previously closed reader can be reset for reuse.
*
* @return this {@link MessageReader} instance.
*/
MessageReader open();
/**
* Reads the bytes from an incoming delivery which might not be complete yet
* but allows the reader to consume pending bytes to prevent stalling the sender
* because the session window was exhausted. Once a delivery has been fully read
* and is no longer partial the readBytes method will return the decoded message
* for dispatch.
*
* @param delivery
* The delivery that has pending incoming bytes.
*/
Message readBytes(Delivery delivery) throws Exception;
/**
* Once a message has been read but before the reader is closed this API offers
* access to any delivery annotations that were present upon decode of the read
* message.
*
* @return any DeliveryAnnotations that were read as part of decoding the message.
*/
DeliveryAnnotations getDeliveryAnnotations();
}

View File

@ -0,0 +1,97 @@
/*
* 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 java.util.function.Consumer;
import org.apache.activemq.artemis.core.server.MessageReference;
/**
* Message writer for outgoing message from and AMQP sender context which will
* handle the encode and write of message payload into am AMQP sender.
*/
public interface MessageWriter extends Consumer<MessageReference> {
/**
* Entry point for asynchronous delivery mechanics which is equivalent to calling
* the {@link #writeBytes(MessageReference)} method.
*
* @param messageReference
* The original message reference that triggered the delivery.
*
* @see #writeBytes(MessageReference)
*/
@Override
default void accept(MessageReference messageReference) {
writeBytes(messageReference);
}
/**
* This should return <code>true</code> when a delivery is still in progress as a
* hint to the sender that new messages can't be accepted yet. The handler can be
* paused during delivery of large payload data due to IO or session back pressure.
* The context is responsible for scheduling itself for resumption when it finds
* that it must halt delivery work.
* <p>
* This could be called from outside the connection thread so the state should be
* thread safe however the sender should take care to restart deliveries in a safe
* way taking into account that this value might not get seen by other threads in
* its non-busy state when the delivery completes.
*
* @return <code>true</code> if the handler is still working on delivering a message.
*/
default boolean isWriting() {
return false;
}
/**
* Begin delivery of a message providing the original message reference instance. The writer
* should be linked to a parent sender or sender controller which it will use for obtaining
* services needed to send and complete sending operations. This must be called from the
* connection thread.
* <p>
* Once delivery processing completes (successful or not) the handler must inform the
* server sender of the outcome so that further deliveries can be sent or error processing
* can commence.
*
* @param messageReference
* The original message reference that triggered the delivery.
*/
void writeBytes(MessageReference messageReference);
/**
* Mark the writer as done and release any resources that it might be holding, this call
* should trigger the busy method to return false for any handler that has a busy state.
* It is expected that the sender will close each handler after it reports that writing
* the message has completed. This must be called from the connection thread.
*/
default void close() {
// By default stateless writers have no reaction to closed events.
}
/**
* Opens the handler and ensures the handler state is in its initial values to prepare for
* a new message write. This is only applicable to handlers that have state data but should
* be called on every handler by the sender context as it doesn't know which instances need
* opened.
*/
default MessageWriter open() {
// Default for stateless handlers is to do nothing here.
return this;
}
}

View File

@ -16,19 +16,17 @@
*/
package org.apache.activemq.artemis.protocol.amqp.proton;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.persistence.impl.nullpm.NullStorageManager;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.RoutingContext;
import org.apache.activemq.artemis.core.server.impl.RoutingContextImpl;
import org.apache.activemq.artemis.core.transaction.Transaction;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPLargeMessage;
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.exceptions.ActiveMQAMQPException;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.transaction.TransactionalState;
import org.apache.qpid.proton.amqp.transport.ErrorCondition;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
@ -46,7 +44,13 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
protected final AMQPSessionCallback sessionSPI;
protected volatile AMQPLargeMessage currentLargeMessage;
// Cached instances used for this receiver which will be swapped as message of varying types
// are sent to this receiver from the remote peer.
protected final MessageReader standardMessageReader = new AMQPMessageReader(this);
protected final MessageReader largeMessageReader = new AMQPLargeMessageReader(this);
protected volatile MessageReader messageReader;
protected final Runnable creditRunnable;
@ -76,20 +80,19 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
this.routingContext = new RoutingContextImpl(null).setDuplicateDetection(connection.getProtocolManager().isAmqpDuplicateDetection());
}
public AMQPSessionContext getSessionContext() {
return protonSession;
}
protected void recoverContext() {
sessionSPI.recoverContext();
}
protected void clearLargeMessage() {
protected void closeCurrentReader() {
connection.runNow(() -> {
if (currentLargeMessage != null) {
try {
currentLargeMessage.deleteFile();
} catch (Throwable error) {
ActiveMQServerLogger.LOGGER.errorDeletingLargeMessageFile(error);
} finally {
currentLargeMessage = null;
}
if (messageReader != null) {
messageReader.close();
messageReader = null;
}
});
}
@ -238,100 +241,114 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
flow();
}
private void handleAbortedDelivery(Delivery delivery) {
Receiver receiver = ((Receiver) delivery.getLink());
closeCurrentReader();
// Aborting implicitly remotely settles, so advance
// receiver to the next delivery and settle locally.
receiver.advance();
delivery.settle();
// Replenish the credit if not doing a drain
if (!receiver.getDrain()) {
receiver.flow(1);
}
}
private MessageReader getOrSelectMessageReader(Receiver receiver, Delivery delivery) {
// The reader will be nulled once a message has been read, otherwise a large message
// is being read in chunks from the remote.
if (messageReader != null) {
return messageReader;
} else {
final MessageReader selected = trySelectMessageReader(receiver, delivery);
if (selected != null) {
return messageReader = selected.open();
} else {
return null;
}
}
}
protected MessageReader trySelectMessageReader(Receiver receiver, Delivery delivery) {
if (sessionSPI.getStorageManager() instanceof NullStorageManager) {
// if we are dealing with the NullStorageManager we should just make it a regular message anyways
return standardMessageReader;
} else if (delivery.isPartial()) {
if (minLargeMessageSize > 0 && delivery.available() >= minLargeMessageSize) {
return largeMessageReader;
} else {
return null; // Not enough context to decide yet.
}
} else if (minLargeMessageSize > 0 && delivery.available() >= minLargeMessageSize) {
// this is treating the case where the frameSize > minLargeMessage and the message is still large enough
return largeMessageReader;
} else {
// Either minLargeMessageSize < 0 which means disable or the entire message has
// arrived and is under the threshold so use the standard variant.
return standardMessageReader;
}
}
/*
* called when Proton receives a message to be delivered via a Delivery.
*
* This may be called more than once per deliver so we have to cache the buffer until we have received it all.
*/
@Override
public void onMessage(Delivery delivery) throws ActiveMQAMQPException {
public final void onMessage(Delivery delivery) throws ActiveMQAMQPException {
connection.requireInHandler();
Receiver receiver = ((Receiver) delivery.getLink());
final Receiver receiver = ((Receiver) delivery.getLink());
if (receiver.current() != delivery) {
return;
}
if (delivery.isAborted()) {
handleAbortedDelivery(delivery);
return;
}
try {
if (delivery.isAborted()) {
clearLargeMessage();
// Aborting implicitly remotely settles, so advance
// receiver to the next delivery and settle locally.
receiver.advance();
delivery.settle();
// Replenish the credit if not doing a drain
if (!receiver.getDrain()) {
receiver.flow(1);
}
return;
} else if (delivery.isPartial()) {
if (sessionSPI.getStorageManager() instanceof NullStorageManager) {
// if we are dealing with the NullStorageManager we should just make it a regular message anyways
return;
}
if (currentLargeMessage == null) {
// minLargeMessageSize < 0 means no large message treatment, make it disabled
if (minLargeMessageSize > 0 && delivery.available() >= minLargeMessageSize) {
initializeCurrentLargeMessage(delivery, receiver);
}
} else {
currentLargeMessage.addBytes(receiver.recv());
}
final MessageReader messageReader = getOrSelectMessageReader(receiver, delivery);
if (messageReader == null) {
return;
}
AMQPMessage message;
final Message message = messageReader.readBytes(delivery);
// this is treating the case where the frameSize > minLargeMessage and the message is still large enough
if (!(sessionSPI.getStorageManager() instanceof NullStorageManager) && currentLargeMessage == null && minLargeMessageSize > 0 && delivery.available() >= minLargeMessageSize) {
initializeCurrentLargeMessage(delivery, receiver);
}
if (message != null) {
// Fetch this before the close of the reader as that will clear any read message
// delivery annotations.
final DeliveryAnnotations deliveryAnnotations = messageReader.getDeliveryAnnotations();
this.messageReader.close();
this.messageReader = null;
if (currentLargeMessage != null) {
currentLargeMessage.addBytes(receiver.recv());
receiver.advance();
message = currentLargeMessage;
currentLargeMessage.releaseResources(true, true);
currentLargeMessage = null;
} else {
ReadableBuffer data = receiver.recv();
receiver.advance();
message = sessionSPI.createStandardMessage(delivery, data);
}
Transaction tx = null;
if (delivery.getRemoteState() instanceof TransactionalState) {
TransactionalState txState = (TransactionalState) delivery.getRemoteState();
tx = this.sessionSPI.getTransaction(txState.getTxnId(), false);
}
Transaction tx = null;
if (delivery.getRemoteState() instanceof TransactionalState) {
TransactionalState txState = (TransactionalState) delivery.getRemoteState();
tx = this.sessionSPI.getTransaction(txState.getTxnId(), false);
}
actualDelivery(message, delivery, receiver, tx);
actualDelivery(message, delivery, deliveryAnnotations, receiver, tx);
}
} catch (Exception e) {
throw new ActiveMQAMQPInternalErrorException(e.getMessage(), e);
}
}
protected void initializeCurrentLargeMessage(Delivery delivery, Receiver receiver) throws Exception {
long id = sessionSPI.getStorageManager().generateID();
currentLargeMessage = new AMQPLargeMessage(id, delivery.getMessageFormat(), null, sessionSPI.getCoreMessageObjectPools(), sessionSPI.getStorageManager());
ReadableBuffer dataBuffer = receiver.recv();
currentLargeMessage.parseHeader(dataBuffer);
sessionSPI.getStorageManager().largeMessageCreated(id, currentLargeMessage);
currentLargeMessage.addBytes(dataBuffer);
}
@Override
public void close(boolean remoteLinkClose) throws ActiveMQAMQPException {
protonSession.removeReceiver(receiver);
clearLargeMessage();
closeCurrentReader();
}
@Override
@ -344,7 +361,7 @@ public abstract class ProtonAbstractReceiver extends ProtonInitializable impleme
return connection;
}
protected abstract void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx);
protected abstract void actualDelivery(Message message, Delivery delivery, DeliveryAnnotations deliveryAnnotations, Receiver receiver, Transaction tx);
// TODO: how to implement flow here?
public abstract void flow();

View File

@ -23,13 +23,13 @@ import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQExceptionType;
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException;
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.security.CheckType;
import org.apache.activemq.artemis.core.server.impl.AddressInfo;
import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy;
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.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPInternalErrorException;
@ -38,6 +38,7 @@ import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPSecurity
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolLogger;
import org.apache.activemq.artemis.protocol.amqp.logger.ActiveMQAMQPProtocolMessageBundle;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Modified;
import org.apache.qpid.proton.amqp.messaging.Outcome;
import org.apache.qpid.proton.amqp.messaging.Rejected;
@ -193,7 +194,7 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver {
}
@Override
protected void actualDelivery(AMQPMessage message, Delivery delivery, Receiver receiver, Transaction tx) {
protected void actualDelivery(Message message, Delivery delivery, DeliveryAnnotations deliveryAnnotations, Receiver receiver, Transaction tx) {
try {
if (sessionSPI != null) {
// message could be null on unit tests (Mocking from ProtonServerReceiverContextTest).
@ -209,7 +210,7 @@ public class ProtonServerReceiverContext extends ProtonAbstractReceiver {
}
}
private void validateAddressOnAnonymousLink(AMQPMessage message) throws Exception {
private void validateAddressOnAnonymousLink(Message message) throws Exception {
SimpleString newAddress = message.getAddressSimpleString();
if (newAddress != null && !newAddress.equals(lastAddress)) {
AddressFullMessagePolicy currentPolicy = sessionSPI.getProtocolManager().getServer().getPagingManager().getPageStore(newAddress).getAddressFullMessagePolicy();

View File

@ -16,9 +16,71 @@
*/
package org.apache.activemq.artemis.protocol.amqp.proton;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.server.Consumer;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPLargeMessage;
public interface SenderController {
class RejectingOutgoingMessageWriter implements MessageWriter {
@Override
public void writeBytes(MessageReference reference) {
throw new UnsupportedOperationException("Message was sent to rejecting writer in error");
}
}
/**
* Used as an initial state for message writers in controllers to ensure that a
* valid version is chosen when a message is dispatched.
*/
MessageWriter REJECTING_MESSAGE_WRITER = new RejectingOutgoingMessageWriter();
/**
* Initialize sender controller state and handle open of AMQP sender resources
*
* @param senderContext
* The sender context that is requesting controller initialization.
*
* @return a server consumer that has been initialize by the controller.
*
* @throws Exception if an error occurs during initialization.
*/
Consumer init(ProtonServerSenderContext senderContext) throws Exception;
/**
* Handle close of the sever sender AMQP resources.
*
* @throws Exception if an error occurs during close.
*/
void close() throws Exception;
/**
* Controller selects a outgoing delivery writer that will handle the encoding and writing
* of the target {@link Message} carried in the given {@link MessageReference}. The selection
* process should take into account how the message pre-processing will mutate the outgoing
* message.
*
* The default implementation performs no caching of writers and should be overridden in
* subclasses to reduce GC churn, the default version is suitable for tests.
*
* @param sender
* The server sender that will make use of the returned delivery context.
* @param reference
* The message that must be sent using an outgoing context
*
* @return an {@link MessageWriter} to use when sending the message in the reference.
*/
default MessageWriter selectOutgoingMessageWriter(ProtonServerSenderContext sender, MessageReference reference) {
final MessageWriter selected;
if (reference.getMessage() instanceof AMQPLargeMessage) {
selected = new AMQPLargeMessageWriter(sender);
} else {
selected = new AMQPMessageWriter(sender);
}
return selected;
}
}

View File

@ -0,0 +1,542 @@
/*
* 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.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Random;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.core.persistence.impl.nullpm.NullStorageManager;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.util.NettyReadable;
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.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Header;
import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
import org.apache.qpid.proton.amqp.messaging.Properties;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Test the handling of AMQP received frames and decoding them into Core large messages.
*/
public class AMQPTunneledCoreLargeMessageReaderTest {
private static final byte DATA_DESCRIPTOR = 0x75;
private static final int DATA_SECTION_ENCODING_BYTES = Long.BYTES;
@Mock
ProtonAbstractReceiver serverReceiver;
@Mock
AMQPSessionContext sessionContext;
@Mock
AMQPSessionCallback sessionSPI;
@Spy
NullStorageManager nullStoreManager = new NullStorageManager();
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
when(serverReceiver.getSessionContext()).thenReturn(sessionContext);
when(sessionContext.getSessionSPI()).thenReturn(sessionSPI);
when(sessionSPI.getStorageManager()).thenReturn(nullStoreManager);
}
@Test
public void testReaderThrowsIllegalStateIfNotOpenedWhenReadCalled() throws Exception {
AMQPTunneledCoreLargeMessageReader reader = new AMQPTunneledCoreLargeMessageReader(serverReceiver);
Delivery delivery = Mockito.mock(Delivery.class);
try {
reader.readBytes(delivery);
fail("Should throw as the reader was not opened.");
} catch (IllegalStateException e) {
// Expected
}
}
@Test
public void testReadDeliveryAnnotationsFromDeliveryBuffer() throws Exception {
AMQPTunneledCoreLargeMessageReader reader = new AMQPTunneledCoreLargeMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
when(receiver.recv()).thenReturn(createAnnotationsBuffer());
when(delivery.isPartial()).thenReturn(true);
reader.open();
assertNull(reader.getDeliveryAnnotations());
try {
reader.readBytes(delivery);
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just delivery annotations.");
}
assertNotNull(reader.getDeliveryAnnotations());
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
reader.close();
try {
reader.readBytes(delivery);
fail("Should throw as the reader was closed.");
} catch (IllegalStateException e) {
// Expected
}
}
@Test
public void testReadMessageByteByByteFromDeliveryBuffer() throws Exception {
AMQPTunneledCoreLargeMessageReader reader = new AMQPTunneledCoreLargeMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
when(delivery.isPartial()).thenReturn(true);
reader.open();
final ReadableBuffer deliveryAnnotations = createAnnotationsBuffer();
for (int i = 1; i <= deliveryAnnotations.remaining(); ++i) {
when(receiver.recv()).thenReturn(deliveryAnnotations.duplicate().position(i - 1).limit(i).slice());
try {
assertNull(reader.readBytes(delivery));
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just delivery annotations.");
}
}
assertNotNull(reader.getDeliveryAnnotations());
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageHeader = createCoreMessageEncoding(message);
for (int i = 1; i <= coreMessageHeader.remaining(); ++i) {
when(receiver.recv()).thenReturn(coreMessageHeader.duplicate().position(i - 1).limit(i).slice());
try {
assertNull(reader.readBytes(delivery));
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
}
final ReadableBuffer coreLargeMessageBody = createRandomLargeMessagePaylod();
Message readMessage = null;
for (int i = 1; i <= coreLargeMessageBody.remaining(); ++i) {
when(receiver.recv()).thenReturn(coreLargeMessageBody.duplicate().position(i - 1).limit(i).slice());
try {
if (i == coreLargeMessageBody.remaining()) {
when(delivery.isPartial()).thenReturn(false);
}
readMessage = reader.readBytes(delivery);
if (i < coreLargeMessageBody.remaining()) {
assertNull(readMessage);
} else {
assertNotNull(readMessage);
}
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
}
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertTrue(readMessage.isLargeMessage());
}
@Test
public void testReadMessageInByteChunksFromDeliveryBuffer() throws Exception {
AMQPTunneledCoreLargeMessageReader reader = new AMQPTunneledCoreLargeMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
when(delivery.isPartial()).thenReturn(true);
reader.open();
final ReadableBuffer deliveryAnnotations = createAnnotationsBuffer();
when(receiver.recv()).thenReturn(deliveryAnnotations.duplicate());
try {
assertNull(reader.readBytes(delivery));
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just delivery annotations.");
}
assertNotNull(reader.getDeliveryAnnotations());
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageHeader = createCoreMessageEncoding(message);
when(receiver.recv()).thenReturn(coreMessageHeader.duplicate());
try {
assertNull(reader.readBytes(delivery));
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
final ReadableBuffer coreLargeMessageBody = createRandomLargeMessagePaylod();
Message readMessage = null;
when(receiver.recv()).thenReturn(coreLargeMessageBody.duplicate());
when(delivery.isPartial()).thenReturn(false);
try {
readMessage = reader.readBytes(delivery);
assertNotNull(readMessage);
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertTrue(readMessage.isLargeMessage());
}
@Test
public void testReadMessageInSingleFrameFromDeliveryBufferWithAnnotations() throws Exception {
doTestReadMessageInSingleFrameFromDeliveryBuffer(true);
}
@Test
public void testReadMessageInSingleFrameFromDeliveryBufferWithoutAnnotations() throws Exception {
doTestReadMessageInSingleFrameFromDeliveryBuffer(false);
}
private void doTestReadMessageInSingleFrameFromDeliveryBuffer(boolean deliveryAnnotations) throws Exception {
AMQPTunneledCoreLargeMessageReader reader = new AMQPTunneledCoreLargeMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
reader.open();
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageFrame = createCoreLargeMessageDelivery(message, deliveryAnnotations);
when(receiver.recv()).thenReturn(coreMessageFrame.duplicate());
when(delivery.isPartial()).thenReturn(false);
Message readMessage = null;
try {
readMessage = reader.readBytes(delivery);
assertNotNull(readMessage);
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
if (deliveryAnnotations) {
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
} else {
assertNull(reader.getDeliveryAnnotations());
}
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertTrue(readMessage.isLargeMessage());
reader.close();
assertNull(reader.getDeliveryAnnotations());
}
@Test
public void testReadLargeMessageAndIgnoreAdditionalSections() throws Exception {
AMQPTunneledCoreLargeMessageReader reader = new AMQPTunneledCoreLargeMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
reader.open();
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageFrame = createCoreLargeMessageWithExtraSections(message);
when(receiver.recv()).thenReturn(coreMessageFrame.duplicate());
when(delivery.isPartial()).thenReturn(false);
Message readMessage = null;
try {
readMessage = reader.readBytes(delivery);
assertNotNull(readMessage);
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertTrue(readMessage.isLargeMessage());
reader.close();
assertNull(reader.getDeliveryAnnotations());
}
private ReadableBuffer createCoreMessageEncoding(CoreMessage message) {
final int encodedSize = message.getHeadersAndPropertiesEncodeSize();
final int dataSectionSize = encodedSize + DATA_SECTION_ENCODING_BYTES;
final ByteBuf buffer = Unpooled.buffer(dataSectionSize, dataSectionSize);
writeDataSectionTypeInfo(buffer, encodedSize);
message.encodeHeadersAndProperties(buffer);
return new NettyReadable(buffer);
}
private ReadableBuffer createRandomLargeMessagePaylod() {
final byte[] payload = new byte[1024];
final Random random = new Random(System.currentTimeMillis());
random.nextBytes(payload);
final int dataSectionSize = payload.length + DATA_SECTION_ENCODING_BYTES;
final ByteBuf buffer = Unpooled.buffer(dataSectionSize, dataSectionSize);
writeDataSectionTypeInfo(buffer, payload.length);
buffer.writeBytes(payload);
return new NettyReadable(buffer);
}
private ReadableBuffer createCoreLargeMessageDelivery(CoreMessage message, boolean annotations) {
final int encodedSize = message.getHeadersAndPropertiesEncodeSize();
final byte[] payload = new byte[1024];
final Random random = new Random(System.currentTimeMillis());
final ByteBuffer deliveryAnnotations;
if (annotations) {
deliveryAnnotations = createAnnotationsBuffer().byteBuffer();
} else {
deliveryAnnotations = ByteBuffer.wrap(new byte[0]);
}
final int dataSection1Size = encodedSize + DATA_SECTION_ENCODING_BYTES;
final int dataSection2Size = payload.length + DATA_SECTION_ENCODING_BYTES;
final int bufferSize = dataSection1Size + dataSection2Size + deliveryAnnotations.remaining();
final ByteBuf buffer = Unpooled.buffer(bufferSize, bufferSize);
buffer.writeBytes(deliveryAnnotations);
writeDataSectionTypeInfo(buffer, encodedSize);
message.encodeHeadersAndProperties(buffer);
random.nextBytes(payload);
writeDataSectionTypeInfo(buffer, payload.length);
buffer.writeBytes(payload);
return new NettyReadable(buffer);
}
private ReadableBuffer createCoreLargeMessageWithExtraSections(CoreMessage message) {
final int encodedSize = message.getHeadersAndPropertiesEncodeSize();
final byte[] payload = new byte[1024];
final Random random = new Random(System.currentTimeMillis());
final ByteBuf sections = Unpooled.buffer();
final EncoderImpl encoder = TLSEncode.getEncoder();
final Header header = new Header();
final DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
final MessageAnnotations msgAnnotations = new MessageAnnotations(new HashMap<>());
final Properties properties = new Properties();
final ApplicationProperties appProperties = new ApplicationProperties(new HashMap<>());
header.setDurable(true);
annotations.getValue().put(Symbol.valueOf("a"), "a");
annotations.getValue().put(Symbol.valueOf("b"), "b");
annotations.getValue().put(Symbol.valueOf("c"), "c");
msgAnnotations.getValue().put(Symbol.valueOf("d"), "d");
properties.setGroupId("group");
appProperties.getValue().put("e", "e");
try {
encoder.setByteBuffer(new NettyWritable(sections));
encoder.writeObject(header);
encoder.writeObject(annotations);
encoder.writeObject(msgAnnotations);
encoder.writeObject(properties);
encoder.writeObject(appProperties);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
final int dataSection1Size = encodedSize + DATA_SECTION_ENCODING_BYTES;
final int dataSection2Size = payload.length + DATA_SECTION_ENCODING_BYTES;
final int bufferSize = dataSection1Size + dataSection2Size + sections.readableBytes();
final ByteBuf buffer = Unpooled.buffer(bufferSize, bufferSize);
buffer.writeBytes(sections);
writeDataSectionTypeInfo(buffer, encodedSize);
message.encodeHeadersAndProperties(buffer);
random.nextBytes(payload);
writeDataSectionTypeInfo(buffer, payload.length);
buffer.writeBytes(payload);
return new NettyReadable(buffer);
}
private void writeDataSectionTypeInfo(ByteBuf buffer, int encodedSize) {
buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
buffer.writeByte(EncodingCodes.SMALLULONG);
buffer.writeByte(DATA_DESCRIPTOR);
buffer.writeByte(EncodingCodes.VBIN32);
buffer.writeInt(encodedSize); // Core message will encode into this size.
}
private ReadableBuffer createAnnotationsBuffer() {
final EncoderImpl encoder = TLSEncode.getEncoder();
final DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
annotations.getValue().put(Symbol.valueOf("a"), "a");
annotations.getValue().put(Symbol.valueOf("b"), "b");
annotations.getValue().put(Symbol.valueOf("c"), "c");
final ByteBuf buffer = Unpooled.buffer();
final NettyWritable writable = new NettyWritable(buffer);
try {
encoder.setByteBuffer(writable);
encoder.writeObject(annotations);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
return new NettyReadable(buffer);
}
}

View File

@ -0,0 +1,330 @@
/*
* 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.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.nio.ByteBuffer;
import java.util.HashMap;
import org.apache.activemq.artemis.core.message.LargeBodyReader;
import org.apache.activemq.artemis.core.persistence.impl.journal.LargeServerMessageImpl;
import org.apache.activemq.artemis.core.persistence.impl.nullpm.NullStorageManager;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
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.DeliveryAnnotations;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Connection;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.apache.qpid.proton.engine.Session;
import org.apache.qpid.proton.engine.impl.TransportImpl;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Tests for basics of core large message writer
*/
public class AMQPTunneledCoreLargeMessageWriterTest {
private static final byte DATA_DESCRIPTOR = 0x75;
@Mock
ProtonServerSenderContext serverSender;
@Mock
Sender protonSender;
@Mock
Session protonSession;
@Mock
Connection protonConnection;
@Mock
TransportImpl protonTransport;
@Mock
Delivery protonDelivery;
@Mock
MessageReference reference;
@Mock
LargeServerMessageImpl message;
@Mock
LargeBodyReader bodyReader;
@Mock
AMQPConnectionContext connectionContext;
@Mock
AMQPSessionContext sessionContext;
@Mock
AMQPSessionCallback sessionSPI;
@Spy
NullStorageManager nullStoreManager = new NullStorageManager();
@Captor
ArgumentCaptor<ReadableBuffer> tunneledCaptor;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
when(serverSender.getSessionContext()).thenReturn(sessionContext);
when(serverSender.getSender()).thenReturn(protonSender);
when(serverSender.createDelivery(any(), anyInt())).thenReturn(protonDelivery);
when(protonSender.getSession()).thenReturn(protonSession);
when(protonSession.getConnection()).thenReturn(protonConnection);
when(protonConnection.getTransport()).thenReturn(protonTransport);
when(protonTransport.getOutboundFrameSizeLimit()).thenReturn(65535);
when(reference.getMessage()).thenReturn(message);
when(message.isLargeMessage()).thenReturn(true);
when(message.getLargeBodyReader()).thenReturn(bodyReader);
when(sessionContext.getSessionSPI()).thenReturn(sessionSPI);
when(sessionContext.getAMQPConnectionContext()).thenReturn(connectionContext);
when(connectionContext.flowControl(any())).thenReturn(true);
when(sessionSPI.getStorageManager()).thenReturn(nullStoreManager);
}
@Test
public void testWriterThrowsIllegalStateIfNotOpenedWhenWriteCalled() throws Exception {
AMQPTunneledCoreLargeMessageWriter writer = new AMQPTunneledCoreLargeMessageWriter(serverSender);
try {
writer.writeBytes(reference);
fail("Should throw as the writer was not opened.");
} catch (IllegalStateException e) {
// Expected
}
}
@Test
public void testNoWritesWhenProtonSenderIsLocallyClosed() throws Exception {
AMQPTunneledCoreLargeMessageWriter writer = new AMQPTunneledCoreLargeMessageWriter(serverSender);
when(protonSender.getLocalState()).thenReturn(EndpointState.CLOSED);
writer.open();
try {
writer.writeBytes(reference);
} catch (IllegalStateException e) {
fail("Should not throw as the delivery is partial so no data should be read.");
}
verify(protonSender).getLocalState();
verifyNoInteractions(reference);
verifyNoInteractions(protonDelivery);
}
@Test
public void testMessageEncodingWrittenToDeliveryWithoutAnnotations() throws Exception {
doTestMessageEncodingWrittenToDeliveryWithAnnotations(false);
}
@Test
public void testMessageEncodingWrittenToDeliveryWithAnnotations() throws Exception {
doTestMessageEncodingWrittenToDeliveryWithAnnotations(true);
}
private void doTestMessageEncodingWrittenToDeliveryWithAnnotations(boolean deliveryAnnotations) throws Exception {
AMQPTunneledCoreLargeMessageWriter writer = new AMQPTunneledCoreLargeMessageWriter(serverSender);
writer.open();
final ByteBuf expectedEncoding = Unpooled.buffer();
final byte[] headersBytes = new byte[4];
headersBytes[0] = 4;
headersBytes[1] = 5;
headersBytes[2] = 6;
headersBytes[3] = 7;
final byte[] payloadBytes = new byte[4];
payloadBytes[0] = 1;
payloadBytes[1] = 2;
payloadBytes[2] = 3;
payloadBytes[3] = 4;
if (deliveryAnnotations) {
final DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
annotations.getValue().put(Symbol.valueOf("a"), "a");
annotations.getValue().put(Symbol.valueOf("b"), "b");
annotations.getValue().put(Symbol.valueOf("c"), "c");
writeDeliveryAnnotations(expectedEncoding, annotations);
when(reference.getProtocolData(any())).thenReturn(annotations);
}
writeDataSection(expectedEncoding, headersBytes);
writeDataSection(expectedEncoding, payloadBytes);
when(protonSender.getLocalState()).thenReturn(EndpointState.ACTIVE);
when(protonDelivery.isPartial()).thenReturn(true);
when(message.getHeadersAndPropertiesEncodeSize()).thenReturn(headersBytes.length);
// Provides the simulated encoded core headers and properties
doAnswer(invocation -> {
final ByteBuf buffer = invocation.getArgument(0);
buffer.writeBytes(headersBytes);
return null;
}).when(message).encodeHeadersAndProperties(any(ByteBuf.class));
when(bodyReader.getSize()).thenReturn((long) payloadBytes.length);
final ByteBuf encodedByteBuf = Unpooled.buffer();
final NettyWritable encodedBytes = new NettyWritable(encodedByteBuf);
// Answer back with the amount of writable bytes
doAnswer(invocation -> {
final ByteBuffer buffer = invocation.getArgument(0);
buffer.put(payloadBytes);
return payloadBytes.length;
}).when(bodyReader).readInto(any());
// Capture the write for comparison, this avoid issues with released netty buffers
doAnswer(invocation -> {
final ReadableBuffer buffer = invocation.getArgument(0);
encodedBytes.put(buffer);
return null;
}).when(protonSender).send(any());
try {
writer.writeBytes(reference);
} catch (IllegalStateException e) {
fail("Should not throw as the delivery is completed so no data should be written.");
}
verify(message).usageUp();
verify(message).getLargeBodyReader();
verify(message).getHeadersAndPropertiesEncodeSize();
verify(message).encodeHeadersAndProperties(any(ByteBuf.class));
verify(reference).getMessage();
verify(reference).getProtocolData(any());
verify(protonSender).getSession();
verify(protonDelivery).getTag();
verify(protonSender, atLeastOnce()).getLocalState();
verify(protonSender, atLeastOnce()).send(any(ReadableBuffer.class));
assertTrue(encodedByteBuf.isReadable());
assertEquals(expectedEncoding.readableBytes(), encodedByteBuf.readableBytes());
assertEquals(expectedEncoding, encodedByteBuf);
verifyNoMoreInteractions(message);
verifyNoMoreInteractions(reference);
verifyNoMoreInteractions(protonDelivery);
}
@Test
public void testLargeMessageUsageLoweredOnCloseWhenWriteNotCompleted() throws Exception {
AMQPTunneledCoreLargeMessageWriter writer = new AMQPTunneledCoreLargeMessageWriter(serverSender);
writer.open();
when(protonSender.getLocalState()).thenReturn(EndpointState.ACTIVE);
when(protonDelivery.isPartial()).thenReturn(true);
// The writer will wait for flow control to resume it
when(connectionContext.flowControl(any())).thenReturn(false);
try {
writer.writeBytes(reference);
} catch (IllegalStateException e) {
fail("Should not throw as the delivery is completed so no data should be written.");
}
try {
writer.close();
} catch (IllegalStateException e) {
fail("Should not throw as the close when write wasn't completed.");
}
verify(message).usageUp();
verify(message).usageDown();
verify(reference).getMessage();
verify(reference).getProtocolData(any());
verify(protonSender).getSession();
verify(protonDelivery).getTag();
verify(protonSender, atLeastOnce()).getLocalState();
verifyNoMoreInteractions(reference);
verifyNoMoreInteractions(protonDelivery);
}
private void writeDeliveryAnnotations(ByteBuf buffer, DeliveryAnnotations annotations) {
final EncoderImpl encoder = TLSEncode.getEncoder();
try {
encoder.setByteBuffer(new NettyWritable(buffer));
encoder.writeObject(annotations);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
}
private void writeDataSection(ByteBuf buffer, byte[] payload) {
buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
buffer.writeByte(EncodingCodes.SMALLULONG);
buffer.writeByte(DATA_DESCRIPTOR);
buffer.writeByte(EncodingCodes.VBIN32);
buffer.writeInt(payload.length);
buffer.writeBytes(payload);
}
}

View File

@ -0,0 +1,351 @@
/*
* 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.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Random;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.Message;
import org.apache.activemq.artemis.core.message.impl.CoreMessage;
import org.apache.activemq.artemis.core.persistence.Persister;
import org.apache.activemq.artemis.core.persistence.impl.nullpm.NullStorageManager;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.util.NettyReadable;
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.ApplicationProperties;
import org.apache.qpid.proton.amqp.messaging.DeliveryAnnotations;
import org.apache.qpid.proton.amqp.messaging.Header;
import org.apache.qpid.proton.amqp.messaging.MessageAnnotations;
import org.apache.qpid.proton.amqp.messaging.Properties;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Checks the functionality of the tunneled core message reader
*/
public class AMQPTunneledCoreMessageReaderTest {
private static final byte DATA_DESCRIPTOR = 0x75;
private static final int DATA_SECTION_ENCODING_BYTES = Long.BYTES;
@Mock
ProtonAbstractReceiver serverReceiver;
@Mock
AMQPSessionContext sessionContext;
@Mock
AMQPSessionCallback sessionSPI;
@Spy
NullStorageManager nullStoreManager = new NullStorageManager();
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
when(serverReceiver.getSessionContext()).thenReturn(sessionContext);
when(sessionContext.getSessionSPI()).thenReturn(sessionSPI);
when(sessionSPI.getStorageManager()).thenReturn(nullStoreManager);
}
@Test
public void testReaderReturnsNullIfCalledOnPartialDelivery() throws Exception {
AMQPTunneledCoreMessageReader reader = new AMQPTunneledCoreMessageReader(serverReceiver);
Delivery delivery = Mockito.mock(Delivery.class);
reader.open();
when(delivery.isPartial()).thenReturn(true);
try {
assertNull(reader.readBytes(delivery));
} catch (IllegalStateException e) {
fail("Should not throw as the delivery is partial so no data should be read.");
}
}
@Test
public void testReadMessageFromDeliveryBuffer() throws Exception {
AMQPTunneledCoreMessageReader reader = new AMQPTunneledCoreMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
reader.open();
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageFrame = createCoreMessageDelivery(message);
when(receiver.recv()).thenReturn(coreMessageFrame.duplicate());
when(delivery.isPartial()).thenReturn(false);
Message readMessage = null;
try {
readMessage = reader.readBytes(delivery);
assertNotNull(readMessage);
assertNull(reader.getDeliveryAnnotations());
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertFalse(readMessage.isLargeMessage());
}
@Test
public void testReadMessageWithAnnotationsFromDeliveryBuffer() throws Exception {
AMQPTunneledCoreMessageReader reader = new AMQPTunneledCoreMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
reader.open();
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageFrame = createCoreMessageDelivery(message, true);
when(receiver.recv()).thenReturn(coreMessageFrame.duplicate());
when(delivery.isPartial()).thenReturn(false);
Message readMessage = null;
try {
readMessage = reader.readBytes(delivery);
assertNotNull(readMessage);
assertNotNull(reader.getDeliveryAnnotations());
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertFalse(readMessage.isLargeMessage());
reader.close();
assertNull(reader.getDeliveryAnnotations());
}
@Test
public void testReadMessageWithAdditionalSectionsThatShouldBeIgnored() throws Exception {
AMQPTunneledCoreMessageReader reader = new AMQPTunneledCoreMessageReader(serverReceiver);
Receiver receiver = Mockito.mock(Receiver.class);
Delivery delivery = Mockito.mock(Delivery.class);
when(delivery.getLink()).thenReturn(receiver);
reader.open();
final CoreMessage message = new CoreMessage();
message.setDurable(true);
message.setExpiration(42);
message.putStringProperty("a", "a");
final ReadableBuffer coreMessageFrame = createCoreMessageWithMoreSections(message);
when(receiver.recv()).thenReturn(coreMessageFrame.duplicate());
when(delivery.isPartial()).thenReturn(false);
Message readMessage = null;
try {
readMessage = reader.readBytes(delivery);
assertNotNull(readMessage);
assertNotNull(reader.getDeliveryAnnotations());
} catch (IllegalStateException e) {
fail("Should not throw as the reader should be able to read just the core message header.");
}
final DeliveryAnnotations annotations = reader.getDeliveryAnnotations();
assertTrue(annotations.getValue().get(Symbol.valueOf("a")).equals("a"));
assertTrue(annotations.getValue().get(Symbol.valueOf("b")).equals("b"));
assertTrue(annotations.getValue().get(Symbol.valueOf("c")).equals("c"));
assertTrue(readMessage.isDurable());
assertEquals(42, readMessage.getExpiration());
assertTrue(readMessage.containsProperty("a"));
assertEquals("a", readMessage.getStringProperty("a"));
assertFalse(readMessage.isLargeMessage());
reader.close();
assertNull(reader.getDeliveryAnnotations());
}
private ReadableBuffer createCoreMessageDelivery(CoreMessage message) {
return createCoreMessageDelivery(message, false);
}
private ReadableBuffer createCoreMessageDelivery(CoreMessage message, boolean addAnnotations) {
final byte[] payload = new byte[1024];
final Random random = new Random(System.currentTimeMillis());
random.nextBytes(payload);
message.initBuffer(payload.length);
message.getBodyBuffer().writeBytes(payload);
final Persister<Message> persister = message.getPersister();
final int encodedSize = persister.getEncodeSize(message);
final ByteBuf buffer = Unpooled.buffer(encodedSize + DATA_SECTION_ENCODING_BYTES); // Account for the data section
if (addAnnotations) {
final EncoderImpl encoder = TLSEncode.getEncoder();
final DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
annotations.getValue().put(Symbol.valueOf("a"), "a");
annotations.getValue().put(Symbol.valueOf("b"), "b");
annotations.getValue().put(Symbol.valueOf("c"), "c");
try {
encoder.setByteBuffer(new NettyWritable(buffer));
encoder.writeObject(annotations);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
}
writeDataSectionTypeInfo(buffer, encodedSize);
final ActiveMQBuffer bufferWrapper = ActiveMQBuffers.wrappedBuffer(buffer);
message.persist(bufferWrapper);
// Update the buffer that was allocated with the bytes that were written using the wrapper
// since the wrapper doesn't update the wrapper buffer.
buffer.writerIndex(buffer.writerIndex() + encodedSize);
return new NettyReadable(buffer);
}
private ReadableBuffer createCoreMessageWithMoreSections(CoreMessage message) {
final byte[] payload = new byte[1024];
final Random random = new Random(System.currentTimeMillis());
random.nextBytes(payload);
message.initBuffer(payload.length);
message.getBodyBuffer().writeBytes(payload);
final Persister<Message> persister = message.getPersister();
final int encodedSize = persister.getEncodeSize(message);
final ByteBuf buffer = Unpooled.buffer(encodedSize + DATA_SECTION_ENCODING_BYTES); // Account for the data section
final EncoderImpl encoder = TLSEncode.getEncoder();
final Header header = new Header();
final DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
final MessageAnnotations msgAnnotations = new MessageAnnotations(new HashMap<>());
final Properties properties = new Properties();
final ApplicationProperties appProperties = new ApplicationProperties(new HashMap<>());
header.setDurable(true);
annotations.getValue().put(Symbol.valueOf("a"), "a");
annotations.getValue().put(Symbol.valueOf("b"), "b");
annotations.getValue().put(Symbol.valueOf("c"), "c");
msgAnnotations.getValue().put(Symbol.valueOf("d"), "d");
properties.setGroupId("group");
appProperties.getValue().put("e", "e");
try {
encoder.setByteBuffer(new NettyWritable(buffer));
encoder.writeObject(header);
encoder.writeObject(annotations);
encoder.writeObject(msgAnnotations);
encoder.writeObject(properties);
encoder.writeObject(appProperties);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
writeDataSectionTypeInfo(buffer, encodedSize);
final ActiveMQBuffer bufferWrapper = ActiveMQBuffers.wrappedBuffer(buffer);
message.persist(bufferWrapper);
// Update the buffer that was allocated with the bytes that were written using the wrapper
// since the wrapper doesn't update the wrapper buffer.
buffer.writerIndex(buffer.writerIndex() + encodedSize);
return new NettyReadable(buffer);
}
private void writeDataSectionTypeInfo(ByteBuf buffer, int encodedSize) {
buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
buffer.writeByte(EncodingCodes.SMALLULONG);
buffer.writeByte(DATA_DESCRIPTOR);
buffer.writeByte(EncodingCodes.VBIN32);
buffer.writeInt(encodedSize); // Core message will encode into this size.
}
}

View File

@ -0,0 +1,218 @@
/*
* 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.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.nio.ByteBuffer;
import java.util.HashMap;
import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ICoreMessage;
import org.apache.activemq.artemis.core.persistence.impl.nullpm.NullStorageManager;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPSessionCallback;
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.DeliveryAnnotations;
import org.apache.qpid.proton.codec.EncoderImpl;
import org.apache.qpid.proton.codec.EncodingCodes;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.codec.WritableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.EndpointState;
import org.apache.qpid.proton.engine.Sender;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Test for the non-large tunneled core message writer
*/
public class AMQPTunneledCoreMessageWriterTest {
private static final byte DATA_DESCRIPTOR = 0x75;
@Mock
ProtonServerSenderContext serverSender;
@Mock
Sender protonSender;
@Mock
Delivery protonDelivery;
@Mock
MessageReference reference;
@Mock
ICoreMessage message;
@Mock
AMQPSessionContext sessionContext;
@Mock
AMQPSessionCallback sessionSPI;
@Spy
NullStorageManager nullStoreManager = new NullStorageManager();
@Captor
ArgumentCaptor<ReadableBuffer> tunneledCaptor;
@Before
public void setUp() {
MockitoAnnotations.openMocks(this);
when(serverSender.getSessionContext()).thenReturn(sessionContext);
when(serverSender.getSender()).thenReturn(protonSender);
when(serverSender.createDelivery(any(), anyInt())).thenReturn(protonDelivery);
when(reference.getMessage()).thenReturn(message);
when(sessionContext.getSessionSPI()).thenReturn(sessionSPI);
when(sessionSPI.getStorageManager()).thenReturn(nullStoreManager);
}
@Test
public void testNoWritesWhenProtonSenderIsLocallyClosed() throws Exception {
AMQPTunneledCoreMessageWriter writer = new AMQPTunneledCoreMessageWriter(serverSender);
when(protonSender.getLocalState()).thenReturn(EndpointState.CLOSED);
writer.open();
try {
writer.writeBytes(reference);
} catch (IllegalStateException e) {
fail("Should not throw as the delivery is partial so no data should be read.");
}
verify(protonSender).getLocalState();
verifyNoInteractions(reference);
verifyNoInteractions(protonDelivery);
}
@Test
public void testMessageEncodingWrittenToDeliveryWithoutAnnotations() throws Exception {
doTestMessageEncodingWrittenToDeliveryWithAnnotations(false);
}
@Test
public void testMessageEncodingWrittenToDeliveryWithAnnotations() throws Exception {
doTestMessageEncodingWrittenToDeliveryWithAnnotations(true);
}
private void doTestMessageEncodingWrittenToDeliveryWithAnnotations(boolean deliveryAnnotations) throws Exception {
AMQPTunneledCoreMessageWriter writer = new AMQPTunneledCoreMessageWriter(serverSender);
final ByteBuf expectedEncoding = Unpooled.buffer();
final byte[] messageBytes = new byte[4];
messageBytes[0] = 1;
messageBytes[1] = 2;
messageBytes[2] = 3;
messageBytes[3] = 4;
if (deliveryAnnotations) {
final DeliveryAnnotations annotations = new DeliveryAnnotations(new HashMap<>());
annotations.getValue().put(Symbol.valueOf("a"), "a");
annotations.getValue().put(Symbol.valueOf("b"), "b");
annotations.getValue().put(Symbol.valueOf("c"), "c");
writeDeliveryAnnotations(expectedEncoding, annotations);
when(reference.getProtocolData(any())).thenReturn(annotations);
}
writeDataSection(expectedEncoding, messageBytes);
when(protonSender.getLocalState()).thenReturn(EndpointState.ACTIVE);
when(message.getPersistSize()).thenReturn(messageBytes.length);
doAnswer(invocation -> {
final ActiveMQBuffer buffer = invocation.getArgument(0);
buffer.writeBytes(messageBytes);
return null;
}).when(message).persist(any(ActiveMQBuffer.class));
writer.open();
try {
writer.writeBytes(reference);
} catch (IllegalStateException e) {
fail("Should not throw as the delivery is complete and all data should have been written.");
}
verify(reference).getMessage();
verify(reference).getProtocolData(any());
verify(protonSender).getLocalState();
verify(protonSender).sendNoCopy(tunneledCaptor.capture());
final ReadableBuffer tunneledBytes = tunneledCaptor.getValue();
final ByteBuffer tunneled = tunneledBytes.byteBuffer();
final ByteBuf tunneledByteBuf = Unpooled.wrappedBuffer(tunneled);
assertTrue(tunneledByteBuf.isReadable());
assertEquals(expectedEncoding.readableBytes(), tunneledByteBuf.readableBytes());
assertEquals(expectedEncoding, tunneledByteBuf);
verifyNoMoreInteractions(reference);
verifyNoInteractions(protonDelivery);
}
private void writeDeliveryAnnotations(ByteBuf buffer, DeliveryAnnotations annotations) {
final EncoderImpl encoder = TLSEncode.getEncoder();
try {
encoder.setByteBuffer(new NettyWritable(buffer));
encoder.writeObject(annotations);
} finally {
encoder.setByteBuffer((WritableBuffer) null);
}
}
private void writeDataSection(ByteBuf buffer, byte[] payload) {
buffer.writeByte(EncodingCodes.DESCRIBED_TYPE_INDICATOR);
buffer.writeByte(EncodingCodes.SMALLULONG);
buffer.writeByte(DATA_DESCRIPTOR);
buffer.writeByte(EncodingCodes.VBIN32);
buffer.writeInt(payload.length); // Core message will encode into this size.
buffer.writeBytes(payload);
}
}

View File

@ -22,6 +22,8 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.messaging.Target;
import org.apache.qpid.proton.engine.Link;
import org.junit.Test;
import org.mockito.Mockito;
@ -108,4 +110,46 @@ public class AmqpSupportTest {
assertFalse(AmqpSupport.verifyDesiredCapability(link, A));
}
@Test
public void testVerifySourceCapability() {
final Source source = Mockito.mock(Source.class);
Mockito.when(source.getCapabilities()).thenReturn(new Symbol[] {B, C});
assertFalse(AmqpSupport.verifySourceCapability(source, A));
Mockito.when(source.getCapabilities()).thenReturn(ALL);
assertTrue(AmqpSupport.verifySourceCapability(source, A));
assertTrue(AmqpSupport.verifySourceCapability(source, B));
assertTrue(AmqpSupport.verifySourceCapability(source, C));
assertThrows(NullPointerException.class, () -> AmqpSupport.verifySourceCapability(source, null));
Mockito.when(source.getCapabilities()).thenReturn(null);
assertFalse(AmqpSupport.verifySourceCapability(source, A));
}
@Test
public void testVerifyTargetCapability() {
final Target target = Mockito.mock(Target.class);
Mockito.when(target.getCapabilities()).thenReturn(new Symbol[] {B, C});
assertFalse(AmqpSupport.verifyTargetCapability(target, A));
Mockito.when(target.getCapabilities()).thenReturn(ALL);
assertTrue(AmqpSupport.verifyTargetCapability(target, A));
assertTrue(AmqpSupport.verifyTargetCapability(target, B));
assertTrue(AmqpSupport.verifyTargetCapability(target, C));
assertThrows(NullPointerException.class, () -> AmqpSupport.verifyTargetCapability(target, null));
Mockito.when(target.getCapabilities()).thenReturn(null);
assertFalse(AmqpSupport.verifyTargetCapability(target, A));
}
}

View File

@ -40,9 +40,12 @@ import org.apache.activemq.artemis.core.server.RoutingContext;
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.AMQPSessionCallback;
import org.apache.activemq.artemis.protocol.amqp.broker.AMQPStandardMessage;
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager;
import org.apache.activemq.artemis.protocol.amqp.converter.AMQPMessageSupport;
import org.apache.activemq.artemis.protocol.amqp.exceptions.ActiveMQAMQPException;
import org.apache.activemq.artemis.protocol.amqp.util.NettyReadable;
import org.apache.activemq.artemis.protocol.amqp.util.NettyWritable;
import org.apache.qpid.proton.amqp.Symbol;
import org.apache.qpid.proton.amqp.messaging.Accepted;
import org.apache.qpid.proton.amqp.messaging.Modified;
@ -50,12 +53,18 @@ import org.apache.qpid.proton.amqp.messaging.Outcome;
import org.apache.qpid.proton.amqp.messaging.Rejected;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.transport.DeliveryState;
import org.apache.qpid.proton.codec.ReadableBuffer;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Receiver;
import org.apache.qpid.proton.message.Message;
import org.apache.qpid.proton.message.impl.MessageImpl;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import io.netty.buffer.Unpooled;
public class ProtonServerReceiverContextTest {
@Test
@ -102,14 +111,22 @@ public class ProtonServerReceiverContextTest {
AMQPSessionCallback mockSessionSpi = mock(AMQPSessionCallback.class);
when(mockSessionSpi.getStorageManager()).thenReturn(new NullStorageManager());
when(mockSessionSpi.createStandardMessage(any(), any())).thenAnswer(new Answer<AMQPStandardMessage>() {
@Override
public AMQPStandardMessage answer(InvocationOnMock invocation) throws Throwable {
return new AMQPStandardMessage(0, createAMQPMessageBuffer(), null, null);
}
});
AMQPSessionContext mockProtonContext = mock(AMQPSessionContext.class);
when(mockProtonContext.getSessionSPI()).thenReturn(mockSessionSpi);
AtomicInteger clearLargeMessage = new AtomicInteger(0);
ProtonServerReceiverContext rc = new ProtonServerReceiverContext(mockSessionSpi, mockConnContext, mockProtonContext, mockReceiver) {
@Override
protected void clearLargeMessage() {
super.clearLargeMessage();
protected void closeCurrentReader() {
super.closeCurrentReader();
clearLargeMessage.incrementAndGet();
}
};
@ -120,6 +137,7 @@ public class ProtonServerReceiverContextTest {
when(mockDelivery.getLink()).thenReturn(mockReceiver);
when(mockReceiver.current()).thenReturn(mockDelivery);
when(mockReceiver.recv()).thenReturn(createAMQPMessageBuffer());
rc.onMessage(mockDelivery);
@ -129,10 +147,8 @@ public class ProtonServerReceiverContextTest {
verify(mockReceiver, times(1)).advance();
Assert.assertTrue(clearLargeMessage.get() > 0);
}
private void doOnMessageWithAbortedDeliveryTestImpl(boolean drain) throws ActiveMQAMQPException {
Receiver mockReceiver = mock(Receiver.class);
AMQPConnectionContext mockConnContext = mock(AMQPConnectionContext.class);
@ -145,8 +161,8 @@ public class ProtonServerReceiverContextTest {
AtomicInteger clearLargeMessage = new AtomicInteger(0);
ProtonServerReceiverContext rc = new ProtonServerReceiverContext(null, mockConnContext, null, mockReceiver) {
@Override
protected void clearLargeMessage() {
super.clearLargeMessage();
protected void closeCurrentReader() {
super.closeCurrentReader();
clearLargeMessage.incrementAndGet();
}
};
@ -198,9 +214,19 @@ public class ProtonServerReceiverContextTest {
when(mockConnContext.getProtocolManager()).thenReturn(mockProtocolManager);
AMQPSessionCallback mockSession = mock(AMQPSessionCallback.class);
when(mockSession.createStandardMessage(any(), any())).thenAnswer(new Answer<AMQPStandardMessage>() {
@Override
public AMQPStandardMessage answer(InvocationOnMock invocation) throws Throwable {
return new AMQPStandardMessage(0, createAMQPMessageBuffer(), null, null);
}
});
AMQPSessionContext mockSessionContext = mock(AMQPSessionContext.class);
when(mockSessionContext.getSessionSPI()).thenReturn(mockSession);
Receiver mockReceiver = mock(Receiver.class);
ProtonServerReceiverContext rc = new ProtonServerReceiverContext(mockSession, mockConnContext, null, mockReceiver);
ProtonServerReceiverContext rc = new ProtonServerReceiverContext(mockSession, mockConnContext, mockSessionContext, mockReceiver);
rc.incrementSettle();
Delivery mockDelivery = mock(Delivery.class);
@ -220,7 +246,6 @@ public class ProtonServerReceiverContextTest {
verify(mockDelivery, times(1)).disposition(any(expectedDeliveryState));
}
@Test
public void calculateFlowControl() {
Assert.assertFalse(ProtonServerReceiverContext.isBellowThreshold(1000, 100, 1000));
@ -230,4 +255,13 @@ public class ProtonServerReceiverContextTest {
Assert.assertEquals(900, ProtonServerReceiverContext.calculatedUpdateRefill(2000, 1000, 100));
}
private ReadableBuffer createAMQPMessageBuffer() {
MessageImpl message = (MessageImpl) Message.Factory.create();
message.setContentType(AMQPMessageSupport.OCTET_STREAM_CONTENT_TYPE);
NettyWritable encoded = new NettyWritable(Unpooled.buffer(1024));
message.encode(encoded);
return new NettyReadable(encoded.getByteBuf());
}
}

View File

@ -55,16 +55,19 @@ public class ProtonServerSenderContextTest {
AMQPSessionCallback mockSessionCallback = mock(AMQPSessionCallback.class);
AMQPSessionContext mockSessionContext = mock(AMQPSessionContext.class);
when(mockSessionContext.getSessionSPI()).thenReturn(mockSessionCallback);
when(mockSessionContext.getAMQPConnectionContext()).thenReturn(mockConnContext);
AddressQueryResult queryResult = new AddressQueryResult(null, Collections.emptySet(), 0, false, false, false, false, 0);
when(mockSessionCallback.addressQuery(any(), any(), anyBoolean())).thenReturn(queryResult);
ProtonServerSenderContext sc = new ProtonServerSenderContext( mockConnContext, mockSender, null, mockSessionCallback);
ProtonServerSenderContext sc = new ProtonServerSenderContext(
mockConnContext, mockSender, mockSessionContext, mockSessionCallback);
Source source = new Source();
source.setAddress(null);
when(mockSender.getRemoteSource()).thenReturn(source);
sc.initialize();
}
}

View File

@ -16,6 +16,9 @@
*/
package org.apache.activemq.artemis.core.config.amqpBrokerConnectivity;
import java.util.HashMap;
import java.util.Map;
import org.apache.activemq.artemis.api.core.SimpleString;
public class AMQPMirrorBrokerConnectionElement extends AMQPBrokerConnectionElement {
@ -34,6 +37,8 @@ public class AMQPMirrorBrokerConnectionElement extends AMQPBrokerConnectionEleme
String addressFilter;
private Map<String, Object> properties = new HashMap<>();
public SimpleString getMirrorSNF() {
return mirrorSNF;
}
@ -108,4 +113,41 @@ public class AMQPMirrorBrokerConnectionElement extends AMQPBrokerConnectionEleme
this.sync = sync;
return this;
}
/**
* Adds the given property key and value to the mirror broker 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 AMQPMirrorBrokerConnectionElement addProperty(String key, String value) {
properties.put(key, value);
return this;
}
/**
* Adds the given property key and value to the mirror broker 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 AMQPMirrorBrokerConnectionElement addProperty(String key, Number value) {
properties.put(key, value);
return this;
}
/**
* @return the collection of configuration properties associated with this mirror configuration element.
*/
public Map<String, Object> getProperties() {
return properties;
}
}

View File

@ -2191,6 +2191,17 @@ public final class FileConfigurationParser extends XMLConfigurationUtil {
amqpMirrorConnectionElement.setMessageAcknowledgements(messageAcks).setQueueCreation(queueCreation).setQueueRemoval(queueRemoval).setDurable(durable).setAddressFilter(addressFilter).setSync(sync);
connectionElement = amqpMirrorConnectionElement;
connectionElement.setType(AMQPBrokerConnectionAddressType.MIRROR);
final NodeList federationAttrs = e2.getChildNodes();
for (int j = 0; j < federationAttrs.getLength(); j++) {
final Node mirrorElement = federationAttrs.item(j);
if (mirrorElement.getNodeName().equals("property")) {
amqpMirrorConnectionElement.addProperty(getAttributeValue(mirrorElement, "key"), getAttributeValue(mirrorElement, "value"));
}
}
} else if (nodeType == AMQPBrokerConnectionAddressType.FEDERATION) {
final AMQPFederatedBrokerConnectionElement amqpFederationConnectionElement = new AMQPFederatedBrokerConnectionElement(name);
final NodeList federationAttrs = e2.getChildNodes();

View File

@ -806,7 +806,7 @@
</xsd:documentation>
</xsd:annotation>
</xsd:element>
<xsd:element name="min-disk-free" type="xsd:string" default="-1" maxOccurs="1" minOccurs="0">
<xsd:annotation>
<xsd:documentation>
@ -2137,7 +2137,16 @@
All events will be send towards this AMQP connection acting like a replica.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="property" minOccurs="0"
maxOccurs="unbounded">
<xsd:annotation>
<xsd:documentation>
Optional properties that can be applied to the mirror configuration
</xsd:documentation>
</xsd:annotation>
</xsd:element>
</xsd:sequence>
<xsd:attribute name="message-acknowledgements" type="xsd:boolean" use="optional" default="true">
<xsd:annotation>
<xsd:documentation>
@ -2183,7 +2192,6 @@
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:complexType name="amqp-federation-type">

View File

@ -747,6 +747,7 @@ public class ConfigurationImplTest extends ActiveMQTestBase {
insertionOrderedProperties.put("AMQPConnections.target.connectionElements.mirror.queueCreation", "true");
insertionOrderedProperties.put("AMQPConnections.target.connectionElements.mirror.queueRemoval", "true");
insertionOrderedProperties.put("AMQPConnections.target.connectionElements.mirror.addressFilter", "foo");
insertionOrderedProperties.put("AMQPConnections.target.connectionElements.mirror.properties.a", "b");
if (sync) {
insertionOrderedProperties.put("AMQPConnections.target.connectionElements.mirror.sync", "true");
} // else we just use the default that is false
@ -772,6 +773,8 @@ public class ConfigurationImplTest extends ActiveMQTestBase {
Assert.assertEquals(true, amqpMirrorBrokerConnectionElement.isQueueRemoval());
Assert.assertEquals(sync, ((AMQPMirrorBrokerConnectionElement) amqpBrokerConnectionElement).isSync());
Assert.assertEquals("foo", amqpMirrorBrokerConnectionElement.getAddressFilter());
Assert.assertFalse(amqpMirrorBrokerConnectionElement.getProperties().isEmpty());
Assert.assertEquals("b", amqpMirrorBrokerConnectionElement.getProperties().get("a"));
}
@Test

View File

@ -495,6 +495,11 @@
<property key="amqpLowCredits" value="1"/>
</federation>
</amqp-connection>
<amqp-connection uri="tcp://false" name="test-property" auto-start="false">
<mirror>
<property key="tunnel-core-messages" value="false"/>
</mirror>
</amqp-connection>
</broker-connections>
<grouping-handler name="gh1">
<type>LOCAL</type>

View File

@ -38,6 +38,9 @@ import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPF
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_PROPERTIES_MAP;
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.FEDERATED_ADDRESS_SOURCE_PROPERTIES;
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.MESSAGE_HOPS_ANNOTATION;
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.generateAddressFilter;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.allOf;
@ -46,11 +49,13 @@ import static org.hamcrest.CoreMatchers.containsString;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.jms.BytesMessage;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.Message;
@ -103,6 +108,11 @@ public class AMQPFederationAddressPolicyTest extends AmqpClientTestSupport {
private static final WildcardConfiguration DEFAULT_WILDCARD_CONFIGURATION = new WildcardConfiguration();
@Override
protected String getConfiguredProtocols() {
return "AMQP,CORE";
}
@Override
protected ActiveMQServer createServer() throws Exception {
// Creates the broker used to make the outgoing connection. The port passed is for
@ -367,9 +377,7 @@ public class AMQPFederationAddressPolicyTest extends AmqpClientTestSupport {
server.start();
server.addAddressInfo(new AddressInfo(SimpleString.toSimpleString("test"), RoutingType.MULTICAST));
final String expectedJMSFilter = "\"m." + MESSAGE_HOPS_ANNOTATION +
"\" IS NULL OR \"m." + MESSAGE_HOPS_ANNOTATION +
"\"<1";
final String expectedJMSFilter = generateAddressFilter(1);
final Map<String, Object> expectedSourceProperties = new HashMap<>();
expectedSourceProperties.put(ADDRESS_AUTO_DELETE, true);
@ -2096,6 +2104,167 @@ public class AMQPFederationAddressPolicyTest extends AmqpClientTestSupport {
}
}
@Test(timeout = 20000)
public void testCoreMessageConvertedToAMQPWhenTunnelingDisabled() throws Exception {
doTestCoreMessageHandlingBasedOnTunnelingState(false);
}
@Test(timeout = 20000)
public void testCoreMessageNotConvertedToAMQPWhenTunnelingEnabled() throws Exception {
doTestCoreMessageHandlingBasedOnTunnelingState(true);
}
private void doTestCoreMessageHandlingBasedOnTunnelingState(boolean tunneling) throws Exception {
server.start();
final String[] receiverOfferedCapabilities;
final int messageFormat;
if (tunneling) {
receiverOfferedCapabilities = new String[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString()};
messageFormat = AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
} else {
receiverOfferedCapabilities = null;
messageFormat = 0;
}
try (ProtonTestClient peer = new ProtonTestClient()) {
scriptFederationConnectToRemote(peer, "test");
peer.connect("localhost", AMQP_PORT);
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectAttach().ofSender().withName("federation-address-receiver")
.withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString())
.withDesiredCapabilities(AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withSource().withAddress("test");
// Connect to remote as if an address had demand and matched our federation policy
// If core message tunneling is enabled we include the desired capability
peer.remoteAttach().ofReceiver()
.withOfferedCapabilities(receiverOfferedCapabilities)
.withDesiredCapabilities(FEDERATION_ADDRESS_RECEIVER.toString())
.withName("federation-address-receiver")
.withSenderSettleModeUnsettled()
.withReceivervSettlesFirst()
.withSource().withDurabilityOfNone()
.withExpiryPolicyOnLinkDetach()
.withAddress("test")
.withCapabilities("topic")
.and()
.withTarget().and()
.now();
peer.remoteFlow().withLinkCredit(10).now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectTransfer().withNonNullPayload()
.withMessageFormat(messageFormat).accept();
final ConnectionFactory factory = CFUtil.createConnectionFactory(
"CORE", "tcp://localhost:" + AMQP_PORT + "?minLargeMessageSize=512");
try (Connection connection = factory.createConnection()) {
final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE);
final MessageProducer producer = session.createProducer(session.createTopic("test"));
producer.send(session.createTextMessage("Hello World"));
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
}
peer.expectClose();
peer.remoteClose().now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.close();
server.stop();
}
}
@Test(timeout = 20000)
public void testCoreLargeMessageConvertedToAMQPWhenTunnelingDisabled() throws Exception {
doTestCoreLargeMessageHandlingBasedOnTunnelingState(false);
}
@Test(timeout = 20000)
public void testCoreLargeMessageNotConvertedToAMQPWhenTunnelingEnabled() throws Exception {
doTestCoreLargeMessageHandlingBasedOnTunnelingState(true);
}
private void doTestCoreLargeMessageHandlingBasedOnTunnelingState(boolean tunneling) throws Exception {
server.start();
final String[] receiverOfferedCapabilities;
final int messageFormat;
if (tunneling) {
receiverOfferedCapabilities = new String[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString()};
messageFormat = AMQP_TUNNELED_CORE_LARGE_MESSAGE_FORMAT;
} else {
receiverOfferedCapabilities = null;
messageFormat = 0;
}
try (ProtonTestClient peer = new ProtonTestClient()) {
scriptFederationConnectToRemote(peer, "test");
peer.connect("localhost", AMQP_PORT);
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectAttach().ofSender().withName("federation-address-receiver")
.withOfferedCapabilities(FEDERATION_ADDRESS_RECEIVER.toString())
.withDesiredCapabilities(AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withSource().withAddress("test");
// Connect to remote as if an address had demand and matched our federation policy
// If core message tunneling is enabled we include the offered capability
peer.remoteAttach().ofReceiver()
.withDesiredCapabilities(FEDERATION_ADDRESS_RECEIVER.toString())
.withOfferedCapabilities(receiverOfferedCapabilities)
.withName("federation-address-receiver")
.withSenderSettleModeUnsettled()
.withReceivervSettlesFirst()
.withSource().withDurabilityOfNone()
.withExpiryPolicyOnLinkDetach()
.withAddress("test")
.withCapabilities("topic")
.and()
.withTarget().and()
.now();
peer.remoteFlow().withLinkCredit(10).now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectTransfer().withNonNullPayload()
.withMessageFormat(messageFormat).accept();
final ConnectionFactory factory = CFUtil.createConnectionFactory(
"CORE", "tcp://localhost:" + AMQP_PORT + "?minLargeMessageSize=512");
try (Connection connection = factory.createConnection()) {
final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE);
final MessageProducer producer = session.createProducer(session.createTopic("test"));
final byte[] payload = new byte[1024];
Arrays.fill(payload, (byte) 65);
final BytesMessage message = session.createBytesMessage();
message.writeBytes(payload);
producer.send(message);
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
}
peer.expectClose();
peer.remoteClose().now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.close();
server.stop();
}
}
public static class ApplicationPropertiesTransformer implements Transformer {
private final Map<String, String> properties = new HashMap<>();

View File

@ -59,6 +59,7 @@ import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFedera
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.protocol.amqp.proton.AmqpSupport;
import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport;
import org.apache.activemq.artemis.utils.Wait;
import org.apache.qpid.protonj2.test.driver.ProtonTestClient;
@ -137,12 +138,14 @@ public class AMQPFederationConnectTest extends AmqpClientTestSupport {
final int AMQP_CREDITS = 100;
final int AMQP_CREDITS_LOW = 50;
final int AMQP_LINK_ATTACH_TIMEOUT = 60;
final boolean AMQP_TUNNEL_CORE_MESSAGES = false;
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);
federationConfiguration.put(AmqpSupport.TUNNEL_CORE_MESSAGES, AMQP_TUNNEL_CORE_MESSAGES);
try (ProtonTestServer peer = new ProtonTestServer()) {
peer.expectSASLAnonymousConnect("PLAIN", "ANONYMOUS");
@ -171,6 +174,7 @@ public class AMQPFederationConnectTest extends AmqpClientTestSupport {
amqpConnection.setReconnectAttempts(0);// No reconnects
final AMQPFederatedBrokerConnectionElement federation = new AMQPFederatedBrokerConnectionElement("myFederation");
federation.addProperty(LINK_ATTACH_TIMEOUT, AMQP_LINK_ATTACH_TIMEOUT);
federation.addProperty(AmqpSupport.TUNNEL_CORE_MESSAGES, Boolean.toString(AMQP_TUNNEL_CORE_MESSAGES));
amqpConnection.addElement(federation);
server.getConfiguration().addAMQPConnection(amqpConnection);
server.start();

View File

@ -36,6 +36,7 @@ import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPF
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationConstants.POLICY_PROPERTIES_MAP;
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationPolicySupport.DEFAULT_QUEUE_RECEIVER_PRIORITY_ADJUSTMENT;
import static org.apache.activemq.artemis.protocol.amqp.connect.federation.AMQPFederationQueueConsumer.DEFAULT_PULL_CREDIT_BATCH_SIZE;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
@ -46,12 +47,15 @@ import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.jms.BytesMessage;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.MessageConsumer;
@ -100,6 +104,11 @@ public class AMQPFederationQueuePolicyTest extends AmqpClientTestSupport {
private static final WildcardConfiguration DEFAULT_WILDCARD_CONFIGURATION = new WildcardConfiguration();
@Override
protected String getConfiguredProtocols() {
return "AMQP,CORE";
}
@Override
protected ActiveMQServer createServer() throws Exception {
// Creates the broker used to make the outgoing connection. The port passed is for
@ -2425,6 +2434,171 @@ public class AMQPFederationQueuePolicyTest extends AmqpClientTestSupport {
}
}
@Test(timeout = 20000)
public void testCoreMessageConvertedToAMQPWhenTunnelingDisabled() throws Exception {
doTestCoreMessageHandlingBasedOnTunnelingState(false);
}
@Test(timeout = 20000)
public void testCoreMessageNotConvertedToAMQPWhenTunnelingEnabled() throws Exception {
doTestCoreMessageHandlingBasedOnTunnelingState(true);
}
private void doTestCoreMessageHandlingBasedOnTunnelingState(boolean tunneling) throws Exception {
server.start();
server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST)
.setAddress("test")
.setAutoCreated(false));
final String[] receiverOfferedCapabilities;
final int messageFormat;
if (tunneling) {
receiverOfferedCapabilities = new String[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString()};
messageFormat = AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
} else {
receiverOfferedCapabilities = null;
messageFormat = 0;
}
try (ProtonTestClient peer = new ProtonTestClient()) {
scriptFederationConnectToRemote(peer, "test");
peer.connect("localhost", AMQP_PORT);
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectAttach().ofSender().withName("federation-queue-receiver")
.withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString())
.withDesiredCapabilities(AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withSource().withAddress("test::test");
// Connect to remote as if an queue had demand and matched our federation policy
// If core message tunneling is enabled we include the desired capability
peer.remoteAttach().ofReceiver()
.withOfferedCapabilities(receiverOfferedCapabilities)
.withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString())
.withName("federation-queue-receiver")
.withSenderSettleModeUnsettled()
.withReceivervSettlesFirst()
.withSource().withDurabilityOfNone()
.withExpiryPolicyOnLinkDetach()
.withAddress("test::test")
.withCapabilities("queue")
.and()
.withTarget().and()
.now();
peer.remoteFlow().withLinkCredit(10).now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectTransfer().withNonNullPayload()
.withMessageFormat(messageFormat).accept();
final ConnectionFactory factory = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + AMQP_PORT);
try (Connection connection = factory.createConnection()) {
final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE);
final MessageProducer producer = session.createProducer(session.createQueue("test"));
producer.send(session.createTextMessage("Hello World"));
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
}
peer.expectClose();
peer.remoteClose().now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.close();
server.stop();
}
}
@Test(timeout = 20000)
public void testCoreLargeMessageConvertedToAMQPWhenTunnelingDisabled() throws Exception {
doTestCoreLargeMessageHandlingBasedOnTunnelingState(false);
}
@Test(timeout = 20000)
public void testCoreLargeMessageNotConvertedToAMQPWhenTunnelingEnabled() throws Exception {
doTestCoreLargeMessageHandlingBasedOnTunnelingState(true);
}
private void doTestCoreLargeMessageHandlingBasedOnTunnelingState(boolean tunneling) throws Exception {
server.start();
server.createQueue(new QueueConfiguration("test").setRoutingType(RoutingType.ANYCAST)
.setAddress("test")
.setAutoCreated(false));
final String[] receiverOfferedCapabilities;
final int messageFormat;
if (tunneling) {
receiverOfferedCapabilities = new String[] {AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString()};
messageFormat = AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
} else {
receiverOfferedCapabilities = null;
messageFormat = 0;
}
try (ProtonTestClient peer = new ProtonTestClient()) {
scriptFederationConnectToRemote(peer, "test");
peer.connect("localhost", AMQP_PORT);
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectAttach().ofSender().withName("federation-queue-receiver")
.withOfferedCapabilities(FEDERATION_QUEUE_RECEIVER.toString())
.withDesiredCapabilities(AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withSource().withAddress("test::test");
// Connect to remote as if an queue had demand and matched our federation policy
// If core message tunneling is enabled we include the desired capability
peer.remoteAttach().ofReceiver()
.withOfferedCapabilities(receiverOfferedCapabilities)
.withDesiredCapabilities(FEDERATION_QUEUE_RECEIVER.toString())
.withName("federation-queue-receiver")
.withSenderSettleModeUnsettled()
.withReceivervSettlesFirst()
.withSource().withDurabilityOfNone()
.withExpiryPolicyOnLinkDetach()
.withAddress("test::test")
.withCapabilities("queue")
.and()
.withTarget().and()
.now();
peer.remoteFlow().withLinkCredit(10).now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.expectTransfer().withNonNullPayload()
.withMessageFormat(messageFormat).accept();
final ConnectionFactory factory = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + AMQP_PORT);
try (Connection connection = factory.createConnection()) {
final Session session = connection.createSession(Session.AUTO_ACKNOWLEDGE);
final MessageProducer producer = session.createProducer(session.createQueue("test"));
final byte[] payload = new byte[1024];
Arrays.fill(payload, (byte) 65);
final BytesMessage message = session.createBytesMessage();
message.writeBytes(payload);
producer.send(message);
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
}
peer.expectClose();
peer.remoteClose().now();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
peer.close();
server.stop();
}
}
public static class ApplicationPropertiesTransformer implements Transformer {
private final Map<String, String> properties = new HashMap<>();

View File

@ -18,8 +18,13 @@
package org.apache.activemq.artemis.tests.integration.amqp.connect;
import java.lang.invoke.MethodHandles;
import java.util.Arrays;
import java.util.Map;
import javax.jms.BytesMessage;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
@ -39,6 +44,7 @@ import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPFedera
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.protocol.amqp.proton.AmqpSupport;
import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport;
import org.apache.activemq.artemis.tests.util.CFUtil;
import org.apache.activemq.artemis.utils.Wait;
@ -56,8 +62,12 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
private static final int SERVER_PORT = AMQP_PORT;
private static final int SERVER_PORT_REMOTE = AMQP_PORT + 1;
private static final int SERVER2_PORT_REMOTE = AMQP_PORT + 2;
private static final int MIN_LARGE_MESSAGE_SIZE = 10 * 1024;
protected ActiveMQServer remoteServer;
protected ActiveMQServer remoteServer2; // Used in two hop tests
@Override
protected String getConfiguredProtocols() {
@ -71,6 +81,11 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
return createServer(SERVER_PORT, false);
}
@Override
protected void configureAMQPAcceptorParameters(Map<String, Object> params) {
params.put("amqpMinLargeMessageSize", MIN_LARGE_MESSAGE_SIZE);
}
@After
@Override
public void tearDown() throws Exception {
@ -79,6 +94,15 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
try {
if (remoteServer != null) {
remoteServer.stop();
remoteServer = null;
}
} catch (Exception e) {
}
try {
if (remoteServer2 != null) {
remoteServer2.stop();
remoteServer2 = null;
}
} catch (Exception e) {
}
@ -140,10 +164,16 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
final MessageProducer producerR = sessionR.createProducer(topic);
final TextMessage message = sessionR.createTextMessage("Hello World");
message.setStringProperty("testProperty", "testValue");
producerR.send(message);
final Message received = consumerL.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Hello World", ((TextMessage) received).getText());
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@ -213,10 +243,16 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
final MessageProducer producerR = sessionR.createProducer(source);
final TextMessage message = sessionR.createTextMessage("Hello World");
message.setStringProperty("testProperty", "testValue");
producerR.send(message);
final Message received = consumerL.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Hello World", ((TextMessage) received).getText());
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@ -275,10 +311,16 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
final MessageProducer producerR = sessionR.createProducer(queue);
final TextMessage message = sessionR.createTextMessage("Hello World");
message.setStringProperty("testProperty", "testValue");
producerR.send(message);
final Message received = consumerL.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Hello World", ((TextMessage) received).getText());
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@ -338,10 +380,16 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
final MessageProducer producerL = sessionL.createProducer(topic);
final TextMessage message = sessionL.createTextMessage("Hello World");
message.setStringProperty("testProperty", "testValue");
producerL.send(message);
final Message received = consumerR.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Hello World", ((TextMessage) received).getText());
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@ -400,10 +448,16 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
final MessageProducer producerL = sessionL.createProducer(queue);
final TextMessage message = sessionL.createTextMessage("Hello World");
message.setStringProperty("testProperty", "testValue");
producerL.send(message);
final Message received = consumerR.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Hello World", ((TextMessage) received).getText());
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@ -475,10 +529,482 @@ public class AMQPFederationServerToServerTest extends AmqpClientTestSupport {
final MessageProducer producerL = sessionL.createProducer(source);
final TextMessage message = sessionL.createTextMessage("Hello World");
message.setStringProperty("testProperty", "testValue");
producerL.send(message);
final Message received = consumerR.receive(5_000);
assertTrue(received instanceof TextMessage);
assertEquals("Hello World", ((TextMessage) received).getText());
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@Test(timeout = 20000)
public void testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemoteAMQP() throws Exception {
// core tunneling shouldn't affect the AMQP message that cross
testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemote("AMQP", true);
}
@Test(timeout = 20000)
public void testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemoteCORENoTunneling() throws Exception {
// core message should be converted to AMQP and back.
testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemote("CORE", false);
}
@Test(timeout = 20000)
public void testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemoteCOREWithTunneling() throws Exception {
// core messages should be tunneled in an AMQP message an then read back
testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemote("CORE", true);
}
private void testAddresDemandOnLocalBrokerFederatesLargeMessagesFromRemote(String clientProtocol, boolean enableCoreTunneling) 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);
localAddressPolicy.addProperty(AmqpSupport.TUNNEL_CORE_MESSAGES, Boolean.toString(enableCoreTunneling));
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;
if (clientProtocol.equals("CORE")) {
factoryRemote = CFUtil.createConnectionFactory(
clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE + "?minLargeMessageSize=" + MIN_LARGE_MESSAGE_SIZE);
} else {
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 BytesMessage message = sessionR.createBytesMessage();
final byte[] bodyBytes = new byte[(int)(MIN_LARGE_MESSAGE_SIZE * 1.5)];
Arrays.fill(bodyBytes, (byte)1);
message.writeBytes(bodyBytes);
message.setStringProperty("testProperty", "testValue");
producerR.send(message);
final Message received = consumerL.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof BytesMessage);
final byte[] receivedBytes = new byte[bodyBytes.length];
final BytesMessage receivedBytesMsg = (BytesMessage) received;
receivedBytesMsg.readBytes(receivedBytes);
assertArrayEquals(bodyBytes, receivedBytes);
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@Test(timeout = 20000)
public void testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemoteAMQP() throws Exception {
// core tunneling shouldn't affect the AMQP message that cross
testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemote("AMQP", true);
}
@Test(timeout = 20000)
public void testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemoteCORENoTunneling() throws Exception {
// core message should be converted to AMQP and back.
testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemote("CORE", false);
}
@Test // (timeout = 20000)
public void testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemoteCOREWithTunneling() throws Exception {
// core messages should be tunneled in an AMQP message an then read back
testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemote("CORE", true);
}
private void testQueueDemandOnLocalBrokerFederatesLargeMessagesFromRemote(String clientProtocol, boolean enableCoreTunneling) throws Exception {
logger.info("Test started: {}", getTestName());
final AMQPFederationQueuePolicyElement localQueuePolicy = new AMQPFederationQueuePolicyElement();
localQueuePolicy.setName("test-policy");
localQueuePolicy.addToIncludes("test", "test");
localQueuePolicy.addProperty(AmqpSupport.TUNNEL_CORE_MESSAGES, Boolean.toString(enableCoreTunneling));
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;
if (clientProtocol.equals("CORE")) {
factoryRemote = CFUtil.createConnectionFactory(
clientProtocol, "tcp://localhost:" + SERVER_PORT_REMOTE + "?minLargeMessageSize=" + MIN_LARGE_MESSAGE_SIZE);
} else {
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 address should trigger receiver on remote.
Wait.assertTrue(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists());
Wait.assertTrue(() -> remoteServer.queueQuery(SimpleString.toSimpleString("test")).isExists());
final MessageProducer producerR = sessionR.createProducer(queue);
final BytesMessage message = sessionR.createBytesMessage();
final byte[] bodyBytes = new byte[(int)(MIN_LARGE_MESSAGE_SIZE * 1.5)];
Arrays.fill(bodyBytes, (byte)1);
message.writeBytes(bodyBytes);
message.setStringProperty("testProperty", "testValue");
producerR.send(message);
final Message received = consumerL.receive(500_000);
assertNotNull(received);
assertTrue(received instanceof BytesMessage);
final byte[] receivedBytes = new byte[bodyBytes.length];
final BytesMessage receivedBytesMsg = (BytesMessage) received;
receivedBytesMsg.readBytes(receivedBytes);
assertArrayEquals(bodyBytes, receivedBytes);
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
}
}
@Test(timeout = 20000)
public void testCoreMessageCrossingAddressWithThreeBrokersWithoutTunneling() throws Exception {
doTestCoreMessageCrossingAddressWithThreeBrokers(false);
}
@Test(timeout = 20000)
public void testCoreMessageCrossingAddressWithThreeBrokersWithTunneling() throws Exception {
doTestCoreMessageCrossingAddressWithThreeBrokers(true);
}
private void doTestCoreMessageCrossingAddressWithThreeBrokers(boolean enableCoreTunneling) throws Exception {
logger.info("Test started: {}", getTestName());
// Create a ring of federated brokers on a target address, messages sent to the address
// on any given broke should traverse the ring size minus one as we never want a loop so
// if the ring is three brokers the max hops should be set to two.
remoteServer2 = createServer(SERVER2_PORT_REMOTE, false);
final String ADDRESS_NAME = "target";
final SimpleString ADDRESS_NAME_SS = SimpleString.toSimpleString(ADDRESS_NAME);
final AMQPFederationAddressPolicyElement localAddressPolicy = new AMQPFederationAddressPolicyElement();
localAddressPolicy.setName("two-hop-policy");
localAddressPolicy.addToIncludes(ADDRESS_NAME);
localAddressPolicy.setAutoDelete(false);
localAddressPolicy.setAutoDeleteDelay(-1L);
localAddressPolicy.setAutoDeleteMessageCount(-1L);
localAddressPolicy.setMaxHops(2);
localAddressPolicy.addProperty(AmqpSupport.TUNNEL_CORE_MESSAGES, Boolean.toString(enableCoreTunneling));
final AMQPFederatedBrokerConnectionElement element = new AMQPFederatedBrokerConnectionElement();
element.setName("hops-test");
element.addLocalAddressPolicy(localAddressPolicy);
final AMQPBrokerConnectConfiguration amqpConnection1 =
new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT_REMOTE);
amqpConnection1.setReconnectAttempts(10);// Limit reconnects
amqpConnection1.setRetryInterval(100);
amqpConnection1.addElement(element);
final AMQPBrokerConnectConfiguration amqpConnection2 =
new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER2_PORT_REMOTE);
amqpConnection2.setReconnectAttempts(10);// Limit reconnects
amqpConnection1.setRetryInterval(100);
amqpConnection2.addElement(element);
final AMQPBrokerConnectConfiguration amqpConnection3 =
new AMQPBrokerConnectConfiguration("test-address-federation", "tcp://localhost:" + SERVER_PORT);
amqpConnection3.setReconnectAttempts(10);// Limit reconnects
amqpConnection1.setRetryInterval(100);
amqpConnection3.addElement(element);
// This is our ring, broker1 -> broker2-> broker3 -> broker1
server.getConfiguration().addAMQPConnection(amqpConnection1);
remoteServer.getConfiguration().addAMQPConnection(amqpConnection2);
remoteServer2.getConfiguration().addAMQPConnection(amqpConnection3);
server.start();
remoteServer.start();
remoteServer2.start();
final ConnectionFactory factory1 = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + SERVER_PORT);
final ConnectionFactory factory2 = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + SERVER_PORT_REMOTE);
final ConnectionFactory factory3 = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + SERVER2_PORT_REMOTE);
try (Connection connection1 = factory1.createConnection();
Connection connection2 = factory2.createConnection();
Connection connection3 = factory3.createConnection()) {
final Session session1 = connection1.createSession(Session.AUTO_ACKNOWLEDGE);
final Session session2 = connection2.createSession(Session.AUTO_ACKNOWLEDGE);
final Session session3 = connection3.createSession(Session.AUTO_ACKNOWLEDGE);
final Topic topic = session1.createTopic(ADDRESS_NAME);
final MessageConsumer consumer1 = session1.createConsumer(topic);
final MessageConsumer consumer2 = session2.createConsumer(topic);
final MessageConsumer consumer3 = session3.createConsumer(topic);
final MessageProducer producer1 = session1.createProducer(topic);
final MessageProducer producer2 = session2.createProducer(topic);
final MessageProducer producer3 = session3.createProducer(topic);
final TextMessage message1 = session1.createTextMessage("Message1");
message1.setStringProperty("test", "1");
final TextMessage message2 = session2.createTextMessage("Message2");
message2.setStringProperty("test", "2");
final TextMessage message3 = session3.createTextMessage("Message3");
message3.setStringProperty("test", "3");
connection1.start();
connection2.start();
connection3.start();
// Demand on local address should trigger receiver on remote.
Wait.assertTrue(() -> server.bindingQuery(ADDRESS_NAME_SS).getQueueNames().size() == 2);
Wait.assertTrue(() -> remoteServer.bindingQuery(ADDRESS_NAME_SS).getQueueNames().size() == 2);
Wait.assertTrue(() -> remoteServer2.bindingQuery(ADDRESS_NAME_SS).getQueueNames().size() == 2);
// Sent from 1 should hit all three then stop
producer1.send(message1);
Message received = consumer1.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message1", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("1", received.getStringProperty("test"));
received = consumer2.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message1", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("1", received.getStringProperty("test"));
received = consumer3.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message1", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("1", received.getStringProperty("test"));
assertNull(consumer1.receive(100));
assertNull(consumer2.receive(100));
assertNull(consumer3.receive(100));
// Sent from 1 should hit all three then stop
producer2.send(message2);
received = consumer1.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message2", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("2", received.getStringProperty("test"));
received = consumer2.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message2", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("2", received.getStringProperty("test"));
received = consumer3.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message2", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("2", received.getStringProperty("test"));
assertNull(consumer1.receiveNoWait());
assertNull(consumer2.receiveNoWait());
assertNull(consumer3.receiveNoWait());
// Sent from 1 should hit all three then stop
producer3.send(message3);
received = consumer1.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message3", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("3", received.getStringProperty("test"));
received = consumer2.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message3", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("3", received.getStringProperty("test"));
received = consumer3.receive(2_000);
assertNotNull(received);
assertTrue(received instanceof TextMessage);
assertEquals("Message3", ((TextMessage) received).getText());
assertTrue(received.propertyExists("test"));
assertEquals("3", received.getStringProperty("test"));
assertNull(consumer1.receiveNoWait());
assertNull(consumer2.receiveNoWait());
assertNull(consumer3.receiveNoWait());
}
}
@Test(timeout = 20000)
public void testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient() throws Exception {
testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient("CORE", "AMQP", false); // Tunneling doesn't matter here
}
@Test(timeout = 20000)
public void testCoreConsumerDemandOnLocalBrokerFederatesMessageFromCoreClientTunneled() throws Exception {
testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient("CORE", "CORE", true);
}
@Test(timeout = 20000)
public void testCoreConsumerDemandOnLocalBrokerFederatesMessageFromCoreClientUnTunneled() throws Exception {
testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient("CORE", "CORE", false);
}
@Test(timeout = 20000)
public void testAMQPConsumerDemandOnLocalBrokerFederatesMessageFromCoreClientTunneled() throws Exception {
testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient("AMQP", "CORE", true);
}
@Test(timeout = 20000)
public void testAMQPConsumerDemandOnLocalBrokerFederatesMessageFromCoreClientNotTunneled() throws Exception {
testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient("AMQP", "CORE", false);
}
private void testCoreConsumerDemandOnLocalBrokerFederatesMessageFromAMQPClient(String localProtocol,
String remoteProtocol,
boolean enableCoreTunneling) throws Exception {
logger.info("Test started: {}", getTestName());
final AMQPFederationQueuePolicyElement localQueuePolicy = new AMQPFederationQueuePolicyElement();
localQueuePolicy.setName("test-policy");
localQueuePolicy.addToIncludes("test", "test");
localQueuePolicy.addProperty(AmqpSupport.TUNNEL_CORE_MESSAGES, Boolean.toString(enableCoreTunneling));
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(localProtocol, "tcp://localhost:" + SERVER_PORT);
final ConnectionFactory factoryRemote = CFUtil.createConnectionFactory(remoteProtocol, "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 MessageConsumer consumerL = sessionL.createConsumer(sessionL.createQueue("test"));
connectionL.start();
connectionR.start();
// Demand on local address should trigger receiver on remote.
Wait.assertTrue(() -> server.queueQuery(SimpleString.toSimpleString("test")).isExists());
Wait.assertTrue(() -> remoteServer.queueQuery(SimpleString.toSimpleString("test")).isExists());
final MessageProducer producerR = sessionR.createProducer(sessionR.createQueue("test"));
final BytesMessage message = sessionR.createBytesMessage();
final byte[] bodyBytes = new byte[(int)(MIN_LARGE_MESSAGE_SIZE * 1.5)];
Arrays.fill(bodyBytes, (byte)1);
message.writeBytes(bodyBytes);
message.setStringProperty("testProperty", "testValue");
message.setIntProperty("testIntProperty", 42);
message.setJMSCorrelationID("myCorrelationId");
message.setJMSReplyTo(sessionR.createTopic("reply-topic"));
producerR.setDeliveryMode(DeliveryMode.PERSISTENT);
producerR.send(message);
final Message received = consumerL.receive(5_000);
assertNotNull(received);
assertTrue(received instanceof BytesMessage);
final byte[] receivedBytes = new byte[bodyBytes.length];
final BytesMessage receivedBytesMsg = (BytesMessage) received;
receivedBytesMsg.readBytes(receivedBytes);
assertArrayEquals(bodyBytes, receivedBytes);
assertTrue(message.propertyExists("testProperty"));
assertEquals("testValue", received.getStringProperty("testProperty"));
assertTrue(message.propertyExists("testIntProperty"));
assertEquals(42, received.getIntProperty("testIntProperty"));
assertEquals("myCorrelationId", received.getJMSCorrelationID());
assertEquals("reply-topic", ((Topic) received.getJMSReplyTo()).getTopicName());
assertEquals(DeliveryMode.PERSISTENT, received.getJMSDeliveryMode());
}
}
}

View File

@ -17,6 +17,9 @@
package org.apache.activemq.artemis.tests.integration.amqp.connect;
import static org.apache.activemq.artemis.protocol.amqp.proton.AMQPTunneledMessageConstants.AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
import static org.apache.activemq.artemis.protocol.amqp.proton.AmqpSupport.TUNNEL_CORE_MESSAGES;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.util.HashMap;
@ -25,8 +28,12 @@ import java.util.concurrent.TimeUnit;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
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;
@ -35,6 +42,7 @@ import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPMirror
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.protocol.amqp.proton.AmqpSupport;
import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport;
import org.apache.activemq.artemis.tests.util.CFUtil;
import org.apache.qpid.protonj2.test.driver.ProtonTestServer;
@ -59,6 +67,11 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
return createServer(BROKER_PORT_NUM, false);
}
@Override
protected String getConfiguredProtocols() {
return "AMQP,CORE";
}
@Test(timeout = 20000)
public void testBrokerMirrorConnectsWithAnonymous() throws Exception {
final Map<String, Object> brokerProperties = new HashMap<>();
@ -70,9 +83,9 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
peer.expectBegin().respond();
peer.expectAttach().ofSender()
.withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR"))
.withDesiredCapabilities("amq.mirror")
.withDesiredCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.respond()
.withOfferedCapabilities("amq.mirror")
.withOfferedCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withPropertiesMap(brokerProperties);
peer.start();
@ -104,9 +117,9 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
peer.expectBegin().respond();
peer.expectAttach().ofSender()
.withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR"))
.withDesiredCapabilities("amq.mirror")
.withDesiredCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.respond()
.withOfferedCapabilities("amq.mirror")
.withOfferedCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withPropertiesMap(brokerProperties);
peer.start();
@ -136,7 +149,7 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
peer.expectBegin().respond();
peer.expectAttach().ofSender()
.withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR"))
.withDesiredCapabilities("amq.mirror")
.withDesiredCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.respond();
peer.start();
@ -168,9 +181,9 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
peer.expectBegin().respond();
peer.expectAttach().ofSender()
.withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR"))
.withDesiredCapabilities("amq.mirror")
.withDesiredCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.respond()
.withOfferedCapabilities("amq.mirror")
.withOfferedCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withPropertiesMap(brokerProperties);
peer.remoteFlow().withLinkCredit(10).queue();
peer.expectTransfer().accept(); // Address create
@ -210,9 +223,9 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
peer.expectBegin().respond();
peer.expectAttach().ofSender()
.withName(Matchers.startsWith("$ACTIVEMQ_ARTEMIS_MIRROR"))
.withDesiredCapabilities("amq.mirror")
.withDesiredCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.respond()
.withOfferedCapabilities("amq.mirror")
.withOfferedCapabilities("amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withPropertiesMap(brokerProperties);
peer.remoteFlow().withLinkCredit(10).queue();
peer.expectTransfer().accept(); // Notification address create
@ -248,4 +261,268 @@ public class AMQPMirrorConnectionTest extends AmqpClientTestSupport {
server.stop();
}
}
@Test(timeout = 20000)
public void testBrokerMirrorHonorsCoreTunnelingEnable() throws Exception {
testBrokerMirrorHonorsCoreTunnelingEnableOrDisable(true);
}
@Test(timeout = 20000)
public void testBrokerMirrorHonorsCoreTunnelingDisable() throws Exception {
testBrokerMirrorHonorsCoreTunnelingEnableOrDisable(false);
}
public void testBrokerMirrorHonorsCoreTunnelingEnableOrDisable(boolean tunneling) throws Exception {
final Map<String, Object> brokerProperties = new HashMap<>();
brokerProperties.put(AMQPMirrorControllerSource.BROKER_ID.toString(), "Test-Broker");
final String[] capabilities;
if (tunneling) {
capabilities = new String[] {"amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString()};
} else {
capabilities = new String[] {"amq.mirror"};
}
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(capabilities)
.respond()
.withOfferedCapabilities(capabilities)
.withPropertiesMap(brokerProperties);
peer.start();
final URI remoteURI = peer.getServerURI();
logger.info("Connect test started, peer listening on: {}", remoteURI);
AMQPMirrorBrokerConnectionElement mirrorElement = new AMQPMirrorBrokerConnectionElement();
mirrorElement.addProperty(TUNNEL_CORE_MESSAGES, Boolean.toString(tunneling));
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser("user");
amqpConnection.setPassword("pass");
amqpConnection.addElement(mirrorElement);
server.getConfiguration().addAMQPConnection(amqpConnection);
server.start();
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
server.stop();
}
}
@Test(timeout = 20000)
public void testProducerMessageIsMirroredWithCoreTunnelingUsesCoreMessageFormat() throws Exception {
doTestProducerMessageIsMirroredWithCorrectMessageFormat(true);
}
@Test(timeout = 20000)
public void testProducerMessageIsMirroredWithoutCoreTunnelingUsesDefaultMessageFormat() throws Exception {
doTestProducerMessageIsMirroredWithCorrectMessageFormat(false);
}
private void doTestProducerMessageIsMirroredWithCorrectMessageFormat(boolean tunneling) throws Exception {
final Map<String, Object> brokerProperties = new HashMap<>();
brokerProperties.put(AMQPMirrorControllerSource.BROKER_ID.toString(), "Test-Broker");
final String[] capabilities;
final int messageFormat;
if (tunneling) {
capabilities = new String[] {"amq.mirror", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString()};
messageFormat = AMQP_TUNNELED_CORE_MESSAGE_FORMAT;
} else {
capabilities = new String[] {"amq.mirror"};
messageFormat = 0; // AMQP default
}
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(capabilities)
.respond()
.withOfferedCapabilities(capabilities)
.withPropertiesMap(brokerProperties);
peer.remoteFlow().withLinkCredit(10).queue();
peer.expectTransfer().accept(); // Notification address create
peer.expectTransfer().accept(); // Address create
peer.expectTransfer().accept(); // Queue create
peer.expectTransfer().withMessageFormat(messageFormat).accept(); // Producer Message
peer.start();
final URI remoteURI = peer.getServerURI();
logger.info("Connect test started, peer listening on: {}", remoteURI);
AMQPMirrorBrokerConnectionElement mirrorElement = new AMQPMirrorBrokerConnectionElement();
mirrorElement.addProperty(TUNNEL_CORE_MESSAGES, Boolean.toString(tunneling));
mirrorElement.setQueueCreation(true);
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser("user");
amqpConnection.setPassword("pass");
amqpConnection.addElement(mirrorElement);
server.createQueue(new QueueConfiguration("myQueue").setDurable(true));
server.getConfiguration().addAMQPConnection(amqpConnection);
server.start();
final ConnectionFactory factory = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + BROKER_PORT_NUM);
try (Connection connection = factory.createConnection()) {
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
Queue queue = session.createQueue("myQueue");
MessageConsumer consumer = session.createConsumer(queue);
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage("test");
connection.start();
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
producer.send(message);
consumer.close();
}
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
server.stop();
}
}
@Test(timeout = 20000)
public void testRemoteDoesNotOfferTunnelingResultsInDefaultAMQPFormattedMessages() 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", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.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.expectTransfer().withMessageFormat(0).accept(); // Producer Message
peer.start();
final URI remoteURI = peer.getServerURI();
logger.info("Connect test started, peer listening on: {}", remoteURI);
AMQPMirrorBrokerConnectionElement mirrorElement = new AMQPMirrorBrokerConnectionElement();
mirrorElement.addProperty(TUNNEL_CORE_MESSAGES, Boolean.toString(true));
mirrorElement.setQueueCreation(true);
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser("user");
amqpConnection.setPassword("pass");
amqpConnection.addElement(mirrorElement);
server.createQueue(new QueueConfiguration("myQueue").setDurable(true));
server.getConfiguration().addAMQPConnection(amqpConnection);
server.start();
final ConnectionFactory factory = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + BROKER_PORT_NUM);
try (Connection connection = factory.createConnection()) {
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
Queue queue = session.createQueue("myQueue");
MessageConsumer consumer = session.createConsumer(queue);
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage("test");
connection.start();
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
producer.send(message);
consumer.close();
}
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
server.stop();
}
}
@Test(timeout = 20000)
public void testTunnelingDisabledButRemoteOffersDoesNotUseTunneling() 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", AmqpSupport.CORE_MESSAGE_TUNNELING_SUPPORT.toString())
.withPropertiesMap(brokerProperties);
peer.remoteFlow().withLinkCredit(10).queue();
peer.expectTransfer().accept(); // Notification address create
peer.expectTransfer().accept(); // Address create
peer.expectTransfer().accept(); // Queue create
peer.expectTransfer().withMessageFormat(0).accept(); // Producer Message
peer.start();
final URI remoteURI = peer.getServerURI();
logger.info("Connect test started, peer listening on: {}", remoteURI);
AMQPMirrorBrokerConnectionElement mirrorElement = new AMQPMirrorBrokerConnectionElement();
mirrorElement.addProperty(TUNNEL_CORE_MESSAGES, Boolean.toString(false));
mirrorElement.setQueueCreation(true);
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://" + remoteURI.getHost() + ":" + remoteURI.getPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser("user");
amqpConnection.setPassword("pass");
amqpConnection.addElement(mirrorElement);
server.createQueue(new QueueConfiguration("myQueue").setDurable(true));
server.getConfiguration().addAMQPConnection(amqpConnection);
server.start();
final ConnectionFactory factory = CFUtil.createConnectionFactory("CORE", "tcp://localhost:" + BROKER_PORT_NUM);
try (Connection connection = factory.createConnection()) {
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
Queue queue = session.createQueue("myQueue");
MessageConsumer consumer = session.createConsumer(queue);
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage("test");
connection.start();
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
producer.send(message);
consumer.close();
}
peer.waitForScriptToComplete(5, TimeUnit.SECONDS);
server.stop();
}
}
}

View File

@ -69,6 +69,11 @@ public class AMQPMirrorFastACKTest extends AmqpClientTestSupport {
private ActiveMQServer slowServer;
@Override
protected String getConfiguredProtocols() {
return "AMQP,CORE,OPENWIRE";
}
@Override
@Before
public void setUp() throws Exception {
@ -89,7 +94,17 @@ public class AMQPMirrorFastACKTest extends AmqpClientTestSupport {
}
@Test
public void testMirrorTargetFastACK() throws Exception {
public void testMirrorTargetFastACKCore() throws Exception {
doTestMirrorTargetFastACK("CORE");
}
@Test
public void testMirrorTargetFastACKAMQP() throws Exception {
doTestMirrorTargetFastACK("AMQP");
}
private void doTestMirrorTargetFastACK(String protocol) throws Exception {
final int NUMBER_OF_MESSAGES = 10;
CountDownLatch done = new CountDownLatch(NUMBER_OF_MESSAGES);
@ -104,12 +119,13 @@ public class AMQPMirrorFastACKTest extends AmqpClientTestSupport {
server.addAddressInfo(new AddressInfo(getQueueName()).addRoutingType(RoutingType.ANYCAST).setAutoCreated(false));
server.createQueue(new QueueConfiguration(getQueueName()).setRoutingType(RoutingType.ANYCAST).setAddress(getQueueName()).setAutoCreated(false));
ConnectionFactory factory = CFUtil.createConnectionFactory("AMQP", "tcp://localhost:" + AMQP_PORT);
ConnectionFactory factory = CFUtil.createConnectionFactory(protocol, "tcp://localhost:" + AMQP_PORT);
try (Connection connection = factory.createConnection()) {
Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(session.createQueue(getQueueName()));
MessageProducer producer = session.createProducer(session.createQueue(getQueueName()));
Session consumerSession = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageConsumer consumer = consumerSession.createConsumer(consumerSession.createQueue(getQueueName()));
Session producerSession = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
MessageProducer producer = producerSession.createProducer(producerSession.createQueue(getQueueName()));
connection.start();
@ -128,7 +144,7 @@ public class AMQPMirrorFastACKTest extends AmqpClientTestSupport {
producer.setDeliveryMode(DeliveryMode.PERSISTENT);
for (int i = 0; i < NUMBER_OF_MESSAGES; i++) {
producer.send(session.createTextMessage("i=" + i));
producer.send(producerSession.createTextMessage("i=" + i));
}
Assert.assertTrue(done.await(5000, TimeUnit.MILLISECONDS));

View File

@ -98,7 +98,6 @@ public class AMQPSyncMirrorTest extends AmqpClientTestSupport {
testPersistedSend("AMQP", false, 200 * 1024);
}
@Test
public void testPersistedSendCore() throws Exception {
testPersistedSend("CORE", false, 100);
@ -173,7 +172,8 @@ public class AMQPSyncMirrorTest extends AmqpClientTestSupport {
logger.warn(e.getMessage(), e);
}
}
if (recordType == JournalRecordIds.ADD_MESSAGE_PROTOCOL) {
if (recordType == JournalRecordIds.ADD_MESSAGE_PROTOCOL ||
recordType == JournalRecordIds.ADD_LARGE_MESSAGE) {
try {
countStored.incrementAndGet();
if (!transactional) {

View File

@ -67,7 +67,7 @@ public class ConfigurationValidationTest extends ActiveMQTestBase {
deploymentManager.addDeployable(fc);
deploymentManager.readConfiguration();
Assert.assertEquals(4, fc.getAMQPConnection().size());
Assert.assertEquals(5, fc.getAMQPConnection().size());
AMQPBrokerConnectConfiguration amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(0);
Assert.assertEquals("testuser", amqpBrokerConnectConfiguration.getUser());
@ -198,6 +198,15 @@ public class ConfigurationValidationTest extends ActiveMQTestBase {
Assert.assertTrue(p.getProperties().containsKey("amqpLowCredits"));
Assert.assertEquals("1", p.getProperties().get("amqpLowCredits"));
});
amqpBrokerConnectConfiguration = fc.getAMQPConnection().get(4);
Assert.assertEquals("test-property", amqpBrokerConnectConfiguration.getName());
Assert.assertFalse(amqpBrokerConnectConfiguration.isAutostart());
Assert.assertNotNull(amqpBrokerConnectConfiguration.getConnectionElements().get(0));
mirrorConnectionElement = (AMQPMirrorBrokerConnectionElement) amqpBrokerConnectConfiguration.getConnectionElements().get(0);
Assert.assertNotNull(mirrorConnectionElement.getProperties());
Assert.assertFalse(mirrorConnectionElement.getProperties().isEmpty());
Assert.assertFalse(Boolean.valueOf((String) mirrorConnectionElement.getProperties().get("tunnel-core-messages")));
}
@Test