fix: allow usage of variables in ConstantThroughputTimer.throughput and PreciseThroughputTimer

Previously, the timers might fail initialization in case variable was not present
when test started.

The current workaround is to avoid implementing TestStateListener in the timers
so JMeter does not attempt evaluating all the properties before test starts.

fixes https://github.com/apache/jmeter/issues/6165
diff --git a/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java b/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java
index 4a764cf..448e0cc 100644
--- a/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java
+++ b/src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java
@@ -23,13 +23,13 @@
 import java.util.ResourceBundle;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicLong;
 
 import org.apache.jmeter.gui.GUIMenuSortOrder;
 import org.apache.jmeter.gui.TestElementMetadata;
 import org.apache.jmeter.testbeans.TestBean;
 import org.apache.jmeter.testbeans.gui.GenericTestBeanCustomizer;
 import org.apache.jmeter.testelement.AbstractTestElement;
-import org.apache.jmeter.testelement.TestStateListener;
 import org.apache.jmeter.testelement.property.DoubleProperty;
 import org.apache.jmeter.testelement.property.IntegerProperty;
 import org.apache.jmeter.testelement.property.JMeterProperty;
@@ -52,7 +52,7 @@
  */
 @GUIMenuSortOrder(4)
 @TestElementMetadata(labelResource = "displayName")
-public class ConstantThroughputTimer extends AbstractTestElement implements Timer, TestStateListener, TestBean {
+public class ConstantThroughputTimer extends AbstractTestElement implements Timer, TestBean {
     private static final long serialVersionUID = 4;
 
     private static class ThroughputInfo{
@@ -60,6 +60,7 @@
         long lastScheduledTime = 0;
     }
     private static final Logger log = LoggerFactory.getLogger(ConstantThroughputTimer.class);
+    private static final AtomicLong PREV_TEST_STARTED = new AtomicLong(0L);
 
     private static final double MILLISEC_PER_MIN = 60000.0;
 
@@ -180,6 +181,13 @@
 
     // Calculate the delay based on the mode
     private long calculateDelay() {
+        long testStarted = JMeterContextService.getTestStartTime();
+        long prevStarted = PREV_TEST_STARTED.get();
+        if (prevStarted != testStarted && PREV_TEST_STARTED.compareAndSet(prevStarted, testStarted)) {
+            // Reset counters if we are calculating throughput for a new test, see https://github.com/apache/jmeter/issues/6165
+            reset();
+        }
+
         long delay;
         // N.B. we fetch the throughput each time, as it may vary during a test
         double msPerRequest = MILLISEC_PER_MIN / getThroughput();
@@ -253,18 +261,6 @@
     }
 
     /**
-     * Get the timer ready to compute delays for a new test.
-     * <p>
-     * {@inheritDoc}
-     */
-    @Override
-    public void testStarted()
-    {
-        log.debug("Test started - reset throughput calculation.");
-        reset();
-    }
-
-    /**
      * Override the setProperty method in order to convert
      * the original String calcMode property.
      * This used the locale-dependent display value, so caused
@@ -300,30 +296,6 @@
         super.setProperty(property);
     }
 
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void testEnded() {
-        //NOOP
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void testStarted(String host) {
-        testStarted();
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    public void testEnded(String host) {
-        //NOOP
-    }
-
     // For access from test code
     Mode getMode() {
         int mode = getCalcMode();
diff --git a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java
index fc3c4a9..4864e22 100644
--- a/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java
+++ b/src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java
@@ -20,13 +20,14 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
 
 import org.apache.jmeter.gui.GUIMenuSortOrder;
 import org.apache.jmeter.gui.TestElementMetadata;
 import org.apache.jmeter.testbeans.TestBean;
 import org.apache.jmeter.testelement.AbstractTestElement;
-import org.apache.jmeter.testelement.TestStateListener;
 import org.apache.jmeter.threads.AbstractThreadGroup;
+import org.apache.jmeter.threads.JMeterContextService;
 import org.apache.jmeter.timers.Timer;
 import org.apache.jorphan.collections.IdentityKey;
 import org.apache.jorphan.util.JMeterStopThreadException;
@@ -41,7 +42,7 @@
  */
 @GUIMenuSortOrder(3)
 @TestElementMetadata(labelResource = "displayName")
-public class PreciseThroughputTimer extends AbstractTestElement implements Cloneable, Timer, TestStateListener, TestBean, ThroughputProvider, DurationProvider {
+public class PreciseThroughputTimer extends AbstractTestElement implements Cloneable, Timer, TestBean, ThroughputProvider, DurationProvider {
     private static final Logger log = LoggerFactory.getLogger(PreciseThroughputTimer.class);
 
     private static final long serialVersionUID = 4;
@@ -50,6 +51,8 @@
     private static final ConcurrentMap<IdentityKey<AbstractThreadGroup>, EventProducer> groupEvents =
             new ConcurrentHashMap<>();
 
+    private static final AtomicLong PREV_TEST_STARTED = new AtomicLong(0L);
+
     /**
      * Desired throughput configured as {@code throughput/throughputPeriod} per second.
      */
@@ -63,8 +66,6 @@
      */
     private long duration;
 
-    private long testStarted;
-
     /**
      * When number of required samples exceeds {@code exactLimit}, random generator would resort to approximate match of
      * number of generated samples.
@@ -87,32 +88,10 @@
     @Override
     public Object clone() {
         final PreciseThroughputTimer newTimer = (PreciseThroughputTimer) super.clone();
-        newTimer.testStarted = testStarted; // JMeter cloning does not clone fields
         return newTimer;
     }
 
     @Override
-    public void testStarted() {
-        testStarted(null);
-    }
-
-    @Override
-    public void testStarted(String host) {
-        groupEvents.clear();
-        testStarted = System.currentTimeMillis();
-    }
-
-    @Override
-    public void testEnded() {
-        // NOOP
-    }
-
-    @Override
-    public void testEnded(String s) {
-        // NOOP
-    }
-
-    @Override
     public long delay() {
         double nextEvent;
         EventProducer events = getEventProducer();
@@ -120,6 +99,7 @@
             nextEvent = events.next();
         }
         long now = System.currentTimeMillis();
+        long testStarted = JMeterContextService.getTestStartTime();
         long delay = (long) (nextEvent * TimeUnit.SECONDS.toMillis(1) + testStarted - now);
         if (log.isDebugEnabled()) {
             log.debug("Calculated delay is {}", delay);
@@ -137,6 +117,13 @@
     }
 
     private EventProducer getEventProducer() {
+        long testStarted = JMeterContextService.getTestStartTime();
+        long prevStarted = PREV_TEST_STARTED.get();
+        if (prevStarted != testStarted && PREV_TEST_STARTED.compareAndSet(prevStarted, testStarted)) {
+            // Reset counters if we are calculating throughput for a new test, see https://github.com/apache/jmeter/issues/6165
+            groupEvents.clear();
+        }
+
         AbstractThreadGroup tg = getThreadContext().getThreadGroup();
         IdentityKey<AbstractThreadGroup> key = new IdentityKey<>(tg);
         EventProducer eventProducer = groupEvents.get(key);
diff --git a/src/functions/src/test/kotlin/org/apache/jmeter/timers/ConstantThroughputTimerKtTest.kt b/src/functions/src/test/kotlin/org/apache/jmeter/timers/ConstantThroughputTimerKtTest.kt
new file mode 100644
index 0000000..1a1dc80
--- /dev/null
+++ b/src/functions/src/test/kotlin/org/apache/jmeter/timers/ConstantThroughputTimerKtTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.timers
+
+import org.apache.jmeter.control.LoopController
+import org.apache.jmeter.junit.JMeterTestCase
+import org.apache.jmeter.sampler.DebugSampler
+import org.apache.jmeter.test.assertions.executePlanAndCollectEvents
+import org.apache.jmeter.threads.ThreadGroup
+import org.apache.jmeter.treebuilder.TreeBuilder
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import kotlin.time.Duration.Companion.seconds
+
+class ConstantThroughputTimerKtTest : JMeterTestCase() {
+    fun TreeBuilder.oneRequest(body: ThreadGroup.() -> Unit) {
+        ThreadGroup::class {
+            numThreads = 1
+            rampUp = 0
+            setSamplerController(
+                LoopController().apply {
+                    loops = 1
+                }
+            )
+            body()
+        }
+    }
+
+    @Test
+    fun `throughput as variable`() {
+        val events = executePlanAndCollectEvents(5.seconds) {
+            oneRequest {
+                DebugSampler::class {
+                    // This initializes the variable during the test execution
+                    name = "\${__groovy( vars.put(\"throughput\"\\, \"10000\") )}"
+                }
+                DebugSampler::class {
+                    ConstantThroughputTimer::class {
+                        setProperty(
+                            "throughput",
+                            "\${__groovy( vars.get(\"throughput\").toDouble() )}"
+                        )
+                        setProperty("calcMode", 0)
+                    }
+                }
+            }
+        }
+        assertEquals(
+            2,
+            events.size,
+            "Test should complete within reasonable time, and the test has 2 debug samplers, so we expect 2 events"
+        )
+    }
+}