com.shopzilla.spring.messaging.jms.mdp.batch.BatchMessageListenerContainer.java Source code

Java tutorial

Introduction

Here is the source code for com.shopzilla.spring.messaging.jms.mdp.batch.BatchMessageListenerContainer.java

Source

/*
 *
 * Copyright (C) 2010 Shopzilla, Inc
 *
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *
 *
 * http://tech.shopzilla.com
 *
 *
 */
package com.shopzilla.spring.messaging.jms.mdp.batch;

import java.util.ArrayList;
import java.util.Collection;

import javax.jms.Connection;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.Session;

import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.DisposableBean;

import org.springframework.context.Lifecycle;

import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;

import org.springframework.jms.listener.AbstractMessageListenerContainer;
import org.springframework.jms.support.JmsUtils;
import org.springframework.jms.support.destination.JmsDestinationAccessor;

/**
 * Modelled on Spring's {@link org.springframework.jms.listener.AbstractMessageListenerContainer} this supports the batching of
 * messages before being sent to an {@link com.shopzilla.spring.messaging.jms.mdp.batch.BatchMessageListener} with a timeout period to ensure
 * timely delivery of messages when a batch is incomplete.
 *
 * <p>
 * This listener container will only commit when all messages in a batch are consumed successfully;
 * this is different from any of Spring's implementations which commit per message.
 * </p>
 *
 * <p>
 * This listener currently supports only a single worker thread which polls for messages; this is
 * necessary to ensure that an incomplete batch can be sent when a timeout expires.
 * </p>
 *
 * @author Tim Morrow
 * @since Sep 27, 2006
 */
public class BatchMessageListenerContainer extends JmsDestinationAccessor implements DisposableBean, Lifecycle {

    static final org.apache.commons.logging.Log log = LogFactory.getLog(BatchMessageListenerContainer.class);

    /**
     * Default length of time to wait after receiving last message before decided to send a batch.
     */
    static final int DEFAULT_QUIET_PERIOD = 1000;

    /**
     * Default length of time to spend batching messages even if quiet period is never reached.
     *
     * <p>
     * After this time, all received messages will be passed to the listener. This ensures that even
     * if messages are continuously received such that there is never a quiet period, we may still
     * send the batch of messages before receiving a complete batch if it is taking too long.
     * </p>
     */
    static final int DEFAULT_BATCH_TIMEOUT = 5000;

    /**
     * Default length of time to spend waiting for a single message. Used to block until a message
     * is received or timeout so that we can evaluate whether to send the batch or check if this
     * container has been requested to be stopped.
     */
    static final int DEFAULT_RECEIVE_TIMEOUT = 1000;

    /** The default batch size. */
    private static final int DEFAULT_BATCH_SIZE = 1;

    int batchSize = DEFAULT_BATCH_SIZE;
    int batchTimeout = DEFAULT_BATCH_TIMEOUT;

    String destinationName;
    BatchMessageListener messageListener;
    int quietPeriod = DEFAULT_QUIET_PERIOD;
    int receiveTimeout = DEFAULT_RECEIVE_TIMEOUT;
    private boolean running = false;
    private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();

    private Worker worker;

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();

        if (destinationName == null) {
            throw new IllegalStateException("destinationName is required");
        }

