/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.jmeter.threads;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.apache.jmeter.engine.StandardJMeterEngine;
import org.apache.jmeter.engine.TreeCloner;
import org.apache.jmeter.testelement.property.BooleanProperty;
import org.apache.jmeter.testelement.property.IntegerProperty;
import org.apache.jmeter.testelement.property.LongProperty;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.collections.ListedHashTree;
import org.apache.jorphan.util.JMeterStopTestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ThreadGroup holds the settings for a JMeter thread group.
*
* This class is intended to be ThreadSafe.
*/
public class ThreadGroup extends AbstractThreadGroup {
private static final long serialVersionUID = 282L;
private static final Logger log = LoggerFactory.getLogger(ThreadGroup.class);
private static final String DATE_FIELD_FORMAT = "yyyy/MM/dd HH:mm:ss"; //$NON-NLS-1$
private static final long WAIT_TO_DIE = JMeterUtils.getPropDefault("jmeterengine.threadstop.wait", 5 * 1000); // 5 seconds
/** How often to check for shutdown during ramp-up, default 1000ms */
private static final int RAMPUP_GRANULARITY =
JMeterUtils.getPropDefault("jmeterthread.rampup.granularity", 1000); // $NON-NLS-1$
//+ JMX entries - do not change the string values
/** Ramp-up time */
public static final String RAMP_TIME = "ThreadGroup.ramp_time";
/** Whether thread startup is delayed until required */
public static final String DELAYED_START = "ThreadGroup.delayedStart";
/** Whether scheduler is being used */
public static final String SCHEDULER = "ThreadGroup.scheduler";
/** Scheduler absolute start time */
public static final String START_TIME = "ThreadGroup.start_time";
/** Scheduler absolute end time */
public static final String END_TIME = "ThreadGroup.end_time";
/** Scheduler duration, overrides end time */
public static final String DURATION = "ThreadGroup.duration";
/** Scheduler start delay, overrides start time */
public static final String DELAY = "ThreadGroup.delay";
//- JMX entries
private transient Thread threadStarter;
// List of active threads
private final Map<JMeterThread, Thread> allThreads = new ConcurrentHashMap<>();
private transient Object addThreadLock = new Object();
/**
* Is test (still) running?
*/
private volatile boolean running = false;
/**
* Thread Group number
*/
private int groupNumber;
/**
* Are we using delayed startup?
*/
private boolean delayedStartup;
/**
* Thread safe class
*/
private ListenerNotifier notifier;
/**
* This property will be cloned
*/
private ListedHashTree threadGroupTree;
/**
* No-arg constructor.
*/
public ThreadGroup() {
super();
}
/**
* Set whether scheduler is being used
*
* @param scheduler true is scheduler is to be used
*/
public void setScheduler(boolean scheduler) {
setProperty(new BooleanProperty(SCHEDULER, scheduler));
}
/**
* Get whether scheduler is being used
*
* @return true if scheduler is being used
*/
public boolean getScheduler() {
return getPropertyAsBoolean(SCHEDULER);
}
/**
* Set the absolute StartTime value.
*
* @param stime -
* the StartTime value.
*/
public void setStartTime(long stime) {
setProperty(new LongProperty(START_TIME, stime));
}
/**
* Get the absolute start time value.
*
* @return the start time value.
*/
public long getStartTime() {
return getPropertyAsLong(START_TIME);
}
/**
* Get the desired duration of the thread group test run
*
* @return the duration (in secs)
*/
public long getDuration() {
return getPropertyAsLong(DURATION);
}
/**
* Set the desired duration of the thread group test run
*
* @param duration
* in seconds
*/
public void setDuration(long duration) {
setProperty(new LongProperty(DURATION, duration));
}
/**
* Get the startup delay
*
* @return the delay (in secs)
*/
public long getDelay() {
return getPropertyAsLong(DELAY);
}
/**
* Set the startup delay
*
* @param delay
* in seconds
*/
public void setDelay(long delay) {
setProperty(new LongProperty(DELAY, delay));
}
/**
* Set the EndTime value.
*
* @param etime -
* the EndTime value.
*/
public void setEndTime(long etime) {
setProperty(new LongProperty(END_TIME, etime));
}
/**
* Get the end time value.
*
* @return the end time value.
*/
public long getEndTime() {
return getPropertyAsLong(END_TIME);
}
/**
* Set the ramp-up value.
*
* @param rampUp
* the ramp-up value.
*/
public void setRampUp(int rampUp) {
setProperty(new IntegerProperty(RAMP_TIME, rampUp));
}
/**
* Get the ramp-up value.
*
* @return the ramp-up value.
*/
public int getRampUp() {
return getPropertyAsInt(ThreadGroup.RAMP_TIME);
}
private boolean isDelayedStartup() {
return getPropertyAsBoolean(DELAYED_START);
}
/**
* This will schedule the time for the JMeterThread.
*
* @param thread JMeterThread
* @param now in milliseconds
*/
private void scheduleThread(JMeterThread thread, long now) {
// if true the Scheduler is enabled
if (getScheduler()) {
// set the start time for the Thread
if (getDelay() > 0) {// Duration is in seconds
thread.setStartTime(getDelay() * 1000 + now);
} else {
long start = getStartTime();
if (start < now) {
start = now; // Force a sensible start time
}
thread.setStartTime(start);
}
// set the endtime for the Thread
if (getDuration() > 0) {// Duration is in seconds
thread.setEndTime(getDuration() * 1000 + (thread.getStartTime()));
} else {
if( getEndTime() <= now ) {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FIELD_FORMAT);
throw new JMeterStopTestException("End Time ("
+ sdf.format(new Date(getEndTime()))+") of Scheduler for Thread Group "+getName()
+ " is in the past, fix value of End Time field");
}
thread.setEndTime(getEndTime());
}
// Enables the scheduler
thread.setScheduled(true);
}
}
@Override
public void start(int groupNum, ListenerNotifier notifier, ListedHashTree threadGroupTree, StandardJMeterEngine engine) {
this.running = true;
this.groupNumber = groupNum;
this.notifier = notifier;
this.threadGroupTree = threadGroupTree;
int numThreads = getNumThreads();
int rampUpPeriodInSeconds = getRampUp();
float perThreadDelayInMillis = (float) (rampUpPeriodInSeconds * 1000) / (float) getNumThreads();
delayedStartup = isDelayedStartup(); // Fetch once; needs to stay constant
log.info("Starting thread group... number={} threads={} ramp-up={} perThread={} delayedStart={}", groupNumber,
numThreads, rampUpPeriodInSeconds, perThreadDelayInMillis, delayedStartup);
if (delayedStartup) {
threadStarter = new Thread(new ThreadStarter(notifier, threadGroupTree, engine), getName()+"-ThreadStarter");
threadStarter.setDaemon(true);
threadStarter.start();
// N.B. we don't wait for the thread to complete, as that would prevent parallel TGs
} else {
long now = System.currentTimeMillis(); // needs to be same time for all threads in the group
final JMeterContext context = JMeterContextService.getContext();
for (int threadNum = 0; running && threadNum < numThreads; threadNum++) {
startNewThread(notifier, threadGroupTree, engine, threadNum, context, now, (int)(threadNum * perThreadDelayInMillis));
}
}
log.info("Started thread group number {}", groupNumber);
}
/**
* Start a new {@link JMeterThread} and registers it
* @param notifier {@link ListenerNotifier}
* @param threadGroupTree {@link ListedHashTree}
* @param engine {@link StandardJMeterEngine}
* @param threadNum Thread number
* @param context {@link JMeterContext}
* @param now Nom in milliseconds
* @param delay int delay in milliseconds
* @return {@link JMeterThread} newly created
*/
private JMeterThread startNewThread(ListenerNotifier notifier, ListedHashTree threadGroupTree, StandardJMeterEngine engine,
int threadNum, final JMeterContext context, long now, int delay) {
JMeterThread jmThread = makeThread(notifier, threadGroupTree, engine, threadNum, context);
scheduleThread(jmThread, now); // set start and end time
jmThread.setInitialDelay(delay);
Thread newThread = new Thread(jmThread, jmThread.getThreadName());
registerStartedThread(jmThread, newThread);
newThread.start();
return jmThread;
}
/*
* Fix NPE for addThreadLock transient object in remote mode (BZ60829)
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
addThreadLock = new Object();
}
/**
* Register Thread when it starts
* @param jMeterThread {@link JMeterThread}
* @param newThread Thread
*/
private void registerStartedThread(JMeterThread jMeterThread, Thread newThread) {
allThreads.put(jMeterThread, newThread);
}
/**
* Create {@link JMeterThread} cloning threadGroupTree
* @param notifier {@link ListenerNotifier}
* @param threadGroupTree {@link ListedHashTree}
* @param engine {@link StandardJMeterEngine}
* @param threadNumber int thread number
* @param context {@link JMeterContext}
* @return {@link JMeterThread}
*/
private JMeterThread makeThread(
ListenerNotifier notifier, ListedHashTree threadGroupTree,
StandardJMeterEngine engine, int threadNumber,
JMeterContext context) { // N.B. Context needs to be fetched in the correct thread
boolean onErrorStopTest = getOnErrorStopTest();
boolean onErrorStopTestNow = getOnErrorStopTestNow();
boolean onErrorStopThread = getOnErrorStopThread();
boolean onErrorStartNextLoop = getOnErrorStartNextLoop();
String groupName = getName();
final JMeterThread jmeterThread = new JMeterThread(cloneTree(threadGroupTree), this, notifier);
jmeterThread.setThreadNum(threadNumber);
jmeterThread.setThreadGroup(this);
jmeterThread.setInitialContext(context);
final String threadName = groupName + " " + groupNumber + "-" + (threadNumber + 1);
jmeterThread.setThreadName(threadName);
jmeterThread.setEngine(engine);
jmeterThread.setOnErrorStopTest(onErrorStopTest);
jmeterThread.setOnErrorStopTestNow(onErrorStopTestNow);
jmeterThread.setOnErrorStopThread(onErrorStopThread);
jmeterThread.setOnErrorStartNextLoop(onErrorStartNextLoop);
return jmeterThread;
}
@Override
public JMeterThread addNewThread(int delay, StandardJMeterEngine engine) {
long now = System.currentTimeMillis();
JMeterContext context = JMeterContextService.getContext();
JMeterThread newJmThread;
int numThreads;
synchronized (addThreadLock) {
numThreads = getNumThreads();
setNumThreads(numThreads + 1);
}
newJmThread = startNewThread(notifier, threadGroupTree, engine, numThreads, context, now, delay);
JMeterContextService.addTotalThreads( 1 );
log.info("Started new thread in group {}", groupNumber);
return newJmThread;
}
/**
* Stop thread called threadName:
* <ol>
* <li>stop JMeter thread</li>
* <li>interrupt JMeter thread</li>
* <li>interrupt underlying thread</li>
* </ol>
* @param threadName String thread name
* @param now boolean for stop
* @return true if thread stopped
*/
@Override
public boolean stopThread(String threadName, boolean now) {
for(Entry<JMeterThread, Thread> entry : allThreads.entrySet()) {
JMeterThread thrd = entry.getKey();
if (thrd.getThreadName().equals(threadName)) {
stopThread(thrd, entry.getValue(), now);
return true;
}
}
return false;
}
/**
* Hard Stop JMeterThread thrd and interrupt JVM Thread if interrupt is true
* @param jmeterThread {@link JMeterThread}
* @param jvmThread {@link Thread}
* @param interrupt Interrupt thread or not
*/
private void stopThread(JMeterThread jmeterThread, Thread jvmThread, boolean interrupt) {
jmeterThread.stop();
jmeterThread.interrupt(); // interrupt sampler if possible
if (interrupt && jvmThread != null) { // Bug 49734
jvmThread.interrupt(); // also interrupt JVM thread
}
}
/**
* Called by JMeterThread when it finishes
*/
@Override
public void threadFinished(JMeterThread thread) {
if (log.isDebugEnabled()) {
log.debug("Ending thread {}", thread.getThreadName());
}
allThreads.remove(thread);
}
/**
* For each thread, invoke:
* <ul>
* <li>{@link JMeterThread#stop()} - set stop flag</li>
* <li>{@link JMeterThread#interrupt()} - interrupt sampler</li>
* <li>{@link Thread#interrupt()} - interrupt JVM thread</li>
* </ul>
*/
@Override
public void tellThreadsToStop() {
running = false;
if (delayedStartup) {
try {
threadStarter.interrupt();
} catch (Exception e) {
log.warn("Exception occurred interrupting ThreadStarter", e);
}
}
for (Entry<JMeterThread, Thread> entry : allThreads.entrySet()) {
stopThread(entry.getKey(), entry.getValue(), true);
}
}
/**
* For each thread, invoke:
* <ul>
* <li>{@link JMeterThread#stop()} - set stop flag</li>
* </ul>
*/
@Override
public void stop() {
running = false;
if (delayedStartup) {
try {
threadStarter.interrupt();
} catch (Exception e) {
log.warn("Exception occurred interrupting ThreadStarter", e);
}
}
for (JMeterThread item : allThreads.keySet()) {
item.stop();
}
}
/**
* @return number of active threads
*/
@Override
public int numberOfActiveThreads() {
return allThreads.size();
}
/**
* @return boolean true if all threads stopped
*/
@Override
public boolean verifyThreadsStopped() {
boolean stoppedAll = true;
if (delayedStartup) {
stoppedAll = verifyThreadStopped(threadStarter);
}
for (Thread t : allThreads.values()) {
stoppedAll = stoppedAll && verifyThreadStopped(t);
}
return stoppedAll;
}
/**
* Verify thread stopped and return true if stopped successfully
* @param thread Thread
* @return boolean
*/
private boolean verifyThreadStopped(Thread thread) {
boolean stopped = true;
if (thread != null && thread.isAlive()) {
try {
thread.join(WAIT_TO_DIE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (thread.isAlive()) {
stopped = false;
if (log.isWarnEnabled()) {
log.warn("Thread won't exit: {}", thread.getName());
}
}
}
return stopped;
}
/**
* Wait for all Group Threads to stop
*/
@Override
public void waitThreadsStopped() {
if (delayedStartup) {
waitThreadStopped(threadStarter);
}
/* @Bugzilla 60933
* Like threads can be added on the fly during a test into allThreads
* we have to check if allThreads is rly empty before stop
*/
while ( !allThreads.isEmpty() ) {
for (Thread t : allThreads.values()) {
waitThreadStopped(t);
}
}
}
/**
* Wait for thread to stop
* @param thread Thread
*/
private void waitThreadStopped(Thread thread) {
if (thread != null) {
while (thread.isAlive()) {
try {
thread.join(WAIT_TO_DIE);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
/**
* @param tree {@link ListedHashTree}
* @return a clone of tree
*/
private ListedHashTree cloneTree(ListedHashTree tree) {
TreeCloner cloner = new TreeCloner(true);
tree.traverse(cloner);
return cloner.getClonedTree();
}
/**
* Pause ms milliseconds
* @param ms long milliseconds
*/
private void pause(long ms){
try {
TimeUnit.MILLISECONDS.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Starts Threads using ramp up
*/
class ThreadStarter implements Runnable {
private final ListenerNotifier notifier;
private final ListedHashTree threadGroupTree;
private final StandardJMeterEngine engine;
private final JMeterContext context;
public ThreadStarter(ListenerNotifier notifier, ListedHashTree threadGroupTree, StandardJMeterEngine engine) {
super();
this.notifier = notifier;
this.threadGroupTree = threadGroupTree;
this.engine = engine;
// Store context from Root Thread to pass it to created threads
this.context = JMeterContextService.getContext();
}
/**
* Wait for delay with RAMPUP_GRANULARITY
* @param delay delay in ms
*/
private void delayBy(long delay) {
if (delay > 0) {
long start = System.currentTimeMillis();
long end = start + delay;
long now;
long pause = RAMPUP_GRANULARITY; // maximum pause to use
while(running && (now = System.currentTimeMillis()) < end) {
long togo = end - now;
if (togo < pause) {
pause = togo;
}
pause(pause); // delay between checks
}
}
}
@Override
public void run() {
try {
// Copy in ThreadStarter thread context from calling Thread
JMeterContextService.getContext().setVariables(this.context.getVariables());
long now = System.currentTimeMillis(); // needs to be constant for all threads
long endtime = 0;
final boolean usingScheduler = getScheduler();
if (usingScheduler) {
// set the start time for the Thread
if (getDelay() > 0) {// Duration is in seconds
delayBy(getDelay() * 1000);
} else {
long start = getStartTime();
if (start >= now) {
delayBy(start-now);
}
// else start immediately
}
// set the endtime for the Thread
endtime = getDuration();
if (endtime > 0) {// Duration is in seconds, starting from when the threads start
endtime = endtime *1000 + System.currentTimeMillis();
} else {
if( getEndTime() <= now ) {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FIELD_FORMAT);
throw new JMeterStopTestException("End Time ("
+ sdf.format(new Date(getEndTime()))+") of Scheduler for Thread Group "+getName()
+ " is in the past, fix value of End Time field");
}
endtime = getEndTime();
}
}
final int numThreads = getNumThreads();
final int perThreadDelayInMillis = Math.round((float) (getRampUp() * 1000) / (float) numThreads);
for (int threadNumber = 0; running && threadNumber < numThreads; threadNumber++) {
if (threadNumber > 0) {
pause(perThreadDelayInMillis); // ramp-up delay (except first)
}
if (usingScheduler && System.currentTimeMillis() > endtime) {
break; // no point continuing beyond the end time
}
JMeterThread jmThread = makeThread(notifier, threadGroupTree, engine, threadNumber, context);
jmThread.setInitialDelay(0); // Already waited
if (usingScheduler) {
jmThread.setScheduled(true);
jmThread.setEndTime(endtime);
}
Thread newThread = new Thread(jmThread, jmThread.getThreadName());
newThread.setDaemon(false); // ThreadStarter is daemon, but we don't want sampler threads to be so too
registerStartedThread(jmThread, newThread);
newThread.start();
}
} catch (Exception ex) {
log.error("An error occurred scheduling delay start of threads for Thread Group: {}", getName(), ex);
}
}
}
}