diff --git a/buildSrc/src/main/groovy/MuzzlePlugin.groovy b/buildSrc/src/main/groovy/MuzzlePlugin.groovy index 99ad0b90dd..2f852d9fd3 100644 --- a/buildSrc/src/main/groovy/MuzzlePlugin.groovy +++ b/buildSrc/src/main/groovy/MuzzlePlugin.groovy @@ -291,7 +291,10 @@ class MuzzlePlugin implements Plugin { doLast { final ClassLoader instrumentationCL = createInstrumentationClassloader(instrumentationProject, toolingProject) def ccl = Thread.currentThread().contextClassLoader - def bogusLoader = new SecureClassLoader() + def bogusLoader = new SecureClassLoader() { + @Override + String toString() { return "bogus" } + } Thread.currentThread().contextClassLoader = bogusLoader final ClassLoader userCL = createClassLoaderForTask(instrumentationProject, bootstrapProject, taskName) try { diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentTooling.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentTooling.java index ad744a15fd..c20192b383 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentTooling.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentTooling.java @@ -16,7 +16,7 @@ public class AgentTooling { } private static final DDLocationStrategy LOCATION_STRATEGY = new DDLocationStrategy(); - private static final DDCachingPoolStrategy POOL_STRATEGY = new DDCachingPoolStrategy(CLEANER); + private static final DDCachingPoolStrategy POOL_STRATEGY = new DDCachingPoolStrategy(); public static void init() { // Only need to trigger static initializers for now. diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Cleaner.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Cleaner.java index 8115fb0f48..61529aff63 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Cleaner.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Cleaner.java @@ -20,6 +20,7 @@ class Cleaner { final Thread thread = new Thread(r, "dd-cleaner"); thread.setDaemon(true); thread.setPriority(Thread.MIN_PRIORITY); + thread.setContextClassLoader(null); return thread; } }; diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDCachingPoolStrategy.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDCachingPoolStrategy.java index 886d742c78..1d4cccdb35 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDCachingPoolStrategy.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDCachingPoolStrategy.java @@ -1,146 +1,235 @@ package datadog.trace.agent.tooling; -import static datadog.trace.agent.tooling.ClassLoaderMatcher.BOOTSTRAP_CLASSLOADER; -import static datadog.trace.agent.tooling.ClassLoaderMatcher.skipClassLoader; import static net.bytebuddy.agent.builder.AgentBuilder.PoolStrategy; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import datadog.trace.bootstrap.WeakMap; -import java.security.SecureClassLoader; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; +import java.lang.ref.WeakReference; +import lombok.extern.slf4j.Slf4j; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.pool.TypePool; /** - * Custom Pool strategy. + * NEW (Jan 2020) Custom Pool strategy. * - *

Here we are using WeakMap.Provider as the backing ClassLoader -> CacheProvider lookup. + *

* - *

We also use our bootstrap proxy when matching against the bootstrap loader. + *

* - *

The CacheProvider is a custom implementation that uses guava's cache to expire and limit size. + *

This design was chosen to create a single limited size cache that can be adjusted for the + * entire application -- without having to create a large number of WeakReference objects. * - *

By evicting from the cache we are able to reduce the memory overhead of the agent for apps - * that have many classes. - * - *

See eviction policy below. + *

Eviction is handled almost entirely through a size restriction; however, softValues are still + * used as a further safeguard. */ -public class DDCachingPoolStrategy - implements PoolStrategy, WeakMap.ValueSupplier { +@Slf4j +public class DDCachingPoolStrategy implements PoolStrategy { + // Many things are package visible for testing purposes -- + // others to avoid creation of synthetic accessors - // Need this because we can't put null into the typePoolCache map. - private static final ClassLoader BOOTSTRAP_CLASSLOADER_PLACEHOLDER = - new SecureClassLoader(null) {}; + static final int CONCURRENCY_LEVEL = 8; + static final int LOADER_CAPACITY = 64; + static final int TYPE_CAPACITY = 64; - private final WeakMap typePoolCache = - WeakMap.Provider.newWeakMap(); - private final Cleaner cleaner; + static final int BOOTSTRAP_HASH = 0; - public DDCachingPoolStrategy(final Cleaner cleaner) { - this.cleaner = cleaner; - } + /** + * Cache of recent ClassLoader WeakReferences; used to... + * + *

+ */ + final Cache> loaderRefCache = + CacheBuilder.newBuilder() + .weakKeys() + .concurrencyLevel(CONCURRENCY_LEVEL) + .initialCapacity(LOADER_CAPACITY / 2) + .maximumSize(LOADER_CAPACITY) + .build(); + + /** + * Single shared Type.Resolution cache -- uses a composite key -- conceptually of loader & name + */ + final Cache sharedResolutionCache = + CacheBuilder.newBuilder() + .softValues() + .concurrencyLevel(CONCURRENCY_LEVEL) + .initialCapacity(TYPE_CAPACITY) + .maximumSize(TYPE_CAPACITY) + .build(); + + /** Fast path for bootstrap */ + final SharedResolutionCacheAdapter bootstrapCacheProvider = + new SharedResolutionCacheAdapter(BOOTSTRAP_HASH, null, sharedResolutionCache); @Override - public TypePool typePool(final ClassFileLocator classFileLocator, final ClassLoader classLoader) { - final ClassLoader key = - BOOTSTRAP_CLASSLOADER == classLoader ? BOOTSTRAP_CLASSLOADER_PLACEHOLDER : classLoader; - final TypePool.CacheProvider cache = typePoolCache.computeIfAbsent(key, this); + public final TypePool typePool( + final ClassFileLocator classFileLocator, final ClassLoader classLoader) { + if (classLoader == null) { + return createCachingTypePool(bootstrapCacheProvider, classFileLocator); + } + WeakReference loaderRef = loaderRefCache.getIfPresent(classLoader); + + if (loaderRef == null) { + loaderRef = new WeakReference<>(classLoader); + loaderRefCache.put(classLoader, loaderRef); + } + + int loaderHash = classLoader.hashCode(); + return createCachingTypePool(loaderHash, loaderRef, classFileLocator); + } + + private final TypePool.CacheProvider createCacheProvider( + final int loaderHash, final WeakReference loaderRef) { + return new SharedResolutionCacheAdapter(loaderHash, loaderRef, sharedResolutionCache); + } + + private final TypePool createCachingTypePool( + final int loaderHash, + final WeakReference loaderRef, + final ClassFileLocator classFileLocator) { return new TypePool.Default.WithLazyResolution( - cache, classFileLocator, TypePool.Default.ReaderMode.FAST); + createCacheProvider(loaderHash, loaderRef), + classFileLocator, + TypePool.Default.ReaderMode.FAST); } - @Override - public TypePool.CacheProvider get(final ClassLoader key) { - if (BOOTSTRAP_CLASSLOADER_PLACEHOLDER != key && skipClassLoader().matches(key)) { - // Don't bother creating a cache for a classloader that won't match. - // (avoiding a lot of DelegatingClassLoader instances) - // This is primarily an optimization. - return TypePool.CacheProvider.NoOp.INSTANCE; - } else { - return EvictingCacheProvider.withObjectType(cleaner, 1, TimeUnit.MINUTES); - } + private final TypePool createCachingTypePool( + final TypePool.CacheProvider cacheProvider, final ClassFileLocator classFileLocator) { + return new TypePool.Default.WithLazyResolution( + cacheProvider, classFileLocator, TypePool.Default.ReaderMode.FAST); } - private static class EvictingCacheProvider implements TypePool.CacheProvider { + final long approximateSize() { + return sharedResolutionCache.size(); + } - /** A map containing all cached resolutions by their names. */ - private final Cache cache; + /** + * TypeCacheKey is key for the sharedResolutionCache. Conceptually, it is a mix of ClassLoader & + * class name. + * + *

For efficiency & GC purposes, it is actually composed of loaderHash & + * WeakReference<ClassLoader> + * + *

The loaderHash exists to avoid calling get & strengthening the Reference. + */ + static final class TypeCacheKey { + private final int loaderHash; + private final WeakReference loaderRef; + private final String className; - /** Creates a new simple cache. */ - private EvictingCacheProvider( - final Cleaner cleaner, final long expireDuration, final TimeUnit unit) { - cache = - CacheBuilder.newBuilder() - .initialCapacity(100) // Per classloader, so we want a small default. - .maximumSize(5000) - .softValues() - .expireAfterAccess(expireDuration, unit) - .build(); + private final int hashCode; - /* - * The cache only does cleanup on occasional reads and writes. - * We want to ensure this happens more regularly, so we schedule a thread to do run cleanup manually. - */ - cleaner.scheduleCleaning(cache, CacheCleaner.CLEANER, expireDuration, unit); - } + TypeCacheKey( + final int loaderHash, final WeakReference loaderRef, final String className) { + this.loaderHash = loaderHash; + this.loaderRef = loaderRef; + this.className = className; - private static EvictingCacheProvider withObjectType( - final Cleaner cleaner, final long expireDuration, final TimeUnit unit) { - final EvictingCacheProvider cacheProvider = - new EvictingCacheProvider(cleaner, expireDuration, unit); - cacheProvider.register( - Object.class.getName(), new TypePool.Resolution.Simple(TypeDescription.OBJECT)); - return cacheProvider; + hashCode = (int) (31 * this.loaderHash) ^ className.hashCode(); } @Override - public TypePool.Resolution find(final String name) { - return cache.getIfPresent(name); + public final int hashCode() { + return hashCode; } @Override - public TypePool.Resolution register(final String name, final TypePool.Resolution resolution) { - try { - return cache.get(name, new ResolutionProvider(resolution)); - } catch (final ExecutionException e) { + public boolean equals(final Object obj) { + if (!(obj instanceof TypeCacheKey)) return false; + + TypeCacheKey that = (TypeCacheKey) obj; + + if (loaderHash != that.loaderHash) return false; + + // Fastpath loaderRef equivalence -- works because of WeakReference cache used + // Also covers the bootstrap null loaderRef case + if (loaderRef == that.loaderRef) { + // still need to check name + return className.equals(that.className); + } else if (className.equals(that.className)) { + // need to perform a deeper loader check -- requires calling Reference.get + // which can strengthen the Reference, so deliberately done last + + // If either reference has gone null, they aren't considered equivalent + // Technically, this is a bit of violation of equals semantics, since + // two equivalent references can become not equivalent. + + // In this case, it is fine because that means the ClassLoader is no + // longer live, so the entries will never match anyway and will fall + // out of the cache. + ClassLoader thisLoader = loaderRef.get(); + if (thisLoader == null) return false; + + ClassLoader thatLoader = that.loaderRef.get(); + if (thatLoader == null) return false; + + return (thisLoader == thatLoader); + } else { + return false; + } + } + } + + static final class SharedResolutionCacheAdapter implements TypePool.CacheProvider { + private static final String OBJECT_NAME = "java.lang.Object"; + private static final TypePool.Resolution OBJECT_RESOLUTION = + new TypePool.Resolution.Simple(TypeDescription.OBJECT); + + private final int loaderHash; + private final WeakReference loaderRef; + private final Cache sharedResolutionCache; + + SharedResolutionCacheAdapter( + final int loaderHash, + final WeakReference loaderRef, + final Cache sharedResolutionCache) { + this.loaderHash = loaderHash; + this.loaderRef = loaderRef; + this.sharedResolutionCache = sharedResolutionCache; + } + + @Override + public TypePool.Resolution find(final String className) { + TypePool.Resolution existingResolution = + sharedResolutionCache.getIfPresent(new TypeCacheKey(loaderHash, loaderRef, className)); + if (existingResolution != null) return existingResolution; + + if (OBJECT_NAME.equals(className)) { + return OBJECT_RESOLUTION; + } + + return null; + } + + @Override + public TypePool.Resolution register( + final String className, final TypePool.Resolution resolution) { + if (OBJECT_NAME.equals(className)) { return resolution; } + + sharedResolutionCache.put(new TypeCacheKey(loaderHash, loaderRef, className), resolution); + return resolution; } @Override public void clear() { - cache.invalidateAll(); - } - - public long size() { - return cache.size(); - } - - private static class CacheCleaner implements Cleaner.Adapter { - private static final CacheCleaner CLEANER = new CacheCleaner(); - - @Override - public void clean(final Cache target) { - target.cleanUp(); - } - } - - private static class ResolutionProvider implements Callable { - private final TypePool.Resolution value; - - private ResolutionProvider(final TypePool.Resolution value) { - this.value = value; - } - - @Override - public TypePool.Resolution call() { - return value; - } + // Allowing the high-level eviction policy make the clearing decisions } } } diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/CacheProviderTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/CacheProviderTest.groovy new file mode 100644 index 0000000000..73d0a1fe19 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/CacheProviderTest.groovy @@ -0,0 +1,221 @@ +package datadog.trace.agent.tooling + +import datadog.trace.util.test.DDSpecification +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.dynamic.ClassFileLocator +import net.bytebuddy.pool.TypePool +import spock.lang.Timeout + +import java.lang.ref.WeakReference + +@Timeout(5) +class CacheProviderTest extends DDSpecification { + def "key bootstrap equivalence"() { + // def loader = null + def loaderHash = DDCachingPoolStrategy.BOOTSTRAP_HASH + def loaderRef = null + + def key1 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + def key2 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + + expect: + key1.hashCode() == key2.hashCode() + key1.equals(key2) + } + + def "key same ref equivalence"() { + setup: + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef = new WeakReference(loader) + + def key1 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + def key2 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + + expect: + key1.hashCode() == key2.hashCode() + key1.equals(key2) + } + + def "key different ref equivalence"() { + setup: + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef1 = new WeakReference(loader) + def loaderRef2 = new WeakReference(loader) + + def key1 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef1, "foo") + def key2 = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef2, "foo") + + expect: + loaderRef1 != loaderRef2 + + key1.hashCode() == key2.hashCode() + key1.equals(key2) + } + + def "key mismatch -- same loader - diff name"() { + setup: + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef = new WeakReference(loader) + def fooKey = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "foo") + def barKey = new DDCachingPoolStrategy.TypeCacheKey(loaderHash, loaderRef, "bar") + + expect: + // not strictly guaranteed -- but important for performance + fooKey.hashCode() != barKey.hashCode() + !fooKey.equals(barKey) + } + + def "key mismatch -- same name - diff loader"() { + setup: + def loader1 = newClassLoader() + def loader1Hash = loader1.hashCode() + def loaderRef1 = new WeakReference(loader1) + + def loader2 = newClassLoader() + def loader2Hash = loader2.hashCode() + def loaderRef2 = new WeakReference(loader2) + + def fooKey1 = new DDCachingPoolStrategy.TypeCacheKey(loader1Hash, loaderRef1, "foo") + def fooKey2 = new DDCachingPoolStrategy.TypeCacheKey(loader2Hash, loaderRef2, "foo") + + expect: + // not strictly guaranteed -- but important for performance + fooKey1.hashCode() != fooKey2.hashCode() + !fooKey1.equals(fooKey2) + } + + def "test basic caching"() { + setup: + def poolStrat = new DDCachingPoolStrategy() + + def loader = newClassLoader() + def loaderHash = loader.hashCode() + def loaderRef = new WeakReference(loader) + + def cacheProvider = poolStrat.createCacheProvider(loaderHash, loaderRef) + + when: + cacheProvider.register("foo", new TypePool.Resolution.Simple(TypeDescription.VOID)) + + then: + // not strictly guaranteed, but fine for this test + cacheProvider.find("foo") != null + poolStrat.approximateSize() == 1 + } + + def "test loader equivalence"() { + setup: + def poolStrat = new DDCachingPoolStrategy() + + def loader1 = newClassLoader() + def loaderHash1 = loader1.hashCode() + def loaderRef1A = new WeakReference(loader1) + def loaderRef1B = new WeakReference(loader1) + + def cacheProvider1A = poolStrat.createCacheProvider(loaderHash1, loaderRef1A) + def cacheProvider1B = poolStrat.createCacheProvider(loaderHash1, loaderRef1B) + + when: + cacheProvider1A.register("foo", newVoid()) + + then: + // not strictly guaranteed, but fine for this test + cacheProvider1A.find("foo") != null + cacheProvider1B.find("foo") != null + + cacheProvider1A.find("foo").is(cacheProvider1B.find("foo")) + poolStrat.approximateSize() == 1 + } + + def "test loader separation"() { + setup: + def poolStrat = new DDCachingPoolStrategy() + + def loader1 = newClassLoader() + def loaderHash1 = loader1.hashCode() + def loaderRef1 = new WeakReference(loader1) + + def loader2 = newClassLoader() + def loaderHash2 = loader2.hashCode() + def loaderRef2 = new WeakReference(loader2) + + def cacheProvider1 = poolStrat.createCacheProvider(loaderHash1, loaderRef1) + def cacheProvider2 = poolStrat.createCacheProvider(loaderHash2, loaderRef2) + + when: + cacheProvider1.register("foo", newVoid()) + cacheProvider2.register("foo", newVoid()) + + then: + // not strictly guaranteed, but fine for this test + cacheProvider1.find("foo") != null + cacheProvider2.find("foo") != null + + !cacheProvider1.find("foo").is(cacheProvider2.find("foo")) + poolStrat.approximateSize() == 2 + } + + def "test capacity"() { + setup: + def poolStrat = new DDCachingPoolStrategy() + def capacity = DDCachingPoolStrategy.TYPE_CAPACITY + + def loader1 = newClassLoader() + def loaderHash1 = loader1.hashCode() + def loaderRef1 = new WeakReference(loader1) + + def loader2 = newClassLoader() + def loaderHash2 = loader2.hashCode() + def loaderRef2 = new WeakReference(loader2) + + def cacheProvider1 = poolStrat.createCacheProvider(loaderHash1, loaderRef1) + def cacheProvider2 = poolStrat.createCacheProvider(loaderHash2, loaderRef2) + + def id = 0 + + when: + (capacity / 2).times { + id += 1 + cacheProvider1.register("foo${id}", newVoid()) + cacheProvider2.register("foo${id}", newVoid()) + } + + then: + // cache will start to proactively free slots & size calc is approximate + poolStrat.approximateSize() > 0.8 * capacity + + when: + 10.times { + id += 1 + cacheProvider1.register("foo${id}", newVoid()) + cacheProvider2.register("foo${id}", newVoid()) + } + + then: + // cache will start to proactively free slots & size calc is approximate + poolStrat.approximateSize() > 0.8 * capacity + } + + static newVoid() { + return new TypePool.Resolution.Simple(TypeDescription.VOID) + } + + static newClassLoader() { + return new URLClassLoader([] as URL[], (ClassLoader)null) + } + + static newLocator() { + return new ClassFileLocator() { + @Override + ClassFileLocator.Resolution locate(String name) throws IOException { + return null + } + + @Override + void close() throws IOException {} + } + } +} diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/EvictingCacheProviderTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/EvictingCacheProviderTest.groovy deleted file mode 100644 index 3de7cc3140..0000000000 --- a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/tooling/EvictingCacheProviderTest.groovy +++ /dev/null @@ -1,102 +0,0 @@ -package datadog.trace.agent.tooling - -import datadog.trace.util.gc.GCUtils -import datadog.trace.util.test.DDSpecification -import net.bytebuddy.description.type.TypeDescription -import net.bytebuddy.pool.TypePool -import spock.lang.Timeout - -import java.lang.ref.WeakReference -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference - -import static datadog.trace.agent.tooling.AgentTooling.CLEANER - -@Timeout(5) -class EvictingCacheProviderTest extends DDSpecification { - - def "test provider"() { - setup: - def provider = new DDCachingPoolStrategy.EvictingCacheProvider(CLEANER, 2, TimeUnit.MINUTES) - - expect: - provider.size() == 0 - provider.find(className) == null - - when: - provider.register(className, new TypePool.Resolution.Simple(TypeDescription.VOID)) - - then: - provider.size() == 1 - provider.find(className) == new TypePool.Resolution.Simple(TypeDescription.VOID) - - when: - provider.clear() - - then: - provider.size() == 0 - provider.find(className) == null - - where: - className = "SomeClass" - } - - def "test timeout eviction"() { - setup: - def provider = new DDCachingPoolStrategy.EvictingCacheProvider(CLEANER, timeout, TimeUnit.MILLISECONDS) - def resolutionRef = new AtomicReference(new TypePool.Resolution.Simple(TypeDescription.VOID)) - def weakRef = new WeakReference(resolutionRef.get()) - - when: - def lastAccess = System.nanoTime() - provider.register(className, resolutionRef.get()) - - then: - // Ensure continued access prevents expiration. - for (int i = 0; i < timeout + 10; i++) { - assert TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - lastAccess) < timeout: "test took too long on " + i - assert provider.find(className) != null - assert provider.size() == 1 - lastAccess = System.nanoTime() - Thread.sleep(1) - } - - when: - Thread.sleep(timeout) - - then: - provider.find(className) == null - - when: - provider.register(className, resolutionRef.get()) - resolutionRef.set(null) - GCUtils.awaitGC(weakRef) - - then: - // Verify properly GC'd - provider.find(className) == null - weakRef.get() == null - - where: - className = "SomeClass" - timeout = 500 // Takes about 50 ms locally, adding an order of magnitude for CI. - } - - def "test size limit"() { - setup: - def provider = new DDCachingPoolStrategy.EvictingCacheProvider(CLEANER, 2, TimeUnit.MINUTES) - def typeDef = new TypePool.Resolution.Simple(TypeDescription.VOID) - for (int i = 0; i < 10000; i++) { - provider.register("ClassName$i", typeDef) - } - - expect: - provider.size() == 5000 - - when: - provider.clear() - - then: - provider.size() == 0 - } -} diff --git a/dd-java-agent/src/test/groovy/datadog/trace/agent/integration/classloading/ClassLoadingTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/agent/integration/classloading/ClassLoadingTest.groovy index 0a24d3b322..c4ba7012fa 100644 --- a/dd-java-agent/src/test/groovy/datadog/trace/agent/integration/classloading/ClassLoadingTest.groovy +++ b/dd-java-agent/src/test/groovy/datadog/trace/agent/integration/classloading/ClassLoadingTest.groovy @@ -78,7 +78,7 @@ class ClassLoadingTest extends Specification { loader.count == countAfterFirstLoad } - def "make sure that ByteBuddy doesn't resue cached type descriptions between different classloaders"() { + def "make sure that ByteBuddy doesn't reuse cached type descriptions between different classloaders"() { setup: CountingClassLoader loader1 = new CountingClassLoader(classpath) CountingClassLoader loader2 = new CountingClassLoader(classpath)