Simple JTA transaction manager, without all the bells and whistles
Sometimes you just need to have a database and message queue share in a single transaction. Wouldn't you wish that was a simple thing? Sometimes you would add a simple component to your application and you would have to complement your backup strategy, because that component saves stuff directly as files. Do you wish you had a choice in that?
Just for that purpose, Simple JTA was created.
Simple JTA, is just that: a simple JTA transaction manager. It does not come with any bells and whistles, it just does the job it was designed to do: it manages the transactions for a single (local) application (component).
Because Simple JTA just implements the base requirements some features have been skipped:
- Enlisting arbitrary (eg. not via the Simple JTA datasource or connection factory adapters) XA resources (
javax.transaction.Transaction#enlistResource
) is not supported. - Nested transactions (
javax.transaction.TransactionManager#begin
when another transaction is already started) are not supported - JMS (durable) connection consumers (
javax.jms.Connection#createConnectionConsumer
andjavax.jms.Connection#createDurableConnectionConsumer
) are not supported
The following undocumented features (or expected inner workings) were introduced during testing:
- Reusing physical database connections within a transaction; when
java.sql.Connection#close
has been called and the transaction has not been ended, a call tojava.sql.DataSource#getConnection
will 're-open' and return the same database connection.
Simple JTA is designed to work in a Spring powered application. The transaction manager is started as a simple bean and the XA resources (database or messaging) are wrapped using simple beans. From there on you can just configure the Spring transaction manager and use the Spring transaction code!
The Simple JTA transaction manager needs two things configured to be able to work correctly:
- Unique name(s) to identify it (and the XA resources) and distinguish its transactions; without a unique name two different transaction managers could use the same transaction id and a XA resource (database or messaging store) would become tangled up. When doing recovery after a crash event it would use that unique name to determine if partial transactions should be committed or rolled back.
- A transaction store to 'stably' store transaction information; without a guaranteed store of transaction information a transaction manager would never be able to reliably determine the transaction status when doing recovery.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="simpleJtaTransactionManager" class="nl.futureedge.simple.jta.JtaTransactionManager">
<property name="uniqueName" value="test"/>
<property name="jtaTransactionStore">
<bean class="nl.futureedge.simple.jta.store.jdbc.JdbcTransactionStore">
<property name="create" value="true"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:${test.database.port}/trans"/>
<property name="user" value="sa"/>
<property name="password" value=""/>
</bean>
</property>
</bean>
<!-- Spring transaction manager -->
<bean name="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="simpleJtaTransactionManager" />
</bean>
</beans>
The transaction store of choice for Simple JTA is a database. Why? Because all (our) applications already have a fully functioning, reliable and properly backed up database any way. We don't want to complicate thing by having a set of files (on each application server) that needs to be backed up (in sync) with that.
Property | Explanation | Required |
---|---|---|
uniqueName | The unique name to use for this transaction manager | Yes |
jtaTransactionStore | Transaction store to 'stably' store transaction information | Yes (Autowired) |
The JdbcTransactionStore
stores the transaction information in a database.
Property | Explanation | Required |
---|---|---|
create | If true, the transaction store will try to create the database objects on startup | No (default false) |
driver | The JDBC Driver classname to load (not needed for JDBC 4.0 drivers) | No |
url | The JDBC url to connect to the database | Yes |
user | The username to use when connecting to the database | No |
password | The password to use when connecting to the database | No |
sqlTemplate | SQL template to use; if left empty the transaction store will try to detect the database type based on the JDBC url and else use a SQL-2003 compatible default | No |
storeAll | If true, the transaction store will record all transaction states; else, the store will only record the minimum state | No (default false) |
Note: creating the database objects by using the 'create' option is not advised (applications should not have DDL rights on databases) The distribution contains a jar-file (maven classifier 'sql') that contains the SQL files that can be used to create the database objects.
The JdbcDatabaseInitializer
can be used to create the database objects needed by the JdbcTransactionStore
for testing purposes.
Property | Explanation | Required |
---|---|---|
driver | The JDBC Driver classname to load (not needed for JDBC 4.0 drivers) | No |
url | The JDBC url to connect to the database | Yes |
user | The username to use when connecting to the database | No |
password | The password to use when connecting to the database | No |
The FileTransactionStore
stores the transaction information on the file system. It is mainly provided for debug and testing.
Property | Explanation | Required |
---|---|---|
baseDirectory | The base directory for the transaction logs | Yes |
storeAll | If true, the transaction store will record all transaction states; else, the store will only record the minimum state | No (default false) |
A 'normal' JDBC datasource/connection is not suited to participate in distributed (JTA) transactions. To handle that an application needs to use a JDBC XA datasource/connection. However, most application frameworks only work on datasources and not on XA datasources. Fortunately a JDBC XA datasource only exposes some methods that only need to be called by the transaction manager and ultimately exposes a 'normal' JDBC connection. Therefor Simple JTA provides an adapter that wraps a XA datasource, handles the transaction manager methods and exposes a 'normal' JDBC datasource.
<!-- Vendor provided XA DataSource -->
<bean name="xaDataSource" class="org.hsqldb.jdbc.pool.JDBCXADataSource">
<property name="url" value="jdbc:hsqldb:hsql://localhost:${test.database.port}/test"/>
<property name="user" value="sa"/>
<property name="password" value=""/>
</bean>
<!-- Simple JTA DataSource wrapper -->
<bean name="dataSource" class="nl.futureedge.simple.jta.jdbc.XADataSourceAdapter">
<property name="uniqueName" value="database1" />
<property name="xaDataSource" ref="xaDataSource" />
<property name="supportsJoin" value="false" />
<property name="supportsSuspend" value="false" />
<property name="allowNonTransactedConnections" value="warn" />
</bean>
Property | Explanation | Required |
---|---|---|
uniqueName | The unique name to use for this resource manager | Yes |
xaDataSource | The vendor provided XA DataSource to adapt | Yes |
jtaTransactionManager | The JtaTransactionManager this datasource is managed by (for recovery) | Yes (Autowired) |
supportsJoin | Set to true if this resource correctly supports joining partial transactions | No (default false) |
supportsSuspend | Set to true if this resource supports transaction suspension | No (default false) |
allowNonTransactedConnections | Allow connections outside a transaction (yes, no or warn) | No (default warn) |
The XA Resource is enlisted in the transaction when DataSource#getConnection
is called to open a connection.
When a connection is requested 'outside' a transaction the adapter will return an unmanaged connection. A connection is closed when to transaction is committed or rolled back
As with the database connection a JMS connection factory is not suited to participate in distributed (JTA) transactions. Simple JTA provides an adapter that wraps a JMS XA connection factory, handles the transaciton manager methods and exposes a 'normal' JMS connection factory.
<!-- Vendor provided XA ConnectionFactory -->
<bean name="xaConnectionFactory" class="org.apache.activemq.ActiveMQXAConnectionFactory">
<property name="brokerURL" value="tcp://localhost:${test.broker.port}"/>
</bean>
<!-- Simple JTA ConnectionFactory wrapper -->
<bean name="connectionFactory" class="nl.futureedge.simple.jta.jms.XAConnectionFactoryAdapter">
<property name="uniqueName" value="message1" />
<property name="xaConnectionFactory" ref="xaConnectionFactory" />
<property name="supportsJoin" value="false" />
<property name="supportsSuspend" value="false" />
</bean>
Property | Explanation | Required |
---|---|---|
uniqueName | The unique name to use for this resource manager | Yes |
xaConnectionFactory | The vendor provided XA ConnectionFactory to adapt | Yes |
jtaTransactionManager | The JtaTransactionManager this datasource is managed by (for recovery) | Yes (Autowired) |
supportsJoin | Set to true if this resource correctly supports joining partial transactions | No (default false) |
supportsSuspend | Set to true if this resource supports transaction suspension | No (default false) |
The XA Resource is enlisted in the transaction when Connection#createSession
is called to create a session with the argument transacted
set to true
.
When a session is created with the argument transacted
set to false
an unmanaged session will be returned; when a session is created with the argument transacted
set to true
outside a transaction the connection will throw an exception.
Using the simple-jta namespace this spring configuration can be compressed considerably:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:simple-jta="http://www.future-edge.nl/schema/simple/jta"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.future-edge.nl/schema/simple/jta http://www.future-edge.nl/schema/simple/jta/simple-jta.xsd">
<!-- TransactionManager -->
<simple-jta:transaction-manager name="transactionManager" unique-name="test">
<simple-jta:jdbc-transaction-store create="true" driver="org.hsqldb.jdbc.JDBCDriver"
url="jdbc:hsqldb:hsql://localhost:${test.database.port}/trans" user="sa" password=""
store-all-states="true" />
</simple-jta:transaction-manager>
<!-- DataSource -->
<simple-jta:xa-data-source name="xaDataSource" driver="org.hsqldb.jdbc.pool.JDBCXADataSource"
host="localhost" port="${test.database.port}" database="test"
user="sa" password=""/>
<simple-jta:data-source name="dataSource" unique-name="database1" xa-data-source="xaDataSource"
supports-join="false" supports-suspend="false" allow-non-transacted-connections="warn"/>
<!-- ConnectionFactory -->
<bean name="xaConnectionFactory" class="org.apache.activemq.ActiveMQXAConnectionFactory">
<property name="brokerURL" value="tcp://localhost:${test.broker.port}"/>
</bean>
<simple-jta:connection-factory name="connectionFactory" unique-name="message1" xa-connection-factory="xaConnectionFactory"
supports-join="false" supports-suspend="false" />
</beans>
The JdbcXADataSourceFactory
can be used to enable switching of XA data sources using only configuration. As there is no standard 'configuration' interface implemented by XA data sources, different XA data sources require setting connection properties via all sorts of methods. The JdbcXADataSourceFactory
abstracts these differences by implementing JdbcXADataSourceSupplier
as services.
Property | Explanation | Required |
---|---|---|
driver | The classname of the XA DataSource | Yes |
host | The host to use when connecting to the database | Yes |
port | The port to use when connecting to the database | No |
database | The name of the database to connect to | Yes |
user | The username to use when connecting to the database | No |
password | The password to use when connecting to the database | No |
note: this is functionality is mainly supplied to connect to a different (in memory) xa database when doing tests
A transaction can be suspended by calling TransactionManager#suspend
; the transaction can be resumed by calling TransactionManager#resume
with the transaction received from the suspend
method.
note: resources cannot be reused between suspended transactions; Simple JTA only calls end(TMSUSPEND) and start (TMRESUME) to preserve resources on the server-side (when the resource supports it).
How does the most important responsibility, recovery, of Simple JTA work?
- Whenever an action is taken that involves the transaction manager (eg. using a database connection, using a messaging connection, calling commit, calling rollback), it is stored in the transaction store using the (global or branch) transaction id and unique resource name.
- When commit is requested: first all enlisted XA resources are asked to prepare its partial transaction; on success, the transaction manager stores its decision to commit (this signifies the final decision and cannot be changed) and all XA resources are asked to commit; on failure all XA resources are asked to rollback.
- When rollback is requested: the transaction manager stores its decision to rollback and all XA resources are asked to rollback
The one and only reason to store the transaction info is to be able to reliably recover from a failure. During the startup of the application, whenever a XA resource is 'adapted' the recovery protocol is executed after the datasource is configured. The recovery protocol executes the following steps:
- Retrieve the list of prepared (partial) transactions from the XA resource
- Filter the list using the unique name of the transaction manager to identify the (partial) transactions to handle
- Determine the state of each (partial) transaction (has the transaction manager decided to commit this transaction?) and process accordingly:
- if committing, then commit the partial transaction
- else, rollback the partial transaction
- Store the 'new ' (partial) transaction information using the unique resource name.
After recovery the transaction store is cleaned; fully committed or rolledback transactions are removed from the store.
The following states are always stored for transactions (globally or per branch, where a branch is specific for a XA resource):
Global / Branch | State | Explanation |
---|---|---|
Global | PREPARING | Identification of the global transaction |
Branch | PREPARED | This locks the changes to the resource |
Global | COMMITTING | The decision to commit the transaction |
Branch | COMMITTED | Signifies the changes in the resource have been succesfully committed |
Global (*) | COMMITTED | The transaction has completed succesfully by committing the changes |
Global (**) | ROLLING_BACK | The decision to rollback the transaction |
Branch (**) | ROLLED_BACK | Records the decision to rollback the transaction |
Global (*) | ROLLED_BACK | The transaction has completed succesfully by rolling back the changes |
Both | COMMIT_FAILED | PROTOCOL ERROR: Always recorded, a resource could not be committed after succesfully preparing |
Both | ROLLBACK_FAILED | PROTOCOL ERROR: Always recorded, a resource could not be rolled back |
(*) | Actually removes all transaction information from the store | |
(**) | Only stored when the transaction had started preparing (on recovery an unknown transaction is always rolled back) |
Note: The COMMIT_FAILED and ROLLBACK_FAILED should not happen as the XA protocol in theory does not allow it (a resource should always be able to commit after successfully preparing and a resource should always be able to rollback).
When the transaction store is configured to record all states the following are stored:
State | Global | Branch (per resource) |
---|---|---|
ACTIVE | A transaction is started | A resource is enlisted in the transaction |
PREPARING | Before preparing resources | Before preparing the resource |
PREPARED | After all resources have been prepared successfully | After the resource has been prepared successfully |
COMMITTING | The decision to commit the transaction | Before committing the resource |
COMMITTED | After all resources have been committed | After the resource has been committed successfully |
COMMIT_FAILED | PROTOCOL ERROR: Unexpected failure | PROTOCOL ERROR: a resource could not be committed after succesfully preparing |
ROLLING_BACK | The decision to rollback the transaction | Before rolling back the resource |
ROLLED_BACK | After all resources have been rolled back | After the resource has been rolled back successfully |
ROLLBACK_FAILED | PROTOCOL ERROR: Unexpected failure | PROTOCOL ERROR: a resource could not be rolled back |