/*******************************************************************************
$Source: /cvs/repositories/openii3/project/java/source/org/openeai/jms/consumer/PubSubConsumer.java,v $
$Revision: 1.19 $
*******************************************************************************/
/**********************************************************************
This file is part of the OpenEAI Application Foundation or
OpenEAI Message Object API created by Tod Jackson
(tod@openeai.org) and Steve Wheat (steve@openeai.org) at
the University of Illinois Urbana-Champaign.
Copyright (C) 2002 The OpenEAI Software Foundation
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
For specific licensing details and examples of how this software
can be used to build commercial integration software or to implement
integrations for your enterprise, visit http://www.OpenEai.org/licensing.
*/
package org.openeai.jms.consumer;
// JNDI Stuff
import javax.naming.*;
import javax.naming.directory.*;
// Java Messaging Service
import javax.jms.*;
// General
import java.util.*;
import java.io.IOException;
import java.sql.SQLException;
import org.openeai.config.*;
import org.openeai.threadpool.*;
import org.openeai.jms.consumer.commands.SyncCommand;
/**
* This consumer consumes messages from a JMS Topic. Then based on the content of the message
* it executes 'SyncCommands' associated to the consumer and the message consumed as specified
* in it's configuration document (deployment descriptor).
*<P>
* The actual business logic related to the message consumed is performed by the Sync command
* implementations.
* <P>
* @author Tod Jackson (tod@openeai.org)
* @author Steve Wheat (steve@openeai.org)
* @version 3.0 - 4 February 2003
*/
public class PubSubConsumer extends MessageConsumer {
private TopicConnectionFactory m_tcf = null;
private Topic m_topic = null;
private TopicConnection m_topicConnection = null;
private TopicSession m_topicSession = null;
private TopicSubscriber m_topicSubscriber = null;
private MyTopicListener m_listener = null;
private PubSubMessageBalancer m_balancer = null;
private boolean m_monitorRunning = false;
public PubSubConsumer() {
setAppName("Pub/Sub Consumer v1.0");
}
public PubSubConsumer(String cfName, String tName) {
setAppName("Pub/Sub Consumer v1.0");
setConnectionFactoryName(cfName);
setDestinationName(tName);
}
/**
* As AppConfig reads through a gateway's deployment document, it will build a
* ConsumerConfig Java object and pass that object to this constructor. Then
* this consumer will have all the information it needs to initialize itself which
* includes:
* <ul>
* <li>Initializing the consumer itself
* <li>Initializing all Commands that the consumer may execute
* <ul>
* <P>
* @param cConfig org.openeai.config.ConsumerConfig
* @see org.openeai.config.ConsumerConfig
**/
public PubSubConsumer(ConsumerConfig cConfig) throws JMSException,
NamingException,
IOException {
setAppName(cConfig.getAppName());
setConfig(cConfig);
setCommandConfigs(cConfig.getCommandConfigs());
if (cConfig.getThreadPoolConfig() == null) {
String errMessage =
"Could not locate a ThreadPoolConfig object in the " +
"configuration document for the PubSub Consumer named " +
getConsumerName() + ". This is required for all consumers.";
logger.fatal(errMessage);
throw new JMSException(errMessage);
}
setThreadPool(new ThreadPoolImpl(cConfig.getThreadPoolConfig()));
// set up the utility class to check if another consumer
// is/has processing this message (dbconnection info).
try {
setBalancer(new PubSubMessageBalancer(cConfig.getDbConnectionPoolConfig()));
logger.info("Initialized message balancer.");
}
catch (IOException e) {
logger.warn("Could not initialize the MessageBalancer for consumer named " +
getConsumerName() + ". " +
"All messages delivered to this Consumer will be processed. If you wish to run multiple instances of this " +
"PubSubConsumer and want the Messages balanced between instances, you must specify " +
"DbConnectionPoolConfig information for the PubSubConsumer in the configuration document. Exception: " +
e.getMessage());
setBalancer(null);
}
init(cConfig.getProperties());
if (getStartOnInitialization()) {
startConsumer();
}
}
// StartGetter/Setters
/**
* This method stops the Consumer's "Monitor Thread" so it won't attempt
* to restart the consumer.
* <P>
* When the consumer is started it starts a Thread that monitors the Consumer's
* connection to the broker. If that connection is broken for some reason, that
* "Monitor Thread" will attempt to restart the consumer. This continues indefinitely
* until the consumer is able to re-connect to the broker.
* <P>
* This method allows an application to in effect stop that monitor thread so they can
* shut the consumer down without it restarting itself.
**/
public void stopMonitor() {
m_monitorRunning = false;
}
/**
* This method starts the Consumer's "Monitor Thread". This is a thread that runs
* for the life of the consumer and checks the status of the consumer's connection
* to the broker every thirty seconds. If that connection is broken
* for some reason, the Monitor Thread will attempt to restart the consumer,
* re-connecting it to the broker. It will continue to do this until either the
* consumer is able to re-connect or the consumer is shutdown.
**/
public void startMonitor() {
if (m_monitorRunning == false) {
MonitorConsumer monitorConsumer = new MonitorConsumer(30000);
new Thread(monitorConsumer).start();
m_monitorRunning = true;
}
}
/**
* Returns the PubSubMessageBalancer associated to this Consumer.
* <P>
* @return PubSubMessageBalancer
* @see PubSubMessageBalancer
**/
public final PubSubMessageBalancer getBalancer() {
return m_balancer;
}
/**
* Sets the PubSubMessageBalancer associated to this Consumer.
* <P>
* @param balancer PubSubMessageBalancer
* @see PubSubMessageBalancer
**/
public final void setBalancer(PubSubMessageBalancer balancer) {
m_balancer = balancer;
}
/**
* Returns the Consumer's TopicConnectionFactory object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @return javax.jms.TopicConnectionFactory
**/
public final TopicConnectionFactory getTopicConnectionFactory() {
return m_tcf;
}
/**
* Sets the Consumer's TopicConnectionFactory object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @param tcf javax.jms.TopicConnectionFactory
**/
public final void setTopicConnectionFactory(TopicConnectionFactory tcf) {
m_tcf = tcf;
}
/**
* Returns the Consumer's Topic object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @return javax.jms.Topic
**/
public final Topic getTopic() {
return m_topic;
}
/**
* Sets the Consumer's Topic object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @param topic javax.jms.Topic
**/
public final void setTopic(Topic topic) {
m_topic = topic;
}
/**
* Returns the Consumer's TopicConnection object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @return javax.jms.TopicConnection
**/
public final TopicConnection getTopicConnection() {
return m_topicConnection;
}
/**
* Sets the Consumer's TopicConnection object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @param topicConnection javax.jms.TopicConnection
**/
public final void setTopicConnection(TopicConnection topicConnection) {
m_topicConnection = topicConnection;
}
/**
* Returns the Consumer's TopicSession object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @return javax.jms.TopicSession
**/
public final TopicSession getTopicSession() {
return m_topicSession;
}
/**
* Sets the Consumer's TopicSession object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @param session javax.jms.TopicSession
**/
public final void setTopicSession(TopicSession session) {
m_topicSession = session;
}
/**
* Returns the Consumer's TopicSubscriber object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @return javax.jms.TopicSubscriber
**/
public final TopicSubscriber getTopicSubscriber() {
return m_topicSubscriber;
}
/**
* Sets the Consumer's TopicSubscriber object.
* <P>
* See the JMS Specification to learn more about JMS objects.
* <P>
* @param topicSubscriber javax.jms.TopicSubscriber
**/
public final void setTopicSubscriber(TopicSubscriber topicSubscriber) {
m_topicSubscriber = topicSubscriber;
}
// End Getter/Setters
/**
* Starts the consumer making it ready to consume messages from the Topic that
* it connects to. This follows the typical JMS pattern of starting a message consumer.
* This includes:
* <ul>
* <li>Retrieving the JMS Administered objects (TopicConnectionFactory and Topic)
* from a directory server or other JNDI source
* <li>Creating a TopicConnection with the TopicConnectionFactory
* <li>Creating a TopicSession with the TopicConnection
* <li>Creating a TopicSubscriber with the TopicSession and Topic. NOTE: All OpenEAI
* PubSubConsumers establish a durable subscription when they start.
* <li>Establishing the MessageListener that will be used when messages are delivered
* to the Topic.
* </ul>
* <P>
* Additionally, this method starts the Consumer's Monitor that will monitor and
* attempt to resolve any broker connection issues encountered for the life of the
* Consumer.
* <P>
* @throws JMSException
* @throws NamingException
* @see PointToPointConsumer#startConsumer
* @see MonitorConsumer
**/
public final void startConsumer() throws NamingException, JMSException {
try {
if (getInitializationStatus().equals(NOT_INITIALIZED)) {
initializeConsumer();
}
}
catch (Exception e) {
logger.fatal(e.getMessage(), e);
throw new JMSException(e.getMessage());
}
logger.info("I'm the " + getConsumerName() + " Pub/Sub Consumer");
// Create InitialContext object
// Create InitialContext object
logger.debug("Creating InitialContext");
DirContext ic = null;
// Assume the m_providerUrl and m_initCtxFactory variables have already
// been set.
try {
ic = getInitialContext();
if (ic == null) {
throw new NamingException("Error creating initial context");
}
logger.debug("Created initial context");
}
catch (NamingException ne) {
logger.fatal(ne.getMessage(), ne);
throw new NamingException(ne.getMessage());
}
try {
logger.debug("Connected to InitialContext");
// Lookup TopicConnectionFactory and Topic names
logger.debug("Looking up topic connection factory name " +
getConnectionFactoryName());
m_tcf = (TopicConnectionFactory)ic.lookup(getConnectionFactoryName());
logger.debug("Looking up topic name " + getDestinationName());
m_topic = (Topic)ic.lookup(getDestinationName());
// Close InitialContext resources
ic.close();
}
catch (NamingException ne) {
logger.fatal(ne.getMessage(), ne);
throw new NamingException(ne.getMessage());
}
// Create and start a TopicConnection
logger.debug("Creating topic connection");
try {
if (getUserName() == null || getUserName().length() < 1) {
m_topicConnection = m_tcf.createTopicConnection();
}
else {
m_topicConnection =
m_tcf.createTopicConnection(getUserName(), getPassword());
}
logger.debug("Creating topic session");
m_topicSession =
m_topicConnection.createTopicSession(getTransacted(), TopicSession.AUTO_ACKNOWLEDGE);
// Create TopicSubscriber
logger.debug("Creating topic subscriber");
if (getInstanceName() != null && getInstanceName().length() > 0) {
m_topicSubscriber =
m_topicSession.createDurableSubscriber(m_topic, getConsumerName() +
"-" + getInstanceName());
}
else {
m_topicSubscriber =
m_topicSession.createDurableSubscriber(m_topic, getConsumerName());
}
// Listen for messages
m_listener = new MyTopicListener(m_topicSubscriber, m_topicConnection);
setConsumerStatus(STARTED);
}
catch (JMSException jmsE) {
// Don't throw exception here? Let MonitorConsumer keep trying???
String errMessage =
"Error starting consumer. Exception: " + jmsE.getMessage() +
" Will let MonitorConsumer Thread attempt to restart.";
logger.fatal(errMessage);
}
// Start the monitor if it isn't already running.
startMonitor();
return;
}
public void stop() {
stopConsumer();
shutdownCommands();
}
/**
* Attempts to cleanly shutdown the Consumer. This includes closing all JMS
* resources (TopicSubscriber, TopicSession and TopicConnection). If errors
* occur, it will log those errors as warnings. However, regardless of the
* outcome of the "clean" shutdown attempt, the consumer will be stopped. This
* method is called anytime the consumer detects connection problems to the broker
* or when the consumer receives a shutdown hook from the operating system.
* <P>
* NOTE: This method DOES NOT unsubscribe from durable subscriptions. This way,
* when the gateway is brought back up, its consumers will have any missed messages
* available for consumption. Durable subscriptions must be cleaned up manually
* if that is desired.
* <P>
* @see MonitorConsumer
* @see ConsumerShutdownHook
**/
public final void stopConsumer() {
boolean exceptionOccurred = false;
setConsumerStatus(STOPPED);
stopMonitor();
try {
if (m_topicSubscriber != null) {
m_topicSubscriber.close();
}
}
catch (Exception e) {
exceptionOccurred = true;
logger.warn("Error closing TopicSubscriber: " + e.getMessage());
}
try {
if (m_topicSession != null) {
m_topicSession.close();
}
}
catch (Exception e) {
exceptionOccurred = true;
logger.warn("Error closing TopicSession: " + e.getMessage());
}
try {
if (m_topicConnection != null) {
m_topicConnection.stop();
}
}
catch (Exception e) {
exceptionOccurred = true;
logger.warn("Error Stopping TopicConnection: " + e.getMessage());
}
try {
if (m_topicConnection != null) {
m_topicConnection.close();
}
}
catch (Exception e) {
exceptionOccurred = true;
logger.warn("Error Closing TopicConnection: " + e.getMessage());
}
if (exceptionOccurred) {
logger.info("Everything was stopped but there were exceptions.");
}
else {
logger.info("Everything was stopped successfully!");
}
m_tcf = null;
m_topic = null;
}
/**
* Shuts down all commands executed by this consumer.
**/
public void shutdownCommands() {
// (tj) 4/17/2003 - now, shutdown the commands this consumer executes.
Iterator it = m_messages.keySet().iterator();
while (it.hasNext()) {
String key = (String)it.next();
SyncCommand s = (SyncCommand)m_messages.get(key);
try {
logger.info("Shutting down SyncCommand '" + key + "'");
s.shutdown();
}
catch (Exception e) {
logger.warn("Error shutting down SyncCommand '" + key +
"' Processing will continue.");
}
}
logger.info("All SyncCommands have been shutdown.");
}
/**
* Invokes MessageConsumer.init(Properties) and adds the ConsumerShutDownHook
* for this consumer.
* <P>
* @param props Properties
* @throws IOException
* @see MessageConsumer#init(Properties)
**/
protected void init(Properties props) throws IOException {
super.init(props);
Runtime.getRuntime().addShutdownHook(new ConsumerShutdownHook());
}
/**
* Invoked by onMessage of the message listener to add this
* message to the thread pool or to start a new thread for
* processing this message. This method will block if no threads
* are available to handle this message.
* <P>
* @param messageCount the index of the message to be processed.
* @param mesg The jms message to be processed.
**/
protected void addMessageToThreadPool(int messageCount, Message mesg) {
if (consumptionStopped()) {
logger.info(getConsumerName() +
" - Cannot consume any more messages, because the consumer is being shutdown. Going to sleep.");
m_inProcessMessages = new Vector();
m_inProcessMessages.add(mesg);
try {
Thread.sleep(300000);
}
catch (Exception te) {
}
}
logger.info(getConsumerName() + " - Handling Message number: " +
messageCount);
// MessageTransaction will process the incoming message.
if (getThreadPool() != null) {
boolean keepTrying = true;
while (keepTrying) {
try {
getThreadPool().addJob(new MessageTransaction(messageCount, mesg));
keepTrying = false;
}
catch (ThreadPoolException e) {
logger.warn("ThreadPool is busy, sleeping and trying it again.");
try {
Thread.sleep(1000);
logger.info("Woke up, trying to add message to ThreadPool again...");
}
catch (Exception te) {
}
}
}
}
else {
new MessageTransaction(messageCount, mesg).run();
}
}
/**
* This is the JMS MessageListener implementation for OpenEAI PubSubConsumers. It
* uses the TopicSubscriber and TopicConnection established when the consumer was
* started to listen for messages delivered to the Topic specified in the Consumer's
* ConsumerConfig object.
* <P>
* When a message is delivered to the topic the this objects onMessage method is invoked.
* <P>
* @author Tod Jackson (tod@openeai.org)
* @version 3.0 - 28 January 2003
* @see org.openeai.config.ConsumerConfig
* @see org.openeai.config.ThreadPoolConfig
* @see org.openeai.threadpool.ThreadPool
* @see PubSubConsumer.MyTopicListener#onMessage(Message)
**/
protected class MyTopicListener implements MessageListener {
private int messageCount = 1;
public MyTopicListener(TopicSubscriber topicSub,
TopicConnection topicConn) throws JMSException {
topicSub.setMessageListener(this);
topicConn.start();
logger.info(getConsumerName() + " - Ready to receive messages...");
}
/**
*
**/
public void onMessage(Message mesg) {
try {
// Before we attempt to 'handle' the message, we need to
// check and see if anyone else that may be connected to
// the same topic is or has handled this message.
// This will be done using properties of the message
// and a lightweight db that all pub/sub consumers connect
// to.....
String msgId = mesg.getStringProperty("MESSAGE_ID");
if (msgId == null) {
msgId = mesg.getStringProperty("message_id");
if (msgId == null) {
// error, or at least have to default to something??
logger.warn("Null message id in JMS Message.");
// Could send it to a command that re-publishes it to the same
// topic with a message id...
}
}
if (getBalancer() != null) {
try {
if (getBalancer().canConsumerProcess(msgId) == false) {
// another consumer is handling this message
logger.info("Another consumer is already handling message: " +
msgId);
return;
}
}
catch (SQLException e) {
// there was an error determining if we can handle this
// message. Should we continue or not???
logger.fatal("Error determining if we can process this message. Exception: " +
e.getMessage());
}
}
addMessageToThreadPool(messageCount, mesg);
if (m_topicSession.getTransacted()) {
m_topicSession.commit();
}
messageCount++;
logger.info(getConsumerName() + " - Ready to receive messages...");
}
catch (Exception e) {
logger.fatal("Error processing message " + messageCount +
" Exception: " + e.getMessage());
logger.fatal(e.getMessage(), e);
try {
if (m_topicSession.getTransacted()) {
m_topicSession.rollback();
}
}
catch (JMSException je) {
logger.fatal("Error rolling transaction back or determining the transaction mode.");
logger.fatal(je.getMessage(), je);
}
}
}
}
/**
* This Thread will be started when the consumer receives a shutdown signal from the os.
* It is established via the Runtime.getRuntime().addShutdownHook(new ConsumerShutdownHook());
* in the init() method. The purpose of this is to allow a "clean" shutdown of the consumers
* without losing any messages that might be in progress when shutdown occurrs.
* <P>
* @author Tod Jackson
*/
protected class ConsumerShutdownHook extends Thread {
public void run() {
logger.info(getConsumerName() +
" - Consumer shutdown hook, stopping consumer");
logger.info(getConsumerName() + " - Waiting for threads to complete...");
// Stop all consumption to give our thread pool a chance to finish up.
stopConsumption();
// we'll probably only want to wait for a maximum period of time
// until we go ahead and stop the consumer regardless of threads
// in progress??
if (getThreadPool() != null) {
int elapsedWaitTime = 0;
while (getThreadPool().getJobsInProgress() > 0 &&
elapsedWaitTime < getMaximumThreadPoolShutdownWaitTime()) {
try {
elapsedWaitTime += 500;
Thread.sleep(500);
}
catch (Exception e) {
}
}
}
if (getThreadPool() != null && getThreadPool().getJobsInProgress() > 0) {
logger.warn("There are still " + getThreadPool().getJobsInProgress() +
" threads in process. However, the maximum time to wait for the " +
"thread pool to empty (" + getMaximumThreadPoolShutdownWaitTime() +
" milliseconds) has expired. Consumer shutdown is continuing.");
}
else {
logger.info(getConsumerName() + " - All threads are complete.");
}
// Process any messages that were in process when shutdown started.
if (m_inProcessMessages != null) {
logger.info(getConsumerName() + " - Processing " +
m_inProcessMessages.size() +
" messages that were in progress when shutdown was started.");
for (int i = 0; i < m_inProcessMessages.size(); i++) {
Message msg = (Message)m_inProcessMessages.get(i);
new MessageTransaction(0, msg).run();
}
}
stopConsumer();
logger.info(getConsumerName() +
" - Consumer shutdown hook, consumer stopped, now exiting.");
}
}
/**
* This Thread will sleep for a specified period of time and then wake up
* and check the status of the consumer by attempting to create/delete a TemporaryTopic.
* If the creation of the TemporaryTopic fails, it assumes there is something wrong
* with the consumer's connection to the broker and it is not consuming any messages.
* When that happens, it attempts to do a "clean" shutdown on the consumer and
* then restarts the consumer which will re-establish its connection to the broker
* and it will start consuming messages again. This means, if brokers must be taken down
* for any reason (on purpose or not), gateways will NOT need to be restarted
* when the broker comes back up, rather, they will do that themselves. This process
* continues until it is stopped via the stopMonitor method or until the gateway
* is stopped. If a broker is down for an extended period of time, this Monitor
* will continue to try and reconnect until the broker is back online.
* <P>
* The thread is started when the consumer is started the first time.
* <P>
* @author Tod Jackson
* @see #stopMonitor
*/
protected class MonitorConsumer implements java.lang.Runnable {
private int m_sleepInterval = 30000; // thirty seconds
public MonitorConsumer(int sleepInterval) {
m_sleepInterval = sleepInterval;
}
private boolean restartConsumer() {
if (consumptionStopped() == false) {
stopConsumer();
try {
startConsumer();
}
catch (Exception e1) {
logger.fatal("Error restarting consumer. Exception: " +
e1.getMessage());
}
return true;
}
else {
logger.info("Consumer is being shutdown, won't attempt to restart in MonitorConsumer.");
return false;
}
}
public void run() {
// sleep for m_sleepInterval
// wake up, try to do something with the session
// if an exception occurs, restart the consumer
boolean stayAlive = true;
while (stayAlive) {
try {
Thread.sleep(m_sleepInterval);
if (m_monitorRunning == false) {
logger.info("Monitor has been stopped. Returning from Monitor Thread.");
return;
}
}
catch (Exception e) {
logger.fatal("Error sleeping...");
}
// wake up and try to access the session
try {
if (m_topicSession != null) {
TemporaryTopic tb = m_topicSession.createTemporaryTopic();
tb.delete();
logger.debug("Session is okay.");
}
else {
logger.fatal("Session is null, need to restart the consumer.");
stayAlive = restartConsumer();
}
}
catch (JMSException e) {
logger.fatal("Session is not usable, need to restart the consumer. Exception: " +
e.getMessage());
stayAlive = restartConsumer();
}
}
}
}
}