com.amazon.sqs.javamessaging.SQSSession.java Source code

Java tutorial

Introduction

Here is the source code for com.amazon.sqs.javamessaging.SQSSession.java

Source

/*
 * Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazon.sqs.javamessaging;

import java.io.Serializable;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.jms.BytesMessage;
import javax.jms.Destination;
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.TopicSubscriber;
import javax.jms.IllegalStateException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.amazon.sqs.javamessaging.SQSMessageConsumerPrefetch.MessageManager;
import com.amazon.sqs.javamessaging.acknowledge.AcknowledgeMode;
import com.amazon.sqs.javamessaging.acknowledge.Acknowledger;
import com.amazon.sqs.javamessaging.acknowledge.NegativeAcknowledger;
import com.amazon.sqs.javamessaging.message.SQSBytesMessage;
import com.amazon.sqs.javamessaging.message.SQSObjectMessage;
import com.amazon.sqs.javamessaging.message.SQSTextMessage;
import com.amazon.sqs.javamessaging.util.SQSMessagingClientThreadFactory;

/**
 * A session serves several purposes:
 * <ul>
 * <li>It is a factory for its message producers and consumers.</li>
 * <li>It provides a way to create Queue objects for those clients that need to
 * dynamically manipulate provider-specific destination names.</li>
 * <li>It retains messages it consumes until they have been acknowledged.</li>
 * <li>It serializes execution of message listeners registered with its message
 * consumers.</li>
 * </ul>
 * <P>
 * Not safe for concurrent use.
 * <P>
 * This session object does not support:
 * <ul>
 * <li>(Temporary)Topic</li>
 * <li>Temporary Queue</li>
 * <li>Browser</li>
 * <li>MapMessage</li>
 * <li>StreamMessage</li>
 * <li>MessageSelector</li>
 * <li>Transactions</li>
 * </ul>
 */
public class SQSSession implements Session, QueueSession {
    private static final Log LOG = LogFactory.getLog(SQSSession.class);

    private static final int SESSION_EXECUTOR_GRACEFUL_SHUTDOWN_TIME = 10;

    static final String SESSION_EXECUTOR_NAME = "SessionCallBackScheduler";

    /** Used to create session callback scheduler threads */
    static final SQSMessagingClientThreadFactory SESSION_THREAD_FACTORY = new SQSMessagingClientThreadFactory(
            SESSION_EXECUTOR_NAME, false, true);

    static final String CONSUMER_PREFETCH_EXECUTER_NAME = "ConsumerPrefetch";

    /** Used to create consumer prefetcher threads */
    static final SQSMessagingClientThreadFactory CONSUMER_PREFETCH_THREAD_FACTORY = new SQSMessagingClientThreadFactory(
            CONSUMER_PREFETCH_EXECUTER_NAME, true);

    /**
     * Non standard acknowledge mode. This is a variation of CLIENT_ACKNOWLEDGE
     * where Clients need to remember to call acknowledge on message. Difference
     * is that calling acknowledge on a message only acknowledge the message
     * being called.
     */
    public static final int UNORDERED_ACKNOWLEDGE = 100;

    /**
     * True if Session is closed.
     */
    private volatile boolean closed = false;

    /**
     * False if Session is stopped.
     */
    volatile boolean running = false;

    /**
     * True if Session is closed or close is in-progress.
     */
    private volatile boolean closing = false;

    private final AmazonSQSMessagingClientWrapper amazonSQSClient;
    private final SQSConnection parentSQSConnection;

    /**
     * AcknowledgeMode of this Session.
     */
    private final AcknowledgeMode acknowledgeMode;

    /**
     * Acknowledger of this Session.
     */
    private final Acknowledger acknowledger;

    /**
     * Set of MessageProducer under this Session
     */
    private final Set<SQSMessageProducer> messageProducers;

    /**
     * Set of MessageConsumer under this Session
     */
    private final Set<SQSMessageConsumer> messageConsumers;

    /**
     * Thread that is responsible to guarantee serial execution of message
     * delivery on message listeners
     */
    private final SQSSessionCallbackScheduler sqsSessionRunnable;

    /**
     * Executor service for running MessageListener.
     */
    private final ExecutorService executor;

    private final Object stateLock = new Object();

    /**
     * Used to determine if the caller thread is the session callback thread.
     * Guarded by stateLock
     */
    private Thread activeCallbackSessionThread;

    /**
     * Used to determine the active consumer, whose is dispatching the message
     * on the callback. Guarded by stateLock
     */
    private SQSMessageConsumer activeConsumerInCallback = null;

