Friday, August 24, 2012

Assorted facts about JBoss. Fact 5: you want control over transaction in @Singleton lifecycle methods? Tough luck.

First, some theory, namely from EJB 3.1 specification (emphasis mine):
Section "4.3.4 Session Bean Life cycle Callback Interceptor Methods"
The following lifecycle event callbacks are supported for session beans. Lifecycle callback interceptor methods may be defined directly on the bean class
...
The PreDestroy lifecycle callback interceptor methods for singleton beans execute in a transaction context determined by the bean's transaction management type and any applicable transaction attribute.
The same is applicable for PostConstruct as well. And then more:
Section "4.8.3 Transaction Semantics of Initialization and Destruction"

PostConstruct and PreDestroy methods of Singletons with container-managed transactions are transactional. From the bean developer's view there is no client of a PostConstruct or PreDestroy method.

A PostConstruct or PreDestroy method of a Singleton with container-managed transactions has transaction attribute REQUIRED, REQUIRES_NEW, or NOT_SUPPORTED (Required , RequiresNew, or NotSupported if the deployment descriptor is used to specify the transaction attribute).
Simple enough, is it not? But such "simple" things not working correctly can cost a lot of effort to find.

Of course it all started with something much more complex. After a seemingly minor change our application started to give problems during JBoss shutdown. JBoss hanged for some time and then started to spew "transaction rolled back" exceptions.

Finding the change that had caused the problem was easy. Why was it a problem? Too much software is too clever for its own good nowadays. This time it was a piece of code (also from JBoss - ha-ha) that managed to hook itself into the ongoing container transaction. The "minor" change caused this software to try ending the ongoing transaction, which it had no rights to do.

Fixing the problem seemed easy: we did not really need the @PreDestroy method to be transactional so adding appropriate @TransactionAttribute to it looked like a solution. Of course it did not work otherwise I would not write this post.

Investigating the problem led to this simple test class:
package p1;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.*;
import javax.naming.InitialContext;
import javax.transaction.TransactionManager;

@Singleton
@Startup
public class TeBean {
    @PostConstruct
    public void startIt() throws Exception {
        System.err.println("TeBean.startIt");
    }

    @PreDestroy
    public void stopIt() throws Exception {
        TransactionManager  tm = (TransactionManager)new InitialContext().lookup("java:/TransactionManager");

        if (tm != null) {
            System.err.println("TeBean.stopIt, transaction: [" + tm.getTransaction() + "] ");
        } else {
            System.err.println("TeBean.stopIt, no transaction manager");
        }
    }
}

Deploying it under JBoss we use (6.1.0.Final) and then undeploying it results in the following message in the server console:
TeBean.stopIt, transaction: [TransactionImple < ac, BasicAction: 0:ffffc0a8016f:... status: ActionStatus.RUNNING >]

Looks perfect, JBoss dutifully runs our @PreDestroy in a transaction. Let's add @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED) to the method and repeat the test. Result:
TeBean.stopIt, transaction: [TransactionImple < ac, BasicAction: 0:ffffc0a8016f:... status: ActionStatus.RUNNING >]

Barring ID difference, the output is exactly the same! Next step: move the annotation to the class itself. Finally I have got what I was expecting:
TeBean.stopIt, transaction: [null]

Apparently JBoss ignores @TransactionAttribute on a @PreDestroy method (and on a @PostConstruct - I verified that).

This behavior is also very clear to see from the stack trace. In case of annotation on the method or no annotation at all:
TeBean.stopIt() line: 24
...
CMTTxInterceptor.invokeInOurTx(TransactionalInvocationContext, TransactionManager) line: 247
CMTTxInterceptor.requiresNew(TransactionalInvocationContext) line: 392
CMTTxInterceptor.invoke(TransactionalInvocationContext) line: 211

Annotation on the class:
TeBean.stopIt() line: 24
...
CMTTxInterceptor.invokeInNoTx(TransactionalInvocationContext) line: 234
CMTTxInterceptor.notSupported(TransactionalInvocationContext) line: 329
CMTTxInterceptor.invoke(TransactionalInvocationContext) line: 207

Can happen. Who cares that this is clearly specified in the EJB 3.1 spec. And it does not matter that JBoss developers knew about this particular nuance of the EJB 3.1 spec.

There is a solution after all. Except for the case that we actually have: we need a transaction in @PostConstruct and at the same time we need "no transaction" in @PreDestroy. Oops. Of course nothing that can't be fixed but it still makes me think how many of such "small things" are around.

For some time already I try all my test things under JBoss 7 as well, using the latest and greatest JBoss 7.1.1.Final. After all, JBoss 6 is the thing from the past, or so RedHat says.

Can JBoss 7 handle this simple test case? Nope. Even the code that worked under JBoss 6 with @TransactionAttribute on the bean class does not work as expected under JBoss 7. It looks like there is no way to control transactions on @Singleton lifecycle methods in JBoss 7. There is no way: I was quite surprised by this so I checked the sources. There was some code that looked at annotations but it was replaced by hardcoded requiresNew somewhere during 7.0 beta. Way to go, guys. Who needs that EJB spec compliance anyway?

1 comment: