package org.radargun.stages.test;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import org.radargun.Operation;
import org.radargun.logging.Log;
import org.radargun.logging.LogFactory;
import org.radargun.stats.Request;
import org.radargun.stats.RequestSet;
import org.radargun.stats.Statistics;
import org.radargun.traits.Transactional;
/**
* Each stressor operates according to its {@link OperationLogic logic} - the instance is private to each thread.
* After finishing the {@linkplain OperationLogic#init(Stressor) init phase}, all stressors synchronously
* execute logic's {@link OperationLogic#run(org.radargun.Operation) run} method until
* the {@link Completion#moreToRun()} returns false.
*
* @author Radim Vansa <rvansa@redhat.com>
*/
public class Stressor extends Thread {
private static Log log = LogFactory.getLog(Stressor.class);
private final TestStage stage;
private final int threadIndex;
private final int globalThreadIndex;
private final OperationLogic logic;
private final Random random;
private final OperationSelector operationSelector;
private final Completion completion;
private final boolean logTransactionExceptions;
private long delayBetweenRequests;
private boolean useTransactions;
private int txRemainingOperations = 0;
private RequestSet requests;
private Transactional.Transaction ongoingTx;
private Statistics stats;
private boolean started = false;
private CountDownLatch threadCountDown;
public Stressor(TestStage stage, OperationLogic logic, int globalThreadIndex, int threadIndex, boolean logTransactionExceptions, CountDownLatch threadCountDown, long delayBetweenRequests) {
super("Stressor-" + threadIndex);
this.stage = stage;
this.threadIndex = threadIndex;
this.globalThreadIndex = globalThreadIndex;
this.logic = logic;
this.random = ThreadLocalRandom.current();
this.completion = stage.getCompletion();
this.operationSelector = stage.getOperationSelector();
this.logTransactionExceptions = logTransactionExceptions;
this.threadCountDown = threadCountDown;
this.delayBetweenRequests = delayBetweenRequests;
}
private boolean recording() {
return this.started && !stage.isFinished();
}
@Override
public void run() {
try {
logic.init(this);
stats = stage.createStatistics();
runInternal();
} catch (Exception e) {
log.error("Unexpected error in stressor!", e);
stage.setTerminated();
} finally {
if (stats != null) {
stats.end();
}
logic.destroy();
}
}
private void runInternal() {
boolean counted = false;
try {
// the operation selector needs to be started before any #next() call
operationSelector.start();
while (!stage.isTerminated()) {
boolean started = stage.isStarted();
if (started) {
txRemainingOperations = 0;
if (ongoingTx != null) {
endTransactionAndRegisterStats(null);
}
break;
} else {
if (!counted) {
threadCountDown.countDown();
counted = true;
}
Operation operation = operationSelector.next(random);
try {
logic.run(operation);
if (delayBetweenRequests > 0)
sleep(delayBetweenRequests);
} catch (OperationLogic.RequestException e) {
// the exception was already logged in makeRequest
} catch (InterruptedException e) {
log.trace("Stressor interrupted.", e);
interrupt();
}
}
}
stats.begin();
this.started = true;
completion.start();
int i = 0;
while (!stage.isTerminated()) {
Operation operation = operationSelector.next(random);
if (!completion.moreToRun()) break;
try {
logic.run(operation);
if (delayBetweenRequests > 0)
sleep(delayBetweenRequests);
} catch (OperationLogic.RequestException e) {
// the exception was already logged in makeRequest
} catch (InterruptedException e) {
log.trace("Stressor interrupted.", e);
interrupt();
}
i++;
completion.logProgress(i);
}
} finally {
if (txRemainingOperations > 0) {
endTransactionAndRegisterStats(null);
}
}
}
public <T> T wrap(T resource) {
return ongoingTx.wrap(resource);
}
public <T> T makeRequest(Invocation<T> invocation) throws OperationLogic.RequestException {
return makeRequest(invocation, true);
}
public <T> T makeRequest(Invocation<T> invocation, boolean countForTx) throws OperationLogic.RequestException {
if (useTransactions && txRemainingOperations <= 0) {
try {
ongoingTx = stage.transactional.getTransaction();
logic.transactionStarted();
if (recording()) {
requests = stats.requestSet();
}
Request beginRequest = startTransaction();
if (requests != null && beginRequest != null) {
requests.add(beginRequest);
}
txRemainingOperations = stage.transactionSize;
} catch (TransactionException e) {
throw new OperationLogic.RequestException(e);
}
}
T result = null;
Exception exception = null;
Request request = recording() ? stats.startRequest() : null;
try {
result = invocation.invoke();
succeeded(request, invocation.operation());
// make sure that the return value cannot be optimized away
// however, we can't be 100% sure about reordering without
// volatile writes/reads here
Blackhole.consume(result);
if (countForTx) {
txRemainingOperations--;
}
} catch (Exception e) {
failed(request, invocation.operation());
log.warn("Error in request", e);
txRemainingOperations = 0;
exception = e;
}
if (requests != null && request != null && recording()) {
requests.add(request);
}
if (useTransactions && txRemainingOperations <= 0) {
endTransactionAndRegisterStats(stage.isSingleTxType() ? invocation.txOperation() : null);
}
if (exception != null) {
throw new OperationLogic.RequestException(exception);
}
return result;
}
public <T> void succeeded(Request request, Operation operation) {
if (request != null) {
if (recording()) {
request.succeeded(operation);
} else {
request.discard();
}
}
}
public <T> void failed(Request request, Operation operation) {
if (request != null) {
if (recording()) {
request.failed(operation);
} else {
request.discard();
}
}
}
private void endTransactionAndRegisterStats(Operation singleTxOperation) {
Request commitRequest = recording() ? stats.startRequest() : null;
try {
if (stage.commitTransactions) {
ongoingTx.commit();
} else {
ongoingTx.rollback();
}
succeeded(commitRequest, stage.commitTransactions ? Transactional.COMMIT : Transactional.ROLLBACK);
} catch (Exception e) {
failed(commitRequest, stage.commitTransactions ? Transactional.COMMIT : Transactional.ROLLBACK);
if (logTransactionExceptions) {
log.error("Failed to end transaction", e);
}
} finally {
if (requests != null) {
if (recording()) {
requests.add(commitRequest);
requests.finished(commitRequest.isSuccessful(), Transactional.DURATION);
if (singleTxOperation != null) {
requests.finished(commitRequest.isSuccessful(), singleTxOperation);
}
} else {
requests.discard();
}
requests = null;
}
clearTransaction();
}
}
public void setUseTransactions(boolean useTransactions) {
this.useTransactions = useTransactions;
}
private void clearTransaction() {
logic.transactionEnded();
ongoingTx = null;
}
public int getThreadIndex() {
return threadIndex;
}
public int getGlobalThreadIndex() {
return globalThreadIndex;
}
public Statistics getStats() {
return stats;
}
public OperationLogic getLogic() {
return logic;
}
public Random getRandom() {
return random;
}
public boolean isUseTransactions() {
return useTransactions;
}
private Request startTransaction() throws TransactionException {
Request request = recording() ? stats.startRequest() : null;
try {
ongoingTx.begin();
if (request != null) {
request.succeeded(Transactional.BEGIN);
}
return request;
} catch (Exception e) {
if (request != null) {
request.failed(Transactional.BEGIN);
}
log.error("Failed to start transaction", e);
throw new TransactionException(request, e);
}
}
private class TransactionException extends Exception {
private final Request request;
public TransactionException(Request request, Exception cause) {
super(cause);
this.request = request;
}
public Request getRequest() {
return request;
}
}
}