Skip to content

An XA Filesystem, update

February 16, 2009

In a comment on my original post, Alessandro Apostoli asks for an example of how to use the XAFileResource class, and specifically how to pass the Xid.

Digging this out has proved an interesting exercise (the code didn’t work at all any more, as I had expected), and it has raised two points worth noting. The second one I believe may be the answer to Alessandro’s question.

The EJB test code is included at the bottom of this post. It was written for Glassfish v2ur2, and I’ve tested that it all works. If you would like a copy of my Eclipse workspace containing the EJB, drop me an email – mdneale on hotmail.com matt@matthewneale.net.

1. String representation of a Xid

As I wrote in the original post, I’m not actually using EJBs in my application, and I’m not using an application server, though I do have a standalone J2EE Transaction Manager. The original tests were done against an application server so that I could prove the theory worked before piling-in more unknowns.

As is noted in the comment at the top of the XAFileResource class, for it to work, the Xid class that is passed to it must have a toString() method which returns a String representation of the Xid. Sadly the Glassfish Xid implementation com.sun.jts.jtsxa.XID doesn’t – invoking toString() on it always returns null. This probably makes sense, as com.sun.jts.jtsxa.XID can’t really know which String form would be useful.

Going back to my original very rough implementation of the XAFileResource class, I had a method in there which converted a Xid to a String, to workaround this exact problem. The problem doesn’t occur in my application as my Xid implementation does provide a String representation when toString() is called.

How you design this is up to you, but for simplicity’s sake, you can just add the following xidToString() method to the XAFileResource class, and change all calls to xid.toString() to a call to this new method. You’ll probably also want to look at the other uses of ‘xid’ as the rest are in log4j debug statements, and you probably will want to see the String representation there too, rather than just “null”.

protected String xidToString(Xid xid) {
	String xidStr = null;

	if (xid != null) {
        	String globalTransactionIdStr = "";
        	String branchQualifierStr = "";

        	if (xid.getGlobalTransactionId() != null) {
                	globalTransactionIdStr = new BigInteger(1, xid.getGlobalTransactionId()).toString(16);
        	}

        	if (xid.getBranchQualifier() != null) {
                	branchQualifierStr = new BigInteger(1, xid.getBranchQualifier()).toString(16);
        	}

        	xidStr = globalTransactionIdStr + "." + branchQualifierStr;
	}

	return xidStr;
}

One word of warning – the Commons Transaction FileResourceManager uses the transaction Id passed to it (the string representation of the Xid) to create a directory on the filesystem, and therefore the String representation needs to be within the limits the filesystem imposes on filenames. The above works fine for me (Glassfish on OS X 10.5), but watch out for that on other Application Servers and Operating Systems.

2. Connection Class

In my original post I alluded to the need for a Connection class, but I didn’t want to include it as I didn’t want to confuse the original intent, which was to communicate the XAResource implementation.  However, looking back at the original test code I can see it could be confusing without a note about it.

The issue is that when the XAFileResource is enlisted in the transaction, the Transaction Manager passes it the Xid to use. However, outside the Transaction Manager and the XAFileResource class, the Xid isn’t visible, so how do we get hold of the Xid in order to pass it to further calls to FileResourceManager (for instance for creating files, writing to files etc)?

The way I dealt with this in my original EJB test case was to store it in a static – a quick dirty fix for the test case, but of no further use as it won’t work for more than one transaction.

The way I’ve dealt with this in my application is to write an XAFileConnection class, which wraps an XAFileResource, but also implements XAResource itself. The XAFileConnection is what is enlisted in the transaction, and thefore, when the start() method is called on it we can store the Xid for use later. The idea is that you keep one XAFileResource class per FileResourceManger, and one XAFileConnection per transaction. This means you need some kind of tracking to ensure one connection per transaction. In a J2EE environment this is normally taken care of for you, but here you’ll have to write some kind of DataSource.

