Chapter 06Lesson 05~200 minutes

Checkpoint Lab — Configuration Elements, Variables, Properties, and Environment Control

The checkpoint makes environment control auditable: one JMX runs unchanged against LAB-A and LAB-B, external property files select the target/workload, a response-derived value stays thread-local, run-wide properties stay process-global, and one intentional variable/property mismatch is diagnosed without editing the plan into separate environment copies.

CheckpointOne JMXTwo environmentsThread-local proofProperty mismatch

Learning objectives

  • Run one unchanged JMX against two disposable loopback configurations.
  • Prove resolved target, workload, label, and data-path inputs from external non-secret properties.
  • Prove a response-derived variable remains isolated per thread.
  • Prove a run-wide JMeter property is shared by threads in one JMeter process.
  • Introduce and diagnose one deliberate ${VAR} versus __P(property) mismatch.
  • Produce a property-resolution table, exact CLI record, JTL/log/server evidence, JMX hash, and validity note.

1. Checkpoint assumptions and hard ceilings

Item Baseline
JMeter Apache JMeter 5.6.3.
Java Java 17 JDK lab baseline; JMeter 5.6.3 requires Java 8+.
Plugins None.
Targets Only 127.0.0.1:8000 (LAB-A) and 127.0.0.1:8001 (LAB-B).
Profile A 2 threads, 3-second lifetime.
Profile B 1 thread, 4-second lifetime.
Sampler One /config request per loop plus an optional bounded verification sampler.
Property data Synthetic, non-secret only.
Remote testing Not executed; -G discussed/validated conceptually only.
Abort: target host not exactly 127.0.0.1, port outside 8000–8001, threads > 2, duration > 4 seconds, unresolved required configuration, unexpected 5xx, or unsafe generator pressure.

2. Workspace

jmeter-ch06-checkpoint/
├── fixtures/
│   └── config_fixture.py
├── config/
│   ├── lab-a.properties
│   └── lab-b.properties
├── plans/
│   ├── config-debug.jmx
│   └── config-load.jmx
├── evidence/
│   ├── resolution-a.txt
│   ├── resolution-b.txt
│   ├── predictions.txt
│   ├── jmx-sha256.txt
│   └── validity.txt
└── results/
    ├── run-a/
    ├── run-b/
    └── broken/

3. Start and preflight both fixtures

python fixtures/config_fixture.py --port 8000 --name LAB-A --log results/run-a/server-events.jsonl
python fixtures/config_fixture.py --port 8001 --name LAB-B --log results/run-b/server-events.jsonl

curl --fail --silent http://127.0.0.1:8000/health
curl --fail --silent http://127.0.0.1:8001/health

Record the returned server names so the configuration files can be verified independently of JMeter.

4. Property profiles

config/lab-a.properties:

# Non-secret local configuration A
lab.host=127.0.0.1
lab.port=8000
lab.name=LAB-A
load.threads=2
load.duration=3
run.label=properties-a
global.mode=project-file
lab.data.path=fixtures/data/config-a.csv

config/lab-b.properties:

# Non-secret local configuration B
lab.host=127.0.0.1
lab.port=8001
lab.name=LAB-B
load.threads=1
load.duration=4
run.label=properties-b
global.mode=project-file
lab.data.path=fixtures/data/config-b.csv

5. One JMX contract

The plan must use:

HOST      = ${__P(lab.host,127.0.0.1)}
PORT      = ${__P(lab.port,8000)}
DATA_PATH = ${__P(lab.data.path,fixtures/data/default.csv)}

Thread Group:
  Number of Threads = ${__P(load.threads,1)}
  Loop Count = 100
  Lifetime enabled
  Duration = ${__P(load.duration,3)}

Headers:
  X-Run-Label   = ${__P(run.label,default-run)}
  X-Global-Mode = ${__P(global.mode,default-mode)}

Sampler:
/config?thread=${__threadNum}&data_path=${DATA_PATH}&run=${__P(run.label,default-run)}

Save the load JMX once, calculate its SHA-256, and keep the same file for A and B:

# Bash
sha256sum plans/config-load.jmx > evidence/jmx-sha256.txt

# PowerShell
Get-FileHash plans\config-load.jmx -Algorithm SHA256 |
  Format-List | Out-File evidence\jmx-sha256.txt

6. Write predictions before any run