    SQSSession(SQSConnection parentSQSConnection, AcknowledgeMode acknowledgeMode) throws JMSException {
        this(parentSQSConnection, acknowledgeMode,
                Collections.newSetFromMap(new ConcurrentHashMap<SQSMessageConsumer, Boolean>()),
                Collections.newSetFromMap(new ConcurrentHashMap<SQSMessageProducer, Boolean>()));
    }

    SQSSession(SQSConnection parentSQSConnection, AcknowledgeMode acknowledgeMode,
            Set<SQSMessageConsumer> messageConsumers, Set<SQSMessageProducer> messageProducers)
            throws JMSException {
        this.parentSQSConnection = parentSQSConnection;
        this.amazonSQSClient = parentSQSConnection.getWrappedAmazonSQSClient();
        this.acknowledgeMode = acknowledgeMode;
        this.acknowledger = this.acknowledgeMode.createAcknowledger(amazonSQSClient, this);
        this.sqsSessionRunnable = new SQSSessionCallbackScheduler(this, acknowledgeMode, acknowledger);
        this.executor = Executors.newSingleThreadExecutor(SESSION_THREAD_FACTORY);
        this.messageConsumers = messageConsumers;
        this.messageProducers = messageProducers;

        executor.execute(sqsSessionRunnable);
    }

    SQSConnection getParentConnection() {
        return parentSQSConnection;
    }

    /**
     * @return True if the current thread is the callback thread
     */
    boolean isActiveCallbackSessionThread() {
        synchronized (stateLock) {
            return activeCallbackSessionThread == Thread.currentThread();
        }
    }

    /**
     * Creates a <code>QueueReceiver</code> for the specified queue.
     * @param queue
     *            a queue receiver
     * @return new message consumer
     * @throws JMSException
     *             If session is closed
     */
    @Override
    public QueueReceiver createReceiver(Queue queue) throws JMSException {
        return (QueueReceiver) createConsumer(queue);
    }

    /**
     * Creates a <code>QueueReceiver</code> for the specified queue. Does not
     * support messageSelector. It will drop anything in messageSelector.
     * 
     * @param queue
     *            a queue destination
     * @param messageSelector           
     * @return new message receiver
     * @throws JMSException
     *             If session is closed
     */
    @Override
    public QueueReceiver createReceiver(Queue queue, String messageSelector) throws JMSException {
        return createReceiver(queue);
    }

    /**
     * Creates a <code>QueueSender</code> for the specified queue.
     * 
     * @param queue
     *            a queue destination 
     * @return new message sender
     * @throws JMSException
     *             If session is closed
     */
    @Override
    public QueueSender createSender(Queue queue) throws JMSException {
        return (QueueSender) createProducer(queue);
    }

    /**
     * Creates a <code>BytesMessage</code>.
     * 
     * @return new <code>BytesMessage</code>
     * @throws JMSException
     *             If session is closed or internal error
     */
    @Override
    public BytesMessage createBytesMessage() throws JMSException {
        checkClosed();
        return new SQSBytesMessage();
    }

    /**
     * According to JMS specification, a message can be sent with only headers
     * without any payload, SQS does not support messages with empty payload. so
     * this method is not supported
     */
    @Override
    public Message createMessage() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /**
     * Creates a <code>ObjectMessage</code>.
     * 
     * @return new <code>ObjectMessage</code>
     * @throws JMSException
     *             If session is closed or internal error
     */
    @Override
    public ObjectMessage createObjectMessage() throws JMSException {
        checkClosed();
        return new SQSObjectMessage();
    }

    /**
     * Creates an initialized <code>ObjectMessage</code>.
     * 
     * @param object
     *            The initialized <code>ObjectMessage</code>
     * @return new <code>ObjectMessage</code>
     * @throws JMSException
     *             If session is closed or internal error
     */
    @Override
    public ObjectMessage createObjectMessage(Serializable object) throws JMSException {
        checkClosed();
        return new SQSObjectMessage(object);
    }

    /**
     * Creates a <code>TextMessage</code>.
     * 
     * @return new <code>TextMessage</code>
     * @throws JMSException
     *             If session is closed or internal error
     */
    @Override
    public TextMessage createTextMessage() throws JMSException {
        checkClosed();
        return new SQSTextMessage();
    }

    /**
     * Creates an initialized <code>TextMessage</code>.
     * 
     * @param text
     *            The initialized <code>TextMessage</code>
     * @return new <code>TextMessage</code>
     * @throws JMSException
     *             If session is closed or internal error
     */
    @Override
    public TextMessage createTextMessage(String text) throws JMSException {
        checkClosed();
        return new SQSTextMessage(text);
    }

