Skip to content

feat: add concurrency limit to Open Model Thread Group (#5966)#6727

Open
vlsi wants to merge 2 commits into
apache:masterfrom
vlsi:jmeter-5966-omtg-concurrency
Open

feat: add concurrency limit to Open Model Thread Group (#5966)#6727
vlsi wants to merge 2 commits into
apache:masterfrom
vlsi:jmeter-5966-omtg-concurrency

Conversation

@vlsi

@vlsi vlsi commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Why

Open Model Thread Group creates a new JMeterThread and a fresh test-plan clone for every arrival, with no upper bound. When the server responds slower than arrivals are scheduled, threads pile up until JMeter runs out of memory — several users report thousands to tens of thousands of threads for a load a handful of threads could serve (#5966). A second, deliberate use case is modeling a system with a hard concurrency limit, such as a fixed-size connection pool.

What

Add a positional concurrency(N) schedule step, alongside rate(...), that caps the number of threads running at the same time. Like rate, it is positional and may vary over time:

concurrency(100) rate(10/sec) random_arrivals(10 min) concurrency(200) random_arrivals(10 min)

caps the load at 100 threads for the first ten minutes and 200 for the next.

  • ConcurrencyLimit reads the cap by the time elapsed since the schedule start, not by the arrivals-generator position, so the cap still rises while the producer is blocked waiting for a free thread.
  • ConcurrencyGate reserves a slot before cloning the test plan, so a capped arrival waits for a free thread instead of allocating a clone. This bounds memory by the limit rather than by the arrival rate.
  • When the limit is saturated, OMTG no longer follows the open model: the achieved rate drops below the target rate, which the reports show as fewer samples. An arrival that still has no free thread when the schedule ends is dropped rather than stretching the test past its nominal end. A summary is logged at the end.
  • openmodel.max_concurrency is an installation-wide safety net for schedules that set no concurrency(...) of their own. It defaults to unlimited to preserve existing behavior.

The design leaves room for the thread_lifetime / thread-reuse ideas discussed in the issue: concurrency counts active threads, so a future clone pool can sit under the gate without changing the meaning of existing schedules.

How to verify

New tests in src/core/.../threads/openmodel/:

  • ConcurrencyLimitTest — the cap changes on the arrival boundary regardless of generator position.
  • ConcurrencyGateTest — blocking, deadline drop, release-unblocks-waiter, and interruptibility of a blocked producer.
  • ConcurrencyParseErrorTestconcurrency(0), non-integer, and malformed input are rejected.
  • ConcurrencyLimitBehaviorTest — end-to-end: at most N threads run at once and delayed arrivals are counted.
  • ThreadScheduleTest — parser cases for concurrency(...).
./gradlew :src:core:test --tests "org.apache.jmeter.threads.openmodel.*"

Open question

openmodel.max_concurrency defaults to unlimited, so an unconfigured plan can still hit OOM — the graceful-by-default behavior asked for in the issue would need a non-zero default (e.g. 10000), which changes behavior for existing plans. Left as unlimited here; happy to switch the default if that is preferred.

🤖 Generated with Claude Code

OMTG created a new JMeterThread and a fresh test-plan clone for every
arrival with no upper bound, so a server that responds slower than
arrivals are scheduled made JMeter pile up threads until it ran out of
memory. Users also asked to model a system with a hard concurrency
limit, such as a fixed-size connection pool.

Add a positional concurrency(N) schedule step, alongside rate(...), that
caps the number of threads running at the same time. The cap may vary
over time: concurrency(100) ... concurrency(200) applies 100 for the
first window and 200 for the next.

ConcurrencyLimit reads the cap by the time elapsed since the schedule
start rather than by the arrivals generator position, so the cap still
rises while the producer is blocked waiting for a free thread.
ConcurrencyGate reserves a slot before cloning the test plan, so a
capped arrival waits instead of allocating a clone, which bounds memory
by the limit. An arrival that still has no free thread when the schedule
ends is dropped rather than stretching the test past its nominal end.

Add openmodel.max_concurrency as an installation-wide safety net for
schedules that set no concurrency(...) of their own; it defaults to
unlimited to preserve existing behavior.

Closes apache#5966

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The schedule preview only drew the target arrival rate. Show the
concurrency limit too, without overloading a single axis: rate and
thread count are different units, so a shared or secondary Y axis would
invite a false visual correlation.

A single limit that applies from the start is shown as a "Max threads: N"
subtitle. A limit that changes over time is drawn as a second panel
stacked below the rate, sharing the time axis (gggrid sharex + align).
The limit uses geomStep, not geomLine, because it is piecewise-constant
and jumps between windows rather than ramping.

Extract TargetRateChart.buildSpec so the lets-plot specification can be
tested without the Swing/Batik rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant