/**
* Redistribution and use of this software and associated documentation
* ("Software"), with or without modification, are permitted provided
* that the following conditions are met:
*
* 1. Redistributions of source code must retain copyright
* statements and notices. Redistributions must also contain a
* copy of this document.
*
* 2. Redistributions in binary form must reproduce the
* above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* 3. The name "Exolab" must not be used to endorse or promote
* products derived from this Software without prior written
* permission of Intalio, Inc. For written permission,
* please contact info@exolab.org.
*
* 4. Products derived from this Software may not be called "Exolab"
* nor may "Exolab" appear in their names without prior written
* permission of Intalio, Inc. Exolab is a registered
* trademark of Intalio, Inc.
*
* 5. Due credit should be given to the Exolab Project
* (http://www.exolab.org/).
*
* THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
* NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* Copyright 1999-2001 (C) Intalio, Inc. All Rights Reserved.
*/
package ctf.jdo.tc0x;
import java.util.Enumeration;
import harness.CastorTestCase;
import harness.TestHarness;
import jdo.JDOCategory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.LockNotGrantedException;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.TransactionAbortedException;
import org.exolab.castor.jdo.TransactionNotInProgressException;
import org.exolab.castor.mapping.AccessMode;
/**
* Tests for deadlock detection. Will report to the console two concurrent
* transactions working on the same objects. The first transaction will
* succeed, the second will fail. All three access modes: Shared (aka
* optimistic locking), Exclusive (aka pessimistic locking) and DbLocked
* (premissitic memory locking with DBMS's locking) are covered by these
* test cases. These tests passed if LockNotGrantedException is thrown
* in the appropriate time for the access mode in action.
*/
public final class TestDeadlock extends CastorTestCase {
/**
* The <a href="http://jakarta.apache.org/commons/logging/">Jakarta
* Commons Logging</a> instance used for all logging.
*/
private static final Log LOG = LogFactory.getLog(TestDeadlock.class);
/**
* The time a transaction sleep and wait for another transaction to
* process
*/
public static final long WAIT = 1000;
/**
* The JDO test suite for this test case
*/
private JDOCategory _category;
/**
* The JDO database used for setUp
*/
private Database _db;
/**
* AccessMode used in the tests
*/
private AccessMode _accessMode;
/**
* The java object to be synchronized on
*/
private Object _lock;
/**
* The JDO database used for first concurrent transactions
*/
private Database _firstDb;
/**
* The thread that the first transaction is running on
*/
private FirstThread _firstThread;
private Exception _firstProblem;
/**
* The JDO database used for second concurrent transactions
*/
private Database _secondDb;
/**
* The thread that the second transaction is running on
*/
private SecondThread _secondThread;
private Exception _secondProblem;
/**
* Constructor
*
* @param category The test suit of these test cases
*/
public TestDeadlock(final TestHarness category) {
super(category, "TC04", "Deadlock detection tests");
_category = (JDOCategory) category;
}
/**
* Get the JDO Databases and create data objects into the database
*/
public void setUp() throws PersistenceException {
_db = _category.getDatabase();
_firstDb = _category.getDatabase();
_secondDb = _category.getDatabase();
Sample object;
Enumeration enumeration;
// Open transaction in order to perform JDO operations
_db.begin();
// Create two objects in the database -- need something to lock
OQLQuery oql = _db.getOQLQuery("SELECT object FROM "
+ Sample.class.getName() + " object WHERE id = $1");
oql.bind(Sample.DEFAULT_ID);
enumeration = oql.execute();
if (enumeration.hasMoreElements()) {
object = (Sample) enumeration.nextElement();
LOG.debug("Retrieved object: " + object);
object.setValue1(Sample.DEFAULT_VALUE_1);
object.setValue2(Sample.DEFAULT_VALUE_2);
} else {
object = new Sample();
LOG.debug("Creating new object: " + object);
_db.create(object);
}
oql.bind(Sample.DEFAULT_ID + 1);
enumeration = oql.execute();
if (enumeration.hasMoreElements()) {
object = (Sample) enumeration.nextElement();
LOG.debug("Retrieved object: " + object);
object.setValue1(Sample.DEFAULT_VALUE_1);
object.setValue2(Sample.DEFAULT_VALUE_2);
} else {
object = new Sample();
object.setId(Sample.DEFAULT_ID + 1);
LOG.debug("Creating new object: " + object);
_db.create(object);
}
_db.commit();
}
/**
* Run tests for each of the three access modes.
* <p>
* Notice that some database have their own deadlock detection mechanisms
* implemented. Depending on the stricktness of the algorithm, when
* these tests are runing in DbLocked mode, some database might throw
* an exception to resolve the deadlock before Castor JDO detects it.
*/
public void runTest() throws PersistenceException, InterruptedException {
LOG.info("Running in access mode shared");
runOnce(Database.SHARED);
LOG.info("Running in access mode exclusive");
runOnce(Database.EXCLUSIVE);
LOG.info("Running in access mode db-locked");
runOnce(Database.DBLOCKED);
}
/**
* Close the JDO databases used in the these test cases.
*/
public void tearDown() throws PersistenceException {
if (_db.isActive()) { _db.rollback(); }
_db.close();
if (_firstDb.isActive()) { _firstDb.rollback(); }
_firstDb.close();
if (_secondDb.isActive()) { _secondDb.rollback(); }
_secondDb.close();
}
/**
* Creates threads to test for deadlock detection behaviors.
*/
public void runOnce(final AccessMode accessMode) throws InterruptedException {
LOG.debug("Note: this test uses a 2 seconds delay between "
+ "threads. CPU and database load might cause the test to not "
+ "perform synchronously, resulting in erroneous results. Make "
+ "sure that execution is not hampered by CPU/datebase load.");
// Run two threads in parallel attempting to update the
// two objects in a different order, with the first
// suceeding and second failing
_accessMode = accessMode;
_lock = new Object();
_firstThread = new FirstThread(_firstDb, this);
_firstProblem = null;
_secondThread = new SecondThread(_secondDb, this);
_secondProblem = null;
_secondThread.start();
_firstThread.start();
_firstThread.join();
_secondThread.join();
if (!_firstThread._resultOk || !_secondThread._resultOk) {
if (!_firstThread._resultOk) {
LOG.error("first failed: ", _firstProblem);
}
if (!_secondThread._resultOk) {
LOG.error("second failed: ", _secondProblem);
}
fail("unexpected deadlock behavior");
}
}
private class FirstThread extends Thread {
private Database _db;
private boolean _resultOk;
private TestDeadlock _theTest;
FirstThread(final Database db, final TestDeadlock theTest) {
_db = db;
_theTest = theTest;
}
boolean result() {
return _resultOk;
}
public void run() {
Sample object;
try {
_db.begin();
// Load first object and change something about it
// (otherwise will not write)
LOG.debug("1.1: Loading object " + Sample.DEFAULT_ID);
object = (Sample) _db.load(Sample.class,
new Integer(Sample.DEFAULT_ID), _accessMode);
object.setValue1(Sample.DEFAULT_VALUE_1 + ":1");
LOG.debug("1.2: Modified to " + object);
// Notify the other thread that it may proceed and suspend
// to give the other thread a opportunity.
synchronized (_lock) {
_lock.notify();
_lock.wait();
}
LOG.debug("1.3: Loading object " + (Sample.DEFAULT_ID + 1));
object = (Sample) _db.load(Sample.class,
new Integer(Sample.DEFAULT_ID + 1), _accessMode);
object.setValue2(Sample.DEFAULT_VALUE_2 + ":1");
LOG.debug("1.4: Modified to " + object);
// Notify the other thread that it may proceed and suspend
// to give the other thread a 2 second opportunity.
if (_secondThread.isAlive()) {
synchronized (_lock) {
_lock.notify();
_lock.wait(2000);
}
}
// Attempt to commit the transaction, must acquire a write
// lock blocking until the first transaction completes.
LOG.debug("1.5: Committing");
_db.commit();
if (_secondThread.isAlive()) {
synchronized (_lock) {
_lock.notify();
}
}
LOG.debug("1.6: Committed");
_resultOk = true;
} catch (Exception ex) {
_theTest._firstProblem = ex;
LOG.info("1.X: ", ex);
} finally {
try {
if (_db.isActive()) { _db.rollback(); }
} catch (TransactionNotInProgressException ex) {
}
}
}
}
private class SecondThread extends Thread {
private Database _db;
private TestDeadlock _theTest;
private boolean _resultOk;
SecondThread(final Database db, final TestDeadlock theTest) {
_db = db;
_theTest = theTest;
}
boolean result() {
return _resultOk;
}
public void run() {
Sample object;
try {
_db.begin();
// Suspend and give the other thread a opportunity.
synchronized (_lock) {
_lock.wait();
}
// Load first object and change something about it
// (otherwise will not write)
LOG.debug("2.1: Loading object " + (Sample.DEFAULT_ID + 1));
object = (Sample) _db.load(Sample.class,
new Integer(Sample.DEFAULT_ID + 1), _accessMode);
object.setValue2(Sample.DEFAULT_VALUE_2 + ":2");
LOG.debug("2.2: Modified to " + object);
// Notify the other thread that it may proceed and suspend
// to give the other thread a 2 second opportunity.
synchronized (_lock) {
_lock.notify();
_lock.wait(2000);
}
LOG.debug("2.3: Loading object " + Sample.DEFAULT_ID);
try {
object = (Sample) _db.load(Sample.class,
new Integer(Sample.DEFAULT_ID), _accessMode);
} catch (LockNotGrantedException ex) {
if (_accessMode == Database.EXCLUSIVE
|| _accessMode == Database.DBLOCKED) {
LOG.debug("2.X OK: Deadlock detected");
} else {
_theTest._secondProblem = ex;
LOG.info("2.X Error: ", ex);
}
_db.rollback();
_resultOk = true;
return;
}
object.setValue1(Sample.DEFAULT_VALUE_1 + ":2");
LOG.debug("2.4: Modified to " + object);
// Notify the other thread that it may proceed and suspend
// to give the other thread a 2 second opportunity.
synchronized (_lock) {
_lock.notify();
_lock.wait(2000);
}
// Attempt to commit the transaction, must acquire a write
// lock blocking until the first transaction completes.
LOG.debug("2.5: Committing");
_db.commit();
synchronized (_lock) {
_lock.notify();
}
LOG.info("2.6 Error: no deadlock and second committed");
_theTest._secondProblem = new Exception(
"deadlock not detected");
} catch (TransactionAbortedException ex) {
if (ex.getCause() instanceof LockNotGrantedException) {
LOG.debug("2.X OK: Deadlock detected");
_resultOk = true;
} else {
_theTest._secondProblem = ex;
LOG.info("2.X Error: ", ex);
}
LOG.debug("2.X Second: aborting");
} catch (Exception ex) {
_theTest._secondProblem = ex;
LOG.info("2.X Error: ", ex);
} finally {
try {
if (_db.isActive()) { _db.rollback(); }
} catch (TransactionNotInProgressException ex) {
}
}
}
}
}