/*
* JBoss, Home of Professional Open Source
* Copyright 2006, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This 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 software 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 software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.soa.esb.listeners.message;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.couriers.PickUpOnlyCourier;
import org.jboss.soa.esb.ConfigurationException;
import org.jboss.soa.esb.addressing.EPR;
import org.jboss.soa.esb.addressing.MalformedEPRException;
import org.jboss.soa.esb.common.TransactionStrategy;
import org.jboss.soa.esb.common.TransactionStrategyException;
import org.jboss.soa.esb.couriers.CourierException;
import org.jboss.soa.esb.couriers.CourierFactory;
import org.jboss.soa.esb.couriers.CourierTimeoutException;
import org.jboss.soa.esb.couriers.CourierUtil;
import org.jboss.soa.esb.couriers.FaultMessageException;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.listeners.ListenerTagNames;
import org.jboss.soa.esb.listeners.ListenerUtil;
import org.jboss.soa.esb.listeners.RegistryUtil;
import org.jboss.soa.esb.listeners.lifecycle.AbstractThreadedManagedLifecycle;
import org.jboss.soa.esb.listeners.lifecycle.ManagedLifecycleException;
import org.jboss.soa.esb.listeners.lifecycle.ManagedLifecycleThreadState;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.services.registry.RegistryException;
import org.jboss.soa.esb.util.Util;
/**
* Esb Message aware transport independent listener. <p/> Relies on the
* CourierFactory to obtain an appropriate Courier for the EPR this listener
* will be listening on <br/>Keeps a thread pool to instantiate
* ActionProcessingPipelines whenever a Message is received
*
* @author <a
* href="mailto:schifest@heuristica.com.ar">schifest@heuristica.com.ar</a>
* @since Version 4.0
*/
public class MessageAwareListener extends AbstractThreadedManagedLifecycle
{
/**
* The minimum error delay.
*/
private static final long MIN_ERROR_DELAY = 1000 ;
/**
* The maximum error delay.
*/
private static final long MAX_ERROR_DELAY = (MIN_ERROR_DELAY << 5) ;
/**
* The action pipeline.
*/
private ActionProcessingPipeline pipeline ;
/**
* The error delay.
*/
private long errorDelay ;
private TransactionStrategy transactionStrategy;
private boolean transactional = false;
private boolean rollbackOnPipelineFaults = true;
/**
* public constructor
*
* @param config
* ConfigTree - Containing 'static' configuration for this
* instance
* @throws ConfigurationException
*/
public MessageAwareListener(final ConfigTree config)
throws ConfigurationException
{
super(config);
_config = config ;
checkMyParms() ;
}
/**
* Check for mandatory and optional attributes in parameter tree
*
* @throws ConfigurationException -
* if mandatory atts are not right or actionClass not in
* classpath
*/
protected void checkMyParms () throws ConfigurationException
{
_eprCategoryName = _config.getAttribute(ListenerTagNames.SERVICE_CATEGORY_NAME_TAG);
_eprName = _config.getAttribute(ListenerTagNames.SERVICE_NAME_TAG);
final String maxThreadVal = _config.getAttribute(ListenerTagNames.MAX_THREADS_TAG) ;
/*
* TODO augment this so that per service filters can be
* registered in the configuration (and removed by the ESB when
* the service is undeployed).
*
* Do the same for gateways.
*/
if (!Util.isNullString(maxThreadVal))
{
try
{
_maxThreads = Integer.parseInt(maxThreadVal) ;
}
catch (NumberFormatException nfe)
{
_maxThreads = _defaultMaxThreads ;
_logger.warn("Invalid " + ListenerTagNames.MAX_THREADS_TAG + " attribute, defaulting to <" + _maxThreads + ">") ;
}
}
if (Util.isNullString(_eprCategoryName))
throw new ConfigurationException(
"Missing or invalid " + ListenerTagNames.SERVICE_CATEGORY_NAME_TAG);
if (Util.isNullString(_eprName))
throw new ConfigurationException(
"Missing or invalid " + ListenerTagNames.SERVICE_NAME_TAG);
ConfigTree eprElement = _config.getFirstChild(ListenerTagNames.EPR_TAG);
if (null == eprElement)
throw new ConfigurationException(
"Missing or invalid " + ListenerTagNames.EPR_TAG + " element");
_epr = ListenerUtil.assembleEpr(eprElement);
String latency = _config.getAttribute(ListenerTagNames.POLL_LATENCY_SECS_TAG);
long lSeconds = 10;
if (null != latency)
{
try
{
lSeconds = Integer.parseInt(latency);
}
catch (NumberFormatException e)
{
_logger.warn("Invalid number format <" + latency + "> using default value (" + lSeconds + ")");
}
}
_latencySecs = lSeconds ;
transactional = _config.getBooleanAttribute(ListenerTagNames.TRANSACTED_TAG, false) ;
transactionStrategy = TransactionStrategy.getTransactionStrategy(transactional) ;
rollbackOnPipelineFaults = _config.getBooleanAttribute(ListenerTagNames.ROLLBACK_ON_PIPELINE_FAULTS, true);
}
/**
* Handle the initialisation of the managed instance.
*
* @throws ManagedLifecycleException for errors while initialisation.
*/
protected void doInitialise()
throws ManagedLifecycleException
{
final ActionProcessingPipeline pipeline ;
try
{
pipeline = new ActionProcessingPipeline(_config) ;
pipeline.setTransactional(transactional);
pipeline.initialise() ;
}
catch (final ConfigurationException ce)
{
throw new ManagedLifecycleException("Error configuring action processing pipeline", ce) ;
}
try
{
RegistryUtil.register(_config, _epr);
}
catch (final RegistryException re)
{
throw new ManagedLifecycleException("Unexpected error during registration for epr " + _epr, re);
}
this.pipeline = pipeline ;
final PickUpOnlyCourier pickUpCourier ;
try
{
pickUpCourier = getCourier() ;
cleanCourier(pickUpCourier) ;
}
catch (final MalformedEPRException mepre)
{
RegistryUtil.unregister(_eprCategoryName, _eprName, _epr) ;
throw new ManagedLifecycleException("Malformed EPR: " + _epr) ;
}
catch (final CourierException ce)
{
RegistryUtil.unregister(_eprCategoryName, _eprName, _epr) ;
throw new ManagedLifecycleException("No appropriate courier can be obtained for " + _epr, ce);
}
}
/**
* Handle the start of the managed instance.
*
* @throws ManagedLifecycleException for errors while starting.
*/
protected void doStart()
throws ManagedLifecycleException
{
checkExecutorTermination() ;
_execService = Executors.newFixedThreadPool(_maxThreads) ;
super.doStart() ;
}
/**
* Execute on the thread.
*/
protected void doRun()
{
if (_logger.isDebugEnabled())
{
_logger.debug("doRun() method of " + this.getClass().getSimpleName()
+ " started on thread " + Thread.currentThread().getName());
}
while (isRunning())
{
// Only pickup a message when a thread is available
if (waitForThread(_pauseLapseInMillis))
{
waitForEventAndProcess(100) ;
}
}
if (_logger.isDebugEnabled())
{
_logger.debug("run() method of " + this.getClass().getSimpleName()
+ " finished on thread " + Thread.currentThread().getName());
}
}
/**
* Handle the stop of the managed instance.
*
* @throws ManagedLifecycleException for errors while stopping.
*/
protected void doStop()
throws ManagedLifecycleException
{
super.doStop();
_execService.shutdown() ;
}
/**
* We have JMS transactional delivery/work semantics: before pulling a unit of work
* we start a transaction. If the pipeline completes successfully then we will
* commit that transaction and the OUW will be deleted. If we have to roll back
* the transaction then the UOW will be placed back on the input "queue" (assumes that
* the courier is transactional).
*
* @param maxWaitMillis
*/
public void waitForEventAndProcess (long maxWaitMillis)
{
Message message = null ;
boolean problem = false;
PickUpOnlyCourier pickUpCourier = null ;
try
{
transactionStrategy.begin();
pickUpCourier = getCourier() ;
message = (maxWaitMillis > 0) ? pickUpCourier
.pickup(maxWaitMillis) : null;
errorDelay = 0 ;
}
catch (TransactionStrategyException ex)
{
_logger.error("Could not begin transaction!");
problem = true;
return;
}
catch (MalformedEPRException e)
{
problem = true;
return;
}
catch (CourierTimeoutException e)
{
problem = true;
return;
}
catch (FaultMessageException fme)
{
message = fme.getReturnedMessage() ;
}
catch (CourierException e)
{
_logger.debug("Courier Exception", e);
if (errorDelay == 0)
{
errorDelay = MIN_ERROR_DELAY ;
}
else if (errorDelay < MAX_ERROR_DELAY)
{
errorDelay <<= 1 ;
}
_logger.warn("Error processing courier, backing off for " + errorDelay + " milliseconds") ;
boolean waitForRunningStateChange = waitForRunningStateChange(ManagedLifecycleThreadState.STOPPING, errorDelay) ;
_logger.info("State reached : " + waitForRunningStateChange);
problem = true;
return;
}
finally
{
if (problem || (message == null))
{
cleanCourier(pickUpCourier) ;
rollbackTransaction();
}
}
if (null != message)
{
final Message pipelineMessage = message ;
final TransactionalRunner txRunner ;
try
{
final Object txHandle = transactionStrategy.suspend();
txRunner = new TransactionalRunner(pickUpCourier, pipelineMessage, txHandle);
}
catch (TransactionStrategyException ex)
{
_logger.warn("Caught transaction related exception: ", ex);
cleanCourier(pickUpCourier);
rollbackTransaction();
return ;
}
updateThreadCount(+1);
try
{
_execService.execute(txRunner);
}
catch (final RejectedExecutionException ree)
{
txRunner.run() ;
}
}
} // ________________________________
/**
* Handle the threaded destroy of the managed instance.
*
* @throws ManagedLifecycleException for errors while destroying.
*/
protected void doThreadedDestroy()
throws ManagedLifecycleException
{
try
{
checkExecutorTermination() ;
}
catch (final ManagedLifecycleException ex)
{
throw ex;
}
catch (final Throwable ex)
{
_logger.warn("Caught throwable during shutdown: "+ex);
}
pipeline.destroy() ;
pipeline = null ;
CourierUtil.cleanCourier(_pickUpCourier);
RegistryUtil.unregister(_eprCategoryName, _eprName, _epr) ;
}
/**
* Check that the existing executor has been closed down.
* @throws ManagedLifecycleException If executor tasks are still active.
*/
private void checkExecutorTermination()
throws ManagedLifecycleException
{
if (_execService != null)
{
try
{
try
{
if (!_execService.awaitTermination(getTerminationPeriod(), TimeUnit.MILLISECONDS))
{
throw new ManagedLifecycleException("Tasks remain active in executor") ;
}
}
catch (final InterruptedException ie)
{
throw new ManagedLifecycleException("Interrupted waiting for active tasks to terminate") ;
}
}
finally
{
_execService = null ;
}
}
}
private boolean waitForThread(final long delay)
{
boolean result = true ;
synchronized(_synchThreads)
{
if (_qRunningThreads >= _maxThreads)
{
try
{
_synchThreads.wait(delay) ;
}
catch (final InterruptedException ie) {}
result = _qRunningThreads < _maxThreads ;
}
}
return result ;
}
private void updateThreadCount(Integer i)
{
synchronized (_synchThreads)
{
_qRunningThreads += i.intValue();
if (_qRunningThreads < _maxThreads)
{
_synchThreads.notifyAll() ;
}
}
}
private void rollbackTransaction ()
{
try
{
transactionStrategy.rollbackOnly();
transactionStrategy.terminate();
}
catch (Throwable ex)
{
_logger.warn("Problem while attempting to rollback transaction!"); // timeout should catch it next!
}
}
protected PickUpOnlyCourier getCourier()
throws MalformedEPRException, CourierException
{
PickUpOnlyCourier pickUpCourier = _pickUpCourier;
if (transactional || (pickUpCourier == null))
{
pickUpCourier = CourierFactory.getPickupCourier(_epr) ;
try
{
final Method setPollLatency = pickUpCourier.getClass().getMethod(
"setPollLatency", new Class[] { Long.class });
setPollLatency.invoke(pickUpCourier, new Long(1000 * _latencySecs));
}
catch (final NoSuchMethodException nsme)
{
// OK, just leave it null
}
catch (final Throwable th)
{
CourierUtil.cleanCourier(pickUpCourier);
throw new CourierException("Problems invoking setPollLatency(long)", th);
}
if (!transactional)
{
_pickUpCourier = pickUpCourier ;
}
}
return pickUpCourier;
}
private void cleanCourier(final PickUpOnlyCourier pickUpOnlyCourier)
{
if (transactional)
{
CourierUtil.cleanCourier(pickUpOnlyCourier) ;
}
}
class TransactionalRunner implements Runnable
{
public TransactionalRunner (PickUpOnlyCourier courier, Message pipelineMessage, Object txHandle)
{
_courier = courier;
_pipelineMessage = pipelineMessage;
_txHandle = txHandle;
}
public void run()
{
boolean problem = false;
try
{
if (_txHandle != null)
{
transactionStrategy.resume(_txHandle);
}
/*
* Current strategy is to commit as long as process returns true.
* If fails, or any exceptions are caught, then we roll back.
*
* TODO re-examine the semantics around true/false from the pipeline.
*/
// TODO consider adding a RollbackOnFalse option to allow override.
problem = rollbackOnPipelineFaults && !pipeline.process(_pipelineMessage);
if (!problem)
{
transactionStrategy.terminate();
}
}
catch (TransactionStrategyException ex)
{
problem = true;
_logger.warn("TransactionalRunner caught transaction exception: ", ex);
}
catch (RuntimeException ex)
{
problem = true;
throw ex;
}
catch (Throwable ex)
{
problem = true;
_logger.warn("TransactionalRunner caught throwable: ",ex);
}
finally
{
cleanCourier(_courier);
if (problem)
{
rollbackTransaction();
}
updateThreadCount(-1);
}
}
private PickUpOnlyCourier _courier;
private Message _pipelineMessage;
private Object _txHandle;
}
private ConfigTree _config;
private String _eprCategoryName;
private String _eprName;
private EPR _epr;
private int _maxThreads;
private int _defaultMaxThreads = 1;
private long _latencySecs;
private long _pauseLapseInMillis = 50 ;
private ExecutorService _execService;
private byte[] _synchThreads = new byte[0];
private int _qRunningThreads;
private Logger _logger = Logger.getLogger(MessageAwareListener.class);
private PickUpOnlyCourier _pickUpCourier;
}