/**
* Copyright 2011-2012 Akiban Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.persistit;
import static com.persistit.TransactionStatus.ABORTED;
import static com.persistit.TransactionStatus.UNCOMMITTED;
import java.util.concurrent.atomic.AtomicLong;
import junit.framework.TestCase;
import org.junit.Test;
public class TransactionIndexTest extends TestCase {
private final TimestampAllocator _tsa = new TimestampAllocator();
@Test
public void testBasicMethods() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
final TransactionStatus ts1 = ti.registerTransaction();
ti.updateActiveTransactionCache();
assertTrue(ti.hasConcurrentTransaction(0, ts1.getTs() + 1));
ts1.commit(_tsa.updateTimestamp());
final TransactionStatus ts2 = ti.registerTransaction();
/*
* True because the ActiveTransactionCache hasn't been updated yet.
*/
assertTrue(ti.hasConcurrentTransaction(0, ts1.getTs() + 1));
assertTrue(ti.hasConcurrentTransaction(0, ts2.getTs() + 1));
ti.updateActiveTransactionCache();
/*
* Floor value can't move until notifyComplete called on ts1.
*/
assertTrue(ti.hasConcurrentTransaction(0, ts1.getTs() + 1));
assertTrue(ti.hasConcurrentTransaction(0, ts2.getTs() + 1));
/*
* Same transaction - illusion that it has committed.
*/
assertTrue(isCommitted(ti.commitStatus(TransactionIndex.ts2vh(ts2.getTs()), ts2.getTs(), 0)));
/*
* Step policy - see my updates and previous updates, but not future
*/
assertTrue(isCommitted(ti.commitStatus(TransactionIndex.ts2vh(ts2.getTs()) + 1, ts2.getTs(), 1)));
assertTrue(isCommitted(ti.commitStatus(TransactionIndex.ts2vh(ts2.getTs()) + 1, ts2.getTs(), 2)));
assertFalse(isCommitted(ti.commitStatus(TransactionIndex.ts2vh(ts2.getTs()) + 2, ts2.getTs(), 1)));
final TransactionStatus ts3 = ti.registerTransaction();
final TransactionStatus ts4 = ti.registerTransaction();
ts2.commit(_tsa.updateTimestamp());
ti.updateActiveTransactionCache();
assertTrue(ti.hasConcurrentTransaction(0, ts2.getTs() + 1));
_tsa.updateTimestamp();
assertEquals(UNCOMMITTED, ti.commitStatus(TransactionIndex.ts2vh(ts3.getTs()), _tsa.getCurrentTimestamp(), 0));
assertEquals(ts3.getTs(), ti.commitStatus(TransactionIndex.ts2vh(ts3.getTs()), ts3.getTs(), 0));
ts3.incrementMvvCount();
ts3.abort();
assertEquals(ABORTED, ti.commitStatus(TransactionIndex.ts2vh(ts3.getTs()), _tsa.getCurrentTimestamp(), 0));
assertEquals(4, ti.getCurrentCount());
ti.notifyCompleted(ts1, -ts1.getTc());
/*
* ts1 committed and not concurrent with ts2
*/
assertTrue(isCommitted(ti.commitStatus(TransactionIndex.ts2vh(ts1.getTs()), ts2.getTs(), 0)));
ti.notifyCompleted(ts2, _tsa.updateTimestamp());
/*
* ts2 committed but ts4 is concurrent
*/
assertFalse(isCommitted(ti.commitStatus(TransactionIndex.ts2vh(ts2.getTs()), ts4.getTs(), 0)));
ts4.commit(_tsa.updateTimestamp());
ti.notifyCompleted(ts3, ABORTED);
ti.updateActiveTransactionCache();
ti.notifyCompleted(ts4, -ts4.getTc() + 1);
assertEquals(3, ti.getCurrentCount());
assertEquals(1, ti.getFreeCount());
assertEquals(0, ti.getAbortedCount());
ts3.decrementMvvCount();
ti.cleanup(); // compute canonical form
assertEquals(0, ti.getCurrentCount());
assertEquals(4, ti.getFreeCount());
assertEquals(0, ti.getAbortedCount());
}
private boolean isCommitted(final long tc) {
return tc >= 0 && tc != UNCOMMITTED;
}
@Test
public void testNonBlockingWwDependency() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
final TransactionStatus ts1 = ti.registerTransaction();
final TransactionStatus ts2 = ti.registerTransaction();
ts1.commit(_tsa.updateTimestamp());
ti.notifyCompleted(ts1, _tsa.updateTimestamp());
/*
* Should return 0 because ts1 has committed and is now primordial.
*/
assertTrue(isCommitted(ti.wwDependency(TransactionIndex.ts2vh(ts1.getTs()), ts2, 1000)));
final TransactionStatus ts3 = ti.registerTransaction();
ts2.abort();
ti.notifyCompleted(ts2, _tsa.updateTimestamp());
/*
* Should return false because ts1 and ts3 are not concurrent
*/
assertTrue(isCommitted(ti.wwDependency(TransactionIndex.ts2vh(ts1.getTs()), ts3, 1000)));
/*
* Should return false because ts2 aborted
*/
assertTrue(isCommitted(ti.wwDependency(TransactionIndex.ts2vh(ts2.getTs()), ts3, 1000)));
ts3.commit(_tsa.updateTimestamp());
}
@Test
public void testReduce() throws Exception {
final TransactionStatus[] array = new TransactionStatus[100];
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
for (int count = 0; count < array.length; count++) {
array[count] = ti.registerTransaction();
array[count].incrementMvvCount();
}
assertEquals(ti.getLongRunningThreshold(), ti.getCurrentCount());
assertEquals(array.length - ti.getLongRunningThreshold(), ti.getLongRunningCount());
for (int count = 20; count < 70; count++) {
array[count].abort();
ti.notifyCompleted(array[count], _tsa.getCurrentTimestamp());
}
for (int count = 50; count < 60; count++) {
array[count].decrementMvvCount();
}
assertEquals(ti.getLongRunningThreshold(), ti.getCurrentCount());
assertEquals(array.length - ti.getLongRunningThreshold() - ti.getAbortedCount() - ti.getFreeCount(),
ti.getLongRunningCount());
assertEquals(50, ti.getAbortedCount());
for (int count = 0; count < 20; count++) {
array[count].commit(array[20].getTs());
ti.notifyCompleted(array[count], array[20].getTs());
}
ti.updateActiveTransactionCache();
ti.cleanup();
assertEquals(ti.getMaxFreeListSize(), ti.getFreeCount());
assertEquals(50, ti.getAbortedCount());
assertEquals(ti.getLongRunningThreshold(), ti.getCurrentCount());
assertEquals(
array.length - ti.getCurrentCount() - ti.getAbortedCount() - ti.getFreeCount() - ti.getDroppedCount(),
ti.getLongRunningCount());
ti.updateActiveTransactionCache();
/*
* aborted set retained due to currently active transactions that
* started before the mvvCount was decremented
*/
assertEquals(50, ti.getAbortedCount());
/*
* Commit all remaining transactions so that there no currently active
* transactions.
*/
for (int count = 70; count < array.length; count++) {
array[count].commit(_tsa.getCurrentTimestamp());
ti.notifyCompleted(array[count], _tsa.updateTimestamp());
}
/*
* Compute canonical form. 40 aborted transactions should be left over
* because their mvv counts were not decremented.
*/
ti.cleanup();
assertEquals(40, ti.getAbortedCount());
}
@Test
public void testBlockingWwDependency() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
final TransactionStatus ts1 = ti.registerTransaction();
final TransactionStatus ts2 = ti.registerTransaction();
final AtomicLong elapsed = new AtomicLong();
final long result1 = tryBlockingWwDependency(ti, ts1, ts2, 1000, 10000, elapsed, true);
assertTrue(result1 > 0);
assertTrue(elapsed.get() >= 900);
final TransactionStatus ts3 = ti.registerTransaction();
final long result2 = tryBlockingWwDependency(ti, ts2, ts3, 1000, 10000, elapsed, false);
assertFalse(result2 > 0);
assertTrue(elapsed.get() >= 900);
}
private long tryBlockingWwDependency(final TransactionIndex ti, final TransactionStatus target,
final TransactionStatus source, final long wait, final long timeout, final AtomicLong elapsed,
final boolean commit) throws Exception {
final AtomicLong result = new AtomicLong();
final Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
final long start = System.currentTimeMillis();
result.set(ti.wwDependency(TransactionIndex.ts2vh(target.getTs()), source, timeout));
elapsed.set(System.currentTimeMillis() - start);
} catch (final IllegalArgumentException e) {
e.printStackTrace();
} catch (final InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(wait);
if (commit) {
target.commit(_tsa.getCurrentTimestamp());
} else {
target.abort();
}
ti.notifyCompleted(target, _tsa.updateTimestamp());
t.join();
return result.get();
}
@Test
public void testDeadlockedWwDependency() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
/*
* Three concurrent transactions
*/
final TransactionStatus ts1 = ti.registerTransaction();
final TransactionStatus ts2 = ti.registerTransaction();
final TransactionStatus ts3 = ti.registerTransaction();
final AtomicLong result1 = new AtomicLong(-42);
final AtomicLong result2 = new AtomicLong(-42);
final AtomicLong result3 = new AtomicLong(-42);
final AtomicLong elapsed1 = new AtomicLong(-42);
final AtomicLong elapsed2 = new AtomicLong(-42);
final AtomicLong elapsed3 = new AtomicLong(-42);
final Thread t1 = tryWwDependency(ti, ts1, ts2, 2000, result1, elapsed1);
final Thread t2 = tryWwDependency(ti, ts2, ts3, 1000000, result2, elapsed2);
Thread.sleep(1000);
final Thread t3 = tryWwDependency(ti, ts3, ts1, 10000, result3, elapsed3);
t3.join();
assertEquals("Deadlock not detected", TransactionStatus.UNCOMMITTED, result3.get());
t1.join();
ts2.abort();
ti.notifyCompleted(ts2, TransactionStatus.ABORTED);
t2.join();
assertEquals(0, result2.get());
assertTrue(elapsed1.get() >= 900);
assertTrue(elapsed2.get() >= 900);
assertTrue(elapsed3.get() < 1000);
}
@Test
public void testNonConcurrentWwDependency() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
final TransactionStatus ts1 = ti.registerTransaction();
/*
* Commit processing
*/
final long commitTimestamp = _tsa.updateTimestamp();
ts1.commit(commitTimestamp);
/*
* Transactions which will ultimately be non-concurrent.
*/
final TransactionStatus ts2 = ti.registerTransaction();
final AtomicLong result1 = new AtomicLong(-42);
final AtomicLong elapsed1 = new AtomicLong(-42);
final Thread t1 = tryWwDependency(ti, ts1, ts2, 10000, result1, elapsed1);
Thread.sleep(1000);
ti.notifyCompleted(ts1, commitTimestamp);
t1.join();
assertEquals("Should be non-current and therefore 0", 0, result1.get());
assertTrue("Should have waited until notifyCompleted", elapsed1.get() >= 900);
}
private Thread tryWwDependency(final TransactionIndex ti, final TransactionStatus target,
final TransactionStatus source, final long timeout, final AtomicLong result, final AtomicLong elapsed)
throws Exception {
final Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
final long start = System.currentTimeMillis();
result.set(ti.wwDependency(TransactionIndex.ts2vh(target.getTs()), source, timeout));
elapsed.set(System.currentTimeMillis() - start);
} catch (final IllegalArgumentException e) {
e.printStackTrace();
} catch (final InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
return t;
}
@Test
public void testNotifyCompletedBelowFloor() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
final TransactionStatus ts1 = ti.registerTransaction();
/*
* Commit processing
*/
final long commitTimestamp = _tsa.updateTimestamp();
ts1.commit(commitTimestamp);
ti.updateActiveTransactionCache();
ti.cleanup();
ti.notifyCompleted(ts1, commitTimestamp);
}
/**
* Bug 914474 is an isolation failure in Stress8txn when run with 10
* threads. Hypothesis is that a TransactionStatus for a committed
* transaction is still needed in the TransactionIndex to enforce
* wwDependency detection, but has been freed. This test asserts that a
* TransactionStatus is retained until there are no other active
* transactions that started earlier (as opposed to the erroneous
* proposition that it can be freed if no other transaction is concurrent).
*
* @throws Exception
*/
@Test
public void testBug914474() throws Exception {
final TransactionIndex ti = new TransactionIndex(_tsa, 1);
final TransactionStatus ts1 = ti.registerTransaction();
final TransactionStatus ts2 = ti.registerTransaction();
ts2.commit(_tsa.updateTimestamp());
ti.notifyCompleted(ts2, _tsa.getCurrentTimestamp());
/*
* Cause ts2 to be "obsolete"
*/
for (int i = 0; i < 100; i++) {
final TransactionStatus ts3 = ti.registerTransaction();
ts3.commit(_tsa.updateTimestamp());
ti.notifyCompleted(ts3, _tsa.getCurrentTimestamp());
}
ti.cleanup();
assertTrue(ti.wwDependency(TransactionIndex.ts2vh(ts2.getTs()), ts1, 0) != 0);
}
}