All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.mats3.test.MatsTestBrokerInterface Maven / Gradle / Ivy

Go to download

Mats^3 Testing tools, as well as the base for specific JUnit or Jupiter (Junit 5) tools.

The newest version!
package io.mats3.test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.Queue;
import javax.jms.Session;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.mats3.MatsFactory;
import io.mats3.MatsFactory.MatsFactoryWrapper;
import io.mats3.impl.jms.JmsMatsFactory;
import io.mats3.serial.MatsSerializer;
import io.mats3.serial.MatsTrace;
import io.mats3.serial.MatsTrace.Call;
import io.mats3.serial.MatsTrace.StackState;

/**
 * Tool that makes it possible to query the underlying broker of a test MatsFactory for messages, in particular
 * "residual" messages and DLQs. DLQ-fetching is useful if the test is designed to fail a stage, i.e. that a stage under
 * test raises some {@link RuntimeException}, or the special MatsRefuseMessageException due to some
 * internal validation performed.
 * 

* Usage: If employing the Mats test tools, one of these should have been created for you. For the JUnit * Rule_Mats, and JUnit Jupiter Extension_Mats, there's a getter directly on the * rule/extension instance. For Spring, employing the @MatsTestContext or corresponding * MatsTestInfrastructureConfiguration, an instance is put into the Spring context for you. If you do not * use those tools, you will have to create a instance of this class as a Spring bean yourself - either directly by * using the {@link #create(ConnectionFactory, MatsFactory) create(..)} methods taking parameters, or indirectly by * using the {@link #createForLaterPopulation()} variant, and rely on the SpringJmsMatsFactoryWrapper * finding it from the Spring context and populating it for you. */ public interface MatsTestBrokerInterface { /** * If you have both the JMS ConnectionFactory and a JmsMatsFactory available from start, then you can create a * working instance right away. Otherwise, typically in a Spring bean context, check out * {@link #_latePopulate(ConnectionFactory, MatsFactory)}. Note that the MatsFactory parameter currently needs to be * a {@link JmsMatsFactory} - read more at _latePopulate(). */ static MatsTestBrokerInterface create(ConnectionFactory connectionFactory, MatsFactory matsFactory) { return createForLaterPopulation()._latePopulate(connectionFactory, matsFactory); } /** * Special factory variant, where the needed parameters must be supplied by * {@link #_latePopulate(ConnectionFactory, MatsFactory)}, which typically will be handled by the * SpringJmsMatsFactoryWrapper. * * @return an empty, not still ready instance. */ static MatsTestBrokerInterface createForLaterPopulation() { return new MatsTestBrokerInterface_JmsMatsFactory(); } /** * Waits a couple of seconds for a message to appear on the Dead Letter Queue for the provided endpoint- or stageId * (Queue name "DLQ."+matsendpointPrefix+endpointOrStageId) - and also checks the standard common ActiveMQ DLQ * (Queue name "ActiveMQ.DLQ") in case the broker is not configured with specific DLQs per Queue, which is relevant * if you fire up an unmodified ActiveMQ distribution on the command line, check the {@code MatsTestBroker} class * JavaDoc for how to use an external MQ instead of the in-VM which otherwise is fired up. (NOTE: It is HIGHLY * suggested to use the "specific DLQ" pattern in any production setting, as this is much easier to reason about * when ugly things starts hitting fans). * * @param endpointOrStageId * the endpoint which is expected to generate a DLQ message. * @return the {@link MatsTrace} of the DLQ'ed message. */ MatsMessageRepresentation getDlqMessage(String endpointOrStageId); /** * This method is most probably not for you!. It is employed by SpringJmsMatsFactoryWrapper, by * reflection invocation when it is both on classpath and as an instance in the Spring context, to perform "late * setting" of the properties which the tool needs to perform its job. (Reason for reflection: This class resides in * the 'mats-test' project - which is for testing, while the SpringJmsMatsFactoryWrapper is in the * 'mats-spring-jms' project - which is for "production"). *

* The matsFactory parameter currently needs to be a JmsMatsFactory. We need all of the following from it *

    *
  • matsFactory.getFactoryConfig().getMatsDestinationPrefix() (standard MatsFactory)
  • *
  • matsFactory.getFactoryConfig().getMatsTraceKey() (standard MatsFactory)
  • *
  • The {@link MatsSerializer} to provide access to contents from messages (from JmsMatsFactory)
  • *
*

* Note: The MatsFactory provided may be a {@link MatsFactoryWrapper}, but it must resolve to a * {@link JmsMatsFactory} via the {@link MatsFactory#unwrapFully()}! Otherwise, it'll throw an * {@link IllegalArgumentException}. * * @return this */ MatsTestBrokerInterface _latePopulate(ConnectionFactory connectionFactory, MatsFactory matsFactory); /** * Representation of the Mats message that sat on the DLQ. */ interface MatsMessageRepresentation { /** * @return the TraceId this message has as being a part of a "call flow" that was initiated with a TraceId. */ String getTraceId(); /** * The message DTO that was provided to the Mats Endpoint which DLQed the message. * * @param type * the type of the message * @param * the type of the message * @return the deserialized DTO. */ I getIncomingMessage(Class type); /** * The state DTO that was provided to the Mats Endpoint which DLQed the message. * * @param type * the type of the state * @param * the type of the state * @return the deserialized STO. */ S getIncomingState(Class type); /** * @return the Mats MessageId of this message. */ String getMatsMessageId(); /** * @return who this message was from, i.e. the stage or initiator that sent the message. */ String getFrom(); /** * @return who this message was for, which obviously should be the value of the endpointId that * you requested in the call to {@link MatsTestBrokerInterface#getDlqMessage(String)}! */ String getTo(); } class MatsTestBrokerInterface_JmsMatsFactory implements MatsTestBrokerInterface { private static final Logger log = LoggerFactory.getLogger(MatsTestBrokerInterface.class); private ConnectionFactory _connectionFactory; private MatsSerializer _matsSerializer; private String _matsDestinationPrefix; private String _matsTraceKey; private MatsTestBrokerInterface_JmsMatsFactory() { /* must be filled later */ } private MatsTestBrokerInterface_JmsMatsFactory(ConnectionFactory connectionFactory, MatsSerializer matsSerializer, String matsDestinationPrefix, String matsTraceKey) { _connectionFactory = connectionFactory; _matsSerializer = matsSerializer; _matsDestinationPrefix = matsDestinationPrefix; _matsTraceKey = matsTraceKey; } public MatsTestBrokerInterface_JmsMatsFactory _latePopulate(ConnectionFactory connectionFactory, MatsFactory matsFactory) { _connectionFactory = connectionFactory; _matsDestinationPrefix = matsFactory.getFactoryConfig().getMatsDestinationPrefix(); _matsTraceKey = matsFactory.getFactoryConfig().getMatsTraceKey(); MatsFactory unwrappedMatsFactory = matsFactory.unwrapFully(); if (!(unwrappedMatsFactory instanceof JmsMatsFactory)) { throw new IllegalArgumentException("The _latePopuplate method was invoked with a MatsFactory, which" + " when 'unwrapFully()' did not give a JmsMatsFactory. Sorry, no can do."); } JmsMatsFactory jmsMatsFactory = (JmsMatsFactory) unwrappedMatsFactory; _matsSerializer = jmsMatsFactory.getMatsSerializer(); return this; } protected void checkCorrectSetup() { if (_connectionFactory == null) { throw new IllegalStateException("Missing _connectionFactory, _latePopulate(..) not run."); } if (_matsSerializer == null) { throw new IllegalStateException("Missing _matsSerializer, _latePopulate(..) not run."); } if (_matsDestinationPrefix == null) { throw new IllegalStateException("Missing _matsDestinationPrefix, _latePopulate(..) not run."); } if (_matsTraceKey == null) { throw new IllegalStateException("Missing _matsTraceKey, _latePopulate(..) not run."); } } /** * Waits a couple of seconds for a message to appear on the Dead Letter Queue for the provided endpoint- or * stageId (Queue name "DLQ."+matsendpointPrefix+endpointOrStageId) - and also checks the standard common * ActiveMQ DLQ (Queue name "ActiveMQ.DLQ") in case the broker is not configured with specific DLQs per Queue, * which is relevant if you fire up an unmodified ActiveMQ distribution on the command line, check the * {@code MatsTestBroker} class JavaDoc for how to use an external MQ instead of the in-VM which otherwise is * fired up. (NOTE: It is HIGHLY suggested to use the "specific DLQ" pattern in any production setting, as this * is much easier to reason about when ugly things starts hitting fans). * * @param endpointOrStageId * the endpoint which is expected to generate a DLQ message. * @return the {@link MatsTrace} of the DLQ'ed message. */ public MatsMessageRepresentation getDlqMessage(String endpointOrStageId) { checkCorrectSetup(); String specificDlqName = "DLQ." + _matsDestinationPrefix + endpointOrStageId; String activeMqStandardDlqName = "ActiveMQ.DLQ"; String artemisMqStandardDlqName = "DLQ"; // Note: Evidently, ActiveMq sets the JMSDestination to the original destination when msg is on DLQ String activeMqStandardDlqSelector = "JMSDestination = 'queue://" + _matsDestinationPrefix + endpointOrStageId + "'"; // Artemis has a special property for the original queue of a DLQ'ed message String artemisMqStandardDlqSelector = "_AMQ_ORIG_QUEUE = '" + _matsDestinationPrefix + endpointOrStageId + "'"; log.debug("getDlqMessage(endpointOrStageId:\"" + endpointOrStageId + "\")"); try { Connection jmsConnection = _connectionFactory.createConnection(); try { jmsConnection.start(); int maxWaitMillis = 30_000; // We'll wait for threads to get into receive before we close connection, even if one of the // threads get the DLQ right away, otherwise there's risk of javax.jms.IllegalStateException: // session closed. CountDownLatch threadsReceiving = new CountDownLatch(3); // The latch to trigger when one of the threads finds a DLQ. CountDownLatch latch = new CountDownLatch(1); Message[] lambdaHackMessage = new Message[1]; // :: Fire up threads, which each listens for the dead letter from two different queues // Specific DLQ, i.e. "DLQ.". new Thread(() -> { try { Session jmsSession = jmsConnection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue specificDlq = jmsSession.createQueue(specificDlqName); MessageConsumer specificDlqConsumer = jmsSession.createConsumer(specificDlq); log.debug("Listening for DLQ message on Specific DLQ [" + specificDlq + "](" + specificDlq.getClass().getName() + ") for max [" + maxWaitMillis + "] millis."); threadsReceiving.countDown(); Message msg = specificDlqConsumer.receive(maxWaitMillis); if (msg != null) { log.info("Found DLQ on Specific DLQ [" + specificDlqName + "]!"); lambdaHackMessage[0] = msg; } } catch (JMSException e) { log.warn("Got a JMSException when trying to receive message on" + " Specific DLQ queue [" + specificDlqName + "].", e); } finally { log.debug("Exiting: DLQ consumer thread for Specific DLQ [" + specificDlqName + "]."); latch.countDown(); } }, this.getClass().getSimpleName() + "-DlqConsumerThread:SpecificDlq:" + specificDlqName) .start(); // ActiveMQ's standard common DLQ, i.e. "ActiveMQ.DLQ". new Thread(() -> { try { Session jmsSession = jmsConnection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue activeMqCommonDlq = jmsSession.createQueue(activeMqStandardDlqName); MessageConsumer activeMqStandardDlqConsumer = jmsSession.createConsumer(activeMqCommonDlq, activeMqStandardDlqSelector); log.debug("Listening for DLQ message on ActiveMQ's common DLQ [" + activeMqCommonDlq + "](" + activeMqCommonDlq.getClass().getName() + ") for max [" + maxWaitMillis + "] millis."); threadsReceiving.countDown(); Message msg = activeMqStandardDlqConsumer.receive(maxWaitMillis); if (msg != null) { log.info("Found DLQ on ActiveMQ's Generic DLQ [" + activeMqCommonDlq + "]!"); lambdaHackMessage[0] = msg; } } catch (JMSException e) { log.warn("Got a JMSException when trying to receive message on" + " ActiveMQ common DLQ queue [" + activeMqStandardDlqName + "].", e); } finally { log.debug("Exiting: DLQ consumer thread for ActiveMQ common DLQ [" + activeMqStandardDlqName + "]."); latch.countDown(); } }, this.getClass().getSimpleName() + "-DlqConsumerThread:ActiveMqCommonDlq:" + activeMqStandardDlqName) .start(); // Artemis's standard common DLQ, i.e. "DLQ". new Thread(() -> { try { Session jmsSession = jmsConnection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue artemisMqCommonDlq = jmsSession.createQueue(artemisMqStandardDlqName); MessageConsumer artemisMqStandardDlqConsumer = jmsSession.createConsumer(artemisMqCommonDlq, artemisMqStandardDlqSelector); log.debug("Listening for DLQ message on Artemis's common DLQ [" + artemisMqCommonDlq + "](" + artemisMqCommonDlq.getClass().getName() + ") for max [" + maxWaitMillis + "] millis."); threadsReceiving.countDown(); Message msg = artemisMqStandardDlqConsumer.receive(maxWaitMillis); if (msg != null) { log.info("Found DLQ on Artemis's common DLQ [" + artemisMqCommonDlq + "]!"); lambdaHackMessage[0] = msg; } } catch (JMSException e) { log.warn("Got a JMSException when trying to receive message on" + " Artemis common DLQ queue [" + artemisMqStandardDlqName + "].", e); } finally { log.debug("Exiting: DLQ consumer thread for Artemis common DLQ [" + artemisMqStandardDlqName + "]."); latch.countDown(); } }, this.getClass().getSimpleName() + "-DlqConsumerThread:ArtemisCommonDlq:" + artemisMqStandardDlqName) .start(); try { threadsReceiving.await(1000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { throw new IllegalStateException("Got interrupted while waiting for threads" + " to enter receive.", e); } try { latch.await(maxWaitMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { throw new IllegalStateException("Got interrupted while waiting for DLQ listening threads" + " to receive a DLQ message.", e); } Message msg = lambdaHackMessage[0]; if (msg == null) { throw new AssertionError("Did not get a message on either of the queues [" + specificDlqName + "], [" + activeMqStandardDlqName + "] or [" + artemisMqStandardDlqName + "] within " + maxWaitMillis + "ms."); } if (!(msg instanceof MapMessage)) { throw new AssertionError("The message gotten from DLQ is not a MapMessage!"); } MapMessage matsMM = (MapMessage) msg; byte[] matsTraceBytes = matsMM.getBytes(_matsTraceKey); String matsTraceMeta = matsMM.getString(_matsTraceKey + ":meta"); if (matsTraceBytes == null) { throw new AssertionError("Missing MatsTrace bytes on the DLQ JMS Message!"); } if (matsTraceMeta == null) { throw new AssertionError("Missing MatsTrace \"meta\" on the DLQ JMS Message!"); } log.debug("Length of byte serialized&compressed MatsTrace: " + matsTraceBytes.length); return genericsHack(matsTraceBytes, matsTraceMeta); } finally { jmsConnection.close(); // Closes both sessions and consumers } } catch (JMSException e) { throw new AssertionError("Got a JMSException when trying to receive Mats message on [" + specificDlqName + "] or [" + activeMqStandardDlqName + "].", e); } } /** * Just a way to "fix" the '?' of MatsSerializer (the Z type), which don't really matter here, so do not want to * expose it for consumers of the tool. */ @SuppressWarnings("unchecked") private MatsMessageRepresentation genericsHack(byte[] matsTraceBytes, String matsTraceMeta) throws JMSException { MatsSerializer matsSerializer = (MatsSerializer) _matsSerializer; MatsTrace matsTrace = matsSerializer.deserializeMatsTrace(matsTraceBytes, matsTraceMeta).getMatsTrace(); return new MatsMessageRepresentationImpl(matsSerializer, matsTrace); } private static class MatsMessageRepresentationImpl implements MatsMessageRepresentation { private final MatsSerializer _matsSerializer; private final MatsTrace _matsTrace; public MatsMessageRepresentationImpl(MatsSerializer matsSerializer, MatsTrace matsTrace) { _matsSerializer = matsSerializer; _matsTrace = matsTrace; } @Override public String getTraceId() { return _matsTrace.getTraceId(); } @Override public I getIncomingMessage(Class type) { Call currentCall = _matsTrace.getCurrentCall(); return _matsSerializer.deserializeObject(currentCall.getData(), type); } @Override public S getIncomingState(Class type) { return _matsTrace.getCurrentState() .map(StackState::getState) .map(z -> _matsSerializer.deserializeObject(z, type)) .orElse(null); } @Override public String getMatsMessageId() { return _matsTrace.getCurrentCall().getMatsMessageId(); } @Override public String getFrom() { return _matsTrace.getCurrentCall().getFrom(); } @Override public String getTo() { return _matsTrace.getCurrentCall().getTo().getId(); } } } }