io.mats3.spring.jms.tx.JmsMatsTransactionManager_JmsAndSpringManagedSqlTx Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mats-spring-jms Show documentation
Show all versions of mats-spring-jms Show documentation
Mats^3 Spring tooling for the Mats^3 JMS implementation, including an implementation of JmsMatsTransactionManager using Spring's PlatformTransactionManager, and tooling for configuring a MatsFactory for different scenarios: development, staging, production.
The newest version!
package io.mats3.spring.jms.tx;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.InfrastructureProxy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import io.mats3.MatsEndpoint.MatsRefuseMessageException;
import io.mats3.MatsEndpoint.ProcessContext;
import io.mats3.MatsFactory.ContextLocal;
import io.mats3.impl.jms.JmsMatsContextLocalCallback;
import io.mats3.impl.jms.JmsMatsException.JmsMatsJmsException;
import io.mats3.impl.jms.JmsMatsException.JmsMatsUndeclaredCheckedExceptionRaisedRuntimeException;
import io.mats3.impl.jms.JmsMatsInternalExecutionContext;
import io.mats3.impl.jms.JmsMatsTransactionManager;
import io.mats3.impl.jms.JmsMatsTransactionManager_Jms;
import io.mats3.util.wrappers.DeferredConnectionProxyDataSourceWrapper;
import io.mats3.util.wrappers.DeferredConnectionProxyDataSourceWrapper.DeferredConnectionProxy;
/**
* Implementation of {@link JmsMatsTransactionManager} that in addition to the JMS transaction keeps a Spring
* {@link PlatformTransactionManager} employing a JDBC DataSource for which it keeps transaction demarcation along with
* the JMS transaction, by means of "Best Effort 1 Phase Commit". Note that you can choose between providing a
* DataSource, in which case this JmsMatsTransactionManager internally creates a {@link DataSourceTransactionManager},
* or you can provide a PlatformTransactionManager
employing a DataSource
that you've created
* and employ externally (i.e. DataSourceTransactionManager
, JpaTransactionManager
or
* HibernateTransactionManager
). In the case where you provide a PlatformTransactionManager
,
* you should definitely wrap the DataSource employed when creating it by the method
* {@link #wrapLazyConnectionDatasource(DataSource)}, so that you get lazy Connection fetching and so that Mats can know
* whether the SQL Connection was actually employed within a Mats Stage - this also elides the committing of an empty DB
* transaction if a Mats Stage does not actually employ a SQL Connection. Note that whether you use the wrapped
* DataSource or the non-wrapped DataSource when creating e.g. {@link JdbcTemplate}s does not matter, as Spring's
* {@link DataSourceUtils} and {@link TransactionSynchronizationManager} has an unwrapping strategy when retrieving the
* transactionally demarcated Connection.
*
* Explanation of Best Effort 1 Phase Commit:
*
* - JMS transaction is entered (a transactional JMS Connection is always within a transaction)
*
- JMS Message is retrieved.
*
- SQL transaction is entered
*
- Code is executed, including SQL statements and production of new "outgoing" JMS Messages.
*
- SQL transaction is committed - Any errors also rollbacks the JMS Transaction, so that none
* of them have happened.
*
- JMS transaction is committed.
*
* Out of that order, one can see that if SQL transaction becomes committed, and then the JMS transaction fails, this
* will be a pretty bad situation. However, of those two transactions, the SQL transaction is absolutely most likely to
* fail, as this is where you can have business logic failures, concurrency problems (e.g. MS SQL's "Deadlock Victim"),
* integrity constraints failing etc - that is, failures in both logic and timing. On the other hand, the JMS
* transaction (which effectively boils down to "yes, I received this message, and sent these") is much harder to
* fail, where the only situation where it can fail is due to infrastructure/hardware failures (exploding server / full
* disk on Message Broker). This is called "Best Effort 1PC", and is nicely explained in
* this article. If this failure occurs, it will be caught and logged on ERROR level (by
* {@link JmsMatsTransactionManager_Jms}) - and then the Message Broker will probably try to redeliver the message. Also
* read the Should I use XA Transactions from Apache
* Active MQ.
*
* Wise tip when working with Message Oriented Middleware: Code idempotent! Handle double-deliveries!
*
* The transactionally demarcated SQL Connection can be retrieved from within Mats Stage lambda code user code using
* {@link ProcessContext#getAttribute(Class, String...) ProcessContext.getAttribute(Connection.class)} - which also is
* available using {@link ContextLocal#getAttribute(Class, String...) ContextLocal.getAttribute(Connection.class)}.
* Notice: In a Spring context, you can also get the transactionally demarcated thread-bound Connection via
* {@link DataSourceUtils#getConnection(DataSource) DataSourceUtils.getConnection(dataSource)} - this is indeed what
* Spring's JDBC Template and friends are doing. If you go directly to the DataSource, you will get a new Connection
* not participating in the transaction! This "feature" might sometimes be of interest if you want something to be
* performed regardless of whether the stage processing fails or not. (However, if you do such a thing, you must
* remember the built-in retry mechanism JMS Message Brokers has: If something fails, whatever database changes you
* performed successfully with such a non-tx-managed Connection will not participate in the rollback, and will already
* have been performed when the message is retried. This might, or might not, be what you want.).
*
* @author Endre Stølsvik 2019-05-09 20:27 - http://stolsvik.com/, [email protected]
*/
public class JmsMatsTransactionManager_JmsAndSpringManagedSqlTx extends JmsMatsTransactionManager_Jms {
private static final Logger log = LoggerFactory.getLogger(JmsMatsTransactionManager_JmsAndSpringManagedSqlTx.class);
private final PlatformTransactionManager _platformTransactionManager;
private final DataSource _dataSource; // Hopefully wrapped.
private final Function _transactionDefinitionFunction;
private final static String LOG_PREFIX = "#SPRINGJMATS# ";
/**
* A {@literal Supplier}
bound to {@link ContextLocal} when inside a Mats-transactional
* demarcation.
*/
public static final String CONTEXT_LOCAL_KEY_CONNECTION_EMPLOYED_STATE_SUPPLIER = "JmsAndSpringManagedSqlTx.connectionEmployedSupplier";
private JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(
PlatformTransactionManager platformTransactionManager, DataSource dataSource,
Function transactionDefinitionFunction) {
// Store the PlatformTransactionManager we got
_platformTransactionManager = platformTransactionManager;
// Store the DataSource
_dataSource = dataSource;
if (!(dataSource instanceof DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy)) {
log.warn(LOG_PREFIX + "The DataSource provided with the PlatformTransactionManager is not wrapped with our"
+ " special '" + DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy.class.getSimpleName()
+ "', which will hinder the SpringJmsMats implementation from knowing whether the SQL Connection"
+ " was actually employed by the MatsFactory's Endpoints' Stages. You can do this wrapping"
+ " (before creating the PlatformTransactionManager) by invoking the static method"
+ " 'wrapLazyConnectionDatasource(dataSource)'. PlatformTransactionManager in question: ["
+ platformTransactionManager + "], of class [" + platformTransactionManager.getClass()
.getName() + "]");
}
// Use the supplied TransactionDefinition Function - which probably is our own default.
_transactionDefinitionFunction = transactionDefinitionFunction;
}
private JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(DataSource dataSource,
Function transactionDefinitionFunction) {
// ?: Is the DataSource already wrapped with our proxy?
if (dataSource instanceof DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy) {
log.info(LOG_PREFIX + "The DataSource provided is already wrapped with "
+ DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy.class.getSimpleName() + ", which"
+ " is okay (we otherwise wrap it ourselves). [" + dataSource + "]");
_dataSource = dataSource;
}
else {
// Wrap the DataSource up in the insanity-inducing stack of wrappers.
_dataSource = wrapLazyConnectionDatasource(dataSource);
}
// Make the internal DataSourceTransactionManager, using the wrapped DataSource.
_platformTransactionManager = new DataSourceTransactionManager(_dataSource);
log.info(LOG_PREFIX + "Created own DataSourceTransactionManager for the JmsMatsTransactionManager: ["
+ _platformTransactionManager + "] with magic wrapped DataSource [" + _dataSource + "].");
// Use the supplied TransactionDefinition Function - which probably is our own default.
_transactionDefinitionFunction = transactionDefinitionFunction;
}
/**
* Simplest, recommended if you do not need the PlatformTransactionManager in your Spring context! - However,
* if you need the PlatformTransaction manager in the Spring context, then make it externally (typically using a
* {@literal @Bean} annotated method, and make sure to wrap the contained DataSource first with
* {@link #wrapLazyConnectionDatasource(DataSource)}), and use the factory method
* {@link #create(PlatformTransactionManager)} (it will find the DataSource from the PlatformTransactionManager by
* introspection).
*
* Creates an internal {@link DataSourceTransactionManager} for this created JmsMatsTransactionManager, and ensures
* that the supplied {@link DataSource} is wrapped using the {@link #wrapLazyConnectionDatasource(DataSource)}
* method. Also with this way to construct the instance, Mats will know whether the stage or initiation actually
* performed any SQL data access.
*
* Uses a default {@link TransactionDefinition} Function, which sets the transaction name, sets Isolation Level to
* {@link TransactionDefinition#ISOLATION_READ_COMMITTED}, and sets Propagation Behavior to
* {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW}.
*
* @param dataSource
* the DataSource to make a {@link DataSourceTransactionManager} from - which will be wrapped using
* {@link #wrapLazyConnectionDatasource(DataSource)} if it not already is.
* @return a new {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
public static JmsMatsTransactionManager_JmsAndSpringManagedSqlTx create(DataSource dataSource) {
log.info(LOG_PREFIX + "create(DataSource) [" + dataSource + "]");
return new JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(dataSource,
getStandardTransactionDefinitionFunctionFor(DataSourceTransactionManager.class));
}
/**
* Creates an internal {@link DataSourceTransactionManager} for this created JmsMatsTransactionManager, and ensures
* that the supplied {@link DataSource} is wrapped using the {@link #wrapLazyConnectionDatasource(DataSource)}
* method. Also with this way to construct the instance, Mats will know whether the stage or initiation actually
* performed any SQL data access.
*
* Uses the supplied {@link TransactionDefinition} Function to define the transactions - consider
* {@link #create(DataSource)} if you are OK with the standard.
*
* @param dataSource
* the DataSource to make a {@link DataSourceTransactionManager} from - which will be wrapped using
* {@link #wrapLazyConnectionDatasource(DataSource)} if it not already is.
* @param transactionDefinitionFunction
* a {@link Function} which returns a {@link DefaultTransactionDefinition}, possibly based on the
* provided {@link JmsMatsTxContextKey} (e.g. different isolation level for a special endpoint).
* @return a new {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
public static JmsMatsTransactionManager_JmsAndSpringManagedSqlTx create(DataSource dataSource,
Function transactionDefinitionFunction) {
log.info(LOG_PREFIX + "create(DataSource, transactionDefinitionFunction) [" + dataSource + "], ["
+ transactionDefinitionFunction + "]");
return new JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(dataSource, transactionDefinitionFunction);
}
/**
* Next simplest, recommended if you also need the PlatformTransactionManager in your Spring context!
* (otherwise, use the {@link #create(DataSource)} factory method). Creates an instance of this class from a
* provided {@link PlatformTransactionManager} (of a type which manages a DataSource), where the supplied instance
* is introspected to find a method getDataSource()
from where to get the underlying DataSource. Do
* note that you should preferably have the {@link DataSource} within the
* {@link PlatformTransactionManager}
wrapped using the
* {@link #wrapLazyConnectionDatasource(DataSource)} method. If not wrapped as such, Mats will not be able to
* know whether the stage or initiation actually performed data access.
*
* Uses the standard {@link TransactionDefinition} Function, which sets the transaction name, sets Isolation Level
* to {@link TransactionDefinition#ISOLATION_READ_COMMITTED} (unless HibernateTxMgr), and sets Propagation Behavior
* to {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW} - see
* {@link #getStandardTransactionDefinitionFunctionFor(Class)}.
*
* @param platformTransactionManager
* the {@link DataSourceTransactionManager} to use for transaction management.
* @return a new {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
public static JmsMatsTransactionManager_JmsAndSpringManagedSqlTx create(
PlatformTransactionManager platformTransactionManager) {
log.info(LOG_PREFIX + "create(PlatformTransactionManager) [" + platformTransactionManager + "]");
log.info(LOG_PREFIX + "Introspecting the supplied PlatformTransactionManager to find a method .getDataSource()"
+ " from where to get the DataSource. [" + platformTransactionManager + "]");
DataSource dataSource = getDataSourceFromTransactionManager(platformTransactionManager);
log.info(LOG_PREFIX + ".. found DataSource [" + dataSource + "].");
return new JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(platformTransactionManager, dataSource,
getStandardTransactionDefinitionFunctionFor(platformTransactionManager.getClass()));
}
/**
* Creates an instance of this class from a provided {@link PlatformTransactionManager} (of a type which manages a
* DataSource), where the supplied instance is introspected to find a method getDataSource()
from where
* to get the underlying DataSource. Do note that you should preferably have the {@link DataSource} within the
* {@link PlatformTransactionManager}
wrapped using the
* {@link #wrapLazyConnectionDatasource(DataSource)} method. If not wrapped as such, Mats will not be able to
* know whether the stage or initiation actually performed data access.
*
* Uses the supplied {@link TransactionDefinition} Function to define the transactions - consider
* {@link #create(PlatformTransactionManager)} if you are OK with the standard.
*
* @param platformTransactionManager
* the {@link DataSourceTransactionManager} to use for transaction management.
* @return a new {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
public static JmsMatsTransactionManager_JmsAndSpringManagedSqlTx create(
PlatformTransactionManager platformTransactionManager,
Function transactionDefinitionFunction) {
log.info(LOG_PREFIX + "create(PlatformTransactionManager) [" + platformTransactionManager + "]");
log.info(LOG_PREFIX + "Introspecting the supplied PlatformTransactionManager to find a method .getDataSource()"
+ " from where to get the DataSource. [" + platformTransactionManager + "]");
DataSource dataSource = getDataSourceFromTransactionManager(platformTransactionManager);
log.info(LOG_PREFIX + ".. found DataSource [" + dataSource + "].");
return new JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(platformTransactionManager, dataSource,
transactionDefinitionFunction);
}
/**
* Creates a {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx} from a provided
* {@link PlatformTransactionManager} (of a type which manages a DataSource) - Do note that you should preferably
* have the {@link DataSource} within the {@link PlatformTransactionManager}
wrapped using the
* {@link #wrapLazyConnectionDatasource(DataSource)} method. If not wrapped as such, Mats will not be able to know
* whether the stage or initiation actually performed data access.
*
* Note: Only use this method if the variants NOT taking a DataSource fails to work. It is imperative that
* the DataSource and the PlatformTransactionManager provided "match up", meaning that the DataSource provided is
* actually the instance which the PlatformTransactionManager handles.
*
* Uses the standard {@link TransactionDefinition} Function, which sets the transaction name, sets Isolation Level
* to {@link TransactionDefinition#ISOLATION_READ_COMMITTED} (unless HibernateTxMgr), and sets Propagation Behavior
* to {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW} - see
* {@link #getStandardTransactionDefinitionFunctionFor(Class)}.
*
* @param platformTransactionManager
* the {@link DataSourceTransactionManager} to use for transaction management.
* @param dataSource
* the {@link DataSource} which the supplied {@link PlatformTransactionManager} handles.
* @return a new {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
public static JmsMatsTransactionManager_JmsAndSpringManagedSqlTx create(
PlatformTransactionManager platformTransactionManager, DataSource dataSource) {
log.info(LOG_PREFIX + "create(PlatformTransactionManager, dataSource) [" + platformTransactionManager + "], ["
+ dataSource + "]");
// Trying to find the DataSource from the PlatformTransactionManager nevertheless
try {
DataSource dataSourceFromTxMgr = getDataSourceFromTransactionManager(platformTransactionManager);
log.warn(LOG_PREFIX + "NOTICE: I managed to get the DataSource from the PlatformTransactionManager you"
+ " provided, and thus I suggest that you instead use the factory methods NOT taking a"
+ " DataSource, to minimise the chances of supplying the wrong DataSource compared to"
+ " what is managed by the PlatformTransactionManager.");
if (dataSourceFromTxMgr != dataSource) {
log.warn(LOG_PREFIX + "NOTICE VERY MUCH! The DataSource provided in the factory method is NOT the same"
+ " instance that I got by introspecting the PlatformTransactionManager. This is MOST PROBABLY"
+ " not what you want, and I was torn whether to throw an Exception here - but you might got"
+ " your reasons?!");
}
}
catch (IllegalArgumentException e) {
/* no-op: Not being able to get the DataSource should be the reason why you employ this factory method! */
}
return new JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(platformTransactionManager, dataSource,
getStandardTransactionDefinitionFunctionFor(platformTransactionManager.getClass()));
}
/**
* Creates a {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx} from a provided
* {@link PlatformTransactionManager} (of a type which manages a DataSource) - Do note that you should preferably
* have the {@link DataSource} within the {@link PlatformTransactionManager}
wrapped using the
* {@link #wrapLazyConnectionDatasource(DataSource)} method. If not wrapped as such, Mats will not be able to know
* whether the stage or initiation actually performed data access.
*
* Note: Only use this method if the variants NOT taking a DataSource fails to work. It is imperative that
* the DataSource and the PlatformTransactionManager provided "match up", meaning that the DataSource provided is
* actually the instance which the PlatformTransactionManager handles.
*
* Uses the supplied {@link TransactionDefinition} Function to define the transactions - consider
* {@link #create(PlatformTransactionManager, DataSource)} if you are OK with the standard.
*
* @param platformTransactionManager
* the {@link PlatformTransactionManager} to use for transaction management (must be one employing a
* DataSource).
* @param dataSource
* the {@link DataSource} which the supplied {@link PlatformTransactionManager} handles.
* @param transactionDefinitionFunction
* a {@link Function} which returns a {@link DefaultTransactionDefinition}, possibly based on the
* provided {@link JmsMatsTxContextKey} (e.g. different isolation level for a special endpoint).
* @return a new {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
public static JmsMatsTransactionManager_JmsAndSpringManagedSqlTx create(
PlatformTransactionManager platformTransactionManager, DataSource dataSource,
Function transactionDefinitionFunction) {
log.info(LOG_PREFIX + "create(PlatformTransactionManager, dataSource, transactionDefinitionFunction) ["
+ platformTransactionManager + "], [" + dataSource + "], [" + transactionDefinitionFunction + "]");
// Trying to find the DataSource from the PlatformTransactionManager nevertheless
try {
DataSource dataSourceFromTxMgr = getDataSourceFromTransactionManager(platformTransactionManager);
log.warn(LOG_PREFIX + "NOTICE: I managed to get the DataSource from the PlatformTransactionManager you"
+ " provided, and thus I suggest that you instead use the factory methods NOT taking a"
+ " DataSource, to minimise the chances of supplying the wrong DataSource compared to"
+ " what is managed by the PlatformTransactionManager.");
if (dataSourceFromTxMgr != dataSource) {
log.warn(LOG_PREFIX + "NOTICE VERY MUCH! The DataSource provided in the factory method is NOT the same"
+ " instance that I got by introspecting the PlatformTransactionManager. This is MOST PROBABLY"
+ " not what you want, and I was torn whether to throw an Exception here - but you might got"
+ " your reasons?!");
}
}
catch (IllegalArgumentException e) {
/* no-op: Not being able to get the DataSource should be the reason why you employ this factory method! */
}
return new JmsMatsTransactionManager_JmsAndSpringManagedSqlTx(platformTransactionManager, dataSource,
transactionDefinitionFunction);
}
/**
* Utility to get the DataSource from a PlatformTransactionManager, assuming that it has a
* getDataSource()
method.
*/
private static DataSource getDataSourceFromTransactionManager(
PlatformTransactionManager platformTransactionManager) {
DataSource dataSource;
try {
Method getDataSource = platformTransactionManager.getClass().getMethod("getDataSource");
getDataSource.setAccessible(true);
dataSource = (DataSource) getDataSource.invoke(platformTransactionManager);
if (dataSource == null) {
throw new IllegalArgumentException("When invoking .getDataSource() on the PlatformTransactionManager,"
+ " we got 'null' return [" + platformTransactionManager + "].");
}
return dataSource;
}
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException("The supplied PlatformTransactionManager does not have a"
+ " .getDataSource() method, or got problems invoking it.", e);
}
}
/**
* Returns the standard TransactionDefinition Function for the supplied PlatformTransactionManager. Sets Isolation
* Level to {@link TransactionDefinition#ISOLATION_READ_COMMITTED ISOLATION_READ_COMMITTED} (unless
* HibernateTransactionManager, which does not support setting Isolation Level) and Propagation Behavior to
* {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW PROPAGATION_REQUIRES_NEW} - and also sets the name of the
* transaction to {@link JmsMatsTxContextKey}.toString().
*/
public static Function getStandardTransactionDefinitionFunctionFor(
Class extends PlatformTransactionManager> platformTransactionManager) {
log.info(LOG_PREFIX + "TransactionDefinition Function not provided, thus using default which sets the"
+ " transaction name, sets Isolation Level to ISOLATION_READ_COMMITTED, and sets Propagation Behavior"
+ " to PROPAGATION_REQUIRES_NEW (unless HibernateTransactionManager, where setting Isolation Level"
+ " evidently is not supported).");
// ?: Is it HibernateTransactionManager?
if (platformTransactionManager.getSimpleName().equals("HibernateTransactionManager")) {
// -> Yes, Hibernate, which does not allow to set Isolation Level
return (txContextKey) -> {
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
transDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transDef.setName(txContextKey.toString());
return transDef;
};
}
// E-> normal mode
return (txContextKey) -> {
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
transDef.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
transDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transDef.setName(txContextKey.toString());
return transDef;
};
}
/**
* Creates a proxy/wrapper that has lazy connection getting, and monitoring of whether the connection was actually
* retrieved. This again enables SpringJmsMats implementation to see if the SQL Connection was actually
* employed (i.e. a Statement was created) - not only whether we went into transactional demarcation, which a
* Mats stage always does. This method is internally employed with the {@link #create(DataSource)
* DataSource-taking} factories of this class which makes an internal {@link DataSourceTransactionManager}, but
* should also be employed if you externally create another type of {@link PlatformTransactionManager}, e.g. the
* HibernateTransactionManager, and provide that to the factories of this class taking a PlatformTransactionManager.
*
* It returns an instance of {@link DeferredConnectionProxyDataSourceWrapper}, but as an extension that also
* implements Spring's {@link InfrastructureProxy}.
*
* We want this Lazy-and-Monitored DataSource which is returned here to "compare equals" with that of the DataSource
* which is supplied to us - and which might be employed by other components "on the outside" of Mats - wrt. how
* Spring's {@link DataSourceUtils} compare them in its ThreadLocal cache-hackery. Therefore, the proxy implement
* {@link InfrastructureProxy} (read its JavaDoc!), which means that Spring can trawl its way down to the actual
* DataSource when it needs to compare. Note: You will find this proxy-handling in the
* {@link TransactionSynchronizationManager#getResource(Object)}, which invokes
* TransactionSynchronizationUtils.unwrapResourceIfNecessary(..)
, where the instanceof-check for
* InfraStructureProxy resides.
*
* "The magnitude of this hack compares favorably with that of the US-of-A's national debt."
*
* Note: It is not a problem if the DataSource supplied is already wrapped in a
* {@link LazyConnectionDataSourceProxy}, but it is completely unnecessary.
*
* Tip: If you would want to check/understand how the LazyConnection stuff work, you may within a Mats stage do a
* DataSourceUtil.getConnection(dataSource) - if the returned Connection's toString() looks like
* "DeferredConnectionProxy@3ec11999 WITHOUT actual Connection from DataSource@3406472c..."
then the
* actual Connection is still not gotten. When it is gotten (e.g. after having done a SQL CRUD operation), the
* toString() will look like "DeferredConnectionProxy@3ec11999 WITH actual Connection@b5cc23a"
*/
public static DataSource wrapLazyConnectionDatasource(DataSource targetDataSource) {
log.info(LOG_PREFIX + "Wrapping provided DataSource in a 'magic lazy proxy' which allows both"
+ " elision of JDBC transaction management if the Connection was never employed, AND allows Mats to"
+ " know whether an initiation or stage employed the Connection or not. [" + targetDataSource + "]");
if (targetDataSource instanceof DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy) {
log.warn(LOG_PREFIX + " \\-> The DataSource you provided was already wrapped with the 'magic lazy proxy',"
+ " so why are you trying to wrap it again? Ignoring by simply returning the already-wrapped"
+ " DataSource you provided (thus not double-wrapping it). [" + targetDataSource + "]");
return targetDataSource;
}
if (targetDataSource instanceof TransactionAwareDataSourceProxy) {
throw new IllegalStateException("The provided DataSource should not be of type"
+ " TransactionAwareDataSourceProxy (read its JavaDoc). Give me a 'cleaner' DataSource, preferably"
+ " a pooled DataSource. [" + targetDataSource + "]");
}
if (targetDataSource instanceof LazyConnectionDataSourceProxy) {
log.info(LOG_PREFIX + " \\-> NOTICE: The provided DataSource is a LazyConnectionDataSourceProxy, which is"
+ " unnecessary, but not really a problem.");
}
DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy deferredDataSource = new DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy(
targetDataSource);
log.info(LOG_PREFIX + "Wrapped the provided DataSource as [" + deferredDataSource + "].");
return deferredDataSource;
}
/**
* @return the employed Spring {@link PlatformTransactionManager}, which either was created by this instance if this
* instance was created using one of the DataSource-taking factory methods, or it was provided if this
* instance was created using one of the PlatformTransactionManager-taking methods.
*/
public PlatformTransactionManager getPlatformTransactionManager() {
return _platformTransactionManager;
}
/**
* @return the employed {@link DataSource}, which either was provided when creating this instance (and thus
* wrapped), or was reflected out of the provided {@link PlatformTransactionManager}. It is hopefully
* wrapped using the {@link #wrapLazyConnectionDatasource(DataSource)}, which is done automatically if this
* instance was created using one of the DataSource-taking factory methods. However, if this instance was
* created using one of the PlatformTransactionManager-taking factory methods, it is up to the user to have
* wrapped it. Note that it works unwrapped too, but SpringJmsMats cannot then know whether the stages
* actually employ the SQL Connection, and must do full transaction demarcation around every stage.
*/
public DataSource getDataSource() {
return _dataSource;
}
/**
* @return the unwrapped variant of {@link #getDataSource()} - note that it is only any {@link InfrastructureProxy}s
* that are unwrapped; Any wrapping done by a database pool is left intact.
*/
public DataSource getDataSourceUnwrapped() {
// ?: Is the DataSource wrapped? (hopefully is..)
if (_dataSource instanceof InfrastructureProxy) {
// -> Yes it is, so unwrap it.
return (DataSource) ((InfrastructureProxy) _dataSource).getWrappedObject();
}
// E-> No, it isn't wrapped, so return directly.
return _dataSource;
}
/**
* Extension of {@link DeferredConnectionProxyDataSourceWrapper} which implements {@link InfrastructureProxy}.
*
* Read JavaDoc for {@link #wrapLazyConnectionDatasource(DataSource)}.
*/
protected static class DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy
extends DeferredConnectionProxyDataSourceWrapper implements InfrastructureProxy {
protected DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy(DataSource targetDataSource) {
super(targetDataSource);
}
@Override
public Object getWrappedObject() {
DataSource targetDataSource = unwrap();
if (targetDataSource instanceof InfrastructureProxy) {
return ((InfrastructureProxy) targetDataSource).getWrappedObject();
}
return targetDataSource;
}
}
@Override
public TransactionContext getTransactionContext(JmsMatsTxContextKey txContextKey) {
// Get the TransactionDefinition for this JmsMatsTxContextKey, which is a constant afterwards.
DefaultTransactionDefinition defaultTransactionDefinition = _transactionDefinitionFunction.apply(txContextKey);
return new TransactionalContext_JmsAndSpringDstm(txContextKey, _platformTransactionManager,
defaultTransactionDefinition, _dataSource);
}
@Override
public String getSystemInformation() {
try {
return "JMS Mats TransactionManager with Spring JDBC Tx Management: " + idThis()
+ "\n DataSource: " + _dataSource
+ "\n Unwrapped DataSource: " + getDataSourceUnwrapped()
+ "\n PlatformTransactionManager: " + _platformTransactionManager;
}
catch (Throwable t) {
return "JMS Mats TransactionManager with Spring JDBC Tx Management: " + idThis()
+ "\n Got Exception when trying to get information: " + t.getClass().getSimpleName()
+ ": " + t.getMessage();
}
}
/**
* The {@link TransactionContext}-implementation for {@link JmsMatsTransactionManager_JmsAndSpringManagedSqlTx}.
*/
private static class TransactionalContext_JmsAndSpringDstm extends TransactionalContext_Jms {
private final static String LOG_PREFIX = "#SPRINGJMATS# ";
private final PlatformTransactionManager _platformTransactionManager;
private final DefaultTransactionDefinition _transactionDefinitionForThisContext;
private final DataSource _dataSource;
public TransactionalContext_JmsAndSpringDstm(
JmsMatsTxContextKey txContextKey,
PlatformTransactionManager platformTransactionManager,
DefaultTransactionDefinition transactionDefinitionForThisContext,
DataSource dataSource) {
super(txContextKey);
_platformTransactionManager = platformTransactionManager;
_transactionDefinitionForThisContext = transactionDefinitionForThisContext;
_dataSource = dataSource;
}
@Override
public void doTransaction(JmsMatsInternalExecutionContext internalExecutionContext,
ProcessingLambda lambda)
throws JmsMatsJmsException, MatsRefuseMessageException {
try { // try-finally: Remove the HookBack from DataSource
// :: We invoke the "outer" transaction, which is the JMS transaction.
super.doTransaction(internalExecutionContext, () -> {
// ----- We're now *within* the JMS Transaction demarcation.
// :: Now go into the SQL Transaction demarcation
TransactionStatus transactionStatus = _platformTransactionManager.getTransaction(
_transactionDefinitionForThisContext);
// ----- We're now *within* the SQL Transaction demarcation.
/*
* IF we do NOT know whether the connection is gotten, because we were NOT given a magic-wrapped
* DataSource, then we cannot give a proper answer here. However, since the logic in the
* doTransaction(..) above always goes into SQL Transactional demarcation, we will always retrieve a
* Connection, and thus 'return true' is the most correct answer we can give in such situation.
* (Only time it would not have been correct, was if there actually was a Lazy proxy in the picture,
* but we can't know what, so we must assume yes.)
*
* NOTE: We COULD have checked if the TransactionSynchronizationManager.getResource(_dataSource) had
* a ConnectionHolder, which had a ConnectionHandle (which by logic above always is true), and then
* checked if the DataSourceUtil.getConnection(dataSource) by any chance was a
* LazyConnectionDataSourceProxy, and then introspect that for whether the Connection was gotten.
* But why could you not then instead use Mats' magic proxy which specifically handles this?!?
*/
// Assume 'true' ("yes, it was employed"), unless we can do better
Supplier connectionEmployedState = () -> true;
// :? If we have a "magic" DataSource, enable the logic where we know whether Stage employed SQL.
if (_dataSource instanceof DeferredConnectionProxyDataSourceWrapper_InfrastructureProxy) {
// -> Yes, special DataSourceWrapper, so magic was-sql-Connection-employed tracking can ensue.
/*
* Fetch the transactional SQL Connection using DataSourceUtils. This should be of our own
* DeferredConnectionProxy (or a wrapped instance of it, which evidently can happen with certain
* setups of Hibernate), which we can utilize to query whether the user lambda code actually
* employed the underlying Connection.
*
* Note that just fetching the transactional SQL Connection instance from DataSourceUtils
* doesn't "employ" it (the actual SQL Connection is not yet fetched from the underlying
* DataSource).
*/
Connection connectionFromDataSourceUtils = DataSourceUtils.getConnection(_dataSource);
// ?: Is it directly a DeferredConnectionProxy? (normal case)
if (connectionFromDataSourceUtils instanceof DeferredConnectionProxy) {
// -> Yes it is directly a DeferredConnectionProxy, so cast it
log.debug(LOG_PREFIX + "SQL Connection directly wrapped as DeferredConnectionProxy gotten"
+ " from DataSourceUtils: [" + connectionFromDataSourceUtils + "]");
DeferredConnectionProxy deferredConnectionProxy = (DeferredConnectionProxy) connectionFromDataSourceUtils;
connectionEmployedState = deferredConnectionProxy::isActualConnectionRetrieved;
}
else {
// -> No, it wasn't directly a DeferredConnectionProxy, so check if it isWrapperFor(..)
try {
if (connectionFromDataSourceUtils.isWrapperFor(DeferredConnectionProxy.class)) {
// -> Yes, it isWrapperFor(..), so unwrap it.
DeferredConnectionProxy deferredConnectionProxy = connectionFromDataSourceUtils
.unwrap(DeferredConnectionProxy.class);
log.debug(LOG_PREFIX + "SQL Connection gotten from DataSourceUtils"
+ " isWrapperFor(DeferredConnectionProxy), thus unwrapped to that:"
+ " [" + deferredConnectionProxy + "], original: ["
+ connectionFromDataSourceUtils + "]");
connectionEmployedState = deferredConnectionProxy::isActualConnectionRetrieved;
}
}
catch (SQLException e) {
log.warn("Got SQLException when trying to invoke .isWrapperFor(DeferredConnectionProxy)"
+ " or .unwrap(DeferredConnectionProxy) on the SQL Connection retrieved by"
+ " DataSourceUtils.getConnection(_dataSource). This should really not happen."
+ " Ignoring (cannot do sql-employ-tracking now) -"
+ " please notify Mats maintainers about this situation.", e);
}
}
}
// :: Make the potential SQL Connection available
// Notice how we here use the DataSourceUtils class, so that we get the tx ThreadLocal Connection.
// Read more at both DataSourceUtils and DataSourceTransactionManager.
internalExecutionContext.setSqlTxConnectionSuppliers(
() -> DataSourceUtils.getConnection(_dataSource), connectionEmployedState);
// :: Also make it available for testing.
JmsMatsContextLocalCallback.bindResource(CONTEXT_LOCAL_KEY_CONNECTION_EMPLOYED_STATE_SUPPLIER,
connectionEmployedState);
try {
if (log.isDebugEnabled()) log.debug(LOG_PREFIX + "About to run ProcessingLambda for "
+ stageOrInit(_txContextKey) + ", within Spring SQL Transactional demarcation.");
/*
* Invoking the provided ProcessingLambda, which typically will be the actual user code (albeit
* wrapped with some code from the JmsMatsStage to parse the MapMessage, deserialize the
* MatsTrace, and fetch the state etc.), which will now be inside both the inner (explicit) SQL
* Transaction demarcation, and the outer (implicit) JMS Transaction demarcation.
*/
lambda.performWithinTransaction();
}
// Catch EVERYTHING that legally can come out of the try-block:
catch (JmsMatsJmsException | MatsRefuseMessageException | RuntimeException | Error e) {
// ----- The user code had some error occur, or want to reject this message.
// !!NOTE!!: The full Exception will be logged by outside JMS-trans class on JMS rollback
// handling.
log.error(LOG_PREFIX + "ROLLBACK SQL: " + e.getClass().getSimpleName() + " while processing "
+ stageOrInit(_txContextKey) + " (should only be from user code)."
+ " Rolling back the SQL Connection.", e);
internalExecutionContext.setUserLambdaExceptionLogged();
/*
* IFF the SQL Connection was fetched, we will now rollback (and close) it.
*/
commitOrRollbackSqlTransaction(internalExecutionContext, connectionEmployedState,
SqlTxAction.ROLLBACK, transactionStatus, e);
// ----- We're *outside* the SQL Transaction demarcation (rolled back).
// We will now throw on the Exception, which will rollback the JMS Transaction.
throw e;
}
catch (Throwable t) {
// ----- This must have been a "sneaky throws"; Throwing an undeclared checked exception.
// !!NOTE!!: The full Exception will be logged by outside JMS-trans class on JMS rollback
// handling.
log.error(LOG_PREFIX + "ROLLBACK SQL: Got an undeclared checked exception " + t.getClass()
.getSimpleName() + " while processing " + stageOrInit(_txContextKey)
+ " (should only be 'sneaky throws' of checked exception in user code)."
+ " Rolling back the SQL Connection.", t);
internalExecutionContext.setUserLambdaExceptionLogged();
/*
* IFF the SQL Connection was fetched, we will now rollback (and close) it.
*/
commitOrRollbackSqlTransaction(internalExecutionContext, connectionEmployedState,
SqlTxAction.ROLLBACK, transactionStatus, t);
// ----- We're *outside* the SQL Transaction demarcation (rolled back).
// Rethrow the Throwable as special RTE, which will rollback the JMS Transaction.
throw new JmsMatsUndeclaredCheckedExceptionRaisedRuntimeException("Got a undeclared checked"
+ " exception " + t.getClass().getSimpleName()
+ " while processing " + stageOrInit(_txContextKey) + ".", t);
}
// ----- The ProcessingLambda went OK, no Exception was raised.
if (log.isDebugEnabled()) log.debug(LOG_PREFIX + "COMMIT SQL: ProcessingLambda finished,"
+ " committing SQL Connection.");
// Check whether Session/Connection is ok before committing DB (per contract with JmsSessionHolder).
try {
internalExecutionContext.getJmsSessionHolder().isSessionOk();
}
catch (JmsMatsJmsException e) {
// ----- Evidently the JMS Session is broken - so rollback SQL
log.error(LOG_PREFIX + "ROLLBACK SQL: " + e.getClass().getSimpleName()
+ " when checking whether"
+ " the JMS Session was OK. Rolling back the SQL Connection.", e);
/*
* IFF the SQL Connection was fetched, we will now rollback (and close) it.
*/
commitOrRollbackSqlTransaction(internalExecutionContext, connectionEmployedState,
SqlTxAction.ROLLBACK, transactionStatus, e);
// ----- We're *outside* the SQL Transaction demarcation (rolled back).
// We will now throw on the Exception, which will rollback the JMS Transaction.
throw e;
}
/*
* IFF the SQL Connection was fetched, we will now commit (and close) it.
*/
commitOrRollbackSqlTransaction(internalExecutionContext, connectionEmployedState,
SqlTxAction.COMMIT, transactionStatus, null);
// ----- We're now *outside* the SQL Transaction demarcation (committed).
// Return nicely, as the SQL Connection.commit() went OK.
// When exiting this lambda, the JMS transaction will be committed by the "outer" JMS tx impl.
});
}
finally {
@SuppressWarnings("unchecked")
Supplier connectionEmployedState = (Supplier) JmsMatsContextLocalCallback.getResource(
CONTEXT_LOCAL_KEY_CONNECTION_EMPLOYED_STATE_SUPPLIER);
if (connectionEmployedState != null) {
if (log.isDebugEnabled())
log.debug("About to exit the SQL Transactional Demarcation - SQL Connection "
+ (connectionEmployedState.get() ? "WAS" : "was NOT") + " employed!");
}
// Unbind the ConnectionAcquiredStateSupplier from ContextLocal
JmsMatsContextLocalCallback.unbindResource(CONTEXT_LOCAL_KEY_CONNECTION_EMPLOYED_STATE_SUPPLIER);
}
}
private enum SqlTxAction {
COMMIT, ROLLBACK
}
/**
* Make note: The Spring DataSourceTransactionManager closes the SQL Connection after commit or rollback. Read
* more e.g. here, or look in
* {@link DataSourceTransactionManager#doCleanupAfterCompletion(Object)}, which invokes
* {@link DataSourceUtils#releaseConnection(Connection, DataSource)}, which invokes doCloseConnection(), which
* eventually calls connection.close().
*/
private void commitOrRollbackSqlTransaction(JmsMatsInternalExecutionContext internalExecutionContext,
Supplier sqlConnectionEmployedSupplier, SqlTxAction sqlTxAction,
TransactionStatus transactionStatus, Throwable exceptionThatHappened) {
// NOTICE: THE FOLLOWING if-STATEMENT IS JUST FOR LOGGING!
// ?: Was connection gotten by code in ProcessingLambda (user code)
// NOTICE: We must commit or rollback the Spring TransactionManager nevertheless, to clean up
if (!sqlConnectionEmployedSupplier.get()) {
// -> No, Connection was not gotten
if (log.isDebugEnabled()) log.debug(LOG_PREFIX + "NOTICE: SQL Connection was NOT requested by stage"
+ " or initiation (user code), so the following commit/rollback is no-op.");
// NOTICE: We must commit or rollback the Spring TransactionManager nevertheless, to clean up.
// NOTICE: NOT returning! The log line is just for informational purposes.
}
long nanosAsStart_DbCommit = System.nanoTime();
// :: Commit or Rollback
try {
// ?: Commit or rollback?
if (sqlTxAction == SqlTxAction.COMMIT) {
// -> Commit.
// ?: Check if we have gotten into "RollbackOnly" state, implying that the user has messed up.
if (transactionStatus.isRollbackOnly()) {
// -> Yes, we're in "RollbackOnly" - so rollback and throw out.
String msg = "When about to commit the SQL Transaction ["
+ transactionStatus + "], we found that it was in a 'RollbackOnly' state. This implies"
+ " that you have performed your own Spring transaction management within the Mats"
+ " Stage/Initiation, which is not supported. Will now rollback the SQL, and throw"
+ " out to rollback JMS.";
log.error(LOG_PREFIX + msg);
// If the rollback throws, it was a rollback (read the Exception-throwing at final catch).
sqlTxAction = SqlTxAction.ROLLBACK;
// Do rollback.
_platformTransactionManager.rollback(transactionStatus);
// Throw out, so that we /do not/ commit the JMS.
// (NOTE: There won't be an exceptionThatHappened in the "good case" of commit.)
throw new MatsSqlCommitWasRollbackOnlyException(msg);
}
// E-> No, we were NOT in "RollbackOnly" - so commit this stuff, and get out.
_platformTransactionManager.commit(transactionStatus);
if (log.isDebugEnabled()) log.debug(LOG_PREFIX + "Committed SQL Transaction ["
+ transactionStatus + "].");
}
else {
// -> Rollback.
_platformTransactionManager.rollback(transactionStatus);
if (log.isDebugEnabled()) log.warn(LOG_PREFIX + "Rolled Back SQL Transaction ["
+ transactionStatus + "].");
}
}
catch (TransactionException e) {
MatsSqlCommitOrRollbackFailedException failedException = new MatsSqlCommitOrRollbackFailedException(
"Could not [" + sqlTxAction + "] SQL Transaction [" + transactionStatus
+ "] - for [" + _txContextKey + "].", e);
// ?: If we had an Exception "on its way out", then we'll have to add this as suppressed.
if (exceptionThatHappened != null) {
failedException.addSuppressed(exceptionThatHappened);
}
throw failedException;
}
finally {
internalExecutionContext.setDbCommitNanos(System.nanoTime() - nanosAsStart_DbCommit);
}
}
/**
* Raised if we come into commit/rollback, and find that TransactionStatus is in "rollbackOnly mode".
*/
static final class MatsSqlCommitWasRollbackOnlyException extends RuntimeException {
public MatsSqlCommitWasRollbackOnlyException(String message) {
super(message);
}
}
/**
* Raised if commit or rollback of the SQL Connection failed.
*/
static final class MatsSqlCommitOrRollbackFailedException extends RuntimeException {
MatsSqlCommitOrRollbackFailedException(String message, Throwable cause) {
super(message, cause);
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy