Skip to content

Commit c9c8ea9

Browse files
committed
fix(http): suppress spurious "Existing CookieManager superseded by" warnings across iterations (#6457)
Since 5.6, HTTPSamplerBase.addTestElement routed CookieManager, CacheManager, AuthManager and DNSCacheManager through their public setXxxManager methods, which unconditionally log a warning whenever the existing manager is non-null. TestCompiler calls addTestElement on every thread-group iteration after clearTestElementChildren, but clearTestElementChildren only clears the HeaderManager property, so the replace-mode manager references survive across iterations and the warning fires N-1 times for N iterations. Fix: - B: addTestElement now routes replace-mode managers through their private setXxxManagerProperty methods, mirroring the existing KeystoreConfig path that was already exempted. - C: the public setXxxManager methods now suppress the warning when the new value is the same instance as the existing one (defensive guard for direct callers). Genuine misuse (attaching a *different* manager of the same type) still produces the warning, so the diagnostic value is preserved. Added ReproIssue6457Test with 17 cases covering: addTestElement x5 iterations for each of the 5 managers, same-instance setter call, different-instance setter call (must still warn), HeaderManager merge path, and an end-to-end smoke test with all 6 managers across 10 iterations. Before fix: 17 tests, 10 failed; 62 "superseded by" log lines. After fix: 17 tests, 0 failed; 5 "superseded by" log lines (expected, from different-instance setter calls only).
1 parent ad6ecbd commit c9c8ea9

3 files changed

Lines changed: 302 additions & 9 deletions

File tree

src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/sampler/HTTPSamplerBase.java

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -730,15 +730,20 @@ public boolean hasArguments() {
730730
@Override
731731
public void addTestElement(TestElement el) {
732732
if (el instanceof CookieManager cookieManager) {
733-
setCookieManager(cookieManager);
733+
// Route through the private property setter to skip the "superseded by" warning:
734+
// addTestElement is invoked on every thread-group iteration by TestCompiler,
735+
// and the config element is the same instance as the one already attached,
736+
// so the warning would always fire spuriously. Users still get the warning
737+
// when they explicitly call setCookieManager with a different instance.
738+
setCookieManagerProperty(cookieManager);
734739
} else if (el instanceof CacheManager cacheManager) {
735-
setCacheManager(cacheManager);
740+
setCacheManagerProperty(cacheManager);
736741
} else if (el instanceof HeaderManager headerManager) {
737742
setHeaderManager(headerManager);
738743
} else if (el instanceof AuthManager authManager) {
739-
setAuthManager(authManager);
744+
setAuthManagerProperty(authManager);
740745
} else if (el instanceof DNSCacheManager dnsCacheManager) {
741-
setDNSResolver(dnsCacheManager);
746+
setDNSResolverProperty(dnsCacheManager);
742747
} else if (el instanceof KeystoreConfig keystoreConfig) {
743748
setKeystoreConfigProperty(keystoreConfig);
744749
} else {
@@ -923,13 +928,19 @@ public boolean getPostBodyRaw() {
923928
return get(getSchema().getPostBodyRaw());
924929
}
925930

931+
@SuppressWarnings("ReferenceEquality")
926932
public void setAuthManager(AuthManager value) {
927933
AuthManager mgr = getAuthManager();
928-
if (mgr != null) {
934+
if (mgr != null && mgr != value) {
929935
if(log.isWarnEnabled()) {
930936
log.warn("Existing AuthManager {} superseded by {}", mgr.getName(), value.getName());
931937
}
932938
}
939+
setAuthManagerProperty(value);
940+
}
941+
942+
// private method to allow AsyncSample and addTestElement to reset the value without performing checks
943+
private void setAuthManagerProperty(AuthManager value) {
933944
set(getSchema().getAuthManager(), value);
934945
}
935946

@@ -961,9 +972,10 @@ private void setCookieManagerProperty(CookieManager value) {
961972
set(getSchema().getCookieManager(), value);;
962973
}
963974

975+
@SuppressWarnings("ReferenceEquality")
964976
public void setCookieManager(CookieManager value) {
965977
CookieManager mgr = getCookieManager();
966-
if (mgr != null) {
978+
if (mgr != null && mgr != value) {
967979
if(log.isWarnEnabled()) {
968980
log.warn("Existing CookieManager {} superseded by {}", mgr.getName(), value.getName());
969981
}
@@ -983,9 +995,10 @@ private void setKeystoreConfigProperty(KeystoreConfig value) {
983995
set(getSchema().getKeystoreConfig(), value);
984996
}
985997

998+
@SuppressWarnings("ReferenceEquality")
986999
public void setKeystoreConfig(KeystoreConfig value) {
9871000
KeystoreConfig mgr = getKeystoreConfig();
988-
if (mgr != null && log.isWarnEnabled()) {
1001+
if (mgr != null && mgr != value && log.isWarnEnabled()) {
9891002
log.warn("Existing KeystoreConfig {} superseded by {}", mgr.getName(), value.getName());
9901003
}
9911004
setKeystoreConfigProperty(value);
@@ -995,9 +1008,10 @@ public KeystoreConfig getKeystoreConfig() {
9951008
return getOrNull(getSchema().getKeystoreConfig());
9961009
}
9971010

1011+
@SuppressWarnings("ReferenceEquality")
9981012
public void setCacheManager(CacheManager value) {
9991013
CacheManager mgr = getCacheManager();
1000-
if (mgr != null) {
1014+
if (mgr != null && mgr != value) {
10011015
if(log.isWarnEnabled()) {
10021016
log.warn("Existing CacheManager {} superseded by {}", mgr.getName(), value.getName());
10031017
}
@@ -1013,13 +1027,19 @@ public DNSCacheManager getDNSResolver() {
10131027
return getOrNull(getSchema().getDnsCacheManager());
10141028
}
10151029

1030+
@SuppressWarnings("ReferenceEquality")
10161031
public void setDNSResolver(DNSCacheManager cacheManager) {
10171032
DNSCacheManager mgr = getDNSResolver();
1018-
if (mgr != null) {
1033+
if (mgr != null && mgr != cacheManager) {
10191034
if(log.isWarnEnabled()) {
10201035
log.warn("Existing DNSCacheManager {} superseded by {}", mgr.getName(), cacheManager.getName());
10211036
}
10221037
}
1038+
setDNSResolverProperty(cacheManager);
1039+
}
1040+
1041+
// private method to allow addTestElement to reset the value without performing checks
1042+
private void setDNSResolverProperty(DNSCacheManager cacheManager) {
10231043
set(getSchema().getDnsCacheManager(), cacheManager);
10241044
}
10251045

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.jmeter.protocol.http.sampler;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertNotSame;
23+
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
25+
import java.io.IOException;
26+
import java.nio.charset.StandardCharsets;
27+
import java.nio.file.Files;
28+
import java.nio.file.Path;
29+
import java.nio.file.Paths;
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
import java.util.stream.Stream;
33+
34+
import org.apache.jmeter.config.KeystoreConfig;
35+
import org.apache.jmeter.protocol.http.control.AuthManager;
36+
import org.apache.jmeter.protocol.http.control.CacheManager;
37+
import org.apache.jmeter.protocol.http.control.CookieManager;
38+
import org.apache.jmeter.protocol.http.control.DNSCacheManager;
39+
import org.apache.jmeter.protocol.http.control.HeaderManager;
40+
import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui;
41+
import org.apache.jmeter.testelement.TestElement;
42+
import org.junit.jupiter.api.BeforeEach;
43+
import org.junit.jupiter.api.Test;
44+
import org.junit.jupiter.params.ParameterizedTest;
45+
import org.junit.jupiter.params.provider.Arguments;
46+
import org.junit.jupiter.params.provider.MethodSource;
47+
48+
/**
49+
* Regression tests for issue #6457: spurious "Existing XxxManager ... superseded by ..."
50+
* warnings on every thread-group iteration after the first one.
51+
*
52+
* <p>Root cause: {@link HTTPSamplerBase#addTestElement(TestElement)} routed CookieManager,
53+
* CacheManager, AuthManager and DNSCacheManager through their public {@code setXxxManager}
54+
* methods, which log a warning whenever the existing manager is non-null. Since
55+
* {@code clearTestElementChildren()} only clears the HeaderManager property, the manager
56+
* reference survives across iterations and the warning fires N-1 times for N iterations.</p>
57+
*
58+
* <p>Fix (B+C):</p>
59+
* <ul>
60+
* <li>B: {@code addTestElement} now routes replace-mode managers through their private
61+
* {@code setXxxManagerProperty} methods, mirroring the existing KeystoreConfig path.</li>
62+
* <li>C: the public {@code setXxxManager} methods now suppress the warning when the new
63+
* value is the same instance as the existing one (defensive guard for direct calls).</li>
64+
* </ul>
65+
*
66+
* <p>These tests verify both the {@code addTestElement} path (used by TestCompiler on each
67+
* iteration) and the public setter path. Warnings are captured by reading the log4j2-managed
68+
* {@code jmeter.log} file, which is the same appender used at runtime.</p>
69+
*/
70+
public class ReproIssue6457Test {
71+
72+
private static final String LOG_FILE = "jmeter.log";
73+
74+
private HTTPSamplerBase sampler;
75+
76+
@BeforeEach
77+
public void setUp() {
78+
sampler = (HTTPSamplerBase) new HttpTestSampleGui().createTestElement();
79+
}
80+
81+
/**
82+
* Provides the 5 replace-mode managers covered by the fix.
83+
* Each argument is: (manager factory, display name).
84+
*/
85+
static Stream<Arguments> replaceModeManagers() {
86+
return Stream.of(
87+
Arguments.of((ManagerFactory<CookieManager>) CookieManager::new, "CookieManager"),
88+
Arguments.of((ManagerFactory<CacheManager>) CacheManager::new, "CacheManager"),
89+
Arguments.of((ManagerFactory<AuthManager>) AuthManager::new, "AuthManager"),
90+
Arguments.of((ManagerFactory<DNSCacheManager>) DNSCacheManager::new, "DNSCacheManager"),
91+
Arguments.of((ManagerFactory<KeystoreConfig>) KeystoreConfig::new, "KeystoreConfig")
92+
);
93+
}
94+
95+
/**
96+
* Scenario 1: simulate N thread-group iterations via {@code addTestElement}.
97+
* Expected: zero "superseded by" warnings across all iterations (issue #6457 regression).
98+
*/
99+
@ParameterizedTest(name = "{1}: addTestElement x5 should produce no warning")
100+
@MethodSource("replaceModeManagers")
101+
public <T extends TestElement> void addTestElementAcrossIterationsShouldNotWarn(
102+
ManagerFactory<T> factory, String displayName) throws IOException {
103+
T manager = factory.create();
104+
manager.setName(displayName + "-UnderTest");
105+
106+
long baseline = countSupersededWarningsInLog();
107+
for (int i = 1; i <= 5; i++) {
108+
sampler.addTestElement(manager);
109+
}
110+
long delta = countSupersededWarningsInLog() - baseline;
111+
assertEquals(0L, delta,
112+
"[" + displayName + "] 5 addTestElement iterations produced " + delta
113+
+ " spurious 'superseded by' warnings (expected 0)");
114+
}
115+
116+
/**
117+
* Scenario 2: public setter called with the SAME instance should not warn (defensive guard).
118+
* This protects against any caller that re-assigns the same manager instance.
119+
*/
120+
@ParameterizedTest(name = "{1}: setXxx(sameInstance) should not warn")
121+
@MethodSource("replaceModeManagers")
122+
public <T extends TestElement> void setXxxManagerWithSameInstanceShouldNotWarn(
123+
ManagerFactory<T> factory, String displayName) throws IOException {
124+
T manager = factory.create();
125+
manager.setName(displayName + "-Same");
126+
127+
// First call: sets the manager (no prior value, no warning expected)
128+
long baseline = countSupersededWarningsInLog();
129+
setManagerOnSampler(displayName, manager);
130+
long deltaAfterFirst = countSupersededWarningsInLog() - baseline;
131+
assertEquals(0L, deltaAfterFirst,
132+
"[" + displayName + "] initial setXxxManager should not warn");
133+
134+
// Second call with the same instance: must not warn (this is the fix C)
135+
setManagerOnSampler(displayName, manager);
136+
long deltaAfterSecond = countSupersededWarningsInLog() - baseline;
137+
assertEquals(0L, deltaAfterSecond,
138+
"[" + displayName + "] setXxxManager with same instance should not warn (fix C)");
139+
}
140+
141+
/**
142+
* Scenario 3: public setter called with a DIFFERENT instance must still warn, so that
143+
* genuine misuses (user attaching multiple managers of the same type) are still detected.
144+
*/
145+
@ParameterizedTest(name = "{1}: setXxx(differentInstance) should still warn")
146+
@MethodSource("replaceModeManagers")
147+
public <T extends TestElement> void setXxxManagerWithDifferentInstanceShouldStillWarn(
148+
ManagerFactory<T> factory, String displayName) throws IOException {
149+
T first = factory.create();
150+
first.setName(displayName + "-First");
151+
T second = factory.create();
152+
second.setName(displayName + "-Second");
153+
assertNotSame(first, second, "test fixture: two distinct instances expected");
154+
155+
setManagerOnSampler(displayName, first);
156+
long baseline = countSupersededWarningsInLog();
157+
setManagerOnSampler(displayName, second);
158+
long delta = countSupersededWarningsInLog() - baseline;
159+
assertTrue(delta >= 1,
160+
"[" + displayName + "] setXxxManager with a different instance must still warn"
161+
+ " (got " + delta + " warnings, expected >= 1)");
162+
}
163+
164+
/**
165+
* Scenario 4: HeaderManager uses merge (not replace) and is the original target of
166+
* {@code clearTestElementChildren}. It must not produce "superseded by" warnings either,
167+
* and the merge accumulation behaviour must keep working across iterations.
168+
*/
169+
@Test
170+
public void headerManagerAddTestElementShouldNotWarnAndShouldMerge() throws IOException {
171+
HeaderManager hm = new HeaderManager();
172+
hm.setName("HeaderManager-UnderTest");
173+
174+
long baseline = countSupersededWarningsInLog();
175+
for (int i = 1; i <= 5; i++) {
176+
sampler.addTestElement(hm);
177+
}
178+
long delta = countSupersededWarningsInLog() - baseline;
179+
assertEquals(0L, delta,
180+
"HeaderManager 5 addTestElement iterations produced " + delta
181+
+ " 'superseded by' warnings (expected 0)");
182+
// HeaderManager is cleared each iteration and re-merged, so it must remain attached.
183+
assertNotNull(sampler.getHeaderManager(),
184+
"HeaderManager should remain attached after iterations");
185+
}
186+
187+
/**
188+
* Scenario 5: end-to-end smoke test that combines all 6 managers in a single sampler
189+
* across 10 iterations. This mirrors the real-world pattern where a Thread Group has
190+
* multiple config elements as children.
191+
*/
192+
@Test
193+
public void allManagersTogetherAcrossIterationsShouldNotWarn() throws IOException {
194+
CookieManager cookieManager = new CookieManager();
195+
cookieManager.setName("HTTP Cookie Manager");
196+
CacheManager cacheManager = new CacheManager();
197+
cacheManager.setName("HTTP Cache Manager");
198+
AuthManager authManager = new AuthManager();
199+
authManager.setName("HTTP Authorization Manager");
200+
DNSCacheManager dnsCacheManager = new DNSCacheManager();
201+
dnsCacheManager.setName("DNS Cache Manager");
202+
KeystoreConfig keystoreConfig = new KeystoreConfig();
203+
keystoreConfig.setName("Keystore Config");
204+
HeaderManager headerManager = new HeaderManager();
205+
headerManager.setName("HTTP Header Manager");
206+
207+
long baseline = countSupersededWarningsInLog();
208+
for (int i = 1; i <= 10; i++) {
209+
sampler.addTestElement(cookieManager);
210+
sampler.addTestElement(cacheManager);
211+
sampler.addTestElement(authManager);
212+
sampler.addTestElement(dnsCacheManager);
213+
sampler.addTestElement(keystoreConfig);
214+
sampler.addTestElement(headerManager);
215+
}
216+
long delta = countSupersededWarningsInLog() - baseline;
217+
assertEquals(0L, delta,
218+
"10 iterations with 6 managers produced " + delta
219+
+ " 'superseded by' warnings (expected 0)");
220+
221+
// Sanity: managers are still attached
222+
assertNotNull(sampler.getCookieManager());
223+
assertNotNull(sampler.getCacheManager());
224+
assertNotNull(sampler.getAuthManager());
225+
assertNotNull(sampler.getDNSResolver());
226+
assertNotNull(sampler.getKeystoreConfig());
227+
assertNotNull(sampler.getHeaderManager());
228+
}
229+
230+
// ---------------- helpers ----------------
231+
232+
/**
233+
* Routes the given manager to the appropriate public setter on the sampler.
234+
* Used by the parameterized tests so each manager type is exercised uniformly.
235+
*/
236+
private void setManagerOnSampler(String displayName, TestElement manager) {
237+
switch (displayName) {
238+
case "CookieManager" -> sampler.setCookieManager((CookieManager) manager);
239+
case "CacheManager" -> sampler.setCacheManager((CacheManager) manager);
240+
case "AuthManager" -> sampler.setAuthManager((AuthManager) manager);
241+
case "DNSCacheManager" -> sampler.setDNSResolver((DNSCacheManager) manager);
242+
case "KeystoreConfig" -> sampler.setKeystoreConfig((KeystoreConfig) manager);
243+
default -> throw new IllegalArgumentException("Unknown manager type: " + displayName);
244+
}
245+
}
246+
247+
private long countSupersededWarningsInLog() throws IOException {
248+
return readSupersededLinesFromLog().size();
249+
}
250+
251+
private List<String> readSupersededLinesFromLog() throws IOException {
252+
Path p = Paths.get(LOG_FILE);
253+
if (!Files.exists(p)) {
254+
return new ArrayList<>();
255+
}
256+
List<String> hits = new ArrayList<>();
257+
for (String line : Files.readAllLines(p, StandardCharsets.UTF_8)) {
258+
if (line.contains("superseded by")) {
259+
hits.add(line);
260+
}
261+
}
262+
return hits;
263+
}
264+
265+
@FunctionalInterface
266+
interface ManagerFactory<T extends TestElement> {
267+
T create();
268+
}
269+
}

xdocs/changes.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ Summary
116116
<li><pr>6620</pr>Fix report generation paths so dashboard output files are created in the correct location after internal refactoring.</li>
117117
<li><bug>6456</bug>Handle malformed percent-encoded URLs gracefully when recording HTTP traffic, logging a warning instead of failing the recording.</li>
118118
</ul>
119+
<h3>HTTP Samplers and Test Script Recorder</h3>
120+
<ul>
121+
<li><issue>6457</issue>Suppress spurious <code>Existing CookieManager &hellip; superseded by &hellip;</code> (and analogous <code>CacheManager</code>, <code>AuthManager</code>, <code>DNSCacheManager</code>, <code>KeystoreConfig</code>) warnings that fired on every thread-group iteration after the first one. <code>addTestElement</code> now routes replace-mode managers through their private property setters (mirroring the existing <code>KeystoreConfig</code> path), and the public setters no longer warn when the new value is the same instance as the existing one.</li>
122+
</ul>
119123
<!-- =================== Thanks =================== -->
120124

121125
<ch_section>Thanks</ch_section>

0 commit comments

Comments
 (0)