/**
* 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.TIMED_OUT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.Test;
import com.persistit.exception.TimeoutException;
import com.persistit.util.ArgParser;
public class TransactionIndexConcurrencyTest {
final static int HASH_TABLE_SIZE = 1000;
final static int MVV_COUNT = 20000;
final static int ITERATIONS = 50000;
final static int THREAD_COUNT = 10;
final static int SEED = 1;
final static Random RANDOM = new Random(SEED);
final TimestampAllocator tsa = new TimestampAllocator();
final TransactionIndex ti;
final MVV[] mvvs;
final AtomicLong commits = new AtomicLong();
final AtomicLong aborts = new AtomicLong();
final AtomicLong timeouts = new AtomicLong();
static int hashTableSize = HASH_TABLE_SIZE;
static int mvvCount = MVV_COUNT;
static int iterations = ITERATIONS;
static int threadCount = THREAD_COUNT;
static boolean sleep = false;
static class MVV {
List<Long> versionHandles = new ArrayList<Long>();
}
public TransactionIndexConcurrencyTest() {
ti = new TransactionIndex(tsa, hashTableSize);
mvvs = new MVV[mvvCount];
for (int i = 0; i < mvvCount; i++) {
mvvs[i] = new MVV();
}
}
static class Txn {
static int counter = 0;
int id = ++counter;
TransactionStatus status;
}
@Test
public void testSingleThreaded() throws Exception {
final Txn txn = new Txn();
for (int i = 0; i < iterations; i++) {
runTransaction(txn, i);
if ((i % 100) == 99) {
ti.updateActiveTransactionCache();
}
}
for (int i = 0; i < mvvCount; i++) {
prune(mvvs[i]);
assertTrue(mvvs[i].versionHandles.isEmpty());
}
}
@Test
public void testConcurrentOperations() throws Exception {
final long start = System.currentTimeMillis();
final AtomicLong reported = new AtomicLong(start);
final Timer timer = new Timer();
final AtomicInteger errCount = new AtomicInteger();
timer.schedule(new TimerTask() {
@Override
public void run() {
ti.updateActiveTransactionCache();
final long now = System.currentTimeMillis();
if (now - reported.get() > 60000) {
reported.addAndGet(60000);
report(now - start);
}
}
}, 10, 10);
final Thread threads[] = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
final Thread thread = new Thread(new Runnable() {
@Override
public void run() {
final Txn txn = new Txn();
try {
for (int i = 0; i < iterations; i++) {
runTransaction(txn, i);
}
} catch (final Exception e) {
errCount.incrementAndGet();
e.printStackTrace();
}
}
}, String.format("Test%03d", i));
threads[i] = thread;
}
for (int i = 0; i < threadCount; i++) {
threads[i].start();
}
for (int i = 0; i < threadCount; i++) {
threads[i].join();
}
timer.cancel();
final long end = System.currentTimeMillis();
System.out.printf("\nBefore cleanup:\n");
report(end - start);
ti.cleanup();
System.out.printf("\nAfter cleanup:\n");
report(end - start);
//
// Verify that all mvv's were cleaned up
//
for (int i = 0; i < mvvCount; i++) {
prune(mvvs[i]);
assertTrue(mvvs[i].versionHandles.isEmpty());
}
assertEquals(0, errCount.get());
assertTrue(aborts.get() > 0);
assertTrue(commits.get() > 0);
assertTrue(timeouts.get() > 0);
}
private void report(final long elapsed) {
System.out.printf("%,8dms: Commits=%,d Aborts=%,d\nCurrentCount=%,d AbortedCount=%,d "
+ "LongRunningCount=%,d FreeCount=%,d\natCache=%s\n\n", elapsed, commits.get(), aborts.get(),
ti.getCurrentCount(), ti.getAbortedCount(), ti.getLongRunningCount(), ti.getFreeCount(),
ti.getActiveTransactionCache());
}
private void runTransaction(final Txn txn, final int count) throws Exception {
txn.status = ti.registerTransaction();
final long ts = txn.status.getTs();
sometimesSleep(5);
final int vcount = RANDOM.nextInt(4);
boolean okay = true;
for (int i = 0; okay && i < vcount; i++) {
final int mvvIndex = RANDOM.nextInt(mvvCount);
final MVV mvv = mvvs[mvvIndex];
boolean retry = true;
int index = 0;
while (retry && okay) {
long versionHandle = 0;
retry = false;
sometimesSleep(1);
synchronized (mvv) {
prune(mvv);
for (; index < mvv.versionHandles.size(); index++) {
final long vh = mvv.versionHandles.get(index);
final long tc = ti.wwDependency(vh, txn.status, 0);
if (tc == TIMED_OUT) {
timeouts.incrementAndGet();
versionHandle = vh;
retry = true;
break;
} else if (tc > 0) {
okay = false;
break;
} else {
}
}
if (okay && !retry) {
mvv.versionHandles.add(TransactionIndex.ts2vh(ts));
txn.status.incrementMvvCount();
}
}
if ((count % 10000) == 0) {
Thread.sleep(100);
}
if (retry) {
final long tc = ti.wwDependency(versionHandle, txn.status, 300000);
if (tc == TIMED_OUT) {
throw new TimeoutException();
}
if (tc > 0) {
okay = false;
}
}
}
}
sometimesSleep(5);
if (okay) {
txn.status.commit(tsa.getCurrentTimestamp());
commits.incrementAndGet();
} else {
txn.status.abort();
aborts.incrementAndGet();
}
sometimesSleep(1);
final long tc = tsa.updateTimestamp();
ti.notifyCompleted(txn.status, tc);
}
void prune(final MVV mvv) throws TimeoutException, InterruptedException {
final long ts0 = 0;
for (int index = 0; index < mvv.versionHandles.size(); index++) {
final long vh = mvv.versionHandles.get(index);
final long tc = ti.commitStatus(vh, Long.MAX_VALUE, 0);
if (tc == ABORTED) {
// remove if aborted
mvv.versionHandles.remove(index);
ti.decrementMvvCount(vh);
index--;
} else if (tc > 0 && !ti.hasConcurrentTransaction(ts0, tc)) {
// remove if primordial - simulation does not need to keep it
mvv.versionHandles.remove(index);
index--;
}
}
}
void sometimesSleep(final int pctProbability) throws InterruptedException {
if (sleep) {
if (RANDOM.nextInt(100) < pctProbability) {
Thread.sleep(1);
}
if (RANDOM.nextInt(10000) < pctProbability) {
Thread.sleep(100);
}
if (RANDOM.nextInt(1000000) < pctProbability) {
Thread.sleep(10000);
System.out.println("Sleep 10 seconds");
}
if (RANDOM.nextInt(100000000) < pctProbability) {
Thread.sleep(100000);
System.out.println("Sleep 100 seconds");
}
}
}
public static void main(final String[] args) throws Exception {
final ArgParser ap = new ArgParser("TransactionIndexConcurrencyTest", args, new String[] {
"iterations|int:20000:0:1000000000|Transaction iterations per thread",
"threads|int:20:1:1000|Thread count", "mvvCount|int:1:1:1000000000|Number of MVV buckets",
"hashCount|int:1000:1:100000000|Hash table size", "_flag|s|Enable sleep intervals" });
iterations = ap.getIntValue("iterations");
threadCount = ap.getIntValue("threads");
hashTableSize = ap.getIntValue("hashCount");
mvvCount = ap.getIntValue("mvvCount");
sleep = ap.isFlag('s');
final TransactionIndexConcurrencyTest test = new TransactionIndexConcurrencyTest();
test.testConcurrentOperations();
}
}