    /**
     * Returns the acknowledge mode of the session. The acknowledge mode is set
     * at the time that the session is created.
     * 
     * @return acknowledge mode
     */
    @Override
    public int getAcknowledgeMode() throws JMSException {
        return acknowledgeMode.getOriginalAcknowledgeMode();
    }

    /**
     * Closes the session.
     * <P>
     * This will not return until all the message consumers and producers close
     * internally, which blocks until receives and/or message listeners in
     * progress have completed. A blocked message consumer receive call returns
     * null when this session is closed.
     * <P>
     * Since consumer prefetch threads use SQS long-poll feature with 20 seconds
     * timeout, closing each consumer prefetch thread can take up to 20 seconds,
     * which in-turn will impact the time on session close.
     * <P>
     * This method is safe for concurrent use.
     * <P>
     * A message listener must not attempt to close its own session; otherwise
     * throws a IllegalStateException.
     * <P>
     * Invoking any other session method on a closed session must throw a
     * <code>IllegalStateException</code>.
     * 
     * @throws IllegalStateException
     *             If called by a message listener on its own
     *             <code>Session</code>.
     * @throws JMSException
     *             On internal error.
     */
    @Override
    public void close() throws JMSException {

        if (closed) {
            return;
        }

        /**
         * A MessageListener must not attempt to close its own Session as
         * this would lead to deadlock
         */
        if (isActiveCallbackSessionThread()) {
            throw new IllegalStateException(
                    "MessageListener must not attempt to close its own Session to prevent potential deadlock issues");
        }

        doClose();
    }

    void doClose() throws JMSException {
        boolean shouldClose = false;
        synchronized (stateLock) {
            if (!closing) {
                shouldClose = true;
                closing = true;
            }
            stateLock.notifyAll();
        }

        if (closed) {
            return;
        }

        if (shouldClose) {
            try {
                parentSQSConnection.removeSession(this);

                for (SQSMessageConsumer messageConsumer : messageConsumers) {
                    messageConsumer.close();
                    /** Nack the messages that were delivered but not acked */
                    messageConsumer.recover();
                }

                try {
                    if (executor != null) {
                        LOG.info("Shutting down " + SESSION_EXECUTOR_NAME + " executor");

                        /** Shut down executor. */
                        executor.shutdown();

                        waitForCallbackComplete();

                        sqsSessionRunnable.close();

                        for (MessageProducer messageProducer : messageProducers) {
                            messageProducer.close();
                        }

                        if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {

                            LOG.warn("Can't terminate executor service " + SESSION_EXECUTOR_NAME + " after "
                                    + SESSION_EXECUTOR_GRACEFUL_SHUTDOWN_TIME
                                    + " seconds, some running threads will be shutdown immediately");
                            executor.shutdownNow();
                        }
                    }
                } catch (InterruptedException e) {
                    LOG.error("Interrupted while closing the session.", e);
                }

            } finally {
                synchronized (stateLock) {
                    closed = true;
                    running = false;
                    stateLock.notifyAll();
                }
            }
        } /** Blocks until closing of the session completes */
        else {
            synchronized (stateLock) {
                while (!closed) {
                    try {
                        stateLock.wait();
                    } catch (InterruptedException e) {
                        LOG.error("Interrupted while waiting the session to close.", e);
                    }
                }
            }
        }
    }

    /**
     * Negative acknowledges all the messages on the session that is delivered
     * but not acknowledged.
     * 
     * @throws JMSException
     *             If session is closed or on internal error.
     */
    @Override
    public void recover() throws JMSException {
        checkClosed();
        for (SQSMessageConsumer messageConsumer : messageConsumers) {
            messageConsumer.recover();
        }
    }

    @Override
    public void run() {
    }

    /**
     * Creates a <code>MessageProducer</code> for the specified destination.
     * Only queue destinations are supported at this time.
     * 
     * @param destination
     *            a queue destination
     * @return new message producer
     * @throws JMSException
     *             If session is closed or queue destination is not used
     */
    @Override
    public MessageProducer createProducer(Destination destination) throws JMSException {
        checkClosed();
        if (destination != null && !(destination instanceof SQSQueueDestination)) {
            throw new JMSException("Actual type of Destination/Queue has to be SQSQueueDestination");
        }
        SQSMessageProducer messageProducer;
        synchronized (stateLock) {
            checkClosing();
            messageProducer = new SQSMessageProducer(amazonSQSClient, this, (SQSQueueDestination) destination);
            messageProducers.add(messageProducer);
        }
        return messageProducer;
    }

