/**
* Copyright 2005-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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.InterruptedIOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import com.persistit.Transaction.CommitPolicy;
import com.persistit.exception.PersistitClosedException;
import com.persistit.exception.PersistitException;
import com.persistit.exception.PersistitIOException;
import com.persistit.exception.PersistitInterruptedException;
import com.persistit.exception.RollbackException;
/**
* Demonstrates the use of Persistit Transactions. This demo runs multiple
* threads that transfer "money" between accounts. At all times the sum of all
* balances should remain unchanged.
*
* @version 1.0
*/
public class TransactionTest2 extends PersistitUnitTestCase {
final static Object LOCK = new Object();
final static CommitPolicy policy = CommitPolicy.SOFT;
final static long TIMEOUT = 10000; // 10 seconds
final static int ITERATIONS_PER_THREAD = 25000;
static int _threadCount = 8;
static int _iterationsPerThread = ITERATIONS_PER_THREAD;
static int _accounts = 5000;
static AtomicInteger _retriedTransactionCount = new AtomicInteger();
static AtomicInteger _completedTransactionCount = new AtomicInteger();
static AtomicInteger _failedTransactionCount = new AtomicInteger();
static AtomicInteger _strandedThreads = new AtomicInteger();
static int _threadCounter = 0;
int _threadIndex = 0;
public static void main(final String[] args) throws Exception {
try {
if (args.length > 0) {
_threadCount = Integer.parseInt(args[0]);
}
if (args.length > 1) {
_iterationsPerThread = Integer.parseInt(args[1]);
}
if (args.length > 2) {
_accounts = Integer.parseInt(args[2]);
}
new TransactionTest2().initAndRunTest();
} catch (final NumberFormatException e) {
usage();
}
}
static void usage() {
final PrintStream o = System.out;
o.println("Demonstrates Persistit transactions. This program");
o.println("transfers random amounts of \"money\" between ");
o.println("randomly chosen accounts. The parameters specify ");
o.println("how many threads, how many iterations per threads, ");
o.println("and the total number of accounts to receive these");
o.println("transfers. Usage:");
o.println();
o.println("java SimpleTransaction <nthreads> <itersPerThread> <accounts>");
o.println();
}
@Override
public String toString() {
return Thread.currentThread().getName();
}
@Test
public void transactions() throws Exception {
//
// An Exchange for the Tree containing account "balances".
//
final Exchange accountEx = _persistit.getExchange("persistit", "account", true);
accountEx.removeAll();
//
// Get the starting "balance", that is, the sum of the amounts in
// each account.
//
System.out.println("Computing balance");
final int startingBalance = balance(accountEx);
System.out.println("Starting balance is " + startingBalance);
//
// Create the threads
//
final Thread[] threadArray = new Thread[_threadCount];
for (int index = 0; index < _threadCount; index++) {
threadArray[index] = new Thread(new Runnable() {
@Override
public void run() {
runIt(_iterationsPerThread);
}
}, "TransactionThread_" + index);
}
//
// Start them all and measure the time until the last thread ends
//
long time = System.currentTimeMillis();
System.out.println("Starting transaction threads");
for (int index = 0; index < _threadCount; index++) {
threadArray[index].start();
}
System.out.println("Waiting for threads to end");
for (int index = 0; index < _threadCount; index++) {
threadArray[index].join();
}
//
// All done
//
time = System.currentTimeMillis() - time;
System.out.println("All threads ended at " + time + " ms");
System.out.println("Completed transactions: " + _completedTransactionCount);
System.out.println("Failed transactions: " + _failedTransactionCount);
System.out.println("Retried transactions: " + _retriedTransactionCount);
final int endingBalance = balance(accountEx);
System.out.print("Ending balance is " + endingBalance + " which ");
System.out.println(endingBalance == startingBalance ? "AGREES" : "DISAGREES");
assertEquals(startingBalance, endingBalance);
}
@Test
public void transactionsWithInterrupts() throws Exception {
final TransactionIndex ti = _persistit.getTransactionIndex();
final Exchange accountEx = _persistit.getExchange("persistit", "account", true);
accountEx.removeAll();
int index = 0;
System.out.println("Computing balance");
final int startingBalance = balance(accountEx);
System.out.println("Starting balance is " + startingBalance);
final long expires = System.currentTimeMillis() + TIMEOUT;
final List<Thread> threads = new ArrayList<Thread>();
while (System.currentTimeMillis() < expires) {
//
// Remove any Thread instances that died (due to interrupt)
//
for (final Iterator<Thread> iter = threads.iterator(); iter.hasNext();) {
if (!iter.next().isAlive()) {
iter.remove();
}
}
//
// Top up the set of running threads
//
while (threads.size() < _threadCount) {
final Thread thread = new Thread(new Runnable() {
@Override
public void run() {
runIt(Integer.MAX_VALUE);
}
}, "TransactionThread_" + ++index);
threads.add(thread);
thread.start();
}
final Thread victim = threads.get(index % threads.size());
victim.interrupt();
Thread.sleep(50);
}
//
// Now interrupt all remaining threads and wait for them to die
//
for (final Thread thread : threads) {
thread.interrupt();
}
for (final Thread thread : threads) {
thread.join();
}
//
// Just to be sure this has been done after all threads died
//
ti.updateActiveTransactionCache();
//
System.out.println("Completed transactions: " + _completedTransactionCount);
System.out.println("Failed transactions: " + _failedTransactionCount);
System.out.println("Retried transactions: " + _retriedTransactionCount);
System.out.printf("\nCurrentCount=%,d AbortedCount=%,d "
+ "LongRunningCount=%,d FreeCount=%,d\natCache=%s\n", ti.getCurrentCount(), ti.getAbortedCount(),
ti.getLongRunningCount(), ti.getFreeCount(), ti.getActiveTransactionCache());
final int endingBalance = balance(accountEx);
System.out.print("Ending balance is " + endingBalance + " which ");
System.out.println(endingBalance == startingBalance ? "AGREES" : "DISAGREES");
assertEquals("Starting and ending balance don't agree", startingBalance, endingBalance);
assertTrue("ATC has very old transaction",
ti.getActiveTransactionCeiling() - ti.getActiveTransactionFloor() < 10000);
}
@Test(expected = IllegalStateException.class)
public void illegalStateExceptionOnRollback() throws Exception {
final Transaction txn = _persistit.getTransaction();
txn.rollback();
txn.begin();
}
@Test(expected = IllegalStateException.class)
public void illegalStateExceptionOnCommit() throws Exception {
final Transaction txn = _persistit.getTransaction();
txn.commit();
txn.begin();
}
@Test
public void transactionsConcurrentWithPersistitClose() throws Exception {
new Thread(new Runnable() {
@Override
public void run() {
final Thread[] threadArray = new Thread[_threadCount];
for (int index = 0; index < _threadCount; index++) {
threadArray[index] = new Thread(new Runnable() {
@Override
public void run() {
runIt(Integer.MAX_VALUE);
}
}, "TransactionThread_" + index);
}
for (int index = 0; index < _threadCount; index++) {
threadArray[index].start();
}
}
}).start();
/*
* Let the threads crank up
*/
Thread.sleep(1000);
_persistit.close();
assertEquals("All threads should have exited correctly", 0, _strandedThreads.get());
}
public void runIt(final int limit) {
_strandedThreads.incrementAndGet();
try {
final Exchange accountEx = _persistit.getExchange("persistit", "account", true);
//
final Random random = new Random();
for (int iterations = 1; iterations <= limit; iterations++) {
final int accountNo1 = random.nextInt(_accounts);
final int accountNo2 = random.nextInt(_accounts);
final int delta = random.nextInt(10000);
transfer(accountEx, accountNo1, accountNo2, delta);
_completedTransactionCount.incrementAndGet();
if (iterations % 25000 == 0) {
System.out.println(this + " has finished " + iterations + " iterations");
System.out.flush();
}
}
_strandedThreads.decrementAndGet();
} catch (final PersistitInterruptedException exception) {
_strandedThreads.decrementAndGet();
// expected
} catch (final PersistitClosedException exception) {
_strandedThreads.decrementAndGet();
// expected
} catch (final PersistitIOException exception) {
if (InterruptedIOException.class.equals(exception.getCause().getClass())) {
_strandedThreads.decrementAndGet();
// expected
} else {
exception.printStackTrace();
_failedTransactionCount.incrementAndGet();
}
} catch (final Exception exception) {
exception.printStackTrace();
_failedTransactionCount.incrementAndGet();
}
}
void transfer(final Exchange ex, final int accountNo1, final int accountNo2, final int delta)
throws PersistitException {
//
// A Transaction object is actually a Transaction context. You invoke
// begin() and commit() on it to create and commit a transaction scope.
//
final Transaction txn = ex.getTransaction();
int remainingAttempts = 500;
boolean done = false;
while (!done) {
//
// Start the scope of a transaction
//
txn.begin();
try {
ex.clear().append(accountNo1).fetch();
final int balance1 = ex.getValue().isDefined() ? ex.getValue().getInt() : 0;
ex.getValue().put(balance1 - delta);
ex.store();
assertEquals(1, txn.getTransactionStatus().getMvvCount());
ex.clear().append(accountNo2).fetch();
final int balance2 = ex.getValue().isDefined() ? ex.getValue().getInt() : 0;
ex.getValue().put(balance2 + delta);
ex.store();
assertEquals(accountNo1 == accountNo2 ? 1 : 2, txn.getTransactionStatus().getMvvCount());
//
// Commit the transaction
//
txn.commit(policy);
//
// Done.
//
done = true;
} catch (final RollbackException rollbackException) {
_retriedTransactionCount.incrementAndGet();
if (--remainingAttempts <= 0) {
throw rollbackException;
}
} finally {
txn.end();
}
}
}
//
// To illustrate another way to use the Transaction API, this
// method uses a TransactionRunnable to perform its logic. The
// TransactionRunnable (see the Balancer class, below) encapsulates
// all the logic to be performed within the transaction scope.
// The Transaction run() method handles provides the transaction scope
// and handles retries appropriately.
//
static int balance(final Exchange ex) throws PersistitException {
final Transaction txn = ex.getTransaction();
final Balancer balancer = new Balancer(ex);
txn.run(balancer);
return balancer.getTotal();
}
//
// A TransactionRunnable that encapsulates the logic to be performed within
// a transaction.
//
static class Balancer implements TransactionRunnable {
int _total = 0;
Exchange _ex;
Balancer(final Exchange ex) {
_ex = ex;
}
@Override
public void runTransaction() throws PersistitException {
_ex.clear().append(Key.BEFORE);
_total = 0;
while (_ex.next()) {
final int balance = _ex.getValue().getInt();
_total += balance;
}
}
int getTotal() {
return _total;
}
}
static final int RETRIES = 20;
void transferZZZ(final Exchange exchange, final int accountNo1, final int accountNo2, final int delta)
throws PersistitException {
//
// A Transaction object is actually a Transaction context. You invoke
// begin() and commit() on it to create and commit a transaction scope.
//
final Transaction txn = exchange.getTransaction();
//
// Because Persistit schedules transactions optimistically,
// applications must always handle rollbacks. This simple
// example simply retries the transaction up to RETRIES times.
//
for (int attempt = 0; attempt < RETRIES; attempt++) {
//
// Begin the scope of a transaction
//
txn.begin();
try {
//
// Fetch the account balance for accountNo1
//
exchange.clear().append(accountNo1).fetch();
final int balance1 = exchange.getValue().getInt();
//
// Update and store the account balance for accountNo1
//
exchange.getValue().put(balance1 - delta);
exchange.store();
//
// Fetch the account balance for accountNo2
//
exchange.clear().append(accountNo2).fetch();
final int balance2 = exchange.getValue().getInt();
//
// Update and store the account balance for accountNo2
//
exchange.getValue().put(balance2 + delta);
exchange.store();
//
// Commit the transaction. By default this is a memory
// commit which is fast but not durable. Use
// txn.commit(true)
// to force transaction updates to volume files.
//
txn.commit(policy);
//
// Done.
//
break;
} catch (final RollbackException rollbackException) {
// Any special logic to handle rollbacks goes here.
} finally {
txn.end();
}
}
throw new RollbackException();
}
}