Saturday, July 22, 2006

J2EE JCA Resource Adapters: The problem with XAResource wrappers

Let's say that you're writing an outbound JCA Resource Adapter. Let's say that it supports XA. Let's say that you would need to know when the transaction is committed. You would be tempted to provide a wrapper around the "native" XAResource. If you are, read on: there are some problems you need to consider before doing that! Warning: technical warning alert! The remainder of this posting is full of technical terms like XAResource and ManagedConnection.


First let me explain in more detail what I am talking about.

Introduction


A client runtime typically is a library that takes care of the communication between a Java client and a server. For instance, a JMS client runtime is one or more jars that implement the JMS api and takes care of the communication with the JMS server. Likewise, a JDBC client runtime library implements the JDBC api to provide connectivity with a database server.



Many adapters that support XA either wrap around or build on top of an existing client runtime. For example, a JMS resource adapter typically wraps around an existing JMS client runtime. A workflow engine adapter may internally use a JDBC connection for persistence so it can be said that it builds on top of a JDBC client runtime.




How does the native XAResource fit in with the JCA container? When the application server requests the XAResource from the managed connection through the getXAResource() call, the managed connection may return its own implementation of the XAResource object, or it may return the XAResource implemented by the client runtime (the "native" XAResource). The former type is essentially a wrapper around the XAResource implemented by the client runtime.




Why is this important? Often it is necessary for a managed connection to be notified of the progress of a transaction: a managed connection may need to update its state after the transaction has been committed or rolled back. The JCA spec does not provide a standard way of doing this other than through the XAResource. This may invite you (the developer of the adapter) to write a wrapper around the XAResource instead of exposing the XAResource of the underlying client runtime directly.


There are some problems associated with the wrapper-approach, which will next be discussed in detail.



How should isSameRM() be implemented?

The isSameRM() method is called by the transaction manager to find out if two XAResource-s use the same underlying resource manager. If this is the case, instead of creating a new transaction branch, the transaction manager will join the second XAResource into the same transaction branch.

The method isSameRM() can be implemented as follows:



















class WrappedXAResource implements XAResource {
  private XAResource delegate;

public boolean isSameRM(XAResource other) {
 if (other instanceof WrappedXAResource) {
  return delegate.isSameRM(other.delegate);
} else {
return delegate.isSameRM(other);
}
}
}




Let's look at a scenario where there are three resources to be enlisted in the same transaction. Two resources belong to the same resource adapter (say W1 and W3), and the other resource belongs to an unknown entity, say R2. Let's assume that the the underlying resource manager is the same. This can happen for example when the resource adapter builds on top of JDBC driver, and the other entity is in fact a database connection to the same database.




Let's say that the application server enlists the resources in this order in the transaction: W1, R2, W3. The transaction manager may call the isSameRM() method as follows:


Case A


  1. enlist W1:
  2. enlist R2:
  3. W1.isSameRM(R2); // returns true; R2 is joined into W1
  4. enlist R3:
  5. W1.isSameRM(W3); // returns true; W3 is joined into W1










In this case, all resources are joined, i.e. one branch with W1 receiving the prepare/commit/rollback calls.




Alternatively, the transaction manager may invoke the isSameRM() call as follows:




Case B


  1. enlist W1:
  2. enlist R2:
  3. R2.isSameRM(W1); // returns false
  4. enlist R3:
  5. W3.isSameRM(W1); // returns true; W3 is joined with W1










In this case there will be two transaction branches with W1 and R2 receiving both the prepare/commit/rollback calls.




Exactly how the transaction manager invokes the isSameRM() method depends on the implementation of the transaction manager and may be differ from one implementation to another.




Now let's look at what happens if the resources happen to be enlisted in this order: R1, W2, W3




Case C


  1. enlist R1
  2. enlist W2
  3. R1.isSameRM(W2); // returns false
  4. enlsit W3
  5. R1.isSameRM(W3); // returns false
  6. W2.isSameRM(W3); // returns true; W3 is joined into W2












In this case there will be two branches with R1 and W2 receiving prepare/commit/rollback calls




Case D


  1. enlist R1
  2. enlist W2
  3. W2.isSameRM(R1); // returns true; W2 is joined into R1
  4. enlist W3
  5. W3.isSameRM(W1); // returns true; W3 is joined into R1










This case results in one transaction branch with R1 receiving the prepare/commit/rollback calls, and W2 or W3 receiving none.




To avoid case D where none of the wrappers receive prepare/commit/rollback calls, the implementation of isSameRM() should only consider other wrappers, and never consider an unwrapped XAResource:

