Skip to content

Commit 5ff7208

Browse files
committed
Avoid wrong results when Object.hashCode() collides, use IdentityHashMap instead of HashMap when key is TestElement
See #693
1 parent 7c16034 commit 5ff7208

File tree

19 files changed

+250
-57
lines changed

19 files changed

+250
-57
lines changed

src/components/src/main/java/org/apache/jmeter/config/CSVDataSet.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.jmeter.testbeans.gui.GenericTestBeanCustomizer;
3636
import org.apache.jmeter.testelement.property.JMeterProperty;
3737
import org.apache.jmeter.testelement.property.StringProperty;
38+
import org.apache.jmeter.threads.AbstractThreadGroup;
3839
import org.apache.jmeter.threads.JMeterContext;
3940
import org.apache.jmeter.threads.JMeterVariables;
4041
import org.apache.jmeter.util.JMeterUtils;

src/components/src/main/java/org/apache/jmeter/timers/ConstantThroughputTimer.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.jmeter.threads.AbstractThreadGroup;
3838
import org.apache.jmeter.threads.JMeterContextService;
3939
import org.apache.jmeter.util.JMeterUtils;
40+
import org.apache.jorphan.collections.IdentityKey;
4041
import org.slf4j.Logger;
4142
import org.slf4j.LoggerFactory;
4243