I don’t want to reproduce my entire XAFileConnection code here, as it’s large and repetitive. You’ll get the idea from a simple example. The following is what is used now in the EJB test case:

package txtest;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

public class XAFileConnection implements XAResource {

	private XAFileResource xaFileResource = null;
	private Xid storedXid = null;

	public XAFileConnection(XAFileResource xaFileResource) {
		this.xaFileResource = xaFileResource;
	}

	/**
	 * The start method stores the Xid for this 'connection' and then invokes
	 * the XAFileResource.
	 */
	public void start(Xid xid, int flags) throws XAException {
		this.storedXid = xid;
		xaFileResource.start(xid, flags);
	}

	// The rest of the methods just pass through to the XAFileResource

	public void commit(Xid xid, boolean onePhase) throws XAException {
		xaFileResource.commit(xid, onePhase);
	}

	public void end(Xid xid, int flags) throws XAException {
		xaFileResource.end(xid, flags);
	}

	public void forget(Xid xid) throws XAException {
		xaFileResource.forget(xid);
	}

	public int getTransactionTimeout() throws XAException {
		return xaFileResource.getTransactionTimeout();
	}

	public boolean isSameRM(XAResource xares) throws XAException {
		return xaFileResource.isSameRM(xares);
	}

	public int prepare(Xid xid) throws XAException {
		return xaFileResource.prepare(xid);
	}

	public Xid[] recover(int flag) throws XAException {
		return xaFileResource.recover(flag);
	}

	public void rollback(Xid xid) throws XAException {
		xaFileResource.rollback(xid);
	}

	public boolean setTransactionTimeout(int seconds) throws XAException {
		return xaFileResource.setTransactionTimeout(seconds);
	}

	public Xid getXid() {
		return storedXid;
	}

	/*
	 * Better to do this, and hide the FileResourceManager inside XAFileConnection.
	 * This way you can remove the getXid() method.
	public void createResource(Object resourceId) throws ResourceManagerException {
	}

	public OutputStream writeResource(Object resourceId) throws ResourceManagerException {
	}
	*/

}

If you look at the EJB test code below you’ll see how it all hangs together. In my real code, I don’t return the Xid as I do here. What I’ve done is add methods to XAFileConnection to perform all the FileResourceManager’s tasks – i.e. createResource, writeResource etc. These don’t take a txid parameter any more, as it’s known to the XAFileConnection class. This way we’ve now completely abstracted the actual FileResourceManager.

3. The EJB test code

The one EJB java file is reproduced below with its remote interface. As I mentioned at the top of this post, if you would like an archive of my Eclipse workspace, just send me an email.

The EJB creates a file, and inserts a row into a table. The first argument to the method test() is used as the filename and the data inserted into the table. The second test() argument says whether we want to commit or not. If we don’t, then an exception is thrown and the container-managed transaction rolls-back.

You’ll notice the xidToString() method is duplicated here – this will go away if you hide the FileResourceManager inside the XAFileConnection class as mentioned above.

package txtest;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;