public boolean isSameRM(XAResource other) {
if (other instanceof WrappedXAResource) {
return delegate.isSameRM(other.delegate);
} else {
return false;
}
}

This will also take care of the intransitive behavior of isSameRM() where W1.isSameRM(R2) returns true, while R2.isSameRM(W1) returns false.




Note that if multiple wrappers are joined together, only one wrapper will receive the prepare/commit/rollback calls. It is possible to keep track of all resources that are joined together, but this code becomes rather complicated although feasible. A simpler approach is to always return false in the isSameRM() method:

public boolean isSameRM(XAResource other) {
return false;
}

The obvious drawback is that this will result in more transaction branches and will be more expensive.




There's another complication that may result in the wrapper not getting any commit/rollback calls. This has to do with optimizations in the resource manager.




XAResource.prepare()


If R1 and W2 are really using the same resource manager, but the isSameRM() call returned false, there will be two transaction branches from the perspective of the transaction manager. The underlying resource manager however will see two branches of the same with the same global transaction id. The resource manager may then decide to join these two branches together internally. The result is that when the transaction manager calls XAResource.prepare() on W2, the underlying XAResource may return XA_RDONLY. If the tranaction manager receives this signal, it should not call commit() or rollback() on that resource.




The wrapper can provide more code to deal with this situation: instead of delegating the call to prepare() to the underlying XAResource and returning the return value to the caller (the transaction manager), the wrapper should make sure that it will never return XA_RDONLY. It should store this fact in its internal state, so that when the transaction manager calls commit() or rollback(), the wrapper will check if it had overruled XA_RDONLY and not call commit() or rollback() on the underlying XAResource.




The expense of having multiple branches


The performance difference between a transaction with a single branch and a transaction with two branches is enormous. In the case of a single branch, the transaction manager can skip the call to prepare() and only needs to call commit(onephase=true). The transaction manager does not need to log any state to its transaction log. Any write operation to the disk, both by the underlying resource manager and the transaction manager writing to the transaction log is expensive. This is because to be able to guarantee transactional integrity, the write-operations will have to guarantee that the data is in fact on the disk, and not in some write cache. This is done by “syncing” the data to disk. This is an expensive operation; even a fast hard drive can not sync to the disk faster than say 100 times per second. So, changing a single branch transaction to a transaction with two branches, is in fact very expensive.


An alternative


Instead of using wrappers around the XAResource, it's also possible to register interest in the outcome of the transaction by registering a javax.transaction.Synchronization object with the transaction manager. This interface declares two methods: beforeCompletion() and afterCompletion(). The latter takes an argument to indicate if the transaction was committed or rolled back.




The Synchronization object needs to be registered with the javax.transaction.Transaction object using the registerSynchronization(Synchronization sync) method; this object can be obtained from the javax.transaction.TransactionManager object using the getTransaction() method. The question is how to obtain a handle to the TransactionManager. This is not specified in the J2EE spec, and different application servers make the transaction manager available in different ways. As it turns out, most application servers bind the transaction manager in JNDI and for a few others, some extra code is necessary to invoke some methods on some classes. A notable exception is IBM WebSphere that does not provide access to the javax.transaction interfaces, but provides its own proprietary interfaces. However, with some extra code, the same behavior can be obtained. The bottom line is that it is doable to develop some code that can register a Synchronization object on all current application servers.




The approach using a Synchronization object does not suffer from the performance penalty of causing multiple transaction branches when only one would suffice. Hence, this is a better alternative than using wrappers.



4 comments:

Michael Giroux said...

Very good article. Thanks.

Regarding the performance issues related to multiple branches --

> even a fast hard drive can not sync to the disk faster than say 100 times per second.

For a transaction engine supporting a single thread of execution, this is a significant problem. However, I would like to refer you to howl.objectweb.org for an implementation of a logger that supports XA 2PC event logging at rates exceeding 10K tx/sec. By combining the sync operations for multiple threads it is possible to achieve exceptional throughput for the system as a whole.

Frank Kieviet said...

Re Michael Giroux,

Thanks Michael. I completely agree with you. I wrote a blog entry about this here: http://blogs.sun.com/fkieviet/entry/transactions_disks_and_performance.

Frank

Mujeeb said...

Hi Thanks for the great Template...I need a favour from you ..I need some examples on How to use Spring + Jms Client to use Resource Adapter...Urgent

ahmed EL IDRYSY said...

Very good article. Thanks.