/******************************************************************************* * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.transaction; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import org.eclipse.persistence.exceptions.DatabaseException; import org.eclipse.persistence.exceptions.ExceptionHandler; import org.eclipse.persistence.exceptions.TransactionException; import org.eclipse.persistence.internal.sequencing.SequencingCallback; import org.eclipse.persistence.internal.sequencing.SequencingCallbackFactory; import org.eclipse.persistence.internal.sessions.AbstractSession; import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl; import org.eclipse.persistence.internal.sessions.UnitOfWorkImpl; import org.eclipse.persistence.logging.SessionLog; import org.eclipse.persistence.sessions.DatabaseSession; import org.eclipse.persistence.sessions.ExternalTransactionController; import org.eclipse.persistence.sessions.broker.SessionBroker; /** * <p> * <b>Purpose</b>: Abstract implementation of an ExternalTransactionController. * <p> * <b>Description</b>: This class implements the ExternalTransactionController * interface. Concrete implementations of this class are responsible for performing * the coordination with the external transaction manager through whatever means * available to that manager. Different transaction services may do this in slightly * different ways, but most common ones (JTA and JTS/OTS) will use a synchronization * interface. * * @see AbstractSynchronizationListener * @see org.eclipse.persistence.sessions.ExternalTransactionController */ public abstract class AbstractTransactionController implements ExternalTransactionController { /** Table of external transaction object keys and unit of work values */ protected ConcurrentMap unitsOfWork; /** The session this controller is responsible for controlling */ protected AbstractSession session; /** Generates listener instances for synchronization */ protected SynchronizationListenerFactory listenerFactory; /** PERF: Cache the active uow in a thread local. */ protected ThreadLocal activeUnitOfWorkThreadLocal; /** Table of external transaction object keys and sequencing listeners values. */ /** Non-null only in case sequencing callbacks are used: numSessionsRequiringSequencingCallback {@literal >} 0 */ protected ConcurrentMap<Object, AbstractSynchronizationListener> sequencingListeners; /** Table of external transaction object keys and listeners that are currently in beforeCompletion. */ /** Request for a new sequencing callback may be triggered by beforeCompletion of existing listener - */ /** in this case avoid creating yet another listener for sequencing but rather use the listener */ /** that has initiated the request */ /** Non-null only in case sequencing callbacks are used: numSessionsRequiringSequencingCallback {@literal >} 0 */ protected ConcurrentMap<Object, AbstractSynchronizationListener> currentlyProcessedListeners; /** Indicates how many sessions require sequencing callbacks: */ /** 0 - sequencing callbacks not used; */ /** 1 - the session is DatabaseSession or ServerSession and requires sequencing callbacks, */ /** or the session is a session broker with only one member requiring sequencing callbacks. */ /** more - the session is a session broker with several members requiring sequencing callbacks. */ protected int numSessionsRequiringSequencingCallback; /** Allow exception in before/after completion to be wrapped. */ protected ExceptionHandler exceptionHandler; /** * INTERNAL: * Return a new controller. */ public AbstractTransactionController() { this.unitsOfWork = new ConcurrentHashMap(); this.activeUnitOfWorkThreadLocal = new ThreadLocal(); } /** * Return the exception handler used to handle or wrap exceptions thrown in before/after completion. */ public ExceptionHandler getExceptionHandler() { return exceptionHandler; } /** * Set an exception handler to handle or wrap exceptions thrown in before/after completion. */ public void setExceptionHandler(ExceptionHandler exceptionHandler) { this.exceptionHandler = exceptionHandler; } /** * INTERNAL: * Associate the given unit of work and EclipseLink session with the active external * transaction. This may be done by whatever means supported by the transaction * manager (i.e. through a synchronization protocol). * * @param unitOfWork The unit of work that is to be bound to the active transaction * @param session The session ancestor of the unit of work */ public void bindToCurrentTransaction(UnitOfWorkImpl unitOfWork, AbstractSession session) { Object status = getTransactionStatus(); logTxStateTrace(unitOfWork, "TX_bind", status); try { Object txn = getTransaction(); if (txn == null) { // If no external transaction is active then start one through the uow unitOfWork.beginTransaction(); txn = getTransaction(); } // If there still is no active txn then something is wrong if (txn == null) { throw TransactionException.externalTransactionNotActive(); } // Create and register the synchronization listener AbstractSynchronizationListener listener = getListenerFactory().newSynchronizationListener(unitOfWork, session, txn, this); registerSynchronization_impl(listener, txn); unitOfWork.setSynchronized(true); } catch (Exception exception) { throw TransactionException.errorBindingToExternalTransaction(exception); } } /** * INTERNAL: * Begin an external transaction. * * @param session The session for which the transaction is being begun. */ public void beginTransaction(AbstractSession session) { try { Object status = getTransactionStatus(); logTxStateTrace(session, "TX_begin", status); // Make sure that we are in a state that we can actually start // a transaction (e.g. ensure one is not already in progress) if (canBeginTransaction_impl(status)) { logTxTrace(session, "TX_beginningTxn", null); beginTransaction_impl(); session.setWasJTSTransactionInternallyStarted(true); } } catch (Exception exception) { throw TransactionException.errorBeginningExternalTransaction(exception); } } /** * INTERNAL: * Commit the external transaction. * * @param session The session for which the transaction is being committed. */ public void commitTransaction(AbstractSession session) { try { Object status = getTransactionStatus(); logTxStateTrace(session, "TX_commit", status); if (canCommitTransaction_impl(status)) { logTxTrace(session, "TX_committingTxn", null); session.setWasJTSTransactionInternallyStarted(false); commitTransaction_impl(); } } catch (Exception exception) { throw TransactionException.errorCommittingExternalTransaction(exception); } } /** * INTERNAL: * Roll back the external transaction. * * @param session The session for which the transaction is being rolled back. */ public void rollbackTransaction(AbstractSession session) { try { Object status = getTransactionStatus(); logTxStateTrace(session, "TX_rollback", status); session.setWasJTSTransactionInternallyStarted(false); // Only roll back if there is a transaction to roll back if ((canRollbackTransaction_impl(status)) && (getTransaction() != null)) { logTxTrace(session, "TX_rollingBackTxn", null); rollbackTransaction_impl(); } } catch (Exception exception) { throw TransactionException.errorRollingBackExternalTransaction(exception); } } /** * INTERNAL: * Mark the external transaction for rollback. */ public void markTransactionForRollback() { try { markTransactionForRollback_impl(); } catch (Exception exception) { throw TransactionException.errorMarkingTransactionForRollback(exception); } } /** * INTERNAL: * Return the active external transaction object, or null if * none is active. This may be in whatever form the transaction system uses. */ public Object getTransaction() { try { return getTransaction_impl(); } catch (Exception exception) { throw TransactionException.errorGettingExternalTransaction(exception); } } /** * INTERNAL: * Return a key for the specified external transaction object. * The key is just something that can be inserted into a hashtable (must support * hashCode() and equals() methods). */ public Object getTransactionKey(Object transaction) { try { return getTransactionKey_impl(transaction); } catch (Exception exception) { throw TransactionException.errorGettingExternalTransaction(exception); } } /** * INTERNAL: * Return the transaction status. This may be any type of status or value, * depending upon the transaction system. */ public Object getTransactionStatus() { try { return getTransactionStatus_impl(); } catch (Exception exception) { throw TransactionException.errorGettingExternalTransactionStatus(exception); } } /** * INTERNAL: * Used the EJB 3.0 to determine if a transaction is in a state where an EntityManager can * be closed */ public boolean noTransactionOrRolledBackOrCommited() { try { Object status = getTransactionStatus(); return canBeginTransaction_impl(status) || canMergeUnitOfWork_impl(status) || isRolledBack_impl(status); } catch (Exception exception) { throw TransactionException.errorGettingExternalTransactionStatus(exception); } } /** * INTERNAL: * Return true if the transaction is in the rolled back state. */ public abstract boolean isRolledBack_impl(Object status); /** * INTERNAL: * Return true if there is a unit of work associated with the active external * transaction. Return false if no transaction is current, or if no uow has * been associated with the active transaction yet. */ public boolean hasActiveUnitOfWork() { return this.lookupActiveUnitOfWork() != null; } /** * INTERNAL: * Return the active unit of work for the current external transaction. * If no transaction is active then return null. If a transaction is active * but no unit of work has been bound to it then create and return one. */ public UnitOfWorkImpl getActiveUnitOfWork() { Object transaction = getTransaction(); if (transaction == null) { return null; } UnitOfWorkImpl activeUnitOfWork = lookupActiveUnitOfWork(transaction); if (activeUnitOfWork == null) { // Txn is active but no UoW has been associated with it yet, so create one. activeUnitOfWork = getSession().acquireUnitOfWork(); Object transactionKey = getTransactionKey(transaction); addUnitOfWork(transactionKey, activeUnitOfWork); activeUnitOfWork.setTransaction(transaction); this.activeUnitOfWorkThreadLocal.set(activeUnitOfWork); } return activeUnitOfWork; } /** * INTERNAL: * Return the unit of work associated with the active external transaction. * Return null if no transaction is active, or if no uow has been associated with * the active transaction yet. */ public UnitOfWorkImpl lookupActiveUnitOfWork() { return lookupActiveUnitOfWork(getTransaction()); } /** * INTERNAL: * Return the unit of work associated with the active external transaction. * Return null if no transaction is active, or if no uow has been associated with * the active transaction yet. */ public UnitOfWorkImpl lookupActiveUnitOfWork(Object transaction) { if (transaction == null) { return null; } Object transactionKey = getTransactionKey(transaction); // PERF: Cache the active unit of work in a thread local. // This is just a heuristic, so uses == and no tx-key and direct access as extremely high throughput. UnitOfWorkImpl activeUnitOfWork = (UnitOfWorkImpl)this.activeUnitOfWorkThreadLocal.get(); if (activeUnitOfWork != null) { if (transaction == activeUnitOfWork.getTransaction()) { return activeUnitOfWork; } } activeUnitOfWork = (UnitOfWorkImpl)getUnitsOfWork().get(transactionKey); if (activeUnitOfWork != null) { activeUnitOfWork.setTransaction(transaction); } this.activeUnitOfWorkThreadLocal.set(activeUnitOfWork); return activeUnitOfWork; } /** * INTERNAL: * Add a UnitOfWork object to the Hashtable keyed on the external transaction object. */ public void addUnitOfWork(Object transactionKey, UnitOfWorkImpl activeUnitOfWork) { this.activeUnitOfWorkThreadLocal.set(null); getUnitsOfWork().put(transactionKey, activeUnitOfWork); } /** * INTERNAL: * Remove the unit of work associated with the transaction passed in. */ public void removeUnitOfWork(Object transactionKey) { if (transactionKey != null) { getUnitsOfWork().remove(transactionKey); } this.activeUnitOfWorkThreadLocal.set(null); } /** * INTERNAL: * Return the manager's session. */ public AbstractSession getSession() { return session; } /** * INTERNAL: * Set the manager's session. */ public void setSession(AbstractSession session) { this.session = session; initializeSequencingListeners(); } /** * INTERNAL: * Return the hashtable keyed on the external transaction objects with values * that are the associated units of work. */ public Map getUnitsOfWork() { return unitsOfWork; } /** * INTERNAL: * Set the table of transactions to units of work. */ protected void setUnitsOfWork(ConcurrentMap unitsOfWork) { this.unitsOfWork = unitsOfWork; } /** * INTERNAL: * Get the factory used to generate synchronization listeners. */ public SynchronizationListenerFactory getListenerFactory() { return listenerFactory; } /** * INTERNAL: * Set the factory used to generate synchronization listeners. This should be * set if a listener other than the default one is being used. */ public void setListenerFactory(SynchronizationListenerFactory factory) { listenerFactory = factory; } /** * INTERNAL: * Associate the given unit of work and EclipseLink session with the current external * transaction. This method is offered only for backwards compatibility. */ public void registerSynchronizationListener(UnitOfWorkImpl uow, AbstractSession session) throws DatabaseException { this.bindToCurrentTransaction(uow, session); } /** * PUBLIC: * Look up a given name in JNDI. This can be used by a subclass or even an * application to look up transaction artifacts that are required for the * implementation. * <p> * The lookup assumes that it is taking place on the server side, and that the * InitialContext can be used without requiring any special properties. * * @param jndiName The name to look up * @return The object bound in JNDI to the specified name * @exception TransactionException Thrown in case of lookup failure */ public Object jndiLookup(String jndiName) { Context context = null; Object jndiObject = null; try { context = new InitialContext(); jndiObject = context.lookup(jndiName); } catch (NamingException ex) { throw TransactionException.jndiLookupException(jndiName, ex); } finally { if (context != null) { try { context.close(); } catch (Exception ex2) { /* ignore */ } } } return jndiObject; } /** * INTERNAL: * Initializes sequencing listeners. * There are two methods calling this method: * 1. setSession method - this could lead to initialization of sequencing listeners * only if sequencing already connected (that would happen if setSession is called * after session.login, which is normally not the case). * 2. in the very end of connecting sequencing or adding descriptors to sequencing, * after it's determined whether sequencing callbacks (and therefore listeners) * will be required. * * In SessionBroker case each member's sequencing may call this method. * Note that the number of sessions requiring callbacks may never decrease, * therefore if isSequencingCallbackRequired method has returned true once, * it will always return true after that (unless clearSequencingListeners method is called). */ public void initializeSequencingListeners() { if(session == null) { return; } AbstractSession parentSession = session; while(parentSession.getParent() != null) { parentSession = parentSession.getParent(); } int newNumSessionsRequiringSequencingCallback = 0; // top parentSession must be DatabaseSessionImpl if(parentSession.isBroker()) { // it could be either SessionBroker newNumSessionsRequiringSequencingCallback = ((SessionBroker)parentSession).howManySequencingCallbacks(); } else { // or DatabaseSessionImpl or ServerSession if(((DatabaseSessionImpl)parentSession).isSequencingCallbackRequired()) { newNumSessionsRequiringSequencingCallback = 1; } } // number of required sessions in not allowed to decrease. if (newNumSessionsRequiringSequencingCallback > numSessionsRequiringSequencingCallback) { // keep the old map if already exists, never remove existing map if (this.sequencingListeners == null) { this.sequencingListeners = new ConcurrentHashMap(); } // keep the old map if already exists, never remove existing map if (this.currentlyProcessedListeners == null) { this.currentlyProcessedListeners = new ConcurrentHashMap(); } this.numSessionsRequiringSequencingCallback = newNumSessionsRequiringSequencingCallback; } } /** * INTERNAL: * Returns sequencingCallback for the current active external transaction. * DatabaseSession is passed for the sake of SessionBroker case. * This method requires active external transaction. */ public SequencingCallback getActiveSequencingCallback(DatabaseSession dbSession, SequencingCallbackFactory sequencingCallbackFactory) { Object transaction = getTransaction(); // This method requires active external transaction. if (transaction == null) { throw TransactionException.externalTransactionNotActive(); } Object transactionKey = getTransactionKey(transaction); AbstractSynchronizationListener listener = sequencingListeners.get(transactionKey); if(listener == null) { // In case this request was triggered from beforeCompletion method of existing listener - // find this listener. listener = currentlyProcessedListeners.get(transactionKey); if(listener == null) { // Create and register the new synchronization listener with uow==session==null. listener = getListenerFactory().newSynchronizationListener(null, null, transaction, this); try { registerSynchronization_impl(listener, transaction); } catch (Exception exception) { throw TransactionException.errorBindingToExternalTransaction(exception); } } sequencingListeners.put(transactionKey, listener); } return listener.getSequencingCallback(dbSession, sequencingCallbackFactory); } /** * INTERNAL: * Clears sequencing listeners. * Called by initializeSequencingListeners and by sequencing on disconnect. */ public void clearSequencingListeners() { this.numSessionsRequiringSequencingCallback = 0; this.sequencingListeners = null; this.currentlyProcessedListeners = null; } /** * INTERNAL: * Indicates whether sequencing callback may be required. */ public boolean isSequencingCallbackRequired() { return this.numSessionsRequiringSequencingCallback > 0; } /** * INTERNAL: * Indicates how many sessions require sequencing callbacks. */ public int numSessionsRequiringSequencingCallback() { return this.numSessionsRequiringSequencingCallback; } /** * INTERNAL: * Clears sequencingCallbacks. * Called by initializeSequencingCallbacks and by sequencing on disconnect. */ public void removeSequencingListener(Object transactionKey) { if (transactionKey != null) { sequencingListeners.remove(transactionKey); } } /* * INTERNAL: * Helper method to log trace statements to the transaction channel. */ public void logTxTrace(AbstractSession session, String msgInd, Object[] args) { session.log(SessionLog.FINER, SessionLog.TRANSACTION, msgInd, args); } /* * INTERNAL: * Helper method to log transaction state to the transaction channel. */ public void logTxStateTrace(AbstractSession session, String msgInd, Object status) { if (session.shouldLog(SessionLog.FINER, SessionLog.TRANSACTION)) { String statusString = statusToString_impl(status); Object[] args = { statusString }; session.log(SessionLog.FINER, SessionLog.TRANSACTION, msgInd, args); } } //--------------------------------------------------------------------------------- // The following methods must be implemented by subclass implementations. //--------------------------------------------------------------------------------- /** * INTERNAL: * Register the specified synchronization listener with the given active * transaction. * * @param listener The synchronization listener created for this transaction * @param txn The active transaction for which notification is being requested */ protected abstract void registerSynchronization_impl(AbstractSynchronizationListener listener, Object txn) throws Exception; /** * INTERNAL: * Return the active external transaction for the calling thread, or null if * none is currently active for this thread. * * @return The active transaction object, or null if no transaction is active */ protected abstract Object getTransaction_impl() throws Exception; /** * INTERNAL: * Return a key for the specified external transaction object. * The key is just something that can be inserted into a hashtable (must support * hashCode() and equals() methods). * * @param transaction The transaction to which the returned key applies (may be null) * @return A key for the passed in transaction, or null if no transaction specified */ protected abstract Object getTransactionKey_impl(Object transaction) throws Exception; /** * INTERNAL: * Return the transaction status. This may be any type of status or value, * depending upon the transaction system. * * @return The current transaction status */ protected abstract Object getTransactionStatus_impl() throws Exception; /** * INTERNAL: * Begin an external transaction. Do this in a way appropriate to the * transaction subsystem. */ protected abstract void beginTransaction_impl() throws Exception; /** * INTERNAL: * Commit the external transaction. Do this in a way appropriate to the * transaction subsystem. */ protected abstract void commitTransaction_impl() throws Exception; /** * INTERNAL: * Roll back the external transaction. Do this in a way appropriate to the * transaction subsystem. */ protected abstract void rollbackTransaction_impl() throws Exception; /** * INTERNAL: * Mark the external transaction for rollback. Do this in a way appropriate to the * transaction subsystem. */ protected abstract void markTransactionForRollback_impl() throws Exception; /** * INTERNAL: * Return true if the status indicates that a transaction can be started. This * would normally mean that no transaction is currently active. * The status is interpreted by the transaction subsystem. * * @param status The current transaction status * @return true if the current state allows for a transaction to be started */ protected abstract boolean canBeginTransaction_impl(Object status); /** * INTERNAL: * Return true if the status indicates that a transaction can be committed. This * would normally mean that a transaction is currently active. * The status is interpreted by the transaction subsystem. * * @param status The current transaction status * @return true if the current state allows for a transaction to be committed */ protected abstract boolean canCommitTransaction_impl(Object status); /** * INTERNAL: * Return true if the status indicates that a transaction can be rolled back. This * would normally mean that a transaction is currently active. * The status is interpreted by the transaction subsystem. * * @param status The current transaction status * @return true if the current state allows for a transaction to be rolled back */ protected abstract boolean canRollbackTransaction_impl(Object status); /** * INTERNAL: * Return true if the status indicates that the SQL should be issued to the db. * This would normally mean that a transaction was active and not being rolled * back or marked for rollback. * The status is interpreted by the transaction subsystem. * * @param status The current transaction status * @return true if the current state allows for the SQL to be sent to the database */ protected abstract boolean canIssueSQLToDatabase_impl(Object status); /** * INTERNAL: * Return true if the status indicates that the unit of work should be merged * into the shared cache. This would normally mean that the transaction was * committed successfully. * The status is interpreted by the transaction subsystem. * * @param status The current transaction status * @return true if the current state dictates that the unit of work should be merged */ protected abstract boolean canMergeUnitOfWork_impl(Object status); /** * INTERNAL: * Convert the status to a string for tracing. */ protected abstract String statusToString_impl(Object status); }