/* * Copyright (c) 2002-2009 "Neo Technology," * Network Engine for Objects in Lund AB [http://neotechnology.com] * * This file is part of Neo4j. * * Neo4j is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.neo4j.kernel.impl.transaction; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import javax.transaction.NotSupportedException; import javax.transaction.Status; import javax.transaction.SystemException; import javax.transaction.Transaction; import javax.transaction.TransactionManager; import javax.transaction.xa.XAResource; import javax.transaction.xa.Xid; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.neo4j.kernel.impl.AbstractNeo4jTestCase; import org.neo4j.kernel.impl.transaction.xaframework.XaConnection; import org.neo4j.kernel.impl.transaction.xaframework.XaDataSource; public class TestJtaCompliance extends AbstractNeo4jTestCase { // the TransactionManager to use when testing for JTA compliance private TransactionManager tm; private XaDataSourceManager xaDsMgr; @Before public void setUpFramework() { getTransaction().finish(); TxModule txModule = getEmbeddedGraphDb().getConfig().getTxModule(); tm = txModule.getTxManager(); xaDsMgr = txModule.getXaDataSourceManager(); java.util.Map<String,FakeXAResource> map1 = new java.util.HashMap<String,FakeXAResource>(); map1.put( "xa_resource", new FakeXAResource( "XAResource1" ) ); java.util.Map<String,FakeXAResource> map2 = new java.util.HashMap<String,FakeXAResource>(); map2.put( "xa_resource", new FakeXAResource( "XAResource2" ) ); try { xaDsMgr.registerDataSource( "fakeRes1", new DummyXaDataSource( map1 ), "0xDDDDDE".getBytes() ); xaDsMgr.registerDataSource( "fakeRes2", new DummyXaDataSource( map2 ), "0xDDDDDF".getBytes() ); } catch ( Exception e ) { e.printStackTrace(); } try { // make sure were not in transaction tm.commit(); } catch ( Exception e ) { } Transaction tx = null; try { tx = tm.getTransaction(); } catch ( Exception e ) { throw new RuntimeException( "Unknown state of TM" ); } if ( tx != null ) { throw new RuntimeException( "We're still in transaction" ); } } @After public void tearDownFramework() { xaDsMgr.unregisterDataSource( "fakeRes1" ); xaDsMgr.unregisterDataSource( "fakeRes2" ); try { if ( tm.getTransaction() == null ) { try { tm.begin(); } catch ( Exception e ) { } } } catch ( SystemException e ) { e.printStackTrace(); } } /** * o Tests that tm.begin() starts a global transaction and associates the * calling thread with that transaction. o Tests that after commit is * invoked transaction is completed and a repeating call to commit/rollback * results in an exception. * * TODO: check if commit is restricted to the thread that started the * transaction, if not, do some testing. */ @Test public void testBeginCommit() throws Exception { tm.begin(); assertTrue( tm.getTransaction() != null ); tm.commit(); // drop current transaction assertEquals( Status.STATUS_NO_TRANSACTION, tm.getStatus() ); try { tm.rollback(); fail( "rollback() should throw an exception -> " + "STATUS_NO_TRANSACTION" ); } catch ( IllegalStateException e ) { // good } try { tm.commit(); fail( "commit() should throw an exception -> " + "STATUS_NO_TRANSACTION" ); } catch ( IllegalStateException e ) { // good } } /** * o Tests that after rollback is invoked the transaction is completed and a * repeating call to rollback/commit results in an exception. * * TODO: check if rollback is restricted to the thread that started the * transaction, if not, do some testing. */ @Test public void testBeginRollback() throws Exception { tm.begin(); assertTrue( tm.getTransaction() != null ); tm.rollback(); // drop current transaction assertTrue( tm.getStatus() == Status.STATUS_NO_TRANSACTION ); try { tm.commit(); fail( "commit() should throw an exception -> " + "STATUS_NO_TRANSACTION" ); } catch ( IllegalStateException e ) { // good } try { tm.rollback(); fail( "rollback() should throw an exception -> " + "STATUS_NO_TRANSACTION" ); } catch ( IllegalStateException e ) { // good } } /** * o Tests that suspend temporarily suspends the transaction associated with * the calling thread. o Tests that resume reinstate the transaction with * the calling thread. o Tests that an invalid transaction passed to resume * won't be associated with the calling thread. o Tests that XAResource.end * is invoked with TMSUSPEND when transaction is suspended. o Tests that * XAResource.start is invoked with TMRESUME when transaction is resumed. * * TODO: o Test that resume throws an exception if the transaction is * already associated with another thread. o Test if a suspended thread may * be resumed by another thread. */ @Test public void testSuspendResume() throws Exception { tm.begin(); Transaction tx = tm.getTransaction(); FakeXAResource res = new FakeXAResource( "XAResource1" ); tx.enlistResource( res ); // suspend assertTrue( tm.suspend() == tx ); tx.delistResource( res, XAResource.TMSUSPEND ); MethodCall calls[] = res.getAndRemoveMethodCalls(); assertEquals( 2, calls.length ); assertEquals( "start", calls[0].getMethodName() ); Object args[] = calls[0].getArgs(); assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); assertEquals( "end", calls[1].getMethodName() ); args = calls[1].getArgs(); assertEquals( XAResource.TMSUSPEND, ((Integer) args[1]).intValue() ); // resume tm.resume( tx ); tx.enlistResource( res ); calls = res.getAndRemoveMethodCalls(); assertEquals( 1, calls.length ); assertEquals( "start", calls[0].getMethodName() ); args = calls[0].getArgs(); assertEquals( XAResource.TMRESUME, ((Integer) args[1]).intValue() ); assertTrue( tm.getTransaction() == tx ); tx.delistResource( res, XAResource.TMSUCCESS ); tm.commit(); tm.resume( tx ); assertTrue( tm.getStatus() == Status.STATUS_NO_TRANSACTION ); assertTrue( tm.getTransaction() == null ); // tm.resume( my fake implementation of transaction ); // assertTrue( tm.getStatus() == Status.STATUS_NO_TRANSACTION ); // assertTrue( tm.getTransaction() == null ); } /** * o Tests two-phase commits with two different fake XAResource * implementations so a branch is created within the same global * transaction. */ @Test public void test2PhaseCommits1() throws Exception { tm.begin(); FakeXAResource res1 = new FakeXAResource( "XAResource1" ); FakeXAResource res2 = new FakeXAResource( "XAResource2" ); // enlist two different resources and verify that the start method // is invoked with correct flags // res1 tm.getTransaction().enlistResource( res1 ); MethodCall calls1[] = res1.getAndRemoveMethodCalls(); assertEquals( 1, calls1.length ); assertEquals( "start", calls1[0].getMethodName() ); // res2 tm.getTransaction().enlistResource( res2 ); MethodCall calls2[] = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); assertEquals( "start", calls2[0].getMethodName() ); // verify Xid Object args[] = calls1[0].getArgs(); Xid xid1 = (Xid) args[0]; assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); args = calls2[0].getArgs(); Xid xid2 = (Xid) args[0]; assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); // should have same global transaction id byte globalTxId1[] = xid1.getGlobalTransactionId(); byte globalTxId2[] = xid2.getGlobalTransactionId(); assertTrue( globalTxId1.length == globalTxId2.length ); for ( int i = 0; i < globalTxId1.length; i++ ) { assertEquals( globalTxId1[i], globalTxId2[i] ); } byte branch1[] = xid1.getBranchQualifier(); byte branch2[] = xid2.getBranchQualifier(); // make sure a different branch was created if ( branch1.length == branch2.length ) { boolean same = true; for ( int i = 0; i < branch1.length; i++ ) { if ( branch1[i] != branch2[i] ) { same = false; break; } } assertTrue( !same ); } // verify delist of resource tm.getTransaction().delistResource( res2, XAResource.TMSUCCESS ); calls2 = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); tm.getTransaction().delistResource( res1, XAResource.TMSUCCESS ); calls1 = res1.getAndRemoveMethodCalls(); // res1 assertEquals( 1, calls1.length ); assertEquals( "end", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // res2 assertEquals( 1, calls2.length ); assertEquals( "end", calls2[0].getMethodName() ); args = calls2[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // verify proper prepare/commit tm.commit(); calls1 = res1.getAndRemoveMethodCalls(); calls2 = res2.getAndRemoveMethodCalls(); // res1 assertEquals( 2, calls1.length ); assertEquals( "prepare", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( "commit", calls1[1].getMethodName() ); args = calls1[1].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( false, ((Boolean) args[1]).booleanValue() ); // res2 assertEquals( 2, calls2.length ); assertEquals( "prepare", calls2[0].getMethodName() ); args = calls2[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); assertEquals( "commit", calls2[1].getMethodName() ); args = calls2[1].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); assertEquals( false, ((Boolean) args[1]).booleanValue() ); } /** * o Tests that two enlistments of same resource (according to the * isSameRM() method) only receive one set of prepare/commit calls. */ @Test public void test2PhaseCommits2() throws Exception { tm.begin(); FakeXAResource res1 = new FakeXAResource( "XAResource1" ); FakeXAResource res2 = new FakeXAResource( "XAResource1" ); // enlist two (same) resources and verify that the start method // is invoked with correct flags // res1 tm.getTransaction().enlistResource( res1 ); MethodCall calls1[] = res1.getAndRemoveMethodCalls(); assertEquals( 1, calls1.length ); assertEquals( "start", calls1[0].getMethodName() ); // res2 tm.getTransaction().enlistResource( res2 ); MethodCall calls2[] = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); assertEquals( "start", calls2[0].getMethodName() ); // make sure we get a two-phase commit FakeXAResource res3 = new FakeXAResource( "XAResource2" ); tm.getTransaction().enlistResource( res3 ); // verify Xid and flags Object args[] = calls1[0].getArgs(); Xid xid1 = (Xid) args[0]; assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); args = calls2[0].getArgs(); Xid xid2 = (Xid) args[0]; assertEquals( XAResource.TMJOIN, ((Integer) args[1]).intValue() ); assertTrue( xid1.equals( xid2 ) ); assertTrue( xid2.equals( xid1 ) ); // verify delist of resource tm.getTransaction().delistResource( res3, XAResource.TMSUCCESS ); tm.getTransaction().delistResource( res2, XAResource.TMSUCCESS ); calls2 = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); tm.getTransaction().delistResource( res1, XAResource.TMSUCCESS ); calls1 = res1.getAndRemoveMethodCalls(); // res1 assertEquals( 1, calls1.length ); assertEquals( "end", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // res2 assertEquals( 1, calls2.length ); assertEquals( "end", calls2[0].getMethodName() ); args = calls2[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // verify proper prepare/commit tm.commit(); calls1 = res1.getAndRemoveMethodCalls(); calls2 = res2.getAndRemoveMethodCalls(); // res1 assertEquals( 2, calls1.length ); assertEquals( "prepare", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( "commit", calls1[1].getMethodName() ); args = calls1[1].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( false, ((Boolean) args[1]).booleanValue() ); // res2 assertEquals( 0, calls2.length ); } /** * o Tests that multiple enlistments receive rollback calls properly. */ @Test public void testRollback1() throws Exception { tm.begin(); FakeXAResource res1 = new FakeXAResource( "XAResource1" ); FakeXAResource res2 = new FakeXAResource( "XAResource2" ); // enlist two different resources and verify that the start method // is invoked with correct flags // res1 tm.getTransaction().enlistResource( res1 ); MethodCall calls1[] = res1.getAndRemoveMethodCalls(); assertEquals( 1, calls1.length ); assertEquals( "start", calls1[0].getMethodName() ); // res2 tm.getTransaction().enlistResource( res2 ); MethodCall calls2[] = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); assertEquals( "start", calls2[0].getMethodName() ); // verify Xid Object args[] = calls1[0].getArgs(); Xid xid1 = (Xid) args[0]; assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); args = calls2[0].getArgs(); Xid xid2 = (Xid) args[0]; assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); // should have same global transaction id byte globalTxId1[] = xid1.getGlobalTransactionId(); byte globalTxId2[] = xid2.getGlobalTransactionId(); assertTrue( globalTxId1.length == globalTxId2.length ); for ( int i = 0; i < globalTxId1.length; i++ ) { assertEquals( globalTxId1[i], globalTxId2[i] ); } byte branch1[] = xid1.getBranchQualifier(); byte branch2[] = xid2.getBranchQualifier(); // make sure a different branch was created if ( branch1.length == branch2.length ) { boolean same = true; for ( int i = 0; i < branch1.length; i++ ) { if ( branch1[i] != branch2[i] ) { same = false; break; } } assertTrue( !same ); } // verify delist of resource tm.getTransaction().delistResource( res2, XAResource.TMSUCCESS ); calls2 = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); tm.getTransaction().delistResource( res1, XAResource.TMSUCCESS ); calls1 = res1.getAndRemoveMethodCalls(); // res1 assertEquals( 1, calls1.length ); assertEquals( "end", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // res2 assertEquals( 1, calls2.length ); assertEquals( "end", calls2[0].getMethodName() ); args = calls2[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // verify proper rollback tm.rollback(); calls1 = res1.getAndRemoveMethodCalls(); calls2 = res2.getAndRemoveMethodCalls(); // res1 assertEquals( 1, calls1.length ); assertEquals( "rollback", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); // res2 assertEquals( 1, calls2.length ); assertEquals( "rollback", calls2[0].getMethodName() ); args = calls2[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); } /* * o Tests that multiple enlistments of same (according to isSameRM() * method) only receive one set of rollback calls. */ @Test public void testRollback2() throws Exception { tm.begin(); FakeXAResource res1 = new FakeXAResource( "XAResource1" ); FakeXAResource res2 = new FakeXAResource( "XAResource1" ); // enlist two (same) resources and verify that the start method // is invoked with correct flags // res1 tm.getTransaction().enlistResource( res1 ); MethodCall calls1[] = res1.getAndRemoveMethodCalls(); assertEquals( 1, calls1.length ); assertEquals( "start", calls1[0].getMethodName() ); // res2 tm.getTransaction().enlistResource( res2 ); MethodCall calls2[] = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); assertEquals( "start", calls2[0].getMethodName() ); // verify Xid and flags Object args[] = calls1[0].getArgs(); Xid xid1 = (Xid) args[0]; assertEquals( XAResource.TMNOFLAGS, ((Integer) args[1]).intValue() ); args = calls2[0].getArgs(); Xid xid2 = (Xid) args[0]; assertEquals( XAResource.TMJOIN, ((Integer) args[1]).intValue() ); assertTrue( xid1.equals( xid2 ) ); assertTrue( xid2.equals( xid1 ) ); // verify delist of resource tm.getTransaction().delistResource( res2, XAResource.TMSUCCESS ); calls2 = res2.getAndRemoveMethodCalls(); assertEquals( 1, calls2.length ); tm.getTransaction().delistResource( res1, XAResource.TMSUCCESS ); calls1 = res1.getAndRemoveMethodCalls(); // res1 assertEquals( 1, calls1.length ); assertEquals( "end", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // res2 assertEquals( 1, calls2.length ); assertEquals( "end", calls2[0].getMethodName() ); args = calls2[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid2 ) ); assertEquals( XAResource.TMSUCCESS, ((Integer) args[1]).intValue() ); // verify proper prepare/commit tm.rollback(); calls1 = res1.getAndRemoveMethodCalls(); calls2 = res2.getAndRemoveMethodCalls(); // res1 assertEquals( 1, calls1.length ); assertEquals( "rollback", calls1[0].getMethodName() ); args = calls1[0].getArgs(); assertTrue( ((Xid) args[0]).equals( xid1 ) ); // res2 assertEquals( 0, calls2.length ); } /** * o Tests if nested transactions are supported * * TODO: if supported, do some testing :) */ @Test public void testNestedTransactions() throws Exception { assertTrue( tm.getTransaction() == null ); tm.begin(); Transaction txParent = tm.getTransaction(); assertTrue( txParent != null ); try { tm.begin(); // ok supported // some tests that might be valid for true nested support // Transaction txChild = tm.getTransaction(); // assertTrue( txChild != txParent ); // tm.commit(); // assertTrue( txParent == tm.getTransaction() ); } catch ( NotSupportedException e ) { // well no nested transactions } tm.commit(); assertTrue( tm.getStatus() == Status.STATUS_NO_TRANSACTION ); } private class TxHook implements javax.transaction.Synchronization { boolean gotBefore = false; boolean gotAfter = false; int statusBefore = -1; int statusAfter = -1; Transaction txBefore = null; Transaction txAfter = null; public void beforeCompletion() { try { statusBefore = tm.getStatus(); txBefore = tm.getTransaction(); gotBefore = true; } catch ( Exception e ) { throw new RuntimeException( "" + e ); } } public void afterCompletion( int status ) { try { statusAfter = status; txAfter = tm.getTransaction(); assertTrue( status == tm.getStatus() ); gotAfter = true; } catch ( Exception e ) { throw new RuntimeException( "" + e ); } } } /** * o Tests that beforeCompletion and afterCompletion are invoked. o Tests * that the call is made in the same transaction context. o Tests status in * before and after methods depending on commit/rollback. * * NOTE: Not sure if the check of Status is correct according to * specification. */ @Test public void testTransactionHook() throws Exception { // test for commit tm.begin(); Transaction tx = tm.getTransaction(); TxHook txHook = new TxHook(); tx.registerSynchronization( txHook ); assertEquals( false, txHook.gotBefore ); assertEquals( false, txHook.gotAfter ); tm.commit(); assertEquals( true, txHook.gotBefore ); assertEquals( true, txHook.gotAfter ); assertTrue( tx == txHook.txBefore ); assertTrue( tx == txHook.txAfter ); assertEquals( Status.STATUS_ACTIVE, txHook.statusBefore ); assertEquals( Status.STATUS_COMMITTED, txHook.statusAfter ); // test for rollback tm.begin(); tx = tm.getTransaction(); txHook = new TxHook(); tx.registerSynchronization( txHook ); assertEquals( false, txHook.gotBefore ); assertEquals( false, txHook.gotAfter ); tm.rollback(); assertEquals( true, txHook.gotBefore ); assertEquals( true, txHook.gotAfter ); assertTrue( tx == txHook.txBefore ); assertTrue( tx == txHook.txAfter ); assertEquals( Status.STATUS_MARKED_ROLLBACK, txHook.statusBefore ); assertEquals( Status.STATUS_ROLLEDBACK, txHook.statusAfter ); } /** * Tests that the correct status is returned from TM. * * TODO: Implement a FakeXAResource to check: STATUS_COMMITTING * STATUS_PREPARED STATUS_PREPEARING STATUS_ROLLING_BACK */ @Test public void testStatus() throws Exception { assertTrue( tm.getStatus() == Status.STATUS_NO_TRANSACTION ); tm.begin(); assertTrue( tm.getStatus() == Status.STATUS_ACTIVE ); tm.getTransaction().setRollbackOnly(); assertTrue( tm.getStatus() == Status.STATUS_MARKED_ROLLBACK ); tm.rollback(); assertTrue( tm.getStatus() == Status.STATUS_NO_TRANSACTION ); } /** * Is one-phase commit always performed when only one (or many isSameRM) * resource(s) are present in the transaction? * * If so it could be tested... */ // public void test1PhaseCommit() // { // // } public static class DummyXaDataSource extends XaDataSource { private XAResource xaResource = null; public DummyXaDataSource( java.util.Map<?,?> map ) throws InstantiationException { super( map ); this.xaResource = (XAResource) map.get( "xa_resource" ); } public void close() { } public XaConnection getXaConnection() { return new DummyXaConnection( xaResource ); } @Override public byte[] getBranchId() { // TODO Auto-generated method stub return null; } @Override public void setBranchId( byte[] branchId ) { // TODO Auto-generated method stub } } private static class DummyXaConnection implements XaConnection { private XAResource xaResource = null; public DummyXaConnection( XAResource xaResource ) { this.xaResource = xaResource; } public XAResource getXaResource() { return xaResource; } public void destroy() { } } }