Skip to content

Commit 0ec1714

Browse files
committed
Avoid wrong results when Object.hashCode() collides, use IdentityHashMap instead of HashMap when key is TestElement
See apache#693
1 parent 1ad164f commit 0ec1714

File tree

15 files changed

+119
-50
lines changed

15 files changed

+119
-50
lines changed

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
import java.beans.BeanInfo;
2121
import java.beans.IntrospectionException;
2222
import java.beans.Introspector;
23+
import java.util.Collections;
24+
import java.util.IdentityHashMap;
25+
import java.util.Map;
2326
import java.util.ResourceBundle;
24-
import java.util.concurrent.ConcurrentHashMap;
25-
import java.util.concurrent.ConcurrentMap;
2627

2728
import org.apache.jmeter.gui.GUIMenuSortOrder;
2829
import org.apache.jmeter.gui.TestElementMetadata;
@@ -103,8 +104,9 @@ 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-
new ConcurrentHashMap<>();
107+
//TestElements can't be used as keys in HashMap.
108+
private static final Map<AbstractThreadGroup, ThroughputInfo> threadGroupsInfoMap =
109+
Collections.synchronizedMap(new IdentityHashMap<>());
108110

109111

110112
/**

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
package org.apache.jmeter.timers.poissonarrivals;
1919

20-
import java.util.concurrent.ConcurrentHashMap;
21-
import java.util.concurrent.ConcurrentMap;
20+
import java.util.Collections;
21+
import java.util.IdentityHashMap;
22+
import java.util.Map;
2223
import java.util.concurrent.TimeUnit;
2324

2425
import org.apache.jmeter.gui.GUIMenuSortOrder;
@@ -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 Map<AbstractThreadGroup, EventProducer> groupEvents =
51+
Collections.synchronizedMap(new IdentityHashMap<>());
4852

4953
/**
5054
* Desired throughput configured as {@code throughput/throughputPeriod} per second.

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 Map<TestElement, JMeterGUIComponent> nodesToGui = new HashMap<>();
106+
private 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: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737

3838
import javax.swing.JPopupMenu;
3939

40-
import org.apache.commons.collections4.map.LRUMap;
4140
import org.apache.jmeter.assertions.Assertion;
4241
import org.apache.jmeter.assertions.gui.AbstractAssertionGui;
4342
import org.apache.jmeter.config.ConfigElement;
@@ -69,6 +68,9 @@
6968
import org.slf4j.Logger;
7069
import org.slf4j.LoggerFactory;
7170

71+
import com.github.benmanes.caffeine.cache.Cache;
72+
import com.github.benmanes.caffeine.cache.Caffeine;
73+
7274
/**
7375
* JMeter GUI element editing for TestBean elements.
7476
* <p>
@@ -109,7 +111,11 @@ public class TestBeanGUI extends AbstractJMeterGuiComponent implements JMeterGUI
109111
* needs to be limited, though, to avoid memory issues when editing very
110112
* large test plans.
111113
*/
112-
private final Map<TestElement, Customizer> customizers = new LRUMap<>(20);
114+
private final Cache<TestElement, Customizer> customizers =
115+
Caffeine.newBuilder()
116+
.weakKeys() // So test elements are compared using identity == rather than .equals
117+
.maximumSize(20)
118+
.build();
113119