        start();
    }

    public void destroy() {
        stop();
        destroyListener();
    }

    public Throwable getFailure() {
        return worker.getFailure();
    }

    public boolean isFailure() {
        return worker.isFailure();
    }

    public boolean isRunning() {
        return running;
    }

    /**
     * Specifies the batch size.
     *
     * @param batchSize
     *        the number of messages to receive in a batch; the default is 1
     */
    public void setBatchSize(int batchSize) {
        this.batchSize = batchSize;
    }

    /**
     * Specifies the greatest length of time, in milliseconds, that should be spent accumulating a
     * batch of messages.
     *
     * <p>
     * After this time, any received messages will be sent to the listener, regardless of whether it
     * makes a full batch.
     * </p>
     *
     * @param batchTimeout
     *        the timeout in ms; the default is 5000 ms (5 seconds)
     */
    public void setBatchTimeout(int batchTimeout) {
        this.batchTimeout = batchTimeout;
    }

    /**
     * Specifies a destination name to listen on.
     *
     * <p>
     * This is resolved at runtime.
     * </p>
     *
     * @param destinationName
     *        the name of the destination to listen on
     *
     * @see #resolveDestinationName(Session, String)
     */
    public void setDestinationName(String destinationName) {
        this.destinationName = destinationName;
    }

    /**
     * Specifies the message listener to pass batches of messages to.
     *
     * @param messageListener
     *        the message listener
     */
    public void setMessageListener(BatchMessageListener messageListener) {
        this.messageListener = messageListener;
    }

    /**
     * Specifies the quiet period.
     *
     * <p>
     * If no message is received for this length of time, the batch is sent.
     * </p>
     *
     * @param quietPeriod
     *        the quiet period; the default is 1000 ms (1 second)
     */
    public void setQuietPeriod(int quietPeriod) {
        this.quietPeriod = quietPeriod;
    }

    public void start() {
        running = true;
        registerListener();
        if (log.isInfoEnabled()) {
            log.info("listener container  started");
        }
    }

    public void stop() {
        try {
            doStop();
            if (log.isInfoEnabled()) {
                log.info("listener container stopped");
            }
        } catch (JMSException e) {
            throw convertJmsAccessException(e);
        }
    }

    void doStop() throws JMSException {
        destroyListener();
        running = false;
    }

    private void destroyListener() {
        worker.stop();
    }

    private void registerListener() {
        worker = new Worker();
        taskExecutor.execute(worker);
    }

    class Worker implements Runnable {

        // connection, session, and consumer are effectively thread-confined to the thread calling
        // run()
        private Connection connection;
        private Session session;
        private MessageConsumer consumer;

        private Throwable failure; // guarded by "this"
        private volatile boolean stopRequested = false;

        public void run() {

            try {

                try {

                    while (!this.stopRequested) {

                        boolean error = false;

                        try {

                            initJms();

                            final Collection<Message> messages = new ArrayList<Message>();
                            Message msg;

                            // Loop until we have a batch or we have taken long enough
                            final long start = System.currentTimeMillis();
                            long lastMessageReceived = System.currentTimeMillis();
                            while ((messages.size() < BatchMessageListenerContainer.this.batchSize)
                                    && ((System.currentTimeMillis()
                                            - lastMessageReceived) < BatchMessageListenerContainer.this.quietPeriod)
                                    && ((System.currentTimeMillis()
                                            - start) < BatchMessageListenerContainer.this.batchTimeout)) {
                                msg = this.consumer.receive(BatchMessageListenerContainer.this.receiveTimeout);
                                if (msg != null) {
                                    messages.add(msg);
                                    lastMessageReceived = System.currentTimeMillis();
                                }
                            }

                            if (!messages.isEmpty()) {
                                final long elapsed = System.currentTimeMillis() - start;
                                if (BatchMessageListenerContainer.log.isDebugEnabled()) {
                                    BatchMessageListenerContainer.log.debug(
                                            "Received a total of " + messages.size() + " in " + elapsed + " ms");
                                }

                                BatchMessageListenerContainer.this.messageListener.onMessages(messages);

                                if (isSessionTransacted()) {
                                    this.session.commit();
                                } else if (getSessionAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE) {
                                    for (Message message : messages) {
                                        message.acknowledge();
                                    }
                                }

                            }

                        } catch (JMSException e) {
                            error = true;
                            BatchMessageListenerContainer.log.error("Error listening for logging messages", e);
                        }

                        if (error) {
                            closeJms();
                            // if we're down, don't spin in a tight loop, but sleep in between
                            // checking
                            Thread.sleep(BatchMessageListenerContainer.this.batchTimeout);
                        }

                    }

                } finally {
                    closeJms();
                }

            } catch (InterruptedException e) {

                synchronized (this) {
                    this.failure = e;
                }

                BatchMessageListenerContainer.log.error("Worker thread interrupted", e);

                // reset the interrupted status
                Thread.currentThread().interrupt();

            } catch (Throwable e) {
                synchronized (this) {
                    this.failure = e;
                }

                BatchMessageListenerContainer.log.error("Error in Worker thread", e);
            }

        }

        private void initJms() throws JMSException {

            if (this.connection == null) {
                try {
                    this.connection = getConnectionFactory().createConnection();
                    this.connection.start();
                } catch (JMSException e) {
                    JmsUtils.closeConnection(this.connection);
                    this.connection = null;
                    throw e;
                }
            }

            if (this.session == null && this.connection != null) {
                this.session = this.connection.createSession(isSessionTransacted(), getSessionAcknowledgeMode());
            }

            if (this.consumer == null && this.session != null) {
                this.consumer = this.session.createConsumer(
                        resolveDestinationName(this.session, BatchMessageListenerContainer.this.destinationName));
            }
        }

        private void closeJms() {
            if (consumer != null) {
                JmsUtils.closeMessageConsumer(consumer);
                this.consumer = null;
            }

            if (this.session != null) {
                JmsUtils.closeSession(session);
                this.session = null;
            }

            if (this.connection != null) {
                JmsUtils.closeConnection(connection);
                this.connection = null;
            }
        }

        public void stop() {
            this.stopRequested = true;
        }

        synchronized Throwable getFailure() {
            return this.failure;
        }

        synchronized boolean isFailure() {
            return this.failure != null;
        }
    }
}