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

com.rabbitmq.jms.client.RMQSession Maven / Gradle / Ivy

There is a newer version: 3.4.0
Show newest version
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2013-2022 VMware, Inc. or its affiliates. All rights reserved.
package com.rabbitmq.jms.client;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction;

import javax.jms.BytesMessage;
import javax.jms.Destination;
import javax.jms.IllegalStateException;
import javax.jms.InvalidSelectorException;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.jms.MessageProducer;
import javax.jms.ObjectMessage;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.QueueReceiver;
import javax.jms.QueueSender;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.StreamMessage;
import javax.jms.TemporaryQueue;
import javax.jms.TemporaryTopic;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicPublisher;
import javax.jms.TopicSession;
import javax.jms.TopicSubscriber;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.jms.util.WhiteListObjectInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.jms.admin.RMQDestination;
import com.rabbitmq.jms.client.message.RMQBytesMessage;
import com.rabbitmq.jms.client.message.RMQMapMessage;
import com.rabbitmq.jms.client.message.RMQObjectMessage;
import com.rabbitmq.jms.client.message.RMQStreamMessage;
import com.rabbitmq.jms.client.message.RMQTextMessage;
import com.rabbitmq.jms.parse.sql.SqlCompiler;
import com.rabbitmq.jms.parse.sql.SqlEvaluator;
import com.rabbitmq.jms.parse.sql.SqlExpressionType;
import com.rabbitmq.jms.parse.sql.SqlParser;
import com.rabbitmq.jms.parse.sql.SqlTokenStream;
import com.rabbitmq.jms.util.RMQJMSException;
import com.rabbitmq.jms.util.RMQJMSSelectorException;
import com.rabbitmq.jms.util.Util;
/**
 * RabbitMQ implementation of JMS {@link Session}
 */
public class RMQSession implements Session, QueueSession, TopicSession {

    private final Logger logger = LoggerFactory.getLogger(RMQSession.class);

    /** The connection that created this session */
    private final RMQConnection connection;
    /** Set to true if this session is transacted. */
    private final boolean transacted;

    /** This value must be the maximum allowed, and contiguous with valid values for {@link #acknowledgeMode}. */
    public static final int CLIENT_INDIVIDUAL_ACKNOWLEDGE = 4; // mode in {0, 1, 2, 3, 4} is valid
    /** The ack mode used when receiving message */
    private final int acknowledgeMode;
    /** Flag to remember if session is {@link #CLIENT_INDIVIDUAL_ACKNOWLEDGE} */
    private final boolean isIndividualAck;

    /**
     * Whether {@link MessageProducer} properties (delivery mode,
     * priority, TTL) take precedence over respective {@link Message}
     * properties or not.
     * Default is true.
     */
    private final boolean preferProducerMessageProperty;

    /**
     * Whether requeue message on {@link RuntimeException} in the
     * {@link javax.jms.MessageListener} or not.
     * Default is false.
     */
    private final boolean requeueOnMessageListenerException;

    private final boolean requeueOnTimeout;

    /**
     * Whether to commit nack on rollback or not.
     * Default is false.
     *
     * @since 1.14.0
     */
    private final boolean nackOnRollback;

    /**
     * Whether using auto-delete for server-named queues for non-durable topics.
     * If set to true, those queues will be deleted when the session is closed.
     * If set to false, queues will be deleted when the owning connection is closed.
     * Default is false.
     * @since 1.8.0
     */
    private final boolean cleanUpServerNamedQueuesForNonDurableTopics;

    /**
     * Callback to customise properties of outbound AMQP messages.
     *
     * @since 1.9.0
     */
    private final BiFunction amqpPropertiesCustomiser;

    /**
     * Callback before sending a message.
     *
     * @since 1.11.0
     */
    private final SendingContextConsumer sendingContextConsumer;

    private final ReceivingContextConsumer receivingContextConsumer;

    private final PublishingListener publishingListener;

    /** The main RabbitMQ channel we use under the hood */
    private final Channel channel;
    /** Set to true if close() has been called and completed */
    private volatile boolean closed = false;
    /** The message listener for this session. */
    private volatile MessageListener messageListener;
    /** A list of all the producers created by this session.
     * When a producer is closed, it will be removed from this list */
    private final ArrayList producers = new ArrayList();
    /** A list of all the consumers created by this session.
     * When a consumer is closed, it will be removed from this list */
    private final ArrayList consumers = new ArrayList();
    /** We keep an ordered set of the message tags (acknowledgement tags) for all messages received and unacknowledged.
     * Each message acknowledgement must ACK all (unacknowledged) messages received up to this point, and
     * we must never acknowledge a message more than once (nor acknowledge a message that doesn't exist). */
    private final SortedSet unackedMessageTags = Collections.synchronizedSortedSet(new TreeSet());

    /* Holds the uncommited tags to commit a nack on rollback */
    private final List uncommittedMessageTags = new ArrayList(); // GuardedBy("commitLock");

    /** List of all our durable subscriptions so we can track them */
    private final Map subscriptions;

    /** Lock for waiting for close */
    private final Object closeLock = new Object();

    /** Lock and parms for commit and rollback blocking of other commands */
    private final Object commitLock = new Object();
    private static final long COMMIT_WAIT_MAX = 2000L; // 2 seconds
    private boolean committing = false; // GuardedBy("commitLock");

    /** Client version obtained from compiled class. */
    private static final GenericVersion CLIENT_VERSION = new GenericVersion(RMQSession.class.getPackage().getImplementationVersion());

    private static final String RJMS_CLIENT_VERSION = CLIENT_VERSION.toString();
    static {
        if (RJMS_CLIENT_VERSION.equals("0.0.0"))
            System.out.println("WARNING: Running test version of RJMS Client with no version information.");
    }

    /** Selector exchange for topic selection */
    private volatile String durableTopicSelectorExchange;
    /** Selector exchange for topic selection */
    private volatile String nonDurableTopicSelectorExchange;
    /** Selector exchange arg key for erlang selector expression */
    private static final String RJMS_COMPILED_SELECTOR_ARG = "rjms_erlang_selector";
    /** Selector exchange arg key for client version */
    private static final String RJMS_VERSION_ARG = "rjms_version";
    /** Selector exchange arguments */
    private static final Map RJMS_SELECTOR_EXCHANGE_ARGS
        = Collections.singletonMap(RJMS_VERSION_ARG, RJMS_CLIENT_VERSION);

    private static Map generateJMSTypeIdents() {
        Map map = new HashMap(6);  // six elements only
        map.put("JMSDeliveryMode",          SqlExpressionType.STRING);
        map.put("JMSPriority",              SqlExpressionType.ARITH );
        map.put("JMSMessageID",             SqlExpressionType.STRING);
        map.put("JMSTimestamp",             SqlExpressionType.ARITH );
        map.put("JMSCorrelationID",         SqlExpressionType.STRING);
        map.put(RMQMessage.JMS_TYPE_HEADER, SqlExpressionType.STRING);
        return Collections.unmodifiableMap(map);
    }
    static final Map JMS_TYPE_IDENTS = generateJMSTypeIdents();

    private static final String JMS_TOPIC_SELECTOR_EXCHANGE_TYPE = "x-jms-topic";

    private final DeliveryExecutor deliveryExecutor;

    /** The channels we use for browsing queues (there may be more than one in operation at a time) */
    private Set browsingChannels = new HashSet(); // @GuardedBy(bcLock)
    private final Object bcLock = new Object();

    /**
     * Classes in these packages can be transferred via ObjectMessage.
     *
     * @see WhiteListObjectInputStream
     */
    private final List trustedPackages;

    /**
     * Arguments to be used when declaring a queue while creating a producer
     *
     * @since 1.14.0
     */
    private Map queueDeclareArguments = null;

    private final boolean keepTextMessageType;

    /**
     * Creates a session object associated with a connection
     * @param sessionParams parameters for this session
     * @throws JMSException if we fail to create a {@link Channel} object on the connection, or if the acknowledgement mode is incorrect
     */
    public RMQSession(SessionParams sessionParams) throws JMSException {
        if (sessionParams.getMode() < 0 || sessionParams.getMode() > CLIENT_INDIVIDUAL_ACKNOWLEDGE) {
            throw new JMSException(String.format("cannot create session with acknowledgement mode = %d.", sessionParams.getMode()));
        }
        if (sessionParams.willRequeueOnTimeout() && !sessionParams.willRequeueOnMessageListenerException()) {
            throw new IllegalArgumentException("requeueOnTimeout can be true only if requeueOnMessageListenerException is true as well");
        }
        this.connection = sessionParams.getConnection();
        this.transacted = sessionParams.isTransacted();
        this.subscriptions = sessionParams.getSubscriptions();
        boolean deliveryExecutorCloseOnTimeout = !sessionParams.willRequeueOnTimeout();
        this.deliveryExecutor = new DeliveryExecutor(sessionParams.getOnMessageTimeoutMs(), deliveryExecutorCloseOnTimeout);
        this.preferProducerMessageProperty = sessionParams.willPreferProducerMessageProperty();
        this.requeueOnMessageListenerException = sessionParams.willRequeueOnMessageListenerException();
        this.nackOnRollback = sessionParams.willNackOnRollback();
        this.cleanUpServerNamedQueuesForNonDurableTopics = sessionParams.isCleanUpServerNamedQueuesForNonDurableTopics();
        this.amqpPropertiesCustomiser = sessionParams.getAmqpPropertiesCustomiser();
        this.sendingContextConsumer = sessionParams.getSendingContextConsumer();
        this.receivingContextConsumer = sessionParams.getReceivingContextConsumer() == null ?
            ReceivingContextConsumer.NO_OP : sessionParams.getReceivingContextConsumer();
        this.trustedPackages = sessionParams.getTrustedPackages();
        this.requeueOnTimeout = sessionParams.willRequeueOnTimeout();
        this.keepTextMessageType = sessionParams.isKeepTextMessageType();

        if (transacted) {
            this.acknowledgeMode = Session.SESSION_TRANSACTED;
            this.isIndividualAck = false;
        } else if (sessionParams.getMode() == CLIENT_INDIVIDUAL_ACKNOWLEDGE) {
            this.acknowledgeMode = Session.CLIENT_ACKNOWLEDGE;
            this.isIndividualAck = true;
        } else {
            this.acknowledgeMode = sessionParams.getMode();
            this.isIndividualAck = false;
        }
        try {
            this.channel = connection.createRabbitChannel(transacted);
            if (sessionParams.getConfirmListener() != null) {
                this.publishingListener = PublisherConfirmsUtils.configurePublisherConfirmsSupport(
                        this.channel, sessionParams.getConfirmListener()
                );
            } else {
                this.publishingListener = null;
            }
        } catch (Exception x) { // includes unchecked exceptions, e.g. ShutdownSignalException
            throw new RMQJMSException(x);
        }
    }

