1

I'm relatively new to XA transactions. I've been struggling a few days to make a simple XA transaction work to no avail.

First, I tried to use two different databases. I set up 2 XA datasources and had succeeded in rolling back the first database operation when the second fails. So far, so good. But then I tried to replace second datasource with JMS connectionFactory and cannot reproduce the same behavior.

Here's the relevant code:

Database logic:

@Stateless
public class FirstDB implements FirstDBLocal {

    @PersistenceContext(unitName = "xaunit")
    private EntityManager em;

    public void doSomething() {
        SomeEntity someEntity = em.find(SomeEntity.class, 1234L);
        someEntity.setSomeFlag(false);
    }

}

JMS code:

@Stateless
public class SecondJMS implements SecondJMSLocal {

    @Resource(mappedName = "java:/JmsXA")
    private ConnectionFactory connFactory;

    @Resource(mappedName = "queue/Some.Queue")
    private Queue q;

    @Override
    @TransactionAttribute(TransactionAttributeType.MANDATORY)
    public void sendMsg() {
        Session session = null;
        Connection conn = null;
        MessageProducer producer = null;
        try {
            conn = connFactory.createConnection("guest", "guest");

            session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE);

            producer = session.createProducer(q);

            // Not sure if I need this, but I found it in the sample code
            conn.start();

            TextMessage tm = session.createTextMessage(new Date().toString());
            producer.send(tm);

            throw new RuntimeException("Fake exception");
        } catch (JMSException e) {
            e.printStackTrace();
        } catch (RuntimeException e) {
            e.printStackTrace();
        } finally {
            // close all resources
        }
    }

}

The glue code:

@Stateless
public class TestDBandJMS implements TestDBandJMSLocal {

    @EJB
    private FirstDBLocal firstDBLocal;

    @EJB
    private SecondJMSLocal secondJMSLocal;

    public void doStuff() {
        firstDBLocal.doSomething();
        secondJMSLocal.sendMsg();
    }

}

XA Connection Factory configuration (everything is JBoss default, except for commented out security settings):

<tx-connection-factory>
      <jndi-name>JmsXA</jndi-name>
      <xa-transaction/>
      <rar-name>jms-ra.rar</rar-name>
      <connection-definition>org.jboss.resource.adapter.jms.JmsConnectionFactory</connection-definition>
      <config-property name="SessionDefaultType" type="java.lang.String">javax.jms.Topic</config-property>
      <config-property name="JmsProviderAdapterJNDI" type="java.lang.String">java:/DefaultJMSProvider</config-property>
      <max-pool-size>20</max-pool-size>
      <!-- <security-domain-and-application>JmsXARealm</security-domain-and-application> -->
      <depends>jboss.messaging:service=ServerPeer</depends>
   </tx-connection-factory>

