Chapter 19Lesson 04~200 minutes

Scripting with JSR223 and Groovy for Advanced Test Logic: Diagnostics, Failure Modes, and Production Practices

Script failures are especially dangerous because a bad script can corrupt both the workload and its evidence: stale variables can alter requests, global objects can cross-contaminate users, log storms can saturate disk, swallowed exceptions can silently continue, and SampleResult mutation can erase genuine target failures. Diagnose the script as generator code while preserving the target's original result.

Cache bugThread safetyLog stormExceptionsSampleResult integrity

Learning objectives

  • Diagnose cached direct variable substitution and repair it with bindings.
  • Identify non-thread-safe process-global objects.
  • Measure and eliminate per-sample logging overhead.
  • Reject BeanShell/JavaScript hot paths without a measured reason.
  • Preserve exceptions instead of swallowing them.
  • Prevent PostProcessors/Assertions from rewriting genuine target failures to success.

1. Preserve first-failure evidence

All runnable diagnosis stays local at 127.0.0.1:8019, ≤2 threads. Preserve JMX, script source/file, cache checkbox/property, input row, vars/props ownership, JTL, jmeter.log, generator CPU/heap/log size, target event log/stats, exact CLI and versions before correction. Never substitute a production target or delete the failed evidence.

2. Diagnostic sequence

JSR223/Groovy diagnostic sequence

Scripting sits inside the existing JMeter execution model. It receives scoped JMeter state, performs one controlled calculation or mutation, and returns control to the test plan; it does not replace workload, protocol, target, result, or safety semantics.

flowchart TD
E[Preserve JTL + jmeter.log + script + target evidence] --> V[Confirm JMeter / Java / Groovy / plugin/tool versions]
V --> C[Confirm exact JMX/data/properties/CLI + authorized target]
C --> S[Inspect JSR223 element scope / cache / source / parameters]
S --> D[Inspect vars vs props vs shared objects + resolved values]
D --> P[Inspect protocol/session/sample state and exception]
P --> G[Inspect generator CPU/GC/log/file/classpath]
G --> T[Inspect SUT event count/service timing]
T --> X[Inspect remote/CI/container user.dir/JAR/file state if relevant]
X --> F[Least destructive correction]
F --> R[Small controlled rerun and compare]

3. Intentionally broken example: ${RAW_NAME} inside cached script

Cache compiled script is checked:

// BROKEN ON PURPOSE when cache is enabled:
def raw = '${RAW_NAME}'
vars.put('REQUEST_NAME', raw.trim().toLowerCase(java.util.Locale.ROOT))

JMeter expands ${RAW_NAME} before compilation. The first expanded script can be cached, so later CSV rows can keep producing the first canonical value. Target events show repeated one-name traffic even though CSV advanced.

Repair:

// Cache-safe: script text stays constant; runtime value is read from JMeterVariables.
def raw = vars.get('RAW_NAME')
vars.put('REQUEST_NAME', raw.trim().toLowerCase(java.util.Locale.ROOT))

Script source is now stable and the current thread's value is read at execution time. Preserve the broken JTL/log/events before rerunning 1 thread ×4 rows.

4. Failure mode: non-thread-safe global object in props

Broken concept:

if (props.get('SEEN_NAMES') == null) {
    props.put('SEEN_NAMES', new ArrayList())
}
props.get('SEEN_NAMES').add(vars.get('REQUEST_NAME'))

Properties being shared does not make the stored ArrayList thread-safe, and the lazy initialization itself races. Worse, per-user collection does not belong in global test-plan state at all.

Repair: keep per-user data in vars. For real cross-thread aggregation, prefer JMeter results/backend telemetry or an explicitly designed concurrent structure with bounded memory and documented lifecycle—not ad-hoc properties.

5. Failure mode: INFO logging per sample

Broken:

log.info("user=${vars.get('REQUEST_NAME')} body=${prev.getResponseDataAsString()}")

This formats data and writes to jmeter.log for every sample, possibly including sensitive response data. Under load it can become disk/lock/GC pressure.

Repair: log one setup summary, use assertions/JTL fields/target telemetry for routine evidence, and temporarily enable narrowly scoped DEBUG only for a small reproduction.

6. Failure mode: BeanShell or JavaScript in a hot path without reason

JMeter explicitly recommends migration from BeanShell to JSR223 + Groovy for performance and newer Java support. Groovy implements Compilable; BeanShell/JavaScript do not provide the same compiled-cache path in JMeter guidance.

Do not mechanically translate a script if a built-in JMeter component can replace it. Migration order: built-in first → cached Groovy for irreducible logic → benchmark again.

7. Failure mode: swallowing exceptions and continuing with stale state

Broken:

try {
    vars.put('REQUEST_NAME', canonicalize(vars.get('RAW_NAME')))
} catch (Exception ignored) {
    // continue; REQUEST_NAME may still contain a previous iteration's value
}