Prediction Run A Run B
Target server LAB-A / port 8000 LAB-B / port 8001
Configured users 2 1
Lifetime 3 seconds 4 seconds
Run label header/query properties-a properties-b
Global mode project-file for every thread project-file for every thread
Data-path string config-a.csv path config-b.csv path
JMX SHA-256 Identical Identical

Add one more prediction: a response-derived THREAD_ECHO variable will differ by JMeter thread, while run.label remains the same for every thread inside that run.

7. Authoring-only variable/property proof

Use the debug copy at 2 threads × 1 loop. Add the same extractor from Lesson 2 to create THREAD_ECHO. Add a verification sampler:

/verify?thread_var=${THREAD_ECHO}&thread_func=${__threadNum}&run=${__P(run.label,default-run)}

Use Debug Sampler/View Results Tree once to inspect thread variables and selected properties. Expected: each thread's THREAD_ECHO matches its own __threadNum, while run.label is shared. Then disable diagnostics for load runs.

8. Run LAB-A

jmeter -n   -t plans/config-load.jmx   -q config/lab-a.properties   -Dlab.jvm.tag=JVM-A   -l results/run-a/results.jtl   -j results/run-a/jmeter.log

Verify server A receives events and server B does not receive this run's requests. Preserve /stats from both endpoints.

9. Run LAB-B with the exact same JMX

jmeter -n   -t plans/config-load.jmx   -q config/lab-b.properties   -Dlab.jvm.tag=JVM-B   -l results/run-b/results.jtl   -j results/run-b/jmeter.log

Recalculate the JMX SHA-256 and verify it has not changed. The resolved target/workload should change because the external properties changed—not because the JMX was edited.

10. Build the property-resolution evidence table

Name Source A Resolved A Source B Resolved B Store
lab.host lab-a.properties 127.0.0.1 lab-b.properties 127.0.0.1 JMeter property
lab.port lab-a.properties 8000 lab-b.properties 8001 JMeter property → PORT variable
load.threads lab-a.properties 2 lab-b.properties 1 JMeter property
load.duration lab-a.properties 3 lab-b.properties 4 JMeter property
run.label lab-a.properties properties-a lab-b.properties properties-b JMeter property
lab.data.path lab-a.properties config-a.csv lab-b.properties config-b.csv JMeter property → DATA_PATH variable
THREAD_ECHO HTTP response extractor per-thread HTTP response extractor per-thread JMeter variable
lab.jvm.tag -D JVM-A -D JVM-B Java system property

11. Deliberately break the variable/property mapping

Create a copy only for the diagnostic failure, plans/broken-port.jmx. Change HTTP Defaults port from ${PORT} to ${LAB_PORT}. Keep the launch input -q config/lab-b.properties, which defines JMeter property lab.port=8001 but does not define variable LAB_PORT.

Prediction: ${LAB_PORT} remains unresolved because it is a missing variable. Preserve the failing JMX/JTL/log. Do not “fix” the property file by adding an unrelated LAB_PORT variable concept.

12. Diagnose and restore the intended ownership

Read the JMX and input table:

  • property exists: lab.port=8001;
  • startup variable exists: PORT=${__P(lab.port,8000)};
  • missing variable: LAB_PORT.

The least destructive correction is to restore HTTP Defaults to ${PORT} (or use ${__P(lab.port,8000)} directly). Rerun LAB-B into a new result directory; preserve the broken evidence.

13. One intentional -J override

Run A once more with the same profile and only this additional non-secret override:

-Jrun.label=checkpoint-cli-override

The target, users, duration, and data path remain from the A file while the run label changes. Record the resolved value and exact command.

14. Explain the distributed property boundary

If this plan later runs remotely, a controller-only -Jrun.label=... is not a contract for remote engine properties. The remote samplers need the relevant JMeter properties provisioned/sent, commonly via -G. Data files, Java -D system properties, plugins, and OS environment state still require separate engine provisioning.

No remote engine is started in this checkpoint.

15. Verification checklist

  • JMX SHA-256 is identical for A and B normal runs.
  • LAB-A receives only the A normal run; LAB-B receives only B normal run.
  • Resolved thread count/duration match the selected property file.
  • Run label/global mode/data-path query/header values match the property resolution table.
  • Bounded debug evidence shows response-derived variable isolation by thread.
  • run.label is shared within one JMeter process as expected.
  • The broken ${LAB_PORT} failure is preserved and correctly attributed to variable/property mismatch.
  • JTL and jmeter.log are unique per run.
  • No real secrets, public targets, remote ports, or distribution-property edits were introduced.