I also have very simple MDB which just prints out received message to console (not going to post the code, since it's trivial).

The problem is, when the exception is thrown in JMS code, the message is still received by MDB and SomeEntity is successfully updated in the database code (whereas I expect it to rollback).

Here is the JMS log. One fishy thing that I see there is this:

received ONE_PHASE_COMMIT request

Like I said, I'm not too familiar with XA yet, but I expect to see here TWO_PHASE_COMMIT, because there should be 2 resources which participate in the active transaction.

Any help would be much appreciated.

UPDATE

It worked eventually, after I tried @djmorton's suggestion. One other important thing to keep in mind when working with JBoss 5.1 is that the lookup name for XA JMS ConnectionFactory is "java:/JmsXA". I tried the same with

@Resource(mappedName = "XAConnectionFactory")
private ConnectionFactory connFactory;

and it didn't work.

jFrenetic
  • 5,384
  • 5
  • 42
  • 67

1 Answers1

3

You are catching your RuntimeException after throwing it in your sendMsg() method. The Exception will not trigger a transaction rollback unless it is thrown up the stack. When using Container managed transactions, the container adds interceptors to the method calls to setup the transactions and handle rollbacks when unchecked exceptions are thrown. If the exception isn't thrown out of the method the interceptor doesn't know it needs to role the transaction back.

Edit 1:

Note that only a RuntimeException or a subclass of RuntimeException being thrown will cause the transaction to rollback. A checked exception (One that extends Exception rather than RuntimeException) will not cause a rollback unless it is annotated with @ApplicationException(rollback=true).

The other alternative is to inject an EJBContext object, and call .setRollbackOnly() to force the transaction to rollback when the method goes out of scope:

@Stateless
public class SomeEjb {    
    @Resource
    private EJBContext context;

    @TransactionAttribute(TransactionAttributeType.MANDATORY)
    public void rollMeBack() {
        context.setRollbackOnly();
    }
}
djmorton
  • 616
  • 4
  • 6
  • Isn't the EJB transaction marked for rollback as soon as any exception happens? For example, in [this article](http://entjavastuff.blogspot.ru/2011/02/ejb-transaction-management-going-deeper.html) in the section **Dealing with exceptions** it's clearly stated that once the exception is thrown, the transaction is marked for rollback and it's impossible to persist some entity even in **catch** clause. Doesn't the **ONE_PHASE_COMMIT** message look suspicious to you? – jFrenetic Mar 22 '14 at 17:16
  • No. The EJB Transaction is only rolled back if a RuntimeException (not just an Exception) is thrown by a transactional method or if .setRollbackOnly() is called on the UserTransaction object. The act of throwing the exception isn't what causes the transaction to rollback, it's the passing of that exception to the Transactional Interceptor that the container has called before calling your EJB service method, so the exception actually has to be passed up the stack. – djmorton Mar 22 '14 at 19:23
  • Thanks for reply. What do you think about this ONE_PHASE_COMMIT message? It makes me suspect that JMS Session doesn't participate in active XA transaction. I'll have to play around a bit before accepting the answer. – jFrenetic Mar 22 '14 at 19:28
  • That message does seem a little strange. Your code shows that you are using the JmsXA connection factory... are you sure you are using a correctly configured XA datasource, including the correct XA driver classname? What version of JBoss are you using? – djmorton Mar 22 '14 at 19:37
  • Yes, the persistence unit is configured to use XA datasource. I'll need to double-check the driver name, though, thanks for that. The JBoss version is 5.1 (Java EE 5 implementation), so the JPA version is 1.0. As I said before, the example works fine with 2 XA datasources. When I used a regular driver, I got an exception saying that the resource can't be enlisted in transaction or smth like that. I guess I'll have to read more about distributed transactions and Single/Two Phase Commits. Though, any ideas would be appreciated. – jFrenetic Mar 22 '14 at 20:15
  • I'm not positive, but the 'ONE_PHASE_COMMIT' message in the logs might be a result of a performance optimization in XA called 'Last Agent Optimization' (sometimes known as 'Last Resource Commit Optimization'), which allows one participant in an XA transaction to be a non-XA resource. – djmorton Mar 22 '14 at 20:17
  • If you remove the RuntimeException catch block from your message producer and allow the exception to propagate are you seeing the database transaction still be committed? – djmorton Mar 22 '14 at 20:19
  • Nope, the changes are not commited, as expected. But I don't think it proves the fact that JMS session is added to XA transaction. – jFrenetic Mar 22 '14 at 20:51
  • 1
    Then modify your test a bit... Do not throw an exception in your message producer. Call your send method first, and then call your DB modification method. However, add a constraint to your table that will fail when your database change is committed. The database exception should trigger a rollback of the transaction. If you see the message gets sent to your MDB, then you know that your Message producer did not participate in the XA transaction. If the message does not get sent, it successfully participated in the transaction. – djmorton Mar 22 '14 at 21:00
  • That's brilliant! Thanks a ton! Will try it later. – jFrenetic Mar 22 '14 at 21:31