The HTTP sampler can now send stale data while the script appears quiet. Repair by validating inputs and allowing the exception/failure to surface, or set an explicit sentinel and fail with an assertion. Preserve the original stack trace in jmeter.log.

8. Failure mode: mutating SampleResult to hide a real failure

Broken PostProcessor:

if (!prev.isSuccessful()) {
    prev.setSuccessful(true)
    prev.setResponseCode('200')
    prev.setResponseMessage('forced success')
}

This destroys the original HTTP/error taxonomy. A performance gate can pass while the target is returning 500/401/assertion failures.

Repair: never force real target samples to success. If an error is expected by scenario design, preserve the original status and classify it with a dedicated label/assertion/analysis rule.

9. Failure mode: Script File works in GUI but fails in CI

Relative JSR223 Script File paths resolve from the Java user.dir system property. Running JMeter from another working directory can make scripts/canonicalize.groovy disappear.

Repair: define a reproducible project root and run CLI from it, or supply an explicit project-root property/path policy. In distributed mode, deploy the file to every engine at the same expected path; controller-local existence is insufficient.

10. Failure mode: dynamic source defeats cache

A script embeds current timestamps, IDs, or variable replacement into source text, creating many unique MD5 cache keys. Compilation/cache churn consumes CPU/memory and can evict legitimate scripts from the default 100-entry cache.

Keep source constant and pass data through bindings/Parameters. Do not “fix” dynamic source by blindly increasing cache size.

11. Failure mode: deep ctx/sampler manipulation when a component exists

ctx/sampler are powerful, but reflective/deep manipulation tightly couples the plan to JMeter implementation details. If HTTP Header Manager, Counter, extractor, assertion, controller, or property field already provides the behavior, use the component.

12. Causal performance table

Symptom Script/generator cause Target cause to distinguish Evidence
Throughput drops, HTTP p95 stable compile/script/log overhead target service slowdown overall JTL duration + generator CPU + target service_wall_ms.
Wrong repeated user value cached ${VAR} source server caching bug CSV row + vars + target name distribution.
Intermittent cross-user data global mutable object/property race server session mix-up thread vars + properties/object ownership + target events.
jmeter.log explodes per-sample logging/stack trace loop server log growth JMeter log bytes + generator disk/CPU.
CI-only script failure user.dir/script/JAR mismatch target regression CI path/classpath + no/unchanged target traffic.
Green JTL despite target errors SampleResult forced success target actually healthy raw target status/server events versus mutated JTL.

13. Security-sensitive scripting

Scripts can read environment variables, filesystem files, JVM properties, credentials, process state, databases/messages/APIs, and can start processes. Treat Groovy as code with the same trust as JMeter itself. Use fake values locally; never dump environment variables/secrets to logs; do not invoke shell/processes from a hot path; keep recorder CA, RMI, CI secrets, containers and OS/JVM tuning outside this lab.

14. Troubleshooting shortcuts to reject

  • Do not add blanket retries or arbitrary long sleeps around script exceptions.
  • Do not increase heap to compensate for unbounded global objects/logs.
  • Do not disable all listeners without evidence; isolate the actual scripting/log/result cost.
  • Do not use global properties as per-user state or synchronization hacks.
  • Do not disable TLS/RMI verification.
  • Do not experiment on production/public targets.
  • Do not increase threads while script correctness/cache/thread-safety is unresolved.
  • Do not delete failed JTL/jmeter.log or target evidence.

Knowledge check

What target symptom points to cached ${VAR} substitution?

Why is an ArrayList stored in props unsafe?

Why can per-sample INFO logging lower achieved load?

What is wrong with catching every exception and continuing?

How can JTL be green while the target failed?

Next lesson

Checkpoint: remove code, keep behavior, measure the generator

Lesson 5 executes the over-scripted/refactored comparison, requires helper self-tests and vars/props ownership evidence, preserves a synthetic exception sample, verifies target equivalence, and closes with a production scripting contract.

Official references and version notes

Version and compatibility note

Version-sensitive statements were rechecked against current Apache JMeter primary documentation on 2026-09-05. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK; JMeter 5.6.3 requires Java 8+. The JMeter 5.6.3 binary line uses Groovy 3.0.20. Groovy's JSR223 engine implements Compilable. JMeter recommends script files (compiled/cached when supported) or inline script text with Cache compiled script if available checked. The compiled-script cache defaults to jsr223.compiled_scripts_cache_size=100. Do not put changing ${VAR} or JMeter function replacement directly inside cached script text: JMeter expands it before the script reaches the engine, so the first replacement can be captured by the cache. Read runtime state through vars, props, Parameters/args, or other supplied bindings instead. JSR223 Sampler exposes log, Label, FileName, Parameters, args, SampleResult, sampler, ctx, vars, props, and OUT; assertions/processors expose the appropriate current/previous-sample bindings. JMeter explicitly recommends migration from BeanShell to JSR223 + Groovy for performance/support, and Groovy is preferred over non-Compilable scripting engines for intensive load paths.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.