Skip to content

Commit 56cc3b8

Browse files
authored
null-safe weak set/map + detached thread local (#1597)
* add null-safe weak map * add null-safe weak set * use null-safe weak maps/sets when possible * prevent npe with threadlocal
1 parent 95a84c6 commit 56cc3b8

File tree

11 files changed

+369
-4
lines changed

11 files changed

+369
-4
lines changed

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ endif::[]
4545
* Fix improper request body capturing - {pull}1579[#1579]
4646
* avoid `NullPointerException` due to null return values instrumentation advices - {pull}1601[#1601]
4747
* Update async-profiler to 1.8.3 {pull}1602[1602]
48+
* Use null-safe data structures to avoid `NullPointerException` {pull}1597[1597]
4849
4950
[float]
5051
===== Refactors

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/HelperClassManager.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import co.elastic.apm.agent.bci.classloading.IndyPluginClassLoader;
2828
import co.elastic.apm.agent.impl.ElasticApmTracer;
29+
import co.elastic.apm.agent.sdk.weakmap.NullSafeWeakConcurrentMap;
2930
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap;
3031
import net.bytebuddy.description.type.TypeDescription;
3132
import net.bytebuddy.dynamic.ClassFileLocator;
@@ -223,7 +224,7 @@ public static class ForAnyClassLoader<T> extends HelperClassManager<T> {
223224
private ForAnyClassLoader(ElasticApmTracer tracer, String implementation, String... additionalHelpers) {
224225
super(tracer, implementation, additionalHelpers);
225226
// deliberately doesn't use WeakMapSupplier as this class manages the cleanup manually
226-
clId2helperMap = new WeakConcurrentMap<>(false);
227+
clId2helperMap = new NullSafeWeakConcurrentMap<>(false);
227228
}
228229

229230
/**

apm-agent-core/src/main/java/co/elastic/apm/agent/bci/bytebuddy/SoftlyReferencingTypePoolCache.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
package co.elastic.apm.agent.bci.bytebuddy;
2626

27+
import co.elastic.apm.agent.sdk.weakmap.NullSafeWeakConcurrentMap;
2728
import co.elastic.apm.agent.util.ExecutorUtils;
2829
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap;
2930
import net.bytebuddy.agent.builder.AgentBuilder;
@@ -56,7 +57,7 @@ public class SoftlyReferencingTypePoolCache extends AgentBuilder.PoolStrategy.Wi
5657
* deliberately doesn't use WeakMapSupplier as this class manages the cleanup manually
5758
*/
5859
private final WeakConcurrentMap<ClassLoader, CacheProviderWrapper> cacheProviders =
59-
new WeakConcurrentMap<ClassLoader, CacheProviderWrapper>(false);
60+
new NullSafeWeakConcurrentMap<ClassLoader, CacheProviderWrapper>(false);
6061
private final ElementMatcher<ClassLoader> ignoredClassLoaders;
6162

6263
public SoftlyReferencingTypePoolCache(final TypePool.Default.ReaderMode readerMode,

apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/state/GlobalThreadLocal.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
package co.elastic.apm.agent.sdk.state;
2626

27+
import co.elastic.apm.agent.sdk.weakmap.NullCheck;
2728
import com.blogspot.mydailyjava.weaklockfree.DetachedThreadLocal;
2829

2930
import javax.annotation.Nullable;
@@ -82,6 +83,14 @@ public T get(T defaultValue) {
8283
return defaultValue;
8384
}
8485

86+
@Override
87+
public void set(@Nullable T value) {
88+
if (NullCheck.isNullKey(value)) {
89+
return;
90+
}
91+
super.set(value);
92+
}
93+
8594
@Override
8695
@Nullable
8796
protected T initialValue(Thread thread) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package co.elastic.apm.agent.sdk.weakmap;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import javax.annotation.Nullable;
7+
8+
public class NullCheck {
9+
10+
private static final Logger logger = LoggerFactory.getLogger(NullCheck.class);
11+
12+
/**
13+
* checks if key or value is {@literal null}
14+
*
15+
* @param v key or value
16+
* @return {@literal true} if key is non-null, {@literal false} if null
17+
*/
18+
private static <T> boolean isNull(@Nullable T v, boolean isKey) {
19+
if (null != v) {
20+
return false;
21+
}
22+
String msg = String.format("trying to use null %s", isKey ? "key" : "value");
23+
if (logger.isDebugEnabled()) {
24+
logger.debug(msg, new RuntimeException(msg));
25+
} else {
26+
logger.warn(msg);
27+
}
28+
return true;
29+
}
30+
31+
public static <T> boolean isNullKey(@Nullable T key){
32+
return isNull(key, true);
33+
}
34+
35+
public static <T> boolean isNullValue(@Nullable T value) {
36+
return isNull(value, false);
37+
}
38+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*-
2+
* #%L
3+
* Elastic APM Java agent
4+
* %%
5+
* Copyright (C) 2018 - 2021 Elastic and contributors
6+
* %%
7+
* Licensed to Elasticsearch B.V. under one or more contributor
8+
* license agreements. See the NOTICE file distributed with
9+
* this work for additional information regarding copyright
10+
* ownership. Elasticsearch B.V. licenses this file to you under
11+
* the Apache License, Version 2.0 (the "License"); you may
12+
* not use this file except in compliance with the License.
13+
* You may obtain a copy of the License at
14+
*
15+
* http://www.apache.org/licenses/LICENSE-2.0
16+
*
17+
* Unless required by applicable law or agreed to in writing,
18+
* software distributed under the License is distributed on an
19+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
20+
* KIND, either express or implied. See the License for the
21+
* specific language governing permissions and limitations
22+
* under the License.
23+
* #L%
24+
*/
25+
package co.elastic.apm.agent.sdk.weakmap;
26+
27+
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentMap;
28+
29+
import javax.annotation.Nullable;
30+
31+
import static co.elastic.apm.agent.sdk.weakmap.NullCheck.isNullKey;
32+
import static co.elastic.apm.agent.sdk.weakmap.NullCheck.isNullValue;
33+
34+
/**
35+
* {@link WeakConcurrentMap} implementation that prevents throwing {@link NullPointerException} and helps debugging if needed
36+
*
37+
* @param <K> key type
38+
* @param <V> value type
39+
*/
40+
public class NullSafeWeakConcurrentMap<K, V> extends WeakConcurrentMap<K, V> {
41+
42+
public NullSafeWeakConcurrentMap(boolean cleanerThread) {
43+
super(cleanerThread);
44+
}
45+
46+
@Nullable
47+
@Override
48+
public V get(K key) {
49+
if (isNullKey(key)) {
50+
// super implementation silently adds entries from default value when there is none
51+
// in the case of 'null', we won't return the default value nor create a map entry with it.
52+
return null;
53+
}
54+
return super.get(key);
55+
}
56+
57+
@Nullable
58+
@Override
59+
public V getIfPresent(K key) {
60+
if (isNullKey(key)) {
61+
return null;
62+
}
63+
return super.getIfPresent(key);
64+
}
65+
66+
@Override
67+
public boolean containsKey(K key) {
68+
if (isNullKey(key)) {
69+
return false;
70+
}
71+
return super.containsKey(key);
72+
}
73+
74+
@Nullable
75+
@Override
76+
public V put(K key, V value) {
77+
if (isNullKey(key) || isNullValue(value)) {
78+
return null;
79+
}
80+
return super.put(key, value);
81+
}
82+
83+
@Nullable
84+
@Override
85+
public V putIfAbsent(K key, V value) {
86+
if (isNullKey(key) || isNullValue(value)) {
87+
return null;
88+
}
89+
return super.putIfAbsent(key, value);
90+
}
91+
92+
@Nullable
93+
@Override
94+
public V putIfProbablyAbsent(K key, V value) {
95+
if (isNullKey(key) || isNullValue(value)) {
96+
return null;
97+
}
98+
return super.putIfProbablyAbsent(key, value);
99+
}
100+
101+
@Nullable
102+
@Override
103+
public V remove(K key) {
104+
if (isNullKey(key)) {
105+
return null;
106+
}
107+
return super.remove(key);
108+
}
109+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package co.elastic.apm.agent.sdk.weakmap;
2+
3+
import com.blogspot.mydailyjava.weaklockfree.WeakConcurrentSet;
4+
5+
import static co.elastic.apm.agent.sdk.weakmap.NullCheck.isNullValue;
6+
7+
/**
8+
* {@link WeakConcurrentSet} implementation that prevents throwing {@link NullPointerException} and helps debugging if needed
9+
*
10+
* @param <V> value type
11+
*/
12+
public class NullSafeWeakConcurrentSet<V> extends WeakConcurrentSet<V> {
13+
14+
public NullSafeWeakConcurrentSet(Cleaner cleaner) {
15+
super(cleaner);
16+
}
17+
18+
@Override
19+
public boolean add(V value) {
20+
if(isNullValue(value)){
21+
return false;
22+
}
23+
return super.add(value);
24+
}
25+
26+
@Override
27+
public boolean contains(V value) {
28+
if(isNullValue(value)){
29+
return false;
30+
}
31+
return super.contains(value);
32+
}
33+
34+
@Override
35+
public boolean remove(V value) {
36+
if(isNullValue(value)){
37+
return false;
38+
}
39+
return super.remove(value);
40+
}
41+
}

apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/weakmap/WeakMapSupplier.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ public class WeakMapSupplier {
3636
private static final WeakConcurrentSet<WeakConcurrentSet<?>> registeredSets = new WeakConcurrentSet<>(WeakConcurrentSet.Cleaner.INLINE);
3737

3838
public static <K, V> WeakConcurrentMap<K, V> createMap() {
39-
WeakConcurrentMap<K, V> result = new WeakConcurrentMap<>(false);
39+
WeakConcurrentMap<K, V> result = new NullSafeWeakConcurrentMap<>(false);
4040
registeredMaps.add(result);
4141
return result;
4242
}
4343

4444
public static <V> WeakConcurrentSet<V> createSet() {
45-
WeakConcurrentSet<V> weakSet = new WeakConcurrentSet<V>(WeakConcurrentSet.Cleaner.MANUAL);
45+
WeakConcurrentSet<V> weakSet = new NullSafeWeakConcurrentSet<>(WeakConcurrentSet.Cleaner.MANUAL);
4646
registeredSets.add(weakSet);
4747
return weakSet;
4848
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package co.elastic.apm.agent.sdk.state;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
public class GlobalThreadLocalTest {
6+
7+
GlobalThreadLocal<String> threadLocal = GlobalThreadLocal.get(GlobalThreadLocalTest.class, "test");
8+
9+
@Test
10+
void setNullValueShouldNotThrow() {
11+
threadLocal.set(null);
12+
}
13+
}

0 commit comments

Comments
 (0)