Fixed issue where transaction sample elapsed time was incorrectly inflated when a thread was stopped - #6770
Fixed issue where transaction sample elapsed time was incorrectly inflated when a thread was stopped#6770ruthst00 wants to merge 1 commit into
Conversation
vlsi
left a comment
There was a problem hiding this comment.
Both new tests pass without the production changes. I reverted TransactionController.java, TransactionSampler.java, and JMeterThread.java to the base commit (ae8f85e), kept the new tests, and TestTransactionController stayed 3/3 green. The tests need to be reworked so they fail on the base commit; please paste that failing run in the PR.
Why the current tests cannot see the bug:
CollectSamplesListenerkeeps a reference to theSampleResult, and the result is mutated after the listeners are notified. Afterrunningbecomesfalse,JMeterThread.run()still callsthreadGroupLoopController.next(), which reachesTransactionController.nextWithTransactionSampler(), and that callssetTransactionDone()on the result that was already reported. AResultCollectorwrites the inflated values at notification time, while a test that reads the result afterwards sees the corrected values. The test listener has to recordgetTime(),getIdleTime(), andgetResponseMessage()insidesampleOccurred.HashTree.add(key, value)isadd(key).add(value), sohashTree.add(sampler, timer)addssampleras a new top-level node and hangs the timer there. Build the tree through the returned subtrees instead:HashTree tcTree = tree.add(loop).add(transactionController); tcTree.add(firstSampler).add(firstTimer); tcTree.add(secondSampler).add(secondTimer);.
A test that does fail on the base commit: parent mode, includeTimers=false, a 500 ms timer before the first sampler, a 5000 ms timer before the second one, and a scheduler end time 1500 ms ahead. On the base commit the snapshot taken in sampleOccurred is t=520 it=0 rm=""; with this PR it is t=10 it=508.
Other items:
- The change in
TransactionController.triggerEndOfLoop()has no test.triggerEndOfLoop()is called only fromcontinueOnCurrentLoop,breakOnCurrentLoop, andcontinueOnThreadLoop(a failed sample with "Start Next Thread Loop", a Flow Control Action), so the test needs one of those, for example a Flow Control Action "Start next thread loop" with a timer, inside a non-parent Transaction Controller. Alternatively, move this change to a separate PR, since #6496 is about parent mode. - An interrupted transaction now gets
Number of samples in transaction : N, number of failing samples : Mas its response message and200as its response code, where it used to have an empty message.TransactionController.isFromTransactionController()therefore starts returningtruefor it, which changes whatSummariser(summariser.ignore_transaction_controller_sample_result),ResultSaver(ignoreTC), and the Backend Listener'sSamplerMetricdo with these samples. Please add an entry toxdocs/changes.xmlthat names both the elapsed-time fix and this change. - An interrupted transaction is still reported as successful, and its elapsed time is now the sum of the children that ran (for example 2 of 3), so it looks faster than a complete one. The issue suggests marking it as failed instead. Please state in the PR which behavior is intended and why, so it can be decided explicitly.
- Please drop the
.github/workflows/gradle-wrapper-validation.ymlchange from this PR. It is unrelated to #6496 and belongs in its own PR (the same edit is in #6773). - Commit messages: the subjects are cut off mid-word ("…inf…", "…becau…"), use past tense, and run past 72 characters. Please use an imperative subject, such as
Exclude timer delays from the elapsed time of a transaction interrupted by thread stop, and put the details in the body. The PR title has the same truncation.
| public void triggerEndOfLoop() { | ||
| if(!isGenerateParentSample()) { | ||
| if (res != null) { | ||
| // See BUG 55816 / GitHub issue #6496 |
There was a problem hiding this comment.
This comment is incorrect: triggerEndOfLoop() is not called when the thread stops. It is called from JMeterThread.continueOnCurrentLoop, breakOnCurrentLoop, and continueOnThreadLoop, that is, when a failed sample triggers "Start Next Thread Loop" or a Flow Control Action ends the loop. In non-parent mode, a thread stop emits no transaction sample at all.
Please describe the actual case, for example: "The loop ends early, so the time since the last child sample ended counts as idle time, as it does in nextWithoutTransactionSampler()." Please also cover this path with a test (see the review summary).
nextWithoutTransactionSampler() now has the same four lines. Please extract them into a private method so the two paths cannot drift apart.
| } | ||
|
|
||
| protected void setTransactionDone() { | ||
| public void setTransactionDone() { |
There was a problem hiding this comment.
This makes setTransactionDone() public API, while calling it at the wrong moment finalizes a transaction that is still running. Please mark it @API(status = API.Status.INTERNAL, since = "6.0.0") (apiguardian is already used in src/core), or add a narrower public method that JMeterThread calls to end an interrupted transaction.
A subclass that overrides this method as protected stops compiling after this change; please mention that in changes.xml under incompatible changes, or avoid widening this method.
| && transactionResult == null | ||
| && transactionSampler != null | ||
| && transactionPack != null) { | ||
| // Thread was stopped mid-transaction (e.g. during ramp-down). |
There was a problem hiding this comment.
- Bug 55816 is about non-parent mode (the time after the last child sample); it has nothing to do with this parent-mode path. Please drop that reference.
- "Ensure setTransactionDone() is called" narrates the next line. Please state the rule instead, for example: "A transaction interrupted by a thread stop reports the sum of its children as elapsed time; the time since the last child ended counts as idle time."
- The issue number should not carry the meaning; the sentence has to state the fact without it.
| // Thread was stopped mid-transaction (e.g. during ramp-down). | ||
| // Ensure setTransactionDone() is called so that elapsed time and idle time | ||
| // are correctly computed (see GitHub issue #6496 / Bug 55816). | ||
| if (!transactionSampler.isTransactionDone()) { |
There was a problem hiding this comment.
This check is always true here: this branch runs only when transactionResult == null, and a transaction that was already done set transactionResult in the first branch of this method, unless doEndTransactionSampler threw. Either drop the check, or keep it and say which case it guards.
| steps: | ||
| - uses: actions/checkout@v6 | ||
| - uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 | ||
| - uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0 |
There was a problem hiding this comment.
Unrelated to #6496. Please move this to its own PR.
| // not inflated by the timer delay between samples. | ||
| // Both child samples have very short simulated elapsed times (10ms each), | ||
| // so the total should be well under the timer delay (200ms). | ||
| assertTrue(transactionResult.getTime() < timerDelayMs, |
There was a problem hiding this comment.
time < timerDelayMs would still pass if the transaction included half the timer. The children have fixed elapsed times, so the expected value is known: please use assertEquals(2 * childElapsedMs, transactionResult.getTime(), ...) (and assert the idle time as well, since the fix moves the delay there). The value must be captured inside the listener's sampleOccurred, because the SampleResult is mutated after notification (see the review summary).
| * elapsed time should equal the child sample elapsed time, not be inflated by the timer. | ||
| */ | ||
| @Test | ||
| public void testIssue6496ParentMode() throws Exception { |
There was a problem hiding this comment.
This test passes on the base commit and does not reproduce the scenario from #6496. The 5000 ms timer runs before the only sampler, TimerService.adjustDelay returns -1 at once, and running becomes false before any child runs. The transaction has zero children and an elapsed time of 0 with or without the fix.
The bug needs at least one child that ran after a delay, and the stop has to happen on the delay before the next child: for example a 500 ms timer on the first sampler, a 5000 ms timer on the second one, and setEndTime(now + 1500). The expected elapsed time is then the first child's time, and the idle time holds the 500 ms delay.
Please also rename the test after the behavior, for example aTransactionInterruptedByTheSchedulerExcludesTimerDelayFromElapsedTime.
| hashTree.add(loop, transactionController); | ||
| hashTree.add(transactionController, listener); | ||
| hashTree.add(transactionController, sampler); | ||
| hashTree.add(sampler, timer); |
There was a problem hiding this comment.
Same tree-building issue as in the other test: hashTree.add(sampler, timer) creates a top-level copy of sampler. Please use tcTree.add(sampler).add(timer).
| // The last transaction event is the one interrupted during ramp-down. | ||
| // Its elapsed time should be close to the child sample elapsed time, | ||
| // not inflated by the long timer delay. | ||
| SampleResult lastTransaction = listener.getEvents().get(listener.getEvents().size() - 1).getResult(); |
There was a problem hiding this comment.
CollectSamplesListener keeps a reference to the SampleResult, and after the thread stops, JMeterThread.run() still calls threadGroupLoopController.next(), which calls setTransactionDone() on this same object. The values read here are therefore the ones computed after notification, not the ones a ResultCollector writes to the file. Please record getTime(), getIdleTime(), and getResponseMessage() inside sampleOccurred and assert on that snapshot.
| SampleResult lastTransaction = listener.getEvents().get(listener.getEvents().size() - 1).getResult(); | ||
| assertTrue(TransactionController.isFromTransactionController(lastTransaction), | ||
| "Result should be from TransactionController"); | ||
| assertTrue(lastTransaction.getTime() < timerDelayMs, |
There was a problem hiding this comment.
Same as in the non-parent test: please assert the exact expected elapsed time and idle time with assertEquals instead of < timerDelayMs. The assertTrue(isFromTransactionController(...)) above prints nothing useful on failure either; asserting the response message with assertEquals shows what was received.
…y thread stop When a thread is stopped mid-transaction (e.g. during scheduler ramp-down) while sleeping through a timer delay, the TransactionSampler's elapsed time was inflated by the full timer delay rather than reflecting only the time spent executing child samplers. Root cause (parent mode): JMeterThread called setTransactionDone() after notifyListeners() had already fired, so the idle-time correction was applied to a result that had already been reported to listeners. Fix: call setTransactionDone() before doEndTransactionSampler() so the result is fully populated before it is broadcast. Root cause (non-parent mode / triggerEndOfLoop): the time elapsed since the last child sample ended was not added to pauseTime before sampleEnd(), so the timer delay was counted as active sampling time. Fix: accumulate the gap into pauseTime in triggerEndOfLoop() when includeTimers is false. Side-effect: interrupted transactions now receive the standard response message 'Number of samples in transaction : N, number of failing samples : M' and response code 200, so TransactionController.isFromTransactionController() returns true for them. This changes how Summariser, ResultSaver, and the Backend Listener's SamplerMetric handle these samples (see changes.xml). Fixes: apache#6496
9792721 to
cfb87d1
Compare
|
@vlsi here is a checklist of every change you requested, and whether it was addressed in the pushed commit ( ✅ Tests capture values inside ✅ HashTree construction fixed — Done. All tests now use the subtree-chaining form ( ✅ Tests fail on base commit — Verified. Running the tests against ✅ Drop ✅ Commit message — Done. Imperative subject under 72 chars, details in body. ✅
|
Description
Root Cause
Parent mode (
Generate parent sample = true): When a thread was stopped mid-transaction (e.g., scheduler end time reached),JMeterThread.processSampler()calleddoEndTransactionSampler()directly without first callingTransactionSampler.setTransactionDone(). This meant the transaction result's end time and idle time were never properly computed — the elapsed time was set byaddSubResult()tolastChildEndTime - startTime, which included any timer delays between child samples.Non-parent mode (
Generate parent sample = false):TransactionController.triggerEndOfLoop()(called during error/loop-restart scenarios) calledres.sampleEnd()without first accounting for the time elapsed since the last child sample ended. This time (which could include a timer delay) was incorrectly included in the transaction elapsed time.Changes
JMeterThread.javaWhen
runningbecomesfalsemid-transaction (scheduler ramp-down),JMeterThread.processSampler()now callstransactionSampler.setTransactionDone()beforedoEndTransactionSampler(). PreviouslysetTransactionDone()was never called in this path, so the idle-time correction (excluding timer delays from elapsed time) was never applied to the result before it was broadcast to listeners.TransactionSampler.javasetTransactionDone()visibility changed fromprotectedtopublicsoJMeterThreadcan call it directly in the mid-transaction stop path.TransactionController.javaIn
triggerEndOfLoop()(non-parent / additional-sample mode), whenincludeTimers=false, the gap betweenprevEndTimeand now is added topauseTimebeforesampleEnd()is called. Without this, the time spent sleeping through a timer at the moment the thread was stopped was counted as active sampling time rather than idle time.Motivation and Context
Transaction sample elapsed time was incorrectly inflated by timer/pause delays when a thread was stopped mid-transaction (e.g., during ramp-down).
Fixes GitHub issue #6496
Generated with Claude Sonnet 4.6 via Cline API Provider
How Has This Been Tested?
TestTransactionController.javaComplete rewrite of the test class:
CollectSamplesListener(which stores a live reference toSampleResult) with a newSnapshotListenerthat capturestime,idleTime, andresponseMessageinsidesampleOccurred(), so later mutations of the result object don't corrupt the recorded values.FixedElapsedTimeSamplerandFixedDelayTimerinner classes for deterministic test control.HashTreeconstruction throughout — usestree.add(loop).add(tc)subtree chaining instead of the brokenhashTree.add(sampler, timer)form.testIssue6496ParentMode: scheduler end time 1500 ms, 500 ms first timer + 10 ms sampler complete, then 5000 ms second timer is cut short. Asserts the transaction snapshot hastime < 5000 msandidleTime > 0. Fails on base commit, passes with fix.testIssue6496NonParentMode: same setup in non-parent mode, exercises thetriggerEndOfLoop()path. Fails on base commit, passes with fix.testIssue57958: updated to useSnapshotListenerand correctHashTreeconstruction.Screenshots (if appropriate):
Types of changes
Checklist:
Generated with Claude Sonnet via Cline