/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file 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.ejb.plugins;
import org.jboss.invocation.Invocation;
import org.jboss.invocation.InvocationType;
import org.jboss.metadata.BeanMetaData;
import org.jboss.metadata.MetaData;
import org.jboss.metadata.XmlLoadable;
import org.jboss.tm.JBossTransactionRolledbackException;
import org.jboss.tm.JBossTransactionRolledbackLocalException;
import org.jboss.tm.TransactionTimeoutConfiguration;
import org.jboss.util.NestedException;
import org.jboss.util.deadlock.ApplicationDeadlockException;
import org.w3c.dom.Element;
import javax.ejb.EJBException;
import javax.ejb.TransactionRequiredLocalException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionRequiredException;
import javax.transaction.TransactionRolledbackException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Iterator;
import java.util.ArrayList;
/**
* This interceptor handles transactions for CMT beans.
*
* @author <a href="mailto:rickard.oberg@telkel.com">Rickard Oberg</a>
* @author <a href="mailto:marc.fleury@telkel.com">Marc Fleury</a>
* @author <a href="mailto:sebastien.alborini@m4x.org">Sebastien Alborini</a>
* @author <a href="mailto:akkerman@cs.nyu.edu">Anatoly Akkerman</a>
* @author <a href="mailto:osh@sparre.dk">Ole Husgaard</a>
* @author <a href="mailto:bill@jboss.org">Bill Burke</a>
* @version $Revision: 81030 $
*/
public class TxInterceptorCMT extends AbstractTxInterceptor implements XmlLoadable
{
// Constants -----------------------------------------------------
public static int MAX_RETRIES = 5;
public static Random random = new Random();
// Attributes ----------------------------------------------------
/**
* Whether an exception should be thrown if the transaction is not
* active, even though the application doesn't throw an exception
*/
private boolean exceptionRollback = true;
private TxRetryExceptionHandler[] retryHandlers = null;
// Static --------------------------------------------------------
/**
* Detects exception contains is or a ApplicationDeadlockException.
*/
public static ApplicationDeadlockException isADE(Throwable t)
{
while (t!=null)
{
if (t instanceof ApplicationDeadlockException)
{
return (ApplicationDeadlockException)t;
}
else if (t instanceof RemoteException)
{
t = ((RemoteException)t).detail;
}
else if (t instanceof EJBException)
{
t = ((EJBException)t).getCausedByException();
}
else
{
return null;
}
}
return null;
}
// Constructors --------------------------------------------------
// Public --------------------------------------------------------
// XmlLoadable implementation ------------------------------------
public void importXml(Element ielement)
{
try
{
Element element = MetaData.getOptionalChild(ielement, "retry-handlers");
if (element == null) return;
ArrayList list = new ArrayList();
Iterator handlers = MetaData.getChildrenByTagName(element, "handler");
while (handlers.hasNext())
{
Element handler = (Element)handlers.next();
String className = MetaData.getElementContent(handler).trim();
Class clazz = SecurityActions.getContextClassLoader().loadClass(className);
list.add(clazz.newInstance());
}
retryHandlers = (TxRetryExceptionHandler[])list.toArray(new TxRetryExceptionHandler[list.size()]);
}
catch (Exception ex)
{
log.warn("Unable to importXml for the TxInterceptorCMT", ex);
}
}
// Interceptor implementation ------------------------------------
public void create() throws Exception
{
super.create();
BeanMetaData bmd = getContainer().getBeanMetaData();
exceptionRollback = bmd.getExceptionRollback();
if (exceptionRollback == false)
exceptionRollback = bmd.getApplicationMetaData().getExceptionRollback();
}
public Object invokeHome(Invocation invocation) throws Exception
{
Transaction oldTransaction = invocation.getTransaction();
for (int i = 0; i < MAX_RETRIES; i++)
{
try
{
return runWithTransactions(invocation);
}
catch (Exception ex)
{
checkRetryable(i, ex, oldTransaction);
}
}
throw new RuntimeException("Unreachable");
}
/**
* This method does invocation interpositioning of tx management
*/
public Object invoke(Invocation invocation) throws Exception
{
Transaction oldTransaction = invocation.getTransaction();
for (int i = 0; i < MAX_RETRIES; i++)
{
try
{
return runWithTransactions(invocation);
}
catch (Exception ex)
{
checkRetryable(i, ex, oldTransaction);
}
}
throw new RuntimeException("Unreachable");
}
private void checkRetryable(int i, Exception ex, Transaction oldTransaction) throws Exception
{
// if oldTransaction != null this means tx was propagated over the wire
// and we cannot retry it
if (i + 1 >= MAX_RETRIES || oldTransaction != null) throw ex;
// Keep ADE check for backward compatibility
ApplicationDeadlockException deadlock = isADE(ex);
if (deadlock != null)
{
if (!deadlock.retryable()) throw deadlock;
log.debug(deadlock.getMessage() + " retrying tx " + (i + 1));
}
else if (retryHandlers != null)
{
boolean retryable = false;
for (int j = 0; j < retryHandlers.length; j++)
{
retryable = retryHandlers[j].retry(ex);
if (retryable) break;
}
if (!retryable) throw ex;
log.debug(ex.getMessage() + " retrying tx " + (i + 1));
}
else
{
throw ex;
}
Thread.sleep(random.nextInt(1 + i), random.nextInt(1000));
}
// Private ------------------------------------------------------
private void printMethod(Method m, byte type)
{
String txName;
switch(type)
{
case MetaData.TX_MANDATORY:
txName = "TX_MANDATORY";
break;
case MetaData.TX_NEVER:
txName = "TX_NEVER";
break;
case MetaData.TX_NOT_SUPPORTED:
txName = "TX_NOT_SUPPORTED";
break;
case MetaData.TX_REQUIRED:
txName = "TX_REQUIRED";
break;
case MetaData.TX_REQUIRES_NEW:
txName = "TX_REQUIRES_NEW";
break;
case MetaData.TX_SUPPORTS:
txName = "TX_SUPPORTS";
break;
default:
txName = "TX_UNKNOWN";
}
String methodName;
if(m != null)
methodName = m.getName();
else
methodName ="<no method>";
if (log.isTraceEnabled())
{
if (m != null && (type == MetaData.TX_REQUIRED || type == MetaData.TX_REQUIRES_NEW))
log.trace(txName + " for " + methodName + " timeout=" + container.getBeanMetaData().getTransactionTimeout(methodName));
else
log.trace(txName + " for " + methodName);
}
}
/*
* This method does invocation interpositioning of tx management.
*
* This is where the meat is. We define what to do with the Tx based
* on the declaration.
* The Invocation is always the final authority on what the Tx
* looks like leaving this interceptor. In other words, interceptors
* down the chain should not rely on the thread association with Tx but
* on the Tx present in the Invocation.
*
* @param remoteInvocation If <code>true</code> this is an invocation
* of a method in the remote interface, otherwise
* it is an invocation of a method in the home
* interface.
* @param invocation The <code>Invocation</code> of this call.
*/
private Object runWithTransactions(Invocation invocation) throws Exception
{
// Old transaction is the transaction that comes with the MI
Transaction oldTransaction = invocation.getTransaction();
// New transaction is the new transaction this might start
Transaction newTransaction = null;
boolean trace = log.isTraceEnabled();
if( trace )
log.trace("Current transaction in MI is " + oldTransaction);
InvocationType type = invocation.getType();
byte transType = container.getBeanMetaData().getTransactionMethod(invocation.getMethod(), type);
if ( trace )
printMethod(invocation.getMethod(), transType);
// Thread arriving must be clean (jboss doesn't set the thread
// previously). However optimized calls come with associated
// thread for example. We suspend the thread association here, and
// resume in the finally block of the following try.
Transaction threadTx = tm.suspend();
if( trace )
log.trace("Thread came in with tx " + threadTx);
try
{
switch (transType)
{
case MetaData.TX_NOT_SUPPORTED:
{
// Do not set a transaction on the thread even if in MI, just run
try
{
invocation.setTransaction(null);
return invokeNext(invocation, false);
}
finally
{
invocation.setTransaction(oldTransaction);
}
}
case MetaData.TX_REQUIRED:
{
int oldTimeout = 0;
Transaction theTransaction = oldTransaction;
if (oldTransaction == null)
{ // No tx running
// Create tx
oldTimeout = startTransaction(invocation);
// get the tx
newTransaction = tm.getTransaction();
if( trace )
log.trace("Starting new tx " + newTransaction);
// Let the method invocation know
invocation.setTransaction(newTransaction);
theTransaction = newTransaction;
}
else
{
// We have a tx propagated
// Associate it with the thread
tm.resume(oldTransaction);
}
// Continue invocation
try
{
Object result = invokeNext(invocation, oldTransaction != null);
checkTransactionStatus(theTransaction, type);
return result;
}
finally
{
if( trace )
log.trace("TxInterceptorCMT: In finally");
// Only do something if we started the transaction
if (newTransaction != null)
endTransaction(invocation, newTransaction, oldTransaction, oldTimeout);
else
tm.suspend();
}
}
case MetaData.TX_SUPPORTS:
{
// Associate old transaction with the thread
// Some TMs cannot resume a null transaction and will throw
// an exception (e.g. Tyrex), so make sure it is not null
if (oldTransaction != null)
{
tm.resume(oldTransaction);
}
try
{
Object result = invokeNext(invocation, oldTransaction != null);
if (oldTransaction != null)
checkTransactionStatus(oldTransaction, type);
return result;
}
finally
{
tm.suspend();
}
// Even on error we don't do anything with the tx,
// we didn't start it
}
case MetaData.TX_REQUIRES_NEW:
{
// Always begin a transaction
int oldTimeout = startTransaction(invocation);
// get it
newTransaction = tm.getTransaction();
// Set it on the method invocation
invocation.setTransaction(newTransaction);
// Continue invocation
try
{
Object result = invokeNext(invocation, false);
checkTransactionStatus(newTransaction, type);
return result;
}
finally
{
// We started the transaction for sure so we commit or roll back
endTransaction(invocation, newTransaction, oldTransaction, oldTimeout);
}
}
case MetaData.TX_MANDATORY:
{
if (oldTransaction == null)
{
if (type == InvocationType.LOCAL ||
type == InvocationType.LOCALHOME)
{
throw new TransactionRequiredLocalException(
"Transaction Required");
}
else
{
throw new TransactionRequiredException(
"Transaction Required");
}
}
// Associate it with the thread
tm.resume(oldTransaction);
try
{
Object result = invokeNext(invocation, true);
checkTransactionStatus(oldTransaction, type);
return result;
}
finally
{
tm.suspend();
}
}
case MetaData.TX_NEVER:
{
if (oldTransaction != null)
{
throw new EJBException("Transaction not allowed");
}
return invokeNext(invocation, false);
}
default:
log.error("Unknown TX attribute "+transType+" for method"+invocation.getMethod());
}
}
finally
{
// IN case we had a Tx associated with the thread reassociate
if (threadTx != null)
tm.resume(threadTx);
}
return null;
}
private int startTransaction(final Invocation invocation) throws Exception
{
// Get the old timeout and set any new timeout
int oldTimeout = -1;
if (tm instanceof TransactionTimeoutConfiguration)
{
oldTimeout = ((TransactionTimeoutConfiguration) tm).getTransactionTimeout();
int newTimeout = container.getBeanMetaData().getTransactionTimeout(invocation.getMethod());
tm.setTransactionTimeout(newTimeout);
}
tm.begin();
return oldTimeout;
}
private void endTransaction(final Invocation invocation, final Transaction tx, final Transaction oldTx, final int oldTimeout)
throws TransactionRolledbackException, SystemException
{
// Assert the correct transaction association
Transaction current = tm.getTransaction();
if ((tx == null && current != null) || tx.equals(current) == false)
throw new IllegalStateException("Wrong transaction association: expected " + tx + " was " + current);
try
{
// Marked rollback
if (tx.getStatus() == Status.STATUS_MARKED_ROLLBACK)
{
tx.rollback();
}
else
{
// Commit tx
// This will happen if
// a) everything goes well
// b) app. exception was thrown
tx.commit();
}
}
catch (RollbackException e)
{
throwJBossException(e, invocation.getType());
}
catch (HeuristicMixedException e)
{
throwJBossException(e, invocation.getType());
}
catch (HeuristicRollbackException e)
{
throwJBossException(e, invocation.getType());
}
catch (SystemException e)
{
throwJBossException(e, invocation.getType());
}
catch (IllegalStateException e)
{
throwJBossException(e, invocation.getType());
}
finally
{
// reassociate the oldTransaction with the Invocation (even null)
invocation.setTransaction(oldTx);
// Always drop thread association even if committing or
// rolling back the newTransaction because not all TMs
// will drop thread associations when commit() or rollback()
// are called through tx itself (see JTA spec that seems to
// indicate that thread assoc is required to be dropped only
// when commit() and rollback() are called through TransactionManager
// interface)
//tx has committed, so we can't throw txRolledbackException.
tm.suspend();
// Reset the transaction timeout (unless we didn't set it)
if (oldTimeout != -1)
tm.setTransactionTimeout(oldTimeout);
}
}
// Protected ----------------------------------------------------
/**
* Rethrow the exception as a rollback or rollback local
*
* @param e the exception
* @param type the invocation type
*/
protected void throwJBossException(Exception e, InvocationType type)
throws TransactionRolledbackException
{
// Unwrap a nested exception if possible. There is no
// point in the extra wrapping, and the EJB spec should have
// just used javax.transaction exceptions
if (e instanceof NestedException)
{
NestedException rollback = (NestedException) e;
if(rollback.getCause() instanceof Exception)
{
e = (Exception) rollback.getCause();
}
}
if (type == InvocationType.LOCAL
|| type == InvocationType.LOCALHOME)
{
throw new JBossTransactionRolledbackLocalException(e);
}
else
{
throw new JBossTransactionRolledbackException(e);
}
}
/**
* The application has not thrown an exception, but...
* When exception-on-rollback is true,
* check whether the transaction is not active.
* If it did not throw an exception anyway.
*
* @param tx the transaction
* @param type the invocation type
* @throws TransactionRolledbackException if transaction is no longer active
*/
protected void checkTransactionStatus(Transaction tx, InvocationType type)
throws TransactionRolledbackException
{
if (exceptionRollback)
{
if (log.isTraceEnabled())
log.trace("No exception from ejb, checking transaction status: " + tx);
int status = Status.STATUS_UNKNOWN;
try
{
status = tx.getStatus();
}
catch (Throwable t)
{
log.debug("Ignored error trying to retrieve transaction status", t);
}
if (status != Status.STATUS_ACTIVE)
{
Exception e = new Exception("Transaction cannot be committed (probably transaction timeout): " + tx);
throwJBossException(e, type);
}
}
}
// Inner classes -------------------------------------------------
// Monitorable implementation ------------------------------------
public void sample(Object s)
{
// Just here to because Monitorable request it but will be removed soon
}
public Map retrieveStatistic()
{
return null;
}
public void resetStatistic()
{
}
}