/**
* Copyright (C) 2006 NetMind Consulting Bt.
*
* 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 3 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
*/
package hu.netmind.persistence;
import java.util.LinkedList;
import java.util.Iterator;
import java.util.Vector;
import java.sql.Connection;
import org.apache.log4j.Logger;
import java.util.HashSet;
import java.io.IOException;
import java.io.StringWriter;
import java.io.PrintWriter;
/**
* This class keeps track of all transaction objects allocated.
* @author Brautigam Robert
* @version Revision: $Revision$
*/
public class TransactionTracker
{
public static int TX_REQUIRED = 1;
public static int TX_NEW = 2;
public static int TX_OPTIONAL = 3;
private static Logger logger = Logger.getLogger(TransactionTracker.class);
StoreContext context;
private ThreadLocal transactions;
private LinkedList listeners;
private LinkedList allTransactions;
private HashSet allTransactionIds;
private Store store;
private ThreadLocal runningListeners;
TransactionTracker(StoreContext context)
{
this.context=context;
transactions = new ThreadLocal();
listeners = new LinkedList();
allTransactions = new LinkedList();
allTransactionIds = new HashSet();
runningListeners =new ThreadLocal();
}
void registerTransactionId(Long serial)
{
synchronized ( allTransactions )
{
allTransactionIds.add(serial);
}
}
boolean hasOpenTransaction(Long serial)
{
synchronized ( allTransactions )
{
return allTransactionIds.contains(serial);
}
}
/**
* Mark all current transactions as rollback only.
*/
public void markRollbackAll(StoreException ex)
{
synchronized ( allTransactions )
{
for ( int i=0; i<allTransactions.size(); i++ )
{
Transaction tx = (Transaction) allTransactions.get(i);
tx.setRollbackException(ex);
}
}
}
/**
* Get a transaction. Following modes are supported:
* <ul>
* <li>TX_REQUIRED: A new transaction is allocated if no current
* transaction exists, otherwise the current transaction is returned.</li>
* <li>TX_NEW: A new transaction is allocated either way.</li>
* <li>TX_OPTIONAL: If there is a current transaction, that is returned,
* null otherwise.</li>
* </ul>
* Note, that each transaction can support multiple levels of begin-commit
* blocks. Each transaction only commits/rollsback is the most outer
* block is commited/rolled back.
*/
public Transaction getTransaction(int mode)
{
LinkedList list = (LinkedList) transactions.get();
if ( list == null )
{
// No list yet, initialize threadlocal
list = new LinkedList();
transactions.set(list);
}
if ( (list.size()==0) && (mode==TX_OPTIONAL) )
return null;
if ( (list.size() == 0) || (mode==TX_NEW) )
{
// No transaction, or new is explicitly required
Transaction transaction = new Transaction(this);
if ( logger.isTraceEnabled() )
logger.trace("transaction allocation trace: "+getStackTrace(transaction.getAllocateTrace()));
transaction.setConnection(context.getDatabase().getConnectionSource().getConnection());
list.add(transaction);
synchronized ( allTransactions )
{
allTransactions.add(transaction);
}
}
return (Transaction) list.getLast();
}
/**
* Send notifications.
* @param transaction The transaction which caused the event.
*/
private void sendNotifications(Transaction transaction, boolean commited)
{
// Initialize running list if nescesssary. Running lists keep
// track of current threads listeners who are currently running.
// This is to prevent infinite recursions, inside a notification run
// a listener cannot cause any transacitions in which they will be
// called a second time.
HashSet runningList = (HashSet) runningListeners.get();
if ( runningList == null )
{
runningList = new HashSet();
runningListeners.set(runningList);
}
if ( logger.isDebugEnabled() )
logger.debug("sending notifications for "+transaction+", running listeners: "+runningList);
// Do notifications
Vector listenersCopy = null;
synchronized ( listeners )
{
listenersCopy = new Vector(listeners);
}
Iterator iterator = listenersCopy.iterator();
while ( iterator.hasNext() )
{
TransactionListener listener = (TransactionListener) iterator.next();
if ( ! runningList.contains(listener) )
{
runningList.add(listener); // Add to running list
try
{
if ( commited )
listener.transactionCommited(transaction); // Run listener
else
listener.transactionRolledback(transaction); // Run listener
} catch ( Throwable e ) {
logger.warn("a transaction listener threw error:",e);
} finally {
runningList.remove(listener); // Remove from running list
}
}
}
}
/**
* Internal commit, this is a callback from store.
*/
void internalCommit(Transaction transaction)
{
// First commit sql
Connection connection = transaction.getConnection();
try
{
connection.commit();
try
{
if ( transaction.getTransactionIsolation() != Connection.TRANSACTION_READ_COMMITTED )
transaction.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
} catch ( Exception e ) {
logger.warn("could not set transaction isolation back to READ_COMMITED.",e);
}
} catch ( Exception e ) {
throw new StoreException("could not commit transaction",e);
} finally {
// Remove transaction from queue
removeTransaction(transaction);
}
// Send notifications
sendNotifications(transaction,true);
}
/**
* Commit a transaction.
*/
void commit(Transaction transaction)
{
// If the transaction is internal, don't go through store
if ( (transaction.getSerial()==null) &&
(transaction.getSaveTables()!=null) && (transaction.getRemoveTables()!=null) )
{
internalCommit(transaction);
} else {
// Commit is going through Store
context.getStore().commit(transaction);
}
}
void rollback(Transaction transaction)
{
// If the transaction is internal, don't go through store
if ( (transaction.getSerial()==null) &&
(transaction.getSaveTables()!=null) && (transaction.getRemoveTables()!=null) )
{
internalRollback(transaction);
} else {
// Commit is going through Store
context.getStore().rollback(transaction);
}
}
/**
* Rollback a transaction.
*/
void internalRollback(Transaction transaction)
{
// First rollback sql
Connection connection = transaction.getConnection();
try
{
connection.rollback();
} catch ( Exception e ) {
throw new StoreException("could not roll back transaction",e);
} finally {
// Remove transaction from queue
removeTransaction(transaction);
}
// Send notifications
sendNotifications(transaction,false);
}
/**
* Remove transaction from queue. Also, if this transaction is not
* toplevel, then throw exception.
*/
private void removeTransaction(Transaction transaction)
{
// Remove from threadlocal list
LinkedList list = (LinkedList) transactions.get();
if ( (list == null) || (list.size()==0) )
throw new StoreException("no transactions present, and tried to use one.");
if ( ! list.getLast().equals(transaction) )
{
Transaction top = (Transaction) list.getLast();
logger.warn("possible transaction leak, tried to commit/rollback transaction, which was not the currenct transaction, investigate! Top was: "+top+", but tried to close: "+transaction+". "+
"Last operation of top was: "+top.getLastOperation()+", other's: "+transaction.getLastOperation()+". "+
"Now follows the allocation trace of top and the transaction to be closed: \n"+
getStackTrace(top.getAllocateTrace())+"\n"+getStackTrace(transaction.getAllocateTrace()));
throw new StoreException("tried to commit/rollback a transaction which is not the current transaction, possible transaction leak!");
}
// Give back connection
context.getDatabase().getConnectionSource().releaseConnection(transaction.getConnection());
transaction.setConnection(null);
// Remove from list
list.removeLast();
// Remove from global list
synchronized ( allTransactions )
{
allTransactions.remove(transaction);
allTransactionIds.remove(transaction.getSerial());
}
}
private String getStackTrace(Exception e)
{
StringWriter trace = new StringWriter();
PrintWriter writer = new PrintWriter(trace);
e.printStackTrace(writer);
return trace.toString();
}
/**
* Register a listener to this tracker.
*/
public void addListener(TransactionListener listener)
{
synchronized ( listeners )
{
if ( ! listeners.contains(listener) )
listeners.add(listener);
}
}
/**
* Remove a listener from this tracker.
*/
public void removeListener(TransactionListener listener)
{
synchronized ( listeners )
{
listeners.remove(listener);
}
}
}
|