16. Required evidence packet

Artifact Purpose
One normal JMX + SHA-256 Proves plan logic stayed unchanged across environments.
A/B property files Explicit non-secret environment/workload inputs.
Exact CLI commands Shows selected -q, -J, -D inputs.
Resolution A/B tables Maps source → resolved value → store/scope.
Bounded Debug Sampler evidence Variables, JMeter properties, system-property visibility, thread IDs.
A/B JTL + jmeter.log Sample/runtime evidence.
A/B server JSONL + stats Independent resolved-target/header/query evidence.
Broken JMX/JTL/log First-failure proof for variable/property mismatch.
Validity note Prevents configuration proof from becoming a capacity claim.

17. Validity statement

Example: “Using Apache JMeter 5.6.3 with Java 17, one unchanged JMX ran against two loopback fixtures by selecting different non-secret JMeter property files. Run-wide target/workload values were JMeter properties (with selected startup copies exposed as UDV variables), while response-derived THREAD_ECHO remained thread-local. Java -Dlab.jvm.tag remained a separate JVM system-property plane. An intentional ${LAB_PORT} reference failed because the launcher/property file defined lab.port, not a variable named LAB_PORT; restoring the intended PORT=${__P(lab.port,...)} mapping repaired the plan. This demonstrates configuration ownership and resolution, not production capacity or distributed-engine behavior.”

18. Cleanup

  1. Stop both local fixture processes.
  2. Keep A/B/broken artifacts until review is complete.
  3. Disable/remove Debug Sampler from load profiles.
  4. Do not copy property dumps into tickets if they might contain real secret values in future environments.
  5. No production target, real credential, remote engine, RMI port, database, container, plugin, or system-wide JVM/OS setting was changed.

19. What Chapter 06 adds to the operating model

The performance-test operating model now has a configuration provenance contract: committed JMX logic, selected non-secret profile, explicit CLI overrides, Java/JMeter property boundaries, thread-local runtime values, target allow-list, and the resolved run manifest are separately reviewable.

Chapter 07 builds on this configuration control to parameterize timers, think time, pacing, and throughput targets safely without confusing a timer property with achieved request rate or server latency.

Knowledge check

What proves the JMX really ran unchanged against LAB-A and LAB-B?

Why does ${LAB_PORT} fail when only -Jlab.port=8001 exists?

What is shared across threads in one run: THREAD_ECHO or run.label?

What does -Dlab.jvm.tag=JVM-A prove?

Why is -G not exercised against live remote engines here?

Next chapter

Timers, Think Time, Pacing, Throughput, and Realistic User Behavior

You can now vary configuration without changing test logic. Chapter 07 uses that capability to control waits and pacing as explicit workload inputs, then measures their effect on achieved rate separately from target response time.

Official references and version notes

  • Functions and Variables — current variable syntax, undefined-variable behavior, __P, __property, __setProperty, and thread-local versus global-property rules.
  • Elements of a Test Plan — User Defined Variables startup processing and variable-copy behavior per thread.
  • Getting Started — -J, -G, -D, -q, property-file loading, command-line processing order, GUI versus CLI guidance, and jmeter.log behavior.
  • Properties Reference — current guidance for setting JMeter properties through user.properties rather than modifying distribution defaults.
  • Apache JMeter downloads — current production release and Java requirement.
Version and compatibility note

Version-sensitive statements were rechecked against current Apache JMeter documentation on 2026-09-04. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK for labs and no third-party plugins; JMeter 5.6.3 itself requires Java 8+. JMeter variables are thread-local after each thread receives its startup copy; JMeter properties are shared within one JMeter instance. An undefined ${VAR} reference is returned unchanged rather than failing automatically. __P(name,default) reads a JMeter property and defaults to 1 when no explicit default is supplied. -J defines a local JMeter property, -G defines/sends properties to remote JMeter servers, and -D defines a Java system property. The standard user.properties layer is loaded after the base property file and before later command-line additions such as -q/-J; JMeter's Properties Reference recommends setting ordinary JMeter properties in user.properties rather than editing distribution defaults. Mandatory labs remain single-process/local; -G is demonstrated as a distributed-boundary concept without starting remote engines.

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.