/* * The contents of this file are subject to the Open Software License * Version 3.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.rosenlaw.com/OSL3.0.htm * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * This file is an original work developed by Netymon Pty Ltd * (http://www.netymon.com, mailto:mail@netymon.com). Portions created * by Netymon Pty Ltd are Copyright (c) 2006 Netymon Pty Ltd. * All Rights Reserved. * * Work deriving from MulgaraTransaction Copyright (c) 2007 Topaz Foundation * under contract by Andrae Muys (mailto:andrae@netymon.com). */ package org.mulgara.resolver; // Java 2 enterprise packages import java.util.HashSet; import java.util.Set; import javax.transaction.Transaction; // Third party packages import org.apache.log4j.Logger; // Local packages import org.mulgara.resolver.spi.DatabaseMetadata; import org.mulgara.resolver.spi.EnlistableResource; import org.mulgara.util.StackTrace; import org.mulgara.query.MulgaraTransactionException; import org.mulgara.query.TuplesException; /** * Responsible for the javax.transaction.Transaction object. * Responsibilities * Ensuring every begin or resume is followed by either a suspend or an end. * Ensuring every suspend or end is preceeded by either a begin or a resume. * In conjunction with TransactionalAnswer ensuring that * all calls to operations on SubqueryAnswer are preceeded by a successful resume. * all calls to operations on SubqueryAnswer conclude with a suspend as the last call prior to returning to the user. * Collaborates with DatabaseTransactionManager to determine when to end the transaction. * * @created 2006-10-06 * * @author <a href="mailto:andrae@netymon.com">Andrae Muys</a> * * @company <a href="mailto:mail@netymon.com">Netymon Pty Ltd</a> * * @copyright ©2006 <a href="http://www.netymon.com/">Netymon Pty Ltd</a> * * @licence Open Software License v3.0 */ public class MulgaraInternalTransaction implements MulgaraTransaction { /** * This is the state machine switch matching these states. switch (state) { case CONSTRUCTEDREF: case CONSTRUCTEDUNREF: case ACTUNREF: case ACTREF: case DEACTREF: case FINISHED: case FAILED: } */ private enum State { CONSTRUCTEDREF, CONSTRUCTEDUNREF, ACTUNREF, ACTREF, DEACTREF, FINISHED, FAILED }; /** Logger. */ private static final Logger logger = Logger.getLogger(MulgaraInternalTransaction.class.getName()); private MulgaraInternalTransactionFactory factory; private DatabaseOperationContext context; private Set<EnlistableResource> enlisted; private Transaction transaction; private Thread currentThread; private volatile long deactivateTime; private boolean inXACompletion; private State state; private int inuse; private int using; private Throwable rollbackCause; public MulgaraInternalTransaction(MulgaraInternalTransactionFactory factory, DatabaseOperationContext context) throws IllegalArgumentException { debugReport("Creating Transaction"); try { if (factory == null) { throw new IllegalArgumentException("Manager null in MulgaraTransaction"); } else if (context == null) { throw new IllegalArgumentException("OperationContext null in MulgaraTransaction"); } this.factory = factory; this.context = context; this.enlisted = new HashSet<EnlistableResource>(); this.currentThread = null; inuse = 0; using = 0; state = State.CONSTRUCTEDUNREF; rollbackCause = null; deactivateTime = 0; } finally { debugReport("Finished Creating Transaction"); } } private void activate() throws MulgaraTransactionException { debugReport("Activating Transaction"); try { if (currentThread != null && !currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } deactivateTime = -1; switch (state) { case CONSTRUCTEDUNREF: startTransaction(); inuse = 1; state = State.ACTUNREF; try { context.initiate(this); } catch (Throwable th) { throw implicitRollback(th); } break; case CONSTRUCTEDREF: startTransaction(); inuse = 1; using = 1; state = State.ACTREF; try { context.initiate(this); } catch (Throwable th) { throw implicitRollback(th); } break; case DEACTREF: resumeTransaction(); inuse = 1; state = State.ACTREF; break; case ACTREF: case ACTUNREF: inuse++; break; case FINISHED: throw new MulgaraTransactionException("Attempt to activate terminated transaction"); case FAILED: throw new MulgaraTransactionException("Attempt to activate failed transaction", rollbackCause); } try { checkActivated(); } catch (MulgaraTransactionException em) { throw abortTransaction("Activate failed post-condition check", em); } } catch (MulgaraTransactionException em) { throw em; } catch (Throwable th) { throw abortTransaction("Error activating transaction", th); } finally { // debugReport("Leaving Activate transaction"); } } private void deactivate() throws MulgaraTransactionException { debugReport("Deactivating transaction"); try { if (currentThread == null) { throw new MulgaraTransactionException("Transaction not associated with thread"); } else if (!currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } deactivateTime = System.currentTimeMillis(); switch (state) { case ACTUNREF: if (inuse == 1) { commitTransaction(); } inuse--; break; case ACTREF: if (inuse == 1) { suspendTransaction(); } inuse--; break; case CONSTRUCTEDREF: throw new MulgaraTransactionException("Attempt to deactivate uninitiated refed transaction"); case CONSTRUCTEDUNREF: throw new MulgaraTransactionException("Attempt to deactivate uninitiated transaction"); case DEACTREF: throw new IllegalStateException("Attempt to deactivate unactivated transaction"); case FINISHED: if (inuse < 0) { errorReport("Activation count failure - too many deacts - in finished transaction", null); } else { inuse--; } break; case FAILED: // Nothing to do here. break; } } catch (MulgaraTransactionException em) { throw em; } catch (Throwable th) { throw abortTransaction("Error deactivating transaction", th); } finally { // debugReport("Leaving Deactivate Transaction"); } } // Note: The transaction is often not activated when these are called. // This occurs when setting autocommit, as this creates and // references a transaction object that won't be started/activated // until it is first used. public void reference() throws MulgaraTransactionException { debugReport("Referencing Transaction"); acquireMutex(0, false, MulgaraTransactionException.class); try { try { if (currentThread != null && !currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } switch (state) { case CONSTRUCTEDUNREF: state = State.CONSTRUCTEDREF; break; case ACTREF: case ACTUNREF: using++; state = State.ACTREF; break; case DEACTREF: using++; break; case CONSTRUCTEDREF: throw new MulgaraTransactionException("Attempt to reference uninitated transaction twice"); case FINISHED: throw new MulgaraTransactionException("Attempt to reference terminated transaction"); case FAILED: throw new MulgaraTransactionException("Attempt to reference failed transaction", rollbackCause); } } catch (MulgaraTransactionException em) { throw em; } catch (Throwable th) { report("Error referencing transaction"); throw implicitRollback(th); } finally { debugReport("Leaving Reference Transaction"); } } finally { releaseMutex(); } } public void dereference() throws MulgaraTransactionException { debugReport("Dereferencing Transaction"); acquireMutex(0, false, MulgaraTransactionException.class); try { try { if (currentThread != null && !currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } switch (state) { case ACTREF: if (using == 1) { state = State.ACTUNREF; } using--; break; case CONSTRUCTEDREF: state = State.CONSTRUCTEDUNREF; break; case FINISHED: case FAILED: if (using < 1) { errorReport("Reference count failure - too many derefs - in finished transaction", null); } else { using--; } break; case ACTUNREF: throw new IllegalStateException("Attempt to dereference unreferenced transaction"); case CONSTRUCTEDUNREF: throw new MulgaraTransactionException("Attempt to dereference uninitated transaction"); case DEACTREF: throw new IllegalStateException("Attempt to dereference deactivated transaction"); } } catch (MulgaraTransactionException em) { throw em; } catch (Throwable th) { throw implicitRollback(th); } finally { debugReport("Dereferenced Transaction"); } } finally { releaseMutex(); } } private void startTransaction() throws MulgaraTransactionException { debugReport("Initiating transaction"); try { transaction = factory.transactionStart(this); currentThread = Thread.currentThread(); } catch (Throwable th) { throw abortTransaction("Failed to start transaction", th); } } private void resumeTransaction() throws MulgaraTransactionException { // debugReport("Resuming transaction"); try { factory.transactionResumed(this, transaction); currentThread = Thread.currentThread(); } catch (Throwable th) { throw abortTransaction("Failed to resume transaction", th); } } private void suspendTransaction() throws MulgaraTransactionException { // debugReport("Suspending Transaction"); try { if (using < 1) { throw implicitRollback( new MulgaraTransactionException("Attempt to suspend unreferenced transaction")); } transaction = factory.transactionSuspended(this); currentThread = null; state = State.DEACTREF; } catch (Throwable th) { throw implicitRollback(th); } finally { // debugReport("Finished suspending transaction"); } } public void commitTransaction() throws MulgaraTransactionException { debugReport("Committing Transaction"); acquireMutex(0, true, MulgaraTransactionException.class); try { try { transaction.commit(); } catch (Throwable th) { throw implicitRollback(th); } try { try { transaction = null; } finally { try { state = State.FINISHED; } finally { try { context.clear(); } finally { try { enlisted.clear(); } finally { try { factory.transactionComplete(this); } finally { debugReport("Committed transaction"); } } } } } } catch (Throwable th) { errorReport("Error cleaning up transaction post-commit", th); throw new MulgaraTransactionException("Error cleaning up transaction post-commit", th); } } finally { releaseMutex(); } } public void heuristicRollback(String cause) throws MulgaraTransactionException { synchronized (factory.getMutexLock()) { if (factory.getMutexHolder() != null && factory.getMutexHolder() != Thread.currentThread()) { if (inXACompletion) { return; // this txn is already being cleaned up, so let it go } } factory.acquireMutexWithInterrupt(0L, MulgaraTransactionException.class); inXACompletion = true; } try { switch (state) { case DEACTREF: activate(); try { implicitRollback(new MulgaraTransactionException(cause)); } finally { currentThread = null; } break; case ACTUNREF: case ACTREF: implicitRollback(new MulgaraTransactionException(cause)); break; case CONSTRUCTEDREF: case CONSTRUCTEDUNREF: // no point in starting and then rolling back immediately, so just clean up abortTransaction(cause, new Throwable()); case FINISHED: case FAILED: // Nothing to do here. return; } } finally { releaseMutex(); } } /** * Rollback the transaction. * We don't throw an exception here when transaction fails - this is expected, * after all we requested it. */ public void explicitRollback() throws MulgaraTransactionException { acquireMutex(0, true, MulgaraTransactionException.class); try { if (currentThread == null) { throw new MulgaraTransactionException("Transaction failed activation check"); } else if (!currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } try { switch (state) { case ACTUNREF: case ACTREF: transaction.rollback(); context.clear(); enlisted.clear(); factory.transactionComplete(this); transaction = null; state = State.FINISHED; break; case DEACTREF: throw new IllegalStateException("Attempt to rollback unactivated transaction"); case CONSTRUCTEDREF: throw new MulgaraTransactionException("Attempt to rollback uninitiated ref'd transaction"); case CONSTRUCTEDUNREF: throw new MulgaraTransactionException("Attempt to rollback uninitiated unref'd transaction"); case FINISHED: throw new MulgaraTransactionException("Attempt to rollback finished transaction"); case FAILED: throw new MulgaraTransactionException("Attempt to rollback failed transaction"); } } catch (MulgaraTransactionException em) { throw em; } catch (Throwable th) { throw implicitRollback(th); } } finally { releaseMutex(); } } /** * This will endevour to terminate the transaction via a rollback - if this * fails it will abort the transaction. * If the rollback succeeds then this method will return a suitable * MulgaraTransactionException to be thrown by the caller. * If the rollback fails then this method will throw the resulting exception * from abortTransaction(). * Post-condition: The transaction is terminated and cleaned up. */ @SuppressWarnings("finally") private MulgaraTransactionException implicitRollback(Throwable cause) throws MulgaraTransactionException { debugReport("Implicit Rollback triggered"); synchronized (factory.getMutexLock()) { inXACompletion = true; } try { if (rollbackCause != null) { errorReport("Cascading error, transaction already rolled back", cause); errorReport("Cascade error, expected initial cause", rollbackCause); return new MulgaraTransactionException("Transaction already in rollback", cause); } switch (state) { case ACTUNREF: case ACTREF: rollbackCause = cause; transaction.rollback(); transaction = null; context.clear(); enlisted.clear(); state = State.FAILED; factory.transactionAborted(this, rollbackCause); return new MulgaraTransactionException("Transaction rollback triggered", cause); case DEACTREF: throw new IllegalStateException("Attempt to rollback deactivated transaction"); case CONSTRUCTEDREF: throw new MulgaraTransactionException("Attempt to rollback uninitiated ref'd transaction"); case CONSTRUCTEDUNREF: throw new MulgaraTransactionException("Attempt to rollback uninitiated unref'd transaction"); case FINISHED: throw new MulgaraTransactionException("Attempt to rollback finished transaction"); case FAILED: throw new MulgaraTransactionException("Attempt to rollback failed transaction"); default: throw new MulgaraTransactionException("Unknown state"); } } catch (Throwable th) { try { errorReport("Attempt to rollback failed; initiating cause: ", cause); } finally { throw abortTransaction("Failed to rollback normally - see log for inititing cause", th); } } finally { debugReport("Leaving implicitRollback"); } } /** * Calls through to {@link #abortTransaction(String,Throwable)} passing the message in * the cause as the message for the transaction abort. * @param cause The state triggering the abort. * @return The exception for aborting. * @throws MulgaraTransactionException Indicated failure to cleanly abort. */ public MulgaraTransactionException abortTransaction(Throwable cause) throws MulgaraTransactionException { return abortTransaction(cause.getMessage(), cause); } /** * Forces the transaction to be abandoned, including bypassing JTA to directly * rollback/abort the underlying store-phases if required. * Heavilly nested try{}finally{} should guarentee that even JVM errors should * not prevent this function from cleaning up anything that can be cleaned up. * We have to delegate to the OperationContext the abort() on the resolvers as * only it has full knowledge of which resolvers are associated with this * transaction. */ public MulgaraTransactionException abortTransaction(String errorMessage, Throwable cause) throws MulgaraTransactionException { // we should actually already have the mutex, but let's make sure acquireMutex(0L, true, MulgaraTransactionException.class); try { // We need to notify the factory here - this is serious, we // need to rollback this transaction, but if we have reached here // we have failed to obtain a valid transaction to rollback! try { if (rollbackCause == null) rollbackCause = cause; try { errorReport(errorMessage + " - Aborting", cause); } finally { try { if (transaction != null) { transaction.rollback(); } } finally { try { factory.transactionAborted(this, cause); } finally { try { abortEnlistedResources(); } finally { try { context.clear(); } finally { try { enlisted.clear(); } finally { try { transaction = null; } finally { state = State.FAILED; } } } } } } } return new MulgaraTransactionException(errorMessage + " - Aborting", cause); } catch (Throwable th) { throw new MulgaraTransactionException(errorMessage + " - Failed to abort cleanly", th); } finally { debugReport("Leaving abortTransaction"); } } finally { releaseMutex(); } } /** * Used to bypass JTA and explicitly abort resources behind the scenes. */ private void abortEnlistedResources() { for (EnlistableResource e : enlisted) { try { e.abort(); } catch (Throwable th) { try { errorReport("Error aborting enlistable resource", th); } catch (Throwable ignore) { /* Unable to log. Would normally log, so ignore. */ } } } } public void execute(Operation operation, DatabaseMetadata metadata) throws MulgaraTransactionException { debugReport("Executing Operation"); acquireMutex(0, false, MulgaraTransactionException.class); try { try { activate(); try { operation.execute(context, context.getSystemResolver(), metadata); } catch (Throwable th) { throw implicitRollback(th); } finally { deactivate(); } } finally { debugReport("Executed Operation"); } } finally { releaseMutex(); } } public AnswerOperationResult execute(AnswerOperation ao) throws TuplesException { debugReport("Executing AnswerOperation"); acquireMutex(0, false, TuplesException.class); try { try { activate(); try { ao.execute(); return ao.getResult(); } catch (Throwable th) { throw implicitRollback(th); } finally { deactivate(); } } catch (MulgaraTransactionException em) { throw new TuplesException("Transaction error", em); } finally { debugReport("Executed AnswerOperation"); } } finally { releaseMutex(); } } public void execute(TransactionOperation to) throws MulgaraTransactionException { debugReport("Executing TransactionOperation"); acquireMutex(0, false, MulgaraTransactionException.class); try { try { activate(); try { to.execute(); } catch (Throwable th) { throw implicitRollback(th); } finally { deactivate(); } } finally { debugReport("Executed TransactionOperation"); } } finally { releaseMutex(); } } public void enlist(EnlistableResource enlistable) throws MulgaraTransactionException { acquireMutex(0, false, MulgaraTransactionException.class); try { try { if (currentThread == null) { throw new MulgaraTransactionException("Transaction not associated with thread"); } else if (!currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } if (enlisted.contains(enlistable)) { return; } switch (state) { case ACTUNREF: case ACTREF: transaction.enlistResource(enlistable.getXAResource()); enlisted.add(enlistable); break; case CONSTRUCTEDREF: throw new MulgaraTransactionException("Attempt to enlist resource in uninitated ref'd transaction"); case CONSTRUCTEDUNREF: throw new MulgaraTransactionException("Attempt to enlist resource in uninitated unref'd transaction"); case DEACTREF: throw new MulgaraTransactionException("Attempt to enlist resource in unactivated transaction"); case FINISHED: throw new MulgaraTransactionException("Attempt to enlist resource in finished transaction"); case FAILED: throw new MulgaraTransactionException("Attempt to enlist resource in failed transaction"); } } catch (Throwable th) { throw implicitRollback(th); } } finally { releaseMutex(); } } public long lastActive() { return deactivateTime; } // // Used internally // private void checkActivated() throws MulgaraTransactionException { if (currentThread == null) { throw new MulgaraTransactionException("Transaction not associated with thread"); } else if (!currentThread.equals(Thread.currentThread())) { throw new MulgaraTransactionException("Concurrent access attempted to transaction: Transaction has NOT been rolledback."); } switch (state) { case ACTUNREF: case ACTREF: if (inuse < 0 || using < 0) { throw new MulgaraTransactionException("Reference Failure, using: " + using + ", inuse: " + inuse); } return; case CONSTRUCTEDREF: throw new MulgaraTransactionException("Transaction (ref) uninitiated"); case CONSTRUCTEDUNREF: throw new MulgaraTransactionException("Transaction (unref) uninitiated"); case DEACTREF: throw new MulgaraTransactionException("Transaction deactivated"); case FINISHED: throw new MulgaraTransactionException("Transaction is terminated"); case FAILED: throw new MulgaraTransactionException("Transaction is failed", rollbackCause); } } private <T extends Throwable> void acquireMutex(long timeout, boolean isXACompletion, Class<T> exc) throws T { synchronized (factory.getMutexLock()) { factory.acquireMutex(timeout, exc); inXACompletion |= isXACompletion; } } private void releaseMutex() { factory.releaseMutex(); } protected void finalize() throws Throwable { try { debugReport("GC-finalize"); if (state != State.FINISHED && state != State.FAILED) { errorReport("Finalizing incomplete transaction - aborting...", null); try { abortTransaction(new MulgaraTransactionException("Transaction finalized while still valid")); } catch (Throwable th) { errorReport("Attempt to abort transaction from finalize failed", th); } } if (state != State.FAILED && (inuse != 0 || using != 0)) { errorReport("Reference counting error in transaction", null); } if (transaction != null) { errorReport("Transaction not terminated properly", null); } } finally { super.finalize(); } } private final void report(String desc) { if (logger.isInfoEnabled()) { logger.info(desc + ": " + System.identityHashCode(this) + ", state=" + state + ", inuse=" + inuse + ", using=" + using); } } private final void debugReport(String desc) { if (logger.isDebugEnabled()) { logger.debug(desc + ": " + System.identityHashCode(this) + ", state=" + state + ", inuse=" + inuse + ", using=" + using); } } private final void errorReport(String desc, Throwable cause) { if (cause != null) { logger.error(desc + ": " + System.identityHashCode(this) + ", state=" + state + ", inuse=" + inuse + ", using=" + using, cause); } else { logger.error(desc + ": " + System.identityHashCode(this) + ", state=" + state + ", inuse=" + inuse + ", using=" + using + "\n" + new StackTrace()); } } }