// Copyright (C) 2000 Paco Gomez
// Copyright (C) 2000 - 2012 Philip Aston
// Copyright (C) 2003 Kalyanaraman Venkatasubramaniy
// Copyright (C) 2004 Slavik Gnatenko
// All rights reserved.
//
// This file is part of The Grinder software distribution. Refer to
// the file LICENSE which is part of The Grinder distribution for
// licensing details. The Grinder distribution is available on the
// Internet at http://grinder.sourceforge.net/
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
package net.grinder.engine.process;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.joran.spi.JoranException;
import net.grinder.common.*;
import net.grinder.common.processidentity.ProcessReport;
import net.grinder.common.processidentity.WorkerIdentity;
import net.grinder.communication.*;
import net.grinder.engine.common.ConnectorFactory;
import net.grinder.engine.common.EngineException;
import net.grinder.engine.communication.ConsoleListener;
import net.grinder.engine.messages.InitialiseGrinderMessage;
import net.grinder.engine.process.dcr.DCRContextImplementation;
import net.grinder.messages.console.RegisterTestsMessage;
import net.grinder.messages.console.ReportStatisticsMessage;
import net.grinder.messages.console.WorkerAddress;
import net.grinder.messages.console.WorkerProcessReportMessage;
import net.grinder.script.Grinder;
import net.grinder.script.InternalScriptContext;
import net.grinder.script.InvalidContextException;
import net.grinder.script.Statistics;
import net.grinder.scriptengine.Instrumenter;
import net.grinder.scriptengine.ScriptEngineService.ScriptEngine;
import net.grinder.scriptengine.ScriptEngineService.WorkerRunnable;
import net.grinder.scriptengine.ScriptExecutionException;
import net.grinder.statistics.*;
import net.grinder.synchronisation.BarrierGroups;
import net.grinder.synchronisation.BarrierIdentityGenerator;
import net.grinder.synchronisation.ClientBarrierGroups;
import net.grinder.synchronisation.LocalBarrierGroups;
import net.grinder.util.*;
import net.grinder.util.ListenerSupport.Informer;
import net.grinder.util.thread.BooleanCondition;
import net.grinder.util.thread.Condition;
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* The controller for a worker process.
* <p/>
* <p>
* Package scope.
* </p>
*
* @author Paco Gomez
* @author Philip Aston
* @author JunHo Yoon (modifed for nGrinder)
* @see GrinderThread
*/
final class GrinderProcess {
private final Logger m_terminalLogger;
private Logger m_logger = null;
private final Logger m_dataLogger;
private final LoggerContext m_logbackLoggerContext;
private final boolean m_reportTimesToConsole;
private final QueuedSender m_consoleSender;
private final Sleeper m_sleeper;
private final InitialiseGrinderMessage m_initialisationMessage;
private final ConsoleListener m_consoleListener;
private final StatisticsServices m_statisticsServices;
private final TestStatisticsMap m_accumulatedStatistics;
private final TestStatisticsHelperImplementation m_testStatisticsHelper;
private final TestRegistryImplementation m_testRegistryImplementation;
private final Condition m_eventSynchronisation = new Condition();
private final MessagePump m_messagePump;
private final ThreadStarter m_invalidThreadStarter = new InvalidThreadStarter();
private final Times m_times = new Times();
private final ThreadContexts m_threadContexts = new ThreadContexts();
private final ListenerSupport<ProcessLifeCycleListener> m_processLifeCycleListeners = new ListenerSupport<ProcessLifeCycleListener>();
// Guarded by m_eventSynchronisation.
private ThreadStarter m_threadStarter = m_invalidThreadStarter;
private boolean m_shutdownTriggered;
private boolean m_communicationShutdown;
/**
* Creates a new <code>GrinderProcess</code> instance.
*
* @param agentReceiver Receiver used to listen to the agent.
* @throws net.grinder.common.GrinderException
* If the process could not be created.
*/
public GrinderProcess(final Receiver agentReceiver) throws GrinderException {
try {
m_initialisationMessage = (InitialiseGrinderMessage) agentReceiver.waitForMessage();
if (m_initialisationMessage == null) {
throw new EngineException("No control stream from agent");
}
final GrinderProperties properties = m_initialisationMessage.getProperties();
final WorkerIdentity workerIdentity = m_initialisationMessage.getWorkerIdentity();
final String workerName = workerIdentity.getName();
final String logDirectory = properties.getProperty(GrinderProperties.LOG_DIRECTORY, ".");
m_terminalLogger = LoggerFactory.getLogger(workerName);
m_reportTimesToConsole = properties.getBoolean("grinder.reportTimesToConsole", true);
m_logbackLoggerContext = configureLogging(workerName, logDirectory);
m_logger = LoggerFactory.getLogger("worker." + workerName);
m_dataLogger = LoggerFactory.getLogger("data");
m_logger.info("The Grinder version {}", GrinderBuild.getVersionString());
m_logger.info(JVM.getInstance().toString());
m_logger.info("time zone is {}", new SimpleDateFormat("z (Z)").format(new Date()));
final MessageDispatchSender messageDispatcher = new MessageDispatchSender();
final BarrierGroups barrierGroups;
if (m_initialisationMessage.getReportToConsole()) {
m_consoleSender = new QueuedSenderDecorator(ClientSender.connect(new ConnectorFactory(
ConnectionType.WORKER).create(properties), new WorkerAddress(workerIdentity)));
barrierGroups = new ClientBarrierGroups(m_consoleSender, messageDispatcher);
} else {
m_consoleSender = new NullQueuedSender();
barrierGroups = new LocalBarrierGroups();
}
final BarrierIdentityGenerator barrierIdentityGenerator = new BarrierIdentityGenerator(
m_initialisationMessage.getWorkerIdentity());
final ThreadStarter delegatingThreadStarter = new ThreadStarter() {
@Override
public int startThread(final Object testRunner) throws EngineException, InvalidContextException {
final ThreadStarter threadStarter;
synchronized (m_eventSynchronisation) {
threadStarter = m_threadStarter;
}
return threadStarter.startThread(testRunner);
}
};
m_statisticsServices = StatisticsServicesImplementation.getInstance();
m_accumulatedStatistics = new TestStatisticsMap(m_statisticsServices.getStatisticsSetFactory());
m_testStatisticsHelper = new TestStatisticsHelperImplementation(
m_statisticsServices.getStatisticsIndexMap());
m_testRegistryImplementation = new TestRegistryImplementation(m_threadContexts,
m_statisticsServices.getStatisticsSetFactory(), m_testStatisticsHelper,
m_times.getTimeAuthority());
final Logger externalLogger = new ExternalLogger(m_logger, m_threadContexts);
m_sleeper = new SleeperImplementation(m_times.getTimeAuthority(), externalLogger, properties.getDouble(
"grinder.sleepTimeFactor", 1.0d), properties.getDouble("grinder.sleepTimeVariation", 0.2d));
final Statistics scriptStatistics = new ScriptStatisticsImplementation(m_threadContexts,
m_statisticsServices, m_consoleSender);
final ThreadStopper threadStopper = new ThreadStopper() {
@Override
public boolean stopThread(final int threadNumber) {
return m_threadContexts.shutdown(threadNumber);
}
};
final InternalScriptContext scriptContext = new ScriptContextImplementation(workerIdentity,
m_initialisationMessage.getFirstWorkerIdentity(), m_threadContexts, properties,
externalLogger, m_sleeper, new SSLControlImplementation(m_threadContexts),
scriptStatistics, m_testRegistryImplementation, delegatingThreadStarter, threadStopper,
barrierGroups, barrierIdentityGenerator);
Grinder.grinder = scriptContext;
final PluginRegistryImplementation pluginRegistry = new PluginRegistryImplementation(externalLogger,
scriptContext, m_threadContexts, m_statisticsServices, m_times.getTimeAuthority());
m_processLifeCycleListeners.add(pluginRegistry);
m_processLifeCycleListeners.add(m_threadContexts);
// If we don't call getLocalHost() before spawning our
// ConsoleListener thread, any attempt to call it afterwards will
// silently crash the JVM. Reproduced with both J2SE 1.3.1-b02 and
// J2SE 1.4.1_03-b02 on W2K. Do not ask me why, I've stopped
// caring.
try {
//noinspection ResultOfMethodCallIgnored
java.net.InetAddress.getLocalHost();
} catch (final UnknownHostException e) { /* Ignore */
}
m_consoleListener = new ConsoleListener(m_eventSynchronisation, m_logger);
m_consoleListener.registerMessageHandlers(messageDispatcher);
m_messagePump = new MessagePump(agentReceiver, messageDispatcher, 1);
} catch (GrinderException e) {
if (m_logger != null) {
m_logger.error("Error running worker process", e);
}
throw e;
}
}
private LoggerContext configureLogging(final String workerName, final String logDirectory) throws EngineException {
final ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory();
if (iLoggerFactory instanceof Context) {
final Context context = (Context) iLoggerFactory;
final LoggerContext result = (LoggerContext) iLoggerFactory;
final JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
context.putProperty("WORKER_NAME", workerName);
context.putProperty("LOG_DIRECTORY", logDirectory);
try {
configurator.doConfigure(GrinderProcess.class.getResource("/logback-worker.xml"));
} catch (final JoranException e) {
throw new EngineException("Could not initialise logger", e);
}
return result;
} else {
m_terminalLogger.warn("Logback not found; grinder log configuration will be ignored.\n"
+ "Consider adding logback-classic to the start of the CLASSPATH.");
return null;
}
}
/**
* The application's main loop. This is split from the constructor as theoretically it might be
* called multiple times. The constructor sets up the static configuration, this does a single
* execution.
* <p/>
* <p>
* This method is interruptible, in the same sense as
* {@link net.grinder.util.thread.InterruptibleRunnable#interruptibleRun()}. We don't implement
* that method because we want to be able to throw exceptions.
* </p>
*
* @throws net.grinder.common.GrinderException
* If something went wrong.
*/
public void run() throws GrinderException {
try {
final GrinderProperties properties = m_initialisationMessage.getProperties();
final ScriptEngineContainer scriptEngineContainer = new ScriptEngineContainer(properties, m_logger,
DCRContextImplementation.create(m_logger), m_initialisationMessage.getScript());
final WorkerIdentity workerIdentity = m_initialisationMessage.getWorkerIdentity();
final StringBuilder numbers = new StringBuilder("worker process ");
numbers.append(workerIdentity.getNumber());
final int agentNumber = workerIdentity.getAgentIdentity().getNumber();
if (agentNumber >= 0) {
numbers.append(" of agent number ");
numbers.append(agentNumber);
}
m_logger.info(numbers.toString());
final short numberOfThreads = properties.getShort("grinder.threads", (short) 1);
final int reportToConsoleInterval = properties.getInt("grinder.reportToConsole.interval", 500);
final int duration = properties.getInt("grinder.duration", 0);
final Instrumenter instrumenter = scriptEngineContainer.createInstrumenter();
m_testRegistryImplementation.setInstrumenter(instrumenter);
m_logger.info("Instrumentation agents: {}", instrumenter.getDescription());
// Force initialisation of the script engine before we start the
// message
// pump. Jython 2.5+ tests to see whether the stdin stream is a tty,
// and
// on some versions of Windows, this synchronises on the stream
// object's
// monitor. This clashes with the message pump which starts a thread
// to
// call StreamRecevier.waitForMessage(), and so also synchronises on
// that
// monitor. See bug 2936167.
final ScriptEngine scriptEngine = scriptEngineContainer
.getScriptEngine(m_initialisationMessage.getScript());
m_logger.info("Running \"{}\" using {}", m_initialisationMessage.getScript(),
scriptEngine.getDescription());
m_messagePump.start();
// Don't write out the data log header until now as the script may
// declare new statistics.
final StringBuilder dataLogHeader = new StringBuilder("Thread, Run, Test, Start time (ms since Epoch)");
final ExpressionView[] detailExpressionViews = m_statisticsServices.getDetailStatisticsView()
.getExpressionViews();
for (final ExpressionView detailExpressionView : detailExpressionViews) {
dataLogHeader.append(", ");
dataLogHeader.append(detailExpressionView.getDisplayName());
}
m_dataLogger.info(dataLogHeader.toString());
sendStatusMessage(ProcessReport.STATE_STARTED, (short) 0, numberOfThreads);
boolean threadRampUp = properties.getBoolean("grinder.threadRampUp", false);
final ThreadSynchronisation threadSynchronisation = threadRampUp ?
new ThreadRampUpEnabledThreadSynchronisation(m_eventSynchronisation, m_sleeper) :
new ThreadSynchronisation(m_eventSynchronisation);
m_terminalLogger.info("Starting threads");
synchronized (m_eventSynchronisation) {
m_threadStarter = new ThreadStarterImplementation(threadSynchronisation, scriptEngine);
for (int i = 0; i < numberOfThreads; i++) {
m_threadStarter.startThread(null);
}
}
threadSynchronisation.startThreads();
m_times.setExecutionStartTime();
m_logger.info("Start time is {} ms since Epoch", m_times.getExecutionStartTime());
final TimerTask reportTimerTask = new ReportToConsoleTimerTask(threadSynchronisation);
final TimerTask shutdownTimerTask = new ShutdownTimerTask();
// Schedule a regular statistics report to the console. We don't
// need to schedule this at a fixed rate. Each report contains the
// work done since the last report.
// First (empty) report to console to start it recording if its
// not already.
reportTimerTask.run();
final Timer timer = new Timer(true);
timer.schedule(reportTimerTask, reportToConsoleInterval, reportToConsoleInterval);
try {
if (duration > 0) {
m_terminalLogger.info("This test will shut down after {} ms", duration);
timer.schedule(shutdownTimerTask, duration);
}
// Wait for a termination event.
synchronized (m_eventSynchronisation) {
while (!threadSynchronisation.isFinished()) {
if (m_consoleListener.checkForMessage(ConsoleListener.ANY ^ ConsoleListener.START)) {
break;
}
if (m_shutdownTriggered) {
m_terminalLogger.info("Specified duration exceeded, Test is shut down");
break;
}
m_eventSynchronisation.waitNoInterrruptException();
}
}
synchronized (m_eventSynchronisation) {
if (!threadSynchronisation.isFinished()) {
m_terminalLogger.info("Waiting for threads to terminate");
m_threadStarter = m_invalidThreadStarter;
m_threadContexts.shutdownAll();
// Interrupt any sleepers.
SleeperImplementation.shutdownAllCurrentSleepers();
final long time = System.currentTimeMillis();
final long maximumShutdownTime = 10000;
while (!threadSynchronisation.isFinished()) {
if (System.currentTimeMillis() - time > maximumShutdownTime) {
m_terminalLogger.info("Ignoring unresponsive threads");
break;
}
m_eventSynchronisation.waitNoInterrruptException(maximumShutdownTime);
}
}
}
} finally {
reportTimerTask.cancel();
shutdownTimerTask.cancel();
}
scriptEngine.shutdown();
// Final report to the console.
reportTimerTask.run();
if (!m_communicationShutdown) {
sendStatusMessage(ProcessReport.STATE_FINISHED, (short) 0, (short) 0);
}
m_consoleSender.shutdown();
final long elapsedTime = m_times.getElapsedTime();
m_logger.info("elapsed time is {} ms", elapsedTime);
m_logger.info("Final statistics for this process:");
final StatisticsTable statisticsTable = new StatisticsTable(
m_statisticsServices.getSummaryStatisticsView(),
m_statisticsServices.getStatisticsIndexMap(), m_accumulatedStatistics);
final StringWriter statistics = new StringWriter();
statistics.write("\n");
statisticsTable.print(new PrintWriter(statistics), elapsedTime);
m_logger.info(statistics.toString());
timer.cancel();
m_terminalLogger.info("Finished");
} catch (final ScriptExecutionException e) {
m_logger.error("Aborting process - {}", e.getShortMessage(), e);
m_terminalLogger.error("aborting process - {}", e.getShortMessage(), e);
} catch (EngineException e) {
m_logger.error("Script error - {}", e.getMessage(), e);
throw e;
}
}
public void shutdown(final boolean inputStreamIsStdin) {
if (!inputStreamIsStdin) {
// Sadly it appears its impossible to interrupt a read() on a
// process
// input stream (at least under W2K), so we can't shut down the
// message
// pump cleanly. It runs in a daemon thread, so this isn't a big
// deal.
m_messagePump.shutdown();
}
// Logback doesn't stop its loggers on exit (see LBCORE-202). We do
// so explicitly to flush our BufferedEchoMessageEncoder.
if (m_logbackLoggerContext != null) {
m_logbackLoggerContext.stop();
}
}
private class ReportToConsoleTimerTask extends TimerTask {
private final ThreadSynchronisation m_threads;
public ReportToConsoleTimerTask(final ThreadSynchronisation threads) {
m_threads = threads;
}
@Override
public void run() {
if (!m_communicationShutdown) {
try {
final TestStatisticsMap sample = m_testRegistryImplementation.getTestStatisticsMap().reset();
m_accumulatedStatistics.add(sample);
// We look up the new tests after we've taken the sample to
// avoid a race condition when new tests are being added.
final Collection<Test> newTests = m_testRegistryImplementation.getNewTests();
if (newTests != null) {
m_consoleSender.send(new RegisterTestsMessage(newTests));
}
if (sample.size() > 0) {
if (!m_reportTimesToConsole) {
m_testStatisticsHelper.removeTestTimeFromSample(sample);
}
m_consoleSender.send(new ReportStatisticsMessage(sample));
}
sendStatusMessage(ProcessReport.STATE_RUNNING, m_threads.getNumberOfRunningThreads(),
m_threads.getTotalNumberOfThreads());
} catch (final CommunicationException e) {
m_terminalLogger.info("Report to console failed", e);
m_communicationShutdown = true;
}
}
}
}
private void sendStatusMessage(final short state, final short numberOfThreads, final short totalNumberOfThreads)
throws CommunicationException {
m_consoleSender.send(new WorkerProcessReportMessage(state, numberOfThreads, totalNumberOfThreads));
m_consoleSender.flush();
}
private class ShutdownTimerTask extends TimerTask {
@Override
public void run() {
synchronized (m_eventSynchronisation) {
m_shutdownTriggered = true;
m_eventSynchronisation.notifyAll();
}
}
}
/**
* Implement {@link net.grinder.engine.process.WorkerThreadSynchronisation}. I looked hard at JSR 166's
* <code>CountDownLatch</code> and <code>CyclicBarrier</code>, but neither of them allow for the
* waiting thread to be interrupted by other events.
* <p/>
* <p>
* Package scope for unit tests.
* </p>
*/
static class ThreadSynchronisation implements WorkerThreadSynchronisation {
final BooleanCondition m_started = new BooleanCondition();
final Condition m_threadEventCondition;
short m_numberCreated = 0;
short m_numberAwaitingStart = 0;
short m_numberFinished = 0;
short m_numberRunning = 0;
ThreadSynchronisation(final Condition condition) {
m_threadEventCondition = condition;
}
/**
* The number of worker threads that have been created but not run to completion.
*/
public short getNumberOfRunningThreads() {
synchronized (m_threadEventCondition) {
return (short) (m_numberCreated - m_numberFinished);
}
}
public boolean isReadyToStart() {
synchronized (m_threadEventCondition) {
return m_numberAwaitingStart >= getNumberOfRunningThreads();
}
}
public boolean isFinished() {
return getNumberOfRunningThreads() <= 0;
}
/**
* The number of worker threads that have been created.
*/
public short getTotalNumberOfThreads() {
synchronized (m_threadEventCondition) {
return m_numberCreated;
}
}
@Override
public void threadCreated() {
synchronized (m_threadEventCondition) {
++m_numberCreated;
}
}
public void startThreads() {
synchronized (m_threadEventCondition) {
while (!isReadyToStart()) {
m_threadEventCondition.waitNoInterrruptException();
}
m_numberAwaitingStart = 0;
}
m_started.set(true);
}
@Override
public void awaitStart() {
synchronized (m_threadEventCondition) {
++m_numberAwaitingStart;
if (isReadyToStart()) {
m_threadEventCondition.notifyAll();
}
}
m_started.await(true);
}
@Override
public void threadFinished() {
synchronized (m_threadEventCondition) {
++m_numberFinished;
if (isReadyToStart() || isFinished()) {
m_threadEventCondition.notifyAll();
}
}
}
}
static class ThreadRampUpEnabledThreadSynchronisation extends ThreadSynchronisation {
private final Sleeper sleeper;
ThreadRampUpEnabledThreadSynchronisation(Condition condition, Sleeper sleeper) {
super(condition);
this.sleeper = sleeper;
}
public void startThreads() {
synchronized (m_threadEventCondition) {
while (!isReadyToStart()) {
m_threadEventCondition.waitNoInterrruptException();
}
m_numberAwaitingStart = 0;
}
// m_started.set(true);
}
@Override
public void awaitStart() {
int waitingTime = doRampUp();
int threadNumber = 0;
if (Grinder.grinder != null) {
threadNumber = Math.max(Grinder.grinder.getThreadNumber(), 0);
}
synchronized (m_threadEventCondition) {
m_numberAwaitingStart++;
m_numberRunning++;
m_threadEventCondition.notifyAll();
}
if (Grinder.grinder != null) {
Grinder.grinder.getLogger().info("thread-{} is invoked after {} ms sleep", threadNumber,
waitingTime);
}
// m_started.await(true);
}
@Override
public short getNumberOfRunningThreads() {
synchronized (m_threadEventCondition) {
return m_numberRunning;
}
}
@Override
public boolean isReadyToStart() {
return true;
}
@Override
public void threadFinished() {
synchronized (m_threadEventCondition) {
++m_numberFinished;
if (isFinished()) {
m_threadEventCondition.notifyAll();
}
}
}
public boolean isFinished() {
return getNumberOfNotFinishedThreads() <= 0;
}
/**
* The number of worker threads that have been created but not run to completion.
*/
public short getNumberOfNotFinishedThreads() {
synchronized (m_threadEventCondition) {
return (short) (m_numberCreated - m_numberFinished);
}
}
public static final String GRINDER_PROP_THREAD_INCREMENT = "grinder.processIncrement";
public static final String GRINDER_PROP_THREAD_INCREMENT_INTERVAL = "grinder.processIncrementInterval";
public static final String GRINDER_PROP_INITIAL_PROCESS = "grinder.initialProcesses";
public static final String GRINDER_PROP_INITIAL_THREAD_SLEEP_TIME = "grinder.initialThreadSleepTime";
protected int doRampUp() {
InternalScriptContext grinder = Grinder.grinder;
if (grinder != null) {
GrinderProperties properties = grinder.getProperties();
int rampUpInterval = properties.getInt(GRINDER_PROP_THREAD_INCREMENT_INTERVAL, 0);
int rampUpStep = properties.getInt(GRINDER_PROP_THREAD_INCREMENT, 0);
int rampUpInitialThread = properties.getInt(GRINDER_PROP_INITIAL_PROCESS, 0);
int rampUpInitialSleep = properties.getInt(GRINDER_PROP_INITIAL_THREAD_SLEEP_TIME, 0);
return doRampUp(rampUpInterval, rampUpStep, rampUpInitialThread, rampUpInitialSleep);
}
return 0;
}
private int doRampUp(int rampUpInterval, int rampUpStep, int rampUpInitialThread, int rampUpInitialSleep) {
int threadNumber = 0;
int waitingTime;
if (Grinder.grinder != null) {
threadNumber = Math.max(Grinder.grinder.getThreadNumber(), 0);
}
try {
waitingTime = getWaitingTime(rampUpInterval, rampUpStep,
rampUpInitialThread, rampUpInitialSleep,
threadNumber);
if (waitingTime != 0) {
if (Grinder.grinder != null) {
Grinder.grinder.getLogger().info("thread-{} is sleeping {} ms for ramp-up", threadNumber,
waitingTime);
}
sleeper.sleepNormal(waitingTime, 0);
}
return waitingTime;
} catch (Sleeper.ShutdownException e) {
throw new RuntimeException(e);
}
}
public int getWaitingTime(int rampUpInterval, int rampUpStep,
int rampUpInitialThread, int rampUpInitialSleep, int threadNumber) {
// 100 2 1 0 3 ==> 100
if (threadNumber < rampUpInitialThread) {
return 0;
}
int remained = (threadNumber - rampUpInitialThread);
int threadStep = (remained / rampUpStep) + 1;
return Math.max(rampUpInitialSleep + (threadStep * rampUpInterval), 0);
}
}
private final class ThreadStarterImplementation implements ThreadStarter {
private final ThreadSynchronisation m_threadSynchronisation;
private final ScriptEngine m_scriptEngine;
private final WorkerRunnableFactory m_defaultWorkerRunnableFactory;
private final ProcessLifeCycleListener m_threadLifeCycleCallbacks = new ProcessLifeCycleListener() {
@Override
public void threadCreated(final ThreadContext threadContext) {
m_processLifeCycleListeners.apply(new Informer<ProcessLifeCycleListener>() {
@Override
public void inform(final ProcessLifeCycleListener listener) {
listener.threadCreated(threadContext);
}
});
}
@Override
public void threadStarted(final ThreadContext threadContext) {
m_processLifeCycleListeners.apply(new Informer<ProcessLifeCycleListener>() {
@Override
public void inform(final ProcessLifeCycleListener listener) {
listener.threadStarted(threadContext);
}
});
}
};
private int m_i = -1;
private ThreadStarterImplementation(final ThreadSynchronisation threadSynchronisation,
final ScriptEngine scriptEngine) {
m_threadSynchronisation = threadSynchronisation;
m_scriptEngine = scriptEngine;
m_defaultWorkerRunnableFactory = new WorkerRunnableFactory() {
@Override
public WorkerRunnable create() throws EngineException {
return m_scriptEngine.createWorkerRunnable();
}
};
}
@Override
public int startThread(final Object testRunner) throws EngineException {
final int threadNumber;
synchronized (this) {
threadNumber = ++m_i;
}
final ThreadContext threadContext = new ThreadContextImplementation(
m_initialisationMessage.getProperties(), m_statisticsServices, threadNumber, m_dataLogger);
final WorkerRunnableFactory workerRunnableFactory;
if (testRunner != null) {
workerRunnableFactory = new WorkerRunnableFactory() {
@Override
public WorkerRunnable create() throws EngineException {
return m_scriptEngine.createWorkerRunnable(testRunner);
}
};
} else {
workerRunnableFactory = m_defaultWorkerRunnableFactory;
}
final GrinderThread runnable = new GrinderThread(m_logger, threadContext, m_threadSynchronisation,
m_threadLifeCycleCallbacks, m_initialisationMessage.getProperties(), m_sleeper,
workerRunnableFactory);
final Thread t = new Thread(runnable, "thread " + threadNumber);
t.setDaemon(true);
t.start();
return threadNumber;
}
}
/**
* Package scope for unit tests.
*/
static final class InvalidThreadStarter implements ThreadStarter {
@Override
public int startThread(final Object testRunner) throws InvalidContextException {
throw new InvalidContextException("You should not start worker threads until the main thread has "
+ "initialised the script engine, or after all other threads have "
+ "shut down. Typically, you should only call startWorkerThread() "
+ "from another worker thread.");
}
}
/**
* Package scope for unit tests.
*/
static final class Times {
private volatile long m_executionStartTime;
private final TimeAuthority m_timeAuthority = new StandardTimeAuthority();
/**
* {@link net.grinder.engine.process.GrinderProcess} calls {@link #setExecutionStartTime} just before launching
* threads, after which it is never called again.
*/
public void setExecutionStartTime() {
m_executionStartTime = m_timeAuthority.getTimeInMilliseconds();
}
/**
* {@link net.grinder.engine.process.GrinderProcess} calls {@link #setExecutionStartTime} just before launching
* threads, after which it is never called again.
*
* @return Start of execution, in milliseconds since the Epoch.
*/
public long getExecutionStartTime() {
return m_executionStartTime;
}
/**
* Elapsed time since execution was started.
*
* @return The time in milliseconds.
* @see #getExecutionStartTime()
*/
public long getElapsedTime() {
return m_timeAuthority.getTimeInMilliseconds() - getExecutionStartTime();
}
public TimeAuthority getTimeAuthority() {
return m_timeAuthority;
}
}
/**
* Package scope for unit tests.
*/
static final class ThreadContexts implements ProcessLifeCycleListener, ThreadContextLocator {
private final ThreadLocal<ThreadContext> m_threadContextThreadLocal = new ThreadLocal<ThreadContext>();
// Guarded by self.
private final Map<Integer, ThreadContext> m_threadContextsMap = new HashMap<Integer, ThreadContext>();
// Guarded by m_threadContextsMap.
private boolean m_allShutdown;
@Override
public ThreadContext get() {
return m_threadContextThreadLocal.get();
}
@Override
public void threadCreated(final ThreadContext threadContext) {
final Integer threadNumber = threadContext.getThreadNumber();
final boolean shutdown;
synchronized (m_threadContextsMap) {
shutdown = m_allShutdown;
if (!shutdown) {
threadContext.registerThreadLifeCycleListener(new SkeletonThreadLifeCycleListener() {
@Override
public void endThread() {
m_threadContextsMap.remove(threadNumber);
}
});
// Very unlikely, harmless race here - we could store a
// reference to
// a thread context that is in the process of shutting down.
m_threadContextsMap.put(threadNumber, threadContext);
}
}
if (shutdown) {
// Stop new threads in their tracks.
threadContext.shutdown();
}
}
@Override
public void threadStarted(final ThreadContext threadContext) {
m_threadContextThreadLocal.set(threadContext);
}
public boolean shutdown(final int threadNumber) {
final ThreadContext threadContext;
synchronized (m_threadContextsMap) {
threadContext = m_threadContextsMap.get(threadNumber);
}
if (threadContext != null) {
threadContext.shutdown();
return true;
}
return false;
}
public void shutdownAll() {
final ThreadContext[] threadContexts;
synchronized (m_threadContextsMap) {
m_allShutdown = true;
threadContexts = m_threadContextsMap.values().toArray(new ThreadContext[m_threadContextsMap.size()]);
}
for (final ThreadContext threadContext : threadContexts) {
threadContext.shutdown();
}
}
}
/**
* Package scope for unit tests.
*/
static final class NullQueuedSender implements QueuedSender {
@Override
public void send(final Message message) {
}
@Override
public void flush() {
}
@Override
public void shutdown() {
}
}
}