/*
* JBoss, Home of Professional Open Source
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2008,
* @author JBoss Inc.
*/
package org.jboss.test.jbossts.ASCrashRecovery01;
import org.jboss.test.jbossts.recovery.ASFailureSpec;
import org.jboss.test.jbossts.recovery.RecoveredXid;
import org.jboss.test.jbossts.taskdefs.JUnitClientTest;
import org.jboss.test.jbossts.taskdefs.TransactionLog;
import org.jboss.test.jbossts.taskdefs.Utils;
import org.jboss.test.jbossts.crash.CrashHelper;
import org.jboss.test.jbossts.crash.JPACrashHelperRem;
import org.jboss.test.jbossts.crash.CrashRem;
import org.jboss.test.jbossts.crash.TestEntity;
import org.jboss.test.jbossts.crash.TestEntityHelper;
import org.jboss.test.jbossts.crash.TestEntityHelperRem;
import org.jboss.remoting.CannotConnectException;
import org.apache.tools.ant.BuildException;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.ejb.EJBTransactionRolledbackException;
import javax.naming.NamingException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
/**
* Crash recovery tests with EJB3 entity.
*
* @author <a href="istudens@redhat.com">Ivo Studensky</a>
* @version $Revision: 1.1 $
*/
public class TestWithJPA extends JUnitClientTest
{
// the longest time to wait in millis before declaring a test a failed (overridable)
private static final int MAX_TEST_TIME = 5 * 60 * 1000; // 5 minutes - allows two intervals of recovery which is 2 minutes by default
private boolean isCMT = false;
private boolean clientTx = false;
private boolean expectFailure = false;
private boolean reverseOrder = false;
private boolean rollbackExpected = false;
private boolean wipeOutTxsInDoubt = false;
private boolean wipeOutTxsInDoubtBeforeTest = false;
private boolean wipeOutTxsInDoubtAfterTest = false;
private int maxTestTime = MAX_TEST_TIME;
private String storeDir = null;
private String storeImple = "com.arjuna.ats.internal.arjuna.objectstore.HashedActionStore";
private String storeType = "StateManager/BasicAction/TwoPhaseCoordinator/AtomicAction";
private TransactionLog store;
private int existingUids;
private Set<RecoveredXid> existingXidsInDoubt;
private String recoveryDatasource = null;
private String serverName = "default";
private TestEntity initEntity;
public void testAction()
{
if (config == null || params == null)
throw new UnsupportedOperationException("The test has not been initiated yet. Call the init() method first.");
StringBuilder sb = new StringBuilder();
ASFailureSpec[] fspecs = null;
for (Map.Entry<String, String> me : params.entrySet())
{
String key = me.getKey().trim();
String val = me.getValue().trim();
if ("name".equals(key))
setName(val);
else if ("cmt".equals(key))
isCMT = val.equalsIgnoreCase("true");
else if ("serverName".equals(key))
serverName = val;
else if ("storeType".equals(key))
storeType = val;
else if ("storeDir".equals(key))
storeDir = val;
else if ("clientTx".equals(key))
clientTx = val.equalsIgnoreCase("true");
else if ("storeImple".equals(key))
storeImple = val;
else if ("testTime".equals(key))
maxTestTime = Utils.parseInt(val, "parameter testTime should represent a number of miliseconds: ");
else if ("specs".equals(key))
fspecs = parseSpecs(val, sb);
else if ("wait".equals(key))
suspendFor(Integer.parseInt(val));
else if ("reverseOrder".equals(key))
reverseOrder = val.equalsIgnoreCase("true");
else if ("rollbackExpected".equals(key))
rollbackExpected = val.equalsIgnoreCase("true");
else if ("wipeOutTxsInDoubt".equals(key))
wipeOutTxsInDoubt = val.equalsIgnoreCase("true");
else if ("wipeOutTxsInDoubtBeforeTest".equals(key))
wipeOutTxsInDoubtBeforeTest = val.equalsIgnoreCase("true");
else if ("wipeOutTxsInDoubtAfterTest".equals(key))
wipeOutTxsInDoubtAfterTest = val.equalsIgnoreCase("true");
else if ("recoveryDatasource".equals(key))
recoveryDatasource = val;
}
sb.insert(0, ":\n").insert(0, getName()).insert(0, "Executing test ");
System.out.println(sb);
try
{
String serverPath = config.getServerPath(serverName);
// get a handle to the transaction logs
if (storeDir == null)
storeDir = serverPath + "data/tx-object-store";
else
storeDir = serverPath + storeDir;
System.out.println("transaction log will be stored in " + storeDir + "(file=" + storeImple+")");
store = new TransactionLog(storeDir, storeImple);
if (expectFailure)
{
// this test may halt the VM so make sure the transaction log is empty
// before starting the test - then the pass/fail check is simply to
// test whether or not the log is empty (see recoverUids() below).
try
{
store.clearXids(storeType);
}
catch (Exception ignore)
{
}
}
existingUids = getPendingUids();
if (wipeOutTxsInDoubtBeforeTest)
wipeOutTxsInDoubt();
existingXidsInDoubt = lookupCrashHelper().checkXidsInDoubt(recoveryDatasource);
if (existingXidsInDoubt.size() > 0)
print(existingXidsInDoubt.size() + " txs in doubt in database before test run");
// name of this test will be the primary key for test record in DB
String testEntityPK = getName();
// initialize database
initDatabase(testEntityPK);
// run the crash test
boolean result = crashTest(fspecs, testEntityPK, reverseOrder);
print("crashTest result: " + result);
// checking the state of DB after recovering
boolean dbChanged = true;
if (result)
{
dbChanged = checkDatabase(testEntityPK);
print("checkDatabase result: " + dbChanged);
}
Set<RecoveredXid> xidsInDoubtAfterTest = lookupCrashHelper().checkXidsInDoubt(recoveryDatasource);
if (wipeOutTxsInDoubt || wipeOutTxsInDoubtAfterTest)
wipeOutTxsInDoubt(existingXidsInDoubt, xidsInDoubtAfterTest);
assertTrue("Crash recovery failed.", result);
assertTrue("Incorrect data in database after crash recovery.", dbChanged);
assertEquals("There are still unrecovered txs in database after crash recovery.", existingXidsInDoubt.size(), xidsInDoubtAfterTest.size());
}
catch (Exception e)
{
if (isDebug)
e.printStackTrace();
throw new BuildException(e);
}
}
private void initDatabase(String entityPK) throws Exception
{
TestEntityHelperRem hlp = (TestEntityHelperRem) config.getNamingContext(serverName).lookup(TestEntityHelper.REMOTE_JNDI_NAME);
initEntity = hlp.initTestEntity(entityPK);
print("TestWithJPA#initDatabase(): initEntity = " + initEntity);
}
private boolean checkDatabase(String entityPK)
{
try
{
TestEntityHelperRem hlp = (TestEntityHelperRem) config.getNamingContext(serverName).lookup(TestEntityHelper.REMOTE_JNDI_NAME);
TestEntity recoveredEntity = hlp.getTestEntity(entityPK);
if (recoveredEntity != null)
{
print("TestWithJPA#checkDatabase(): initEntity = " + initEntity);
print("TestWithJPA#checkDatabase(): recoveredEntity = " + recoveredEntity);
return (rollbackExpected) ? recoveredEntity.getA() == initEntity.getA()
: recoveredEntity.getA() != initEntity.getA();
}
}
catch (Exception e)
{
e.printStackTrace();
}
return false;
}
/**
* Wipes out all the txs in doubt from database.
*
* @return true in success, fail otherwise
*/
private boolean wipeOutTxsInDoubt()
{
// wipes out all txs in doubt from DB
return wipeOutTxsInDoubt(null);
}
/**
* Wipes out only new txs in doubt from database after test run.
*
* @param xidsInDoubtBeforeTest txs in doubt in database before test run
* @param xidsInDoubtBeforeTest txs in doubt in database after test run
* @return true in success, fail otherwise
*/
private boolean wipeOutTxsInDoubt(Set<RecoveredXid> xidsInDoubtBeforeTest, Set<RecoveredXid> xidsInDoubtAfterTest)
{
Set<RecoveredXid> xidsToRecover = new HashSet<RecoveredXid>(xidsInDoubtAfterTest);
xidsToRecover.removeAll(xidsInDoubtBeforeTest);
if (xidsToRecover.isEmpty())
return true;
return wipeOutTxsInDoubt(xidsToRecover);
}
/**
* Wipes out txs in doubt from database according to a xidsToRecover list.
*
* @param xidsToRecover list of xids to recover
* @return true in success, fail otherwise
*/
private boolean wipeOutTxsInDoubt(Set<RecoveredXid> xidsToRecover)
{
print("wiping out txs in doubt");
try
{
lookupCrashHelper().wipeOutTxsInDoubt(recoveryDatasource, xidsToRecover);
}
catch (Exception e)
{
e.printStackTrace();
}
return false;
}
private ASFailureSpec[] parseSpecs(String specArg, StringBuilder sb)
{
ASFailureSpec[] fspecs = config.parseSpecs(specArg);
for (ASFailureSpec spec : fspecs)
{
String name = (spec == null ? "INVALID" : spec.getName());
if (spec != null && spec.willTerminateVM())
expectFailure = true;
sb.append("\t").append(name).append('\n');
}
return fspecs;
}
// count how many pending transaction branches there are in the transaction log
private int getPendingUids()
{
try
{
return store.getIds(storeType).size();
}
catch (Exception e)
{
e.printStackTrace();
return -1;
}
}
private CrashRem lookupCrashBean(String name) throws Exception
{
return (CrashRem) config.getNamingContext(serverName).lookup(name);
}
private JPACrashHelperRem lookupCrashHelper() throws Exception
{
return (JPACrashHelperRem) config.getNamingContext(serverName).lookup(CrashHelper.REMOTE_JNDI_NAME);
}
private UserTransaction startTx() throws NamingException, SystemException, NotSupportedException
{
UserTransaction tx = (UserTransaction) config.getNamingContext(serverName).lookup("UserTransaction");
tx.begin();
return tx;
}
private boolean crashTest(ASFailureSpec[] sa, String testEntityPK, boolean reverseOrder) throws Exception
{
UserTransaction tx = null;
try
{
CrashRem cr = lookupCrashBean(isCMT ? CrashRem.CMT_JNDI_NAME : CrashRem.BMT_JNDI_NAME);
if (clientTx)
tx = startTx();
String res = cr.testXA(testEntityPK, reverseOrder, sa);
return "Passed".equalsIgnoreCase(res);
}
catch (CannotConnectException e)
{
if (expectFailure)
{
print("Failure was expected: " + e.getMessage());
return recoverUids();
}
else
{
System.err.println("XACrashTest:crashTest: Caught[1] " + e);
e.printStackTrace();
}
}
catch (EJBTransactionRolledbackException re)
{
// try to recover, this failure was expected maybe?!
print("Failure was expected (maybe): " + re.getMessage());
return recoverUids();
}
catch (RuntimeException re)
{
if (re.getCause() instanceof HeuristicMixedException)
{
// try to recover, this failure was expected maybe?!
print("Failure was expected (maybe): " + re.getMessage());
return recoverUids();
}
else
{
System.err.println("XACrashTest:crashTest: Caught[2] " + re);
re.printStackTrace();
}
}
catch (Throwable t)
{
t.printStackTrace();
System.err.println("XACrashTest:crashTest: Caught[3] " + t);
}
finally {
if (clientTx)
try
{
tx.commit();
}
catch (Throwable e)
{
System.out.println("User tx commit failure: " + e.getMessage());
}
}
return false;
}
/**
* Wait for any pending transactions to recover by restarting the AS.
* @return true if all pending branches have been recovered
* @throws IOException if the server cannot be started
*/
private boolean recoverUids() throws IOException
{
int retryPeriod = 60000; // 1 minute
int maxWait = maxTestTime;
Set<RecoveredXid> xidsInDoubtAfterTest;
int pendingUids;
int pendingXidsInDoubt;
int totalExistingXidsInDoubt = existingXidsInDoubt.size();
// wait for the server to start up the first time through, we will need it for later database checking
suspendFor(2000); // a little waiting is needed sometimes in order to be able to start server again, 2 secs
config.startServer(serverName);
do
{
pendingUids = getPendingUids();
try
{
xidsInDoubtAfterTest = lookupCrashHelper().checkXidsInDoubt(recoveryDatasource);
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
pendingXidsInDoubt = xidsInDoubtAfterTest.size();
if (pendingUids == -1)
{
print("recoverUids failed, object store error, pendingUids == -1");
return false; // object store error
}
if (pendingUids <= existingUids && pendingXidsInDoubt <= totalExistingXidsInDoubt)
{
print("recoverUids success");
return true; // all uids in AS and txs in doubt in DB recovered
}
pendingUids -= existingUids;
pendingXidsInDoubt -= totalExistingXidsInDoubt;
print("waiting for " + pendingUids + " branches");
print("waiting for " + pendingXidsInDoubt + " txs in doubt");
suspendFor(retryPeriod);
maxWait -= retryPeriod;
} while (maxWait > 0);
print("recoverUids failed, took too long to recover");
// the test failed to recover some uids - clear them out ready for the next test
if (pendingUids > 0)
{
try
{
store.clearXids(storeType);
}
catch (Exception e)
{
e.printStackTrace();
}
}
// the test failed to recover some xids in the database - clear them out ready for the next test
if (pendingXidsInDoubt > 0)
{
print(pendingXidsInDoubt + " new txs in doubt in database after the test");
if (wipeOutTxsInDoubt || wipeOutTxsInDoubtAfterTest)
wipeOutTxsInDoubt(existingXidsInDoubt, xidsInDoubtAfterTest);
}
return false;
}
}