Virtually nesting J2EE - JTA transactions, exception handling and tips to remember
It was a fascinating afternoon at work trying to properly address an interesting problem. Always very pleasant to discuss and elaborate with some of my colleagues, like Dimitris Sapounas. Many Thanks to Dimitris for the much appreciated discussion and tips.
1. A common problem - Transacted actions, operations within a transaction.
The problem is not very exotic even though in a typical J2EE application you don't have to address it - most of the time - mostly because of design and simple CRUD operations.
The scenario involves a typical transacted operation, imagine a Session Bean method that already participates in a Transaction (Master T1) and within it's boundaries - it has to complete a variable number of single actions that have to be performed and treated in an atomic manner - meaning either they will complete successfully or fail (and properly roll back). The Master transaction (method) spawning these operations should be remain intact - and successfully commit - complete it;s own master functionality.
1.1 An Overview example:
InvoiceSystemServiceBean, has the method doCompleteBatchUpdateOfInvoices(). This method already participates in a transaction what it does - is updates some generic objects (DB operations) and then calls using a simple loop construct - another method of the same bean (** InvoiceSystemServiceBean**) that is called - performAtomicInvoicePayment(). When the multiple invocations complete no matter of the result - the master method will have to do some final house keeping (doing some db inserts and selects) in order to gather statistics - and then complete saving the statistics information.
2. Error Handling using nested transactions and potential ways of implementing house keeping.
2.1 The atomic business operation.
As you might have red - the term nested transactions in the J2EE context is not valid. Nested transactions are not supported by the spec (up until now) and this feature is highly depended on the underlying RDBMS. What we really want to achieve here with the help of Java's Exception handling and JTA semantics is virtually nest complex atomic operations - in the context of a bigger operation. The execution of the atomic operations involves the insert. update and select of various db resources
- in a manner that (in my case) was almost impossible to provide custom house keeping - clean up code that would revert the state of my business objects to it's original state - if any error occured. I wanted a an automatic way of catching all error cases and roll back the overall atomic operation + propagate the same principle to the underlying transaction.
The proposed way was the following - when it comes to the single 'atomic_ business operation. I will simplify the code in some cases.
@LocalMethod(transactionAttribute = Constants.TransactionAttribute.REQUIRES_NEW)
public void performAtomicInvoicePayment (String invoiceRef) throws InvoiceCheckedException{
try{
//do update of complex object 1
//do select of object 2
//delete object 3
//insert object 5
}catch(Exception e){ //yes catch every error
LOGGER.error("Log ERROR E");
//get the container Session Context
//and mark the transaction for roll back
getSessionContext().setRollBackOnly();
throw new InvoiceCheckedException("The atomic transaction failed",e);
}
}//end of method
Explaining the code above: The atomic operation is not so atomic - just calling it that way - business wise. Numerous business objects are being retrieved or updated. The specs dictate that if anything fails the overall operation should fail and all the objects should revert to their previous state. Since they are complex object relationships it would be better the container and the RDBMS to properly revert of any potential changes - rather than me. So I have enclosed everything to a try catch statement - securing the overall operation from ANY error. At the same time if an error occurs we log the exception, then retrieved the container's- EJB Session Context (EJBContext) and mark the transaction for rolling back. Since this method is called within a bigger batch method we would like the master transaction to be notified in a controlled way for the error - since it has to provide some statistics. Last but not least we have marked the method with a new Transaction Boundary - that is REQUIRES_NEW - forcing the creating of new (flat) transaction.
2.2 Coding the master method - batch operation
As we have already elaborated we have a master method that is coordinating the overall batch operation, apart from invoking the above method is doing it's own house keeping and at the end gather statistics. We have already stated that the master operation already participates in a transaction. I am going to provide an image with a small overview.
Simplified - implementation of the batch method:
@LocalMethod(transactionAttribute =
Constants.TransactionAttribute.REQUIRED)
public void doCompleteBatchUpdateOfInvoices (){
//do update of complex object 1 - house keeping //gets InvoiceReferences, creates error lists try{ for(String s: InvoiceReferences){ //THIS IS A CRITICAL POINT - YOU MUST GET A NEW PROXY INSTANCE!
//it wont work if you do, this.performSingle .
InvoiceSystemService aNewProxy =
ServiceProxy.getInstance(InvoiceSystemService.class);
aNewProxy.performAtomicInvoicePayment(s);
} }catch(InvoiceCheckedException ex){ LOGGER.error("LOG ex",e); anErrorslist.add(e.getMessage()); }
//do some more clean up provide statistics
}//end of method
Explaining the code above: As we can already see the transaction boundary setting for the method is REQUIRED, in my case it always participates in a active transaction. The method does some DB operations of the very first steps marked as house keeping and then in a simple for loop - (using some custome) code - that may vary from project to project depending on the container and structures used, we obtain A NEW ejb PROXY of the same type of session bean (rather than using the this notation or none at all) and then we call the performAtomicInvoicePayment method.
2.3 Why the part in red is so important? In the original version I made the mistake to do the following.
for(String s: InvoiceReferences){
performAtomicInvoicePayment(s);
}
This is valid and no errors or anything will be triggerd. But the result was wrong. What was happening was the following. In an example I had to loop through 3 atomic invoice numbers and perform the operation, one of the was throwing an exception (some conditions were not right) so the exception was trhown, it was caught and the transaction marked for roll back. The order was like this
-
Start batch method - T1
-
Do Invoice T1.1 - Ok Commit
-
Do Invoice T1.2 - Ok Commit
-
Do Invoice T2.3 - Failure - Mark roll back
-
End T1 - try to commit -but failed - transaction was marked as rolled back.!!
This is was definetely not what I was looking for. It seems that the session instance proxy was trying to complete something in a boundary of a transaction (even though it was marked as new) that was already set to roll back. According the spec a specific interface has to be used in order to clean up the instance - if you want to go through such operation. In my case asking for a new proxy did the trick. The related section on the J2EE 6 tutorial can be found here.
Anyway, I hope that all the above, help, tips comments or any other help is always much appreciated.