    /**
     * Creates a session object associated with a connection
     * @param connection the connection that we will send data on
     * @param transacted whether this session is transacted or not
     * @param onMessageTimeoutMs how long to wait for onMessage to return, in milliseconds
     * @param mode the (fixed) acknowledgement mode for this session
     * @param subscriptions the connection's subscriptions, shared with all sessions
     * @throws JMSException if we fail to create a {@link Channel} object on the connection, or if the acknowledgement mode is incorrect
     */
    public RMQSession(RMQConnection connection, boolean transacted, int onMessageTimeoutMs, int mode, Map subscriptions) throws JMSException {
        this(new SessionParams()
            .setConnection(connection)
            .setTransacted(transacted)
            .setOnMessageTimeoutMs(onMessageTimeoutMs)
            .setMode(mode)
            .setSubscriptions(subscriptions)
        );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BytesMessage createBytesMessage() throws JMSException {
        illegalStateExceptionIfClosed();
        return new RMQBytesMessage();
    }

    private void illegalStateExceptionIfClosed() throws IllegalStateException {
        if (this.closed) throw new IllegalStateException("Session is closed");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public MapMessage createMapMessage() throws JMSException {
        illegalStateExceptionIfClosed();
        return new RMQMapMessage();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Message createMessage() throws JMSException {
        illegalStateExceptionIfClosed();
        return createTextMessage();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ObjectMessage createObjectMessage() throws JMSException {
        illegalStateExceptionIfClosed();
        return new RMQObjectMessage();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ObjectMessage createObjectMessage(Serializable object) throws JMSException {
        illegalStateExceptionIfClosed();
        ObjectMessage message = createObjectMessage();
        message.setObject(object);
        return message;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public StreamMessage createStreamMessage() throws JMSException {
        illegalStateExceptionIfClosed();
        return new RMQStreamMessage();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TextMessage createTextMessage() throws JMSException {
        illegalStateExceptionIfClosed();
        return new RMQTextMessage();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TextMessage createTextMessage(String text) throws JMSException {
        illegalStateExceptionIfClosed();
        TextMessage msg = createTextMessage();
        msg.setText(text);
        return msg;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getTransacted() throws JMSException {
        illegalStateExceptionIfClosed();
        return getTransactedNoException();
    }

    /**
     * Same as {@link #getTransacted()}
     * but does not declare a JMSException in the throw clause
     * @return true if this session is transacted
     */
    private boolean getTransactedNoException() {
        return this.transacted;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getAcknowledgeMode() throws JMSException {
        illegalStateExceptionIfClosed();
        return getAcknowledgeModeNoException();
    }

    public List getTrustedPackages() {
        return trustedPackages;
    }

    /**
     * Set arguments to be used when declaring a queue while creating a producer.
     * 

* Use this method only when you need to customize the creation of an AMQP queue. * Note calling this method requires to cast the JMS {@link Session} to {@link RMQSession}, * coupling the code to the JMS implementation. Common usage is to keep this call in * one place, not scattered in the application code. * * @param queueDeclareArguments * @since 1.14.0 */ public void setQueueDeclareArguments(Map queueDeclareArguments) { this.queueDeclareArguments = queueDeclareArguments; } /** * Same as {@link RMQSession#getAcknowledgeMode()} but without * a declared exception in the throws clause. * @return the acknowledge mode, one of {@link Session#AUTO_ACKNOWLEDGE}, * {@link Session#CLIENT_ACKNOWLEDGE}, {@link Session#DUPS_OK_ACKNOWLEDGE} or * {@link Session#SESSION_TRANSACTED} */ int getAcknowledgeModeNoException() { return this.acknowledgeMode; } private boolean enterCommittingBlock() { synchronized(this.commitLock){ try { while(this.committing) { this.commitLock.wait(COMMIT_WAIT_MAX); } this.committing = true; return true; } catch(InterruptedException ie) { Thread.currentThread().interrupt(); return false; } } } private void leaveCommittingBlock() { synchronized(this.commitLock){ this.committing = false; this.commitLock.notifyAll(); } } /** * {@inheritDoc} */ @Override public void commit() throws JMSException { logger.trace("commit transaction on session {}", this); illegalStateExceptionIfClosed(); if (!this.transacted) throw new IllegalStateException("Session is not transacted"); if (this.enterCommittingBlock()) { try { // Call commit on the channel. // All messages ought already to have been acked. this.channel.txCommit(); this.clearUncommittedTags(); } catch (Exception x) { this.logger.error("RabbitMQ exception on channel.txCommit() in session {}", this, x); throw new RMQJMSException(x); } finally { this.leaveCommittingBlock(); } } } /** * {@inheritDoc} */ @Override public void rollback() throws JMSException { logger.trace("rollback transaction on session {}", this); illegalStateExceptionIfClosed(); if (!this.transacted) throw new IllegalStateException("Session is not transacted"); if (this.enterCommittingBlock()) { try { // rollback the RabbitMQ transaction which may cause some messages to become unacknowledged this.channel.txRollback(); if (this.nackOnRollback && this.uncommittedMessageTags.size() > 0) { for (Long dtag : this.uncommittedMessageTags) { this.channel.basicNack(dtag, false, false); } this.channel.txCommit(); this.clearUncommittedTags(); } // requeue all unacknowledged messages (not automatically done by RabbitMQ) this.channel.basicRecover(true); // requeue } catch (IOException x) { this.logger.error("RabbitMQ exception on channel.txRollback() or channel.basicRecover(true) in session {}", this, x); throw new RMQJMSException(x); } finally { this.leaveCommittingBlock(); } } } void explicitAck(long deliveryTag) { if (this.enterCommittingBlock()) { try { this.channel.basicAck(deliveryTag, false); } catch (Exception x) { // this is problematic, we have received a message, but we can't ACK it to the server this.logger.error("Cannot acknowledge message received (dTag={})", deliveryTag, x); // TODO should we deliver the message at this time, knowing that we can't ack it? // My recommendation is that we bail out here and not proceed -- but how? } finally { this.leaveCommittingBlock(); } } } void explicitNack(long deliveryTag) { if (this.enterCommittingBlock()) { try { this.channel.basicNack(deliveryTag, false, true); } catch (Exception x) { // TODO logging impl debug message this.logger.warn("Cannot reject/requeue message received (dTag={})", deliveryTag, x); // this is fine. we didn't ack the message in the first place } finally { this.leaveCommittingBlock(); } } } /** * {@inheritDoc} */ @Override public void close() throws JMSException { this.getConnection().sessionClose(this); } void internalClose() throws JMSException { if (this.closed) return; logger.trace("close session {}", this); synchronized (this.closeLock) { try { // close consumers first (to prevent requeues being consumed) closeAllConsumers(); // rollback anything not committed already if (this.getTransactedNoException()) { // don't nack messages on close this.clearUncommittedTags(); this.rollback(); } //clear up potential executor this.deliveryExecutor.close(); //close all producers created by this session for (RMQMessageProducer producer : this.producers) { producer.internalClose(); } this.producers.clear(); //now commit anything done during close if (this.getTransactedNoException()) { this.commit(); } this.closeRabbitChannels(); } finally { this.closed = true; } } } private void closeAllConsumers() { //close all consumers created by this session for (RMQMessageConsumer consumer : this.consumers) { try { consumer.internalClose(); } catch (JMSException x) { this.logger.error("Consumer ({}) cannot be closed", consumer, x); } } this.consumers.clear(); } void deliverMessage(RMQMessage rmqMessage, MessageListener messageListener) throws JMSException, InterruptedException { this.deliveryExecutor.deliverMessageWithProtection(rmqMessage, messageListener); } private void closeRabbitChannels() throws JMSException { this.clearBrowsingChannels(); // does not throw exception if (this.channel == null) return; try { this.channel.close(); } catch (ShutdownSignalException x) { // nothing to do } catch (Exception x) { if (x instanceof IOException) { IOException ioe = (IOException) x; if (!(ioe.getCause() instanceof ShutdownSignalException)) { this.logger.warn("RabbitMQ channel({}) failed to close on session {}", this.channel, this, ioe); throw new RMQJMSException(ioe); } } else if (x instanceof TimeoutException) { TimeoutException te = (TimeoutException) x; this.logger.warn("RabbitMQ channel({}) timed out trying to close session {}", this.channel, this, te); throw new RMQJMSException(te); } else { throw new RMQJMSException("Unexpected exception from channel.close()", x); } } } /** * {@inheritDoc} */ @Override public void recover() throws JMSException { illegalStateExceptionIfClosed(); if (getTransactedNoException()) { throw new javax.jms.IllegalStateException("Session is transacted."); } else { synchronized (this.unackedMessageTags) { /* If we have messages to recover */ if (!this.unackedMessageTags.isEmpty()) { try { this.channel.basicRecover(true); // requeue } catch (IOException x) { logger.warn("basicRecover on channel({}) failed", this.channel, x); throw new RMQJMSException(x); } this.unackedMessageTags.clear(); } } } } /** * {@inheritDoc} */ @Override public MessageListener getMessageListener() throws JMSException { illegalStateExceptionIfClosed(); return this.messageListener; } /** * {@inheritDoc} */ @Override public void setMessageListener(MessageListener listener) throws JMSException { // not implemented throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public void run() { //this will not be implemented throw new UnsupportedOperationException(); } /** * {@inheritDoc} *

* Note: The destination may be null. *

*/ @Override public MessageProducer createProducer(Destination destination) throws JMSException { logger.trace("create producer for destination '{}' on session '{}'", destination, this); illegalStateExceptionIfClosed(); RMQDestination dest = (RMQDestination) destination; declareDestinationIfNecessary(dest); RMQMessageProducer producer = new RMQMessageProducer(this, dest, this.preferProducerMessageProperty, this.amqpPropertiesCustomiser, this.sendingContextConsumer, this.publishingListener, this.keepTextMessageType); this.producers.add(producer); return producer; } void declareDestinationIfNecessary(RMQDestination destination) throws JMSException { if (destination != null && !destination.isAmqp() && !destination.isDeclared()) { if (destination.isQueue()) { declareRMQQueue(destination, null, false, true); } else { declareTopic(destination); } } } /** * {@inheritDoc} */ @Override public MessageConsumer createConsumer(Destination destination) throws JMSException { illegalStateExceptionIfClosed(); return createConsumerInternal((RMQDestination) destination, null, false, null); } /** All consumers on a session must be used all synchronously or all asynchronously */ boolean syncAllowed() { // Return (None of the MessageConsumers have a (non-null) MessageListener set). for (RMQMessageConsumer mc : consumers) { if (mc.messageListenerIsSet()) return false; } return true; } boolean aSyncAllowed() { // Return (Number of receives is zero for all MessageConsumers.) for (RMQMessageConsumer mc : consumers) { if (0 != mc.getNumberOfReceives()) return false; } return true; } /** * Creates a consumer for a destination. * @param dest internal destination object * @param uuidTag only used for topics, if null, one is generated as the queue name for this topic * @param durableSubscriber true if this is a durable topic subscription * @param jmsSelector selector expression - null if no selection required * @return {@link #createConsumer(Destination)} * @throws JMSException if destination is null or we fail to create the destination on the broker * @see #createConsumer(Destination) */ private RMQMessageConsumer createConsumerInternal(RMQDestination dest, String uuidTag, boolean durableSubscriber, String jmsSelector) throws JMSException { String consumerTag = uuidTag != null ? uuidTag : Util.generateUUID("jms-cons-"); logger.trace("create consumer for destination '{}' with consumerTag '{}' and selector '{}'", dest, consumerTag, jmsSelector); declareDestinationIfNecessary(dest); if (!dest.isQueue()) { // This is a topic, we need to define a queue, and bind to it. // The queue name is distinct for each consumer. try { String queueName = consumerTag; this.declareRMQQueue(dest, queueName, durableSubscriber, false); if (nullOrEmpty(jmsSelector)) { // bind the queue to the exchange with the correct routing key this.channel.queueBind(queueName, dest.getAmqpExchangeName(), dest.getAmqpRoutingKey()); } else { // get this session's topic selector exchange (name) String selectionExchange = this.getSelectionExchange(durableSubscriber); // bind it to the topic exchange with the topic routing key this.channel.exchangeBind(selectionExchange, dest.getAmqpExchangeName(), dest.getAmqpRoutingKey()); this.bindSelectorQueue(dest, jmsSelector, queueName, selectionExchange); } } catch (IOException x) { logger.error("consumer with tag '{}' could not be created", consumerTag, x); throw new RMQJMSException("RabbitMQ Exception creating Consumer", x); } } RMQMessageConsumer consumer = new RMQMessageConsumer(this, dest, consumerTag, getConnection().isStopped(), jmsSelector, this.requeueOnMessageListenerException, this.receivingContextConsumer, this.requeueOnTimeout); this.consumers.add(consumer); return consumer; } private void bindSelectorQueue(RMQDestination dest, String jmsSelector, String queueName, String selectionExchange) throws InvalidSelectorException, IOException { SqlCompiler compiler = new SqlCompiler(new SqlEvaluator(new SqlParser(new SqlTokenStream(jmsSelector)), JMS_TYPE_IDENTS)); if (compiler.compileOk()) { Map args = new HashMap(5); args.put(RJMS_COMPILED_SELECTOR_ARG, (Object)compiler.compile()); args.put(RJMS_VERSION_ARG, (Object)RJMS_CLIENT_VERSION); // bind the queue to the topic selector exchange with the jmsSelector expression as argument this.channel.queueBind(queueName, selectionExchange, dest.getAmqpRoutingKey(), args); } else { throw new RMQJMSSelectorException(String.format("Selector expression failure: \"%s\".", jmsSelector)); } } /** * The topic selector exchange may be created for this session (there are at most two per session). * @param durableSubscriber - set to true if we need to use a durable exchange, false otherwise. * @return this session's Selection Exchange * @throws IOException */ private String getSelectionExchange(boolean durableSubscriber) throws IOException { if (durableSubscriber) { return this.getDurableTopicSelectorExchange(); } else { return this.getNonDurableTopicSelectorExchange(); } } private String getDurableTopicSelectorExchange() throws IOException { if (this.durableTopicSelectorExchange==null) { this.durableTopicSelectorExchange = Util.generateUUID("jms-dutop-slx-"); } this.channel.exchangeDeclare(this.durableTopicSelectorExchange, JMS_TOPIC_SELECTOR_EXCHANGE_TYPE, true, true, RJMS_SELECTOR_EXCHANGE_ARGS); return this.durableTopicSelectorExchange; } private String getNonDurableTopicSelectorExchange() throws IOException { if (this.nonDurableTopicSelectorExchange==null) { this.nonDurableTopicSelectorExchange = Util.generateUUID("jms-ndtop-slx-"); } this.channel.exchangeDeclare(this.nonDurableTopicSelectorExchange, JMS_TOPIC_SELECTOR_EXCHANGE_TYPE, false, true, RJMS_SELECTOR_EXCHANGE_ARGS); return this.nonDurableTopicSelectorExchange; } /** * {@inheritDoc} * @throws UnsupportedOperationException - method not implemented until we support selectors */ @Override public MessageConsumer createConsumer(Destination destination, String messageSelector) throws JMSException { illegalStateExceptionIfClosed(); if (nullOrEmpty(messageSelector)) { return createConsumer(destination); } else if (isTopic(destination)) { return createConsumerInternal((RMQDestination) destination, null, false, messageSelector); } else { // selectors are not supported for queues throw new UnsupportedOperationException(); } } private static boolean nullOrEmpty(String str) { return str==null || str.trim().isEmpty(); } private static boolean isTopic(Destination destination) { return !((RMQDestination) destination).isQueue(); } /** * {@inheritDoc} * @throws UnsupportedOperationException - method not implemented until we support selectors */ @Override public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean noLocal) throws JMSException { illegalStateExceptionIfClosed(); if (nullOrEmpty(messageSelector)) { RMQMessageConsumer consumer = (RMQMessageConsumer)createConsumer(destination); consumer.setNoLocal(noLocal); return consumer; } else if (isTopic(destination)) { RMQMessageConsumer consumer = createConsumerInternal((RMQDestination) destination, null, false, messageSelector); consumer.setNoLocal(noLocal); return consumer; } else { // selectors are not supported for queues throw new UnsupportedOperationException(); } } /** * {@inheritDoc} */ @Override public Queue createQueue(String queueName) throws JMSException { illegalStateExceptionIfClosed(); RMQDestination dest = new RMQDestination(queueName, true, false); declareRMQQueue(dest, null, false, true); return dest; } /** * Invokes {@link Channel#queueDeclare(String, boolean, boolean, boolean, java.util.Map)} to define a queue on the RabbitMQ broker * this method invokes {@link RMQDestination#setDeclared(boolean)} with a true value * @param dest - the Queue Destination object * @param queueNameOverride name of queue to declare (if different from destination name) * @param durableSubscriber - true if the subscriber ius * @throws JMSException if an IOException occurs in the {@link Channel#queueDeclare(String, boolean, boolean, boolean, java.util.Map)} call */ private void declareRMQQueue(RMQDestination dest, String queueNameOverride, boolean durableSubscriber, boolean bind) throws JMSException { logger.trace("declare RabbitMQ queue for destination '{}', explicitName '{}', durableSubscriber={}", dest, queueNameOverride, durableSubscriber); String queueName = queueNameOverride!=null ? queueNameOverride : dest.getQueueName(); String exchangeName = dest.getAmqpExchangeName(); String exchangeType = dest.amqpExchangeType(); /* * We only want destinations to survive server restarts if * 1. They are durable topic subscriptions OR * 2. They are permanent queues */ boolean durable = durableSubscriber || (dest.isQueue() & (!dest.isTemporary())); /* * A queue is exclusive, meaning it can only be accessed by the current connection * and will be deleted when the connection is closed if * 1. It's a temporary destination OR * 2. It's a non durable topic */ boolean exclusive = dest.isTemporary() || ((!dest.isQueue()) && (!durableSubscriber)); if (dest.isQueue()) { if (dest.noNeedToDeclareExchange()) { logger.warn("no need to declare built-in exchange for queue destination '{}'", dest); } else { logger.trace("declare RabbitMQ exchange for queue destinations '{}'", dest); try { this.channel.exchangeDeclare(exchangeName, exchangeType, durable, false, // autoDelete false, // internal null); // object properties } catch (Exception x) { throw new RMQJMSException(x); } } } /* broker queues declared for a non-durable topic that have an auto-generated name must go down with consumer/producer or the broker will leak them until the connection is brought down */ boolean autoDelete = cleanUpServerNamedQueuesForNonDurableTopics ? !durable && queueNameOverride != null && !dest.isQueue() : false; try { /* Declare the queue to RabbitMQ -- this creates it if it doesn't already exist */ this.logger.debug("declare RabbitMQ queue name({}), durable({}), exclusive({}), auto-delete({}), arguments({} + {})", queueName, durable, exclusive, false, this.queueDeclareArguments, dest.getQueueDeclareArguments()); Map arguments = merge(this.queueDeclareArguments, dest.getQueueDeclareArguments()); this.channel.queueDeclare(queueName, durable, exclusive, autoDelete, arguments); /* Temporary or 'topic queues' are exclusive and therefore get deleted by RabbitMQ on close */ } catch (Exception x) { this.logger.error("RabbitMQ exception on queue declare name({}), durable({}), exclusive({}), auto-delete({}), arguments({} + {})", queueName, durable, exclusive, autoDelete, this.queueDeclareArguments, dest.getQueueDeclareArguments(), x); throw new RMQJMSException(x); } if (bind) { try { /* Bind the queue to our exchange -- this allows publications to succeed. */ this.logger.debug("bind queue name({}), to exchange({}), with r-key({}), no arguments", queueName, exchangeName, queueName); this.channel.queueBind(queueName, exchangeName, queueName, // routing key null); // arguments } catch (Exception x) { this.logger.error("RabbitMQ exception on queue declare name({}), durable({}), exclusive({}), auto-delete({}), properties({})", queueName, durable, exclusive, false, queueDeclareArguments, x); throw new RMQJMSException(x); } } dest.setDeclared(true); } /** * {@inheritDoc} */ @Override public Topic createTopic(String topicName) throws JMSException { illegalStateExceptionIfClosed(); RMQDestination dest = new RMQDestination(topicName, false, false); declareTopic(dest); return dest; } /** * Declares a topic exchange in RabbitMQ. * @param dest the topic destination * @throws JMSException */ private void declareTopic(RMQDestination dest) throws JMSException { if (dest.noNeedToDeclareExchange()) { logger.warn("no need to declare built-in exchange for topic destination '{}'", dest); } else { logger.trace("declare RabbitMQ exchange for topic destination '{}'", dest); try { this.channel.exchangeDeclare(/* the name of the exchange */ dest.getAmqpExchangeName(), /* the type of exchange to use */ dest.amqpExchangeType(), /* durable for all except temporary topics */ !dest.isTemporary(), // TODO: how do we delete exchanges used for temporary topics /* auto delete is always false */ false, /* internal is false: JMS clients will want to publish directly to the exchange */ false, /* object parameters */ null); } catch (IOException x) { throw new RMQJMSException(x); } } dest.setDeclared(true); } /** * {@inheritDoc} */ @Override public TopicSubscriber createDurableSubscriber(Topic topic, String name) throws JMSException { return createDurableSubscriber(topic, name, null, false); } /** * {@inheritDoc} */ @Override public TopicSubscriber createDurableSubscriber(Topic topic, String name, String messageSelector, boolean noLocal) throws JMSException { illegalStateExceptionIfClosed(); RMQDestination topicDest = (RMQDestination) topic; RMQMessageConsumer previousConsumer = this.subscriptions.get(name); if (previousConsumer!=null) { // we are changing subscription, or not, if called with the same topic RMQDestination prevDest = previousConsumer.getDestination(); if (prevDest.equals(topicDest)) { if (previousConsumer.isClosed()) { // They called TopicSubscriber.close but didn't unsubscribe // and they are simply resubscribing with a new one logger.warn("Re-subscribing to topic '{}' with name '{}'", topicDest, name); } else { logger.error("Subscription with name '{}' for topic '{}' already exists", name, topicDest); throw new JMSException(String.format("Subscription with name [%s] and topic [%s] already exists", name, topicDest)); } } else { logger.warn("Previous subscription with name '{}' was for topic '{}' and is replaced by one for topic '{}'", name, prevDest, topicDest); unsubscribe(name); } } // Create a new subscription RMQMessageConsumer consumer = (RMQMessageConsumer)createConsumerInternal(topicDest, name, true, messageSelector); consumer.setDurable(true); consumer.setNoLocal(noLocal); this.subscriptions.put(name, consumer); return consumer; } /** * {@inheritDoc} */ @Override public QueueBrowser createBrowser(Queue queue) throws JMSException { illegalStateExceptionIfClosed(); return createBrowser(queue, null); } /** * {@inheritDoc} */ @Override public QueueBrowser createBrowser(Queue queue, String messageSelector) throws JMSException { illegalStateExceptionIfClosed(); if (queue instanceof RMQDestination) { RMQDestination rmqDest = (RMQDestination) queue; if (rmqDest.isQueue()) { return new BrowsingMessageQueue(this, rmqDest, messageSelector, this.connection.getQueueBrowserReadMax(), this.receivingContextConsumer); } } throw new UnsupportedOperationException("Unknown destination"); } /** * Get a (new) channel for queue browsing. * @return channel for browsing queues * @throws JMSException if channel not available */ Channel getBrowsingChannel() throws JMSException { try { synchronized (this.bcLock) { Channel chan = this.getConnection().createRabbitChannel(false); // not transactional this.browsingChannels.add(chan); return chan; } } catch (Exception e) { // includes unchecked exceptions, e.g. ShutdownSignalException throw new RMQJMSException("Cannot create browsing channel", e); } } private static Map merge(Map m1, Map m2) { if (m1 == null) { return m2; } else if (m2 == null) { return m1; } else { Map merged = new HashMap<>(); merged.putAll(m1); merged.putAll(m2); return merged; } } /** * Silently close and discard browsing channels, if any. */ private void clearBrowsingChannels() { synchronized (this.bcLock) { for (Channel chan : this.browsingChannels) { try { if (chan.isOpen()) chan.close(); } catch (Exception e) { // ignore any failures, we are clearing up } } this.browsingChannels.clear(); } } /** * Close a specific browsing channel. */ void closeBrowsingChannel(Channel chan) { try { synchronized (this.bcLock) { if (this.browsingChannels.remove(chan)) { if (chan.isOpen()) chan.close(); } } } catch (Exception e) { // throw new RMQJMSException("Cannot close browsing channel", _); // ignore errors in clearing up } } /** * {@inheritDoc} */ @Override public TemporaryQueue createTemporaryQueue() throws JMSException { illegalStateExceptionIfClosed(); return new RMQDestination(Util.generateUUID("jms-temp-queue-"), true, true); } /** * {@inheritDoc} */ @Override public TemporaryTopic createTemporaryTopic() throws JMSException { illegalStateExceptionIfClosed(); return new RMQDestination(Util.generateUUID("jms-temp-topic-"), false, true); } /** * This is only available for topic subscriptions. * {@inheritDoc} */ @Override public void unsubscribe(String name) throws JMSException { illegalStateExceptionIfClosed(); try { if (name != null && this.subscriptions.remove(name) != null) { // remove the queue this.channel.queueDelete(name); } else { logger.warn("Cannot unsubscribe subscription named '{}'", name); } } catch (IOException x) { logger.error("RabbitMQ Queue delete for queue named '{}' failed", name, x); throw new RMQJMSException(x); } } /** * {@inheritDoc} */ @Override public QueueReceiver createReceiver(Queue queue) throws JMSException { illegalStateExceptionIfClosed(); return (QueueReceiver) this.createConsumer(queue); } /** * {@inheritDoc} */ @Override public QueueReceiver createReceiver(Queue queue, String messageSelector) throws JMSException { illegalStateExceptionIfClosed(); return (QueueReceiver) this.createConsumer(queue, messageSelector); } /** * {@inheritDoc} *

* Note: The queue may be null -- see {@link #createProducer}. *

*/ @Override public QueueSender createSender(Queue queue) throws JMSException { illegalStateExceptionIfClosed(); return (QueueSender) this.createProducer(queue); } /** * {@inheritDoc} */ @Override public TopicSubscriber createSubscriber(Topic topic) throws JMSException { illegalStateExceptionIfClosed(); return (TopicSubscriber) this.createConsumer(topic); } /** * {@inheritDoc} */ @Override public TopicSubscriber createSubscriber(Topic topic, String messageSelector, boolean noLocal) throws JMSException { illegalStateExceptionIfClosed(); RMQMessageConsumer consumer = createConsumerInternal((RMQDestination) topic, null, false, messageSelector); consumer.setNoLocal(noLocal); return consumer; } /** * {@inheritDoc} */ @Override public TopicPublisher createPublisher(Topic topic) throws JMSException { illegalStateExceptionIfClosed(); return (TopicPublisher) this.createProducer(topic); } /** * Returns the connection this session is associated with * @return */ RMQConnection getConnection() { return this.connection; } /** * Returns the {@link Channel} this session has created * @return */ Channel getChannel() { return this.channel; } void consumerClose(RMQMessageConsumer consumer) throws JMSException { if (this.consumers.remove(consumer)) { //TODO: if (consumer.isDurable()) { don't cancel it? cancel it? -- decide } consumer.internalClose(); } } void removeProducer(RMQMessageProducer producer) { if (this.producers.remove(producer)) { producer.internalClose(); } } boolean isAutoAck() { return (getAcknowledgeModeNoException()!=Session.CLIENT_ACKNOWLEDGE); // only case when auto ack not required } /** * Stops all consumers from receiving messages. This is called by the * session indirectly after {@link javax.jms.Connection#stop()} has been * invoked. In this implementation, any async consumers will be cancelled, * only to be re-subscribed when * * @throws javax.jms.JMSException if the thread is interrupted */ void pause() throws JMSException { for (RMQMessageConsumer consumer : this.consumers) { try { consumer.pause(); } catch (JMSException e) { throw e; } catch (InterruptedException x) { logger.error("Consumer({}) pause interrupted", consumer, x); throw new RMQJMSException(x); } catch (Exception x) { logger.error("Error while pausing consumer({})", consumer, x); throw new RMQJMSException(x); } } } /** * Resubscribes all async listeners and continues to receive messages * * @see javax.jms.Connection#stop() * @throws javax.jms.JMSException if the thread is interrupted */ void resume() throws JMSException { for (RMQMessageConsumer consumer : this.consumers) { try { consumer.resume(); } catch (IllegalStateException x) { throw new RMQJMSException(x); } } } void unackedMessageReceived(long dTag) { if (!getTransactedNoException()) { synchronized (this.unackedMessageTags) { this.unackedMessageTags.add(dTag); } } } /** * Acknowledges messages in this session. * Invoked when the method {@link javax.jms.Message#acknowledge()} is called. * @param message - the message to be acknowledged, or the carrier to acknowledge all messages */ void acknowledgeMessage(RMQMessage message) throws JMSException { illegalStateExceptionIfClosed(); boolean individualAck = this.getIndividualAck(); boolean groupAck = true; // This assumption is new in RJMS 1.2.0 and is consistent with other implementations. It allows a form of group acknowledge. if (!isAutoAck() && !this.unackedMessageTags.isEmpty()) { /** * Per JMS specification of {@link Message#acknowledge()}, if we ack the last message in a group, we will ack all the ones prior received. *

But, JMS spec 11.2.21 says:

*
"Note that the acknowledge method of Message acknowledges all messages
             * received on that message's session."
*

* The groupAck option acknowledges all previous messages in this session (and this one, too, if not acknowledged already). * The individualAck option is set by session mode (CLIENT_INDIVIDUAL_ACKNOWLEDGE) and overrides groupAck (default) and acknowledges at most a single message. *

*/ synchronized (this.unackedMessageTags) { try { if (individualAck) { long messageTag = message.getRabbitDeliveryTag(); if (!this.unackedMessageTags.contains(messageTag)) return; // this message already acknowledged /* ACK a single message */ this.getChannel().basicAck(messageTag, false); // we ack the single message with this tag this.unackedMessageTags.remove(messageTag); } else if (groupAck) { long messageTag = message.getRabbitDeliveryTag(); /** The tags that precede the given one, and the given one, if unacknowledged */ SortedSet previousTags = this.unackedMessageTags.headSet(messageTag+1); if (previousTags.isEmpty()) return; // no message to acknowledge /* ack multiple message up until the existing tag */ this.getChannel().basicAck(previousTags.last(), // we ack the latest one (which might be this one, but might not be) true); // and everything prior to that // now remove all the tags <= messageTag previousTags.clear(); } else { // this block is no longer possible (groupAck == true) after RJMS 1.2.0 this.getChannel().basicAck(this.unackedMessageTags.last(), // we ack the highest tag true); // and everything prior to that this.unackedMessageTags.clear(); } } catch (IOException x) { this.logger.error("RabbitMQ exception on basicAck of message {}; on session '{}'", message, this, x); throw new RMQJMSException(x); } } } } private final boolean getIndividualAck() { return this.isIndividualAck; } void addUncommittedTag(long deliveryTag) { if (this.nackOnRollback && this.getTransactedNoException()) { if (this.enterCommittingBlock()) { this.uncommittedMessageTags.add(deliveryTag); this.leaveCommittingBlock(); } } } private void clearUncommittedTags() { if (this.nackOnRollback) { this.uncommittedMessageTags.clear(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy