Search code examples
jpajakarta-eetransactionswildflyjca

Why does an exception in LocalXAResourceImpl.commit() merely give a warning?


In WildFly 8, I am using a distributed transaction containing an Oracle XA datasource and a custom JCA LocalTransaction resource (a connection to a file).

Basically it works as expected - if one of the resources fails to commit, the whole transaction is rolled back and neither the database nor the file is updated/written.

There is a special case however. Using JPA, if I am doing an entityManager.merge(entity); and the values of the entity are the same as those in the database, and writing to the file fails (i.e. because it does not exist), all I get is a warning and no exception thrown in my EJB:

@Stateless
public class JCABean {

    @PersistenceContext(unitName = "file-tx") 
    private EntityManager entityManager;

    @Resource(name = "java:/FileDataSource")
    private IDataSource fileDataSource;

    public void insert(
            final long id, 
            final String value, 
            final boolean update) {

        final FileTxTest entity = new FileTxTest();
        entity.setId(id);
        entity.setValue(value);

        if (update) {
            entityManager.merge(entity);
        } else {
            entityManager.persist(entity);
        }

        final File file = new File(FileHelper.BASE_PATH, 
                String.format("%s.txt", id));
        try (final IConnection connection = fileDataSource.getConnection(
                file.getAbsolutePath())) {
            connection.write(String.format("%s%n", value));
        }
    }
}

fileDataSource.getConnection() returns an instance of FileConnection implementing LocalTransaction.

If file is not writeable, connection.commit() throws a ResourceException.

If the value given to entity.setValue() equals the value in the database (and update == true so a merge is done), only this warning is issued:

16:33:58,276 WARN  [com.arjuna.ats.jta] (default task-4) ARJUNA016039: onePhaseCommit on < formatId=131077, gtrid_length=29, bqual_length=36, tx_uid=0:ffff7f000101:842ac6d:56699b01:22, node_name=1, branch_uid=0:ffff7f000101:842ac6d:56699b01:2a, subordinatenodename=null, eis_name=java:/FileDataSource > (LocalXAResourceImpl@1e22d60[connectionListener=e1ca39 connectionManager=1b9de9e warned=false currentXid=null productName=Generic JCA productVersion=1.0 jndiName=java:/FileDataSource]) failed with exception XAException.XA_RBROLLBACK: org.jboss.jca.core.spi.transaction.local.LocalXAException: IJ001156: Could not commit local transaction
        at org.jboss.jca.core.tx.jbossts.LocalXAResourceImpl.commit(LocalXAResourceImpl.java:180) [ironjacamar-core-impl-1.1.9.Final.jar:1.1.9.Final]
        at com.arjuna.ats.internal.jta.resources.arjunacore.XAOnePhaseResource.commit(XAOnePhaseResource.java:113)
        at com.arjuna.ats.internal.arjuna.abstractrecords.LastResourceRecord.topLevelPrepare(LastResourceRecord.java:152)

If the entity being merged has an updated value, the whole transaction fails as expected:

Caused by: javax.transaction.RollbackException: ARJUNA016053: Could not commit transaction.
    at com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionImple.commitAndDisassociate(TransactionImple.java:1178)
    at com.arjuna.ats.internal.jta.transaction.arjunacore.BaseTransaction.commit(BaseTransaction.java:126)
    at com.arjuna.ats.jbossatx.BaseTransactionManagerDelegate.commit(BaseTransactionManagerDelegate.java:75)
    at org.jboss.as.ejb3.tx.CMTTxInterceptor.endTransaction(CMTTxInterceptor.java:93) [wildfly-ejb3-8.2.1.Final.jar:8.2.1.Final]

So effectively, the "all or nothing" requirement is satisfied, but I'd like to have the transaction fail with an exception in any case so I can raise an error.

The Oracle XA datasource:

<xa-datasource jndi-name="java:/OracleDS" pool-name="OracleDS" enabled="true">
    <xa-datasource-property name="URL">
        jdbc:oracle:thin:@host.domain.tld:1521:NAME
    </xa-datasource-property>
    <driver>oracle</driver>
    <xa-pool>
        <min-pool-size>1</min-pool-size>
        <max-pool-size>5</max-pool-size>
        <prefill>true</prefill>
    </xa-pool>
    <security>
        <user-name>user</user-name>
        <password>pass</password>
    </security>
</xa-datasource>
<drivers>
    <driver name="oracle" module="com.oracle">
        <xa-datasource-class>oracle.jdbc.xa.client.OracleXADataSource</xa-datasource-class>
    </driver>
</drivers>

and the resource adapter:

<resource-adapters>
    <resource-adapter id="file-tx-jca.rar">
        <archive>
            file-tx-jca.rar
        </archive>
        <transaction-support>LocalTransaction</transaction-support>
        <config-property name="Server">
            localhost
        </config-property>
        <config-property name="Port">
            19000
        </config-property>
        <connection-definitions>
            <connection-definition class-name="my.package.GenericManagedConnectionFactory" jndi-name="java:/FileDataSource" pool-name="FileConnectionFactory">
                <pool>
                    <min-pool-size>1</min-pool-size>
                    <max-pool-size>5</max-pool-size>
                </pool>
                <security>
                    <application/>
                </security>
            </connection-definition>
        </connection-definitions>
    </resource-adapter>
</resource-adapters>

NOTE: I am aware that I can enlist at most one LocalTransaction resource (last resource) in a transaction. Since I might need to handle more than one file in the same transaction, I have changed the FileConnection and the <resource-adapter> to an XAResource/XATransaction. Here, the transaction always fails if a commit fails (i.e. by throwing an XAException(XAException.XA_HEURHAZ)). But I still would like to understand why there is only a warning logged if commit of a local transaction fails.

Is there some property I can set to get an exception even in that case where I currently only get a warning?


Solution

  • I've read this documentation from JBOSS 5 EAP which describes the LRCO algorithm used when you enlist a single one-phase-aware resource in a transaction which involves other two-phase-aware resources:

    Although the XA transaction protocol is designed to provide ACID properties by using a two-phase commit protocol, model may not always be appropriate. Sometimes it is necessary to allow a non-XA-aware resource manager to participate in a transaction. This is often the case with data stores that do not support distributed transactions.

    In this situation, you can use a technique known as Last Resource Commit Optimization (LRCO). This is sometimes called the Last Resource Gambit. The one-phase-aware resource is processed last in the prepare phase of the transaction, at which time an attempt is made to commit it. If the attempt is successful, the transaction log is written and the remaining resources go through the phase-two commit. If the last resource fails to commit, the transaction is rolled back. Although this protocol allows most transactions to complete normally, some errors can cause an inconsistent transaction outcome. For this reason, use LRCO as a last resort. When a single is used in a transaction, the LRCO is automatically applied to it. In other situations, you can designate a last resource by using a special marker interface. Refer to the JBoss Transactions Programmer's Guide for more details.

    My assumption is the following: When you don't modify the merged entity, nothing is written in the undo log during the prepare phase of the two-phase-aware resource transaction.

    Then, when the commit of the one-phase resource transaction fails (file write) which occurs at the end of the prepare phase, nothing has to be undone for the two-phase resource transaction. Therefore you don't get the RollbackException anymore, which corresponded to a rollback of the two-phase resource transaction involved in global transaction.