/*
* Copyright (c) 1998, 2021 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 v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// 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;
/**
*
* Purpose : Abstract implementation of an ExternalTransactionController.
*
* Description : 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 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 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.
*/
protected AbstractTransactionController() {
this.unitsOfWork = new ConcurrentHashMap<>();
this.activeUnitOfWorkThreadLocal = new ThreadLocal<>();
}
/**
* Return the exception handler used to handle or wrap exceptions thrown in before/after completion.
*/
@Override
public ExceptionHandler getExceptionHandler() {
return exceptionHandler;
}
/**
* Set an exception handler to handle or wrap exceptions thrown in before/after completion.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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 = this.activeUnitOfWorkThreadLocal.get();
if (activeUnitOfWork != null) {
if (transaction == activeUnitOfWork.getTransaction()) {
return activeUnitOfWork;
}
}
activeUnitOfWork = 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.
*/
@Override
public AbstractSession getSession() {
return session;
}
/**
* INTERNAL:
* Set the manager's session.
*/
@Override
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.
*/
@Override
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.
*
* 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).
*/
@Override
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.
*/
@Override
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.
*/
@Override
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);
}