114120
/** Index of the customizer in the JPanel's child component list: */
115121
private int customizerIndexInPanel;
@@ -325,7 +331,7 @@ private void setValues(TestElement element) {
325331
if (initialized){
326332
remove(customizerIndexInPanel);
327333
}
328-
Customizer c = customizers.computeIfAbsent(element, e -> {
334+
Customizer c = customizers.get(element, e -> {
329335
Customizer result = createCustomizer();
330336
result.setObject(propertyMap);
331337
return result;

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;
@@ -50,7 +49,7 @@ public abstract class AbstractThreadGroup extends AbstractTestElement
5049
private static final long serialVersionUID = 240L;
5150

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

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

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

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

src/core/src/main/java/org/apache/jmeter/util/JSR223TestElement.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ public abstract class JSR223TestElement extends ScriptingTestElement
5858
/**
5959
* Cache of compiled scripts
6060
*/
61+
@SuppressWarnings("RedundantTypeArguments")
6162
private static final Map<String, CompiledScript> compiledScriptsCache =
62-
Collections.synchronizedMap(
63+
// Type arguments are needed to workaround https://bugs.openjdk.org/browse/JDK-8288590
64+
Collections.<String, CompiledScript>synchronizedMap(
6365
new LRUMap<>(JMeterUtils.getPropDefault("jsr223.compiled_scripts_cache_size", 100)));
6466

6567
/** If not empty then script in ScriptText will be compiled and cached */

src/jorphan/src/main/java/org/apache/jorphan/collections/HashTree.java

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.Collection;
2626
import java.util.HashMap;
2727
import java.util.HashSet;
28+
import java.util.IdentityHashMap;
2829
import java.util.Map;
2930
import java.util.Set;
3031

@@ -54,7 +55,7 @@ public class HashTree implements Serializable, Map<Object, HashTree>, Cloneable
5455
private static final String FOUND = "found"; // $NON-NLS-1$
5556

5657
// N.B. The keys can be either JMeterTreeNode or TestElement
57-
protected final Map<Object, HashTree> data;
58+
protected final IdentityHashMap<Object, HashTree> data;
5859

5960
/**
6061
* Creates an empty new HashTree.
@@ -78,7 +79,7 @@ protected HashTree(Map<Object, HashTree> _map) {
7879
* name of the new top-level node
7980
*/
8081
public HashTree(Object key) {
81-
this(new HashMap<Object, HashTree>(), key);
82+
this(new IdentityHashMap<>(), key);
8283
}
8384

8485
/**
@@ -94,9 +95,17 @@ public HashTree(Object key) {
9495
*/
9596
private HashTree(Map<Object, HashTree> _map, Object key) {
9697
if(_map != null) {
97-
data = _map;
98+
if (_map instanceof IdentityHashMap) {
99+
data = (IdentityHashMap<Object, HashTree>) _map;
100+
} else {
101+
// Technically speaking, TestElements can't be placed in HashMapk keys,
102+
// so we have to convert the map to an IdentityHashMap.
103+
@SuppressWarnings("IdentityHashMapUsage")
104+
IdentityHashMap<Object, HashTree> identityMap = new IdentityHashMap<>(_map);
105+
data = identityMap;
106+
}
98107
} else {
99-
data = new HashMap<>();
108+
data = new IdentityHashMap<>();
100109
}
101110
if(key != null) {
102111
data.put(key, new HashTree());
@@ -213,7 +222,7 @@ public void add(HashTree newTree) {
213222
* a collection of objects to be added to the created HashTree.
214223
*/
215224
public HashTree(Collection<?> keys) {
216-
data = new HashMap<>();
225+
data = new IdentityHashMap<>();
217226
for (Object o : keys) {
218227
data.put(o, new HashTree());
219228
}
@@ -227,7 +236,7 @@ public HashTree(Collection<?> keys) {
227236
* array with names for the new top-level nodes
228237
*/
229238
public HashTree(Object[] keys) {
230-
data = new HashMap<>();
239+
data = new IdentityHashMap<>();
231240
for (Object key : keys) {
232241
data.put(key, new HashTree());
233242
}
@@ -873,7 +882,24 @@ protected HashTree getTreePath(Collection<?> treePath) {
873882
*/
874883
@Override
875884
public int hashCode() {
876-
return data.hashCode() * 7;
885+
int xor = 0;
886+
int sum = 0;
887+
int mul = 0;
888+
// Unordered hash function which uses reference equality for keys, and object equality for values
889+
// We use several commutative functions to reduce the possibility of collisions.
890+
// Note: IdentityHashMap.hashCode uses reference equality for both keys and values, so we can't use it.
891+
for (Entry<Object, HashTree> objectHashTreeEntry : data.entrySet()) {
892+
int key = System.identityHashCode(objectHashTreeEntry.getKey());
893+
int value = objectHashTreeEntry.getValue().hashCode();
894+
xor = xor + key ^ value;
895+
sum = sum + key + value;
896+
mul = mul + (key|1) * (value|1);
897+
}
898+
int hash = 1;
899+
hash = hash * 31 + xor;
900+
hash = hash * 31 + sum;
901+
hash = hash * 31 + mul;
902+
return hash;
877903
}
878904

879905
/**
@@ -887,14 +913,24 @@ public int hashCode() {
887913
*/
888914
@Override
889915
public boolean equals(Object o) {
916+
if (o == this) {
917+
return true;
918+
}
890919
if (!(o instanceof HashTree)) {
891920
return false;
892921
}
893922
HashTree oo = (HashTree) o;
894923
if (oo.size() != this.size()) {
895924
return false;
896925
}
897-
return data.equals(oo.data);
926+
// data.equals(oo.data); uses reference identity for keys
927+
for (Entry<Object, HashTree> entry : data.entrySet()) {
928+
HashTree otherValue = oo.data.get(entry.getKey());
929+
if (!entry.getValue().equals(otherValue)) {
930+
return false;
931+
}
932+
}
933+
return true;
898934
}
899935

900936
/**

src/jorphan/src/main/java/org/apache/jorphan/collections/ListedHashTree.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,9 @@ public Collection<Object> list() {
175175
/** {@inheritDoc} */
176176
@Override
177177
public HashTree remove(Object key) {
178-
order.remove(key);
178+
if (data.containsKey(key)) {
179+
order.removeIf(x -> x == key);
180+
}
179181
return data.remove(key);
180182
}
181183

@@ -190,7 +192,9 @@ public Object[] getArray() {
190192
@Override
191193
public int hashCode() {
192194
int hc = 17;
193-
hc = hc * 37 + (order == null ? 0 : order.hashCode());
195+
for (Object o : order) {
196+
hc = hc * 37 + System.identityHashCode(o);
197+
}
194198
hc = hc * 37 + super.hashCode();
195199
return hc;
196200
}
@@ -202,7 +206,15 @@ public boolean equals(Object o) {
202206
return false;
203207
}
204208
ListedHashTree lht = (ListedHashTree) o;
205-
return super.equals(lht) && order.equals(lht.order);
209+
if (!super.equals(lht)) {
210+
return false;
211+
}
212+
for (int i = 0; i < order.size(); i++) {
213+
if (order.get(i) != lht.order.get(i)) {
214+
return false;
215+
}
216+
}
217+
return true;
206218
}
207219

208220

0 commit comments

Comments
 (0)