Skip to content

Commit 8697de9

Browse files
author
Anuraag Agrawal
authored
Detect GC leaks of scopes in StrictContextStorage. (#2164)
* Detect GC leaks of scopes in StrictContextStorage. * More * Finish * Force GC more aggressively * Cleanup * Vendor code directly * Copy test too * Try waiting more * ep * oops * Remove from build.gradle * Drift * Log on multiple * Cleaner ourselves. * EP * Move into if * Revert accidental
1 parent 643b697 commit 8697de9

File tree

9 files changed

+949
-98
lines changed

9 files changed

+949
-98
lines changed

api/build.gradle

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,3 @@ dependencies {
1919
testImplementation libraries.jqf,
2020
libraries.guava_testlib
2121
}
22-
23-
javadoc {
24-
exclude 'io/opentelemetry/internal/**'
25-
}

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ configure(opentelemetryProjects) {
200200
withSourcesJar()
201201
}
202202

203+
javadoc {
204+
exclude 'io/opentelemetry/internal/**'
205+
}
206+
203207
tasks {
204208
def testJava8 = register('testJava8', Test) {
205209
javaLauncher = javaToolchains.launcherFor {
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// Includes work from:
7+
/*
8+
* Copyright Rafael Winterhalter
9+
*
10+
* Licensed under the Apache License, Version 2.0 (the "License");
11+
* you may not use this file except in compliance with the License.
12+
* You may obtain a copy of the License at
13+
*
14+
* http://www.apache.org/licenses/LICENSE-2.0
15+
*
16+
* Unless required by applicable law or agreed to in writing, software
17+
* distributed under the License is distributed on an "AS IS" BASIS,
18+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19+
* See the License for the specific language governing permissions and
20+
* limitations under the License.
21+
*/
22+
23+
// Suppress warnings since this is vendored as-is.
24+
// CHECKSTYLE:OFF
25+
26+
package io.opentelemetry.context.internal.shaded;
27+
28+
import java.lang.ref.Reference;
29+
import java.lang.ref.ReferenceQueue;
30+
import java.lang.ref.WeakReference;
31+
import java.util.Iterator;
32+
import java.util.Map;
33+
import java.util.NoSuchElementException;
34+
import java.util.concurrent.ConcurrentHashMap;
35+
import java.util.concurrent.ConcurrentMap;
36+
37+
/**
38+
* A thread-safe map with weak keys. Entries are based on a key's system hash code and keys are
39+
* considered equal only by reference equality. This class offers an abstract-base implementation
40+
* that allows to override methods. This class does not implement the {@link Map} interface because
41+
* this implementation is incompatible with the map contract. While iterating over a map's entries,
42+
* any key that has not passed iteration is referenced non-weakly.
43+
*
44+
* <p>This class has been copied as is from
45+
* https://github.com/raphw/weak-lock-free/blob/ad0e5e0c04d4a31f9485bf12b89afbc9d75473b3/src/main/java/com/blogspot/mydailyjava/weaklockfree/WeakConcurrentMap.java
46+
* This is used in multiple artifacts in OpenTelemetry and while it is in our internal API,
47+
* generally backwards compatible changes should not be made to avoid a situation where different
48+
* versions of OpenTelemetry artifacts become incompatible with each other.
49+
*/
50+
// Suppress warnings since this is vendored as-is.
51+
@SuppressWarnings({"MissingSummary", "EqualsBrokenForNull", "FieldMissingNullable"})
52+
public abstract class AbstractWeakConcurrentMap<K, V, L> extends ReferenceQueue<K>
53+
implements Runnable, Iterable<Map.Entry<K, V>> {
54+
55+
final ConcurrentMap<WeakKey<K>, V> target;
56+
57+
protected AbstractWeakConcurrentMap() {
58+
this(new ConcurrentHashMap<WeakKey<K>, V>());
59+
}
60+
61+
/** @param target ConcurrentMap implementation that this class wraps. */
62+
protected AbstractWeakConcurrentMap(ConcurrentMap<WeakKey<K>, V> target) {
63+
this.target = target;
64+
}
65+
66+
/**
67+
* Override with care as it can cause lookup failures if done incorrectly. The result must have
68+
* the same {@link Object#hashCode()} as the input and be {@link Object#equals(Object) equal to} a
69+
* weak reference of the key. When overriding this, also override {@link #resetLookupKey}.
70+
*/
71+
protected abstract L getLookupKey(K key);
72+
73+
/** Resets any reusable state in the {@linkplain #getLookupKey lookup key}. */
74+
protected abstract void resetLookupKey(L lookupKey);
75+
76+
/**
77+
* @param key The key of the entry.
78+
* @return The value of the entry or the default value if it did not exist.
79+
*/
80+
public V get(K key) {
81+
if (key == null) throw new NullPointerException();
82+
V value;
83+
L lookupKey = getLookupKey(key);
84+
try {
85+
value = target.get(lookupKey);
86+
} finally {
87+
resetLookupKey(lookupKey);
88+
}
89+
if (value == null) {
90+
value = defaultValue(key);
91+
if (value != null) {
92+
V previousValue = target.putIfAbsent(new WeakKey<K>(key, this), value);
93+
if (previousValue != null) {
94+
value = previousValue;
95+
}
96+
}
97+
}
98+
return value;
99+
}
100+
101+
/**
102+
* @param key The key of the entry.
103+
* @return The value of the entry or null if it did not exist.
104+
*/
105+
public V getIfPresent(K key) {
106+
if (key == null) throw new NullPointerException();
107+
L lookupKey = getLookupKey(key);
108+
try {
109+
return target.get(lookupKey);
110+
} finally {
111+
resetLookupKey(lookupKey);
112+
}
113+
}
114+
115+
/**
116+
* @param key The key of the entry.
117+
* @return {@code true} if the key already defines a value.
118+
*/
119+
public boolean containsKey(K key) {
120+
if (key == null) throw new NullPointerException();
121+
L lookupKey = getLookupKey(key);
122+
try {
123+
return target.containsKey(lookupKey);
124+
} finally {
125+
resetLookupKey(lookupKey);
126+
}
127+
}
128+
129+
/**
130+
* @param key The key of the entry.
131+
* @param value The value of the entry.
132+
* @return The previous entry or {@code null} if it does not exist.
133+
*/
134+
public V put(K key, V value) {
135+
if (key == null || value == null) throw new NullPointerException();
136+
return target.put(new WeakKey<K>(key, this), value);
137+
}
138+
139+
/**
140+
* @param key The key of the entry.
141+
* @param value The value of the entry.
142+
* @return The previous entry or {@code null} if it does not exist.
143+
*/
144+
public V putIfAbsent(K key, V value) {
145+
if (key == null || value == null) throw new NullPointerException();
146+
V previous;
147+
L lookupKey = getLookupKey(key);
148+
try {
149+
previous = target.get(lookupKey);
150+
} finally {
151+
resetLookupKey(lookupKey);
152+
}
153+
return previous == null ? target.putIfAbsent(new WeakKey<K>(key, this), value) : previous;
154+
}
155+
156+
/**
157+
* @param key The key of the entry.
158+
* @param value The value of the entry.
159+
* @return The previous entry or {@code null} if it does not exist.
160+
*/
161+
public V putIfProbablyAbsent(K key, V value) {
162+
if (key == null || value == null) throw new NullPointerException();
163+
return target.putIfAbsent(new WeakKey<K>(key, this), value);
164+
}
165+
166+
/**
167+
* @param key The key of the entry.
168+
* @return The removed entry or {@code null} if it does not exist.
169+
*/
170+
public V remove(K key) {
171+
if (key == null) throw new NullPointerException();
172+
L lookupKey = getLookupKey(key);
173+
try {
174+
return target.remove(lookupKey);
175+
} finally {
176+
resetLookupKey(lookupKey);
177+
}
178+
}
179+
180+
/** Clears the entire map. */
181+
public void clear() {
182+
target.clear();
183+
}
184+
185+
/**
186+
* Creates a default value. There is no guarantee that the requested value will be set as a once
187+
* it is created in case that another thread requests a value for a key concurrently.
188+
*
189+
* @param key The key for which to create a default value.
190+
* @return The default value for a key without value or {@code null} for not defining a default
191+
* value.
192+
*/
193+
protected V defaultValue(K key) {
194+
return null;
195+
}
196+
197+
/** Cleans all unused references. */
198+
public void expungeStaleEntries() {
199+
Reference<?> reference;
200+
while ((reference = poll()) != null) {
201+
target.remove(reference);
202+
}
203+
}
204+
205+
/**
206+
* Returns the approximate size of this map where the returned number is at least as big as the
207+
* actual number of entries.
208+
*
209+
* @return The minimum size of this map.
210+
*/
211+
public int approximateSize() {
212+
return target.size();
213+
}
214+
215+
@Override
216+
public void run() {
217+
try {
218+
while (!Thread.interrupted()) {
219+
target.remove(remove());
220+
}
221+
} catch (InterruptedException ignored) {
222+
// do nothing
223+
}
224+
}
225+
226+
@Override
227+
public Iterator<Map.Entry<K, V>> iterator() {
228+
return new EntryIterator(target.entrySet().iterator());
229+
}
230+
231+
@Override
232+
public String toString() {
233+
return target.toString();
234+
}
235+
236+
/*
237+
* Why this works:
238+
* ---------------
239+
*
240+
* Note that this map only supports reference equality for keys and uses system hash codes. Also, for the
241+
* WeakKey instances to function correctly, we are voluntarily breaking the Java API contract for
242+
* hashCode/equals of these instances.
243+
*
244+
* System hash codes are immutable and can therefore be computed prematurely and are stored explicitly
245+
* within the WeakKey instances. This way, we always know the correct hash code of a key and always
246+
* end up in the correct bucket of our target map. This remains true even after the weakly referenced
247+
* key is collected.
248+
*
249+
* If we are looking up the value of the current key via WeakConcurrentMap::get or any other public
250+
* API method, we know that any value associated with this key must still be in the map as the mere
251+
* existence of this key makes it ineligible for garbage collection. Therefore, looking up a value
252+
* using another WeakKey wrapper guarantees a correct result.
253+
*
254+
* If we are looking up the map entry of a WeakKey after polling it from the reference queue, we know
255+
* that the actual key was already collected and calling WeakKey::get returns null for both the polled
256+
* instance and the instance within the map. Since we explicitly stored the identity hash code for the
257+
* referenced value, it is however trivial to identify the correct bucket. From this bucket, the first
258+
* weak key with a null reference is removed. Due to hash collision, we do not know if this entry
259+
* represents the weak key. However, we do know that the reference queue polls at least as many weak
260+
* keys as there are stale map entries within the target map. If no key is ever removed from the map
261+
* explicitly, the reference queue eventually polls exactly as many weak keys as there are stale entries.
262+
*
263+
* Therefore, we can guarantee that there is no memory leak.
264+
*
265+
* It is the responsibility of the actual map implementation to implement a lookup key that is used for
266+
* lookups. The lookup key must supply the same semantics as the weak key with regards to hash code.
267+
* The weak key invokes the latent key's equality method upon evaluation.
268+
*/
269+
270+
public static final class WeakKey<K> extends WeakReference<K> {
271+
272+
private final int hashCode;
273+
274+
WeakKey(K key, ReferenceQueue<? super K> queue) {
275+
super(key, queue);
276+
hashCode = System.identityHashCode(key);
277+
}
278+
279+
@Override
280+
public int hashCode() {
281+
return hashCode;
282+
}
283+
284+
@Override
285+
public boolean equals(Object other) {
286+
if (other instanceof WeakKey<?>) {
287+
return ((WeakKey<?>) other).get() == get();
288+
} else {
289+
return other.equals(this);
290+
}
291+
}
292+
293+
@Override
294+
public String toString() {
295+
return String.valueOf(get());
296+
}
297+
}
298+
299+
private class EntryIterator implements Iterator<Map.Entry<K, V>> {
300+
301+
private final Iterator<Map.Entry<WeakKey<K>, V>> iterator;
302+
303+
private Map.Entry<WeakKey<K>, V> nextEntry;
304+
305+
private K nextKey;
306+
307+
private EntryIterator(Iterator<Map.Entry<WeakKey<K>, V>> iterator) {
308+
this.iterator = iterator;
309+
findNext();
310+
}
311+
312+
private void findNext() {
313+
while (iterator.hasNext()) {
314+
nextEntry = iterator.next();
315+
nextKey = nextEntry.getKey().get();
316+
if (nextKey != null) {
317+
return;
318+
}
319+
}
320+
nextEntry = null;
321+
nextKey = null;
322+
}
323+
324+
@Override
325+
public boolean hasNext() {
326+
return nextKey != null;
327+
}
328+
329+
@Override
330+
public Map.Entry<K, V> next() {
331+
if (nextKey == null) {
332+
throw new NoSuchElementException();
333+
}
334+
try {
335+
return new SimpleEntry(nextKey, nextEntry);
336+
} finally {
337+
findNext();
338+
}
339+
}
340+
341+
@Override
342+
public void remove() {
343+
throw new UnsupportedOperationException();
344+
}
345+
}
346+
347+
private class SimpleEntry implements Map.Entry<K, V> {
348+
349+
private final K key;
350+
351+
final Map.Entry<WeakKey<K>, V> entry;
352+
353+
private SimpleEntry(K key, Map.Entry<WeakKey<K>, V> entry) {
354+
this.key = key;
355+
this.entry = entry;
356+
}
357+
358+
@Override
359+
public K getKey() {
360+
return key;
361+
}
362+
363+
@Override
364+
public V getValue() {
365+
return entry.getValue();
366+
}
367+
368+
@Override
369+
public V setValue(V value) {
370+
if (value == null) throw new NullPointerException();
371+
return entry.setValue(value);
372+
}
373+
}
374+
}

0 commit comments

Comments
 (0)