@@ -103,10 +104,10 @@ public String toString() {
103104
private static final ThroughputInfo allThreadsInfo = new ThroughputInfo();
104105

105106
//For holding the ThroughputInfo objects for all ThreadGroups. Keyed by AbstractThreadGroup objects
106-
private static final ConcurrentMap<AbstractThreadGroup, ThroughputInfo> threadGroupsInfoMap =
107+
//TestElements can't be used as keys in HashMap.
108+
private static final ConcurrentMap<IdentityKey<AbstractThreadGroup>, ThroughputInfo> threadGroupsInfoMap =
107109
new ConcurrentHashMap<>();
108110

109-
110111
/**
111112
* Constructor for a non-configured ConstantThroughputTimer.
112113
*/
@@ -198,13 +199,10 @@ private long calculateDelay() {
198199
case AllActiveThreadsInCurrentThreadGroup_Shared: //All threads in this group - alternate calculation
199200
final org.apache.jmeter.threads.AbstractThreadGroup group =
200201
JMeterContextService.getContext().getThreadGroup();
201-
ThroughputInfo groupInfo = threadGroupsInfoMap.get(group);
202+
IdentityKey<AbstractThreadGroup> key = new IdentityKey<>(group);
203+
ThroughputInfo groupInfo = threadGroupsInfoMap.get(key);
202204
if (groupInfo == null) {
203-
groupInfo = new ThroughputInfo();
204-
ThroughputInfo previous = threadGroupsInfoMap.putIfAbsent(group, groupInfo);
205-
if (previous != null) { // We did not replace the entry
206-
groupInfo = previous; // so use the existing one
207-
}
205+
groupInfo = threadGroupsInfoMap.computeIfAbsent(key, (k) -> new ThroughputInfo());
208206
}
209207
delay = calculateSharedDelay(groupInfo,Math.round(msPerRequest));
210208
break;

src/components/src/main/java/org/apache/jmeter/timers/poissonarrivals/PreciseThroughputTimer.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.jmeter.testelement.TestStateListener;
2929
import org.apache.jmeter.threads.AbstractThreadGroup;
3030
import org.apache.jmeter.timers.Timer;
31+
import org.apache.jorphan.collections.IdentityKey;
3132
import org.apache.jorphan.util.JMeterStopThreadException;
3233
import org.apiguardian.api.API;
3334
import org.slf4j.Logger;
@@ -44,7 +45,10 @@ public class PreciseThroughputTimer extends AbstractTestElement implements Clone
4445
private static final Logger log = LoggerFactory.getLogger(PreciseThroughputTimer.class);
4546

4647
private static final long serialVersionUID = 4;
47-
private static final ConcurrentMap<AbstractThreadGroup, EventProducer> groupEvents = new ConcurrentHashMap<>();
48+
49+
// TestElements can't be used as keys in a HashMap, so we use IdentityHashMap
50+
private static final ConcurrentMap<IdentityKey<AbstractThreadGroup>, EventProducer> groupEvents =
51+
new ConcurrentHashMap<>();
4852

4953
/**
5054
* Desired throughput configured as {@code throughput/throughputPeriod} per second.
@@ -134,9 +138,14 @@ public long delay() {
134138

135139
private EventProducer getEventProducer() {
136140
AbstractThreadGroup tg = getThreadContext().getThreadGroup();
141+
IdentityKey<AbstractThreadGroup> key = new IdentityKey<>(tg);
142+
EventProducer eventProducer = groupEvents.get(key);
143+
if (eventProducer != null) {
144+
return eventProducer;
145+
}
137146
Long seed = randomSeed == null || randomSeed == 0 ? null : randomSeed;
138147
return
139-
groupEvents.computeIfAbsent(tg, x -> new ConstantPoissonProcessGenerator(
148+
groupEvents.computeIfAbsent(key, x -> new ConstantPoissonProcessGenerator(
140149
() -> PreciseThroughputTimer.this.getThroughput() / throughputPeriod,
141150
batchSize, batchThreadDelay, this, seed, true));
142151
}

src/core/src/main/java/org/apache/jmeter/control/GenericController.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,9 @@
2121
import java.util.ArrayDeque;
2222
import java.util.ArrayList;
2323
import java.util.Deque;
24+
import java.util.IdentityHashMap;
2425
import java.util.Iterator;
2526
import java.util.List;
26-
import java.util.concurrent.ConcurrentHashMap;
27-
import java.util.concurrent.ConcurrentMap;
2827

2928
import org.apache.jmeter.engine.event.LoopIterationEvent;
3029
import org.apache.jmeter.engine.event.LoopIterationListener;
@@ -58,7 +57,7 @@ public class GenericController extends AbstractTestElement implements Controller
5857
private transient Deque<LoopIterationListener> iterationListeners = new ArrayDeque<>();
5958

6059
// Only create the map if it is required
61-
private transient ConcurrentMap<TestElement, Object> children = new ConcurrentHashMap<>();
60+
private transient IdentityHashMap<TestElement, Object> children = new IdentityHashMap<>();
6261

6362
private static final Object DUMMY = new Object();
6463

@@ -353,10 +352,13 @@ public void addTestElement(TestElement child) {
353352
* {@inheritDoc}
354353
*/
355354
@Override
355+
@SuppressWarnings("SynchronizeOnNonFinalField")
356356
public final boolean addTestElementOnce(TestElement child){
357-
if (children.putIfAbsent(child, DUMMY) == null) {
358-
addTestElement(child);
359-
return true;
357+
synchronized (children) {
358+
if (children.putIfAbsent(child, DUMMY) == null) {
359+
addTestElement(child);
360+
return true;
361+
}
360362
}
361363
return false;
362364
}
@@ -414,7 +416,7 @@ protected void resetIterCount() {
414416

415417
protected Object readResolve(){
416418
iterationListeners = new ArrayDeque<>();
417-
children = new ConcurrentHashMap<>();
419+
children = new IdentityHashMap<>();
418420
subControllersAndSamplers = new ArrayList<>();
419421

420422
return this;

src/core/src/main/java/org/apache/jmeter/gui/GuiPackage.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.ArrayList;
2525
import java.util.Collections;
2626
import java.util.HashMap;
27+
import java.util.IdentityHashMap;
2728
import java.util.List;
2829
import java.util.Map;
2930
import java.util.concurrent.atomic.AtomicInteger;
@@ -102,7 +103,7 @@ public final class GuiPackage implements LocaleChangeListener, HistoryListener {
102103
* to their corresponding GUI components.
103104
* <p>This enables to associate {@link UnsharedComponent} UIs with their {@link TestElement}.</p>
104105
*/
105-
private final Map<TestElement, JMeterGUIComponent> nodesToGui = new HashMap<>();
106+
private final IdentityHashMap<TestElement, JMeterGUIComponent> nodesToGui = new IdentityHashMap<>();
106107

107108
/**
108109
* Map from Class to JMeterGUIComponent, mapping the Class of a GUI

src/core/src/main/java/org/apache/jmeter/testbeans/gui/TestBeanGUI.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ public class TestBeanGUI extends AbstractJMeterGuiComponent implements JMeterGUI
112112
* large test plans.
113113
*/
114114
private final Cache<TestElement, Customizer> customizers =
115-
Caffeine.newBuilder() // TOOD: should this be made static?
115+
Caffeine.newBuilder() // TODO: should this be made static?
116+
.weakKeys() // So test elements are compared using identity == rather than .equals
116117
.maximumSize(20)
117118
.build();
118119

src/core/src/main/java/org/apache/jmeter/testelement/AbstractTestElement.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,12 @@ public boolean equals(Object o) {
122122
}
123123
}
124124

125-
// TODO temporary hack to avoid unnecessary bug reports for subclasses
126-
127125
/**
128126
* {@inheritDoc}
129127
*/
130128
@Override
131-
public int hashCode(){
132-
return System.identityHashCode(this);
129+
public int hashCode() {
130+
return propMap.hashCode();
133131
}
134132

135133
/*

src/core/src/main/java/org/apache/jmeter/threads/AbstractThreadGroup.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
import java.io.Serializable;
2121
import java.time.Duration;
22-
import java.util.concurrent.ConcurrentHashMap;
23-
import java.util.concurrent.ConcurrentMap;
22+
import java.util.IdentityHashMap;
2423
import java.util.concurrent.atomic.AtomicInteger;
2524

2625
import org.apache.jmeter.control.Controller;
@@ -51,7 +50,7 @@ public abstract class AbstractThreadGroup extends AbstractTestElement
5150
private static final long serialVersionUID = 240L;
5251

5352
// Only create the map if it is required
54-
private final transient ConcurrentMap<TestElement, Object> children = new ConcurrentHashMap<>();
53+
private final transient IdentityHashMap<TestElement, Object> children = new IdentityHashMap<>();
5554

5655
private static final Object DUMMY = new Object();
5756

@@ -136,9 +135,11 @@ public void addTestElement(TestElement child) {
136135
*/
137136
@Override
138137
public final boolean addTestElementOnce(TestElement child){
139-
if (children.putIfAbsent(child, DUMMY) == null) {
140-
addTestElement(child);
141-
return true;
138+
synchronized (children) {
139+
if (children.putIfAbsent(child, DUMMY) == null) {
140+
addTestElement(child);
141+
return true;
142+
}
142143
}
143144
return false;
144145
}

src/core/src/main/java/org/apache/jmeter/threads/TestCompiler.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@
1818
package org.apache.jmeter.threads;
1919

2020
import java.util.ArrayList;
21-
import java.util.HashMap;
2221
import java.util.HashSet;
22+
import java.util.IdentityHashMap;
2323
import java.util.LinkedList;
2424
import java.util.List;
2525
import java.util.ListIterator;
26-
import java.util.Map;
2726
import java.util.Set;
2827

2928
import org.apache.jmeter.assertions.Assertion;
@@ -69,10 +68,10 @@ public class TestCompiler implements HashTreeTraverser {
6968
// TODO: replace with ArrayDequeue
7069
private final LinkedList<TestElement> stack = new LinkedList<>();
7170

72-
private final Map<Sampler, SamplePackage> samplerConfigMap = new HashMap<>();
71+
private final IdentityHashMap<Sampler, SamplePackage> samplerConfigMap = new IdentityHashMap<>();
7372

74-
private final Map<TransactionController, SamplePackage> transactionControllerConfigMap =
75-
new HashMap<>();
73+
private final IdentityHashMap<TransactionController, SamplePackage> transactionControllerConfigMap =
74+
new IdentityHashMap<>();
7675

7776
private final HashTree testTree;
7877

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.jorphan.collections
19+
20+
import org.apache.jmeter.threads.AbstractThreadGroup
21+
import org.apache.jmeter.threads.ThreadGroup
22+
import org.junit.jupiter.api.Assertions
23+
import org.junit.jupiter.api.Test
24+
25+
class SearchByClassTest {
26+
@Test
27+
fun `search finds all matching elements`() {
28+
val tree = ListedHashTree()
29+
val count = 100000
30+
for (i in 0 until count) {
31+
tree.add(ThreadGroup())
32+
}
33+
val searcher = SearchByClass(
34+
AbstractThreadGroup::class.java
35+
)
36+
tree.traverse(searcher)
37+
Assertions.assertEquals(count, searcher.searchResults.size) {
38+
"Test plan included $count ThreadGroup elements, so SearchByClass should find all of them"
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)