    /**
     * Creates a <code>MessageConsumer</code> for the specified destination.
     * Only queue destinations are supported at this time.
     * 
     * @param destination
     *            a queue destination
     * @return new message consumer
     * @throws JMSException
     *             If session is closed or queue destination is not used
     */
    @Override
    public MessageConsumer createConsumer(Destination destination) throws JMSException {
        checkClosed();
        if (!(destination instanceof SQSQueueDestination)) {
            throw new JMSException("Actual type of Destination/Queue has to be SQSQueueDestination");
        }
        SQSMessageConsumer messageConsumer;
        synchronized (stateLock) {
            checkClosing();
            messageConsumer = createSQSMessageConsumer((SQSQueueDestination) destination);
            messageConsumers.add(messageConsumer);
            if (running) {
                messageConsumer.startPrefetch();
            }
        }
        return messageConsumer;
    }

    SQSMessageConsumer createSQSMessageConsumer(SQSQueueDestination destination) {
        return new SQSMessageConsumer(parentSQSConnection, this, sqsSessionRunnable,
                (SQSQueueDestination) destination, acknowledger, new NegativeAcknowledger(amazonSQSClient),
                CONSUMER_PREFETCH_THREAD_FACTORY);
    }

    /**
     *
     * Creates a <code>MessageConsumer</code> for the specified destination.
     * Only queue destinations are supported at this time.
     * It will ignore any argument in messageSelector.
     * 
     * @param destination
     *            a queue destination
     * @param messageSelector           
     * @return new message consumer
     * @throws JMSException
     *             If session is closed or queue destination is not used
     */

    @Override
    public MessageConsumer createConsumer(Destination destination, String messageSelector) throws JMSException {
        if (messageSelector != null) {
            throw new JMSException("SQSSession does not support MessageSelector. This should be null.");
        }
        return createConsumer(destination);
    }

    /**
     * Creates a <code>MessageConsumer</code> for the specified destination.
     * Only queue destinations are supported at this time. It will ignore any
     * argument in messageSelector and NoLocal.
     * 
     * @param destination
     *            a queue destination
     * @param messageSelector
     * @param NoLocal
     * @return new message consumer
     * @throws JMSException
     *             If session is closed or queue destination is not used
     */
    @Override
    public MessageConsumer createConsumer(Destination destination, String messageSelector, boolean NoLocal)
            throws JMSException {
        if (messageSelector != null) {
            throw new JMSException("SQSSession does not support MessageSelector. This should be null.");
        }
        return createConsumer(destination);
    }

    /**
     * This does not create SQS Queue. This method is only to create JMS Queue Object.
     * Make sure the queue exists corresponding to the queueName.
     * @param queueName
     * @return a queue destination
     * @throws JMSException
     *         If session is closed or invalid queue is provided
     */
    @Override
    public Queue createQueue(String queueName) throws JMSException {
        checkClosed();
        return new SQSQueueDestination(queueName, amazonSQSClient.getQueueUrl(queueName).getQueueUrl());
    }

    /**
     * This does not create SQS Queue. This method is only to create JMS Queue
     * Object. Make sure the queue exists corresponding to the queueName and
     * ownerAccountId.
     * 
     * @param queueName
     * @param ownerAccountId
     *            the account id, which originally created the queue on SQS
     * @return a queue destination
     * @throws JMSException
     *             If session is closed or invalid queue is provided
     */
    public Queue createQueue(String queueName, String ownerAccountId) throws JMSException {
        checkClosed();
        return new SQSQueueDestination(queueName,
                amazonSQSClient.getQueueUrl(queueName, ownerAccountId).getQueueUrl());
    }

    /**
     * This is used in MessageConsumer. When MessageConsumer is closed
     * it will remove itself from list of consumers.
     */
    void removeConsumer(SQSMessageConsumer consumer) {
        messageConsumers.remove(consumer);
    }

    /**
     * This is used in MessageProducer. When MessageProducer is closed
     * it will remove itself from list of producers.
     */
    void removeProducer(SQSMessageProducer producer) {
        messageProducers.remove(producer);
    }

    void startingCallback(SQSMessageConsumer consumer) throws InterruptedException, JMSException {
        if (closed) {
            return;
        }
        synchronized (stateLock) {
            if (activeConsumerInCallback != null) {
                throw new IllegalStateException("Callback already in progress");
            }
            assert activeCallbackSessionThread == null;

            while (!running && !closing) {
                try {
                    stateLock.wait();
                } catch (InterruptedException e) {
                    LOG.warn("Interrupted while waiting on session start. Continue to wait...", e);
                }
            }
            checkClosing();
            activeConsumerInCallback = consumer;
            activeCallbackSessionThread = Thread.currentThread();
        }
    }

