javamultithreadingjmeterperformance-testing

Abstracta JMeter Java DSL - Finish threads gracefully using RPSThreadGroup


I would like to run a test case for an x amount of time, with a y amount of requests per second. I'm using the RPSThreadGroup (Requests Per Second ThreadGroup) to achieve this.

The issue that I'm facing: when the time runs out, the test stops completely and the final active threads do not finish gracefully. This is because the threads are stopped once the RPS schedule finishes, as stated here.This causes the following exceptions:

[ThreadgroupNameHere-ThreadStarter 1-1] ERROR org.apache.jmeter.functions.Jexl2Function -- An error occurred while evaluating the expression "props.get('lambdaScript5').run(new('us.abstracta.jmeter.javadsl.core.util.PropertyScriptBuilder$PropertyScriptVars',ctx,log))"

org.apache.commons.jexl2.JexlException$Cancel: org.apache.jmeter.functions.Jexl2Function.execute@94![0,128]: 'props.get('lambdaScript5').run(new ('us.abstracta.jmeter.javadsl.core.util.PropertyScriptBuilder$PropertyScriptVars', ctx, log));' execution cancelled

Non HTTP response code: java.lang.IllegalArgumentException/Non HTTP response message: Host may not be blank

How can I make sure the final threads either don't execute at all or finish gracefully?

My code:

private static RpsThreadGroup getRpsThreadgroup(String threadGroupName, int maxThreads, int requestsPerSecond, int rampDurationInSeconds, int holdDurationInMinutes) {
    return rpsThreadGroup(threadGroupName)
        .maxThreads(maxThreads)
        .rampToAndHold(
            requestsPerSecond,
            Duration.ofSeconds(rampDurationInSeconds),
            Duration.ofMinutes(holdDurationInMinutes)
        )
        .rampTo(
            0,
            Duration.ofSeconds(5)
        );
}

NOTE: the actual HttpSamplers are added to the threadgroup at different stages in my code. getRpsThreadgroup() is purely to make the threadgroup and configure it.

So let's say I run a test for 30 seconds with 1 request per second, around 30 threads get executed, but the final few threads fail because they never finish properly. See image.

I have tried to ramp down to 0 requests per second and hold it for a few seconds. However, this doesn't seem to work. I get the same result (1+ failing requests).


Solution

  • I opened a GitHub issue under abstracta jmeter-java-dsl. The main maintainer of the DSL was able to reproduce my issue and stated the following:

    The reason is due to the combination rpsThreadGroup (which uses ConcurrencyThreadGroup + VariableThroughputTime) + lambdas (which uses jexl pre processor).

    When threads need to go down, the variableThroughputTimer sends a stop to the ConcurrencyThreadGroup which interrupts the threads and if jexl is in the middle/executing, then it throws the exception and ends up with the behavior you are noticing. It could also interrupt other things and you might get other errors (eg: some IOException, socketclosed or things like that).

    As a temporary solution he provided the following code snippet, replacing the default RpsThreadGroup:

    import kg.apc.jmeter.JMeterPluginsUtils;
    import kg.apc.jmeter.timers.VariableThroughputTimer;
    import kg.apc.jmeter.timers.VariableThroughputTimerGui;
    import org.apache.jmeter.gui.util.PowerTableModel;
    import org.apache.jmeter.sampler.TestAction;
    import org.apache.jmeter.sampler.gui.TestActionGui;
    import org.apache.jmeter.testelement.TestElement;
    import org.apache.jmeter.testelement.property.CollectionProperty;
    import org.apache.jmeter.threads.JMeterContextService;
    import org.apache.jorphan.collections.HashTree;
    import us.abstracta.jmeter.javadsl.core.BuildTreeContext;
    import us.abstracta.jmeter.javadsl.core.threadgroups.RpsThreadGroup;
    
    public class NonInterruptingRpsThreadGroup extends RpsThreadGroup {
        private static int timerId = 1;
    
        public NonInterruptingRpsThreadGroup(String name) {
            super(name);
        }
    
        @Override
        public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) {
            HashTree ret = parent.add(buildConfiguredTestElement());
            HashTree timerParent = counting == EventType.ITERATIONS ? ret.add(buildTestAction()) : ret;
    
            timerParent.add(buildTimer());
            children.forEach(c -> context.buildChild(c, ret));
    
            return ret;
        }
    
        private TestElement buildTestAction() {
            TestAction ret = new TestAction();
    
            ret.setAction(TestAction.PAUSE);
            ret.setDuration("0");
            configureTestElement(ret, "Flow Control Action", TestActionGui.class);
    
            return ret;
        }
    
        private TestElement buildTimer() {
            VariableThroughputTimer ret = new NonInterruptingVariableThroughputTimer();
    
            ret.setData(buildTimerSchedulesData());
            configureTestElement(ret, buildTimerName(timerId++), VariableThroughputTimerGui.class);
    
            return ret;
        }
    
        public static class NonInterruptingVariableThroughputTimer extends VariableThroughputTimer {
            @Override
            protected void stopTest() {
                // This is actually the main change from the original code of rpsThreadGroup.
                JMeterContextService.getContext().getThreadGroup().tellThreadsToStop();
            }
    
        }
    
        private String buildTimerName(int id) {
            return "rpsTimer" + id;
        }
    
        private CollectionProperty buildTimerSchedulesData() {
            PowerTableModel table = new PowerTableModel(
                new String[]{"Start RPS", "End RPS", "Duration, sec"},
                new Class[]{String.class, String.class, String.class}
            );
    
            schedules.forEach(s -> table.addRow(s.buildTableRow()));
    
            return JMeterPluginsUtils.tableModelRowsToCollectionProperty(table, "load_profile");
        }
    }
    

    You would use the NonInterruptingRpsThreadGroup like so:

    private static RpsThreadGroup createRpsThreadgroup(
        String threadGroupName,
        int maxThreads,
        int requestsPerSecond,
        int rampDurationInSeconds,
        int holdDurationInMinutes
    ) {
        return new NonInterruptingRpsThreadGroup(threadGroupName)
            .maxThreads(maxThreads)
            .rampToAndHold(
                requestsPerSecond,
                Duration.ofSeconds(rampDurationInSeconds),
                Duration.ofMinutes(holdDurationInMinutes)
            );
    }
    

    You can read the GitHub issue in detail here.