package org.jboss.cache.lock;
import static org.testng.AssertJUnit.assertEquals;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import org.jboss.cache.util.TestingUtil;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* Various tests that test isolation level semantics provided by locks
*
* @author Bela Ban
* @version $Id: LockTest.java 7552 2009-01-20 21:56:37Z mircea.markus $
*/
@Test(groups = {"functional"}, testName = "lock.LockTest")
public class LockTest
{
int value = 10;
Throwable t1_ex, t2_ex;
long start = 0;
final long TIMEOUT = 5000;
final long SLEEP = 500;
volatile boolean committed;
@BeforeMethod(alwaysRun = true)
public void setUp() throws Exception
{
value = 10;
t1_ex = t2_ex = null;
committed = false;
}
static class MyFIFOSemaphore extends Semaphore
{
private static final long serialVersionUID = 3247961778517846603L;
public MyFIFOSemaphore(int permits)
{
super(permits);
}
public void acquire() throws InterruptedException
{
super.acquire();
}
public void release()
{
super.release();
}
}
/**
* Thread1 reads data, thread2 changes and - before thread2 commits - t1 should see t2's changes.
* Timeline:
* <ol>
* T1 reads data - 10
* T2 writes data - 20
* T1 reads data - 20 (see's T2's uncommitted modfication)
* T2 commits (releases its lock)
* T1 reads data - 20
* </ol>
*/
public void testReadUncommitted() throws Throwable
{
final LockStrategy s = new LockStrategyReadUncommitted();
final Semaphore sem = new MyFIFOSemaphore(1);
final CyclicBarrier barrier = new CyclicBarrier(2);
Thread t1 = new Thread("t1")
{
Lock lock = null;
public void run()
{
try
{
sem.acquire(); // we're first to the semaphore
// log("waiting on barrier");
barrier.await(); // wait until t2 joins us
// log("passed barrier");
lock = s.readLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("1st read: value is " + value);
assertEquals(10, value);
sem.release(); // give t2 time to make the modification
TestingUtil.sleepThread(100);
sem.acquire(); // to read the uncommitted modification by t2
log("2nd read: value is " + value + "; we should see t2's uncommitted change (20)");
assertEquals(20, value); // we're seeing the modification by t2 before t2 committed (a.k.a. released the lock)
sem.release();
TestingUtil.sleepThread(100);
sem.acquire(); // to read the committed change by t2
log("3rd read: value is still " + value + "; we should see t2's committed change");
assertEquals(20, value);
}
catch (Throwable ex)
{
t1_ex = ex;
}
finally
{
if (lock != null)
lock.unlock();
sem.release();
}
}
};
Thread t2 = new Thread("t2")
{
Lock lock = null;
public void run()
{
try
{
TestingUtil.sleepThread(100);
barrier.await();
sem.acquire();
lock = s.writeLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("changing value from " + value + " to 20");
value = 20;
sem.release(); // now t1 can read the uncommitted modification
TestingUtil.sleepThread(100);
sem.acquire(); // to unlock the lock
log("committing the TX");
lock.unlock();
}
catch (Throwable ex)
{
t2_ex = ex;
}
finally
{
if (lock != null)
lock.unlock();
sem.release();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
if (t1_ex != null)
throw t1_ex;
if (t2_ex != null)
throw t2_ex;
}
/**
* Thread1 reads data, thread2 changes and - before thread2 commits - t1 should *not* see t2's changes.
* Timeline:
* <ol>
* T1 reads data - 10
* T2 writes data - 20 (*not* visible to T1)
* T1 reads data - 10 (should *not* see T2's uncommitted modfication)
* T2 commits (releases its lock)
* T1 sees T2's committed modification - 20
* </ol>
* <em>Commented for now, until we get the right semantics</em>
* See http://www-128.ibm.com/developerworks/java/library/j-jtp0514.html for a discussion of
* isolation levels
*/
// removed in favor of o.j.c.transaction.IsolationLevelReadCommittedTest.testReadCommitted()
// The lock interceptor makes one request a read lock before *each* read.
/*public void testReadCommitted() throws Throwable {
final IdentityLock identity_lock=new IdentityLock(IsolationLevel.READ_COMMITTED);
Thread t1=new Thread("t1") {
public void run() {
try {
identity_lock.acquireReadLock(this, TIMEOUT);
log("1st read: value is " + value);
assertEquals(10, value);
TestingUtil.sleepThread(SLEEP);
log("2nd read: value is " + value + "; we should *not* see t2's uncommitted change (20)");
// we're seeing the modification by t2 before t2 committed (a.k.a. released the lock)
assertEquals("This is due to incorrect impl of READ_COMMITTED", 10, value);
TestingUtil.sleepThread(SLEEP);
log("3rd read: value is still " + value + "; we should see t2's committed change");
assertEquals(20, value);
}
catch(Throwable ex) {
t1_ex=ex;
}
finally {
identity_lock.unlock(this);
}
}
};
Thread t2=new Thread("t2") {
public void run() {
try {
TestingUtil.sleepThread(100);
identity_lock.acquireWriteLock(this, TIMEOUT);
log("changing value from " + value + " to 20");
value=20;
TestingUtil.sleepThread(SLEEP * 2);
log("committing the TX");
identity_lock.unlock(this);
}
catch(Throwable ex) {
t2_ex=ex;
}
finally {
identity_lock.unlock(this);
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
if(t1_ex != null)
throw t1_ex;
if(t2_ex != null)
throw t2_ex;
}*/
public void testWriteThanRead() throws Throwable
{
final LockStrategy s = new LockStrategyReadCommitted();
Thread t1 = new Thread("t1")
{
Lock lock = null;
public void run()
{
try
{
TestingUtil.sleepThread(100);
lock = s.readLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("1st read: value is " + value);
assertEquals(20, value);
TestingUtil.sleepThread(SLEEP);
log("2nd read: value is " + value + "; we should see t2's uncommitted change (20)");
assertEquals(20, value); // we're seeing the modification by t2 before t2 committed (a.k.a. released the lock)
TestingUtil.sleepThread(SLEEP);
}
catch (Throwable ex)
{
t1_ex = ex;
}
finally
{
lock.unlock();
}
}
};
Thread t2 = new Thread("t2")
{
Lock lock = null;
public void run()
{
try
{
lock = s.writeLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("changing value from " + value + " to 20");
value = 20;
TestingUtil.sleepThread(SLEEP);
log("committing the TX");
lock.unlock();
}
catch (Throwable ex)
{
t2_ex = ex;
}
finally
{
lock.unlock();
}
}
};
t2.start();
t1.start();
t2.join();
t1.join();
if (t1_ex != null)
throw t1_ex;
if (t2_ex != null)
throw t2_ex;
}
/**
* Thread1 reads data, thread2 changes and - before thread2 commits - t1 should *not* see t2's changes.
* In addition, Thread1 should *not* see thread2's changes even after thread2 commits, until thread1 commits.
* Timeline:
* <ol>
* T1 reads data - 10
* T2 writes data - 20 (*not* visible to T1)
* T1 reads data - 10 (should *not* see T2's uncommitted modfication)
* T2 commits (releases its lock)
* T1 reads data, should *not* see T2's committed modification - 10
* T1 commits
* T1 starts a new TX - should see 20
* </ol>
* Note: because we use pessimistic locking, the above sequence will effectively be serialized into sequential
* execution: thread1 will acquire the read lock on the data and hold on to it until TX commit, only then will
* thread2 be able to access the data with a write lock.
*/
public void testRepeatableRead() throws Throwable
{
final LockStrategy s = new LockStrategyRepeatableRead();
Thread t1 = new Thread("t1")
{
Lock lock = null;
public void run()
{
try
{
lock = s.readLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("1st read: value is " + value);
assertEquals(10, value);
TestingUtil.sleepThread(SLEEP);
log("2nd read: value is " + value + "; we should *not* see t2's uncommitted change (20)");
assertEquals(10, value);
TestingUtil.sleepThread(SLEEP);
log("3rd read: value is still " + value + "; we should not see t2's committed change");
assertEquals(10, value);
lock.unlock();
TestingUtil.sleepThread(SLEEP);
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("4th read: value is now " + value + "; we should see t2's committed change in our new TX");
assertEquals(20, value);
}
catch (Throwable ex)
{
t1_ex = ex;
}
finally
{
lock.unlock();
}
}
};
Thread t2 = new Thread("t2")
{
Lock lock = null;
public void run()
{
try
{
TestingUtil.sleepThread(100);
lock = s.writeLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("changing value from " + value + " to 20");
value = 20;
TestingUtil.sleepThread(SLEEP);
log("committing the TX");
lock.unlock();
}
catch (Throwable ex)
{
t2_ex = ex;
}
finally
{
lock.unlock();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
if (t1_ex != null)
throw t1_ex;
if (t2_ex != null)
throw t2_ex;
}
/**
* Because only 1 reader or writer can hold the lock at any given time, since thread1 is the first to get the lock,
* it will hold on to it until it commits. The the writer thread (thread2) will have a chance to change the value.
* Timeline:
* <ol>
* T1 reads data - 10
* T1 commits
* T2 writes data - 20
* T2 commits
* T1 starts a new TX and reads data - 20
* T2 commits (releases its lock)
* </ol>
*/
@Test (invocationCount = 5, successPercentage = 80) //relies on thread.sleep, might fail from time to time
public void testSerializable() throws Throwable
{
final LockStrategy s = new LockStrategySerializable();
Thread t1 = new Thread("t1")
{
Lock lock = null;
public void run()
{
try
{
lock = s.readLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("1st read: value is " + value);
assertEquals(10, value);
lock.unlock();
TestingUtil.sleepThread(SLEEP);
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("2nd read: value is " + value + "; we should see t2's committed change (20)");
assertEquals(20, value);
}
catch (Throwable ex)
{
t1_ex = ex;
}
finally
{
lock.unlock();
}
}
};
Thread t2 = new Thread("t2")
{
Lock lock = null;
public void run()
{
try
{
TestingUtil.sleepThread(100);
lock = s.writeLock();
lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS);
log("changing value from " + value + " to 20");
value = 20;
log("committing the TX");
lock.unlock();
}
catch (Throwable ex)
{
t2_ex = ex;
}
finally
{
lock.unlock();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
if (t1_ex != null)
throw t1_ex;
if (t2_ex != null)
throw t2_ex;
}
void log(String s)
{
long now;
if (start == 0)
start = System.currentTimeMillis();
now = System.currentTimeMillis();
}
}