Skip to content

Commit 8488947

Browse files
Fix type-pool cache (#1828)
* Draft fix for type-pool cache * Enhance and fix cache clearance logic * Fix periodic clear * Fix SoftlyReferencingTypePoolCacheTest * Adding to CHANGELOG Co-authored-by: Felix Barnsteiner <[email protected]>
1 parent 283ecb9 commit 8488947

File tree

3 files changed

+59
-25
lines changed

3 files changed

+59
-25
lines changed

CHANGELOG.asciidoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ endif::[]
3333
=== Java Agent version 1.x
3434
3535
[[release-notes-1.24.0]]
36-
==== 1.24.0 - 2021/05/28
36+
==== 1.24.0 - 2021/05/31
3737
3838
[float]
3939
===== Features
@@ -52,6 +52,8 @@ events. With the new `OVERRIDE` option, non-file logs can be ECS-reformatted aut
5252
* Avoid `IllegalStateException` when multiple `tracestate` headers are used - {pull}1808[#1808]
5353
* Ensure CLI attach avoids `sudo` only when required and avoid blocking - {pull}1819[#1819]
5454
* Avoid sending metric-sets without samples, so to adhere to the intake API - {pull}1826[#1826]
55+
* Fixing our type-pool cache, so that it can't cause OOM (softly-referenced), and it gets cleared when not used for
56+
a while - {pull}1828[#1828]
5557
5658
[float]
5759
===== Refactors

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

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import java.util.Map;
3838
import java.util.concurrent.ConcurrentMap;
3939
import java.util.concurrent.TimeUnit;
40-
import java.util.concurrent.atomic.AtomicLong;
4140

4241
/**
4342
* Caches {@link TypeDescription}s which speeds up type matching -
@@ -46,7 +45,12 @@
4645
* Without a type pool cache those types would have to be re-loaded from the file system if their {@link Class} has not been loaded yet.
4746
* <p>
4847
* In order to avoid {@link OutOfMemoryError}s because of this cache,
49-
* the {@link TypePool.CacheProvider}s are wrapped in a {@link SoftReference}
48+
* the {@link TypePool.CacheProvider}s are wrapped in a {@link SoftReference}.
49+
* If the underlying cache is being GC'ed, a new one is immediately being created instead, so to both prevent OOME and
50+
* enable continuous caching.
51+
* Any cache that is not being accessed for some time (configurable through constructor), is being cleared, so that
52+
* caches don't occupy heap memory until there is a heap stress. This fits the typical pattern of most type matching
53+
* occurring at the beginning of the agent attachment.
5054
* </p>
5155
*/
5256
public class SoftlyReferencingTypePoolCache extends AgentBuilder.PoolStrategy.WithTypePoolCache {
@@ -58,6 +62,7 @@ public class SoftlyReferencingTypePoolCache extends AgentBuilder.PoolStrategy.Wi
5862
*/
5963
private final WeakConcurrentMap<ClassLoader, CacheProviderWrapper> cacheProviders =
6064
new NullSafeWeakConcurrentMap<ClassLoader, CacheProviderWrapper>(false);
65+
6166
private final ElementMatcher<ClassLoader> ignoredClassLoaders;
6267

6368
public SoftlyReferencingTypePoolCache(final TypePool.Default.ReaderMode readerMode,
@@ -80,16 +85,13 @@ protected TypePool.CacheProvider locate(ClassLoader classLoader) {
8085
return TypePool.CacheProvider.Simple.withObjectType();
8186
}
8287
classLoader = classLoader == null ? getBootstrapMarkerLoader() : classLoader;
83-
CacheProviderWrapper cacheProviderRef = cacheProviders.get(classLoader);
84-
if (cacheProviderRef == null || cacheProviderRef.get() == null) {
85-
cacheProviderRef = new CacheProviderWrapper();
86-
cacheProviders.put(classLoader, cacheProviderRef);
88+
CacheProviderWrapper cacheProviderWrapper = cacheProviders.get(classLoader);
89+
if (cacheProviderWrapper == null) {
90+
cacheProviders.put(classLoader, new CacheProviderWrapper());
8791
// accommodate for race condition
88-
cacheProviderRef = cacheProviders.get(classLoader);
92+
cacheProviderWrapper = cacheProviders.get(classLoader);
8993
}
90-
final TypePool.CacheProvider cacheProvider = cacheProviderRef.get();
91-
// guard against edge case when the soft reference has already been cleared since evaluating the loop condition
92-
return cacheProvider != null ? cacheProvider : TypePool.CacheProvider.Simple.withObjectType();
94+
return cacheProviderWrapper;
9395
}
9496

9597
/**
@@ -115,8 +117,9 @@ protected TypePool.CacheProvider locate(ClassLoader classLoader) {
115117
*/
116118
void clearIfNotAccessedSince(long clearIfNotAccessedSinceMinutes) {
117119
for (Map.Entry<ClassLoader, CacheProviderWrapper> entry : cacheProviders) {
118-
if (System.currentTimeMillis() >= entry.getValue().getLastAccess() + TimeUnit.MINUTES.toMillis(clearIfNotAccessedSinceMinutes)) {
119-
cacheProviders.remove(entry.getKey());
120+
CacheProviderWrapper cacheWrapper = entry.getValue();
121+
if (System.currentTimeMillis() >= cacheWrapper.getLastAccess() + TimeUnit.MINUTES.toMillis(clearIfNotAccessedSinceMinutes)) {
122+
cacheWrapper.clear();
120123
}
121124
}
122125
}
@@ -125,21 +128,49 @@ WeakConcurrentMap<ClassLoader, CacheProviderWrapper> getCacheProviders() {
125128
return cacheProviders;
126129
}
127130

128-
private static class CacheProviderWrapper {
129-
private final AtomicLong lastAccess = new AtomicLong(System.currentTimeMillis());
130-
private final SoftReference<TypePool.CacheProvider> delegate;
131+
private static class CacheProviderWrapper implements TypePool.CacheProvider {
132+
133+
private volatile long lastAccess = System.currentTimeMillis();
134+
private volatile SoftReference<TypePool.CacheProvider> delegate;
131135

132136
private CacheProviderWrapper() {
133-
this.delegate = new SoftReference<TypePool.CacheProvider>(new TypePool.CacheProvider.Simple());
137+
delegate = new SoftReference<TypePool.CacheProvider>(new Simple());
134138
}
135139

136140
long getLastAccess() {
137-
return lastAccess.get();
141+
return lastAccess;
142+
}
143+
144+
private TypePool.CacheProvider getDelegate() {
145+
TypePool.CacheProvider cacheProvider = delegate.get();
146+
if (cacheProvider == null) {
147+
synchronized (this) {
148+
cacheProvider = delegate.get();
149+
if (cacheProvider == null) {
150+
cacheProvider = new Simple();
151+
delegate = new SoftReference<TypePool.CacheProvider>(cacheProvider);
152+
}
153+
}
154+
}
155+
return cacheProvider;
138156
}
139157

158+
@Override
140159
@Nullable
141-
TypePool.CacheProvider get() {
142-
return delegate.get();
160+
public TypePool.Resolution find(String name) {
161+
lastAccess = System.currentTimeMillis();
162+
return getDelegate().find(name);
163+
}
164+
165+
@Override
166+
public TypePool.Resolution register(String name, TypePool.Resolution resolution) {
167+
lastAccess = System.currentTimeMillis();
168+
return getDelegate().register(name, resolution);
169+
}
170+
171+
@Override
172+
public void clear() {
173+
getDelegate().clear();
143174
}
144175
}
145176

apm-agent-core/src/test/java/co/elastic/apm/agent/bci/bytebuddy/SoftlyReferencingTypePoolCacheTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
* the Apache License, Version 2.0 (the "License"); you may
1212
* not use this file except in compliance with the License.
1313
* You may obtain a copy of the License at
14-
*
14+
*
1515
* http://www.apache.org/licenses/LICENSE-2.0
16-
*
16+
*
1717
* Unless required by applicable law or agreed to in writing,
1818
* software distributed under the License is distributed on an
1919
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -24,6 +24,7 @@
2424
*/
2525
package co.elastic.apm.agent.bci.bytebuddy;
2626

27+
import net.bytebuddy.description.type.TypeDescription;
2728
import net.bytebuddy.matcher.ElementMatchers;
2829
import net.bytebuddy.pool.TypePool;
2930
import org.junit.jupiter.api.BeforeEach;
@@ -42,9 +43,9 @@ void setUp() {
4243

4344
@Test
4445
void testClearEntries() {
45-
cache.locate(ClassLoader.getSystemClassLoader());
46-
assertThat(cache.getCacheProviders().approximateSize()).isEqualTo(1);
46+
cache.locate(ClassLoader.getSystemClassLoader()).register(String.class.getName(), new TypePool.Resolution.Simple(TypeDescription.STRING));
47+
assertThat(cache.locate(ClassLoader.getSystemClassLoader()).find(String.class.getName())).isNotNull();
4748
cache.clearIfNotAccessedSince(0);
48-
assertThat(cache.getCacheProviders().approximateSize()).isEqualTo(0);
49+
assertThat(cache.locate(ClassLoader.getSystemClassLoader()).find(String.class.getName())).isNull();
4950
}
5051
}

0 commit comments

Comments
 (0)