    void finishedCallback() throws JMSException {
        synchronized (stateLock) {
            if (activeConsumerInCallback == null) {
                throw new IllegalStateException("Callback not in progress");
            }
            activeConsumerInCallback = null;
            activeCallbackSessionThread = null;
            stateLock.notifyAll();
        }
    }

    void waitForConsumerCallbackToComplete(SQSMessageConsumer consumer) throws InterruptedException {
        synchronized (stateLock) {
            while (activeConsumerInCallback == consumer) {
                try {
                    stateLock.wait();
                } catch (InterruptedException e) {
                    LOG.warn(
                            "Interrupted while waiting the active consumer in callback to complete. Continue to wait...",
                            e);
                }
            }
        }
    }

    void waitForCallbackComplete() {
        synchronized (stateLock) {
            while (activeConsumerInCallback != null) {
                try {
                    stateLock.wait();
                } catch (InterruptedException e) {
                    LOG.warn("Interrupted while waiting on session callback completion. Continue to wait...", e);
                }
            }
        }
    }

    /** SQS does not support transacted. Transacted will always be false. */
    @Override
    public boolean getTransacted() throws JMSException {
        return false;
    }

    /** This method is not supported. This method is related to transaction which SQS doesn't support */
    @Override
    public void commit() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. This method is related to transaction which SQS doesn't support */
    @Override
    public void rollback() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. This method is related to Topic which SQS doesn't support */
    @Override
    public void unsubscribe(String name) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public Topic createTopic(String topicName) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public TopicSubscriber createDurableSubscriber(Topic topic, String name) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public TopicSubscriber createDurableSubscriber(Topic topic, String name, String messageSelector,
            boolean noLocal) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public QueueBrowser createBrowser(Queue queue) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public QueueBrowser createBrowser(Queue queue, String messageSelector) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public TemporaryQueue createTemporaryQueue() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public TemporaryTopic createTemporaryTopic() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public MessageListener getMessageListener() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public void setMessageListener(MessageListener listener) throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public StreamMessage createStreamMessage() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    /** This method is not supported. */
    @Override
    public MapMessage createMapMessage() throws JMSException {
        throw new JMSException(SQSMessagingClientConstants.UNSUPPORTED_METHOD);
    }

    static class CallbackEntry {
        private final MessageListener messageListener;
        private final MessageManager messageManager;

        CallbackEntry(MessageListener messageListener, MessageManager messageManager) {
            this.messageListener = messageListener;
            this.messageManager = messageManager;
        }

        public MessageListener getMessageListener() {
            return messageListener;
        }

        public MessageManager getMessageManager() {
            return messageManager;
        }
    }

    /**
     * Check if session is closed.
     */
    public void checkClosed() throws IllegalStateException {
        if (closed) {
            throw new IllegalStateException("Session is closed");
        }
    }

    /**
     * Check if session is closed or closing.
     */
    public void checkClosing() throws IllegalStateException {
        if (closing) {
            throw new IllegalStateException("Session is closed or closing");
        }
    }

    void start() throws IllegalStateException {
        checkClosed();
        synchronized (stateLock) {
            checkClosing();
            running = true;
            for (SQSMessageConsumer messageConsumer : messageConsumers) {
                messageConsumer.startPrefetch();
            }
            stateLock.notifyAll();
        }
    }

    void stop() throws IllegalStateException {
        checkClosed();
        synchronized (stateLock) {
            checkClosing();
            running = false;
            for (SQSMessageConsumer messageConsumer : messageConsumers) {
                messageConsumer.stopPrefetch();
            }
            waitForCallbackComplete();

            stateLock.notifyAll();
        }
    }

    /*
     * Unit Tests Utility Functions
     */

    boolean isCallbackActive() {
        return activeConsumerInCallback != null;
    }

    void setActiveConsumerInCallback(SQSMessageConsumer consumer) {
        activeConsumerInCallback = consumer;
    }

    Object getStateLock() {
        return stateLock;
    }

    boolean isClosed() {
        return closed;
    }

    boolean isClosing() {
        return closing;
    }

    void setClosed(boolean closed) {
        this.closed = closed;
    }

    void setClosing(boolean closing) {
        this.closing = closing;
    }

    void setRunning(boolean running) {
        this.running = running;
    }

    boolean isRunning() {
        return running;
    }

    SQSSessionCallbackScheduler getSqsSessionRunnable() {
        return sqsSessionRunnable;
    }
}