/*
* 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.ASCrashRecovery02;
import org.jboss.test.jbossts.recovery.ASFailureSpec;
import org.jboss.test.jbossts.recovery.CrashHelperRem;
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.jms.JMSCrashHelper;
import org.jboss.test.jbossts.jms.JMSCrashRem;
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.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.transaction.HeuristicMixedException;
import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.UserTransaction;
/**
* Crash recovery tests with JMS.
*
* @author <a href="istudens@redhat.com">Ivo Studensky</a>
* @version $Revision: 1.1 $
*/
public class TestWithJMS 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 = "HashedActionStore";
private String storeType = "StateManager/BasicAction/TwoPhaseCoordinator/AtomicAction";
private TransactionLog store;
private int existingUids;
private Set<RecoveredXid> existingXidsInDoubt;
private String serverName = "default";
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 ("debug".equals(key))
isDebug = 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");
}
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();
if (existingXidsInDoubt.size() > 0)
print(existingXidsInDoubt.size() + " txs in doubt before test run");
String message = getName();
// run the crash test
boolean result = crashTest(message, fspecs, reverseOrder);
print("crashTest result: " + result);
// checking the state of JMS after recovering
boolean jmsResult = true;
if (result)
{
jmsResult = checkJMS(message);
print("checkJMS result: " + jmsResult);
}
Set<RecoveredXid> xidsInDoubtAfterTest = lookupCrashHelper().checkXidsInDoubt();
if (wipeOutTxsInDoubt || wipeOutTxsInDoubtAfterTest)
wipeOutTxsInDoubt(existingXidsInDoubt, xidsInDoubtAfterTest);
assertTrue("Crash recovery failed.", result);
assertTrue("Incorrect JMS state after crash recovery.", jmsResult);
assertEquals("There are still unrecovered txs in JMS after crash recovery.", existingXidsInDoubt.size(), xidsInDoubtAfterTest.size());
}
catch (Exception e)
{
if (isDebug)
e.printStackTrace();
throw new BuildException(e);
}
}
private boolean checkJMS(String sentMessage)
{
String receivedMessage = null;
try
{
receivedMessage = receiveMessage();
}
catch (Exception e)
{
return false;
}
return (rollbackExpected) ? (receivedMessage == null) : sentMessage.equals(receivedMessage);
}
private String receiveMessage() throws Exception
{
Context context = null;
Connection connection = null;
try
{
context = config.getNamingContext(serverName);
ConnectionFactory cf = (ConnectionFactory) context.lookup("/ConnectionFactory");
Queue queue = (Queue) context.lookup("queue/crashRecoveryQueue");
connection = cf.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageConsumer consumer = session.createConsumer(queue);
connection.start();
print("waiting to receive a message from queue/crashRecoveryQueue...");
TextMessage message = (TextMessage) consumer.receive(5 * 1000);
if (isDebug)
print("received message: " + ((message != null) ? message.getText() : message));
return (message != null) ? message.getText() : null;
}
catch (Exception e)
{
print("Error in receiving a message: " + e);
e.printStackTrace();
throw e;
}
finally
{
if (connection != null)
{
try
{
connection.close();
}
catch (JMSException e)
{
e.printStackTrace();
}
}
}
}
private boolean crashTest(String message, ASFailureSpec[] sa, boolean reverseOrder) throws Exception
{
UserTransaction tx = null;
try
{
JMSCrashRem cr = lookupCrashBean(isCMT ? JMSCrashRem.CMT_JNDI_NAME : JMSCrashRem.BMT_JNDI_NAME);
if (clientTx)
tx = startTx();
String res = cr.testXA(message, 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 checking
suspendFor(2000); // short waiting is needed sometimes in order to be able to start the server again, 2 secs
config.startServer(serverName);
do
{
pendingUids = getPendingUids();
try
{
xidsInDoubtAfterTest = lookupCrashHelper().checkXidsInDoubt();
}
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 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 JMS - clear them out ready for the next test
if (pendingXidsInDoubt > 0)
{
print(pendingXidsInDoubt + " new txs in doubt after the test");
if (wipeOutTxsInDoubt || wipeOutTxsInDoubtAfterTest)
wipeOutTxsInDoubt(existingXidsInDoubt, xidsInDoubtAfterTest);
}
return false;
}
/**
* Wipes out all the txs in doubt.
*
* @return true in success, fail otherwise
*/
private boolean wipeOutTxsInDoubt()
{
// wipes out all txs in doubt
return wipeOutTxsInDoubt(null);
}
/**
* Wipes out only new txs in doubt after test run.
*
* @param xidsInDoubtBeforeTest txs in doubt before test run
* @param xidsInDoubtBeforeTest txs in doubt 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 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(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 JMSCrashRem lookupCrashBean(String name) throws Exception
{
return (JMSCrashRem) config.getNamingContext(serverName).lookup(name);
}
private CrashHelperRem lookupCrashHelper() throws Exception
{
return (CrashHelperRem) config.getNamingContext(serverName).lookup(JMSCrashHelper.REMOTE_JNDI_NAME);
}
private UserTransaction startTx() throws NamingException, SystemException, NotSupportedException
{
UserTransaction tx = (UserTransaction) config.getNamingContext(serverName).lookup("UserTransaction");
tx.begin();
return tx;
}
}