JTA spanning multiple RestEasy calls
hipjon May 21, 2010 5:24 PMI just put together a prototype of a RestEasy transaction manager that hooks into the JBoss transaction manager. My goal was to allow multiple web service calls to be wrapped within a single JTA transaction. I wanted to make use of the built in JBoss transaction manager so that Hibernate and my backing EJBs would still participate in the transaction without custom code/configuration. Here's the class:
{code:java}
@Path("/tx")
public class TransactionResource {
public static Map<String, Transaction> txCache = Collections.synchronizedMap(new HashMap<String, Transaction>());
@GET@Path("/start")
@Produces("text/plain")
public String startTX() {
try {
TransactionManager tmgr = (TransactionManager)new InitialContext().lookup("java:/TransactionManager");
tmgr.begin();Transaction tx = tmgr.suspend();
String uuid = UUID.randomUUID().toString();
txCache.put(uuid, tx);
return uuid;
} catch (Throwable t) {
return ResponseXMLWriter.toXML(t, "file/update");
}
}@GET
@Path("/commit/{id}")
@Produces("text/plain")
public String commitTX(@PathParam("id") String txid) {
try {
Transaction tx = txCache.get(txid);
if (tx == null)
throw new ValidationException("Transaction not found for id " + txid);
TransactionManager tmgr = (TransactionManager)new InitialContext().lookup("java:/TransactionManager");
tmgr.resume(tx);
tmgr.commit();
}
return "COMMIT SUCCEEDED";
} catch (Throwable t) {
return ResponseXMLWriter.toXML(t, "file/update");
} finally {
txCache.remove(txid);
}@GET
}
@Path("/rollback/{id}")
@Produces("text/plain")
public String rollbackTX(@PathParam("id") String txid) {
try {
Transaction tx = txCache.get(txid);
if (tx == null)
throw new ValidationException("Transaction not found for id " + txid);
TransactionManager tmgr = (TransactionManager)new InitialContext().lookup("java:/TransactionManager");
tmgr.resume(tx);
tmgr.rollback();
return "ROLLBACK SUCCEEDED";
} catch (Throwable t) {
return ResponseXMLWriter.toXML(t, "file/update");
} finally {
txCache.remove(txid);
}
}
It's a simple prototype but it appears to work. I can call /tx/start to get back a transaction ID. I can then pass that ID to any other RestEasy call which will look up the Transaction from the cache and call TransactionManager.resume(tx) before calling my EJBs. Here's an example:
{code:java}
@GET
@Path("/{id}/update")
@Produces("text/plain")
public String updateStatus(@PathParam("id") String id, @QueryParam("status") String status, @QueryParam("tx") String txId) {
try {
Transaction tx = TransactionResource.txCache.get(txId);
if (tx == null)
throw new ValidationException("Transaction not found for id " + txId);
InitialContext ctx = new InitialContext();
TransactionManager tmgr = (TransactionManager)ctx.lookup("java:/TransactionManager");
tmgr.resume(tx);
FileLookupBean bean = (FileLookupBean)(ctx.lookup("FileLookupBean/no-interface"));
bean.updateStatus(Long.parseLong(id), status);
tmgr.suspend();
return "Updated file " + id + "with status " + status;
} catch (Throwable t) {
return ResponseXMLWriter.toXML(t, "file/update");
}}
{code}
The method FileLookupBean.updateStatus() simply updates one of my JPA classes. The updates are committed or rolled back correctly by calling /tx/commit/{txid} or /tx/rollback/{txid}
While this all seems to work, I'm no expert regarding the inner workings of JTA or JBoss's implementation. I was wondering if anyone could see anything clearly broken with this prototype? I'm aware of the architectural concerns around long running transactions and obviously the txCache will need code to handle orphaned Transactions. However, is there anything technically flawed with what I'm doing here?
Your input is appreciated!