package org.radargun.stages.cache;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.radargun.DistStageAck;
import org.radargun.StageResult;
import org.radargun.config.Property;
import org.radargun.config.Stage;
import org.radargun.state.SlaveState;
import org.radargun.traits.BasicOperations;
import org.radargun.traits.InjectTrait;
import org.radargun.traits.Transactional;
import org.radargun.utils.Projections;
import org.radargun.utils.TimeConverter;
/**
* Stage for testing guaranties of isolation levels.
*
* @author Radim Vansa <rvansa@redhat.com>
*/
@Stage(doc = "Stage for testing guaranties of isolation levels.")
public class IsolationLevelCheckStage extends CheckStage {
private static final String ISOLATION_CHECK_KEY = "isolationCheckKey";
public static final String REPEATABLE_READ = "REPEATABLE_READ";
public static final String READ_COMMITTED = "READ_COMMITTED";
@Property(doc = "Number of concurrent threads that modify the value. Default is 2.")
private int writers = 2;
@Property(doc = "Number of concurrent threads that try to retrieve the value. Default is 10.")
private int readers = 10;
@Property(converter = TimeConverter.class, doc = "How long should this stage take. Default is 1 minute.")
private long duration = 60000;
@Property(doc = "Number of reads executed inside on transaction. Default is 30.")
private int transactionSize = 30;
@Property(optional = false, doc = "Expected isolation level (should match to cache configuration). Supported " +
"values are [" + IsolationLevelCheckStage.READ_COMMITTED + ", " + IsolationLevelCheckStage.REPEATABLE_READ + "].")
private String expectedLevel;
private volatile boolean finished;
private volatile boolean valueChangeDetected = false;
@InjectTrait
private BasicOperations basicOperations;
@InjectTrait
private Transactional transactional;
@Override
public DistStageAck executeOnSlave() {
if (expectedLevel == null) {
return errorResponse("No expected level set", null);
} else if (!expectedLevel.equalsIgnoreCase(REPEATABLE_READ) && !expectedLevel.equalsIgnoreCase(READ_COMMITTED)) {
return errorResponse("Expected level should be one of " + READ_COMMITTED + " and " + REPEATABLE_READ, null);
}
BasicOperations.Cache<Object, Object> cache = basicOperations.getCache(null);
try {
cache.put(ISOLATION_CHECK_KEY, new Long(0));
} catch (Exception e) {
return errorResponse("Failed to insert first value", e);
}
List<ClientThread> threads = new ArrayList<ClientThread>();
for (int i = 0; i < writers; ++i) {
ClientThread t = new WriterThread();
t.setName("Writer-" + i);
t.start();
threads.add(t);
}
for (int i = 0; i < readers; ++i) {
ClientThread t = new ReaderThread();
t.setName("Reader-" + i);
t.start();
threads.add(t);
}
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
}
finished = true;
DistStageAck error = checkThreads(threads);
if (error != null) return error;
return new ChangeAck(slaveState, valueChangeDetected);
}
@Override
public StageResult processAckOnMaster(List<DistStageAck> acks) {
StageResult result = super.processAckOnMaster(acks);
if (result.isError()) return result;
boolean anyValueChangeDetected = false;
for (ChangeAck ack : Projections.instancesOf(acks, ChangeAck.class)) {
log.debugf("Value change detected on slave %d: %s", ack.getSlaveIndex(), ack.valueChangeDetected);
if (expectedLevel.equalsIgnoreCase(REPEATABLE_READ) && ack.valueChangeDetected) {
log.error("Value change was detected but this should not happen with isolation " + expectedLevel);
return errorResult();
}
anyValueChangeDetected |= ack.valueChangeDetected;
}
if (expectedLevel.equalsIgnoreCase(READ_COMMITTED) && !anyValueChangeDetected) {
log.error("Value change was expected with isolation " + expectedLevel + " but none was detected");
return errorResult();
}
return StageResult.SUCCESS;
}
private static class ChangeAck extends DistStageAck {
final boolean valueChangeDetected;
private ChangeAck(SlaveState slaveState, boolean valueChangeDetected) {
super(slaveState);
this.valueChangeDetected = valueChangeDetected;
}
}
private class WriterThread extends ClientThread {
@Override
public void run() {
BasicOperations.Cache nonTxCache = basicOperations.getCache(null);
Random rand = new Random();
while (!finished) {
try {
Transactional.Transaction tx = transactional.getTransaction();
BasicOperations.Cache txCache = tx.wrap(nonTxCache);
log.trace("Starting transaction");
tx.begin();
txCache.put(ISOLATION_CHECK_KEY, -1);
Thread.sleep(10);
long value = rand.nextInt(1000);
txCache.put(ISOLATION_CHECK_KEY, value);
log.trace("Inserted value " + value);
tx.commit();
log.trace("Ended transaction");
} catch (Exception e) {
exception = e;
return;
}
}
}
}
private class ReaderThread extends ClientThread {
@Override
public void run() {
BasicOperations.Cache nonTxCache = basicOperations.getCache(null);
while (!finished) {
Transactional.Transaction tx = transactional.getTransaction();
BasicOperations.Cache txCache = tx.wrap(nonTxCache);
log.trace("Starting transaction");
tx.begin();
Object lastValue = null;
for (int i = 0; i < transactionSize; ++i) {
try {
Object value = txCache.get(ISOLATION_CHECK_KEY);
log.trace("Read value " + value + ", previous value is " + lastValue);
if (!(value instanceof Long) || ((Long) value) < 0) {
exception = new IllegalStateException("Unexpected value " + value);
tx.rollback();
return;
}
if (lastValue != null && !lastValue.equals(value)) {
log.trace("Change detected");
valueChangeDetected = true;
}
lastValue = value;
Thread.sleep(10);
} catch (Exception e) {
exception = e;
return;
}
}
tx.commit();
log.trace("Ended transaction");
}
}
}
}