import org.apache.commons.transaction.file.FileResourceManager;
import org.apache.commons.transaction.file.ResourceManagerException;
import org.apache.commons.transaction.file.ResourceManagerSystemException;
import org.apache.commons.transaction.util.Log4jLogger;
import org.apache.commons.transaction.util.LoggerFacade;
import org.apache.log4j.Logger;

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class XATestEJB implements XATestEJBRemote {

	private static final Logger logger = Logger.getLogger(XATestEJB.class);

	private static FileResourceManager fileResourceManager = null;

	static {
		LoggerFacade loggerFacade = new Log4jLogger(logger);

		fileResourceManager = new FileResourceManager(
				"/Users/mdneale/xa-test/base",
				"/Users/mdneale/xa-test/work",
				false,
				loggerFacade);

		try {
			fileResourceManager.start();
		} catch (ResourceManagerSystemException e) {
			logger.error(e);
		}
	}

	@TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void test(String filename, boolean commit) throws XATestEJBException {
    	logger.debug("test(filename=" + filename + ", commit=" + commit + ")");

		try {
			insertRow(filename);
			createFile(filename);

			if (!commit) {
				throw new XATestEJBException("Forced Rollback");
			}
		} catch (Exception e) {
			logger.error(e);
			throw new XATestEJBException(e);
		}
    }

	protected void insertRow(String filename) throws SQLException, NamingException {
		Context context = new InitialContext();

		DataSource dataSource = (DataSource)context.lookup("jdbc/xatest");

		Connection connection = null;
		PreparedStatement statement = null;

		try {
			connection = dataSource.getConnection();

			statement = connection.prepareStatement("INSERT INTO TEST ( filename ) VALUES ( ? )");
			statement.setString(1, filename);

			statement.executeUpdate();
		} finally {
			if (statement != null) { statement.close(); }
			if (connection != null) { connection.close(); }
		}
	}

	protected void createFile(String filename) throws NamingException, SystemException, XAException, IllegalStateException, RollbackException, ResourceManagerException, IOException, XATestEJBException {
		Context context = new InitialContext();

		TransactionManager transactionManager = (TransactionManager)context.lookup("java:appserver/TransactionManager");

		Transaction transaction = transactionManager.getTransaction();

		// Create the XAFileResource
		XAFileResource xaFileResource = new XAFileResource(fileResourceManager);

		// Wrap the XAFileResource in an XAFileConnection
		XAFileConnection xaFileConnection = new XAFileConnection(xaFileResource);

		if (!transaction.enlistResource(xaFileConnection)) {
			throw new XATestEJBException("Failed to enlist resource in transaction");
		}

		fileResourceManager.createResource(xidToString(xaFileConnection.getXid()), filename);

		OutputStream outputStream = fileResourceManager.writeResource(xidToString(xaFileConnection.getXid()), filename);

		PrintWriter writer = new PrintWriter(outputStream);

		writer.print("HELLO");
		writer.flush();

		outputStream.close();
	}

	protected String xidToString(Xid xid) {
		String xidStr = null;

		if (xid != null) {
	        String globalTransactionIdStr = "";
	        String branchQualifierStr = "";

	        if (xid.getGlobalTransactionId() != null) {
                globalTransactionIdStr = new BigInteger(1, xid.getGlobalTransactionId()).toString(16);
	        }

	        if (xid.getBranchQualifier() != null) {
                branchQualifierStr = new BigInteger(1, xid.getBranchQualifier()).toString(16);
	        }

	        xidStr = globalTransactionIdStr + "." + branchQualifierStr;
		}

		return xidStr;
	}

}

The simple Remote interface for completeness:

package txtest;

import javax.ejb.Remote;

@Remote
public interface XATestEJBRemote {
	
    public void test(String filename, boolean commit) throws XATestEJBException;
    
}

Notes:
1. This test uses Glassfish v2ur2, MySQL 5.0, MySQL Connector/J 5.1.7 and Commons Transactions 1.2.
2. My database has one table CREATE TABLE TEST ( filename VARCHAR(50) NOT NULL ).
3. Filesystem directories are defined in the static block at the top of XATestEJB.
4. Nothing special exists in XATestEJBException, except that it is defined with the annotation @ApplicationException(rollback = true).

Advertisements
3 Comments
  1. i got some issue with xid. It returns null here (and all oher stuff like this) :

    return xaFileResource.prepare(xid);

    so i replaced everywhere with the custom xidToString.
    Same for the XAFileConnection and getXid returning String instead of Xid class.

    Everything is fine now

    • On Glassfish v3, Postgres 9.0 and Commons Transactions 1.2.

      • also while creating a file then deleting this file on same global transaction leads to a locktimeout as first transaction doesnt free the lock on the created file. Transaction branch id is different on the 2 operations

        :s

Comments are closed.

%d bloggers like this: