+{
+ /**
+ * The value.
+ */
+ private T value;
+
+ /**
+ * Creates a new Holder instance and initializes it with a null value.
+ */
+ public Holder()
+ {
+ this(null);
+ }
+
+ /**
+ * Creates a new Holder instance and initializes it with the given value.
+ *
+ * @param value
+ * the initial value of the holder
+ */
+ public Holder(final T value)
+ {
+ this.value = value;
+ }
+
+ /**
+ * Returns the current value.
+ *
+ * @return the value
+ */
+ public T get()
+ {
+ return value;
+ }
+
+ /**
+ * Sets the new value.
+ *
+ * @param value
+ * the new value
+ */
+ public void set(T value)
+ {
+ this.value = value;
+ }
+
+ /**
+ * Removes the current value.
+ *
+ * @return the value just removed
+ */
+ public T remove()
+ {
+ final T oldValue = value;
+ value = null;
+
+ return oldValue;
+ }
+}
diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTest.java b/src/main/java/com/xceptance/xlt/agent/LoadTest.java
index 3669c82f8..a1924ed96 100644
--- a/src/main/java/com/xceptance/xlt/agent/LoadTest.java
+++ b/src/main/java/com/xceptance/xlt/agent/LoadTest.java
@@ -24,6 +24,7 @@
import com.xceptance.xlt.agentcontroller.TestUserConfiguration;
import com.xceptance.xlt.api.util.XltProperties;
import com.xceptance.xlt.common.XltConstants;
+import com.xceptance.xlt.engine.XltThreadFactory;
/**
* Class responsible for running a load test.
@@ -37,6 +38,12 @@ public class LoadTest
*/
private static final long DEFAULT_GRACE_PERIOD = 30 * 1000;
+ /**
+ * The thread factory that creates either virtual threads or platform threads for the {@link LoadTestRunner}
+ * instances.
+ */
+ private static final XltThreadFactory xltThreadFactory;
+
/**
* The configured time period [ms] to wait for threads to finish voluntarily before quitting the JVM.
*/
@@ -44,10 +51,15 @@ public class LoadTest
static
{
- final long v = XltProperties.getInstance().getProperty(XltConstants.XLT_PACKAGE_PATH + ".hangingUsersGracePeriod",
- DEFAULT_GRACE_PERIOD);
+ final XltProperties props = XltProperties.getInstance();
+ // grace period
+ final long v = props.getProperty(XltConstants.XLT_PACKAGE_PATH + ".hangingUsersGracePeriod", DEFAULT_GRACE_PERIOD);
gracePeriod = (v < 0) ? DEFAULT_GRACE_PERIOD : v;
+
+ // thread factory
+ final boolean useVirtualThreads = props.getProperty(XltConstants.PROP_VIRTUAL_THREADS_ENABLED, false);
+ xltThreadFactory = new XltThreadFactory(useVirtualThreads, false);
}
/**
@@ -85,8 +97,7 @@ public void run()
final AbstractExecutionTimer timer = ExecutionTimerFactory.createTimer(config);
// create runner for configuration
- final LoadTestRunner runner = new LoadTestRunner(config, agentInfo, timer);
- runner.setDaemon(true);
+ final LoadTestRunner runner = new LoadTestRunner(config, agentInfo, timer, xltThreadFactory);
// add runner to list of known runners
testRunners.add(runner);
diff --git a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java
index edbe507bd..07c95e609 100644
--- a/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java
+++ b/src/main/java/com/xceptance/xlt/agent/LoadTestRunner.java
@@ -30,6 +30,7 @@
import com.xceptance.xlt.api.engine.GlobalClock;
import com.xceptance.xlt.engine.DataManagerImpl;
import com.xceptance.xlt.engine.SessionImpl;
+import com.xceptance.xlt.engine.XltThreadFactory;
import com.xceptance.xlt.engine.util.TimerUtils;
/**
@@ -40,7 +41,7 @@
*
* @author Jörg Werner (Xceptance Software Technologies GmbH)
*/
-public class LoadTestRunner extends Thread
+public class LoadTestRunner
{
/**
* Class logger instance.
@@ -72,6 +73,11 @@ public class LoadTestRunner extends Thread
*/
private volatile boolean aborted;
+ /**
+ * The main thread of this runner.
+ */
+ private final Thread thread;
+
/**
* Creates a new LoadTestRunner object for the given load test configuration. Typically, multiple runners are
* started for one test case configuration, so the number of the current runner is passed as well.
@@ -82,19 +88,49 @@ public class LoadTestRunner extends Thread
* load test agent information
* @param timer
* the execution timer that controls this load test runner
+ * @param xltThreadFactory
+ * the thread factory to create the worker thread
*/
- public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentInfo, final AbstractExecutionTimer timer)
+ public LoadTestRunner(final TestUserConfiguration config, final AgentInfo agentInfo, final AbstractExecutionTimer timer,
+ final XltThreadFactory xltThreadFactory)
{
- // create a new thread group for each LoadTestRunner as a means
- // to keep the main thread and any supporting threads together
- super(new ThreadGroup(config.getUserId()), config.getUserId());
-
this.config = config;
this.agentInfo = agentInfo;
this.timer = timer;
status = new TestUserStatus();
status.setUserName(config.getUserId());
+
+ thread = xltThreadFactory.newThread(this::run);
+ thread.setName(config.getUserId());
+ }
+
+ /**
+ * Returns the thread this runner is using under the hood.
+ *
+ * Note: For unit-testing only.
+ *
+ * @return the thread
+ */
+ Thread getThread()
+ {
+ return thread;
+ }
+
+ /**
+ * Starts this runner.
+ */
+ public void start()
+ {
+ thread.start();
+ }
+
+ /**
+ * Waits until this runner is finished.
+ */
+ public void join() throws InterruptedException
+ {
+ thread.join();
}
/**
@@ -110,8 +146,7 @@ public TestUserStatus getTestUserStatus()
/**
* Runs the test case as configured in the test case configuration.
*/
- @Override
- public void run()
+ private void run()
{
try
{
@@ -196,7 +231,7 @@ public void run()
}
catch (final Exception ex)
{
- log.error("Failed to run test as user: " + getName(), ex);
+ log.error("Failed to run test as user: " + thread.getName(), ex);
status.setState(TestUserStatus.State.Failed);
status.setException(ex);
diff --git a/src/main/java/com/xceptance/xlt/common/XltConstants.java b/src/main/java/com/xceptance/xlt/common/XltConstants.java
index 81c237e07..4c31de3d3 100644
--- a/src/main/java/com/xceptance/xlt/common/XltConstants.java
+++ b/src/main/java/com/xceptance/xlt/common/XltConstants.java
@@ -509,4 +509,10 @@ private XltConstants()
* The name of the HTML output file for rendering the scorecard report page.
*/
public static final String SCORECARD_REPORT_HTML_FILENAME = "scorecard.html";
+
+ /*
+ * Virtual threads
+ */
+
+ public static final String PROP_VIRTUAL_THREADS_ENABLED = XLT_PACKAGE_PATH + ".virtualThreads.enabled";
}
diff --git a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java
index c8b5a73e3..bc2b23a71 100644
--- a/src/main/java/com/xceptance/xlt/engine/RequestQueue.java
+++ b/src/main/java/com/xceptance/xlt/engine/RequestQueue.java
@@ -22,7 +22,6 @@
import java.util.concurrent.ThreadFactory;
import com.xceptance.common.util.SynchronizingCounter;
-import com.xceptance.common.util.concurrent.DaemonThreadFactory;
import com.xceptance.xlt.api.engine.Session;
import com.xceptance.xlt.api.util.XltLogger;
@@ -74,14 +73,16 @@ public class RequestQueue
* the web client to use
* @param threadCount
* the number of threads
+ * @param useVirtualThreads
+ * whether to use virtual threads instead of platform threads
*/
- public RequestQueue(final XltWebClient webClient, final int threadCount)
+ public RequestQueue(final XltWebClient webClient, final int threadCount, final boolean useVirtualThreads)
{
this.webClient = webClient;
this.threadCount = threadCount;
parallelModeEnabled = true;
- final ThreadFactory threadFactory = new DaemonThreadFactory(i -> Session.getCurrent().getUserID() + "-pool-" + i);
+ final ThreadFactory threadFactory = new XltThreadFactory(useVirtualThreads, true, Session.getCurrent().getUserID() + "-pool-");
executorService = Executors.newFixedThreadPool(threadCount, threadFactory);
ongoingRequestsCount = new SynchronizingCounter(0);
diff --git a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java
index 545da40a0..33708b55d 100644
--- a/src/main/java/com/xceptance/xlt/engine/SessionImpl.java
+++ b/src/main/java/com/xceptance/xlt/engine/SessionImpl.java
@@ -31,7 +31,9 @@
import com.xceptance.common.io.FileUtils;
import com.xceptance.common.lang.ParseNumbers;
+import com.xceptance.common.util.Holder;
import com.xceptance.common.util.ParameterCheckUtils;
+import com.xceptance.xlt.agent.AbstractExecutionTimer;
import com.xceptance.xlt.api.actions.AbstractAction;
import com.xceptance.xlt.api.engine.GlobalClock;
import com.xceptance.xlt.api.engine.NetworkDataManager;
@@ -83,13 +85,22 @@ public class SessionImpl extends Session
private static final String UNKNOWN_USER_NAME = "UnknownUser";
/**
- * The Session instances keyed by thread group.
+ * All Session instances keyed by thread. Needed by {@link AbstractExecutionTimer} only.
*/
- private static final Map sessions = new ConcurrentHashMap(101);
+ private static final Map sessions = new ConcurrentHashMap<>(101);
/**
- * Name of the removeUserInfoFromURL property.
+ * The Session instance of the current thread. We use a Holder in-between so the main thread and sub-threads can
+ * share the same session and also remove the session if needed.
*/
+ private static final InheritableThreadLocal> sessionHolder = new InheritableThreadLocal<>()
+ {
+ @Override
+ protected Holder initialValue()
+ {
+ return new Holder<>();
+ }
+ };
/**
* Returns the Session instance for the calling thread. If no such instance exists yet, it will be created.
@@ -98,7 +109,25 @@ public class SessionImpl extends Session
*/
public static SessionImpl getCurrent()
{
- return getSessionForThread(Thread.currentThread());
+ final Holder holder = sessionHolder.get();
+
+ SessionImpl sessionImpl = holder.get();
+ if (sessionImpl == null)
+ {
+ synchronized (holder)
+ {
+ if (sessionImpl == null)
+ {
+ sessionImpl = new SessionImpl(XltPropertiesImpl.getInstance());
+ holder.set(sessionImpl);
+ }
+ }
+ }
+
+ // TODO: would be cool to get rid of that
+ sessions.putIfAbsent(Thread.currentThread(), sessionImpl);
+
+ return sessionImpl;
}
/**
@@ -109,44 +138,20 @@ public static SessionImpl getCurrent()
*/
public static SessionImpl removeCurrent()
{
- return sessions.remove(Thread.currentThread().getThreadGroup());
+ // TODO: remove for all threads sharing the same session
+ sessions.remove(Thread.currentThread());
+
+ return sessionHolder.get().remove();
}
/**
- * Returns the Session instance for the given thread. If no such instance exists yet, it will be created.
+ * Returns the Session instance for the given thread.
*
* @return the Session instance for the given thread
*/
public static SessionImpl getSessionForThread(final Thread thread)
{
- final ThreadGroup threadGroup = thread.getThreadGroup();
-
- if (threadGroup == null)
- {
- // the thread died in between so there is no session
- return null;
- }
- else
- {
- SessionImpl s = sessions.get(threadGroup);
-
- if (s == null)
- {
- synchronized (threadGroup)
- {
- // check again because two threads might have waited at the
- // sync block and the first one created the session already
- s = sessions.get(threadGroup);
- if (s == null)
- {
- s = new SessionImpl(XltPropertiesImpl.getInstance());
- sessions.put(threadGroup, s);
- }
- }
- }
-
- return s;
- }
+ return sessions.get(thread);
}
/**
@@ -300,6 +305,7 @@ protected SessionImpl()
this.shutdownListeners = null;
this.transactionTimeout = 0;
}
+
/**
* Creates a new Session object.
*/
@@ -330,12 +336,11 @@ public SessionImpl(final XltPropertiesImpl properties)
// create more session-specific helper objects
requestHistory = new RequestHistory(this, properties);
- this.isTransactionExpirationTimerEnabled = properties.getProperty(this, XltConstants.XLT_PACKAGE_PATH + ".abortLongRunningTransactions")
- .map(Boolean::valueOf)
- .orElse(false);
- this.transactionTimeout = properties.getProperty(this, PROP_MAX_TRANSACTION_TIMEOUT)
- .flatMap(ParseNumbers::parseOptionalInt)
- .orElse(DEFAULT_TRANSACTION_TIMEOUT);
+ this.isTransactionExpirationTimerEnabled = properties.getProperty(this,
+ XltConstants.XLT_PACKAGE_PATH + ".abortLongRunningTransactions")
+ .map(Boolean::valueOf).orElse(false);
+ this.transactionTimeout = properties.getProperty(this, PROP_MAX_TRANSACTION_TIMEOUT).flatMap(ParseNumbers::parseOptionalInt)
+ .orElse(DEFAULT_TRANSACTION_TIMEOUT);
}
/**
@@ -396,8 +401,9 @@ public void clear()
transactionTimer = null; // just for safety's sake
valueLog.clear();
- // we cannot reset the name, because the session is recycled over and over again but never fully inited by the load test framework again
- //userName = UNKNOWN_USER_NAME;
+ // we cannot reset the name, because the session is recycled over and over again but never fully inited by
+ // the load test framework again
+ // userName = UNKNOWN_USER_NAME;
dataManagerImpl.close();
}
@@ -492,7 +498,7 @@ public Path getResultsDirectory()
// create new file handle for result directory rooted at the
// user name directory which itself is rooted at the configured
// result dir
- // resultDir = new File(new File(resultDirName, cleanUserName), String.valueOf(userNumber));
+ // resultDir = new File(new File(resultDirName, cleanUserName), String.valueOf(userNumber));
resultDir = Path.of(resultDirName, cleanUserName, String.valueOf(userNumber));
if (!Files.exists(resultDir))
@@ -507,8 +513,7 @@ public Path getResultsDirectory()
}
catch (IOException e)
{
- XltLogger.runTimeLogger.error("Cannot create file for output of timer: "
- + resultDir.toString(), e);
+ XltLogger.runTimeLogger.error("Cannot create file for output of timer: " + resultDir.toString(), e);
return null;
}
diff --git a/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java
new file mode 100644
index 000000000..97df1e6ed
--- /dev/null
+++ b/src/main/java/com/xceptance/xlt/engine/XltThreadFactory.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2005-2024 Xceptance Software Technologies GmbH
+ *
+ * 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.xceptance.xlt.engine;
+
+import java.util.Objects;
+import java.util.concurrent.ThreadFactory;
+
+import javax.annotation.Nullable;
+
+/**
+ * A {@link ThreadFactory} that creates either virtual or platform daemon threads.
+ */
+public class XltThreadFactory implements ThreadFactory
+{
+ private static final String DEFAULT_THREAD_NAME_PREFIX = "XltThread-";
+
+ /**
+ * The underlying thread factory that does the hard work.
+ */
+ private final ThreadFactory threadFactory;
+
+ /**
+ * Creates a thread factory creating daemon threads that have the default name prefix and don't inherit inheritable
+ * thread-locals.
+ *
+ * @param createVirtualThreads
+ * whether to create virtual threads instead of platform threads
+ */
+ public XltThreadFactory(boolean createVirtualThreads)
+ {
+ this(createVirtualThreads, false);
+ }
+
+ /**
+ * Creates a thread factory creating daemon threads that have the default name prefix.
+ *
+ * @param createVirtualThreads
+ * whether to create virtual threads instead of platform threads
+ * @param inheritInheritableThreadLocals
+ * whether or not the threads will inherit inheritable thread-locals
+ */
+ public XltThreadFactory(boolean createVirtualThreads, final boolean inheritInheritableThreadLocals)
+ {
+ this(createVirtualThreads, inheritInheritableThreadLocals, DEFAULT_THREAD_NAME_PREFIX);
+ }
+
+ /**
+ * Creates a thread factory creating daemon threads.
+ *
+ * @param createVirtualThreads
+ * whether to create virtual threads instead of platform threads
+ * @param inheritInheritableThreadLocals
+ * whether or not the threads will inherit inheritable thread-locals
+ * @param threadNamePrefix
+ * an optional thread name prefix (a counter will be appended to it)
+ */
+ public XltThreadFactory(boolean createVirtualThreads, final boolean inheritInheritableThreadLocals,
+ @Nullable final String threadNamePrefix)
+ {
+ // set up the thread builder
+ final Thread.Builder threadBuilder = createVirtualThreads ? Thread.ofVirtual() : Thread.ofPlatform().daemon();
+ threadBuilder.inheritInheritableThreadLocals(inheritInheritableThreadLocals);
+ threadBuilder.name(Objects.toString(threadNamePrefix, DEFAULT_THREAD_NAME_PREFIX), 0);
+
+ // create a thread-safe thread factory from the thread builder
+ threadFactory = threadBuilder.factory();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Thread newThread(final Runnable r)
+ {
+ return threadFactory.newThread(r);
+ }
+}
diff --git a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java
index 6fd2d8131..15c6bf75d 100644
--- a/src/main/java/com/xceptance/xlt/engine/XltWebClient.java
+++ b/src/main/java/com/xceptance/xlt/engine/XltWebClient.java
@@ -87,6 +87,7 @@
import com.xceptance.xlt.api.util.XltException;
import com.xceptance.xlt.api.util.XltLogger;
import com.xceptance.xlt.api.util.XltProperties;
+import com.xceptance.xlt.common.XltConstants;
import com.xceptance.xlt.engine.htmlunit.apache.XltApacheHttpWebConnection;
import com.xceptance.xlt.engine.htmlunit.okhttp3.OkHttp3WebConnection;
import com.xceptance.xlt.engine.socket.XltSockets;
@@ -288,7 +289,10 @@ public XltWebClient(final BrowserVersion browserVersion, final boolean javaScrip
XltLogger.runTimeLogger.warn("Property 'com.xceptance.xlt.staticContent.downloadThreads' is set to an invalid value. Will use 1 instead.");
threadCount = 1;
}
- requestQueue = new RequestQueue(this, threadCount);
+
+ final boolean useVirtualThreads = props.getProperty(XltConstants.PROP_VIRTUAL_THREADS_ENABLED, false);
+
+ requestQueue = new RequestQueue(this, threadCount, useVirtualThreads);
/*
* Configure the super class.
diff --git a/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java b/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java
index 11bc23c91..0a25b3659 100644
--- a/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java
+++ b/src/test/java/com/xceptance/xlt/agent/DataRecordLoggingTest.java
@@ -86,6 +86,7 @@
import com.xceptance.xlt.api.util.XltProperties;
import com.xceptance.xlt.engine.DataManagerImpl;
import com.xceptance.xlt.engine.SessionImpl;
+import com.xceptance.xlt.engine.XltThreadFactory;
import com.xceptance.xlt.engine.XltWebClient;
import util.xlt.IntentionalError;
@@ -109,7 +110,7 @@
})
@PowerMockIgnore(
{
- "javax.*", "org.xml.*", "org.w3c.dom.*"
+ "javax.*", "org.xml.*", "org.w3c.dom.*", "org.apache.commons.vfs2.*"
})
public class DataRecordLoggingTest
{
@@ -167,7 +168,7 @@ public void initMocks() throws Exception
public void clear()
{
dataRecordCaptors.clear();
- mockDataManagers.clear();
+ mockDataManager = null;
}
@Test
@@ -203,12 +204,9 @@ protected void postValidate() throws Exception
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup),
- expect(RequestData.class, hasName("Action1.1"), hasFailed(false), hasUrl(url1)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(RequestData.class, hasName("Action1.1"), hasFailed(false), hasUrl(url1)),
expect(ActionData.class, hasName("Action1"), hasFailed(false)),
expect(EventData.class, hasName("Event 1"), hasMessage("Message 1"),
hasTestCaseName(expectedUserName())),
@@ -242,12 +240,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup),
- expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)),
expect(ActionData.class, hasName("FailedAction-Uncaught"), hasFailed(true)),
// failedActionName is only set once
expect(TransactionData.class, hasFailed(true), hasFailedActionName("FailedAction-Caught")));
@@ -270,11 +265,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)),
expect(0, ActionData.class, hasName("NotApplicableAction")),
expect(1, ActionData.class, hasName("LastAction"), hasFailed(false)),
expect(1, TransactionData.class, hasFailed(false), hasFailedActionName(null),
@@ -300,12 +293,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup),
- expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)),
expect(ActionData.class, hasName("LastAction"), hasFailed(false)),
expect(TransactionData.class, hasFailed(false), hasFailureStackTrace(null),
hasFailedActionName("FailedAction-Caught")));
@@ -329,12 +319,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup),
- expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FailedAction-Caught"), hasFailed(true)),
expect(TransactionData.class, hasFailed(false), hasFailureStackTrace(null),
hasFailedActionName("FailedAction-Caught")));
}
@@ -352,21 +339,18 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- final DataManagerImpl instance = mockDataManagerFor(threadGroup);
if (kindOfLoadTestClass.isXltDerived() || testExecutionThreadStrategy.usesLoadTestRunner)
{
- verifyDataRecordsLoggedWith(instance, expect(0, ActionData.class),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(0, ActionData.class),
expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName(""),
hasFailureStackTraceMatching(expectedFailureStacktraceRegex(STACKTRACE_REGEX_FOR_THROW_INTENTIONAL_ERROR,
defaultUserId()))));
}
else
{
- Assert.assertNull("No action -> no session -> no data manager", instance);
+ Assert.assertNull("No action -> no session -> no data manager", mockDataManager);
}
}
@@ -384,12 +368,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- final DataManagerImpl instance = mockDataManagerFor(threadGroup);
- verifyDataRecordsLoggedWith(instance, expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(1, ActionData.class, hasName("FirstAction"), hasFailed(false)),
expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true), hasFailedActionName(""),
hasFailureStackTraceMatching(expectedFailureStacktraceRegex(STACKTRACE_REGEX_FOR_THROW_INTENTIONAL_ERROR,
defaultUserId()))));
@@ -416,11 +397,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(ActionData.class, hasName("FirstAction"), hasFailed(false)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FirstAction"), hasFailed(false)),
expect(ActionData.class, hasName("FailedAction-preValidate"), hasFailed(true)),
expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true),
hasFailedActionName("FailedAction-preValidate"),
@@ -442,11 +421,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(ActionData.class, hasName("FirstAction"), hasFailed(false)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FirstAction"), hasFailed(false)),
expect(ActionData.class, hasName("FailedAction-execute"), hasFailed(true)),
expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true),
hasFailedActionName("FailedAction-execute"),
@@ -468,11 +445,9 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
-
startAndWaitFor(testExecutionThread);
- verifyDataRecordsLoggedWith(mockDataManagerFor(threadGroup), expect(ActionData.class, hasName("FirstAction"), hasFailed(false)),
+ verifyDataRecordsLoggedWith(mockDataManager, expect(ActionData.class, hasName("FirstAction"), hasFailed(false)),
expect(ActionData.class, hasName("FailedAction-postValidate"), hasFailed(true)),
expect(TransactionData.class, hasName(expectedUserName()), hasFailed(true),
hasFailedActionName("FailedAction-postValidate"),
@@ -522,16 +497,14 @@ public void test() throws Throwable
}
});
- final ThreadGroup threadGroup = testExecutionThread.getThreadGroup();
GlobalClock.installFixed(startTime);
startAndWaitFor(testExecutionThread);
- final DataManagerImpl dataManager = mockDataManagerFor(threadGroup);
- final InOrder inOrder = Mockito.inOrder(dataManager);
- inOrder.verify(dataManager).setStartOfLoggingPeriod(startTime + initialDelay + warmUpPeriod);
- inOrder.verify(dataManager).setEndOfLoggingPeriod(startTime + initialDelay + warmUpPeriod + measurementPeriod);
- inOrder.verify(dataManager).logDataRecord(argThat(has(EventData.class, hasTime(eventTime), hasName(eventName))));
+ final InOrder inOrder = Mockito.inOrder(mockDataManager);
+ inOrder.verify(mockDataManager).setStartOfLoggingPeriod(startTime + initialDelay + warmUpPeriod);
+ inOrder.verify(mockDataManager).setEndOfLoggingPeriod(startTime + initialDelay + warmUpPeriod + measurementPeriod);
+ inOrder.verify(mockDataManager).logDataRecord(argThat(has(EventData.class, hasTime(eventTime), hasName(eventName))));
}
static final Pattern EOL_PLACEHOLDER_PATTERN = Pattern.compile(EOL_PLACEHOLDER_IN_STACKTRACE_REGEXES);
@@ -647,7 +620,7 @@ public static void throwIntentionalError(Long timeOfFailure)
throw new IntentionalError();
}
- private Map mockDataManagers = createThreadSafeWeakHashMap();
+ private DataManagerImpl mockDataManager;
private Map> dataRecordCaptors = createThreadSafeWeakHashMap();
@@ -664,6 +637,13 @@ private void mockDataManagerCreation() throws Exception
@Override
public DataManagerImpl answer(InvocationOnMock invocation) throws Throwable
{
+ // This method seems to be called immediately and with null parameters as part of setting up the answer
+ // (Powermock bug?) -> ignore.
+ if (invocation.getArgument(0, Session.class) == null)
+ {
+ return null;
+ }
+
// limit to constructor new DataManagerImpl(Session) and avoid using (Session, Metrics)
final DataManagerImpl instance = Whitebox.invokeConstructor(DataManagerImpl.class, new Class>[]
{
@@ -672,40 +652,28 @@ public DataManagerImpl answer(InvocationOnMock invocation) throws Throwable
{
invocation.getArgument(0, Session.class)
});
- return mockDataManagers.computeIfAbsent(Thread.currentThread().getThreadGroup(), __ -> createMockDataManager(instance));
+
+ return createMockDataManager(instance);
}
});
}
- /**
- * Returns the mock {@link DataManagerImpl} that will be used by the {@link SessionImpl} object for the specified
- * thread group.
- *
- * ATTENTION: If using this in a test, it needs to be called before the thread has finished
- *
- * @param thread
- * @param session
- * @return mock {@link DataManagerImpl} object used by {@link SessionImpl} for the specified thread
- * @see #mockDataManagerCreation()
- */
- private DataManagerImpl mockDataManagerFor(ThreadGroup threadGroup)
- {
- return mockDataManagers.get(threadGroup);
- }
-
private DataManagerImpl createMockDataManager(final DataManagerImpl instance)
{
- final ArgumentCaptor dataRecordCaptor = ArgumentCaptor.forClass(Data.class);
- final DataManagerImpl mock = Mockito.spy(instance);
+ // set up mock only if not done so before
+ if (mockDataManager == null)
+ {
+ final ArgumentCaptor dataRecordCaptor = ArgumentCaptor.forClass(Data.class);
+ final DataManagerImpl mock = Mockito.spy(instance);
- Mockito.doNothing().when(mock).logDataRecord(dataRecordCaptor.capture());
+ Mockito.doNothing().when(mock).logDataRecord(dataRecordCaptor.capture());
- // We want to see the logging of EventData records, so we'll have to let logEvent do its job
- Mockito.doCallRealMethod().when(mock).logEvent(Mockito.any(), Mockito.any());
+ dataRecordCaptors.put(mock, dataRecordCaptor);
- dataRecordCaptors.put(mock, dataRecordCaptor);
+ mockDataManager = mock;
+ }
- return mock;
+ return mockDataManager;
}
public List getDataRecordsCapturedFor(DataManager mockDataManager)
@@ -736,7 +704,6 @@ private static void startAndWaitFor(Thread... threads) throws InterruptedExcepti
}
}
- @SuppressWarnings("serial")
static class CountingMap extends LinkedHashMap
{
public void increaseValueFor(final Key key, final int increaseBy)
@@ -973,9 +940,8 @@ enum TestExecutionThreadStrategy
public Thread createThreadFor(Class> loadTestClassObject, TestUserConfiguration testUserConfiguration, AgentInfo agentInfo,
DataRecordLoggingTest thisTestInstance)
{
- return new LoadTestRunner(testUserConfiguration, agentInfo, dummyExecutionTimer());
+ return new LoadTestRunner(testUserConfiguration, agentInfo, dummyExecutionTimer()).getThread();
}
-
},
/**
@@ -990,7 +956,7 @@ public Thread createThreadFor(Class> loadTestClassObject, TestUserConfiguratio
DataRecordLoggingTest thisTestInstance)
{
final Runnable r = () -> Request.aClass(loadTestClassObject).getRunner().run(new RunNotifier());
- return new Thread(new ThreadGroup("JUnitRequestRunner"), r);
+ return new XltThreadFactory(false, null).newThread(r);
}
};